pan-stack-cli 0.1.2 → 0.1.5

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.
@@ -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
+ };
@@ -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(`Chưa có thư mục: ${dir} (chạy 'pan clone' trước?)`);
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 thiếu 'exec' hoặc 'shell'.");
50
+ throw new Error("Command is missing 'exec' or 'shell'.");
50
51
  }
51
52
 
52
- const result = spawnSync(execLine, {
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
- const result = spawnSync(def.shell, {
69
- cwd,
70
- shell: true,
71
- stdio: "inherit",
72
- env: buildPanEnv(source, forwardedArgs),
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(`Không tìm thấy lệnh '${commandName}' trong ${source.path}`);
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(`Lệnh '${commandName}' thiếu 'exec' hoặc 'shell' trong ${source.path}`);
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("Chọn dự án (số): ", (answer) => {
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("Lựa chọn không hợp lệ."));
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
- `Không tìm thấy lệnh '${commandName}' trong bất kỳ .pan.yaml nào.`
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(`Không tìm thấy dự án '${projectFlag}'. Có: ${names}`);
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
- `Lệnh '${commandName}' trùng ở nhiều dự án. Dùng --project <tên>.\n${lines.join("\n")}`
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(`Không tìm thấy script: ${scriptPath}`);
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.2",
4
- "description": "CLI điều phối PAN Stack — git đa repo và lệnh dự án từ .project.yaml",
3
+ "version": "0.1.5",
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.
@@ -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,25 @@
1
+ # Workspace-level commands. Per-repo commands live in apps/*/.pan.yaml
2
+ commands:
3
+ install:
4
+ alias: i
5
+ description: Install dependencies across cloned repositories
6
+ shell: |
7
+ set -euo pipefail
8
+ root="${PAN_WORKSPACE_ROOT}"
9
+ for dir in apps/web apps/api; do
10
+ [[ -d "$root/$dir" ]] || { echo "Missing: $root/$dir (run 'pan clone' first?)" >&2; exit 1; }
11
+ echo "-> install $dir"
12
+ (cd "$root/$dir" && npm install)
13
+ done
14
+ echo "Done."
15
+
16
+ test:
17
+ default: web
18
+ description: Run tests (web | api)
19
+ targets:
20
+ web:
21
+ cwd: apps/web
22
+ exec: npm test
23
+ api:
24
+ cwd: apps/api
25
+ exec: npm test
@@ -0,0 +1,15 @@
1
+ project: __PROJECT_NAME__
2
+ managed_by: your-team
3
+
4
+ stack:
5
+ repo: repo_url_to_be_filled
6
+
7
+ repositories:
8
+ - name: bookshop-web
9
+ description: Customer-facing web application
10
+ url: repo_url_to_be_filled
11
+ path: apps/web
12
+ - name: bookshop-api
13
+ description: REST API service
14
+ url: repo_url_to_be_filled
15
+ path: apps/api
@@ -0,0 +1,35 @@
1
+ # __PROJECT_NAME__
2
+
3
+ PAN Stack workspace shell for the Bookshop example project.
4
+
5
+ This repository is the **coordination layer** — not the product source tree. It holds
6
+ agent skills, workspace manifests, and `pan` commands. Product code lives in the
7
+ repositories listed in `.project.yaml`.
8
+
9
+ ## Layout
10
+
11
+ ```text
12
+ __PROJECT_NAME__/
13
+ .project.yaml # Workspace manifest (repository list)
14
+ .pan.yaml # Workspace-level pan commands
15
+ .claude/skills/ # Claude Code skills
16
+ .cursor/skills/ # Cursor agent skills
17
+ .codex/skills/ # Codex agent skills
18
+ apps/
19
+ web/ # bookshop-web (after pan clone)
20
+ api/ # bookshop-api (after pan clone)
21
+ ```
22
+
23
+ ## Quick start
24
+
25
+ ```bash
26
+ # Edit repository URLs in .project.yaml, then:
27
+ pan clone
28
+ pan install
29
+ pan test web
30
+ ```
31
+
32
+ ## Agent skills
33
+
34
+ All three agent folders ship with the shared `writing-guidelines` skill. Add more
35
+ skills under each `.claude`, `.cursor`, and `.codex` directory as your team grows.