pan-stack-cli 0.2.1 → 0.2.3

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 (59) hide show
  1. package/README.md +37 -731
  2. package/bin/pan.js +4978 -269
  3. package/package.json +17 -5
  4. package/lib/ai-skills.js +0 -829
  5. package/lib/create.js +0 -105
  6. package/lib/discover.js +0 -294
  7. package/lib/git/git-checkout.js +0 -395
  8. package/lib/git/git-clone.js +0 -206
  9. package/lib/git/git-fetch-all.js +0 -129
  10. package/lib/git/git-grv.js +0 -125
  11. package/lib/git/git-gst.js +0 -165
  12. package/lib/git/git-mr.js +0 -546
  13. package/lib/git/git-pull.js +0 -137
  14. package/lib/git/git-push.js +0 -185
  15. package/lib/git/git-sync-main.js +0 -334
  16. package/lib/git/helpers/bulk-report.js +0 -202
  17. package/lib/git/helpers/cli.js +0 -188
  18. package/lib/git/helpers/colors.js +0 -19
  19. package/lib/git/helpers/git.js +0 -136
  20. package/lib/git/helpers/manifest.js +0 -223
  21. package/lib/git/helpers/repo-select.js +0 -416
  22. package/lib/git/helpers/repo-summary.js +0 -161
  23. package/lib/i18n.js +0 -61
  24. package/lib/inquirer-theme.js +0 -20
  25. package/lib/language.js +0 -121
  26. package/lib/languages.js +0 -14
  27. package/lib/manifest.js +0 -467
  28. package/lib/menu-prompt.js +0 -201
  29. package/lib/menu.js +0 -134
  30. package/lib/messages.js +0 -647
  31. package/lib/platform.js +0 -140
  32. package/lib/run-command.js +0 -334
  33. package/lib/select.js +0 -0
  34. package/lib/serve.js +0 -75
  35. package/lib/skill-registry-client.js +0 -173
  36. package/lib/stats/aggregate.js +0 -51
  37. package/lib/stats/identity.js +0 -20
  38. package/lib/stats/index.js +0 -189
  39. package/lib/stats/parse.js +0 -46
  40. package/lib/stats/range.js +0 -38
  41. package/lib/stats/report.js +0 -264
  42. package/lib/stats/review.js +0 -157
  43. package/lib/stats/scan.js +0 -92
  44. package/lib/stats/serve.js +0 -25
  45. package/lib/ui/api.js +0 -202
  46. package/lib/ui/clone.js +0 -115
  47. package/lib/ui/config.js +0 -182
  48. package/lib/ui/diff.js +0 -128
  49. package/lib/ui/export-gate.js +0 -79
  50. package/lib/ui/gen-react-mobile-code.js +0 -6
  51. package/lib/ui/gen-react-web-code.js +0 -75
  52. package/lib/ui/index.js +0 -89
  53. package/lib/ui/init.js +0 -252
  54. package/lib/ui/login.js +0 -128
  55. package/lib/ui/logout.js +0 -22
  56. package/lib/ui/palettes.js +0 -103
  57. package/lib/ui/published-html.js +0 -24
  58. package/lib/ui/sync.js +0 -137
  59. package/lib/ui/workspace.js +0 -213
