@pplethai/cli 0.2.2 → 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.
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 +22 -16
  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 +6 -9
  36. package/templates/golden-path/web/vite-env.d.ts +3 -3
  37. package/templates/golden-path/web/NotInApp.tsx +0 -23
package/dist/update.js CHANGED
@@ -27,7 +27,7 @@ export function wantsUpdateCheck(argv) {
27
27
  }
28
28
  /** The one line that opens every "there's a newer pple" conversation. */
29
29
  export function announceUpdate(deps, latest) {
30
- deps.out.say(`There's a newer pple (${latest}). You have ${deps.version}.`);
30
+ deps.out.say(`มี pple เวอร์ชันใหม่ (${latest}) คุณใช้เวอร์ชัน ${deps.version}`);
31
31
  }
32
32
  /**
33
33
  * What to say about an install npm's global root doesn't hold. Installing
@@ -35,13 +35,13 @@ export function announceUpdate(deps, latest) {
35
35
  * one just ran (ADR-0007).
36
36
  */
37
37
  export function sayOtherChannel(deps) {
38
- deps.out.say("This copy of pple didn't come from npm, so updating it here");
39
- deps.out.say("could leave you with two versions. Update it the way you");
40
- deps.out.say(`installed it, or install it with npm:\n\n ${MANUAL_INSTALL}`);
38
+ deps.out.say("pple ชุดนี้ไม่ได้ติดตั้งผ่าน npm การอัปเดตที่นี่อาจทำให้คุณ");
39
+ deps.out.say("มี pple สองเวอร์ชัน กรุณาอัปเดตด้วยวิธีเดิมที่คุณติดตั้งไว้");
40
+ deps.out.say(`หรือติดตั้งด้วย npm:\n\n ${MANUAL_INSTALL}`);
41
41
  }
42
42
  /** Why npm gave up, and the line the Builder can always run themselves. */
43
43
  export function manualHint(result) {
44
- return `${npmMessage(result)}\n\nTry running it yourself:\n\n ${MANUAL_INSTALL}`;
44
+ return `${npmMessage(result)}\n\nลองรันคำสั่งนี้ด้วยตัวเอง:\n\n ${MANUAL_INSTALL}`;
45
45
  }
46
46
  /**
47
47
  * Look for a newer CLI, and act on it as far as this session allows. Returns
@@ -68,18 +68,18 @@ export async function checkForUpdate(deps, argv) {
68
68
  return null;
69
69
  }
70
70
  if (!deps.interactive) {
71
- deps.out.say("Run `pple upgrade` when you're ready.");
71
+ deps.out.say("รัน `pple upgrade` เมื่อคุณพร้อม");
72
72
  deps.out.say();
73
73
  return null;
74
74
  }
75
- if (!(await deps.confirm("Update pple now?"))) {
75
+ if (!(await deps.confirm("อัปเดต pple ตอนนี้เลยไหม?"))) {
76
76
  deps.out.say();
77
77
  return null;
78
78
  }
79
79
  const result = await installGlobally(deps, latest);
80
80
  if (result.code !== 0)
81
81
  return await afterFailedUpgrade(deps, latest, result);
82
- deps.out.say(`pple is now ${latest}. Picking up where you left off…`);
82
+ deps.out.say(`pple เป็นเวอร์ชัน ${latest} แล้ว กำลังทำงานต่อจากที่ค้างไว้…`);
83
83
  deps.out.say();
84
84
  return deps.reexec(argv);
85
85
  }
@@ -91,14 +91,14 @@ export async function checkForUpdate(deps, argv) {
91
91
  * place — so carrying on is a question, not a default.
92
92
  */
