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

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 (42) hide show
  1. package/dist/cli/commands/app/index.js +67 -7
  2. package/dist/cli/commands/index.js +36 -1
  3. package/dist/cli/commands/skills/index.js +18 -2
  4. package/dist/cli/handlers/app/index.js +4 -1
  5. package/dist/cli/handlers/app/init.js +53 -9
  6. package/dist/cli/handlers/app/sync.js +220 -0
  7. package/dist/cli/handlers/skills/sync.js +15 -4
  8. package/dist/config/fullstack-cli-pin.js +13 -0
  9. package/dist/config/sync-configs/design-stack.js +98 -0
  10. package/dist/config/sync-configs/index.js +62 -0
  11. package/dist/config/sync-configs/nestjs-react-fullstack.js +177 -0
  12. package/dist/config/sync.js +14 -0
  13. package/dist/services/app/init/install.js +35 -13
  14. package/dist/services/app/init/template.js +23 -6
  15. package/dist/utils/coding-steering.js +107 -28
  16. package/dist/utils/file-ops.js +45 -0
  17. package/dist/utils/githooks.js +55 -0
  18. package/dist/utils/merge-json.js +63 -0
  19. package/dist/utils/platform-sync.js +160 -0
  20. package/dist/utils/sync-rule.js +295 -0
  21. package/package.json +5 -3
  22. package/upgrade/templates/README.md +34 -0
  23. package/upgrade/templates/design-stack/templates/.githooks/pre-commit +4 -0
  24. package/upgrade/templates/design-stack/templates/scripts/dev-local.js +83 -0
  25. package/upgrade/templates/design-stack/templates/scripts/dev.sh +25 -0
  26. package/upgrade/templates/design-stack/templates/scripts/hooks/run-precommit.js +37 -0
  27. package/upgrade/templates/nestjs-react-fullstack/templates/.githooks/pre-commit +4 -0
  28. package/upgrade/templates/nestjs-react-fullstack/templates/.gitignore.append +8 -0
  29. package/upgrade/templates/nestjs-react-fullstack/templates/.spark_project +16 -0
  30. package/upgrade/templates/nestjs-react-fullstack/templates/drizzle.config.ts +55 -0
  31. package/upgrade/templates/nestjs-react-fullstack/templates/helper/gen-openapi.ts +34 -0
  32. package/upgrade/templates/nestjs-react-fullstack/templates/nest-cli.json +25 -0
  33. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/build.sh +207 -0
  34. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/dev-local.js +111 -0
  35. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/dev.js +295 -0
  36. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/dev.sh +25 -0
  37. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/hooks/run-precommit.js +37 -0
  38. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/lint.js +150 -0
  39. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/prune-smart.js +330 -0
  40. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/run.sh +8 -0
  41. package/upgrade/templates/nestjs-react-fullstack/templates/server/global.d.ts +19 -0
  42. package/upgrade/templates/nestjs-react-fullstack/templates/tsconfig.node.json +5 -0
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ /**
3
+ * design-stack 的 sync 规则。
4
+ *
5
+ * design-stack 是 SSR-only 全栈 stack(design scene,MIAODA_APP_TYPE=4),无 nest 后端、
6
+ * 无数据库、无 nestjs-cli。所以规则集是 nestjs-react-fullstack 的子集:只保留
7
+ * scripts/.githooks/ 派生 + 基础 npm scripts,不动 nest-cli.json / drizzle / tsconfig.node.json。
8
+ *
9
+ * dev-local.js 跟 nestjs-react-fullstack 不同(单进程 npm run dev,MIAODA_APP_TYPE=4),
10
+ * 单独维护在 upgrade/templates/design-stack/templates/scripts/dev-local.js。
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.SYNC_CONFIG = void 0;
14
+ const fullstack_cli_pin_1 = require("../../config/fullstack-cli-pin");
15
+ exports.SYNC_CONFIG = {
16
+ sync: [
17
+ // 1. 派生 scripts/(含 design 专属 dev-local.js + 通用 dev.sh / hooks/run-precommit.js)
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
32
+ {
33
+ type: 'add-script',
34
+ name: 'prepare',
35
+ command: 'chmod +x .githooks/pre-commit 2>/dev/null; git config core.hooksPath .githooks 2>/dev/null || true',
36
+ overwrite: false,
37
+ },
38
+ // 1c. scripts.precommit
39
+ {
40
+ type: 'add-script',
41
+ name: 'precommit',
42
+ command: 'node scripts/hooks/run-precommit.js',
43
+ overwrite: false,
44
+ },
45
+ // 2. .gitignore 卫生:移除 package-lock.json + 加 .agent/
46
+ {
47
+ type: 'remove-line',
48
+ to: '.gitignore',
49
+ pattern: 'package-lock.json',
50
+ },
51
+ {
52
+ type: 'add-line',
53
+ to: '.gitignore',
54
+ line: '.agent/',
55
+ },
56
+ // 3. 老 npm scripts 迁移(裸 fullstack-cli → 钉版 npx)。两条 ifStartsWith 分别覆盖
57
+ // "裸 fullstack-cli"(最老形态)和 "npx 未钉版" 形态,统一升到 FULLSTACK_CLI_PIN_SPEC。
58
+ {
59
+ type: 'patch-script',
60
+ name: 'postinstall',
61
+ to: `npx -y ${fullstack_cli_pin_1.FULLSTACK_CLI_PIN_SPEC} action-plugin init`,
62
+ ifStartsWith: 'fullstack-cli action-plugin init',
63
+ },
64
+ {
65
+ type: 'patch-script',
66
+ name: 'postinstall',
67
+ to: `npx -y ${fullstack_cli_pin_1.FULLSTACK_CLI_PIN_SPEC} action-plugin init`,
68
+ ifStartsWith: 'npx -y @lark-apaas/fullstack-cli action-plugin init',
69
+ },
70
+ {
71
+ type: 'patch-script',
72
+ name: 'upgrade',
73
+ to: `npx -y ${fullstack_cli_pin_1.FULLSTACK_CLI_PIN_SPEC} sync --disable-gen-openapi`,
74
+ ifStartsWith: 'fullstack-cli sync',
75
+ },
76
+ {
77
+ type: 'patch-script',
78
+ name: 'upgrade',
79
+ to: `npx -y ${fullstack_cli_pin_1.FULLSTACK_CLI_PIN_SPEC} sync --disable-gen-openapi`,
80
+ ifStartsWith: 'npx -y @lark-apaas/fullstack-cli sync',
81
+ },
82
+ {
83
+ type: 'patch-script',
84
+ name: 'dev',
85
+ to: './scripts/dev.sh',
86
+ ifStartsWith: 'npm run upgrade && ',
87
+ },
88
+ // ===== miaoda-cli 本地开发新增规则 =====
89
+ // M1. scripts.dev:local —— 本地用户绕过 SANDBOX_ID 分发直接跑本地链路(npm run dev:local)。
90
+ {
91
+ type: 'add-script',
92
+ name: 'dev:local',
93
+ command: 'node ./scripts/dev-local.js',
94
+ overwrite: false,
95
+ },
96
+ ],
97
+ };
98
+ exports.default = exports.SYNC_CONFIG;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ /**
3
+ * 按 stack 短名拿到对应的 SyncConfig + 模板源根目录。
4
+ *
5
+ * - 类型化的 SyncConfig 放在 src/config/sync-configs/<stack>.ts(被 tsc 编进 dist 一起发)。
6
+ * - 模板资源文件放在 upgrade/templates/<stack>/templates/(cli 发布时整个目录原样 ship)。
7
+ *
8
+ * handler 拿到 stack 后,先 getSyncConfig 拿 rule 列表,再 getStackTemplatesRoot 拿模板根,
9
+ * 一起喂给 applySyncRules。
10
+ *
11
+ * 新加 stack:
12
+ * 1. src/config/sync-configs/<stack>.ts 里 export default SYNC_CONFIG
13
+ * 2. upgrade/templates/<stack>/templates/ 下放对应资源文件
14
+ * 3. 在 STACK_REGISTRY 里加一条
15
+ */
16
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17
+ return (mod && mod.__esModule) ? mod : { "default": mod };
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.getSyncConfig = getSyncConfig;
21
+ exports.getStackTemplatesRoot = getStackTemplatesRoot;
22
+ const node_fs_1 = __importDefault(require("node:fs"));
23
+ const node_path_1 = __importDefault(require("node:path"));
24
+ const nestjs_react_fullstack_1 = __importDefault(require("./nestjs-react-fullstack"));
25
+ const design_stack_1 = __importDefault(require("./design-stack"));
26
+ /**
27
+ * 已纳入新 sync 机制的 stack 注册表。未在表里的 stack(如老 vite-react / html)走旧的
28
+ * `upgrade/templates/<stack>/{files,patches}` 机制,由 sync handler 兜底。
29
+ */
30
+ const STACK_REGISTRY = {
31
+ 'nestjs-react-fullstack': nestjs_react_fullstack_1.default,
32
+ 'design-stack': design_stack_1.default,
33
+ };
34
+ /** 返回该 stack 的 SyncConfig;表里没有时返回 null。 */
35
+ function getSyncConfig(stack) {
36
+ return STACK_REGISTRY[stack] ?? null;
37
+ }
38
+ /**
39
+ * 计算 stack 模板资源根目录(绝对路径)。
40
+ *
41
+ * cli 发布时 upgrade/ 目录整个 ship,编译产物在 dist/ 下若干层。从当前模块向上回溯找含
42
+ * upgrade/ + package.json 的目录就是 cli 根。
43
+ *
44
+ * 跟 utils/platform-sync.ts 同款 pattern;保持对齐避免两边漂移。
45
+ */
46
+ function getStackTemplatesRoot(stack) {
47
+ return node_path_1.default.join(findCliRoot(), 'upgrade', 'templates', stack, 'templates');
48
+ }
49
+ let cliRootCache = null;
50
+ function findCliRoot() {
51
+ if (cliRootCache !== null)
52
+ return cliRootCache;
53
+ let dir = __dirname;
54
+ while (dir !== node_path_1.default.dirname(dir)) {
55
+ if (node_fs_1.default.existsSync(node_path_1.default.join(dir, 'upgrade')) && node_fs_1.default.existsSync(node_path_1.default.join(dir, 'package.json'))) {
56
+ cliRootCache = dir;
57
+ return dir;
58
+ }
59
+ dir = node_path_1.default.dirname(dir);
60
+ }
61
+ throw new Error(`miaoda-cli root (containing upgrade/) not found from ${__dirname}`);
62
+ }
@@ -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 });
@@ -44,18 +44,31 @@ function installDependencies(opts) {
44
44
  return { installed: true, source: 'cache', hash, cacheZip };
45
45
  }
