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 +160 -96
- package/bin/pan.js +27 -14
- package/lib/create.js +87 -0
- package/lib/discover.js +12 -7
- package/lib/git/git-clone.js +2 -1
- package/lib/git/git-grv.js +3 -3
- package/lib/git/git-gst.js +3 -3
- package/lib/git/helpers/cli.js +2 -2
- 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/README.md
CHANGED
|
@@ -1,152 +1,216 @@
|
|
|
1
1
|
# pan-stack-cli
|
|
2
2
|
|
|
3
|
-
CLI
|
|
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
|
-
#
|
|
10
|
-
npm i -g /path/to
|
|
8
|
+
# or from source before npm publish:
|
|
9
|
+
npm i -g /path/to/pan-stack-cli
|
|
11
10
|
```
|
|
12
11
|
|
|
13
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
##
|
|
45
|
+
## Windows support
|
|
22
46
|
|
|
23
|
-
|
|
47
|
+
Built-in commands (`create`, `clone`, `pull`, `push`, `fetch`, `grv`, `gst`) run on
|
|
48
|
+
Windows, macOS, and Linux. Notes for Windows:
|
|
24
49
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
59
|
+
## Bootstrap a new workspace
|
|
35
60
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
+
`pan create` fails if the target folder already exists.
|
|
43
74
|
|
|
44
|
-
|
|
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
|
-
|
|
77
|
+
Two files drive the CLI. Keep responsibilities separate:
|
|
50
78
|
|
|
51
|
-
|
|
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
|
-
|
|
84
|
+
### `.project.yaml` example
|
|
54
85
|
|
|
55
86
|
```yaml
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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:
|
|
113
|
+
description: Install dependencies across cloned repositories
|
|
64
114
|
shell: |
|
|
65
115
|
set -euo pipefail
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
134
|
+
### `.pan.yaml` — child repo
|
|
75
135
|
|
|
76
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
141
|
+
dev:
|
|
142
|
+
description: Start local dev server
|
|
143
|
+
exec: npm run dev
|
|
85
144
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
151
|
+
Aliases: `alias: i` or `aliases: i, ins` (comma-separated).
|
|
98
152
|
|
|
99
|
-
|
|
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
|
-
|
|
155
|
+
Read `repositories` from the nearest `.project.yaml` (walk-up from cwd):
|
|
107
156
|
|
|
108
|
-
|
|
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
|
-
|
|
166
|
+
Git options:
|
|
111
167
|
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
cd ~/work/btp/btp-csdl-cc
|
|
172
|
+
Works from child repo directories — the CLI walks up to find `.project.yaml`.
|
|
116
173
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
pan
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
+
### Registry (optional)
|
|
132
190
|
|
|
133
|
-
|
|
134
|
-
|
|
191
|
+
- `PAN_PROJECT_ROOTS` — extra workspace roots (`;` on Windows, `:` or `;` on Unix)
|
|
192
|
+
- `~/.pan/projects` — one workspace root per line
|
|
135
193
|
|
|
136
|
-
##
|
|
194
|
+
## Package layout
|
|
137
195
|
|
|
138
196
|
```text
|
|
139
|
-
|
|
140
|
-
bin/pan.js
|
|
141
|
-
lib/
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
##
|
|
210
|
+
## Local development
|
|
147
211
|
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
57
|
-
gst Git status
|
|
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
|
-
|
|
63
|
+
Loaded from .pan.yaml (workspace root + each repo in .project.yaml)
|
|
61
64
|
|
|
62
65
|
options:
|
|
63
|
-
--project NAME
|
|
64
|
-
--base-dir DIR (git commands)
|
|
65
|
-
--dry-run (git commands)
|
|
66
|
-
-h, --help
|
|
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
|
|
70
|
-
~/.pan/projects
|
|
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
|
|
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("
|
|
87
|
+
console.log("No .pan.yaml found (workspace or child repos).");
|
|
84
88
|
return;
|
|
85
89
|
}
|
|
86
90
|
|
|
87
|
-
console.log("
|
|
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
|
|
20
|
-
|
|
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 = [`
|
|
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("
|
|
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
|
};
|
package/lib/git/git-clone.js
CHANGED
|
@@ -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
|
}
|
package/lib/git/git-grv.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
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("(
|
|
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 (
|
|
48
|
+
--no-workspace Skip workspace root (manifest repos only)`);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
function main() {
|
package/lib/git/git-gst.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
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 (
|
|
41
|
+
Extra args are forwarded to git status (e.g. -s, -sb).
|
|
42
42
|
|
|
43
43
|
${printCommonOptions()}
|
|
44
|
-
--no-workspace Skip workspace root (
|
|
44
|
+
--no-workspace Skip workspace root (manifest repos only)`);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
function main() {
|
package/lib/git/helpers/cli.js
CHANGED
|
@@ -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} (
|
|
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:
|
|
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
|
|
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.
|
|
@@ -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.
|