@pippit-dev/cli 1.0.41 → 1.0.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -11
- package/checksums.txt +6 -6
- package/cmd/auth/auth.go +19 -5
- package/cmd/auth/auth_test.go +23 -0
- package/cmd/canvas/canvas.go +7 -7
- package/cmd/download_result.go +3 -2
- package/cmd/marketing.go +19 -3
- package/cmd/marketing_test.go +11 -5
- package/cmd/model.go +5 -4
- package/cmd/root.go +17 -5
- package/cmd/root_update_check_test.go +139 -0
- package/cmd/short_drama_test.go +2 -0
- package/cmd/source_test.go +27 -4
- package/cmd/space.go +199 -0
- package/cmd/space_test.go +469 -0
- package/cmd/upload_file_test.go +3 -1
- package/dist/checksums.txt +6 -6
- package/internal/adaptercli/commands_test.go +9 -5
- package/internal/auth/loopback.go +16 -11
- package/internal/auth/loopback_origins_test.go +152 -0
- package/internal/auth/space.go +26 -0
- package/internal/auth/space_test.go +42 -0
- package/internal/canvas/get.go +9 -2
- package/internal/commandnames/names.go +9 -4
- package/internal/common/client.go +23 -0
- package/internal/common/host_source.go +5 -0
- package/internal/common/host_source_test.go +9 -1
- package/internal/common/runner.go +5 -3
- package/internal/common/space.go +185 -0
- package/internal/common/space_test.go +132 -0
- package/internal/common/submit_run.go +2 -0
- package/internal/common/upload_file.go +2 -0
- package/internal/config/config.go +15 -12
- package/internal/generate_image/generate_image.go +3 -9
- package/internal/short_drama/submit_run.go +3 -9
- package/internal/space/session.go +153 -0
- package/internal/space/store.go +211 -0
- package/internal/space/store_test.go +246 -0
- package/internal/version/version.go +13 -0
- package/internal/version/version_test.go +38 -0
- package/internal/versioncheck/check.go +147 -0
- package/internal/versioncheck/check_test.go +204 -0
- package/internal/versioncheck/semver.go +84 -0
- package/package.json +1 -1
- package/scripts/canvas-command.js +47 -5
- package/scripts/install-wizard.js +1 -1
- package/scripts/run.js +15 -4
- package/scripts/telemetry.js +3 -1
- package/scripts/version-check.js +8 -1
- package/skills/short-drama/SKILL.md +12 -1
- package/skills/xyq-marketing-skill/SKILL.md +22 -3
- package/skills/xyq-marketing-skill/references/api.md +7 -1
- package/skills/xyq-marketing-skill/scripts/marketing.js +51 -19
- package/skills/xyq-nest-skill/SKILL.md +15 -4
- package/skills/xyq-nest-skill/commands/canvas-assets.md +1 -1
- package/skills/xyq-nest-skill/commands/canvas.md +5 -3
- package/skills/xyq-nest-skill/commands/get-credit-balance.md +2 -2
- package/skills/xyq-nest-skill/commands/model.md +1 -1
- package/skills/xyq-nest-skill/scripts/ensure-cli.js +8 -1
- package/skills/xyq-nest-skill/scripts/install.md +1 -1
- package/skills/xyq-nest-skill/workflows/canvas-edit.md +2 -2
package/README.md
CHANGED
|
@@ -24,17 +24,43 @@ x-tt-env = ""
|
|
|
24
24
|
| 技能 | 说明 | 路径 |
|
|
25
25
|
|-------|-------------|------|
|
|
26
26
|
| `xyq-short-drama-skill` | 短剧工作流技能,支持提交创作任务、上传参考文件、查询进度、列出会话文件和下载产物。 | `skills/short-drama/` |
|
|
27
|
-
| `xyq-skill` |
|
|
27
|
+
| `xyq-skill` | 图片生成与参考图编辑、视频生成、视频超分与擦字幕、异步结果交付、个人或团队 Canvas 编辑、积分查询及登录授权。 | `skills/xyq-nest-skill/` |
|
|
28
28
|
| `xyq-marketing-skill` | 商品图文营销一键成片:剧情广告、品牌大片、达人带货,含素材上传、进度查询与媒体下载。 | `skills/xyq-marketing-skill/` |
|
|
29
29
|
|
|
30
30
|
### 技能路由
|
|
31
31
|
|
|
32
|
-
-
|
|
32
|
+
- 图片生成与参考图编辑、视频生成(含首尾帧和参考素材)、视频超分、擦字幕、结果查询、个人或团队 Canvas 编辑、积分和授权由 `xyq-skill` 处理。
|
|
33
33
|
- 短剧生成、续写、改写、人物设定、分集创作和短剧会话文件处理使用 `xyq-short-drama-skill`。
|
|
34
34
|
- 商品图文营销成片、剧情广告、品牌大片和达人带货视频使用 `xyq-marketing-skill`;由公开营销 API 完成创作编排。
|
|
35
35
|
|
|
36
36
|
需要补充、选择或确认时,使用宿主实际暴露且当前模式允许的工具:Codex 的 `request_user_input` / `request_user_input_async`、WorkBuddy 的 `ask_user_question`;不可用时用普通聊天。
|
|
37
37
|
|
|
38
|
+
## 个人与团队空间
|
|
39
|
+
|
|
40
|
+
同一个 AK 可以切换个人与所属团队,无需重新登录。首次使用默认个人;当前账号与 API origin 的选择独立保存。
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pippit-tool-cli space list # 查看个人和所属团队,并标记当前默认空间
|
|
44
|
+
pippit-tool-cli space use 123456 # 保存团队为默认空间
|
|
45
|
+
pippit-tool-cli space use 0 # 保存个人为默认空间
|
|
46
|
+
pippit-tool-cli get-credit-balance --team-id 123456 # 仅本次使用团队
|
|
47
|
+
pippit-tool-cli get-credit-balance --team-id 0 # 仅本次使用个人
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`space` 仅提供 `list` 和 `use <TEAM_ID>`,统一输出 JSON。`list` 返回完整分页后的 `spaces` 和已保存的默认空间 `current`,列表项用 `current: true` 标记当前选择;`use` 必须指定 ID,不等待交互输入,成功后返回保存的空间。团队 ID 按 int64 的十进制字符串处理,不接受负数、小数或溢出值。切换到团队前核实账号所属团队列表,失败保留旧选择;切回个人不依赖列表接口。
|
|
51
|
+
|
|
52
|
+
新任务按「单次 `--team-id` → 已保存默认 → 个人」确定空间;上传、提交、轮询固定同一空间。生成、视频处理、短剧、上传、会话查询、文件列表、积分、营销与 Canvas 命令均适用。同源 API 请求统一携带当前团队的 `X-Team-Id`,由 API 决定是否使用;模型接口内部仍按个人身份查询;Canvas 复用相同空间选择,`--team-id 0` 可单次切回个人。帮助、安装和版本命令无需读取空间或登录。
|
|
53
|
+
|
|
54
|
+
显式 `--team-id` 不要求 CLI 能读取本地账号标识或空间记录,认证继续使用当前 AK/adapter,由 API 校验团队权限。本地记录可用时照常保存;不可用时本次仍固定使用指定空间,宿主应保存返回的任务 ID 和 `team_id`,后续显式传入原 TeamID。`space use` 保存默认空间仍需要可识别的账号;`space list` 在账号不可识别时返回 `current: null`。
|
|
55
|
+
|
|
56
|
+
提交成功会按当前账号、API origin 保存 thread/run 的空间记录,上传成功保存资产归属。之后即使切到另一团队,直接 `get-thread --thread-id ...`、`query-result`、`list-thread-file` 或 `marketing query` 仍查询原任务;带原 thread 的续写也使用原空间。显式指定与记录冲突的团队会在请求前停止,已知跨空间素材同样会拦截。跨设备或旧任务无记录时应显式传原 `--team-id`;失败不会遍历其它团队。查询成功后可补齐记录。
|
|
57
|
+
|
|
58
|
+
空间记录位于系统用户配置目录的 `pippit-cli/spaces/`,按账号与 API origin 摘要隔离,每条记录原子保存,不存 AK。空间账号标识使用本地登录凭证的 UID 摘要,同 UID 换 AK 保留选择。损坏记录报错,不当作首次使用;按错误提示用原 `--team-id` 查询恢复。保存失败不把已经成功的提交改成失败:输出保留 ID 与 `team_id`,警告写 stderr,后续沿用原空间查询,不能重提。
|
|
59
|
+
|
|
60
|
+
业务 stdout 保留既有结构;上传和提交增加字符串 `team_id`,`status` 增加 `default_space`,空间提示写 stderr。`marketing.js query --wait` 自动向各次 CLI 子进程传递固定空间;有账号记录时同时传递账号/API origin 约束,切换账号会停止。无本地账号记录时可显式传入 `--team-id` 轮询,各次请求仍由 API 鉴权;默认空间切换不会改变已固定的 TeamID。团队出现在列表中不等于有 WRITE 权限、有效会员或可用积分;最终由服务端授权和扣费,CLI 不自动回退。
|
|
61
|
+
|
|
62
|
+
`space list/use TEAM_ID` 依赖 API 部署 `/api/biz/v1/skill/get_lite_team_list` 的 AK 适配入口,业务团队使用还依赖对应 API 团队权限实现;不直连 commerce Web 接口。宿主 Agent 的选择、任务恢复与不回退规则已写入三个 Skill 的 `Space Selection` 章节。
|
|
63
|
+
|
|
38
64
|
## 小云雀营销成片技能
|
|
39
65
|
|
|
40
66
|
入口:[skills/xyq-marketing-skill/SKILL.md](skills/xyq-marketing-skill/SKILL.md)。使用 [官网](https://xyq.jianying.com/cli?tab=api) 已开放的营销 API,支持素材上传、营销视频提交、结果查询与下载、积分查询。安装器会从 `skills/` 自动安装该 Skill,也可以单独安装:
|
|
@@ -53,7 +79,7 @@ node skills/xyq-marketing-skill/scripts/marketing.js generate --request request.
|
|
|
53
79
|
node skills/xyq-marketing-skill/scripts/marketing.js query --thread-id THREAD_ID --run-id RUN_ID --wait --output-dir ./results
|
|
54
80
|
```
|
|
55
81
|
|
|
56
|
-
参数与错误处理见 [接口契约](skills/xyq-marketing-skill/references/api.md),完整示例见 [商品图到营销视频](skills/xyq-marketing-skill/examples/product-video.md)
|
|
82
|
+
参数与错误处理见 [接口契约](skills/xyq-marketing-skill/references/api.md),完整示例见 [商品图到营销视频](skills/xyq-marketing-skill/examples/product-video.md)。Skill 脚本通过原生 `marketing` 命令执行;两者均支持 `--team-id`,空间经 HTTP Header 传递,营销请求 JSON 无需增加字段。
|
|
57
83
|
|
|
58
84
|
维护后运行 `node scripts/marketing-skill.test.js` 和 `node scripts/skills.test.js`,验证请求、上传、任务状态、下载与 Skill 引用。测试使用本地模拟服务,不会创建真实付费任务。
|
|
59
85
|
|
|
@@ -64,7 +90,7 @@ node skills/xyq-marketing-skill/scripts/marketing.js query --thread-id THREAD_ID
|
|
|
64
90
|
| 操作 | CLI | 文档 |
|
|
65
91
|
| --- | --- | --- |
|
|
66
92
|
| 登录授权 | `status` / `login` / `logout` | [授权](skills/xyq-nest-skill/commands/auth.md) |
|
|
67
|
-
|
|
|
93
|
+
| 个人或团队 Canvas 画布与节点编辑 | `canvas` | [画布](skills/xyq-nest-skill/commands/canvas.md) |
|
|
68
94
|
| 生图、参考图编辑 | `generate-image` | [图片](skills/xyq-nest-skill/commands/generate-image.md) |
|
|
69
95
|
| 查看可用图片/视频模型、参数配置 | `model list` / `model describe` | [模型发现](skills/xyq-nest-skill/commands/model.md) |
|
|
70
96
|
| 生视频、首尾帧 | `generate-video` | [视频](skills/xyq-nest-skill/commands/generate-video.md) |
|
|
@@ -149,7 +175,7 @@ pippit-tool-cli list-thread-file --thread-id thread_123 --page-num 1 --page-size
|
|
|
149
175
|
pippit-tool-cli download-result --output-path ./thread_123/results/result.mp4 --url URL --updated-at 1779716734
|
|
150
176
|
```
|
|
151
177
|
|
|
152
|
-
`get-credit-balance`:
|
|
178
|
+
`get-credit-balance`: 使用当前登录凭证查询所选个人或团队空间的有效积分余额,并输出 `{"total_remain_amount":"123"}`;零余额会显式输出为 `"0"`。加 `--with-log-id` 可在输出中同时保留本次请求的 `log_id`。
|
|
153
179
|
|
|
154
180
|
`+submit-run`: 输出 `thread_id`、`run_id` 和 `web_thread_link`;其中 `--message` 为必填参数。
|
|
155
181
|
`get-thread`: 请求中带 `version=v2`,并输出 `readable_text`。
|
|
@@ -161,7 +187,7 @@ pippit-tool-cli download-result --output-path ./thread_123/results/result.mp4 --
|
|
|
161
187
|
|
|
162
188
|
## Canvas 原子命令
|
|
163
189
|
|
|
164
|
-
CLI
|
|
190
|
+
CLI 提供个人和团队漫剧画布的通用原子命令,不包含特定来源的导入或转换逻辑:
|
|
165
191
|
|
|
166
192
|
```bash
|
|
167
193
|
# 首次使用时打开小云雀网页授权
|
|
@@ -176,6 +202,8 @@ pippit-tool-cli canvas upload --path ./reference.png
|
|
|
176
202
|
pippit-tool-cli canvas apply --project-id PROJECT_ID --file ./patch.json
|
|
177
203
|
```
|
|
178
204
|
|
|
205
|
+
五个命令复用 `space use TEAM_ID` 保存的默认空间,也支持单次 `--team-id TEAM_ID`,个人空间使用 `0`。操作已有画布时显式使用其原 `--team-id`,CLI 不根据画布 ID 自动选择团队。`canvas get` 返回实际 `team_id`,本地账号可识别时还返回 `space_context`;`canvas command run` 据此固定后续子进程的空间与账号/API origin,期间切换默认空间不影响当前操作。本地恢复记录和检查点按账号、团队与画布隔离,原有个人记录保持可读。
|
|
206
|
+
|
|
179
207
|
五个命令均输出单行 JSON,资源 ID 保持字符串。`allocate` 只预留 ID,实际资产仍由后续 `apply` transaction 创建。`create` 的 `request_id` 用于追踪,不是跨服务崩溃窗口的严格幂等键;写请求结果不明确时不要盲目重放,应先使用 `canvas get` 回读确认。`apply` 当前只接受一个 transaction,但该 transaction 可以包含多个 patches;CLI 会严格检查 transaction ACK 和每个目标资产的新版本。
|
|
180
208
|
|
|
181
209
|
通过 npm 安装的 CLI 还提供基于同一 Canvas SDK 的语义命令目录:
|
|
@@ -308,7 +336,7 @@ pippit-tool-cli model describe MiniMax-H3
|
|
|
308
336
|
pippit-tool-cli model list --refresh
|
|
309
337
|
```
|
|
310
338
|
|
|
311
|
-
|
|
339
|
+
使用当前登录凭证查询服务端 Skill 模型接口;图片 scene 为 `web_image_agent`,视频为 `web_turbo_video_generator`。团队空间的请求携带 `X-Team-Id`,API 内部仍按个人身份查询模型。成功结果按账号、环境和场景隔离缓存 5 分钟,`--refresh` 强制刷新。失败时提示重试,不回退静态列表或过期缓存。`describe` 提供可直接传给生成命令的参数:图片比例保留数字枚举并附比例说明,视频比例转换为字符串,同时整理分辨率、图片推理强度、视频时长和素材限制;不展示内部 `config_key`,未知比例枚举跳过。图片列表和详情仅用 `name` 标识模型,不展示底层模型枚举;搜索、详情查询和 `generate-image --model` 均使用名称,包含空格时加引号。图片详情保留原始 `parameter_config`,包括条件、必选标记、选项说明和参数组合约束。生成仍由服务端校验;服务端尚未开放图片场景或提交参数时会返回错误,不回退静态模型清单。详见 [模型发现](skills/xyq-nest-skill/commands/model.md)。
|
|
312
340
|
|
|
313
341
|
## 生视频 CLI
|
|
314
342
|
|
|
@@ -412,6 +440,7 @@ pippit-tool-cli query-result \
|
|
|
412
440
|
- 同源判断由公共 HTTP client 按 scheme、hostname、有效端口执行;只有同源 API 请求才调用 `PrepareAPIRequest`。非同源请求清除默认认证和 adapter 管理的敏感字段;API 跨域重定向会被拒绝。
|
|
413
441
|
- `AuthProvided` 只表示 adapter 已附加认证材料,不代表服务端验证通过;失败立即返回,不回退到本机认证。`UseDefaultAuth` 才调用默认认证器,未知决策会被拒绝。
|
|
414
442
|
- CLI 策略与 Cobra 解耦。`DefaultAllowRootCommands` 明确指定是否默认放行一级业务命令;为 false 时只开放 `AllowedRootCommands` 白名单,nil 和空列表都表示没有白名单命令。默认 adapter 将该开关设为 true,本机认证和自更新仍受各自开关控制;标准帮助和版本行为仍由 Cobra 提供。可通过 `auth.WithCLIOptions` 将本机认证能力配置应用于独立 Manager。
|
|
443
|
+
- `space` 按普通业务命令白名单开放,不随 `LocalCredentialsEnabled` 隐藏。`space list` 复用请求认证;没有可识别账号时仍可查询列表,JSON 的 `current` 为 `null`。`space use` 需要稳定账号标识,缺失时明确报错;不读取或写入匿名空间配置。显式 `--team-id` 的业务请求仍可使用 adapter 认证。关闭本地凭据后,未用于实际认证的环境 AK 不作为宿主账号标识。
|
|
415
444
|
- 业务 client 与更新遥测 client 都在构造时包装 Transport;遥测 client 属于命令实例,避免使用不同 adapter 的实例共享可变客户端。
|
|
416
445
|
|
|
417
446
|
例如,自定义 adapter 可以复用默认实现,仅调整命令能力,并在自己的程序入口注入:
|
|
@@ -452,7 +481,7 @@ npx @pippit-dev/cli install --source HOST
|
|
|
452
481
|
pippit-tool-cli update --source HOST
|
|
453
482
|
```
|
|
454
483
|
|
|
455
|
-
直接使用 `npm install -g @pippit-dev/cli` 时,可由宿主在该次命令的环境中设置 `PIPPIT_CLI_SOURCE
|
|
484
|
+
直接使用 `npm install -g @pippit-dev/cli` 时,可由宿主在该次命令的环境中设置 `PIPPIT_CLI_SOURCE`。安装和更新均按非空显式 `--source`(命令支持时)、`PIPPIT_CLI_SOURCE`、已核实宿主运行标记的顺序识别来源;去掉首尾空白后,未传或空值继续尝试下一来源。该值不持久化;生成命令运行环境中若也存在此变量,同样会读取它作为宿主来源,具体兜底规则见下文。
|
|
456
485
|
|
|
457
486
|
`report_telemetry` 新增可选请求字段 `host_platform` 上报宿主标识;原有 `source` 保留 `npm_install`、`npx_install`、`cli_update`,`event` 区分 install/update,`platform` 仍表示操作系统。服务端通过独立的 `host_platform` 指标标签统计宿主来源。帮助不安装也不上报,`PIPPIT_CLI_DISABLE_TELEMETRY=1` 仍可关闭上报。内部 `install-cli.js` 仅安装二进制,沿用不安装 Skill、不上报的原有行为。
|
|
458
487
|
|
|
@@ -460,16 +489,18 @@ pippit-tool-cli update --source HOST
|
|
|
460
489
|
|
|
461
490
|
生成提交命令均支持可选 `--source`:`generate-image`、`generate-video`、`video-super-resolution`、`erase-video-subtitle`、`short-drama +submit-run`、`marketing generate`。
|
|
462
491
|
|
|
463
|
-
由宿主 Agent 根据实际环境静默填写稳定标识,例如豆包办公 `doubao_office`、WorkBuddy `workbuddy`、Codex `codex`。其它来源可使用其真实产品标识;来源未知时省略,不询问用户,也不从 prompt 猜测。该值去掉首尾空白后写入请求顶层 `platform
|
|
492
|
+
由宿主 Agent 根据实际环境静默填写稳定标识,例如豆包办公 `doubao_office`、WorkBuddy `workbuddy`、Codex `codex`。其它来源可使用其真实产品标识;来源未知时省略,不询问用户,也不从 prompt 猜测。该值去掉首尾空白后写入请求顶层 `platform`,仅供统计,不参与创作、模型选择或鉴权;无法解析来源时不发送该字段。省略 `--source`、显式空值或纯空白时,CLI 按以下顺序尽力补齐来源:
|
|
464
493
|
|
|
465
494
|
1. 宿主显式设置的 `PIPPIT_CLI_SOURCE`。
|
|
466
|
-
2. 运行标记:`CODEX_THREAD_ID` / `CODEX_SESSION_ID` → `codex`,`CLAUDECODE` → `claude_code`,`CURSOR_AGENT` → `cursor`,`GEMINI_CLI` → `gemini_cli`。
|
|
495
|
+
2. 运行标记:`CODEX_THREAD_ID` / `CODEX_SESSION_ID` → `codex`,`CLAUDECODE` → `claude_code`,`CURSOR_AGENT` → `cursor`,`GEMINI_CLI` → `gemini_cli`,`OPENCODE` → `opencode`,`QWEN_CODE` → `qwen_code`,`CLINE_ACTIVE` → `cline`,`COPILOT_CLI` / `COPILOT_AGENT_SESSION_ID` → `github_copilot_cli`。
|
|
467
496
|
3. 标记缺失、为 0/false 或不同宿主标记冲突时不猜测,省略来源。
|
|
468
497
|
|
|
469
|
-
|
|
498
|
+
非空显式 `--source` 始终优先,空值继续尝试环境识别。仅消费环境标记是否存在,不上传会话标识,不根据 API Key、已安装软件、普通终端名或用户 prompt 猜来源,也不持久化。查询、上传、下载、Canvas 不附加来源。营销脚本的预览与提交采用相同规则;原生 update 也复用该解析,仍写入 `host_platform`,保留旧 source。豆包办公、WorkBuddy 等尚无已核实运行标记的宿主,应优先主动传参或设置上述环境变量。
|
|
470
499
|
|
|
471
500
|
Cursor 标记依据:[官方终端文档](https://docs.cursor.com/en/agent/terminal);Gemini 标记依据:[官方命令文档](https://geminicli.com/docs/reference/commands/)。Codex 标记已在本机运行环境核实;Claude Code 标记已在本机安装产物核实。
|
|
472
501
|
|
|
502
|
+
新增标记依据:[OpenCode 启动入口](https://github.com/anomalyco/opencode/blob/388406238bd5ca15564a762840a2362c3a45bd9c/packages/opencode/src/index.ts#L75-L77)、[Qwen Code shell 环境](https://github.com/QwenLM/qwen-code/blob/e6e2c9efd1dd48bf93143510f7ed1c2413bcea6b/packages/core/src/services/shellExecutionService.ts#L988-L994)、[Cline VS Code 终端环境](https://github.com/cline/cline/blob/e6ad24c43aa2d7551cecdbb97a15269566d3650f/apps/vscode/src/hosts/vscode/terminal/VscodeTerminalRegistry.ts#L28-L34)、[GitHub Copilot CLI 变更记录](https://github.com/github/copilot-cli/blob/main/changelog.md)(0.0.421 增加 `COPILOT_CLI`,1.0.29 增加 `COPILOT_AGENT_SESSION_ID`)。只在子进程实际继承标记时识别,不保证所有宿主版本或启动方式都提供这些标记。
|
|
503
|
+
|
|
473
504
|
```bash
|
|
474
505
|
pippit-tool-cli generate-video --prompt "小猫在花园散步" --model Seedance_2.0_mini --source workbuddy
|
|
475
506
|
```
|
package/checksums.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
e7ee32acef1c0a16912c46500ae7f15c46d920953792d1012886fac7e6424b2e pippit-tool-cli-1.0.43-darwin-amd64.tar.gz
|
|
2
|
+
57e4df86eaa244cd9845eb966299c8c2e1186f74495da0c16f0b378fb4bc5125 pippit-tool-cli-1.0.43-darwin-arm64.tar.gz
|
|
3
|
+
21823c7a675ebcaadebe2ddbfa0324b9e939220d4dd74d5d5d4546f5e6938060 pippit-tool-cli-1.0.43-linux-amd64.tar.gz
|
|
4
|
+
e387bc8eaeafa430a9a594ed1cc381964953b598875b520a6d009a12aec9d619 pippit-tool-cli-1.0.43-linux-arm64.tar.gz
|
|
5
|
+
4569d389a20c75f0485ecc24ad5034308a818f294886dabdf07b2804e915f3a2 pippit-tool-cli-1.0.43-windows-amd64.zip
|
|
6
|
+
078fb4fb09353a58095e1370668884664aad1760ae065983bb23abfdcd9939d1 pippit-tool-cli-1.0.43-windows-arm64.zip
|
package/cmd/auth/auth.go
CHANGED
|
@@ -11,15 +11,17 @@ import (
|
|
|
11
11
|
internal_auth "github.com/Pippit-dev/pippit-cli/internal/auth"
|
|
12
12
|
"github.com/Pippit-dev/pippit-cli/internal/commandnames"
|
|
13
13
|
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
14
|
+
"github.com/Pippit-dev/pippit-cli/internal/space"
|
|
14
15
|
"github.com/spf13/cobra"
|
|
15
16
|
)
|
|
16
17
|
|
|
17
18
|
type loginResult struct {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
DefaultSpace *space.Selection `json:"default_space,omitempty"`
|
|
20
|
+
LoggedIn bool `json:"logged_in"`
|
|
21
|
+
Source string `json:"source"`
|
|
22
|
+
UID string `json:"uid,omitempty"`
|
|
23
|
+
CredentialScope string `json:"credential_scope,omitempty"`
|
|
24
|
+
ExpiresAt string `json:"expires_at,omitempty"`
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
type logoutResult struct {
|
|
@@ -80,6 +82,18 @@ func NewStatusCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Co
|
|
|
80
82
|
return err
|
|
81
83
|
}
|
|
82
84
|
result := loginResult{LoggedIn: status.LoggedIn, Source: status.Source, UID: status.UID, CredentialScope: status.CredentialScope}
|
|
85
|
+
store, scopeErr := common.SpaceStoreForRunner(command.Context(), runner)
|
|
86
|
+
if scopeErr == nil && store != nil {
|
|
87
|
+
selected, err := store.Current()
|
|
88
|
+
scopeErr = err
|
|
89
|
+
if err == nil {
|
|
90
|
+
result.DefaultSpace = &selected
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if scopeErr != nil {
|
|
94
|
+
// Space metadata failures must not be mistaken for expired login.
|
|
95
|
+
fmt.Fprintf(stderr, "无法读取默认空间:%v\n", scopeErr)
|
|
96
|
+
}
|
|
83
97
|
if !status.ExpiresAt.IsZero() {
|
|
84
98
|
result.ExpiresAt = status.ExpiresAt.Format(time.RFC3339)
|
|
85
99
|
}
|
package/cmd/auth/auth_test.go
CHANGED
|
@@ -5,6 +5,8 @@ import (
|
|
|
5
5
|
"context"
|
|
6
6
|
"encoding/json"
|
|
7
7
|
"io"
|
|
8
|
+
"os"
|
|
9
|
+
"path/filepath"
|
|
8
10
|
"strings"
|
|
9
11
|
"testing"
|
|
10
12
|
"time"
|
|
@@ -12,6 +14,7 @@ import (
|
|
|
12
14
|
internal_auth "github.com/Pippit-dev/pippit-cli/internal/auth"
|
|
13
15
|
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
14
16
|
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
17
|
+
"github.com/Pippit-dev/pippit-cli/internal/space"
|
|
15
18
|
)
|
|
16
19
|
|
|
17
20
|
type fakeAuthManager struct {
|
|
@@ -132,3 +135,23 @@ func TestStatusAndLogoutDoNotModifyExplicitEnvironmentCredential(t *testing.T) {
|
|
|
132
135
|
t.Fatalf("logout result/stderr = %#v/%q", result, logoutErr.String())
|
|
133
136
|
}
|
|
134
137
|
}
|
|
138
|
+
|
|
139
|
+
func TestStatusDoesNotMistakeSpaceFailureForLoggedOut(t *testing.T) {
|
|
140
|
+
path := filepath.Join(t.TempDir(), "blocked")
|
|
141
|
+
if err := os.WriteFile(path, []byte("file"), 0600); err != nil {
|
|
142
|
+
t.Fatal(err)
|
|
143
|
+
}
|
|
144
|
+
manager := &fakeAuthManager{status: &internal_auth.Status{LoggedIn: true, Source: "browser", UID: "123"}}
|
|
145
|
+
runner := &common.Runner{Config: &config.Config{}, Auth: manager, SpaceStore: &space.Store{Dir: path}}
|
|
146
|
+
var out, warnings bytes.Buffer
|
|
147
|
+
if err := NewStatusCommand(&out, &warnings, runner).Execute(); err != nil {
|
|
148
|
+
t.Fatal(err)
|
|
149
|
+
}
|
|
150
|
+
var result loginResult
|
|
151
|
+
if err := json.Unmarshal(out.Bytes(), &result); err != nil {
|
|
152
|
+
t.Fatal(err)
|
|
153
|
+
}
|
|
154
|
+
if !result.LoggedIn || result.DefaultSpace != nil || !strings.Contains(warnings.String(), "无法读取默认空间") {
|
|
155
|
+
t.Fatalf("status=%s warning=%s", out.String(), warnings.String())
|
|
156
|
+
}
|
|
157
|
+
}
|
package/cmd/canvas/canvas.go
CHANGED
|
@@ -18,12 +18,12 @@ import (
|
|
|
18
18
|
|
|
19
19
|
const maxApplyRequestBytes = 64 << 20
|
|
20
20
|
|
|
21
|
-
// NewCommand builds the provider-neutral
|
|
21
|
+
// NewCommand builds the provider-neutral Canvas command tree.
|
|
22
22
|
func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
|
|
23
23
|
cmd := &cobra.Command{
|
|
24
24
|
Use: "canvas",
|
|
25
|
-
Short: "Create and operate
|
|
26
|
-
Long: "Create and operate personal
|
|
25
|
+
Short: "Create and operate novel canvases",
|
|
26
|
+
Long: "Create and operate novel canvases in the selected personal or team space.\n\n" + CommandDiscoveryHelp,
|
|
27
27
|
Args: cobra.NoArgs,
|
|
28
28
|
}
|
|
29
29
|
cmd.SetOut(stdout)
|
|
@@ -62,7 +62,7 @@ func newCreateCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Co
|
|
|
62
62
|
var opts canvascore.CreateOptions
|
|
63
63
|
cmd := &cobra.Command{
|
|
64
64
|
Use: "create",
|
|
65
|
-
Short: "Create a
|
|
65
|
+
Short: "Create a novel Canvas project in the selected space",
|
|
66
66
|
Args: cobra.NoArgs,
|
|
67
67
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
68
68
|
if strings.TrimSpace(opts.RequestID) == "" {
|
|
@@ -100,7 +100,7 @@ func newGetCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Comma
|
|
|
100
100
|
var assetIDs []string
|
|
101
101
|
cmd := &cobra.Command{
|
|
102
102
|
Use: "get",
|
|
103
|
-
Short: "Get
|
|
103
|
+
Short: "Get Canvas assets by ID in the selected space",
|
|
104
104
|
Args: cobra.NoArgs,
|
|
105
105
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
106
106
|
result, err := canvascore.Get(cmd.Context(), canvascore.GetOptions{AssetIDs: assetIDs}, runner)
|
|
@@ -150,7 +150,7 @@ func newApplyCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Com
|
|
|
150
150
|
cmd.SetErr(stderr)
|
|
151
151
|
flags := cmd.Flags()
|
|
152
152
|
flags.StringVar(&filePath, "file", "-", "BatchPatch JSON request file, or - for stdin")
|
|
153
|
-
flags.StringVar(&projectID, "project-id", "", "
|
|
153
|
+
flags.StringVar(&projectID, "project-id", "", "novel project ID as a decimal string")
|
|
154
154
|
flags.BoolVar(&transportResult, "transport-result", false, "return explicit transaction outcomes for transport decoding")
|
|
155
155
|
if err := flags.MarkHidden("transport-result"); err != nil {
|
|
156
156
|
panic(err)
|
|
@@ -162,7 +162,7 @@ func newUploadCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Co
|
|
|
162
162
|
var opts canvascore.UploadOptions
|
|
163
163
|
cmd := &cobra.Command{
|
|
164
164
|
Use: "upload",
|
|
165
|
-
Short: "Upload a file to
|
|
165
|
+
Short: "Upload a file to the selected space and wait until it is queryable",
|
|
166
166
|
Args: cobra.NoArgs,
|
|
167
167
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
168
168
|
result, err := canvascore.Upload(cmd.Context(), opts, runner)
|
package/cmd/download_result.go
CHANGED
|
@@ -6,6 +6,7 @@ import (
|
|
|
6
6
|
"strconv"
|
|
7
7
|
"strings"
|
|
8
8
|
|
|
9
|
+
"github.com/Pippit-dev/pippit-cli/internal/commandnames"
|
|
9
10
|
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
10
11
|
"github.com/spf13/cobra"
|
|
11
12
|
)
|
|
@@ -14,10 +15,10 @@ func newDownloadResultCommand(stdout, stderr io.Writer, runner *common.Runner) *
|
|
|
14
15
|
var opts common.DownloadResultOptions
|
|
15
16
|
|
|
16
17
|
cmd := &cobra.Command{
|
|
17
|
-
Use:
|
|
18
|
+
Use: commandnames.DownloadResult,
|
|
18
19
|
Short: "Download a generated result URL",
|
|
19
20
|
Args: cobra.NoArgs,
|
|
20
|
-
RunE: withErrorLog(
|
|
21
|
+
RunE: withErrorLog(commandnames.DownloadResult, func() map[string]string {
|
|
21
22
|
fields := map[string]string{
|
|
22
23
|
"output_path": opts.OutputPath,
|
|
23
24
|
"has_url": strconv.FormatBool(strings.TrimSpace(opts.URL) != ""),
|
package/cmd/marketing.go
CHANGED
|
@@ -18,8 +18,8 @@ import (
|
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
var marketingPaths = map[string]string{
|
|
21
|
-
"generate":
|
|
22
|
-
"query":
|
|
21
|
+
"generate": config.SubmitMarketingRunPath,
|
|
22
|
+
"query": config.QueryGenerateVideoResultPath,
|
|
23
23
|
"upload": config.UploadFilePath,
|
|
24
24
|
"balance": config.GetCreditBalancePath,
|
|
25
25
|
}
|
|
@@ -103,7 +103,19 @@ func newMarketingAction(action string, stdout, stderr io.Writer, runner *common.
|
|
|
103
103
|
}
|
|
104
104
|
body = value
|
|
105
105
|
if !execute {
|
|
106
|
-
|
|
106
|
+
if session := common.SpaceSession(cmd.Context()); session != nil {
|
|
107
|
+
if err := session.CheckIdentity(cmd.Context()); err != nil {
|
|
108
|
+
return err
|
|
109
|
+
}
|
|
110
|
+
ref, err := common.SpaceReferences(body)
|
|
111
|
+
if err != nil {
|
|
112
|
+
return err
|
|
113
|
+
}
|
|
114
|
+
if _, err := session.Resolve(ref); err != nil {
|
|
115
|
+
return err
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return common.WriteJSON(stdout, map[string]any{"dry_run": true, "url": config.DefaultBaseURL + marketingPaths[action], "body": body, "team_id": common.SpaceTeamID(cmd.Context())})
|
|
107
119
|
}
|
|
108
120
|
case "query":
|
|
109
121
|
if strings.TrimSpace(threadID) == "" || strings.TrimSpace(runID) == "" {
|
|
@@ -143,6 +155,10 @@ func newMarketingAction(action string, stdout, stderr io.Writer, runner *common.
|
|
|
143
155
|
_ = json.Unmarshal(result["log_id"], &logID)
|
|
144
156
|
return common.NewLogIDError(fmt.Sprintf("营销 API 失败: ret=%s errmsg=%s", ret, message), logID)
|
|
145
157
|
}
|
|
158
|
+
result["team_id"], _ = json.Marshal(common.SpaceTeamID(ctx))
|
|
159
|
+
if session := common.SpaceSession(ctx); session != nil {
|
|
160
|
+
result["space_context"], _ = json.Marshal(session.Token())
|
|
161
|
+
}
|
|
146
162
|
return common.WriteJSON(stdout, result)
|
|
147
163
|
}
|
|
148
164
|
return command
|
package/cmd/marketing_test.go
CHANGED
|
@@ -17,6 +17,7 @@ import (
|
|
|
17
17
|
"github.com/Pippit-dev/pippit-cli/internal/adapter"
|
|
18
18
|
"github.com/Pippit-dev/pippit-cli/internal/auth"
|
|
19
19
|
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
20
|
+
"github.com/Pippit-dev/pippit-cli/internal/space"
|
|
20
21
|
)
|
|
21
22
|
|
|
22
23
|
type marketingCredentialStore struct {
|
|
@@ -128,6 +129,7 @@ func TestMarketingUsesSharedBrowserAuth(t *testing.T) {
|
|
|
128
129
|
cfg.AccessKey = ""
|
|
129
130
|
store := &marketingCredentialStore{credential: &auth.Credential{AccessKey: "browser-secret", UID: "user", DeviceID: "device", ExpiredAt: time.Now().Add(time.Hour).Unix()}}
|
|
130
131
|
runner := newRootRunner(cfg)
|
|
132
|
+
runner.SpaceStore = &space.Store{Dir: t.TempDir()}
|
|
131
133
|
runner.Auth = auth.NewManager(cfg, auth.WithCredentialStore(store))
|
|
132
134
|
var output bytes.Buffer
|
|
133
135
|
root := newRootCommand(&output, io.Discard, runner)
|
|
@@ -149,7 +151,7 @@ func TestMarketingUsesSharedBrowserAuth(t *testing.T) {
|
|
|
149
151
|
if err := root.Execute(); err != nil {
|
|
150
152
|
t.Fatal(err)
|
|
151
153
|
}
|
|
152
|
-
if calls != 1 || store.loads !=
|
|
154
|
+
if calls != 1 || store.loads != 2 {
|
|
153
155
|
t.Fatalf("HTTP calls=%d credential loads=%d", calls, store.loads)
|
|
154
156
|
}
|
|
155
157
|
if strings.Contains(output.String(), "browser-secret") || !strings.Contains(output.String(), "log-test") {
|
|
@@ -187,7 +189,7 @@ func TestMarketingAuthFailureAndEnvironmentPrecedence(t *testing.T) {
|
|
|
187
189
|
root.SetArgs([]string{"marketing", "balance"})
|
|
188
190
|
err := root.Execute()
|
|
189
191
|
if scenario == "override" {
|
|
190
|
-
if err != nil || calls != 1 || store.loads !=
|
|
192
|
+
if err != nil || calls != 1 || store.loads != 1 {
|
|
191
193
|
t.Fatalf("override: err=%v calls=%d loads=%d", err, calls, store.loads)
|
|
192
194
|
}
|
|
193
195
|
} else if err == nil || !strings.Contains(err.Error(), "pippit-tool-cli login") || calls != 0 {
|
|
@@ -197,7 +199,7 @@ func TestMarketingAuthFailureAndEnvironmentPrecedence(t *testing.T) {
|
|
|
197
199
|
}
|
|
198
200
|
}
|
|
199
201
|
|
|
200
|
-
func
|
|
202
|
+
func TestMarketingOfflineCommandsReadIdentityOnlyWhenNeeded(t *testing.T) {
|
|
201
203
|
for _, args := range [][]string{{"marketing", "--help"}, {"marketing", "generate", "--request", "-"}, {"marketing", "query"}} {
|
|
202
204
|
cfg := config.Load()
|
|
203
205
|
cfg.AccessKey = ""
|
|
@@ -214,8 +216,12 @@ func TestMarketingPreviewAndHelpDoNotReadCredentials(t *testing.T) {
|
|
|
214
216
|
if args[1] == "query" && err == nil {
|
|
215
217
|
t.Fatal("missing IDs accepted")
|
|
216
218
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
+
wantLoads := 1 // Resolve the local account for space selection, without requiring login.
|
|
220
|
+
if args[1] == "--help" {
|
|
221
|
+
wantLoads = 0
|
|
222
|
+
}
|
|
223
|
+
if store.loads != wantLoads {
|
|
224
|
+
t.Fatalf("%v: credential loads=%d, want %d", args, store.loads, wantLoads)
|
|
219
225
|
}
|
|
220
226
|
}
|
|
221
227
|
}
|
package/cmd/model.go
CHANGED
|
@@ -6,6 +6,7 @@ import (
|
|
|
6
6
|
"strings"
|
|
7
7
|
"time"
|
|
8
8
|
|
|
9
|
+
"github.com/Pippit-dev/pippit-cli/internal/commandnames"
|
|
9
10
|
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
10
11
|
"github.com/Pippit-dev/pippit-cli/internal/models"
|
|
11
12
|
"github.com/spf13/cobra"
|
|
@@ -40,11 +41,11 @@ func newModelCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Com
|
|
|
40
41
|
return common.WriteJSON(stdout, output)
|
|
41
42
|
}
|
|
42
43
|
cmd := &cobra.Command{
|
|
43
|
-
Use: "
|
|
44
|
+
Use: commandnames.Model + " [name-or-key]",
|
|
44
45
|
Short: "Discover available image and video models and their server configuration",
|
|
45
46
|
Long: "Query available image and video models using your current credentials. Select images by their full display name (in quotes), videos by key. Successful queries are cached locally for 5 minutes. Use --refresh to bypass the cache; retry if a query fails.",
|
|
46
47
|
Args: cobra.MaximumNArgs(1),
|
|
47
|
-
RunE: withErrorLog(
|
|
48
|
+
RunE: withErrorLog(commandnames.Model, nil, func(cmd *cobra.Command, args []string) error {
|
|
48
49
|
return query(cmd, args, len(args) > 0)
|
|
49
50
|
}),
|
|
50
51
|
}
|
|
@@ -56,14 +57,14 @@ func newModelCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Com
|
|
|
56
57
|
Use: "list [query]", Aliases: []string{"search"},
|
|
57
58
|
Short: "List models; search images by name, videos by key or name",
|
|
58
59
|
Args: cobra.MaximumNArgs(1),
|
|
59
|
-
RunE: withErrorLog("
|
|
60
|
+
RunE: withErrorLog(commandnames.Model+" list", nil, func(cmd *cobra.Command, args []string) error {
|
|
60
61
|
return query(cmd, args, false)
|
|
61
62
|
}),
|
|
62
63
|
})
|
|
63
64
|
cmd.AddCommand(&cobra.Command{
|
|
64
65
|
Use: "describe <name-or-key>", Short: "Show model parameters; use the full image name or video key",
|
|
65
66
|
Args: cobra.ExactArgs(1),
|
|
66
|
-
RunE: withErrorLog("
|
|
67
|
+
RunE: withErrorLog(commandnames.Model+" describe", nil, func(cmd *cobra.Command, args []string) error {
|
|
67
68
|
return query(cmd, args, true)
|
|
68
69
|
}),
|
|
69
70
|
})
|
package/cmd/root.go
CHANGED
|
@@ -20,6 +20,7 @@ import (
|
|
|
20
20
|
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
21
21
|
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
22
22
|
"github.com/Pippit-dev/pippit-cli/internal/version"
|
|
23
|
+
"github.com/Pippit-dev/pippit-cli/internal/versioncheck"
|
|
23
24
|
"github.com/spf13/cobra"
|
|
24
25
|
)
|
|
25
26
|
|
|
@@ -86,17 +87,27 @@ func newRootCommandWithAdapter(stdout, stderr io.Writer, runner *common.Runner,
|
|
|
86
87
|
SilenceUsage: true,
|
|
87
88
|
SilenceErrors: true,
|
|
88
89
|
}
|
|
89
|
-
// Only commands already exposing --source receive attribution.
|
|
90
|
-
// flags
|
|
90
|
+
// Only commands already exposing --source receive attribution. Missing or blank
|
|
91
|
+
// flags allow fallback, and no source is persisted between CLI processes.
|
|
92
|
+
// 子命令执行 pre hook
|
|
91
93
|
root.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {
|
|
94
|
+
if policy.CLIOptions().SelfUpdateEnabled && cmd.Runnable() {
|
|
95
|
+
versioncheck.Check(cmd.Context(), stderr, version.BuildVersion(), cmd.Name())
|
|
96
|
+
}
|
|
92
97
|
flag := cmd.Flags().Lookup("source")
|
|
93
|
-
if flag != nil && !flag.Changed {
|
|
98
|
+
if flag != nil && (!flag.Changed || strings.TrimSpace(flag.Value.String()) == "") {
|
|
94
99
|
if source := common.DetectHostSource(); source != "" {
|
|
95
|
-
|
|
100
|
+
if err := cmd.Flags().Set("source", source); err != nil {
|
|
101
|
+
return err
|
|
102
|
+
}
|
|
96
103
|
}
|
|
97
104
|
}
|
|
98
|
-
return
|
|
105
|
+
return prepareCommandSpace(cmd, runner, stderr)
|
|
99
106
|
}
|
|
107
|
+
// 定义根命令和子命令都能使用的字符串参数
|
|
108
|
+
root.PersistentFlags().String("team-id", "", "本次业务操作的空间:0 为个人空间,正整数为团队空间;不改变默认空间")
|
|
109
|
+
root.PersistentFlags().String("space-context", "", "固定跨进程操作的账号、API origin 和空间")
|
|
110
|
+
_ = root.PersistentFlags().MarkHidden("space-context")
|
|
100
111
|
root.CompletionOptions.DisableDefaultCmd = true
|
|
101
112
|
root.SetVersionTemplate("{{.Version}}\n")
|
|
102
113
|
root.SetOut(stdout)
|
|
@@ -104,6 +115,7 @@ func newRootCommandWithAdapter(stdout, stderr io.Writer, runner *common.Runner,
|
|
|
104
115
|
root.AddCommand(authcmd.NewLoginCommand(stdout, stderr, runner))
|
|
105
116
|
root.AddCommand(authcmd.NewStatusCommand(stdout, stderr, runner))
|
|
106
117
|
root.AddCommand(authcmd.NewLogoutCommand(stdout, stderr, runner))
|
|
118
|
+
root.AddCommand(newSpaceCommand(stdout, runner))
|
|
107
119
|
root.AddCommand(canvascmd.NewCommand(stdout, stderr, runner))
|
|
108
120
|
root.AddCommand(newDownloadResultCommand(stdout, stderr, runner))
|
|
109
121
|
root.AddCommand(newGetCreditBalanceCommand(stdout, stderr, runner))
|