@lark-apaas/miaoda-cli 0.1.24-alpha.c761e99 → 0.1.24-alpha.cb67974

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.
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getAppInfo = getAppInfo;
4
4
  exports.updateAppMeta = updateAppMeta;
5
+ exports.getCodeArchive = getCodeArchive;
5
6
  const http_1 = require("../../utils/http");
6
7
  const devops_error_1 = require("../../utils/devops-error");
7
8
  const DEFAULT_ERR_CODE = 'INTERNAL_DEVOPS_ERROR';
@@ -23,3 +24,22 @@ async function updateAppMeta(req) {
23
24
  // 路径已带 appID,body 里也保留以匹配 BAM IDL 的 sensitive:"no" 透传约定
24
25
  return (0, http_1.postInnerApi)(url, req, envelopeOpts('Failed to update app'));
25
26
  }
27
+ /**
28
+ * GET /api/v1/studio/innerapi/apps/:appID/code-archive?sourceAppID=xxx — 导出源应用源码归档。
29
+ * 走 studio_server inner 通道(force 身份,服务侧 force→lark 换取后按 DOWNLOAD 鉴权,
30
+ * 鉴权对象是 sourceAppID),与 comment 域同一 inner 信封;创意应用升级链路(app export)消费。
31
+ *
32
+ * path :appID 必须是当前沙箱应用:网关 apaas_force_admin_plugin 强校验 URL appID 与沙箱
33
+ * AK 绑定应用一致(不一致 400 "appID in url is illegal"),跨应用导出目标因此走 query。
34
+ */
35
+ async function getCodeArchive(params) {
36
+ const { appID, sourceAppID, checkpointID } = params;
37
+ let url = `/api/v1/studio/innerapi/apps/${encodeURIComponent(appID)}/code-archive` +
38
+ `?sourceAppID=${encodeURIComponent(sourceAppID)}`;
39
+ if (checkpointID !== undefined && checkpointID > 0) {
40
+ url += `&checkPointID=${String(checkpointID)}`;
41
+ }
42
+ return (0, http_1.getInnerApi)(url, {
43
+ errPrefix: 'Failed to export code archive',
44
+ });
45
+ }
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ArchType = exports.Source = exports.BizType = exports.AppType = exports.AppMode = exports.AppStatus = exports.appMetaSchema = exports.updateAppMeta = exports.getAppInfo = void 0;
3
+ exports.ArchType = exports.Source = exports.BizType = exports.AppType = exports.AppMode = exports.AppStatus = exports.appMetaSchema = exports.getCodeArchive = exports.updateAppMeta = exports.getAppInfo = void 0;
4
4
  var api_1 = require("./api");
5
5
  Object.defineProperty(exports, "getAppInfo", { enumerable: true, get: function () { return api_1.getAppInfo; } });
6
6
  Object.defineProperty(exports, "updateAppMeta", { enumerable: true, get: function () { return api_1.updateAppMeta; } });
7
+ Object.defineProperty(exports, "getCodeArchive", { enumerable: true, get: function () { return api_1.getCodeArchive; } });
7
8
  var schemas_1 = require("./schemas");
8
9
  Object.defineProperty(exports, "appMetaSchema", { enumerable: true, get: function () { return schemas_1.appMetaSchema; } });
9
10
  var types_1 = require("./types");
