@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
package/README.md CHANGED
@@ -4,7 +4,9 @@ The Builder's terminal tool: three commands between a PPLE ID and a live app.
4
4
 
5
5
  ```bash
6
6
  pple auth login # sign in with your PPLE ID (browser, no password in the terminal)
7
- pple init brave-otter # set up the app's code in ./brave-otter, linked to the app
7
+ pple init # create an app (asks for App Name) and set up its code
8
+ pple init --name "Sign-up" # same, without a prompt (for agents/scripts)
9
+ pple init brave-otter # set up an app that already exists on the platform
8
10
  pple deploy -m "Add the form" # send the changes and put them live
9
11
  pple upgrade # update pple itself from npm
10
12
  ```
@@ -38,11 +40,14 @@ Builder approves it. Tokens land in `$XDG_CONFIG_HOME/pple/credentials.json`
38
40
  (mode `0600`) — never inside an app folder, which is a repo the platform
39
41
  owns. `pple whoami` renews an expiring token silently via the refresh token.
40
42
 
41
- **`pple init <app>`** takes an app the Provisioner already created (ADR-0001
42
- — the CLI never creates repos), fetches its code, scaffolds the golden-path
43
- template into it under the app's own name, writes `.pple/app.json`
44
- linking the folder to the app, and installs the platform Skill overlay the
45
- Builder's AI coding agent can load (ADR-0006).
43
+ **`pple init`** creates a Builder App (prompt for App Name, or `--name`) via
44
+ the same Provisioner `POST /apps` the Console uses, waits until the app is
45
+ ready, then scaffolds it — or, given an App Slug, scaffolds an app that
46
+ already exists. Either way it fetches the app's code, copies the golden-path
47
+ template under the app's own name, writes `.pple/app.json` linking the folder
48
+ to the app, and installs the platform Skill overlay the Builder's AI coding
49
+ agent can load (ADR-0001, ADR-0006). The App Slug stays platform-generated;
50
+ bare `pple init` in a folder that already has an App Link is refused.
46
51
 
47
52
  **`pple deploy`** refreshes that same Skill overlay, records everything in
48
53
  the folder, sends it to the app's repo — merging in a Collaborator's work if
package/dist/api.js CHANGED
@@ -15,48 +15,94 @@ export class ProvisionerClient {
15
15
  app(slug) {
16
16
  return this.request("GET", `/apps/${encodeURIComponent(slug)}`, { slug });
17
17
  }
18
+ /** Create a Builder App; the Provisioner assigns the App Slug (ADR-0001). */
19
+ createApp(name) {
20
+ return this.request("POST", "/apps", { body: { name } });
21
+ }
18
22
  repoToken(slug) {
19
23
  return this.request("POST", `/apps/${encodeURIComponent(slug)}/repo-token`, { slug });
20
24
  }
21
25
  deploys(slug) {
22
26
  return this.request("GET", `/apps/${encodeURIComponent(slug)}/deploys`, { slug }).then((r) => r.deploys);
23
27
  }
28
+ env(slug) {
29
+ return this.request("GET", `/apps/${encodeURIComponent(slug)}/env`, { slug }).then((r) => r.env);
30
+ }
31
+ metrics(slug) {
32
+ return this.request("GET", `/apps/${encodeURIComponent(slug)}/metrics`, { slug });
33
+ }
34
+ collaborators(slug) {
35
+ return this.request("GET", `/apps/${encodeURIComponent(slug)}/collaborators`, { slug }).then((r) => r.collaborators);
36
+ }
37
+ requests(slug) {
38
+ return this.request("GET", `/apps/${encodeURIComponent(slug)}/requests`, { slug }).then((r) => r.requests);
39
+ }
40
+ invites(slug) {
41
+ return this.request("GET", `/apps/${encodeURIComponent(slug)}/invites`, { slug }).then((r) => r.invites);
42
+ }
43
+ setEnv(slug, key, value, secret) {
44
+ return this.request("PUT", `/apps/${encodeURIComponent(slug)}/env/${encodeURIComponent(key)}`, { slug, body: { value, secret } });
45
+ }
46
+ deleteEnv(slug, key) {
47
+ return this.request("DELETE", `/apps/${encodeURIComponent(slug)}/env/${encodeURIComponent(key)}`, { slug, envKey: key });
48
+ }
24
49
  async request(method, path, context = {}) {
25
50
  const url = `${this.deps.config.apiBase}${path}`;
51
+ const headers = {
52
+ authorization: `Bearer ${this.token}`,
53
+ };
54
+ let body;
55
+ if (context.body !== undefined) {
56
+ headers["content-type"] = "application/json";
57
+ body = JSON.stringify(context.body);
58
+ }
26
59
  const res = await this.deps
27
- .fetch(url, {
28
- method,
29
- headers: { authorization: `Bearer ${this.token}` },
30
- })
60
+ .fetch(url, { method, headers, body })
31
61
  .catch(() => null);
32
62
  if (!res) {
33
- throw new CliError("Couldn't reach the PPLE Platform.", "Check your internet connection and try again.");
63
+ throw new CliError("เชื่อมต่อ PPLE Platform ไม่ได้", "ตรวจสอบการเชื่อมต่ออินเทอร์เน็ตแล้วลองใหม่อีกครั้ง");
34
64
  }
35
- if (res.ok)
65
+ if (res.ok) {
66
+ if (res.status === 204)
67
+ return undefined;
36
68
  return (await res.json());
37
- const body = (await res.json().catch(() => ({})));
38
- throw platformError(res.status, body.error, context.slug);
69
+ }
70
+ const errBody = (await res.json().catch(() => ({})));
71
+ throw platformError(res.status, errBody.error, context);
39
72
  }
40
73
  }
