@pplethai/cli 0.5.1 → 0.7.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.
@@ -0,0 +1,256 @@
1
+ // KYC Status on the actor, and getLegalIdentity (issue #216). Every refusal
2
+ // the helper can return is named here. The offline path never yields a
3
+ // citizen ID that would pass the Thai checksum.
4
+
5
+ import { describe, expect, it, vi } from "vitest";
6
+ import { DEV_PERSONAS } from "../src/dev-personas";
7
+ import {
8
+ getLegalIdentity,
9
+ kycStartUrl,
10
+ OFFLINE_INVALID_ID,
11
+ OFFLINE_LEGAL_NAME,
12
+ thaiIdChecksumValid,
13
+ type LegalIdentityReason,
14
+ } from "../src/legal-identity";
15
+ import { makeHarness } from "./harness";
16
+
17
+ describe("actor.kyc", () => {
18
+ it("gives every Persona a KYC Status, and members are verified via membership", () => {
19
+ for (const persona of Object.values(DEV_PERSONAS)) {
20
+ expect(persona.actor.kyc).toEqual(
21
+ expect.objectContaining({
22
+ status: expect.any(String),
23
+ verified: expect.any(Boolean),
24
+ }),
25
+ );
26
+ expect("kyc" in persona.effective).toBe(false);
27
+ }
28
+ expect(DEV_PERSONAS["hq-staff"]!.actor.kyc).toMatchObject({
29
+ status: "verified",
30
+ verified: true,
31
+ source: "membership",
32
+ });
33
+ expect(DEV_PERSONAS.unverified!.actor.kyc.status).toBe("none");
34
+ expect(DEV_PERSONAS["kyc-pending"]!.actor.kyc.status).toBe("pending_review");
35
+ expect(DEV_PERSONAS["kyc-verified-nonmember"]!.actor.kyc).toMatchObject({
36
+ status: "verified",
37
+ source: "kyc",
38
+ });
39
+ });
40
+
41
+ it("keeps the delegate's KYC Status, not the supervisor's", async () => {
42
+ const delegate = makeHarness({}, { DEV_PERSONA: "delegate" });
43
+ const supervisor = makeHarness({}, { DEV_PERSONA: "province-coordinator" });
44
+ const delegateMe = await (await delegate.request("/api/me")).json<{
45
+ actor: { kyc: { verifiedAt: string; source: string } };
46
+ effective: Record<string, unknown>;
47
+ }>();
48
+ const supervisorMe = await (await supervisor.request("/api/me")).json<{
49
+ actor: { kyc: { verifiedAt: string } };
50
+ }>();
51
+ expect(delegateMe.actor.kyc.verifiedAt).toBe("2024-04-01T00:00:00.000Z");
52
+ expect(delegateMe.actor.kyc.verifiedAt).not.toBe(supervisorMe.actor.kyc.verifiedAt);
53
+ expect(delegateMe.actor.kyc.source).toBe("membership");
54
+ expect(delegateMe.effective.kyc).toBeUndefined();
55
+ });
56
+
57
+ it("parses userinfo kyc onto the actor only, including a delegated session", async () => {
58
+ const h = makeHarness(
59
+ {
60
+ sso: () =>
61
+ Response.json({
62
+ sub: "u-delegate",
63
+ name: "Nok",
64
+ kyc: {
65
+ status: "verified",
66
+ verified: true,
67
+ verifiedAt: "2024-06-01T00:00:00.000Z",
68
+ source: "kyc",
69
+ },
70
+ ad: {
71
+ user: { id: 9, role: "province", mobile: "0810000009" },
72
+ actualPpleUser: { id: 14, role: "delegate", mobile: "0810000014" },
73
+ activeRole: "province",
74
+ eligibleRoles: ["province"],
75
+ eligibleProvinces: ["เชียงใหม่"],
76
+ },
77
+ }),
78
+ },
79
+ { DEV_PERSONA: "none" },
80
+ );
81
+ const me = await (await h.request("/api/me", {}, "valid-token")).json<{
82
+ actor: { kyc: { source: string; verifiedAt: string } };
83
+ effective: Record<string, unknown>;
84
+ }>();
85
+ expect(me.actor.kyc).toEqual({
86
+ status: "verified",
87
+ verified: true,
88
+ verifiedAt: "2024-06-01T00:00:00.000Z",
89
+ source: "kyc",
90
+ });
91
+ expect(me.effective.kyc).toBeUndefined();
92
+ });
93
+
94
+ it("defaults a userinfo payload with no kyc to none", async () => {
95
+ const h = makeHarness(
96
+ {
97
+ sso: () =>
98
+ Response.json({
99
+ sub: "u-42",
100
+ name: "Somchai",
101
+ ad: {
102
+ user: { id: 7, role: "hq", mobile: "0810000007" },
103
+ actualPpleUser: { id: 7, role: "hq", mobile: "0810000007" },
104
+ activeRole: "hq",
105
+ eligibleRoles: ["hq"],
106
+ eligibleProvinces: ["all"],
107
+ },
108
+ }),
109
+ },
110
+ { DEV_PERSONA: "none" },
111
+ );
112
+ const me = await (await h.request("/api/me", {}, "valid-token")).json<{
113
+ actor: { kyc: { status: string; verified: boolean; source: null } };
114
+ }>();
115
+ expect(me.actor.kyc).toEqual({
116
+ status: "none",
117
+ verified: false,
118
+ verifiedAt: null,
119
+ source: null,
120
+ });
121
+ });
122
+ });
123
+
124
+ describe("getLegalIdentity offline", () => {
125
+ it("never returns a checksum-valid citizen ID", async () => {
126
+ expect(thaiIdChecksumValid(OFFLINE_INVALID_ID)).toBe(false);
127
+ // A known-valid check digit for the same first 12 digits is 8.
128
+ expect(thaiIdChecksumValid("1100000000008")).toBe(true);
129
+
130
+ for (const name of Object.keys(DEV_PERSONAS)) {
131
+ const result = await getLegalIdentity({ DEV_PERSONA: name }, "ignored");
132
+ if (!result.ok) {
133
+ expect(result.reason).toBe("not_verified");
134
+ continue;
135
+ }
136
+ expect(result.legalIdentity.name).toBe(OFFLINE_LEGAL_NAME);
137
+ if (result.legalIdentity.idCardNumber !== null) {
138
+ expect(thaiIdChecksumValid(result.legalIdentity.idCardNumber)).toBe(false);
139
+ }
140
+ }
141
+
142
+ const member = await getLegalIdentity({ DEV_PERSONA: "hq-staff" }, "t");
143
+ expect(member).toEqual({
144
+ ok: true,
145
+ legalIdentity: { source: "membership", name: OFFLINE_LEGAL_NAME, idCardNumber: null },
146
+ });
147
+ const card = await getLegalIdentity({ DEV_PERSONA: "kyc-verified-nonmember" }, "t");
148
+ expect(card).toEqual({
149
+ ok: true,
150
+ legalIdentity: { source: "kyc", name: OFFLINE_LEGAL_NAME, idCardNumber: OFFLINE_INVALID_ID },
151
+ });
152
+ expect(await getLegalIdentity({ DEV_PERSONA: "unverified" }, "t")).toEqual({
153
+ ok: false,
154
+ reason: "not_verified",
155
+ });
156
+ expect(await getLegalIdentity({ DEV_PERSONA: "kyc-pending" }, "t")).toEqual({
157
+ ok: false,
158
+ reason: "not_verified",
159
+ });
160
+ });
161
+ });
162
+
163
+ describe("getLegalIdentity against Platform Services", () => {
164
+ const env = {
165
+ DEV_PERSONA: "none",
166
+ PPLE_SERVICES_URL: "https://services.test",
167
+ PPLE_APP_KEY: "app-key",
168
+ };
169
+
170
+ it("is not_configured until both bindings exist, and does not call out", async () => {
171
+ const fetchImpl = vi.fn<typeof fetch>();
172
+ expect(await getLegalIdentity({ DEV_PERSONA: "none" }, "tok", fetchImpl)).toEqual({
173
+ ok: false,
174
+ reason: "not_configured",
175
+ });
176
+ expect(
177
+ await getLegalIdentity({ DEV_PERSONA: "none", PPLE_SERVICES_URL: "https://services.test" }, "tok", fetchImpl),
178
+ ).toEqual({ ok: false, reason: "not_configured" });
179
+ expect(fetchImpl).not.toHaveBeenCalled();
180
+ });
181
+
182
+ it("names every error the service can return", async () => {
183
+ const reasons: LegalIdentityReason[] = [
184
+ "invalid_app_key",
185
+ "not_granted",
186
+ "not_verified",
187
+ "user_token_invalid",
188
+ "identity_not_enabled",
189
+ "upstream_unavailable",
190
+ ];
191
+ for (const reason of reasons) {
192
+ const status = reason === "invalid_app_key" || reason === "user_token_invalid" ? 401 : 403;
193
+ const fetchImpl = vi.fn<typeof fetch>(async () => Response.json({ error: reason }, { status }));
194
+ expect(await getLegalIdentity(env, "user-token", fetchImpl)).toEqual({ ok: false, reason });
195
+ const [url, init] = fetchImpl.mock.calls[0]!;
196
+ expect(url).toBe("https://services.test/legal-identity");
197
+ expect(new Headers(init?.headers).get("authorization")).toBe("Bearer app-key");
198
+ expect(JSON.parse(String(init?.body))).toEqual({ token: "user-token" });
199
+ }
200
+ });
201
+
202
+ it("returns the Legal Identity the service answered with", async () => {
203
+ const fetchImpl = vi.fn<typeof fetch>(async () =>
204
+ Response.json({ source: "kyc", name: "นาย ก", idCardNumber: "1100000000008" }),
205
+ );
206
+ expect(await getLegalIdentity(env, "user-token", fetchImpl)).toEqual({
207
+ ok: true,
208
+ legalIdentity: { source: "kyc", name: "นาย ก", idCardNumber: "1100000000008" },
209
+ });
210
+ });
211
+
212
+ it("treats a transport failure and a body-less error as upstream_unavailable", async () => {
213
+ const down = vi.fn<typeof fetch>(async () => {
214
+ throw new Error("network");
215
+ });
216
+ expect(await getLegalIdentity(env, "user-token", down)).toEqual({
217
+ ok: false,
218
+ reason: "upstream_unavailable",
219
+ });
220
+
221
+ const odd = vi.fn<typeof fetch>(async () => new Response("nope", { status: 502 }));
222
+ expect(await getLegalIdentity(env, "user-token", odd)).toEqual({
223
+ ok: false,
224
+ reason: "upstream_unavailable",
225
+ });
226
+ });
227
+ });
228
+
229
+ describe("local KYC redirect", () => {
230
+ it("sends a local click straight back with ?kyc=cancelled", () => {
231
+ const back = kycStartUrl({
232
+ ssoBaseUrl: "https://id.example",
233
+ clientId: "zitadel-app",
234
+ returnTo: "https://app.example/checkout?step=1#pay",
235
+ dev: true,
236
+ });
237
+ const url = new URL(back);
238
+ expect(url.origin + url.pathname).toBe("https://app.example/checkout");
239
+ expect(url.searchParams.get("step")).toBe("1");
240
+ expect(url.searchParams.get("kyc")).toBe("cancelled");
241
+ expect(url.hash).toBe("#pay");
242
+ });
243
+
244
+ it("points production at PPLE ID /kyc", () => {
245
+ expect(
246
+ kycStartUrl({
247
+ ssoBaseUrl: "https://id.example",
248
+ clientId: "zitadel-app",
249
+ returnTo: "https://app.example/checkout",
250
+ dev: false,
251
+ }),
252
+ ).toBe(
253
+ "https://id.example/kyc?client_id=zitadel-app&return_to=https%3A%2F%2Fapp.example%2Fcheckout",
254
+ );
255
+ });
256
+ });
@@ -20,7 +20,8 @@
20
20
 