93
93
  async function afterFailedUpgrade(deps, latest, result) {
94
- deps.out.say(`Couldn't update pple to ${latest}.`);
94
+ deps.out.say(`อัปเดต pple เป็น ${latest} ไม่สำเร็จ`);
95
95
  deps.out.say(manualHint(result));
96
96
  deps.out.say();
97
- if (await deps.confirm(`Carry on with pple ${deps.version} for now?`)) {
97
+ if (await deps.confirm(`ใช้ pple ${deps.version} ทำงานต่อไปก่อนไหม?`)) {
98
98
  deps.out.say();
99
99
  return null;
100
100
  }
101
- deps.out.say("Stopped. Nothing has changed.");
101
+ deps.out.say("หยุดแล้ว ไม่มีอะไรเปลี่ยนแปลง");
102
102
  return 1;
103
103
  }
104
104
  async function isDue(deps) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pplethai/cli",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,14 +18,15 @@ The PPLE Platform lets party workforce members (Builders) ship mini-apps
18
18
  without holding cloud or GitHub credentials. Every new **Builder App** plugs
19
19
  into three pillars on party-managed infrastructure:
20
20
 
21
- 1. **PPLE ID** — OIDC single sign-on. End users authenticate in **PPLE Today**;
22
- the mini-app receives a bearer token via the mini-app SDK and the Worker
23
- validates it against the OIDC `userinfo` endpoint. Builders also use PPLE
24
- ID for the Console (browser login) and `pple auth login` (device flow).
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).
25
26
  2. **Mini-app integration** — apps run inside PPLE Today's WebView using
26
- `@pplethai/pple-today-miniapp-sdk`, and send audience-bound notifications
27
- (title/body, optional path-only `linkPath` into this app) through the
28
- platform-provisioned notify helper.
27
+ `@pplethai/pple-today-miniapp-sdk` (and the same SDK in a plain browser),
28
+ and send audience-bound notifications (title/body, optional path-only
29
+ `linkPath` into this app) through the platform-provisioned notify helper.
29
30
  3. **PPLE Design System** — `@pplethai/components`, a shadcn/ui-based React
30
31
  library with party branding. Docs: https://components.pplethai.org/
31
32
 
@@ -63,30 +64,38 @@ Read only the file(s) relevant to the task at hand:
63
64
  | Task | Read |
64
65
  | ---- | ---- |
65
66
  | Start a new app / local layout & commands | [references/project-scaffold.md](references/project-scaffold.md) |
66
- | Login, tokens, NOT_IN_APP, DEV_AUTH | [references/pple-id-auth.md](references/pple-id-auth.md) |
67
+ | Login, tokens, browser PPLE ID flow, DEV_AUTH | [references/pple-id-auth.md](references/pple-id-auth.md) |
67
68
  | Mini-app SDK behavior, push notifications | [references/mini-app.md](references/mini-app.md) |
68
69
  | UI, styling, components | [references/design-system.md](references/design-system.md) |
69
- | Create → init → deploy on managed hosting | [references/deployment.md](references/deployment.md) |
70
+ | Create → init → deploy; status / usage (`pple info`) | [references/deployment.md](references/deployment.md) |
70
71
 
71
72
  ## Golden path for a new app
72
73
 