46
46
  (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 };
47
+ return runInstallSoft(opts.targetDir, childStdio, hash);
49
48
  }
50
49
  (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 };
50
+ return runInstallSoft(opts.targetDir, childStdio, hash);
53
51
  }
54
52
  if (cacheDir && !hasPkg) {
55
53
  (0, logger_1.log)('init', `${DEP_CACHE_ENV} set but no package.json in target; running npm install`);
56
54
  }
57
- runNpmInstall(opts.targetDir, childStdio);
58
- return { installed: true, source: 'npm' };
55
+ return runInstallSoft(opts.targetDir, childStdio);
56
+ }
57
+ /**
58
+ * 跑 npm install;失败不抛错,返回 source='failed' 让上层 init handler 继续走 writeSparkMeta
59
+ * 等后续步骤(避免 install 挂了 .spark/meta.json 没写出来,下次 init 看到半渲染状态又得重跑全套)。
60
+ * 失败信息通过 stderr 直出 + emit data.installError 透传给用户。
61
+ */
62
+ function runInstallSoft(targetDir, stdio, hash) {
63
+ try {
64
+ runNpmInstall(targetDir, stdio);
65
+ return { installed: true, source: 'npm', hash };
66
+ }
67
+ catch (err) {
68
+ const msg = err instanceof Error ? err.message : String(err);
69
+ (0, logger_1.log)('init', `⚠ npm install failed (continuing init): ${msg}`);
70
+ return { installed: false, source: 'failed', hash, error: msg };
71
+ }
59
72
  }
