pan-stack-cli 0.1.2 → 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 CHANGED
@@ -1,152 +1,216 @@
1
1
  # pan-stack-cli
2
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`:
3
+ Global CLI for PAN Stack workspaces. Install once, use in any project that has
4
+ `.project.yaml`.
6
5
 
7
6
  ```bash
8
7
  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
8
+ # or from source before npm publish:
9
+ npm i -g /path/to/pan-stack-cli
11
10
  ```
12
11
 
13
- Sau đó gõ `pan` thay cho `./pan`.
12
+ ## What is PAN Stack?
13
+
14
+ PAN Stack is a **workspace shell** for coding with AI agents — not a monorepo that
15
+ contains every product repository. The stack repo holds:
16
+
17
+ - Agent skills and prompts (`.claude`, `.cursor`, `.codex`)
18
+ - Workspace manifests (`.project.yaml`, `.pan.yaml`)
19
+ - Team docs and coordination files
14
20
 
15
- ## Yêu cầu
21
+ Product source code lives in separate repositories listed in `.project.yaml`. The
22
+ `pan` CLI ties them together: clone all repos, run git ops across the workspace,
23
+ and execute project commands defined in `.pan.yaml`.
24
+
25
+ ```text
26
+ bookshop-stack/ # PAN Stack workspace (shell)
27
+ .project.yaml # Which repos belong to this workspace
28
+ .pan.yaml # Workspace-level pan commands
29
+ .claude/skills/ # Claude Code skills
30
+ .cursor/skills/ # Cursor agent skills
31
+ .codex/skills/ # Codex agent skills
32
+ apps/
33
+ web/ # bookshop-web (cloned product repo)
34
+ api/ # bookshop-api (cloned product repo)
35
+ ```
36
+
37
+ Run `pan create` to scaffold this layout with the shared `writing-guidelines`
38
+ skill pre-installed for all three agent folders.
39
+
40
+ ## Requirements
16
41
 
17
42
  - 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, …)
43
+ - Git (for `pan clone`, `pull`, `fetch`, `push`, `grv`, `gst`)
20
44
 
21
- ## Lệnh git (built-in)
45
+ ## Windows support
22
46
 
23
- Đọc `repositories` từ `.project.yaml` gần nhất (walk-up từ thư mục hiện tại):
47
+ Built-in commands (`create`, `clone`, `pull`, `push`, `fetch`, `grv`, `gst`) run on
48
+ Windows, macOS, and Linux. Notes for Windows:
24
49
 
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`) |
50
+ - `shell:` blocks in `.pan.yaml` run through **bash** on all platforms (Git Bash on
51
+ Windows, system bash on macOS/Linux). Use `exec:` for simple one-liners (`npm test`)
52
+ when possible; those work without Bash.
53
+ - Install **Git for Windows** if bash is not already on `PATH`.
54
+ - Set `PAN_PROJECT_ROOTS` with **semicolon** separators (`C:\work\a;C:\work\b`).
55
+ Do not use `:` — it conflicts with drive letters.
56
+ - Optional: set `PAN_GIT_BASH` to the full path of `bash.exe` if Git is installed in
57
+ a non-standard location.
33
58
 
34
- Tuỳ chọn git (forward xuống script):
59
+ ## Bootstrap a new workspace
35
60
 
36
- - `--base-dir DIR` — thư mục gốc cho path repo con (mặc định: thư mục chứa `.project.yaml`; tự walk-up từ cwd nếu không có ngay tại đó)
37
- - `--dry-run` — chỉ in lệnh
38
- - `[manifest]` — đường dẫn manifest (mặc định: `.project.yaml`)
61
+ ```bash
62
+ mkdir ~/work && cd ~/work
63
+ pan create # prompts for folder name
64
+ # or
65
+ pan create bookshop-stack # non-interactive
39
66
 
40
- ## Lệnh dự án (từ `.pan.yaml`)
67
+ cd bookshop-stack
68
+ # Edit .project.yaml — replace repo_url_to_be_filled with real URLs
69
+ pan clone
70
+ pan install
71
+ ```
41
72
 
