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.
- package/README.md +37 -731
- package/bin/pan.js +4955 -269
- package/package.json +17 -5
- package/lib/ai-skills.js +0 -829
- package/lib/create.js +0 -105
- package/lib/discover.js +0 -294
- package/lib/git/git-checkout.js +0 -395
- package/lib/git/git-clone.js +0 -206
- package/lib/git/git-fetch-all.js +0 -129
- package/lib/git/git-grv.js +0 -125
- package/lib/git/git-gst.js +0 -165
- package/lib/git/git-mr.js +0 -546
- package/lib/git/git-pull.js +0 -137
- package/lib/git/git-push.js +0 -185
- package/lib/git/git-sync-main.js +0 -334
- package/lib/git/helpers/bulk-report.js +0 -202
- package/lib/git/helpers/cli.js +0 -188
- package/lib/git/helpers/colors.js +0 -19
- package/lib/git/helpers/git.js +0 -136
- package/lib/git/helpers/manifest.js +0 -223
- package/lib/git/helpers/repo-select.js +0 -416
- package/lib/git/helpers/repo-summary.js +0 -161
- package/lib/i18n.js +0 -61
- package/lib/inquirer-theme.js +0 -20
- package/lib/language.js +0 -121
- package/lib/languages.js +0 -14
- package/lib/manifest.js +0 -467
- package/lib/menu-prompt.js +0 -201
- package/lib/menu.js +0 -134
- package/lib/messages.js +0 -647
- package/lib/platform.js +0 -140
- package/lib/run-command.js +0 -334
- package/lib/select.js +0 -0
- package/lib/serve.js +0 -75
- package/lib/skill-registry-client.js +0 -173
- package/lib/stats/aggregate.js +0 -51
- package/lib/stats/identity.js +0 -20
- package/lib/stats/index.js +0 -189
- package/lib/stats/parse.js +0 -46
- package/lib/stats/range.js +0 -38
- package/lib/stats/report.js +0 -264
- package/lib/stats/review.js +0 -157
- package/lib/stats/scan.js +0 -92
- package/lib/stats/serve.js +0 -25
- package/lib/ui/api.js +0 -202
- package/lib/ui/clone.js +0 -115
- package/lib/ui/config.js +0 -182
- package/lib/ui/diff.js +0 -128
- package/lib/ui/export-gate.js +0 -79
- package/lib/ui/gen-react-mobile-code.js +0 -6
- package/lib/ui/gen-react-web-code.js +0 -75
- package/lib/ui/index.js +0 -89
- package/lib/ui/init.js +0 -252
- package/lib/ui/login.js +0 -128
- package/lib/ui/logout.js +0 -22
- package/lib/ui/palettes.js +0 -103
- package/lib/ui/published-html.js +0 -24
- package/lib/ui/sync.js +0 -137
- package/lib/ui/workspace.js +0 -213
package/lib/git/git-checkout.js
DELETED
|
@@ -1,395 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/*
|
|
3
|
-
* Checkout a branch across PAN Stack workspace + child repos.
|
|
4
|
-
*
|
|
5
|
-
* Reads repos from .project.yaml (same source as other pan git commands).
|
|
6
|
-
* Interactive multi-select picker when running in a TTY without an explicit
|
|
7
|
-
* selection. Non-interactive selection: pass "all", a single repo name, or a
|
|
8
|
-
* comma/space-separated list of repo names.
|
|
9
|
-
*
|
|
10
|
-
* Modes:
|
|
11
|
-
* pan checkout <branch> switch to existing branch (local or origin/*)
|
|
12
|
-
* pan checkout -b <branch> create new branch (fails if it already exists)
|
|
13
|
-
* pan checkout -B <branch> create OR reset (git checkout -B)
|
|
14
|
-
* pan checkout -b <branch> --start-point <ref>
|
|
15
|
-
* base new branch on <ref> (e.g. origin/main)
|
|
16
|
-
*
|
|
17
|
-
* Does NOT push — commit + `pan push` yourself afterwards.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
const fs = require("fs");
|
|
21
|
-
const path = require("path");
|
|
22
|
-
const {
|
|
23
|
-
looksLikeManifest,
|
|
24
|
-
loadManifestRepos,
|
|
25
|
-
printCommonOptions,
|
|
26
|
-
} = require("./helpers/cli");
|
|
27
|
-
const colors = require("./helpers/colors");
|
|
28
|
-
const { resolveMultiTargets } = require("./helpers/repo-select");
|
|
29
|
-
const {
|
|
30
|
-
runGitCapture,
|
|
31
|
-
getCurrentBranch,
|
|
32
|
-
refExists,
|
|
33
|
-
countDirtyFiles,
|
|
34
|
-
} = require("./helpers/git");
|
|
35
|
-
const { t } = require("../i18n");
|
|
36
|
-
|
|
37
|
-
const ICON = {
|
|
38
|
-
ok: "✓",
|
|
39
|
-
exists: "=",
|
|
40
|
-
warn: "!",
|
|
41
|
-
skip: "-",
|
|
42
|
-
err: "✗",
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
function printHelp() {
|
|
46
|
-
console.log(`usage: pan checkout [-b|-B] <branch> [repo|all|list] [options] (alias: pan co)
|
|
47
|
-
|
|
48
|
-
Chuyển/tạo nhánh trên workspace + các repo trong .project.yaml.
|
|
49
|
-
Interactive multi-select khi chạy trong TTY và không truyền lựa chọn.
|
|
50
|
-
|
|
51
|
-
positional:
|
|
52
|
-
branch Tên nhánh (bắt buộc). Vd: feat/#1234, main, develop.
|
|
53
|
-
repo|all|<list> Repo áp dụng: tên repo trong .project.yaml, "all",
|
|
54
|
-
hoặc danh sách phân tách bởi phẩy/space.
|
|
55
|
-
|
|
56
|
-
flags:
|
|
57
|
-
-b Tạo nhánh mới (git checkout -b). Fail nếu đã tồn tại.
|
|
58
|
-
-B Tạo hoặc reset nhánh (git checkout -B).
|
|
59
|
-
--start-point REF Base ref cho -b/-B (default: HEAD hiện tại).
|
|
60
|
-
|
|
61
|
-
${printCommonOptions()}
|
|
62
|
-
--no-workspace Bỏ qua workspace root (chỉ repo con)
|
|
63
|
-
|
|
64
|
-
examples:
|
|
65
|
-
pan checkout main # switch tất cả sang main (tương tác chọn repo)
|
|
66
|
-
pan checkout -b feat/#1234 # tạo nhánh mới (tương tác chọn repo)
|
|
67
|
-
pan co -b feat/#1234 all # tạo trên tất cả repo
|
|
68
|
-
pan co -b feat/#1234 csdl-cc-backend,csdl-cc-b-frontend
|
|
69
|
-
pan co -B hotfix/x --start-point origin/main # reset hotfix/x từ origin/main
|
|
70
|
-
pan checkout develop csdl-cc-backend # switch một repo
|
|
71
|
-
|
|
72
|
-
Kết quả:
|
|
73
|
-
✓ = đã chuyển / tạo · = = đã ở nhánh sẵn · ! = cảnh báo (đã tồn tại)
|
|
74
|
-
- = skip · ✗ = lỗi (dirty tree, không tồn tại, v.v.)`);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function parseCheckoutArgs(argv) {
|
|
78
|
-
const args = {
|
|
79
|
-
help: false,
|
|
80
|
-
manifest: "",
|
|
81
|
-
baseDir: ".",
|
|
82
|
-
dryRun: false,
|
|
83
|
-
noWorkspace: false,
|
|
84
|
-
create: false,
|
|
85
|
-
forceCreate: false,
|
|
86
|
-
startPoint: "",
|
|
87
|
-
branch: "",
|
|
88
|
-
selection: "",
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const positional = [];
|
|
92
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
93
|
-
const arg = argv[index];
|
|
94
|
-
if (arg === "-h" || arg === "--help") {
|
|
95
|
-
args.help = true;
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
if (arg === "-b") {
|
|
99
|
-
args.create = true;
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
if (arg === "-B") {
|
|
103
|
-
args.forceCreate = true;
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
if (arg === "--base-dir") {
|
|
107
|
-
args.baseDir = argv[++index] || ".";
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
if (arg === "--dry-run") {
|
|
111
|
-
args.dryRun = true;
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
if (arg === "--no-workspace") {
|
|
115
|
-
args.noWorkspace = true;
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
if (arg === "--start-point") {
|
|
119
|
-
args.startPoint = argv[++index] || "";
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
if (arg.startsWith("--start-point=")) {
|
|
123
|
-
args.startPoint = arg.slice("--start-point=".length);
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (arg.startsWith("-")) {
|
|
127
|
-
throw new Error(`Unexpected argument: ${arg}`);
|
|
128
|
-
}
|
|
129
|
-
if (looksLikeManifest(arg) && !args.manifest) {
|
|
130
|
-
args.manifest = arg;
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
positional.push(arg);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (args.create && args.forceCreate) {
|
|
137
|
-
throw new Error(t("CHECKOUT_ERR_B_AND_BB"));
|
|
138
|
-
}
|
|
139
|
-
if (positional[0]) {
|
|
140
|
-
args.branch = positional[0];
|
|
141
|
-
}
|
|
142
|
-
if (positional.length > 1) {
|
|
143
|
-
args.selection = positional.slice(1).join(" ");
|
|
144
|
-
}
|
|
145
|
-
if (args.startPoint && !args.create && !args.forceCreate) {
|
|
146
|
-
throw new Error(t("CHECKOUT_ERR_STARTPOINT"));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return args;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function truncate(text, max) {
|
|
153
|
-
const clean = String(text || "").replace(/\s+/g, " ").trim();
|
|
154
|
-
return clean.length > max ? `${clean.slice(0, max)}…` : clean;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function processRepo({
|
|
158
|
-
name,
|
|
159
|
-
destination,
|
|
160
|
-
branch,
|
|
161
|
-
create,
|
|
162
|
-
forceCreate,
|
|
163
|
-
startPoint,
|
|
164
|
-
dryRun,
|
|
165
|
-
}) {
|
|
166
|
-
const record = { name, status: "", message: "" };
|
|
167
|
-
|
|
168
|
-
if (!fs.existsSync(path.join(destination, ".git"))) {
|
|
169
|
-
return { ...record, status: "skip", message: t("CHECKOUT_MSG_NOT_CLONED") };
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const currentBranch = getCurrentBranch(destination);
|
|
173
|
-
const from = currentBranch || t("CHECKOUT_DETACHED");
|
|
174
|
-
|
|
175
|
-
const localExists = refExists(destination, `refs/heads/${branch}`);
|
|
176
|
-
|
|
177
|
-
if (currentBranch === branch && !forceCreate) {
|
|
178
|
-
return {
|
|
179
|
-
...record,
|
|
180
|
-
status: "exists",
|
|
181
|
-
message: t("CHECKOUT_MSG_ON_BRANCH__BRANCH", { branch }),
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (create) {
|
|
186
|
-
if (localExists) {
|
|
187
|
-
return {
|
|
188
|
-
...record,
|
|
189
|
-
status: "warn",
|
|
190
|
-
message: t("CHECKOUT_MSG_LOCAL_EXISTS__BRANCH", { branch }),
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
if (startPoint && !refExists(destination, startPoint)) {
|
|
194
|
-
return {
|
|
195
|
-
...record,
|
|
196
|
-
status: "err",
|
|
197
|
-
message: t("CHECKOUT_MSG_STARTPOINT_MISSING__STARTPOINT", { startPoint }),
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
} else if (!forceCreate) {
|
|
201
|
-
const remoteExists = refExists(destination, `refs/remotes/origin/${branch}`);
|
|
202
|
-
if (!localExists && !remoteExists) {
|
|
203
|
-
return {
|
|
204
|
-
...record,
|
|
205
|
-
status: "err",
|
|
206
|
-
message: t("CHECKOUT_MSG_NO_BRANCH__BRANCH", { branch }),
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
} else if (forceCreate) {
|
|
210
|
-
if (startPoint && !refExists(destination, startPoint)) {
|
|
211
|
-
return {
|
|
212
|
-
...record,
|
|
213
|
-
status: "err",
|
|
214
|
-
message: t("CHECKOUT_MSG_STARTPOINT_MISSING__STARTPOINT", { startPoint }),
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const gitArgs = ["checkout"];
|
|
220
|
-
if (forceCreate) {
|
|
221
|
-
gitArgs.push("-B", branch);
|
|
222
|
-
} else if (create) {
|
|
223
|
-
gitArgs.push("-b", branch);
|
|
224
|
-
} else {
|
|
225
|
-
gitArgs.push(branch);
|
|
226
|
-
}
|
|
227
|
-
if ((create || forceCreate) && startPoint) {
|
|
228
|
-
gitArgs.push(startPoint);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (dryRun) {
|
|
232
|
-
return {
|
|
233
|
-
...record,
|
|
234
|
-
status: "skip",
|
|
235
|
-
message: `[dry-run] git ${gitArgs.join(" ")}`,
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const dirty = countDirtyFiles(destination);
|
|
240
|
-
|
|
241
|
-
const result = runGitCapture(gitArgs, { cwd: destination });
|
|
242
|
-
if (result.status !== 0) {
|
|
243
|
-
const raw = result.stderr || result.stdout || t("CHECKOUT_MSG_FAILED");
|
|
244
|
-
const message = truncate(raw, 240);
|
|
245
|
-
if (dirty > 0 && /would be overwritten|local changes/i.test(raw)) {
|
|
246
|
-
return {
|
|
247
|
-
...record,
|
|
248
|
-
status: "err",
|
|
249
|
-
message: `${message}${t("CHECKOUT_MSG_DIRTY__DIRTY", { dirty })}`,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
return { ...record, status: "err", message };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (forceCreate) {
|
|
256
|
-
if (localExists) {
|
|
257
|
-
return {
|
|
258
|
-
...record,
|
|
259
|
-
status: "ok",
|
|
260
|
-
message: t("CHECKOUT_MSG_RESET__BRANCH_FROM", { branch, from: startPoint || from }),
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
return {
|
|
264
|
-
...record,
|
|
265
|
-
status: "ok",
|
|
266
|
-
message: t("CHECKOUT_MSG_CREATE__BRANCH_FROM", { branch, from: startPoint || from }),
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
if (create) {
|
|
270
|
-
return {
|
|
271
|
-
...record,
|
|
272
|
-
status: "ok",
|
|
273
|
-
message: t("CHECKOUT_MSG_CREATE__BRANCH_FROM", { branch, from: startPoint || from }),
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
return { ...record, status: "ok", message: `${from} → ${branch}` };
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function styleForStatus(status) {
|
|
280
|
-
switch (status) {
|
|
281
|
-
case "ok":
|
|
282
|
-
return { icon: ICON.ok, color: colors.green };
|
|
283
|
-
case "exists":
|
|
284
|
-
return { icon: ICON.exists, color: colors.cyan };
|
|
285
|
-
case "warn":
|
|
286
|
-
return { icon: ICON.warn, color: colors.yellow };
|
|
287
|
-
case "skip":
|
|
288
|
-
return { icon: ICON.skip, color: colors.dim };
|
|
289
|
-
case "err":
|
|
290
|
-
return { icon: ICON.err, color: colors.red };
|
|
291
|
-
default:
|
|
292
|
-
return { icon: "?", color: (text) => text };
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function printSummary(results, { branch, mode }) {
|
|
297
|
-
const nameWidth = results.reduce(
|
|
298
|
-
(max, item) => Math.max(max, item.name.length),
|
|
299
|
-
0
|
|
300
|
-
);
|
|
301
|
-
|
|
302
|
-
console.log("");
|
|
303
|
-
console.log(colors.bold(t("CHECKOUT_RESULT_HEADER__MODE_BRANCH", { mode, branch })));
|
|
304
|
-
for (const result of results) {
|
|
305
|
-
const { icon, color } = styleForStatus(result.status);
|
|
306
|
-
const line = `${icon} ${result.name.padEnd(nameWidth)} ${result.message}`;
|
|
307
|
-
console.log(color(line));
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const ok = results.filter((r) => r.status === "ok").length;
|
|
311
|
-
const already = results.filter((r) => r.status === "exists").length;
|
|
312
|
-
const warned = results.filter((r) => r.status === "warn").length;
|
|
313
|
-
const skipped = results.filter((r) => r.status === "skip").length;
|
|
314
|
-
const errored = results.filter((r) => r.status === "err").length;
|
|
315
|
-
|
|
316
|
-
console.log("");
|
|
317
|
-
console.log(colors.dim(t("CHECKOUT_LEGEND")));
|
|
318
|
-
console.log(
|
|
319
|
-
colors.bold(
|
|
320
|
-
t("CHECKOUT_SUMMARY__OK_EXISTS_WARN_SKIP_ERR", {
|
|
321
|
-
ok, exists: already, warn: warned, skip: skipped, err: errored,
|
|
322
|
-
})
|
|
323
|
-
)
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
async function main() {
|
|
328
|
-
const args = parseCheckoutArgs(process.argv.slice(2));
|
|
329
|
-
if (args.help) {
|
|
330
|
-
printHelp();
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (!args.branch) {
|
|
335
|
-
console.error(t("CHECKOUT_MISSING_BRANCH"));
|
|
336
|
-
console.error(t("CHECKOUT_MISSING_BRANCH_HELP"));
|
|
337
|
-
process.exit(1);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const mode = args.forceCreate ? "reset -B" : args.create ? "create -b" : "switch";
|
|
341
|
-
|
|
342
|
-
const { repos, workspaceRoot } = loadManifestRepos({
|
|
343
|
-
manifestPath: args.manifest,
|
|
344
|
-
baseDir: args.baseDir,
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
const targets = await resolveMultiTargets({
|
|
348
|
-
repos,
|
|
349
|
-
workspaceRoot,
|
|
350
|
-
includeWorkspace: !args.noWorkspace,
|
|
351
|
-
selection: args.selection,
|
|
352
|
-
action: t("CHECKOUT_SELECT_ACTION__MODE_BRANCH", { mode, branch: args.branch }),
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
const fromPart = args.startPoint
|
|
356
|
-
? t("CHECKOUT_HEADER_FROM_SUFFIX__START", { start: args.startPoint })
|
|
357
|
-
: "";
|
|
358
|
-
console.log("");
|
|
359
|
-
console.log(
|
|
360
|
-
colors.bold(
|
|
361
|
-
t("CHECKOUT_HEADER__MODE_BRANCH_FROM_N", {
|
|
362
|
-
mode, branch: args.branch, fromPart, n: targets.length,
|
|
363
|
-
})
|
|
364
|
-
)
|
|
365
|
-
);
|
|
366
|
-
for (const target of targets) {
|
|
367
|
-
console.log(colors.dim(` • ${target.name} (${target.destination})`));
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const results = [];
|
|
371
|
-
for (const target of targets) {
|
|
372
|
-
const result = processRepo({
|
|
373
|
-
name: target.name,
|
|
374
|
-
destination: target.destination,
|
|
375
|
-
branch: args.branch,
|
|
376
|
-
create: args.create,
|
|
377
|
-
forceCreate: args.forceCreate,
|
|
378
|
-
startPoint: args.startPoint,
|
|
379
|
-
dryRun: args.dryRun,
|
|
380
|
-
});
|
|
381
|
-
results.push(result);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
printSummary(results, { branch: args.branch, mode });
|
|
385
|
-
|
|
386
|
-
const hasError = results.some(
|
|
387
|
-
(item) => item.status === "err" || item.status === "warn"
|
|
388
|
-
);
|
|
389
|
-
process.exit(hasError ? 1 : 0);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
main().catch((error) => {
|
|
393
|
-
console.error(error.message);
|
|
394
|
-
process.exit(1);
|
|
395
|
-
});
|
package/lib/git/git-clone.js
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/*
|
|
3
|
-
* Clone PAN Stack project repositories from a small clone manifest.
|
|
4
|
-
* Compact summary table (giống pull/push/fetch): repo · result · note — KHÔNG in
|
|
5
|
-
* [warn]/lỗi rời rạc theo từng repo, chỉ tổng hợp cuối (cloned · đã có sẵn · không
|
|
6
|
-
* có quyền · không tìm thấy · lỗi khác).
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const fs = require("fs");
|
|
10
|
-
const path = require("path");
|
|
11
|
-
const { spawnSync } = require("child_process");
|
|
12
|
-
const { spawnOptions } = require("../platform");
|
|
13
|
-
const { parseCommonArgs, loadManifestRepos, resolveProjectWorkspace, printCommonOptions } = require("./helpers/cli");
|
|
14
|
-
const { looksLikeGitUrl, repoNameFromUrl, appendRepository } = require("./helpers/manifest");
|
|
15
|
-
const { printSummary, statusBadge, bulkColumns } = require("./helpers/bulk-report");
|
|
16
|
-
const colors = require("./helpers/colors");
|
|
17
|
-
const { t } = require("../i18n");
|
|
18
|
-
|
|
19
|
-
// Dòng lỗi ngắn gọn cho cột "note" — bỏ qua dòng tiến trình "Cloning into '<path dài>'..." (luôn
|
|
20
|
-
// là dòng đầu của git clone, không có ý nghĩa), ưu tiên dòng thật (fatal:/remote:).
|
|
21
|
-
function shortCloneError(stderr) {
|
|
22
|
-
const lines = stderr.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
23
|
-
const meaningful = lines.find((l) => /^(fatal:|remote:|error:)/i.test(l)) || lines[lines.length - 1] || lines[0] || "";
|
|
24
|
-
return meaningful.slice(0, 100);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Phân loại lỗi clone từ stderr — KHÔNG in message thô cho user, chỉ dùng để chọn nhãn tổng hợp.
|
|
28
|
-
function classifyCloneError(stderr) {
|
|
29
|
-
if (/permission denied|authentication failed|access denied|403|could not read username|invalid username or password/i.test(stderr)) {
|
|
30
|
-
return "no-access";
|
|
31
|
-
}
|
|
32
|
-
if (/repository .* not found|not found|does not exist|404/i.test(stderr)) {
|
|
33
|
-
return "not-found";
|
|
34
|
-
}
|
|
35
|
-
return "failed";
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function cloneRepo({ name, url, destination, dryRun }) {
|
|
39
|
-
if (fs.existsSync(destination)) {
|
|
40
|
-
return { name, exists: true };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (dryRun) {
|
|
44
|
-
console.log(colors.dim(`[dry-run] git clone ${url} ${destination}`));
|
|
45
|
-
return { name, dryRun: true };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
49
|
-
process.stdout.write(colors.dim(` ⇩ ${name} ... `));
|
|
50
|
-
const result = spawnSync("git", ["clone", url, destination], spawnOptions({ encoding: "utf8" }));
|
|
51
|
-
const ok = result.status === 0;
|
|
52
|
-
const stderr = (result.stderr || "").trim();
|
|
53
|
-
const category = ok ? "ok" : classifyCloneError(stderr);
|
|
54
|
-
const shortMsg = ok ? t("CLONE_STATUS_CLONED")
|
|
55
|
-
: category === "no-access" ? t("CLONE_STATUS_NO_ACCESS")
|
|
56
|
-
: category === "not-found" ? t("CLONE_STATUS_NOT_FOUND")
|
|
57
|
-
: t("BULK_STATUS_FAILED");
|
|
58
|
-
console.log((ok ? colors.green : category === "no-access" ? colors.yellow : colors.red)(shortMsg));
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
name,
|
|
62
|
-
status: result.status,
|
|
63
|
-
category,
|
|
64
|
-
error: !ok ? shortCloneError(stderr) : "",
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function row(r) {
|
|
69
|
-
if (r.dryRun) return { repo: r.name, result: colors.dim(t("BULK_STATUS_DRY_RUN")), note: "" };
|
|
70
|
-
if (r.exists) return { repo: r.name, result: statusBadge("skip", t("CLONE_STATUS_EXISTS")), note: "" };
|
|
71
|
-
if (!r.error) return { repo: r.name, result: statusBadge("ok", t("CLONE_STATUS_CLONED")), note: "" };
|
|
72
|
-
const label =
|
|
73
|
-
r.category === "no-access" ? t("CLONE_STATUS_NO_ACCESS")
|
|
74
|
-
: r.category === "not-found" ? t("CLONE_STATUS_NOT_FOUND")
|
|
75
|
-
: t("BULK_STATUS_FAILED");
|
|
76
|
-
const kind = r.category === "no-access" ? "warn" : "fail";
|
|
77
|
-
return { repo: r.name, result: statusBadge(kind, label), note: colors.dim(r.error) };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function printHelp() {
|
|
81
|
-
console.log(`usage: git-clone.js [--base-dir BASE_DIR] [--dry-run] [manifest]
|
|
82
|
-
git-clone.js <git-url> [--dry-run]
|
|
83
|
-
|
|
84
|
-
Không tham số (hoặc [manifest]) → clone TẤT CẢ repo trong manifest (clone all).
|
|
85
|
-
Có <git-url> (https://…, git@host:…, hoặc kết thúc .git) → tự tách tên dự án từ
|
|
86
|
-
URL, THÊM 1 entry mới vào cuối 'repositories:' trong .project.yaml (path mặc
|
|
87
|
-
định 'apps/<tên>'), rồi clone NGAY repo đó — tương tự 'git clone <url>' nhưng
|
|
88
|
-
tiện hơn: repo được ghi lại vào manifest luôn, lần sau 'pan clone'/'pan pull'... tự
|
|
89
|
-
thấy nó. Đã có tên đó trong manifest rồi → KHÔNG thêm trùng, dùng lại entry cũ.
|
|
90
|
-
|
|
91
|
-
Prints a compact summary table (repo · result · note) — KHÔNG dừng ở lỗi đầu
|
|
92
|
-
tiên, clone hết các repo rồi tổng hợp (đã có sẵn / không có quyền / không tìm
|
|
93
|
-
thấy / lỗi khác) ở cuối.
|
|
94
|
-
|
|
95
|
-
${printCommonOptions()}`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function printCloneSummary(results, dryRun) {
|
|
99
|
-
const cloned = results.filter((r) => !r.dryRun && !r.exists && !r.error);
|
|
100
|
-
const exists = results.filter((r) => r.exists);
|
|
101
|
-
const noAccess = results.filter((r) => r.category === "no-access");
|
|
102
|
-
const notFound = results.filter((r) => r.category === "not-found");
|
|
103
|
-
const failed = results.filter((r) => r.error && r.category === "failed");
|
|
104
|
-
const rows = results.map(row);
|
|
105
|
-
|
|
106
|
-
const footer = [];
|
|
107
|
-
footer.push(colors.dim(t("CLONE_FOOTER_COUNTS__TOTAL_CLONED_EXISTS_NOACCESS_NOTFOUND_FAILED", {
|
|
108
|
-
total: results.length, cloned: cloned.length, exists: exists.length,
|
|
109
|
-
noAccess: noAccess.length, notFound: notFound.length, failed: failed.length,
|
|
110
|
-
})));
|
|
111
|
-
if (noAccess.length) {
|
|
112
|
-
footer.push("");
|
|
113
|
-
footer.push(colors.yellow(t("CLONE_NO_ACCESS_HEADER")));
|
|
114
|
-
for (const r of noAccess) footer.push(` ${colors.cyan(r.name)}`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (!dryRun && results.every((r) => r.exists)) {
|
|
118
|
-
console.log("");
|
|
119
|
-
console.log(colors.bold(`─── ${t("CLONE_TITLE")} ───`));
|
|
120
|
-
console.log(colors.green(t("CLONE_ALL_EXIST__N", { n: results.length })));
|
|
121
|
-
for (const line of footer) console.log(line);
|
|
122
|
-
} else {
|
|
123
|
-
printSummary({
|
|
124
|
-
title: t("CLONE_TITLE"),
|
|
125
|
-
rows,
|
|
126
|
-
columns: bulkColumns([
|
|
127
|
-
{ key: "repo", labelKey: "BULK_COL_REPO" },
|
|
128
|
-
{ key: "result", labelKey: "BULK_COL_RESULT" },
|
|
129
|
-
{ key: "note", labelKey: "BULK_COL_NOTE" },
|
|
130
|
-
]),
|
|
131
|
-
footerLines: footer,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (noAccess.length || notFound.length || failed.length) process.exitCode = 1;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// `pan clone <url>` — thêm 1 repo mới vào .project.yaml (KHÔNG phải .project.local.yaml — repo
|
|
139
|
-
// thêm qua đường này coi như chính thức, muốn giữ riêng thì tự thêm tay vào .project.local.yaml)
|
|
140
|
-
// rồi clone ngay repo đó. Đã có tên trùng trong manifest (kể cả từ .project.local.yaml) → dùng
|
|
141
|
-
// lại entry cũ, không thêm trùng.
|
|
142
|
-
function runAddAndCloneOne(urlArg, { baseDirArg, dryRun }) {
|
|
143
|
-
const name = repoNameFromUrl(urlArg);
|
|
144
|
-
if (!name) {
|
|
145
|
-
throw new Error(`Không tách được tên dự án từ URL: ${urlArg}`);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const { manifestPath, baseDir } = resolveProjectWorkspace({ baseDir: baseDirArg || "." });
|
|
149
|
-
const { repos: existingRepos } = loadManifestRepos({ baseDir: baseDirArg, allowFallback: false });
|
|
150
|
-
const already = existingRepos.find((r) => r.name === name);
|
|
151
|
-
|
|
152
|
-
let url = urlArg;
|
|
153
|
-
let destination = path.resolve(baseDir, "apps", name);
|
|
154
|
-
if (already) {
|
|
155
|
-
console.log(colors.dim(`ℹ️ '${name}' đã có trong manifest — dùng entry hiện có (${already.url}).`));
|
|
156
|
-
url = already.url;
|
|
157
|
-
destination = already.destination;
|
|
158
|
-
} else {
|
|
159
|
-
const repoPath = `apps/${name}`;
|
|
160
|
-
if (dryRun) {
|
|
161
|
-
console.log(colors.dim(`[dry-run] + thêm '${name}' vào ${manifestPath} (url: ${urlArg}, path: ${repoPath})`));
|
|
162
|
-
} else {
|
|
163
|
-
appendRepository(manifestPath, { name, url: urlArg, path: repoPath });
|
|
164
|
-
console.log(colors.green(`✓ Đã thêm '${name}' vào ${manifestPath} (path: ${repoPath})`));
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const result = cloneRepo({ name, url, destination, dryRun });
|
|
169
|
-
printCloneSummary([result], dryRun);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function main() {
|
|
173
|
-
const rawArgs = process.argv.slice(2);
|
|
174
|
-
if (rawArgs.includes("-h") || rawArgs.includes("--help")) {
|
|
175
|
-
printHelp();
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const dryRun = rawArgs.includes("--dry-run");
|
|
180
|
-
const baseDirIdx = rawArgs.indexOf("--base-dir");
|
|
181
|
-
const baseDirArg = baseDirIdx >= 0 ? rawArgs[baseDirIdx + 1] : "";
|
|
182
|
-
const urlArg = rawArgs.find((a, i) => !a.startsWith("--") && rawArgs[i - 1] !== "--base-dir" && looksLikeGitUrl(a));
|
|
183
|
-
|
|
184
|
-
if (urlArg) {
|
|
185
|
-
runAddAndCloneOne(urlArg, { baseDirArg, dryRun });
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const args = parseCommonArgs(rawArgs);
|
|
190
|
-
|
|
191
|
-
const { repos } = loadManifestRepos({
|
|
192
|
-
manifestPath: args.manifest,
|
|
193
|
-
baseDir: args.baseDir,
|
|
194
|
-
allowFallback: false,
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
const results = repos.map((r) => cloneRepo({ name: r.name, url: r.url, destination: r.destination, dryRun: args.dryRun }));
|
|
198
|
-
printCloneSummary(results, args.dryRun);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
try {
|
|
202
|
-
main();
|
|
203
|
-
} catch (error) {
|
|
204
|
-
console.error(error.message);
|
|
205
|
-
process.exit(1);
|
|
206
|
-
}
|