41
- function platformError(status, code, slug) {
74
+ function platformError(status, code, context = {}) {
75
+ const { slug, envKey } = context;
42
76
  if (status === 401)
43
77
  return SIGN_IN_EXPIRED;
44
78
  if (code === "not_org_member") {
45
- return new CliError("Your PPLE ID isn't part of the party organization, so it can't build apps.", "Ask your team lead to add you, then try again.");
79
+ return new CliError("PPLE ID ของคุณยังไม่ได้อยู่ในองค์กรของพรรค จึงสร้างแอปไม่ได้", "กรุณาติดต่อหัวหน้าทีมของคุณเพื่อเพิ่มสิทธิ์ แล้วลองใหม่อีกครั้ง");
46
80
  }
47
81
  if (code === "agreement_required") {
48
- return new CliError("You haven't accepted the Builder agreement yet.", "Accept it at https://console.ppleth.ai and run this command again.");
82
+ return new CliError("คุณยังไม่ได้ยอมรับข้อตกลงผู้พัฒนา", "ยอมรับได้ที่ https://console.ppleth.ai แล้วรันคำสั่งนี้อีกครั้ง");
49
83
  }
50
84
  if (code === "app_has_no_repo") {
51
- return new CliError(`${slug ? `"${slug}"` : "That app"} isn't finished being set up yet.`, "Wait a moment and try again; if it stays like this, ask the platform team.");
85
+ return new CliError(`${slug ? `"${slug}"` : "แอปนี้"} ยังตั้งค่าไม่เสร็จ`, "รอสักครู่แล้วลองใหม่อีกครั้ง หากยังเป็นแบบนี้ กรุณาติดต่อทีมแพลตฟอร์ม");
86
+ }
87
+ if (code === "invalid_key") {
88
+ return new CliError("คีย์นี้ใช้เป็นชื่อไม่ได้", "คีย์ต้องขึ้นต้นด้วยตัวอักษรหรือขีดล่าง ตามด้วยตัวอักษร ตัวเลข หรือขีดล่าง");
89
+ }
90
+ if (code === "reserved_key") {
91
+ return new CliError("คีย์นี้ถูกสงวนไว้โดยแพลตฟอร์ม", "เลือกชื่ออื่น — APP_SLUG, DB, FILES, NOTIFY_* และ PPLE_DEPLOY_ID ถูกใช้ไปแล้ว");
92
+ }
93
+ if (code === "invalid_value") {
94
+ return new CliError("บันทึกค่านี้ไม่สำเร็จ", "ส่งค่ามาเป็น argument ผ่าน `--file` หรือทาง stdin");
52
95
  }
53
96
  if (status === 404) {
97
+ if (envKey) {
98
+ return new CliError(`แอปนี้ไม่ได้ตั้งค่า "${envKey}" ไว้`, "รัน `pple env list` เพื่อดูค่าที่ตั้งไว้");
99
+ }
54
100
  return new CliError(slug
55
- ? `There's no app called "${slug}" that belongs to you.`
56
- : "That doesn't exist on the platform.", "Check the app list at https://console.ppleth.ai.");
101
+ ? `ไม่มีแอปชื่อ "${slug}" ที่เป็นของคุณ`
102
+ : "ไม่พบสิ่งนี้บนแพลตฟอร์ม", "ตรวจสอบรายการแอปได้ที่ https://console.ppleth.ai");
57
103
  }
58
104
  if (status >= 500 || code === "sso_unavailable") {
59
- return new CliError("The PPLE Platform is having trouble right now.", "Wait a moment and try again; if it keeps happening, tell the platform team.");
105
+ return new CliError("PPLE Platform กำลังมีปัญหาในขณะนี้", "รอสักครู่แล้วลองใหม่อีกครั้ง หากยังเกิดขึ้นอีก กรุณาแจ้งทีมแพลตฟอร์ม");
60
106
  }
61
- return new CliError("The PPLE Platform refused that request.", code ? `It said: ${code}` : undefined);
107
+ return new CliError("PPLE Platform ปฏิเสธคำขอนี้", code ? `ข้อความจากระบบ: ${code}` : undefined);
62
108
  }
