minutework 0.1.20 → 0.1.21

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 (45) hide show
  1. package/EXTERNAL_ALPHA.md +1 -1
  2. package/README.md +18 -5
  3. package/assets/claude-local/skills/README.md +1 -0
  4. package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +10 -2
  5. package/assets/claude-local/skills/shell-architecture/SKILL.md +3 -0
  6. package/assets/claude-local/skills/standalone-mobile-client/SKILL.md +105 -0
  7. package/assets/templates/mobile-app/.env.example +16 -0
  8. package/assets/templates/mobile-app/AGENTS.md +44 -0
  9. package/assets/templates/mobile-app/README.md +123 -0
  10. package/assets/templates/mobile-app/app/(app)/_layout.tsx +10 -0
  11. package/assets/templates/mobile-app/app/(app)/index.tsx +72 -0
  12. package/assets/templates/mobile-app/app/(auth)/login.tsx +91 -0
  13. package/assets/templates/mobile-app/app/_layout.tsx +15 -0
  14. package/assets/templates/mobile-app/app.json +31 -0
  15. package/assets/templates/mobile-app/babel.config.js +7 -0
  16. package/assets/templates/mobile-app/eas.json +24 -0
  17. package/assets/templates/mobile-app/expo-env.d.ts +5 -0
  18. package/assets/templates/mobile-app/metro.config.js +7 -0
  19. package/assets/templates/mobile-app/package.json +32 -0
  20. package/assets/templates/mobile-app/src/mw/client.ts +251 -0
  21. package/assets/templates/mobile-app/src/mw/contracts.ts +79 -0
  22. package/assets/templates/mobile-app/src/mw/endpoints.ts +42 -0
  23. package/assets/templates/mobile-app/src/mw/env.ts +59 -0
  24. package/assets/templates/mobile-app/src/mw/session.ts +50 -0
  25. package/assets/templates/mobile-app/template.json +18 -0
  26. package/assets/templates/mobile-app/tools/template/validate-template.mjs +69 -0
  27. package/assets/templates/mobile-app/tsconfig.json +16 -0
  28. package/assets/templates/next-tenant-app/README.md +4 -1
  29. package/dist/developer-client.d.ts +22 -0
  30. package/dist/developer-client.js +9 -0
  31. package/dist/developer-client.js.map +1 -1
  32. package/dist/index.js +9 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/init-prompt.js +10 -2
  35. package/dist/init-prompt.js.map +1 -1
  36. package/dist/init.d.ts +2 -1
  37. package/dist/init.js +59 -2
  38. package/dist/init.js.map +1 -1
  39. package/dist/publish.d.ts +22 -6
  40. package/dist/publish.js +222 -24
  41. package/dist/publish.js.map +1 -1
  42. package/dist/workspace-bootstrap.d.ts +1 -0
  43. package/dist/workspace-bootstrap.js.map +1 -1
  44. package/package.json +2 -2
  45. package/vendor/workspace-mcp/types.d.ts +2 -0
package/EXTERNAL_ALPHA.md CHANGED
@@ -140,7 +140,7 @@ The CLI does not fabricate success. If the backend cannot materialize a hosted p
140
140
 
141
141
  ## Machine-readable output (`--json`)
142
142
 
143
- `validate`, `compile`, `codegen`, and `deploy --preview` accept `--json` for unattended, agent-driven use. The command prints a single result envelope to stdout (`{ cliJsonVersion, command, ok, status, result, error }`) and nothing else; `ok` mirrors the exit code (fail-closed). `deploy --preview --json` never prompts — pair it with `--yes` to authorize, otherwise it returns `status: "confirmation_required"` and exits non-zero. The automatic bug report is suppressed in `--json` mode.
143
+ `validate`, `compile`, `codegen`, `deploy --preview`, and `publish` accept `--json` for unattended, agent-driven use. The command prints a single result envelope to stdout (`{ cliJsonVersion, command, ok, status, result, error }`) and nothing else; `ok` mirrors the exit code (fail-closed). `deploy --preview --json` and `publish --json` never prompt — pair them with `--yes` to authorize, otherwise they return `status: "confirmation_required"` and exit non-zero. For `publish`, a rejected publication-review attestation is reported as `ok: false` with the gate findings in `result.findings` (a real review outcome, not a transport error). The automatic bug report is suppressed in `--json` mode.
144
144
 
145
145
  ## Failure semantics
146
146
 
package/README.md CHANGED
@@ -1,17 +1,17 @@
1
1
  # `minutework`
2
2
 
