@lark-apaas/miaoda-cli 0.1.6 → 0.1.7-alpha.785a451

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 (48) hide show
  1. package/dist/api/deploy/schemas.js +3 -0
  2. package/dist/cli/commands/app/index.js +67 -7
  3. package/dist/cli/commands/deploy/modern.js +9 -0
  4. package/dist/cli/commands/index.js +36 -1
  5. package/dist/cli/commands/skills/index.js +18 -2
  6. package/dist/cli/handlers/app/index.js +4 -1
  7. package/dist/cli/handlers/app/init.js +60 -9
  8. package/dist/cli/handlers/app/sync.js +266 -0
  9. package/dist/cli/handlers/deploy/modern.js +39 -0
  10. package/dist/cli/handlers/plugin/plugin-local.js +1 -1
  11. package/dist/cli/handlers/skills/sync.js +15 -4
  12. package/dist/config/fullstack-cli-pin.js +13 -0
  13. package/dist/config/sync-configs/design-stack.js +98 -0
  14. package/dist/config/sync-configs/index.js +62 -0
  15. package/dist/config/sync-configs/nestjs-react-fullstack.js +177 -0
  16. package/dist/config/sync.js +14 -0
  17. package/dist/services/app/init/install.js +44 -13
  18. package/dist/services/app/init/template.js +23 -6
  19. package/dist/services/deploy/modern/atoms/local-release.js +7 -2
  20. package/dist/services/deploy/modern/pipelines/local.js +4 -1
  21. package/dist/utils/coding-steering.js +107 -28
  22. package/dist/utils/file-ops.js +45 -0
  23. package/dist/utils/githooks.js +55 -0
  24. package/dist/utils/merge-json.js +63 -0
  25. package/dist/utils/platform-sync.js +160 -0
  26. package/dist/utils/sync-rule.js +295 -0
  27. package/package.json +7 -4
  28. package/upgrade/templates/README.md +34 -0
  29. package/upgrade/templates/design-stack/templates/.githooks/pre-commit +4 -0
  30. package/upgrade/templates/design-stack/templates/scripts/dev-local.js +84 -0
  31. package/upgrade/templates/design-stack/templates/scripts/dev.sh +26 -0
  32. package/upgrade/templates/design-stack/templates/scripts/hooks/run-precommit.js +37 -0
  33. package/upgrade/templates/nestjs-react-fullstack/templates/.githooks/pre-commit +4 -0
  34. package/upgrade/templates/nestjs-react-fullstack/templates/.gitignore.append +8 -0
  35. package/upgrade/templates/nestjs-react-fullstack/templates/.spark_project +16 -0
  36. package/upgrade/templates/nestjs-react-fullstack/templates/drizzle.config.ts +55 -0
  37. package/upgrade/templates/nestjs-react-fullstack/templates/helper/gen-openapi.ts +34 -0
  38. package/upgrade/templates/nestjs-react-fullstack/templates/nest-cli.json +25 -0
  39. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/build.sh +207 -0
  40. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/dev-local.js +113 -0
  41. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/dev.js +295 -0
  42. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/dev.sh +26 -0
  43. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/hooks/run-precommit.js +37 -0
  44. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/lint.js +150 -0
  45. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/prune-smart.js +330 -0
  46. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/run.sh +8 -0
  47. package/upgrade/templates/nestjs-react-fullstack/templates/server/global.d.ts +19 -0
  48. package/upgrade/templates/nestjs-react-fullstack/templates/tsconfig.node.json +5 -0
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ /**
3
+ * nestjs-react-fullstack stack 的 sync 规则。
4
+ *
5
+ * 跟沙箱里 `npm run upgrade → npx fullstack-cli sync --disable-gen-openapi` 行为对齐
6
+ * (fullstack-cli/src/config/sync.ts),新增的 miaoda-cli 本地开发规则单独标注。
7
+ *
8
+ * 改这里请同步把 user app 端到端 smoke 跑一次(init → sync → npm run dev)。
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.SYNC_CONFIG = void 0;
12
+ const fullstack_cli_pin_1 = require("../../config/fullstack-cli-pin");
13
+ exports.SYNC_CONFIG = {
14
+ sync: [
15
+ // ===== fullstack-cli sync 同款规则(对齐沙箱行为) =====
16
+ // 1. 派生 scripts 目录(包含 dev.sh / dev-local.js / dev.js / build.sh / run.sh /
17
+ // lint.js / prune-smart.js / hooks/run-precommit.js;directory rule 自动给 .sh +x)
18
+ {
19
+ type: 'directory',
20
+ from: 'scripts',
21
+ to: 'scripts',
22
+ overwrite: true,
23
+ },
24
+ // 1a. 同步 .githooks 目录(hook 入口,可执行 sh 脚本)
25
+ {
26
+ type: 'directory',
27
+ from: '.githooks',
28
+ to: '.githooks',
29
+ overwrite: true,
30
+ },
31
+ // 1b. scripts.prepare:npm install 后自动激活 git hooks(原生 git,无第三方依赖)
32
+ // 直接写 core.hooksPath,并保底给 pre-commit 加执行位;非 git 仓库下静默退出
33
+ {
34
+ type: 'add-script',
35
+ name: 'prepare',
36
+ command: 'chmod +x .githooks/pre-commit 2>/dev/null; git config core.hooksPath .githooks 2>/dev/null || true',
37
+ overwrite: false,
38
+ },
39
+ // 1c. scripts.precommit = pre-commit 真正的执行体(跑 npm run lint)
40
+ {
41
+ type: 'add-script',
42
+ name: 'precommit',
43
+ command: 'node scripts/hooks/run-precommit.js',
44
+ overwrite: false,
45
+ },
46
+ // 2. 智能合并 nest-cli.json 配置(保留用户自定义的 assets、plugins 等)
47
+ {
48
+ type: 'merge-json',
49
+ from: 'nest-cli.json',
50
+ to: 'nest-cli.json',
51
+ arrayMerge: {
52
+ 'compilerOptions.assets': { key: 'include' },
53
+ 'compilerOptions.plugins': { key: 'name' },
54
+ },
55
+ },
56
+ // 3. 删除 .swc 缓存目录(如果存在)
57
+ {
58
+ type: 'delete-directory',
59
+ to: '.swc',
60
+ },
61
+ // 4. 从 .gitignore 中移除 package-lock.json
62
+ {
63
+ type: 'remove-line',
64
+ to: '.gitignore',
65
+ pattern: 'package-lock.json',
66
+ },
67
+ // 5. 注册 postinstall 脚本,自动恢复 action plugins。
68
+ // 用 npx -y 形式而不是裸 fullstack-cli:用户项目的 deps 没有 fullstack-cli,
69
+ // bare 形式在新机器(PATH 没全局 fullstack-cli)下 npm i 立即 ENOENT。
70
+ // fullstack-cli 版本临时钉到 FULLSTACK_CLI_PIN_SPEC,避免 npx 走 latest 碰外部回归版本。
71
+ {
72
+ type: 'add-script',
73
+ name: 'postinstall',
74
+ command: `npx -y ${fullstack_cli_pin_1.FULLSTACK_CLI_PIN_SPEC} action-plugin init`,
75
+ overwrite: false,
76
+ },
77
+ // 5a. 迁移已有应用的老形式 postinstall(裸 fullstack-cli → npx -y 钉版)。
78
+ // patch-script 只在脚本以指定前缀开头时改写,用户真正手改过的脚本保持原样。
79
+ {
80
+ type: 'patch-script',
81
+ name: 'postinstall',
82
+ to: `npx -y ${fullstack_cli_pin_1.FULLSTACK_CLI_PIN_SPEC} action-plugin init`,
83
+ ifStartsWith: 'fullstack-cli action-plugin init',
84
+ },
85
+ // 5b. 迁移已有应用 npx 未钉版的 postinstall —— sync 之前的版本生成的是
86
+ // `npx -y @lark-apaas/fullstack-cli action-plugin init`(无版本号),现在统一钉版。
87
+ {
88
+ type: 'patch-script',
89
+ name: 'postinstall',
90
+ to: `npx -y ${fullstack_cli_pin_1.FULLSTACK_CLI_PIN_SPEC} action-plugin init`,
91
+ ifStartsWith: 'npx -y @lark-apaas/fullstack-cli action-plugin init',
92
+ },
93
+ // 5c. gen:db-schema —— 把裸 `fullstack-cli gen-db-schema` 迁到钉版 npx。
94
+ // 这条只在 user app 自己 package.json 已经有 gen:db-schema 时才动(patch-script
95
+ // 语义),新模板里如果有就一并升级。
96
+ {
97
+ type: 'patch-script',
98
+ name: 'gen:db-schema',
99
+ to: `npx -y ${fullstack_cli_pin_1.FULLSTACK_CLI_PIN_SPEC} gen-db-schema`,
100
+ ifStartsWith: 'fullstack-cli gen-db-schema',
101
+ },
102
+ // 6. 替换 drizzle.config.ts(仅当文件存在时)
103
+ {
104
+ type: 'file',
105
+ from: 'drizzle.config.ts',
106
+ to: 'drizzle.config.ts',
107
+ overwrite: true,
108
+ onlyIfExists: true,
109
+ },
110
+ // 7. 确保 .gitignore 包含 .agent/ 目录
111
+ {
112
+ type: 'add-line',
113
+ to: '.gitignore',
114
+ line: '.agent/',
115
+ },
116
+ // 8. 同步 .spark_project 配置文件(总是覆盖)
117
+ {
118
+ type: 'file',
119
+ from: '.spark_project',
120
+ to: '.spark_project',
121
+ overwrite: true,
122
+ },
123
+ // 9. 把模板版本的 lint 脚本替换为支持 --files 的 runner
124
+ // 只识别平台模板生成的 `concurrently ...` 形态,用户真正改写过的脚本保持原样
125
+ {
126
+ type: 'patch-script',
127
+ name: 'lint',
128
+ to: 'node ./scripts/lint.js',
129
+ ifStartsWith: 'concurrently ',
130
+ },
131
+ // 10. 把老 `npm run upgrade && ./scripts/dev.sh` 形式迁移到 `./scripts/dev.sh`,
132
+ // 让本地 `npm run dev` 不再前置跑 fullstack-cli sync。新版 dev.sh 按 SANDBOX_ID 是否非空
133
+ // 判分支:沙箱直接 exec dev.js(脚本同步由平台 pod 启动时做过,dev 入口不再 upgrade);
134
+ // 本地走 miaoda app sync 兜底 + dev-local.js。
135
+ {
136
+ type: 'patch-script',
137
+ name: 'dev',
138
+ to: './scripts/dev.sh',
139
+ ifStartsWith: 'npm run upgrade && ',
140
+ },
141
+ // 11. 把 `upgrade` 脚本里裸 `fullstack-cli sync` 改成钉版 npx。
142
+ // 跟 postinstall 同款根因:fullstack-cli 不是用户应用的 dep,新机器无全局命令会 ENOENT。
143
+ {
144
+ type: 'patch-script',
145
+ name: 'upgrade',
146
+ to: `npx -y ${fullstack_cli_pin_1.FULLSTACK_CLI_PIN_SPEC} sync --disable-gen-openapi`,
147
+ ifStartsWith: 'fullstack-cli sync',
148
+ },
149
+ // 11a. 迁移已有 npx 未钉版的 upgrade
150
+ {
151
+ type: 'patch-script',
152
+ name: 'upgrade',
153
+ to: `npx -y ${fullstack_cli_pin_1.FULLSTACK_CLI_PIN_SPEC} sync --disable-gen-openapi`,
154
+ ifStartsWith: 'npx -y @lark-apaas/fullstack-cli sync',
155
+ },
156
+ // ===== miaoda-cli 本地开发新增规则(fullstack-cli sync 不会执行) =====
157
+ // M1. scripts.dev:local —— 本地用户绕过 SANDBOX_ID 分发直接跑本地链路(npm run dev:local)。
158
+ // dev.sh 在 SANDBOX_ID 非空时跑 dev.js(沙箱保活)、否则 exec dev-local.js;显式 dev:local
159
+ // 用于 agent 在本地稳定调试,不受任何 env 影响。
160
+ {
161
+ type: 'add-script',
162
+ name: 'dev:local',
163
+ command: 'node ./scripts/dev-local.js',
164
+ overwrite: false,
165
+ },
166
+ // M2. tsconfig.node.json 合并 watchOptions.preserveWatchOutput —— tsc --watch 默认每次
167
+ // 重编译都会 clear screen + scrollback,本地 nest start --watch 跑下来日志全没。打开
168
+ // preserveWatchOutput 让 watch 模式追加而不是清屏。模板只放需要 merge 的几个字段,
169
+ // 不动用户的 compilerOptions / include / exclude。
170
+ {
171
+ type: 'merge-json',
172
+ from: 'tsconfig.node.json',
173
+ to: 'tsconfig.node.json',
174
+ },
175
+ ],
176
+ };
177
+ exports.default = exports.SYNC_CONFIG;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ /**
3
+ * SyncRule 类型定义 —— miaoda app sync 的核心抽象。
4
+ *
5
+ * 完整搬自 @lark-apaas/fullstack-cli(packages/tools/fullstack-cli/src/config/sync.ts),
6
+ * 保持 rule shape 与 fullstack-cli 严格一致。沙箱里跑的 `npm run upgrade →
7
+ * npx fullstack-cli sync` 走 fullstack-cli 一侧;本地 `miaoda app sync` 走 miaoda-cli
8
+ * 一侧。两边的 apply 行为对齐,rule 配置则按 stack 拆开(每个 stack 一份
9
+ * upgrade/templates/<stack>/sync.config.ts)。
10
+ *
11
+ * 不同于 fullstack-cli 把 rule 全局 hardcode 在包内:miaoda-cli 把 rule 拆到 stack 级别
12
+ * 文件,便于 nestjs-react-fullstack / vite-react / html / design-stack 各自维护。
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.installDependencies = installDependencies;
7
+ exports.resolveNpmInstallRegistry = resolveNpmInstallRegistry;
7
8
  const node_child_process_1 = require("node:child_process");
8
9
  const node_crypto_1 = __importDefault(require("node:crypto"));
9
10
  const node_fs_1 = __importDefault(require("node:fs"));
@@ -44,18 +45,31 @@ function installDependencies(opts) {
44
45
  return { installed: true, source: 'cache', hash, cacheZip };
45
46
  }
46
47
  (0, logger_1.log)('init', 'Cache zip extracted but node_modules/ missing or empty; falling back to npm install');
47
- runNpmInstall(opts.targetDir, childStdio);
48
- return { installed: true, source: 'npm', hash };
48
+ return runInstallSoft(opts.targetDir, childStdio, hash);
49
49
  }
50
50
  (0, logger_1.log)('init', `Cache miss for ${hash}.zip, falling back to npm install`);
51
- runNpmInstall(opts.targetDir, childStdio);
52
- return { installed: true, source: 'npm', hash };
51
+ return runInstallSoft(opts.targetDir, childStdio, hash);
53
52
  }
54
53
  if (cacheDir && !hasPkg) {
55
54
  (0, logger_1.log)('init', `${DEP_CACHE_ENV} set but no package.json in target; running npm install`);
56
55
  }
57
- runNpmInstall(opts.targetDir, childStdio);
58
- return { installed: true, source: 'npm' };
56
+ return runInstallSoft(opts.targetDir, childStdio);
57
+ }
58
+ /**
59
+ * 跑 npm install;失败不抛错,返回 source='failed' 让上层 init handler 继续走 writeSparkMeta
60
+ * 等后续步骤(避免 install 挂了 .spark/meta.json 没写出来,下次 init 看到半渲染状态又得重跑全套)。
61
+ * 失败信息通过 stderr 直出 + emit data.installError 透传给用户。
62
+ */
63
+ function runInstallSoft(targetDir, stdio, hash) {
64
+ try {
65
+ runNpmInstall(targetDir, stdio);
66
+ return { installed: true, source: 'npm', hash };
67
+ }
68
+ catch (err) {
69
+ const msg = err instanceof Error ? err.message : String(err);
70
+ (0, logger_1.log)('init', `⚠ npm install failed (continuing init): ${msg}`);
71
+ return { installed: false, source: 'failed', hash, error: msg };
72
+ }
59
73
  }