73
- 1. **Create the app** in the Console (`console.ppleth.ai`) — one click
74
- provisions the slug, Worker, D1, R2, OAuth client, and hosting.
75
- 2. **Sign in on the machine**: `pple auth login` (PPLE ID device flow; no
74
+ 1. **Sign in on the machine**: `pple auth login` (PPLE ID device flow; no
76
75
  personal cloud credentials — ADR-0005).
77
- 3. **Scaffold locally**: `pple init <slug>` copies the golden-path template
78
- and links the folder to the app.
79
- 4. **Build** against the template helpers (`auth`, `db`, `files`, `notify`)
76
+ 2. **Create + scaffold** — pick one path (both use the same Provisioner
77
+ create; the App Slug is always platform-generated):
78
+ - **CLI create**: `pple init` (prompts for App Name) or
79
+ `pple init --name "<App Name>"` (non-interactive) — creates the app,
80
+ waits until it is ready, then scaffolds and writes the App Link.
81
+ - **Console create**: create in the Console (`console.ppleth.ai`), then
82
+ `pple init <slug>` to scaffold and link the folder.
83
+ 3. **Build** against the template helpers (`auth`, `db`, `files`, `notify`)
80
84
  and `@pplethai/components` (see the reference files).
81
- 5. **Ship**: `pple deploy` — the platform builds and puts the app live.
85
+ 4. **Ship**: `pple deploy` — the platform builds and puts the app live.
86
+ 5. **Status / usage**: `pple info` (from a linked folder, or
87
+ `pple info <slug>`) — read-only overview, usage vs Resource Limits,
88
+ recent deploys, env key list. Invites, Requests, and collaborators stay
89
+ Console-only (see [deployment.md](references/deployment.md)).
82
90
 
83
91
  ## Invariants — things that are true no matter what you build
84
92
 
85
- - **Mini-apps authenticate through PPLE Today, not a plain browser.** In
86
- production, a normal browser shows the friendly NOT_IN_APP screen. Local
87
- development uses `DEV_AUTH` in `.dev.vars` only — never provisioned in
88
- production. (Builders separately hold tokens from Console login and
89
- `pple auth login`; that does not change how end users enter a mini-app.)
93
+ - **Mini-apps authenticate through the mini-app SDK.** Inside PPLE Today the
94
+ SDK reads the launch-URL token; in a plain browser it runs the PPLE ID
95
+ OIDC login. Do not add a NotInApp guard or a custom browser sign-in page.
96
+ Local development uses `DEV_AUTH` in `.dev.vars` only — never provisioned
97
+ in production. (Builders separately hold tokens from Console login and
98
+ `pple auth login`.)
90
99
  - **Never mention git, branches, remotes, GitHub, Cloudflare accounts, or
91
100
  personal deploy credentials** in the happy path. Talk about the app's
92
101
  code and `pple init` / `pple deploy` only.
@@ -22,7 +22,7 @@
22
22
  {
23
23
  "id": 4,
24
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": "Refuse browser login for the mini-app; show NOT_IN_APP in plain browsers; real tokens via PPLE Today; 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)",
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
26
  "files": []
27
27
  }
28
28
  ]
@@ -5,16 +5,24 @@ Builder Apps run only on party-managed Cloudflare infrastructure
5
5
  (ADR-0005). The happy path uses the Console and the `pple` CLI — not git
6
6
  vocabulary, Docker, Cloud Build, or personal Cloudflare/Pages projects.
7
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
+
8
12
  ## Happy path
9
13
 
10
14
  ```bash
11
- # 1. Create the app in the Console (console.ppleth.ai)
12
- # → slug, Worker, D1, R2, OAuth client, hosting — all provisioned
13
-
14
15
  pple auth login # PPLE ID device flow on this machine
15
- pple init <slug> # scaffold template + App Link (.pple/app.json)
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
+
16
23
  # …edit locally with npm run dev…
17
24
  pple deploy -m "short note" # send changes; platform builds and goes live
25
+ pple info # read-only status, usage vs limits, deploys
18
26
  ```
19
27
 
20
28
  What the Builder hears: the app's name and live URL (e.g.
@@ -24,10 +32,69 @@ source-control jargon.
24
32
  | Command | Meaning for the Builder |
25
33
  | ------- | ----------------------- |
26
34
  | `pple auth login` | Sign in with PPLE ID |
27
- | `pple init <slug>` | Set up this app's code on this computer |
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 |
28
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) |
29
40
  | `pple whoami` / `pple auth logout` | Check / forget this machine's sign-in |
30
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
+
31
98
  ## What the platform owns
32
99
 
33
100
  - **Hosting**: Workers for Platforms dispatch namespace; one tenant Worker per
@@ -37,8 +104,9 @@ source-control jargon.
37
104
  - **The app's code**: stored and shipped by the platform. `pple init` /
38
105
  `pple deploy` authenticate under the hood — the Builder never configures
39
106
  a source-control account or pastes a deploy token.