3
- MinuteWork CLI for initializing a workspace, authenticating against a MinuteWork platform, linking a repo to a tenant-scoped public-site property, launching developer-local session broker flows, running local preview/test loops, and submitting hosted preview deploys.
3
+ MinuteWork CLI for initializing a workspace, authenticating against a MinuteWork platform, linking a repo to a tenant-scoped public-site property, launching developer-local session broker flows, running local preview/test loops, submitting hosted preview deploys, and publishing app packs to the marketplace through the publication-review gate.
4
4
 
5
5
  ## External alpha scope
6
6
 
7
7
  The current external alpha is intentionally narrow:
8
8
 
9
- - Starter: `tenant-app`
9
+ - Starters: `tenant-app` (the deployable web surface) and `mobile` (init-only)
10
10
  - Developer-local broker surface: `minutework session start|resume|status`
11
11
  - Hosted release class: `ssr_container`
12
12
  - Deploy surface: `minutework deploy --preview`
13
- - Deferred: `--live`, additional local coding engines, sidecar/runtime-backed deploys, marketplace publish flows
14
- - Reserved verb: `minutework publish` is present but **fails closed** (a typed `not_implemented` result) until the marketplace publication-review + attestation backend lands
13
+ - Deferred: `--live`, additional local coding engines, sidecar/runtime-backed deploys, and the commercial marketplace listing/pricing layer
14
+ - Marketplace publish: `minutework publish` submits the compiled, digest-bound app pack through the platform publication-review + attestation gate and publishes it to the public marketplace catalog **only** on an approved (or approved-with-warnings) attestation; a rejected attestation prints the review findings and exits 1 without publishing
15
15
 
16
16
  The shipped `tenant-app` starter is the combined web surface: public routes at
17
17
  the root plus a private `/app` workspace. Public-site content follows the
@@ -44,7 +44,20 @@ npx minutework login
44
44
  npx minutework link
