@lark-apaas/miaoda-cli 0.1.22 → 0.1.24

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.
Files changed (44) hide show
  1. package/README.md +36 -2
  2. package/dist/api/comment/api.js +24 -0
  3. package/dist/api/comment/index.js +6 -0
  4. package/dist/api/comment/types.js +6 -0
  5. package/dist/api/deploy/index.js +3 -1
  6. package/dist/api/deploy/modern-types.js +4 -1
  7. package/dist/api/deploy/modern.js +8 -0
  8. package/dist/api/index.js +3 -1
  9. package/dist/cli/commands/app/index.js +4 -2
  10. package/dist/cli/commands/comment/index.js +55 -0
  11. package/dist/cli/commands/deploy/modern.js +34 -0
  12. package/dist/cli/commands/index.js +19 -0
  13. package/dist/cli/commands/registry/index.js +114 -0
  14. package/dist/cli/handlers/app/init.js +6 -2
  15. package/dist/cli/handlers/comment/index.js +7 -0
  16. package/dist/cli/handlers/comment/list.js +132 -0
  17. package/dist/cli/handlers/comment/resolve.js +44 -0
  18. package/dist/cli/handlers/deploy/index.js +3 -1
  19. package/dist/cli/handlers/deploy/patch.js +17 -0
  20. package/dist/cli/handlers/registry/add.js +97 -0
  21. package/dist/cli/handlers/registry/index.js +7 -0
  22. package/dist/cli/handlers/registry/list.js +48 -0
  23. package/dist/cli/handlers/registry/shared.js +25 -0
  24. package/dist/cli/handlers/skills/sync.js +12 -4
  25. package/dist/services/app/init/template.js +1 -0
  26. package/dist/services/deploy/modern/atoms/design-build.js +49 -0
  27. package/dist/services/deploy/modern/atoms/design-upload.js +74 -0
  28. package/dist/services/deploy/modern/atoms/index.js +6 -1
  29. package/dist/services/deploy/modern/atoms/tosutil.js +246 -0
  30. package/dist/services/deploy/modern/atoms/upload.js +4 -127
  31. package/dist/services/deploy/modern/check.js +28 -16
  32. package/dist/services/deploy/modern/patch/actions.js +61 -0
  33. package/dist/services/deploy/modern/patch/content.js +18 -0
  34. package/dist/services/deploy/modern/patch/index.js +46 -0
  35. package/dist/services/deploy/modern/patch/routes.js +37 -0
  36. package/dist/services/deploy/modern/patch/source-scan.js +58 -0
  37. package/dist/services/deploy/modern/pipelines/design-local.js +56 -0
  38. package/dist/services/deploy/modern/pipelines/index.js +3 -1
  39. package/dist/services/deploy/modern/protocol.js +7 -0
  40. package/dist/services/deploy/modern/run.js +10 -4
  41. package/dist/services/deploy/modern/template-key-map.js +4 -0
  42. package/dist/services/registry/index.js +293 -0
  43. package/dist/utils/http.js +1 -1
  44. package/package.json +1 -1