40
- - **Config / secrets**: set in the Console; applied as Worker bindings on the
41
- **next** deploy. Secrets are write-only (set/unset, never read back).
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).
42
110
 
43
111
  ## Local vs production auth
44
112
 
@@ -7,8 +7,9 @@ Builder Apps ship as **mini-apps**: web apps opened in PPLE Today's WebView.
7
7
  [pple-id-auth.md](pple-id-auth.md). Facts that shape your design:
8
8
 
9
9
  - `sdk.init()` must complete before any other SDK call.
10
- - Outside PPLE Today (plain browser), show NOT_IN_APP — that is expected, not
11
- an error. Locally, `DEV_AUTH` covers development.
10
+ - Outside PPLE Today (plain browser), the same SDK runs the PPLE ID browser
11
+ login — that is expected, not an error. Locally, `DEV_AUTH` covers
12
+ development.
12
13
  - The OAuth client and mini-app URL are provisioned when the app is created in
13
14
  the Console; Builders do not register them manually with the Today team.
14
15
  - Design mobile-first; test at ~375 px width.
@@ -9,38 +9,43 @@ PPLE Today ──opens mini-app in WebView──▶ web (React)
9
9
  Worker ──GET {SSO_BASE_URL}/oidc/v1/userinfo──▶ PPLE ID
10
10
  ```
11
11
 
12
+ In a plain browser the same SDK runs the PPLE ID authorization-code login
13
+ (redirect / callback) instead of reading tokens from the launch URL; the
14
+ Worker still validates the bearer token the same way.
15
+
12
16
  The Worker never sees a password and holds no session; the bearer token is
13
17
  the credential on every request (`src/auth.ts`).
14
18
 
15
19
  Tokens also exist outside mini-apps for **Builders** (Console browser login,
16
- `pple auth login` device flow). That does not change end-user auth for the
17
- app you are building: do not invent a browser sign-in for the mini-app itself.
20
+ `pple auth login` device flow). End users of a Builder App authenticate
21
+ through the mini-app SDK — either inside PPLE Today or via that SDK's
22
+ browser PPLE ID flow. Do not invent a second custom login page.
18
23
 
19
24
  ## Three ways the mini-app can start
20
25
 
21
- Canonical wiring: `web/pple.ts` + `web/main.tsx` + `web/NotInApp.tsx` in the
22
- golden-path template.
26
+ Canonical wiring: `web/pple.ts` + `web/main.tsx` in the golden-path template.
23
27
 
24
28
  1. **Local `vite dev`** — set `token` to the `DEV_AUTH` value from `.dev.vars`
25
29
  and treat the session as signed-in. No PPLE Today required.
26
- 2. **Inside PPLE Today** — detect the mini-app user agent, init
27
- `@pplethai/pple-today-miniapp-sdk`, capture the access token from the
28
- launch URL, attach it on every `api()` call.
29
- 3. **Plain browser (production)** — show the friendly NOT_IN_APP screen
30
- ("เปิดในแอป PPLE Today"). There is no browser login for mini-apps.
30
+ 2. **Inside PPLE Today** — init `@pplethai/pple-today-miniapp-sdk`, capture
31
+ the access token from the launch URL, attach it on every `api()` call.
32
+ 3. **Plain browser (production)** — the same SDK completes the OIDC sign-in
33
+ callback or redirects to PPLE ID; on return it stores the access token
34
+ and the app continues as signed-in.
31
35
 
32
36
  ```tsx
33
- import { isInMiniAppUserAgent } from "@pplethai/components";
34
37
  import { PPLEMiniApp } from "@pplethai/pple-today-miniapp-sdk";
35
38
 
36
39
  // Local: import.meta.env.DEV → DEV_AUTH token ("dev-token" by default)
