@pplethai/cli 0.2.1 → 0.3.0
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 +11 -6
- package/dist/api.js +62 -16
- package/dist/bin.js +1 -1
- package/dist/cli.js +64 -16
- package/dist/commands/auth.js +8 -8
- package/dist/commands/deploy.js +26 -19
- package/dist/commands/env.js +70 -0
- package/dist/commands/info.js +238 -0
- package/dist/commands/init.js +69 -16
- package/dist/commands/upgrade.js +5 -5
- package/dist/device-flow.js +4 -4
- package/dist/errors.js +2 -2
- package/dist/git.js +12 -12
- package/dist/link.js +12 -2
- package/dist/node-deps.js +23 -0
- package/dist/npm.js +1 -1
- package/dist/scaffold.js +1 -1
- package/dist/skills.js +2 -2
- package/dist/update.js +11 -11
- package/package.json +1 -1
- package/skills/pple-platform/SKILL.md +30 -21
- package/skills/pple-platform/evals/evals.json +1 -1
- package/skills/pple-platform/references/deployment.md +75 -7
- package/skills/pple-platform/references/mini-app.md +3 -2
- package/skills/pple-platform/references/pple-id-auth.md +22 -16
- package/skills/pple-platform/references/project-scaffold.md +6 -6
- package/templates/golden-path/.env.example +5 -3
- package/templates/golden-path/CLAUDE.md +6 -2
- package/templates/golden-path/README.md +4 -4
- package/templates/golden-path/package-lock.json +8 -7
- package/templates/golden-path/package.json +2 -1
- package/templates/golden-path/web/App.tsx +22 -4
- package/templates/golden-path/web/app.css +0 -20
- package/templates/golden-path/web/main.tsx +16 -5
- package/templates/golden-path/web/pple.ts +6 -9
- package/templates/golden-path/web/vite-env.d.ts +3 -3
- package/templates/golden-path/wrangler.jsonc +9 -0
- 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
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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("
|
|
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
|
-
|
|
38
|
-
|
|
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,
|
|
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("
|
|
79
|
+
return new CliError("PPLE ID ของคุณยังไม่ได้อยู่ในองค์กรของพรรค จึงสร้างแอปไม่ได้", "กรุณาติดต่อหัวหน้าทีมของคุณเพื่อเพิ่มสิทธิ์ แล้วลองใหม่อีกครั้ง");
|
|
46
80
|
}
|
|
47
81
|
if (code === "agreement_required") {
|
|
48
|
-
return new CliError("
|
|
82
|
+
return new CliError("คุณยังไม่ได้ยอมรับข้อตกลงผู้พัฒนา", "ยอมรับได้ที่ https://console.ppleth.ai แล้วรันคำสั่งนี้อีกครั้ง");
|
|
49
83
|
}
|
|
50
84
|
if (code === "app_has_no_repo") {
|
|
51
|
-
return new CliError(`${slug ? `"${slug}"` : "
|
|
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
|
-
?
|
|
56
|
-
: "
|
|
101
|
+
? `ไม่มีแอปชื่อ "${slug}" ที่เป็นของคุณ`
|
|
102
|
+
: "ไม่พบสิ่งนี้บนแพลตฟอร์ม", "ตรวจสอบรายการแอปได้ที่ https://console.ppleth.ai");
|
|
57
103
|
}
|
|
58
104
|
if (status >= 500 || code === "sso_unavailable") {
|
|
59
|
-
return new CliError("
|
|
105
|
+
return new CliError("PPLE Platform กำลังมีปัญหาในขณะนี้", "รอสักครู่แล้วลองใหม่อีกครั้ง หากยังเกิดขึ้นอีก กรุณาแจ้งทีมแพลตฟอร์ม");
|
|
60
106
|
}
|
|
61
|
-
return new CliError("
|
|
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("
|
|
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 —
|
|
9
|
+
const USAGE = `pple — สร้างและส่งแอปขึ้น PPLE Platform
|
|
8
10
|
|
|
9
|
-
pple auth login
|
|
10
|
-
pple auth logout
|
|
11
|
-
pple whoami
|
|
11
|
+
pple auth login เข้าสู่ระบบด้วย PPLE ID ของคุณ
|
|
12
|
+
pple auth logout ลบการเข้าสู่ระบบออกจากเครื่องนี้
|
|
13
|
+
pple whoami ดูว่ากำลังเข้าสู่ระบบด้วยบัญชีใด
|
|
12
14
|
|
|
13
|
-
pple init
|
|
14
|
-
pple
|
|
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
|
|
21
|
+
pple env list ดูตัวแปรและ secret ของแอปนี้
|
|
22
|
+
pple env set <KEY> [VAL] ตั้งค่าตัวแปร (หรือ --secret) รับค่าจาก arg, --file หรือ stdin
|
|
23
|
+
pple env delete <KEY> ลบตัวแปรหรือ secret
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
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 (
|
|
73
|
-
throw new CliError("
|
|
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(
|
|
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}\`
|
|
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
|
+
}
|
package/dist/commands/auth.js
CHANGED
|
@@ -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("
|
|
7
|
+
deps.out.say("เข้าสู่ระบบโดยเปิดหน้านี้แล้วกรอกรหัส:");
|
|
8
8
|
deps.out.say();
|
|
9
9
|
deps.out.say(` ${device.verificationUri}`);
|
|
10
|
-
deps.out.say(`
|
|
10
|
+
deps.out.say(` รหัส: ${device.userCode}`);
|
|
11
11
|
deps.out.say();
|
|
12
|
-
deps.out.say("
|
|
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(
|
|
17
|
+
deps.out.say(`เข้าสู่ระบบในชื่อ ${me.name ?? me.sub} แล้ว`);
|
|
18
18
|
if (!me.isBuilder) {
|
|
19
|
-
deps.out.say("
|
|
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 ? "
|
|
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(
|
|
28
|
+
deps.out.say(`เข้าสู่ระบบในชื่อ ${me.name ?? me.sub} แล้ว`);
|
|
29
29
|
if (!me.isBuilder) {
|
|
30
|
-
deps.out.say("
|
|
30
|
+
deps.out.say("คุณยังไม่ได้ยอมรับข้อตกลงผู้พัฒนา — ยอมรับได้ที่ https://console.ppleth.ai");
|
|
31
31
|
}
|
|
32
32
|
}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -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("
|
|
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}"
|
|
26
|
+
throw new CliError(`"${app.name}" ถูกปลดระวางแล้ว`, "แอปที่ปลดระวางแล้วดีพลอยไม่ได้ หากต้องการสานงานนี้ต่อ ให้สร้างแอปใหม่ในคอนโซล");
|
|
27
27
|
}
|
|
28
28
|
if (app.status !== "ready") {
|
|
29
|
-
throw new CliError(`"${app.name}"
|
|
30
|
-
?
|
|
31
|
-
: "
|
|
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("
|
|
41
|
-
await recordChanges(deps, link.dir, options.message ??
|
|
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(
|
|
44
|
+
deps.out.say(`ไม่มีการเปลี่ยนแปลงใหม่ — ${app.name} ใช้งานอยู่แล้วที่ ${app.url}`);
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
|
-
deps.out.say("
|
|
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("
|
|
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
|
-
? "
|
|
72
|
-
: "
|
|
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("
|
|
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,16 +90,23 @@ function report(deps, app, build) {
|
|
|
90
90
|
deps.out.say(detail);
|
|
91
91
|
deps.out.say();
|
|
92
92
|
}
|
|
93
|
-
throw new CliError(
|
|
94
|
-
? "
|
|
95
|
-
:
|
|
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} รายการกับฐานข้อมูลของคุณแล้ว`);
|
|
96
103
|
}
|
|
97
104
|
if (build.status === "unverified") {
|
|
98
|
-
deps.out.say(
|
|
99
|
-
deps.out.say(
|
|
105
|
+
deps.out.say(`อัปเดต ${app.name} แล้ว แต่แอปยังไม่ตอบสนอง`);
|
|
106
|
+
deps.out.say(`รอสักครู่ แล้วเปิด ${app.url}`);
|
|
100
107
|
return;
|
|
101
108
|
}
|
|
102
|
-
deps.out.say(`${app.name}
|
|
109
|
+
deps.out.say(`${app.name} ใช้งานได้แล้วที่ ${app.url}`);
|
|
103
110
|
}
|
|
104
111
|
function tail(log, lines) {
|
|
105
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
|
+
}
|