@pplethai/cli 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +11 -6
  2. package/dist/api.js +62 -16
  3. package/dist/bin.js +1 -1
  4. package/dist/cli.js +64 -16
  5. package/dist/commands/auth.js +8 -8
  6. package/dist/commands/deploy.js +20 -21
  7. package/dist/commands/env.js +70 -0
  8. package/dist/commands/info.js +238 -0
  9. package/dist/commands/init.js +69 -16
  10. package/dist/commands/upgrade.js +5 -5
  11. package/dist/device-flow.js +4 -4
  12. package/dist/errors.js +2 -2
  13. package/dist/git.js +12 -12
  14. package/dist/link.js +12 -2
  15. package/dist/node-deps.js +23 -0
  16. package/dist/npm.js +1 -1
  17. package/dist/scaffold.js +1 -1
  18. package/dist/skills.js +2 -2
  19. package/dist/update.js +11 -11
  20. package/package.json +1 -1
  21. package/skills/pple-platform/SKILL.md +30 -21
  22. package/skills/pple-platform/evals/evals.json +1 -1
  23. package/skills/pple-platform/references/deployment.md +75 -7
  24. package/skills/pple-platform/references/mini-app.md +3 -2
  25. package/skills/pple-platform/references/pple-id-auth.md +32 -18
  26. package/skills/pple-platform/references/project-scaffold.md +6 -6
  27. package/templates/golden-path/.env.example +5 -3
  28. package/templates/golden-path/CLAUDE.md +6 -2
  29. package/templates/golden-path/README.md +4 -4
  30. package/templates/golden-path/package-lock.json +8 -7
  31. package/templates/golden-path/package.json +2 -1
  32. package/templates/golden-path/web/App.tsx +22 -4
  33. package/templates/golden-path/web/app.css +0 -20
  34. package/templates/golden-path/web/main.tsx +16 -5
  35. package/templates/golden-path/web/pple.ts +20 -7
  36. package/templates/golden-path/web/vite-env.d.ts +3 -3
  37. package/templates/golden-path/web/NotInApp.tsx +0 -23
@@ -0,0 +1,238 @@
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
+ }
@@ -2,33 +2,40 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { CliError } from "../errors.js";
4
4
  import { cloneApp, hasLocalChanges, recordChanges } from "../git.js";
5
- import { writeLink } from "../link.js";
5
+ import { peekLink, writeLink } from "../link.js";
6
6
  import { scaffold } from "../scaffold.js";
7
7
  import { client } from "../session.js";
8
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;
9
13
  /**
10
- * Bring an app that already exists on the platform (ADR-0001: the Provisioner
11
- * creates it, not the CLI) onto this computer, with the golden-path template
12
- * in place and linked, ready for the Builder's AI agent to work in.
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).
13
18
  */