37
- // Plain browser: !isInMiniAppUserAgent → { kind: "not-in-app" }
38
- // Inside Today: sdk.init() → getAccessToken() → { kind: "signed-in" }
40
+ // Production (Today or browser): sdk.init() → getAccessToken() → { kind: "signed-in" }
39
41
  ```
40
42
 
41
43
  OAuth client values (`VITE_OAUTH_URL`, `VITE_OAUTH_CLIENT_ID`,
42
44
  `VITE_OAUTH_REDIRECT_URI`) are provisioned per app and injected at build
43
- time — Builders do not register clients by hand.
45
+ time — Builders do not register clients by hand. The redirect URI defaults
46
+ to the app's own origin, which matches the mini-app URL the platform
47
+ registers as an allowed redirect on that OAuth client
48
+ (`docs/integration/today-sso-changes.md` §5).
44
49
 
45
50
  ## Worker: validating the token
46
51
 
@@ -79,7 +84,8 @@ Rules:
79
84
  ## What not to tell the Builder
80
85
 
81
86
  - Do not claim "tokens only exist inside PPLE Today" as a platform-wide fact
82
- (Console and CLI also hold Builder tokens).
83
- - Do not add a custom OAuth redirect login page to the mini-app "so it works
84
- in Chrome."
87
+ (Console and CLI also hold Builder tokens; end users can also sign in via
88
+ the SDK's browser PPLE ID flow).
89
+ - Do not add a custom OAuth redirect login page, or a NotInApp / "open in
90
+ PPLE Today only" guard — the SDK already handles the browser case.
85
91
  - Do not ask for GitHub or Cloudflare credentials to "get a token."
@@ -1,9 +1,9 @@
1
1
  # Project scaffold & local development
2
2
 
3
3
  New Builder Apps start from the golden-path template in
4
- `pple-platform/template`, copied by `pple init <slug>` after the Console
5
- creates the app. Do not scaffold Express/Prisma monorepos or copy
6
- `pple-erp` — that layout is the old path.
4
+ `pple-platform/template`, copied by `pple init` (create-and-scaffold, or
5
+ `pple init <slug>` after Console create). Do not scaffold Express/Prisma
6
+ monorepos or copy `pple-erp` — that layout is the old path.
7
7
 
8
8
  ## Layout (single package)
9
9
 
@@ -21,10 +21,9 @@ my-app/ # folder name matches the App Slug
21
21
  │ ├── notify.ts # audience-bound notifications
22
22
  │ └── env.ts # Env bindings type
23
23
  ├── web/ # React SPA (Vite)
24
- │ ├── main.tsx # bootstrap → App | NotInApp | error
24
+ │ ├── main.tsx # bootstrap → App | error
25
25
  │ ├── pple.ts # SDK / DEV_AUTH / api() helpers
26
26
  │ ├── App.tsx
27
- │ ├── NotInApp.tsx
28
27
  │ └── app.css # mobile-first layout only
29
28
  ├── shared/ # Zod schemas + types used by both sides
30
29
  │ └── api.ts
@@ -52,7 +51,8 @@ there once; both sides import the Zod schema / inferred type.
52
51
  ## Local development
53
52
 
54
53
  ```bash
55
- # After Console create-app + pple auth login + pple init <slug>:
54
+ # After pple auth login + create/scaffold (pple init / --name, or Console
55
+ # then pple init <slug>):
56
56
  cd <slug>
57
57
  npm install
58
58
  cp .dev.vars.example .dev.vars # enables DEV_AUTH for plain-browser work
@@ -1,8 +1,10 @@
1
1
  # Frontend build-time config (Vite exposes VITE_* to the browser bundle).
2
2
  # These are the app's PPLE ID OIDC client details, used by the mini-app SDK
3
- # when running inside PPLE Today. They are public values, safe to ship.
4
- # The platform injects them at deploy time; copy to `.env` to set locally.
3
+ # inside PPLE Today and for the browser PPLE ID login. They are public values,
4
+ # safe to ship. The platform injects them at deploy time; copy to `.env` to
5
+ # set locally.
5
6
  VITE_OAUTH_URL="https://id.peoplesparty.or.th"