60
73
  function nodeModulesUsable(targetDir) {
61
74
  const nm = node_path_1.default.join(targetDir, 'node_modules');
@@ -89,13 +102,22 @@ function extractZip(zipPath, targetDir, stdio) {
89
102
  });
90
103
  }
91
104
  }
105
+ /**
106
+ * 显式钉死 npmmirror。原因:
107
+ * - user 全局 `~/.npmrc` 千奇百怪(bnpm / npmjs / 自定义代理),npm config 层叠下
108
+ * CLI flag 优先级最高,写在这里能盖掉所有上游配置,行为可预测。
109
+ * - 模板 `_npmrc → .npmrc` 虽然项目级 registry 写得是 npmmirror,但只在 cwd 含 .npmrc
110
+ * 时才生效,env / CLI 层先到先得,落到这里钉死最稳。
111
+ * - 字节内网 DNS 会把 npmmirror 域名透明指向公司镜像,外网就是公网阿里源;同一行配置
112
+ * 在两套环境都拉得到 @lark-apaas/* 私包(内网经公司镜像,外网经阿里同步过去的副本)。
113
+ * - 留 `MIAODA_NPM_REGISTRY` env 应急覆盖。
114
+ */
115
+ const NPM_INSTALL_REGISTRY_DEFAULT = 'https://registry.npmmirror.com/';
92
116
  function runNpmInstall(targetDir, stdio) {
93
117
  (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
- }
118
+ const registry = process.env.MIAODA_NPM_REGISTRY ?? NPM_INSTALL_REGISTRY_DEFAULT;
119
+ (0, node_child_process_1.execFileSync)('npm', ['install', '--no-audit', '--no-fund', '--registry', registry], {
120
+ cwd: targetDir,
121
+ stdio,
122
+ });
101
123
  }
@@ -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)) {
@@ -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 })) {