pan-stack-cli 0.2.1 → 0.2.2

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 (59) hide show
  1. package/README.md +37 -731
  2. package/bin/pan.js +4955 -269
  3. package/package.json +17 -5
  4. package/lib/ai-skills.js +0 -829
  5. package/lib/create.js +0 -105
  6. package/lib/discover.js +0 -294
  7. package/lib/git/git-checkout.js +0 -395
  8. package/lib/git/git-clone.js +0 -206
  9. package/lib/git/git-fetch-all.js +0 -129
  10. package/lib/git/git-grv.js +0 -125
  11. package/lib/git/git-gst.js +0 -165
  12. package/lib/git/git-mr.js +0 -546
  13. package/lib/git/git-pull.js +0 -137
  14. package/lib/git/git-push.js +0 -185
  15. package/lib/git/git-sync-main.js +0 -334
  16. package/lib/git/helpers/bulk-report.js +0 -202
  17. package/lib/git/helpers/cli.js +0 -188
  18. package/lib/git/helpers/colors.js +0 -19
  19. package/lib/git/helpers/git.js +0 -136
  20. package/lib/git/helpers/manifest.js +0 -223
  21. package/lib/git/helpers/repo-select.js +0 -416
  22. package/lib/git/helpers/repo-summary.js +0 -161
  23. package/lib/i18n.js +0 -61
  24. package/lib/inquirer-theme.js +0 -20
  25. package/lib/language.js +0 -121
  26. package/lib/languages.js +0 -14
  27. package/lib/manifest.js +0 -467
  28. package/lib/menu-prompt.js +0 -201
  29. package/lib/menu.js +0 -134
  30. package/lib/messages.js +0 -647
  31. package/lib/platform.js +0 -140
  32. package/lib/run-command.js +0 -334
  33. package/lib/select.js +0 -0
  34. package/lib/serve.js +0 -75
  35. package/lib/skill-registry-client.js +0 -173
  36. package/lib/stats/aggregate.js +0 -51
  37. package/lib/stats/identity.js +0 -20
  38. package/lib/stats/index.js +0 -189
  39. package/lib/stats/parse.js +0 -46
  40. package/lib/stats/range.js +0 -38
  41. package/lib/stats/report.js +0 -264
  42. package/lib/stats/review.js +0 -157
  43. package/lib/stats/scan.js +0 -92
  44. package/lib/stats/serve.js +0 -25
  45. package/lib/ui/api.js +0 -202
  46. package/lib/ui/clone.js +0 -115
  47. package/lib/ui/config.js +0 -182
  48. package/lib/ui/diff.js +0 -128
  49. package/lib/ui/export-gate.js +0 -79
  50. package/lib/ui/gen-react-mobile-code.js +0 -6
  51. package/lib/ui/gen-react-web-code.js +0 -75
  52. package/lib/ui/index.js +0 -89
  53. package/lib/ui/init.js +0 -252
  54. package/lib/ui/login.js +0 -128
  55. package/lib/ui/logout.js +0 -22
  56. package/lib/ui/palettes.js +0 -103
  57. package/lib/ui/published-html.js +0 -24
  58. package/lib/ui/sync.js +0 -137
  59. package/lib/ui/workspace.js +0 -213
