frontier-os-app-builder 1.0.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 (55) hide show
  1. package/README.md +92 -0
  2. package/agents/fos-executor.md +460 -0
  3. package/agents/fos-plan-checker.md +386 -0
  4. package/agents/fos-planner.md +416 -0
  5. package/agents/fos-researcher.md +358 -0
  6. package/agents/fos-verifier.md +491 -0
  7. package/bin/fos-tools.cjs +794 -0
  8. package/bin/install.js +234 -0
  9. package/commands/fos/add-feature.md +29 -0
  10. package/commands/fos/discuss.md +31 -0
  11. package/commands/fos/execute.md +35 -0
  12. package/commands/fos/new-app.md +39 -0
  13. package/commands/fos/new-milestone.md +28 -0
  14. package/commands/fos/next.md +29 -0
  15. package/commands/fos/plan.md +37 -0
  16. package/commands/fos/ship.md +29 -0
  17. package/commands/fos/status.md +22 -0
  18. package/package.json +30 -0
  19. package/references/app-patterns.md +501 -0
  20. package/references/deployment.md +395 -0
  21. package/references/module-inference.md +349 -0
  22. package/references/sdk-surface.md +1622 -0
  23. package/references/verification-rules.md +404 -0
  24. package/templates/app/gitignore +25 -0
  25. package/templates/app/index.css +111 -0
  26. package/templates/app/index.html +19 -0
  27. package/templates/app/layout.tsx +45 -0
  28. package/templates/app/main-router.tsx +17 -0
  29. package/templates/app/main-simple.tsx +19 -0
  30. package/templates/app/package.json +36 -0
  31. package/templates/app/postcss.config.js +5 -0
  32. package/templates/app/router.tsx +22 -0
  33. package/templates/app/sdk-context.tsx +33 -0
  34. package/templates/app/test-setup.ts +19 -0
  35. package/templates/app/tsconfig.json +22 -0
  36. package/templates/app/vercel.json +127 -0
  37. package/templates/app/vite.config.ts +15 -0
  38. package/templates/state/context.md +248 -0
  39. package/templates/state/manifest.json +11 -0
  40. package/templates/state/plan.md +187 -0
  41. package/templates/state/project.md +118 -0
  42. package/templates/state/requirements.md +133 -0
  43. package/templates/state/roadmap.md +129 -0
  44. package/templates/state/state.md +131 -0
  45. package/templates/state/summary.md +273 -0
  46. package/workflows/add-feature.md +234 -0
  47. package/workflows/discuss.md +310 -0
  48. package/workflows/execute-plan.md +222 -0
  49. package/workflows/execute.md +338 -0
  50. package/workflows/new-app.md +331 -0
  51. package/workflows/new-milestone.md +258 -0
  52. package/workflows/next.md +157 -0
  53. package/workflows/plan.md +310 -0
  54. package/workflows/ship.md +296 -0
  55. package/workflows/status.md +145 -0