42
- Hai lớp cấu hình:
73
+ `pan create` fails if the target folder already exists.
43
74
 
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 |
75
+ ## Configuration files
48
76
 
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`).
77
+ Two files drive the CLI. Keep responsibilities separate:
50
78
 
51
- ### Schema `.pan.yaml` (repo con)
79
+ | File | Role |
80
+ |------|------|
81
+ | `.project.yaml` | Workspace manifest — lists `repositories` (name, url, path). **No commands.** |
82
+ | `.pan.yaml` | `pan` commands — at workspace root and/or inside each child repo |
52
83
 
53
- `cwd` / `exec` tương đối **root repo** (thư mục chứa file `.pan.yaml`):
84
+ ### `.project.yaml` example
54
85
 
55
86
  ```yaml
56
- commands:
57
- be:
58
- description: Deploy backend local
59
- exec: scripts/deploy.sh
87
+ project: bookshop-stack
88
+ managed_by: your-team
89
+
90
+ stack:
91
+ repo: https://github.com/example/bookshop-stack.git
92
+
93
+ repositories:
94
+ - name: bookshop-web
95
+ description: Customer-facing web application
96
+ url: https://github.com/example/bookshop-web.git
97
+ path: apps/web
98
+ - name: bookshop-api
99
+ description: REST API service
100
+ url: https://github.com/example/bookshop-api.git
101
+ path: apps/api
102
+ ```
103
+
104
+ ### `.pan.yaml` — workspace root
105
+
106
+ Orchestration commands. Paths are relative to the workspace root. Use
107
+ `PAN_WORKSPACE_ROOT` in shell blocks:
60
108
 
109
+ ```yaml
110
+ commands:
61
111
  install:
62
112
  alias: i
63
- description: Cài dependencies (pan i hoặc pan install)
113
+ description: Install dependencies across cloned repositories
64
114
  shell: |
65
115
  set -euo pipefail
66
- pnpm install
116
+ root="${PAN_WORKSPACE_ROOT}"
117
+ for dir in apps/web apps/api; do
118
+ [[ -d "$root/$dir" ]] || { echo "Missing: $root/$dir" >&2; exit 1; }
119
+ (cd "$root/$dir" && npm install)
120
+ done
67
121
 
68
- lint:
69
- shell: |
70
- set -euo pipefail
71
- pnpm lint
122
+ test:
123
+ default: web
124
+ description: Run tests (web | api)
125
+ targets:
126
+ web:
127
+ cwd: apps/web
128
+ exec: npm test
129
+ api:
130
+ cwd: apps/api
131
+ exec: npm test
72
132
  ```
73
133
 
74
- Alias: `alias: i` hoặc `aliases: i, ins` (cách nhau bằng dấu phẩy).
134
+ ### `.pan.yaml` — child repo
75
135
 
76
- ### Schema `.pan.yaml` (workspace root)
77
-
78
- Lệnh orchestration — `cwd` tương đối workspace root; dùng `PAN_WORKSPACE_ROOT` trong shell:
136
+ Place a `.pan.yaml` inside a cloned repo. `cwd` / `exec` are relative to that
137
+ repo's root:
79
138
 
80
139
  ```yaml
81
140
  commands:
82
- install:
83
- shell: |
84
- cd "${PAN_WORKSPACE_ROOT}/apps/frontend" && pnpm install
141
+ dev:
142
+ description: Start local dev server
143
+ exec: npm run dev
85
144
 
