关于 sasjs-cli
sasjs-cli 是 SASjs CLI 的命令行技能参考,面向编译、构建、测试和部署 SAS 项目的开发者。它说明命令、目标配置、身份验证与 CI 工作流,提供操作指南而不执行命令。
主要功能
- 01创建 SASjs 应用模板
- 02编译 SAS 作业与服务
- 03构建并部署目标产物
- 04运行作业、服务、测试和流程
- 05管理目标、凭据与 Viya 计算上下文
- 06生成文档并检查 SAS 代码
适用场景
- 01查找将 SASjs 项目部署到 Viya 的步骤
- 02规划编译、构建和部署的 CI 流程
- 03排查 CLI 身份验证和目标配置问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ sasjs/ skills --skill sasjs-cli安装后在 Claude Code 中输入/sasjs-cli确认已加载。
详细介绍
@sasjs/cli
SASjs CLI(npm i -g @sasjs/cli,调用方式为 sasjs)可自动编译、构建和部署 SAS 项目。大多数命令都接受 -t <target>,用于从 sasjsconfig.json 中选择目标;省略时会回退到默认目标。不会访问服务器的命令(lint、db、create、init、doc init)则完全没有目标选项。
已根据 @sasjs/cli 6.2.5 核对。标志取自已安装 CLI 自带的 --help,因此使用此处未列出的标志前,请重新检查 sasjs <command> --help。
目标与身份验证
- 目标 =
{ name, serverUrl, serverType, appLoc }。serverType为SAS9、SASVIYA或SASJS。 - 凭据:使用
sasjs add cred(或本地环境文件)。Viya 使用 client/secret,或使用sasjs auth login(用户名/密码,无需 client/secret,见下文);SAS 9 使用用户名/密码;SASJS 服务使用访问令牌。 sasjs context用于管理 Viya 计算上下文;sasjs add target用于添加新目标。
核心工作流
sasjs create myapp # scaffold a new app (templates available)
sasjs compile # gather macros/services/jobs into per-file build outputs (sasjsbuild/)
sasjs compile service -s sasjs/services/public/viewdata.sas # compile one file only
sasjs build # produce deployable artefacts: JSON + .sas per target
sasjs deploy # deploy compiled/built artefacts to the target server
sasjs cbd # compile + build + deploy in one step (-t viya etc.)
编译单个文件
sasjs compile 也可以一次编译一个文件,而不触及项目的其他部分。这是检查标头修改是否生效,或查看某个服务具体包含哪些内容的最快方式:
sasjs compile <job|service|test|identify> --source <file.sas> [-o <dir>] [-t <target>]
- 子命令名称对应文件类型;
identify会根据文件本身识别类型,因此如果不确定文件是服务还是作业,这是安全的选择。 - 输出会在类型文件夹下保留源文件路径:
sasjs compile identify -s sasjs/services/public/viewdata.sas -o out/会写入out/services/public/viewdata.sas。如果不使用-o,输出将根据类型写入sasjsbuild/jobs或sasjsbuild/services。 - 示例中展示了
-s/--source和-o/--output,但sasjs compile --help的Options:下只列出了--target;它们都能按示例中的说明正常使用。 - 完整编译会打印依赖覆盖率摘要(
Services coverage: 26/57 (46%))。如果项目中的服务大多是针对特定目标的副本,覆盖率偏低很正常,并非错误。
命令参考
| 命令 | 用途 |
|---|---|
sasjs create / init | 搭建新应用(--template angular|react|minimal|jobs|sasonly),或将 SASjs 添加到现有仓库 |
sasjs compile [job|service|test|identify] | 将依赖项(例如 <h4> SAS Macros </h4>)解析到 sasjsbuild/;使用子命令和 --source 可编译单个文件 |
sasjs build | 将编译后的输出汇总为每个目标对应的部署 .sas 和 .json 文件 |
sasjs deploy / cbd | 部署到服务(使用 servicepack 或直接部署)。cb = 编译 + 构建 |
sasjs run <file.sas> | 在服务上执行任意 SAS 文件并返回日志。-c 会先编译,-s 以 JSON 格式提供作业变量,-l 保存日志 |
sasjs request <path> | 使用输入数据执行已部署的服务/作业:-d 数据 JSON,-c 配置 JSON,-l 日志(隐含等待),-o 输出 JSON |
sasjs job execute <jobPath> | 运行已部署的作业:-w 等待,-l 日志,-o 输出,-i 忽略警告,-v 详细输出 |
sasjs flow execute | 按依赖关系运行一系列作业:-s 流程 JSON,-l 日志文件夹,-c CSV,--streamLog |
sasjs servicepack deploy | 从 JSON service pack 部署(-s,-f 用于覆盖) |
sasjs context create/edit/delete/list/export | 管理 Viya 计算上下文 |
sasjs web | 构建前端并将其流式传输到 SAS Web 根目录(streamConfig) |
sasjs db | 将 sasjs/db 中的 DDL/数据脚本合并到构建输出中 |
sasjs doc [init|lineage] | 生成 doxygen 文档,或生成 lineage .dot 文件(-o 输出目录) |
sasjs snippets | 根据项目宏的 Doxygen 标头生成 VS Code 代码片段 |
sasjs folder create/delete/list/move | 管理 SAS 元数据 / SAS Drive 中的文件夹(create -f 强制重新创建) |
sasjs fs sync / sasjs fs compile | 根据 syncDirectories 同步本地和远程文件夹;将远程文件夹编译为单个程序(-o) |
sasjs test [filters..] | 使用 init/term 程序执行 sasjs/tests 中定义的测试:-s 测试流程,--out 结果目录,-f 永不使流水线失败 |
sasjs lint [init|fix|find] | 根据 .sasjslint 检查 .sas 文件;直接运行 sasjs lint 等同于 find。详情见 sasjs-lint 技能 |
sasjs version | 打印/设置版本信息 |
别名:b build,c compile,cb compilebuild,cbd compilebuilddeploy,d deploy,docs doc,h help,rq request,r run,v version,w web,build-db/DB db。
约定
-
仅通过
sasjs compile(或sasjs c)生成测试覆盖率。该命令接受一个目标(-t <target>),但不会向该目标部署任何内容——编译和覆盖率统计完全在本地离线完成,因此无需服务器处于可用或可访问状态。CLI 自带@sasjs/core依赖,所以即使项目没有node_modules,由@sasjs/core提供的宏(例如mp_ds2csv.sas)也能解析;如果某个宏在两者中都不存在,那就确实缺少依赖。 -
依赖项在 doxygen 标头中声明:
<h4> SAS Macros </h4>、<h4> SAS Files </h4>、<h4> SAS Folders </h4>和@li item条目——CLI 会据此构建依赖树。 -
@li列表是编译器唯一的依赖来源:代码中调用但未声明的宏不会内联到构建产物中,部署后的作业会在运行时失败,并显示Apparent invocation of macro X not resolved。如果另一个已声明宏的@li链恰好将其内联(传递解析),它仍可能碰巧正常工作,但只要该宏的依赖项发生变化,这种情况就会失效。请声明代码调用的每个宏,包括间接调用的辅助宏(例如mf_getuser.sas);编辑现有服务时,请重新核对主体中的%macro()调用是否都对应@li条目。 -
sasjs compile的输出位于sasjsbuild/文件夹中(请将其加入 git 忽略列表);sasjsresults/存放测试/运行输出。 -
CI/CD:
sasjs cbd -t viya是标准部署步骤;基于制品发布时,可结合使用sasjs servicepack deploy。 -
失败时会返回非零退出码——可安全用于流水线。
注意事项
-
运行
sasjs compile前不必执行npm i。 CLI 本身依赖@sasjs/core,并从自身副本解析核心宏,因此即使全新克隆或工作树中没有node_modules,仍然可以编译(已在 6.2.5 上验证:在没有依赖项的工作树中完整编译项目,且生成的服务包含核心宏)。仍请在package.json中保留@sasjs/core(以及用到时的@sasjs/adapter),这样构建和测试所用的版本就有明确记录并受 lockfile 管理,而不是跟随已安装 CLI 打包的版本。 -
streamWeb编译即使记录了 ERROR,也可能以 0 退出。 当streamConfig.streamWeb: true时,如果缺少webSourcePath(通常为client/dist,前端构建前该目录不存在),会打印ERROR An error has occurred when compiling web app services. Error: webSourcePath: '...' doesn't exist.,随后又显示The project was successfully compiled ...——退出码为 0。因此,仅依据退出码判断的流水线步骤会通过,但 Web 应用实际上并未构建;请检查日志中是否包含该行,或在编译前先构建前端。 -
sasjs lint仅发出警告时会以 0 退出,并且即使--help将子命令标为必填,不带参数的sasjs lint也会按sasjs lint find执行。若要将其用作质量门禁,请通过severityLevel提升你关注规则的严重级别(除noEncodedPasswords外,所有规则的默认值均为"warn")。规则和配置的详细信息见sasjs-lint技能。 -
sasjs folder --help将create、delete和list列为可选项,而顶层帮助中也记录了move。 在编写脚本前,请针对已安装的版本运行sasjs folder move --help并确认。 -
凭据文件按目标区分:环境文件命名为
.env.<targetname>(例如.env.server),并包含CLIENT、ACCESS_TOKEN和REFRESH_TOKEN条目。切勿提交这些文件——请将.env*加入 git 忽略列表。
Viya 无需客户端/密钥即可进行身份验证(sasjs auth login)
sasjs auth login -t <target> 使用常规 SAS 用户名/密码,通过内置的无密钥 sas.cli 公共客户端进行 OAuth2 密码授权。无需由管理员注册 OAuth 客户端,是在开发/演示环境中让 sasjs run/deploy 快速运行起来的方式。密码不会被存储;生成的令牌对会保存到本地环境文件(.env.<target>)或 ~/.sasjsrc(全局),并通过 /identities/users/@currentUser 验证(Logged in as <id> (<name>))。单独使用 sasjs auth 仍是 sasjs add cred 的别名。
- 令牌过期:CLI 会使用存储的刷新令牌静默刷新(无论是否有客户端/密钥均可),并重新保存轮换后的令牌对。Viya 刷新令牌是单次使用/轮换式的,因此保存令牌对才能确保后续调用正常工作。如果刷新失败,请重新运行
sasjs auth login。 - 某些环境为
sas.cli设置了较短的访问令牌 TTL(例如 1h);大多数调用时都进行刷新是正常现象。 - 不透明(非 JWT)令牌会被视为可用,令牌是否过期由服务端判定。
- 限制:仅支持本地/LDAP 账户(不支持 SSO/SAML/MFA 环境);必须为
sas.cli启用密码授权(Viya 3.5+/4 默认启用);ROPC 已在 OAuth 2.1 中弃用,CI/生产环境请使用已注册的客户端/密钥。 - 创建会话时,
sasjs run返回 403,表示该账户无权使用所配置的计算上下文;请在目标配置中设置contextName: "SAS Studio compute context"。在尚未启动的环境中首次运行可能需要很多分钟(计算 Pod 启动时间),看起来可能像是卡住了。 - 自签名环境:在
auth login中使用--insecure,或在目标配置中设置httpsAgentOptions。
非交互式身份验证(CI 和 Agent)
sasjs auth login 不一定会提示输入信息。设置 SAS_USERNAME 和 SAS_PASSWORD 环境变量后,命令会读取它们;使用 --passwordStdin 则会改为从标准输入读取密码:
SAS_USERNAME=user SAS_PASSWORD=secret sasjs auth login -t <target>
echo "$SAS_PASSWORD" | sasjs auth login --password-stdin -t <target>
--password-stdin 可避免密码出现在进程列表和 Shell 历史记录中,因此只要密码来自密钥存储,就应优先使用此方式。如果两者都未设置,CLI 会提示输入密码;无人值守的作业会因此等待终端输入而挂起。
限制
此技能是 SASjs CLI 的静态参考资料,提供有关使用 sasjs 命令行工具的指导。它不会执行 CLI 命令、访问文件系统、读取凭据或发送网络请求。此处展示的所有命令仅供参考;用户必须在自己的环境中运行这些命令。有关凭据文件(.env、~/.sasjsrc)的说明仅用于描述 CLI 保存身份验证令牌的位置;此技能本身不会读取、写入或访问这些文件。
Viya 流式应用(streamConfig.streamWeb)
设置 streamWeb: true 后,sasjs web/cbd -t viya 会将前端部署到 SAS Files Service:位于 <appLoc>/services/<streamServiceName>.html 的流式作业负责提供 index.html,资源则放在 <appLoc>/services/web/... 中。构建时,HTML 中的每个资源/脚本/CSS URL 都会重写为 /SASJobExecution?_FILE=<appLoc>/services/web/...,适配器配置元素(<sasjs apploc=...>)也会写入目标 appLoc。
因此,只有从部署时使用的确切 appLoc 提供应用,应用才能正常工作。 如果将流式 HTML 放到其他位置(例如手动上传到用户主目录,如 /Users/<id>/myapp/...),所有重写后的 /Public/app/... 资源链接都会返回 404,适配器也会调用错误的服务路径,导致页面渲染后没有 CSS/JS,也没有后端。请重新部署以修复。原始构建(sasjs cb)使用 sasjsconfig.json 中的 apploc;将应用部署为 SAS 程序时,会用 compiled_apploc(构建时值)替换提供的 %let apploc = (运行时值),以便在部署时将应用动态部署到指定的 apploc。
在无头环境中验证 Viya 部署(无浏览器)
- 获取令牌——优先使用 CLI,而不是直接处理凭据:
sasjs auth login -t <target>会处理 OAuth 流程并存储令牌。如果你无论如何都需要用 curl 获取令牌,请通过/SASLogon/oauth/token上的 OAuth 密码授权,从公开的无密钥客户端(sas.ec)获取令牌——参阅 SAS Viya REST 身份验证文档;不要在脚本或命令行中嵌入真实凭据。 - 获取应用:
GET /SASJobExecution/?_FILE=<appLoc>/services/<name>.html,并附带Authorization: Bearer-> 预期返回200和完整的index.html。401= 身份验证问题,其他任何状态码 = 应用未部署在该位置。 - 获取 HTML 引用的每个资源。响应会指出问题所在:
200且包含文件内容 -> 资源已正确部署。202+ 一小段纯文本正文(例如Parameter Error\nFile error)-> 该 Drive 路径下不存在此文件(这是典型的 appLoc 不匹配症状)。_FILE响应可能是异步的:添加&_action=wait以同步获取内容。- 服务:
POST /SASJobExecution/?_program=<appLoc>/services/<svc>&_action=wait->Parameter Error / Unable to get job definition表示该服务从未作为 JES 作业部署(仅将文件放在 Drive 上并不足够——只有sasjs deploy/cbd/servicepack deploy才会注册这些文件)。
与 SAS 通信——始终使用适配器或 CLI
出于任何目的执行 SAS 服务或作业(调试、复现、CI)时,始终通过 @sasjs/adapter 或 sasjs CLI——切勿手动编写针对 SASJobExecution 的 curl 请求。适配器和 CLI 会处理那些手动操作时很容易出错的事项:输入数据 CSV 格式(以空格分隔的 name:$format. 表头、CRLF、特殊值加双引号、用 %nrstr(...) 包装)、执行模式路由(_executionTasks=true 会将 sasjs<N>data 读取为宏变量,而非文件上传)、_debug/>>weboutBEGIN<</>>weboutEND<< 包装内容解析、令牌刷新,以及 _contextname URL 参数。手动构造的逗号分隔 CSV 会被读取为空白值,静默跳过受条件保护的宏代码块,让你陷入虚构 bug 的排查泥潭。
- 将输入数据构建为 JSON 文件:
{ "<table>": [{ "<col>": "<val>", ... }] },例如{ "config": [{ "rootdir": "/export/...", "runastask": "true", "usecomputeapi": "null", "contextname": "Compute Reusable" }] }。 - 运行:
sasjs request '<appLoc>/services/common/<svc>' -t viya -d data.json -l <path>.log -o <path>.json。CLI 会将debug: true硬编码,因此-l标志始终会捕获完整的 MPRINT/NOTE/&syscc日志;-o会保存解析后的 webout。始终传入-l——不保留日志就复现 bug,意味着必须重跑整个过程。 - Viya Folders/JES REST API 可以直接使用 curl(令牌 +
Authorization: Bearer)调用,用于文件夹/文件/成员管理以及GET /SASJobExecution/?_FILE=...资源检查——只是不能用于以输入数据执行你自己的服务。
适配器执行上下文和输入数据说明
- 适配器会自动将
_contextname=<value>作为URL 参数追加到每个 Viya JES 请求中(无需自行传递)。但是,JES 请求参数不会自动转换为 SAS 宏变量——在服务中,%symexist(_contextname)的结果为 false。如果服务需要所选上下文名称(例如,将其写入流式传输的 HTML),请将其传入输入数据表(contextname列),并使用call symputx读取——这是可靠的传递渠道。 sasjsconfig.json中目标的contextName决定服务使用哪个计算上下文运行(因此也决定runAs身份)。适配器 URL 中的_contextname参数与该值相同。若要使用sasjs request在批处理/可复用上下文下复现服务,请将目标的contextName设为该可复用上下文(例如Compute Reusable,runAs=sasbatch)——否则服务会以你自己的身份运行,针对批处理拥有的文件夹执行写入测试时会失败,并显示User does not have appropriate authorization level。- 适配器 CSV 格式(以便正确读取日志中的
NOTE: The infile ... is:规则):第 1 行为表头,其中name:$informat.配对以空格分隔;数据行以逗号分隔(CRLF),仅当值包含特殊字符(,、"、制表符、换行符)时才用双引号括起。sasjs/core webout 读取器会使用dsd、firstobs=2,以及根据表头生成的input <name>:$informat.;语句读取数据。 _executionTasks=true(runAsTask)会改变输入数据的传入方式:数据会作为sasjs<N>data宏变量传入(分块为sasjs<N>data0..N),而非作为_WEBIN_FILE上传文件传入。core webout 的mv_webout宏会同时处理这两种方式,但会根据_EXECUTIONTASKS分支——查看日志时请留意。
在 Viya 上干净地重新部署(409 Conflict 问题)
针对已存在的 appLoc 重新运行 sasjs cbd/sasjs run viya.sas 时,部署通常会在中途失败并返回 409 Conflict(然后触发 mp_abort -> abort cancel):mv_createfile 会删除旧文件 ID,再尝试重新创建文件,但如果某个中间文件夹已存在(例如 <appLoc>/services/web/js),重新创建步骤就会发生冲突,导致整个部署中止——留下一个只部署了一部分的应用(服务已存在,但某些资源缺失)。使用 ?recursive=true 删除文件夹也会返回 409 You cannot delete the folder because it is not empty;即使是所有者,单独删除成员/文件夹也可能返回 403,因此很难手动清理整个目录树。
可靠的解决办法是先将顶层 appLoc 文件夹移到别处,再重新部署到原路径——部署会创建一棵全新的文件夹树,不会发生冲突,而旧文件夹则会保留为备份。这比逐个删除成员省时得多,也可靠得多;建议在 Viya 上进行任何非 CI 重新部署前采用此步骤。移动方式有两种:
- 就地重命名(最简单):使用
PATCH /folders/folders/{id}和{"name":"<old>.bak.<ts>","version":2}——释放原名称,并将旧文件夹保留为同级备份。 - MOVE 到备份父文件夹(更整洁——将所有备份集中存放):创建一个备份父文件夹(只需创建一次),例如
/Users/<id>/macrodash-backups,然后调用POST /folders/folders/{backupParentId}/members/{appLocFolderId}?action=move,将整棵目录树(包括内容)移入其中,为全新部署释放原路径。
(在 sasjs/server 上,只需先运行 sasjs fs delete 删除 appLoc——该处的 Drive 支持干净地递归删除。)
流式传输时,JES 会应用自己的 CSP 标头(包含 unsafe-inline/unsafe-eval);应用 HTML 中严格的 CSP meta 标签仍然有效,而且对应用代码来说才是关键。