pan-stack-cli 0.1.4 → 0.2.1

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.
Files changed (64) hide show
  1. package/README.md +572 -23
  2. package/bin/pan.js +206 -20
  3. package/lib/ai-skills.js +829 -0
  4. package/lib/create.js +22 -4
  5. package/lib/discover.js +69 -2
  6. package/lib/git/git-checkout.js +395 -0
  7. package/lib/git/git-clone.js +163 -28
  8. package/lib/git/git-fetch-all.js +100 -33
  9. package/lib/git/git-grv.js +68 -37
  10. package/lib/git/git-gst.js +97 -26
  11. package/lib/git/git-mr.js +546 -0
  12. package/lib/git/git-pull.js +104 -43
  13. package/lib/git/git-push.js +150 -41
  14. package/lib/git/git-sync-main.js +334 -0
  15. package/lib/git/helpers/bulk-report.js +202 -0
  16. package/lib/git/helpers/cli.js +41 -5
  17. package/lib/git/helpers/colors.js +19 -0
  18. package/lib/git/helpers/git.js +74 -0
  19. package/lib/git/helpers/manifest.js +93 -0
  20. package/lib/git/helpers/repo-select.js +416 -0
  21. package/lib/git/helpers/repo-summary.js +161 -0
  22. package/lib/i18n.js +61 -0
  23. package/lib/inquirer-theme.js +20 -0
  24. package/lib/language.js +121 -0
  25. package/lib/languages.js +14 -0
  26. package/lib/manifest.js +115 -14
  27. package/lib/menu-prompt.js +201 -0
  28. package/lib/menu.js +134 -0
  29. package/lib/messages.js +647 -0
  30. package/lib/run-command.js +173 -52
  31. package/lib/select.js +0 -0
  32. package/lib/serve.js +75 -0
  33. package/lib/skill-registry-client.js +173 -0
  34. package/lib/stats/aggregate.js +51 -0
  35. package/lib/stats/identity.js +20 -0
  36. package/lib/stats/index.js +189 -0
  37. package/lib/stats/parse.js +46 -0
  38. package/lib/stats/range.js +38 -0
  39. package/lib/stats/report.js +264 -0
  40. package/lib/stats/review.js +157 -0
  41. package/lib/stats/scan.js +92 -0
  42. package/lib/stats/serve.js +25 -0
  43. package/lib/ui/api.js +202 -0
  44. package/lib/ui/clone.js +115 -0
  45. package/lib/ui/config.js +182 -0
  46. package/lib/ui/diff.js +128 -0
  47. package/lib/ui/export-gate.js +79 -0
  48. package/lib/ui/gen-react-mobile-code.js +6 -0
  49. package/lib/ui/gen-react-web-code.js +75 -0
  50. package/lib/ui/index.js +89 -0
  51. package/lib/ui/init.js +252 -0
  52. package/lib/ui/login.js +128 -0
  53. package/lib/ui/logout.js +22 -0
  54. package/lib/ui/palettes.js +103 -0
  55. package/lib/ui/published-html.js +24 -0
  56. package/lib/ui/sync.js +137 -0
  57. package/lib/ui/workspace.js +213 -0
  58. package/package.json +10 -3
  59. package/templates/workspace/.claude/skills/writing-guidelines/SKILL.md +0 -48
  60. package/templates/workspace/.codex/skills/writing-guidelines/SKILL.md +0 -48
  61. package/templates/workspace/.cursor/skills/writing-guidelines/SKILL.md +0 -48
  62. package/templates/workspace/.pan.yaml +0 -25
  63. package/templates/workspace/.project.yaml +0 -15
  64. package/templates/workspace/README.md +0 -35