package/dist/bin.js CHANGED
@@ -10,7 +10,7 @@ try {
10
10
  catch (e) {
11
11
  // Not a failure the CLI knows how to explain — say so plainly instead of
12
12
  // dumping a stack trace on a Builder who can't act on it.
13
- process.stderr.write("Something went wrong inside the pple CLI. Please send this to the platform team:\n");
13
+ process.stderr.write("เกิดข้อผิดพลาดภายใน pple CLI กรุณาส่งข้อความนี้ให้ทีมแพลตฟอร์ม:\n");
14
14
  process.stderr.write(`${e instanceof Error ? (e.stack ?? e.message) : String(e)}\n`);
15
15
  process.exitCode = 1;
16
16
  }
package/dist/cli.js CHANGED
@@ -1,24 +1,36 @@
1
1
  import * as auth from "./commands/auth.js";
2
2
  import { deploy } from "./commands/deploy.js";
3
+ import * as env from "./commands/env.js";
4
+ import { info } from "./commands/info.js";
3
5
  import { init } from "./commands/init.js";
4
6
  import { upgrade } from "./commands/upgrade.js";
5
7
  import { CliError } from "./errors.js";
6
8
  import { checkForUpdate, wantsUpdateCheck } from "./update.js";
7
- const USAGE = `pple — build and ship apps on the PPLE Platform
9
+ const USAGE = `pple — สร้างและส่งแอปขึ้น PPLE Platform
8
10
 
9
- pple auth login sign in with your PPLE ID
10
- pple auth logout forget this computer's sign-in
11
- pple whoami show who you're signed in as
11
+ pple auth login เข้าสู่ระบบด้วย PPLE ID ของคุณ
12
+ pple auth logout ลบการเข้าสู่ระบบออกจากเครื่องนี้
13
+ pple whoami ดูว่ากำลังเข้าสู่ระบบด้วยบัญชีใด
12
14
 
13
- pple init <app> set up an app's code on this computer
14
- pple deploy [-m "note"] send your changes and put them live
15
+ pple init สร้างแอปและตั้งค่าโค้ดบนเครื่องนี้
16
+ pple init --name "App" สร้างแอปโดยไม่ต้องถาม (สำหรับ agent/สคริปต์)
17
+ pple init <app> ตั้งค่าแอปที่มีอยู่แล้วบนแพลตฟอร์ม
18
+ pple deploy [-m "note"] ส่งการเปลี่ยนแปลงของคุณขึ้นใช้งานจริง
19
+ pple info [<slug>] ดูสถานะ การใช้งาน การดีพลอย และคีย์ env ของแอปนี้
15
20
 
16
- pple upgrade update pple itself to the newest version
21
+ pple env list ดูตัวแปรและ secret ของแอปนี้
22
+ pple env set <KEY> [VAL] ตั้งค่าตัวแปร (หรือ --secret) รับค่าจาก arg, --file หรือ stdin
23
+ pple env delete <KEY> ลบตัวแปรหรือ secret
17
24
 
18
- Options
19
- -m, --message <note> what changed, shown in the app's history
20
- --dir <folder> where to put the app's code (pple init)
21
- -h, --help show this help
25
+ pple upgrade อัปเดต pple เป็นเวอร์ชันล่าสุด
26
+
27
+ ตัวเลือก
28
+ -m, --message <note> สิ่งที่เปลี่ยนแปลง แสดงในประวัติของแอป
29
+ --name <App Name> สร้างแอปใหม่ด้วยชื่อนี้ (pple init)
30
+ --dir <folder> ตำแหน่งที่จะวางโค้ดของแอป (pple init)
31
+ --file <path> อ่านค่า env จากไฟล์ (pple env set)
32
+ --secret เก็บค่าเป็น secret แบบเขียนอย่างเดียว (pple env set)
33
+ -h, --help แสดงวิธีใช้นี้
22
34
  `;
23
35
  /**
24
36
  * Parse and run one command line. Returns the process exit code; expected
@@ -68,16 +80,44 @@ async function dispatch(argv, deps) {
68
80
  return auth.whoami(deps);
69
81
  case "init": {
70
82
  const dir = takeOption(args, "--dir");
83
+ const name = takeOption(args, "--name");
71
84
  const slug = args.shift();
72
- if (!slug) {
73
- throw new CliError("Which app? `pple init` needs the app's name.", "Create the app at https://console.ppleth.ai — it shows you the exact command to run.");
85
+ if (name !== undefined && slug) {
86
+ throw new CliError("`pple init` รับได้เฉพาะชื่อแอปใหม่ หรือแอปที่มีอยู่แล้ว อย่างใดอย่างหนึ่งเท่านั้น", 'ใช้ `pple init --name "ชื่อแอปของคุณ"` เพื่อสร้างใหม่ หรือ `pple init <app>` สำหรับแอปที่มีอยู่แล้ว');
74
87
  }
75
- return init(deps, { slug, dir });
88
+ return init(deps, { slug, name, dir });
76
89
  }
77
90
  case "deploy": {
78
91
  const message = takeOption(args, "--message", "-m");
79
92
  return deploy(deps, { message });
80
93
  }
94
+ case "info": {
95
+ const slug = args.shift();
96
+ return info(deps, slug);
97
+ }
98
+ case "env": {
99
+ const sub = args.shift();
100
+ if (sub === "list")
101
+ return env.list(deps);
102
+ if (sub === "set") {
103
+ const secret = takeFlag(args, "--secret");
104
+ const file = takeOption(args, "--file");
105
+ const key = args.shift();
106
+ const value = args.shift();
107
+ if (!key) {
108
+ throw new CliError("`pple env set` ต้องระบุ KEY", "ตัวอย่าง: `pple env set API_URL https://example.com`");
109
+ }
110
+ return env.set(deps, { key, value, secret, file });
111
+ }
112
+ if (sub === "delete") {
113
+ const key = args.shift();
114
+ if (!key) {
115
+ throw new CliError("`pple env delete` ต้องระบุ KEY", "ตัวอย่าง: `pple env delete API_URL`");
116
+ }
117
+ return env.remove(deps, key);
118
+ }
119
+ throw unknown(`env ${sub ?? ""}`.trim());
120
+ }
81
121
  case "upgrade":