@@ -21,6 +21,7 @@ function registerAppCommands(program, opts = {}) {
21
21
  `);
22
22
  registerAppGet(appCmd);
23
23
  registerAppUpdate(appCmd);
24
+ registerAppExport(appCmd);
24
25
  if (opts.includeInit) {
25
26
  registerAppInit(appCmd);
26
27
  registerAppSync(appCmd);
@@ -119,6 +120,54 @@ JSON 输出
119
120
  });
120
121
  }));
121
122
  }
123
+ function registerAppExport(parent) {
124
+ const cmd = parent
125
+ .command('export <app-id>')
126
+ .description('导出指定应用的源码到本工程(创意应用升级:拉取创意稿作只读参考物)')
127
+ .option('--out <dir>', '输出目录(相对当前目录)', 'source_package/creative')
128
+ .option('--checkpoint <n>', '按检查点版本导出;缺省为最新提交(HEAD)')
129
+ .addHelpText('after', `
130
+ 说明
131
+ 从服务端导出 <app-id> 对应应用(源应用,可以不是当前应用)的源码归档并解压到 --out
132
+ 目录,导出完成后写 --out/.upgrade-manifest.json(含 sourceAppId / 导出时间 / 文件数),
133
+ 供升级链路幂等判定与 skill 发现使用。调用者须为源应用协作者(服务端 DOWNLOAD 鉴权)。
134
+ 请求以当前应用(MIAODA_APP_ID)为路径主体、源应用走 sourceAppID 参数。
135
+
136
+ 幂等
137
+ --out 已存在 manifest 且 sourceAppId 一致时跳过导出(skipped=true),重复调用安全。
138
+
139
+ JSON 输出
140
+ {"data": {"appId": "...", "out": "source_package/creative", "fileCount": <n>,
141
+ "archiveName": "...", "skipped": false}}
142
+ 跳过时:{"data": {"appId": "...", "out": "...", "skipped": true, "reason": "already_exported"}}
143
+
144
+ 失败
145
+ 源应用无可导出内容(从未提交过代码)→ EXPORT_EMPTY_ARCHIVE,请先在源应用完成一轮创作。
146
+ 无权限(非源应用协作者)→ 服务端鉴权错误透传。
147
+
148
+ 示例
149
+ $ miaoda app export app_demo_source
150
+ $ miaoda app export app_demo_source --out source_package/creative
151
+ $ miaoda app export app_demo_source --checkpoint 3
152
+ `);
153
+ cmd.action((0, shared_1.withHelp)(cmd, async (appId, rawOpts) => {
154
+ const checkpointRaw = rawOpts.checkpoint;
155
+ let checkpoint;
156
+ if (checkpointRaw !== undefined) {
157
+ checkpoint = Number(checkpointRaw);
158
+ if (!Number.isInteger(checkpoint) || checkpoint <= 0) {
159
+ (0, shared_1.failArgs)(`--checkpoint 须为正整数:${checkpointRaw}`);
160
+ }
161
+ }
162
+ await (0, index_1.handleAppExport)({
163
+ appId,
164
+ // path :appID 必须是当前沙箱应用(网关 AK 校验),从 MIAODA_APP_ID/app_id 解析
165
+ currentAppId: (0, shared_1.resolveAppId)({}),
166
+ out: rawOpts.out,
167
+ checkpoint,
168
+ });
169
+ }));
170
+ }
122
171
  function registerAppGet(parent) {
123
172
  const cmd = parent
124
173
  .command('get')
@@ -21,11 +21,15 @@ function registerSkillsCommands(program) {
21
21
  function registerSkillsSync(parent) {
22
22
  const cmd = parent
23
23
  .command('sync')
24
- .description('同步 coding-steering(latest 或指定版本)到 user app')
24
+ .description('同步 skills 到 user app(默认 coding-steering;带 --type 时走 sandbox-skills)')
25
25
  .option('--dir <path>', '项目目录,默认当前目录', '.')
26
26
  .option('--version <ver>', 'coding-steering 包版本或 dist-tag,缺省 latest')
27
27
  .option('--local', '走本地 dev / agent 输出形态:拷到 .agents/skills 平铺 + 创建 .claude/skills 软链。' +
28
28
  '不传时退回老形态:拷到 .agent/skills/steering/<stack>/skills/(跟沙箱端 update-skills.sh 对齐)', false)
29
+ .option('--type <type>', '走 sandbox-skills 分支:miaoda | miaoda-modern | miaoda-design | openclaw。' +
30
+ '有值时忽略 --version/--local,改从 @lark-apaas/coding-<pkg>-sandbox 拉对应包写入 .agent/skills/')
31
+ .option('--tgz-key <key>', '配合 --type 使用;老 TCC skillsUpdateTgzKey 值透传,' +
32
+ '从 skills_versions/all_<X>.tgz 反推出 dist-tag lane-<X>;缺省走 @latest')
29
33
  .addHelpText('after', `
30
34
  前置要求
31
35
  当前目录(或 --dir)已走过 'miaoda app init'(.spark/meta.json 含 stack)
@@ -56,6 +60,8 @@ JSON 输出
56
60
  dir: rawOpts.dir,
57
61
  version: rawOpts.version,
58
62
  local: rawOpts.local,
63
+ type: rawOpts.type,
64
+ tgzKey: rawOpts.tgzKey,
59
65
  });
60
66
  }));
