@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,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
+ }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ /**
3
+ * Deep merge JSON 对象,支持按 key 去重合并数组。
4
+ *
5
+ * 搬自 @lark-apaas/fullstack-cli/src/utils/merge-json.ts,行为完全对齐:
6
+ * - scalar(string / number / boolean / null):template 覆盖
7
+ * - object:递归 merge(template 字段覆盖,user 多出的字段保留)
8
+ * - 在 arrayMerge 配置里的 array:按指定 key 去重合并(template item 覆盖同 key 的 user item,
9
+ * user 多出的 item 保留)
10
+ * - 不在 arrayMerge 配置里的 array:template 覆盖
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.deepMergeJson = deepMergeJson;
14
+ function isPlainObject(value) {
15
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
16
+ }
17
+ function mergeArrayByKey(userArr, templateArr, key) {
18
+ const result = [...userArr];
19
+ for (const templateItem of templateArr) {
20
+ if (!isPlainObject(templateItem))
21
+ continue;
22
+ const templateKey = templateItem[key];
23
+ const existingIndex = result.findIndex((item) => isPlainObject(item) && item[key] === templateKey);
24
+ if (existingIndex >= 0) {
25
+ result[existingIndex] = templateItem;
26
+ }
27
+ else {
28
+ result.push(templateItem);
29
+ }
30
+ }
31
+ return result;
32
+ }
33
+ function deepMergeJson(user, template, arrayMerge = {}, currentPath = '') {
34
+ const result = { ...user };
35
+ for (const key of Object.keys(template)) {
36
+ const fullPath = currentPath ? `${currentPath}.${key}` : key;
37
+ const templateValue = template[key];
38
+ const userValue = user[key];
39
+ if (Array.isArray(templateValue)) {
40
+ const cfg = Object.prototype.hasOwnProperty.call(arrayMerge, fullPath)
41
+ ? arrayMerge[fullPath]
42
+ : undefined;
43
+ if (cfg !== undefined && Array.isArray(userValue)) {
44
+ result[key] = mergeArrayByKey(userValue, templateValue, cfg.key);
45
+ }
46
+ else {
47
+ result[key] = templateValue;
48
+ }
49
+ }
50
+ else if (isPlainObject(templateValue)) {
51
+ if (isPlainObject(userValue)) {
52
+ result[key] = deepMergeJson(userValue, templateValue, arrayMerge, fullPath);
53
+ }
54
+ else {
55
+ result[key] = templateValue;
56
+ }
57
+ }
58
+ else {
59
+ result[key] = templateValue;
60
+ }
61
+ }
62
+ return result;
63
+ }
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.syncPlatformControlled = syncPlatformControlled;
40
+ const node_fs_1 = __importDefault(require("node:fs"));
41
+ const node_path_1 = __importDefault(require("node:path"));
42
+ const jsonc = __importStar(require("jsonc-parser"));
43
+ const logger_1 = require("../utils/logger");
44
+ function syncPlatformControlled(opts) {
45
+ const logPrefix = opts.logPrefix ?? 'app-sync';
46
+ const cliRoot = opts.cliRoot ?? resolveCliRoot();
47
+ const stackDir = node_path_1.default.join(cliRoot, 'upgrade', 'templates', opts.stack);
48
+ if (!node_fs_1.default.existsSync(stackDir)) {
49
+ return { stackFound: false, syncedFiles: [], mergedJsonFiles: [] };
50
+ }
51
+ (0, logger_1.log)(logPrefix, `Syncing upgrade/templates/${opts.stack}/ → ${opts.targetDir}`);
52
+ const syncedFiles = syncFilesDir(node_path_1.default.join(stackDir, 'files'), opts.targetDir, logPrefix);
53
+ const mergedJsonFiles = applyJsonPatches(node_path_1.default.join(stackDir, 'patches'), opts.targetDir, logPrefix);
54
+ return {
55
+ stackFound: true,
56
+ syncedFiles,
57
+ mergedJsonFiles,
58
+ };
59
+ }
60
+ /** 推断 cli 安装根:dist/utils/platform-sync.js 上溯两级即仓根 */
61
+ function resolveCliRoot() {
62
+ return node_path_1.default.resolve(__dirname, '..', '..');
63
+ }
64
+ /** 递归同步 srcDir 下所有文件到 destDir,保持相对路径;.sh 文件 +x。返回 dest 相对路径列表 */
65
+ function syncFilesDir(srcDir, destDir, logPrefix) {
66
+ if (!node_fs_1.default.existsSync(srcDir))
67
+ return [];
68
+ const synced = [];
69
+ walk(srcDir, srcDir, destDir, (rel, srcPath, destPath) => {
70
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(destPath), { recursive: true });
71
+ node_fs_1.default.copyFileSync(srcPath, destPath);
72
+ if (rel.endsWith('.sh')) {
73
+ node_fs_1.default.chmodSync(destPath, 0o755);
74
+ }
75
+ synced.push(rel);
76
+ (0, logger_1.log)(logPrefix, ` + ${rel}`);
77
+ });
78
+ return synced;
79
+ }
80
+ /** 递归遍历 patchesDir 下每个 JSON 文件,deep merge 到 destDir 对应路径 */
81
+ function applyJsonPatches(patchesDir, destDir, logPrefix) {
82
+ if (!node_fs_1.default.existsSync(patchesDir))
83
+ return [];
84
+ const merged = [];
85
+ walk(patchesDir, patchesDir, destDir, (rel, patchPath, destPath) => {
86
+ if (!rel.endsWith('.json')) {
87
+ (0, logger_1.log)(logPrefix, ` (skip non-json patch: ${rel})`);
88
+ return;
89
+ }
90
+ if (!node_fs_1.default.existsSync(destPath)) {
91
+ throw new Error(`[${logPrefix}] target JSON not found: ${destPath} ` +
92
+ `(patches/${rel} 要求项目根含同路径文件;app init 应该先创建它)`);
93
+ }
94
+ const patch = readJson(patchPath);
95
+ const target = readJson(destPath);
96
+ if (!isPlainObject(patch) || !isPlainObject(target)) {
97
+ throw new Error(`[${logPrefix}] patch 与 target 必须都是 JSON object: ${rel}`);
98
+ }
99
+ const changed = [];
100
+ for (const key of Object.keys(patch)) {
101
+ const before = JSON.stringify(target[key]);
102
+ target[key] = deepMerge(target[key], patch[key]);
103
+ const after = JSON.stringify(target[key]);
104
+ if (before !== after)
105
+ changed.push(key);
106
+ }
107
+ if (changed.length > 0) {
108
+ node_fs_1.default.writeFileSync(destPath, JSON.stringify(target, null, 2) + '\n', 'utf-8');
109
+ merged.push({ path: rel, keys: changed });
110
+ (0, logger_1.log)(logPrefix, ` ~ ${rel} (keys: ${changed.join(', ')})`);
111
+ }
112
+ else {
113
+ (0, logger_1.log)(logPrefix, ` = ${rel} (unchanged)`);
114
+ }
115
+ });
116
+ return merged;
117
+ }
118
+ /** 通用递归遍历:对 rootSrc 下每个文件调 visit(rel, srcAbsPath, destAbsPath) */
119
+ function walk(rootSrc, curSrc, destDir, visit) {
120
+ for (const name of node_fs_1.default.readdirSync(curSrc)) {
121
+ const srcPath = node_path_1.default.join(curSrc, name);
122
+ const stat = node_fs_1.default.statSync(srcPath);
123
+ if (stat.isDirectory()) {
124
+ walk(rootSrc, srcPath, destDir, visit);
125
+ continue;
126
+ }
127
+ const rel = node_path_1.default.relative(rootSrc, srcPath);
128
+ const destPath = node_path_1.default.join(destDir, rel);
129
+ visit(rel, srcPath, destPath);
130
+ }
131
+ }
132
+ function readJson(filePath) {
133
+ // 用 jsonc 解析以兼容 tsconfig*/nest-cli.json 等允许注释和 trailing comma 的配置;
134
+ // 我们仓自带的 patches/*.json 是严格 JSON,jsonc 也读得通。
135
+ // 写回时仍走 JSON.stringify,注释和 trailing comma 会丢——user app 的 tsconfig 经过
136
+ // 一次 deep merge 后会变成严格 JSON,可接受(tsc 同样读得动)。
137
+ const text = node_fs_1.default.readFileSync(filePath, 'utf-8');
138
+ const errors = [];
139
+ const parsed = jsonc.parse(text, errors, { allowTrailingComma: true });
140
+ if (errors.length > 0) {
141
+ const first = errors[0];
142
+ const code = jsonc.printParseErrorCode(first.error);
143
+ throw new Error(`${filePath}: jsonc parse error ${code} at offset ${String(first.offset)}`);
144
+ }
145
+ return parsed;
146
+ }
147
+ function isPlainObject(v) {
148
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
149
+ }
150
+ /** value 是 object 时递归 merge,其它(包括数组)直接以 patch 覆盖 */
151
+ function deepMerge(target, patch) {
152
+ if (isPlainObject(patch) && isPlainObject(target)) {
153
+ const merged = { ...target };
154
+ for (const k of Object.keys(patch)) {
155
+ merged[k] = deepMerge(target[k], patch[k]);
156
+ }
157
+ return merged;
158
+ }
159
+ return patch;
160
+ }
@@ -0,0 +1,295 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.applySyncRules = applySyncRules;
40
+ const node_fs_1 = __importDefault(require("node:fs"));
41
+ const node_path_1 = __importDefault(require("node:path"));
42
+ const jsonc = __importStar(require("jsonc-parser"));
43
+ const file_ops_1 = require("./file-ops");
44
+ const merge_json_1 = require("./merge-json");
45
+ const logger_1 = require("./logger");
46
+ /**
47
+ * 顺序执行 SyncRule[],逐条 apply。一条 rule 报错不影响后续 rule(catch + 记录),最终
48
+ * 在 handler 层决定是否整体 fail。
49
+ */
50
+ function applySyncRules(rules, opts) {
51
+ const results = [];
52
+ for (const rule of rules) {
53
+ results.push(applyOne(rule, opts));
54
+ }
55
+ return results;
56
+ }
57
+ function applyOne(rule, opts) {
58
+ const { sourceRoot, targetDir } = opts;
59
+ const logPrefix = opts.logPrefix ?? 'sync';
60
+ switch (rule.type) {
61
+ case 'delete-file': {
62
+ const dest = node_path_1.default.join(targetDir, rule.to);
63
+ if (!node_fs_1.default.existsSync(dest)) {
64
+ (0, logger_1.log)(logPrefix, ` ○ ${rule.to} (not found, skip delete)`);
65
+ return { rule, action: 'noop', path: rule.to };
66
+ }
67
+ node_fs_1.default.unlinkSync(dest);
68
+ (0, logger_1.log)(logPrefix, ` ✗ ${rule.to} (deleted)`);
69
+ return { rule, action: 'deleted', path: rule.to };
70
+ }
71
+ case 'delete-directory': {
72
+ const dest = node_path_1.default.join(targetDir, rule.to);
73
+ if (!node_fs_1.default.existsSync(dest)) {
74
+ (0, logger_1.log)(logPrefix, ` ○ ${rule.to} (not found, skip delete)`);
75
+ return { rule, action: 'noop', path: rule.to };
76
+ }
77
+ node_fs_1.default.rmSync(dest, { recursive: true });
78
+ (0, logger_1.log)(logPrefix, ` ✗ ${rule.to} (deleted)`);
79
+ return { rule, action: 'deleted', path: rule.to };
80
+ }
81
+ case 'remove-line': {
82
+ const dest = node_path_1.default.join(targetDir, rule.to);
83
+ const changed = (0, file_ops_1.removeLineFromFile)(dest, rule.pattern, logPrefix);
84
+ return { rule, action: changed ? 'patched' : 'noop', path: rule.to };
85
+ }
86
+ case 'add-line': {
87
+ const dest = node_path_1.default.join(targetDir, rule.to);
88
+ if (!node_fs_1.default.existsSync(dest)) {
89
+ (0, logger_1.log)(logPrefix, ` ○ ${rule.to} (not found, skip add-line)`);
90
+ return { rule, action: 'skipped', path: rule.to };
91
+ }
92
+ const content = node_fs_1.default.readFileSync(dest, 'utf-8');
93
+ const lines = content.split('\n').map((l) => l.trim());
94
+ if (lines.includes(rule.line)) {
95
+ (0, logger_1.log)(logPrefix, ` ○ ${rule.to} (line already present: ${rule.line})`);
96
+ return { rule, action: 'noop', path: rule.to };
97
+ }
98
+ const sep = content.endsWith('\n') ? '' : '\n';
99
+ node_fs_1.default.appendFileSync(dest, `${sep}${rule.line}\n`);
100
+ (0, logger_1.log)(logPrefix, ` ✓ ${rule.to} (added: ${rule.line})`);
101
+ return { rule, action: 'patched', path: rule.to };
102
+ }
103
+ case 'add-script': {
104
+ return applyAddScript(rule, targetDir, logPrefix);
105
+ }
106
+ case 'patch-script': {
107
+ return applyPatchScript(rule, targetDir, logPrefix);
108
+ }
109
+ case 'merge-json': {
110
+ return applyMergeJson(rule, sourceRoot, targetDir, logPrefix);
111
+ }
112
+ case 'directory': {
113
+ const src = node_path_1.default.join(sourceRoot, rule.from);
114
+ const dest = node_path_1.default.join(targetDir, rule.to);
115
+ if (!node_fs_1.default.existsSync(src)) {
116
+ (0, logger_1.log)(logPrefix, ` ⚠ source not found: ${rule.from}`);
117
+ return { rule, action: 'skipped', path: rule.to };
118
+ }
119
+ const count = syncDirectory(src, dest, rule.overwrite ?? true);
120
+ if (count === 0) {
121
+ (0, logger_1.log)(logPrefix, ` ○ ${rule.to}/ (no new files)`);
122
+ return { rule, action: 'noop', path: rule.to };
123
+ }
124
+ (0, logger_1.log)(logPrefix, ` ✓ ${rule.to}/ (synced ${count.toString()} file(s))`);
125
+ return { rule, action: 'synced', path: rule.to, detail: `${count.toString()} file(s)` };
126
+ }
127
+ case 'file': {
128
+ const src = node_path_1.default.join(sourceRoot, rule.from);
129
+ const dest = node_path_1.default.join(targetDir, rule.to);
130
+ if (!node_fs_1.default.existsSync(src)) {
131
+ (0, logger_1.log)(logPrefix, ` ⚠ source not found: ${rule.from}`);
132
+ return { rule, action: 'skipped', path: rule.to };
133
+ }
134
+ const onlyIfExists = rule.onlyIfExists ?? false;
135
+ const overwrite = rule.overwrite ?? true;
136
+ if (onlyIfExists && !node_fs_1.default.existsSync(dest)) {
137
+ (0, logger_1.log)(logPrefix, ` ○ ${rule.to} (skipped, target not exists)`);
138
+ return { rule, action: 'skipped', path: rule.to };
139
+ }
140
+ if (node_fs_1.default.existsSync(dest) && !overwrite) {
141
+ (0, logger_1.log)(logPrefix, ` ○ ${rule.to} (skipped, already exists)`);
142
+ return { rule, action: 'skipped', path: rule.to };
143
+ }
144
+ const destDir = node_path_1.default.dirname(dest);
145
+ if (!node_fs_1.default.existsSync(destDir)) {
146
+ node_fs_1.default.mkdirSync(destDir, { recursive: true });
147
+ }
148
+ node_fs_1.default.copyFileSync(src, dest);
149
+ (0, logger_1.log)(logPrefix, ` ✓ ${rule.to}`);
150
+ return { rule, action: 'synced', path: rule.to };
151
+ }
152
+ case 'append': {
153
+ const src = node_path_1.default.join(sourceRoot, rule.from);
154
+ const dest = node_path_1.default.join(targetDir, rule.to);
155
+ if (!node_fs_1.default.existsSync(src)) {
156
+ (0, logger_1.log)(logPrefix, ` ⚠ source not found: ${rule.from}`);
157
+ return { rule, action: 'skipped', path: rule.to };
158
+ }
159
+ const content = node_fs_1.default.readFileSync(src, 'utf-8');
160
+ const existing = node_fs_1.default.existsSync(dest) ? node_fs_1.default.readFileSync(dest, 'utf-8') : '';
161
+ if (existing.includes(content.trim())) {
162
+ (0, logger_1.log)(logPrefix, ` ○ ${rule.to} (already contains content)`);
163
+ return { rule, action: 'noop', path: rule.to };
164
+ }
165
+ node_fs_1.default.appendFileSync(dest, content);
166
+ (0, logger_1.log)(logPrefix, ` ✓ ${rule.to} (appended)`);
167
+ return { rule, action: 'appended', path: rule.to };
168
+ }
169
+ }
170
+ }
171
+ function readPkgJson(pkgPath) {
172
+ if (!node_fs_1.default.existsSync(pkgPath))
173
+ return null;
174
+ return JSON.parse(node_fs_1.default.readFileSync(pkgPath, 'utf-8'));
175
+ }
176
+ function writePkgJson(pkgPath, pkg) {
177
+ node_fs_1.default.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
178
+ }
179
+ function applyAddScript(rule, targetDir, logPrefix) {
180
+ const pkgPath = node_path_1.default.join(targetDir, 'package.json');
181
+ const pkg = readPkgJson(pkgPath);
182
+ if (!pkg) {
183
+ (0, logger_1.log)(logPrefix, ` ⚠ package.json not found, skip add-script ${rule.name}`);
184
+ return { rule, action: 'skipped', path: 'package.json' };
185
+ }
186
+ pkg.scripts ??= {};
187
+ const overwrite = rule.overwrite ?? false;
188
+ const has = Object.prototype.hasOwnProperty.call(pkg.scripts, rule.name);
189
+ if (has && !overwrite) {
190
+ (0, logger_1.log)(logPrefix, ` ○ scripts.${rule.name} (already exists)`);
191
+ return { rule, action: 'skipped', path: 'package.json' };
192
+ }
193
+ if (pkg.scripts[rule.name] === rule.command) {
194
+ (0, logger_1.log)(logPrefix, ` ○ scripts.${rule.name} (already set)`);
195
+ return { rule, action: 'noop', path: 'package.json' };
196
+ }
197
+ pkg.scripts[rule.name] = rule.command;
198
+ writePkgJson(pkgPath, pkg);
199
+ (0, logger_1.log)(logPrefix, ` ✓ scripts.${rule.name}`);
200
+ return { rule, action: 'created', path: 'package.json', detail: rule.name };
201
+ }
202
+ function applyPatchScript(rule, targetDir, logPrefix) {
203
+ const pkgPath = node_path_1.default.join(targetDir, 'package.json');
204
+ const pkg = readPkgJson(pkgPath);
205
+ if (!pkg) {
206
+ (0, logger_1.log)(logPrefix, ` ⚠ package.json not found, skip patch-script ${rule.name}`);
207
+ return { rule, action: 'skipped', path: 'package.json' };
208
+ }
209
+ const current = pkg.scripts?.[rule.name];
210
+ if (current === undefined) {
211
+ (0, logger_1.log)(logPrefix, ` ○ scripts.${rule.name} (not present, skip patch)`);
212
+ return { rule, action: 'skipped', path: 'package.json' };
213
+ }
214
+ if (current === rule.to) {
215
+ (0, logger_1.log)(logPrefix, ` ○ scripts.${rule.name} (already patched)`);
216
+ return { rule, action: 'noop', path: 'package.json' };
217
+ }
218
+ if (!current.startsWith(rule.ifStartsWith)) {
219
+ (0, logger_1.log)(logPrefix, ` ⚠ scripts.${rule.name} customized, skip patch`);
220
+ return { rule, action: 'skipped', path: 'package.json' };
221
+ }
222
+ pkg.scripts ??= {};
223
+ pkg.scripts[rule.name] = rule.to;
224
+ writePkgJson(pkgPath, pkg);
225
+ (0, logger_1.log)(logPrefix, ` ✓ scripts.${rule.name} (patched)`);
226
+ return { rule, action: 'patched', path: 'package.json', detail: rule.name };
227
+ }
228
+ function applyMergeJson(rule, sourceRoot, targetDir, logPrefix) {
229
+ const src = node_path_1.default.join(sourceRoot, rule.from);
230
+ const dest = node_path_1.default.join(targetDir, rule.to);
231
+ if (!node_fs_1.default.existsSync(src)) {
232
+ (0, logger_1.log)(logPrefix, ` ⚠ source not found: ${rule.from}`);
233
+ return { rule, action: 'skipped', path: rule.to };
234
+ }
235
+ const templateContent = JSON.parse(node_fs_1.default.readFileSync(src, 'utf-8'));
236
+ if (!node_fs_1.default.existsSync(dest)) {
237
+ const destDir = node_path_1.default.dirname(dest);
238
+ if (!node_fs_1.default.existsSync(destDir))
239
+ node_fs_1.default.mkdirSync(destDir, { recursive: true });
240
+ node_fs_1.default.writeFileSync(dest, `${JSON.stringify(templateContent, null, 2)}\n`);
241
+ (0, logger_1.log)(logPrefix, ` ✓ ${rule.to} (created from template)`);
242
+ return { rule, action: 'created', path: rule.to };
243
+ }
244
+ // 用户项目里的 JSON 可能是 jsonc(tsconfig*、nest-cli.json 都允许 trailing comma / 注释)。
245
+ // 这里读取走 jsonc-parser,写回继续标准 JSON.stringify。
246
+ const userRaw = node_fs_1.default.readFileSync(dest, 'utf-8');
247
+ const userContent = parseUserJson(userRaw);
248
+ const merged = (0, merge_json_1.deepMergeJson)(userContent, templateContent, rule.arrayMerge ?? {});
249
+ const userStr = JSON.stringify(userContent, null, 2);
250
+ const mergedStr = JSON.stringify(merged, null, 2);
251
+ if (userStr === mergedStr) {
252
+ (0, logger_1.log)(logPrefix, ` ○ ${rule.to} (already up to date)`);
253
+ return { rule, action: 'noop', path: rule.to };
254
+ }
255
+ node_fs_1.default.writeFileSync(dest, `${mergedStr}\n`);
256
+ const keys = Object.keys(templateContent).join(',');
257
+ (0, logger_1.log)(logPrefix, ` ✓ ${rule.to} (merged: ${keys})`);
258
+ return { rule, action: 'merged', path: rule.to, detail: keys };
259
+ }
260
+ /**
261
+ * 用 jsonc-parser 读 user 端 JSON 文件,兼容 tsconfig/nest-cli.json 等带 trailing comma 或
262
+ * 注释的配置。模板侧严格 JSON 走 JSON.parse 也能通,统一走 jsonc 简化代码。
263
+ */
264
+ function parseUserJson(text) {
265
+ const errors = [];
266
+ const parsed = jsonc.parse(text, errors, { allowTrailingComma: true });
267
+ if (errors.length > 0) {
268
+ const first = errors[0];
269
+ throw new Error(`jsonc parse error ${jsonc.printParseErrorCode(first.error)} at offset ${String(first.offset)}`);
270
+ }
271
+ return parsed;
272
+ }
273
+ function syncDirectory(src, dest, overwrite) {
274
+ if (!node_fs_1.default.existsSync(dest)) {
275
+ node_fs_1.default.mkdirSync(dest, { recursive: true });
276
+ }
277
+ let count = 0;
278
+ for (const file of node_fs_1.default.readdirSync(src)) {
279
+ const srcChild = node_path_1.default.join(src, file);
280
+ const destChild = node_path_1.default.join(dest, file);
281
+ const stat = node_fs_1.default.statSync(srcChild);
282
+ if (stat.isDirectory()) {
283
+ count += syncDirectory(srcChild, destChild, overwrite);
284
+ }
285
+ else if (overwrite || !node_fs_1.default.existsSync(destChild)) {
286
+ node_fs_1.default.copyFileSync(srcChild, destChild);
287
+ // .sh 自动 +x
288
+ if (destChild.endsWith('.sh')) {
289
+ node_fs_1.default.chmodSync(destChild, 0o755);
290
+ }
291
+ count++;
292
+ }
293
+ }
294
+ return count;
295
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/miaoda-cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.7-alpha.eb0aa5c",
4
4
  "description": "Miaoda 平台命令行工具,面向 Agent 调用",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -10,10 +10,11 @@
10
10
  "access": "public"
11
11
  },
12
12
  "files": [
13
+ "LICENSE",
14
+ "README.md",
13
15
  "bin",
14
16
  "dist",
15
- "README.md",
16
- "LICENSE"
17
+ "upgrade"
17
18
  ],
18
19
  "keywords": [
19
20
  "miaoda",
@@ -27,6 +28,7 @@
27
28
  "dependencies": {
28
29
  "@lark-apaas/http-client": "^0.1.5",
29
30
  "commander": "^13.1.0",
31
+ "jsonc-parser": "^3.3.1",
30
32
  "ora": "^5.4.1",
31
33
  "picocolors": "^1.1.1"
32
34
  },