@lark-apaas/miaoda-cli 0.1.29 → 0.1.31

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.
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ /**
3
+ * html → design-html 的跨 stack 迁移规则。
4
+ *
5
+ * html stack:应用文件在 src/(src/index.html 等),dev/build 走
6
+ * @lark-apaas/coding-html-devserver(devDependency + scripts.dev/build + package-lock)。
7
+ * design-html stack:buildless —— 文件直接放项目根(任意深度),无依赖、无 scripts,
8
+ * deploy 由 CLI 闭环(扫 *.html 生成 routes.json,见 services/deploy/modern/atoms/design-build)。
9
+ *
10
+ * 设计原则:**最小改动**。三件事 —— 布局搬迁、devserver 工具链清理、set-stack:
11
+ * 1. src/ 内容整体搬到项目根(merge:同名冲突 src/ 侧覆盖,搬完删 src/)
12
+ * 2. 清 devserver 残留:package-lock.json + package.json 的 dev/build scripts 与
13
+ * coding-html-devserver devDependency(design-html 模板的 package.json 无任何
14
+ * 依赖 / scripts,用户其余自定义字段一律保留)
15
+ * 3. set-stack 切 .spark/meta.json(archType / templateVersion 由 handler 从
16
+ * design-html template 包回写)
17
+ *
18
+ * 不做的事:
19
+ * - 不从 design-html template 拷任何文件(模板本身只有 _gitignore + package.json;
20
+ * 用户已有的 .gitignore 是 html 版超集,保留无害)
21
+ * - 不动 README.md / .npmrc / .env —— 部署时均被 EXCLUDES 过滤
22
+ * - 不管 dev 进程 / MIAODA_APP_TYPE 切换(平台职责;沙箱 pkill 匹配的
23
+ * scripts/dev.js 在 html 项目里不存在,自然跳过)
24
+ *
25
+ * handler 收尾的 npm install 照常跑:devserver 依赖删掉后重新生成精简 lockfile,
26
+ * 部署时被 EXCLUDES 过滤,无害;不为此引入 skip-install 能力。
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.MIGRATE_CONFIG = void 0;
30
+ exports.MIGRATE_CONFIG = {
31
+ from: 'html',
32
+ to: 'design-html',
33
+ rules: [
34
+ // ===== 1. 布局搬迁:src/ 内容整体搬到项目根 =====
35
+ // 目标 '.'(项目根)已存在 → 走 merge 分支:src/ 每个 entry overlay 到根,
36
+ // 同名冲突 src/ 侧覆盖,完成后删除 src/。相对引用路径不变。
37
+ { type: 'move-directory', from: 'src', to: '.' },
38
+ // ===== 2. 清理 devserver 工具链残留 =====
39
+ // 依赖集合变了(清空),lockfile 删掉由 handler 收尾的 npm install 重新生成
40
+ { type: 'delete-file', to: 'package-lock.json' },
41
+ {
42
+ type: 'delete-json-keys',
43
+ to: 'package.json',
44
+ keys: [
45
+ // coding-html-devserver 的 dev/build 入口,design-html 无 scripts
46
+ 'scripts.dev',
47
+ 'scripts.build',
48
+ // devserver 工具链本体
49
+ 'devDependencies.@lark-apaas/coding-html-devserver',
50
+ ],
51
+ },
52
+ // ===== 3. 切 stack =====
53
+ { type: 'set-stack', stack: 'design-html' },
54
+ ],
55
+ };
56
+ exports.default = exports.MIGRATE_CONFIG;
@@ -18,9 +18,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.getMigrateConfig = getMigrateConfig;
19
19
  exports.listSupportedMigrations = listSupportedMigrations;
20
20
  const vite_react_to_nestjs_react_fullstack_1 = __importDefault(require("./vite-react-to-nestjs-react-fullstack"));
21
+ const html_to_design_html_1 = __importDefault(require("./html-to-design-html"));
21
22
  /** key 格式:`<from>:<to>` */
22
23
  const MIGRATE_REGISTRY = {
23
24
  'vite-react:nestjs-react-fullstack': vite_react_to_nestjs_react_fullstack_1.default,
25
+ 'html:design-html': html_to_design_html_1.default,
24
26
  };