@@ -0,0 +1,58 @@
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.EXCLUDES = void 0;
7
+ exports.listSourceFiles = listSourceFiles;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ // 与模板 scripts/build.sh 的 rsync EXCLUDES 对齐:这些名字(任意层级 basename)不进产物/不计入路由。
11
+ exports.EXCLUDES = new Set([
12
+ '.git',
13
+ 'node_modules',
14
+ 'dist',
15
+ 'scripts',
16
+ 'package.json',
17
+ 'package-lock.json',
18
+ 'pnpm-lock.yaml',
19
+ 'yarn.lock',
20
+ '.gitignore',
21
+ '.npmrc',
22
+ '.agent',
23
+ '.agents',
24
+ 'skills',
25
+ '.env',
26
+ '.env.local',
27
+ 'README.md',
28
+ '.DS_Store',
29
+ '.spark',
30
+ ]);
31
+ /**
32
+ * 递归列出 root 下所有文件的相对 posix 路径,跳过 EXCLUDES(任意层级 basename),
33
+ * 用 statSync 判定目录/文件以 follow 符号链接(含指向目录的软链),断链等无法 stat 的条目跳过。
34
+ */
35
+ function listSourceFiles(root) {
36
+ const out = [];
37
+ const walk = (dir, rel) => {
38
+ for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
39
+ if (exports.EXCLUDES.has(entry.name))
40
+ continue;
41
+ const childRel = rel ? `${rel}/${entry.name}` : entry.name;
42
+ const abs = node_path_1.default.join(dir, entry.name);
43
+ let isDir;
44
+ try {
45
+ isDir = node_fs_1.default.statSync(abs).isDirectory();
46
+ }
47
+ catch {
48
+ continue;
49
+ }
50
+ if (isDir)
51
+ walk(abs, childRel);
52
+ else
53
+ out.push(childRel);
54
+ }
55
+ };
56
+ walk(root, '');
57
+ return out;
58
+ }
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.designLocalPublishPipeline = designLocalPublishPipeline;
4
+ const spark_meta_1 = require("../../../../utils/spark-meta");
5
+ const error_1 = require("../../../../utils/error");
6
+ const logger_1 = require("../../../../utils/logger");
7
+ const index_1 = require("../atoms/index");
8
+ /**
9
+ * design(design_local_deploy)本地构建 + 本地部署 pipeline。
10
+ *
11
+ * 与 client 版差异:build 走 buildDesignOutput(CLI 闭环,不跑 npm run build)、upload 走 uploadDesignArtifacts(version+latest
12
+ * 双写、latest 精确镜像)、不注册插件能力(design-html 无 capability)。其余复用同一套 atom。
13
+ */
14
+ async function designLocalPublishPipeline(opts) {
15
+ const ctx = (0, index_1.prepareDeployContext)(opts.projectDir, opts.appId);
16
+ (0, logger_1.log)('deploy', 'Pre-releasing...');
17
+ const pre = await (0, index_1.preRelease)(ctx.appId, ctx.templateKey);
18
+ (0, logger_1.log)('deploy', `pre_release_id=${pre.preReleaseID} version=${pre.version}`);
19
+ if (pre.data === undefined) {
20
+ throw new error_1.AppError('DEPLOY_PRE_RELEASE_DATA_MISSING', 'preRelease did not return a `data` map — backend tcc may not be configured for current template_key');
21
+ }
22
+ const data = pre.data;
23
+ // dist 是本地构建产物,部署动作结束(无论成败)都清掉,不在用户项目里留产物。
24
+ try {
25
+ if (!opts.skipBuild) {
26
+ (0, index_1.buildDesignOutput)({ projectDir: ctx.projectDir });
27
+ }
28
+ (0, logger_1.log)('deploy', 'Creating local release...');
29
+ const release = await (0, index_1.createLocalRelease)(ctx.appId, pre.version, {
30
+ checkPointVersion: opts.checkPointVersion,
31
+ commitID: opts.commitID,
32
+ });
33
+ try {
34
+ await (0, index_1.uploadDesignArtifacts)({ projectDir: ctx.projectDir, data });
35
+ await (0, index_1.finalizeLocalRelease)(ctx.appId, release.releaseID, index_1.LocalReleaseStatus.Finished);
36
+ }
37
+ catch (err) {
38
+ await (0, index_1.finalizeLocalRelease)(ctx.appId, release.releaseID, index_1.LocalReleaseStatus.Failed);
39
+ throw err;
40
+ }
41
+ if (release.onlineUrl !== undefined && release.onlineUrl !== '') {
42
+ (0, spark_meta_1.writeSparkMeta)(ctx.projectDir, { appUrl: release.onlineUrl });
43
+ }
44
+ (0, logger_1.log)('deploy', 'Deployed successfully');
45
+ return {
46
+ appId: ctx.appId,
47
+ version: pre.version,
48
+ url: release.onlineUrl ?? '',
49
+ releaseID: release.releaseID,
50
+ preReleaseID: pre.preReleaseID,
51
+ };
52
+ }
53
+ finally {
54
+ (0, index_1.cleanDesignDist)(ctx.projectDir);
55
+ }
56
+ }
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.localBuildLocalPublishPipeline = void 0;
3
+ exports.designLocalPublishPipeline = exports.localBuildLocalPublishPipeline = void 0;
4
4
  var local_1 = require("./local");
5
5
  Object.defineProperty(exports, "localBuildLocalPublishPipeline", { enumerable: true, get: function () { return local_1.localBuildLocalPublishPipeline; } });
