@lark-apaas/miaoda-cli 0.1.45 → 0.1.47

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 (37) hide show
  1. package/README.md +20 -0
  2. package/dist/api/deploy/api.js +3 -1
  3. package/dist/api/deploy/approval.js +77 -0
  4. package/dist/api/deploy/index.js +7 -1
  5. package/dist/api/deploy/schemas.js +3 -0
  6. package/dist/api/deploy/types.js +19 -1
  7. package/dist/cli/commands/app/index.js +87 -15
  8. package/dist/cli/commands/deploy/index.js +30 -6
  9. package/dist/cli/handlers/app/init.js +74 -2
  10. package/dist/cli/handlers/app/migrate.js +131 -212
  11. package/dist/cli/handlers/app/sync.js +41 -4
  12. package/dist/cli/handlers/deploy/deploy.js +57 -4
  13. package/dist/cli/handlers/deploy/format.js +13 -0
  14. package/dist/cli/handlers/deploy/get.js +41 -1
  15. package/dist/cli/handlers/deploy/helpers.js +29 -2
  16. package/dist/cli/handlers/deploy/polling.js +13 -9
  17. package/dist/config/fullstack-managed-files.js +67 -0
  18. package/dist/config/migrate-configs/html-to-design-html.js +1 -1
  19. package/dist/config/migrate-configs/index.js +9 -0
  20. package/dist/config/migrate-configs/vite-react-to-nestjs-react-fullstack.js +227 -197
  21. package/dist/config/sync-configs/design-stack.js +3 -4
  22. package/dist/config/sync-configs/nestjs-react-fullstack.js +11 -23
  23. package/dist/services/app/init/async-install.js +21 -0
  24. package/dist/services/app/init/index.js +3 -1
  25. package/dist/services/app/init/workspace-check.js +105 -0
  26. package/dist/services/app/migrate/detect.js +34 -0
  27. package/dist/services/app/migrate/index.js +8 -0
  28. package/dist/services/app/migrate/restart.js +59 -0
  29. package/dist/services/app/migrate/run.js +565 -0
  30. package/dist/services/app/migrate/symlinks.js +146 -0
  31. package/dist/utils/engineering-state.js +52 -0
  32. package/dist/utils/migrate-adapters.js +634 -0
  33. package/dist/utils/migrate-rule.js +801 -36
  34. package/dist/utils/project-snapshot.js +99 -0
  35. package/dist/utils/spark-meta.js +31 -10
  36. package/dist/utils/sync-rule.js +80 -11
  37. package/package.json +1 -1
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ /**
3
+ * 工程目录快照 / 差异计算 —— migrate 记录本次写入了哪些工程文件。
4
+ *
5
+ * snapshotTree:按内容哈希记录工程文件(排除依赖、构建产物、本地状态等目录)
6
+ * diffSnapshots:对比转换前后快照,得到新增 / 修改 / 删除清单
7
+ * isExcludedPath:排除规则,符号链接安全检查共用
8
+ */
9
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.isExcludedPath = isExcludedPath;
14
+ exports.snapshotTree = snapshotTree;
15
+ exports.diffSnapshots = diffSnapshots;
16
+ const node_crypto_1 = __importDefault(require("node:crypto"));
17
+ const node_fs_1 = __importDefault(require("node:fs"));
18
+ const node_path_1 = __importDefault(require("node:path"));
19
+ /** 任意层级出现即排除的目录名 */
20
+ const EXCLUDED_NAMES = new Set(['node_modules', '.git']);
21
+ /** 仅项目根下排除的路径(相对项目根,POSIX 分隔) */
22
+ const EXCLUDED_ROOT_PATHS = new Set([
23
+ 'dist',
24
+ 'logs',
25
+ 'coverage',
26
+ '.swc',
27
+ '.vite',
28
+ '.npm_cache',
29
+ '.agent',
30
+ '.agents',
31
+ '.claude',
32
+ '.spark/local',
33
+ ]);
34
+ function toPosix(rel) {
35
+ return rel.split(node_path_1.default.sep).join('/');
36
+ }
37
+ function isExcludedPath(relPosix) {
38
+ if (EXCLUDED_ROOT_PATHS.has(relPosix))
39
+ return true;
40
+ return relPosix.split('/').some((seg) => EXCLUDED_NAMES.has(seg));
41
+ }
42
+ function hashFile(abs) {
43
+ return node_crypto_1.default.createHash('sha256').update(node_fs_1.default.readFileSync(abs)).digest('hex');
44
+ }
45
+ function walk(root, visit) {
46
+ const stack = [''];
47
+ while (stack.length > 0) {
48
+ const rel = stack.pop() ?? '';
49
+ const absDir = node_path_1.default.join(root, rel);
50
+ for (const name of node_fs_1.default.readdirSync(absDir)) {
51
+ const childRel = rel === '' ? name : node_path_1.default.join(rel, name);
52
+ const relPosix = toPosix(childRel);
53
+ if (isExcludedPath(relPosix))
54
+ continue;
55
+ const abs = node_path_1.default.join(root, childRel);
56
+ const stat = node_fs_1.default.lstatSync(abs);
57
+ if (stat.isDirectory()) {
58
+ stack.push(childRel);
59
+ continue;
60
+ }
61
+ visit(relPosix, abs, stat);
62
+ }
63
+ }
64
+ }
65
+ function snapshotTree(root) {
66
+ const snap = new Map();
67
+ walk(root, (relPosix, abs, stat) => {
68
+ if (stat.isSymbolicLink()) {
69
+ const target = node_fs_1.default.readlinkSync(abs);
70
+ snap.set(relPosix, {
71
+ hash: node_crypto_1.default.createHash('sha256').update(`symlink:${target}`).digest('hex'),
72
+ mode: 0,
73
+ symlink: true,
74
+ });
75
+ return;
76
+ }
77
+ if (!stat.isFile())
78
+ return;
79
+ snap.set(relPosix, { hash: hashFile(abs), mode: stat.mode & 0o777, symlink: false });
80
+ });
81
+ return snap;
82
+ }
83
+ function diffSnapshots(before, after) {
84
+ const added = [];
85
+ const modified = [];
86
+ const deleted = [];
87
+ for (const [rel, entry] of after) {
88
+ const prev = before.get(rel);
89
+ if (prev === undefined)
90
+ added.push(rel);
91
+ else if (prev.hash !== entry.hash || prev.mode !== entry.mode)
92
+ modified.push(rel);
93
+ }
94
+ for (const rel of before.keys()) {
95
+ if (!after.has(rel))
96
+ deleted.push(rel);
97
+ }
98
+ return { added: added.sort(), modified: modified.sort(), deleted: deleted.sort() };
99
+ }
@@ -4,9 +4,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.readSparkMeta = readSparkMeta;
7
+ exports.readSparkMetaStrict = readSparkMetaStrict;
7
8
  exports.writeSparkMeta = writeSparkMeta;
