@lark-apaas/miaoda-cli 0.1.6 → 0.1.7-alpha.eb0aa5c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/cli/commands/app/index.js +67 -7
  2. package/dist/cli/commands/index.js +36 -1
  3. package/dist/cli/commands/skills/index.js +18 -2
  4. package/dist/cli/handlers/app/index.js +4 -1
  5. package/dist/cli/handlers/app/init.js +53 -9
  6. package/dist/cli/handlers/app/sync.js +220 -0
  7. package/dist/cli/handlers/skills/sync.js +15 -4
  8. package/dist/config/fullstack-cli-pin.js +13 -0
  9. package/dist/config/sync-configs/design-stack.js +98 -0
  10. package/dist/config/sync-configs/index.js +62 -0
  11. package/dist/config/sync-configs/nestjs-react-fullstack.js +177 -0
  12. package/dist/config/sync.js +14 -0
  13. package/dist/services/app/init/install.js +35 -13
  14. package/dist/services/app/init/template.js +23 -6
  15. package/dist/utils/coding-steering.js +107 -28
  16. package/dist/utils/file-ops.js +45 -0
  17. package/dist/utils/githooks.js +55 -0
  18. package/dist/utils/merge-json.js +63 -0
  19. package/dist/utils/platform-sync.js +160 -0
  20. package/dist/utils/sync-rule.js +295 -0
  21. package/package.json +5 -3
  22. package/upgrade/templates/README.md +34 -0
  23. package/upgrade/templates/design-stack/templates/.githooks/pre-commit +4 -0
  24. package/upgrade/templates/design-stack/templates/scripts/dev-local.js +83 -0
  25. package/upgrade/templates/design-stack/templates/scripts/dev.sh +25 -0
  26. package/upgrade/templates/design-stack/templates/scripts/hooks/run-precommit.js +37 -0
  27. package/upgrade/templates/nestjs-react-fullstack/templates/.githooks/pre-commit +4 -0
  28. package/upgrade/templates/nestjs-react-fullstack/templates/.gitignore.append +8 -0
  29. package/upgrade/templates/nestjs-react-fullstack/templates/.spark_project +16 -0
  30. package/upgrade/templates/nestjs-react-fullstack/templates/drizzle.config.ts +55 -0
  31. package/upgrade/templates/nestjs-react-fullstack/templates/helper/gen-openapi.ts +34 -0
  32. package/upgrade/templates/nestjs-react-fullstack/templates/nest-cli.json +25 -0
  33. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/build.sh +207 -0
  34. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/dev-local.js +111 -0
  35. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/dev.js +295 -0
  36. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/dev.sh +25 -0
  37. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/hooks/run-precommit.js +37 -0
  38. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/lint.js +150 -0
  39. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/prune-smart.js +330 -0
  40. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/run.sh +8 -0
  41. package/upgrade/templates/nestjs-react-fullstack/templates/server/global.d.ts +19 -0
  42. package/upgrade/templates/nestjs-react-fullstack/templates/tsconfig.node.json +5 -0