6
+ var design_local_1 = require("./design-local");
7
+ Object.defineProperty(exports, "designLocalPublishPipeline", { enumerable: true, get: function () { return design_local_1.designLocalPublishPipeline; } });
@@ -33,6 +33,13 @@ exports.DataKey = {
33
33
  // downloadURLPrefix 替代原 static_cdn_prefix 注入 build env。
34
34
  /** dist/output_static 上传凭证(仅当目录存在时使用) */
35
35
  OUTPUT_STATIC_PAAS_STORAGE_CREDENTIAL: 'output_static_paas_storage_credential',
36
+ // ── design 部署专用(design_local_deploy) ──
37
+ /** 覆盖整个 {app_id} 文件夹的上传凭证(形态同 TosUploadCredential) */
38
+ OUTPUT_ALL_TOS_UPLOAD_CREDENTIAL: 'output_all_tos_upload_credential',
39
+ /** version/备份份真实路径(后端已替换 {app_id}/{version}) */
40
+ OUTPUT_BACKUP_PATH: 'output_backup_path',
41
+ /** latest 份真实路径(后端已替换 {app_id}/latest) */
42
+ OUTPUT_LATEST_PATH: 'output_latest_path',
36
43
  };
37
44
  /** 取必传 key,缺失抛 AppError(带 key 名,便于后端 tcc 配置排查) */