61
67
  }
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.UPGRADE_MANIFEST_FILENAME = exports.DEFAULT_EXPORT_OUT = void 0;
40
+ exports.handleAppExport = handleAppExport;
41
+ const node_child_process_1 = require("node:child_process");
42
+ const node_fs_1 = __importDefault(require("node:fs"));
43
+ const node_os_1 = __importDefault(require("node:os"));
44
+ const node_path_1 = __importDefault(require("node:path"));
45
+ const api = __importStar(require("../../../api/index"));
46
+ const error_1 = require("../../../utils/error");
47
+ const output_1 = require("../../../utils/output");
48
+ const logger_1 = require("../../../utils/logger");
49
+ /** 导出目录默认值:创意升级链路的参考物约定位置。 */
50
+ exports.DEFAULT_EXPORT_OUT = 'source_package/creative';
51
+ /** 升级 manifest 文件名:前置钩子幂等标记 / skill workspace-contains 锚点 / skill 第 0 步信息源。 */
52
+ exports.UPGRADE_MANIFEST_FILENAME = '.upgrade-manifest.json';
53
+ /** 解码后归档大小上限(对齐服务端 UploadHtmlArtifact 的 50MB 口径)。 */
54
+ const MAX_ARCHIVE_BYTES = 50 * 1024 * 1024;
55
+ /** 校验相对输出路径:禁绝对路径、禁含 ".." 段(与 deploy patch 的 normalizeRel 同口径)。 */
56
+ function normalizeOutDir(rel) {
57
+ if (node_path_1.default.isAbsolute(rel) || rel.startsWith('/')) {
58
+ throw new error_1.AppError('ARGS_INVALID', `输出目录不能是绝对路径:${rel}`);
59
+ }
60
+ const posix = rel.split(node_path_1.default.sep).join('/').replace(/^\.\//, '');
61
+ if (posix.split('/').includes('..')) {
62
+ throw new error_1.AppError('ARGS_INVALID', `输出目录不能包含 "..":${rel}`);
63
+ }
64
+ return posix;
65
+ }
66
+ /**
67
+ * 按魔数识别归档格式并解压到 destDir。
68
+ * 服务端 GetCodeArchive 的归档名以 .zip 结尾,但底层 git archive 格式由 devops_platform
69
+ * 决定,历史文档亦有 tar.gz 记载 —— 两种都兼容,识别不了才报错。
70
+ */
71
+ function extractArchive(archivePath, destDir, head) {
72
+ const isZip = head.length >= 4 && head[0] === 0x50 && head[1] === 0x4b;
73
+ const isGzip = head.length >= 2 && head[0] === 0x1f && head[1] === 0x8b;
74
+ if (isZip) {
75
+ (0, node_child_process_1.execFileSync)('unzip', ['-q', '-o', archivePath, '-d', destDir], { stdio: 'pipe' });
76
+ return;
77
+ }
78
+ if (isGzip) {
79
+ (0, node_child_process_1.execFileSync)('tar', ['-xzf', archivePath, '-C', destDir], { stdio: 'pipe' });
80
+ return;
81
+ }
82
+ throw new error_1.AppError('EXPORT_ARCHIVE_INVALID', '无法识别归档格式(非 zip / tar.gz)', {
83
+ next_actions: ['稍后重试 miaoda app export;持续失败请携带 log_id 反馈平台'],
84
+ });
85
+ }
86
+ function countFiles(dir) {
87
+ let count = 0;
88
+ const walk = (cur) => {
89
+ for (const entry of node_fs_1.default.readdirSync(cur, { withFileTypes: true })) {
90
+ if (entry.isDirectory())
91
+ walk(node_path_1.default.join(cur, entry.name));
92
+ else
93
+ count += 1;
94
+ }
95
+ };
96
+ walk(dir);
97
+ return count;
98
+ }
99
+ /**
100
+ * miaoda app export <app-id> [--out <dir>] [--checkpoint <n>]
101
+ *
102
+ * 创意应用升级链路:把源应用(创意稿)的源码从服务端导出到本工程,作为只读参考物。
103
+ * 链路:studio_server InnerGetCodeArchive(inner 通道,force 身份鉴权:调用者须为源应用
104
+ * 协作者)→ git archive → 本地解压 → 末尾写 .upgrade-manifest.json(幂等标记 +
105
+ * skill workspace-contains 锚点 + skill 第 0 步信息源)。
106
+ *
107
+ * URL 形状:path :appID 是当前沙箱应用(网关强校验其与沙箱 AK 一致),源应用走 query
108
+ * sourceAppID —— 跨应用(升级常态:新应用沙箱里导出源创意应用)必须如此。
109
+ *
110
+ * 幂等:--out 已存在 manifest 且 sourceAppId 一致时跳过导出直接返回(重试/多轮不重复拉取)。
111
+ */
112
+ async function handleAppExport(opts) {
113
+ const appId = (opts.appId || '').trim();
114
+ if (!appId) {
115
+ throw new error_1.AppError('ARGS_INVALID', 'app-id 不能为空');
116
+ }
117
+ // 当前沙箱应用:网关 AK 校验要求 path :appID 与沙箱绑定应用一致
118
+ const currentAppId = (opts.currentAppId || '').trim();
119
+ if (!currentAppId) {
120
+ throw new error_1.AppError('APP_ID_MISSING', '未指定当前应用', {
121
+ next_actions: ['设置 export MIAODA_APP_ID=<id>'],
122
+ });
123
+ }
124
+ const outRel = normalizeOutDir(opts.out ?? exports.DEFAULT_EXPORT_OUT);
125
+ const outDir = node_path_1.default.resolve(process.cwd(), outRel);
126
+ const manifestPath = node_path_1.default.join(outDir, exports.UPGRADE_MANIFEST_FILENAME);
127
+ // 幂等:同源应用已导出过则直接复用(前置钩子重试 / 多轮对话不重复拉取)
128
+ if (node_fs_1.default.existsSync(manifestPath)) {
129
+ try {
130
+ const prev = JSON.parse(node_fs_1.default.readFileSync(manifestPath, 'utf-8'));
131
+ if (prev.sourceAppId === appId) {
132
+ (0, logger_1.log)('export', `Already exported (manifest matched), skip: ${outRel}`);
133
+ (0, output_1.emit)({ data: { appId, out: outRel, skipped: true, reason: 'already_exported' } });
134
+ return;
135
+ }
136
+ }
137
+ catch {
138
+ // manifest 损坏视为未导出,走覆盖导出
139
+ }
140
+ }
141
+ (0, logger_1.log)('export', `Fetching code archive of ${appId}...`);
142
+ const resp = await api.app.getCodeArchive({
143
+ appID: currentAppId,
144
+ sourceAppID: appId,
145
+ checkpointID: opts.checkpoint,
146
+ });
147
+ const fileBase64 = resp.file ?? '';
148
+ if (!fileBase64) {
149
+ throw new error_1.AppError('EXPORT_EMPTY_ARCHIVE', '源应用没有可导出的内容(可能从未提交过代码)', {
150
+ next_actions: ['请先在源应用完成一轮创作(至少一次成功提交)后重试 miaoda app export'],
151
+ });
152
+ }
153
+ const archive = Buffer.from(fileBase64, 'base64');
154
+ if (archive.length === 0) {
155
+ throw new error_1.AppError('EXPORT_EMPTY_ARCHIVE', '归档内容为空,请稍后重试');
156
+ }
157
+ if (archive.length > MAX_ARCHIVE_BYTES) {
158
+ throw new error_1.AppError('EXPORT_ARCHIVE_TOO_LARGE', `归档超过大小上限(${String(archive.length)} > ${String(MAX_ARCHIVE_BYTES)} bytes)`);
159
+ }
160
+ // 归档先落临时文件再解压;解压成功前不动已存在的 outDir 内容之外的文件
161
+ const tmpDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'miaoda-export-'));
162
+ const archivePath = node_path_1.default.join(tmpDir, resp.name ?? `${appId}.zip`);
163
+ try {
164
+ node_fs_1.default.writeFileSync(archivePath, archive);
165
+ node_fs_1.default.mkdirSync(outDir, { recursive: true });
166
+ (0, logger_1.log)('export', `Extracting to ${outRel}...`);
167
+ extractArchive(archivePath, outDir, archive.subarray(0, 4));
168
+ }
169
+ finally {
170
+ node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
171
+ }
172
+ // 防御清理:git archive 语义不含 .git,防通道实现变化带出嵌套仓(git 会记 gitlink,
173
+ // 参考稿进不了提交);源仓 .gitignore 会成为新仓子树的嵌套忽略规则,极端 case(源仓
174
+ // force-add 过被自身 ignore 的文件)会让参考稿部分文件被 git add 静默忽略。
175
+ // 两者对 UX 对照均无价值,解压后移除,保证"进 git 的参考稿 = 导出的全部内容文件"。
176
+ node_fs_1.default.rmSync(node_path_1.default.join(outDir, '.git'), { recursive: true, force: true });
177
+ node_fs_1.default.rmSync(node_path_1.default.join(outDir, '.gitignore'), { force: true });
178
+ const fileCount = countFiles(outDir);
179
+ const manifest = {
180
+ sourceAppId: appId,
181
+ archiveName: resp.name ?? '',
182
+ checkpointId: opts.checkpoint ?? 0,
183
+ exportedAt: new Date().toISOString(),
184
+ fileCount,
185
+ };
186
+ node_fs_1.default.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
187
+ (0, logger_1.log)('export', 'Exported successfully');
188
+ (0, output_1.emit)({
189
+ data: { appId, out: outRel, fileCount, archiveName: manifest.archiveName, skipped: false },
190
+ });
191
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SUPPORTED_STACKS = exports.handleAppMigrate = exports.handleAppUpgrade = exports.handleAppSync = exports.handleAppInit = exports.handleAppUpdate = exports.handleAppGet = void 0;
3
+ exports.SUPPORTED_STACKS = exports.handleAppMigrate = exports.UPGRADE_MANIFEST_FILENAME = exports.DEFAULT_EXPORT_OUT = exports.handleAppExport = exports.handleAppUpgrade = exports.handleAppSync = exports.handleAppInit = exports.handleAppUpdate = exports.handleAppGet = void 0;
4
4
  var get_1 = require("./get");