45
45
  ```
46
46
 
47
- After `link`, the current alpha has two supported lanes.
47
+ After `link`, the current alpha has two supported lanes (both `tenant-app`).
48
+
49
+ For a native mobile client, scaffold the **`mobile`** starter instead:
50
+
51
+ ```bash
52
+ npx minutework init my-app --starter mobile
53
+ ```
54
+
55
+ It is **init-only** and bring-your-own-UI: a minimal Expo (React Native) client
56
+ that authenticates **directly** against the platform with a native device-flow
57
+ session token (PKCE authorize -> exchange -> bearer + refresh in the device
58
+ keychain), not the `tenant-app` BFF cookie path. There is no `link`/`deploy`
59
+ lane for it — distribution is your own EAS pipeline (`eas build`/`eas submit`,
60
+ EAS Update), not `minutework deploy`.
48
61
 
49
62
  ### Developer-local broker lane
50
63
 
@@ -32,6 +32,7 @@ Generated-workspace-first guidance should live here, especially:
32
32
  - `vuilder-public-site-authoring/SKILL.md`
33
33
  - `workspace-guidance-refresh/SKILL.md`
34
34
  - `shell-architecture/SKILL.md`
35
+ - `standalone-mobile-client/SKILL.md`
35
36
  - `runtime-capability-inventory/SKILL.md`
36
37
  - `layering-and-import-modes/SKILL.md`
37
38
  - `capability-gap-reporting/SKILL.md`
@@ -19,9 +19,17 @@ the monorepo or a live tenant runtime.
19
19
  - app packs
20
20
  - skills
21
21
  - tenant overlays
22
- - `tenant-app` and optional `sidecar` are implementation surfaces inside the
23
- tenant product, not the whole architecture.
22
+ - `tenant-app`, optional `sidecar`, and the optional `mobile` starter are
23
+ implementation surfaces inside the tenant product, not the whole architecture.
24
24
  - `tenant-app` is the combined public plus private web starter.
25
+ - A mobile / native client (Expo, React Native, native iOS/Android) is a
26
+ standalone-client exception authored in the `mobile` starter, not in
27
+ `tenant-app` or `sidecar`. It is a direct platform API client that consumes
28
+ the platform native-token API (`/api/v1/native/...`) directly with a
29
+ platform-issued native session token, rather than the `tenant-app` BFF cookie
30
+ session.
31
+ - If the `mobile` starter is enabled (`starters.mobile.enabled`), read
32
+ `standalone-mobile-client/SKILL.md` before proposing the mobile surface.
25
33
  - `vuilder-public-site` is a Vuilder-owned public-site authoring workspace for
26
34
  landing, blog, and onboarding routes backed by public-dj CMS manifests.
27
35
  - If `template_kind` is `vuilder-public-site`, or the workspace profile says
@@ -44,6 +44,9 @@ threads, channels, inboxes, guest collaboration, approvals, dashboards, or
44
44
  - the surface is public marketing
45
45
  - the surface is public intake for non-members
46
46
  - the surface is an embeddable widget
47
+ - the surface is a mobile app (Expo / React Native / native iOS/Android) ->
48
+ author in the `mobile` starter, not `tenant-app`; see
49
+ `standalone-mobile-client/SKILL.md`
47
50
  - If shell fit is correct but this generated workspace cannot directly author
48
51
  the shell extension, keep the shell-first recommendation and record a
49
52
  capability gap instead of silently converting the request into standalone
@@ -0,0 +1,105 @@
1
+ ---
2
+ name: standalone-mobile-client
3
+ description: "Building a native mobile app (Expo / React Native / native iOS/Android). It is a standalone-client exception authored in the mobile starter as a direct platform API client using a native session token, not in tenant-app or sidecar."
4
+ ---
5
+
6
+ # Standalone Mobile Client
7
+
8
+ Use this skill when the request is "can I build a mobile app?", or anything
9
+ that involves a native iOS/Android surface (Expo, React Native, or fully native).
10
+ The answer is yes, and the shape is fixed. Do not flip-flop on it.
11
+
12
+ ## Where it lives
13
+
14
+ - A native mobile app is a **standalone-client exception**. It is authored in
15
+ the `mobile` starter (`destinationDirectory: mobile`), enabled via the
16
+ workspace config key `starters.mobile = { enabled: true, path: "mobile" }`.
17
+ - It is **not** part of `tenant-app`, and **not** part of `sidecar`. Do not put
18
+ native code, Expo config, or React Native screens inside either of those
19
+ surfaces.
20
+ - `tenant-app` stays web-only. The mobile app is a sibling client, not a tab or
21
+ route inside the web BFF.
22
+
23
+ ## What it is
24
+
25
+ - The mobile app is a **direct platform API client**. It talks to the platform
26
+ HTTP API at `/api/v1/native/session/...` and nothing else. It does not go
27
+ through the `tenant-app` BFF.
28
+ - Authentication uses a platform-issued **native session token** -- a distinct
29
+ principal class, the "native app session token holder". This is exactly what
30
+ the auth contract anticipates: non-browser clients must mint explicit scoped
31
+ tokens after browser-assisted login or a device flow
32
+ (`reference/mwv3-dj6-docs/auth_and_credential_contract.md`, Rule 7).
33
+ - The app reads the platform base URL from `EXPO_PUBLIC_MW_PLATFORM_BASE_URL`.
34
+
35
+ ## Endpoints (shipped)
36
+
37
+ All native endpoints live under `/api/v1/native/session/`:
38
+
39
+ - `authorize/` -- browser-assisted consent (GET renders the login/consent page
40
+ or redirects to the onboarding host; POST confirms and 302-redirects to the
41
+ app `redirect_uri` with `?code=...&state=...`).
42
+ - `token-exchange/` -- POST `{code, code_verifier, redirect_uri}` -> `{access,
43
+ refresh, expires_at, ...}`. No bearer required.
44
+ - `refresh/` -- POST `{refresh_token}` -> a new `{access, refresh}` pair. No
45
+ bearer required.
46
+ - `me/` and `context/` -- GET (bearer) -> the tenant session payload for the
47
+ bound user/tenant. `context/` is currently identical to `me/`.
48
+ - `logout/` -- POST (bearer) -> revokes the presented token's access/refresh
49
+ family.
50
+
51
+ ## Auth flow
52
+
53
+ The native token flow mirrors the existing CLI developer-token flow
54
+ (PKCE authorize -> exchange -> opaque scoped tokens):
55
+
56
+ 1. **Authorize (browser-assisted device flow):** the app generates a PKCE
57
+ verifier and opens `GET /api/v1/native/session/authorize/` (with
58
+ `code_challenge`, `state`, `redirect_uri`) in a system browser. The user logs
59
+ in and confirms on the platform; the platform POSTs the confirmation and
60
+ 302-redirects back to the app's `redirect_uri` with a one-time `code` and the
61
+ `state`.
62
+ 2. **Exchange:** the app POSTs that one-time `code` plus the matching PKCE
63
+ `code_verifier` and the same `redirect_uri` to
64
+ `/api/v1/native/session/token-exchange/`, receiving an **access token +
65
+ refresh token** (opaque, scoped; access ~15 min, refresh ~30 days).
66
+ 3. **Store:** both tokens are persisted in `expo-secure-store` (the device
67
+ keystore), never in plain React/web state.
68
+ 4. **Call:** every request to `/api/v1/native/session/...` carries
69
+ `Authorization: Bearer <access>`.
70
+ 5. **Refresh:** on access-token expiry (`401`), POST the refresh token to
71
+ `/api/v1/native/session/refresh/` to mint a new pair, then retry. Refresh is
72
+ **rotating** -- the old access/refresh family is revoked, so persist the
73
+ freshly returned pair. Re-run the browser-assisted authorize step only when
74
+ the refresh token itself is invalid or revoked.
75
+
76
+ The browser/BFF cookie path belongs to `tenant-app` and is **web-only**. The
77
+ mobile app does not ride the `tenant-app` cookie jar, and it does not read or
78
+ forward CSRF -- native uses the bearer token directly against the platform.
79
+
80
+ ### Token binding (what is and isn't enforced)
81
+
82
+ The token is bound to one tenant + membership, and that binding **is** enforced:
83
+ a request that names a different tenant is rejected, and a token whose membership
84
+ has been deactivated stops authenticating. The optional `audience` and
85
+ `device_id` fields are **informational only** -- the platform carries them
86
+ through authorize -> exchange (and preserves them across refresh) but does **not**
87
+ validate or compare them. Do not treat `audience` or `device_id` as a security
88
+ boundary.
89
+
90
+ ## What not to do
91
+
92
+ - Do not build a parallel auth stack.
93
+ - Do not mint a JWT inside the app or stand up a local user table / token issuer.
94
+ - Do not expose access or refresh tokens to web pages or generic React state;
95
+ keep them in `expo-secure-store`.
96
+ - Do not route the mobile app through the `tenant-app` BFF cookie session.
97
+ - Do not read or forward CSRF from native; bearer-token auth does not use it.
98
+ - Do not put native code inside `tenant-app` or `sidecar`.
99
+
100
+ ## Distribution
101
+
102
+ - The `mobile` starter is **init-only**. Scaffolding it lands the app in the
103
+ workspace; it does not deploy it.
104
+ - Distribution is the developer's own pipeline: EAS Build, TestFlight, and the
105
+ Play Store. It is **not** `minutework deploy`.
@@ -0,0 +1,16 @@
1
+ # DEVELOPER-OWNED — replace freely. Only src/mw/ is MinuteWork substrate.
2
+ #
3
+ # Copy this file to `.env` and set the values for your environment.
4
+ #
5
+ # Only EXPO_PUBLIC_* variables are exposed to the app bundle. They are NOT
6
+ # secret — anyone with the app binary can read them. Put only non-secret config
7
+ # here. Never add API keys, client secrets, or tokens as EXPO_PUBLIC_* values;
8
+ # the platform issues short-lived bearer tokens at runtime via the native
9
+ # device flow.
10
+
11
+ # Base URL of the MinuteWork platform this app talks to directly
12
+ # (e.g. https://platform.minutework.dev). The app calls /api/v1/native/... here.
13
+ EXPO_PUBLIC_MW_PLATFORM_BASE_URL=https://<your-platform-base-url>
14
+
15
+ # Optional display name shown in the UI. Defaults to "MinuteWork".
16
+ # EXPO_PUBLIC_MW_APP_NAME=MinuteWork
@@ -0,0 +1,44 @@
1
+ # AGENTS.md — mobile-app starter
2
+
3
+ You are working in a **bring-your-own-UI Expo (React Native + Expo Router)**
4
+ app. **You own all UI/UX.** The only MinuteWork-managed code is the thin
5
+ substrate under `src/mw/`.
6
+
7
+ ## The one rule
8
+
9
+ **Only `src/mw/` is MinuteWork substrate.** Build your product in `app/` and your
10
+ own components/modules. Do not move product logic into `src/mw/`, and do not
11
+ build a parallel/local auth stack — the platform owns identity. The mobile app
12
+ is a **direct platform native API client** (bearer token to `/api/v1/native/...`),
13
+ **not** a `tenant-app` BFF cookie client and **not** a `sidecar`.
14
+
15
+ ## Use your IDE's Expo skills
16
+
17
+ For the native craft (UI, data, build/release), lean on your local IDE skills:
18
+
19
+ - `building-native-ui` — Expo Router UI, styling, navigation, animations
20
+ - `native-data-fetching` — fetch/React Query/SWR, caching, offline, loaders
21
+ - `expo-deployment` — App Store / Play Store / EAS distribution
22
+ - `expo-dev-client` — custom dev client (needed for the browser-assisted auth flow)
23
+ - `expo-tailwind-setup` — NativeWind/Tailwind if you want utility styling
24
+ - `push-notification-setup` — APNs/FCM push
25
+ - `upgrading-expo` — SDK upgrades and dependency fixes
26
+ - `expo-cicd-workflows` — EAS build/deploy pipelines (`.eas/workflows/`)
27
+
28
+ ## MinuteWork integration shape
29
+
30
+ For how this app fits the platform (the standalone-client exception, the native
31
+ session token, and the device flow), read the Builder skill
32
+ **`standalone-mobile-client`** (in the Builder bundle) and the auth contract
33
+ `reference/mwv3-dj6-docs/auth_and_credential_contract.md` (Rule 7: non-browser
34
+ clients mint explicit scoped tokens after browser-assisted login / a device
35
+ flow).
36
+
37
+ ## Status
38
+
39
+ `src/mw/client.ts` and `src/mw/session.ts` are **implemented**: a real
40
+ browser-assisted PKCE device flow against the platform native session endpoints
41
+ (`/api/v1/native/session/*`), with tokens persisted in the device keychain via
42
+ `expo-secure-store`. Wire your UI against the documented client methods
43
+ (`authorize` -> `exchange`, `loadSession`, `logout`). Set `app.json` `expo.scheme`
44
+ to your own unique scheme — that is the OAuth-style redirect target.
@@ -0,0 +1,123 @@
1
+ # Mobile App Starter (Expo, bring-your-own-UI)
2
+
3
+ A minimal **Expo (React Native + Expo Router)** starter for building a native
4
+ MinuteWork client. **You own all of the UI/UX.** The only MinuteWork-managed
5
+ code is the thin substrate under `src/mw/`. Everything else in this template is
6
+ a deliberately plain placeholder meant to be replaced.
7
+
8
+ There is **no MinuteWork design system here** — bring your own. Style with
9
+ whatever you like (plain `StyleSheet`, NativeWind/Tailwind, Tamagui, etc.). The
10
+ plain screens use `StyleSheet` only so there is nothing to rip out.
11
+
12
+ ## What's substrate vs. yours
13
+
14
+ | Path | Owner | Notes |
15
+ | --- | --- | --- |
16
+ | `src/mw/env.ts` | **MinuteWork substrate** | Validates `EXPO_PUBLIC_MW_PLATFORM_BASE_URL` (+ optional app name) |
17
+ | `src/mw/endpoints.ts` | **MinuteWork substrate** | Builds platform `/api/v1/native/...` URLs |
18
+ | `src/mw/contracts.ts` | **MinuteWork substrate** | Zod schemas for native session + token payloads |
19
+ | `src/mw/client.ts` | **MinuteWork substrate** | Native token client — real browser-assisted PKCE device flow |
20
+ | `src/mw/session.ts` | **MinuteWork substrate** | Secure-store token wrapper (`expo-secure-store`) |
21
+ | `tools/template/` | **MinuteWork substrate** | Template-governance tooling, not shipped app code |
22
+ | `app/**` | **You** | Expo Router screens — replace freely |
23
+ | `package.json`, `app.json`, `eas.json` | **You** | App config — replace freely |
24
+ | `tsconfig.json`, `babel.config.js`, `metro.config.js` | **You** | Tooling config — replace freely |
25
+
26
+ Rule of thumb: **only `src/mw/` is MinuteWork substrate.** If you find yourself
27
+ editing `src/mw/` to add product behavior, that behavior probably belongs in
28
+ your own `app/` / components instead.
29
+
30
+ ## Auth model (read this)
31
+
32
+ This app authenticates as a **direct platform native client**:
33
+
34
+ - The platform issues a **bearer token** to the device through a
35
+ **browser-assisted PKCE device flow**: `GET /api/v1/native/session/authorize/`
36
+ (with a PKCE `code_challenge`) opens in a system browser; after login/consent
37
+ the platform redirects back with a one-time `code`, which you exchange at
38
+ `POST /api/v1/native/session/token-exchange/` (with the PKCE `code_verifier`)
39
+ for an `{access, refresh, expires_at}` pair, then store in the device keychain
40
+ via `expo-secure-store`.
41
+ - The app then calls the platform **directly** (e.g.
42
+ `GET /api/v1/native/session/me/` or `.../context/`) with
43
+ `Authorization: Bearer <access>`, and on `401` mints a fresh pair at
44
+ `POST /api/v1/native/session/refresh/` (rotating — persist the new pair) before
45
+ retrying. `POST /api/v1/native/session/logout/` revokes the token family.
46
+
47
+ This is **NOT** the Next.js `tenant-app` model. The tenant-app uses a
48
+ server-owned **BFF session cookie** (`platform_session_bff`) because a browser
49
+ can't safely hold tokens. A native app has secure device storage and no
50
+ per-app server in front of it, so it talks to the platform API directly with a
51
+ token instead of a cookie. **Do not** try to reuse the BFF cookie path here, and
52
+ **do not** build a parallel/local auth stack (no JWT minting, no local user
53
+ table) — the platform owns identity.
54
+
55
+ The token is bound to a single tenant + membership, and that binding **is**
56
+ enforced. The optional `audience` and `device_id` fields are **informational
57
+ only**: the platform carries them through the flow but does **not** validate or
58
+ compare them, so do not rely on them as a security boundary.
59
+
60
+ ### The auth client is real — configure your redirect scheme
61
+
62
+ `src/mw/client.ts` and `src/mw/session.ts` are **implemented**. The client:
63
+
64
+ - generates a PKCE `code_verifier` + S256 `code_challenge` (`expo-crypto`),
65
+ - opens the platform `authorize` URL in a system browser
66
+ (`expo-web-browser`'s `openAuthSessionAsync`) with an anti-forgery `state`,
67
+ - captures the returned one-time `code` from the deep-link redirect, exchanges
68
+ it (plus the `code_verifier`) for a token pair, and stores it in the device
69
+ keychain (`expo-secure-store`),
70
+ - sends `Authorization: Bearer <access>` on every platform call and
71
+ auto-refreshes once on a `401`, and
72
+ - revokes + clears local storage on `logout()`.
73
+
74
+ The redirect target is your app's deep-link scheme. The starter ships
75
+ `"scheme": "mobileapp"` in `app.json`, so the redirect is
76
+ `mobileapp://auth/native-callback` (built at runtime via `expo-linking`'s
77
+ `createURL`). **Set `app.json` `expo.scheme` to your own unique scheme** before
78
+ shipping; the client follows whatever you configure, and a unique scheme avoids
79
+ collisions with other apps that could intercept the callback. The browser-based
80
+ flow needs a custom dev client (not stock Expo Go) — see the `expo-dev-client`
81
+ IDE skill.
82
+
83
+ The full flow is documented in the doc-comment at the top of `src/mw/client.ts`.
84
+
85
+ ## Environment
86
+
87
+ Copy `.env.example` to `.env` and set:
88
+
89
+ ```
90
+ EXPO_PUBLIC_MW_PLATFORM_BASE_URL=https://<your-platform-base-url>
91
+ ```
92
+
93
+ Only `EXPO_PUBLIC_*` variables are bundled into the app, and they are **not
94
+ secret** — never put keys/tokens in them. `src/mw/env.ts` validates this value
95
+ at startup with zod and fails fast if it's missing or not a URL.
96
+
97
+ ## Running locally
98
+
99
+ ```
100
+ npm install # or pnpm/yarn/bun
101
+ npm run start # Expo dev server (then press i / a, or scan with Expo Go)
102
+ npm run ios
103
+ npm run android
104
+ npm run typecheck # tsc --noEmit
105
+ ```
106
+
107
+ The native auth flow opens a system browser, which needs a custom dev client
108
+ rather than stock Expo Go; see the `expo-dev-client` IDE skill.
109
+
110
+ ## Distribution
111
+
112
+ **Distribution is your EAS pipeline, not `minutework deploy`.** This starter is
113
+ not a hosted web/sidecar deployable; it produces native binaries. Build and ship
114
+ with EAS (`eas build`, `eas submit`, EAS Update). `eas.json` ships minimal
115
+ `preview` and `production` profiles to start from. See the `expo-deployment` and
116
+ `expo-cicd-workflows` IDE skills.
117
+
118
+ ## Template manifest
119
+
120
+ `template.json` declares `template_kind: "mobile-app"` and `deployable: false`.
121
+ Because mobile is not a web/sidecar deployable, it is validated by
122
+ `tools/template/validate-template.mjs` (run `node tools/template/validate-template.mjs`),
123
+ **not** the strict shared `runtime/builder/templates/template.schema.json`.
@@ -0,0 +1,10 @@
1
+ // DEVELOPER-OWNED — replace freely. Only src/mw/ is MinuteWork substrate.
2
+ //
3
+ // Authed area layout. This is where you'd guard on a loaded MinuteWork session
4
+ // (via `mwClient.loadSession()`) and redirect to /(auth)/login when there is no
5
+ // valid token. Left as a plain Stack so you can build your own gating/UX.
6
+ import { Stack } from "expo-router";
7
+
8
+ export default function AppLayout() {
9
+ return <Stack screenOptions={{ headerShown: false }} />;
10
+ }
@@ -0,0 +1,72 @@
1
+ // DEVELOPER-OWNED — replace freely. Only src/mw/ is MinuteWork substrate.
2
+ //
3
+ // Trivial authed screen. Replace with your real product home. Build your data
4
+ // fetching against the platform native API using the bearer token from
5
+ // `src/mw/` (see the `native-data-fetching` IDE skill).
6
+ import { Pressable, StyleSheet, Text, View } from "react-native";
7
+ import { router } from "expo-router";
8
+
9
+ import { mwClient } from "@/mw/client";
10
+
11
+ export default function HomeScreen() {
12
+ async function onSignOut() {
13
+ try {
14
+ await mwClient.logout();
15
+ } finally {
16
+ router.replace("/(auth)/login");
17
+ }
18
+ }
19
+
20
+ return (
21
+ <View style={styles.container}>
22
+ <Text style={styles.title}>You are in.</Text>
23
+ <Text style={styles.body}>
24
+ Replace this screen with your product. Only `src/mw/` is MinuteWork
25
+ substrate — everything else is yours.
26
+ </Text>
27
+
28
+ <Pressable
29
+ accessibilityRole="button"
30
+ onPress={onSignOut}
31
+ style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
32
+ >
33
+ <Text style={styles.buttonText}>Sign out</Text>
34
+ </Pressable>
35
+ </View>
36
+ );
37
+ }
38
+
39
+ const styles = StyleSheet.create({
40
+ container: {
41
+ flex: 1,
42
+ alignItems: "center",
43
+ justifyContent: "center",
44
+ padding: 24,
45
+ gap: 12,
46
+ },
47
+ title: {
48
+ fontSize: 24,
49
+ fontWeight: "700",
50
+ },
51
+ body: {
52
+ fontSize: 15,
53
+ opacity: 0.7,
54
+ textAlign: "center",
55
+ },
56
+ button: {
57
+ marginTop: 16,
58
+ paddingVertical: 12,
59
+ paddingHorizontal: 20,
60
+ borderRadius: 10,
61
+ borderWidth: 1,
62
+ borderColor: "#111827",
63
+ },
64
+ buttonPressed: {
65
+ opacity: 0.6,
66
+ },
67
+ buttonText: {
68
+ fontSize: 15,
69
+ fontWeight: "600",
70
+ color: "#111827",
71
+ },
72
+ });
@@ -0,0 +1,91 @@
1
+ // DEVELOPER-OWNED — replace freely. Only src/mw/ is MinuteWork substrate.
2
+ //
3
+ // This screen is intentionally plain and is meant to be REWRITTEN. It exists to
4
+ // show the one integration seam you care about: kicking off MinuteWork's
5
+ // browser-assisted native sign-in through `src/mw/client.ts`.
6
+ //
7
+ // Pressing "Sign in" runs the real device flow: authorize in a system browser ->
8
+ // exchange the returned code for a platform bearer token pair -> route into the
9
+ // authed stack. Wire your own UI/UX around this call.
10
+ import { useState } from "react";
11
+ import { Alert, Pressable, StyleSheet, Text, View } from "react-native";
12
+ import { router } from "expo-router";
13
+
14
+ import { mwClient } from "@/mw/client";
15
+ import { mwEnv } from "@/mw/env";
16
+
17
+ export default function LoginScreen() {
18
+ const [busy, setBusy] = useState(false);
19
+
20
+ async function onSignIn() {
21
+ setBusy(true);
22
+ try {
23
+ // Device flow: authorize (browser) -> exchange code+verifier for tokens
24
+ // (stored in the keychain by the client), then route into the authed stack.
25
+ const { code, codeVerifier, redirectUri } = await mwClient.authorize();
26
+ await mwClient.exchange(code, codeVerifier, redirectUri);
27
+ router.replace("/(app)");
28
+ } catch (error) {
29
+ Alert.alert(
30
+ "Sign in unavailable",
31
+ error instanceof Error ? error.message : "Unknown error",
32
+ );
33
+ } finally {
34
+ setBusy(false);
35
+ }
36
+ }
37
+
38
+ return (
39
+ <View style={styles.container}>
40
+ <Text style={styles.title}>{mwEnv.appName}</Text>
41
+ <Text style={styles.subtitle}>Sign in to continue</Text>
42
+
43
+ <Pressable
44
+ accessibilityRole="button"
45
+ disabled={busy}
46
+ onPress={onSignIn}
47
+ style={({ pressed }) => [
48
+ styles.button,
49
+ (pressed || busy) && styles.buttonPressed,
50
+ ]}
51
+ >
52
+ <Text style={styles.buttonText}>
53
+ {busy ? "Opening sign in…" : "Sign in with MinuteWork"}
54
+ </Text>
55
+ </Pressable>
56
+ </View>
57
+ );
58
+ }
59
+
60
+ const styles = StyleSheet.create({
61
+ container: {
62
+ flex: 1,
63
+ alignItems: "center",
64
+ justifyContent: "center",
65
+ padding: 24,
66
+ gap: 12,
67
+ },
68
+ title: {
69
+ fontSize: 28,
70
+ fontWeight: "700",
71
+ },
72
+ subtitle: {
73
+ fontSize: 16,
74
+ opacity: 0.7,
75
+ marginBottom: 12,
76
+ },
77
+ button: {
78
+ backgroundColor: "#111827",
79
+ paddingVertical: 14,
80
+ paddingHorizontal: 24,
81
+ borderRadius: 10,
82
+ },
83
+ buttonPressed: {
84
+ opacity: 0.6,
85
+ },
86
+ buttonText: {
87
+ color: "#ffffff",
88
+ fontSize: 16,
89
+ fontWeight: "600",
90
+ },
91
+ });
@@ -0,0 +1,15 @@
1
+ // DEVELOPER-OWNED — replace freely. Only src/mw/ is MinuteWork substrate.
2
+ import { Stack } from "expo-router";
3
+ import { StatusBar } from "expo-status-bar";
4
+
5
+ export default function RootLayout() {
6
+ return (
7
+ <>
8
+ <Stack screenOptions={{ headerShown: false }}>
9
+ <Stack.Screen name="(auth)/login" />
10
+ <Stack.Screen name="(app)" />
11
+ </Stack>
12
+ <StatusBar style="auto" />
13
+ </>
14
+ );
15
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "//": "DEVELOPER-OWNED — replace freely. Only src/mw/ is MinuteWork substrate.",
3
+ "//scheme": "expo.scheme is the OAuth-style redirect target for the native auth device flow (src/mw/client.ts builds <scheme>://auth/native-callback). Set a unique scheme before shipping.",
4
+ "expo": {
5
+ "name": "mobile-app",
6
+ "slug": "mobile-app",
7
+ "scheme": "mobileapp",
8
+ "version": "0.1.0",
9
+ "orientation": "portrait",
10
+ "userInterfaceStyle": "automatic",
11
+ "newArchEnabled": true,
12
+ "ios": {
13
+ "supportsTablet": true,
14
+ "bundleIdentifier": "com.example.mobileapp"
15
+ },
16
+ "android": {
17
+ "package": "com.example.mobileapp"
18
+ },
19
+ "web": {
20
+ "bundler": "metro",
21
+ "output": "static"
22
+ },
23
+ "plugins": [
24
+ "expo-router",
25
+ "expo-secure-store"
26
+ ],
27
+ "experiments": {
28
+ "typedRoutes": true
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,7 @@
1
+ // DEVELOPER-OWNED — replace freely. Only src/mw/ is MinuteWork substrate.
2
+ module.exports = function (api) {
3
+ api.cache(true);
4
+ return {
5
+ presets: ["babel-preset-expo"],
6
+ };
7
+ };
@@ -0,0 +1,24 @@
1
+ {
2
+ "//": "DEVELOPER-OWNED — replace freely. Only src/mw/ is MinuteWork substrate. Distribution is YOUR EAS pipeline, not `minutework deploy`.",
3
+ "cli": {
4
+ "version": ">= 12.0.0",
5
+ "appVersionSource": "remote"
6
+ },
7
+ "build": {
8
+ "development": {
9
+ "developmentClient": true,
10
+ "distribution": "internal"
11
+ },
12
+ "preview": {
13
+ "distribution": "internal",
14
+ "channel": "preview"
15
+ },
16
+ "production": {
17
+ "channel": "production",
18
+ "autoIncrement": true
19
+ }
20
+ },
21
+ "submit": {
22
+ "production": {}
23
+ }
24
+ }
@@ -0,0 +1,5 @@
1
+ /// <reference types="expo/types" />
2
+
3
+ // DEVELOPER-OWNED — replace freely. Only src/mw/ is MinuteWork substrate.
4
+ // NOTE: This file should not be edited and should be committed; Expo regenerates
5
+ // it. It gives `process.env.EXPO_PUBLIC_*` string typings for `src/mw/env.ts`.
@@ -0,0 +1,7 @@
1
+ // DEVELOPER-OWNED — replace freely. Only src/mw/ is MinuteWork substrate.
2
+ // Default Expo Metro config. Customize bundling/resolver here if you need to.
3
+ const { getDefaultConfig } = require("expo/metro-config");
4
+
5
+ const config = getDefaultConfig(__dirname);
6
+
7
+ module.exports = config;