82
122
  return upgrade(deps);
83
123
  case "help":
@@ -88,7 +128,7 @@ async function dispatch(argv, deps) {
88
128
  }
89
129
  }
90
130
  function unknown(command) {
91
- return new CliError(`There's no \`pple ${command}\` command.`, "Run `pple --help` to see what you can do.");
131
+ return new CliError(`ไม่มีคำสั่ง \`pple ${command}\``, "รัน `pple --help` เพื่อดูคำสั่งทั้งหมด");
92
132
  }
93
133
  /** Pull `--name value` out of the argument list, wherever it appears. */
94
134
  function takeOption(args, long, short) {
@@ -97,7 +137,15 @@ function takeOption(args, long, short) {
97
137
  return undefined;
98
138
  const [, value] = args.splice(index, 2);
99
139
  if (value === undefined || value.startsWith("-")) {
100
- throw new CliError(`\`${long}\` needs a value after it.`);
140
+ throw new CliError(`\`${long}\` ต้องระบุค่าตามหลัง`);
101
141
  }
102
142
  return value;
103
143
  }
144
+ /** Pull a bare `--flag` out of the argument list, wherever it appears. */
145
+ function takeFlag(args, flag) {
146
+ const index = args.indexOf(flag);
147
+ if (index === -1)
148
+ return false;
149
+ args.splice(index, 1);
150
+ return true;
151
+ }
@@ -4,29 +4,29 @@ import { awaitToken, requestDeviceCode } from "../device-flow.js";
4
4
  import { client } from "../session.js";
5
5
  export async function login(deps) {
6
6
  const device = await requestDeviceCode(deps);
7
- deps.out.say("To sign in, open this page and enter the code:");
7
+ deps.out.say("เข้าสู่ระบบโดยเปิดหน้านี้แล้วกรอกรหัส:");
8
8
  deps.out.say();
9
9
  deps.out.say(` ${device.verificationUri}`);
10
- deps.out.say(` code: ${device.userCode}`);
10
+ deps.out.say(` รหัส: ${device.userCode}`);
11
11
  deps.out.say();
12
- deps.out.say("Waiting for you to approve the sign-in…");
12
+ deps.out.say("กำลังรอให้คุณอนุมัติการเข้าสู่ระบบ…");
13
13
  await deps.openBrowser(device.verificationUriComplete ?? device.verificationUri);
14
14
  const tokens = await awaitToken(deps, device);
15
15
  await saveCredentials(deps, { ...tokens, ssoBase: deps.config.ssoBase });
16
16
  const me = await new ProvisionerClient(deps, tokens.accessToken).me();
17
- deps.out.say(`Signed in as ${me.name ?? me.sub}.`);
17
+ deps.out.say(`เข้าสู่ระบบในชื่อ ${me.name ?? me.sub} แล้ว`);
18
18
  if (!me.isBuilder) {
19
- deps.out.say("One more step before you can create apps: accept the Builder agreement at https://console.ppleth.ai");
19
+ deps.out.say("อีกหนึ่งขั้นตอนก่อนสร้างแอปได้: ยอมรับข้อตกลงผู้พัฒนาที่ https://console.ppleth.ai");
20
20
  }
21
21
  }
22
22
  export async function logout(deps) {
23
23
  const had = await clearCredentials(deps);
24
- deps.out.say(had ? "Signed out." : "You weren't signed in.");
24
+ deps.out.say(had ? "ออกจากระบบแล้ว" : "คุณยังไม่ได้เข้าสู่ระบบ");
25
25
  }
26
26
  export async function whoami(deps) {
27
27
  const me = await (await client(deps)).me();
28
- deps.out.say(`Signed in as ${me.name ?? me.sub}.`);
28
+ deps.out.say(`เข้าสู่ระบบในชื่อ ${me.name ?? me.sub} แล้ว`);
29
29
  if (!me.isBuilder) {
30
- deps.out.say("You haven't accepted the Builder agreement yet — do that at https://console.ppleth.ai");
30
+ deps.out.say("คุณยังไม่ได้ยอมรับข้อตกลงผู้พัฒนา — ยอมรับได้ที่ https://console.ppleth.ai");
31
31
  }
32
32
  }
@@ -18,17 +18,17 @@ const LOG_TAIL_LINES = 20;
18
18
  export async function deploy(deps, options) {
19
19
  const link = await findLink(deps);
20
20
  if (link.apiBase !== "" && link.apiBase !== deps.config.apiBase) {
21
- throw new CliError("This app belongs to a different PPLE Platform than the one you're signed in to.", `It was set up against ${link.apiBase}.`);
21
+ throw new CliError("แอปนี้อยู่บน PPLE Platform คนละตัวกับที่คุณเข้าสู่ระบบอยู่", `แอปนี้ถูกตั้งค่าไว้กับ ${link.apiBase}`);
22
22
  }
23
23
  const api = await client(deps);
24
24
  const app = await api.app(link.slug);
25
25
  if (app.status === "retired") {
26
- throw new CliError(`"${app.name}" has been retired.`, "A retired app can't be deployed to. Create a new app in the Console if you want to pick this work back up.");
26
+ throw new CliError(`"${app.name}" ถูกปลดระวางแล้ว`, "แอปที่ปลดระวางแล้วดีพลอยไม่ได้ หากต้องการสานงานนี้ต่อ ให้สร้างแอปใหม่ในคอนโซล");
27
27
  }
28
28
  if (app.status !== "ready") {
29
- throw new CliError(`"${app.name}" isn't ready to receive changes yet.`, app.error
30
- ? `Setting it up ran into a problem: ${app.error}`
31
- : "Check on it at https://console.ppleth.ai and try again in a moment.");
29
+ throw new CliError(`"${app.name}" ยังไม่พร้อมรับการเปลี่ยนแปลง`, app.error
30
+ ? `การตั้งค่าพบปัญหา: ${app.error}`
31
+ : "ตรวจสอบได้ที่ https://console.ppleth.ai แล้วลองใหม่ในอีกสักครู่");
32
32
  }
33
33
  // One token for the whole command, kept in memory: it says both what this
34
34
  // machine may push and whose name the work goes out under (ADR-0005).
@@ -37,17 +37,17 @@ export async function deploy(deps, options) {
37
37
  // send — including first seed for folders set up before Skills shipped.
38
38
  await refreshSkills(deps, link.dir);
39
39
  if (await hasLocalChanges(deps, link.dir)) {
40
- deps.out.say("Saving your changes…");
41
- await recordChanges(deps, link.dir, options.message ?? `Update ${app.name}`, access);
40
+ deps.out.say("กำลังบันทึกการเปลี่ยนแปลงของคุณ…");
41
+ await recordChanges(deps, link.dir, options.message ?? `อัปเดต ${app.name}`, access);
42
42
  }
43
43
  else if (await isUpToDateWithRemote(deps, link.dir, access)) {
44
- deps.out.say(`Nothing new to send — ${app.name} is already live at ${app.url}`);
44
+ deps.out.say(`ไม่มีการเปลี่ยนแปลงใหม่ — ${app.name} ใช้งานอยู่แล้วที่ ${app.url}`);
45
45
  return;
46
46
  }
47
- deps.out.say("Sending your changes…");
47
+ deps.out.say("กำลังส่งการเปลี่ยนแปลงของคุณ…");
48
48
  await sendChanges(deps, link.dir, access);
49
49
  const commit = await headCommit(deps, link.dir);
50
- deps.out.say("Building your app… (this usually takes a minute)");
50
+ deps.out.say("กำลังบิลด์แอปของคุณ… (ปกติใช้เวลาราวหนึ่งนาที)");
51
51
  const result = await watchDeploy(deps, api, link.slug, commit);
52
52
  report(deps, app, result);
53
53
  }
@@ -68,8 +68,8 @@ async function watchDeploy(deps, api, slug, commit) {
68
68
  const limit = build ? BUILD_TIMEOUT_MS : BUILD_START_TIMEOUT_MS;
69
69
  if (waited >= limit) {
70
70
  throw new CliError(build
71
- ? "Your changes were sent, but the build is taking longer than expected."
72
- : "Your changes were sent, but the platform hasn't started building them.", `Watch it finish at https://console.ppleth.ai/apps/${slug}.`);
71
+ ? "ส่งการเปลี่ยนแปลงของคุณแล้ว แต่การบิลด์ใช้เวลานานกว่าที่คาดไว้"
72
+ : "ส่งการเปลี่ยนแปลงของคุณแล้ว แต่แพลตฟอร์มยังไม่เริ่มบิลด์", `ติดตามจนเสร็จได้ที่ https://console.ppleth.ai/apps/${slug}`);
73
73
  }
74
74
  await deps.sleep(POLL_INTERVAL_MS);
75
75
  }
@@ -79,7 +79,7 @@ const SUPERSEDED = "superseded by a newer deploy";
79
79
  function report(deps, app, build) {
80
80
  if (build.status === "failed") {
81
81
  if (build.error === SUPERSEDED) {
82
- throw new CliError("A newer deploy of this app replaced the one you were waiting for.", `Watch that one finish at https://console.ppleth.ai/apps/${app.slug}.`);
82
+ throw new CliError("มีการดีพลอยครั้งใหม่มาแทนที่ครั้งที่คุณกำลังรออยู่", `ติดตามการดีพลอยครั้งนั้นได้ที่ https://console.ppleth.ai/apps/${app.slug}`);
83
83
  }
84
84
  // A build that compiled badly explains itself in its log; a deploy that
85
85
  // failed after the build — a bad bundle, a rejected upload — only has the
@@ -90,24 +90,23 @@ function report(deps, app, build) {
90
90
  deps.out.say(detail);
91
91
  deps.out.say();
92
92
  }
93
- throw new CliError(`${app.name} couldn't be built, so what's live hasn't changed.`, detail
94
- ? "Show the message above to your AI coding agent — it says what to fix."
95
- : `See what happened at https://console.ppleth.ai/apps/${app.slug}.`);
93
+ throw new CliError(`บิลด์ ${app.name} ไม่สำเร็จ สิ่งที่ใช้งานอยู่จึงยังไม่เปลี่ยนแปลง`, detail
94
+ ? "ส่งข้อความด้านบนให้ AI coding agent ของคุณ — ในนั้นบอกว่าต้องแก้อะไร"
95
+ : `ดูรายละเอียดได้ที่ https://console.ppleth.ai/apps/${app.slug}`);
96
96
  }
97
97
  // Migrations are the part of a deploy a Builder can't see for themselves —
98
98
  // no Cloudflare account (ADR-0001/0002) — so say when some landed. Silence
99
99
  // means the database is unchanged (#75).
100
100
  const applied = build.migrations.applied ?? [];
101
101
  if (applied.length > 0) {
102
- const what = applied.length === 1 ? "1 migration" : `${applied.length} migrations`;
103
- deps.out.say(`Applied ${what} to your database.`);
102
+ deps.out.say(`ใช้ migration ${applied.length} รายการกับฐานข้อมูลของคุณแล้ว`);
104
103
  }
105
104
  if (build.status === "unverified") {
106
- deps.out.say(`${app.name} was updated, but it hasn't answered yet.`);
107
- deps.out.say(`Give it a minute, then open ${app.url}`);
105
+ deps.out.say(`อัปเดต ${app.name} แล้ว แต่แอปยังไม่ตอบสนอง`);
106
+ deps.out.say(`รอสักครู่ แล้วเปิด ${app.url}`);
108
107
  return;
109
108
  }
110
- deps.out.say(`${app.name} is live at ${app.url}`);
109
+ deps.out.say(`${app.name} ใช้งานได้แล้วที่ ${app.url}`);
111
110
  }
112
111
  function tail(log, lines) {
113
112
  return log.split("\n").slice(-lines).join("\n").trim();
@@ -0,0 +1,70 @@
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
+ }