pan-stack-cli 0.1.4 → 0.2.1
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 +572 -23
- package/bin/pan.js +206 -20
- package/lib/ai-skills.js +829 -0
- package/lib/create.js +22 -4
- package/lib/discover.js +69 -2
- package/lib/git/git-checkout.js +395 -0
- package/lib/git/git-clone.js +163 -28
- package/lib/git/git-fetch-all.js +100 -33
- package/lib/git/git-grv.js +68 -37
- package/lib/git/git-gst.js +97 -26
- package/lib/git/git-mr.js +546 -0
- package/lib/git/git-pull.js +104 -43
- package/lib/git/git-push.js +150 -41
- package/lib/git/git-sync-main.js +334 -0
- package/lib/git/helpers/bulk-report.js +202 -0
- package/lib/git/helpers/cli.js +41 -5
- package/lib/git/helpers/colors.js +19 -0
- package/lib/git/helpers/git.js +74 -0
- package/lib/git/helpers/manifest.js +93 -0
- package/lib/git/helpers/repo-select.js +416 -0
- package/lib/git/helpers/repo-summary.js +161 -0
- package/lib/i18n.js +61 -0
- package/lib/inquirer-theme.js +20 -0
- package/lib/language.js +121 -0
- package/lib/languages.js +14 -0
- package/lib/manifest.js +115 -14
- package/lib/menu-prompt.js +201 -0
- package/lib/menu.js +134 -0
- package/lib/messages.js +647 -0
- package/lib/run-command.js +173 -52
- package/lib/select.js +0 -0
- package/lib/serve.js +75 -0
- package/lib/skill-registry-client.js +173 -0
- package/lib/stats/aggregate.js +51 -0
- package/lib/stats/identity.js +20 -0
- package/lib/stats/index.js +189 -0
- package/lib/stats/parse.js +46 -0
- package/lib/stats/range.js +38 -0
- package/lib/stats/report.js +264 -0
- package/lib/stats/review.js +157 -0
- package/lib/stats/scan.js +92 -0
- package/lib/stats/serve.js +25 -0
- package/lib/ui/api.js +202 -0
- package/lib/ui/clone.js +115 -0
- package/lib/ui/config.js +182 -0
- package/lib/ui/diff.js +128 -0
- package/lib/ui/export-gate.js +79 -0
- package/lib/ui/gen-react-mobile-code.js +6 -0
- package/lib/ui/gen-react-web-code.js +75 -0
- package/lib/ui/index.js +89 -0
- package/lib/ui/init.js +252 -0
- package/lib/ui/login.js +128 -0
- package/lib/ui/logout.js +22 -0
- package/lib/ui/palettes.js +103 -0
- package/lib/ui/published-html.js +24 -0
- package/lib/ui/sync.js +137 -0
- package/lib/ui/workspace.js +213 -0
- package/package.json +10 -3
- package/templates/workspace/.claude/skills/writing-guidelines/SKILL.md +0 -48
- package/templates/workspace/.codex/skills/writing-guidelines/SKILL.md +0 -48
- package/templates/workspace/.cursor/skills/writing-guidelines/SKILL.md +0 -48
- package/templates/workspace/.pan.yaml +0 -25
- package/templates/workspace/.project.yaml +0 -15
- package/templates/workspace/README.md +0 -35
package/lib/create.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
|
+
const os = require("os");
|
|
2
3
|
const path = require("path");
|
|
3
4
|
const readline = require("readline");
|
|
4
5
|
const { validateFolderName } = require("./platform");
|
|
6
|
+
const { getRegistryBaseUrl, fetchRegistry, downloadAndExtract } = require("./skill-registry-client");
|
|
5
7
|
|
|
6
|
-
const
|
|
8
|
+
const TEMPLATE_NAME = "workspace";
|
|
7
9
|
|
|
8
10
|
function promptFolderName() {
|
|
9
11
|
return new Promise((resolve, reject) => {
|
|
@@ -32,6 +34,7 @@ function promptFolderName() {
|
|
|
32
34
|
function copyDir(src, dest) {
|
|
33
35
|
fs.mkdirSync(dest, { recursive: true });
|
|
34
36
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
37
|
+
if (entry.name === "template.json") continue; // metadata riêng cho registry, không phải file scaffold
|
|
35
38
|
const srcPath = path.join(src, entry.name);
|
|
36
39
|
const destPath = path.join(dest, entry.name);
|
|
37
40
|
if (entry.isDirectory()) {
|
|
@@ -67,11 +70,26 @@ async function runCreate(args = []) {
|
|
|
67
70
|
throw new Error(`Folder already exists: ${targetDir}`);
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
// Template scaffold không còn bundle sẵn trong pan-stack-cli — tải từ registry server (giống
|
|
74
|
+
// cơ chế `pan ai install`). Cần PAN_SKILL_REGISTRY_URL (hoặc default localhost:4600) chạy được.
|
|
75
|
+
const baseUrl = getRegistryBaseUrl();
|
|
76
|
+
const items = await fetchRegistry(baseUrl);
|
|
77
|
+
const template = items.find((i) => i.type === "template" && i.name === TEMPLATE_NAME);
|
|
78
|
+
if (!template) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Không tìm thấy template '${TEMPLATE_NAME}' trên registry server (${baseUrl}). Kiểm tra apps/pan-agent-skill-registry đã build/deploy đủ registry/templates/${TEMPLATE_NAME} chưa.`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const stagingDir = fs.mkdtempSync(path.join(os.tmpdir(), "pan-create-"));
|
|
85
|
+
let extractedDir;
|
|
86
|
+
try {
|
|
87
|
+
extractedDir = await downloadAndExtract(baseUrl, template, stagingDir);
|
|
88
|
+
copyDir(extractedDir, targetDir);
|
|
89
|
+
} finally {
|
|
90
|
+
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
72
91
|
}
|
|
73
92
|
|
|
74
|
-
copyDir(TEMPLATE_ROOT, targetDir);
|
|
75
93
|
applyPlaceholders(targetDir, folderName);
|
|
76
94
|
|
|
77
95
|
console.log(`Created PAN Stack workspace: ${targetDir}`);
|
package/lib/discover.js
CHANGED
|
@@ -12,8 +12,33 @@ const {
|
|
|
12
12
|
cleanValue,
|
|
13
13
|
} = require("./manifest");
|
|
14
14
|
|
|
15
|
-
const BUILTIN_GIT_COMMANDS = new Set([
|
|
16
|
-
|
|
15
|
+
const BUILTIN_GIT_COMMANDS = new Set([
|
|
16
|
+
"clone",
|
|
17
|
+
"pull",
|
|
18
|
+
"sync-main",
|
|
19
|
+
"sm",
|
|
20
|
+
"fetch",
|
|
21
|
+
"gfa",
|
|
22
|
+
"push",
|
|
23
|
+
"grv",
|
|
24
|
+
"gst",
|
|
25
|
+
"mr",
|
|
26
|
+
"checkout",
|
|
27
|
+
"co",
|
|
28
|
+
]);
|
|
29
|
+
const BUILTIN_WORKSPACE_COMMANDS = new Set([
|
|
30
|
+
"create",
|
|
31
|
+
"serve",
|
|
32
|
+
"language",
|
|
33
|
+
"lang",
|
|
34
|
+
"ai",
|
|
35
|
+
"skills",
|
|
36
|
+
"ui",
|
|
37
|
+
"stats",
|
|
38
|
+
"graph",
|
|
39
|
+
"git",
|
|
40
|
+
...BUILTIN_GIT_COMMANDS,
|
|
41
|
+
]);
|
|
17
42
|
|
|
18
43
|
function readProjectRoots() {
|
|
19
44
|
const roots = [];
|
|
@@ -68,6 +93,47 @@ function findNearestProjectManifest(startDir = process.cwd()) {
|
|
|
68
93
|
return walked[0] || null;
|
|
69
94
|
}
|
|
70
95
|
|
|
96
|
+
const DISCOVER_SKIP_DIRS = new Set([
|
|
97
|
+
".git", "node_modules", ".pnpm", "dist", "build", ".next", "target", "vendor",
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Fallback khi không có .project.yaml: quét git repo từ CHÍNH thư mục đang gõ
|
|
102
|
+
* lệnh (startDir) xuống tối đa `maxDepth` cấp — bao gồm chính startDir, thư mục
|
|
103
|
+
* con trực tiếp, và các cấp con tiếp theo (đủ bắt submodule). Dùng chung cho
|
|
104
|
+
* `pan stats` và các lệnh git built-in (`pan pull|push|fetch|checkout|...`) khi
|
|
105
|
+
* chạy ngoài workspace pan-stack (không có `.project.yaml`).
|
|
106
|
+
* @returns {Array<{name,dir}>}
|
|
107
|
+
*/
|
|
108
|
+
function discoverGitRepos(startDir = process.cwd(), maxDepth = 2) {
|
|
109
|
+
const base = path.resolve(startDir);
|
|
110
|
+
const found = [];
|
|
111
|
+
const seen = new Set();
|
|
112
|
+
|
|
113
|
+
function walk(dir, depth) {
|
|
114
|
+
if (depth > maxDepth) return;
|
|
115
|
+
let entries;
|
|
116
|
+
try {
|
|
117
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
118
|
+
} catch {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (entries.some((e) => e.name === ".git")) {
|
|
122
|
+
const abs = path.resolve(dir);
|
|
123
|
+
if (!seen.has(abs)) {
|
|
124
|
+
seen.add(abs);
|
|
125
|
+
found.push({ name: path.basename(abs), dir: abs });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const e of entries) {
|
|
129
|
+
if (e.isDirectory() && !DISCOVER_SKIP_DIRS.has(e.name)) walk(path.join(dir, e.name), depth + 1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
walk(base, 0);
|
|
134
|
+
return found.sort((a, b) => a.name.localeCompare(b.name));
|
|
135
|
+
}
|
|
136
|
+
|
|
71
137
|
function findNearestWorkspace(startDir = process.cwd()) {
|
|
72
138
|
const manifestPath = findNearestProjectManifest(startDir);
|
|
73
139
|
if (!manifestPath) {
|
|
@@ -216,6 +282,7 @@ module.exports = {
|
|
|
216
282
|
PAN_CONFIG_FILE,
|
|
217
283
|
findNearestProjectManifest,
|
|
218
284
|
findNearestWorkspace,
|
|
285
|
+
discoverGitRepos,
|
|
219
286
|
loadAllCommandSources,
|
|
220
287
|
loadCommandSourcesForWorkspace,
|
|
221
288
|
findSourcesWithCommand,
|
|
@@ -0,0 +1,395 @@
|
|
|
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
|
+
});
|