@@ -1,103 +0,0 @@
1
- // lib/ui/palettes.js — bảng màu cho `pan ui init` scaffold.
2
- // Mục tiêu: chỉ đổi màu chủ đạo (primary/secondary/accent...) — panui theme + inline HTML
3
- // của template dùng placeholder __PRIMARY__ / __PRIMARY_DARK__ / ... init thay bằng hex bên dưới.
4
- //
5
- // theme app.panui.yaml chỉ nhận primary/sidebar/accent; các sắc còn lại (primaryDark, secondary,
6
- // primarySoft, onPrimary) dùng cho gradient/nền trong HTML inline (html-ui) của template landing.
7
-
8
- /** @typedef {{ id, label, primary, primaryDark, secondary, accent, sidebar, primarySoft, onPrimary }} Palette */
9
-
10
- /** Danh sách placeholder init sẽ thay trong MỌI file template. */
11
- const PALETTE_TOKENS = [
12
- "primary",
13
- "primaryDark",
14
- "secondary",
15
- "accent",
16
- "sidebar",
17
- "primarySoft",
18
- "onPrimary",
19
- ];
20
-
21
- /** @type {Palette[]} */
22
- const PALETTES = [
23
- {
24
- id: "indigo",
25
- label: "Indigo — Xanh tím than (mặc định, hiện đại)",
26
- primary: "#4f46e5",
27
- primaryDark: "#4338ca",
28
- secondary: "#0ea5e9",
29
- accent: "#f59e0b",
30
- sidebar: "#1e1b4b",
31
- primarySoft: "#eef2ff",
32
- onPrimary: "#ffffff",
33
- },
34
- {
35
- id: "emerald",
36
- label: "Emerald — Xanh lá (tươi, tin cậy)",
37
- primary: "#059669",
38
- primaryDark: "#047857",
39
- secondary: "#14b8a6",
40
- accent: "#f59e0b",
41
- sidebar: "#064e3b",
42
- primarySoft: "#ecfdf5",
43
- onPrimary: "#ffffff",
44
- },
45
- {
46
- id: "rose",
47
- label: "Rose — Đỏ hồng (nổi bật, năng lượng)",
48
- primary: "#e11d48",
49
- primaryDark: "#be123c",
50
- secondary: "#f43f5e",
51
- accent: "#fb7185",
52
- sidebar: "#4c0519",
53
- primarySoft: "#fff1f2",
54
- onPrimary: "#ffffff",
55
- },
56
- {
57
- id: "violet",
58
- label: "Violet — Tím (sáng tạo, cao cấp)",
59
- primary: "#7c3aed",
60
- primaryDark: "#6d28d9",
61
- secondary: "#a855f7",
62
- accent: "#ec4899",
63
- sidebar: "#2e1065",
64
- primarySoft: "#f5f3ff",
65
- onPrimary: "#ffffff",
66
- },
67
- {
68
- id: "amber",
69
- label: "Amber — Cam hổ phách (ấm, thân thiện)",
70
- primary: "#ea580c",
71
- primaryDark: "#c2410c",
72
- secondary: "#f59e0b",
73
- accent: "#fbbf24",
74
- sidebar: "#431407",
75
- primarySoft: "#fff7ed",
76
- onPrimary: "#ffffff",
77
- },
78
- {
79
- id: "graphite",
80
- label: "Graphite — Xám than (tối giản, sang trọng)",
81
- primary: "#0f172a",
82
- primaryDark: "#020617",
83
- secondary: "#334155",
84
- accent: "#38bdf8",
85
- sidebar: "#020617",
86
- primarySoft: "#f1f5f9",
87
- onPrimary: "#ffffff",
88
- },
89
- ];
90
-
91
- const PALETTE_BY_ID = new Map(PALETTES.map((p) => [p.id, p]));
92
-
93
- /** Bản đồ placeholder `__TOKEN__` → hex cho 1 palette (dùng khi thay thế template). */
94
- function paletteReplacements(palette) {
95
- const map = {};
96
- for (const token of PALETTE_TOKENS) {
97
- const upper = token.replace(/([a-z])([A-Z])/g, "$1_$2").toUpperCase();
98
- map[`__${upper}__`] = palette[token];
99
- }
100
- return map; // vd { "__PRIMARY__": "#4f46e5", "__PRIMARY_DARK__": "#4338ca", ... }
101
- }
102
-
103
- module.exports = { PALETTES, PALETTE_BY_ID, PALETTE_TOKENS, paletteReplacements };
@@ -1,24 +0,0 @@
1
- // lib/ui/published-html.js — GATE sync (§4.6, OD-U2) rồi POST /projects/:id/export/html,
2
- // poll export/versions tới done, in URL công khai <apiBase>/p/<urlPublicPrefix>.
3
- const { ensureSynced, pollExportDone } = require("./export-gate");
4
-
5
- async function run(_args) {
6
- const { api, project, backend } = await ensureSynced();
7
-
8
- console.log(`Đang build & publish HTML cho '${project.name || project.id}'...`);
9
- await api.exportHtml(backend.id);
10
- console.log("Đang chờ build hoàn tất...");
11
- const version = await pollExportDone(api, backend.id);
12
-
13
- const prefix = project.urlPublicPrefix || (version && version.urlPublicPrefix);
14
- if (!prefix) {
15
- console.log(
16
- "\nBuild xong nhưng không xác định được urlPublicPrefix — kiểm tra thủ công trên `GET /projects/:id`."
17
- );
18
- return;
19
- }
20
- const url = `${backend.apiUrl}/p/${prefix}`;
21
- console.log(`\nĐã publish: ${url}`);
22
- }
23
-
24
- module.exports = { run };
package/lib/ui/sync.js DELETED
@@ -1,137 +0,0 @@
1
- // lib/ui/sync.js — push local→backend + diff + cảnh báo tên/id dự án + xác nhận (§4.5,
2
- // OD-U3). Xuất thêm `resolveProject`/`diffProject`/`pushDiff` để published-html.js và
3
- // gen-react-web-code.js tái dùng làm GATE (§4.6, OD-U2) qua lib/ui/export-gate.js.
4
- const fs = require("fs");
5
- const path = require("path");
6
- const { createApiClient } = require("./api");
7
- const { getApiConfig, loadCredential, isExpired } = require("./config");
8
- const {
9
- findWorkspaceRoot,
10
- scanPanuiProjects,
11
- selectProject,
12
- readBackendId,
13
- } = require("./workspace");
14
- const { computeDiff, isDiffClean, printDiff } = require("./diff");
15
-
16
- async function loadInquirer() {
17
- const inq = await import("@inquirer/prompts").catch(() => null);
18
- if (!inq) {
19
- throw new Error("Cần TTY (@inquirer/prompts) cho lệnh này.");
20
- }
21
- return inq;
22
- }
23
-
24
- function toList(res) {
25
- if (Array.isArray(res)) return res;
26
- if (res && Array.isArray(res.items)) return res.items;
27
- if (res && Array.isArray(res.data)) return res.data;
28
- return [];
29
- }
30
-
31
- /**
32
- * Monorepo scan (§4.7) → chọn dự án panui → đọc backend.id. Thiếu backend.id → gợi ý clone.
33
- * @returns {Promise<{ projectDir: string, appYamlPath: string, backend: {id:string, apiUrl:string} }>}
34
- */
35
- async function resolveProject() {
36
- const workspaceRoot = findWorkspaceRoot() || process.cwd();
37
- const candidates = scanPanuiProjects(workspaceRoot);
38
- const picked = await selectProject(candidates);
39
- const backend = readBackendId(picked.appYamlPath);
40
- if (!backend || !backend.id || !backend.apiUrl) {
41
- throw new Error(
42
- `Dự án '${picked.name}' chưa liên kết backend (thiếu backend.id/apiUrl trong app.panui.yaml) — chạy \`pan ui clone\` để tạo liên kết.`
43
- );
44
- }
45
- return { projectDir: picked.dir, appYamlPath: picked.appYamlPath, backend };
46
- }
47
-
48
- /**
49
- * Chạy diff local↔backend cho 1 dự án đã resolve (yêu cầu credential hợp lệ).
50
- * @param {{ projectDir: string, backend: {id:string, apiUrl:string} }} resolved
51
- * @returns {Promise<{ api, project, backend, projectDir, diff }>}
52
- */
53
- async function diffProject({ projectDir, backend }) {
54
- const cred = loadCredential(backend.apiUrl);
55
- if (!cred || isExpired(cred)) {
56
- throw new Error("Chưa đăng nhập hoặc key đã hết hạn — chạy `pan ui init` trước.");
57
- }
58
- const api = createApiClient({ apiUrl: backend.apiUrl, token: cred.token });
59
- const project = await api.getProject(backend.id);
60
- const treeRes = await api.listFiles(backend.id);
61
- const backendFiles = toList(treeRes);
62
- const diff = await computeDiff(projectDir, backendFiles, async (relPath) => {
63
- const res = await api.getContent(backend.id, relPath);
64
- return typeof res === "string" ? res : res && res.content;
65
- });
66
- return { api, project, backend, projectDir, diff };
67
- }
68
-
69
- /**
70
- * Đẩy thay đổi lên backend theo kết quả diffProject(). Hỏi riêng trước khi xóa.
71
- * @param {{ api, backend, projectDir, diff }} state
72
- */
73
- async function pushDiff({ api, backend, projectDir, diff }) {
74
- for (const relPath of [...diff.added, ...diff.changed]) {
75
- const content = fs.readFileSync(path.join(projectDir, relPath), "utf8");
76
- await api.putFile(backend.id, {
77
- path: relPath,
78
- content,
79
- editNote: "pan-cli sync",
80
- });
81
- console.log(` đã đẩy: ${relPath}`);
82
- }
83
-
84
- if (diff.removed.length) {
85
- const inq = await loadInquirer();
86
- const ok = await inq.confirm({
87
- message: `Xóa ${diff.removed.length} file trên backend không còn ở local?`,
88
- default: false,
89
- });
90
- if (ok) {
91
- for (const relPath of diff.removed) {
92
- await api.deleteFile(backend.id, relPath);
93
- console.log(` đã xóa: ${relPath}`);
94
- }
95
- } else {
96
- console.log("Bỏ qua xóa file trên backend.");
97
- }
98
- }
99
- }
100
-
101
- async function run(_args) {
102
- const resolved = await resolveProject();
103
- const state = await diffProject(resolved);
104
- const { project, backend, diff } = state;
105
-
106
- const inq = await loadInquirer();
107
- console.log(
108
- `Đồng bộ lên dự án '${project.name || project.id}' (id ${backend.id}) — đúng dự án?`
109
- );
110
- const proceed = await inq.confirm({ message: "Tiếp tục?", default: false });
111
- if (!proceed) {
112
- console.log("Đã hủy.");
113
- return;
114
- }
115
-
116
- if (isDiffClean(diff)) {
117
- console.log("Không có thay đổi để đồng bộ — local đã khớp backend.");
118
- return;
119
- }
120
-
121
- console.log("Thay đổi phát hiện:");
122
- printDiff(diff);
123
-
124
- const ok = await inq.confirm({
125
- message: "Đẩy các thay đổi trên lên backend?",
126
- default: false,
127
- });
128
- if (!ok) {
129
- console.log("Đã hủy.");
130
- return;
131
- }
132
-
133
- await pushDiff(state);
134
- console.log("Đồng bộ hoàn tất.");
135
- }
136
-
137
- module.exports = { run, resolveProject, diffProject, pushDiff };
@@ -1,213 +0,0 @@
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
- };