@yangtzeu-zhiyun/zhiyun-cli 0.1.0

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/AGENT_GUIDE.md ADDED
@@ -0,0 +1,54 @@
1
+ # Agent 使用指南(zhiyun CLI)
2
+
3
+ 面向 AI Agent 的操作说明。先读平台文档索引 `https://zhiyun.lansuan.cc/docs/llms.txt`,再按本指南驱动 CLI。
4
+
5
+ ## 核心原则
6
+
7
+ 1. **机器可读优先**:所有命令加 `--json`,stdout 只有一个稳定 JSON 对象;进度与错误在 stderr。用退出码判断结果(0 成功 | 2 参数 | 3 未登录 | 4 无权限 | 5 校验失败 | 6 网络 | 7 审核状态冲突)。
8
+ 2. **写远端的命令先 `--dry-run`**:`create / push / submit` 支持 `--dry-run`(不发写请求),确认计划后再真实执行。
9
+ 3. **不要重复创建项目**:`zhiyun.json` 记录 `remoteId`;`create` 对已创建项目直接报告而不重复建。幂等靠它,不靠猜。
10
+ 4. **完整 ID 以服务端返回为准**:本地模板中的 `〈登录账号派生段〉` 占位符由 `create` 自动回填(manifest.yaml / app.json)。
11
+ 5. **本地预检 ≠ 平台校验**:`validate --local` 只查格式、大小、路径、清单一致性;最终以服务端校验为准。
12
+ 6. **永不处理 Token 明文**:凭据来自 `ZHIYUN_TOKEN` 或本机凭据文件;不要在日志、交付说明或 JSON 里输出。
13
+
14
+ ## 标准工作流(以 Skill 为例)
15
+
16
+ ```bash
17
+ # 1. 让 AI 按 public/docs/prompts.md 模板产出 SKILL.md 与交付清单
18
+ # 2. 在交付清单给出的目录初始化并放置文件
19
+ zhiyun skill init --name "水稻病虫害诊治助手" --id rice-pest-guard --description "…"
20
+ # 3. 平台创建项目(写远端;先 --dry-run 查看请求)
21
+ zhiyun skill create --dry-run && zhiyun skill create
22
+ # 4. 打包并上传草稿
23
+ zhiyun skill pack
24
+ zhiyun skill push --zip ./rice-pest-guard.skill.zip
25
+ # 5. 校验(服务端为准)
26
+ zhiyun skill validate
27
+ # 6. 提审 → 跟踪状态
28
+ zhiyun skill submit --release-notes "首版"
29
+ zhiyun skill status --json
30
+ ```
31
+
32
+ Plugin / App 同构(`zhiyun plugin ...` / `zhiyun app ...`)。差异:
33
+
34
+ - Plugin 的 remote-http / remote-mcp 方式不传代码包;`plugin test --capability <id> --input '<json>'` 真实执行一次。
35
+ - App 审核通过即上架:没有 `publish`;`app test` 只报告沙箱预览与本地 mock 的用法。
36
+
37
+ ## 典型错误处置
38
+
39
+ - 退出码 3:未登录。提示用户完成登录(设备授权或 ZHIYUN_TOKEN),不要伪造凭据。
40
+ - 退出码 7:审核状态冲突。先 `status --json` 读取版本状态,再决定 `withdraw` 或等待;不要重试写操作。
41
+ - 包校验失败(5):按 `validate --local` 输出逐项修复,不要跳过预检直接 push。
42
+ - `skill_list` 查询的是本人开发者项目;面向用户的市场目录是 `GET /api/skills`,不要混用。
43
+
44
+ ## 命令速查
45
+
46
+ | 命令 | 写远端 | 说明 |
47
+ | --- | --- | --- |
48
+ | `doctor` / `whoami` | 否 | 环境与登录诊断 |
49
+ | `capability list` / `<type> list` | 否 | 目录查询(需登录) |
50
+ | `<type> init` / `pack` / `validate --local` | 否(只写本地) | 本地工具链 |
51
+ | `<type> create` / `push` | 是 | 支持幂等(zhiyun.json)与 --dry-run |
52
+ | `<type> validate`(默认) | 否 | 调服务端校验草稿 |
53
+ | `<type> test` | Skill 否 / Plugin 是 / App 否 | 见上 |
54
+ | `<type> submit` / `withdraw` / `publish` | 是 | 审核流转;publish 仅 APPROVED 的 Skill/Plugin |
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @yangtzeu-zhiyun/zhiyun-cli
2
+
3
+ 长大智耘(Zhiyun)开发者命令行:在本地创建、校验、打包 Skill / Plugin / App,并提交审核。
4
+
5
+ ```bash
6
+ npm install -g @yangtzeu-zhiyun/zhiyun-cli
7
+ zhiyun --help
8
+ ```
9
+
10
+ ## 当前版本覆盖范围
11
+
12
+ - 本地工具:`init`(模板 + `zhiyun.json`)、`pack`(跨平台零依赖 zip)、`validate --local`(本地预检)。
13
+ - 只读远端:`doctor`、`whoami`、`capability list`、`skill|plugin|app list`(含 `skill_list` 等别名)。
14
+ - 远端写入:`create / push / validate / test / submit / status / withdraw / publish`(需登录态;浏览器设备授权登录在后续版本提供,当前可临时导出 `ZHIYUN_TOKEN=<开发者域 JWT>`)。
15
+
16
+ 完整命令契约与示例见 <https://zhiyun.lansuan.cc/docs/cli.md>。
17
+
18
+ ## 登录(当前方式)
19
+
20
+ ```bash
21
+ export ZHIYUN_TOKEN=<开发者域 JWT>
22
+ zhiyun whoami # 校验身份
23
+ zhiyun logout # 清理本机凭据副本
24
+ ```
25
+
26
+ ## 退出码
27
+
28
+ 0 成功 | 2 参数错误 | 3 未登录 | 4 无权限 | 5 校验失败 | 6 网络故障 | 7 审核状态冲突。
29
+
30
+ ## 安全约定
31
+
32
+ - Token 只来自 `ZHIYUN_TOKEN` 或本机凭据文件(`~/.config/zhiyun-cli/credentials.json`,0600),终端与 `--json` 输出永不回显。
33
+ - 凭据不会发往非 https 地址(本地调试仅允许 `http://localhost:*`)。
34
+ - 本地预检不能替代平台校验:服务端校验才是最终结论。
35
+
36
+ ## 开发
37
+
38
+ ```bash
39
+ npm install
40
+ npm run build # tsc → dist/
41
+ npm test # node --test test/
42
+ ```
package/bin/zhiyun.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../dist/cli.js'
3
+
4
+ main(process.argv.slice(2)).catch((error) => {
5
+ console.error(error?.stack ?? String(error))
6
+ process.exit(1)
7
+ })
package/dist/api.js ADDED
@@ -0,0 +1,137 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { CliError, ExitCode } from './exit.js';
5
+ export const DEFAULT_BASE_URL = 'https://zhiyun.lansuan.cc';
6
+ export function resolveBaseUrl(flag) {
7
+ const raw = flag ?? process.env.ZHIYUN_BASE_URL ?? DEFAULT_BASE_URL;
8
+ const url = raw.replace(/\/+$/, '');
9
+ if (url.startsWith('http://')) {
10
+ const host = new URL(url).hostname;
11
+ if (host !== 'localhost' && host !== '127.0.0.1' && host !== '[::1]') {
12
+ throw new CliError(ExitCode.Usage, '凭据不会发往非 https 地址;本地调试请使用 http://localhost:<port>', `当前 --base-url: ${url}`);
13
+ }
14
+ }
15
+ else if (!url.startsWith('https://')) {
16
+ throw new CliError(ExitCode.Usage, `--base-url 必须以 https:// 开头(本地可用 http://localhost:<port>): ${url}`);
17
+ }
18
+ return url;
19
+ }
20
+ function credentialsPath() {
21
+ const configHome = process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config');
22
+ return join(configHome, 'zhiyun-cli', 'credentials.json');
23
+ }
24
+ /** 凭据解析顺序:ZHIYUN_TOKEN 环境变量 > 本机凭据文件(0600,设备授权登录在后续版本写入)。 */
25
+ export function loadToken() {
26
+ const envToken = process.env.ZHIYUN_TOKEN?.trim();
27
+ if (envToken)
28
+ return envToken;
29
+ const path = credentialsPath();
30
+ if (!existsSync(path))
31
+ return undefined;
32
+ try {
33
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
34
+ return parsed.developerToken?.trim() || undefined;
35
+ }
36
+ catch {
37
+ return undefined;
38
+ }
39
+ }
40
+ export function requireToken() {
41
+ const token = loadToken();
42
+ if (!token) {
43
+ throw new CliError(ExitCode.NotLoggedIn, '尚未登录智耘开发者账号', '浏览器设备授权登录(zhiyun login)将在后续版本提供;当前可临时导出 ZHIYUN_TOKEN=<开发者域 JWT> 使用远端命令');
44
+ }
45
+ return token;
46
+ }
47
+ export function saveCredentials(developerToken) {
48
+ const path = credentialsPath();
49
+ mkdirSync(join(path, '..'), { recursive: true });
50
+ writeFileSync(path, `${JSON.stringify({ developerToken, storedAt: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
51
+ return path;
52
+ }
53
+ export function clearCredentials() {
54
+ const path = credentialsPath();
55
+ if (!existsSync(path))
56
+ return false;
57
+ writeFileSync(path, '{}\n', { mode: 0o600 });
58
+ return true;
59
+ }
60
+ export class ApiError extends CliError {
61
+ status;
62
+ constructor(status, message, hint) {
63
+ super(mapStatusToExitCode(status), message, hint);
64
+ this.name = 'ApiError';
65
+ this.status = status;
66
+ }
67
+ }
68
+ function mapStatusToExitCode(status) {
69
+ if (status === 401)
70
+ return ExitCode.NotLoggedIn;
71
+ if (status === 403)
72
+ return ExitCode.Forbidden;
73
+ if (status === 409 || status === 423)
74
+ return ExitCode.ReviewConflict;
75
+ if (status === 400 || status === 422)
76
+ return ExitCode.Validation;
77
+ return ExitCode.Network;
78
+ }
79
+ /** 统一 HTTP 客户端:401/403/409/400 映射到契约退出码,网络层故障统一 6。 */
80
+ export async function requestJson(baseUrl, path, options = {}) {
81
+ const headers = { Accept: 'application/json' };
82
+ if (options.token)
83
+ headers.Authorization = `Bearer ${options.token}`;
84
+ if (options.body !== undefined)
85
+ headers['Content-Type'] = 'application/json';
86
+ let response;
87
+ try {
88
+ response = await fetch(`${baseUrl}${path}`, {
89
+ method: options.method ?? 'GET',
90
+ headers,
91
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
92
+ signal: AbortSignal.timeout(options.timeoutMs ?? 30_000),
93
+ });
94
+ }
95
+ catch (error) {
96
+ const reason = error instanceof Error ? error.message : String(error);
97
+ throw new CliError(ExitCode.Network, `网络请求失败: ${path}`, reason);
98
+ }
99
+ return parseResponse(response);
100
+ }
101
+ export async function uploadMultipart(baseUrl, path, token, fileName, data) {
102
+ const form = new FormData();
103
+ form.append('file', new Blob([new Uint8Array(data)]), fileName);
104
+ let response;
105
+ try {
106
+ response = await fetch(`${baseUrl}${path}`, {
107
+ method: 'POST',
108
+ headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
109
+ body: form,
110
+ signal: AbortSignal.timeout(60_000),
111
+ });
112
+ }
113
+ catch (error) {
114
+ const reason = error instanceof Error ? error.message : String(error);
115
+ throw new CliError(ExitCode.Network, `上传失败: ${path}`, reason);
116
+ }
117
+ return parseResponse(response);
118
+ }
119
+ async function parseResponse(response) {
120
+ const text = await response.text();
121
+ let payload = undefined;
122
+ if (text) {
123
+ try {
124
+ payload = JSON.parse(text);
125
+ }
126
+ catch {
127
+ payload = undefined;
128
+ }
129
+ }
130
+ if (!response.ok) {
131
+ const serverMessage = (payload !== null && typeof payload === 'object' && 'message' in payload && typeof payload.message === 'string'
132
+ ? payload.message
133
+ : undefined) ?? response.statusText;
134
+ throw new ApiError(response.status, `HTTP ${response.status}: ${serverMessage}`);
135
+ }
136
+ return payload;
137
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,146 @@
1
+ import { runDoctor } from './commands/doctor.js';
2
+ import { commandHelp, topHelp } from './commands/help.js';
3
+ import { runAppList, runCapabilityList, runProjectList } from './commands/list.js';
4
+ import { runProjectCommand } from './commands/project.js';
5
+ import { clearCredentials, requestJson, requireToken, resolveBaseUrl } from './api.js';
6
+ import { CliError, ExitCode } from './exit.js';
7
+ import { printError } from './output.js';
8
+ const KNOWN_BOOLEAN_FLAGS = new Set(['json', 'dry-run', 'help', 'local', 'no-browser']);
9
+ export function parseArgs(argv) {
10
+ const positionals = [];
11
+ const flags = {};
12
+ for (let i = 0; i < argv.length; i++) {
13
+ const arg = argv[i];
14
+ if (arg.startsWith('--')) {
15
+ const eq = arg.indexOf('=');
16
+ if (eq > 0) {
17
+ flags[arg.slice(2, eq)] = arg.slice(eq + 1);
18
+ continue;
19
+ }
20
+ const name = arg.slice(2);
21
+ const next = argv[i + 1];
22
+ if (!KNOWN_BOOLEAN_FLAGS.has(name) && next !== undefined && !next.startsWith('--')) {
23
+ flags[name] = next;
24
+ i++;
25
+ }
26
+ else {
27
+ flags[name] = true;
28
+ }
29
+ }
30
+ else if (arg === '-h') {
31
+ flags.help = true;
32
+ }
33
+ else {
34
+ positionals.push(arg);
35
+ }
36
+ }
37
+ return { positionals, flags };
38
+ }
39
+ const KINDS = new Set(['skill', 'plugin', 'app']);
40
+ const KIND_ALIASES = {
41
+ skill: 'Skill',
42
+ plugin: 'Plugin',
43
+ app: 'App',
44
+ };
45
+ export async function main(argv) {
46
+ let code = ExitCode.Unexpected;
47
+ try {
48
+ code = await dispatch(argv);
49
+ }
50
+ catch (error) {
51
+ if (error instanceof CliError) {
52
+ printError(error);
53
+ code = error.code;
54
+ }
55
+ else {
56
+ console.error(error instanceof Error ? (error.stack ?? error.message) : String(error));
57
+ code = ExitCode.Unexpected;
58
+ }
59
+ }
60
+ process.exit(code);
61
+ }
62
+ async function dispatch(argv) {
63
+ const { positionals, flags } = parseArgs(argv);
64
+ if (flags.help) {
65
+ console.log(topHelp());
66
+ return 0;
67
+ }
68
+ if (flags.version || positionals[0] === 'version') {
69
+ console.log(await packageVersion());
70
+ return 0;
71
+ }
72
+ const global = {
73
+ json: flags.json === true,
74
+ dryRun: flags.dryRun === true,
75
+ baseUrl: typeof flags['base-url'] === 'string' ? flags['base-url'] : undefined,
76
+ };
77
+ if (global.baseUrl)
78
+ global.baseUrl = resolveBaseUrl(global.baseUrl);
79
+ const baseUrl = global.baseUrl ?? resolveBaseUrl();
80
+ const [first] = positionals;
81
+ const rest = positionals.slice(1);
82
+ const wantsHelp = rest.length > 0 && rest[rest.length - 1] === 'help';
83
+ if (!first || first === 'help') {
84
+ console.log(topHelp());
85
+ return 0;
86
+ }
87
+ if (first === 'doctor')
88
+ return runDoctor(global, baseUrl);
89
+ if (first === 'login') {
90
+ console.log('浏览器设备授权登录将在后续版本提供。');
91
+ console.log('当前可临时导出 ZHIYUN_TOKEN=<开发者域 JWT> 使用远端命令;详见 https://zhiyun.lansuan.cc/docs/cli.md');
92
+ return ExitCode.Usage;
93
+ }
94
+ if (first === 'logout') {
95
+ const cleared = clearCredentials();
96
+ if (flags.json === true)
97
+ console.log(JSON.stringify({ command: 'logout', cleared }));
98
+ else
99
+ console.log(cleared ? '已清理本机凭据。' : '本机没有已保存的凭据。');
100
+ return 0;
101
+ }
102
+ if (first === 'whoami') {
103
+ const profile = await requestJson(baseUrl, '/api/profile', { token: requireToken() });
104
+ if (global.json)
105
+ console.log(JSON.stringify({ command: 'whoami', profile }, null, 2));
106
+ else
107
+ console.log(JSON.stringify(profile, null, 2));
108
+ return 0;
109
+ }
110
+ if (first === 'capability') {
111
+ const sub = rest[0] ?? 'list';
112
+ if (wantsHelp) {
113
+ console.log(commandHelp('capability list'));
114
+ return 0;
115
+ }
116
+ if (sub !== 'list')
117
+ throw new CliError(ExitCode.Usage, `capability 只支持 list 子命令: ${sub}`);
118
+ return runCapabilityList(global, baseUrl);
119
+ }
120
+ const aliasMatch = /^(skill|plugin)_(list)$/.exec(first);
121
+ if (aliasMatch) {
122
+ const kind = KIND_ALIASES[aliasMatch[1]];
123
+ return runProjectList(kind, global, baseUrl);
124
+ }
125
+ if (KINDS.has(first)) {
126
+ const kind = KIND_ALIASES[first];
127
+ const [sub] = rest;
128
+ if (!sub || sub === 'help' || flags.help) {
129
+ console.log(commandHelp(sub && sub !== 'help' ? sub : 'list'));
130
+ return 0;
131
+ }
132
+ if (sub === 'list' || sub === 'ls') {
133
+ return kind === 'App' ? runAppList(global, baseUrl) : runProjectList(kind, global, baseUrl);
134
+ }
135
+ return runProjectCommand(kind, sub, flags, global, baseUrl);
136
+ }
137
+ throw new CliError(ExitCode.Usage, `未知命令: ${first}`, '运行 zhiyun help 查看命令清单');
138
+ }
139
+ async function packageVersion() {
140
+ const { createRequire } = await import('node:module');
141
+ const { join } = await import('node:path');
142
+ const { fileURLToPath } = await import('node:url');
143
+ const require = createRequire(import.meta.url);
144
+ const pkg = require(join(fileURLToPath(import.meta.url), '../../package.json'));
145
+ return `zhiyun ${pkg.version ?? '0.0.0'}`;
146
+ }
@@ -0,0 +1,53 @@
1
+ import { loadToken, requestJson } from '../api.js';
2
+ import { emitJson, emitText } from '../output.js';
3
+ export async function runDoctor(global, baseUrl) {
4
+ const nodeOk = Number.parseInt(process.versions.node, 10) >= 22;
5
+ const token = loadToken();
6
+ let serverOk = false;
7
+ let serverDetail = '';
8
+ let authOk = false;
9
+ let authDetail = token ? '已配置凭据' : '未配置凭据(ZHIYUN_TOKEN 或 zhiyun login)';
10
+ try {
11
+ const health = await requestJson(baseUrl, '/api/health', { timeoutMs: 8000 });
12
+ serverOk = health.status === 'ok';
13
+ serverDetail = `${health.app ?? 'zhiyun'} ${health.time ?? ''}`.trim();
14
+ }
15
+ catch (error) {
16
+ serverDetail = error instanceof Error ? error.message : String(error);
17
+ }
18
+ if (token) {
19
+ try {
20
+ const profile = await requestJson(baseUrl, '/api/profile', {
21
+ token,
22
+ timeoutMs: 8000,
23
+ });
24
+ const account = typeof profile.username === 'string' ? profile.username : typeof profile.account === 'string' ? profile.account : '未知';
25
+ authOk = true;
26
+ authDetail = `开发者凭据有效(${account})`;
27
+ }
28
+ catch (error) {
29
+ authDetail = `凭据校验失败: ${error instanceof Error ? error.message : String(error)}`;
30
+ }
31
+ }
32
+ const report = {
33
+ command: 'doctor',
34
+ baseUrl,
35
+ checks: [
36
+ { name: 'node>=22', ok: nodeOk, detail: process.versions.node },
37
+ { name: 'server', ok: serverOk, detail: serverDetail },
38
+ { name: 'auth', ok: authOk, detail: authDetail },
39
+ ],
40
+ ok: nodeOk && serverOk,
41
+ };
42
+ if (global.json) {
43
+ emitJson(report);
44
+ }
45
+ else {
46
+ emitText([
47
+ `Node >=22: ${nodeOk ? 'OK' : 'FAIL'}(当前 ${process.versions.node})`,
48
+ `服务端: ${serverOk ? 'OK' : 'FAIL'} ${serverDetail}`,
49
+ `登录状态: ${authOk ? 'OK' : '未登录'} ${authDetail}`,
50
+ ]);
51
+ }
52
+ return 0;
53
+ }
@@ -0,0 +1,111 @@
1
+ export function topHelp() {
2
+ return `zhiyun — 长大智耘开发者命令行
3
+
4
+ 用法: zhiyun <命令> [子命令] [参数]
5
+
6
+ 常用命令:
7
+ zhiyun login [--no-browser] 浏览器授权登录开发者账号(后续版本提供;当前可设 ZHIYUN_TOKEN)
8
+ zhiyun whoami | logout 查看身份 / 撤销登录
9
+ zhiyun doctor 检查 Node 版本、服务可达与登录状态
10
+ zhiyun capability list 已注册能力目录
11
+
12
+ 项目命令(<type> = skill | plugin | app):
13
+ zhiyun <type> list 我的项目列表(别名 skill_list / plugin_list / app_list)
14
+ zhiyun <type> init 生成本地模板与 zhiyun.json(只写本地)
15
+ zhiyun <type> create 在平台创建归属项目(写远端)
16
+ zhiyun <type> pack [--out ...] 打包 zip(只写本地)
17
+ zhiyun <type> validate [--local] 校验(默认调服务端;--local 仅本地预检)
18
+ zhiyun <type> push [--zip ...] 上传/更新草稿包(写远端)
19
+ zhiyun <type> test 调试运行(Skill 调试会话 / Plugin 测试接口)
20
+ zhiyun <type> submit 提交审核(写远端)
21
+ zhiyun <type> status 查看版本与审核状态
22
+ zhiyun <type> withdraw 审核中撤回(写远端)
23
+ zhiyun skill|plugin publish 发布已批准版本(写远端;App 无此命令)
24
+
25
+ 通用参数:
26
+ --json stdout 只输出一个稳定 JSON 对象(Agent 优先)
27
+ --dry-run 只做本地预检与展示请求计划,不发写请求
28
+ --base-url <url> 覆盖服务端地址(默认 https://zhiyun.lansuan.cc;本地调试用 http://localhost:<port>)
29
+ --help 查看命令说明(含是否修改远端状态)
30
+
31
+ 退出码: 0 成功 | 2 参数错误 | 3 未登录 | 4 无权限 | 5 校验失败 | 6 网络故障 | 7 审核状态冲突
32
+
33
+ 文档: https://zhiyun.lansuan.cc/docs/cli.md`;
34
+ }
35
+ export function commandHelp(path) {
36
+ const pages = {
37
+ login: `zhiyun login [--no-browser]
38
+
39
+ 发起设备授权登录(浏览器授权智耘开发者账号)。终端展示验证网址与用户码并尝试打开浏览器;
40
+ --no-browser 只展示(SSH / 无浏览器环境)。
41
+ 状态: 写远端(创建/兑换凭据)——当前版本尚未开放,可临时使用 ZHIYUN_TOKEN 环境变量。`,
42
+ whoami: `zhiyun whoami
43
+
44
+ 校验并显示当前登录的开发者身份。
45
+ 状态: 只读。`,
46
+ logout: `zhiyun logout
47
+
48
+ 撤销登录:清理本机凭据副本(服务端撤销在后续版本一并提供)。
49
+ 状态: 清理本机凭据。`,
50
+ doctor: `zhiyun doctor
51
+
52
+ 检查 Node 版本、服务端连通性(/api/health)与登录状态。
53
+ 状态: 只读。`,
54
+ 'capability list': `zhiyun capability list [--json]
55
+
56
+ 列出平台已注册能力目录(系统内置 + 已启用扩展)。
57
+ 状态: 只读(需登录)。`,
58
+ list: `zhiyun <skill|plugin|app> list [--json]
59
+
60
+ 查询本人的开发者项目并按类型过滤(skill_list / plugin_list / app_list 为兼容别名)。
61
+ 注意这是本人项目列表,不是面向用户的市场目录。
62
+ 状态: 只读(需登录)。`,
63
+ init: `zhiyun <skill|plugin|app> init --name <名称> --id <ID短码> --description <一句话> [--version 0.1.0]
64
+ [--runtime local-python|local-node|remote-http|remote-mcp] # 仅 plugin
65
+ [--category <分类>] [--permissions p1,p2] # 仅 app
66
+
67
+ 在当前目录生成本地模板与 zhiyun.json。
68
+ 状态: 只写本地。`,
69
+ create: `zhiyun <skill|plugin|app> create [--dry-run]
70
+
71
+ 在智耘平台创建归属项目(Skill/Plugin 走 /api/developer/projects;App 走 /api/developer/apps),
72
+ 成功后把远端项目 ID 与完整 ID 写回 zhiyun.json,并回填本地 manifest/app.json 的占位完整 ID。
73
+ 重复执行不会误建项目:zhiyun.json 已有 remoteId 时直接报告已有项目。
74
+ 状态: 写远端(需登录)。`,
75
+ pack: `zhiyun <skill|plugin|app> pack [--out <zip路径>]
76
+
77
+ 按类型收集文件并打包 zip(Skill 白名单目录;Plugin manifest.yaml + capabilities/;App app.json + 全量资源)。
78
+ 状态: 只写本地。`,
79
+ validate: `zhiyun <skill|plugin|app> validate [--local] [--json]
80
+
81
+ --local 只做本地预检(格式、大小、路径、清单一致性);默认先本地预检再调服务端校验草稿版本。
82
+ 本地预检不能替代平台校验,以服务端结果为最终结论。
83
+ 状态: 默认调服务端(需登录);--local 纯本地。`,
84
+ push: `zhiyun <skill|plugin|app> push [--zip <已有zip>] [--dry-run]
85
+
86
+ 上传/覆盖当前草稿包(Skill/Plugin: /package;App: /package)。无 --zip 时先本地打包。
87
+ 状态: 写远端(需登录)。`,
88
+ test: `zhiyun skill test 打开/引导该技能的调试会话
89
+ zhiyun plugin test --capability <能力id> --input '<JSON>'
90
+ zhiyun app test 报告平台沙箱预览与本地 mock 的使用方式
91
+
92
+ 状态: Skill 只读引导;Plugin 调用平台测试接口真实执行一次(需登录);App 只输出指引。`,
93
+ submit: `zhiyun <skill|plugin|app> submit [--release-notes <说明>] [--dry-run]
94
+
95
+ 把当前草稿提交审核(Skill/Plugin 走 versions/{vid}/submit;App 走 releases/{version}/submit)。
96
+ 状态: 写远端(需登录)。审核由管理员完成。`,
97
+ status: `zhiyun <skill|plugin|app> status [--json]
98
+
99
+ 查看版本与审核状态(DRAFT → IN_REVIEW → APPROVED → PUBLISHED)。
100
+ 状态: 只读(需登录)。`,
101
+ withdraw: `zhiyun <skill|plugin|app> withdraw [--version <x.y.z>]
102
+
103
+ 撤回审核中的版本回到草稿。
104
+ 状态: 写远端(需登录)。`,
105
+ publish: `zhiyun skill|plugin publish --version <x.y.z>
106
+
107
+ 发布已批准(APPROVED)的版本。App 审核通过即上架,没有 publish 命令。
108
+ 状态: 写远端(需登录)。`,
109
+ };
110
+ return pages[path] ?? topHelp();
111
+ }
@@ -0,0 +1,55 @@
1
+ import { requestJson, requireToken } from '../api.js';
2
+ import { emitJson, emitText, fail } from '../output.js';
3
+ import { ExitCode } from '../exit.js';
4
+ export async function runCapabilityList(global, baseUrl) {
5
+ const token = requireToken();
6
+ const capabilities = await requestJson(baseUrl, '/api/developer/capabilities', { token });
7
+ if (global.json) {
8
+ emitJson({ command: 'capability list', count: capabilities.length, capabilities });
9
+ }
10
+ else {
11
+ emitText(capabilities.map((cap) => {
12
+ const record = cap;
13
+ const risk = typeof record.risk === 'string' ? record.risk : '';
14
+ const scope = typeof record.scope === 'string' ? record.scope : '';
15
+ return `${String(record.id ?? '?')} ${String(record.name ?? '')} ${[scope, risk].filter(Boolean).join('/')}`;
16
+ }));
17
+ }
18
+ return 0;
19
+ }
20
+ export async function runProjectList(kind, global, baseUrl) {
21
+ const token = requireToken();
22
+ const projects = await requestJson(baseUrl, '/api/developer/projects', { token });
23
+ const filtered = projects.filter((project) => project.kind === kind);
24
+ if (global.json) {
25
+ emitJson({ command: `${kind.toLowerCase()} list`, count: filtered.length, projects: filtered });
26
+ }
27
+ else if (filtered.length === 0) {
28
+ emitText([`(无 ${kind} 项目)`]);
29
+ }
30
+ else {
31
+ emitText(filtered.map((project) => `#${project.id} ${project.extId} 「${project.name}」 ${project.status ?? ''} ` +
32
+ `latest=${project.latestVersion ?? '-'} draft=${project.draftVersion ?? '-'}`));
33
+ }
34
+ return 0;
35
+ }
36
+ export async function runAppList(global, baseUrl) {
37
+ const token = requireToken();
38
+ const apps = await requestJson(baseUrl, '/api/developer/apps', { token });
39
+ if (global.json) {
40
+ emitJson({ command: 'app list', count: apps.length, apps });
41
+ }
42
+ else if (apps.length === 0) {
43
+ emitText(['(无 App 项目)']);
44
+ }
45
+ else {
46
+ emitText(apps.map((app) => {
47
+ const release = app.releases?.[0];
48
+ return `${app.appId} 「${app.name}」 ${release ? `${release.version} ${release.status}` : '(无版本)'}`;
49
+ }));
50
+ }
51
+ return 0;
52
+ }
53
+ export function unknownKind(kind) {
54
+ fail(ExitCode.Usage, `未知类型: ${kind}`, '可用类型: skill | plugin | app');
55
+ }