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.
- package/README.md +37 -731
- package/bin/pan.js +4978 -269
- package/package.json +17 -5
- package/lib/ai-skills.js +0 -829
- package/lib/create.js +0 -105
- package/lib/discover.js +0 -294
- package/lib/git/git-checkout.js +0 -395
- package/lib/git/git-clone.js +0 -206
- package/lib/git/git-fetch-all.js +0 -129
- package/lib/git/git-grv.js +0 -125
- package/lib/git/git-gst.js +0 -165
- package/lib/git/git-mr.js +0 -546
- package/lib/git/git-pull.js +0 -137
- package/lib/git/git-push.js +0 -185
- package/lib/git/git-sync-main.js +0 -334
- package/lib/git/helpers/bulk-report.js +0 -202
- package/lib/git/helpers/cli.js +0 -188
- package/lib/git/helpers/colors.js +0 -19
- package/lib/git/helpers/git.js +0 -136
- package/lib/git/helpers/manifest.js +0 -223
- package/lib/git/helpers/repo-select.js +0 -416
- package/lib/git/helpers/repo-summary.js +0 -161
- package/lib/i18n.js +0 -61
- package/lib/inquirer-theme.js +0 -20
- package/lib/language.js +0 -121
- package/lib/languages.js +0 -14
- package/lib/manifest.js +0 -467
- package/lib/menu-prompt.js +0 -201
- package/lib/menu.js +0 -134
- package/lib/messages.js +0 -647
- package/lib/platform.js +0 -140
- package/lib/run-command.js +0 -334
- package/lib/select.js +0 -0
- package/lib/serve.js +0 -75
- package/lib/skill-registry-client.js +0 -173
- package/lib/stats/aggregate.js +0 -51
- package/lib/stats/identity.js +0 -20
- package/lib/stats/index.js +0 -189
- package/lib/stats/parse.js +0 -46
- package/lib/stats/range.js +0 -38
- package/lib/stats/report.js +0 -264
- package/lib/stats/review.js +0 -157
- package/lib/stats/scan.js +0 -92
- package/lib/stats/serve.js +0 -25
- package/lib/ui/api.js +0 -202
- package/lib/ui/clone.js +0 -115
- package/lib/ui/config.js +0 -182
- package/lib/ui/diff.js +0 -128
- package/lib/ui/export-gate.js +0 -79
- package/lib/ui/gen-react-mobile-code.js +0 -6
- package/lib/ui/gen-react-web-code.js +0 -75
- package/lib/ui/index.js +0 -89
- package/lib/ui/init.js +0 -252
- package/lib/ui/login.js +0 -128
- package/lib/ui/logout.js +0 -22
- package/lib/ui/palettes.js +0 -103
- package/lib/ui/published-html.js +0 -24
- package/lib/ui/sync.js +0 -137
- package/lib/ui/workspace.js +0 -213
package/lib/ui/palettes.js
DELETED
|
@@ -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 };
|
package/lib/ui/published-html.js
DELETED
|
@@ -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 };
|
package/lib/ui/workspace.js
DELETED
|
@@ -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
|
-
};
|