8
9
  const node_fs_1 = __importDefault(require("node:fs"));
9
10
  const node_path_1 = __importDefault(require("node:path"));
11
+ const error_1 = require("./error");
10
12
  const SPARK_DIR = '.spark';
11
13
  const SPARK_META_FILE = 'meta.json';
12
14
  function readSparkMeta(appDir) {
@@ -20,22 +22,41 @@ function readSparkMeta(appDir) {
20
22
  return {};
21
23
  }
22
24
  }
25
+ function corruptMetaError(metaPath, detail) {
26
+ return new error_1.AppError('SPARK_META_CORRUPT', `${metaPath} 无法解析:${detail}`, {
27
+ next_actions: ['从 Git 历史恢复 .spark/meta.json,或修复其 JSON 格式后重试'],
28
+ });
29
+ }
23
30
  /**
24
- * merge 写入:已有字段保留,undefined 不覆盖;不再维护时间戳。
31
+ * 严格读取:文件不存在返回 undefined;存在但不是合法 JSON 对象时抛 SPARK_META_CORRUPT,
32
+ * 而不是当成空对象继续 —— 工程类操作(sync / migrate / init --strict)不能把损坏的 meta
33
+ * 读成"未初始化"后覆盖掉 app_id 等字段。
34
+ */
35
+ function readSparkMetaStrict(appDir) {
36
+ const metaPath = node_path_1.default.join(appDir, SPARK_DIR, SPARK_META_FILE);
37
+ if (!node_fs_1.default.existsSync(metaPath))
38
+ return undefined;
39
+ let parsed;
40
+ try {
41
+ parsed = JSON.parse(node_fs_1.default.readFileSync(metaPath, 'utf-8'));
42
+ }
43
+ catch (err) {
44
+ throw corruptMetaError(metaPath, err instanceof Error ? err.message : String(err));
45
+ }
46
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
47
+ throw corruptMetaError(metaPath, 'not a JSON object');
48
+ }
49
+ return parsed;
50
+ }
51
+ /**
52
+ * merge 写入:已有字段保留(含未知键),undefined 不覆盖;不再维护时间戳。
53
+ * 已有文件损坏时抛 SPARK_META_CORRUPT,不把损坏内容当空对象覆盖。
25
54
  */
