@lark-apaas/miaoda-cli 0.1.4 → 0.1.5-alpha.2f1e0ff

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 (84) hide show
  1. package/README.md +6 -5
  2. package/dist/api/deploy/index.js +13 -1
  3. package/dist/api/deploy/modern-types.js +23 -0
  4. package/dist/api/deploy/modern.js +78 -0
  5. package/dist/api/deploy/plugin-instances-types.js +6 -0
  6. package/dist/api/deploy/plugin-instances.js +22 -0
  7. package/dist/api/observability/api.js +10 -0
  8. package/dist/api/observability/index.js +2 -1
  9. package/dist/cli/commands/app/index.js +144 -2
  10. package/dist/cli/commands/deploy/modern.js +50 -0
  11. package/dist/cli/commands/index.js +68 -5
  12. package/dist/cli/commands/observability/index.js +62 -6
  13. package/dist/cli/commands/shared.js +5 -0
  14. package/dist/cli/commands/skills/index.js +79 -0
  15. package/dist/cli/handlers/app/index.js +9 -1
  16. package/dist/cli/handlers/app/init.js +132 -0
  17. package/dist/cli/handlers/app/sync.js +215 -0
  18. package/dist/cli/handlers/deploy/modern.js +33 -0
  19. package/dist/cli/handlers/observability/helpers.js +4 -0
  20. package/dist/cli/handlers/observability/index.js +3 -1
  21. package/dist/cli/handlers/observability/log.js +8 -2
  22. package/dist/cli/handlers/observability/source-stack.js +389 -0
  23. package/dist/cli/handlers/observability/trace.js +7 -1
  24. package/dist/cli/handlers/skills/index.js +7 -0
  25. package/dist/cli/handlers/skills/status.js +31 -0
  26. package/dist/cli/handlers/skills/sync.js +49 -0
  27. package/dist/config/fullstack-cli-pin.js +17 -0
  28. package/dist/config/sync-configs/design-stack.js +98 -0
  29. package/dist/config/sync-configs/index.js +62 -0
  30. package/dist/config/sync-configs/nestjs-react-fullstack.js +177 -0
  31. package/dist/config/sync.js +14 -0
  32. package/dist/services/app/init/index.js +12 -0
  33. package/dist/services/app/init/install.js +123 -0
  34. package/dist/services/app/init/template.js +108 -0
  35. package/dist/services/deploy/modern/atoms/build.js +59 -0
  36. package/dist/services/deploy/modern/atoms/context.js +27 -0
  37. package/dist/services/deploy/modern/atoms/index.js +17 -0
  38. package/dist/services/deploy/modern/atoms/local-release.js +27 -0
  39. package/dist/services/deploy/modern/atoms/pre-release.js +13 -0
  40. package/dist/services/deploy/modern/atoms/save-plugin-instances.js +72 -0
  41. package/dist/services/deploy/modern/atoms/upload.js +246 -0
  42. package/dist/services/deploy/modern/check.js +53 -0
  43. package/dist/services/deploy/modern/constants.js +13 -0
  44. package/dist/services/deploy/modern/index.js +16 -0
  45. package/dist/services/deploy/modern/pipelines/index.js +5 -0
  46. package/dist/services/deploy/modern/pipelines/local.js +75 -0
  47. package/dist/services/deploy/modern/protocol.js +122 -0
  48. package/dist/services/deploy/modern/run-types.js +4 -0
  49. package/dist/services/deploy/modern/run.js +13 -0
  50. package/dist/services/deploy/modern/template-key-map.js +22 -0
  51. package/dist/services/skills/index.js +5 -0
  52. package/dist/services/skills/status.js +37 -0
  53. package/dist/utils/coding-steering.js +169 -0
  54. package/dist/utils/file-ops.js +45 -0
  55. package/dist/utils/git.js +22 -0
  56. package/dist/utils/githooks.js +55 -0
  57. package/dist/utils/http.js +21 -11
  58. package/dist/utils/merge-json.js +63 -0
  59. package/dist/utils/npm-pack.js +55 -0
  60. package/dist/utils/platform-sync.js +160 -0
  61. package/dist/utils/spark-meta.js +42 -0
  62. package/dist/utils/sync-rule.js +295 -0
  63. package/package.json +5 -3
  64. package/upgrade/templates/README.md +34 -0
  65. package/upgrade/templates/design-stack/templates/.githooks/pre-commit +4 -0
  66. package/upgrade/templates/design-stack/templates/scripts/dev-local.sh +53 -0
  67. package/upgrade/templates/design-stack/templates/scripts/dev.sh +25 -0
  68. package/upgrade/templates/design-stack/templates/scripts/hooks/run-precommit.js +37 -0
  69. package/upgrade/templates/nestjs-react-fullstack/templates/.githooks/pre-commit +4 -0
  70. package/upgrade/templates/nestjs-react-fullstack/templates/.gitignore.append +8 -0
  71. package/upgrade/templates/nestjs-react-fullstack/templates/.spark_project +16 -0
  72. package/upgrade/templates/nestjs-react-fullstack/templates/drizzle.config.ts +55 -0
  73. package/upgrade/templates/nestjs-react-fullstack/templates/helper/gen-openapi.ts +34 -0
  74. package/upgrade/templates/nestjs-react-fullstack/templates/nest-cli.json +25 -0
  75. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/build.sh +207 -0
  76. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/dev-local.sh +61 -0
  77. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/dev.js +295 -0
  78. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/dev.sh +25 -0
  79. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/hooks/run-precommit.js +37 -0
  80. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/lint.js +150 -0
  81. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/prune-smart.js +330 -0
  82. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/run.sh +8 -0
  83. package/upgrade/templates/nestjs-react-fullstack/templates/server/global.d.ts +19 -0
  84. package/upgrade/templates/nestjs-react-fullstack/templates/tsconfig.node.json +5 -0