86
- test:
87
- default: fe
88
- targets:
89
- be:
90
- cwd: apps/backend
91
- exec: ./gradlew test
92
- fe:
93
- cwd: apps/frontend
94
- exec: pnpm test
145
+ lint:
146
+ shell: |
147
+ set -euo pipefail
148
+ npm run lint
95
149
  ```
96
150
 
97
- ### Biến môi trường
151
+ Aliases: `alias: i` or `aliases: i, ins` (comma-separated).
98
152
 
99
- | Biến | Ý nghĩa |
100
- |------|---------|
101
- | `PAN_WORKSPACE_ROOT` | Thư mục chứa `.project.yaml` |
102
- | `PAN_REPO_ROOT` | Thư mục chứa `.pan.yaml` đang chạy |
103
- | `PAN_PROJECT_ROOT` | Alias của `PAN_WORKSPACE_ROOT` |
104
- | `PAN_ARG_1`, … | Args sau tên lệnh |
153
+ ## Built-in git commands
105
154
 
106
- ### Nhiều repo trùng tên lệnh
155
+ Read `repositories` from the nearest `.project.yaml` (walk-up from cwd):
107
156
 
108
- CLI quét workspace gần nhất (walk-up `.project.yaml`) rồi load mọi `.pan.yaml` con.
157
+ | Command | Action |
158
+ |---------|--------|
159
+ | `pan clone` | Clone all repositories in the manifest |
160
+ | `pan pull` | Pull all cloned repositories |
161
+ | `pan fetch` | Fetch all remotes |
162
+ | `pan push` | Push all repos (`-u origin HEAD` by default) |
163
+ | `pan grv` | `git remote -v` for workspace + each child repo |
164
+ | `pan gst` | `git status` for workspace + each child repo |
109
165
 
110
- ## Ví dụ (CSDL-CC)
166
+ Git options:
111
167
 
112
- Trong repo `btp-csdl-cc` (đã có `.project.yaml`):
168
+ - `--base-dir DIR` — base for relative repo paths (default: directory containing `.project.yaml`)
169
+ - `--dry-run` — print commands only
170
+ - `[manifest]` — manifest path (default: `.project.yaml`)
113
171
 
114
- ```bash
115
- cd ~/work/btp/btp-csdl-cc
172
+ Works from child repo directories — the CLI walks up to find `.project.yaml`.
116
173
 
117
- pan clone
118
- pan install
119
- pan be # deploy backend :8080
120
- pan fe # Vite :5173
121
- pan faker # pan-faker :8090
122
- pan test fe # vitest
123
- pan demo uc2 # use case demo qua faker
124
- pan status
125
- pan down # docker compose down (giữ volume)
126
- pan reset -y # xoá volume + deploy lại
127
- ```
174
+ ## Project commands
175
+
176
+ The CLI loads **all** `.pan.yaml` files in the workspace (root + each
177
+ `repositories[].path`). When the same command name appears in multiple repos,
178
+ use `--project <name>` or pick interactively.
179
+
180
+ ### Environment variables
128
181
 
129
- ## `./pan` ở root repo
182
+ | Variable | Meaning |
183
+ |----------|---------|
184
+ | `PAN_WORKSPACE_ROOT` | Directory containing `.project.yaml` |
185
+ | `PAN_REPO_ROOT` | Directory containing the active `.pan.yaml` |
186
+ | `PAN_PROJECT_ROOT` | Alias of `PAN_WORKSPACE_ROOT` |
187
+ | `PAN_ARG_1`, … | Arguments after the command name |
130
188
 
131
- Script `./pan` là shim mỏng:
189
+ ### Registry (optional)
132
190
 
133
- 1. Dùng `pan` global nếu đã cài
134
- 2. Fallback `node .pan/pan-stack-cli/bin/pan.js`
191
+ - `PAN_PROJECT_ROOTS` — extra workspace roots (`;` on Windows, `:` or `;` on Unix)
192
+ - `~/.pan/projects` — one workspace root per line
135
193
 
136
- ## Cấu trúc package
194
+ ## Package layout
137
195
 
138
196
  ```text
