@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.
- package/dist/api/deploy/schemas.js +3 -0
- package/dist/cli/commands/app/index.js +67 -7
- package/dist/cli/commands/deploy/modern.js +9 -0
- package/dist/cli/commands/index.js +36 -1
- package/dist/cli/commands/skills/index.js +18 -2
- package/dist/cli/handlers/app/index.js +4 -1
- package/dist/cli/handlers/app/init.js +60 -9
- package/dist/cli/handlers/app/sync.js +266 -0
- package/dist/cli/handlers/deploy/modern.js +39 -0
- package/dist/cli/handlers/plugin/plugin-local.js +1 -1
- package/dist/cli/handlers/skills/sync.js +15 -4
- package/dist/config/fullstack-cli-pin.js +13 -0
- package/dist/config/sync-configs/design-stack.js +98 -0
- package/dist/config/sync-configs/index.js +62 -0
- package/dist/config/sync-configs/nestjs-react-fullstack.js +177 -0
- package/dist/config/sync.js +14 -0
- package/dist/services/app/init/install.js +44 -13
- package/dist/services/app/init/template.js +23 -6
- package/dist/services/deploy/modern/atoms/local-release.js +7 -2
- package/dist/services/deploy/modern/pipelines/local.js +4 -1
- package/dist/utils/coding-steering.js +107 -28
- package/dist/utils/file-ops.js +45 -0
- package/dist/utils/githooks.js +55 -0
- package/dist/utils/merge-json.js +63 -0
- package/dist/utils/platform-sync.js +160 -0
- package/dist/utils/sync-rule.js +295 -0
- package/package.json +7 -4
- package/upgrade/templates/README.md +34 -0
- package/upgrade/templates/design-stack/templates/.githooks/pre-commit +4 -0
- package/upgrade/templates/design-stack/templates/scripts/dev-local.js +84 -0
- package/upgrade/templates/design-stack/templates/scripts/dev.sh +26 -0
- package/upgrade/templates/design-stack/templates/scripts/hooks/run-precommit.js +37 -0
- package/upgrade/templates/nestjs-react-fullstack/templates/.githooks/pre-commit +4 -0
- package/upgrade/templates/nestjs-react-fullstack/templates/.gitignore.append +8 -0
- package/upgrade/templates/nestjs-react-fullstack/templates/.spark_project +16 -0
- package/upgrade/templates/nestjs-react-fullstack/templates/drizzle.config.ts +55 -0
- package/upgrade/templates/nestjs-react-fullstack/templates/helper/gen-openapi.ts +34 -0
- package/upgrade/templates/nestjs-react-fullstack/templates/nest-cli.json +25 -0
- package/upgrade/templates/nestjs-react-fullstack/templates/scripts/build.sh +207 -0
- package/upgrade/templates/nestjs-react-fullstack/templates/scripts/dev-local.js +113 -0
- package/upgrade/templates/nestjs-react-fullstack/templates/scripts/dev.js +295 -0
- package/upgrade/templates/nestjs-react-fullstack/templates/scripts/dev.sh +26 -0
- package/upgrade/templates/nestjs-react-fullstack/templates/scripts/hooks/run-precommit.js +37 -0
- package/upgrade/templates/nestjs-react-fullstack/templates/scripts/lint.js +150 -0
- package/upgrade/templates/nestjs-react-fullstack/templates/scripts/prune-smart.js +330 -0
- package/upgrade/templates/nestjs-react-fullstack/templates/scripts/run.sh +8 -0
- package/upgrade/templates/nestjs-react-fullstack/templates/server/global.d.ts +19 -0
- package/upgrade/templates/nestjs-react-fullstack/templates/tsconfig.node.json +5 -0
|
@@ -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,19 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/miaoda-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7-alpha.785a451",
|
|
4
4
|
"description": "Miaoda 平台命令行工具,面向 Agent 调用",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"miaoda": "bin/miaoda.js"
|
|
8
8
|
},
|
|
9
9
|
"publishConfig": {
|
|
10
|
-
"access": "public"
|
|
10
|
+
"access": "public",
|
|
11
|
+
"registry": "https://registry.npmjs.org/"
|
|
11
12
|
},
|
|
12
13
|
"files": [
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"README.md",
|
|
13
16
|
"bin",
|
|
14
17
|
"dist",
|
|
15
|
-
"
|
|
16
|
-
"LICENSE"
|
|
18
|
+
"upgrade"
|
|
17
19
|
],
|
|
18
20
|
"keywords": [
|
|
19
21
|
"miaoda",
|
|
@@ -27,6 +29,7 @@
|
|
|
27
29
|
"dependencies": {
|
|
28
30
|
"@lark-apaas/http-client": "^0.1.5",
|
|
29
31
|
"commander": "^13.1.0",
|
|
32
|
+
"jsonc-parser": "^3.3.1",
|
|
30
33
|
"ora": "^5.4.1",
|
|
31
34
|
"picocolors": "^1.1.1"
|
|
32
35
|
},
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# upgrade/templates/
|
|
2
|
+
|
|
3
|
+
平台管控的脚手架增量内容,按 stack 分目录,由 `miaoda app upgrade` 同步到用户项目。
|
|
4
|
+
|
|
5
|
+
## 目录约定
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
upgrade/
|
|
9
|
+
└── templates/
|
|
10
|
+
└── <stack>/
|
|
11
|
+
├── files/ ← 文件模式:按相对路径整文件覆盖
|
|
12
|
+
│ └── <rel-path> ← 例如 scripts/dev-local.js → <app>/scripts/dev-local.js
|
|
13
|
+
└── patches/ ← JSON merge 模式:按相对路径 deep merge(任意 JSON 文件)
|
|
14
|
+
└── <rel-path>.json ← 例如 package.json / tsconfig.json / nest-cli.json
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 同步行为
|
|
18
|
+
|
|
19
|
+
- **files/**:递归遍历,按相对路径全量覆盖 targetDir。`.sh` 后缀文件同步后自动 `chmod +x`。
|
|
20
|
+
- **patches/**:递归遍历,**每个 `.json` 文件** 都跟 targetDir 同路径文件做 deep merge:
|
|
21
|
+
- 两边都必须是 JSON object(否则报错)
|
|
22
|
+
- value 是 object → 递归 merge;value 是数组 / 标量 → 整体替换
|
|
23
|
+
- 目标文件不存在则报错(patches 是补丁性质,不负责创建文件;app init 应该先创建)
|
|
24
|
+
- patches 下非 `.json` 文件会被跳过(预留给将来其它 patch 类型)
|
|
25
|
+
- 不做用户改动保护(本期约定:`upgrade/templates/` 下的文件路径由平台拥有,用户不应手改)。
|
|
26
|
+
|
|
27
|
+
## 跟 `miaoda skills sync` 的区别
|
|
28
|
+
|
|
29
|
+
| 链路 | 源 | 内容 | 目标 |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
| `miaoda skills sync` | NPM 包 `@lark-apaas/coding-steering` | agent skills(`.md`) | `<app>/.agent/skills/steering/<stack>/` |
|
|
32
|
+
| `miaoda app upgrade` | 本仓 `upgrade/templates/<stack>/` | 启动脚本、package.json 字段等 | `<app>/` 项目根 |
|
|
33
|
+
|
|
34
|
+
两者独立调用,互不依赖。
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// 本地开发启动脚本(由 miaoda app sync 维护,请勿手改)
|
|
4
|
+
// Stack: design-stack
|
|
5
|
+
//
|
|
6
|
+
// 流程:
|
|
7
|
+
// 1. env pull —— 拉沙箱身份/凭证到 .env.local
|
|
8
|
+
// 2. skills sync —— 同步当前 stack 的 agent skills
|
|
9
|
+
// 3. dotenv 加载 .env / .env.local 到 process.env(含 SUDA_WEBUSER 适配)
|
|
10
|
+
// 4. 起单进程 dev server(design-stack 单进程,无 server/client 拆分)
|
|
11
|
+
//
|
|
12
|
+
// 设计同 nestjs-react-fullstack/dev-local.js,SDK 包不需要自己 require('dotenv'),
|
|
13
|
+
// env 加载收敛在启动脚本单点。SUDA_WEBUSER 适配同 nrf 那份。
|
|
14
|
+
// ============================================================================
|
|
15
|
+
const fs = require('node:fs');
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
const { execSync, spawn, spawnSync } = require('node:child_process');
|
|
18
|
+
|
|
19
|
+
process.chdir(path.resolve(__dirname, '..'));
|
|
20
|
+
|
|
21
|
+
function warn(msg) {
|
|
22
|
+
if (process.stderr.isTTY) process.stderr.write(`\x1b[33mWARNING: ${msg}\x1b[0m\n`);
|
|
23
|
+
else process.stderr.write(`WARNING: ${msg}\n`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!process.env.MIAODA_APP_TYPE) process.env.MIAODA_APP_TYPE = '4';
|
|
27
|
+
process.env.MIAODA_LOCAL_DEV = '1';
|
|
28
|
+
|
|
29
|
+
console.log('[dev-local] (1/4) env pull...');
|
|
30
|
+
const hasLarkCli = spawnSync('command', ['-v', 'lark-cli'], { shell: true, stdio: 'ignore' }).status === 0;
|
|
31
|
+
if (hasLarkCli) {
|
|
32
|
+
let appId = '';
|
|
33
|
+
try {
|
|
34
|
+
appId = JSON.parse(fs.readFileSync('.spark/meta.json', 'utf8')).app_id || '';
|
|
35
|
+
} catch {
|
|
36
|
+
/* meta.json 不存在或非法 JSON */
|
|
37
|
+
}
|
|
38
|
+
if (appId) {
|
|
39
|
+
const r = spawnSync('lark-cli', ['apps', '+env-pull', '--app-id', appId, '--as', 'user'], {
|
|
40
|
+
stdio: 'inherit',
|
|
41
|
+
});
|
|
42
|
+
if (r.status !== 0) warn('env pull 失败,继续按 .env.local 现状启动');
|
|
43
|
+
} else {
|
|
44
|
+
warn('.spark/meta.json 缺 app_id,请先跑 `miaoda app init --app-id <id>`');
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
warn('lark-cli 未安装,跳过 env pull;请确保 .env.local 已就绪');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// skills sync —— --local 切 flat layout;临时钉 @alpha,等 0.1.7 发到 latest 后切回。
|
|
51
|
+
console.log('[dev-local] (2/4) miaoda skills sync...');
|
|
52
|
+
try {
|
|
53
|
+
execSync('npx -y @lark-apaas/miaoda-cli@alpha skills sync --local', { stdio: 'inherit' });
|
|
54
|
+
} catch {
|
|
55
|
+
console.log(' (skills sync 失败,继续启动)');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log('[dev-local] (3/4) loading .env / .env.local...');
|
|
59
|
+
const dotenv = require('dotenv');
|
|
60
|
+
dotenv.config({ path: '.env.local' });
|
|
61
|
+
dotenv.config({ path: '.env' });
|
|
62
|
+
|
|
63
|
+
if (process.env.SUDA_WEBUSER) {
|
|
64
|
+
const raw = process.env.SUDA_WEBUSER;
|
|
65
|
+
try {
|
|
66
|
+
JSON.parse(raw);
|
|
67
|
+
} catch {
|
|
68
|
+
try {
|
|
69
|
+
const unescaped = raw.replace(/\\"/g, '"');
|
|
70
|
+
JSON.parse(unescaped);
|
|
71
|
+
process.env.SUDA_WEBUSER = unescaped;
|
|
72
|
+
} catch {
|
|
73
|
+
warn(`SUDA_WEBUSER 解析失败,值头部: ${raw.slice(0, 80)}...`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log('[dev-local] (4/4) npm run dev');
|
|
79
|
+
const child = spawn('npm', ['run', 'dev'], { stdio: 'inherit', env: process.env });
|
|
80
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
81
|
+
child.on('error', (err) => {
|
|
82
|
+
console.error(err);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
});
|