package/lib/platform.js DELETED
@@ -1,140 +0,0 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
- const { spawnSync } = require("child_process");
4
-
5
- const WINDOWS_RESERVED_NAMES = new Set([
6
- "CON",
7
- "PRN",
8
- "AUX",
9
- "NUL",
10
- "COM1",
11
- "COM2",
12
- "COM3",
13
- "COM4",
14
- "COM5",
15
- "COM6",
16
- "COM7",
17
- "COM8",
18
- "COM9",
19
- "LPT1",
20
- "LPT2",
21
- "LPT3",
22
- "LPT4",
23
- "LPT5",
24
- "LPT6",
25
- "LPT7",
26
- "LPT8",
27
- "LPT9",
28
- ]);
29
-
30
- function isWindows() {
31
- return process.platform === "win32";
32
- }
33
-
34
- function splitEnvList(value) {
35
- if (!value) {
36
- return [];
37
- }
38
- // On Windows, do not split on ":" — drive letters use "C:".
39
- const parts = isWindows() ? value.split(/[;,]/) : value.split(/[:;,]/);
40
- return parts.map((part) => part.trim()).filter(Boolean);
41
- }
42
-
43
- function findBash() {
44
- const candidates = isWindows()
45
- ? [
46
- process.env.PAN_GIT_BASH,
47
- process.env.PAN_BASH,
48
- process.env.GIT_BASH,
49
- process.env.PROGRAMFILES && path.join(process.env.PROGRAMFILES, "Git", "bin", "bash.exe"),
50
- process.env["ProgramFiles(x86)"] &&
51
- path.join(process.env["ProgramFiles(x86)"], "Git", "bin", "bash.exe"),
52
- process.env.LOCALAPPDATA &&
53
- path.join(process.env.LOCALAPPDATA, "Programs", "Git", "bin", "bash.exe"),
54
- ]
55
- : [process.env.PAN_BASH, process.env.BASH, "/usr/bin/bash", "/bin/bash"];
56
-
57
- for (const candidate of candidates.filter(Boolean)) {
58
- if (fs.existsSync(candidate)) {
59
- return candidate;
60
- }
61
- }
62
-
63
- return null;
64
- }
65
-
66
- /** @deprecated use findBash */
67
- function findGitBash() {
68
- return findBash();
69
- }
70
-
71
- function spawnOptions(extra = {}) {
72
- return isWindows() ? { windowsHide: true, ...extra } : extra;
73
- }
74
-
75
- function normalizeShellEnv(env) {
76
- if (!isWindows()) {
77
- return env;
78
- }
79
-
80
- const normalized = { ...env };
81
- for (const key of ["PAN_WORKSPACE_ROOT", "PAN_REPO_ROOT", "PAN_PROJECT_ROOT"]) {
82
- if (typeof normalized[key] === "string") {
83
- normalized[key] = normalized[key].replace(/\\/g, "/");
84
- }
85
- }
86
- return normalized;
87
- }
88
-
89
- function runShellScript(script, { cwd, env, stdio = "inherit" } = {}) {
90
- const shellEnv = normalizeShellEnv(env);
91
- const bash = findBash();
92
-
93
- if (!bash) {
94
- const hint = isWindows()
95
- ? "Install Git for Windows (https://git-scm.com/download/win) or set PAN_GIT_BASH / PAN_BASH to bash.exe."
96
- : "Install bash or set PAN_BASH to its path.";
97
- throw new Error(
98
- `Shell commands in .pan.yaml require bash. ${hint} Alternatively, use 'exec' instead of 'shell'.`
99
- );
100
- }
101
-
102
- return spawnSync(bash, ["-lc", script], spawnOptions({ cwd, env: shellEnv, stdio }));
103
- }
104
-
105
- function runExecLine(execLine, { cwd, env, stdio = "inherit" } = {}) {
106
- return spawnSync(
107
- execLine,
108
- spawnOptions({ cwd, env: normalizeShellEnv(env), stdio, shell: true })
109
- );
110
- }
111
-
112
- function validateFolderName(name) {
113
- if (!name || !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name)) {
114
- throw new Error(
115
- "Invalid folder name. Use letters, numbers, dots, hyphens, or underscores."
116
- );
117
- }
118
-
119
- if (isWindows()) {
120
- if (/[<>:"/\\|?*]/.test(name)) {
121
- throw new Error("Invalid folder name for Windows (<>:\"/\\|?* are not allowed).");
122
- }
123
- const base = name.replace(/\.+$/, "").toUpperCase();
124
- if (WINDOWS_RESERVED_NAMES.has(base)) {
125
- throw new Error(`Invalid folder name for Windows (reserved name: ${base}).`);
126
- }
127
- }
128
- }
129
-
130
- module.exports = {
131
- isWindows,
132
- splitEnvList,
133
- findBash,
134
- findGitBash,
135
- spawnOptions,
136
- normalizeShellEnv,
137
- runShellScript,
138
- runExecLine,
139
- validateFolderName,
140
- };
@@ -1,334 +0,0 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
- const { spawn, spawnSync } = require("child_process");
4
- const {
5
- getCommandDefinition,
6
- commandBaseDir,
7
- resolveCommandName,
8
- parseSequenceSteps,
9
- } = require("./manifest");
10
- const { sourceMatchesProject } = require("./discover");
11
- const { runShellScript, runExecLine, spawnOptions } = require("./platform");
12
- const { hasSelect, resolveChoices, runSelect, SELECT_BACK } = require("./select");
13
- const { t } = require("./i18n");
14
-
15
- function needDir(dir) {
16
- if (!fs.existsSync(dir)) {
17
- throw new Error(t("DIRECTORY_NOT_FOUND__DIR", { dir }));
18
- }
19
- }
20
-
21
- function resolveCwd(source, cwd) {
22
- const base = commandBaseDir(source);
23
- if (!cwd) {
24
- return base;
25
- }
26
- if (path.isAbsolute(cwd)) {
27
- return cwd;
28
- }
29
- return path.resolve(base, cwd);
30
- }
31
-
32
- function buildPanEnv(source, forwardedArgs, selected, extraEnv) {
33
- const panEnv = {
34
- ...process.env,
35
- PAN_REPO_ROOT: source.root,
36
- PAN_WORKSPACE_ROOT: source.workspaceRoot,
37
- PAN_PROJECT_ROOT: source.workspaceRoot,
38
- PAN_ARGS: forwardedArgs.join(" "),
39
- };
40
- forwardedArgs.forEach((value, index) => {
41
- panEnv[`PAN_ARG_${index + 1}`] = value;
42
- });
43
- // Kết quả picker (field select trong .pan.yaml) → script đọc qua $PAN_SELECTED (mỗi dòng 1 giá trị).
44
- if (Array.isArray(selected)) {
45
- panEnv.PAN_SELECTED = selected.join("\n");
46
- panEnv.PAN_SELECTED_COUNT = String(selected.length);
47
- }
48
- // Biến riêng cho 1 bước trong `sequence` (vd "MODE=prod build-be") — đè lên env chung.
49
- if (extraEnv && Object.keys(extraEnv).length) {
50
- Object.assign(panEnv, extraEnv);
51
- }
52
- return panEnv;
53
- }
54
-
55
- // Core chạy `exec:` — SYNC, KHÔNG process.exit (để sequence gọi lặp được). Trả {status}.
56
- function runExecCore(source, def, forwardedArgs, selected, extraEnv) {
57
- const cwd = resolveCwd(source, def.cwd);
58
- needDir(cwd);
59
-
60
- const execLine = [def.exec, ...forwardedArgs].filter(Boolean).join(" ").trim();
61
- if (!execLine) {
62
- throw new Error(t("COMMAND_MISSING_EXEC_OR_SHELL"));
63
- }
64
-
65
- return runExecLine(execLine, {
66
- cwd,
67
- stdio: "inherit",
68
- env: buildPanEnv(source, forwardedArgs, selected, extraEnv),
69
- });
70
- }
71
-
72
- // Core chạy `shell:` — SYNC, KHÔNG process.exit (để sequence gọi lặp được). Trả {status}.
73
- function runShellCore(source, def, forwardedArgs, selected, extraEnv) {
74
- const cwd = resolveCwd(source, def.cwd || ".");
75
- if (def.cwd) {
76
- needDir(cwd);
77
- }
78
- return runShellScript(def.shell, {
79
- cwd,
80
- stdio: "inherit",
81
- env: buildPanEnv(source, forwardedArgs, selected, extraEnv),
82
- });
83
- }
84
-
85
- function runExec(source, def, forwardedArgs, selected) {
86
- const result = runExecCore(source, def, forwardedArgs, selected);
87
- process.exit(result.status ?? 1);
88
- }
89
-
90
- function runShell(source, def, forwardedArgs, selected) {
91
- let result;
92
- try {
93
- result = runShellCore(source, def, forwardedArgs, selected);
94
- } catch (error) {
95
- console.error(error.message);
96
- process.exit(1);
97
- }
98
- process.exit(result.status ?? 1);
99
- }
100
-
101
- // Chạy 1 command def (đã resolve target nếu có) mà KHÔNG process.exit — dùng trong sequence.
102
- // Trả {status} (0 = ok). Ném lỗi nếu command thiếu cả exec/shell.
103
- async function runOneStepDef(source, commandName, resolved, extraEnv) {
104
- let selected = await resolveSelection(source, resolved, []);
105
- // Bước trong sequence không có "menu cha" để quay lại — 'b' ở đây coi như bước này không chọn gì.
106
- if (selected === SELECT_BACK) selected = [];
107
- if (selected && selected.length === 0) {
108
- console.error(t("NOTHING_SELECTED_EXITING"));
109
- return { status: 0, skipped: true };
110
- }
111
- if (resolved.shell) {
112
- return runShellCore(source, resolved, [], selected, extraEnv);
113
- }
114
- if (resolved.exec) {
115
- return runExecCore(source, resolved, [], selected, extraEnv);
116
- }
117
- throw new Error(
118
- t("COMMAND_MISSING_EXEC_OR_SHELL_IN_SOURCE__COMMAND_PATH", { command: commandName, path: source.path })
119
- );
120
- }
121
-
122
- // `sequence:` — chạy nhiều command ĐÃ khai báo trong CÙNG file .pan.yaml, TUẦN TỰ theo thứ tự,
123
- // dừng ngay khi 1 bước lỗi (trừ bước có "?" cuối tên lệnh → cảnh báo rồi chạy tiếp).
124
- async function runSequence(source, def, commandName) {
125
- const steps = parseSequenceSteps(def.sequence);
126
- if (steps.length === 0) {
127
- throw new Error(`Command '${commandName}' has an empty sequence.`);
128
- }
129
-
130
- console.log(`▶ ${commandName}: ${steps.length} step(s)`);
131
- for (let index = 0; index < steps.length; index += 1) {
132
- const step = steps[index];
133
- if (!source.commands[step.name]) {
134
- throw new Error(
135
- `sequence step ${index + 1}/${steps.length} in '${commandName}': command '${step.name}' not found in ${source.path}`
136
- );
137
- }
138
- const resolved = getCommandDefinition(source, step.name, step.target);
139
- const label = step.target ? `${step.name}:${step.target}` : step.name;
140
- const envHint = Object.keys(step.env).length ? ` (${Object.entries(step.env).map(([k, v]) => `${k}=${v}`).join(" ")})` : "";
141
- console.log(`\n── [${index + 1}/${steps.length}] ${label}${envHint} ──`);
142
-
143
- let result;
144
- try {
145
- result = await runOneStepDef(source, step.name, resolved, step.env);
146
- } catch (error) {
147
- result = { status: 1, error };
148
- }
149
-
150
- const status = result.status ?? 1;
151
- if (status !== 0) {
152
- if (step.optional) {
153
- console.warn(`⚠️ [${index + 1}/${steps.length}] ${label} failed (exit ${status}) — optional, continuing.${result.error ? " " + result.error.message : ""}`);
154
- continue;
155
- }
156
- if (result.error) console.error(result.error.message);
157
- console.error(`✗ sequence '${commandName}' stopped at step ${index + 1}/${steps.length} (${label}), exit ${status}.`);
158
- return status;
159
- }
160
- }
161
- console.log(`\n✓ sequence '${commandName}' — ${steps.length} step(s) done.`);
162
- return 0;
163
- }
164
-
165
- // Nếu command khai báo select (selectFrom/selectChoices) → render picker, trả mảng value.
166
- // 'all' arg / PAN_ALL=1 / non-TTY ⇒ chọn hết. Trả null nếu command không có select.
167
- async function resolveSelection(source, def, forwardedArgs) {
168
- if (!hasSelect(def)) return null;
169
- const cwd = resolveCwd(source, def.cwd || ".");
170
- const env = buildPanEnv(source, forwardedArgs);
171
- const choices = resolveChoices(def, { cwd, env });
172
- const forceAll =
173
- forwardedArgs.includes("all") || process.env.PAN_ALL === "1" || !process.stdin.isTTY;
174
- return runSelect(
175
- {
176
- message: def.selectMessage || "Chọn:",
177
- multi: def.selectMulti !== "false",
178
- all: def.selectAll !== "false",
179
- choices,
180
- },
181
- { forceAll }
182
- );
183
- }
184
-
185
- async function runProjectCommand(source, commandName, args) {
186
- let subArg = "";
187
- let forwardedArgs = [...args];
188
-
189
- const def = source.commands[commandName];
190
- if (def?.targets) {
191
- if (forwardedArgs.length > 0 && def.targets[forwardedArgs[0]]) {
192
- subArg = forwardedArgs.shift();
193
- }
194
- }
195
-
196
- const resolved = getCommandDefinition(source, commandName, subArg);
197
- if (!resolved) {
198
- throw new Error(t("COMMAND_NOT_FOUND_IN_SOURCE__COMMAND_PATH", { command: commandName, path: source.path }));
199
- }
200
-
201
- if (resolved.sequence) {
202
- const status = await runSequence(source, resolved, commandName);
203
- process.exit(status);
204
- return;
205
- }
206
-
207
- const selected = await resolveSelection(source, resolved, forwardedArgs);
208
- // 'b' ở bước chọn (selectFrom/selectChoices) — KHÔNG thoát hẳn tiến trình: trả tín hiệu để
209
- // caller (menu tương tác `pan` trống) quay lại bước chọn TRƯỚC đó thay vì chạy lệnh/thoát.
210
- // Lời gọi trực tiếp không qua menu (`pan <command>` / runWithProjectResolution) không có "menu
211
- // cha" để quay lại nên coi tín hiệu này như huỷ lệnh êm (xem runWithProjectResolution bên dưới).
212
- if (selected === SELECT_BACK) {
213
- return { backed: true };
214
- }
215
- if (selected && selected.length === 0) {
216
- console.error(t("NOTHING_SELECTED_EXITING"));
217
- process.exit(0);
218
- }
219
-
220
- if (resolved.shell) {
221
- runShell(source, resolved, forwardedArgs, selected);
222
- return;
223
- }
224
-
225
- if (resolved.exec) {
226
- runExec(source, resolved, forwardedArgs, selected);
227
- return;
228
- }
229
-
230
- throw new Error(
231
- t("COMMAND_MISSING_EXEC_OR_SHELL_IN_SOURCE__COMMAND_PATH", { command: commandName, path: source.path })
232
- );
233
- }
234
-
235
- function sourceLabel(source) {
236
- if (source.scope === "workspace") {
237
- return `${source.project} (workspace)`;
238
- }
239
- return source.repoName || source.project;
240
- }
241
-
242
- async function promptProjectChoice(matches, commandName) {
243
- const choices = matches.map((source) => {
244
- const def = source.commands[commandName];
245
- const desc = def?.description ? ` — ${def.description}` : "";
246
- return { name: `${sourceLabel(source)}${desc}`, value: source };
247
- });
248
- const picked = await runSelect(
249
- {
250
- message: t("COMMAND_MULTIPLE_PLACES_SELECT_PROJECT__COMMAND", { command: commandName }),
251
- multi: false,
252
- all: false,
253
- choices,
254
- },
255
- { forceAll: false }
256
- );
257
- const source = Array.isArray(picked) ? picked[0] : picked;
258
- if (!source || source === SELECT_BACK) {
259
- throw new Error(t("NOTHING_SELECTED_EXITING"));
260
- }
261
- return source;
262
- }
263
-
264
- async function runWithProjectResolution(
265
- matches,
266
- commandName,
267
- args,
268
- projectFlag,
269
- allSources = matches
270
- ) {
271
- const resolvedName = resolveCommandName(allSources, commandName);
272
-
273
- if (matches.length === 0) {
274
- throw new Error(t("COMMAND_NOT_FOUND_ANYWHERE__COMMAND", { command: commandName }));
275
- }
276
-
277
- let source = null;
278
- if (projectFlag) {
279
- source = matches.find((item) => sourceMatchesProject(item, projectFlag));
280
- if (!source) {
281
- const names = matches.map((item) => sourceLabel(item)).join(", ");
282
- throw new Error(t("PROJECT_NOT_FOUND__PROJECT_NAMES", { project: projectFlag, names }));
283
- }
284
- } else if (matches.length === 1) {
285
- source = matches[0];
286
- } else {
287
- const withTargetArg = matches.filter((item) => {
288
- const def = item.commands[resolvedName];
289
- return def?.targets && args[0] && def.targets[args[0]];
290
- });
291
- if (withTargetArg.length === 1) {
292
- source = withTargetArg[0];
293
- } else if (process.stdin.isTTY) {
294
- source = await promptProjectChoice(matches, commandName);
295
- } else {
296
- const lines = matches.map(
297
- (item, index) => `${index + 1}. ${sourceLabel(item)} (${item.root})`
298
- );
299
- throw new Error(
300
- t("COMMAND_MULTIPLE_PROJECTS__COMMAND_LINES", { command: commandName, lines: lines.join("\n") })
301
- );
302
- }
303
- }
304
-
305
- // Lời gọi trực tiếp (không qua menu tương tác) không có "bước trước" để quay lại — 'b' ở đây
306
- // chỉ đơn giản là huỷ êm (giống 'không chọn gì'), KHÔNG lỗi.
307
- const result = await runProjectCommand(source, resolvedName, args);
308
- if (result && result.backed) {
309
- console.log(t("NOTHING_SELECTED_EXITING"));
310
- }
311
- }
312
-
313
- function runGitScript(scriptName, args) {
314
- const scriptPath = path.join(__dirname, "git", scriptName);
315
- if (!fs.existsSync(scriptPath)) {
316
- throw new Error(t("SCRIPT_NOT_FOUND__PATH", { path: scriptPath }));
317
- }
318
-
319
- const child = spawn(process.execPath, [scriptPath, ...args], spawnOptions({
320
- stdio: "inherit",
321
- env: process.env,
322
- }));
323
-
324
- child.on("exit", (code) => {
325
- process.exit(code ?? 1);
326
- });
327
- }
328
-
329
- module.exports = {
330
- runProjectCommand,
331
- runWithProjectResolution,
332
- runGitScript,
333
- sourceLabel,
334
- };
package/lib/select.js DELETED
Binary file
package/lib/serve.js DELETED
@@ -1,75 +0,0 @@
1
- // pan serve — chạy static HTTP server cho 1 thư mục build (vd output `panui:gen`).
2
- // Bọc package `serve` (npm). Ưu tiên bin local (đã cài trong pan-stack-cli); nếu chưa có
3
- // thì fallback `npx --yes serve` (tự tải). SPA mode (-s) để refresh/route sâu vẫn trả index.html.
4
- const { spawn } = require("child_process");
5
- const fs = require("fs");
6
- const path = require("path");
7
-
8
- function printServeHelp() {
9
- console.log(`usage: pan serve [dir] [options]
10
-
11
- Chạy static server cho thư mục build (mặc định thư mục hiện tại).
12
- Ví dụ:
13
- pnpm panui:gen examples/blog-project -o panui-demo # build web demo tĩnh
14
- pan serve panui-demo # xem demo (http://localhost:3000)
15
- pan serve panui-demo -p 8080
16
-
17
- options:
18
- -p, --port <port> Cổng lắng nghe (mặc định 3000)
19
- -h, --help Hiện trợ giúp này`);
20
- }
21
-
22
- function parseServeArgs(args) {
23
- let dir = ".";
24
- let port = "";
25
- let dirSet = false;
26
- for (let i = 0; i < args.length; i += 1) {
27
- const a = args[i];
28
- if (a === "-h" || a === "--help") {
29
- printServeHelp();
30
- process.exit(0);
31
- } else if (a === "-p" || a === "--port") {
32
- port = args[i + 1] || "";
33
- i += 1;
34
- } else if (a.startsWith("--port=")) {
35
- port = a.slice("--port=".length);
36
- } else if (!dirSet) {
37
- dir = a;
38
- dirSet = true;
39
- }
40
- }
41
- return { dir, port };
42
- }
43
-
44
- /** Bin `serve` cài cục bộ trong pan-stack-cli (nếu có). */
45
- function localServeBin() {
46
- const bin = path.join(__dirname, "..", "node_modules", ".bin", "serve");
47
- return fs.existsSync(bin) ? bin : null;
48
- }
49
-
50
- function runServe(args) {
51
- const { dir, port } = parseServeArgs(args);
52
- const target = path.resolve(dir);
53
- if (!fs.existsSync(target)) {
54
- console.error(`\n✗ Không tồn tại thư mục: ${target}\n`);
55
- process.exit(1);
56
- }
57
-
58
- // `serve <dir> -s [-l <port>]` — `-s` single-page (fallback index.html).
59
- const serveArgs = [target, "-s"];
60
- if (port) serveArgs.push("-l", String(port));
61
-
62
- const local = localServeBin();
63
- const cmd = local || "npx";
64
- const spawnArgs = local ? serveArgs : ["--yes", "serve", ...serveArgs];
65
-
66
- const child = spawn(cmd, spawnArgs, { stdio: "inherit" });
67
- child.on("error", (err) => {
68
- console.error(`\n✗ Không chạy được 'serve': ${err.message}`);
69
- console.error(" Cài: cd apps/pan-stack-tools/pan-stack-cli && pnpm install (hoặc dùng npx serve).\n");
70
- process.exit(1);
71
- });
72
- child.on("close", (code) => process.exit(code || 0));
73
- }
74
-
75
- module.exports = { runServe };