@@ -0,0 +1,501 @@
1
+ # Frontier OS App Patterns
2
+
3
+ Reference for the standard structure, conventions, and tech stack used by all Frontier OS apps.
4
+
5
+ ---
6
+
7
+ ## Tech Stack
8
+
9
+ | Layer | Package | Version |
10
+ | ------------ | ------------------------------- | --------- |
11
+ | Runtime | React | 19 |
12
+ | Bundler | Vite | 7 |
13
+ | Language | TypeScript | 5.9 |
14
+ | CSS | Tailwind CSS (via PostCSS) | 4 |
15
+ | Testing | Vitest + jsdom + Testing Library| 4 / 27 |
16
+ | SDK | @frontiertower/frontier-sdk | 0.15.0 |
17
+ | Routing | react-router-dom | 7 |
18
+
19
+ ---
20
+
21
+ ## Directory Layout
22
+
23
+ Every app follows this structure:
24
+
25
+ ```
26
+ app-<name>/
27
+ index.html # HTML shell (parameterized)
28
+ package.json # Project manifest (parameterized)
29
+ postcss.config.js # PostCSS with Tailwind (identical)
30
+ tsconfig.json # TypeScript config (identical)
31
+ vercel.json # Vercel deployment + CORS (identical)
32
+ vite.config.ts # Vite + Vitest config (parameterized)
33
+ src/
34
+ main.tsx # React root + RouterProvider (identical pattern)
35
+ router.tsx # Route definitions (parameterized)
36
+ lib/
37
+ sdk-context.tsx # SdkProvider + useSdk hook (identical)
38
+ views/
39
+ Layout.tsx # Shell layout component (parameterized)
40
+ <FeatureViews>.tsx # App-specific view components
41
+ components/ # Reusable UI components
42
+ hooks/ # Custom React hooks
43
+ styles/
44
+ index.css # Tailwind + dark theme variables (parameterized)
45
+ test/
46
+ setup.ts # Vitest setup (identical pattern)
47
+ lib/ # Unit tests for lib/
48
+ views/ # Tests for view components
49
+ hooks/ # Tests for hooks
50
+ components/ # Tests for components
51
+ ```
52
+
53
+ ### Naming Conventions
54
+
55
+ - Repository: `frontier-os-app-<name>` (kebab-case)
56
+ - Package name in `package.json`: `app-<name>` (kebab-case, no org scope)
57
+ - Views: PascalCase files in `src/views/`
58
+ - Components: PascalCase files in `src/components/`
59
+ - Hooks: camelCase `use<Name>` files in `src/hooks/`
60
+ - Tests: Mirror source structure under `src/test/`, with `.test.ts` or `.test.tsx` suffix
61
+
62
+ ---
63
+
64
+ ## Files Identical Across All Apps
65
+
66
+ These files are copied verbatim. They must not be modified per app.
67
+
68
+ ### `src/lib/sdk-context.tsx`
69
+
70
+ ```tsx
71
+ import { createContext, useContext, useEffect, useRef, useState, type ReactNode } from 'react';
72
+ import { FrontierSDK } from '@frontiertower/frontier-sdk';
73
+
74
+ const SdkContext = createContext<FrontierSDK | null>(null);
75
+
76
+ export const useSdk = (): FrontierSDK => {
77
+ const sdk = useContext(SdkContext);
78
+ if (!sdk) throw new Error('useSdk must be used within SdkProvider');
79
+ return sdk;
80
+ };
81
+
82
+ export const SdkProvider = ({ children }: { children: ReactNode }) => {
83
+ const sdkRef = useRef<FrontierSDK | null>(null);
84
+ const [ready, setReady] = useState(false);
85
+
86
+ useEffect(() => {
87
+ const sdk = new FrontierSDK();
88
+ sdkRef.current = sdk;
89
+ setReady(true);
90
+
91
+ return () => {
92
+ sdk.destroy();
93
+ };
94
+ }, []);
95
+
96
+ if (!ready) return null;
97
+
98
+ return (
99
+ <SdkContext.Provider value={sdkRef.current}>
100
+ {children}
101
+ </SdkContext.Provider>
102
+ );
103
+ };
104
+ ```
105
+
106
+ ### `postcss.config.js`
107
+
108
+ ```js
109
+ import tailwindcss from '@tailwindcss/postcss';
110
+
111
+ export default {
112
+ plugins: [tailwindcss()],
113
+ };
114
+ ```
115
+
116
+ ### `tsconfig.json`
117
+
118
+ ```json
119
+ {
120
+ "compilerOptions": {
121
+ "target": "ES2020",
122
+ "useDefineForClassFields": true,
123
+ "module": "ESNext",
124
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
125
+ "skipLibCheck": true,
126
+ "jsx": "react-jsx",
127
+ "moduleResolution": "bundler",
128
+ "allowImportingTsExtensions": true,
129
+ "resolveJsonModule": true,
130
+ "isolatedModules": true,
131
+ "noEmit": true,
132
+ "strict": true,
133
+ "noUnusedLocals": true,
134
+ "noUnusedParameters": true,
135
+ "noFallthroughCasesInSwitch": true,
136
+ "types": ["vitest/globals", "@testing-library/jest-dom"]
137
+ },
138
+ "include": ["src"],
139
+ "exclude": ["src/test", "**/*.test.ts", "**/*.test.tsx"]
140
+ }
141
+ ```
142
+
143
+ ### `vercel.json`
144
+
145
+ See [deployment.md](deployment.md) for the full file. All apps share the same CORS configuration with 5 origin blocks.
146
+
147
+ ---
148
+
149
+ ## Files Parameterized Per App
150
+
151
+ These files follow a fixed template but contain app-specific values.
152
+
153
+ ### `package.json`
154
+
155
+ Parameterized fields:
156
+
157
+ | Field | Example value |
158
+ | --------------- | ------------------------------------ |
159
+ | `name` | `"app-subscriptions"` |
160
+ | `dependencies` | App-specific packages added here |
161
+
162
+ Fixed fields (do not change):
163
+
164
+ - `"version": "1.0.0"`
165
+ - `"private": true`
166
+ - `"type": "module"`
167
+ - `scripts` block (see Package Scripts below)
168
+ - Core dependencies: `@frontiertower/frontier-sdk`, `react`, `react-dom`, `react-router-dom`
169
+ - Core devDependencies: `@tailwindcss/postcss`, `@types/react`, `@types/react-dom`, `@vitejs/plugin-react`, `postcss`, `tailwindcss`, `typescript`, `vite`
170
+ - Test devDependencies (when tests exist): `@testing-library/jest-dom`, `@testing-library/react`, `@testing-library/user-event`, `@vitest/coverage-v8`, `jsdom`, `vitest`
171
+
172
+ ### `index.html`
173
+
174
+ Parameterized fields:
175
+
176
+ | Element | What changes |
177
+ | ----------------------- | ------------------------- |
178
+ | `<title>` | App display name |
179
+ | `<meta name="description">` | App description |
180
+
181
+ Fixed structure:
182
+
183
+ ```html
184
+ <!doctype html>
185
+ <html lang="en">
186
+ <head>
187
+ <meta charset="utf-8" />
188
+ <title>{{APP_TITLE}}</title>
189
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
190
+ <meta name="description" content="{{APP_DESCRIPTION}}">
191
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
192
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
193
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
194
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
195
+ </head>
196
+ <body class="dark">
197
+ <div id="root"></div>
198
+ <script type="module" src="/src/main.tsx"></script>
199
+ </body>
200
+ </html>
201
+ ```
202
+
203
+ Key points:
204
+ - `body class="dark"` is always set (dark-only theme)
205
+ - Plus Jakarta Sans font is loaded from Google Fonts with weights 400, 500, 600, 700
206
+ - Favicon is always `/favicon.svg`
207
+
208
+ ### `vite.config.ts`
209
+
210
+ Parameterized fields:
211
+
212
+ | Field | What changes |
213
+ | --------------- | ----------------------------------- |
214
+ | `server.port` | Each app gets a unique dev port |
215
+
216
+ Template:
217
+
218
+ ```ts
219
+ import { defineConfig } from 'vitest/config';
220
+ import react from '@vitejs/plugin-react';
221
+
222
+ export default defineConfig({
223
+ plugins: [react()],
224
+ server: {
225
+ port: {{PORT}},
226
+ },
227
+ test: {
228
+ globals: true,
229
+ environment: 'jsdom',
230
+ setupFiles: ['./src/test/setup.ts'],
231
+ include: ['src/test/**/*.test.{ts,tsx}'],
232
+ },
233
+ });
234
+ ```
235
+
236
+ Note: Import `defineConfig` from `vitest/config` (not `vite`) so the `test` block is typed. If the app has no tests, the `test` block and vitest devDependencies may be omitted.
237
+
238
+ ### `Layout.tsx`
239
+
240
+ Parameterized field: the app name passed to `createStandaloneHTML()`.
241
+
242
+ ### `router.tsx`
243
+
244
+ Fully app-specific. Defines routes as children of the Layout element.
245
+
246
+ ### `src/styles/index.css`
247
+
248
+ The `@theme` block and `@layer base` are standard. Apps may add extra `@layer components` rules for app-specific component styles.
249
+
250
+ ---
251
+
252
+ ## Layout Pattern
253
+
254
+ Every app's `Layout.tsx` follows this exact flow:
255
+
256
+ ```
257
+ 1. isInFrontierApp() -- Check if running inside Frontier Wallet iframe
258
+ 2. If NOT in Frontier:
259
+ createStandaloneHTML('App Name') -- Render a standalone fallback page
260
+ return early
261
+ 3. If loading:
262
+ Show spinner
263
+ 4. If in Frontier and ready:
264
+ <SdkProvider>
265
+ <Outlet /> -- react-router child routes render here
266
+ </SdkProvider>
267
+ ```
268
+
269
+ Implementation:
270
+
271
+ ```tsx
272
+ import { useEffect, useState } from 'react';
273
+ import { Outlet } from 'react-router-dom';
274
+ import { isInFrontierApp, createStandaloneHTML } from '@frontiertower/frontier-sdk/ui-utils';
275
+ import { SdkProvider } from '../lib/sdk-context';
276
+
277
+ export const Layout = () => {
278
+ const [loading, setLoading] = useState(true);
279
+ const [standaloneHtml, setStandaloneHtml] = useState('');
280
+
281
+ useEffect(() => {
282
+ const inFrontier = isInFrontierApp();
283
+
284
+ if (!inFrontier) {
285
+ setStandaloneHtml(createStandaloneHTML('{{APP_NAME}}'));
286
+ setLoading(false);
287
+ return;
288
+ }
289
+
290
+ setLoading(false);
291
+ }, []);
292
+
293
+ if (standaloneHtml) {
294
+ return (
295
+ <div
296
+ className="min-h-screen bg-background text-foreground"
297
+ dangerouslySetInnerHTML={{ __html: standaloneHtml }}
298
+ />
299
+ );
300
+ }
301
+
302
+ if (loading) {
303
+ return (
304
+ <div className="loading-screen">
305
+ <div className="spinner spinner-lg" />
306
+ <p className="text-sm text-muted-foreground">Loading...</p>
307
+ </div>
308
+ );
309
+ }
310
+
311
+ return (
312
+ <SdkProvider>
313
+ <Outlet />
314
+ </SdkProvider>
315
+ );
316
+ };
317
+ ```
318
+
319
+ The `isInFrontierApp()` function checks `window.self !== window.top` -- it returns `true` when the app is loaded inside an iframe (the Frontier Wallet PWA embeds apps this way).
320
+
321
+ The `createStandaloneHTML()` fallback renders a branded page telling the user to open the app inside Frontier Wallet.
322
+
323
+ ---
324
+
325
+ ## Router Variant (Standard)
326
+
327
+ All current apps use react-router-dom with `createBrowserRouter`. The router file defines a single root route with `Layout` as the element and feature views as children:
328
+
329
+ ```tsx
330
+ import { createBrowserRouter } from 'react-router-dom';
331
+ import { Layout } from './views/Layout';
332
+ // ... import views
333
+
334
+ export const router = createBrowserRouter([
335
+ {
336
+ path: '/',
337
+ element: <Layout />,
338
+ children: [
339
+ { index: true, element: <Home /> },
340
+ { path: 'rooms', element: <Rooms /> },
341
+ { path: 'book/:categoryId', element: <Book /> },
342
+ // ... more routes
343
+ ],
344
+ },
345
+ ]);
346
+ ```
347
+
348
+ ### Single-Component Variant
349
+
350
+ For very simple apps that need only one view, the router can be omitted. In this case:
351
+ - `main.tsx` renders the Layout directly instead of `<RouterProvider>`
352
+ - `Layout.tsx` renders the single view component instead of `<Outlet />`
353
+ - No `router.tsx` file needed
354
+ - `react-router-dom` can be removed from dependencies
355
+
356
+ ### `main.tsx` (Identical Pattern)
357
+
358
+ ```tsx
359
+ import { StrictMode } from 'react';
360
+ import { createRoot } from 'react-dom/client';
361
+ import { RouterProvider } from 'react-router-dom';
362
+ import { router } from './router';
363
+ import './styles/index.css';
364
+
365
+ const rootElement = document.getElementById('root');
366
+
367
+ if (!rootElement) {
368
+ throw new Error('Root element #root not found in document.');
369
+ }
370
+
371
+ createRoot(rootElement).render(
372
+ <StrictMode>
373
+ <RouterProvider router={router} />
374
+ </StrictMode>
375
+ );
376
+ ```
377
+
378
+ ---
379
+
380
+ ## Package Scripts
381
+
382
+ | Script | Command | Purpose |
383
+ | --------- | ----------------- | ------------------------------------- |
384
+ | `dev` | `vite` | Start dev server with HMR |
385
+ | `build` | `tsc && vite build` | Type-check then production build |
386
+ | `preview` | `vite preview` | Preview production build locally |
387
+ | `lint` | `tsc --noEmit` | Type-check without emitting |
388
+ | `test` | `vitest run` | Run tests once (CI mode) |
389
+
390
+ The `test` script is only included when the app has tests and vitest is in devDependencies.
391
+
392
+ ---
393
+
394
+ ## Dark Theme CSS Variables
395
+
396
+ All apps use a dark-only theme defined in `src/styles/index.css` via Tailwind 4's `@theme` directive. These CSS custom properties are available as Tailwind utilities (e.g., `bg-background`, `text-foreground`, `border-border`).
397
+
398
+ ### Color Tokens
399
+
400
+ | Token | Value | Usage |
401
+ | ------------------------ | --------- | ------------------------------- |
402
+ | `--color-primary` | `#764AE2` | Primary actions, links |
403
+ | `--color-primary-foreground` | `#ffffff` | Text on primary backgrounds |
404
+ | `--color-accent` | `#E4DCF9` | Accent highlights |
405
+ | `--color-accent-foreground` | `#0A0A0A` | Text on accent backgrounds |
406
+ | `--color-alert` | `#EF4444` | Error states |
407
+ | `--color-alert-foreground` | `#F5F5F5` | Text on alert backgrounds |
408
+ | `--color-danger` | `#F87171` | Destructive actions |
409
+ | `--color-danger-foreground` | `#ffffff` | Text on danger backgrounds |
410
+ | `--color-success` | `#10b981` | Success states |
411
+ | `--color-background` | `#000000` | Page background |
412
+ | `--color-foreground` | `#FAFAFA` | Default text |
413
+ | `--color-muted` | `#1E293B` | Muted background |
414
+ | `--color-muted-foreground` | `#A3A3A3` | Secondary text |
415
+ | `--color-muted-background` | `#262626` | Alternative muted bg |
416
+ | `--color-card` | `#0A0A0A` | Card backgrounds |
417
+ | `--color-card-foreground` | `#F5F5F5` | Card text |
418
+ | `--color-border` | `#242424` | Borders |
419
+ | `--color-input` | `#242424` | Input borders/backgrounds |
420
+ | `--color-ring` | `#B6B1F6` | Focus rings |
421
+ | `--color-outline` | `#404040` | Outlines |
422
+
423
+ ### Typography
424
+
425
+ | Token | Value |
426
+ | ------------- | -------------------------------------------------- |
427
+ | `--font-sans` | `"Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif` |
428
+
429
+ ### Base Styles
430
+
431
+ The `@layer base` block sets:
432
+ - `box-sizing: border-box` on all elements
433
+ - Font smoothing (webkit + moz)
434
+ - `body` font-size `0.875rem` (14px), line-height `1.5`
435
+ - `-webkit-user-select: none` and `touch-action: manipulation` for mobile feel
436
+ - `#root` as a flex column filling viewport height
437
+ - Heading weights at `600`, line-height `1.25`
438
+ - Paragraph color set to `--color-muted-foreground`
439
+ - Link color set to `--color-primary`
440
+
441
+ ### Component Styles
442
+
443
+ The `@layer components` block provides:
444
+ - `.loading-screen` -- centered full-height container
445
+ - `.spinner` / `.spinner-lg` -- CSS-only spinning loader using border-top trick
446
+
447
+ Apps may add additional component styles in this layer for app-specific needs (e.g., phone input styling).
448
+
449
+ ---
450
+
451
+ ## Test Setup Pattern
452
+
453
+ ### `src/test/setup.ts`
454
+
455
+ ```ts
456
+ import '@testing-library/jest-dom';
457
+ import { vi } from 'vitest';
458
+
459
+ // Mock window.open for external links
460
+ vi.stubGlobal('open', vi.fn());
461
+
462
+ // Mock ResizeObserver
463
+ vi.stubGlobal('ResizeObserver', vi.fn().mockImplementation(() => ({
464
+ observe: vi.fn(),
465
+ unobserve: vi.fn(),
466
+ disconnect: vi.fn(),
467
+ })));
468
+
469
+ // Mock IntersectionObserver
470
+ vi.stubGlobal('IntersectionObserver', vi.fn().mockImplementation(() => ({
471
+ observe: vi.fn(),
472
+ unobserve: vi.fn(),
473
+ disconnect: vi.fn(),
474
+ })));
475
+ ```
476
+
477
+ ### Test Directory Structure
478
+
479
+ Tests mirror the source structure under `src/test/`:
480
+
481
+ ```
482
+ src/test/
483
+ setup.ts # Global test setup
484
+ lib/ # Tests for src/lib/
485
+ views/ # Tests for src/views/
486
+ hooks/ # Tests for src/hooks/
487
+ components/ # Tests for src/components/
488
+ ```
489
+
490
+ ### Vitest Configuration
491
+
492
+ Configured in `vite.config.ts`:
493
+ - `globals: true` -- no need to import `describe`, `it`, `expect`
494
+ - `environment: 'jsdom'` -- DOM simulation
495
+ - `setupFiles: ['./src/test/setup.ts']` -- runs before every test file
496
+ - `include: ['src/test/**/*.test.{ts,tsx}']` -- only files under `src/test/`
497
+
498
+ ### tsconfig.json Test Handling
499
+
500
+ - `"types": ["vitest/globals", "@testing-library/jest-dom"]` -- global type augmentation
501
+ - `"exclude": ["src/test", "**/*.test.ts", "**/*.test.tsx"]` -- test files excluded from production type-check (`tsc --noEmit` / `tsc && vite build`)