139
- .pan/pan-stack-cli/
140
- bin/pan.js # entry CLI
141
- lib/ # manifest, discover, run-command, git/
142
- lib/git/ # git-clone, pull, fetch, push, grv, gst (+ helpers)
143
- references/ # hướng dẫn publish npm
197
+ pan-stack-cli/
198
+ bin/pan.js # CLI entry
199
+ lib/
200
+ create.js # pan create scaffold
201
+ discover.js # workspace discovery
202
+ platform.js # Windows/cross-platform helpers
203
+ manifest.js # YAML parsing
204
+ run-command.js # command execution
205
+ git/ # clone, pull, push, fetch, grv, gst
206
+ templates/workspace/ # pan create template
207
+ references/ # npm publish docs
144
208
  ```
145
209
 
146
- ## Phát triển local
210
+ ## Local development
147
211
 
148
- Cài global từ source (khuyên dùng trước publish): [references/npm-development.md](references/npm-development.md).
212
+ Install globally from source: [references/npm-development.md](references/npm-development.md).
149
213
 
150
- ## Publish npm
214
+ ## Publish to npm
151
215
 
152
- Xem [references/npm-release.md](references/npm-release.md).
216
+ See [references/npm-release.md](references/npm-release.md).
package/bin/pan.js CHANGED
@@ -6,9 +6,11 @@ const {
6
6
  findSourcesWithCommand,
7
7
  formatProjectList,
8
8
  isBuiltinGitCommand,
9
+ isBuiltinWorkspaceCommand,
9
10
  } = require("../lib/discover");
10
11
  const { listCommands } = require("../lib/manifest");
11
12
  const { runWithProjectResolution, runGitScript, sourceLabel } = require("../lib/run-command");
13
+ const { runCreate } = require("../lib/create");
12
14
 
13
15
  const GIT_SCRIPT_MAP = {
14
16
  clone: "git-clone.js",
@@ -48,43 +50,45 @@ function parseGlobalArgs(argv) {
48
50
  function printUsage() {
49
51
  console.log(`usage: pan <command> [options] [-- args...]
50
52
 
51
- repo commands (built-in):
53
+ workspace commands (built-in):
54
+ create Scaffold a new PAN Stack workspace (prompts for folder name)
52
55
  clone Clone all repositories from .project.yaml
53
56
  pull Pull latest changes for all repositories
54
57
  fetch Fetch all remotes for all repositories
55
58
  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)
59
+ grv List git remotes -v for workspace + child repos
60
+ gst Git status for workspace + child repos (e.g. pan gst -sb)
58
61
 
59
62
  project commands:
60
- Gom từ .pan.yaml (workspace root + từng repo con trong .project.yaml)
63
+ Loaded from .pan.yaml (workspace root + each repo in .project.yaml)
61
64
 
62
65
  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
66
+ --project NAME Pick a repo when multiple .pan.yaml files define the same command
67
+ --base-dir DIR (git commands) base directory for child repo paths
68
+ --dry-run (git commands) print commands only
69
+ -h, --help Show help
67
70
 
68
71
  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
72
+ PAN_PROJECT_ROOTS Semicolon-separated workspace roots (; on Windows, : or ; on Unix)
73
+ ~/.pan/projects One workspace root per line
71
74
 
72
75
  examples:
76
+ pan create
77
+ pan create bookshop-stack
73
78
  pan clone
74
79
  pan grv
75
- pan be
76
80
  pan test fe