38
45
  function requireDataKey(data, key) {
@@ -1,13 +1,19 @@
1
1
  "use strict";
2
2
  // modern deploy 主入口(dispatch 层)。
3
3
  //
4
- // 当前阶段(Scope A)只接入"本地构建 + 本地部署"一条 pipeline
5
- // 后续接入"本地构建 + 远端部署 / 远端构建 + 远端部署"时,在此处按 template_key
6
- // 或显式 flag 选不同 pipeline。pipeline 之间彼此独立、不共享中间状态。
4
+ // templateKey pipeline:design_local_deploy → design pipeline,其余 → client pipeline。
5
+ // 仅为路由轻量解析 templateKey(读 meta.stack + 映射),不跑前置检查——前置检查在各
6
+ // pipeline 内部的 prepareDeployContext 里做。client 分支与原行为完全一致。
7
+ // pipeline 之间彼此独立、不共享中间状态。
7
8
  Object.defineProperty(exports, "__esModule", { value: true });
8
9
  exports.runModernDeploy = runModernDeploy;
10
+ const spark_meta_1 = require("../../../utils/spark-meta");
11
+ const template_key_map_1 = require("./template-key-map");
9
12
  const index_1 = require("./pipelines/index");
10
13
  async function runModernDeploy(opts) {
11
- // 暂只支持本地构建 + 本地部署
14
+ const stack = (0, spark_meta_1.readSparkMeta)(opts.projectDir).stack ?? '';
15
+ if ((0, template_key_map_1.resolveTemplateKey)(stack) === template_key_map_1.DESIGN_LOCAL_DEPLOY) {
16
+ return (0, index_1.designLocalPublishPipeline)(opts);
17
+ }
12
18
  return (0, index_1.localBuildLocalPublishPipeline)(opts);
13
19
  }
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DESIGN_LOCAL_DEPLOY = void 0;
3
4
  exports.resolveTemplateKey = resolveTemplateKey;
4
5
  /**
5
6
  * 本地 stack 短名 → 后端发布模板 key 的映射。
@@ -10,9 +11,12 @@ exports.resolveTemplateKey = resolveTemplateKey;
10
11
  * 未来出现需要走别的 templateKey 的 stack,再在表里加一行覆盖默认。
11
12
  */
12
13
  const DEFAULT_TEMPLATE_KEY = 'client_local_deploy';
14
+ /** design 部署 templateKey(run.ts 据此分流到 design pipeline) */
15
+ exports.DESIGN_LOCAL_DEPLOY = 'design_local_deploy';
13
16
  const TEMPLATE_KEY_MAP = {
14
17
  'vite-react': DEFAULT_TEMPLATE_KEY,
15
18
  html: DEFAULT_TEMPLATE_KEY,
19
+ 'design-html': exports.DESIGN_LOCAL_DEPLOY,
16
20
  };
17
21
  /**
18
22
  * 把本地 stack 短名映射到后端 templateKey;未配置的 stack 走默认 client_local_deploy。
@@ -0,0 +1,293 @@
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.REGISTRY_SUPPORTED_STACKS = exports.REGISTRY_BY_RUNTIME = exports.STACK_RUNTIME = void 0;
7
+ exports.resolveRegistryPackage = resolveRegistryPackage;
8
+ exports.loadRegistry = loadRegistry;
9
+ exports.extractUsageBlock = extractUsageBlock;
10
+ exports.parseDependsOn = parseDependsOn;
11
+ exports.tokenMatches = tokenMatches;
12
+ exports.resolveDependencyClosure = resolveDependencyClosure;
13
+ exports.planAdd = planAdd;
14
+ exports.applyAdd = applyAdd;
15
+ const node_fs_1 = __importDefault(require("node:fs"));
16
+ const node_path_1 = __importDefault(require("node:path"));
17
+ const npm_pack_1 = require("../../utils/npm-pack");
18
+ const error_1 = require("../../utils/error");
19
+ const logger_1 = require("../../utils/logger");
20
+ /**
21
+ * 解析链:stack → runtime → registry 包名。
22
+ *
23
+ * registry 包按 **运行时(runtime)** 命名分发(而非 stack):多个 stack 可共享同一 runtime
24
+ * 的同一份 registry。runtime 不引入新的 meta 字段,从已有的 `.spark/meta.json.stack` 推得。
25
+ * STACK_RUNTIME: stack → runtime(design-html 跑在 buildless 运行时)
26
+ * REGISTRY_BY_RUNTIME: runtime → registry 包名
27
+ * 任一步查不到都明确报错(stack 无对应 runtime / runtime 无对应 registry)。
28
+ */
29
+ exports.STACK_RUNTIME = {
30
+ 'design-html': 'buildless',
31
+ };
32
+ exports.REGISTRY_BY_RUNTIME = {
33
+ buildless: '@lark-apaas/coding-registry-buildless',
34
+ };
35
+ /** 有对应 registry 的 stack 列表(stack 有 runtime 且该 runtime 有 registry 包)。 */
36
+ exports.REGISTRY_SUPPORTED_STACKS = Object.keys(exports.STACK_RUNTIME).filter((stack) => Boolean(exports.REGISTRY_BY_RUNTIME[exports.STACK_RUNTIME[stack]]));
37
+ /**
38
+ * stack → registry 包名。走 STACK_RUNTIME → REGISTRY_BY_RUNTIME 两跳。
39
+ * stack 无对应 runtime,或 runtime 无对应 registry,均抛 AppError。
40
+ */
41
+ function resolveRegistryPackage(stack) {
42
+ const runtime = exports.STACK_RUNTIME[stack];
43
+ if (!runtime) {
44
+ throw new error_1.AppError('REGISTRY_STACK_UNSUPPORTED', `stack "${stack}" 没有对应的 runtime`, {
45
+ next_actions: [`支持 registry 的 stack:${exports.REGISTRY_SUPPORTED_STACKS.join(', ') || '(none)'}`],
46
+ });
47
+ }
48
+ const packageName = exports.REGISTRY_BY_RUNTIME[runtime];
49
+ if (!packageName) {
50
+ throw new error_1.AppError('REGISTRY_RUNTIME_UNSUPPORTED', `runtime "${runtime}"(来自 stack "${stack}")没有对应的 registry 包`);
51
+ }
52
+ return packageName;
53
+ }
54
+ const USAGE_BEGIN = '/* BEGIN USAGE */';
55
+ const USAGE_END = '/* END USAGE */';
56
+ /**
57
+ * 拉取并解析某 stack 的 registry 包。
58
+ *
59
+ * 复用 fetchNpmPackage(npm pack + tar -xzf + 临时目录),与 template / steering 同一套
60
+ * tarball 基建。包内布局约定(SDK 文件**平铺在包根**,无外层 registry/ 目录):
61
+ * package/<subdir>/<sdk-file> ← 按目录组织的 SDK 文件,每个以 USAGE 块开头
62
+ * (scripts/ · components/ · stylesheets/ 等;CLI 不约束子目录名)
63
+ *
64
+ * 从包根递归扫整棵树,判定含 USAGE 块的为 SDK 条目(package.json / README / LICENSE 等无
65
+ * USAGE 块的文件自然跳过),解析其 USAGE 原文 + Depends。
66
+ * 调用方用完 **必须** 调 result.cleanup() 清理临时目录。
67
+ */
68
+ function loadRegistry(opts) {
69
+ const packageName = resolveRegistryPackage(opts.stack);
70
+ const effectiveVersion = opts.version ?? 'latest';
71
+ (0, logger_1.log)('registry', `Fetching ${packageName}@${effectiveVersion}...`);
72
+ const fetched = (0, npm_pack_1.fetchNpmPackage)({ packageName, version: effectiveVersion });
73
+ try {
74
+ // 扫描根 = 包根(fetchNpmPackage 的 extractDir 即 tarball 内 package/ 目录)
75
+ const registryRoot = fetched.extractDir;
76
+ const entries = scanEntries(registryRoot);
77
+ return {
78
+ entries,
79
+ registryRoot,
80
+ version: fetched.version,
81
+ cleanup: () => {
82
+ fetched.cleanup();
83
+ },
84
+ };
85
+ }
86
+ catch (err) {
87
+ fetched.cleanup();
88
+ throw err;
89
+ }
90
+ }
91
+ /**
92
+ * 可作为 SDK 条目的代码文件扩展名白名单。
93
+ * 只扫代码文件 —— README.md / LICENSE / package.json 等不在此列,避免文档里出现的
94
+ * USAGE 字面量(如 README 解释 `/* BEGIN USAGE *​/` 格式)被误判为 SDK 条目。
95
+ */
96
+ const SDK_FILE_EXTENSIONS = new Set(['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.css']);
97
+ /**
98
+ * 条目所在的顶层目录白名单。只有包根下这几个目录(任意深度)里的代码文件才算条目,
99
+ * 包根直放的文件(eslint.config.js 等)、其它顶层目录(lib/ 等)一律排除——
100
+ * 避免 USAGE 可选后把工程配置 / 非 SDK 代码误判为条目。
101
+ */
102
+ const SDK_TOP_DIRS = new Set(['components', 'scripts', 'stylesheets']);
103
+ /**
104
+ * 从包根递归扫整棵树,提取 SDK 条目。条目判据:
105
+ * 1. 在白名单顶层目录下(SDK_TOP_DIRS,任意深度);
106
+ * 2. 是代码文件(SDK_FILE_EXTENSIONS)。
107
+ * USAGE 块**可选**:有则解析描述 + Depends,无则 usage 为空、dependsOn 为空——仍是条目。
108
+ * name(去扩展名 basename)**不要求唯一**:同名的不同文件全部收录为独立条目,由 relPath
109
+ * (树内唯一)区分;寻址 / 拉取由 tokenMatches + resolveDependencyClosure 处理(同名 → 全拉)。
110
+ */
111
+ function scanEntries(registryRoot) {
112
+ const entries = [];
113
+ const walk = (dir) => {
114
+ for (const dirent of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
115
+ const absPath = node_path_1.default.join(dir, dirent.name);
116
+ if (dirent.isDirectory()) {
117
+ walk(absPath);
118
+ continue;
119
+ }
120
+ if (!dirent.isFile())
121
+ continue;
122
+ // 只扫代码文件,排除 README/LICENSE/package.json 等非 SDK 文件
123
+ if (!SDK_FILE_EXTENSIONS.has(node_path_1.default.extname(dirent.name).toLowerCase()))
124
+ continue;
125
+ // 相对包根的路径,统一用 '/' 分隔(落地时再按平台 join)
126
+ const relPath = node_path_1.default.relative(registryRoot, absPath).split(node_path_1.default.sep).join('/');
127
+ // 只认白名单顶层目录下的文件(顶层段 ∈ SDK_TOP_DIRS)
128
+ if (!SDK_TOP_DIRS.has(relPath.split('/')[0]))
129
+ continue;
130
+ const usage = extractUsageBlock(node_fs_1.default.readFileSync(absPath, 'utf-8')) ?? '';
131
+ entries.push({
132
+ name: stripExt(dirent.name),
133
+ relPath,
134
+ usage,
135
+ dependsOn: parseDependsOn(usage),
136
+ absPath,
137
+ });
138
+ }
139
+ };
140
+ walk(registryRoot);
141
+ // 按 relPath(唯一)稳定排序,便于 list 输出可预期
142
+ entries.sort((a, b) => a.relPath.localeCompare(b.relPath));
143
+ return entries;
144
+ }
145
+ /** 提取 BEGIN/END USAGE 标记之间的原文(含标记行)。无 USAGE 块返回 null。 */
146
+ function extractUsageBlock(content) {
147
+ const begin = content.indexOf(USAGE_BEGIN);
148
+ if (begin === -1)
149
+ return null;
150
+ const end = content.indexOf(USAGE_END, begin);
151
+ if (end === -1)
152
+ return null;
153
+ return content.slice(begin, end + USAGE_END.length);
154
+ }
155
+ /** 从 USAGE 块文本里解析 `// Depends: <name>` 行(可多行),返回去重后的依赖名列表。 */
156
+ function parseDependsOn(usage) {
157
+ const deps = [];
158
+ const seen = new Set();
159
+ for (const line of usage.split(/\r?\n/)) {
160
+ const m = /^\s*\/\/\s*Depends:\s*(.+?)\s*$/.exec(line);
161
+ if (m === null)
162
+ continue;
163
+ // 一行内允许逗号 / 空白分隔多个依赖
164
+ for (const raw of m[1].split(/[\s,]+/)) {
165
+ const name = raw.trim();
166
+ if (name !== '' && !seen.has(name)) {
167
+ seen.add(name);
168
+ deps.push(name);
169
+ }
170
+ }
171
+ }
172
+ return deps;
173
+ }
174
+ function stripExt(file) {
175
+ const ext = node_path_1.default.extname(file);
176
+ return ext === '' ? file : file.slice(0, -ext.length);
177
+ }
178
+ /**
179
+ * 一个 token 是否命中某条目。token 可写三种寻址形式(按特异度,任一相等即命中):
180
+ * - relPath:相对包根路径,如 components/foo.jsx(含 '/')
181
+ * - 完整文件名:含扩展名的 basename,如 foo.jsx
182
+ * - name:去扩展名的 basename,如 foo
183
+ * 三种形式无歧义重叠(relPath 含 '/',basename 不含;完整文件名含原扩展名,name 不含),
184
+ * 故用一个等值谓词即可。多个条目可同时命中同一 token(同名 → 全拉),由调用方处理。
185
+ */
186
+ function tokenMatches(entry, token) {
187
+ if (token === entry.relPath)
188
+ return true;
189
+ const fileName = entry.relPath.split('/').pop() ?? '';
190
+ if (token === fileName)
191
+ return true;
192
+ return token === entry.name;
193
+ }
194
+ /**
195
+ * 解析 Depends 的传递闭包:从 requested token 出发,递归把所有依赖一并纳入。
196
+ *
197
+ * token 经 tokenMatches 解析成**一组**条目(同名 → 全拉),逐个展开。返回拓扑序
198
+ * (依赖在前、被依赖在后),便于按序拷贝。token 零命中抛 REGISTRY_ENTRY_NOT_FOUND;
199
+ * 检测循环依赖并抛 REGISTRY_CYCLE。visited / onStack / 环检测均以 relPath(唯一)为键,
200
+ * 故同名(去扩展名)的不同文件互不干扰、不会误判成环。
201
+ */
202
+ function resolveDependencyClosure(entries, requested) {
203
+ const ordered = [];
204
+ const visited = new Set(); // by relPath
205
+ const onStack = new Set(); // by relPath
206
+ // token → 命中条目集合;零命中 fail-fast(避免拷半套)
207
+ const resolveToken = (token, trail) => {
208
+ const matches = entries.filter((e) => tokenMatches(e, token));
209
+ if (matches.length === 0) {
210
+ const via = trail.length > 0 ? `(被 "${trail[trail.length - 1]}" 依赖)` : '';
211
+ throw new error_1.AppError('REGISTRY_ENTRY_NOT_FOUND', `registry 没有条目 "${token}"${via}`, {
212
+ next_actions: ['用 `miaoda registry list` 查看可用条目'],
213
+ });
214
+ }
215
+ return matches;
216
+ };
217
+ const visit = (entry, trail) => {
218
+ if (visited.has(entry.relPath))
219
+ return;
220
+ if (onStack.has(entry.relPath)) {
221
+ throw new error_1.AppError('REGISTRY_CYCLE', `registry 条目依赖成环:${[...trail, entry.relPath].join(' -> ')}`);
222
+ }
223
+ onStack.add(entry.relPath);
224
+ for (const depToken of entry.dependsOn) {
225
+ for (const depEntry of resolveToken(depToken, [...trail, entry.relPath])) {
226
+ visit(depEntry, [...trail, entry.relPath]);
227
+ }
228
+ }
229
+ onStack.delete(entry.relPath);
230
+ visited.add(entry.relPath);
231
+ ordered.push(entry);
232
+ };
233
+ for (const token of requested) {
234
+ for (const entry of resolveToken(token, []))
235
+ visit(entry, []);
236
+ }
237
+ return ordered;
238
+ }
239
+ /**
240
+ * 规划 add:解析依赖闭包 → 每个条目**拍平为 basename** 落到项目根 → 标记是否已存在。
241
+ * 纯计算,不落盘(dry-run 与真实 add 共用)。exists 在此快照(落盘前),故闭包内同名
242
+ * basename 撞车时双方 exists 都为 false、applyAdd 里后者覆盖前者。
243
+ */
244
+ function planAdd(opts) {
245
+ const entries = resolveDependencyClosure(opts.entries, opts.names);
246
+ const files = entries.map((entry) => {
247
+ const dest = entry.relPath.split('/').pop() ?? entry.relPath; // 拍平为 basename
248
+ const destAbs = node_path_1.default.join(opts.targetDir, dest);
249
+ return {
250
+ entry: entry.name,
251
+ srcRelPath: entry.relPath,
252
+ dest,
253
+ destAbs,
254
+ srcAbs: entry.absPath,
255
+ exists: node_fs_1.default.existsSync(destAbs),
256
+ };
257
+ });
258
+ return { entries, files };
259
+ }
260
+ /**
261
+ * 执行 add 计划:把 SDK 文件**拍平**拷到项目根。
262
+ * - 目标已存在且未 overwrite → skip(skip-by-disk 保护用户改动)
263
+ * - 目标已存在且 overwrite → 覆盖写
264
+ * - 目标不存在 → 写
265
+ * 闭包内同名 basename 撞车(双方 exists 均为 plan 前快照 false)→ 顺序拷贝、后者覆盖前者,
266
+ * added 去重只报一次。dryRun=true 时只分类不落盘。不写任何安装记账文件(项目文件树即状态)。
267
+ */
268
+ function applyAdd(files, opts) {
269
+ const added = [];
270
+ const skipped = [];
271
+ const seen = new Set();
272
+ const pushUnique = (list, dest) => {
273
+ if (seen.has(dest))
274
+ return;
275
+ seen.add(dest);
276
+ list.push(dest);
277
+ };
278
+ for (const f of files) {
279
+ if (f.exists && !opts.overwrite) {
280
+ pushUnique(skipped, f.dest);
281
+ continue;
282
+ }
283
+ if (!opts.dryRun) {
284
+ if (!node_fs_1.default.existsSync(f.srcAbs)) {
285
+ throw new error_1.AppError('REGISTRY_FILE_MISSING', `registry 包内缺少文件 ${f.srcRelPath}(条目 "${f.entry}")`);
286
+ }
287
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(f.destAbs), { recursive: true });
288
+ node_fs_1.default.copyFileSync(f.srcAbs, f.destAbs);
289
+ }
290
+ pushUnique(added, f.dest);
291
+ }
292
+ return { added, skipped };
293
+ }
@@ -64,7 +64,7 @@ function createClient(opts) {
64
64
  * 默认灰度泳道。当前 miaoda CLI 接入的新接口(modern deploy 等)只在该泳道生效,
65
65
  * 缺省走这个。MIAODA_CANARY_HEADER 可显式覆盖;设为空字符串则去掉头。
66
66
  */
67
- const DEFAULT_CANARY_HEADER = 'boe_miaoda_doubao';
67
+ const DEFAULT_CANARY_HEADER = 'boe_miaoda_ccm';
68
68
  /**
69
69
  * 注入 x-tt-env 小流量头:env 显式设值 > DEFAULT_CANARY_HEADER。
70
70
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/miaoda-cli",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "Miaoda 平台命令行工具,面向 Agent 调用",
5
5
  "type": "commonjs",
6
6
  "bin": {