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,79 +0,0 @@
1
- // lib/ui/export-gate.js — GATE dùng chung cho published-html.js & gen-react-web-code.js
2
- // (§4.6, OD-U2): chạy diff (như sync), nếu local≠backend thì cảnh báo + hỏi sync trước;
3
- // đồng thời cung cấp poll trạng thái export/build.
4
- const sync = require("./sync");
5
- const { isDiffClean, printDiff } = require("./diff");
6
-
7
- async function loadInquirer() {
8
- const inq = await import("@inquirer/prompts").catch(() => null);
9
- if (!inq) {
10
- throw new Error("Cần TTY (@inquirer/prompts) cho lệnh này.");
11
- }
12
- return inq;
13
- }
14
-
15
- function sleep(ms) {
16
- return new Promise((resolve) => setTimeout(resolve, ms));
17
- }
18
-
19
- function toList(res) {
20
- if (Array.isArray(res)) return res;
21
- if (res && Array.isArray(res.items)) return res.items;
22
- if (res && Array.isArray(res.data)) return res.data;
23
- return [];
24
- }
25
-
26
- /**
27
- * Chọn dự án + đảm bảo local == backend trước khi build (sync nếu user đồng ý).
28
- * @returns {Promise<{ api, project, backend, projectDir }>}
29
- */
30
- async function ensureSynced() {
31
- const resolved = await sync.resolveProject();
32
- let state = await sync.diffProject(resolved);
33
-
34
- if (!isDiffClean(state.diff)) {
35
- console.log("Local khác backend — cần đồng bộ trước khi build/publish:");
36
- printDiff(state.diff);
37
- const inq = await loadInquirer();
38
- const doSync = await inq.confirm({
39
- message: "Đồng bộ (sync) ngay bây giờ?",
40
- default: true,
41
- });
42
- if (!doSync) {
43
- throw new Error("Đã dừng — chạy `pan ui sync` trước khi publish/build.");
44
- }
45
- await sync.pushDiff(state);
46
- state = await sync.diffProject(resolved);
47
- if (!isDiffClean(state.diff)) {
48
- throw new Error("Vẫn còn khác biệt sau khi sync — kiểm tra lại thủ công.");
49
- }
50
- }
51
- return state;
52
- }
53
-
54
- /**
55
- * Poll GET /projects/:id/export/versions tới khi phiên bản mới nhất done/failed.
56
- * @param {*} api
57
- * @param {string} projectId
58
- * @param {{ intervalMs?: number, timeoutMs?: number }} [opts]
59
- */
60
- async function pollExportDone(api, projectId, opts = {}) {
61
- const { intervalMs = 3000, timeoutMs = 5 * 60 * 1000 } = opts;
62
- const deadline = Date.now() + timeoutMs;
63
- while (Date.now() < deadline) {
64
- const versionsRes = await api.pollExport(projectId);
65
- const list = toList(versionsRes);
66
- const latest = list[0];
67
- if (latest && ["done", "completed", "success"].includes(latest.status)) {
68
- return latest;
69
- }
70
- if (latest && ["failed", "error"].includes(latest.status)) {
71
- throw new Error(`Build thất bại: ${latest.error || latest.status}`);
72
- }
73
- process.stdout.write(".");
74
- await sleep(intervalMs);
75
- }
76
- throw new Error("Hết thời gian chờ build (timeout).");
77
- }
78
-
79
- module.exports = { ensureSynced, pollExportDone };
@@ -1,6 +0,0 @@
1
- // lib/ui/gen-react-mobile-code.js — STUB. Chưa có kế hoạch (chờ chốt) — chỉ báo "Đang phát triển".
2
- async function run(_args) {
3
- console.log("Đang phát triển");
4
- }
5
-
6
- module.exports = { run };
@@ -1,75 +0,0 @@
1
- // lib/ui/gen-react-web-code.js — GATE sync (§4.6, OD-U2) rồi POST /projects/:id/export/react,
2
- // poll, download .tar.gz (api.js download), giải nén ra thư mục đích (mặc định
3
- // ./pan-ui-react, lỗi nếu đã tồn tại). Giải nén dùng lệnh `tar` hệ thống qua child_process
4
- // (không thêm dep nén) — nếu không có `tar`, lưu file .tar.gz + in hướng dẫn (honest-TODO).
5
- const fs = require("fs");
6
- const path = require("path");
7
- const { execFileSync } = require("child_process");
8
- const { ensureSynced, pollExportDone } = require("./export-gate");
9
-
10
- async function loadInquirer() {
11
- const inq = await import("@inquirer/prompts").catch(() => null);
12
- if (!inq) {
13
- throw new Error("Cần TTY (@inquirer/prompts) cho lệnh này.");
14
- }
15
- return inq;
16
- }
17
-
18
- function hasTarBinary() {
19
- try {
20
- execFileSync("tar", ["--version"], { stdio: "ignore" });
21
- return true;
22
- } catch {
23
- return false;
24
- }
25
- }
26
-
27
- async function run(_args) {
28
- const { api, project, backend } = await ensureSynced();
29
-
30
- console.log(`Đang build mã React web cho '${project.name || project.id}'...`);
31
- await api.exportReact(backend.id);
32
- console.log("Đang chờ build hoàn tất...");
33
- const version = await pollExportDone(api, backend.id);
34
-
35
- const inq = await loadInquirer();
36
- const targetInput = await inq.input({
37
- message: "Thư mục đích để giải nén mã React (vd: pan-ui-react, ./out/react, /abs/path):",
38
- default: "pan-ui-react",
39
- });
40
- const targetDir = path.isAbsolute(targetInput)
41
- ? targetInput
42
- : path.resolve(process.cwd(), targetInput);
43
-
44
- if (fs.existsSync(targetDir)) {
45
- throw new Error(`Thư mục đã tồn tại: ${targetDir} — xóa thủ công rồi thử lại.`);
46
- }
47
-
48
- const downloadUrl =
49
- version.downloadUrl ||
50
- (version.id && `/export/versions/${version.id}/download`);
51
- if (!downloadUrl) {
52
- throw new Error(
53
- "Build xong nhưng không xác định được downloadUrl/version.id — kiểm tra thủ công `GET /projects/:id/export/versions`."
54
- );
55
- }
56
-
57
- console.log("Đang tải file build...");
58
- const buffer = await api.download(downloadUrl);
59
- const tarballPath = `${targetDir}.tar.gz`;
60
- fs.writeFileSync(tarballPath, buffer);
61
-
62
- if (hasTarBinary()) {
63
- fs.mkdirSync(targetDir, { recursive: true });
64
- execFileSync("tar", ["-xzf", tarballPath, "-C", targetDir]);
65
- fs.unlinkSync(tarballPath);
66
- console.log(`Đã giải nén mã React vào: ${targetDir}`);
67
- } else {
68
- console.log(
69
- `[honest-TODO] Không tìm thấy lệnh 'tar' trên hệ thống — đã lưu file nén tại: ${tarballPath}`
70
- );
71
- console.log(`Giải nén thủ công: tar -xzf ${tarballPath} -C <thư mục đích>`);
72
- }
73
- }
74
-
75
- module.exports = { run };
package/lib/ui/index.js DELETED
@@ -1,89 +0,0 @@
1
- // lib/ui/index.js — router + menu cho `pan ui` (§4.1). Subcommand thật (Wave 2) sống trong
2
- // lib/ui/<name>.js; ở Phase 2 mọi file đó CHỈ là stub.
3
- const { menuSelect } = require("../menu-prompt");
4
- const { t } = require("../i18n");
5
-
6
- const SUBCOMMANDS = [
7
- { name: "init", label: "init — tạo dự án panui mẫu (chọn loại + bảng màu)" },
8
- { name: "login", label: "login — đăng nhập Pan Studio (mở trình duyệt, device-flow)" },
9
- { name: "logout", label: "logout — đăng xuất, xóa key local" },
10
- { name: "clone", label: "clone — tải dự án panui về local" },
11
- { name: "sync", label: "sync — đẩy thay đổi local lên backend" },
12
- { name: "published-html", label: "published-html — build & publish trang HTML" },
13
- { name: "gen-react-web-code", label: "gen-react-web-code — sinh mã React web" },
14
- { name: "gen-react-mobile-code", label: "gen-react-mobile-code — sinh mã React mobile" },
15
- ];
16
-
17
- const ALIASES = {};
18
-
19
- const SUBCOMMAND_NAMES = new Set(SUBCOMMANDS.map((s) => s.name));
20
-
21
- function requireHandler(name) {
22
- // eslint-disable-next-line global-require, import/no-dynamic-require
23
- return require(`./${name}`);
24
- }
25
-
26
- async function runSubcommand(name, args) {
27
- const handler = requireHandler(name);
28
- await handler.run(args);
29
- }
30
-
31
- async function promptMenu() {
32
- const message = "pan ui — chọn hành động:";
33
- const choices = SUBCOMMANDS.map((s) => ({ name: s.label, value: s.name }));
34
- const hotkeys = [{ key: "x", label: t("EXIT_LABEL"), value: "exit" }];
35
-
36
- // Menu gốc (không có cấp cha) — chỉ cần phím x thoát; nhất quán với các menu khác trong CLI
37
- // (menuSelect ưu tiên; thiếu @inquirer/core → @inquirer/prompts select + item thoát ẩn).
38
- try {
39
- return await menuSelect({ message, choices, hotkeys });
40
- } catch (err) {
41
- if (String(err && err.message) !== "no-core") throw err;
42
- }
43
-
44
- const inq = await import("@inquirer/prompts").catch(() => null);
45
- if (!inq || typeof inq.select !== "function") {
46
- throw new Error(
47
- "Không có TTY / @inquirer/prompts để hiện menu — chạy trực tiếp: pan ui <subcommand>."
48
- );
49
- }
50
- return inq.select({
51
- message,
52
- choices: [
53
- ...choices,
54
- new inq.Separator(),
55
- { name: `x ${t("EXIT_LABEL")}`, value: "exit" },
56
- ],
57
- loop: false,
58
- });
59
- }
60
-
61
- /**
62
- * @param {string[]} args - vd ["clone", "--foo"] hoặc [] (menu tương tác)
63
- */
64
- async function runUi(args) {
65
- const [rawSub, ...rest] = args;
66
- const maybeSub = rawSub && ALIASES[rawSub] ? ALIASES[rawSub] : rawSub;
67
-
68
- if (maybeSub && SUBCOMMAND_NAMES.has(maybeSub)) {
69
- await runSubcommand(maybeSub, rest);
70
- return;
71
- }
72
-
73
- if (maybeSub) {
74
- throw new Error(
75
- `pan ui: subcommand không hợp lệ '${maybeSub}'. Các subcommand hợp lệ: ${[...SUBCOMMAND_NAMES].join(", ")}`
76
- );
77
- }
78
-
79
- const choice = await promptMenu();
80
- if (choice === "exit") {
81
- return;
82
- }
83
- await runSubcommand(choice, []);
84
- }
85
-
86
- module.exports = {
87
- SUBCOMMANDS,
88
- runUi,
89
- };
package/lib/ui/init.js DELETED
@@ -1,252 +0,0 @@
1
- // lib/ui/init.js — `pan ui init`: scaffold dự án panui mẫu.
2
- // Flow: chọn loại (web landing / mobile / cms) → chọn template → chọn bảng màu →
3
- // nhập tên/đường dẫn → (tồn tại→lỗi) → copy template + thay placeholder palette/tên.
4
- // KHÔNG cần auth/backend — thuần local.
5
- const fs = require("fs");
6
- const os = require("os");
7
- const path = require("path");
8
- const { PALETTES, PALETTE_BY_ID, paletteReplacements } = require("./palettes");
9
- const { menuSelect } = require("../menu-prompt");
10
- const { t } = require("../i18n");
11
- const { getRegistryBaseUrl, fetchRegistry, downloadAndExtract } = require("../skill-registry-client");
12
-
13
- // Template pan-ui (landing-glass, mobile-app, ...) không còn bundle local — đăng ký trên registry
14
- // server với name prefix "pan-ui-" (xem apps/pan-agent-skill-registry/registry/templates/pan-ui/).
15
-
16
- const CATEGORY_LABEL = {
17
- landing: "Web — Landing page",
18
- mobile: "Mobile — Ứng dụng điện thoại",
19
- cms: "CMS — Trang quản trị",
20
- };
21
- const CATEGORY_ORDER = ["landing", "mobile", "cms"];
22
-
23
- // Design System khai ở cấp app (field `designSystem` trong app.panui.yaml). shadcn = mặc định
24
- // (đứng đầu danh sách), antd = lựa chọn thứ hai. Thiếu field → renderer coi như shadcn.
25
- const DESIGN_SYSTEMS = [
26
- { id: "shadcn", label: "shadcn ui (Recommended)" },
27
- { id: "antd", label: "antd" },
28
- ];
29
-
30
- const TEXT_EXT = new Set([".yaml", ".yml", ".html", ".htm", ".md", ".json", ".txt", ".css"]);
31
-
32
- /**
33
- * Lấy danh sách template pan-ui từ registry server (KHÔNG tải zip — chỉ đọc metadata
34
- * `extra.{category,label,platform}` + `description` để dựng picker). Cần server đang chạy
35
- * (PAN_SKILL_REGISTRY_URL, mặc định localhost:4600) — xem apps/pan-agent-skill-registry.
36
- * @returns {Promise<Array<{id, category, label, description, platform, item}>>}
37
- */
38
- async function discoverTemplates() {
39
- const baseUrl = getRegistryBaseUrl();
40
- const items = await fetchRegistry(baseUrl);
41
- return items
42
- .filter((i) => i.type === "template" && i.name.startsWith("pan-ui-"))
43
- .map((item) => ({
44
- id: item.name.slice("pan-ui-".length),
45
- category: (item.extra && item.extra.category) || "",
46
- label: (item.extra && item.extra.label) || item.name,
47
- description: item.description || "",
48
- platform: item.extra && item.extra.platform,
49
- item, // giữ nguyên để downloadAndExtract sau khi user chọn xong (tải NGAY 1 cái, không tải hết)
50
- }));
51
- }
52
-
53
- /** Copy đệ quy src→dest; file text thì thay placeholder theo `replacements`. */
54
- function copyWithReplace(src, dest, replacements) {
55
- const stat = fs.statSync(src);
56
- if (stat.isDirectory()) {
57
- fs.mkdirSync(dest, { recursive: true });
58
- for (const name of fs.readdirSync(src)) {
59
- if (name === "template.json") continue; // manifest không copy vào dự án
60
- copyWithReplace(path.join(src, name), path.join(dest, name), replacements);
61
- }
62
- return;
63
- }
64
- const ext = path.extname(src).toLowerCase();
65
- if (TEXT_EXT.has(ext)) {
66
- let content = fs.readFileSync(src, "utf8");
67
- for (const [token, value] of Object.entries(replacements)) {
68
- content = content.split(token).join(value);
69
- }
70
- fs.writeFileSync(dest, content);
71
- } else {
72
- fs.copyFileSync(src, dest);
73
- }
74
- }
75
-
76
- /**
77
- * Ghi field `designSystem: <ds>` cấp app vào app.panui.yaml của dự án vừa sinh.
78
- * Nếu file đã có dòng `designSystem:` → thay giá trị; chưa có → chèn ngay sau dòng `app:`.
79
- * Không tìm thấy file/anchor → bỏ qua im lặng (renderer mặc định shadcn).
80
- */
81
- function applyDesignSystem(targetDir, ds) {
82
- const file = path.join(targetDir, "app.panui.yaml");
83
- if (!fs.existsSync(file)) return;
84
- const content = fs.readFileSync(file, "utf8");
85
- const lines = content.split("\n");
86
- const dsIdx = lines.findIndex((l) => /^designSystem\s*:/.test(l));
87
- if (dsIdx >= 0) {
88
- lines[dsIdx] = `designSystem: ${ds}`;
89
- } else {
90
- const appIdx = lines.findIndex((l) => /^app\s*:/.test(l));
91
- const at = appIdx >= 0 ? appIdx + 1 : 1; // sau `app:`, hoặc sau `panui:` nếu không có `app:`
92
- lines.splice(at, 0, `designSystem: ${ds}`);
93
- }
94
- fs.writeFileSync(file, lines.join("\n"));
95
- }
96
-
97
- /** Chuẩn hóa tên nhập → { targetDir (tuyệt đối), projectName }. Cho tên đơn (pan-ui) hoặc path. */
98
- function resolveTarget(input) {
99
- const raw = String(input || "").trim();
100
- const value = raw || "pan-ui";
101
- const targetDir = path.resolve(process.cwd(), value);
102
- const projectName = path.basename(targetDir);
103
- return { targetDir, projectName };
104
- }
105
-
106
- // Chọn 1 item; ưu tiên menu-prompt (hotkey b/x nhấn-1-phím) → @inquirer/prompts select với
107
- // item b/x ẩn ở cuối (không hotkey thật, vẫn chọn được bằng Enter) → lỗi nếu không có gì.
108
- async function pick(inq, { message, choices, hotkeys = [] }) {
109
- try {
110
- return await menuSelect({ message, choices, hotkeys });
111
- } catch (err) {
112
- if (String(err && err.message) !== "no-core") throw err;
113
- if (!inq || typeof inq.select !== "function") {
114
- throw new Error("Cần terminal tương tác (@inquirer/prompts) để chạy `pan ui init`.");
115
- }
116
- const extra = hotkeys.map((h) => ({ name: `${h.key} ${h.label}`, value: h.value }));
117
- const list = extra.length ? [...choices, { name: "──────────", disabled: true }, ...extra] : choices;
118
- return inq.select({ message, choices: list, loop: false });
119
- }
120
- }
121
-
122
- async function run(_args) {
123
- const inq = await import("@inquirer/prompts").catch(() => null);
124
-
125
- const baseUrl = getRegistryBaseUrl();
126
- const templates = await discoverTemplates();
127
- if (templates.length === 0) {
128
- throw new Error(
129
- `Không tìm thấy template pan-ui nào trên registry server (${baseUrl}). Kiểm tra server đang chạy và apps/pan-agent-skill-registry/registry/templates/pan-ui/ đã build chưa.`
130
- );
131
- }
132
-
133
- const categories = CATEGORY_ORDER.filter((c) => templates.some((tpl) => tpl.category === c));
134
-
135
- let designSystem = null;
136
- let category = null;
137
- let chosen = null;
138
- let palette = null;
139
-
140
- // Vòng lặp 4 cấp (Design System → loại → template → bảng màu); 'b' quay lại cấp trước, 'x' thoát hẳn.
141
- designSystemLoop: for (;;) {
142
- // 0) Design System — bước ĐẦU TIÊN. shadcn (Recommended) mặc định đứng đầu, antd thứ hai.
143
- designSystem = await pick(inq, {
144
- message: "Chọn Design System:",
145
- choices: DESIGN_SYSTEMS.map((d) => ({ name: d.label, value: d.id })),
146
- hotkeys: [{ key: "x", label: t("EXIT_LABEL"), value: "__exit__" }],
147
- });
148
- if (designSystem === "__exit__") {
149
- console.log(t("EXITED"));
150
- process.exit(0);
151
- }
152
-
153
- categoryLoop: for (;;) {
154
- category = await pick(inq, {
155
- message: "Loại dự án:",
156
- choices: categories.map((c) => ({ name: CATEGORY_LABEL[c] || c, value: c })),
157
- hotkeys: [
158
- { key: "b", label: t("BACK_LABEL"), value: "__back__" },
159
- { key: "x", label: t("EXIT_LABEL"), value: "__exit__" },
160
- ],
161
- });
162
- if (category === "__exit__") {
163
- console.log(t("EXITED"));
164
- process.exit(0);
165
- }
166
- if (category === "__back__") continue designSystemLoop;
167
-
168
- const inCat = templates.filter((tpl) => tpl.category === category);
169
-
170
- templateLoop: for (;;) {
171
- const template = await pick(inq, {
172
- message: "Chọn giao diện mẫu:",
173
- choices: inCat.map((tpl) => ({
174
- name: `${tpl.label}${tpl.description ? ` — ${tpl.description}` : ""}`,
175
- value: tpl.id,
176
- })),
177
- hotkeys: [
178
- { key: "b", label: t("BACK_LABEL"), value: "__back__" },
179
- { key: "x", label: t("EXIT_LABEL"), value: "__exit__" },
180
- ],
181
- });
182
- if (template === "__exit__") {
183
- console.log(t("EXITED"));
184
- process.exit(0);
185
- }
186
- if (template === "__back__") continue categoryLoop;
187
- chosen = inCat.find((tpl) => tpl.id === template);
188
-
189
- for (;;) {
190
- const paletteId = await pick(inq, {
191
- message: "Bảng màu (chỉ đổi màu chủ đạo primary/accent...):",
192
- choices: PALETTES.map((p) => ({ name: p.label, value: p.id })),
193
- hotkeys: [
194
- { key: "b", label: t("BACK_LABEL"), value: "__back__" },
195
- { key: "x", label: t("EXIT_LABEL"), value: "__exit__" },
196
- ],
197
- });
198
- if (paletteId === "__exit__") {
199
- console.log(t("EXITED"));
200
- process.exit(0);
201
- }
202
- if (paletteId === "__back__") continue templateLoop;
203
- palette = PALETTE_BY_ID.get(paletteId);
204
- break designSystemLoop;
205
- }
206
- }
207
- }
208
- }
209
-
210
- if (!inq || typeof inq.input !== "function") {
211
- throw new Error("Cần terminal tương tác (@inquirer/prompts) để chạy `pan ui init`.");
212
- }
213
-
214
- // 4) Tên / đường dẫn dự án.
215
- const nameInput = await inq.input({
216
- message: "Tên dự án hoặc đường dẫn lưu (vd: pan-ui, ./design/landing, /abs/duan):",
217
- default: "pan-ui",
218
- });
219
- const { targetDir, projectName } = resolveTarget(nameInput);
220
-
221
- if (fs.existsSync(targetDir)) {
222
- throw new Error(
223
- `Thư mục đã tồn tại: ${targetDir}\n→ Xóa thủ công (CLI không tự xóa) rồi chạy lại, hoặc chọn tên khác.`
224
- );
225
- }
226
-
227
- // 5) Tải ĐÚNG 1 template đã chọn (zip) rồi scaffold: copy + thay placeholder (palette + tên dự án).
228
- const replacements = {
229
- ...paletteReplacements(palette),
230
- __PROJECT_NAME__: projectName,
231
- };
232
- const stagingDir = fs.mkdtempSync(path.join(os.tmpdir(), "pan-ui-init-"));
233
- try {
234
- const extractedDir = await downloadAndExtract(baseUrl, chosen.item, stagingDir);
235
- copyWithReplace(extractedDir, targetDir, replacements);
236
- } finally {
237
- fs.rmSync(stagingDir, { recursive: true, force: true });
238
- }
239
-
240
- // Ghi Design System đã chọn vào app.panui.yaml cấp app.
241
- applyDesignSystem(targetDir, designSystem);
242
-
243
- const dsLabel = (DESIGN_SYSTEMS.find((d) => d.id === designSystem) || {}).label || designSystem;
244
- console.log(`\n✓ Đã tạo dự án "${projectName}" (${dsLabel} · ${CATEGORY_LABEL[category]} · ${chosen.label} · màu ${palette.label.split(" —")[0]})`);
245
- console.log(` ${targetDir}`);
246
- console.log("\nBước tiếp theo:");
247
- console.log(` • Xem thử: pnpm --dir apps/pan-stack-design-system panui:gen ${targetDir} -o /tmp/preview && pan serve /tmp/preview`);
248
- console.log(" • Sửa nội dung: mở app.panui.yaml + screens/ + components/*.html");
249
- console.log(" • Đưa lên Pan Studio: pan ui login → (tạo dự án) → pan ui sync");
250
- }
251
-
252
- module.exports = { run, discoverTemplates, resolveTarget, applyDesignSystem, DESIGN_SYSTEMS };
package/lib/ui/login.js DELETED
@@ -1,128 +0,0 @@
1
- // lib/ui/login.js — device-flow auth (§4.3, OD-U1): POST /auth/device/start, mở browser
2
- // verificationUriComplete, poll /auth/device/poll mỗi `interval`s, lưu credential.
3
- const os = require("os");
4
- const { spawn } = require("child_process");
5
- const { createApiClient } = require("./api");
6
- const { getApiConfig, saveCredential, loadCredential, isExpired } = require("./config");
7
- const { findWorkspaceRoot } = require("./workspace");
8
-
9
- /**
10
- * Mở browser theo OS. Không throw — trả false nếu không mở được (fallback in URL).
11
- * @param {string} url
12
- * @returns {Promise<boolean>}
13
- */
14
- function openBrowser(url) {
15
- return new Promise((resolve) => {
16
- let cmd;
17
- let args;
18
- if (process.platform === "darwin") {
19
- cmd = "open";
20
- args = [url];
21
- } else if (process.platform === "win32") {
22
- cmd = "cmd";
23
- args = ["/c", "start", '""', url];
24
- } else {
25
- cmd = "xdg-open";
26
- args = [url];
27
- }
28
- try {
29
- const child = spawn(cmd, args, { stdio: "ignore", detached: true });
30
- child.on("error", () => resolve(false));
31
- child.unref();
32
- resolve(true);
33
- } catch {
34
- resolve(false);
35
- }
36
- });
37
- }
38
-
39
- function sleep(ms) {
40
- return new Promise((resolve) => setTimeout(resolve, ms));
41
- }
42
-
43
- async function run(args = []) {
44
- const force = args.includes("--force") || args.includes("-f");
45
-
46
- const workspaceRoot = findWorkspaceRoot() || process.cwd();
47
- const { apiUrl } = getApiConfig(workspaceRoot);
48
-
49
- // Đã có key CÒN HẠN cho đúng apiUrl này → báo info + hướng dẫn thay vì âm thầm login lại đè key.
50
- const existing = loadCredential(apiUrl);
51
- if (existing && !isExpired(existing) && !force) {
52
- const email = (existing.user && existing.user.email) || "?";
53
- const expDate = existing.expiresAt ? new Date(existing.expiresAt).toLocaleString() : "?";
54
- console.log(`✓ Đã đăng nhập ${email} tại ${apiUrl} (key hết hạn ${expDate}).`);
55
- console.log("");
56
- console.log("Muốn đổi tài khoản / đăng nhập lại?");
57
- console.log(" pan ui logout # đăng xuất trước, rồi 'pan ui login' lại");
58
- console.log(" pan ui login --force # đăng nhập lại ngay, ghi đè key hiện tại");
59
- return;
60
- }
61
-
62
- const api = createApiClient({ apiUrl });
63
-
64
- console.log(`Đăng nhập Pan Studio (${apiUrl})...`);
65
- const start = await api.deviceStart({
66
- clientName: `pan-cli@${os.hostname()}`,
67
- scope: "WRITE",
68
- });
69
- const { deviceCode, userCode, verificationUriComplete, interval, expiresIn } = start;
70
-
71
- if (!deviceCode || !verificationUriComplete) {
72
- throw new Error(
73
- "Phản hồi /auth/device/start không hợp lệ (thiếu deviceCode/verificationUriComplete) — kiểm tra backend."
74
- );
75
- }
76
-
77
- console.log(`Mã đăng nhập: ${userCode}`);
78
- console.log(`Mở trình duyệt tại: ${verificationUriComplete}`);
79
- const opened = await openBrowser(verificationUriComplete);
80
- if (!opened) {
81
- console.log(
82
- "Không tự mở được trình duyệt — hãy dán URL trên vào trình duyệt và làm theo hướng dẫn."
83
- );
84
- }
85
-
86
- const intervalMs = Math.max(1, Number(interval) || 4) * 1000;
87
- const deadline = Date.now() + Math.max(1, Number(expiresIn) || 600) * 1000;
88
-
89
- console.log("Đang chờ xác nhận trên trình duyệt...");
90
- while (Date.now() < deadline) {
91
- await sleep(intervalMs);
92
- let poll;
93
- try {
94
- poll = await api.devicePoll(deviceCode);
95
- } catch (err) {
96
- console.log(`\nLỗi khi kiểm tra trạng thái: ${err.message}`);
97
- continue;
98
- }
99
-
100
- if (poll.status === "approved") {
101
- if (!poll.token) {
102
- throw new Error(
103
- "Trạng thái approved nhưng thiếu token trong phản hồi — không thể lưu credential."
104
- );
105
- }
106
- saveCredential(apiUrl, {
107
- token: poll.token,
108
- tokenType: poll.tokenType || "pmcp",
109
- expiresAt: poll.expiresAt,
110
- user: poll.user,
111
- });
112
- const email = poll.user && poll.user.email;
113
- const expDate = poll.expiresAt ? new Date(poll.expiresAt).toLocaleString() : "?";
114
- console.log(`\nĐã đăng nhập ${email || ""} — key hết hạn ${expDate}.`);
115
- return;
116
- }
117
- if (poll.status === "denied") {
118
- throw new Error("Yêu cầu đăng nhập đã bị từ chối trên trình duyệt.");
119
- }
120
- if (poll.status === "expired") {
121
- throw new Error("Yêu cầu đăng nhập đã hết hạn — chạy lại `pan ui login`.");
122
- }
123
- process.stdout.write(".");
124
- }
125
- throw new Error("Hết thời gian chờ xác nhận đăng nhập — chạy lại `pan ui login`.");
126
- }
127
-
128
- module.exports = { run };
package/lib/ui/logout.js DELETED
@@ -1,22 +0,0 @@
1
- // lib/ui/logout.js — xóa credential local (~/.pan/credentials.json) cho API hiện tại.
2
- const { createApiClient } = require("./api");
3
- const { getApiConfig, loadCredential, clearCredential } = require("./config");
4
- const { findWorkspaceRoot } = require("./workspace");
5
-
6
- async function run(_args) {
7
- const workspaceRoot = findWorkspaceRoot() || process.cwd();
8
- const { apiUrl } = getApiConfig(workspaceRoot);
9
-
10
- const cred = loadCredential(apiUrl);
11
- if (!cred) {
12
- console.log(`Chưa đăng nhập Pan Studio (${apiUrl}) — không có gì để đăng xuất.`);
13
- return;
14
- }
15
-
16
- clearCredential(apiUrl);
17
- const email = cred.user && cred.user.email;
18
- console.log(`Đã đăng xuất${email ? ` ${email}` : ""} khỏi ${apiUrl}.`);
19
- console.log("Key local đã xóa. (Token trên backend vẫn còn hạn — thu hồi tại UI/mcp-tokens nếu cần.)");
20
- }
21
-
22
- module.exports = { run };