26
55
  function writeSparkMeta(appDir, meta) {
27
56
  const sparkDir = node_path_1.default.join(appDir, SPARK_DIR);
28
57
  node_fs_1.default.mkdirSync(sparkDir, { recursive: true });
29
58
  const metaPath = node_path_1.default.join(sparkDir, SPARK_META_FILE);
30
- let existing = {};
31
- if (node_fs_1.default.existsSync(metaPath)) {
32
- try {
33
- existing = JSON.parse(node_fs_1.default.readFileSync(metaPath, 'utf-8'));
34
- }
35
- catch {
36
- /* 读不出就当空 */
37
- }
38
- }
59
+ const existing = readSparkMetaStrict(appDir) ?? {};
39
60
  const defined = Object.fromEntries(Object.entries(meta).filter(([, v]) => v !== undefined));
40
61
  const merged = { ...existing, ...defined };
41
62
  node_fs_1.default.writeFileSync(metaPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
@@ -107,6 +107,9 @@ function applyOne(rule, opts) {
107
107
  case 'patch-script': {
108
108
  return applyPatchScript(rule, targetDir, logPrefix);
109
109
  }
110
+ case 'ensure-dependency': {
111
+ return applyEnsureDependency(rule, targetDir, logPrefix);
112
+ }
110
113
  case 'typescript-exclude': {
111
114
  const changed = (0, typescript_exclude_1.excludeTypeScriptPath)(node_path_1.default.join(targetDir, rule.to), rule.path);
112
115
  (0, logger_1.log)(logPrefix, ` ${changed ? '✓' : '○'} ${rule.to} (exclude ${rule.path})`);
@@ -147,11 +150,11 @@ function applyOne(rule, opts) {
147
150
  (0, logger_1.log)(logPrefix, ` ○ ${rule.to} (skipped, already exists)`);
148
151
  return { rule, action: 'skipped', path: rule.to };
149
152
  }
150
- const destDir = node_path_1.default.dirname(dest);
151
- if (!node_fs_1.default.existsSync(destDir)) {
152
- node_fs_1.default.mkdirSync(destDir, { recursive: true });
153
+ if (sameFile(src, dest)) {
154
+ (0, logger_1.log)(logPrefix, ` ○ ${rule.to} (already up to date)`);
155
+ return { rule, action: 'noop', path: rule.to };
153
156
  }
154
- node_fs_1.default.copyFileSync(src, dest);
157
+ copyManagedFile(src, dest);
155
158
  (0, logger_1.log)(logPrefix, ` ✓ ${rule.to}`);
156
159
  return { rule, action: 'synced', path: rule.to };
157
160
  }
@@ -221,7 +224,7 @@ function applyPatchScript(rule, targetDir, logPrefix) {
221
224
  (0, logger_1.log)(logPrefix, ` ○ scripts.${rule.name} (already patched)`);
222
225
  return { rule, action: 'noop', path: 'package.json' };
223
226
  }
224
- if (!current.startsWith(rule.ifStartsWith)) {
227
+ if (!patchConditionMatches(rule, current)) {
225
228
  (0, logger_1.log)(logPrefix, ` ⚠ scripts.${rule.name} customized, skip patch`);
226
229
  return { rule, action: 'skipped', path: 'package.json' };
227
230
  }
@@ -231,6 +234,53 @@ function applyPatchScript(rule, targetDir, logPrefix) {
231
234
  (0, logger_1.log)(logPrefix, ` ✓ scripts.${rule.name} (patched)`);
232
235
  return { rule, action: 'patched', path: 'package.json', detail: rule.name };
233
236
  }
237
+ function patchConditionMatches(rule, current) {
238
+ if (rule.ifStartsWith !== undefined && current.startsWith(rule.ifStartsWith))
239
+ return true;
240
+ return (rule.ifMatches ?? []).some((re) => re.test(current));
241
+ }
242
+ function applyEnsureDependency(rule, targetDir, logPrefix) {
243
+ const pkgPath = node_path_1.default.join(targetDir, 'package.json');
244
+ const pkg = readPkgJson(pkgPath);
245
+ if (!pkg) {
246
+ (0, logger_1.log)(logPrefix, ` ⚠ package.json not found, skip ensure-dependency ${rule.name}`);
247
+ return { rule, action: 'skipped', path: 'package.json' };
248
+ }
249
+ for (const section of ['dependencies', 'devDependencies']) {
250
+ const declared = dependencyGroup(pkg, section)?.[rule.name];
251
+ if (declared !== undefined) {
252
+ // 用户已声明的版本不降级 / 不覆盖:兼容性由后续安装与构建验证
253
+ (0, logger_1.log)(logPrefix, ` ○ ${section}.${rule.name} (already declared: ${declared})`);
254
+ return {
255
+ rule,
256
+ action: 'noop',
257
+ path: 'package.json',
258
+ detail: `${section}.${rule.name}@${declared}`,
259
+ };
260
+ }
261
+ }
262
+ if (pkg[rule.section] !== undefined && dependencyGroup(pkg, rule.section) === undefined) {
263
+ (0, logger_1.log)(logPrefix, ` ⚠ package.json ${rule.section} is not an object, skip ensure-dependency`);
264
+ return { rule, action: 'skipped', path: 'package.json' };
265
+ }
266
+ const group = dependencyGroup(pkg, rule.section) ?? {};
267
+ group[rule.name] = rule.version;
268
+ pkg[rule.section] = group;
269
+ writePkgJson(pkgPath, pkg);
270
+ (0, logger_1.log)(logPrefix, ` ✓ ${rule.section}.${rule.name}@${rule.version} (added)`);
271
+ return {
272
+ rule,
273
+ action: 'patched',
274
+ path: 'package.json',
275
+ detail: `${rule.section}.${rule.name}@${rule.version}`,
276
+ };
277
+ }
278
+ function dependencyGroup(pkg, section) {
279
+ const group = pkg[section];
280
+ if (group === null || typeof group !== 'object' || Array.isArray(group))
281
+ return undefined;
282
+ return group;
283
+ }
234
284
  function applyMergeJson(rule, sourceRoot, targetDir, logPrefix) {
235
285
  const src = node_path_1.default.join(sourceRoot, rule.from);
236
286
  const dest = node_path_1.default.join(targetDir, rule.to);
@@ -288,14 +338,33 @@ function syncDirectory(src, dest, overwrite) {
288
338
  if (stat.isDirectory()) {
289
339
  count += syncDirectory(srcChild, destChild, overwrite);
290
340
  }
291
- else if (overwrite || !node_fs_1.default.existsSync(destChild)) {
292
- node_fs_1.default.copyFileSync(srcChild, destChild);
293
- // .sh 自动 +x
294
- if (destChild.endsWith('.sh')) {
295
- node_fs_1.default.chmodSync(destChild, 0o755);
296
- }
341
+ else if ((overwrite || !node_fs_1.default.existsSync(destChild)) && !sameFile(srcChild, destChild)) {
342
+ copyManagedFile(srcChild, destChild);
297
343
  count++;
298
344
  }
299
345
  }
300
346
  return count;
301
347
  }
348
+ /** 目标权限位:沿用源文件权限,`.sh` 一律 +x(历史行为) */
349
+ function targetMode(src, dest) {
350
+ const srcMode = node_fs_1.default.statSync(src).mode & 0o777;
351
+ return dest.endsWith('.sh') ? srcMode | 0o755 : srcMode;
352
+ }
353
+ /** 内容与权限都一致时视为无需同步,保证连续 sync 不产生"已同步"的伪变化 */
354
+ function sameFile(src, dest) {
355
+ if (!node_fs_1.default.existsSync(dest))
356
+ return false;
357
+ const destStat = node_fs_1.default.statSync(dest);
358
+ if (!destStat.isFile())
359
+ return false;
360
+ // Windows 上权限位不可靠(chmod 基本无效),只比内容
361
+ if (process.platform !== 'win32' && (destStat.mode & 0o777) !== targetMode(src, dest)) {
362
+ return false;
363
+ }
364
+ return node_fs_1.default.readFileSync(src).equals(node_fs_1.default.readFileSync(dest));
365
+ }
366
+ function copyManagedFile(src, dest) {
367
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(dest), { recursive: true });
368
+ node_fs_1.default.copyFileSync(src, dest);
369
+ node_fs_1.default.chmodSync(dest, targetMode(src, dest));
370
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/miaoda-cli",
3
- "version": "0.1.45",
3
+ "version": "0.1.47",
4
4
  "description": "Miaoda 平台命令行工具,面向 Agent 调用",
5
5
  "type": "commonjs",
6
6
  "bin": {