@@ -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,42 @@
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.readSparkMeta = readSparkMeta;
7
+ exports.writeSparkMeta = writeSparkMeta;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const SPARK_DIR = '.spark';
11
+ const SPARK_META_FILE = 'meta.json';
12
+ function readSparkMeta(appDir) {
13
+ const metaPath = node_path_1.default.join(appDir, SPARK_DIR, SPARK_META_FILE);
14
+ if (!node_fs_1.default.existsSync(metaPath))
15
+ return {};
16
+ try {
17
+ return JSON.parse(node_fs_1.default.readFileSync(metaPath, 'utf-8'));
18
+ }
19
+ catch {
20
+ return {};
21
+ }
22
+ }
23
+ /**
24
+ * merge 写入:已有字段保留,undefined 不覆盖;不再维护时间戳。
25
+ */
26
+ function writeSparkMeta(appDir, meta) {
27
+ const sparkDir = node_path_1.default.join(appDir, SPARK_DIR);
28
+ node_fs_1.default.mkdirSync(sparkDir, { recursive: true });
29
+ 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
+ }
39
+ const defined = Object.fromEntries(Object.entries(meta).filter(([, v]) => v !== undefined));
40
+ const merged = { ...existing, ...defined };
41
+ node_fs_1.default.writeFileSync(metaPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
42
+ }
@@ -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.4",
3
+ "version": "0.1.5-alpha.2f1e0ff",
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
  },
