pan-stack-cli 0.1.5 → 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 -183
- package/bin/pan.js +4992 -123
- package/package.json +24 -5
- package/lib/create.js +0 -87
- package/lib/discover.js +0 -236
- package/lib/git/git-clone.js +0 -71
- package/lib/git/git-fetch-all.js +0 -62
- package/lib/git/git-grv.js +0 -94
- package/lib/git/git-gst.js +0 -94
- package/lib/git/git-pull.js +0 -76
- package/lib/git/git-push.js +0 -76
- package/lib/git/git-sync-main.js +0 -320
- package/lib/git/helpers/cli.js +0 -153
- package/lib/git/helpers/colors.js +0 -19
- package/lib/git/helpers/git.js +0 -136
- package/lib/git/helpers/manifest.js +0 -130
- package/lib/git/helpers/repo-select.js +0 -97
- package/lib/manifest.js +0 -366
- package/lib/platform.js +0 -140
- package/lib/run-command.js +0 -213
- 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/git/helpers/git.js
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
|
-
const path = require("path");
|
|
3
|
-
const { spawnSync } = require("child_process");
|
|
4
|
-
const { spawnOptions } = require("../../platform");
|
|
5
|
-
|
|
6
|
-
function isGitRepo(directory) {
|
|
7
|
-
return fs.existsSync(path.join(directory, ".git"));
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function runGit(gitArgs, { cwd, dryRun, action, name }) {
|
|
11
|
-
const command = ["git", ...gitArgs];
|
|
12
|
-
if (dryRun) {
|
|
13
|
-
console.log(`[dry-run] (in ${cwd}) ${command.join(" ")}`);
|
|
14
|
-
return 0;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
console.log(`[${action}] ${name}: ${cwd}`);
|
|
18
|
-
const result = spawnSync(command[0], command.slice(1), spawnOptions({ stdio: "inherit", cwd }));
|
|
19
|
-
return result.status ?? 1;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function ensureRepoExists({ name, destination }) {
|
|
23
|
-
if (!fs.existsSync(destination)) {
|
|
24
|
-
console.log(`[skip] ${name}: ${destination} does not exist`);
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
if (!isGitRepo(destination)) {
|
|
28
|
-
console.log(`[skip] ${name}: ${destination} is not a git repository`);
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function listRemotesVerbose(directory) {
|
|
35
|
-
const result = spawnSync("git", ["remote", "-v"], spawnOptions({
|
|
36
|
-
cwd: directory,
|
|
37
|
-
encoding: "utf8",
|
|
38
|
-
}));
|
|
39
|
-
if (result.status !== 0) {
|
|
40
|
-
return [];
|
|
41
|
-
}
|
|
42
|
-
return result.stdout
|
|
43
|
-
.split(/\r?\n/)
|
|
44
|
-
.map((line) => line.trimEnd())
|
|
45
|
-
.filter(Boolean);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function runGitStatus(directory, gitArgs = []) {
|
|
49
|
-
const result = spawnSync("git", ["status", ...gitArgs], spawnOptions({
|
|
50
|
-
cwd: directory,
|
|
51
|
-
stdio: "inherit",
|
|
52
|
-
}));
|
|
53
|
-
return result.status ?? 1;
|
|
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
|
-
|
|
124
|
-
module.exports = {
|
|
125
|
-
isGitRepo,
|
|
126
|
-
runGit,
|
|
127
|
-
runGitCapture,
|
|
128
|
-
getCurrentBranch,
|
|
129
|
-
refExists,
|
|
130
|
-
listConflictedFiles,
|
|
131
|
-
isMergeOrRebaseInProgress,
|
|
132
|
-
countDirtyFiles,
|
|
133
|
-
ensureRepoExists,
|
|
134
|
-
listRemotesVerbose,
|
|
135
|
-
runGitStatus,
|
|
136
|
-
};
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
|
-
|
|
3
|
-
const PLACEHOLDER_VALUES = new Set(["", "repo_url_to_be_filled", "pan_stack_repo_url"]);
|
|
4
|
-
|
|
5
|
-
function cleanValue(value) {
|
|
6
|
-
let result = String(value || "").trim();
|
|
7
|
-
const commentIndex = result.indexOf(" #");
|
|
8
|
-
if (commentIndex >= 0) {
|
|
9
|
-
result = result.slice(0, commentIndex).trimEnd();
|
|
10
|
-
}
|
|
11
|
-
if (
|
|
12
|
-
(result.startsWith('"') && result.endsWith('"')) ||
|
|
13
|
-
(result.startsWith("'") && result.endsWith("'"))
|
|
14
|
-
) {
|
|
15
|
-
result = result.slice(1, -1);
|
|
16
|
-
}
|
|
17
|
-
return result;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function splitKeyValue(text) {
|
|
21
|
-
const index = text.indexOf(":");
|
|
22
|
-
if (index < 0) {
|
|
23
|
-
return [text.trim(), ""];
|
|
24
|
-
}
|
|
25
|
-
return [text.slice(0, index).trim(), cleanValue(text.slice(index + 1))];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function parseManifest(manifestPath) {
|
|
29
|
-
const data = { repositories: [], stack: {} };
|
|
30
|
-
let section = "";
|
|
31
|
-
let currentRepo = null;
|
|
32
|
-
|
|
33
|
-
const lines = fs.readFileSync(manifestPath, "utf8").split(/\r?\n/);
|
|
34
|
-
for (const rawLine of lines) {
|
|
35
|
-
if (!rawLine.trim() || rawLine.trimStart().startsWith("#")) {
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const indent = rawLine.length - rawLine.trimStart().length;
|
|
40
|
-
const text = rawLine.trim();
|
|
41
|
-
|
|
42
|
-
if (indent === 0) {
|
|
43
|
-
const [key, value] = splitKeyValue(text);
|
|
44
|
-
section = key;
|
|
45
|
-
currentRepo = null;
|
|
46
|
-
if (value) {
|
|
47
|
-
data[key] = value;
|
|
48
|
-
} else if (!(key in data)) {
|
|
49
|
-
data[key] = {};
|
|
50
|
-
}
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (section === "stack" && indent === 2) {
|
|
55
|
-
const [key, value] = splitKeyValue(text);
|
|
56
|
-
data.stack[key] = value;
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (section === "repositories") {
|
|
61
|
-
if (indent === 2 && text.startsWith("- ")) {
|
|
62
|
-
currentRepo = {};
|
|
63
|
-
data.repositories.push(currentRepo);
|
|
64
|
-
const rest = text.slice(2).trim();
|
|
65
|
-
if (rest) {
|
|
66
|
-
const [key, value] = splitKeyValue(rest);
|
|
67
|
-
currentRepo[key] = value;
|
|
68
|
-
}
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
if (currentRepo && indent === 4) {
|
|
72
|
-
const [key, value] = splitKeyValue(text);
|
|
73
|
-
currentRepo[key] = value;
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return data;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function uniqueRepos(repos) {
|
|
83
|
-
const seen = new Set();
|
|
84
|
-
const result = [];
|
|
85
|
-
for (const repo of repos) {
|
|
86
|
-
const key = [
|
|
87
|
-
cleanValue(repo.name),
|
|
88
|
-
cleanValue(repo.url),
|
|
89
|
-
cleanValue(repo.path),
|
|
90
|
-
].join("\u0000");
|
|
91
|
-
if (seen.has(key)) {
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
seen.add(key);
|
|
95
|
-
result.push(repo);
|
|
96
|
-
}
|
|
97
|
-
return result;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function manifestRepos(data) {
|
|
101
|
-
const repositories = data.repositories || [];
|
|
102
|
-
return uniqueRepos(repositories);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function validateRepo(repo) {
|
|
106
|
-
const name = cleanValue(repo.name);
|
|
107
|
-
const url = cleanValue(repo.url);
|
|
108
|
-
const repoPath = cleanValue(repo.path) || name;
|
|
109
|
-
|
|
110
|
-
if (!name) {
|
|
111
|
-
throw new Error("Repository entry is missing 'name'.");
|
|
112
|
-
}
|
|
113
|
-
if (PLACEHOLDER_VALUES.has(url)) {
|
|
114
|
-
throw new Error(`Repository '${name}' is missing a real url.`);
|
|
115
|
-
}
|
|
116
|
-
if (!repoPath) {
|
|
117
|
-
throw new Error(`Repository '${name}' is missing 'path'.`);
|
|
118
|
-
}
|
|
119
|
-
return { name, url, repoPath };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
module.exports = {
|
|
123
|
-
PLACEHOLDER_VALUES,
|
|
124
|
-
cleanValue,
|
|
125
|
-
splitKeyValue,
|
|
126
|
-
parseManifest,
|
|
127
|
-
uniqueRepos,
|
|
128
|
-
manifestRepos,
|
|
129
|
-
validateRepo,
|
|
130
|
-
};
|
|
@@ -1,97 +0,0 @@
|
|
|
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
|
-
};
|