pan-stack-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -0
- package/bin/pan.js +140 -0
- package/lib/discover.js +206 -0
- package/lib/git/git-clone.js +70 -0
- package/lib/git/git-fetch-all.js +62 -0
- package/lib/git/git-grv.js +98 -0
- package/lib/git/git-gst.js +98 -0
- package/lib/git/git-pull.js +76 -0
- package/lib/git/git-push.js +76 -0
- package/lib/git/helpers/cli.js +103 -0
- package/lib/git/helpers/git.js +61 -0
- package/lib/git/helpers/manifest.js +130 -0
- package/lib/manifest.js +312 -0
- package/lib/run-command.js +196 -0
- package/package.json +27 -0
package/lib/manifest.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const PLACEHOLDER_VALUES = new Set(["", "repo_url_to_be_filled", "pan_stack_repo_url"]);
|
|
5
|
+
|
|
6
|
+
function cleanValue(value) {
|
|
7
|
+
let result = String(value || "").trim();
|
|
8
|
+
const commentIndex = result.indexOf(" #");
|
|
9
|
+
if (commentIndex >= 0) {
|
|
10
|
+
result = result.slice(0, commentIndex).trimEnd();
|
|
11
|
+
}
|
|
12
|
+
if (
|
|
13
|
+
(result.startsWith('"') && result.endsWith('"')) ||
|
|
14
|
+
(result.startsWith("'") && result.endsWith("'"))
|
|
15
|
+
) {
|
|
16
|
+
result = result.slice(1, -1);
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function splitKeyValue(text) {
|
|
22
|
+
const index = text.indexOf(":");
|
|
23
|
+
if (index < 0) {
|
|
24
|
+
return [text.trim(), ""];
|
|
25
|
+
}
|
|
26
|
+
return [text.slice(0, index).trim(), cleanValue(text.slice(index + 1))];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseManifest(manifestPath) {
|
|
30
|
+
const data = { repositories: [], stack: {}, commands: {} };
|
|
31
|
+
let section = "";
|
|
32
|
+
let currentRepo = null;
|
|
33
|
+
let currentCommandName = "";
|
|
34
|
+
let currentCommand = null;
|
|
35
|
+
let currentTargetName = "";
|
|
36
|
+
let blockMode = null;
|
|
37
|
+
let blockIndent = 0;
|
|
38
|
+
let blockLines = [];
|
|
39
|
+
|
|
40
|
+
function flushBlock() {
|
|
41
|
+
if (!blockMode || !currentCommand) {
|
|
42
|
+
blockMode = null;
|
|
43
|
+
blockLines = [];
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const body = blockLines.join("\n");
|
|
47
|
+
if (blockMode === "command-shell") {
|
|
48
|
+
currentCommand.shell = body;
|
|
49
|
+
} else if (blockMode === "target-shell" && currentCommand.targets[currentTargetName]) {
|
|
50
|
+
currentCommand.targets[currentTargetName].shell = body;
|
|
51
|
+
}
|
|
52
|
+
blockMode = null;
|
|
53
|
+
blockLines = [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const lines = fs.readFileSync(manifestPath, "utf8").split(/\r?\n/);
|
|
57
|
+
for (const rawLine of lines) {
|
|
58
|
+
if (blockMode) {
|
|
59
|
+
const trimmed = rawLine.trimEnd();
|
|
60
|
+
if (!trimmed.trim()) {
|
|
61
|
+
blockLines.push("");
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const indent = rawLine.length - rawLine.trimStart().length;
|
|
65
|
+
if (indent < blockIndent) {
|
|
66
|
+
flushBlock();
|
|
67
|
+
} else {
|
|
68
|
+
blockLines.push(rawLine.slice(blockIndent));
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!rawLine.trim() || rawLine.trimStart().startsWith("#")) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const indent = rawLine.length - rawLine.trimStart().length;
|
|
78
|
+
const text = rawLine.trim();
|
|
79
|
+
|
|
80
|
+
if (indent === 0) {
|
|
81
|
+
flushBlock();
|
|
82
|
+
const [key, value] = splitKeyValue(text);
|
|
83
|
+
section = key;
|
|
84
|
+
currentRepo = null;
|
|
85
|
+
currentCommandName = "";
|
|
86
|
+
currentCommand = null;
|
|
87
|
+
currentTargetName = "";
|
|
88
|
+
if (value) {
|
|
89
|
+
data[key] = value;
|
|
90
|
+
} else if (!(key in data)) {
|
|
91
|
+
data[key] = key === "commands" ? {} : {};
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (section === "stack" && indent === 2) {
|
|
97
|
+
const [key, value] = splitKeyValue(text);
|
|
98
|
+
data.stack[key] = value;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (section === "repositories") {
|
|
103
|
+
if (indent === 2 && text.startsWith("- ")) {
|
|
104
|
+
currentRepo = {};
|
|
105
|
+
data.repositories.push(currentRepo);
|
|
106
|
+
const rest = text.slice(2).trim();
|
|
107
|
+
if (rest) {
|
|
108
|
+
const [key, value] = splitKeyValue(rest);
|
|
109
|
+
currentRepo[key] = value;
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (currentRepo && indent === 4) {
|
|
114
|
+
const [key, value] = splitKeyValue(text);
|
|
115
|
+
currentRepo[key] = value;
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (section === "commands") {
|
|
121
|
+
if (indent === 2) {
|
|
122
|
+
flushBlock();
|
|
123
|
+
const [key, value] = splitKeyValue(text);
|
|
124
|
+
currentCommandName = key;
|
|
125
|
+
currentCommand = {};
|
|
126
|
+
currentTargetName = "";
|
|
127
|
+
data.commands[currentCommandName] = currentCommand;
|
|
128
|
+
if (value) {
|
|
129
|
+
currentCommand[key] = value;
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (indent === 4 && currentCommand) {
|
|
135
|
+
flushBlock();
|
|
136
|
+
const [key, value] = splitKeyValue(text);
|
|
137
|
+
if (key === "shell" && (value === "|" || value === ">")) {
|
|
138
|
+
blockMode = "command-shell";
|
|
139
|
+
blockIndent = indent + 2;
|
|
140
|
+
blockLines = [];
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (key === "targets") {
|
|
144
|
+
currentCommand.targets = currentCommand.targets || {};
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
currentCommand[key] = value;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (indent === 6 && currentCommand) {
|
|
152
|
+
flushBlock();
|
|
153
|
+
const [key, value] = splitKeyValue(text);
|
|
154
|
+
currentTargetName = key;
|
|
155
|
+
currentCommand.targets = currentCommand.targets || {};
|
|
156
|
+
const target = {};
|
|
157
|
+
currentCommand.targets[currentTargetName] = target;
|
|
158
|
+
if (value) {
|
|
159
|
+
target[key] = value;
|
|
160
|
+
}
|
|
161
|
+
if (key === "shell" && (value === "|" || value === ">")) {
|
|
162
|
+
blockMode = "target-shell";
|
|
163
|
+
blockIndent = indent + 2;
|
|
164
|
+
blockLines = [];
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (indent === 8 && currentCommand && currentCommand.targets && currentTargetName) {
|
|
170
|
+
flushBlock();
|
|
171
|
+
const target = currentCommand.targets[currentTargetName];
|
|
172
|
+
const [key, value] = splitKeyValue(text);
|
|
173
|
+
if (key === "shell" && (value === "|" || value === ">")) {
|
|
174
|
+
blockMode = "target-shell";
|
|
175
|
+
blockIndent = indent + 2;
|
|
176
|
+
blockLines = [];
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
target[key] = value;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
flushBlock();
|
|
185
|
+
return data;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function uniqueRepos(repos) {
|
|
189
|
+
const seen = new Set();
|
|
190
|
+
const result = [];
|
|
191
|
+
for (const repo of repos) {
|
|
192
|
+
const key = [
|
|
193
|
+
cleanValue(repo.name),
|
|
194
|
+
cleanValue(repo.url),
|
|
195
|
+
cleanValue(repo.path),
|
|
196
|
+
].join("\u0000");
|
|
197
|
+
if (seen.has(key)) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
seen.add(key);
|
|
201
|
+
result.push(repo);
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function manifestRepos(data) {
|
|
207
|
+
return uniqueRepos(data.repositories || []);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function validateRepo(repo) {
|
|
211
|
+
const name = cleanValue(repo.name);
|
|
212
|
+
const url = cleanValue(repo.url);
|
|
213
|
+
const repoPath = cleanValue(repo.path) || name;
|
|
214
|
+
|
|
215
|
+
if (!name) {
|
|
216
|
+
throw new Error("Repository entry is missing 'name'.");
|
|
217
|
+
}
|
|
218
|
+
if (PLACEHOLDER_VALUES.has(url)) {
|
|
219
|
+
throw new Error(`Repository '${name}' is missing a real url.`);
|
|
220
|
+
}
|
|
221
|
+
if (!repoPath) {
|
|
222
|
+
throw new Error(`Repository '${name}' is missing 'path'.`);
|
|
223
|
+
}
|
|
224
|
+
return { name, url, repoPath };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function loadManifest(manifestPath) {
|
|
228
|
+
const resolved = path.resolve(manifestPath);
|
|
229
|
+
if (!fs.existsSync(resolved)) {
|
|
230
|
+
throw new Error(`Manifest not found: ${manifestPath}`);
|
|
231
|
+
}
|
|
232
|
+
const data = parseManifest(resolved);
|
|
233
|
+
return {
|
|
234
|
+
path: resolved,
|
|
235
|
+
root: path.dirname(resolved),
|
|
236
|
+
project: cleanValue(data.project) || path.basename(path.dirname(resolved)),
|
|
237
|
+
managedBy: cleanValue(data.managed_by),
|
|
238
|
+
stack: data.stack || {},
|
|
239
|
+
repositories: manifestRepos(data),
|
|
240
|
+
commands: data.commands || {},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const PAN_CONFIG_FILE = ".pan.yaml";
|
|
245
|
+
|
|
246
|
+
function loadPanConfig(configPath, meta = {}) {
|
|
247
|
+
const resolved = path.resolve(configPath);
|
|
248
|
+
if (!fs.existsSync(resolved)) {
|
|
249
|
+
throw new Error(`Config not found: ${configPath}`);
|
|
250
|
+
}
|
|
251
|
+
const data = parseManifest(resolved);
|
|
252
|
+
const root = path.dirname(resolved);
|
|
253
|
+
return {
|
|
254
|
+
path: resolved,
|
|
255
|
+
root,
|
|
256
|
+
workspaceRoot: meta.workspaceRoot || root,
|
|
257
|
+
project: cleanValue(data.project) || meta.project || path.basename(root),
|
|
258
|
+
repoName: meta.repoName || "",
|
|
259
|
+
scope: meta.scope || "repo",
|
|
260
|
+
commands: data.commands || {},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function getCommandDefinition(source, commandName, subArg) {
|
|
265
|
+
const def = source.commands[commandName];
|
|
266
|
+
if (!def) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (def.targets && typeof def.targets === "object") {
|
|
271
|
+
const targetName = subArg || def.default || Object.keys(def.targets)[0];
|
|
272
|
+
const target = def.targets[targetName];
|
|
273
|
+
if (!target) {
|
|
274
|
+
const valid = Object.keys(def.targets).join(" | ");
|
|
275
|
+
throw new Error(`Target hợp lệ cho '${commandName}': ${valid}`);
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
...target,
|
|
279
|
+
targetName,
|
|
280
|
+
description: def.description,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return def;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function listCommands(source) {
|
|
288
|
+
return Object.entries(source.commands || {}).map(([name, def]) => ({
|
|
289
|
+
name,
|
|
290
|
+
description: def.description || "",
|
|
291
|
+
hasTargets: Boolean(def.targets),
|
|
292
|
+
targets: def.targets ? Object.keys(def.targets) : [],
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function commandBaseDir(source) {
|
|
297
|
+
return source.scope === "workspace" ? source.workspaceRoot : source.root;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
module.exports = {
|
|
301
|
+
PLACEHOLDER_VALUES,
|
|
302
|
+
PAN_CONFIG_FILE,
|
|
303
|
+
cleanValue,
|
|
304
|
+
parseManifest,
|
|
305
|
+
loadManifest,
|
|
306
|
+
loadPanConfig,
|
|
307
|
+
manifestRepos,
|
|
308
|
+
validateRepo,
|
|
309
|
+
getCommandDefinition,
|
|
310
|
+
listCommands,
|
|
311
|
+
commandBaseDir,
|
|
312
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { spawn, spawnSync } = require("child_process");
|
|
4
|
+
const readline = require("readline");
|
|
5
|
+
const { getCommandDefinition, commandBaseDir } = require("./manifest");
|
|
6
|
+
const { sourceMatchesProject } = require("./discover");
|
|
7
|
+
|
|
8
|
+
function needDir(dir) {
|
|
9
|
+
if (!fs.existsSync(dir)) {
|
|
10
|
+
throw new Error(`Chưa có thư mục: ${dir} (chạy 'pan clone' trước?)`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveCwd(source, cwd) {
|
|
15
|
+
const base = commandBaseDir(source);
|
|
16
|
+
if (!cwd) {
|
|
17
|
+
return base;
|
|
18
|
+
}
|
|
19
|
+
if (path.isAbsolute(cwd)) {
|
|
20
|
+
return cwd;
|
|
21
|
+
}
|
|
22
|
+
return path.resolve(base, cwd);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildPanEnv(source, forwardedArgs) {
|
|
26
|
+
const panEnv = {
|
|
27
|
+
...process.env,
|
|
28
|
+
PAN_REPO_ROOT: source.root,
|
|
29
|
+
PAN_WORKSPACE_ROOT: source.workspaceRoot,
|
|
30
|
+
PAN_PROJECT_ROOT: source.workspaceRoot,
|
|
31
|
+
PAN_ARGS: forwardedArgs.join(" "),
|
|
32
|
+
};
|
|
33
|
+
forwardedArgs.forEach((value, index) => {
|
|
34
|
+
panEnv[`PAN_ARG_${index + 1}`] = value;
|
|
35
|
+
});
|
|
36
|
+
return panEnv;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function runExec(source, def, forwardedArgs) {
|
|
40
|
+
const cwd = resolveCwd(source, def.cwd);
|
|
41
|
+
needDir(cwd);
|
|
42
|
+
|
|
43
|
+
const execLine = [def.exec, ...forwardedArgs].filter(Boolean).join(" ").trim();
|
|
44
|
+
if (!execLine) {
|
|
45
|
+
throw new Error("Command thiếu 'exec' hoặc 'shell'.");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const result = spawnSync(execLine, {
|
|
49
|
+
cwd,
|
|
50
|
+
shell: true,
|
|
51
|
+
stdio: "inherit",
|
|
52
|
+
env: buildPanEnv(source, forwardedArgs),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
process.exit(result.status ?? 1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function runShell(source, def, forwardedArgs) {
|
|
59
|
+
const cwd = resolveCwd(source, def.cwd || ".");
|
|
60
|
+
if (def.cwd) {
|
|
61
|
+
needDir(cwd);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const result = spawnSync(def.shell, {
|
|
65
|
+
cwd,
|
|
66
|
+
shell: true,
|
|
67
|
+
stdio: "inherit",
|
|
68
|
+
env: buildPanEnv(source, forwardedArgs),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
process.exit(result.status ?? 1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function runProjectCommand(source, commandName, args) {
|
|
75
|
+
let subArg = "";
|
|
76
|
+
let forwardedArgs = [...args];
|
|
77
|
+
|
|
78
|
+
const def = source.commands[commandName];
|
|
79
|
+
if (def?.targets) {
|
|
80
|
+
if (forwardedArgs.length > 0 && def.targets[forwardedArgs[0]]) {
|
|
81
|
+
subArg = forwardedArgs.shift();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const resolved = getCommandDefinition(source, commandName, subArg);
|
|
86
|
+
if (!resolved) {
|
|
87
|
+
throw new Error(`Không tìm thấy lệnh '${commandName}' trong ${source.path}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (resolved.shell) {
|
|
91
|
+
runShell(source, resolved, forwardedArgs);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (resolved.exec) {
|
|
96
|
+
runExec(source, resolved, forwardedArgs);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
throw new Error(`Lệnh '${commandName}' thiếu 'exec' hoặc 'shell' trong ${source.path}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function sourceLabel(source) {
|
|
104
|
+
if (source.scope === "workspace") {
|
|
105
|
+
return `${source.project} (workspace)`;
|
|
106
|
+
}
|
|
107
|
+
return source.repoName || source.project;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function promptProjectChoice(matches, commandName) {
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
const rl = readline.createInterface({
|
|
113
|
+
input: process.stdin,
|
|
114
|
+
output: process.stdout,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
console.log("");
|
|
118
|
+
matches.forEach((source, index) => {
|
|
119
|
+
const def = source.commands[commandName];
|
|
120
|
+
const desc = def?.description ? ` — ${def.description}` : "";
|
|
121
|
+
console.log(` ${index + 1}. ${sourceLabel(source)} (${source.root})${desc}`);
|
|
122
|
+
});
|
|
123
|
+
console.log("");
|
|
124
|
+
|
|
125
|
+
rl.question("Chọn dự án (số): ", (answer) => {
|
|
126
|
+
rl.close();
|
|
127
|
+
const choice = Number.parseInt(answer, 10);
|
|
128
|
+
if (!Number.isFinite(choice) || choice < 1 || choice > matches.length) {
|
|
129
|
+
reject(new Error("Lựa chọn không hợp lệ."));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
resolve(matches[choice - 1]);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function runWithProjectResolution(matches, commandName, args, projectFlag) {
|
|
138
|
+
if (matches.length === 0) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Không tìm thấy lệnh '${commandName}' trong bất kỳ .pan.yaml nào.`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let source = null;
|
|
145
|
+
if (projectFlag) {
|
|
146
|
+
source = matches.find((item) => sourceMatchesProject(item, projectFlag));
|
|
147
|
+
if (!source) {
|
|
148
|
+
const names = matches.map((item) => sourceLabel(item)).join(", ");
|
|
149
|
+
throw new Error(`Không tìm thấy dự án '${projectFlag}'. Có: ${names}`);
|
|
150
|
+
}
|
|
151
|
+
} else if (matches.length === 1) {
|
|
152
|
+
source = matches[0];
|
|
153
|
+
} else {
|
|
154
|
+
const withTargetArg = matches.filter((item) => {
|
|
155
|
+
const def = item.commands[commandName];
|
|
156
|
+
return def?.targets && args[0] && def.targets[args[0]];
|
|
157
|
+
});
|
|
158
|
+
if (withTargetArg.length === 1) {
|
|
159
|
+
source = withTargetArg[0];
|
|
160
|
+
} else if (process.stdin.isTTY) {
|
|
161
|
+
source = await promptProjectChoice(matches, commandName);
|
|
162
|
+
} else {
|
|
163
|
+
const lines = matches.map(
|
|
164
|
+
(item, index) => `${index + 1}. ${sourceLabel(item)} (${item.root})`
|
|
165
|
+
);
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Lệnh '${commandName}' trùng ở nhiều dự án. Dùng --project <tên>.\n${lines.join("\n")}`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
runProjectCommand(source, commandName, args);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function runGitScript(scriptName, args) {
|
|
176
|
+
const scriptPath = path.join(__dirname, "git", scriptName);
|
|
177
|
+
if (!fs.existsSync(scriptPath)) {
|
|
178
|
+
throw new Error(`Không tìm thấy script: ${scriptPath}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const child = spawn(process.execPath, [scriptPath, ...args], {
|
|
182
|
+
stdio: "inherit",
|
|
183
|
+
env: process.env,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
child.on("exit", (code) => {
|
|
187
|
+
process.exit(code ?? 1);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = {
|
|
192
|
+
runProjectCommand,
|
|
193
|
+
runWithProjectResolution,
|
|
194
|
+
runGitScript,
|
|
195
|
+
sourceLabel,
|
|
196
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pan-stack-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI điều phối PAN Stack — git đa repo và lệnh dự án từ .project.yaml",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pan": "bin/pan.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"lib"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"pan-stack",
|
|
18
|
+
"monorepo",
|
|
19
|
+
"git",
|
|
20
|
+
"cli"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/doiya46/pan-stack.git",
|
|
25
|
+
"directory": ".pan/pan-stack-cli"
|
|
26
|
+
}
|
|
27
|
+
}
|