6
7
  VITE_OAUTH_CLIENT_ID=""
7
- # Defaults to the app's own origin if unset.
8
+ # Defaults to the app's own origin if unset — must match the redirect URI
9
+ # on the app's provisioned PPLE ID OAuth client (the mini-app URL).
8
10
  # VITE_OAUTH_REDIRECT_URI=""
@@ -5,6 +5,9 @@ inside PPLE Today. The person you're helping is a party workforce member who
5
5
  may not read stack traces; keep changes simple, explain them plainly, and
6
6
  prefer editing the existing demo cards over introducing new patterns.
7
7
 
8
+ Ops (create / deploy / status): Console, `pple info`, and the Skill
9
+ deployment reference — not a long ops section here.
10
+
8
11
  ## Architecture (read once)
9
12
 
10
13
  One Cloudflare Worker serves everything:
@@ -27,8 +30,9 @@ which attach the PPLE ID bearer token. Same origin, so paths are relative
27
30
  - Inside PPLE Today, the mini-app SDK (`web/pple.ts`) captures the access
28
31
  token from the launch URL. The Worker validates it against PPLE ID
29
32
  (`src/auth.ts`).
30
- - In a plain browser, the app shows the NOT_IN_APP screen — there is no
31
- browser sign-in for mini-apps.
33
+ - In a plain browser, the same SDK runs the PPLE ID OIDC login (redirect /
34
+ callback) and returns an access token — do not add a custom sign-in page
35
+ or resurrect a NotInApp guard.
32
36
  - Locally, `DEV_AUTH` in `.dev.vars` bypasses PPLE Today. **Never** set
33
37
  `DEV_AUTH` in production; the platform never provisions it.
34
38
 
@@ -14,7 +14,7 @@ what you want to your AI coding agent and edit the demo cards. See
14
14
  | Capability | Where | Demo |
15
15
  | ---------- | ----- | ---- |
16
16
  | **API** | Hono Worker, `src/index.ts` | routes under `/api/*` |
17
- | **Identity** | `src/auth.ts` + `web/pple.ts` | validates PPLE ID tokens; NOT_IN_APP screen; dev bypass |
17
+ | **Identity** | `src/auth.ts` + `web/pple.ts` | validates PPLE ID tokens; browser PPLE ID login; dev bypass |
18
18
  | **Database** | D1, `src/db.ts` + `migrations/` | the Notes card |
19
19
  | **Files** | R2, `src/files.ts` | the Files card |
20
20
  | **Notifications** | `src/notify.ts` | the Notify card (audience-bound; optional `linkPath`) |
