pan-stack-cli 0.1.2 → 0.1.5
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 +161 -96
- package/bin/pan.js +30 -14
- package/lib/create.js +87 -0
- package/lib/discover.js +22 -8
- package/lib/git/git-clone.js +2 -1
- package/lib/git/git-grv.js +3 -3
- package/lib/git/git-gst.js +3 -3
- package/lib/git/git-sync-main.js +320 -0
- package/lib/git/helpers/cli.js +3 -2
- package/lib/git/helpers/colors.js +19 -0
- package/lib/git/helpers/git.js +80 -5
- package/lib/git/helpers/repo-select.js +97 -0
- package/lib/manifest.js +1 -1
- package/lib/platform.js +140 -0
- package/lib/run-command.js +25 -20
- package/package.json +4 -3
- package/templates/workspace/.claude/skills/writing-guidelines/SKILL.md +48 -0
- package/templates/workspace/.codex/skills/writing-guidelines/SKILL.md +48 -0
- package/templates/workspace/.cursor/skills/writing-guidelines/SKILL.md +48 -0
- package/templates/workspace/.pan.yaml +25 -0
- package/templates/workspace/.project.yaml +15 -0
- package/templates/workspace/README.md +35 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Sync origin/main (or --ref) into the current branch for selected PAN Stack repos.
|
|
4
|
+
*
|
|
5
|
+
* Fetches first, then merge (default) or rebase (--rebase). Interactive repo picker
|
|
6
|
+
* when TTY; pass repo name or "all" as first positional argument.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
looksLikeManifest,
|
|
11
|
+
loadManifestRepos,
|
|
12
|
+
printCommonOptions,
|
|
13
|
+
} = require("./helpers/cli");
|
|
14
|
+
const colors = require("./helpers/colors");
|
|
15
|
+
const { resolveTargets } = require("./helpers/repo-select");
|
|
16
|
+
const {
|
|
17
|
+
ensureRepoExists,
|
|
18
|
+
runGit,
|
|
19
|
+
runGitCapture,
|
|
20
|
+
getCurrentBranch,
|
|
21
|
+
refExists,
|
|
22
|
+
listConflictedFiles,
|
|
23
|
+
isMergeOrRebaseInProgress,
|
|
24
|
+
countDirtyFiles,
|
|
25
|
+
} = require("./helpers/git");
|
|
26
|
+
|
|
27
|
+
const ICON = {
|
|
28
|
+
ok: "✅",
|
|
29
|
+
warn: "⚠️",
|
|
30
|
+
conflict: "⛔",
|
|
31
|
+
skip: "⏭️",
|
|
32
|
+
info: "ℹ️",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function printHelp() {
|
|
36
|
+
console.log(`usage: pan sync-main [repo|all] [options] (alias: pan sm)
|
|
37
|
+
|
|
38
|
+
Fetch remote main (or --ref) and merge into the current branch for workspace and/or child repos.
|
|
39
|
+
|
|
40
|
+
positional:
|
|
41
|
+
repo|all Repo name from .project.yaml, or "all". Interactive picker if omitted (TTY).
|
|
42
|
+
|
|
43
|
+
${printCommonOptions()}
|
|
44
|
+
--ref REF Remote-tracking ref to merge (default: origin/main)
|
|
45
|
+
--rebase Use git rebase instead of merge
|
|
46
|
+
--no-workspace Only manifest child repos (skip workspace stack root)`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseSyncMainArgs(argv) {
|
|
50
|
+
const args = {
|
|
51
|
+
help: false,
|
|
52
|
+
manifest: "",
|
|
53
|
+
baseDir: ".",
|
|
54
|
+
dryRun: false,
|
|
55
|
+
ref: "origin/main",
|
|
56
|
+
rebase: false,
|
|
57
|
+
noWorkspace: false,
|
|
58
|
+
selection: "",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
62
|
+
const arg = argv[index];
|
|
63
|
+
if (arg === "-h" || arg === "--help") {
|
|
64
|
+
args.help = true;
|
|
65
|
+
} else if (arg === "--base-dir") {
|
|
66
|
+
args.baseDir = argv[++index] || ".";
|
|
67
|
+
} else if (arg === "--dry-run") {
|
|
68
|
+
args.dryRun = true;
|
|
69
|
+
} else if (arg === "--ref") {
|
|
70
|
+
args.ref = argv[++index] || "origin/main";
|
|
71
|
+
} else if (arg === "--rebase") {
|
|
72
|
+
args.rebase = true;
|
|
73
|
+
} else if (arg === "--no-workspace") {
|
|
74
|
+
args.noWorkspace = true;
|
|
75
|
+
} else if (!arg.startsWith("-")) {
|
|
76
|
+
if (looksLikeManifest(arg) && !args.manifest) {
|
|
77
|
+
args.manifest = arg;
|
|
78
|
+
} else if (!args.selection) {
|
|
79
|
+
args.selection = arg;
|
|
80
|
+
} else {
|
|
81
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return args;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function printEarlyConflict({ name, branch, ref, files, mode }) {
|
|
92
|
+
const headline = colors.bold(
|
|
93
|
+
colors.red(`${ICON.conflict} [conflict] ${name} (${branch})`)
|
|
94
|
+
);
|
|
95
|
+
const detail = colors.yellow(
|
|
96
|
+
`${mode} ${ref} thất bại — ${files.length} file conflict`
|
|
97
|
+
);
|
|
98
|
+
console.log(`${headline} — ${detail}`);
|
|
99
|
+
for (const file of files.slice(0, 8)) {
|
|
100
|
+
console.log(colors.dim(` • ${file}`));
|
|
101
|
+
}
|
|
102
|
+
if (files.length > 8) {
|
|
103
|
+
console.log(colors.dim(` … và ${files.length - 8} file khác`));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function syncMainRepo({
|
|
108
|
+
name,
|
|
109
|
+
destination,
|
|
110
|
+
dryRun,
|
|
111
|
+
ref,
|
|
112
|
+
rebase,
|
|
113
|
+
}) {
|
|
114
|
+
const result = {
|
|
115
|
+
name,
|
|
116
|
+
destination,
|
|
117
|
+
branch: "",
|
|
118
|
+
status: "skipped",
|
|
119
|
+
message: "",
|
|
120
|
+
conflictFiles: [],
|
|
121
|
+
mode: rebase ? "rebase" : "merge",
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (!ensureRepoExists({ name, destination })) {
|
|
125
|
+
result.message = "repo không tồn tại hoặc chưa clone";
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const branch = getCurrentBranch(destination);
|
|
130
|
+
result.branch = branch || "(detached HEAD)";
|
|
131
|
+
if (!branch) {
|
|
132
|
+
result.message = "đang detached HEAD — bỏ qua";
|
|
133
|
+
console.log(`${ICON.skip} [skip] ${name}: ${result.message}`);
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const inProgress = isMergeOrRebaseInProgress(destination);
|
|
138
|
+
if (inProgress) {
|
|
139
|
+
const files = listConflictedFiles(destination);
|
|
140
|
+
result.status = "conflict";
|
|
141
|
+
result.conflictFiles = files;
|
|
142
|
+
result.message = `đang dở ${inProgress} — cần resolve trước`;
|
|
143
|
+
printEarlyConflict({ name, branch, ref, files, mode: inProgress });
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const dirtyCount = countDirtyFiles(destination);
|
|
148
|
+
if (dirtyCount > 0) {
|
|
149
|
+
console.log(
|
|
150
|
+
`${ICON.warn} ${colors.yellow(`[warn] ${name}: working tree có ${dirtyCount} thay đổi chưa commit`)}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (dryRun) {
|
|
155
|
+
console.log(
|
|
156
|
+
`[dry-run] (in ${destination}) git fetch origin && git ${rebase ? "rebase" : "merge"} ${ref}`
|
|
157
|
+
);
|
|
158
|
+
result.status = "dry-run";
|
|
159
|
+
result.message = "dry-run";
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log(`[fetch] ${name}: ${destination}`);
|
|
164
|
+
const fetch = runGitCapture(["fetch", "origin"], { cwd: destination });
|
|
165
|
+
if (fetch.status !== 0) {
|
|
166
|
+
result.status = "error";
|
|
167
|
+
result.message = fetch.stderr || "git fetch origin thất bại";
|
|
168
|
+
console.log(`${ICON.warn} ${colors.yellow(`[warn] ${name}: ${result.message}`)}`);
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!refExists(destination, ref)) {
|
|
173
|
+
result.status = "error";
|
|
174
|
+
result.message = `không tìm thấy ref ${ref} (chạy fetch trước?)`;
|
|
175
|
+
console.log(`${ICON.warn} ${colors.yellow(`[warn] ${name}: ${result.message}`)}`);
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const gitArgs = rebase ? ["rebase", ref] : ["merge", "--no-edit", ref];
|
|
180
|
+
console.log(`[${result.mode}] ${name} (${branch}) ← ${ref}`);
|
|
181
|
+
const action = runGit(gitArgs, {
|
|
182
|
+
cwd: destination,
|
|
183
|
+
dryRun: false,
|
|
184
|
+
action: result.mode,
|
|
185
|
+
name,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const conflictFiles = listConflictedFiles(destination);
|
|
189
|
+
if (conflictFiles.length > 0 || action !== 0) {
|
|
190
|
+
result.status = "conflict";
|
|
191
|
+
result.conflictFiles = conflictFiles.length > 0 ? conflictFiles : ["(xem git status)"];
|
|
192
|
+
result.message = `${result.mode} conflict`;
|
|
193
|
+
printEarlyConflict({
|
|
194
|
+
name,
|
|
195
|
+
branch,
|
|
196
|
+
ref,
|
|
197
|
+
files: result.conflictFiles,
|
|
198
|
+
mode: result.mode,
|
|
199
|
+
});
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
result.status = "ok";
|
|
204
|
+
result.message = "đã cập nhật";
|
|
205
|
+
console.log(`${ICON.ok} ${colors.green(`[ok] ${name}: ${branch} đã sync ${ref}`)}`);
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function printSummary(results, { ref, rebase }) {
|
|
210
|
+
const ok = results.filter((item) => item.status === "ok");
|
|
211
|
+
const conflicts = results.filter((item) => item.status === "conflict");
|
|
212
|
+
const errors = results.filter((item) => item.status === "error");
|
|
213
|
+
const skipped = results.filter(
|
|
214
|
+
(item) => item.status === "skipped" || item.status === "dry-run"
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
console.log("");
|
|
218
|
+
console.log(colors.bold("─── sync-main summary ───"));
|
|
219
|
+
console.log(
|
|
220
|
+
`${ICON.info} ref: ${ref} · mode: ${rebase ? "rebase" : "merge"} · repos: ${results.length}`
|
|
221
|
+
);
|
|
222
|
+
console.log(
|
|
223
|
+
` ${ICON.ok} ok: ${ok.length} · ${ICON.conflict} conflict: ${conflicts.length} · ${ICON.warn} error: ${errors.length} · ${ICON.skip} skip: ${skipped.length}`
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
if (conflicts.length > 0) {
|
|
227
|
+
console.log("");
|
|
228
|
+
console.log(colors.bold(colors.red(`${ICON.conflict} CONFLICT — cần xử lý thủ công`)));
|
|
229
|
+
for (const item of conflicts) {
|
|
230
|
+
console.log(colors.yellow(`\n ${item.name} (${item.branch})`));
|
|
231
|
+
console.log(colors.dim(` ${item.destination}`));
|
|
232
|
+
for (const file of item.conflictFiles) {
|
|
233
|
+
console.log(` • ${file}`);
|
|
234
|
+
}
|
|
235
|
+
const hint = item.mode === "rebase"
|
|
236
|
+
? "git add <file>… && git rebase --continue (hoặc git rebase --abort)"
|
|
237
|
+
: "git add <file>… && git commit";
|
|
238
|
+
console.log(colors.dim(` → ${hint}`));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (errors.length > 0) {
|
|
243
|
+
console.log("");
|
|
244
|
+
console.log(colors.bold(colors.yellow(`${ICON.warn} LỖI`)));
|
|
245
|
+
for (const item of errors) {
|
|
246
|
+
console.log(` • ${item.name}: ${item.message}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (conflicts.length > 0) {
|
|
251
|
+
console.log("");
|
|
252
|
+
console.log(
|
|
253
|
+
colors.bold(
|
|
254
|
+
colors.red(
|
|
255
|
+
`${ICON.conflict} Kết thúc với conflict — resolve từng repo ở trên rồi commit/rebase --continue`
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
);
|
|
259
|
+
} else if (errors.length > 0) {
|
|
260
|
+
console.log("");
|
|
261
|
+
console.log(
|
|
262
|
+
colors.bold(colors.yellow(`${ICON.warn} Kết thúc với lỗi — xem chi tiết ở trên`))
|
|
263
|
+
);
|
|
264
|
+
} else if (ok.length > 0) {
|
|
265
|
+
console.log("");
|
|
266
|
+
console.log(colors.green(`${ICON.ok} Tất cả repo đã chọn đã sync ${ref}`));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function main() {
|
|
271
|
+
const argv = process.argv.slice(2);
|
|
272
|
+
const args = parseSyncMainArgs(argv);
|
|
273
|
+
if (args.help) {
|
|
274
|
+
printHelp();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const { repos, workspaceRoot } = loadManifestRepos({
|
|
279
|
+
manifestPath: args.manifest,
|
|
280
|
+
baseDir: args.baseDir,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const targets = await resolveTargets({
|
|
284
|
+
repos,
|
|
285
|
+
workspaceRoot,
|
|
286
|
+
includeWorkspace: !args.noWorkspace,
|
|
287
|
+
selection: args.selection,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
console.log(
|
|
291
|
+
`${ICON.info} sync-main: ${targets.length} repo · ${args.rebase ? "rebase" : "merge"} ← ${args.ref}`
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const results = [];
|
|
295
|
+
for (const target of targets) {
|
|
296
|
+
results.push(
|
|
297
|
+
syncMainRepo({
|
|
298
|
+
name: target.name,
|
|
299
|
+
destination: target.destination,
|
|
300
|
+
dryRun: args.dryRun,
|
|
301
|
+
ref: args.ref,
|
|
302
|
+
rebase: args.rebase,
|
|
303
|
+
})
|
|
304
|
+
);
|
|
305
|
+
console.log("");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
printSummary(results, { ref: args.ref, rebase: args.rebase });
|
|
309
|
+
|
|
310
|
+
const hasConflict = results.some((item) => item.status === "conflict");
|
|
311
|
+
const hasError = results.some((item) => item.status === "error");
|
|
312
|
+
if (hasConflict || hasError) {
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
main().catch((error) => {
|
|
318
|
+
console.error(error.message);
|
|
319
|
+
process.exit(1);
|
|
320
|
+
});
|
package/lib/git/helpers/cli.js
CHANGED
|
@@ -72,7 +72,7 @@ function resolveProjectWorkspace({
|
|
|
72
72
|
if (!found) {
|
|
73
73
|
const label = manifestPath || ".project.yaml";
|
|
74
74
|
throw new Error(
|
|
75
|
-
`Manifest not found: ${label} (
|
|
75
|
+
`Manifest not found: ${label} (walked up from ${startDir})`
|
|
76
76
|
);
|
|
77
77
|
}
|
|
78
78
|
resolvedManifest = found;
|
|
@@ -140,12 +140,13 @@ function printCommonOptions() {
|
|
|
140
140
|
|
|
141
141
|
options:
|
|
142
142
|
-h, --help show this help message and exit
|
|
143
|
-
--base-dir BASE_DIR Base directory for relative repo paths. Default:
|
|
143
|
+
--base-dir BASE_DIR Base directory for relative repo paths. Default: directory containing .project.yaml (walk-up from cwd when needed).
|
|
144
144
|
--dry-run Print commands only`;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
module.exports = {
|
|
148
148
|
parseCommonArgs,
|
|
149
|
+
looksLikeManifest,
|
|
149
150
|
resolveProjectWorkspace,
|
|
150
151
|
loadManifestRepos,
|
|
151
152
|
printCommonOptions,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const supportsColor =
|
|
2
|
+
Boolean(process.stdout.isTTY) && !process.env.NO_COLOR && process.env.FORCE_COLOR !== "0";
|
|
3
|
+
|
|
4
|
+
function wrap(code, text) {
|
|
5
|
+
if (!supportsColor || text == null) {
|
|
6
|
+
return String(text ?? "");
|
|
7
|
+
}
|
|
8
|
+
return `\u001b[${code}m${text}\u001b[0m`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
supportsColor,
|
|
13
|
+
bold: (text) => wrap("1", text),
|
|
14
|
+
red: (text) => wrap("31", text),
|
|
15
|
+
green: (text) => wrap("32", text),
|
|
16
|
+
yellow: (text) => wrap("33", text),
|
|
17
|
+
cyan: (text) => wrap("36", text),
|
|
18
|
+
dim: (text) => wrap("2", text),
|
|
19
|
+
};
|
package/lib/git/helpers/git.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const { spawnSync } = require("child_process");
|
|
4
|
+
const { spawnOptions } = require("../../platform");
|
|
4
5
|
|
|
5
6
|
function isGitRepo(directory) {
|
|
6
7
|
return fs.existsSync(path.join(directory, ".git"));
|
|
@@ -14,7 +15,7 @@ function runGit(gitArgs, { cwd, dryRun, action, name }) {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
console.log(`[${action}] ${name}: ${cwd}`);
|
|
17
|
-
const result = spawnSync(command[0], command.slice(1), { stdio: "inherit", cwd });
|
|
18
|
+
const result = spawnSync(command[0], command.slice(1), spawnOptions({ stdio: "inherit", cwd }));
|
|
18
19
|
return result.status ?? 1;
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -31,10 +32,10 @@ function ensureRepoExists({ name, destination }) {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
function listRemotesVerbose(directory) {
|
|
34
|
-
const result = spawnSync("git", ["remote", "-v"], {
|
|
35
|
+
const result = spawnSync("git", ["remote", "-v"], spawnOptions({
|
|
35
36
|
cwd: directory,
|
|
36
37
|
encoding: "utf8",
|
|
37
|
-
});
|
|
38
|
+
}));
|
|
38
39
|
if (result.status !== 0) {
|
|
39
40
|
return [];
|
|
40
41
|
}
|
|
@@ -45,16 +46,90 @@ function listRemotesVerbose(directory) {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
function runGitStatus(directory, gitArgs = []) {
|
|
48
|
-
const result = spawnSync("git", ["status", ...gitArgs], {
|
|
49
|
+
const result = spawnSync("git", ["status", ...gitArgs], spawnOptions({
|
|
49
50
|
cwd: directory,
|
|
50
51
|
stdio: "inherit",
|
|
51
|
-
});
|
|
52
|
+
}));
|
|
52
53
|
return result.status ?? 1;
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
function runGitCapture(gitArgs, { cwd } = {}) {
|
|
57
|
+
const result = spawnSync("git", gitArgs, spawnOptions({
|
|
58
|
+
cwd,
|
|
59
|
+
encoding: "utf8",
|
|
60
|
+
}));
|
|
61
|
+
return {
|
|
62
|
+
status: result.status ?? 1,
|
|
63
|
+
stdout: (result.stdout || "").trim(),
|
|
64
|
+
stderr: (result.stderr || "").trim(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getCurrentBranch(directory) {
|
|
69
|
+
const branch = runGitCapture(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: directory });
|
|
70
|
+
if (branch.status !== 0) {
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
if (branch.stdout === "HEAD") {
|
|
74
|
+
return "";
|
|
75
|
+
}
|
|
76
|
+
return branch.stdout;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function refExists(directory, ref) {
|
|
80
|
+
const result = runGitCapture(["rev-parse", "--verify", ref], { cwd: directory });
|
|
81
|
+
return result.status === 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function listConflictedFiles(directory) {
|
|
85
|
+
const result = runGitCapture(
|
|
86
|
+
["diff", "--name-only", "--diff-filter=U"],
|
|
87
|
+
{ cwd: directory }
|
|
88
|
+
);
|
|
89
|
+
if (result.status !== 0) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
return result.stdout
|
|
93
|
+
.split(/\r?\n/)
|
|
94
|
+
.map((line) => line.trim())
|
|
95
|
+
.filter(Boolean);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isMergeOrRebaseInProgress(directory) {
|
|
99
|
+
const merge = runGitCapture(["rev-parse", "-q", "--verify", "MERGE_HEAD"], {
|
|
100
|
+
cwd: directory,
|
|
101
|
+
});
|
|
102
|
+
if (merge.status === 0) {
|
|
103
|
+
return "merge";
|
|
104
|
+
}
|
|
105
|
+
const rebaseMerge = fs.existsSync(path.join(directory, ".git", "rebase-merge"));
|
|
106
|
+
const rebaseApply = fs.existsSync(path.join(directory, ".git", "rebase-apply"));
|
|
107
|
+
if (rebaseMerge || rebaseApply) {
|
|
108
|
+
return "rebase";
|
|
109
|
+
}
|
|
110
|
+
return "";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function countDirtyFiles(directory) {
|
|
114
|
+
const result = runGitCapture(["status", "--porcelain"], { cwd: directory });
|
|
115
|
+
if (result.status !== 0) {
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
return result.stdout
|
|
119
|
+
.split(/\r?\n/)
|
|
120
|
+
.map((line) => line.trim())
|
|
121
|
+
.filter(Boolean).length;
|
|
122
|
+
}
|
|
123
|
+
|
|
55
124
|
module.exports = {
|
|
56
125
|
isGitRepo,
|
|
57
126
|
runGit,
|
|
127
|
+
runGitCapture,
|
|
128
|
+
getCurrentBranch,
|
|
129
|
+
refExists,
|
|
130
|
+
listConflictedFiles,
|
|
131
|
+
isMergeOrRebaseInProgress,
|
|
132
|
+
countDirtyFiles,
|
|
58
133
|
ensureRepoExists,
|
|
59
134
|
listRemotesVerbose,
|
|
60
135
|
runGitStatus,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const readline = require("readline");
|
|
2
|
+
const { isGitRepo } = require("./git");
|
|
3
|
+
|
|
4
|
+
function buildTargets({ repos, workspaceRoot, includeWorkspace }) {
|
|
5
|
+
const targets = [];
|
|
6
|
+
if (includeWorkspace && isGitRepo(workspaceRoot)) {
|
|
7
|
+
targets.push({
|
|
8
|
+
name: "workspace",
|
|
9
|
+
destination: workspaceRoot,
|
|
10
|
+
label: "workspace (stack root)",
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
for (const repo of repos) {
|
|
14
|
+
targets.push({
|
|
15
|
+
name: repo.name,
|
|
16
|
+
destination: repo.destination,
|
|
17
|
+
label: repo.name,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return targets;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function findTargetByName(targets, name) {
|
|
24
|
+
const needle = String(name || "").trim();
|
|
25
|
+
if (!needle) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
return targets.filter((target) => target.name === needle);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function promptRepoChoice(targets) {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const rl = readline.createInterface({
|
|
34
|
+
input: process.stdin,
|
|
35
|
+
output: process.stdout,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
console.log("");
|
|
39
|
+
console.log("Chọn repo để sync-main:");
|
|
40
|
+
console.log(` 1. all (${targets.length} repo)`);
|
|
41
|
+
targets.forEach((target, index) => {
|
|
42
|
+
console.log(` ${index + 2}. ${target.label}`);
|
|
43
|
+
console.log(` ${target.destination}`);
|
|
44
|
+
});
|
|
45
|
+
console.log("");
|
|
46
|
+
|
|
47
|
+
rl.question("Nhập số (1 = all): ", (answer) => {
|
|
48
|
+
rl.close();
|
|
49
|
+
const choice = Number.parseInt(String(answer).trim(), 10);
|
|
50
|
+
if (!Number.isFinite(choice) || choice < 1 || choice > targets.length + 1) {
|
|
51
|
+
reject(new Error("Lựa chọn không hợp lệ."));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (choice === 1) {
|
|
55
|
+
resolve(targets);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
resolve([targets[choice - 2]]);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function resolveTargets({
|
|
64
|
+
repos,
|
|
65
|
+
workspaceRoot,
|
|
66
|
+
includeWorkspace,
|
|
67
|
+
selection,
|
|
68
|
+
}) {
|
|
69
|
+
const targets = buildTargets({ repos, workspaceRoot, includeWorkspace });
|
|
70
|
+
if (targets.length === 0) {
|
|
71
|
+
throw new Error("Không có repo git nào để sync-main.");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const picked = String(selection || "").trim();
|
|
75
|
+
if (picked === "all") {
|
|
76
|
+
return targets;
|
|
77
|
+
}
|
|
78
|
+
if (picked) {
|
|
79
|
+
const matches = findTargetByName(targets, picked);
|
|
80
|
+
if (matches.length === 0) {
|
|
81
|
+
const names = targets.map((target) => target.name).join(", ");
|
|
82
|
+
throw new Error(`Repo '${picked}' không có trong manifest. Có: ${names}`);
|
|
83
|
+
}
|
|
84
|
+
return matches;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (process.stdin.isTTY) {
|
|
88
|
+
return promptRepoChoice(targets);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return targets;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
buildTargets,
|
|
96
|
+
resolveTargets,
|
|
97
|
+
};
|
package/lib/manifest.js
CHANGED
|
@@ -323,7 +323,7 @@ function getCommandDefinition(source, commandName, subArg) {
|
|
|
323
323
|
const target = def.targets[targetName];
|
|
324
324
|
if (!target) {
|
|
325
325
|
const valid = Object.keys(def.targets).join(" | ");
|
|
326
|
-
throw new Error(`
|
|
326
|
+
throw new Error(`Valid targets for '${commandName}': ${valid}`);
|
|
327
327
|
}
|
|
328
328
|
return {
|
|
329
329
|
...target,
|