77
- pan test --project csdl-cc-b-frontend-e2e
81
+ pan test --project bookshop-web
78
82
  `);
79
83
  }
80
84
 
81
85
  function printAllCommands(sources) {
82
86
  if (sources.length === 0) {
83
- console.log("Chưa tìm thấy .pan.yaml (workspace hoặc repo con).");
87
+ console.log("No .pan.yaml found (workspace or child repos).");
84
88
  return;
85
89
  }
86
90
 
87
- console.log("Lệnh đã load:");
91
+ console.log("Loaded commands:");
88
92
  for (const source of sources) {
89
93
  const commands = listCommands(source);
90
94
  if (commands.length === 0) {
@@ -119,6 +123,11 @@ async function main() {
119
123
 
120
124
  const { command, args, project } = parsed;
121
125
 
126
+ if (command === "create") {
127
+ await runCreate(args);
128
+ return;
129
+ }
130
+
122
131
  if (isBuiltinGitCommand(command)) {
123
132
  const gitArgs = [...args];
124
133
  if (command === "push" && !gitArgs.includes("--set-upstream")) {
@@ -128,6 +137,10 @@ async function main() {
128
137
  return;
129
138
  }
130
139
 
140
+ if (isBuiltinWorkspaceCommand(command)) {
141
+ throw new Error(`Unknown built-in command: ${command}`);
142
+ }
143
+
131
144
  const allSources = loadAllCommandSources();
132
145
  const matches = findSourcesWithCommand(command);
133
146
  if (matches.length > 1 && !project && process.stdin.isTTY) {
package/lib/create.js ADDED
@@ -0,0 +1,87 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const readline = require("readline");
4
+ const { validateFolderName } = require("./platform");
5
+
6
+ const TEMPLATE_ROOT = path.join(__dirname, "..", "templates", "workspace");
7
+
8
+ function promptFolderName() {
9
+ return new Promise((resolve, reject) => {
10
+ if (!process.stdin.isTTY) {
11
+ reject(new Error("Folder name required: pan create <folder-name>"));
12
+ return;
13
+ }
14
+
15
+ const rl = readline.createInterface({
16
+ input: process.stdin,
17
+ output: process.stdout,
18
+ });
19
+
20
+ rl.question("Workspace folder name: ", (answer) => {
21
+ rl.close();
22
+ const name = answer.trim();
23
+ if (!name) {
24
+ reject(new Error("Folder name cannot be empty."));
25
+ return;
26
+ }
27
+ resolve(name);
28
+ });
29
+ });
30
+ }
31
+
32
+ function copyDir(src, dest) {
33
+ fs.mkdirSync(dest, { recursive: true });
34
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
35
+ const srcPath = path.join(src, entry.name);
36
+ const destPath = path.join(dest, entry.name);
37
+ if (entry.isDirectory()) {
38
+ copyDir(srcPath, destPath);
39
+ } else {
40
+ fs.copyFileSync(srcPath, destPath);
41
+ }
42
+ }
43
+ }
44
+
45
+ function applyPlaceholders(targetDir, folderName) {
46
+ const files = [".project.yaml", "README.md"];
47
+ for (const relativePath of files) {
48
+ const filePath = path.join(targetDir, relativePath);
49
+ if (!fs.existsSync(filePath)) {
50
+ continue;
51
+ }
52
+ const content = fs.readFileSync(filePath, "utf8");
53
+ fs.writeFileSync(filePath, content.replaceAll("__PROJECT_NAME__", folderName));
54
+ }
55
+ }
56
+
57
+ async function runCreate(args = []) {
58
+ let folderName = args[0]?.trim();
59
+ if (!folderName) {
60
+ folderName = await promptFolderName();
61
+ }
62
+
63
+ validateFolderName(folderName);
64
+
65
+ const targetDir = path.resolve(process.cwd(), folderName);
66
+ if (fs.existsSync(targetDir)) {
67
+ throw new Error(`Folder already exists: ${targetDir}`);
68
+ }
69
+
70
+ if (!fs.existsSync(TEMPLATE_ROOT)) {
71
+ throw new Error(`Template not found: ${TEMPLATE_ROOT}`);
72
+ }
73
+
74
+ copyDir(TEMPLATE_ROOT, targetDir);
75
+ applyPlaceholders(targetDir, folderName);
76
+
77
+ console.log(`Created PAN Stack workspace: ${targetDir}`);
78
+ console.log("");
79
+ console.log("Next steps:");
80
+ console.log(` cd ${folderName}`);
81
+ console.log(" # Edit .project.yaml with your repository URLs");
82
+ console.log(" pan clone");
83
+ }
84
+
85
+ module.exports = {
86
+ runCreate,
87
+ };
package/lib/discover.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
  const os = require("os");
4
+ const { splitEnvList } = require("./platform");
4
5
  const {
5
6
  PAN_CONFIG_FILE,
6
7
  loadManifest,
@@ -12,15 +13,13 @@ const {
12
13
  } = require("./manifest");
13
14
 
14
15
  const BUILTIN_GIT_COMMANDS = new Set(["clone", "pull", "fetch", "push", "grv", "gst"]);
16
+ const BUILTIN_WORKSPACE_COMMANDS = new Set(["create", ...BUILTIN_GIT_COMMANDS]);
15
17
 
16
18
  function readProjectRoots() {
17
19
  const roots = [];
18
20
  const fromEnv = process.env.PAN_PROJECT_ROOTS || "";
19
- for (const part of fromEnv.split(/[:;,]/)) {
20
- const trimmed = part.trim();
21
- if (trimmed) {
22
- roots.push(path.resolve(trimmed));
23
- }
21
+ for (const part of splitEnvList(fromEnv)) {
22
+ roots.push(path.resolve(part));
24
23
  }
25
24
 
26
25
  const registry = path.join(os.homedir(), ".pan", "projects");
@@ -188,7 +187,7 @@ function findSourcesWithCommand(commandName, startDir = process.cwd()) {
188
187
  }
189
188
 
190
189
  function formatProjectList(matches, commandName) {
191
- const lines = [`Lệnh '${commandName}' có ở nhiều nơi:`];
190
+ const lines = [`Command '${commandName}' is defined in multiple places:`];
192
191
  matches.forEach((source, index) => {
193
192
  const def = source.commands[commandName];
194
193
  const desc = def?.description ? ` — ${def.description}` : "";
@@ -199,7 +198,7 @@ function formatProjectList(matches, commandName) {
199
198
  lines.push(` ${index + 1}. ${label} (${source.root})${desc}`);
200
199
  });
201
200
  lines.push("");
202
- lines.push("Chọn số thứ tự, hoặc chạy lại với --project <tên-repo>.");
201
+ lines.push("Pick a number, or rerun with --project <repo-name>.");
203
202
  return lines.join("\n");
204
203
  }
205
204
 
@@ -207,8 +206,13 @@ function isBuiltinGitCommand(name) {
207
206
  return BUILTIN_GIT_COMMANDS.has(name);
208
207
  }
209
208
 
209
+ function isBuiltinWorkspaceCommand(name) {
210
+ return BUILTIN_WORKSPACE_COMMANDS.has(name);
211
+ }
212
+
210
213
  module.exports = {
211
214
  BUILTIN_GIT_COMMANDS,
215
+ BUILTIN_WORKSPACE_COMMANDS,
212
216
  PAN_CONFIG_FILE,
213
217
  findNearestProjectManifest,
214
218
  findNearestWorkspace,
@@ -217,6 +221,7 @@ module.exports = {
217
221
  findSourcesWithCommand,
218
222
  formatProjectList,
219
223
  isBuiltinGitCommand,
224
+ isBuiltinWorkspaceCommand,
220
225
  getCommandDefinition,
221
226
  sourceMatchesProject,
222
227
  };
@@ -10,6 +10,7 @@
10
10
  const fs = require("fs");
11
11
  const path = require("path");
12
12
  const { spawnSync } = require("child_process");
13
+ const { spawnOptions } = require("../../platform");
13
14
  const { parseCommonArgs, loadManifestRepos, printCommonOptions } = require("./helpers/cli");
14
15
 
15
16
  function cloneRepo({ name, url, destination, dryRun }) {
@@ -26,7 +27,7 @@ function cloneRepo({ name, url, destination, dryRun }) {
26
27
  }
27
28
 
28
29
  console.log(`[clone] ${name}: ${url} -> ${destination}`);
29
- const result = spawnSync(command[0], command.slice(1), { stdio: "inherit" });
30
+ const result = spawnSync(command[0], command.slice(1), spawnOptions({ stdio: "inherit" }));
30
31
  if (result.status !== 0) {
31
32
  console.log(`[warn] ${name}: clone failed, check repo access or url`);
32
33
  }
@@ -2,7 +2,7 @@
2
2
  /*
3
3
  * List git remotes (git remote -v) for PAN Stack project repositories.
4
4
  *
5
- * Alias tương đương grv trong oh-my-zsh — hiển thị URL đầy đủ (fetch/push).
5
+ * oh-my-zsh-style alias for grv — full remote URLs (fetch/push).
6
6
  */
7
7
 
8
8
  const { parseCommonArgs, loadManifestRepos, printCommonOptions } = require("./helpers/cli");
@@ -18,7 +18,7 @@ function printRepoRemotes({ label, destination, dryRun }) {
18
18
  console.log(destination);
19
19
  const lines = listRemotesVerbose(destination);
20
20
  if (lines.length === 0) {
21
- console.log("(không có remote)");
21
+ console.log("(no remotes)");
22
22
  } else {
23
23
  for (const line of lines) {
24
24
  console.log(line);
@@ -45,7 +45,7 @@ function printHelp() {
45
45
  List git remotes with full URLs (git remote -v) for each repository.
46
46
 
47
47
  ${printCommonOptions()}
48
- --no-workspace Skip workspace root (chỉ repo trong manifest)`);
48
+ --no-workspace Skip workspace root (manifest repos only)`);
49
49
  }
50
50
 
51
51
  function main() {
@@ -2,7 +2,7 @@
2
2
  /*
3
3
  * Git status for PAN Stack project repositories.
4
4
  *
5
- * Alias tương đương gst trong oh-my-zsh — chạy git status trên workspace + repo con.
5
+ * oh-my-zsh-style alias for gst — git status on workspace + child repos.
6
6
  */
7
7
 
8
8
  const { parseCommonArgs, loadManifestRepos, printCommonOptions } = require("./helpers/cli");
@@ -38,10 +38,10 @@ function printHelp() {
38
38
  console.log(`usage: git-gst.js [--base-dir BASE_DIR] [--dry-run] [--no-workspace] [manifest] [git status args...]
