@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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Mock identities for local development (`npm run dev`).
|
|
2
|
+
//
|
|
3
|
+
// Pick one with DEV_PERSONA in .dev.vars (default "hq-staff"); add your own by
|
|
4
|
+
// appending to DEV_PERSONAS. Each persona is a real Identity shape, so what
|
|
5
|
+
// works against a persona works against a real PPLE ID user in production.
|
|
6
|
+
//
|
|
7
|
+
// This whole module is compiled out of the deployed Worker: it is only reached
|
|
8
|
+
// from the `import.meta.env.DEV` branch in src/auth.ts, which `vite build`
|
|
9
|
+
// eliminates (docs/adr/0011). Nothing here can run in production.
|
|
10
|
+
//
|
|
11
|
+
// The rule your app code must follow (docs/adr/0012): gate authorization on
|
|
12
|
+
// `effective`, use `actor` for audit/display only, and honor
|
|
13
|
+
// `effective.isDelegated`.
|
|
14
|
+
import type { Identity } from "../shared/api";
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_DEV_PERSONA = "hq-staff";
|
|
17
|
+
|
|
18
|
+
export const DEV_PERSONAS: Record<string, Identity> = {
|
|
19
|
+
// Party HQ staff: every province in scope, no special capabilities. Default.
|
|
20
|
+
"hq-staff": {
|
|
21
|
+
actor: { sub: "dev-hq-staff", name: "Local Dev (HQ)", mobile: "0800000001" },
|
|
22
|
+
effective: {
|
|
23
|
+
isDelegated: false,
|
|
24
|
+
activeRole: "hq",
|
|
25
|
+
eligibleRoles: ["hq"],
|
|
26
|
+
provinces: ["all"],
|
|
27
|
+
extraRoles: [],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
// A province coordinator: scoped to a single province.
|
|
32
|
+
"province-coordinator": {
|
|
33
|
+
actor: {
|
|
34
|
+
sub: "dev-province-coordinator",
|
|
35
|
+
name: "Local Dev (จังหวัด)",
|
|
36
|
+
mobile: "0800000002",
|
|
37
|
+
},
|
|
38
|
+
effective: {
|
|
39
|
+
isDelegated: false,
|
|
40
|
+
activeRole: "province",
|
|
41
|
+
eligibleRoles: ["province"],
|
|
42
|
+
provinces: ["เชียงใหม่"],
|
|
43
|
+
extraRoles: ["province_co"],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// Carries the platform:admin extra-role (may work the platform admin queue).
|
|
48
|
+
"platform-admin": {
|
|
49
|
+
actor: {
|
|
50
|
+
sub: "dev-platform-admin",
|
|
51
|
+
name: "Local Dev (Admin)",
|
|
52
|
+
mobile: "0800000003",
|
|
53
|
+
},
|
|
54
|
+
effective: {
|
|
55
|
+
isDelegated: false,
|
|
56
|
+
activeRole: "hq",
|
|
57
|
+
eligibleRoles: ["hq"],
|
|
58
|
+
provinces: ["all"],
|
|
59
|
+
extraRoles: ["platform:admin"],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// A delegate acting for a supervisor: effective.* are the SUPERVISOR's role
|
|
64
|
+
// and scope, actor.* is the delegate human, and isDelegated is true.
|
|
65
|
+
delegate: {
|
|
66
|
+
actor: { sub: "dev-delegate", name: "Local Dev (ผู้รับมอบ)", mobile: "0800000004" },
|
|
67
|
+
effective: {
|
|
68
|
+
isDelegated: true,
|
|
69
|
+
activeRole: "province",
|
|
70
|
+
eligibleRoles: ["province"],
|
|
71
|
+
provinces: ["เชียงใหม่"],
|
|
72
|
+
extraRoles: [],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** Resolve the configured persona; an unknown name warns and falls back to the
|
|
78
|
+
* default so a typo never breaks local dev. */
|
|
79
|
+
export function resolveDevPersona(name: string | undefined): Identity {
|
|
80
|
+
const key = name?.trim() || DEFAULT_DEV_PERSONA;
|
|
81
|
+
const persona = DEV_PERSONAS[key];
|
|
82
|
+
if (persona) return persona;
|
|
83
|
+
console.warn(
|
|
84
|
+
`Unknown DEV_PERSONA "${key}"; using "${DEFAULT_DEV_PERSONA}". ` +
|
|
85
|
+
`Valid personas: ${Object.keys(DEV_PERSONAS).join(", ")}.`,
|
|
86
|
+
);
|
|
87
|
+
return DEV_PERSONAS[DEFAULT_DEV_PERSONA] as Identity;
|
|
88
|
+
}
|
|
@@ -14,6 +14,8 @@ export interface Env {
|
|
|
14
14
|
NOTIFY_API_BASE: string;
|
|
15
15
|
NOTIFY_API_KEY?: string;
|
|
16
16
|
|
|
17
|
-
// Local development
|
|
18
|
-
|
|
17
|
+
// Local development only: which mock persona to sign in as (src/dev-personas.ts).
|
|
18
|
+
// Unset means the default persona; "none" turns the bypass off. Never set in
|
|
19
|
+
// production — the dev bypass is compiled out of the deployed Worker anyway.
|
|
20
|
+
DEV_PERSONA?: string;
|
|
19
21
|
}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// so each concern is easy to find and test.
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
|
7
|
+
import type { Identity } from "../shared/api";
|
|
7
8
|
import { CreateNoteSchema, NotificationSchema } from "../shared/api";
|
|
8
|
-
import { authenticate
|
|
9
|
+
import { authenticate } from "./auth";
|
|
9
10
|
import { addNote, listNotes } from "./db";
|
|
10
11
|
import type { Env } from "./env";
|
|
11
12
|
import { getFile, listFiles, putFile } from "./files";
|
|
@@ -29,12 +30,13 @@ export function createApp(overrides: AppOverrides = {}) {
|
|
|
29
30
|
// Liveness check — no auth, so uptime monitors and CI smoke tests can hit it.
|
|
30
31
|
app.get("/health", (c) => c.json({ ok: true }));
|
|
31
32
|
|
|
32
|
-
// Everything below requires a valid PPLE ID token.
|
|
33
|
+
// Everything below requires a valid PPLE ID token. authenticate() owns the
|
|
34
|
+
// whole decision — including the local dev-persona bypass — so a missing
|
|
35
|
+
// token is just one of the ways it can say 401.
|
|
33
36
|
app.use("*", async (c, next) => {
|
|
34
37
|
const token = (c.req.header("authorization") ?? "")
|
|
35
38
|
.replace(/^Bearer\s+/i, "")
|
|
36
39
|
.trim();
|
|
37
|
-
if (!token) return c.json({ error: "not_authenticated" }, 401);
|
|
38
40
|
const result = await authenticate(c.env, token, fetchImpl);
|
|
39
41
|
if (!result.ok) {
|
|
40
42
|
return c.json(
|
|
@@ -46,10 +48,9 @@ export function createApp(overrides: AppOverrides = {}) {
|
|
|
46
48
|
await next();
|
|
47
49
|
});
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
});
|
|
51
|
+
// The signed-in caller. `actor` is who is clicking; `effective` is what they
|
|
52
|
+
// may do — gate authorization on `effective`, never on `actor`.
|
|
53
|
+
app.get("/me", (c) => c.json(c.get("identity")));
|
|
53
54
|
|
|
54
55
|
// --- D1 demo: a shared notes list ---
|
|
55
56
|
app.get("/notes", async (c) => c.json({ notes: await listNotes(c.env.DB) }));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// The Worker is built by Vite (via @cloudflare/vite-plugin), so `import.meta.env`
|
|
2
|
+
// is available here just as it is in the browser bundle. Vite replaces
|
|
3
|
+
// `import.meta.env.DEV` with a literal: `true` under `vite dev` and `vitest`,
|
|
4
|
+
// `false` in `vite build` — which is what lets the local auth bypass in
|
|
5
|
+
// src/auth.ts be dead-code-eliminated from the deployed Worker.
|
|
6
|
+
interface ImportMetaEnv {
|
|
7
|
+
readonly DEV: boolean;
|
|
8
|
+
readonly PROD: boolean;
|
|
9
|
+
readonly MODE: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ImportMeta {
|
|
13
|
+
readonly env: ImportMetaEnv;
|
|
14
|
+
}
|
|
@@ -2,7 +2,10 @@ import { env } from "cloudflare:test";
|
|
|
2
2
|
import type { Env } from "../src/env";
|
|
3
3
|
import { createApp } from "../src/index";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// A placeholder bearer. In tests `import.meta.env.DEV` is true, so the Worker
|
|
6
|
+
// signs in as the default persona and ignores the token — the value only
|
|
7
|
+
// matters on the real PPLE ID path, where tests pass an explicit one.
|
|
8
|
+
export const DEV_TOKEN = "dev";
|
|
6
9
|
|
|
7
10
|
export interface Fakes {
|
|
8
11
|
/** Handles GET {SSO_BASE_URL}/oidc/v1/userinfo */
|
|
@@ -39,7 +42,8 @@ export function makeHarness(fakes: Fakes = {}, envOverrides: Partial<Env> = {})
|
|
|
39
42
|
...env,
|
|
40
43
|
SSO_BASE_URL: "https://sso.test",
|
|
41
44
|
NOTIFY_API_BASE: "",
|
|
42
|
-
|
|
45
|
+
// No DEV_PERSONA ⇒ the default persona. Pass { DEV_PERSONA: "none" } to
|
|
46
|
+
// exercise the real PPLE ID path instead.
|
|
43
47
|
...envOverrides,
|
|
44
48
|
};
|
|
45
49
|
|
|
@@ -11,38 +11,121 @@ describe("health", () => {
|
|
|
11
11
|
});
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
describe("
|
|
15
|
-
it("
|
|
14
|
+
describe("local dev personas", () => {
|
|
15
|
+
it("signs in as the default persona (hq-staff) with no config", async () => {
|
|
16
16
|
const h = makeHarness();
|
|
17
|
-
const res = await h.request("/api/me"
|
|
18
|
-
expect(res.status).toBe(
|
|
19
|
-
expect(await res.json()).toMatchObject({
|
|
17
|
+
const res = await h.request("/api/me");
|
|
18
|
+
expect(res.status).toBe(200);
|
|
19
|
+
expect(await res.json()).toMatchObject({
|
|
20
|
+
actor: { sub: "dev-hq-staff" },
|
|
21
|
+
effective: { activeRole: "hq", provinces: ["all"], isDelegated: false },
|
|
22
|
+
});
|
|
20
23
|
});
|
|
21
24
|
|
|
22
|
-
it("
|
|
23
|
-
const h = makeHarness();
|
|
24
|
-
const res = await h.request("/api/me");
|
|
25
|
+
it("switches persona via DEV_PERSONA, even without a token", async () => {
|
|
26
|
+
const h = makeHarness({}, { DEV_PERSONA: "province-coordinator" });
|
|
27
|
+
const res = await h.request("/api/me", {}, null);
|
|
25
28
|
expect(res.status).toBe(200);
|
|
26
|
-
expect(await res.json()).toMatchObject({
|
|
29
|
+
expect(await res.json()).toMatchObject({
|
|
30
|
+
effective: { activeRole: "province", provinces: ["เชียงใหม่"], extraRoles: ["province_co"] },
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("exposes a delegate persona as effective≠actor", async () => {
|
|
35
|
+
const h = makeHarness({}, { DEV_PERSONA: "delegate" });
|
|
36
|
+
const { actor, effective } = await (await h.request("/api/me")).json<{
|
|
37
|
+
actor: { sub: string };
|
|
38
|
+
effective: { isDelegated: boolean };
|
|
39
|
+
}>();
|
|
40
|
+
expect(actor.sub).toBe("dev-delegate");
|
|
41
|
+
expect(effective.isDelegated).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("falls back to the default persona on an unknown DEV_PERSONA", async () => {
|
|
45
|
+
const h = makeHarness({}, { DEV_PERSONA: "does-not-exist" });
|
|
46
|
+
expect(await (await h.request("/api/me")).json()).toMatchObject({
|
|
47
|
+
actor: { sub: "dev-hq-staff" },
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("real PPLE ID authentication (DEV_PERSONA=none)", () => {
|
|
53
|
+
it("rejects requests without a token", async () => {
|
|
54
|
+
const h = makeHarness({}, { DEV_PERSONA: "none" });
|
|
55
|
+
const res = await h.request("/api/me", {}, null);
|
|
56
|
+
expect(res.status).toBe(401);
|
|
57
|
+
expect(await res.json()).toMatchObject({ error: "not_authenticated" });
|
|
27
58
|
});
|
|
28
59
|
|
|
29
60
|
it("validates real tokens against PPLE ID userinfo", async () => {
|
|
30
|
-
// No dev bypass here; an unknown token must be checked with PPLE ID.
|
|
31
61
|
const h = makeHarness(
|
|
32
62
|
{ sso: () => Response.json({ error: "invalid_token" }, { status: 401 }) },
|
|
33
|
-
{
|
|
63
|
+
{ DEV_PERSONA: "none" },
|
|
34
64
|
);
|
|
35
65
|
const res = await h.request("/api/me", {}, "some-real-looking-token");
|
|
36
66
|
expect(res.status).toBe(401);
|
|
37
67
|
});
|
|
38
68
|
|
|
39
|
-
it("resolves
|
|
69
|
+
it("resolves an effective/actor Identity from a valid token", async () => {
|
|
40
70
|
const h = makeHarness(
|
|
41
|
-
{
|
|
42
|
-
|
|
71
|
+
{
|
|
72
|
+
sso: () =>
|
|
73
|
+
Response.json({
|
|
74
|
+
sub: "u-42",
|
|
75
|
+
name: "Somchai",
|
|
76
|
+
ad: {
|
|
77
|
+
user: {
|
|
78
|
+
id: 7,
|
|
79
|
+
role: "hq",
|
|
80
|
+
mobile: "0810000007",
|
|
81
|
+
metadata: { ad: { extra_roles: ["exco"] } },
|
|
82
|
+
},
|
|
83
|
+
actualPpleUser: { id: 7, role: "hq", mobile: "0810000007" },
|
|
84
|
+
activeRole: "hq",
|
|
85
|
+
eligibleRoles: ["hq"],
|
|
86
|
+
eligibleProvinces: ["all"],
|
|
87
|
+
},
|
|
88
|
+
}),
|
|
89
|
+
},
|
|
90
|
+
{ DEV_PERSONA: "none" },
|
|
43
91
|
);
|
|
44
92
|
const res = await h.request("/api/me", {}, "valid-token");
|
|
45
|
-
expect(await res.json()).toMatchObject({
|
|
93
|
+
expect(await res.json()).toMatchObject({
|
|
94
|
+
actor: { sub: "u-42", name: "Somchai", mobile: "0810000007" },
|
|
95
|
+
effective: {
|
|
96
|
+
isDelegated: false,
|
|
97
|
+
activeRole: "hq",
|
|
98
|
+
provinces: ["all"],
|
|
99
|
+
extraRoles: ["exco"],
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("keeps actor (the delegate) distinct from effective (the supervisor)", async () => {
|
|
105
|
+
const h = makeHarness(
|
|
106
|
+
{
|
|
107
|
+
sso: () =>
|
|
108
|
+
Response.json({
|
|
109
|
+
sub: "u-delegate",
|
|
110
|
+
name: "Nok",
|
|
111
|
+
ad: {
|
|
112
|
+
// The supervisor whose authority is in effect…
|
|
113
|
+
user: { id: 9, role: "province", mobile: "0810000009" },
|
|
114
|
+
// …while the real human acting is the delegate.
|
|
115
|
+
actualPpleUser: { id: 14, role: "delegate", mobile: "0810000014" },
|
|
116
|
+
activeRole: "province",
|
|
117
|
+
eligibleRoles: ["province"],
|
|
118
|
+
eligibleProvinces: ["เชียงใหม่"],
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
121
|
+
},
|
|
122
|
+
{ DEV_PERSONA: "none" },
|
|
123
|
+
);
|
|
124
|
+
const res = await h.request("/api/me", {}, "valid-token");
|
|
125
|
+
expect(await res.json()).toMatchObject({
|
|
126
|
+
actor: { sub: "u-delegate", mobile: "0810000014" },
|
|
127
|
+
effective: { isDelegated: true, activeRole: "province", provinces: ["เชียงใหม่"] },
|
|
128
|
+
});
|
|
46
129
|
});
|
|
47
130
|
});
|
|
48
131
|
|
|
@@ -42,7 +42,7 @@ export function App() {
|
|
|
42
42
|
<header className="appbar">
|
|
43
43
|
<Logo className="appbar__logo" title="People's Party" />
|
|
44
44
|
<span className="appbar__title">PPLE App</span>
|
|
45
|
-
{me && <Badge>{me.name ?? me.sub}</Badge>}
|
|
45
|
+
{me && <Badge>{me.actor.name ?? me.actor.sub}</Badge>}
|
|
46
46
|
</header>
|
|
47
47
|
|
|
48
48
|
<div className="stack">
|
|
@@ -35,8 +35,8 @@ function ErrorScreen({ message }: { message: string }) {
|
|
|
35
35
|
|
|
36
36
|
const root = createRoot(document.getElementById("root")!);
|
|
37
37
|
|
|
38
|
-
// Establish the session (PPLE Today token, browser PPLE ID login, or local
|
|
39
|
-
//
|
|
38
|
+
// Establish the session (PPLE Today token, browser PPLE ID login, or a local
|
|
39
|
+
// dev persona) before rendering, so the first paint is the right screen.
|
|
40
40
|
bootstrap().then((result) => {
|
|
41
41
|
root.render(
|
|
42
42
|
<StrictMode>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// The single place the app talks to PPLE Today and to its own API.
|
|
2
2
|
//
|
|
3
3
|
// Three ways the app can start:
|
|
4
|
-
// 1. Local dev (`vite dev`) —
|
|
5
|
-
//
|
|
4
|
+
// 1. Local dev (`vite dev`) — no login. The Worker signs you in as a mock
|
|
5
|
+
// persona (DEV_PERSONA); we just attach a placeholder bearer it ignores.
|
|
6
6
|
// 2. Inside PPLE Today — the SDK captures the access token from the launch
|
|
7
7
|
// URL; we attach it to every API call.
|
|
8
8
|
// 3. A plain browser — the same SDK runs the PPLE ID OIDC sign-in (redirect
|
|
@@ -21,8 +21,9 @@ const OAUTH_CLIENT_ID = import.meta.env.VITE_OAUTH_CLIENT_ID ?? "";
|
|
|
21
21
|
const OAUTH_REDIRECT_URI =
|
|
22
22
|
import.meta.env.VITE_OAUTH_REDIRECT_URI ?? window.location.origin;
|
|
23
23
|
|
|
24
|
-
//
|
|
25
|
-
|
|
24
|
+
// In local dev the Worker ignores the token entirely (it signs you in as a
|
|
25
|
+
// mock persona), but api() still needs a non-null bearer to attach.
|
|
26
|
+
const DEV_PLACEHOLDER_TOKEN = "dev";
|
|
26
27
|
|
|
27
28
|
export type Bootstrap =
|
|
28
29
|
| { kind: "signed-in" }
|
|
@@ -34,7 +35,7 @@ let token: string | null = null;
|
|
|
34
35
|
export async function bootstrap(): Promise<Bootstrap> {
|
|
35
36
|
try {
|
|
36
37
|
if (import.meta.env.DEV) {
|
|
37
|
-
token =
|
|
38
|
+
token = DEV_PLACEHOLDER_TOKEN;
|
|
38
39
|
return { kind: "signed-in" };
|
|
39
40
|
}
|
|
40
41
|
|
package/dist/api.js
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
// The Provisioner API as the CLI sees it. Every non-2xx answer becomes a
|
|
2
|
-
// sentence a Builder can act on — this is the only place in the CLI that
|
|
3
|
-
// knows the platform's error codes.
|
|
4
|
-
import { CliError, SIGN_IN_EXPIRED } from "./errors.js";
|
|
5
|
-
export class ProvisionerClient {
|
|
6
|
-
deps;
|
|
7
|
-
token;
|
|
8
|
-
constructor(deps, token) {
|
|
9
|
-
this.deps = deps;
|
|
10
|
-
this.token = token;
|
|
11
|
-
}
|
|
12
|
-
me() {
|
|
13
|
-
return this.request("GET", "/me");
|
|
14
|
-
}
|
|
15
|
-
app(slug) {
|
|
16
|
-
return this.request("GET", `/apps/${encodeURIComponent(slug)}`, { slug });
|
|
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
|
-
}
|
|
22
|
-
repoToken(slug) {
|
|
23
|
-
return this.request("POST", `/apps/${encodeURIComponent(slug)}/repo-token`, { slug });
|
|
24
|
-
}
|
|
25
|
-
deploys(slug) {
|
|
26
|
-
return this.request("GET", `/apps/${encodeURIComponent(slug)}/deploys`, { slug }).then((r) => r.deploys);
|
|
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
|
-
}
|
|
49
|
-
async request(method, path, context = {}) {
|
|
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
|
-
}
|
|
59
|
-
const res = await this.deps
|
|
60
|
-
.fetch(url, { method, headers, body })
|
|
61
|
-
.catch(() => null);
|
|
62
|
-
if (!res) {
|
|
63
|
-
throw new CliError("เชื่อมต่อ PPLE Platform ไม่ได้", "ตรวจสอบการเชื่อมต่ออินเทอร์เน็ตแล้วลองใหม่อีกครั้ง");
|
|
64
|
-
}
|
|
65
|
-
if (res.ok) {
|
|
66
|
-
if (res.status === 204)
|
|
67
|
-
return undefined;
|
|
68
|
-
return (await res.json());
|
|
69
|
-
}
|
|
70
|
-
const errBody = (await res.json().catch(() => ({})));
|
|
71
|
-
throw platformError(res.status, errBody.error, context);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
function platformError(status, code, context = {}) {
|
|
75
|
-
const { slug, envKey } = context;
|
|
76
|
-
if (status === 401)
|
|
77
|
-
return SIGN_IN_EXPIRED;
|
|
78
|
-
if (code === "not_org_member") {
|
|
79
|
-
return new CliError("PPLE ID ของคุณยังไม่ได้อยู่ในองค์กรของพรรค จึงสร้างแอปไม่ได้", "กรุณาติดต่อหัวหน้าทีมของคุณเพื่อเพิ่มสิทธิ์ แล้วลองใหม่อีกครั้ง");
|
|
80
|
-
}
|
|
81
|
-
if (code === "agreement_required") {
|
|
82
|
-
return new CliError("คุณยังไม่ได้ยอมรับข้อตกลงผู้พัฒนา", "ยอมรับได้ที่ https://console.ppleth.ai แล้วรันคำสั่งนี้อีกครั้ง");
|
|
83
|
-
}
|
|
84
|
-
if (code === "app_has_no_repo") {
|
|
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");
|
|
95
|
-
}
|
|
96
|
-
if (status === 404) {
|
|
97
|
-
if (envKey) {
|
|
98
|
-
return new CliError(`แอปนี้ไม่ได้ตั้งค่า "${envKey}" ไว้`, "รัน `pple env list` เพื่อดูค่าที่ตั้งไว้");
|
|
99
|
-
}
|
|
100
|
-
return new CliError(slug
|
|
101
|
-
? `ไม่มีแอปชื่อ "${slug}" ที่เป็นของคุณ`
|
|
102
|
-
: "ไม่พบสิ่งนี้บนแพลตฟอร์ม", "ตรวจสอบรายการแอปได้ที่ https://console.ppleth.ai");
|
|
103
|
-
}
|
|
104
|
-
if (status >= 500 || code === "sso_unavailable") {
|
|
105
|
-
return new CliError("PPLE Platform กำลังมีปัญหาในขณะนี้", "รอสักครู่แล้วลองใหม่อีกครั้ง หากยังเกิดขึ้นอีก กรุณาแจ้งทีมแพลตฟอร์ม");
|
|
106
|
-
}
|
|
107
|
-
return new CliError("PPLE Platform ปฏิเสธคำขอนี้", code ? `ข้อความจากระบบ: ${code}` : undefined);
|
|
108
|
-
}
|
package/dist/cli.js
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import * as auth from "./commands/auth.js";
|
|
2
|
-
import { deploy } from "./commands/deploy.js";
|
|
3
|
-
import * as env from "./commands/env.js";
|
|
4
|
-
import { info } from "./commands/info.js";
|
|
5
|
-
import { init } from "./commands/init.js";
|
|
6
|
-
import { upgrade } from "./commands/upgrade.js";
|
|
7
|
-
import { CliError } from "./errors.js";
|
|
8
|
-
import { checkForUpdate, wantsUpdateCheck } from "./update.js";
|
|
9
|
-
const USAGE = `pple — สร้างและส่งแอปขึ้น PPLE Platform
|
|
10
|
-
|
|
11
|
-
pple auth login เข้าสู่ระบบด้วย PPLE ID ของคุณ
|
|
12
|
-
pple auth logout ลบการเข้าสู่ระบบออกจากเครื่องนี้
|
|
13
|
-
pple whoami ดูว่ากำลังเข้าสู่ระบบด้วยบัญชีใด
|
|
14
|
-
|
|
15
|
-
pple init สร้างแอปและตั้งค่าโค้ดบนเครื่องนี้
|
|
16
|
-
pple init --name "App" สร้างแอปโดยไม่ต้องถาม (สำหรับ agent/สคริปต์)
|
|
17
|
-
pple init <app> ตั้งค่าแอปที่มีอยู่แล้วบนแพลตฟอร์ม
|
|
18
|
-
pple deploy [-m "note"] ส่งการเปลี่ยนแปลงของคุณขึ้นใช้งานจริง
|
|
19
|
-
pple info [<slug>] ดูสถานะ การใช้งาน การดีพลอย และคีย์ env ของแอปนี้
|
|
20
|
-
|
|
21
|
-
pple env list ดูตัวแปรและ secret ของแอปนี้
|
|
22
|
-
pple env set <KEY> [VAL] ตั้งค่าตัวแปร (หรือ --secret) รับค่าจาก arg, --file หรือ stdin
|
|
23
|
-
pple env delete <KEY> ลบตัวแปรหรือ secret
|
|
24
|
-
|
|
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 แสดงวิธีใช้นี้
|
|
34
|
-
`;
|
|
35
|
-
/**
|
|
36
|
-
* Parse and run one command line. Returns the process exit code; expected
|
|
37
|
-
* failures come back as 1 with a Builder-language message already printed,
|
|
38
|
-
* and anything else throws for the entry point to report as a CLI bug.
|
|
39
|
-
*/
|
|
40
|
-
export async function run(argv, deps) {
|
|
41
|
-
try {
|
|
42
|
-
// Before the command, never during it: an upgrade mid-deploy would swap
|
|
43
|
-
// the CLI under its own feet, and this is the one point where handing the
|
|
44
|
-
// whole command line to the new version costs nothing.
|
|
45
|
-
if (wantsUpdateCheck(argv)) {
|
|
46
|
-
const handled = await checkForUpdate(deps, argv);
|
|
47
|
-
if (handled !== null)
|
|
48
|
-
return handled;
|
|
49
|
-
}
|
|
50
|
-
await dispatch(argv, deps);
|
|
51
|
-
return 0;
|
|
52
|
-
}
|
|
53
|
-
catch (e) {
|
|
54
|
-
if (e instanceof CliError) {
|
|
55
|
-
deps.out.fail(e);
|
|
56
|
-
return 1;
|
|
57
|
-
}
|
|
58
|
-
throw e;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
async function dispatch(argv, deps) {
|
|
62
|
-
const args = [...argv];
|
|
63
|
-
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
64
|
-
deps.out.say(USAGE);
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
const command = args.shift();
|
|
68
|
-
switch (command) {
|
|
69
|
-
case "auth": {
|
|
70
|
-
const sub = args.shift();
|
|
71
|
-
if (sub === "login")
|
|
72
|
-
return auth.login(deps);
|
|
73
|
-
if (sub === "logout")
|
|
74
|
-
return auth.logout(deps);
|
|
75
|
-
if (sub === "whoami")
|
|
76
|
-
return auth.whoami(deps);
|
|
77
|
-
throw unknown(`auth ${sub ?? ""}`.trim());
|
|
78
|
-
}
|
|
79
|
-
case "whoami":
|
|
80
|
-
return auth.whoami(deps);
|
|
81
|
-
case "init": {
|
|
82
|
-
const dir = takeOption(args, "--dir");
|
|
83
|
-
const name = takeOption(args, "--name");
|
|
84
|
-
const slug = args.shift();
|
|
85
|
-
if (name !== undefined && slug) {
|
|
86
|
-
throw new CliError("`pple init` รับได้เฉพาะชื่อแอปใหม่ หรือแอปที่มีอยู่แล้ว อย่างใดอย่างหนึ่งเท่านั้น", 'ใช้ `pple init --name "ชื่อแอปของคุณ"` เพื่อสร้างใหม่ หรือ `pple init <app>` สำหรับแอปที่มีอยู่แล้ว');
|
|
87
|
-
}
|
|
88
|
-
return init(deps, { slug, name, dir });
|
|
89
|
-
}
|
|
90
|
-
case "deploy": {
|
|
91
|
-
const message = takeOption(args, "--message", "-m");
|
|
92
|
-
return deploy(deps, { message });
|
|
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
|
-
}
|
|
121
|
-
case "upgrade":
|
|
122
|
-
return upgrade(deps);
|
|
123
|
-
case "help":
|
|
124
|
-
deps.out.say(USAGE);
|
|
125
|
-
return;
|
|
126
|
-
default:
|
|
127
|
-
throw unknown(command ?? "");
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
function unknown(command) {
|
|
131
|
-
return new CliError(`ไม่มีคำสั่ง \`pple ${command}\``, "รัน `pple --help` เพื่อดูคำสั่งทั้งหมด");
|
|
132
|
-
}
|
|
133
|
-
/** Pull `--name value` out of the argument list, wherever it appears. */
|
|
134
|
-
function takeOption(args, long, short) {
|
|
135
|
-
const index = args.findIndex((arg) => arg === long || (short && arg === short));
|
|
136
|
-
if (index === -1)
|
|
137
|
-
return undefined;
|
|
138
|
-
const [, value] = args.splice(index, 2);
|
|
139
|
-
if (value === undefined || value.startsWith("-")) {
|
|
140
|
-
throw new CliError(`\`${long}\` ต้องระบุค่าตามหลัง`);
|
|
141
|
-
}
|
|
142
|
-
return value;
|
|
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
|
-
}
|