21
21
  // The React SPA (built to dist/client by Vite) is served for any request
22
22
  // that isn't worker-first. "/api/*" is the app's API; "/.pple/*" is reserved
23
- // for the platform deploy probe. "single-page-application" makes every
23
+ // for the platform (the deploy probe and scheduled jobs) — don't add routes
24
+ // under it. "single-page-application" makes every
24
25
  // other path return index.html so client-side routing works.
25
26
  "assets": {
26
27
  "not_found_handling": "single-page-application",
@@ -46,6 +47,18 @@
46
47
  }
47
48
  ],
48
49
 
50
+ // Scheduled jobs (Cron Schedules, "งานตั้งเวลา"). Declare when the app runs
51
+ // its own code, in UTC (Bangkok is UTC+7: 08:00 in Bangkok is "0 1 * * *"),
52
+ // and export `scheduled()` beside `fetch` in src/index.ts. The platform fires
53
+ // them, not Cloudflare, so only standard five-field cron works (lists,
54
+ // ranges, steps, JAN-DEC months, and weekdays as NAMES only — MON-FRI, never
55
+ // digits, which count differently from Cloudflare's docs), at most 3 schedules per app and
56
+ // no closer than 5 minutes. Delivery is at-most-once: no catch-up and no
57
+ // retry, so write jobs that are safe to repeat. Deploying is the only way to
58
+ // change this list; `pple cron` shows, pauses and runs them. Uncomment to use:
59
+ //
60
+ // "triggers": { "crons": ["0 1 * * *"] },
61
+
49
62
  "vars": {
50
63
  // PPLE ID (identity). The Worker validates bearer tokens against this.
51
64
  "SSO_BASE_URL": "https://id.peoplesparty.or.th",
@@ -1,114 +0,0 @@
1
- ---
2
- name: pple-platform
3
- description: >
4
- Build Builder Apps on the PPLE Platform — the People's Party (พรรคประชาชน)
5
- digital infrastructure: PPLE ID single sign-on, PPLE Today mini-apps and
6
- audience-bound push notifications, the @pplethai/components design system,
7
- and managed Cloudflare Workers + D1 + R2 hosting via the pple CLI. Use this
8
- skill whenever a task mentions PPLE, PPLE ID, PPLE Today, mini-app,
9
- พรรคประชาชน, People's Party, @pplethai packages, Builder App, party
10
- internal tools, or when scaffolding, authenticating, styling, notifying, or
11
- deploying any app in the PPLE ecosystem — even if the user doesn't name the
12
- platform explicitly.
13
- ---
14
-
15
- # Building on the PPLE Platform
16
-
17
- The PPLE Platform lets party workforce members (Builders) ship mini-apps
18
- without holding cloud or GitHub credentials. Every new **Builder App** plugs
19
- into three pillars on party-managed infrastructure:
20
-
21
- 1. **PPLE ID** — OIDC single sign-on. End users authenticate via the mini-app
22
- SDK (launch token inside **PPLE Today**, or the SDK's PPLE ID browser
23
- login); the Worker validates the bearer token against the OIDC `userinfo`
24
- endpoint. Builders also use PPLE ID for the Console (browser login) and
25
- `pple auth login` (device flow).
26
- 2. **Mini-app integration** — apps run inside PPLE Today's WebView using
27
- `@pplethai/pple-today-miniapp-sdk` (and the same SDK in a plain browser),
28
- and send audience-bound notifications — a **Broadcast** to everyone this
29
- app can reach, or a **Direct** send to people it names — through the
30
- platform-provisioned notify helper.
31
- 3. **PPLE Design System** — `@pplethai/components`, a shadcn/ui-based React
32
- library with party branding. Docs: https://components.pplethai.org/
33
-
34
- ## Standard tech stack (Builder Apps)
35
-
36
- Use this stack for every new Builder App unless the user is working on a
37
- central-team core service — uniformity is deliberate (ADR-0002):
38
-
39
- | Layer | Choice |
40
- | ------------ | ------ |
41
- | Runtime | One Cloudflare Worker serves `/api/*` and the React SPA |
42
- | Frontend | Vite + React 19 + TypeScript + `@pplethai/components` |
43
- | Backend | Hono + TypeScript on the Workers runtime |
44
- | Database | D1 (SQLite) via bound queries + SQL migrations |
45
- | Files | R2 |
46
- | Validation | Zod schemas in `shared/` |
47
- | Auth | PPLE ID (OIDC `userinfo`); local `DEV_AUTH` bypass only |
48
- | Notifications| `src/notify.ts` — every send states its audience (`all` or `direct`) |
49
- | Deploy | `pple deploy` (platform build → tenant Worker); never personal cloud |
50
-
51
- **Not the Builder App path** (do not resurrect these for new apps): Express +
52
- Prisma + PostgreSQL, Docker / Cloud Build / Artifact Registry, or a separate
53
- Cloudflare Pages project for the SPA. Those belong to older reference apps
54
- and central-team services, not the Workers golden path.
55
-
56
- Source of truth for new apps:
57
-
58
- - **`pple-platform/template`** — the golden-path seed `pple init` copies.
59
- Prefer this over obsolete examples (`pple-erp`, `mini-app-example`).
60
-
61
- ## Which reference to read
62
-
63
- Read only the file(s) relevant to the task at hand:
64
-
65
- | Task | Read |
66
- | ---- | ---- |
67
- | Start a new app / local layout & commands | [references/project-scaffold.md](references/project-scaffold.md) |
68
- | Login, tokens, browser PPLE ID flow, DEV_AUTH | [references/pple-id-auth.md](references/pple-id-auth.md) |
69
- | Mini-app SDK behavior, push notifications | [references/mini-app.md](references/mini-app.md) |
70
- | UI, styling, components | [references/design-system.md](references/design-system.md) |
71
- | Create → init → deploy; status / usage (`pple info`) | [references/deployment.md](references/deployment.md) |
72
-
73
- ## Golden path for a new app
74
-
75
- 1. **Sign in on the machine**: `pple auth login` (PPLE ID device flow; no
76
- personal cloud credentials — ADR-0005).
77
- 2. **Create + scaffold** — pick one path (both use the same Provisioner
78
- create; the App Slug is always platform-generated):
79
- - **CLI create**: `pple init` (prompts for App Name) or
80
- `pple init --name "<App Name>"` (non-interactive) — creates the app,
81
- waits until it is ready, then scaffolds and writes the App Link.
82
- - **Console create**: create in the Console (`console.ppleth.ai`), then
83
- `pple init <slug>` to scaffold and link the folder.
84
- 3. **Build** against the template helpers (`auth`, `db`, `files`, `notify`)
85
- and `@pplethai/components` (see the reference files).
86
- 4. **Ship**: `pple deploy` — the platform builds and puts the app live.
87
- 5. **Status / usage**: `pple info` (from a linked folder, or
88
- `pple info <slug>`) — read-only overview, usage vs Resource Limits,
89
- recent deploys, env key list. Invites, Requests, and collaborators stay
90
- Console-only (see [deployment.md](references/deployment.md)).
91
-
92
- ## Invariants — things that are true no matter what you build
93
-
94
- - **Mini-apps authenticate through the mini-app SDK.** Inside PPLE Today the
95
- SDK reads the launch-URL token; in a plain browser it runs the PPLE ID
96
- OIDC login. Do not add a NotInApp guard or a custom browser sign-in page.
97
- Local development uses `DEV_AUTH` in `.dev.vars` only — never provisioned
98
- in production. (Builders separately hold tokens from Console login and
99
- `pple auth login`.)
100
- - **Never mention git, branches, remotes, GitHub, Cloudflare accounts, or
101
- personal deploy credentials** in the happy path. Talk about the app's
102
- code and `pple init` / `pple deploy` only.
103
- - **Notifications are audience-bound.** Every send states its audience:
104
- Broadcast `{ kind: "all" }` or Direct `{ kind: "direct", recipients }`
105
- (by `sub` or `phone`). Naming narrows the reachable set and never widens
106
- it; `not_reachable` is one collapsed status — do not decompose it.
107
- Self-links only (`linkPath` into this app). Shape in `shared/api.ts`;
108
- details in [mini-app.md](references/mini-app.md).
109
- - **Stack is Workers + D1 + R2.** Bind D1 parameters; never concatenate SQL.
110
- One Worker, same origin — no CORS between SPA and API.
111
- - **`DEV_AUTH` must be unset in production.** Its absence is what forces real
112
- PPLE ID validation.
113
- - **Design tokens are CSS variables** from `@pplethai/components/styles.css`.
114
- Prefer design-system components over hardcoding brand colors.
@@ -1,29 +0,0 @@
1
- {
2
- "skill_name": "pple-platform",
3
- "evals": [
4
- {
5
- "id": 1,
6
- "prompt": "I created an app called brave-otter in the Console. Help me get it running locally and understand the project layout so I can add features with my AI agent.",
7
- "expected_output": "pple auth login → pple init brave-otter; single-package Workers template (src/ Hono Worker, web/ React, shared/ Zod, migrations/ D1); npm install, .dev.vars from example, db:migrate:local, npm run dev; DEV_AUTH local bypass; no Express/Prisma/Pages scaffold",
8
- "files": []
9
- },
10
- {
11
- "id": 2,
12
- "prompt": "When a reimbursement is approved, send a push notification to that one person. We already have their PPLE ID sub. Use the PPLE notification service the platform wired up.",
13
- "expected_output": "Use src/notify.ts sendNotification with required audience { kind: \"direct\", recipients: [{ sub }] } (or { phone }); title+body; optional path-only linkPath self-link into this app; naming narrows the reachable set and never widens it; not_reachable is one collapsed status — do not decompose it; never omit audience; never reimplement the wire POST; never free-form / cross-app destinations; never legacy /external/notifications/send with PHONE_NUMBER; Broadcast to everyone this app can reach is { kind: \"all\" }",
14
- "files": []
15
- },
16
- {
17
- "id": 3,
18
- "prompt": "How do I put my changes live? Set up deployment for this Builder App.",
19
- "expected_output": "pple deploy (after pple auth login); managed Workers+D1+R2; no Dockerfile, Cloud Build, Artifact Registry, or separate Cloudflare Pages project; no git/GitHub credential instructions in the happy path",
20
- "files": []
21
- },
22
- {
23
- "id": 4,
24
- "prompt": "The app should work when I open it in Chrome on my laptop. Add a normal browser login with PPLE ID.",
25
- "expected_output": "Use the mini-app SDK's existing browser PPLE ID flow (web/pple.ts bootstrap → sdk.init); do not add a custom OAuth login page or a NotInApp / open-in-Today-only guard; local DEV_AUTH only for vite dev — do not claim tokens only exist inside PPLE Today as a platform-wide absolute (Console/CLI hold Builder tokens; end users can also sign in via the SDK browser flow)",
26
- "files": []
27
- }
28
- ]
29
- }
@@ -1,129 +0,0 @@
1
- # Deployment (managed Workers hosting)
2
-
3
- Builder Apps run only on party-managed Cloudflare infrastructure
4
- (ADR-0001, ADR-0002). Builders never hold cloud or GitHub credentials
5
- (ADR-0005). The happy path uses the Console and the `pple` CLI — not git
6
- vocabulary, Docker, Cloud Build, or personal Cloudflare/Pages projects.
7
-
8
- Ops detail for create, status, and what stays Console-only lives here
9
- (ADR-0006: the Skill owns deeper platform guidance; `CLAUDE.md` stays
10
- short).
11
-
12
- ## Happy path
13
-
14
- ```bash
15
- pple auth login # PPLE ID device flow on this machine
16
-
17
- # Create + scaffold — either path works:
18
- pple init # prompt for App Name → create → wait → scaffold
19
- # or: pple init --name "My App" # non-interactive create (agents/scripts)
20
- # or: create in Console (console.ppleth.ai), then:
21
- # pple init <slug> # scaffold an app that already exists
22
-
23
- # …edit locally with npm run dev…
24
- pple deploy -m "short note" # send changes; platform builds and goes live
25
- pple info # read-only status, usage vs limits, deploys
26
- ```
27
-
28
- What the Builder hears: the app's name and live URL (e.g.
29
- `https://brave-otter.ppleth.ai`). Not branches, remotes, registries, or
30
- source-control jargon.
31
-
32
- | Command | Meaning for the Builder |
33
- | ------- | ----------------------- |
34
- | `pple auth login` | Sign in with PPLE ID |
35
- | `pple init` / `pple init --name "…"` | Create a new app, wait until ready, scaffold + App Link |
36
- | `pple init <slug>` | Set up an existing app's code on this computer |
37
- | `pple deploy` | Send changes and put them live |
38
- | `pple info` / `pple info <slug>` | Read-only status, usage vs Resource Limits, deploys |
39
- | `pple env list` / `set` / `delete` | Manage env vars and secrets (effect on next deploy) |
40
- | `pple whoami` / `pple auth logout` | Check / forget this machine's sign-in |
41
-
42
- ## Creating an app (`pple init`)
43
-
44
- Same Provisioner create as the Console (`POST /apps`). The **App Slug** is
45
- always platform-generated — Builders choose only the **App Name**.
46
-
47
- | Invocation | Behavior |
48
- | ---------- | -------- |
49
- | `pple init` (TTY, no App Link in cwd) | Prompt for App Name → create → poll until ready → scaffold |
50
- | `pple init --name "<App Name>"` | Non-interactive create → poll → scaffold (agents/scripts) |
51
- | `pple init` non-TTY without `--name` or slug | Error clearly — do not hang waiting on stdin |
52
- | `pple init <slug>` | Scaffold an app that already exists (e.g. Console-created) |
53
- | `pple init` inside a folder with an App Link | **Refuse** — already linked; use `pple info` / `pple deploy`, or `cd` elsewhere to create another app |
54
-
55
- After create, the CLI **blocks/polls** until the app is ready to scaffold
56
- (repo present, not stuck in `provisioning`). On timeout it fails loudly and
57
- prints the App Slug so the Builder can retry with `pple init <slug>`. If
58
- scaffold fails after a successful create, the app still exists on the
59
- platform (same as Console-then-init).
60
-
61
- ## Status and usage (`pple info`)
62
-
63
- Read-only terminal mirror of the Console app dashboard for one Builder App.
64
- **No mutations.**
65
-
66
- - Resolve the app from the **App Link** (`.pple/app.json`) when run inside
67
- an app folder, **or** from `pple info <slug>`.
68
- - No App Link and no slug → clear error: `cd` into the app, pass a slug, or
69
- use the Console.
70
- - Prints safe dashboard surfaces:
71
- - Overview (App Name, App Slug, URL, status, Publication Tier, role, …)
72
- - Usage vs Resource Limits — treat `null` usage fields as unavailable,
73
- not zero (same semantics as Console)
74
- - Recent deploys
75
- - Env key list — secrets: key present only, never values
76
- - Owner-only surfaces (publication / collaborators / request history as
77
- the Console shows): include when the caller is Owner; **omit** for
78
- Collaborators (same gates as Console — do not fail the whole command)
79
-
80
- ## Console vs CLI
81
-
82
- | Need | Where |
83
- | ---- | ----- |
84
- | Create app | Console **or** `pple init` / `pple init --name` |
85
- | Scaffold / link locally | `pple init <slug>` (or create path above) |
86
- | Deploy | `pple deploy` |
87
- | Read status / usage / deploys / env keys | `pple info` (or Console) |
88
- | Env vars & secrets | Console **or** `pple env` |
89
- | Beta invites, Publication Tier / PublishRequest | **Console only** |
90
- | File ScopeRequest / LimitRequest / ChangeRequest / RetireRequest | **Console only** |
91
- | Manage Collaborators | **Console only** |
92
- | Dataset catalog / App Scope browsing | **Console only** |
93
- | Platform Admin approval queue | **Console only** |
94
-
95
- Point Builders at the Console (`console.ppleth.ai`) for anything in the
96
- Console-only rows — do not invent CLI flags for those.
97
-
98
- ## What the platform owns
99
-
100
- - **Hosting**: Workers for Platforms dispatch namespace; one tenant Worker per
101
- app serving API + static assets; D1 database; R2 bucket.
102
- - **Identity bindings**: `SSO_BASE_URL`, OAuth client env for the SPA,
103
- audience-bound `NOTIFY_API_BASE` / `NOTIFY_API_KEY`.
104
- - **The app's code**: stored and shipped by the platform. `pple init` /
105
- `pple deploy` authenticate under the hood — the Builder never configures
106
- a source-control account or pastes a deploy token.
107
- - **Config / secrets**: set in the Console or via `pple env`; applied as
108
- Worker bindings on the **next** deploy. Secrets are write-only (set/unset,
109
- never read back).
110
-
111
- ## Local vs production auth
112
-
113
- - Local: `DEV_AUTH` in `.dev.vars` (see [pple-id-auth.md](pple-id-auth.md)).
114
- - Production: `DEV_AUTH` must be absent. The platform never provisions it.
115
-
116
- ## Archived / do-not-use for Builder Apps
117
-
118
- These were the previous golden path. Do not set them up for new Builder Apps:
119
-
120
- - Separate **Cloudflare Pages** project for a Vite SPA + Dockerized Express API
121
- - **Google Cloud Build** / Kaniko / Artifact Registry Docker deploys
122
- - Asking the Builder to sign into GitHub, create a Cloudflare account, or
123
- paste long-lived deploy tokens
124
- - `api/Dockerfile` + `api/cloudbuild.yaml` monorepo layouts copied from
125
- older Express-era apps
126
-
127
- If the user is maintaining a central-team service that still uses those
128
- tools, say so explicitly — that is outside the Builder App path this skill
129
- steers.
@@ -1,55 +0,0 @@
1
- # PPLE Design System (@pplethai/components)
2
-
3
- `@pplethai/components` is the party's React component library, built on
4
- shadcn/ui. Live docs: **https://components.pplethai.org/**
5
-
6
- Builder Apps start with the golden-path template (`pple-platform/template`),
7
- which already imports the library. Prefer editing that wiring over
8
- reinventing a design-system setup from `pple-erp` or other older apps.
9
-
10
- ## Consumer setup (already in the template)
11
-
12
- ```tsx
13
- // web/main.tsx — design-system CSS first, then local layout
14
- import "@pplethai/components/styles.css";
15
- import "./app.css";
16
-
17
- import { Button, Card, CardContent, Navbar } from "@pplethai/components";
18
- ```
19
-
20
- Package entry points:
21
-
22
- | Import | Contents |
23
- | ------ | -------- |
24
- | `@pplethai/components` | UI primitives, layout, brand (`Navbar`, `Logo`, …), `cn()`, toasts |
25
- | `@pplethai/components/form` | react-hook-form helpers when you need forms |
26
- | `@pplethai/components/styles.css` | Brand tokens — import once |
27
-
28
- Vite note (template already pins this): the package's `"development"` export
29
- can point at unpublished source. Keep the template's resolve alias that pins
30
- the bare `@pplethai/components` import to `dist/index.js`.
31
-
32
- ## Layout conventions in Builder Apps
33
-
34
- - Put **page layout** in `web/app.css` (mobile-first, safe-area padding for
35
- the WebView). Do not fight the design-system stylesheet with a second
36
- full Tailwind stack unless you need it.
37
- - Top-level chrome uses Shared `<Navbar>` (see `web/App.tsx`). Do not hand-roll
38
- a header/app bar with `Logo`. With a client router, pass `renderHomeLink` /
39
- `renderLink` (as in pple-erp); otherwise default `<a href>` links are fine.
40
- Navbar is `position: fixed` below the `md` breakpoint — pad the page content
41
- accordingly (the template's `.screen` already does).
42
- - Compose screens from design-system components (`Card`, `Button`, `Input`,
43
- `Textarea`, `Badge`, `Alert`, …) like the demo cards in `web/App.tsx`.
44
- - **Colors and type** come from CSS variables in `styles.css`
45
- (`--primary`, fonts, gradients). Prefer components and those tokens over
46
- hardcoded hex brand colors.
47
- - Typography: headings **Anakotmai**, body **IBM Plex Sans Thai Looped** —
48
- both arrive with `styles.css`.
49
-
50
- ## Filling gaps
51
-
52
- When the library lacks a component, generate a local shadcn/ui piece into
53
- your app and keep tokens aligned — or stick to plain markup + `app.css`.
54
- Prefer `@pplethai/components` whenever an equivalent exists so apps stay
55
- visually consistent.