@lark-apaas/miaoda-cli 0.1.46 → 0.1.48
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.
- package/README.md +20 -0
- package/dist/cli/commands/app/index.js +87 -15
- package/dist/cli/handlers/app/init.js +74 -2
- package/dist/cli/handlers/app/migrate.js +131 -212
- package/dist/cli/handlers/app/sync.js +41 -4
- package/dist/config/fullstack-managed-files.js +67 -0
- package/dist/config/migrate-configs/html-to-design-html.js +1 -1
- package/dist/config/migrate-configs/index.js +9 -0
- package/dist/config/migrate-configs/vite-react-to-nestjs-react-fullstack.js +227 -197
- package/dist/config/sync-configs/design-stack.js +3 -4
- package/dist/config/sync-configs/nestjs-react-fullstack.js +11 -23
- package/dist/services/app/init/async-install.js +21 -0
- package/dist/services/app/init/index.js +3 -1
- package/dist/services/app/init/workspace-check.js +105 -0
- package/dist/services/app/migrate/detect.js +34 -0
- package/dist/services/app/migrate/index.js +8 -0
- package/dist/services/app/migrate/restart.js +59 -0
- package/dist/services/app/migrate/run.js +565 -0
- package/dist/services/app/migrate/symlinks.js +146 -0
- package/dist/utils/engineering-state.js +52 -0
- package/dist/utils/migrate-adapters.js +634 -0
- package/dist/utils/migrate-rule.js +801 -36
- package/dist/utils/project-snapshot.js +99 -0
- package/dist/utils/spark-meta.js +31 -10
- package/dist/utils/sync-rule.js +80 -11
- 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
|
+
}
|
package/dist/utils/spark-meta.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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');
|
package/dist/utils/sync-rule.js
CHANGED
|
@@ -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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
+
}
|