60
74
  function nodeModulesUsable(targetDir) {
61
75
  const nm = node_path_1.default.join(targetDir, 'node_modules');
@@ -89,13 +103,30 @@ function extractZip(zipPath, targetDir, stdio) {
89
103
  });
90
104
  }
91
105
  }
106
+ /**
107
+ * 显式钉死 npmmirror。原因:
108
+ * - user 全局 `~/.npmrc` 千奇百怪(bnpm / npmjs / 自定义代理),npm config 层叠下
109
+ * CLI flag 优先级最高,写在这里能盖掉所有上游配置,行为可预测。
110
+ * - 模板 `_npmrc → .npmrc` 虽然项目级 registry 写得是 npmmirror,但只在 cwd 含 .npmrc
111
+ * 时才生效,env / CLI 层先到先得,落到这里钉死最稳。
112
+ * - 字节内网 DNS 会把 npmmirror 域名透明指向公司镜像,外网就是公网阿里源;同一行配置
113
+ * 在两套环境都拉得到 @lark-apaas/* 私包(内网经公司镜像,外网经阿里同步过去的副本)。
114
+ * - 留 `MIAODA_NPM_REGISTRY` env 应急覆盖。
115
+ *
116
+ * `miaoda app sync` 也用同款规则,通过 resolveNpmInstallRegistry() 共享。
117
+ */
118
+ const NPM_INSTALL_REGISTRY_DEFAULT = 'https://registry.npmmirror.com/';
119
+ /**
120
+ * 解析 miaoda 内部 npm install 命令使用的 registry。init / sync 共用。
121
+ * 优先级:`MIAODA_NPM_REGISTRY` env > `NPM_INSTALL_REGISTRY_DEFAULT`。
122
+ */
123
+ function resolveNpmInstallRegistry() {
124
+ return process.env.MIAODA_NPM_REGISTRY ?? NPM_INSTALL_REGISTRY_DEFAULT;
125
+ }
92
126
  function runNpmInstall(targetDir, stdio) {
93
127
  (0, logger_1.log)('init', `npm install in ${targetDir}...`);
94
- try {
95
- (0, node_child_process_1.execFileSync)('npm', ['install', '--no-audit', '--no-fund'], { cwd: targetDir, stdio });
96
- }
97
- catch (err) {
98
- const msg = err instanceof Error ? err.message : String(err);
99
- throw new error_1.AppError('NPM_INSTALL_FAILED', `npm install 失败: ${msg}`);
100
- }
128
+ (0, node_child_process_1.execFileSync)('npm', ['install', '--no-audit', '--no-fund', '--registry', resolveNpmInstallRegistry()], {
129
+ cwd: targetDir,
130
+ stdio,
131
+ });
101
132
  }
