pan-stack-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -0
- package/bin/pan.js +140 -0
- package/lib/discover.js +206 -0
- package/lib/git/git-clone.js +70 -0
- package/lib/git/git-fetch-all.js +62 -0
- package/lib/git/git-grv.js +98 -0
- package/lib/git/git-gst.js +98 -0
- package/lib/git/git-pull.js +76 -0
- package/lib/git/git-push.js +76 -0
- package/lib/git/helpers/cli.js +103 -0
- package/lib/git/helpers/git.js +61 -0
- package/lib/git/helpers/manifest.js +130 -0
- package/lib/manifest.js +312 -0
- package/lib/run-command.js +196 -0
- package/package.json +27 -0
package/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
|
+
});
|
package/lib/discover.js
ADDED
|
@@ -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
|
+
}
|