25
27
  /** 返回 (from, to) 对应的 MigrateConfig;未注册时返回 null。 */
26
28
  function getMigrateConfig(from, to) {
@@ -37,11 +37,15 @@ exports.LOCAL_STEERING_STACKS = new Set(['nestjs-react-fullstack', 'vite-react']
37
37
  /**
38
38
  * app_type(业务类型)→ stack 短名映射。仅在调用方未显式传 --template 时用于解析 stack。
39
39
  * 与 src/api/app/types.ts 的 AppType 枚举无关,是独立的业务类型概念。
40
+ *
41
+ * 注:`frontend` 是 lark-cli 对外的纯前端应用类型;`jspage` 是内部历史代号,二者同映射
42
+ * 到 vite-react。保留 jspage 键以兼容内部既有调用方(豆包 / 妙搭链路),不删除。
40
43
  */
41
44
  exports.STACK_BY_APP_TYPE = {
42
45
  html: 'design-html',
43
46
  modern_html: 'html',
44
47
  jspage: 'vite-react',
48
+ frontend: 'vite-react',
45
49
  full_stack: 'nestjs-react-fullstack',
46
50
  };
47
51
  exports.APP_TYPES = Object.keys(exports.STACK_BY_APP_TYPE);
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.savePluginInstances = exports.LocalReleaseStatus = exports.finalizeLocalRelease = exports.createLocalRelease = exports.uploadArtifacts = exports.uploadDesignArtifacts = exports.cleanDesignDist = exports.buildDesignOutput = exports.runBuild = exports.preRelease = exports.prepareDeployContext = void 0;
3
+ exports.savePluginInstances = exports.resolveReleaseTracker = exports.createManagedReleaseTracker = exports.NOOP_RELEASE_TRACKER = exports.LocalReleaseStatus = exports.finalizeLocalRelease = exports.createLocalRelease = exports.uploadArtifacts = exports.uploadDesignArtifacts = exports.cleanDesignDist = exports.buildDesignOutput = exports.runBuild = exports.preRelease = exports.prepareDeployContext = void 0;
4
4
  var context_1 = require("./context");
5
5
  Object.defineProperty(exports, "prepareDeployContext", { enumerable: true, get: function () { return context_1.prepareDeployContext; } });
6
6
  var pre_release_1 = require("./pre-release");
@@ -18,5 +18,9 @@ var local_release_1 = require("./local-release");
18
18
  Object.defineProperty(exports, "createLocalRelease", { enumerable: true, get: function () { return local_release_1.createLocalRelease; } });
19
19
  Object.defineProperty(exports, "finalizeLocalRelease", { enumerable: true, get: function () { return local_release_1.finalizeLocalRelease; } });
20
20
  Object.defineProperty(exports, "LocalReleaseStatus", { enumerable: true, get: function () { return local_release_1.LocalReleaseStatus; } });
21
+ var release_tracker_1 = require("./release-tracker");
22
+ Object.defineProperty(exports, "NOOP_RELEASE_TRACKER", { enumerable: true, get: function () { return release_tracker_1.NOOP_RELEASE_TRACKER; } });
23
+ Object.defineProperty(exports, "createManagedReleaseTracker", { enumerable: true, get: function () { return release_tracker_1.createManagedReleaseTracker; } });
24
+ Object.defineProperty(exports, "resolveReleaseTracker", { enumerable: true, get: function () { return release_tracker_1.resolveReleaseTracker; } });
21
25
  var save_plugin_instances_1 = require("./save-plugin-instances");
22
26
  Object.defineProperty(exports, "savePluginInstances", { enumerable: true, get: function () { return save_plugin_instances_1.savePluginInstances; } });
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NOOP_RELEASE_TRACKER = void 0;
4
+ exports.createManagedReleaseTracker = createManagedReleaseTracker;
5
+ exports.resolveReleaseTracker = resolveReleaseTracker;
6
+ const logger_1 = require("../../../../utils/logger");
7
+ const local_release_1 = require("./local-release");
8
+ /**
9
+ * 建本地发布单并返回受管 tracker。
10
+ *
11
+ * url 沿用原有口径:后端 onlineUrl 缺失时为空串(`''` 表示"后端没给",
12
+ * 与外部托管模式的 `null`「本模式不产生」是两个不同语义,别合并)。
13
+ */
14
+ async function createManagedReleaseTracker(appId, version, extra) {
15
+ (0, logger_1.log)('deploy', 'Creating local release...');
16
+ const release = await (0, local_release_1.createLocalRelease)(appId, version, extra);
17
+ return {
18
+ releaseID: release.releaseID,
19
+ url: release.onlineUrl ?? '',
20
+ finish: () => (0, local_release_1.finalizeLocalRelease)(appId, release.releaseID, local_release_1.LocalReleaseStatus.Finished),
21
+ fail: () => (0, local_release_1.finalizeLocalRelease)(appId, release.releaseID, local_release_1.LocalReleaseStatus.Failed),
22
+ };
23
+ }
24
+ /**
25
+ * 空实现:发布单由外部托管,CLI 不建单也不翻终态。
26
+ *
27
+ * 外部据 CLI 的退出码决定把自己的发布单翻成 Finished 还是 Failed,
28
+ * 因此这里的 finish/fail 必须是真正的 no-op,不能顺手打日志误导排查。
29
+ */
30
+ exports.NOOP_RELEASE_TRACKER = {
31
+ releaseID: null,
32
+ url: null,
33
+ finish: () => Promise.resolve(),
34
+ fail: () => Promise.resolve(),
35
+ };
36
+ /** 按 skipRelease 选 tracker:跳过则 NOOP,否则建真实发布单。 */
37
+ async function resolveReleaseTracker(opts) {
38
+ if (opts.skipRelease) {
39
+ (0, logger_1.log)('deploy', 'Skipping local release (managed externally)');
40
+ return exports.NOOP_RELEASE_TRACKER;
41
+ }
42
+ return createManagedReleaseTracker(opts.appId, opts.version, opts.extra);
43
+ }
@@ -24,25 +24,26 @@ async function designLocalPublishPipeline(opts) {
24
24
  if (!opts.skipBuild) {
25
25
  (0, index_1.buildDesignOutput)({ projectDir: ctx.projectDir });
26
26
  }
27
- (0, logger_1.log)('deploy', 'Creating local release...');
28
- const release = await (0, index_1.createLocalRelease)(ctx.appId, pre.version, {
29
- checkPointVersion: opts.checkPointVersion,
30
- commitID: opts.commitID,
27
+ const tracker = await (0, index_1.resolveReleaseTracker)({
28
+ skipRelease: opts.skipRelease ?? false,
29
+ appId: ctx.appId,
30
+ version: pre.version,
31
+ extra: { checkPointVersion: opts.checkPointVersion, commitID: opts.commitID },
31
32
  });
32
33
  try {
33
34
  await (0, index_1.uploadDesignArtifacts)({ projectDir: ctx.projectDir, data });
34
- await (0, index_1.finalizeLocalRelease)(ctx.appId, release.releaseID, index_1.LocalReleaseStatus.Finished);
35
+ await tracker.finish();
35
36
  }
36
37
  catch (err) {
37
- await (0, index_1.finalizeLocalRelease)(ctx.appId, release.releaseID, index_1.LocalReleaseStatus.Failed);
38
+ await tracker.fail();
38
39
  throw err;
39
40
  }
40
41
  (0, logger_1.log)('deploy', 'Deployed successfully');
41
42
  return {
42
43
  appId: ctx.appId,
43
44
  version: pre.version,
44
- url: release.onlineUrl ?? '',
45
- releaseID: release.releaseID,
45
+ url: tracker.url,
46
+ releaseID: tracker.releaseID,
46
47
  preReleaseID: pre.preReleaseID,
47
48
  };
48
49
  }
@@ -11,14 +11,14 @@ const index_1 = require("../atoms/index");
11
11
  * 1. prepareDeployContext — 校验项目 + 读 meta
12
12
  * 2. preRelease — 签发 pre_release_id / version / 动态配置 data
13
13
  * 3. runBuild — 本地 npm run build(注入 data 中的 CDN 前缀)
14
- * 4. createLocalRelease — 创建发布单(加锁、拿 releaseID
14
+ * 4. resolveReleaseTracker — 创建发布单(加锁、拿 releaseID);--skip-release 下为 NOOP
15
15
  * 5. uploadArtifacts — 把产物推到 preRelease 下发的 pre-signed URL
16
16
  * 6. savePluginInstances — 扫描 dist/output_capabilities/*.json,批量注册插件能力
17
- * 7. finalizeLocalRelease — Finished / Failed 翻终态
17
+ * 7. tracker.finish/fail — Finished / Failed 翻终态
18
18
  *
19
- * createLocalRelease 提前到 upload 之前:发布单先建出来,
20
- * upload / savePluginInstances 在已知 releaseID 的前提下进行;
19
+ * 发布单先于 upload 建出来:upload / savePluginInstances 在已知 releaseID 的前提下进行;
21
20
  * 任一步失败都会精准把该发布单标记为 Failed,避免遗留挂态。
21
+ * `--skip-release` 下发布单由外部托管,tracker 为 NOOP,失败信息通过非零退出码传递。
22
22
  */
23
23
  async function localBuildLocalPublishPipeline(opts) {
24
24
  const ctx = (0, index_1.prepareDeployContext)(opts.projectDir, opts.appId);
@@ -41,10 +41,11 @@ async function localBuildLocalPublishPipeline(opts) {
41
41
  data,
42
42
  });
43
43
  }
44
- (0, logger_1.log)('deploy', 'Creating local release...');
45
- const release = await (0, index_1.createLocalRelease)(ctx.appId, pre.version, {
46
- checkPointVersion: opts.checkPointVersion,
47
- commitID: opts.commitID,
44
+ const tracker = await (0, index_1.resolveReleaseTracker)({
45
+ skipRelease: opts.skipRelease ?? false,
46
+ appId: ctx.appId,
47
+ version: pre.version,
48
+ extra: { checkPointVersion: opts.checkPointVersion, commitID: opts.commitID },
48
49
  });
49
50
  try {
50
51
  (0, logger_1.log)('deploy', 'Uploading artifacts...');
@@ -57,18 +58,18 @@ async function localBuildLocalPublishPipeline(opts) {
57
58
  if (saveResult.saved > 0) {
58
59
  (0, logger_1.log)('deploy', `${String(saveResult.saved)} capability instance(s) registered`);
59
60
  }
60
- await (0, index_1.finalizeLocalRelease)(ctx.appId, release.releaseID, index_1.LocalReleaseStatus.Finished);
61
+ await tracker.finish();
61
62
  }
62
63
  catch (err) {
63
- await (0, index_1.finalizeLocalRelease)(ctx.appId, release.releaseID, index_1.LocalReleaseStatus.Failed);
64
+ await tracker.fail();
64
65
  throw err;
65
66
  }
66
67
  (0, logger_1.log)('deploy', 'Deployed successfully');
67
68
  return {
68
69
  appId: ctx.appId,
69
70
  version: pre.version,
70
- url: release.onlineUrl ?? '',
71
- releaseID: release.releaseID,
71
+ url: tracker.url,
72
+ releaseID: tracker.releaseID,
72
73
  preReleaseID: pre.preReleaseID,
73
74
  };
74
75
  }
@@ -22,12 +22,13 @@ const logger_1 = require("../../utils/logger");
22
22
  *
23
23
  * registry 包按 **运行时(runtime)** 命名分发(而非 stack):多个 stack 可共享同一 runtime
24
24
  * 的同一份 registry。runtime 不引入新的 meta 字段,从已有的 `.spark/meta.json.stack` 推得。
25
- * STACK_RUNTIME: stack → runtime(design-html 跑在 buildless 运行时)
25
+ * STACK_RUNTIME: stack → runtime(design-html / html 都跑在 buildless 运行时)
26
26
  * REGISTRY_BY_RUNTIME: runtime → registry 包名
27
27
  * 任一步查不到都明确报错(stack 无对应 runtime / runtime 无对应 registry)。
28
28
  */
29
29
  exports.STACK_RUNTIME = {
30
30
  'design-html': 'buildless',
31
+ html: 'buildless',
31
32
  };
32
33
  exports.REGISTRY_BY_RUNTIME = {
33
34
  buildless: '@lark-apaas/coding-registry-buildless',
@@ -63,6 +63,16 @@ function syncCodingSteering(opts) {
63
63
  dstSkillsDir: node_path_1.default.join(opts.targetDir, '.agent', 'skills', 'steering', opts.stack, 'skills'),
64
64
  dstTechPath: node_path_1.default.join(opts.targetDir, '.agent', 'skills', 'steering', opts.stack, 'tech.md'),
65
65
  };
66
+ // **本函数不碰任何权限位**——解锁是调用方的事:
67
+ // - 老链路:`update-skills.sh:162` 在跑一切之前统一 `chmod -R u+w "$TARGET_DIR"`
68
+ // - 新链路(`skills sync --type X`):同一次调用里 syncSandboxSkills 已经递归解锁过
69
+ // 整个 skills 根,本函数紧随其后执行,目录必然可写
70
+ // - 本地 dev / `app init`:目录从来不会被锁
71
+ //
72
+ // 曾在这里加过「解锁 + 写完还原」,两个理由下掉:steering 子树的所有权明确归 CLI
73
+ // (老脚本 :179/:208 写明 `steering/ 子树由 miaoda CLI 独占,update.sh 不写不删`),
74
+ // 锁它没收益;而权限逻辑散在写入方会跟收尾的 lockDirReadonly 形成隐式耦合,
75
+ // 范围一旦不对称就会出「跑一次 skills sync 把整个 skills 目录解封」这种事。
66
76
  node_fs_1.default.mkdirSync(dstSkillsDir, { recursive: true });
67
77
  let techSynced = false;
68
78
  const techSrc = node_path_1.default.join(stackDir, 'tech.md');
@@ -1,24 +1,38 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BizErrCode = void 0;
3
4
  exports.mapDevopsError = mapDevopsError;
4
5
  const error_1 = require("./error");
6
+ exports.BizErrCode = {
7
+ /** devops:应用不存在(含把非应用 ID 当 app-id 传入的情况) */
8
+ DEVOPS_APP_NOT_FOUND: '400002577',
9
+ /** devops:应用不存在(另一路径,与上同义) */
10
+ DEVOPS_APP_NOT_FOUND_ALT: '400002569',
11
+ /** devops:无应用权限 */
12
+ DEVOPS_APP_PERMISSION_DENIED: '400002579',
13
+ /** devops:参数非法(如 name / description 超长) */
14
+ DEVOPS_INVALID_ARG: '400002575',
15
+ /** studio_server:ErrUnauthenticated —— 身份换取失败或未通过资源鉴权 */
16
+ STUDIO_UNAUTHENTICATED: '400002671',
17
+ /** studio_server:ErrStudioAppNotFound —— meta_token 无效或未绑定妙搭应用 */
18
+ STUDIO_APP_NOT_FOUND: '400002667',
19
+ };
5
20
  /**
6
21
  * 把 BAM lark.apaas.devops 的业务码(数字字符串)映射成 CLI 错误码。
7
- * 来源:服务端技术方案 wiki L7P8wlNQ5iyShLkxzlOcCo8LnTg「业务错误码映射表」。
8
22
  *
9
23
  * 放在 utils 下而不是某个具体 api 域里:app / deploy 都跑在 lark.apaas.devops
10
24
  * 这同一 PSM,业务码命名空间也共享;boundaries 规则又禁止 api 域间互引。
11
25
  */
12
26
  function mapDevopsError(bizCode, message) {
13
27
  switch (bizCode) {
14
- case '400002577':
15
- case '400002569':
28
+ case exports.BizErrCode.DEVOPS_APP_NOT_FOUND:
29
+ case exports.BizErrCode.DEVOPS_APP_NOT_FOUND_ALT:
16
30
  return new error_1.AppError('APP_NOT_FOUND', message, {
17
31
  next_actions: ['Run `miaoda app list` to see available apps'],
18
32
  });
19
- case '400002579':
33
+ case exports.BizErrCode.DEVOPS_APP_PERMISSION_DENIED:
20
34
  return new error_1.AppError('APP_PERMISSION_DENIED', message);
21
- case '400002575':
35
+ case exports.BizErrCode.DEVOPS_INVALID_ARG:
22
36
  return new error_1.AppError('INVALID_ARG_VALUE', message, {
23
37
  next_actions: ['缩短 --name / --description 后重试(后端有长度限制)'],
24
38
  });
@@ -5,6 +5,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.removeLineFromFile = removeLineFromFile;
7
7
  exports.removeLineFromContent = removeLineFromContent;
8
+ exports.unlockDirForWrite = unlockDirForWrite;
9
+ exports.lockDirReadonly = lockDirReadonly;
10
+ exports.forceRemove = forceRemove;
11
+ const node_child_process_1 = require("node:child_process");
8
12
  const node_fs_1 = __importDefault(require("node:fs"));
9
13
  const node_path_1 = __importDefault(require("node:path"));
10
14
  const logger_1 = require("./logger");
@@ -43,3 +47,80 @@ function removeLineFromContent(content, pattern) {
43
47
  changed: filtered.length !== lines.length,
44
48
  };
45
49
  }
50
+ // ─────────────────────────────────────────────────────────────────────────────
51
+ // 目录权限:沙箱 skill 目录被刻意锁成只读,写入前须解锁、写完须锁回。
52
+ //
53
+ // 背景:老 update-skills.sh 同步末尾 `chmod -R a=rX "$TARGET_DIR"`(:241)把
54
+ // skills 锁成只读——skills 是平台下发的资产,不该被用户/agent 手改;下一轮同步
55
+ // 开头再 `chmod -R u+w`(:162)统一解锁。目录 owner 是沙箱当前用户,虽无写位但
56
+ // 有权改权限,所以不需要 sudo(沙箱容器带 no_new_privileges,sudo 本就被禁)。
57
+ //
58
+ // **解锁只做在「整棵树的接管者」那一层**:新链路里就是 syncSandboxSkills(它重写
59
+ // 整个平铺层),对齐老脚本 :162 那一次统一解锁。写 steering 的 syncCodingSteering
60
+ // 不碰权限 —— 它跑在同一次调用里、紧随 syncSandboxSkills 之后,目录已经可写;
61
+ // 各写入方各自 chmod 会与收尾的 lockDirReadonly 形成隐式耦合,范围一旦不对称就会
62
+ // 出「跑一次 skills sync 把整个 skills 目录解封」这种事。
63
+ // ─────────────────────────────────────────────────────────────────────────────
64
+ /**
65
+ * 解锁目录使其可写(`chmod -R u+w`)。
66
+ *
67
+ * 写入被只读锁保护的目录前必须调用,否则 `fs.cpSync` / `fs.rmSync` 报 EACCES。
68
+ * 只加 owner 写位、不动 g/o,与老链路一致。目录不存在直接返回 false(调用方
69
+ * 通常紧接着 mkdir)。失败只 debug 不抛:解锁是尽力而为,真写不进去让后续
70
+ * 写操作自己报错,错误信息更具体。
71
+ */
72
+ function unlockDirForWrite(dir) {
73
+ if (!node_fs_1.default.existsSync(dir))
74
+ return false;
75
+ try {
76
+ (0, node_child_process_1.execFileSync)('chmod', ['-R', 'u+w', dir], { stdio: ['ignore', 'ignore', 'pipe'] });
77
+ return true;
78
+ }
79
+ catch (err) {
80
+ (0, logger_1.debug)(`file-ops: chmod -R u+w ${dir} failed: ${err.message}`);
81
+ return false;
82
+ }
83
+ }
84
+ /**
85
+ * 把目录锁成只读(`chmod -R a=rX`),对齐 update-skills.sh:241。
86
+ *
87
+ * `a=rX`:所有人只读;`X` 是大写,只给目录和「原本已有执行位」的文件加 x,
88
+ * 不会把 SKILL.md 变成可执行。会连 owner 写位一并清掉,所以下一轮写入前必须
89
+ * {@link unlockDirForWrite}。
90
+ *
91
+ * **调用时机必须是全部写入完成之后**——锁上就写不进去了。
92
+ */
93
+ function lockDirReadonly(dir) {
94
+ if (!node_fs_1.default.existsSync(dir))
95
+ return false;
96
+ try {
97
+ (0, node_child_process_1.execFileSync)('chmod', ['-R', 'a=rX', dir], { stdio: ['ignore', 'ignore', 'pipe'] });
98
+ return true;
99
+ }
100
+ catch (err) {
101
+ (0, logger_1.debug)(`file-ops: chmod -R a=rX ${dir} failed: ${err.message}`);
102
+ return false;
103
+ }
104
+ }
105
+ /**
106
+ * 强制删除文件或目录,三层兜底:`chmod -R u+w` → `rm -rf` → `fs.rmSync`。
107
+ *
108
+ * 为什么不能直接 `fs.rmSync`:删条目需要**父目录**写权限,而 skill 目录常处于
109
+ * 只读锁下;另外老 update.sh 曾以 root 跑过,可能留下异主文件(如 root-owned 的
110
+ * `.last-update`),`rmSync{force:true}` 只忽略 ENOENT、遇 EACCES 仍抛。
111
+ * shell `rm -rf` 对异主文件 / 怪符号链接比 rmSync 宽容,故作主档。
112
+ */
113
+ function forceRemove(target) {
114
+ if (!node_fs_1.default.existsSync(target))
115
+ return;
116
+ unlockDirForWrite(target);
117
+ try {
118
+ (0, node_child_process_1.execFileSync)('rm', ['-rf', target], { stdio: ['ignore', 'ignore', 'pipe'] });
119
+ if (!node_fs_1.default.existsSync(target))
120
+ return;
121
+ }
122
+ catch (err) {
123
+ (0, logger_1.debug)(`file-ops: rm -rf ${target} failed: ${err.message}`);
124
+ }
125
+ node_fs_1.default.rmSync(target, { recursive: true, force: true });
126
+ }
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.pickLaneVersion = pickLaneVersion;
6
7
  exports.fetchNpmPackage = fetchNpmPackage;
7
8
  const node_child_process_1 = require("node:child_process");
8
9
  const node_fs_1 = __importDefault(require("node:fs"));
@@ -11,6 +12,38 @@ const node_path_1 = __importDefault(require("node:path"));
11
12
  const error_1 = require("./error");
12
13
  const logger_1 = require("./logger");
13
14
  const DEFAULT_REGISTRY = 'https://registry.npmmirror.com/';
15
+ /**
16
+ * 从 `npm view <pkg> versions --json` 里过滤出后缀 `-alpha.<X>` 的版本。
17
+ *
18
+ * 用于 sandbox-skills 新姿势(2026-08 迁离 lane-<X> dist-tag 之后):
19
+ * publish-sandbox-skills.sh 每次发的版本号 = `<base>-alpha.<LANE>-<YYYYMMDDHHMMSS>`,
20
+ * TCC 里的 X = `<LANE>-<TIMESTAMP>` 全局唯一。反推侧过滤 versions 数组即可精确命中。
21
+ *
22
+ * **不 throw 是硬要求**(消费方 fallback @latest 依赖返回 null 判定):
23
+ * - `npm view` 网络挂、包不存在、metadata 解析失败 → 返回 null
24
+ * - 无版本匹配 → 返回 null
25
+ * - 多版本匹配(理论上不该发生,因 timestamp 精度到秒)→ 字符串排序取最大(timestamp 最大)
26
+ */
27
+ function pickLaneVersion(packageName, X, registry) {
28
+ const reg = registry ?? process.env.MIAODA_NPM_REGISTRY ?? DEFAULT_REGISTRY;
29
+ const suffix = `-alpha.${X}`;
30
+ try {
31
+ const stdout = (0, node_child_process_1.execFileSync)('npm', ['view', packageName, 'versions', '--json', '--registry', reg], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
32
+ const versions = JSON.parse(stdout);
33
+ if (!Array.isArray(versions))
34
+ return null;
35
+ const matched = versions.filter((v) => typeof v === 'string' && v.endsWith(suffix));
36
+ if (matched.length === 0)
37
+ return null;
38
+ // 通常唯一命中(X 含 timestamp 到秒);多命中时字符串排序取最大(timestamp 更晚)
39
+ matched.sort();
40
+ return matched[matched.length - 1] ?? null;
41
+ }
42
+ catch (err) {
43
+ (0, logger_1.debug)(`pickLaneVersion(${packageName}, X=${X}) failed: ${err.message}`);
44
+ return null;
45
+ }
46
+ }
14
47
  /**
15
48
  * `npm pack <pkg>@<ver> --registry <r>` + `tar -xzf`,解到临时目录。
16
49
  * extractDir 是包内容根(含 package.json)。使用完必须 cleanup()。