@@ -3,7 +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.SUPPORTED_STACKS = exports.TEMPLATE_PACKAGE_BY_STACK = void 0;
6
+ exports.SUPPORTED_STACKS = exports.TEMPLATE_PINNED_VERSION_BY_STACK = exports.TEMPLATE_PACKAGE_BY_STACK = void 0;
7
7
  exports.renderTemplate = renderTemplate;
8
8
  const node_fs_1 = __importDefault(require("node:fs"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
@@ -14,13 +14,25 @@ const logger_1 = require("../../../utils/logger");
14
14
  exports.TEMPLATE_PACKAGE_BY_STACK = {
15
15
  'vite-react': '@lark-apaas/coding-template-vite-react',
16
16
  html: '@lark-apaas/coding-template-html',
17
+ 'nestjs-react-fullstack': '@lark-apaas/coding-template-nestjs-react-fullstack',
17
18
  };
19
+ /**
20
+ * 短名 → template 包钉版表。renderTemplate 默认按这张表取版本号,表外 stack 跟 npm latest。
21
+ *
22
+ * 上线起所有 stack 跟随 latest dist-tag(保留 export + 表结构是为了紧急止血时可以一行钉版
23
+ * 不动 caller,例如 `'nestjs-react-fullstack': '0.1.0-alpha.6'`)。
24
+ * 用户显式传 `--conf '{"version": "..."}'` 仍 override 此默认值。
25
+ */
26
+ exports.TEMPLATE_PINNED_VERSION_BY_STACK = {};
18
27
  exports.SUPPORTED_STACKS = Object.keys(exports.TEMPLATE_PACKAGE_BY_STACK);
19
- // template/ 内以下划线开头的占位文件名在渲染时改名(npm publish 会把 .gitignore / .npmrc 吞掉,
20
- // 模板里用 _gitignore / _npmrc 提交,渲染时再改回)。
28
+ // template/ 内以下划线开头的占位文件名在渲染时改名。
29
+ // 原因:
30
+ // 1. npm pack 强制剥的:.npmrc(防 auth token 泄露)
31
+ // 2. 沙箱平台特殊处理的:.gitignore(避免与平台仓库 .gitignore 冲突)
32
+ // .env 通过 negation pattern 例外直接 ship;.env.local 完全由 lark-cli +env-pull
33
+ // 运行时下发,模板里不再放占位文件。
21
34
  const RENAME_FILES = {
22
35
  _gitignore: '.gitignore',
23
- '_env.local': '.env.local',
24
36
  _npmrc: '.npmrc',
25
37
  };
26
38
  function renderTemplate(opts) {
@@ -30,8 +42,13 @@ function renderTemplate(opts) {
30
42
  next_actions: [`可用 stack:${exports.SUPPORTED_STACKS.join(', ')}`],
31
43
  });
32
44
  }
33
- (0, logger_1.log)('init', `Fetching ${packageName}@${opts.version ?? 'latest'}...`);
34
- const fetched = (0, npm_pack_1.fetchNpmPackage)({ packageName, version: opts.version });
45
+ // 缺省走 latest dist-tag;表里有钉版(紧急止血时填)则用钉版;--conf.version override。
46
+ const pinned = Object.prototype.hasOwnProperty.call(exports.TEMPLATE_PINNED_VERSION_BY_STACK, opts.stack)
47
+ ? exports.TEMPLATE_PINNED_VERSION_BY_STACK[opts.stack]
48
+ : undefined;
49
+ const effectiveVersion = opts.version ?? pinned ?? 'latest';
50
+ (0, logger_1.log)('init', `Fetching ${packageName}@${effectiveVersion}...`);
51
+ const fetched = (0, npm_pack_1.fetchNpmPackage)({ packageName, version: effectiveVersion });
35
52
  try {
36
53
  const templateDir = node_path_1.default.join(fetched.extractDir, 'template');
37
54
  if (!node_fs_1.default.existsSync(templateDir)) {
@@ -10,8 +10,13 @@ const logger_1 = require("../../../../utils/logger");
10
10
  * 创建本地发布单(加锁;不挂 pipeline)。
11
11
  * 返回的 releaseId 必须由 finalizeLocalRelease 翻为终态,否则发布单一直挂着。
12
12
  */
13
- async function createLocalRelease(appId, version) {
14
- return (0, index_1.createLocalRelease)({ appID: appId, version });
13
+ async function createLocalRelease(appId, version, extra) {
14
+ return (0, index_1.createLocalRelease)({
15
+ appID: appId,
16
+ version,
17
+ checkPointVersion: extra?.checkPointVersion,
18
+ commitID: extra?.commitID,
19
+ });
15
20
  }
16
21
  /**
17
22
  * 把本地发布单翻为终态。Finished / Failed 由 pipeline 视上下游结果决定。
@@ -43,7 +43,10 @@ async function localBuildLocalPublishPipeline(opts) {
43
43
  });
44
44
  }
45
45
  (0, logger_1.log)('deploy', 'Creating local release...');
46
- const release = await (0, index_1.createLocalRelease)(ctx.appId, pre.version);
46
+ const release = await (0, index_1.createLocalRelease)(ctx.appId, pre.version, {
47
+ checkPointVersion: opts.checkPointVersion,
48
+ commitID: opts.commitID,
49
+ });
47
50
  try {
48
51
  (0, logger_1.log)('deploy', 'Uploading artifacts...');
49
52
  await (0, index_1.uploadArtifacts)({ projectDir: ctx.projectDir, appId: ctx.appId, data });
@@ -9,67 +9,146 @@ const node_path_1 = __importDefault(require("node:path"));
9
9
  const npm_pack_1 = require("../utils/npm-pack");
10
10
  const logger_1 = require("../utils/logger");
11
11
  const STEERING_PACKAGE = '@lark-apaas/coding-steering';
12
+ function inferMode() {
13
+ // process.env.SANDBOX_ID 非空 → 沙箱;其他(undefined / 空串)→ 本地
14
+ return process.env.SANDBOX_ID ? 'sandbox' : 'local';
15
+ }
12
16
  /**
13
- * 把 coding-steering 包内 _common/skills + <stack>/skills 同步到 <targetDir>/.agent/steering/skills/,
14
- * 同名 stack 子目录覆盖 _commontech.md 存在时也同步到 .agent/steering/tech.md。
17
+ * 把 coding-steering 包内 <stack>/skills_common + 专有层(skills skills_local)同步到
18
+ * user app
19
+ *
20
+ * 包内目录约定(每个 stack 三层):
21
+ * <stack>/skills_common → 沙箱 + 本地都下发的共有层
22
+ * <stack>/skills → 沙箱专有(沙箱端 update-skills.sh / SANDBOX_ID 非空时拿)
23
+ * <stack>/skills_local → 本地专有(本地 dev / agent 环境拿)
24
+ *
25
+ * 下发组合(mode):
26
+ * mode='sandbox' → skills_common + skills
27
+ * mode='local' → skills_common + skills_local
28
+ *
29
+ * 目标路径形态(outputLayout):
30
+ * 'nested' (默认,向后兼容):拷到 .agent/skills/steering/<stack>/skills/,tech.md 同目录
31
+ * 'flat':拷到 .agents/skills/(平铺),tech.md → .agents/tech.md,并创建 .claude/skills
32
+ * 软链指向 ../.agents/skills 让 Claude Code 识别同一份
33
+ *
34
+ * 同名 skill 覆盖顺序:common < 专有(专有覆盖 common 同名)。mode 缺省按 SANDBOX_ID env
35
+ * 推断(沙箱平台运行时注入应用所属沙箱 ID;空 / undefined 则视为本地)。
15
36
  *
16
37
  * 纯本地 atom(pull npm package + 拷贝文件),不属于任何 cli 域;放在 utils 让
17
38
  * app init handler 与独立 skills sync handler 都能复用。
18
39
  */
19
40
  function syncCodingSteering(opts) {
20
41
  const logPrefix = opts.logPrefix ?? 'skills';
21
- (0, logger_1.log)(logPrefix, `Fetching ${STEERING_PACKAGE}@${opts.version ?? 'latest'}...`);
42
+ const effectiveVersion = opts.version ?? 'latest';
43
+ const mode = opts.mode ?? inferMode();
44
+ const layout = opts.outputLayout ?? 'nested';
45
+ (0, logger_1.log)(logPrefix, `Fetching ${STEERING_PACKAGE}@${effectiveVersion} (mode=${mode}, layout=${layout})...`);
22
46
  const fetched = (0, npm_pack_1.fetchNpmPackage)({
23
47
  packageName: STEERING_PACKAGE,
24
- version: opts.version,
48
+ version: effectiveVersion,
25
49
  });
26
50
  try {
27
51
  const steeringRoot = node_path_1.default.join(fetched.extractDir, 'steering');
28
52
  const stackDir = node_path_1.default.join(steeringRoot, opts.stack);
29
- const commonSkillsDir = node_path_1.default.join(steeringRoot, '_common', 'skills');
30
- // steering 落到 .agent/skills/steering/<stack>/。.agent/skills/ agent skills 的
31
- // 公共容器,steering 是其中一个 producer,<stack> 再做命名空间隔离。内部仍保留
32
- // 包内 steering/<stack>/skills/ 的结构,便于按"包内子目录 本地子目录"一一对应排查。
33
- const dstRoot = node_path_1.default.join(opts.targetDir, '.agent', 'skills', 'steering', opts.stack);
34
- const dstSkillsDir = node_path_1.default.join(dstRoot, 'skills');
53
+ // 目标路径按 layout 切:
54
+ // nested .agent/skills/steering/<stack>/{skills, tech.md}(老链路、stack 命名空间)
55
+ // flat → .agents/{skills, tech.md}(平铺,无 stack 命名空间,跟 .claude/skills 软链对齐)
56
+ const { dstSkillsDir, dstTechPath } = layout === 'flat'
57
+ ? {
58
+ dstSkillsDir: node_path_1.default.join(opts.targetDir, '.agents', 'skills'),
59
+ dstTechPath: node_path_1.default.join(opts.targetDir, '.agents', 'tech.md'),
60
+ }
61
+ : {
62
+ dstSkillsDir: node_path_1.default.join(opts.targetDir, '.agent', 'skills', 'steering', opts.stack, 'skills'),
63
+ dstTechPath: node_path_1.default.join(opts.targetDir, '.agent', 'skills', 'steering', opts.stack, 'tech.md'),
64
+ };
35
65
  node_fs_1.default.mkdirSync(dstSkillsDir, { recursive: true });
36
66
  let techSynced = false;
37
67
  const techSrc = node_path_1.default.join(stackDir, 'tech.md');
38
68
  if (node_fs_1.default.existsSync(techSrc)) {
39
- node_fs_1.default.copyFileSync(techSrc, node_path_1.default.join(dstRoot, 'tech.md'));
69
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(dstTechPath), { recursive: true });
70
+ node_fs_1.default.copyFileSync(techSrc, dstTechPath);
40
71
  techSynced = true;
41
72
  }
42
73
  const synced = [];
43
- // 先拷 _common/skills/,再拷 <stack>/skills/。stack 同名覆盖 common
44
- if (node_fs_1.default.existsSync(commonSkillsDir)) {
45
- for (const name of node_fs_1.default.readdirSync(commonSkillsDir)) {
46
- const src = node_path_1.default.join(commonSkillsDir, name);
47
- if (!node_fs_1.default.statSync(src).isDirectory())
48
- continue;
49
- copyDir(src, node_path_1.default.join(dstSkillsDir, name));
50
- synced.push(`_common/skills/${name}`);
51
- }
52
- }
53
- const stackSkillsDir = node_path_1.default.join(stackDir, 'skills');
54
- if (node_fs_1.default.existsSync(stackSkillsDir)) {
55
- for (const name of node_fs_1.default.readdirSync(stackSkillsDir)) {
56
- const src = node_path_1.default.join(stackSkillsDir, name);
74
+ // 顺序拷贝,后者覆盖前者:skills_common 铺底,专有层(skills skills_local)覆盖同名
75
+ const exclusiveLayer = mode === 'sandbox' ? 'skills' : 'skills_local';
76
+ const layers = [
77
+ { dir: node_path_1.default.join(stackDir, 'skills_common'), label: `${opts.stack}/skills_common` },
78
+ { dir: node_path_1.default.join(stackDir, exclusiveLayer), label: `${opts.stack}/${exclusiveLayer}` },
79
+ ];
80
+ for (const layer of layers) {
81
+ if (!node_fs_1.default.existsSync(layer.dir))
82
+ continue;
83
+ for (const name of node_fs_1.default.readdirSync(layer.dir)) {
84
+ const src = node_path_1.default.join(layer.dir, name);
57
85
  if (!node_fs_1.default.statSync(src).isDirectory())
58
86
  continue;
59
87
  const dst = node_path_1.default.join(dstSkillsDir, name);
60
88
  if (node_fs_1.default.existsSync(dst))
61
89
  node_fs_1.default.rmSync(dst, { recursive: true, force: true });
62
90
  copyDir(src, dst);
63
- synced.push(`${opts.stack}/skills/${name}`);
91
+ synced.push(`${layer.label}/${name}`);
64
92
  }
65
93
  }
66
- (0, logger_1.log)(logPrefix, `Synced ${String(synced.length)} skill(s), tech.md ${techSynced ? 'yes' : 'no'}`);
67
- return { version: fetched.version, syncedSkills: synced, techSynced };
94
+ let claudeSkillsLink;
95
+ if (layout === 'flat') {
96
+ claudeSkillsLink = ensureClaudeSkillsSymlink(opts.targetDir, logPrefix);
97
+ }
98
+ (0, logger_1.log)(logPrefix, `Synced ${String(synced.length)} skill(s), tech.md ${techSynced ? 'yes' : 'no'} (mode=${mode}, layout=${layout})`);
99
+ return {
100
+ version: fetched.version,
101
+ syncedSkills: synced,
102
+ techSynced,
103
+ ...(claudeSkillsLink !== undefined ? { claudeSkillsLink } : {}),
104
+ };
68
105
  }
69
106
  finally {
70
107
  fetched.cleanup();
71
108
  }
72
109
  }
110
+ /**
111
+ * flat layout 下创建 `<targetDir>/.claude/skills` 软链 → `../.agents/skills`,让 Claude Code
112
+ * 默认的 skills 加载路径跟 user app 的 .agents/skills 是同一份内容。
113
+ *
114
+ * 用相对路径软链(不是 absolute)—— user app 移动目录 / clone 到别处不会断链。
115
+ *
116
+ * 处理 4 种已有状态:
117
+ * - 已是软链且指向对的:noop
118
+ * - 已是软链但指错地方:删旧的,建新的(updated)
119
+ * - 已是普通目录 / 文件:不覆盖,警告并跳过(conflict)—— 用户可能手动放了别的 skills
120
+ * - 不存在:建新的(created)
121
+ */
122
+ function ensureClaudeSkillsSymlink(targetDir, logPrefix) {
123
+ const linkPath = node_path_1.default.join(targetDir, '.claude', 'skills');
124
+ const linkTarget = node_path_1.default.join('..', '.agents', 'skills');
125
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(linkPath), { recursive: true });
126
+ let existing = null;
127
+ try {
128
+ existing = node_fs_1.default.lstatSync(linkPath);
129
+ }
130
+ catch (err) {
131
+ if (err.code !== 'ENOENT')
132
+ throw err;
133
+ }
134
+ if (existing === null) {
135
+ node_fs_1.default.symlinkSync(linkTarget, linkPath, 'dir');
136
+ (0, logger_1.log)(logPrefix, ` ✓ .claude/skills → ${linkTarget} (symlink created)`);
137
+ return 'created';
138
+ }
139
+ if (existing.isSymbolicLink()) {
140
+ const current = node_fs_1.default.readlinkSync(linkPath);
141
+ if (current === linkTarget) {
142
+ return 'noop';
143
+ }
144
+ node_fs_1.default.unlinkSync(linkPath);
145
+ node_fs_1.default.symlinkSync(linkTarget, linkPath, 'dir');
146
+ (0, logger_1.log)(logPrefix, ` ✓ .claude/skills → ${linkTarget} (symlink updated, was → ${current})`);
147
+ return 'updated';
148
+ }
149
+ (0, logger_1.log)(logPrefix, ` ⚠ .claude/skills 已是普通目录/文件,跳过软链创建`);
150
+ return 'conflict';
151
+ }
73
152
  function copyDir(src, dest) {
74
153
  node_fs_1.default.mkdirSync(dest, { recursive: true });
75
154
  for (const entry of node_fs_1.default.readdirSync(src, { withFileTypes: true })) {
@@ -0,0 +1,45 @@
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.removeLineFromFile = removeLineFromFile;
7
+ exports.removeLineFromContent = removeLineFromContent;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const logger_1 = require("./logger");
11
+ /**
12
+ * 从文件中移除匹配的行。
13
+ * 搬自 @lark-apaas/fullstack-cli/src/utils/file-ops.ts,行为完全对齐:精确匹配(trim 后)。
14
+ *
15
+ * @returns 是否真的移除过内容
16
+ */
17
+ function removeLineFromFile(filePath, pattern, logPrefix = 'sync') {
18
+ if (!node_fs_1.default.existsSync(filePath)) {
19
+ (0, logger_1.log)(logPrefix, ` ○ ${node_path_1.default.basename(filePath)} (not found)`);
20
+ return false;
21
+ }
22
+ const content = node_fs_1.default.readFileSync(filePath, 'utf-8');
23
+ const trimmed = pattern.trim();
24
+ const lines = content.split('\n');
25
+ const filtered = lines.filter((line) => line.trim() !== trimmed);
26
+ if (filtered.length === lines.length) {
27
+ (0, logger_1.log)(logPrefix, ` ○ ${node_path_1.default.basename(filePath)} (pattern not found: ${pattern})`);
28
+ return false;
29
+ }
30
+ node_fs_1.default.writeFileSync(filePath, filtered.join('\n'));
31
+ (0, logger_1.log)(logPrefix, ` ✓ ${node_path_1.default.basename(filePath)} (removed: ${pattern})`);
32
+ return true;
33
+ }
34
+ /**
35
+ * 纯函数版本:便于单测,不读写文件。
36
+ */
37
+ function removeLineFromContent(content, pattern) {
38
+ const trimmed = pattern.trim();
39
+ const lines = content.split('\n');
40
+ const filtered = lines.filter((line) => line.trim() !== trimmed);
41
+ return {
42
+ result: filtered.join('\n'),
43
+ changed: filtered.length !== lines.length,
44
+ };
45
+ }
@@ -0,0 +1,55 @@
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.activateGitHooks = activateGitHooks;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const node_child_process_1 = require("node:child_process");
10
+ const logger_1 = require("../utils/logger");
11
+ /**
12
+ * 把 `.githooks/` 注册为本仓库的 git hooks 目录,让 pre-commit 等 hook 立即生效。
13
+ *
14
+ * 三件事,都先检查再写(幂等):
15
+ * 1. `.git` 存在(非 git 仓库直接跳)
16
+ * 2. `.githooks/pre-commit` 存在(没 hook 文件直接跳,且不写 git config 防止误指空目录)
17
+ * 3. `core.hooksPath` 是 `.githooks`(已经是就短路)
18
+ *
19
+ * 镜像 fullstack-cli `src/commands/sync/activate-hooks.ts`,逻辑保持一致;本地用户
20
+ * 通过 `miaoda app upgrade` 走的这一份,沙箱用户走 `npm run upgrade`(fullstack-cli sync)
21
+ * 那一份,两条通道结果应等价。
22
+ */
23
+ function activateGitHooks(targetDir) {
24
+ if (!node_fs_1.default.existsSync(node_path_1.default.join(targetDir, '.git'))) {
25
+ return { action: 'skipped-no-git' };
26
+ }
27
+ const hookFile = node_path_1.default.join(targetDir, '.githooks', 'pre-commit');
28
+ if (!node_fs_1.default.existsSync(hookFile)) {
29
+ return { action: 'skipped-no-hook-file' };
30
+ }
31
+ // copyFileSync 不保留 mode,确保 +x(重要:没 +x 时 git 不会执行 hook)
32
+ const currentMode = node_fs_1.default.statSync(hookFile).mode & 0o777;
33
+ if ((currentMode & 0o111) !== 0o111) {
34
+ node_fs_1.default.chmodSync(hookFile, 0o755);
35
+ }
36
+ const probe = (0, node_child_process_1.spawnSync)('git', ['config', '--get', 'core.hooksPath'], {
37
+ cwd: targetDir,
38
+ stdio: ['ignore', 'pipe', 'ignore'],
39
+ });
40
+ const currentHooksPath = probe.stdout.toString().trim();
41
+ if (currentHooksPath === '.githooks') {
42
+ return { action: 'already-active' };
43
+ }
44
+ const res = (0, node_child_process_1.spawnSync)('git', ['config', 'core.hooksPath', '.githooks'], {
45
+ cwd: targetDir,
46
+ stdio: ['ignore', 'ignore', 'pipe'],
47
+ });
48
+ if (res.status !== 0) {
49
+ const stderr = res.stderr.toString().trim();
50
+ (0, logger_1.log)('githooks', `⚠️ failed to set core.hooksPath: ${stderr}`);
51
+ return { action: 'skipped-no-git' };
52
+ }
53
+ (0, logger_1.log)('githooks', `activated: core.hooksPath = .githooks`);
54
+ return { action: 'activated' };
55
+ }