39
39
 
40
40
  Run git status for workspace root and each repository in the manifest.
41
- Extra args are forwarded to git status (vd: -s, -sb).
41
+ Extra args are forwarded to git status (e.g. -s, -sb).
42
42
 
43
43
  ${printCommonOptions()}
44
- --no-workspace Skip workspace root (chỉ repo trong manifest)`);
44
+ --no-workspace Skip workspace root (manifest repos only)`);
45
45
  }
46
46
 
47
47
  function main() {
@@ -72,7 +72,7 @@ function resolveProjectWorkspace({
72
72
  if (!found) {
73
73
  const label = manifestPath || ".project.yaml";
74
74
  throw new Error(
75
- `Manifest not found: ${label} (đã tìm từ ${startDir} lên thư mục cha)`
75
+ `Manifest not found: ${label} (walked up from ${startDir})`
76
76
  );
77
77
  }
78
78
  resolvedManifest = found;
@@ -140,7 +140,7 @@ function printCommonOptions() {
140
140
 
141
141
  options:
142
142
  -h, --help show this help message and exit
143
- --base-dir BASE_DIR Base directory for relative repo paths. Default: thư mục chứa .project.yaml (walk-up từ cwd nếu cần).
143
+ --base-dir BASE_DIR Base directory for relative repo paths. Default: directory containing .project.yaml (walk-up from cwd when needed).
144
144
  --dry-run Print commands only`;
145
145
  }
146
146
 
@@ -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(`Target hợp lệ cho '${commandName}': ${valid}`);
326
+ throw new Error(`Valid targets for '${commandName}': ${valid}`);
327
327
  }
328
328
  return {
329
329
  ...target,
@@ -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.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.
@@ -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.