@@ -0,0 +1,295 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { spawn, execSync } = require('child_process');
7
+ const readline = require('readline');
8
+
9
+ // ── Project root ──────────────────────────────────────────────────────────────
10
+ const PROJECT_ROOT = path.resolve(__dirname, '..');
11
+ process.chdir(PROJECT_ROOT);
12
+
13
+ // ── Load .env ─────────────────────────────────────────────────────────────────
14
+ function loadEnv() {
15
+ const envPath = path.join(PROJECT_ROOT, '.env');
16
+ if (!fs.existsSync(envPath)) return;
17
+ const lines = fs.readFileSync(envPath, 'utf8').split('\n');
18
+ for (const line of lines) {
19
+ const trimmed = line.trim();
20
+ if (!trimmed || trimmed.startsWith('#')) continue;
21
+ const eqIdx = trimmed.indexOf('=');
22
+ if (eqIdx === -1) continue;
23
+ const key = trimmed.slice(0, eqIdx).trim();
24
+ const value = trimmed.slice(eqIdx + 1).trim();
25
+ if (!(key in process.env)) {
26
+ process.env[key] = value;
27
+ }
28
+ }
29
+ }
30
+ loadEnv();
31
+
32
+ // ── Configuration ─────────────────────────────────────────────────────────────
33
+ const LOG_DIR = process.env.LOG_DIR || 'logs';
34
+ const MAX_RESTART_COUNT = process.env.MAX_RESTART_COUNT != null && process.env.MAX_RESTART_COUNT !== ''
35
+ ? parseInt(process.env.MAX_RESTART_COUNT, 10)
36
+ : Infinity;
37
+ const RESTART_DELAY = parseInt(process.env.RESTART_DELAY, 10) || 2;
38
+ const MAX_DELAY = 8;
39
+ const SERVER_PORT = process.env.SERVER_PORT || '3000';
40
+ const CLIENT_DEV_PORT = process.env.CLIENT_DEV_PORT || '8080';
41
+
42
+ fs.mkdirSync(LOG_DIR, { recursive: true });
43
+
44
+ // ── Logging infrastructure ────────────────────────────────────────────────────
45
+ const devStdLogPath = path.join(LOG_DIR, 'dev.std.log');
46
+ const devLogPath = path.join(LOG_DIR, 'dev.log');
47
+ const devStdLogFd = fs.openSync(devStdLogPath, 'a');
48
+ const devLogFd = fs.openSync(devLogPath, 'a');
49
+
50
+ function timestamp() {
51
+ const now = new Date();
52
+ return (
53
+ now.getFullYear() + '-' +
54
+ String(now.getMonth() + 1).padStart(2, '0') + '-' +
55
+ String(now.getDate()).padStart(2, '0') + ' ' +
56
+ String(now.getHours()).padStart(2, '0') + ':' +
57
+ String(now.getMinutes()).padStart(2, '0') + ':' +
58
+ String(now.getSeconds()).padStart(2, '0')
59
+ );
60
+ }
61
+
62
+ /** Write to terminal + dev.std.log */
63
+ function writeOutput(msg) {
64
+ try { process.stdout.write(msg); } catch {}
65
+ try { fs.writeSync(devStdLogFd, msg); } catch {}
66
+ }
67
+
68
+ /** Structured event log → terminal + dev.std.log + dev.log */
69
+ function logEvent(level, name, message) {
70
+ const msg = `[${timestamp()}] [${level}] [${name}] ${message}\n`;
71
+ writeOutput(msg);
72
+ try { fs.writeSync(devLogFd, msg); } catch {}
73
+ }
74
+
75
+ // ── Process group management ──────────────────────────────────────────────────
76
+ function killProcessGroup(pid, signal) {
77
+ try {
78
+ process.kill(-pid, signal);
79
+ } catch {}
80
+ }
81
+
82
+ function killOrphansByPort(port) {
83
+ try {
84
+ const pids = execSync(`lsof -ti :${port}`, { encoding: 'utf8', timeout: 5000 }).trim();
85
+ if (pids) {
86
+ const pidList = pids.split('\n').filter(Boolean);
87
+ for (const p of pidList) {
88
+ try { process.kill(parseInt(p, 10), 'SIGKILL'); } catch {}
89
+ }
90
+ return pidList;
91
+ }
92
+ } catch {}
93
+ return [];
94
+ }
95
+
96
+ // ── Process supervision ───────────────────────────────────────────────────────
97
+ let stopping = false;
98
+ const managedProcesses = []; // { name, pid, child }
99
+
100
+ function sleep(ms) {
101
+ return new Promise((resolve) => setTimeout(resolve, ms));
102
+ }
103
+
104
+ /**
105
+ * Start and supervise a process with auto-restart and log piping.
106
+ * Returns a promise that resolves when the process loop ends.
107
+ */
108
+ function startProcess({ name, command, args, cleanupPort }) {
109
+ const logFilePath = path.join(LOG_DIR, `${name}.std.log`);
110
+ const logFd = fs.openSync(logFilePath, 'a');
111
+
112
+ const entry = { name, pid: null, child: null };
113
+ managedProcesses.push(entry);
114
+
115
+ const run = async () => {
116
+ let restartCount = 0;
117
+
118
+ while (!stopping) {
119
+ const child = spawn(command, args, {
120
+ detached: true,
121
+ stdio: ['ignore', 'pipe', 'pipe'],
122
+ shell: true,
123
+ cwd: PROJECT_ROOT,
124
+ env: { ...process.env },
125
+ });
126
+
127
+ entry.pid = child.pid;
128
+ entry.child = child;
129
+
130
+ const startTime = Date.now();
131
+ logEvent('INFO', name, `Process started (PGID: ${child.pid}): ${command} ${args.join(' ')}`);
132
+
133
+ // Pipe stdout and stderr through readline for timestamped logging
134
+ const pipeLines = (stream) => {
135
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
136
+ rl.on('line', (line) => {
137
+ const msg = `[${timestamp()}] [${name}] ${line}\n`;
138
+ try { fs.writeSync(logFd, msg); } catch {}
139
+ writeOutput(msg);
140
+ });
141
+ };
142
+ if (child.stdout) pipeLines(child.stdout);
143
+ if (child.stderr) pipeLines(child.stderr);
144
+
145
+ // Wait for the direct child to exit.
146
+ // NOTE: must use 'exit', not 'close'. With shell:true, grandchild processes
147
+ // (e.g. nest's server) inherit stdout pipes. 'close' won't fire until ALL
148
+ // pipe holders exit, causing dev.js to hang when npm/nest dies but server survives.
149
+ const exitCode = await new Promise((resolve) => {
150
+ child.on('exit', (code) => resolve(code ?? 1));
151
+ child.on('error', () => resolve(1));
152
+ });
153
+
154
+ // Kill the entire process group
155
+ if (entry.pid) {
156
+ killProcessGroup(entry.pid, 'SIGTERM');
157
+ await sleep(2000);
158
+ killProcessGroup(entry.pid, 'SIGKILL');
159
+ }
160
+ entry.pid = null;
161
+ entry.child = null;
162
+
163
+ // Port cleanup fallback
164
+ if (cleanupPort) {
165
+ const orphans = killOrphansByPort(cleanupPort);
166
+ if (orphans.length > 0) {
167
+ logEvent('WARN', name, `Killed orphan processes on port ${cleanupPort}: ${orphans.join(' ')}`);
168
+ await sleep(500);
169
+ }
170
+ }
171
+
172
+ if (stopping) break;
173
+
174
+ const runDuration = (Date.now() - startTime) / 1000;
175
+ if (runDuration >= 60) {
176
+ restartCount = 0;
177
+ logEvent('INFO', name, `Ran for ${Math.round(runDuration)}s, resetting restart counter`);
178
+ } else {
179
+ restartCount++;
180
+ }
181
+ if (restartCount >= MAX_RESTART_COUNT) {
182
+ logEvent('ERROR', name, `Max restart count (${MAX_RESTART_COUNT}) reached, giving up`);
183
+ break;
184
+ }
185
+
186
+ const delay = Math.min(RESTART_DELAY * (1 << Math.max(0, restartCount - 1)), MAX_DELAY);
187
+ logEvent('WARN', name, `Process exited with code ${exitCode}, restarting (${restartCount}/${MAX_RESTART_COUNT}) in ${delay}s...`);
188
+ await sleep(delay * 1000);
189
+ }
190
+
191
+ try { fs.closeSync(logFd); } catch {}
192
+ };
193
+
194
+ return run();
195
+ }
196
+
197
+ // ── Cleanup ───────────────────────────────────────────────────────────────────
198
+ let cleanupDone = false;
199
+
200
+ async function cleanup() {
201
+ if (cleanupDone) return;
202
+ cleanupDone = true;
203
+ stopping = true;
204
+
205
+ logEvent('INFO', 'main', 'Shutting down all processes...');
206
+
207
+ // Kill all managed process groups
208
+ for (const entry of managedProcesses) {
209
+ if (entry.pid) {
210
+ logEvent('INFO', 'main', `Stopping process group (PGID: ${entry.pid})`);
211
+ killProcessGroup(entry.pid, 'SIGTERM');
212
+ }
213
+ }
214
+
215
+ // Wait for graceful shutdown
216
+ await sleep(2000);
217
+
218
+ // Force kill any remaining
219
+ for (const entry of managedProcesses) {
220
+ if (entry.pid) {
221
+ logEvent('WARN', 'main', `Force killing process group (PGID: ${entry.pid})`);
222
+ killProcessGroup(entry.pid, 'SIGKILL');
223
+ }
224
+ }
225
+
226
+ // Port cleanup fallback
227
+ killOrphansByPort(SERVER_PORT);
228
+ killOrphansByPort(CLIENT_DEV_PORT);
229
+
230
+ logEvent('INFO', 'main', 'All processes stopped');
231
+
232
+ try { fs.closeSync(devStdLogFd); } catch {}
233
+ try { fs.closeSync(devLogFd); } catch {}
234
+
235
+ process.exit(0);
236
+ }
237
+
238
+ process.on('SIGTERM', cleanup);
239
+ process.on('SIGINT', cleanup);
240
+ process.on('SIGHUP', cleanup);
241
+
242
+ // Stale dist makes nest --watch skip missing files; watcher won't self-heal.
243
+ function cleanStaleDist() {
244
+ const distPath = path.join(PROJECT_ROOT, 'dist');
245
+ if (fs.existsSync(distPath)) {
246
+ fs.rmSync(distPath, { recursive: true, force: true });
247
+ logEvent('INFO', 'main', 'Cleaned dist/ to force full rebuild');
248
+ }
249
+ }
250
+
251
+ // ── Main ──────────────────────────────────────────────────────────────────────
252
+ async function main() {
253
+ logEvent('INFO', 'main', '========== Dev session started ==========');
254
+
255
+ cleanStaleDist();
256
+
257
+ // Initialize action plugins
258
+ writeOutput('\n🔌 Initializing action plugins...\n');
259
+ try {
260
+ execSync('fullstack-cli action-plugin init', { cwd: PROJECT_ROOT, stdio: 'inherit' });
261
+ writeOutput('✅ Action plugins initialized\n\n');
262
+ } catch {
263
+ writeOutput('⚠️ Action plugin initialization failed, continuing anyway...\n\n');
264
+ }
265
+
266
+ // Start server and client
267
+ const serverPromise = startProcess({
268
+ name: 'server',
269
+ command: 'npm',
270
+ args: ['run', 'dev:server'],
271
+ cleanupPort: SERVER_PORT,
272
+ });
273
+
274
+ const clientPromise = startProcess({
275
+ name: 'client',
276
+ command: 'npm',
277
+ args: ['run', 'dev:client'],
278
+ cleanupPort: CLIENT_DEV_PORT,
279
+ });
280
+
281
+ writeOutput(`📋 Dev processes running. Press Ctrl+C to stop.\n`);
282
+ writeOutput(`📄 Logs: ${devStdLogPath}\n\n`);
283
+
284
+ // Wait for both (they loop until stopping or max restarts)
285
+ await Promise.all([serverPromise, clientPromise]);
286
+
287
+ if (!cleanupDone) {
288
+ await cleanup();
289
+ }
290
+ }
291
+
292
+ main().catch((err) => {
293
+ console.error('Fatal error:', err);
294
+ process.exit(1);
295
+ });
@@ -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.js:纯 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 [ ! -f "$SCRIPT_DIR/dev-local.js" ]; then
16
+ echo "[dev] scripts/dev-local.js 缺失;先跑 \`npx -y @lark-apaas/miaoda-cli@latest app sync\` 同步平台脚本" >&2
17
+ exit 1
18
+ fi
19
+
20
+ # 本地启动前先跑一次 miaoda app sync:同步 platform-controlled 内容 + 升 @lark-apaas/* 到
21
+ # latest + 迁移老 npm scripts。沙箱不走这里(SANDBOX_ID 分支已经 exec return)。
22
+ # 走 npx 不依赖用户全局装 miaoda;跟 latest dist-tag。
23
+ npx -y @lark-apaas/miaoda-cli@latest app sync || echo "[dev] miaoda app sync 失败,按现状继续" >&2
24
+
25
+ exec node "$SCRIPT_DIR/dev-local.js" "$@"
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ // FULLSTACK_PRECOMMIT_V1
3
+ 'use strict';
4
+
5
+ const { spawnSync } = require('node:child_process');
6
+
7
+ const SEP = ' ' + '─'.repeat(36);
8
+
9
+ function failAndExit(step, body) {
10
+ process.stderr.write('\n✗ pre-commit failed: ' + step + '\n');
11
+ process.stderr.write(SEP + '\n');
12
+ if (body && body.length > 0) {
13
+ process.stderr.write(body.replace(/\s+$/, '') + '\n');
14
+ }
15
+ process.stderr.write(SEP + '\n');
16
+ process.stderr.write(' bypass: git commit --no-verify\n');
17
+ process.exit(1);
18
+ }
19
+
20
+ function runLint() {
21
+ const cwd = process.cwd();
22
+ const res = spawnSync('npm', ['run', 'lint'], {
23
+ cwd,
24
+ stdio: ['ignore', 'pipe', 'pipe'],
25
+ env: process.env,
26
+ });
27
+ if (res.error) {
28
+ failAndExit('lint', String(res.error.message || res.error));
29
+ }
30
+ if (res.status !== 0) {
31
+ const stdout = res.stdout ? res.stdout.toString() : '';
32
+ const stderr = res.stderr ? res.stderr.toString() : '';
33
+ failAndExit('lint', stdout + '\n' + stderr);
34
+ }
35
+ }
36
+
37
+ runLint();
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+
3
+ const path = require('node:path');
4
+ const { spawn } = require('node:child_process');
5
+ const fs = require('node:fs');
6
+
7
+ const cwd = process.cwd();
8
+
9
+ function getBinName(name) {
10
+ return process.platform === 'win32' ? `${name}.cmd` : name;
11
+ }
12
+
13
+ function runCommand(command, args) {
14
+ return new Promise((resolve) => {
15
+ const child = spawn(command, args, {
16
+ cwd,
17
+ stdio: 'inherit',
18
+ shell: false,
19
+ });
20
+
21
+ child.on('close', (code) => resolve(code || 0));
22
+ child.on('error', () => resolve(1));
23
+ });
24
+ }
25
+
26
+ function normalizeProjectFile(filePath) {
27
+ const absolutePath = path.isAbsolute(filePath)
28
+ ? filePath
29
+ : path.resolve(cwd, filePath);
30
+
31
+ if (!fs.existsSync(absolutePath)) {
32
+ console.warn(`[lint] Skip missing file: ${filePath}`);
33
+ return null;
34
+ }
35
+
36
+ const relativePath = path.relative(cwd, absolutePath);
37
+ if (relativePath.startsWith('..')) {
38
+ console.warn(`[lint] Skip file outside project: ${filePath}`);
39
+ return null;
40
+ }
41
+
42
+ return relativePath.split(path.sep).join('/');
43
+ }
44
+
45
+ function parseFilesArg(argv) {
46
+ const filesIndex = argv.indexOf('--files');
47
+ if (filesIndex === -1) {
48
+ return null;
49
+ }
50
+
51
+ return argv.slice(filesIndex + 1).filter(Boolean);
52
+ }
53
+
54
+ function isEslintTarget(filePath) {
55
+ return /\.(c|m)?(j|t)sx?$/.test(filePath);
56
+ }
57
+
58
+ function isTypeCheckTarget(filePath) {
59
+ return /\.(ts|tsx|mts|cts)$/.test(filePath);
60
+ }
61
+
62
+ function isStylelintTarget(filePath) {
63
+ return filePath.endsWith('.css');
64
+ }
65
+
66
+ async function runDefaultLint() {
67
+ const code = await runCommand(getBinName('npx'), [
68
+ 'concurrently',
69
+ 'npm run eslint',
70
+ 'npm run type:check',
71
+ 'npm run stylelint',
72
+ ]);
73
+ process.exit(code);
74
+ }
75
+
76
+ async function runSelectiveLint(inputFiles) {
77
+ const normalizedFiles = Array.from(
78
+ new Set(inputFiles.map(normalizeProjectFile).filter(Boolean)),
79
+ );
80
+
81
+ if (normalizedFiles.length === 0) {
82
+ console.log('[lint] No supported project files found');
83
+ process.exit(0);
84
+ }
85
+
86
+ const eslintFiles = normalizedFiles.filter(isEslintTarget);
87
+ const stylelintFiles = normalizedFiles.filter(isStylelintTarget);
88
+ const typeCheckFiles = normalizedFiles.filter(isTypeCheckTarget);
89
+
90
+ const clientTypeFiles = [];
91
+ const serverTypeFiles = [];
92
+
93
+ for (const filePath of typeCheckFiles) {
94
+ if (filePath.startsWith('client/')) {
95
+ clientTypeFiles.push(filePath);
96
+ } else if (filePath.startsWith('server/')) {
97
+ serverTypeFiles.push(filePath);
98
+ } else if (filePath.startsWith('shared/')) {
99
+ clientTypeFiles.push(filePath);
100
+ serverTypeFiles.push(filePath);
101
+ }
102
+ }
103
+
104
+ const tasks = [];
105
+
106
+ if (eslintFiles.length > 0) {
107
+ tasks.push(runCommand(getBinName('npx'), ['eslint', '--quiet', ...eslintFiles]));
108
+ }
109
+
110
+ if (stylelintFiles.length > 0) {
111
+ tasks.push(runCommand(getBinName('npx'), ['stylelint', '--quiet', ...stylelintFiles]));
112
+ }
113
+
114
+ if (clientTypeFiles.length > 0) {
115
+ tasks.push(runCommand(getBinName('npm'), ['run', 'type:check:client']));
116
+ }
117
+
118
+ if (serverTypeFiles.length > 0) {
119
+ tasks.push(runCommand(getBinName('npm'), ['run', 'type:check:server']));
120
+ }
121
+
122
+ if (tasks.length === 0) {
123
+ console.log('[lint] No supported files matched for lint');
124
+ process.exit(0);
125
+ }
126
+
127
+ const results = await Promise.all(tasks);
128
+ process.exit(results.some(code => code !== 0) ? 1 : 0);
129
+ }
130
+
131
+ async function main() {
132
+ const files = parseFilesArg(process.argv.slice(2));
133
+ if (files === null) {
134
+ await runDefaultLint();
135
+ return;
136
+ }
137
+
138
+ if (files.length === 0) {
139
+ console.error('[lint] --files requires at least one file path');
140
+ process.exit(1);
141
+ }
142
+
143
+ await runSelectiveLint(files);
144
+ }
145
+
146
+ main().catch((error) => {
147
+ const message = error instanceof Error ? error.message : String(error);
148
+ console.error(`[lint] Failed to run lint: ${message}`);
149
+ process.exit(1);
150
+ });