@@ -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.sh → <app>/scripts/dev-local.sh
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,4 @@
1
+ #!/usr/bin/env sh
2
+ [ "$SKIP_GIT_HOOKS" = "1" ] && exit 0
3
+ export PATH="node_modules/.bin:$PATH"
4
+ npm run precommit
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================================
3
+ # 本地 dev 启动脚本(平台控制,通过 `miaoda app upgrade` 同步,请勿手改)
4
+ # Stack: design-stack(SSR-only,无 server 业务逻辑,无数据库)
5
+ # 流程:env pull → skills sync → 起 dev server
6
+ # ============================================================================
7
+ set -euo pipefail
8
+ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
9
+ cd "$ROOT_DIR"
10
+
11
+ # Stack 是 design-stack,对应 miaoda CLI 的 design scene
12
+ # 沙箱里这个 env 由平台注入,本地 dev 由本脚本兜底,确保 miaoda 子命令(skills/sync)能看见
13
+ export MIAODA_APP_TYPE="${MIAODA_APP_TYPE:-4}"
14
+
15
+ # 本地开发总开关:SDK 据此启用本地兜底逻辑(view-context 占位填充 / __runtime__ 反代 / csrf+webuser)
16
+ # 沙箱 dev / 生产不设此 env,SDK 行为完全不变
17
+ export MIAODA_LOCAL_DEV=1
18
+
19
+ # 临时:绕 npmmirror/bnpm 内网到 npmjs 的同步延迟,等 coding-steering 正式版去掉
20
+ export MIAODA_NPM_REGISTRY="${MIAODA_NPM_REGISTRY:-https://registry.npmjs.org/}"
21
+
22
+ # === .env.local / .env 加载由 SDK 侧负责,本脚本不再手工 source ===
23
+ # fullstack-nestjs-core 在 module init 通过 dotenv 读 .env.local + .env;
24
+ # fullstack-{vite,rspack}-preset 的 defineConfig 同理。优先级:shell > .env.local > .env。
25
+
26
+ # 1. 拉本地 dev 所需 ENV(jwt / cookie 等,写入 .env.local)
27
+ # app_id 从 .spark/meta.json 读 —— `miaoda app init --app-id <id>` 时持久化的权威源,
28
+ # 后续 dev / sync 全部沿用这一个值;不在脚本里 hack 别的推断渠道。
29
+ # lark-cli 子命令真名是 `+env-pull`(带 + 前缀单 token,不是 `env pull` 空格分隔);
30
+ # `--as user` 必须显式:env-pull 只接受 user identity,默认 bot 会校验失败。
31
+ echo "[dev-local] (1/3) env pull..."
32
+ if command -v lark-cli >/dev/null 2>&1; then
33
+ # 用 node 读 .spark/meta.json,不引 python3 依赖(user app 必装 node)。
34
+ APP_ID=$(node -e 'try{process.stdout.write(JSON.parse(require("fs").readFileSync(".spark/meta.json","utf8")).app_id||"")}catch(e){}' 2>/dev/null || echo "")
35
+ if [ -n "$APP_ID" ]; then
36
+ lark-cli apps +env-pull --app-id "$APP_ID" --as user || echo " (env pull 失败,后续按 .env.local 现状继续)"
37
+ else
38
+ echo " (.spark/meta.json 缺 app_id,先跑 \`miaoda app init --app-id <id>\` 写入)"
39
+ fi
40
+ else
41
+ echo " (lark-cli 未装,跳过 env pull;请确保 .env.local 已就绪)"
42
+ fi
43
+
44
+ # 2. 同步 stack 对应 skills(coding-steering)。走 npx 不依赖用户全局装 miaoda;
45
+ # 钉到 @alpha dist-tag 跟随最新发布,miaoda-cli 通道我们自己控制风险小。
46
+ echo "[dev-local] (2/3) miaoda skills sync..."
47
+ npx -y @lark-apaas/miaoda-cli@alpha skills sync || echo " (skills sync 失败,继续启动)"
48
+
49
+ # 3. 起 dev server(design-stack 只有 SSR 渲染,单进程即可;不走 scripts/dev.js 的保活)
50
+ # 如果模板的 npm run dev 本身就是裸进程(无保活)则直接调即可;
51
+ # 如果 design-stack 模板的 dev 也是 dev.js 包装,后续需要新增 dev:raw 之类的直通命令
52
+ echo "[dev-local] (3/3) npm run dev"
53
+ exec npm run dev
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env bash
2
+ # `npm run dev` 入口;按 SANDBOX_ID 是否非空判断运行环境:
3
+ # - SANDBOX_ID 非空(沙箱平台注入应用所属沙箱 ID)→ 直接跑 dev.js
4
+ # (保活 / restart loop / 文件日志 —— 沙箱生产形态)。脚本同步由平台 pod 启动阶段做过,
5
+ # dev 入口不再额外 `npm run upgrade`。
6
+ # - 否则(本地)→ 走 miaoda app sync 兜底 + 跑 dev-local.sh:纯 stdout、崩了就崩、Agent 友好。
7
+ # 显式想跑本地路径可用 `npm run dev:local`(绕过 SANDBOX_ID 判断)。
8
+ set -euo pipefail
9
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
10
+
11
+ if [ -n "${SANDBOX_ID:-}" ]; then
12
+ exec node "$SCRIPT_DIR/dev.js" "$@"
13
+ fi
14
+
15
+ if [ ! -x "$SCRIPT_DIR/dev-local.sh" ]; then
16
+ echo "[dev] scripts/dev-local.sh 缺失或不可执行;先跑 \`npx -y @lark-apaas/miaoda-cli@alpha app sync\` 同步平台脚本" >&2
17
+ exit 1
18
+ fi
19
+
20
+ # 本地启动前先跑一次 miaoda app sync:同步 platform-controlled 内容 + 升 @lark-apaas/* 到
21
+ # 钉版 + 迁移老 npm scripts。沙箱不走这里(SANDBOX_ID 分支已经 exec return)。
22
+ # 走 npx 不依赖用户全局装 miaoda;钉到 @alpha dist-tag 跟随最新发布。
23
+ npx -y @lark-apaas/miaoda-cli@alpha app sync || echo "[dev] miaoda app sync 失败,按现状继续" >&2
24
+
25
+ exec "$SCRIPT_DIR/dev-local.sh" "$@"