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 ADDED
@@ -0,0 +1,143 @@
1
+ # pan-stack-cli
2
+
3
+ CLI toàn cục cho PAN Stack workspace — thay script `./pan` ở root dự án.
4
+
5
+ Cài một lần, dùng ở mọi dự án có `.project.yaml`:
6
+
7
+ ```bash
8
+ npm i -g pan-stack-cli
9
+ # hoặc từ source (trước khi publish npm):
10
+ npm i -g /path/to/.pan/pan-stack-cli
11
+ ```
12
+
13
+ Sau đó gõ `pan` thay cho `./pan`.
14
+
15
+ ## Yêu cầu
16
+
17
+ - Node.js 18+
18
+ - Git (cho `pan clone`, `pull`, `fetch`, `push`, `grv`, `gst`)
19
+ - Công cụ riêng từng lệnh dự án (pnpm, docker, curl, …)
20
+
21
+ ## Lệnh git (built-in)
22
+
23
+ Đọc `repositories` từ `.project.yaml` gần nhất (walk-up từ thư mục hiện tại):
24
+
25
+ | Lệnh | Việc |
26
+ |------|------|
27
+ | `pan clone` | Clone tất cả repo trong manifest |
28
+ | `pan pull` | Pull tất cả repo đã clone |
29
+ | `pan fetch` | Fetch all remotes |
30
+ | `pan push` | Push và set upstream (`-u origin HEAD`) |
31
+ | `pan grv` | `git remote -v` — URL đầy đủ cho workspace + từng repo con |
32
+ | `pan gst` | `git status` cho workspace + từng repo con (vd `pan gst -sb`) |
33
+
34
+ Tuỳ chọn git (forward xuống script):
35
+
36
+ - `--base-dir DIR` — thư mục gốc cho path repo con (mặc định: `.`)
37
+ - `--dry-run` — chỉ in lệnh
38
+ - `[manifest]` — đường dẫn manifest (mặc định: `.project.yaml`)
39
+
40
+ ## Lệnh dự án (từ `.pan.yaml`)
41
+
42
+ Hai lớp cấu hình:
43
+
44
+ | File | Vai trò |
45
+ |------|---------|
46
+ | `.project.yaml` | Workspace: danh sách repo (`repositories`) — **không** chứa `commands` |
47
+ | `.pan.yaml` | Lệnh `pan` — đặt ở workspace root và/hoặc từng repo con |
48
+
49
+ CLI load **tất cả** `.pan.yaml` trong workspace (root + mỗi `repositories[].path`). Lệnh trùng tên → hỏi chọn repo (`--project`) hoặc tự chọn nếu arg khớp `targets` (vd `pan test fe`).
50
+
51
+ ### Schema `.pan.yaml` (repo con)
52
+
53
+ `cwd` / `exec` tương đối **root repo** (thư mục chứa file `.pan.yaml`):
54
+
55
+ ```yaml
56
+ commands:
57
+ be:
58
+ description: Deploy backend local
59
+ exec: scripts/deploy.sh
60
+
61
+ lint:
62
+ shell: |
63
+ set -euo pipefail
64
+ pnpm lint
65
+ ```
66
+
67
+ ### Schema `.pan.yaml` (workspace root)
68
+
69
+ Lệnh orchestration — `cwd` tương đối workspace root; dùng `PAN_WORKSPACE_ROOT` trong shell:
70
+
71
+ ```yaml
72
+ commands:
73
+ install:
74
+ shell: |
75
+ cd "${PAN_WORKSPACE_ROOT}/apps/frontend" && pnpm install
76
+
77
+ test:
78
+ default: fe
79
+ targets:
80
+ be:
81
+ cwd: apps/backend
82
+ exec: ./gradlew test
83
+ fe:
84
+ cwd: apps/frontend
85
+ exec: pnpm test
86
+ ```
87
+
88
+ ### Biến môi trường
89
+
90
+ | Biến | Ý nghĩa |
91
+ |------|---------|
92
+ | `PAN_WORKSPACE_ROOT` | Thư mục chứa `.project.yaml` |
93
+ | `PAN_REPO_ROOT` | Thư mục chứa `.pan.yaml` đang chạy |
94
+ | `PAN_PROJECT_ROOT` | Alias của `PAN_WORKSPACE_ROOT` |
95
+ | `PAN_ARG_1`, … | Args sau tên lệnh |
96
+
97
+ ### Nhiều repo trùng tên lệnh
98
+
99
+ CLI quét workspace gần nhất (walk-up `.project.yaml`) rồi load mọi `.pan.yaml` con.
100
+
101
+ ## Ví dụ (CSDL-CC)
102
+
103
+ Trong repo `btp-csdl-cc` (đã có `.project.yaml`):
104
+
105
+ ```bash
106
+ cd ~/work/btp/btp-csdl-cc
107
+
108
+ pan clone
109
+ pan install
110
+ pan be # deploy backend :8080
111
+ pan fe # Vite :5173
112
+ pan faker # pan-faker :8090
113
+ pan test fe # vitest
114
+ pan demo uc2 # use case demo qua faker
115
+ pan status
116
+ pan down # docker compose down (giữ volume)
117
+ pan reset -y # xoá volume + deploy lại
118
+ ```
119
+
120
+ ## `./pan` ở root repo
121
+
122
+ Script `./pan` là shim mỏng:
123
+
124
+ 1. Dùng `pan` global nếu đã cài
125
+ 2. Fallback `node .pan/pan-stack-cli/bin/pan.js`
126
+
127
+ ## Cấu trúc package
128
+
129
+ ```text
130
+ .pan/pan-stack-cli/
131
+ bin/pan.js # entry CLI
132
+ lib/ # manifest, discover, run-command, git/
133
+ lib/git/ # git-clone, pull, fetch, push, grv, gst (+ helpers)
134
+ references/ # hướng dẫn publish npm
135
+ ```
136
+
137
+ ## Phát triển local
138
+
139
+ Cài global từ source (khuyên dùng trước publish): [references/npm-development.md](references/npm-development.md).
140
+
141
+ ## Publish npm
142
+
143
+ Xem [references/npm-release.md](references/npm-release.md).
package/bin/pan.js ADDED
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+
3
+ const {
4
+ findNearestWorkspace,
5
+ loadAllCommandSources,
6
+ findSourcesWithCommand,
7
+ formatProjectList,
8
+ isBuiltinGitCommand,
9
+ } = require("../lib/discover");
10
+ const { listCommands } = require("../lib/manifest");
11
+ const { runWithProjectResolution, runGitScript, sourceLabel } = require("../lib/run-command");
12
+
13
+ const GIT_SCRIPT_MAP = {
14
+ clone: "git-clone.js",
15
+ pull: "git-pull.js",
16
+ fetch: "git-fetch-all.js",
17
+ push: "git-push.js",
18
+ grv: "git-grv.js",
19
+ gst: "git-gst.js",
20
+ };
21
+
22
+ function parseGlobalArgs(argv) {
23
+ const result = {
24
+ help: false,
25
+ project: "",
26
+ command: "",
27
+ args: [],
28
+ };
29
+
30
+ for (let index = 0; index < argv.length; index += 1) {
31
+ const arg = argv[index];
32
+ if (arg === "-h" || arg === "--help" || arg === "help") {
33
+ result.help = true;
34
+ } else if (arg === "--project") {
35
+ result.project = argv[++index] || "";
36
+ } else if (arg.startsWith("--project=")) {
37
+ result.project = arg.slice("--project=".length);
38
+ } else if (!result.command) {
39
+ result.command = arg;
40
+ } else {
41
+ result.args.push(arg);
42
+ }
43
+ }
44
+
45
+ return result;
46
+ }
47
+
48
+ function printUsage() {
49
+ console.log(`usage: pan <command> [options] [-- args...]
50
+
51
+ repo commands (built-in):
52
+ clone Clone all repositories from .project.yaml
53
+ pull Pull latest changes for all repositories
54
+ fetch Fetch all remotes for all repositories
55
+ push Push all repositories and set upstream
56
+ grv List git remotes -v (URL đầy đủ) cho workspace + repo con
57
+ gst Git status cho workspace + repo con (forward args: pan gst -sb)
58
+
59
+ project commands:
60
+ Gom từ .pan.yaml (workspace root + từng repo con trong .project.yaml)
61
+
62
+ options:
63
+ --project NAME Chọn repo khi nhiều .pan.yaml có cùng lệnh
64
+ --base-dir DIR (git commands) thư mục gốc repo con
65
+ --dry-run (git commands) chỉ in lệnh
66
+ -h, --help Hiện trợ giúp
67
+
68
+ env:
69
+ PAN_PROJECT_ROOTS Danh sách root workspace (phân cách bằng , hoặc ;)
70
+ ~/.pan/projects Mỗi dòng một workspace root
71
+
72
+ examples:
73
+ pan clone
74
+ pan grv
75
+ pan be
76
+ pan test fe
77
+ pan test --project csdl-cc-b-frontend-e2e
78
+ `);
79
+ }
80
+
81
+ function printAllCommands(sources) {
82
+ if (sources.length === 0) {
83
+ console.log("Chưa tìm thấy .pan.yaml (workspace hoặc repo con).");
84
+ return;
85
+ }
86
+
87
+ console.log("Lệnh đã load:");
88
+ for (const source of sources) {
89
+ const commands = listCommands(source);
90
+ if (commands.length === 0) {
91
+ continue;
92
+ }
93
+ console.log(`\n [${sourceLabel(source)}] ${source.root}`);
94
+ for (const command of commands) {
95
+ const targetHint = command.hasTargets
96
+ ? ` [${command.targets.join("|")}]`
97
+ : "";
98
+ const desc = command.description ? ` — ${command.description}` : "";
99
+ console.log(` ${command.name}${targetHint}${desc}`);
100
+ }
101
+ }
102
+ }
103
+
104
+ async function main() {
105
+ const parsed = parseGlobalArgs(process.argv.slice(2));
106
+
107
+ if (parsed.help || !parsed.command) {
108
+ printUsage();
109
+ const workspace = findNearestWorkspace();
110
+ if (workspace) {
111
+ console.log("");
112
+ console.log(`Workspace: ${workspace.project} (${workspace.root})`);
113
+ printAllCommands(loadAllCommandSources());
114
+ }
115
+ return;
116
+ }
117
+
118
+ const { command, args, project } = parsed;
119
+
120
+ if (isBuiltinGitCommand(command)) {
121
+ const gitArgs = [...args];
122
+ if (command === "push" && !gitArgs.includes("--set-upstream")) {
123
+ gitArgs.unshift("--set-upstream");
124
+ }
125
+ runGitScript(GIT_SCRIPT_MAP[command], gitArgs);
126
+ return;
127
+ }
128
+
129
+ const matches = findSourcesWithCommand(command);
130
+ if (matches.length > 1 && !project && process.stdin.isTTY) {
131
+ console.log(formatProjectList(matches, command));
132
+ }
133
+
134
+ await runWithProjectResolution(matches, command, args, project);
135
+ }
136
+
137
+ main().catch((error) => {
138
+ console.error(error.message);
139
+ process.exit(1);
140
+ });
@@ -0,0 +1,206 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const os = require("os");
4
+ const {
5
+ PAN_CONFIG_FILE,
6
+ loadManifest,
7
+ loadPanConfig,
8
+ getCommandDefinition,
9
+ validateRepo,
10
+ cleanValue,
11
+ } = require("./manifest");
12
+
13
+ const BUILTIN_GIT_COMMANDS = new Set(["clone", "pull", "fetch", "push", "grv", "gst"]);
14
+
15
+ function readProjectRoots() {
16
+ const roots = [];
17
+ const fromEnv = process.env.PAN_PROJECT_ROOTS || "";
18
+ for (const part of fromEnv.split(/[:;,]/)) {
19
+ const trimmed = part.trim();
20
+ if (trimmed) {
21
+ roots.push(path.resolve(trimmed));
22
+ }
23
+ }
24
+
25
+ const registry = path.join(os.homedir(), ".pan", "projects");
26
+ if (fs.existsSync(registry)) {
27
+ const content = fs.readFileSync(registry, "utf8");
28
+ for (const line of content.split(/\r?\n/)) {
29
+ const trimmed = line.trim();
30
+ if (!trimmed || trimmed.startsWith("#")) {
31
+ continue;
32
+ }
33
+ roots.push(path.resolve(trimmed));
34
+ }
35
+ }
36
+
37
+ return [...new Set(roots)];
38
+ }
39
+
40
+ function manifestPathInDir(dir) {
41
+ const candidate = path.join(dir, ".project.yaml");
42
+ return fs.existsSync(candidate) ? candidate : null;
43
+ }
44
+
45
+ function walkUpManifests(startDir) {
46
+ const found = [];
47
+ let current = path.resolve(startDir);
48
+ const seen = new Set();
49
+
50
+ while (true) {
51
+ const manifestPath = manifestPathInDir(current);
52
+ if (manifestPath && !seen.has(manifestPath)) {
53
+ seen.add(manifestPath);
54
+ found.push(manifestPath);
55
+ }
56
+ const parent = path.dirname(current);
57
+ if (parent === current) {
58
+ break;
59
+ }
60
+ current = parent;
61
+ }
62
+
63
+ return found;
64
+ }
65
+
66
+ function findNearestWorkspace(startDir = process.cwd()) {
67
+ const walked = walkUpManifests(startDir);
68
+ if (walked.length === 0) {
69
+ return null;
70
+ }
71
+ return loadManifest(walked[0]);
72
+ }
73
+
74
+ function loadCommandSourcesForWorkspace(workspace) {
75
+ const sources = [];
76
+ const workspaceRoot = workspace.root;
77
+ const workspacePan = path.join(workspaceRoot, PAN_CONFIG_FILE);
78
+
79
+ if (fs.existsSync(workspacePan)) {
80
+ sources.push(
81
+ loadPanConfig(workspacePan, {
82
+ workspaceRoot,
83
+ project: workspace.project,
84
+ repoName: workspace.project,
85
+ scope: "workspace",
86
+ })
87
+ );
88
+ }
89
+
90
+ for (const repo of workspace.repositories) {
91
+ try {
92
+ const { name, repoPath } = validateRepo(repo);
93
+ const repoDir = path.resolve(workspaceRoot, repoPath);
94
+ const repoPan = path.join(repoDir, PAN_CONFIG_FILE);
95
+ if (!fs.existsSync(repoPan)) {
96
+ continue;
97
+ }
98
+ sources.push(
99
+ loadPanConfig(repoPan, {
100
+ workspaceRoot,
101
+ project: name,
102
+ repoName: name,
103
+ scope: "repo",
104
+ })
105
+ );
106
+ } catch {
107
+ // skip invalid repo entries
108
+ }
109
+ }
110
+
111
+ return sources;
112
+ }
113
+
114
+ function collectWorkspaces(startDir = process.cwd()) {
115
+ const workspaces = new Map();
116
+
117
+ for (const manifestPath of walkUpManifests(startDir)) {
118
+ try {
119
+ const workspace = loadManifest(manifestPath);
120
+ workspaces.set(workspace.root, workspace);
121
+ } catch {
122
+ // ignore
123
+ }
124
+ }
125
+
126
+ for (const root of readProjectRoots()) {
127
+ const manifestPath = manifestPathInDir(root);
128
+ if (!manifestPath) {
129
+ continue;
130
+ }
131
+ try {
132
+ const workspace = loadManifest(manifestPath);
133
+ workspaces.set(workspace.root, workspace);
134
+ } catch {
135
+ // ignore
136
+ }
137
+ }
138
+
139
+ return [...workspaces.values()];
140
+ }
141
+
142
+ function loadAllCommandSources(startDir = process.cwd()) {
143
+ const sources = [];
144
+ const seen = new Set();
145
+
146
+ for (const workspace of collectWorkspaces(startDir)) {
147
+ for (const source of loadCommandSourcesForWorkspace(workspace)) {
148
+ const key = source.path;
149
+ if (seen.has(key)) {
150
+ continue;
151
+ }
152
+ seen.add(key);
153
+ sources.push(source);
154
+ }
155
+ }
156
+
157
+ return sources;
158
+ }
159
+
160
+ function sourceMatchesProject(source, projectFlag) {
161
+ const flag = projectFlag.trim();
162
+ return (
163
+ source.project === flag ||
164
+ source.repoName === flag ||
165
+ path.basename(source.root) === flag
166
+ );
167
+ }
168
+
169
+ function findSourcesWithCommand(commandName, startDir = process.cwd()) {
170
+ return loadAllCommandSources(startDir).filter(
171
+ (source) => source.commands[commandName]
172
+ );
173
+ }
174
+
175
+ function formatProjectList(matches, commandName) {
176
+ const lines = [`Lệnh '${commandName}' có ở nhiều nơi:`];
177
+ matches.forEach((source, index) => {
178
+ const def = source.commands[commandName];
179
+ const desc = def?.description ? ` — ${def.description}` : "";
180
+ const label =
181
+ source.scope === "workspace"
182
+ ? `${source.project} (workspace)`
183
+ : source.repoName || source.project;
184
+ lines.push(` ${index + 1}. ${label} (${source.root})${desc}`);
185
+ });
186
+ lines.push("");
187
+ lines.push("Chọn số thứ tự, hoặc chạy lại với --project <tên-repo>.");
188
+ return lines.join("\n");
189
+ }
190
+
191
+ function isBuiltinGitCommand(name) {
192
+ return BUILTIN_GIT_COMMANDS.has(name);
193
+ }
194
+
195
+ module.exports = {
196
+ BUILTIN_GIT_COMMANDS,
197
+ PAN_CONFIG_FILE,
198
+ findNearestWorkspace,
199
+ loadAllCommandSources,
200
+ loadCommandSourcesForWorkspace,
201
+ findSourcesWithCommand,
202
+ formatProjectList,
203
+ isBuiltinGitCommand,
204
+ getCommandDefinition,
205
+ sourceMatchesProject,
206
+ };
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Clone PAN Stack project repositories from a small clone manifest.
4
+ *
5
+ * The parser intentionally supports only the manifest shape documented in
6
+ * PAN_STACK.md. It avoids external dependencies so the script can run anywhere
7
+ * Node.js and Git are available.
8
+ */
9
+
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+ const { spawnSync } = require("child_process");
13
+ const { parseCommonArgs, loadManifestRepos, printCommonOptions } = require("./helpers/cli");
14
+
15
+ function cloneRepo({ name, url, destination, dryRun }) {
16
+ if (fs.existsSync(destination)) {
17
+ console.log(`[skip] ${name}: ${destination} already exists`);
18
+ return;
19
+ }
20
+
21
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
22
+ const command = ["git", "clone", url, destination];
23
+ if (dryRun) {
24
+ console.log(`[dry-run] ${command.join(" ")}`);
25
+ return;
26
+ }
27
+
28
+ console.log(`[clone] ${name}: ${url} -> ${destination}`);
29
+ const result = spawnSync(command[0], command.slice(1), { stdio: "inherit" });
30
+ if (result.status !== 0) {
31
+ console.log(`[warn] ${name}: clone failed, check repo access or url`);
32
+ }
33
+ }
34
+
35
+ function printHelp() {
36
+ console.log(`usage: git-clone.js [--base-dir BASE_DIR] [--dry-run] [manifest]
37
+
38
+ Clone repositories from a PAN Stack clone manifest.
39
+
40
+ ${printCommonOptions()}`);
41
+ }
42
+
43
+ function main() {
44
+ const args = parseCommonArgs(process.argv.slice(2));
45
+ if (args.help) {
46
+ printHelp();
47
+ return;
48
+ }
49
+
50
+ const repos = loadManifestRepos({
51
+ manifestPath: args.manifest,
52
+ baseDir: args.baseDir,
53
+ });
54
+
55
+ for (const repo of repos) {
56
+ cloneRepo({
57
+ name: repo.name,
58
+ url: repo.url,
59
+ destination: repo.destination,
60
+ dryRun: args.dryRun,
61
+ });
62
+ }
63
+ }
64
+
65
+ try {
66
+ main();
67
+ } catch (error) {
68
+ console.error(error.message);
69
+ process.exit(1);
70
+ }
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Fetch all remotes for PAN Stack project repositories from a clone manifest.
4
+ *
5
+ * Skips repositories that do not exist or are not git checkouts. Failures warn
6
+ * and continue so one broken repo does not block the rest.
7
+ */
8
+
9
+ const { parseCommonArgs, loadManifestRepos, printCommonOptions } = require("./helpers/cli");
10
+ const { ensureRepoExists, runGit } = require("./helpers/git");
11
+
12
+ function fetchRepo({ name, destination, dryRun }) {
13
+ if (!ensureRepoExists({ name, destination })) {
14
+ return;
15
+ }
16
+
17
+ const status = runGit(["fetch", "--all"], {
18
+ cwd: destination,
19
+ dryRun,
20
+ action: "fetch",
21
+ name,
22
+ });
23
+ if (status !== 0) {
24
+ console.log(`[warn] ${name}: fetch failed, check remote access`);
25
+ }
26
+ }
27
+
28
+ function printHelp() {
29
+ console.log(`usage: git-fetch-all.js [--base-dir BASE_DIR] [--dry-run] [manifest]
30
+
31
+ Fetch all remotes for repositories listed in a PAN Stack clone manifest.
32
+
33
+ ${printCommonOptions()}`);
34
+ }
35
+
36
+ function main() {
37
+ const args = parseCommonArgs(process.argv.slice(2));
38
+ if (args.help) {
39
+ printHelp();
40
+ return;
41
+ }
42
+
43
+ const repos = loadManifestRepos({
44
+ manifestPath: args.manifest,
45
+ baseDir: args.baseDir,
46
+ });
47
+
48
+ for (const repo of repos) {
49
+ fetchRepo({
50
+ name: repo.name,
51
+ destination: repo.destination,
52
+ dryRun: args.dryRun,
53
+ });
54
+ }
55
+ }
56
+
57
+ try {
58
+ main();
59
+ } catch (error) {
60
+ console.error(error.message);
61
+ process.exit(1);
62
+ }
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * List git remotes (git remote -v) for PAN Stack project repositories.
4
+ *
5
+ * Alias tương đương grv trong oh-my-zsh — hiển thị URL đầy đủ (fetch/push).
6
+ */
7
+
8
+ const path = require("path");
9
+ const { parseCommonArgs, loadManifestRepos, printCommonOptions } = require("./helpers/cli");
10
+ const { ensureRepoExists, isGitRepo, listRemotesVerbose } = require("./helpers/git");
11
+
12
+ function printRepoRemotes({ label, destination, dryRun }) {
13
+ if (dryRun) {
14
+ console.log(`[dry-run] (in ${destination}) git remote -v`);
15
+ return;
16
+ }
17
+
18
+ console.log(`== ${label} ==`);
19
+ console.log(destination);
20
+ const lines = listRemotesVerbose(destination);
21
+ if (lines.length === 0) {
22
+ console.log("(không có remote)");
23
+ } else {
24
+ for (const line of lines) {
25
+ console.log(line);
26
+ }
27
+ }
28
+ console.log("");
29
+ }
30
+
31
+ function showWorkspaceRootRemotes({ manifestPath, baseDir, dryRun }) {
32
+ const workspaceRoot = path.resolve(baseDir || path.dirname(manifestPath || ".project.yaml"));
33
+ if (!isGitRepo(workspaceRoot)) {
34
+ return;
35
+ }
36
+
37
+ printRepoRemotes({
38
+ label: "workspace",
39
+ destination: workspaceRoot,
40
+ dryRun,
41
+ });
42
+ }
43
+
44
+ function printHelp() {
45
+ console.log(`usage: git-grv.js [--base-dir BASE_DIR] [--dry-run] [--no-workspace] [manifest]
46
+
47
+ List git remotes with full URLs (git remote -v) for each repository.
48
+
49
+ ${printCommonOptions()}
50
+ --no-workspace Skip workspace root (chỉ repo trong manifest)`);
51
+ }
52
+
53
+ function main() {
54
+ const args = parseCommonArgs(process.argv.slice(2), {
55
+ defaults: { noWorkspace: false },
56
+ handlers: {
57
+ "--no-workspace": (parsed) => {
58
+ parsed.noWorkspace = true;
59
+ },
60
+ },
61
+ });
62
+ if (args.help) {
63
+ printHelp();
64
+ return;
65
+ }
66
+
67
+ const manifestPath = path.resolve(args.manifest || ".project.yaml");
68
+ const repos = loadManifestRepos({
69
+ manifestPath,
70
+ baseDir: args.baseDir,
71
+ });
72
+
73
+ if (!args.noWorkspace) {
74
+ showWorkspaceRootRemotes({
75
+ manifestPath,
76
+ baseDir: args.baseDir,
77
+ dryRun: args.dryRun,
78
+ });
79
+ }
80
+
81
+ for (const repo of repos) {
82
+ if (!ensureRepoExists({ name: repo.name, destination: repo.destination })) {
83
+ continue;
84
+ }
85
+ printRepoRemotes({
86
+ label: repo.name,
87
+ destination: repo.destination,
88
+ dryRun: args.dryRun,
89
+ });
90
+ }
91
+ }
92
+
93
+ try {
94
+ main();
95
+ } catch (error) {
96
+ console.error(error.message);
97
+ process.exit(1);
98
+ }