pan-stack-cli 0.1.1 → 0.1.4
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 +160 -96
- package/bin/pan.js +27 -14
- package/lib/create.js +87 -0
- package/lib/discover.js +21 -10
- package/lib/git/git-clone.js +3 -2
- package/lib/git/git-fetch-all.js +1 -1
- package/lib/git/git-grv.js +7 -11
- package/lib/git/git-gst.js +7 -11
- package/lib/git/git-pull.js +1 -1
- package/lib/git/git-push.js +1 -1
- package/lib/git/helpers/cli.js +56 -7
- package/lib/git/helpers/git.js +6 -5
- 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
package/lib/git/git-grv.js
CHANGED
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
/*
|
|
3
3
|
* List git remotes (git remote -v) for PAN Stack project repositories.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* oh-my-zsh-style alias for grv — full remote URLs (fetch/push).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const path = require("path");
|
|
9
8
|
const { parseCommonArgs, loadManifestRepos, printCommonOptions } = require("./helpers/cli");
|
|
10
9
|
const { ensureRepoExists, isGitRepo, listRemotesVerbose } = require("./helpers/git");
|
|
11
10
|
|
|
@@ -19,7 +18,7 @@ function printRepoRemotes({ label, destination, dryRun }) {
|
|
|
19
18
|
console.log(destination);
|
|
20
19
|
const lines = listRemotesVerbose(destination);
|
|
21
20
|
if (lines.length === 0) {
|
|
22
|
-
console.log("(
|
|
21
|
+
console.log("(no remotes)");
|
|
23
22
|
} else {
|
|
24
23
|
for (const line of lines) {
|
|
25
24
|
console.log(line);
|
|
@@ -28,8 +27,7 @@ function printRepoRemotes({ label, destination, dryRun }) {
|
|
|
28
27
|
console.log("");
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
function showWorkspaceRootRemotes({
|
|
32
|
-
const workspaceRoot = path.resolve(baseDir || path.dirname(manifestPath || ".project.yaml"));
|
|
30
|
+
function showWorkspaceRootRemotes({ workspaceRoot, dryRun }) {
|
|
33
31
|
if (!isGitRepo(workspaceRoot)) {
|
|
34
32
|
return;
|
|
35
33
|
}
|
|
@@ -47,7 +45,7 @@ function printHelp() {
|
|
|
47
45
|
List git remotes with full URLs (git remote -v) for each repository.
|
|
48
46
|
|
|
49
47
|
${printCommonOptions()}
|
|
50
|
-
--no-workspace Skip workspace root (
|
|
48
|
+
--no-workspace Skip workspace root (manifest repos only)`);
|
|
51
49
|
}
|
|
52
50
|
|
|
53
51
|
function main() {
|
|
@@ -64,16 +62,14 @@ function main() {
|
|
|
64
62
|
return;
|
|
65
63
|
}
|
|
66
64
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
manifestPath,
|
|
65
|
+
const { repos, workspaceRoot } = loadManifestRepos({
|
|
66
|
+
manifestPath: args.manifest,
|
|
70
67
|
baseDir: args.baseDir,
|
|
71
68
|
});
|
|
72
69
|
|
|
73
70
|
if (!args.noWorkspace) {
|
|
74
71
|
showWorkspaceRootRemotes({
|
|
75
|
-
|
|
76
|
-
baseDir: args.baseDir,
|
|
72
|
+
workspaceRoot,
|
|
77
73
|
dryRun: args.dryRun,
|
|
78
74
|
});
|
|
79
75
|
}
|
package/lib/git/git-gst.js
CHANGED
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
/*
|
|
3
3
|
* Git status for PAN Stack project repositories.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* oh-my-zsh-style alias for gst — git status on workspace + child repos.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const path = require("path");
|
|
9
8
|
const { parseCommonArgs, loadManifestRepos, printCommonOptions } = require("./helpers/cli");
|
|
10
9
|
const { ensureRepoExists, isGitRepo, runGitStatus } = require("./helpers/git");
|
|
11
10
|
|
|
@@ -22,8 +21,7 @@ function printRepoStatus({ label, destination, dryRun, gitArgs }) {
|
|
|
22
21
|
console.log("");
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
function showWorkspaceRootStatus({
|
|
26
|
-
const workspaceRoot = path.resolve(baseDir || path.dirname(manifestPath || ".project.yaml"));
|
|
24
|
+
function showWorkspaceRootStatus({ workspaceRoot, dryRun, gitArgs }) {
|
|
27
25
|
if (!isGitRepo(workspaceRoot)) {
|
|
28
26
|
return;
|
|
29
27
|
}
|
|
@@ -40,10 +38,10 @@ function printHelp() {
|
|
|
40
38
|
console.log(`usage: git-gst.js [--base-dir BASE_DIR] [--dry-run] [--no-workspace] [manifest] [git status args...]
|
|
41
39
|
|
|
42
40
|
Run git status for workspace root and each repository in the manifest.
|
|
43
|
-
Extra args are forwarded to git status (
|
|
41
|
+
Extra args are forwarded to git status (e.g. -s, -sb).
|
|
44
42
|
|
|
45
43
|
${printCommonOptions()}
|
|
46
|
-
--no-workspace Skip workspace root (
|
|
44
|
+
--no-workspace Skip workspace root (manifest repos only)`);
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
function main() {
|
|
@@ -62,16 +60,14 @@ function main() {
|
|
|
62
60
|
}
|
|
63
61
|
|
|
64
62
|
const gitArgs = args.gitArgs || [];
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
manifestPath,
|
|
63
|
+
const { repos, workspaceRoot } = loadManifestRepos({
|
|
64
|
+
manifestPath: args.manifest,
|
|
68
65
|
baseDir: args.baseDir,
|
|
69
66
|
});
|
|
70
67
|
|
|
71
68
|
if (!args.noWorkspace) {
|
|
72
69
|
showWorkspaceRootStatus({
|
|
73
|
-
|
|
74
|
-
baseDir: args.baseDir,
|
|
70
|
+
workspaceRoot,
|
|
75
71
|
dryRun: args.dryRun,
|
|
76
72
|
gitArgs,
|
|
77
73
|
});
|
package/lib/git/git-pull.js
CHANGED
package/lib/git/git-push.js
CHANGED
package/lib/git/helpers/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
2
|
const fs = require("fs");
|
|
3
|
+
const { findNearestProjectManifest } = require("../../discover");
|
|
3
4
|
const { parseManifest, manifestRepos, validateRepo } = require("./manifest");
|
|
4
5
|
|
|
5
6
|
function parseCommonArgs(argv, extraFlags = {}) {
|
|
@@ -51,19 +52,61 @@ function looksLikeManifest(arg) {
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
function resolveProjectWorkspace({
|
|
56
|
+
manifestPath = "",
|
|
57
|
+
baseDir = ".",
|
|
58
|
+
startDir = process.cwd(),
|
|
59
|
+
}) {
|
|
60
|
+
const manifestName = manifestPath || ".project.yaml";
|
|
61
|
+
const cwdManifest = path.isAbsolute(manifestName)
|
|
62
|
+
? manifestName
|
|
63
|
+
: path.resolve(startDir, manifestName);
|
|
64
|
+
|
|
65
|
+
let resolvedManifest = "";
|
|
66
|
+
let usedWalkUp = false;
|
|
67
|
+
|
|
68
|
+
if (fs.existsSync(cwdManifest)) {
|
|
69
|
+
resolvedManifest = cwdManifest;
|
|
70
|
+
} else {
|
|
71
|
+
const found = findNearestProjectManifest(startDir);
|
|
72
|
+
if (!found) {
|
|
73
|
+
const label = manifestPath || ".project.yaml";
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Manifest not found: ${label} (walked up from ${startDir})`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
resolvedManifest = found;
|
|
79
|
+
usedWalkUp = true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const workspaceRoot = path.dirname(resolvedManifest);
|
|
83
|
+
let resolvedBaseDir;
|
|
84
|
+
if (baseDir === ".") {
|
|
85
|
+
resolvedBaseDir = usedWalkUp ? workspaceRoot : path.resolve(startDir, ".");
|
|
86
|
+
} else if (path.isAbsolute(baseDir)) {
|
|
87
|
+
resolvedBaseDir = baseDir;
|
|
88
|
+
} else {
|
|
89
|
+
resolvedBaseDir = path.resolve(startDir, baseDir);
|
|
58
90
|
}
|
|
59
91
|
|
|
92
|
+
return {
|
|
93
|
+
manifestPath: resolvedManifest,
|
|
94
|
+
baseDir: resolvedBaseDir,
|
|
95
|
+
workspaceRoot,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function loadManifestRepos({ manifestPath, baseDir, startDir = process.cwd() }) {
|
|
100
|
+
const workspace = resolveProjectWorkspace({ manifestPath, baseDir, startDir });
|
|
101
|
+
const { manifestPath: resolvedManifest, baseDir: resolvedBaseDir, workspaceRoot } =
|
|
102
|
+
workspace;
|
|
103
|
+
|
|
60
104
|
const data = parseManifest(resolvedManifest);
|
|
61
105
|
const repos = manifestRepos(data);
|
|
62
106
|
if (repos.length === 0) {
|
|
63
107
|
throw new Error("No repositories found in manifest.");
|
|
64
108
|
}
|
|
65
109
|
|
|
66
|
-
const resolvedBaseDir = path.resolve(baseDir);
|
|
67
110
|
const results = [];
|
|
68
111
|
for (const repo of repos) {
|
|
69
112
|
try {
|
|
@@ -83,7 +126,12 @@ function loadManifestRepos({ manifestPath, baseDir }) {
|
|
|
83
126
|
throw new Error("No valid repositories found in manifest.");
|
|
84
127
|
}
|
|
85
128
|
|
|
86
|
-
return
|
|
129
|
+
return {
|
|
130
|
+
repos: results,
|
|
131
|
+
manifestPath: resolvedManifest,
|
|
132
|
+
baseDir: resolvedBaseDir,
|
|
133
|
+
workspaceRoot,
|
|
134
|
+
};
|
|
87
135
|
}
|
|
88
136
|
|
|
89
137
|
function printCommonOptions() {
|
|
@@ -92,12 +140,13 @@ function printCommonOptions() {
|
|
|
92
140
|
|
|
93
141
|
options:
|
|
94
142
|
-h, --help show this help message and exit
|
|
95
|
-
--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).
|
|
96
144
|
--dry-run Print commands only`;
|
|
97
145
|
}
|
|
98
146
|
|
|
99
147
|
module.exports = {
|
|
100
148
|
parseCommonArgs,
|
|
149
|
+
resolveProjectWorkspace,
|
|
101
150
|
loadManifestRepos,
|
|
102
151
|
printCommonOptions,
|
|
103
152
|
};
|
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,10 +46,10 @@ 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
|
|
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,
|
package/lib/platform.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { spawnSync } = require("child_process");
|
|
4
|
+
|
|
5
|
+
const WINDOWS_RESERVED_NAMES = new Set([
|
|
6
|
+
"CON",
|
|
7
|
+
"PRN",
|
|
8
|
+
"AUX",
|
|
9
|
+
"NUL",
|
|
10
|
+
"COM1",
|
|
11
|
+
"COM2",
|
|
12
|
+
"COM3",
|
|
13
|
+
"COM4",
|
|
14
|
+
"COM5",
|
|
15
|
+
"COM6",
|
|
16
|
+
"COM7",
|
|
17
|
+
"COM8",
|
|
18
|
+
"COM9",
|
|
19
|
+
"LPT1",
|
|
20
|
+
"LPT2",
|
|
21
|
+
"LPT3",
|
|
22
|
+
"LPT4",
|
|
23
|
+
"LPT5",
|
|
24
|
+
"LPT6",
|
|
25
|
+
"LPT7",
|
|
26
|
+
"LPT8",
|
|
27
|
+
"LPT9",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function isWindows() {
|
|
31
|
+
return process.platform === "win32";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function splitEnvList(value) {
|
|
35
|
+
if (!value) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
// On Windows, do not split on ":" — drive letters use "C:".
|
|
39
|
+
const parts = isWindows() ? value.split(/[;,]/) : value.split(/[:;,]/);
|
|
40
|
+
return parts.map((part) => part.trim()).filter(Boolean);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function findBash() {
|
|
44
|
+
const candidates = isWindows()
|
|
45
|
+
? [
|
|
46
|
+
process.env.PAN_GIT_BASH,
|
|
47
|
+
process.env.PAN_BASH,
|
|
48
|
+
process.env.GIT_BASH,
|
|
49
|
+
process.env.PROGRAMFILES && path.join(process.env.PROGRAMFILES, "Git", "bin", "bash.exe"),
|
|
50
|
+
process.env["ProgramFiles(x86)"] &&
|
|
51
|
+
path.join(process.env["ProgramFiles(x86)"], "Git", "bin", "bash.exe"),
|
|
52
|
+
process.env.LOCALAPPDATA &&
|
|
53
|
+
path.join(process.env.LOCALAPPDATA, "Programs", "Git", "bin", "bash.exe"),
|
|
54
|
+
]
|
|
55
|
+
: [process.env.PAN_BASH, process.env.BASH, "/usr/bin/bash", "/bin/bash"];
|
|
56
|
+
|
|
57
|
+
for (const candidate of candidates.filter(Boolean)) {
|
|
58
|
+
if (fs.existsSync(candidate)) {
|
|
59
|
+
return candidate;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @deprecated use findBash */
|
|
67
|
+
function findGitBash() {
|
|
68
|
+
return findBash();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function spawnOptions(extra = {}) {
|
|
72
|
+
return isWindows() ? { windowsHide: true, ...extra } : extra;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeShellEnv(env) {
|
|
76
|
+
if (!isWindows()) {
|
|
77
|
+
return env;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const normalized = { ...env };
|
|
81
|
+
for (const key of ["PAN_WORKSPACE_ROOT", "PAN_REPO_ROOT", "PAN_PROJECT_ROOT"]) {
|
|
82
|
+
if (typeof normalized[key] === "string") {
|
|
83
|
+
normalized[key] = normalized[key].replace(/\\/g, "/");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return normalized;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function runShellScript(script, { cwd, env, stdio = "inherit" } = {}) {
|
|
90
|
+
const shellEnv = normalizeShellEnv(env);
|
|
91
|
+
const bash = findBash();
|
|
92
|
+
|
|
93
|
+
if (!bash) {
|
|
94
|
+
const hint = isWindows()
|
|
95
|
+
? "Install Git for Windows (https://git-scm.com/download/win) or set PAN_GIT_BASH / PAN_BASH to bash.exe."
|
|
96
|
+
: "Install bash or set PAN_BASH to its path.";
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Shell commands in .pan.yaml require bash. ${hint} Alternatively, use 'exec' instead of 'shell'.`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return spawnSync(bash, ["-lc", script], spawnOptions({ cwd, env: shellEnv, stdio }));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function runExecLine(execLine, { cwd, env, stdio = "inherit" } = {}) {
|
|
106
|
+
return spawnSync(
|
|
107
|
+
execLine,
|
|
108
|
+
spawnOptions({ cwd, env: normalizeShellEnv(env), stdio, shell: true })
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function validateFolderName(name) {
|
|
113
|
+
if (!name || !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name)) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
"Invalid folder name. Use letters, numbers, dots, hyphens, or underscores."
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (isWindows()) {
|
|
120
|
+
if (/[<>:"/\\|?*]/.test(name)) {
|
|
121
|
+
throw new Error("Invalid folder name for Windows (<>:\"/\\|?* are not allowed).");
|
|
122
|
+
}
|
|
123
|
+
const base = name.replace(/\.+$/, "").toUpperCase();
|
|
124
|
+
if (WINDOWS_RESERVED_NAMES.has(base)) {
|
|
125
|
+
throw new Error(`Invalid folder name for Windows (reserved name: ${base}).`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
isWindows,
|
|
132
|
+
splitEnvList,
|
|
133
|
+
findBash,
|
|
134
|
+
findGitBash,
|
|
135
|
+
spawnOptions,
|
|
136
|
+
normalizeShellEnv,
|
|
137
|
+
runShellScript,
|
|
138
|
+
runExecLine,
|
|
139
|
+
validateFolderName,
|
|
140
|
+
};
|
package/lib/run-command.js
CHANGED
|
@@ -8,10 +8,11 @@ const {
|
|
|
8
8
|
resolveCommandName,
|
|
9
9
|
} = require("./manifest");
|
|
10
10
|
const { sourceMatchesProject } = require("./discover");
|
|
11
|
+
const { runShellScript, runExecLine, spawnOptions } = require("./platform");
|
|
11
12
|
|
|
12
13
|
function needDir(dir) {
|
|
13
14
|
if (!fs.existsSync(dir)) {
|
|
14
|
-
throw new Error(`
|
|
15
|
+
throw new Error(`Directory not found: ${dir} (run 'pan clone' first?)`);
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -46,12 +47,11 @@ function runExec(source, def, forwardedArgs) {
|
|
|
46
47
|
|
|
47
48
|
const execLine = [def.exec, ...forwardedArgs].filter(Boolean).join(" ").trim();
|
|
48
49
|
if (!execLine) {
|
|
49
|
-
throw new Error("Command
|
|
50
|
+
throw new Error("Command is missing 'exec' or 'shell'.");
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
const result =
|
|
53
|
+
const result = runExecLine(execLine, {
|
|
53
54
|
cwd,
|
|
54
|
-
shell: true,
|
|
55
55
|
stdio: "inherit",
|
|
56
56
|
env: buildPanEnv(source, forwardedArgs),
|
|
57
57
|
});
|
|
@@ -65,12 +65,17 @@ function runShell(source, def, forwardedArgs) {
|
|
|
65
65
|
needDir(cwd);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
shell
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
let result;
|
|
69
|
+
try {
|
|
70
|
+
result = runShellScript(def.shell, {
|
|
71
|
+
cwd,
|
|
72
|
+
stdio: "inherit",
|
|
73
|
+
env: buildPanEnv(source, forwardedArgs),
|
|
74
|
+
});
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error(error.message);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
74
79
|
|
|
75
80
|
process.exit(result.status ?? 1);
|
|
76
81
|
}
|
|
@@ -88,7 +93,7 @@ function runProjectCommand(source, commandName, args) {
|
|
|
88
93
|
|
|
89
94
|
const resolved = getCommandDefinition(source, commandName, subArg);
|
|
90
95
|
if (!resolved) {
|
|
91
|
-
throw new Error(`
|
|
96
|
+
throw new Error(`Command '${commandName}' not found in ${source.path}`);
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
if (resolved.shell) {
|
|
@@ -101,7 +106,7 @@ function runProjectCommand(source, commandName, args) {
|
|
|
101
106
|
return;
|
|
102
107
|
}
|
|
103
108
|
|
|
104
|
-
throw new Error(`
|
|
109
|
+
throw new Error(`Command '${commandName}' is missing 'exec' or 'shell' in ${source.path}`);
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
function sourceLabel(source) {
|
|
@@ -126,11 +131,11 @@ function promptProjectChoice(matches, commandName) {
|
|
|
126
131
|
});
|
|
127
132
|
console.log("");
|
|
128
133
|
|
|
129
|
-
rl.question("
|
|
134
|
+
rl.question("Pick a project (number): ", (answer) => {
|
|
130
135
|
rl.close();
|
|
131
136
|
const choice = Number.parseInt(answer, 10);
|
|
132
137
|
if (!Number.isFinite(choice) || choice < 1 || choice > matches.length) {
|
|
133
|
-
reject(new Error("
|
|
138
|
+
reject(new Error("Invalid choice."));
|
|
134
139
|
return;
|
|
135
140
|
}
|
|
136
141
|
resolve(matches[choice - 1]);
|
|
@@ -149,7 +154,7 @@ async function runWithProjectResolution(
|
|
|
149
154
|
|
|
150
155
|
if (matches.length === 0) {
|
|
151
156
|
throw new Error(
|
|
152
|
-
`
|
|
157
|
+
`Command '${commandName}' not found in any .pan.yaml file.`
|
|
153
158
|
);
|
|
154
159
|
}
|
|
155
160
|
|
|
@@ -158,7 +163,7 @@ async function runWithProjectResolution(
|
|
|
158
163
|
source = matches.find((item) => sourceMatchesProject(item, projectFlag));
|
|
159
164
|
if (!source) {
|
|
160
165
|
const names = matches.map((item) => sourceLabel(item)).join(", ");
|
|
161
|
-
throw new Error(`
|
|
166
|
+
throw new Error(`Project '${projectFlag}' not found. Available: ${names}`);
|
|
162
167
|
}
|
|
163
168
|
} else if (matches.length === 1) {
|
|
164
169
|
source = matches[0];
|
|
@@ -176,7 +181,7 @@ async function runWithProjectResolution(
|
|
|
176
181
|
(item, index) => `${index + 1}. ${sourceLabel(item)} (${item.root})`
|
|
177
182
|
);
|
|
178
183
|
throw new Error(
|
|
179
|
-
`
|
|
184
|
+
`Command '${commandName}' is defined in multiple projects. Use --project <name>.\n${lines.join("\n")}`
|
|
180
185
|
);
|
|
181
186
|
}
|
|
182
187
|
}
|
|
@@ -187,13 +192,13 @@ async function runWithProjectResolution(
|
|
|
187
192
|
function runGitScript(scriptName, args) {
|
|
188
193
|
const scriptPath = path.join(__dirname, "git", scriptName);
|
|
189
194
|
if (!fs.existsSync(scriptPath)) {
|
|
190
|
-
throw new Error(`
|
|
195
|
+
throw new Error(`Script not found: ${scriptPath}`);
|
|
191
196
|
}
|
|
192
197
|
|
|
193
|
-
const child = spawn(process.execPath, [scriptPath, ...args], {
|
|
198
|
+
const child = spawn(process.execPath, [scriptPath, ...args], spawnOptions({
|
|
194
199
|
stdio: "inherit",
|
|
195
200
|
env: process.env,
|
|
196
|
-
});
|
|
201
|
+
}));
|
|
197
202
|
|
|
198
203
|
child.on("exit", (code) => {
|
|
199
204
|
process.exit(code ?? 1);
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pan-stack-cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "PAN Stack CLI — multi-repo git and project commands from .project.yaml",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
7
7
|
"pan": "bin/pan.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
|
11
|
-
"lib"
|
|
11
|
+
"lib",
|
|
12
|
+
"templates"
|
|
12
13
|
],
|
|
13
14
|
"engines": {
|
|
14
15
|
"node": ">=18"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: writing-guidelines
|
|
3
|
+
description: Review docs/prose for Writing Guidelines compliance. Use when asked to "review my docs", "check writing style", "audit prose", "review docs voice and tone", or "check this page against the writing handbook".
|
|
4
|
+
metadata:
|
|
5
|
+
author: vercel
|
|
6
|
+
version: "1.0.0"
|
|
7
|
+
argument-hint: <file-or-pattern>
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Writing Guidelines
|
|
11
|
+
|
|
12
|
+
Review files for compliance with Writing Guidelines.
|
|
13
|
+
|
|
14
|
+
## How It Works
|
|
15
|
+
|
|
16
|
+
1. Fetch the latest guidelines from the source URL below
|
|
17
|
+
2. Read the specified files (or prompt user for files/pattern)
|
|
18
|
+
3. Check against all rules in the fetched guidelines
|
|
19
|
+
4. Output findings in the terse `file:line` format
|
|
20
|
+
|
|
21
|
+
## Guidelines Source
|
|
22
|
+
|
|
23
|
+
Fetch fresh guidelines before each review:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
https://raw.githubusercontent.com/vercel-labs/writing-guidelines/main/command.md
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Use WebFetch to retrieve the latest rules. The fetched content contains all the rules and output format instructions.
|
|
30
|
+
|
|
31
|
+
## Local Markdown Rules
|
|
32
|
+
|
|
33
|
+
Also check generated or edited Markdown against local markdownlint conventions:
|
|
34
|
+
|
|
35
|
+
- Keep at most one blank line between blocks. Do not create multiple consecutive blank lines. This avoids `MD012/no-multiple-blanks`.
|
|
36
|
+
- Use language tags on fenced code blocks.
|
|
37
|
+
- Before finishing edits, scan changed Markdown files for repeated blank lines.
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
When a user provides a file or pattern argument:
|
|
42
|
+
1. Fetch guidelines from the source URL above
|
|
43
|
+
2. Read the specified files
|
|
44
|
+
3. Apply all rules from the fetched guidelines
|
|
45
|
+
4. Apply the local Markdown rules above
|
|
46
|
+
5. Output findings using the format specified in the guidelines
|
|
47
|
+
|
|
48
|
+
If no files specified, ask the user which files to review.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: writing-guidelines
|
|
3
|
+
description: Review docs/prose for Writing Guidelines compliance. Use when asked to "review my docs", "check writing style", "audit prose", "review docs voice and tone", or "check this page against the writing handbook".
|
|
4
|
+
metadata:
|
|
5
|
+
author: vercel
|
|
6
|
+
version: "1.0.0"
|
|
7
|
+
argument-hint: <file-or-pattern>
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Writing Guidelines
|
|
11
|
+
|
|
12
|
+
Review files for compliance with Writing Guidelines.
|
|
13
|
+
|
|
14
|
+
## How It Works
|
|
15
|
+
|
|
16
|
+
1. Fetch the latest guidelines from the source URL below
|
|
17
|
+
2. Read the specified files (or prompt user for files/pattern)
|
|
18
|
+
3. Check against all rules in the fetched guidelines
|
|
19
|
+
4. Output findings in the terse `file:line` format
|
|
20
|
+
|
|
21
|
+
## Guidelines Source
|
|
22
|
+
|
|
23
|
+
Fetch fresh guidelines before each review:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
https://raw.githubusercontent.com/vercel-labs/writing-guidelines/main/command.md
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Use WebFetch to retrieve the latest rules. The fetched content contains all the rules and output format instructions.
|
|
30
|
+
|
|
31
|
+
## Local Markdown Rules
|
|
32
|
+
|
|
33
|
+
Also check generated or edited Markdown against local markdownlint conventions:
|
|
34
|
+
|
|
35
|
+
- Keep at most one blank line between blocks. Do not create multiple consecutive blank lines. This avoids `MD012/no-multiple-blanks`.
|
|
36
|
+
- Use language tags on fenced code blocks.
|
|
37
|
+
- Before finishing edits, scan changed Markdown files for repeated blank lines.
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
When a user provides a file or pattern argument:
|
|
42
|
+
1. Fetch guidelines from the source URL above
|
|
43
|
+
2. Read the specified files
|
|
44
|
+
3. Apply all rules from the fetched guidelines
|
|
45
|
+
4. Apply the local Markdown rules above
|
|
46
|
+
5. Output findings using the format specified in the guidelines
|
|
47
|
+
|
|
48
|
+
If no files specified, ask the user which files to review.
|