@@ -0,0 +1,213 @@
1
+ // lib/ui/workspace.js — monorepo scan cho `pan ui` (§4.7): tìm workspace root, quét
2
+ // mọi app.panui.yaml (KHÔNG dùng `repositories:` trong .project.yaml), chọn dự án,
3
+ // đọc/ghi khối `backend: { id, apiUrl }` trong app.panui.yaml.
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const { findNearestProjectManifest } = require("../discover");
7
+ const { menuSelect } = require("../menu-prompt");
8
+ const { t } = require("../i18n");
9
+
10
+ const IGNORE_DIRS = new Set(["node_modules", ".git", "dist", ".codegen-check"]);
11
+ const MAX_DEPTH = 5;
12
+ const APP_YAML_NAME = "app.panui.yaml";
13
+
14
+ /**
15
+ * Tìm workspace root bằng walkUp `.project.yaml` (tái dùng lib/discover.js).
16
+ * @param {string} [cwd]
17
+ * @returns {string|null}
18
+ */
19
+ function findWorkspaceRoot(cwd = process.cwd()) {
20
+ const manifestPath = findNearestProjectManifest(cwd);
21
+ return manifestPath ? path.dirname(manifestPath) : null;
22
+ }
23
+
24
+ /**
25
+ * Quét đệ quy tối đa MAX_DEPTH cấp từ root, tìm mọi app.panui.yaml.
26
+ * @param {string} root
27
+ * @returns {Array<{ appYamlPath: string, dir: string, name: string }>}
28
+ */
29
+ function scanPanuiProjects(root) {
30
+ const results = [];
31
+
32
+ function walk(dir, depth) {
33
+ if (depth > MAX_DEPTH) {
34
+ return;
35
+ }
36
+ let entries;
37
+ try {
38
+ entries = fs.readdirSync(dir, { withFileTypes: true });
39
+ } catch {
40
+ return;
41
+ }
42
+
43
+ for (const entry of entries) {
44
+ if (entry.name.startsWith(".") && entry.name !== ".") {
45
+ // vẫn cho phép quét vào các thư mục ẩn không nằm trong IGNORE_DIRS? Không —
46
+ // giữ đơn giản: bỏ mọi thư mục ẩn để tránh .git, .cache, ... rơi vào quét.
47
+ if (entry.isDirectory()) {
48
+ continue;
49
+ }
50
+ }
51
+ const fullPath = path.join(dir, entry.name);
52
+ if (entry.isDirectory()) {
53
+ if (IGNORE_DIRS.has(entry.name)) {
54
+ continue;
55
+ }
56
+ walk(fullPath, depth + 1);
57
+ continue;
58
+ }
59
+ if (entry.isFile() && entry.name === APP_YAML_NAME) {
60
+ results.push({
61
+ appYamlPath: fullPath,
62
+ dir,
63
+ name: path.basename(dir),
64
+ });
65
+ }
66
+ }
67
+ }
68
+
69
+ walk(root, 0);
70
+ return results;
71
+ }
72
+
73
+ /**
74
+ * @param {Array<{ appYamlPath: string, dir: string, name: string }>} projects
75
+ * @returns {Promise<{ appYamlPath: string, dir: string, name: string }>}
76
+ */
77
+ async function selectProject(projects) {
78
+ if (!projects || projects.length === 0) {
79
+ throw new Error(
80
+ `Không tìm thấy dự án panui nào (không có ${APP_YAML_NAME} trong workspace, quét tối đa ${MAX_DEPTH} cấp).`
81
+ );
82
+ }
83
+ if (projects.length === 1) {
84
+ return projects[0];
85
+ }
86
+
87
+ const message = `Chọn dự án panui (${projects.length} dự án):`;
88
+ const choices = projects.map((p) => ({
89
+ name: `${p.name} (${path.relative(process.cwd(), p.appYamlPath)})`,
90
+ value: p.appYamlPath,
91
+ }));
92
+ const hotkeys = [{ key: "x", label: t("EXIT_LABEL"), value: "__exit__" }];
93
+
94
+ let choice;
95
+ try {
96
+ choice = await menuSelect({ message, choices, hotkeys });
97
+ } catch (err) {
98
+ if (String(err && err.message) !== "no-core") throw err;
99
+ const inq = await import("@inquirer/prompts").catch(() => null);
100
+ if (!inq || typeof inq.select !== "function") {
101
+ throw new Error(
102
+ "Nhiều dự án panui — cần TTY để chọn (không tìm thấy @inquirer/prompts)."
103
+ );
104
+ }
105
+ choice = await inq.select({
106
+ message,
107
+ choices: [...choices, new inq.Separator(), { name: `x ${t("EXIT_LABEL")}`, value: "__exit__" }],
108
+ loop: false,
109
+ });
110
+ }
111
+ if (choice === "__exit__") {
112
+ console.log(t("EXITED"));
113
+ process.exit(0);
114
+ }
115
+ return projects.find((p) => p.appYamlPath === choice);
116
+ }
117
+
118
+ /**
119
+ * Đọc khối `backend: { id, apiUrl }` trong app.panui.yaml (parser tối thiểu —
120
+ * chỉ hỗ trợ khối phẳng 2 dòng con `id:`/`apiUrl:` thụt lề 2 khoảng trắng).
121
+ * @param {string} appYamlPath
122
+ * @returns {{ id: string, apiUrl: string } | null}
123
+ */
124
+ function readBackendId(appYamlPath) {
125
+ if (!fs.existsSync(appYamlPath)) {
126
+ return null;
127
+ }
128
+ const lines = fs.readFileSync(appYamlPath, "utf8").split(/\r?\n/);
129
+ let inBackend = false;
130
+ const result = {};
131
+ for (const rawLine of lines) {
132
+ if (!rawLine.trim()) {
133
+ continue;
134
+ }
135
+ const indent = rawLine.length - rawLine.trimStart().length;
136
+ const text = rawLine.trim();
137
+ if (indent === 0) {
138
+ inBackend = text.replace(/:$/, "") === "backend";
139
+ continue;
140
+ }
141
+ if (inBackend && indent === 2) {
142
+ const idx = text.indexOf(":");
143
+ if (idx < 0) continue;
144
+ const key = text.slice(0, idx).trim();
145
+ let value = text.slice(idx + 1).trim();
146
+ if (
147
+ (value.startsWith('"') && value.endsWith('"')) ||
148
+ (value.startsWith("'") && value.endsWith("'"))
149
+ ) {
150
+ value = value.slice(1, -1);
151
+ }
152
+ if (key === "id" || key === "apiUrl") {
153
+ result[key] = value;
154
+ }
155
+ }
156
+ }
157
+ return result.id || result.apiUrl ? result : null;
158
+ }
159
+
160
+ /**
161
+ * Ghi/cập nhật khối `backend: { id, apiUrl }` trong app.panui.yaml, giữ nguyên
162
+ * mọi nội dung khác của file (thay thế khối backend cũ nếu có, nếu không thì
163
+ * append vào cuối file).
164
+ * @param {string} appYamlPath
165
+ * @param {{ id: string, apiUrl: string }} backend
166
+ */
167
+ function writeBackendId(appYamlPath, { id, apiUrl }) {
168
+ const original = fs.existsSync(appYamlPath)
169
+ ? fs.readFileSync(appYamlPath, "utf8")
170
+ : "";
171
+ const lines = original.length ? original.split(/\r?\n/) : [];
172
+
173
+ const newBlock = ["backend:", ` id: ${id}`, ` apiUrl: ${apiUrl}`];
174
+
175
+ let start = -1;
176
+ let end = -1;
177
+ for (let i = 0; i < lines.length; i += 1) {
178
+ const trimmed = lines[i].trim();
179
+ const indent = lines[i].length - lines[i].trimStart().length;
180
+ if (indent === 0 && trimmed.replace(/:$/, "") === "backend") {
181
+ start = i;
182
+ end = i + 1;
183
+ while (end < lines.length) {
184
+ const nextIndent = lines[end].length - lines[end].trimStart().length;
185
+ if (!lines[end].trim() || nextIndent > 0) {
186
+ end += 1;
187
+ continue;
188
+ }
189
+ break;
190
+ }
191
+ break;
192
+ }
193
+ }
194
+
195
+ let updatedLines;
196
+ if (start >= 0) {
197
+ updatedLines = [...lines.slice(0, start), ...newBlock, ...lines.slice(end)];
198
+ } else {
199
+ updatedLines = lines.length ? [...lines, "", ...newBlock] : [...newBlock];
200
+ }
201
+
202
+ fs.mkdirSync(path.dirname(appYamlPath), { recursive: true });
203
+ fs.writeFileSync(appYamlPath, `${updatedLines.join("\n")}\n`, "utf8");
204
+ }
205
+
206
+ module.exports = {
207
+ APP_YAML_NAME,
208
+ findWorkspaceRoot,
209
+ scanPanuiProjects,
210
+ selectProject,
211
+ readBackendId,
212
+ writeBackendId,
213
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pan-stack-cli",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "PAN Stack CLI — multi-repo git and project commands from .project.yaml",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -8,12 +8,16 @@
8
8
  },
9
9
  "files": [
10
10
  "bin",
11
- "lib",
12
- "templates"
11
+ "lib"
13
12
  ],
14
13
  "engines": {
15
14
  "node": ">=18"
16
15
  },
16
+ "dependencies": {
17
+ "@inquirer/core": "^11.2.1",
18
+ "@inquirer/prompts": "^8.5.2",
19
+ "serve": "^14.2.4"
20
+ },
17
21
  "keywords": [
18
22
  "pan-stack",
19
23
  "monorepo",
@@ -24,5 +28,8 @@
24
28
  "type": "git",
25
29
  "url": "https://github.com/doiya46/pan-stack.git",
26
30
  "directory": ".pan/pan-stack-cli"
31
+ },
32
+ "devDependencies": {
33
+ "@inquirer/testing": "^3.3.9"
27
34
  }
28
35
  }
@@ -1,48 +0,0 @@
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.
@@ -1,48 +0,0 @@
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.
@@ -1,48 +0,0 @@
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.
@@ -1,25 +0,0 @@
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
@@ -1,15 +0,0 @@
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
@@ -1,35 +0,0 @@
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.