5
5
  Object.defineProperty(exports, "handleAppGet", { enumerable: true, get: function () { return get_1.handleAppGet; } });
6
6
  var update_1 = require("./update");
@@ -10,6 +10,10 @@ Object.defineProperty(exports, "handleAppInit", { enumerable: true, get: functio
10
10
  var sync_1 = require("./sync");
11
11
  Object.defineProperty(exports, "handleAppSync", { enumerable: true, get: function () { return sync_1.handleAppSync; } });
12
12
  Object.defineProperty(exports, "handleAppUpgrade", { enumerable: true, get: function () { return sync_1.handleAppUpgrade; } });
13
+ var export_1 = require("./export");
14
+ Object.defineProperty(exports, "handleAppExport", { enumerable: true, get: function () { return export_1.handleAppExport; } });
15
+ Object.defineProperty(exports, "DEFAULT_EXPORT_OUT", { enumerable: true, get: function () { return export_1.DEFAULT_EXPORT_OUT; } });
16
+ Object.defineProperty(exports, "UPGRADE_MANIFEST_FILENAME", { enumerable: true, get: function () { return export_1.UPGRADE_MANIFEST_FILENAME; } });
13
17
  var migrate_1 = require("./migrate");
14
18
  Object.defineProperty(exports, "handleAppMigrate", { enumerable: true, get: function () { return migrate_1.handleAppMigrate; } });
15
19
  // commands 层渲染 help 时需要这份枚举;从 handler barrel 转发,避免 commands → services 越界
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.handleSkillsSync = handleSkillsSync;
7
7
  const node_path_1 = __importDefault(require("node:path"));
8
8
  const coding_steering_1 = require("../../../utils/coding-steering");
9
+ const sandbox_skills_1 = require("../../../utils/sandbox-skills");
9
10
  const spark_meta_1 = require("../../../utils/spark-meta");
10
11
  const error_1 = require("../../../utils/error");
11
12
  const output_1 = require("../../../utils/output");
@@ -26,6 +27,25 @@ const output_1 = require("../../../utils/output");
26
27
  async function handleSkillsSync(opts) {
27
28
  await Promise.resolve();
28
29
  const targetDir = node_path_1.default.resolve(opts.dir ?? process.cwd());
30
+ // --type 走 sandbox-skills 分支(迁仓后沙箱内 update-skills.sh 调用),跟本地 dev 的
31
+ // steering 同步逻辑并列——两条路径的目标 / 拉包 / 目录结构完全不同,用 --type 显式路由。
32
+ if (opts.type !== undefined && opts.type !== '') {
33
+ const { type, tgzKey } = (0, sandbox_skills_1.validateSandboxSyncOpts)({ type: opts.type, tgzKey: opts.tgzKey });
34
+ const result = (0, sandbox_skills_1.syncSandboxSkills)({ type, tgzKey, targetDir });
35
+ (0, output_1.emit)({
36
+ data: {
37
+ mode: 'sandbox',
38
+ type,
39
+ packageName: result.packageName,
40
+ version: result.version,
41
+ lane: result.lane,
42
+ syncedTop: result.syncedTop,
43
+ targetDir: result.targetDir,
44
+ },
45
+ });
46
+ return;
47
+ }
48
+ // 原有 coding-steering 同步逻辑(本地 dev / miaoda app init 用)
29
49
  const meta = (0, spark_meta_1.readSparkMeta)(targetDir);
30
50
  if (meta.stack === undefined || meta.stack === '') {
31
51
  throw new error_1.AppError('SKILLS_META_INCOMPLETE', '.spark/meta.json missing stack — run `miaoda app init` first');
package/dist/main.js CHANGED
@@ -1,8 +1,45 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const program_1 = require("./cli/program");
6
+ const commander_1 = require("commander");
7
+ const index_1 = require("./cli/commands/index");
8
+ const shared_1 = require("./cli/commands/shared");
9
+ const help_1 = require("./cli/help");
10
+ const config_1 = require("./utils/config");
11
+ const log_id_1 = require("./utils/log_id");
4
12
  const output_1 = require("./utils/output");
5
- const program = (0, program_1.createProgram)();
13
+ const package_json_1 = __importDefault(require("../package.json"));
14
+ // MiaodaHelp 对齐 CLI 规范(描述置顶 / Flags / Global Flags / 段顺序):
15
+ // 在 Command.prototype 上 patch createHelp,让所有命令实例(含动态注册的
16
+ // 子命令)统一走 MiaodaHelp 渲染,避免在每个子命令上重复 configureHelp。
17
+ commander_1.Command.prototype.createHelp = function () {
18
+ return Object.assign(new help_1.MiaodaHelp(), this.configureHelp());
19
+ };
20
+ const program = new commander_1.Command();
21
+ const { version } = package_json_1.default;
22
+ program
23
+ .name('miaoda')
24
+ .description('妙搭平台 CLI,提供数据服务、文件存储等命令行操作。')
25
+ .usage('<command> [flags]')
26
+ .version(version, '-v, --version', '显示版本号')
27
+ .option('--json [fields]', 'JSON 输出,可选字段级选择')
28
+ .addOption(new commander_1.Option('--output <format>', '输出格式(pretty | json,大小写不敏感)')
29
+ .choices(['pretty', 'json'])
30
+ .argParser((0, shared_1.caseInsensitiveChoice)(['pretty', 'json']))
31
+ .default('pretty'))
32
+ .option('--verbose', 'debug 日志到 stderr')
33
+ // 全局接受 -y/--yes:作为 root 全局 option 下发到所有子命令解析(同 --json/--verbose),
34
+ // 让任意命令带 --yes 都不报 unknown option。对有确认门的破坏性命令生效,其余命令忽略。
35
+ .option('-y, --yes', '跳过确认提示(对有确认门的命令生效,其余命令忽略)')
36
+ .helpOption('-h, --help', '显示帮助信息')
37
+ .hook('preAction', (_thisCmd, actionCmd) => {
38
+ (0, log_id_1.generateLogId)();
39
+ const opts = actionCmd.optsWithGlobals();
40
+ (0, config_1.initConfigFromOpts)(opts);
41
+ });
42
+ (0, index_1.registerCommands)(program);
6
43
  program.parseAsync(process.argv).catch((err) => {
7
44
  (0, output_1.emitError)(err);
8
45
  process.exitCode = 1;
@@ -63,8 +63,14 @@ function createClient(opts) {
63
63
  /**
64
64
  * 默认灰度泳道。当前 miaoda CLI 接入的新接口(modern deploy 等)只在该泳道生效,
65
65
  * 缺省走这个。MIAODA_CANARY_HEADER 可显式覆盖;设为空字符串则去掉头。
66
+ *
67
+ * ⚠️ TEMP(creative-upgrade 联调用):默认泳道临时指向开发泳道 boe_debug_detail_lookup,
68
+ * 使 alpha 发版进沙箱镜像后,app export / code-archive 等新接口无需 wrapper 即可命中
69
+ * 尚未合流的 studio_server 改动。
70
+ * 上线前必须还原为 'boe_miaoda_ccm',或改为由 agent_tool 按请求 x-tt-env 注入
71
+ * MIAODA_CANARY_HEADER(详见 http.ts 的 applyCanaryHeader 与 agent_tool ExecShellCmd)。
66
72
  */
67
- const DEFAULT_CANARY_HEADER = 'boe_miaoda_ccm';
73
+ const DEFAULT_CANARY_HEADER = 'boe_debug_detail_lookup';
68
74
  /**
69
75
  * 注入 x-tt-env 小流量头:env 显式设值 > DEFAULT_CANARY_HEADER。
70
76
  *
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.extractLaneFromTgzKey = extractLaneFromTgzKey;
7
+ exports.syncSandboxSkills = syncSandboxSkills;
8
+ exports.validateSandboxSyncOpts = validateSandboxSyncOpts;
9
+ const node_child_process_1 = require("node:child_process");
10
+ const node_fs_1 = __importDefault(require("node:fs"));
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ const npm_pack_1 = require("../utils/npm-pack");
13
+ const error_1 = require("../utils/error");
14
+ const logger_1 = require("../utils/logger");
15
+ /**
16
+ * 覆盖式清空目录:先 shell `rm -rf`,挂了再 fallback 到 fs.rmSync。
17
+ *
18
+ * 为什么不直接 fs.rmSync:老沙箱残留的文件可能是**别的用户**写的(例如老 update.sh
19
+ * 以 root 用户跑,写下 root owned 的 `.last-update`)。`fs.rmSync {force:true}` 只
20
+ * 吞 ENOENT,遇到 EACCES 直接 throw;而 shell `rm -rf` 对权限的处理更宽松(只要
21
+ * 对**目录**有写权限就能删掉里面的文件,无视文件本身的 owner/权限)。
22
+ */
23
+ function forceRemoveDir(dir) {
24
+ if (!node_fs_1.default.existsSync(dir))
25
+ return;
26
+ try {
27
+ (0, node_child_process_1.execFileSync)('rm', ['-rf', dir], { stdio: ['ignore', 'ignore', 'pipe'] });
28
+ if (!node_fs_1.default.existsSync(dir))
29
+ return;
30
+ }
31
+ catch (err) {
32
+ (0, logger_1.debug)(`sandbox-skills: shell rm -rf ${dir} failed: ${err.message}, fallback to fs.rmSync`);
33
+ }
34
+ // Fallback(比如沙箱没 rm、或权限受限):尽力用 Node fs.rmSync
35
+ node_fs_1.default.rmSync(dir, { recursive: true, force: true });
36
+ }
37
+ const PACKAGE_BY_TYPE = {
38
+ miaoda: '@lark-apaas/coding-miaoda-sandbox',
39
+ 'miaoda-modern': '@lark-apaas/coding-miaoda-sandbox',
40
+ 'miaoda-design': '@lark-apaas/coding-miaoda-sandbox',
41
+ openclaw: '@lark-apaas/coding-openclaw-sandbox',
42
+ };
43
+ // 合并包内的业务线子目录(miaoda / miaoda-modern / miaoda-design);shared 单独处理。
44
+ const BUSINESS_LINE_BY_TYPE = {
45
+ miaoda: 'miaoda',
46
+ 'miaoda-modern': 'miaoda-modern',
47
+ 'miaoda-design': 'miaoda-design',
48
+ openclaw: null, // openclaw 独立包,直接展平
49
+ };
50
+ /**
51
+ * 从 TCC skillsUpdateTgzKey 反推 lane 名。
52
+ * skills_versions/all_feat-xxx.tgz → feat-xxx
53
+ * skills_versions/all_latest.tgz → null(走 @latest)
54
+ * 任何 all_<X>.tgz → <X>
55
+ *
56
+ * 用 nullable 而不是空字符串 —— 让调用方能判断"没 lane 就走 latest"。
57
+ */
58
+ function extractLaneFromTgzKey(tgzKey) {
59
+ if (tgzKey === undefined || tgzKey === '') {
60
+ return null;
61
+ }
62
+ const match = /(?:^|\/)all_([A-Za-z0-9][A-Za-z0-9_-]*)\.tgz$/.exec(tgzKey);
63
+ if (match === null) {
64
+ return null;
65
+ }
66
+ const lane = match[1];
67
+ if (lane === 'latest') {
68
+ return null;
69
+ }
70
+ return lane;
71
+ }
72
+ /**
73
+ * 主流程:按 type 拉包 + sync 到目标目录。
74
+ *
75
+ * sync 策略:**覆盖式写入 + 老文件清理**,对齐老 cp-skills.sh 行为
76
+ * (`rm -rf $TARGET; mkdir -p $TARGET; cp -r ...`)。
77
+ */
78
+ function syncSandboxSkills(opts) {
79
+ const targetDir = node_path_1.default.resolve(opts.targetDir ?? process.cwd());
80
+ const packageName = PACKAGE_BY_TYPE[opts.type];
81
+ const businessLine = BUSINESS_LINE_BY_TYPE[opts.type];
82
+ const lane = extractLaneFromTgzKey(opts.tgzKey);
83
+ // 优先拉 lane dist-tag;失败 fallback 到 latest(防灰度切换期间 dist-tag 缺失 404)
84
+ const primaryVersion = lane !== null ? `lane-${lane}` : 'latest';
85
+ let fetched;
86
+ try {
87
+ (0, logger_1.debug)(`sandbox-skills: fetching ${packageName}@${primaryVersion}`);
88
+ fetched = (0, npm_pack_1.fetchNpmPackage)({
89
+ packageName,
90
+ version: primaryVersion,
91
+ registry: opts.registry,
92
+ });
93
+ }
94
+ catch (err) {
95
+ if (lane === null) {
96
+ throw err;
97
+ }
98
+ (0, logger_1.debug)(`sandbox-skills: lane ${primaryVersion} not available, fallback to @latest (err: ${err.message})`);
99
+ fetched = (0, npm_pack_1.fetchNpmPackage)({
100
+ packageName,
101
+ version: 'latest',
102
+ registry: opts.registry,
103
+ });
104
+ }
105
+ try {
106
+ // 计算 skill 目标根目录:
107
+ // - openclaw → <targetDir>/skills/(跟老 cp-skills.sh 的 openclaw 分支对齐,落 $WORKSPACE_DIR/agent/skills)
108
+ // - 其它 → <targetDir>/.agent/skills/
109
+ const skillsRoot = opts.type === 'openclaw'
110
+ ? node_path_1.default.join(targetDir, 'skills')
111
+ : node_path_1.default.join(targetDir, '.agent', 'skills');
112
+ // 覆盖式:清空目标 skill 根目录(对齐 cp-skills.sh 行为,避免遗留老 skill)
113
+ // 用 forceRemoveDir 兜底:老沙箱可能残留其他用户写的 stale 文件(例如老 update.sh
114
+ // 以 root 跑,写下 root owned 的 `.last-update`),fs.rmSync {force:true} 遇到 EACCES
115
+ // 会 throw。见 forceRemoveDir 注释。
116
+ forceRemoveDir(skillsRoot);
117
+ node_fs_1.default.mkdirSync(skillsRoot, { recursive: true });
118
+ const syncedTop = [];
119
+ if (businessLine !== null) {
120
+ // 合并包 miaoda/miaoda-modern/miaoda-design:cp <extractDir>/<business-line>/* 到 target
121
+ const sourceDir = node_path_1.default.join(fetched.extractDir, businessLine);
122
+ if (node_fs_1.default.existsSync(sourceDir)) {
123
+ for (const entry of node_fs_1.default.readdirSync(sourceDir)) {
124
+ const from = node_path_1.default.join(sourceDir, entry);
125
+ const to = node_path_1.default.join(skillsRoot, entry);
126
+ node_fs_1.default.cpSync(from, to, { recursive: true });
127
+ syncedTop.push(entry);
128
+ }
129
+ }
130
+ else {
131
+ (0, logger_1.debug)(`sandbox-skills: source dir ${sourceDir} not found in package (skipped)`);
132
+ }
133
+ // shared:sync 合并包内 shared/* 到 target(对齐老 build.sh 的 shared 分发行为)
134
+ const sharedDir = node_path_1.default.join(fetched.extractDir, 'shared');
135
+ if (node_fs_1.default.existsSync(sharedDir)) {
136
+ for (const entry of node_fs_1.default.readdirSync(sharedDir)) {
137
+ const from = node_path_1.default.join(sharedDir, entry);
138
+ const to = node_path_1.default.join(skillsRoot, entry);
139
+ if (node_fs_1.default.existsSync(to)) {
140
+ // shared 命中已存在的 skill(如同名)— 保留业务线版本,跳过 shared(对齐老 build.sh:shared 是补丁,同名不覆盖)
141
+ (0, logger_1.debug)(`sandbox-skills: shared/${entry} conflicts with business-line skill, skipped`);
142
+ continue;
143
+ }
144
+ node_fs_1.default.cpSync(from, to, { recursive: true });
145
+ syncedTop.push(entry);
146
+ }
147
+ }
148
+ }
149
+ else {
150
+ // openclaw 独立包:直接把包内所有 skill 目录(除 package.json 等元文件外)平铺到 skillsRoot
151
+ for (const entry of node_fs_1.default.readdirSync(fetched.extractDir)) {
152
+ if (entry === 'package.json' || entry === 'README.md' || entry === 'LICENSE')
153
+ continue;
154
+ const from = node_path_1.default.join(fetched.extractDir, entry);
155
+ const stat = node_fs_1.default.statSync(from);
156
+ if (!stat.isDirectory())
157
+ continue;
158
+ const to = node_path_1.default.join(skillsRoot, entry);
159
+ node_fs_1.default.cpSync(from, to, { recursive: true });
160
+ syncedTop.push(entry);
161
+ }
162
+ }
163
+ // 写 .last-update 时间戳(对齐老 update-skills.sh;供 feida-ai freshness 检查用)
164
+ const lastUpdatePath = node_path_1.default.join(skillsRoot, '.last-update');
165
+ node_fs_1.default.writeFileSync(lastUpdatePath, String(Date.now()));
166
+ return {
167
+ packageName,
168
+ version: fetched.version,
169
+ lane,
170
+ syncedTop,
171
+ targetDir: skillsRoot,
172
+ };
173
+ }
174
+ finally {
175
+ fetched.cleanup();
176
+ }
177
+ }
178
+ /**
179
+ * 参数校验:type 白名单 + tgzKey 格式(避免非法值透传到 npm 包名)。
180
+ */
181
+ function validateSandboxSyncOpts(opts) {
182
+ const validTypes = ['miaoda', 'miaoda-modern', 'miaoda-design', 'openclaw'];
183
+ if (opts.type === undefined || !validTypes.includes(opts.type)) {
184
+ throw new error_1.AppError('SKILLS_INVALID_TYPE', `--type must be one of: ${validTypes.join(', ')} (got: ${opts.type ?? 'undefined'})`);
185
+ }
186
+ if (opts.tgzKey !== undefined && opts.tgzKey !== '') {
187
+ // 校验 tgz key 大致格式(前缀 skills_versions/ + all_ + 允许字符 + .tgz);避免非法值影响 lane 反推
188
+ if (!/^[A-Za-z0-9/_\-.]+\.tgz$/.test(opts.tgzKey)) {
189
+ throw new error_1.AppError('SKILLS_INVALID_TGZ_KEY', `--tgz-key value looks invalid: ${opts.tgzKey}`);
190
+ }
191
+ }
192
+ return { type: opts.type, tgzKey: opts.tgzKey };
193
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/miaoda-cli",
3
- "version": "0.1.24-alpha.c761e99",
3
+ "version": "0.1.24-alpha.cb67974",
4
4
  "description": "Miaoda 平台命令行工具,面向 Agent 调用",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -1,63 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.createProgram = createProgram;
7
- const commander_1 = require("commander");
8
- const index_1 = require("../cli/commands/index");
9
- const shared_1 = require("../cli/commands/shared");
10
- const help_1 = require("../cli/help");
11
- const version_option_1 = require("../cli/version-option");
12
- const config_1 = require("../utils/config");
13
- const log_id_1 = require("../utils/log_id");
14
- const package_json_1 = __importDefault(require("../../package.json"));
15
- // 副作用:MiaodaHelp 对齐 CLI 规范(描述置顶 / Flags / Global Flags / 段顺序)。
16
- // 在 Command.prototype 上 patch createHelp,让所有命令实例(含动态注册的子命令)统一走
17
- // MiaodaHelp 渲染,避免在每个子命令上重复 configureHelp。
18
- commander_1.Command.prototype.createHelp = function () {
19
- return Object.assign(new help_1.MiaodaHelp(), this.configureHelp());
20
- };
21
- /**
22
- * 装配 root program(命令树 + 全局参数 + preAction hook)。
23
- *
24
- * 抽成工厂而非写在 main.ts 里,是为了让接线可测 —— main.ts 在 import 期就会 parseAsync,
25
- * 测试无法 import 它,只能复刻装配,那测到的就不是生产代码。
26
- * 见 test/cli/program-version-wiring.test.ts。
27
- *
28
- * @param argv 供 `--version` 归属判定使用的 argv(process.argv 形态)。显式传入而非直接读
29
- * process.argv,同样是为了可测。
30
- */
31
- function createProgram(argv = process.argv) {
32
- const program = new commander_1.Command();
33
- const { version } = package_json_1.default;
34
- program.name('miaoda');
35
- // 命令树必须先建:shouldExposeRootVersion 要从树上反推「哪些命令自己声明了 --version」。
36
- (0, index_1.registerCommands)(program);
37
- program
38
- .description('妙搭平台 CLI,提供数据服务、文件存储等命令行操作。')
39
- .usage('<command> [flags]');
40
- // 条件注册:仅当本次 argv 未命中「声明了 --version 的命令路径」时,root 才接管 --version。
41
- // 注册位置保持在 option 链首位,否则 -v, --version 会从 Global Flags 第一行掉到末尾。
42
- // 详见 src/cli/version-option.ts。
43
- if ((0, version_option_1.shouldExposeRootVersion)(program, argv)) {
44
- program.version(version, '-v, --version', '显示版本号');
45
- }
46
- program
47
- .option('--json [fields]', 'JSON 输出,可选字段级选择')
48
- .addOption(new commander_1.Option('--output <format>', '输出格式(pretty | json,大小写不敏感)')
49
- .choices(['pretty', 'json'])
50
- .argParser((0, shared_1.caseInsensitiveChoice)(['pretty', 'json']))
51
- .default('pretty'))
52
- .option('--verbose', 'debug 日志到 stderr')
53
- // 全局接受 -y/--yes:作为 root 全局 option 下发到所有子命令解析(同 --json/--verbose),
54
- // 让任意命令带 --yes 都不报 unknown option。对有确认门的破坏性命令生效,其余命令忽略。
55
- .option('-y, --yes', '跳过确认提示(对有确认门的命令生效,其余命令忽略)')
56
- .helpOption('-h, --help', '显示帮助信息')
57
- .hook('preAction', (_thisCmd, actionCmd) => {
58
- (0, log_id_1.generateLogId)();
59
- const opts = actionCmd.optsWithGlobals();
60
- (0, config_1.initConfigFromOpts)(opts);
61
- });
62
- return program;
63
- }
@@ -1,56 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.shouldExposeRootVersion = shouldExposeRootVersion;
4
- /**
5
- * root 是否应该注册 `-v, --version`。
6
- *
7
- * 背景:Commander 13 默认关闭 positional options,root 的 parseOptions 会扫描整个 argv
8
- * (这也正是 `--json` / `--verbose` 写在子命令之后仍生效的机制)。因此 root 一旦声明
9
- * `--version`,任何位置的 `--version` 都会先被 root 的 version listener 命中、打印版本并
10
- * exit 0,导致 `skills sync --version <ver>` 这类子命令 flag 永远拿不到值。
11
- *
12
- * 解法:仅当本次 argv 连续命中「某条自己声明了 --version 的命令路径」时才不注册,把
13
- * `--version` 让给叶子;其余一切情况照常注册,行为与历史完全一致(例如
14
- * `miaoda app get --version` 仍打印 CLI 版本)。
15
- *
16
- * 路径集从命令树反推,不硬编码 —— 以后任何叶子加 `--version` 都自动纳入。
17
- *
18
- * 设计与行为矩阵见
19
- * docs/superpowers/specs/2026-07-29-root-version-option-collision-design.md。
20
- */
21
- function shouldExposeRootVersion(root, argv) {
22
- const tokens = argv.slice(2);
23
- if (tokens.length === 0)
24
- return true;
25
- return !collectVersionOwningPaths(root).some((path) => containsPath(tokens, path));
26
- }
27
- /**
28
- * 收集自己声明了 `--version` 的命令路径。
29
- *
30
- * 每条路径是一个「段」数组,每段是该层可接受的 token 名(正名 + 全部 alias)。
31
- * 例如 `skills sync` → `[['skills'], ['sync']]`。
32
- */
33
- function collectVersionOwningPaths(root) {
34
- const paths = [];
35
- const walk = (cmd, trail) => {
36
- for (const sub of cmd.commands) {
37
- const here = [...trail, [sub.name(), ...sub.aliases()]];
38
- if (sub.options.some((o) => o.long === '--version'))
39
- paths.push(here);
40
- walk(sub, here);
41
- }
42
- };
43
- walk(root, []);
44
- return paths;
45
- }
46
- /** tokens 里是否存在一段连续子序列,逐位命中 path 各段的正名或 alias。 */
47
- function containsPath(tokens, path) {
48
- outer: for (let i = 0; i + path.length <= tokens.length; i++) {
49
- for (let j = 0; j < path.length; j++) {
50
- if (!path[j].includes(tokens[i + j]))
51
- continue outer;
52
- }
53
- return true;
54
- }
55
- return false;
56
- }