14
19
  export async function init(deps, options) {
15
20
  const api = await client(deps);
16
- const app = await api.app(options.slug);
21
+ const app = options.slug
22
+ ? await api.app(options.slug)
23
+ : await createAndWait(deps, api, await resolveName(deps, options.name));
17
24
  if (!app.repo) {
18
- throw new CliError(`"${app.name}" isn't finished being set up yet.`, "Wait a moment and run `pple init` again; if it stays like this, ask the platform team.");
25
+ throw new CliError(`"${app.name}" ยังตั้งค่าไม่เสร็จ`, `รอสักครู่แล้วรัน \`pple init ${app.slug}\` หากยังเป็นแบบนี้ กรุณาติดต่อทีมแพลตฟอร์ม`);
19
26
  }
20
27
  if (app.status === "provisioning") {
21
- deps.out.say("Your app is still being set up on the platform…");
28
+ deps.out.say("แอปของคุณยังอยู่ระหว่างการตั้งค่าบนแพลตฟอร์ม…");
22
29
  }
23
30
  const targetDir = path.resolve(deps.cwd, options.dir ?? app.slug);
24
31
  await ensureAvailable(targetDir);
25
32
  // The Builder's machine holds no credential of its own; the platform hands
26
33
  // out one that opens this app's code and nothing else (ADR-0005).
27
34
  const access = await api.repoToken(app.slug);
28
- deps.out.say(`Setting up "${app.name}" in ${path.basename(targetDir)}/…`);
35
+ deps.out.say(`กำลังตั้งค่า "${app.name}" ใน ${path.basename(targetDir)}/…`);
29
36
  await cloneApp(deps, repoUrl(deps, app.repo), targetDir, access);
30
37
  if (await isAlreadyBuilt(targetDir)) {
31
- throw new CliError(`"${app.name}" already has code, so there's nothing to set up.`, `Work in that folder and run \`pple deploy\` to put changes live.`);
38
+ throw new CliError(`"${app.name}" มีโค้ดอยู่แล้ว จึงไม่มีอะไรต้องตั้งค่า`, `ทำงานในโฟลเดอร์นั้นแล้วรัน \`pple deploy\` เพื่อนำการเปลี่ยนแปลงขึ้นใช้งานจริง`);
32
39
  }
33
40
  await scaffold(deps.templateDir, targetDir, app.slug);
34
41
  await writeLink(targetDir, {
@@ -40,10 +47,56 @@ export async function init(deps, options) {
40
47
  // first commit so the ignore rule is in place and Skill files stay untracked.
41
48
  await refreshSkills(deps, targetDir);
42
49
  if (await hasLocalChanges(deps, targetDir)) {
43
- await recordChanges(deps, targetDir, "Start from the PPLE golden-path template", access);
50
+ await recordChanges(deps, targetDir, "เริ่มจากเทมเพลตมาตรฐานของ PPLE", access);
44
51
  }
45
52
  report(deps, app, targetDir);
46
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
+ }
47
100
  /** A repo the Builder may not have cloned before; https works with any helper. */
48
101
  function repoUrl(deps, repoFullName) {
49
102
  return `${deps.config.repoBase}/${repoFullName}.git`;
@@ -53,7 +106,7 @@ async function ensureAvailable(targetDir) {
53
106
  if (entries === null)
54
107
  return;
55
108
  if (entries.length > 0) {
56
- throw new CliError(`The folder ${path.basename(targetDir)}/ already has something in it.`, "Run `pple init` somewhere else, or pass `--dir <folder>` to pick another folder.");
109
+ throw new CliError(`โฟลเดอร์ ${path.basename(targetDir)}/ มีไฟล์อยู่แล้ว`, "รัน `pple init` ที่อื่น หรือใส่ `--dir <folder>` เพื่อเลือกโฟลเดอร์อื่น");
57
110
  }
58
111
  }
59
112
  /** A repo that already carries an app is one somebody has already set up. */
@@ -66,12 +119,12 @@ async function isAlreadyBuilt(targetDir) {
66
119
  function report(deps, app, targetDir) {
67
120
  const folder = path.basename(targetDir);
68
121
  deps.out.say();
69
- deps.out.say(`"${app.name}" is ready to work on in ${folder}/`);
70
- deps.out.say(`It will be live at ${app.url}`);
122
+ deps.out.say(`"${app.name}" พร้อมให้ทำงานแล้วใน ${folder}/`);
123
+ deps.out.say(`แอปจะใช้งานได้ที่ ${app.url}`);
71
124
  deps.out.say();
72
- deps.out.say("Next:");
125
+ deps.out.say("ขั้นต่อไป:");
73
126
  deps.out.say(` cd ${folder}`);
74
127
  deps.out.say(" npm install");
75
- deps.out.say(" npm run dev # see it on this computer");
76
- deps.out.say(" pple deploy # put it live");
128
+ deps.out.say(" npm run dev # ดูผลบนเครื่องนี้");
129
+ deps.out.say(" pple deploy # นำขึ้นใช้งานจริง");
77
130
  }
@@ -8,10 +8,10 @@ import { announceUpdate, manualHint, sayOtherChannel } from "../update.js";
8
8
  export async function upgrade(deps) {
9
9
  const latest = await latestVersion(deps);
10
10
  if (latest === null) {
11
- throw new CliError("Couldn't reach npm to check for a newer pple.", `Check your internet connection, or upgrade directly:\n\n ${MANUAL_INSTALL}`);
11
+ throw new CliError("เชื่อมต่อ npm เพื่อตรวจหาเวอร์ชันใหม่ของ pple ไม่ได้", `ตรวจสอบการเชื่อมต่ออินเทอร์เน็ต หรืออัปเกรดโดยตรง:\n\n ${MANUAL_INSTALL}`);
12
12
  }
13
13
  if (!isNewer(latest, deps.version)) {
14
- deps.out.say(`You're on the latest pple (${deps.version}).`);
14
+ deps.out.say(`คุณใช้ pple เวอร์ชันล่าสุดอยู่แล้ว (${deps.version})`);
15
15
  return;
16
16
  }
17
17
  if (!(await isNpmGlobalInstall(deps))) {
@@ -20,10 +20,10 @@ export async function upgrade(deps) {
20
20
  sayOtherChannel(deps);
21
21
  return;
22
22
  }
23
- deps.out.say(`Updating pple ${deps.version} → ${latest}…`);
23
+ deps.out.say(`กำลังอัปเดต pple ${deps.version} → ${latest}…`);
24
24
  const result = await installGlobally(deps, latest);
25
25
  if (result.code !== 0) {
26
- throw new CliError(`Couldn't update pple to ${latest}.`, manualHint(result));
26
+ throw new CliError(`อัปเดต pple เป็น ${latest} ไม่สำเร็จ`, manualHint(result));
27
27
  }
28
- deps.out.say(`pple is now ${latest}.`);
28
+ deps.out.say(`pple เป็นเวอร์ชัน ${latest} แล้ว`);
29
29
  }
@@ -24,7 +24,7 @@ function form(fields) {
24
24
  * couldn't reach is the one fact that tells the two apart.
25
25
  */
26
26
  function unreachable(ssoBase) {
27
- return new CliError(`Couldn't reach PPLE ID (${ssoBase}) to sign you in.`, "Check your internet connection and try `pple auth login` again.");
27
+ return new CliError(`เชื่อมต่อ PPLE ID (${ssoBase}) เพื่อเข้าสู่ระบบไม่ได้`, "ตรวจสอบการเชื่อมต่ออินเทอร์เน็ตแล้วลอง `pple auth login` อีกครั้ง");
28
28
  }
29
29
  export async function requestDeviceCode(deps) {
30
30
  const { ssoBase, clientId } = deps.config;
@@ -77,17 +77,17 @@ export async function awaitToken(deps, device) {
77
77
  intervalMs += 5000;
78
78
  break;
79
79
  case "access_denied":
80
- throw new CliError("Sign-in was declined in the browser.", "Run `pple auth login` again if that wasn't you.");
80
+ throw new CliError("การเข้าสู่ระบบถูกปฏิเสธในเบราว์เซอร์", "หากไม่ใช่คุณที่ปฏิเสธ ให้รัน `pple auth login` อีกครั้ง");
81
81
  case "expired_token":
82
82
  throw expiredCode();
83
83
  default:
84
- throw new CliError("PPLE ID refused the sign-in.", body.error_description ?? "Try `pple auth login` again.");
84
+ throw new CliError("PPLE ID ปฏิเสธการเข้าสู่ระบบ", body.error_description ?? "ลอง `pple auth login` อีกครั้ง");
85
85
  }
86
86
  }
87
87
  throw expiredCode();
88
88
  }
89
89
  function expiredCode() {
90
- return new CliError("The sign-in code expired before it was approved.", "Run `pple auth login` again — the browser step has to finish within a few minutes.");
90
+ return new CliError("รหัสเข้าสู่ระบบหมดอายุก่อนได้รับการอนุมัติ", "รัน `pple auth login` อีกครั้ง — ขั้นตอนในเบราว์เซอร์ต้องทำให้เสร็จภายในไม่กี่นาที");
91
91
  }
92
92
  /** Trade a refresh token for a fresh access token; null if it no longer works. */
93
93
  export async function refresh(deps, refreshToken) {
package/dist/errors.js CHANGED
@@ -12,5 +12,5 @@ export class CliError extends Error {
12
12
  }
13
13
  }
14
14
  /** Both ends of the sign-in — the stored token, and what the platform says. */
15
- export const SIGNED_OUT = new CliError("You're not signed in.", "Run `pple auth login` first.");
16
- export const SIGN_IN_EXPIRED = new CliError("Your sign-in has expired.", "Run `pple auth login` to sign in again.");
15
+ export const SIGNED_OUT = new CliError("คุณยังไม่ได้เข้าสู่ระบบ", "รัน `pple auth login` ก่อน");
16
+ export const SIGN_IN_EXPIRED = new CliError("การเข้าสู่ระบบของคุณหมดอายุแล้ว", "รัน `pple auth login` เพื่อเข้าสู่ระบบอีกครั้ง");
package/dist/git.js CHANGED
@@ -55,14 +55,14 @@ function isAccessFailure(result) {
55
55
  return /authentication|could not read|403|denied|not found/i.test(`${result.stdout}\n${result.stderr}`);
56
56
  }
57
57
  function noAccess(action, command) {
58
- return new CliError(`The platform wouldn't let this computer ${action}.`, `Check that the app is still yours at https://console.ppleth.ai, then run \`${command}\` again.`);
58
+ return new CliError(`แพลตฟอร์มไม่อนุญาตให้เครื่องนี้${action}`, `ตรวจสอบว่าแอปยังเป็นของคุณอยู่ที่ https://console.ppleth.ai แล้วรัน \`${command}\` อีกครั้ง`);
59
59
  }
60
60
  function unreadableFolder(result) {
61
- return new CliError("Couldn't read your app's folder.", result.stderr.trim());
61
+ return new CliError("อ่านโฟลเดอร์ของแอปไม่ได้", result.stderr.trim());
62
62
  }
63
63
  async function gitOrThrow(deps, dir, args, onFailure, access) {
64
64
  const result = await git(deps, dir, args, access).catch(() => {
65
- throw new CliError("This computer doesn't have the tools the PPLE CLI needs.", "Install git (https://git-scm.com/downloads) and run the command again.");
65
+ throw new CliError("เครื่องนี้ยังไม่มีเครื่องมือที่ PPLE CLI ต้องใช้", "ติดตั้ง git (https://git-scm.com/downloads) แล้วรันคำสั่งอีกครั้ง");
66
66
  });
67
67
  if (result.code !== 0)
68
68
  throw onFailure(result);
@@ -71,8 +71,8 @@ async function gitOrThrow(deps, dir, args, onFailure, access) {
71
71
  /** Fetch an app's code into `targetDir`, which must not exist yet. */
72
72
  export async function cloneApp(deps, repoUrl, targetDir, access) {
73
73
  await gitOrThrow(deps, deps.cwd, ["clone", repoUrl, targetDir], (result) => isAccessFailure(result)
74
- ? noAccess("reach your app's code", "pple init")
75
- : new CliError("Couldn't fetch your app's code.", result.stderr.trim()), access);
74
+ ? noAccess("เข้าถึงโค้ดของแอปคุณ", "pple init")
75
+ : new CliError("ดึงโค้ดของแอปไม่สำเร็จ", result.stderr.trim()), access);
76
76
  }
77
77
  /** True when the working copy holds changes that haven't been recorded yet. */
78
78
  export async function hasLocalChanges(deps, dir) {
@@ -82,10 +82,10 @@ export async function hasLocalChanges(deps, dir) {
82
82
  /** Record every change in the folder as one entry in the app's history. */
83
83
  export async function recordChanges(deps, dir, message, access) {
84
84
  await gitOrThrow(deps, dir, ["add", "-A"], unreadableFolder);
85
- await gitOrThrow(deps, dir, [...authorArgs(access), "commit", "-m", message], (r) => new CliError("Couldn't save your changes.", r.stderr.trim()));
85
+ await gitOrThrow(deps, dir, [...authorArgs(access), "commit", "-m", message], (r) => new CliError("บันทึกการเปลี่ยนแปลงของคุณไม่สำเร็จ", r.stderr.trim()));
86
86
  }
87
87
  export async function headCommit(deps, dir) {
88
- const result = await gitOrThrow(deps, dir, ["rev-parse", "HEAD"], (r) => new CliError("Couldn't read your app's history.", r.stderr.trim()));
88
+ const result = await gitOrThrow(deps, dir, ["rev-parse", "HEAD"], (r) => new CliError("อ่านประวัติของแอปไม่สำเร็จ", r.stderr.trim()));
89
89
  return result.stdout.trim();
90
90
  }
91
91
  /** True when the local branch already matches what the platform has. */
@@ -110,18 +110,18 @@ export async function sendChanges(deps, dir, access) {
110
110
  const output = `${pushed.stdout}\n${pushed.stderr}`;
111
111
  if (!/rejected|non-fast-forward|fetch first/i.test(output)) {
112
112
  if (isAccessFailure(pushed)) {
113
- throw noAccess("send changes to your app", "pple deploy");
113
+ throw noAccess("ส่งการเปลี่ยนแปลงไปยังแอปของคุณ", "pple deploy");
114
114
  }
115
- throw new CliError("Couldn't send your changes.", pushed.stderr.trim());
115
+ throw new CliError("ส่งการเปลี่ยนแปลงของคุณไม่สำเร็จ", pushed.stderr.trim());
116
116
  }
117
- deps.out.say("Someone else changed this app — bringing their work in first…");
117
+ deps.out.say("มีคนอื่นแก้ไขแอปนี้ — กำลังนำงานของเขาเข้ามาก่อน…");
118
118
  const rebase = await git(deps, dir, [...authorArgs(access), "pull", "--rebase", "origin", MAIN_BRANCH], access);
119
119
  if (rebase.code !== 0) {
120
120
  await git(deps, dir, ["rebase", "--abort"]);
121
- throw new CliError("Your changes clash with changes someone else already deployed.", "Ask your AI coding agent to reconcile the two versions, then run `pple deploy` again.");
121
+ throw new CliError("การเปลี่ยนแปลงของคุณขัดแย้งกับที่คนอื่นดีพลอยไปแล้ว", "ให้ AI coding agent ของคุณรวมสองเวอร์ชันเข้าด้วยกัน แล้วรัน `pple deploy` อีกครั้ง");
122
122
  }
123
123
  const retried = await git(deps, dir, push, access);
124
124
  if (retried.code !== 0) {
125
- throw new CliError("Couldn't send your changes.", retried.stderr.trim());
125
+ throw new CliError("ส่งการเปลี่ยนแปลงของคุณไม่สำเร็จ", retried.stderr.trim());
126
126
  }
127
127
  }
package/dist/link.js CHANGED
@@ -15,6 +15,16 @@ export async function writeLink(dir, link) {
15
15
  * so `pple deploy` works from anywhere inside the app.
16
16
  */
17
17
  export async function findLink(deps) {
18
+ const found = await peekLink(deps);
19
+ if (found)
20
+ return found;
21
+ throw new CliError("โฟลเดอร์นี้ไม่ใช่แอป PPLE", "เข้าไปในโฟลเดอร์ของแอป หรือรัน `pple init <app>` เพื่อตั้งค่าแอปใหม่");
22
+ }
23
+ /**
24
+ * Like `findLink`, but returns null when no App Link is nearby — used by
25
+ * `pple init` to refuse creating a second app inside an already-linked folder.
26
+ */
27
+ export async function peekLink(deps) {
18
28
  let dir = path.resolve(deps.cwd);
19
29
  for (;;) {
20
30
  const candidate = path.join(dir, LINK_DIR, LINK_FILE);
@@ -23,14 +33,14 @@ export async function findLink(deps) {
23
33
  const link = parseLink(raw);
24
34
  if (link)
25
35
  return { ...link, dir };
26
- throw new CliError("This app's platform link is damaged.", "Run `pple init <app>` in a fresh folder to set it up again.");
36
+ throw new CliError("การเชื่อมโยงแอปนี้กับแพลตฟอร์มเสียหาย", "รัน `pple init <app>` ในโฟลเดอร์ใหม่เพื่อตั้งค่าอีกครั้ง");
27
37
  }
28
38
  const parent = path.dirname(dir);
29
39
  if (parent === dir)
30
40
  break;
31
41
  dir = parent;
32
42
  }
33
- throw new CliError("This folder isn't a PPLE app.", "Move into your app's folder, or run `pple init <app>` to set one up.");
43
+ return null;
34
44
  }
35
45
  function parseLink(raw) {
36
46
  try {
package/dist/node-deps.js CHANGED
@@ -41,6 +41,27 @@ async function confirm(question) {
41
41
  rl.close();
42
42
  }
43
43
  }
44
+ /** A free-text question at the terminal (App Name, etc.). */
45
+ async function ask(question) {
46
+ const rl = readline.createInterface({
47
+ input: process.stdin,
48
+ output: process.stdout,
49
+ });
50
+ try {
51
+ return await rl.question(`${question} `);
52
+ }
53
+ finally {
54
+ rl.close();
55
+ }
56
+ }
57
+ /** Drain stdin to EOF — for piping multiline or secret env values. */
58
+ async function readStdin() {
59
+ const chunks = [];
60
+ for await (const chunk of process.stdin) {
61
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
62
+ }
63
+ return Buffer.concat(chunks).toString("utf8");
64
+ }
44
65
  /**
45
66
  * Run the Builder's command line again on the `pple` that is installed now.
46
67
  * The child inherits the terminal, so a re-run deploy still prompts and
@@ -76,6 +97,8 @@ export function nodeDeps(out, homeDir) {
76
97
  version: packageVersion(),
77
98
  interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
78
99
  confirm,
100
+ ask,
101
+ readStdin,
79
102
  reexec,
80
103
  updateChecks: updateChecksAllowed(process.env),
81
104
  gitAskpass: askpassHelper(),
package/dist/npm.js CHANGED
@@ -123,5 +123,5 @@ export function npmMessage(result) {
123
123
  .split("\n")
124
124
  .map((line) => line.replace(/^npm (ERR!|error|warn)\s*/i, "").trim())
125
125
  .filter((line) => line !== "");
126
- return lines.slice(0, 3).join("\n") || "it stopped without saying why.";
126
+ return lines.slice(0, 3).join("\n") || "หยุดทำงานโดยไม่ได้ระบุสาเหตุ";
127
127
  }
package/dist/scaffold.js CHANGED
@@ -24,7 +24,7 @@ const PERSONALIZED = new Set(["package.json", "package-lock.json", "wrangler.jso
24
24
  export async function scaffold(templateDir, targetDir, slug) {
25
25
  const entries = await fs.readdir(templateDir).catch(() => null);
26
26
  if (entries === null) {
27
- throw new CliError("The PPLE app template is missing from this installation of the CLI.", `Reinstall the CLI (\`${MANUAL_INSTALL}\`) and try again.`);
27
+ throw new CliError("ไม่พบเทมเพลตแอป PPLE ในการติดตั้ง CLI ชุดนี้", `ติดตั้ง CLI ใหม่ (\`${MANUAL_INSTALL}\`) แล้วลองอีกครั้ง`);
28
28
  }
29
29
  const written = [];
30
30
  await copyDir({ from: templateDir, to: targetDir, prefix: "", slug, written });
package/dist/skills.js CHANGED
@@ -17,7 +17,7 @@ export async function refreshSkills(deps, appDir) {
17
17
  const source = deps.skillsDir;
18
18
  const entries = await fs.readdir(source).catch(() => null);
19
19
  if (entries === null) {
20
- throw new CliError("The PPLE platform Skills are missing from this installation of the CLI.", `Reinstall the CLI (\`${MANUAL_INSTALL}\`) and try again.`);
20
+ throw new CliError("ไม่พบ Skill ของ PPLE Platform ในการติดตั้ง CLI ชุดนี้", `ติดตั้ง CLI ใหม่ (\`${MANUAL_INSTALL}\`) แล้วลองอีกครั้ง`);
21
21
  }
22
22
  const destination = path.join(appDir, PLATFORM_SKILL_REL);
23
23
  const before = await contentFingerprint(destination);
@@ -26,7 +26,7 @@ export async function refreshSkills(deps, appDir) {
26
26
  const after = await contentFingerprint(destination);
27
27
  if (before === after)
28
28
  return;
29
- deps.out.say(before === null ? "Installed platform Skills." : "Updated platform Skills.");
29
+ deps.out.say(before === null ? "ติดตั้ง Skill ของแพลตฟอร์มแล้ว" : "อัปเดต Skill ของแพลตฟอร์มแล้ว");
30
30
  }
31
31
  async function replaceDir(from, to) {
32
32
  await fs.rm(to, { recursive: true, force: true });