@@ -33,9 +33,9 @@ npm run dev # http://localhost:5173
33
33
  ```
34
34
 
35
35
  In local dev you're signed in automatically as a fake user (the `DEV_AUTH`
36
- bypass), so you can build without PPLE Today. Opened in a plain browser after
37
- deploy, the app shows a friendly "open in PPLE Today" screen; opened inside
38
- PPLE Today, it signs the real user in via the mini-app SDK.
36
+ bypass), so you can build without PPLE Today. Opened inside PPLE Today, the
37
+ mini-app SDK signs the real user in from the launch URL; opened in a plain
38
+ browser after deploy, the same SDK runs the PPLE ID login.
39
39
 
40
40
  ## Everyday commands
41
41
 
@@ -6,7 +6,8 @@
6
6
  "": {
7
7
  "name": "pple-app",
8
8
  "dependencies": {
9
- "@pplethai/components": "^1.4.1",
9
+ "@fontsource/ibm-plex-sans-thai-looped": "^5.3.0",
10
+ "@pplethai/components": "^1.4.3",
10
11
  "@pplethai/pple-today-miniapp-sdk": "^0.1.4",
11
12
  "hono": "^4.12.30",
12
13
  "react": "^19.2.0",
@@ -730,9 +731,9 @@
730
731
  "license": "MIT"
731
732
  },
732
733
  "node_modules/@fontsource/ibm-plex-sans-thai-looped": {
733
- "version": "5.2.8",
734
- "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-sans-thai-looped/-/ibm-plex-sans-thai-looped-5.2.8.tgz",
735
- "integrity": "sha512-3YYIGoTBuj4GMC5Lw0NDIvAxdCUr9NyL4/kQVWBkoa8o8jLS5q9MUpbBWSmisHEVnn9WhbLyZPU2eZ8fLtJH8A==",
734
+ "version": "5.3.0",
735
+ "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-sans-thai-looped/-/ibm-plex-sans-thai-looped-5.3.0.tgz",
736
+ "integrity": "sha512-5NNySQuBNRMk44sUwQypp971+2Fn1LJ9HAEZg9J76+7803j/aCS9HHH7FNvpq6mJDqzTrTrv+VSNsODgCAhxQQ==",
736
737
  "license": "OFL-1.1",
737
738
  "funding": {
738
739
  "url": "https://github.com/sponsors/ayuhito"
@@ -1324,9 +1325,9 @@
1324
1325
  "license": "MIT"
1325
1326
  },
1326
1327
  "node_modules/@pplethai/components": {
1327
- "version": "1.4.1",
1328
- "resolved": "https://registry.npmjs.org/@pplethai/components/-/components-1.4.1.tgz",
1329
- "integrity": "sha512-QTvdjpTbKdQk3knTqPI4U8gb662oaXKYg0kO+DxflsPHRXCN9l9h9Tvr7cXpZ9adEcHZLThaOToMdHOxe49kuw==",
1328
+ "version": "1.4.3",
1329
+ "resolved": "https://registry.npmjs.org/@pplethai/components/-/components-1.4.3.tgz",
1330
+ "integrity": "sha512-YB8y5WsGduRI0pRos6vrjqpVWYvpxK9gDPXPSAhSC24hM7Xofk1A4o1poZ7g8DFVfG91FWGx8LXtAZ8okHhqsw==",
1330
1331
  "license": "MIT",
1331
1332
  "dependencies": {
1332
1333
  "@fontsource/ibm-plex-sans-thai-looped": "^5.1.0",
@@ -14,7 +14,8 @@
14
14
  "deploy": "vite build && wrangler deploy"
15
15
  },
16
16
  "dependencies": {
17
- "@pplethai/components": "^1.4.1",
17
+ "@fontsource/ibm-plex-sans-thai-looped": "^5.3.0",
18
+ "@pplethai/components": "^1.4.3",
18
19
  "@pplethai/pple-today-miniapp-sdk": "^0.1.4",
19
20
  "hono": "^4.12.30",
20
21
  "react": "^19.2.0",
@@ -67,12 +67,18 @@ function NotesCard() {
67
67
  const load = () =>
68
68
  apiJson<{ notes: Note[] }>("/api/notes")
69
69
  .then((r) => setNotes(r.notes))
70
- .catch((e) => showToast({ title: "Could not load notes", description: message(e), variant: "error" }));
70
+ .catch((e) => {
71
+ showToast({ title: "Could not load notes", description: message(e), variant: "error" });
72
+ // Land on the empty state. Leaving this null would spin forever.
73
+ setNotes([]);
74
+ });
71
75
 
72
76
  useEffect(() => void load(), []);
73
77
 
74
78
  async function add() {
75
- if (!body.trim()) return;
79
+ // `saving` guards the Enter key too — the button is disabled while a save
80
+ // is in flight, but a keypress doesn't go through the button.
81
+ if (saving || !body.trim()) return;
76
82
  setSaving(true);
77
83
  try {
78
84
  await apiJson<Note>("/api/notes", {
@@ -137,7 +143,11 @@ function FilesCard() {
137
143
  const load = () =>
138
144
  apiJson<{ files: UploadedFile[] }>("/api/files")
139
145
  .then((r) => setFiles(r.files))
140
- .catch((e) => showToast({ title: "Could not load files", description: message(e), variant: "error" }));
146
+ .catch((e) => {
147
+ showToast({ title: "Could not load files", description: message(e), variant: "error" });
148
+ // Land on the empty state. Leaving this null would spin forever.
149
+ setFiles([]);
150
+ });
141
151
 
142
152
  useEffect(() => void load(), []);
143
153
 
@@ -168,7 +178,15 @@ function FilesCard() {
168
178
  <CardContent>
169
179
  <div className="stack">
170
180
  <div className="row">
171
- <input ref={inputRef} type="file" className="file-input" onChange={upload} />
181
+ {/* Disabled while uploading, so picking a second file can't start
182
+ a concurrent upload. */}
183
+ <input
184
+ ref={inputRef}
185
+ type="file"
186
+ className="file-input"
187
+ onChange={upload}
188
+ disabled={uploading}
189
+ />
172
190
  {uploading && <Spinner />}
173
191
  </div>
174
192
  {files === null ? (
@@ -90,23 +90,3 @@ body {
90
90
  flex: 1;
91
91
  min-width: 0;
92
92
  }
93
-
94
- .notinapp {
95
- text-align: center;
96
- display: flex;
97
- flex-direction: column;
98
- gap: 12px;
99
- align-items: center;
100
- padding: 16px;
101
- }
102
-
103
- .notinapp__logo {
104
- height: 48px;
105
- width: auto;
106
- color: #ff6a13;
107
- }
108
-
109
- .notinapp__title {
110
- margin: 0;
111
- font-size: 20px;
112
- }
@@ -1,12 +1,25 @@
1
1
  // App entry point. Import the design-system styles first, then our own.
2
2
  import "@pplethai/components/styles.css";
3
+
4
+ // The body font, re-declared from @fontsource so it actually loads:
5
+ // @pplethai/components inlines Fontsource's @font-face rules but ships no
6
+ // dist/files/*.woff2, so every `url(files/…)` 404s and the body font silently
7
+ // falls back to system-ui. Anakotmai, the heading face, loads from an absolute
8
+ // URL and is unaffected. Importing the same weights gives Vite real files to
9
+ // bundle; identical @font-face rules declared later win the cascade. These are
10
+ // the all-subset entrypoints, not `thai-400.css` — only those carry
11
+ // `unicode-range`, without which one rule would win for every character and
12
+ // Thai text would match the Latin file, which has no Thai glyphs.
13
+ // Remove once the upstream package ships its font files: PPLEThai/pple-design-system#16.
14
+ import "@fontsource/ibm-plex-sans-thai-looped/400.css";
15
+ import "@fontsource/ibm-plex-sans-thai-looped/600.css";
16
+
3
17
  import "./app.css";
4
18
 
5
19
  import { Alert, AlertDescription, AlertTitle, Toaster } from "@pplethai/components";
6
20
  import { StrictMode } from "react";
7
21
  import { createRoot } from "react-dom/client";
8
22
  import { App } from "./App";
9
- import { NotInApp } from "./NotInApp";
10
23
  import { bootstrap } from "./pple";
11
24
 
12
25
  function ErrorScreen({ message }: { message: string }) {
@@ -22,16 +35,14 @@ function ErrorScreen({ message }: { message: string }) {
22
35
 
23
36
  const root = createRoot(document.getElementById("root")!);
24
37
 
25
- // Establish the session (PPLE Today token, dev bypass, or "not in app") before
26
- // rendering, so the first paint is the right screen.
38
+ // Establish the session (PPLE Today token, browser PPLE ID login, or local
39
+ // DEV_AUTH) before rendering, so the first paint is the right screen.
27
40
  bootstrap().then((result) => {
28
41
  root.render(
29
42
  <StrictMode>
30
43
  <Toaster />
31
44
  {result.kind === "signed-in" ? (
32
45
  <App />
33
- ) : result.kind === "not-in-app" ? (
34
- <NotInApp />
35
46
  ) : (
36
47
  <ErrorScreen message={result.message} />
37
48
  )}