@pplethai/cli 0.3.1 → 0.4.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.
- package/README.md +1 -1
- package/dist/bin.js +53 -15
- package/package.json +3 -2
- package/templates/golden-path/.dev.vars.example +10 -13
- package/templates/golden-path/CLAUDE.md +22 -7
- package/templates/golden-path/README.md +13 -8
- package/templates/golden-path/package.json +1 -0
- package/templates/golden-path/shared/api.ts +36 -3
- package/templates/golden-path/src/auth.ts +101 -16
- package/templates/golden-path/src/dev-personas.ts +88 -0
- package/templates/golden-path/src/env.ts +4 -2
- package/templates/golden-path/src/index.ts +8 -7
- package/templates/golden-path/src/vite-env.d.ts +14 -0
- package/templates/golden-path/test/harness.ts +6 -2
- package/templates/golden-path/test/smoke.test.ts +98 -15
- package/templates/golden-path/web/App.tsx +1 -1
- package/templates/golden-path/web/main.tsx +2 -2
- package/templates/golden-path/web/pple.ts +6 -5
- package/dist/api.js +0 -108
- package/dist/cli.js +0 -151
- package/dist/commands/auth.js +0 -32
- package/dist/commands/deploy.js +0 -113
- package/dist/commands/env.js +0 -70
- package/dist/commands/info.js +0 -238
- package/dist/commands/init.js +0 -130
- package/dist/commands/upgrade.js +0 -29
- package/dist/credentials.js +0 -53
- package/dist/deps.js +0 -87
- package/dist/device-flow.js +0 -122
- package/dist/errors.js +0 -16
- package/dist/git.js +0 -127
- package/dist/link.js +0 -59
- package/dist/node-deps.js +0 -107
- package/dist/npm.js +0 -127
- package/dist/scaffold.js +0 -58
- package/dist/session.js +0 -29
- package/dist/skills.js +0 -86
- package/dist/ui.js +0 -12
- package/dist/update.js +0 -142
package/dist/commands/auth.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { ProvisionerClient } from "../api.js";
|
|
2
|
-
import { clearCredentials, saveCredentials } from "../credentials.js";
|
|
3
|
-
import { awaitToken, requestDeviceCode } from "../device-flow.js";
|
|
4
|
-
import { client } from "../session.js";
|
|
5
|
-
export async function login(deps) {
|
|
6
|
-
const device = await requestDeviceCode(deps);
|
|
7
|
-
deps.out.say("เข้าสู่ระบบโดยเปิดหน้านี้แล้วกรอกรหัส:");
|
|
8
|
-
deps.out.say();
|
|
9
|
-
deps.out.say(` ${device.verificationUri}`);
|
|
10
|
-
deps.out.say(` รหัส: ${device.userCode}`);
|
|
11
|
-
deps.out.say();
|
|
12
|
-
deps.out.say("กำลังรอให้คุณอนุมัติการเข้าสู่ระบบ…");
|
|
13
|
-
await deps.openBrowser(device.verificationUriComplete ?? device.verificationUri);
|
|
14
|
-
const tokens = await awaitToken(deps, device);
|
|
15
|
-
await saveCredentials(deps, { ...tokens, ssoBase: deps.config.ssoBase });
|
|
16
|
-
const me = await new ProvisionerClient(deps, tokens.accessToken).me();
|
|
17
|
-
deps.out.say(`เข้าสู่ระบบในชื่อ ${me.name ?? me.sub} แล้ว`);
|
|
18
|
-
if (!me.isBuilder) {
|
|
19
|
-
deps.out.say("อีกหนึ่งขั้นตอนก่อนสร้างแอปได้: ยอมรับข้อตกลงผู้พัฒนาที่ https://console.ppleth.ai");
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
export async function logout(deps) {
|
|
23
|
-
const had = await clearCredentials(deps);
|
|
24
|
-
deps.out.say(had ? "ออกจากระบบแล้ว" : "คุณยังไม่ได้เข้าสู่ระบบ");
|
|
25
|
-
}
|
|
26
|
-
export async function whoami(deps) {
|
|
27
|
-
const me = await (await client(deps)).me();
|
|
28
|
-
deps.out.say(`เข้าสู่ระบบในชื่อ ${me.name ?? me.sub} แล้ว`);
|
|
29
|
-
if (!me.isBuilder) {
|
|
30
|
-
deps.out.say("คุณยังไม่ได้ยอมรับข้อตกลงผู้พัฒนา — ยอมรับได้ที่ https://console.ppleth.ai");
|
|
31
|
-
}
|
|
32
|
-
}
|
package/dist/commands/deploy.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { CliError } from "../errors.js";
|
|
2
|
-
import { hasLocalChanges, headCommit, isUpToDateWithRemote, recordChanges, sendChanges, } from "../git.js";
|
|
3
|
-
import { findLink } from "../link.js";
|
|
4
|
-
import { client } from "../session.js";
|
|
5
|
-
import { refreshSkills } from "../skills.js";
|
|
6
|
-
/** How often to ask the platform how the build is going. */
|
|
7
|
-
const POLL_INTERVAL_MS = 3000;
|
|
8
|
-
/** How long to wait for CI to pick the push up before saying so. */
|
|
9
|
-
const BUILD_START_TIMEOUT_MS = 3 * 60_000;
|
|
10
|
-
/** How long a build itself may take before the CLI stops watching. */
|
|
11
|
-
const BUILD_TIMEOUT_MS = 10 * 60_000;
|
|
12
|
-
/** How much of a failed build's log to show; the rest is in the console. */
|
|
13
|
-
const LOG_TAIL_LINES = 20;
|
|
14
|
-
/**
|
|
15
|
-
* Put the Builder's current work live: record it, send it, and watch the
|
|
16
|
-
* platform build it — reporting the live URL, or a failure they can read.
|
|
17
|
-
*/
|
|
18
|
-
export async function deploy(deps, options) {
|
|
19
|
-
const link = await findLink(deps);
|
|
20
|
-
if (link.apiBase !== "" && link.apiBase !== deps.config.apiBase) {
|
|
21
|
-
throw new CliError("แอปนี้อยู่บน PPLE Platform คนละตัวกับที่คุณเข้าสู่ระบบอยู่", `แอปนี้ถูกตั้งค่าไว้กับ ${link.apiBase}`);
|
|
22
|
-
}
|
|
23
|
-
const api = await client(deps);
|
|
24
|
-
const app = await api.app(link.slug);
|
|
25
|
-
if (app.status === "retired") {
|
|
26
|
-
throw new CliError(`"${app.name}" ถูกปลดระวางแล้ว`, "แอปที่ปลดระวางแล้วดีพลอยไม่ได้ หากต้องการสานงานนี้ต่อ ให้สร้างแอปใหม่ในคอนโซล");
|
|
27
|
-
}
|
|
28
|
-
if (app.status !== "ready") {
|
|
29
|
-
throw new CliError(`"${app.name}" ยังไม่พร้อมรับการเปลี่ยนแปลง`, app.error
|
|
30
|
-
? `การตั้งค่าพบปัญหา: ${app.error}`
|
|
31
|
-
: "ตรวจสอบได้ที่ https://console.ppleth.ai แล้วลองใหม่ในอีกสักครู่");
|
|
32
|
-
}
|
|
33
|
-
// One token for the whole command, kept in memory: it says both what this
|
|
34
|
-
// machine may push and whose name the work goes out under (ADR-0005).
|
|
35
|
-
const access = await api.repoToken(link.slug);
|
|
36
|
-
// Keep the local Skill overlay current even when there is nothing new to
|
|
37
|
-
// send — including first seed for folders set up before Skills shipped.
|
|
38
|
-
await refreshSkills(deps, link.dir);
|
|
39
|
-
if (await hasLocalChanges(deps, link.dir)) {
|
|
40
|
-
deps.out.say("กำลังบันทึกการเปลี่ยนแปลงของคุณ…");
|
|
41
|
-
await recordChanges(deps, link.dir, options.message ?? `อัปเดต ${app.name}`, access);
|
|
42
|
-
}
|
|
43
|
-
else if (await isUpToDateWithRemote(deps, link.dir, access)) {
|
|
44
|
-
deps.out.say(`ไม่มีการเปลี่ยนแปลงใหม่ — ${app.name} ใช้งานอยู่แล้วที่ ${app.url}`);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
deps.out.say("กำลังส่งการเปลี่ยนแปลงของคุณ…");
|
|
48
|
-
await sendChanges(deps, link.dir, access);
|
|
49
|
-
const commit = await headCommit(deps, link.dir);
|
|
50
|
-
deps.out.say("กำลังบิลด์แอปของคุณ… (ปกติใช้เวลาราวหนึ่งนาที)");
|
|
51
|
-
const result = await watchDeploy(deps, api, link.slug, commit);
|
|
52
|
-
report(deps, app, result);
|
|
53
|
-
}
|
|
54
|
-
async function watchDeploy(deps, api, slug, commit) {
|
|
55
|
-
const started = deps.now();
|
|
56
|
-
let build;
|
|
57
|
-
for (;;) {
|
|
58
|
-
const deploys = await api.deploys(slug);
|
|
59
|
-
const found = deploys.find((d) => d.commit === commit);
|
|
60
|
-
if (found) {
|
|
61
|
-
build = found;
|
|
62
|
-
if (found.status !== "building")
|
|
63
|
-
return found;
|
|
64
|
-
}
|
|
65
|
-
const waited = deps.now() - started;
|
|
66
|
-
// Before CI has opened a deploy there is nothing to watch; after it has,
|
|
67
|
-
// the clock that matters is the build's.
|
|
68
|
-
const limit = build ? BUILD_TIMEOUT_MS : BUILD_START_TIMEOUT_MS;
|
|
69
|
-
if (waited >= limit) {
|
|
70
|
-
throw new CliError(build
|
|
71
|
-
? "ส่งการเปลี่ยนแปลงของคุณแล้ว แต่การบิลด์ใช้เวลานานกว่าที่คาดไว้"
|
|
72
|
-
: "ส่งการเปลี่ยนแปลงของคุณแล้ว แต่แพลตฟอร์มยังไม่เริ่มบิลด์", `ติดตามจนเสร็จได้ที่ https://console.ppleth.ai/apps/${slug}`);
|
|
73
|
-
}
|
|
74
|
-
await deps.sleep(POLL_INTERVAL_MS);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
/** How the Provisioner marks a deploy that a newer one overtook. */
|
|
78
|
-
const SUPERSEDED = "superseded by a newer deploy";
|
|
79
|
-
function report(deps, app, build) {
|
|
80
|
-
if (build.status === "failed") {
|
|
81
|
-
if (build.error === SUPERSEDED) {
|
|
82
|
-
throw new CliError("มีการดีพลอยครั้งใหม่มาแทนที่ครั้งที่คุณกำลังรออยู่", `ติดตามการดีพลอยครั้งนั้นได้ที่ https://console.ppleth.ai/apps/${app.slug}`);
|
|
83
|
-
}
|
|
84
|
-
// A build that compiled badly explains itself in its log; a deploy that
|
|
85
|
-
// failed after the build — a bad bundle, a rejected upload — only has the
|
|
86
|
-
// platform's own reason, and the Builder must still be told something.
|
|
87
|
-
const detail = build.log ? tail(build.log, LOG_TAIL_LINES) : build.error;
|
|
88
|
-
if (detail) {
|
|
89
|
-
deps.out.say();
|
|
90
|
-
deps.out.say(detail);
|
|
91
|
-
deps.out.say();
|
|
92
|
-
}
|
|
93
|
-
throw new CliError(`บิลด์ ${app.name} ไม่สำเร็จ สิ่งที่ใช้งานอยู่จึงยังไม่เปลี่ยนแปลง`, detail
|
|
94
|
-
? "ส่งข้อความด้านบนให้ AI coding agent ของคุณ — ในนั้นบอกว่าต้องแก้อะไร"
|
|
95
|
-
: `ดูรายละเอียดได้ที่ https://console.ppleth.ai/apps/${app.slug}`);
|
|
96
|
-
}
|
|
97
|
-
// Migrations are the part of a deploy a Builder can't see for themselves —
|
|
98
|
-
// no Cloudflare account (ADR-0001/0002) — so say when some landed. Silence
|
|
99
|
-
// means the database is unchanged (#75).
|
|
100
|
-
const applied = build.migrations.applied ?? [];
|
|
101
|
-
if (applied.length > 0) {
|
|
102
|
-
deps.out.say(`ใช้ migration ${applied.length} รายการกับฐานข้อมูลของคุณแล้ว`);
|
|
103
|
-
}
|
|
104
|
-
if (build.status === "unverified") {
|
|
105
|
-
deps.out.say(`อัปเดต ${app.name} แล้ว แต่แอปยังไม่ตอบสนอง`);
|
|
106
|
-
deps.out.say(`รอสักครู่ แล้วเปิด ${app.url}`);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
deps.out.say(`${app.name} ใช้งานได้แล้วที่ ${app.url}`);
|
|
110
|
-
}
|
|
111
|
-
function tail(log, lines) {
|
|
112
|
-
return log.split("\n").slice(-lines).join("\n").trim();
|
|
113
|
-
}
|
package/dist/commands/env.js
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { CliError } from "../errors.js";
|
|
4
|
-
import { findLink } from "../link.js";
|
|
5
|
-
import { client } from "../session.js";
|
|
6
|
-
/**
|
|
7
|
-
* Show the app's environment variables and secrets. Secrets report that a
|
|
8
|
-
* key is set, never the value — same contract as the Console.
|
|
9
|
-
*/
|
|
10
|
-
export async function list(deps) {
|
|
11
|
-
const link = await findLink(deps);
|
|
12
|
-
const api = await client(deps);
|
|
13
|
-
const entries = await api.env(link.slug);
|
|
14
|
-
if (entries.length === 0) {
|
|
15
|
-
deps.out.say("ยังไม่ได้ตั้งค่าใด ๆ");
|
|
16
|
-
deps.out.say("ตัวแปรที่คุณตั้งจะมีผลในการดีพลอยครั้งถัดไป");
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
for (const entry of entries) {
|
|
20
|
-
if (entry.secret) {
|
|
21
|
-
deps.out.say(`${entry.key}=(secret)`);
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
deps.out.say(`${entry.key}=${entry.value ?? ""}`);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
deps.out.say("");
|
|
28
|
-
deps.out.say("จะมีผลในการดีพลอยครั้งถัดไป");
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Create or replace one env entry. The value comes from an argument, a file
|
|
32
|
-
* (`--file`), or stdin — so multiline and secret values need not land in
|
|
33
|
-
* shell history.
|
|
34
|
-
*/
|
|
35
|
-
export async function set(deps, options) {
|
|
36
|
-
const link = await findLink(deps);
|
|
37
|
-
const value = await resolveValue(deps, options);
|
|
38
|
-
const api = await client(deps);
|
|
39
|
-
const result = await api.setEnv(link.slug, options.key, value, options.secret);
|
|
40
|
-
deps.out.say(`บันทึก ${result.key}${result.secret ? " (secret)" : ""} แล้ว`);
|
|
41
|
-
deps.out.say("จะมีผลในการดีพลอยครั้งถัดไป");
|
|
42
|
-
}
|
|
43
|
-
export async function remove(deps, key) {
|
|
44
|
-
const link = await findLink(deps);
|
|
45
|
-
const api = await client(deps);
|
|
46
|
-
await api.deleteEnv(link.slug, key);
|
|
47
|
-
deps.out.say(`ลบ ${key} แล้ว`);
|
|
48
|
-
deps.out.say("จะมีผลในการดีพลอยครั้งถัดไป");
|
|
49
|
-
}
|
|
50
|
-
async function resolveValue(deps, options) {
|
|
51
|
-
if (options.file !== undefined && options.value !== undefined) {
|
|
52
|
-
throw new CliError("ส่งค่ามาเป็น argument หรือผ่าน `--file` อย่างใดอย่างหนึ่ง ไม่ใช่ทั้งสองอย่าง");
|
|
53
|
-
}
|
|
54
|
-
if (options.file !== undefined) {
|
|
55
|
-
const filePath = path.resolve(deps.cwd, options.file);
|
|
56
|
-
const raw = await fs.readFile(filePath, "utf8").catch(() => null);
|
|
57
|
-
if (raw === null) {
|
|
58
|
-
throw new CliError(`อ่านไฟล์ ${options.file} ไม่ได้`, "ตรวจสอบพาธแล้วลองใหม่อีกครั้ง");
|
|
59
|
-
}
|
|
60
|
-
return raw;
|
|
61
|
-
}
|
|
62
|
-
if (options.value !== undefined)
|
|
63
|
-
return options.value;
|
|
64
|
-
// A TTY with no value would hang waiting for EOF — tell the Builder how
|
|
65
|
-
// to pass one instead.
|
|
66
|
-
if (deps.interactive) {
|
|
67
|
-
throw new CliError("`pple env set` ต้องระบุค่า", "ส่งค่ามาเป็น argument ผ่าน `--file` หรือ pipe ทาง stdin");
|
|
68
|
-
}
|
|
69
|
-
return deps.readStdin();
|
|
70
|
-
}
|
package/dist/commands/info.js
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
import { CliError } from "../errors.js";
|
|
2
|
-
import { findLink } from "../link.js";
|
|
3
|
-
import { client } from "../session.js";
|
|
4
|
-
/** Same cap the Console / Provisioner enforce for Beta testers. */
|
|
5
|
-
const BETA_INVITE_CAP = 20;
|
|
6
|
-
/**
|
|
7
|
-
* Thai combining vowels and tone marks render with zero width, so `padEnd`
|
|
8
|
-
* over-pads any label containing them. Measure without them to keep the
|
|
9
|
-
* two-column layout below actually aligned in a terminal.
|
|
10
|
-
*/
|
|
11
|
-
const THAI_COMBINING = /[\u0E31\u0E34-\u0E3A\u0E47-\u0E4E]/g;
|
|
12
|
-
export function pad(text, width) {
|
|
13
|
-
const visible = text.replace(THAI_COMBINING, "").length;
|
|
14
|
-
return text + " ".repeat(Math.max(0, width - visible));
|
|
15
|
-
}
|
|
16
|
-
/** API values are English; these are what a Builder reads. Mirrors the Console. */
|
|
17
|
-
const STATUS_LABEL = {
|
|
18
|
-
ready: "พร้อมใช้งาน",
|
|
19
|
-
provisioning: "กำลังจัดเตรียม",
|
|
20
|
-
failed: "ล้มเหลว",
|
|
21
|
-
retired: "ปลดระวางแล้ว",
|
|
22
|
-
};
|
|
23
|
-
const TIER_LABEL = {
|
|
24
|
-
draft: "ฉบับร่าง",
|
|
25
|
-
beta: "ทดลองใช้",
|
|
26
|
-
live: "เผยแพร่จริง",
|
|
27
|
-
};
|
|
28
|
-
const ROLE_LABEL = {
|
|
29
|
-
owner: "เจ้าของ",
|
|
30
|
-
collaborator: "ผู้ร่วมพัฒนา",
|
|
31
|
-
};
|
|
32
|
-
const DEPLOY_STATUS_LABEL = {
|
|
33
|
-
live: "ใช้งานอยู่",
|
|
34
|
-
building: "กำลังบิลด์",
|
|
35
|
-
unverified: "ยังไม่ยืนยัน",
|
|
36
|
-
failed: "ล้มเหลว",
|
|
37
|
-
};
|
|
38
|
-
const REQUEST_STATUS_LABEL = {
|
|
39
|
-
pending: "รอดำเนินการ",
|
|
40
|
-
approved: "อนุมัติแล้ว",
|
|
41
|
-
denied: "ปฏิเสธแล้ว",
|
|
42
|
-
};
|
|
43
|
-
/** An unknown value from the platform is shown as-is rather than hidden. */
|
|
44
|
-
function label(map, value) {
|
|
45
|
-
return map[value] ?? value;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Read-only mirror of the Console dashboard for one app: overview, usage vs
|
|
49
|
-
* Resource Limits, recent deploys, env keys, and — for Owners — publication,
|
|
50
|
-
* collaborators, and request history. No mutations; point Builders at Console
|
|
51
|
-
* for those.
|
|
52
|
-
*/
|
|
53
|
-
export async function info(deps, slugArg) {
|
|
54
|
-
const slug = await resolveSlug(deps, slugArg);
|
|
55
|
-
const api = await client(deps);
|
|
56
|
-
const app = await api.app(slug);
|
|
57
|
-
const isOwner = app.role !== "collaborator";
|
|
58
|
-
const [metrics, deploys, envEntries, ownerSurfaces] = await Promise.all([
|
|
59
|
-
api.metrics(slug),
|
|
60
|
-
api.deploys(slug),
|
|
61
|
-
api.env(slug),
|
|
62
|
-
isOwner ? loadOwnerSurfaces(api, slug) : Promise.resolve(null),
|
|
63
|
-
]);
|
|
64
|
-
printOverview(deps, app);
|
|
65
|
-
printUsage(deps, metrics);
|
|
66
|
-
printDeploys(deps, deploys);
|
|
67
|
-
printEnv(deps, envEntries);
|
|
68
|
-
if (ownerSurfaces) {
|
|
69
|
-
printPublication(deps, app, ownerSurfaces.requests, ownerSurfaces.invites);
|
|
70
|
-
printCollaborators(deps, ownerSurfaces.collaborators);
|
|
71
|
-
printRequests(deps, ownerSurfaces.requests);
|
|
72
|
-
}
|
|
73
|
-
deps.out.say("");
|
|
74
|
-
deps.out.say("หากต้องการเปลี่ยนโควตา ค่า env ผู้ร่วมพัฒนา หรือการเผยแพร่ ใช้ https://console.ppleth.ai");
|
|
75
|
-
}
|
|
76
|
-
async function resolveSlug(deps, slugArg) {
|
|
77
|
-
if (slugArg)
|
|
78
|
-
return slugArg;
|
|
79
|
-
try {
|
|
80
|
-
return (await findLink(deps)).slug;
|
|
81
|
-
}
|
|
82
|
-
catch (e) {
|
|
83
|
-
// Only rewrite the "no App Link" case; damaged links keep findLink's hint.
|
|
84
|
-
if (e instanceof CliError &&
|
|
85
|
-
e.message === "โฟลเดอร์นี้ไม่ใช่แอป PPLE") {
|
|
86
|
-
throw new CliError("แอปไหน? `pple info` ต้องมี App Link หรือ slug", "เข้าไปในโฟลเดอร์ของแอป ใส่ slug (`pple info <slug>`) หรือเปิด https://console.ppleth.ai");
|
|
87
|
-
}
|
|
88
|
-
throw e;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
async function loadOwnerSurfaces(api, slug) {
|
|
92
|
-
const [collaborators, requests, invites] = await Promise.all([
|
|
93
|
-
api.collaborators(slug),
|
|
94
|
-
api.requests(slug),
|
|
95
|
-
api.invites(slug),
|
|
96
|
-
]);
|
|
97
|
-
return { collaborators, requests, invites };
|
|
98
|
-
}
|
|
99
|
-
function printOverview(deps, app) {
|
|
100
|
-
deps.out.say(app.name);
|
|
101
|
-
deps.out.say(` ${pad("slug", 18)}${app.slug}`);
|
|
102
|
-
deps.out.say(` ${pad("URL", 18)}${app.url}`);
|
|
103
|
-
if (app.customAddress) {
|
|
104
|
-
deps.out.say(` ${pad("ที่อยู่ที่กำหนดเอง", 18)}${app.customAddress}`);
|
|
105
|
-
}
|
|
106
|
-
deps.out.say(` ${pad("สถานะ", 18)}${label(STATUS_LABEL, app.status)}`);
|
|
107
|
-
deps.out.say(` ${pad("การเผยแพร่", 18)}${label(TIER_LABEL, app.tier ?? "draft")}`);
|
|
108
|
-
if (app.role) {
|
|
109
|
-
deps.out.say(` ${pad("บทบาทของคุณ", 18)}${label(ROLE_LABEL, app.role)}`);
|
|
110
|
-
}
|
|
111
|
-
if (app.deploy) {
|
|
112
|
-
deps.out.say(` ${pad("การดีพลอยปัจจุบัน", 18)}${label(DEPLOY_STATUS_LABEL, app.deploy.status)}` +
|
|
113
|
-
(app.deploy.message ? ` — ${app.deploy.message}` : ""));
|
|
114
|
-
}
|
|
115
|
-
if (app.error) {
|
|
116
|
-
deps.out.say(` ${pad("ข้อผิดพลาด", 18)}${app.error}`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
function printUsage(deps, metrics) {
|
|
120
|
-
deps.out.say("");
|
|
121
|
-
deps.out.say("การใช้งานและโควตาทรัพยากร");
|
|
122
|
-
deps.out.say(` ${pad("CPU ต่อ request", 20)}${metrics.limits.cpuMs} ms`);
|
|
123
|
-
deps.out.say(` ${pad("การแจ้งเตือน", 20)}${formatCount(metrics.usage.notificationsSent)}` +
|
|
124
|
-
(metrics.usage.notificationsSent === null
|
|
125
|
-
? " (ดูข้อมูลไม่ได้ในขณะนี้)"
|
|
126
|
-
: ` จากโควตา ${formatCount(metrics.limits.notificationsPerDay)} ครั้งวันนี้`));
|
|
127
|
-
deps.out.say(` ${pad("ข้อผิดพลาด (24 ชม.)", 20)}${formatCount(metrics.usage.errors)}` +
|
|
128
|
-
(metrics.usage.errors === null
|
|
129
|
-
? " (ดูข้อมูลไม่ได้ในขณะนี้)"
|
|
130
|
-
: metrics.usage.errors === 0
|
|
131
|
-
? " (ไม่มีใน 24 ชั่วโมงที่ผ่านมา)"
|
|
132
|
-
: metrics.usage.requests === null
|
|
133
|
-
? " (คำขอที่ล้มเหลวใน 24 ชั่วโมงที่ผ่านมา)"
|
|
134
|
-
: ` จากทั้งหมด ${formatCount(metrics.usage.requests)} คำขอ`));
|
|
135
|
-
deps.out.say(` ${pad("ผู้ใช้แอป", 20)}${formatCount(metrics.usage.appUsers)}` +
|
|
136
|
-
(metrics.usage.appUsers === null ? " (ดูข้อมูลไม่ได้ในขณะนี้)" : ""));
|
|
137
|
-
}
|
|
138
|
-
function formatCount(value) {
|
|
139
|
-
// Same contract as Console: null means unavailable, never render as "0".
|
|
140
|
-
return value === null ? "—" : String(value);
|
|
141
|
-
}
|
|
142
|
-
function printDeploys(deps, deploys) {
|
|
143
|
-
deps.out.say("");
|
|
144
|
-
deps.out.say("การดีพลอยล่าสุด");
|
|
145
|
-
if (deploys.length === 0) {
|
|
146
|
-
deps.out.say(" ยังไม่มี");
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
for (const deploy of deploys.slice(0, 10)) {
|
|
150
|
-
const short = deploy.commit.slice(0, 7);
|
|
151
|
-
const note = deploy.message ? ` — ${deploy.message}` : "";
|
|
152
|
-
const status = label(DEPLOY_STATUS_LABEL, deploy.status);
|
|
153
|
-
deps.out.say(` ${pad(status, 13)} ${short}${note}`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
function printEnv(deps, entries) {
|
|
157
|
-
deps.out.say("");
|
|
158
|
-
deps.out.say("ค่า env");
|
|
159
|
-
if (entries.length === 0) {
|
|
160
|
-
deps.out.say(" ยังไม่ได้ตั้งค่าใด ๆ");
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
for (const entry of entries) {
|
|
164
|
-
// Secrets: key present only, never values — same contract as Console.
|
|
165
|
-
if (entry.secret) {
|
|
166
|
-
deps.out.say(` ${entry.key}=(secret)`);
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
deps.out.say(` ${entry.key}=${entry.value ?? ""}`);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
function printPublication(deps, app, requests, invites) {
|
|
174
|
-
deps.out.say("");
|
|
175
|
-
deps.out.say("การเผยแพร่");
|
|
176
|
-
deps.out.say(` ${pad("ระดับ", 18)}${label(TIER_LABEL, app.tier ?? "draft")}`);
|
|
177
|
-
if (app.customAddress) {
|
|
178
|
-
deps.out.say(` ${pad("ที่อยู่ที่กำหนดเอง", 18)}${app.customAddress}`);
|
|
179
|
-
}
|
|
180
|
-
const pendingPublish = requests.find((r) => r.type === "publish" && r.status === "pending");
|
|
181
|
-
if (pendingPublish) {
|
|
182
|
-
deps.out.say(` ${pad("คำขอที่รออยู่", 18)}${requestSummary(pendingPublish)}`);
|
|
183
|
-
}
|
|
184
|
-
if (app.tier === "beta") {
|
|
185
|
-
// Held = any invite that isn't declined — same slot count as Console.
|
|
186
|
-
const held = invites.filter((i) => i.status !== "DECLINED").length;
|
|
187
|
-
deps.out.say(` ${pad("คำเชิญทดลองใช้", 18)}ใช้ไป ${held} จาก ${BETA_INVITE_CAP} ที่นั่ง`);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
function printCollaborators(deps, collaborators) {
|
|
191
|
-
deps.out.say("");
|
|
192
|
-
deps.out.say("ผู้ร่วมพัฒนา");
|
|
193
|
-
if (collaborators.length === 0) {
|
|
194
|
-
deps.out.say(" ยังไม่มี");
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
for (const c of collaborators) {
|
|
198
|
-
const who = c.maskedName ?? "ผู้ร่วมพัฒนา";
|
|
199
|
-
const phone = c.phoneNumber ? ` (${c.phoneNumber})` : "";
|
|
200
|
-
deps.out.say(` ${who}${phone}`);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
function printRequests(deps, requests) {
|
|
204
|
-
deps.out.say("");
|
|
205
|
-
deps.out.say("ประวัติคำขอ");
|
|
206
|
-
if (requests.length === 0) {
|
|
207
|
-
deps.out.say(" ยังไม่มี");
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
for (const r of requests.slice(0, 10)) {
|
|
211
|
-
const reason = r.reason ? ` — ${r.reason}` : "";
|
|
212
|
-
const status = label(REQUEST_STATUS_LABEL, r.status);
|
|
213
|
-
deps.out.say(` ${pad(status, 13)} ${requestSummary(r)}${reason}`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
/** One-line summary of what a typed Request asked for — mirrors Console. */
|
|
217
|
-
function requestSummary(r) {
|
|
218
|
-
if (r.type === "publish") {
|
|
219
|
-
const roles = Array.isArray(r.payload.roles)
|
|
220
|
-
? r.payload.roles
|
|
221
|
-
: [];
|
|
222
|
-
const address = typeof r.payload.customAddress === "string"
|
|
223
|
-
? ` · ที่ ${r.payload.customAddress}`
|
|
224
|
-
: "";
|
|
225
|
-
const who = roles.length === 0 ? "ทุกคน" : roles.join(", ");
|
|
226
|
-
return `คำขอเผยแพร่ · ขึ้นเผยแพร่จริง · ${who}${address}`;
|
|
227
|
-
}
|
|
228
|
-
if (r.type === "retire")
|
|
229
|
-
return "คำขอปลดระวาง · ปลดระวางแอป";
|
|
230
|
-
const parts = [];
|
|
231
|
-
if (typeof r.payload.cpuMs === "number") {
|
|
232
|
-
parts.push(`CPU ${r.payload.cpuMs} ms/request`);
|
|
233
|
-
}
|
|
234
|
-
if (typeof r.payload.notificationsPerDay === "number") {
|
|
235
|
-
parts.push(`การแจ้งเตือน ${r.payload.notificationsPerDay} ครั้ง/วัน`);
|
|
236
|
-
}
|
|
237
|
-
return `คำขอเพิ่มโควตา · ขอเพิ่ม ${parts.length > 0 ? parts.join(" และ ") : "โควตา"}`;
|
|
238
|
-
}
|
package/dist/commands/init.js
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { CliError } from "../errors.js";
|
|
4
|
-
import { cloneApp, hasLocalChanges, recordChanges } from "../git.js";
|
|
5
|
-
import { peekLink, writeLink } from "../link.js";
|
|
6
|
-
import { scaffold } from "../scaffold.js";
|
|
7
|
-
import { client } from "../session.js";
|
|
8
|
-
import { refreshSkills } from "../skills.js";
|
|
9
|
-
/** How often to ask whether a newly created app is ready to scaffold. */
|
|
10
|
-
const PROVISION_POLL_MS = 3000;
|
|
11
|
-
/** How long create may wait before telling the Builder to retry by slug. */
|
|
12
|
-
const PROVISION_TIMEOUT_MS = 5 * 60_000;
|
|
13
|
-
/**
|
|
14
|
-
* Bring a Builder App onto this computer: either create one (App Name via
|
|
15
|
-
* prompt or `--name`) or scaffold an app that already exists (`<slug>`), with
|
|
16
|
-
* the golden-path template in place and linked, ready for the Builder's AI
|
|
17
|
-
* agent to work in. The Provisioner still creates the infra (ADR-0001).
|
|
18
|
-
*/
|
|
19
|
-
export async function init(deps, options) {
|
|
20
|
-
const api = await client(deps);
|
|
21
|
-
const app = options.slug
|
|
22
|
-
? await api.app(options.slug)
|
|
23
|
-
: await createAndWait(deps, api, await resolveName(deps, options.name));
|
|
24
|
-
if (!app.repo) {
|
|
25
|
-
throw new CliError(`"${app.name}" ยังตั้งค่าไม่เสร็จ`, `รอสักครู่แล้วรัน \`pple init ${app.slug}\` หากยังเป็นแบบนี้ กรุณาติดต่อทีมแพลตฟอร์ม`);
|
|
26
|
-
}
|
|
27
|
-
if (app.status === "provisioning") {
|
|
28
|
-
deps.out.say("แอปของคุณยังอยู่ระหว่างการตั้งค่าบนแพลตฟอร์ม…");
|
|
29
|
-
}
|
|
30
|
-
const targetDir = path.resolve(deps.cwd, options.dir ?? app.slug);
|
|
31
|
-
await ensureAvailable(targetDir);
|
|
32
|
-
// The Builder's machine holds no credential of its own; the platform hands
|
|
33
|
-
// out one that opens this app's code and nothing else (ADR-0005).
|
|
34
|
-
const access = await api.repoToken(app.slug);
|
|
35
|
-
deps.out.say(`กำลังตั้งค่า "${app.name}" ใน ${path.basename(targetDir)}/…`);
|
|
36
|
-
await cloneApp(deps, repoUrl(deps, app.repo), targetDir, access);
|
|
37
|
-
if (await isAlreadyBuilt(targetDir)) {
|
|
38
|
-
throw new CliError(`"${app.name}" มีโค้ดอยู่แล้ว จึงไม่มีอะไรต้องตั้งค่า`, `ทำงานในโฟลเดอร์นั้นแล้วรัน \`pple deploy\` เพื่อนำการเปลี่ยนแปลงขึ้นใช้งานจริง`);
|
|
39
|
-
}
|
|
40
|
-
await scaffold(deps.templateDir, targetDir, app.slug);
|
|
41
|
-
await writeLink(targetDir, {
|
|
42
|
-
slug: app.slug,
|
|
43
|
-
apiBase: deps.config.apiBase,
|
|
44
|
-
url: app.url,
|
|
45
|
-
});
|
|
46
|
-
// Skills are a local overlay (ADR-0006), not app source — refresh before the
|
|
47
|
-
// first commit so the ignore rule is in place and Skill files stay untracked.
|
|
48
|
-
await refreshSkills(deps, targetDir);
|
|
49
|
-
if (await hasLocalChanges(deps, targetDir)) {
|
|
50
|
-
await recordChanges(deps, targetDir, "เริ่มจากเทมเพลตมาตรฐานของ PPLE", access);
|
|
51
|
-
}
|
|
52
|
-
report(deps, app, targetDir);
|
|
53
|
-
}
|
|
54
|
-
async function resolveName(deps, name) {
|
|
55
|
-
if (await peekLink(deps)) {
|
|
56
|
-
throw new CliError("โฟลเดอร์นี้เชื่อมกับแอป PPLE อยู่แล้ว", "รัน `pple info` หรือ `pple deploy` ที่นี่ หรือย้ายไปโฟลเดอร์อื่นเพื่อสร้างแอปใหม่");
|
|
57
|
-
}
|
|
58
|
-
if (name !== undefined) {
|
|
59
|
-
const trimmed = name.trim();
|
|
60
|
-
if (!trimmed) {
|
|
61
|
-
throw new CliError("`--name` ต้องระบุชื่อแอป", 'ตัวอย่าง: `pple init --name "ลงทะเบียนอาสาสมัคร"`');
|
|
62
|
-
}
|
|
63
|
-
return trimmed;
|
|
64
|
-
}
|
|
65
|
-
if (!deps.interactive) {
|
|
66
|
-
throw new CliError("`pple init` ต้องระบุชื่อแอป", 'ใส่ `--name "ชื่อแอปของคุณ"` หรือรันในเทอร์มินัลเพื่อให้ระบบถาม');
|
|
67
|
-
}
|
|
68
|
-
const answered = (await deps.ask("ชื่อแอป:")).trim();
|
|
69
|
-
if (!answered) {
|
|
70
|
-
throw new CliError("ต้องระบุชื่อแอปเพื่อสร้างแอป", 'ลองใหม่อีกครั้ง หรือใส่ `--name "ชื่อแอปของคุณ"`');
|
|
71
|
-
}
|
|
72
|
-
return answered;
|
|
73
|
-
}
|
|
74
|
-
async function createAndWait(deps, api, name) {
|
|
75
|
-
deps.out.say(`กำลังสร้าง "${name}" บนแพลตฟอร์ม…`);
|
|
76
|
-
let app = await api.createApp(name);
|
|
77
|
-
if (readyToScaffold(app))
|
|
78
|
-
return app;
|
|
79
|
-
deps.out.say("กำลังรอแพลตฟอร์มตั้งค่าให้เสร็จ…");
|
|
80
|
-
const started = deps.now();
|
|
81
|
-
for (;;) {
|
|
82
|
-
if (app.status === "failed") {
|
|
83
|
-
throw new CliError(`ตั้งค่า "${app.name}" บนแพลตฟอร์มไม่สำเร็จ`, app.error
|
|
84
|
-
? `${app.error} slug ของแอปคือ ${app.slug} — กรุณาติดต่อทีมแพลตฟอร์ม หรือลอง \`pple init ${app.slug}\` ภายหลัง`
|
|
85
|
-
: `slug ของแอปคือ ${app.slug} — กรุณาติดต่อทีมแพลตฟอร์ม หรือลอง \`pple init ${app.slug}\` ภายหลัง`);
|
|
86
|
-
}
|
|
87
|
-
if (deps.now() - started >= PROVISION_TIMEOUT_MS) {
|
|
88
|
-
throw new CliError(`"${app.name}" ยังอยู่ระหว่างการตั้งค่าบนแพลตฟอร์ม`, `ลองใหม่ด้วย \`pple init ${app.slug}\` ในอีกสักครู่`);
|
|
89
|
-
}
|
|
90
|
-
await deps.sleep(PROVISION_POLL_MS);
|
|
91
|
-
app = await api.app(app.slug);
|
|
92
|
-
if (readyToScaffold(app))
|
|
93
|
-
return app;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
/** Ready with a repo — safe to clone and scaffold. */
|
|
97
|
-
function readyToScaffold(app) {
|
|
98
|
-
return app.status === "ready" && app.repo !== null;
|
|
99
|
-
}
|
|
100
|
-
/** A repo the Builder may not have cloned before; https works with any helper. */
|
|
101
|
-
function repoUrl(deps, repoFullName) {
|
|
102
|
-
return `${deps.config.repoBase}/${repoFullName}.git`;
|
|
103
|
-
}
|
|
104
|
-
async function ensureAvailable(targetDir) {
|
|
105
|
-
const entries = await fs.readdir(targetDir).catch(() => null);
|
|
106
|
-
if (entries === null)
|
|
107
|
-
return;
|
|
108
|
-
if (entries.length > 0) {
|
|
109
|
-
throw new CliError(`โฟลเดอร์ ${path.basename(targetDir)}/ มีไฟล์อยู่แล้ว`, "รัน `pple init` ที่อื่น หรือใส่ `--dir <folder>` เพื่อเลือกโฟลเดอร์อื่น");
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
/** A repo that already carries an app is one somebody has already set up. */
|
|
113
|
-
async function isAlreadyBuilt(targetDir) {
|
|
114
|
-
return fs
|
|
115
|
-
.access(path.join(targetDir, "package.json"))
|
|
116
|
-
.then(() => true)
|
|
117
|
-
.catch(() => false);
|
|
118
|
-
}
|
|
119
|
-
function report(deps, app, targetDir) {
|
|
120
|
-
const folder = path.basename(targetDir);
|
|
121
|
-
deps.out.say();
|
|
122
|
-
deps.out.say(`"${app.name}" พร้อมให้ทำงานแล้วใน ${folder}/`);
|
|
123
|
-
deps.out.say(`แอปจะใช้งานได้ที่ ${app.url}`);
|
|
124
|
-
deps.out.say();
|
|
125
|
-
deps.out.say("ขั้นต่อไป:");
|
|
126
|
-
deps.out.say(` cd ${folder}`);
|
|
127
|
-
deps.out.say(" npm install");
|
|
128
|
-
deps.out.say(" npm run dev # ดูผลบนเครื่องนี้");
|
|
129
|
-
deps.out.say(" pple deploy # นำขึ้นใช้งานจริง");
|
|
130
|
-
}
|
package/dist/commands/upgrade.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// `pple upgrade` — update the CLI itself, which is also how Skills and the
|
|
2
|
-
// golden-path template move forward (ADR-0006). It replaces only an install
|
|
3
|
-
// npm put there, never with sudo: a global folder the Builder can't write to
|
|
4
|
-
// is theirs to fix, not the CLI's to force.
|
|
5
|
-
import { CliError } from "../errors.js";
|
|
6
|
-
import { installGlobally, isNewer, isNpmGlobalInstall, latestVersion, MANUAL_INSTALL, } from "../npm.js";
|
|
7
|
-
import { announceUpdate, manualHint, sayOtherChannel } from "../update.js";
|
|
8
|
-
export async function upgrade(deps) {
|
|
9
|
-
const latest = await latestVersion(deps);
|
|
10
|
-
if (latest === null) {
|
|
11
|
-
throw new CliError("เชื่อมต่อ npm เพื่อตรวจหาเวอร์ชันใหม่ของ pple ไม่ได้", `ตรวจสอบการเชื่อมต่ออินเทอร์เน็ต หรืออัปเกรดโดยตรง:\n\n ${MANUAL_INSTALL}`);
|
|
12
|
-
}
|
|
13
|
-
if (!isNewer(latest, deps.version)) {
|
|
14
|
-
deps.out.say(`คุณใช้ pple เวอร์ชันล่าสุดอยู่แล้ว (${deps.version})`);
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
if (!(await isNpmGlobalInstall(deps))) {
|
|
18
|
-
announceUpdate(deps, latest);
|
|
19
|
-
deps.out.say();
|
|
20
|
-
sayOtherChannel(deps);
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
deps.out.say(`กำลังอัปเดต pple ${deps.version} → ${latest}…`);
|
|
24
|
-
const result = await installGlobally(deps, latest);
|
|
25
|
-
if (result.code !== 0) {
|
|
26
|
-
throw new CliError(`อัปเดต pple เป็น ${latest} ไม่สำเร็จ`, manualHint(result));
|
|
27
|
-
}
|
|
28
|
-
deps.out.say(`pple เป็นเวอร์ชัน ${latest} แล้ว`);
|
|
29
|
-
}
|