@mr.dj2u/cli 0.1.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 (50) hide show
  1. package/dist/cli.d.ts +12 -0
  2. package/dist/cli.d.ts.map +1 -0
  3. package/dist/cli.js +475 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/commands/agent.d.ts +41 -0
  6. package/dist/commands/agent.d.ts.map +1 -0
  7. package/dist/commands/agent.js +661 -0
  8. package/dist/commands/agent.js.map +1 -0
  9. package/dist/commands/continue.d.ts +59 -0
  10. package/dist/commands/continue.d.ts.map +1 -0
  11. package/dist/commands/continue.js +336 -0
  12. package/dist/commands/continue.js.map +1 -0
  13. package/dist/commands/dev-tools.d.ts +14 -0
  14. package/dist/commands/dev-tools.d.ts.map +1 -0
  15. package/dist/commands/dev-tools.js +283 -0
  16. package/dist/commands/dev-tools.js.map +1 -0
  17. package/dist/commands/explain.d.ts +29 -0
  18. package/dist/commands/explain.d.ts.map +1 -0
  19. package/dist/commands/explain.js +216 -0
  20. package/dist/commands/explain.js.map +1 -0
  21. package/dist/commands/mcp-install.d.ts +28 -0
  22. package/dist/commands/mcp-install.d.ts.map +1 -0
  23. package/dist/commands/mcp-install.js +265 -0
  24. package/dist/commands/mcp-install.js.map +1 -0
  25. package/dist/commands/onboard.d.ts +71 -0
  26. package/dist/commands/onboard.d.ts.map +1 -0
  27. package/dist/commands/onboard.js +552 -0
  28. package/dist/commands/onboard.js.map +1 -0
  29. package/dist/commands/report.d.ts +12 -0
  30. package/dist/commands/report.d.ts.map +1 -0
  31. package/dist/commands/report.js +70 -0
  32. package/dist/commands/report.js.map +1 -0
  33. package/dist/commands/skills.d.ts +19 -0
  34. package/dist/commands/skills.d.ts.map +1 -0
  35. package/dist/commands/skills.js +81 -0
  36. package/dist/commands/skills.js.map +1 -0
  37. package/dist/commands/test-and-iterate.d.ts +9 -0
  38. package/dist/commands/test-and-iterate.d.ts.map +1 -0
  39. package/dist/commands/test-and-iterate.js +90 -0
  40. package/dist/commands/test-and-iterate.js.map +1 -0
  41. package/dist/continue.d.ts +3 -0
  42. package/dist/continue.d.ts.map +1 -0
  43. package/dist/continue.js +2 -0
  44. package/dist/continue.js.map +1 -0
  45. package/dist/project-memory.d.ts +58 -0
  46. package/dist/project-memory.d.ts.map +1 -0
  47. package/dist/project-memory.js +2161 -0
  48. package/dist/project-memory.js.map +1 -0
  49. package/package.json +64 -0
  50. package/templates/project/guidelines.md +88 -0
@@ -0,0 +1,2161 @@
1
+ import { access, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const SOFTWARE_MANSION_CORE_DEPENDENCIES = {
5
+ 'react-native-gesture-handler': '~2.30.0',
6
+ 'react-native-reanimated': '4.2.1',
7
+ 'react-native-screens': '~4.23.0',
8
+ 'react-native-svg': '15.15.3',
9
+ 'react-native-keyboard-controller': '1.20.7',
10
+ 'react-native-worklets': '0.7.4',
11
+ };
12
+ const LOCAL_DATA_DEPENDENCIES = {
13
+ 'expo-sqlite': '~55.0.15',
14
+ };
15
+ const SUPABASE_DEPENDENCIES = {
16
+ '@react-native-async-storage/async-storage': '2.2.0',
17
+ '@supabase/supabase-js': '^2.105.4',
18
+ };
19
+ const UNIWIND_DEPENDENCIES = {
20
+ uniwind: '^1.6.4',
21
+ };
22
+ const EXPOSITION_NOTICE = 'These exposition pages are temporary developer and client-research scaffolds. Use them to evaluate styling, base packages, and data direction, then delete or prune them before production once the app direction is settled.';
23
+ const UNIWIND_DEV_DEPENDENCIES = {
24
+ tailwindcss: '^4.2.4',
25
+ };
26
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
27
+ const DEFAULT_GUIDELINES_TEMPLATE_PATH = path.join(PACKAGE_ROOT, 'templates', 'project', 'guidelines.md');
28
+ const INFO_HEADINGS = [
29
+ 'Overview',
30
+ 'Target Users',
31
+ 'Product Goals',
32
+ 'Non-Goals',
33
+ 'Core Features',
34
+ 'Core User Flows',
35
+ 'Data And Backend',
36
+ 'Platforms',
37
+ 'Package Choices',
38
+ 'Monetization Strategy',
39
+ 'Team Context',
40
+ 'Release Strategy',
41
+ 'Questions To Revisit',
42
+ 'Open Questions',
43
+ 'Resources',
44
+ 'Tech Stack & MDS Onboarding',
45
+ ];
46
+ const STYLE_HEADINGS = [
47
+ 'Visual Direction',
48
+ 'Brand/References',
49
+ 'Colors',
50
+ 'Typography',
51
+ 'Layout/Spacing',
52
+ 'Motion Tone',
53
+ 'Accessibility Notes',
54
+ 'Style Questions To Revisit',
55
+ 'Open Style Questions',
56
+ ];
57
+ export async function scaffoldProjectMemory(projectPath, answers, options = {}) {
58
+ const projectDir = path.join(projectPath, 'project');
59
+ await mkdir(projectDir, { recursive: true });
60
+ const force = Boolean(options.force);
61
+ const infoPath = path.join(projectDir, 'info.md');
62
+ const stylePath = path.join(projectDir, 'style.md');
63
+ const existingInfo = await readOptionalText(infoPath);
64
+ const existingStyle = await readOptionalText(stylePath);
65
+ const guidelines = await resolveGuidelines(answers, options);
66
+ const results = await Promise.all([
67
+ writeProjectMemoryFile(infoPath, renderInfo(projectPath, answers, existingInfo), force, true),
68
+ writeIfAllowed(path.join(projectDir, 'todo.md'), renderTodo(answers), force),
69
+ writeProjectMemoryFile(stylePath, renderStyle(answers, existingStyle), force, true),
70
+ writeIfAllowed(path.join(projectDir, 'guidelines.md'), guidelines, force),
71
+ writeIfAllowed(path.join(projectPath, 'AGENTS.md'), renderAgentInstructions(answers), force),
72
+ writeIfAllowed(path.join(projectPath, 'CLAUDE.md'), renderClaudeMd(answers), force),
73
+ ]);
74
+ if (shouldGenerateIntakeAgentHandoff(answers, existingInfo, existingStyle)) {
75
+ results.push(await writeIfAllowed(path.join(projectDir, 'intake-agent.md'), renderIntakeAgentHandoff(answers), force));
76
+ }
77
+ if (options.richBoilerplate ?? true) {
78
+ results.push(...(await scaffoldRichBoilerplate(projectPath, answers, force, {
79
+ manageUniwind: options.manageUniwind ?? true,
80
+ })));
81
+ }
82
+ return results;
83
+ }
84
+ export async function scaffoldRichBoilerplate(projectPath, answers, force, options = { manageUniwind: true }) {
85
+ const results = [];
86
+ await mkdir(path.join(projectPath, 'src', 'features', 'home'), { recursive: true });
87
+ await mkdir(path.join(projectPath, 'src', 'features', 'onboarding'), { recursive: true });
88
+ await mkdir(path.join(projectPath, 'src', 'features', 'settings'), { recursive: true });
89
+ await mkdir(path.join(projectPath, 'src', 'features', 'exposition'), { recursive: true });
90
+ await mkdir(path.join(projectPath, 'src', 'components', 'exposition'), { recursive: true });
91
+ await mkdir(path.join(projectPath, 'src', 'data'), { recursive: true });
92
+ await mkdir(path.join(projectPath, 'src', 'services'), { recursive: true });
93
+ results.push(await writeIfAllowed(path.join(projectPath, 'src', 'data', 'mock-app.ts'), renderMockData(answers), force), await writeIfAllowed(path.join(projectPath, 'src', 'services', 'local-data.ts'), renderLocalDataService(answers), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'exposition', 'animated-pressable.tsx'), renderAnimatedPressable(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'exposition', 'gesture-card.tsx'), renderGestureCard(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'exposition', 'keyboard-form.tsx'), renderKeyboardForm(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'exposition', 'svg-mark.tsx'), renderSvgMark(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'exposition', 'screens-card.tsx'), renderScreensCard(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'exposition', 'notice.tsx'), renderExpositionNotice(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'exposition', 'package-card.tsx'), renderPackageCard(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'exposition', 'index.ts'), renderExpositionComponentIndex(), force), await writeIfAllowed(path.join(projectPath, 'src', 'features', 'home', 'home-screen.tsx'), renderHomeScreen(answers), force), await writeIfAllowed(path.join(projectPath, 'src', 'features', 'onboarding', 'onboarding-screen.tsx'), renderOnboardingScreen(), force), await writeIfAllowed(path.join(projectPath, 'src', 'features', 'settings', 'settings-screen.tsx'), renderSettingsScreen(), force), await writeIfAllowed(path.join(projectPath, 'src', 'features', 'exposition', 'exposition-screen.tsx'), renderExpositionScreen(answers), force), await writeIfAllowed(path.join(projectPath, 'src', 'features', 'exposition', 'style-guide-screen.tsx'), renderStyleGuideScreen(answers), force), await writeIfAllowed(path.join(projectPath, 'src', 'features', 'exposition', 'data-screen.tsx'), renderDataScreen(answers), force));
94
+ if (answers.dataStart === 'local') {
95
+ results.push(await writeIfAllowed(path.join(projectPath, 'src', 'services', 'local-data.native.ts'), renderNativeLocalDataService(), force));
96
+ }
97
+ const appDir = getExpoRouterAppDir(projectPath, answers.appDirectory);
98
+ const expositionRouteDir = path.join(appDir, 'exposition');
99
+ await mkdir(expositionRouteDir, { recursive: true });
100
+ if (await pathExists(appDir)) {
101
+ const routeForce = force || !answers.includeCreateExpoComponents;
102
+ const shouldWriteRootLayout = routeForce && (await canWriteRichRootLayout(path.join(appDir, '_layout.tsx')));
103
+ results.push(await writeIfAllowed(path.join(appDir, 'index.tsx'), renderRouteExport(appDir, path.join(projectPath, 'src', 'features', 'home', 'home-screen')), routeForce), await writeIfAllowed(path.join(appDir, 'onboarding.tsx'), renderRouteExport(appDir, path.join(projectPath, 'src', 'features', 'onboarding', 'onboarding-screen')), routeForce), await writeIfAllowed(path.join(appDir, 'settings.tsx'), renderRouteExport(appDir, path.join(projectPath, 'src', 'features', 'settings', 'settings-screen')), routeForce), await writeIfAllowed(path.join(expositionRouteDir, 'index.tsx'), renderRouteExport(expositionRouteDir, path.join(projectPath, 'src', 'features', 'exposition', 'exposition-screen')), routeForce), await writeIfAllowed(path.join(expositionRouteDir, 'style-guide.tsx'), renderRouteExport(expositionRouteDir, path.join(projectPath, 'src', 'features', 'exposition', 'style-guide-screen')), routeForce), await writeIfAllowed(path.join(expositionRouteDir, 'data.tsx'), renderRouteExport(expositionRouteDir, path.join(projectPath, 'src', 'features', 'exposition', 'data-screen')), routeForce));
104
+ if (shouldWriteRootLayout) {
105
+ results.push(await writeIfAllowed(path.join(appDir, '_layout.tsx'), renderRichRootLayout(projectPath, appDir), routeForce));
106
+ }
107
+ if (!answers.includeCreateExpoComponents) {
108
+ await removeOptionalFile(path.join(appDir, 'details.tsx'));
109
+ }
110
+ }
111
+ if (answers.dataStart === 'supabase') {
112
+ results.push(await writeIfAllowed(path.join(projectPath, 'src', 'services', 'supabase.ts'), renderSupabaseClient(), force));
113
+ }
114
+ if (answers.testToMainSafeguards) {
115
+ await mkdir(path.join(projectPath, '.github', 'workflows'), { recursive: true });
116
+ results.push(await writeIfAllowed(path.join(projectPath, '.github', 'workflows', 'mds-pr-checks.yml'), renderGitHubPrChecksWorkflow(), force), await writeIfAllowed(path.join(projectPath, 'project', 'release-flow.md'), renderReleaseFlow(answers), force));
117
+ }
118
+ if (options.manageUniwind) {
119
+ results.push(await writeIfAllowed(path.join(projectPath, 'global.css'), renderGlobalCss(), force));
120
+ }
121
+ await ensurePackageJson(projectPath, answers, options.manageUniwind);
122
+ if (options.manageUniwind) {
123
+ await ensureUniwindGlobalCss(projectPath);
124
+ await ensureUniwindMetroConfig(projectPath);
125
+ await removeNativeWindArtifacts(projectPath);
126
+ }
127
+ await ensureGlobalCssImport(projectPath, answers.appDirectory);
128
+ return results;
129
+ }
130
+ export function renderInfo(projectPath, answers, existingInfo) {
131
+ const importedNotes = renderImportedNotes(existingInfo, INFO_HEADINGS);
132
+ return [
133
+ `# ${answers.appName} Project Info`,
134
+ '',
135
+ '## Overview',
136
+ '',
137
+ `Build an Expo app for ${answers.audience}.`,
138
+ '',
139
+ '## Target Users',
140
+ '',
141
+ answers.audience,
142
+ '',
143
+ '## Product Goals',
144
+ '',
145
+ '# TodoForContext(optional): Add the business/product outcomes that would make this app successful.',
146
+ '',
147
+ '## Non-Goals',
148
+ '',
149
+ '# TodoForContext(optional): Add anything this app should intentionally avoid for the MVP.',
150
+ '',
151
+ '## Core Features',
152
+ '',
153
+ `Derived from the first planned flows: ${answers.coreFlows}`,
154
+ '',
155
+ '## Core User Flows',
156
+ '',
157
+ answers.coreFlows,
158
+ '',
159
+ '## Data And Backend',
160
+ '',
161
+ answers.dataNeeds,
162
+ '',
163
+ `Starting mode: ${formatDataStart(answers.dataStart)}.`,
164
+ '',
165
+ '## Platforms',
166
+ '',
167
+ `- Target platforms: ${answers.targetPlatforms.join(', ') || 'none selected'}`,
168
+ `- First MVP platform: ${answers.firstTargetPlatform}`,
169
+ `- Expo Router app directory: ${formatAppDirectory(answers.appDirectory)}`,
170
+ `- Platform-specific organization: ${formatPlatformStrategy(answers.platformFileStrategy)}`,
171
+ `- Platform layout mode: ${formatPlatformLayoutMode(answers.platformLayoutMode)}`,
172
+ `- Web output: ${answers.webOutput}`,
173
+ `- Deployed server: ${formatServerChoice(answers.deployedServer)}`,
174
+ `- Expo UI: ${formatBoolean(answers.usesExpoUi)}`,
175
+ `- Expo Native Tabs: ${formatBoolean(answers.usesExpoNativeTabs)}`,
176
+ '',
177
+ '## Package Choices',
178
+ '',
179
+ answers.defaults.map((item) => `- ${item}`).join('\n'),
180
+ '',
181
+ '- Software Mansion core examples are included for Reanimated/Worklets, Gesture Handler, Screens, SVG, and Keyboard Controller.',
182
+ '- Prune package examples and dependencies after reviewing the exposition pages.',
183
+ '',
184
+ '## Monetization Strategy',
185
+ '',
186
+ '# TodoForContext(optional): Add monetization notes when relevant. Include pricing, subscriptions, ads, sponsorship, lead-gen, internal ROI, or note that monetization is not planned.',
187
+ '',
188
+ '## Team Context',
189
+ '',
190
+ '# TodoForContext(optional): Add team size, roles, delegated responsibilities, stakeholders, and client contacts if useful.',
191
+ '',
192
+ '## Release Strategy',
193
+ '',
194
+ `- Deployment plan: ${answers.deploymentTarget}`,
195
+ `- EAS usage: ${answers.easUses.length > 0 ? answers.easUses.join(', ') : 'not planned yet'}`,
196
+ `- Test-to-main safeguards: ${formatBoolean(answers.testToMainSafeguards)}`,
197
+ '',
198
+ '## Questions To Revisit',
199
+ '',
200
+ ...(hasThinOnboardingAnswers(answers)
201
+ ? [
202
+ '- Replace generic onboarding defaults with app-specific decisions.',
203
+ '- Confirm the exact first user flow before production buildout starts.',
204
+ ]
205
+ : []),
206
+ '',
207
+ '## Resources',
208
+ '',
209
+ `- Source project: ${projectPath}`,
210
+ '- # TodoForContext(optional): Add designs, repos, docs, client notes, analytics, credentials process, or research links.',
211
+ '',
212
+ ...importedNotes,
213
+ '',
214
+ '## Tech Stack & MDS Onboarding',
215
+ '',
216
+ '> Quick-reference stack summary for agents and collaborators. Fill in or correct any items marked below.',
217
+ '',
218
+ `- **App:** ${answers.appName} — ${answers.audience}`,
219
+ '- **Language:** TypeScript',
220
+ '- **Package manager:** # TodoForContext(optional): pnpm / npm / yarn / bun',
221
+ `- **Routing:** Expo Router (${formatAppDirectory(answers.appDirectory)})`,
222
+ `- **Styling:** ${formatStyleStack(answers)}`,
223
+ '- **State management:** # TodoForContext(optional): Zustand / Jotai / React context / none',
224
+ `- **Auth:** ${formatAuthSummary(answers)}`,
225
+ `- **Data:** ${formatDataStart(answers.dataStart)}`,
226
+ `- **Platforms:** ${answers.targetPlatforms.join(', ') || 'none selected'}, first MVP target: ${answers.firstTargetPlatform}`,
227
+ `- **Code organization:** ${formatCodeOrg(answers)}`,
228
+ `- **Deployed server:** ${formatServerAdapterSummary(answers)}`,
229
+ `- **Distribution:** ${answers.deploymentTarget}`,
230
+ `- **EAS:** ${answers.easUses.length > 0 ? answers.easUses.join(', ') : 'not planned yet'}`,
231
+ '',
232
+ '### MDS Onboarding Decisions',
233
+ '',
234
+ `- Advanced package setup: ${formatBoolean(answers.advancedPackageSetup)}`,
235
+ `- Create Expo starter components: ${formatBoolean(answers.includeCreateExpoComponents)}`,
236
+ `- Latest Expo SDK preference: ${formatBoolean(answers.useLatestExpoSdk)}`,
237
+ `- MDS guidelines template: yes`,
238
+ `- Expo UI: ${formatBoolean(answers.usesExpoUi)}`,
239
+ `- Expo Native Tabs: ${formatBoolean(answers.usesExpoNativeTabs)}`,
240
+ `- Test-to-main safeguards: ${formatBoolean(answers.testToMainSafeguards)}`,
241
+ `- Data start: ${formatDataStart(answers.dataStart)}`,
242
+ `- Defaults selected: ${answers.defaults.join(', ')}`,
243
+ '',
244
+ ].join('\n');
245
+ }
246
+ export function renderTodo(answers) {
247
+ const needsReview = hasThinOnboardingAnswers(answers);
248
+ return [
249
+ `# ${answers.appName} TODO`,
250
+ '',
251
+ '## Next Steps After Onboarding',
252
+ '',
253
+ '- [ ] Play with styling in the style-guide page.',
254
+ '- [ ] Browse exposition pages to understand included base packages.',
255
+ '- [ ] Review `project/` files for accuracy and planning adjustments.',
256
+ '- [ ] Resolve every `# TodoForContext(optional):` marker by filling the section underneath or deleting the marker line to acknowledge no extra context is needed.',
257
+ '- [ ] Tell the agent to commence development phase by phase.',
258
+ '',
259
+ '## Phase 0: Orientation And Planning',
260
+ '',
261
+ '- [ ] Confirm app purpose, audience, and primary flows in `project/info.md`.',
262
+ '- [ ] Confirm visual direction in `project/style.md` after using the style-guide page.',
263
+ '- [ ] Keep or prune included package examples after reviewing `/exposition`.',
264
+ '- [ ] Remove exposition pages before production once their lessons are absorbed.',
265
+ ...(needsReview
266
+ ? ['- [ ] Replace generic onboarding placeholders with real app decisions before full implementation.']
267
+ : []),
268
+ '',
269
+ '## Phase 1: App Shell And First Flow',
270
+ '',
271
+ `- [ ] Build the MVP first for ${answers.firstTargetPlatform}.`,
272
+ `- [ ] Establish app shell, navigation, layouts, and route groups in ${formatAppDirectory(answers.appDirectory)}.`,
273
+ `- [ ] Use ${formatPlatformLayoutMode(answers.platformLayoutMode)} unless project memory is updated.`,
274
+ `- [ ] Implement the first core flow from project info: ${answers.coreFlows}.`,
275
+ '- [ ] Keep route files thin and move real UI into feature screens.',
276
+ '',
277
+ '## Phase 2: Data Layer',
278
+ '',
279
+ `- [ ] Start with ${formatDataStart(answers.dataStart)}.`,
280
+ ...(answers.dataStart === 'local'
281
+ ? [
282
+ '- [ ] Use the local Expo SQLite demo as the first adapter.',
283
+ '- [ ] Replace the local adapter with Supabase when the product needs synced/authenticated data.',
284
+ ]
285
+ : [
286
+ '- [ ] Create separate Supabase projects for test/staging and production.',
287
+ '- [ ] Wire publishable client keys through environment files, never service-role keys.',
288
+ ]),
289
+ '- [ ] Verify data requirements against `project/info.md` before adding tables or auth.',
290
+ '',
291
+ '## Phase 3: Complete Product Flows',
292
+ '',
293
+ '- [ ] Build the remaining core flows from `project/info.md` phase by phase.',
294
+ '- [ ] Add shared state only when state crosses screens or features.',
295
+ '- [ ] Verify each selected platform after the MVP flow works.',
296
+ ...answers.targetPlatforms.map((platform) => `- [ ] Verify ${platform} behavior.`),
297
+ ...(answers.usesExpoUi ? ['- [ ] Add Expo UI examples where they improve native feel.'] : []),
298
+ ...(answers.usesExpoNativeTabs ? ['- [ ] Prototype Expo Native Tabs for mobile navigation.'] : []),
299
+ ...(answers.easUses.length > 0 ? answers.easUses.map((item) => `- [ ] Configure EAS for ${item}.`) : []),
300
+ '',
301
+ '## Phase 4: Polish, Safeguards, And Release',
302
+ '',
303
+ '- [ ] Prune unused Software Mansion examples and remove unneeded packages.',
304
+ '- [ ] Run `mds doctor --ci` and address errors.',
305
+ ...(answers.testToMainSafeguards
306
+ ? [
307
+ '- [ ] Follow `project/release-flow.md` for test-to-main development.',
308
+ '- [ ] Add GitHub branch protection so PR checks pass before merging into `test` or `main`.',
309
+ ]
310
+ : ['- [ ] Decide on release safeguards before production work begins.']),
311
+ ...(answers.webOutput !== 'none' ? [`- [ ] Confirm Expo web output mode: ${answers.webOutput}.`] : []),
312
+ ...(answers.deployedServer !== 'none'
313
+ ? [`- [ ] Plan deployed server work: ${formatServerChoice(answers.deployedServer)}.`]
314
+ : []),
315
+ '- [ ] Add monorepo support after the MVP is stable.',
316
+ '',
317
+ ].join('\n');
318
+ }
319
+ export function renderStyle(answers, existingStyle) {
320
+ const importedNotes = renderImportedNotes(existingStyle, STYLE_HEADINGS);
321
+ return [
322
+ `# ${answers.appName} Style`,
323
+ '',
324
+ '## Visual Direction',
325
+ '',
326
+ '- Define how the app should look and feel before building final screens.',
327
+ '- Keep this file focused on visual/design direction only.',
328
+ '',
329
+ '## Brand/References',
330
+ '',
331
+ '# TodoForContext(optional): Add brand words, competitor references, client examples, screenshots, or links.',
332
+ '',
333
+ '## Colors',
334
+ '',
335
+ '# TodoForContext(optional): Add palette direction, semantic color meaning, and light/dark mode expectations.',
336
+ '',
337
+ '## Typography',
338
+ '',
339
+ '# TodoForContext(optional): Add font choices, type scale, readability constraints, and tone.',
340
+ '',
341
+ '## Layout/Spacing',
342
+ '',
343
+ '# TodoForContext(optional): Add density, spacing, border radius, information hierarchy, and platform layout notes.',
344
+ '',
345
+ '## Motion Tone',
346
+ '',
347
+ '# TodoForContext(optional): Add animation feel: playful, calm, utility-first, premium, minimal, etc.',
348
+ '',
349
+ '## Accessibility Notes',
350
+ '',
351
+ '- Prefer readable contrast, scalable type, clear focus/pressed states, and platform-appropriate interactions.',
352
+ '- Add user-specific accessibility needs here when known.',
353
+ '',
354
+ '## Style Questions To Revisit',
355
+ '',
356
+ '# TodoForContext(optional): Add unresolved visual decisions to revisit later in `/exposition/style-guide`; delete this marker if there are none.',
357
+ '',
358
+ ...importedNotes,
359
+ '',
360
+ ].join('\n');
361
+ }
362
+ export function renderGuidelines(answers) {
363
+ return [
364
+ `# ${answers.appName} Guidelines`,
365
+ '',
366
+ '## Source Of Truth',
367
+ '',
368
+ '- The `project/` folder is the golden source of truth for product intent, roadmap, visual style, and technical rules.',
369
+ '- Agents and contributors must read `project/info.md`, `project/todo.md`, `project/style.md`, and this file before making product or architecture changes.',
370
+ '- Never make a change that conflicts with the project memory files unless the user explicitly updates them first.',
371
+ '',
372
+ '## TodoForContext Markers Block Onboarding',
373
+ '',
374
+ '- The string `# TodoForContext(optional):` marks sections the user has not yet decided about.',
375
+ '- Before agentic intake, planning, or scaffolding, scan every `project/` file for this marker.',
376
+ '- If any marker is present: stop, list each file + line, and tell the user to fill the section underneath OR delete the marker line to acknowledge they do not want to add that context.',
377
+ '- Only proceed when zero markers remain. `mds doctor --ci` treats unresolved markers as errors.',
378
+ '',
379
+ '## Expo Architecture',
380
+ '',
381
+ '- Keep Expo Router route files thin; route files should import feature screens or layouts.',
382
+ '- Put reusable business logic in `src/features`, `src/services`, `src/data`, or shared hooks.',
383
+ '- Prefer Uniwind with Tailwind v4 for new styling work.',
384
+ '- Use Zustand only when state is shared across screens or features.',
385
+ '- Keep private environment variables server-side and never expose secrets with `EXPO_PUBLIC_`.',
386
+ `- Keep Expo Router routes in ${formatAppDirectory(answers.appDirectory)} unless the project memory changes.`,
387
+ `- Use ${formatPlatformStrategy(answers.platformFileStrategy)} for platform-specific code when the selected targets diverge.`,
388
+ `- Use ${formatPlatformLayoutMode(answers.platformLayoutMode)} for selected platform shells.`,
389
+ `- Treat ${answers.firstTargetPlatform} as the first MVP platform until the roadmap says otherwise.`,
390
+ '',
391
+ '## Default Package Support',
392
+ '',
393
+ '- Software Mansion core support starts with Reanimated/Worklets, Gesture Handler, Screens, SVG, and Keyboard Controller.',
394
+ '- Use the temporary `/exposition` pages to decide which package examples should stay, be replaced, or be removed.',
395
+ '- Use `react-native-keyboard-controller` for real keyboard-heavy flows instead of piling up manual keyboard offsets.',
396
+ '- Use Reanimated for meaningful motion, but avoid expensive animation loops in long lists.',
397
+ `- Data starting point: ${formatDataStart(answers.dataStart)}.`,
398
+ ...(answers.dataStart === 'supabase'
399
+ ? [
400
+ '- Use separate Supabase projects for test/staging and production.',
401
+ '- Never expose Supabase service-role or secret keys in client code.',
402
+ ]
403
+ : ['- Keep local dummy data behind an adapter so Supabase can replace it later.']),
404
+ '',
405
+ '## Workflow',
406
+ '',
407
+ '- If the user says `mds continue` or `MDS Continue`, first run the MDS Continue command from the app root and use its session brief to propose a plan. Do not jump straight into intake or file edits.',
408
+ '- Run `mds doctor --ci` before pushing.',
409
+ '- Use `mds clear-expo-start` when Metro or server ports get wedged.',
410
+ ...(answers.testToMainSafeguards
411
+ ? ['- Develop through feature branches into `test`, then promote validated work from `test` to `main`.']
412
+ : []),
413
+ `- Latest Expo SDK preference captured during onboarding: ${formatBoolean(answers.useLatestExpoSdk)}.`,
414
+ '- Treat monorepo scaffolding as future work until the single-app MVP is stable.',
415
+ '',
416
+ ].join('\n');
417
+ }
418
+ export async function renderGuidelinesTemplate(answers, templatePath = DEFAULT_GUIDELINES_TEMPLATE_PATH) {
419
+ const template = await readFile(templatePath, 'utf8');
420
+ return applyGuidelinesTemplate(template, answers);
421
+ }
422
+ export function renderAgentInstructions(answers) {
423
+ return [
424
+ `# ${answers.appName} Agent Instructions`,
425
+ '',
426
+ 'The `project/` folder is the source of truth. Before changing behavior, architecture, styling, or roadmap details, read:',
427
+ '',
428
+ '- `project/info.md`',
429
+ '- `project/todo.md`',
430
+ '- `project/style.md`',
431
+ '- `project/guidelines.md`',
432
+ '',
433
+ `Expo Router routes belong in ${formatAppDirectory(answers.appDirectory)}. Platform layout mode: ${formatPlatformLayoutMode(answers.platformLayoutMode)}.`,
434
+ '',
435
+ 'If the user says `mds continue` or `MDS Continue`, first run `mds continue` from the app root if available. Use the MDS Continue brief to propose the next plan and wait for approval before editing files. If the command is unavailable, manually inspect markers, Doctor status, git status, and `project/todo.md` in that order.',
436
+ '',
437
+ 'Before any intake, planning, scaffolding, or phase work, scan every `project/` file for the marker `# TodoForContext(optional):`. If any are present, stop and tell the user to fill the section underneath OR delete the marker line to acknowledge they do not want to add that context. Only proceed when zero markers remain.',
438
+ '',
439
+ 'Then build from `project/todo.md` in phase order. Do not make changes that conflict with project memory. If the files are unclear or generic, update the project memory first or ask the user.',
440
+ '',
441
+ ].join('\n');
442
+ }
443
+ export function renderClaudeMd(answers) {
444
+ const spinUpDev = [
445
+ '## Spin up dev',
446
+ '',
447
+ 'Run `npm run clear-expo-start` (or `npx @mr.dj2u/cli clear-expo-start .`) instead of bare `expo start` or `npx expo start`.',
448
+ 'Kills port 8081, clears all Metro and Expo caches (including the Windows system cache), and starts `expo start --clear`.',
449
+ 'Expo Router API routes work automatically in this mode.',
450
+ 'Never fall back to a non-default port — always free the default port first.',
451
+ '',
452
+ ];
453
+ const backendAlongside = answers.customBackend
454
+ ? [
455
+ '## Also start the backend API server',
456
+ '',
457
+ `Run \`node ${answers.customBackendEntry}\` from the project root in a background process alongside Expo.`,
458
+ 'Both must be running for full local functionality — Expo on port 8081, backend on its own port.',
459
+ '',
460
+ ]
461
+ : [];
462
+ const spinUpProd = buildSpinUpProdSection(answers);
463
+ return [
464
+ `# ${answers.appName} — Agent Guidelines`,
465
+ '',
466
+ '## Before every git commit',
467
+ '',
468
+ 'Run `npm run mds:doctor` (or `npx @mr.dj2u/cli doctor --fast .`) before committing. Fix all errors first; warnings are OK to proceed with.',
469
+ '',
470
+ '## Before moving to the next phase',
471
+ '',
472
+ 'Run doctor before beginning each new development phase. Resolve all errors before continuing.',
473
+ '',
474
+ ...spinUpDev,
475
+ ...backendAlongside,
476
+ ...spinUpProd,
477
+ ].join('\n');
478
+ }
479
+ function buildSpinUpProdSection(answers) {
480
+ if (answers.webOutput === 'none')
481
+ return [];
482
+ if (answers.customBackend) {
483
+ return [
484
+ '## Spin up prod',
485
+ '',
486
+ 'Run `npm run serve:prod:fresh` for the Expo web server.',
487
+ 'Run `npm run serve:prod:api:fresh` for the backend API server.',
488
+ 'Both must be running for full prod-parity.',
489
+ '# TodoForContext(optional): Confirm api-server port and build script name in package.json.',
490
+ '',
491
+ ];
492
+ }
493
+ if (answers.expoServerAdapter === 'express' || answers.expoServerAdapter === 'bun') {
494
+ return [
495
+ '## Spin up prod',
496
+ '',
497
+ 'Run `npm run serve:prod:fresh` — kills port 3000, builds web dist, starts the Node server.',
498
+ 'Run `npm run serve:prod` to restart without rebuilding.',
499
+ 'Server runs on http://localhost:3000. Mirrors your self-hosted (Plesk/VPS) environment.',
500
+ '',
501
+ ];
502
+ }
503
+ if (answers.expoServerAdapter === 'eas') {
504
+ return [
505
+ '## Spin up prod',
506
+ '',
507
+ 'Run `npm run serve:prod:fresh` — builds web dist and starts `npx expo serve`.',
508
+ 'The terminal will show the local URL when ready. Mirrors EAS hosting.',
509
+ '',
510
+ ];
511
+ }
512
+ return [
513
+ '## Spin up prod',
514
+ '',
515
+ 'Run `npm run serve:prod:fresh` to build and serve the production bundle.',
516
+ '# TodoForContext(optional): Confirm this command matches your deployment environment.',
517
+ '',
518
+ ];
519
+ }
520
+ export function renderIntakeAgentHandoff(answers) {
521
+ return [
522
+ `# ${answers.appName} Intake Agent Handoff`,
523
+ '',
524
+ 'Use this file when the terminal intake was intentionally concise, generic, or included pre-existing notes that need a real agent conversation.',
525
+ '',
526
+ '## Agent Prompt',
527
+ '',
528
+ 'Read `project/info.md`, `project/style.md`, `project/guidelines.md`, and `project/todo.md`.',
529
+ 'If the user said `mds continue`, run `mds continue` first when available and use its session brief as the starting point.',
530
+ 'First, search every `project/` file for `# TodoForContext(optional):`. If any markers remain, stop before intake and tell the user to fill the section underneath or delete the marker line to acknowledge no extra context is needed.',
531
+ 'Ask conversational follow-up questions until the app plan is clear enough to build phase by phase.',
532
+ 'Move any imported notes into the correct canonical sections, preserve useful context, and remove uncertainty only after the user confirms it.',
533
+ 'Update `project/todo.md` so Phase 0 through Phase 4 reflect the clarified app, business, data, style, package, and release plan.',
534
+ '',
535
+ '## Places To Clarify',
536
+ '',
537
+ ...(hasThinOnboardingAnswers(answers)
538
+ ? [
539
+ '- The current onboarding answers still include generic defaults.',
540
+ '- Confirm the target users, first core flow, data model, deployment plan, monetization, and team context.',
541
+ ]
542
+ : ['- Review Imported Notes sections and optional TodoForContext markers.']),
543
+ '',
544
+ '## No API Keys Required',
545
+ '',
546
+ 'The public CLI does not require OpenAI, Anthropic, or other provider keys. This handoff is for Codex, Claude, or another agent environment the developer already chose to use.',
547
+ '',
548
+ ].join('\n');
549
+ }
550
+ async function ensurePackageJson(projectPath, answers, manageUniwind) {
551
+ const packageJsonPath = path.join(projectPath, 'package.json');
552
+ const raw = await readOptionalText(packageJsonPath);
553
+ if (!raw) {
554
+ return;
555
+ }
556
+ const packageJson = JSON.parse(raw);
557
+ packageJson.scripts = {
558
+ ...packageJson.scripts,
559
+ typecheck: packageJson.scripts?.typecheck ?? 'tsc --noEmit',
560
+ 'build:web': packageJson.scripts?.['build:web'] ?? 'expo export --platform web',
561
+ 'mds:continue': packageJson.scripts?.['mds:continue'] ?? 'npx @mr.dj2u/cli continue',
562
+ 'mds:doctor': packageJson.scripts?.['mds:doctor'] ?? 'npx @mr.dj2u/cli doctor',
563
+ 'mds:doctor:ci': packageJson.scripts?.['mds:doctor:ci'] ?? 'npx @mr.dj2u/cli doctor --ci',
564
+ 'free-port': packageJson.scripts?.['free-port'] ?? 'npx @mr.dj2u/cli free-port',
565
+ 'clear-expo-start': packageJson.scripts?.['clear-expo-start'] ?? 'npx @mr.dj2u/cli clear-expo-start',
566
+ 'expo-install-fix': packageJson.scripts?.['expo-install-fix'] ?? 'npx expo install --fix',
567
+ 'expo-doctor': packageJson.scripts?.['expo-doctor'] ?? 'npx expo-doctor',
568
+ 'post-create-check': packageJson.scripts?.['post-create-check'] ?? 'npx expo install --fix && npx expo-doctor',
569
+ 'ci:verify': packageJson.scripts?.['ci:verify'] ?? 'npx @mr.dj2u/cli doctor --ci',
570
+ };
571
+ if (answers.webOutput !== 'none') {
572
+ const serveProd = deriveServeProdScript(answers);
573
+ const serveProdFresh = deriveServeProdFreshScript(answers);
574
+ packageJson.scripts = {
575
+ ...packageJson.scripts,
576
+ 'serve:prod': packageJson.scripts?.['serve:prod'] ?? serveProd,
577
+ 'serve:prod:fresh': packageJson.scripts?.['serve:prod:fresh'] ?? serveProdFresh,
578
+ };
579
+ if (answers.customBackend) {
580
+ const entry = answers.customBackendEntry || 'server.js';
581
+ packageJson.scripts = {
582
+ ...packageJson.scripts,
583
+ 'serve:prod:api': packageJson.scripts?.['serve:prod:api'] ?? `node ${entry}`,
584
+ 'serve:prod:api:fresh': packageJson.scripts?.['serve:prod:api:fresh'] ??
585
+ `npm run build:api-server && node ${entry}`,
586
+ };
587
+ }
588
+ }
589
+ packageJson.dependencies = {
590
+ ...SOFTWARE_MANSION_CORE_DEPENDENCIES,
591
+ ...packageJson.dependencies,
592
+ };
593
+ if (answers.dataStart === 'local') {
594
+ packageJson.dependencies = {
595
+ ...LOCAL_DATA_DEPENDENCIES,
596
+ ...packageJson.dependencies,
597
+ };
598
+ }
599
+ if (answers.dataStart === 'supabase') {
600
+ packageJson.dependencies = {
601
+ ...SUPABASE_DEPENDENCIES,
602
+ ...packageJson.dependencies,
603
+ };
604
+ }
605
+ if (manageUniwind) {
606
+ packageJson.dependencies = {
607
+ ...UNIWIND_DEPENDENCIES,
608
+ ...packageJson.dependencies,
609
+ };
610
+ delete packageJson.dependencies.nativewind;
611
+ packageJson.dependencies.uniwind = UNIWIND_DEPENDENCIES.uniwind;
612
+ packageJson.devDependencies = {
613
+ ...packageJson.devDependencies,
614
+ ...UNIWIND_DEV_DEPENDENCIES,
615
+ };
616
+ delete packageJson.devDependencies.nativewind;
617
+ delete packageJson.devDependencies['prettier-plugin-tailwindcss'];
618
+ }
619
+ await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8');
620
+ }
621
+ async function resolveGuidelines(answers, options) {
622
+ if (!options.guidelinesTemplate && !options.guidelinesTemplatePath) {
623
+ return renderGuidelines(answers);
624
+ }
625
+ return renderGuidelinesTemplate(answers, options.guidelinesTemplatePath);
626
+ }
627
+ function applyGuidelinesTemplate(template, answers) {
628
+ const replacements = {
629
+ appName: answers.appName,
630
+ audience: answers.audience,
631
+ coreFlows: answers.coreFlows,
632
+ dataNeeds: answers.dataNeeds,
633
+ deploymentTarget: answers.deploymentTarget,
634
+ advancedPackageSetup: formatBoolean(answers.advancedPackageSetup),
635
+ includeCreateExpoComponents: formatBoolean(answers.includeCreateExpoComponents),
636
+ useLatestExpoSdk: formatBoolean(answers.useLatestExpoSdk),
637
+ targetPlatforms: answers.targetPlatforms.map((item) => `- ${item}`).join('\n'),
638
+ firstTargetPlatform: answers.firstTargetPlatform,
639
+ appDirectory: formatAppDirectory(answers.appDirectory),
640
+ platformFileStrategy: formatPlatformStrategy(answers.platformFileStrategy),
641
+ platformLayoutMode: formatPlatformLayoutMode(answers.platformLayoutMode),
642
+ webOutput: answers.webOutput,
643
+ deployedServer: formatServerChoice(answers.deployedServer),
644
+ usesExpoUi: formatBoolean(answers.usesExpoUi),
645
+ usesExpoNativeTabs: formatBoolean(answers.usesExpoNativeTabs),
646
+ easUses: answers.easUses.map((item) => `- ${item}`).join('\n') || '- not planned yet',
647
+ dataStart: formatDataStart(answers.dataStart),
648
+ testToMainSafeguards: formatBoolean(answers.testToMainSafeguards),
649
+ defaults: answers.defaults.map((item) => `- ${item}`).join('\n'),
650
+ };
651
+ let output = template;
652
+ for (const [key, value] of Object.entries(replacements)) {
653
+ output = output.split(`{{${key}}}`).join(value);
654
+ }
655
+ return output.endsWith('\n') ? output : `${output}\n`;
656
+ }
657
+ function formatBoolean(value) {
658
+ return value ? 'yes' : 'no';
659
+ }
660
+ function formatPlatformStrategy(value) {
661
+ return value === 'folders' ? 'platform-specific folders' : 'platform-specific files only';
662
+ }
663
+ function formatAppDirectory(value) {
664
+ return value === 'src' ? '`src/app`' : '`app`';
665
+ }
666
+ function formatPlatformLayoutMode(value) {
667
+ return value === 'platform-specific' ? 'platform-specific layouts' : 'shared layouts';
668
+ }
669
+ function formatServerChoice(value) {
670
+ switch (value) {
671
+ case 'standard-expo':
672
+ return 'yes, standard Expo server/API routes';
673
+ case 'custom':
674
+ return 'yes, custom server/backend';
675
+ case 'none':
676
+ return 'no deployed server planned';
677
+ }
678
+ }
679
+ function formatDataStart(value) {
680
+ return value === 'supabase' ? 'Supabase from the start' : 'local dummy data with Expo SQLite';
681
+ }
682
+ function formatServerAdapterSummary(answers) {
683
+ if (answers.webOutput === 'none')
684
+ return 'none (native-only)';
685
+ switch (answers.expoServerAdapter) {
686
+ case 'eas': return 'EAS hosting';
687
+ case 'express': return 'Express adapter (node server.js, port 3000)';
688
+ case 'bun': return 'Bun adapter (node server.js)';
689
+ case 'other': return 'custom (not yet specified)';
690
+ default: return formatServerChoice(answers.deployedServer);
691
+ }
692
+ }
693
+ function deriveServeProdScript(answers) {
694
+ if (answers.expoServerAdapter === 'express' || answers.expoServerAdapter === 'bun') {
695
+ return 'node server.js';
696
+ }
697
+ return 'npx expo serve';
698
+ }
699
+ function deriveServeProdFreshScript(answers) {
700
+ if (answers.expoServerAdapter === 'express' || answers.expoServerAdapter === 'bun') {
701
+ return 'npx @mr.dj2u/cli free-port 3000 && npm run build:web && node server.js';
702
+ }
703
+ return 'npx @mr.dj2u/cli free-port 8081 && npm run build:web && npx expo serve';
704
+ }
705
+ function formatStyleStack(answers) {
706
+ if (answers.defaults.includes('uniwind')) {
707
+ return 'Uniwind / Tailwind CSS v4';
708
+ }
709
+ return 'standard React Native StyleSheet';
710
+ }
711
+ function formatAuthSummary(answers) {
712
+ if (answers.dataStart === 'supabase' || answers.defaults.includes('supabase')) {
713
+ return 'Supabase auth (available via supabase-js)';
714
+ }
715
+ return 'no auth planned yet';
716
+ }
717
+ function formatCodeOrg(answers) {
718
+ const parts = [
719
+ formatPlatformStrategy(answers.platformFileStrategy),
720
+ `${formatAppDirectory(answers.appDirectory)} routes`,
721
+ formatPlatformLayoutMode(answers.platformLayoutMode),
722
+ ];
723
+ if (answers.webOutput === 'none') {
724
+ parts.push('no web');
725
+ }
726
+ else {
727
+ parts.push(`web: ${answers.webOutput}`);
728
+ }
729
+ return parts.join(', ');
730
+ }
731
+ function hasThinOnboardingAnswers(answers) {
732
+ const genericValues = new Set([
733
+ 'Expo app users',
734
+ 'Onboarding, primary app workflow, settings',
735
+ 'Agent should derive the first core user flows from project/info.md during intake.',
736
+ 'Local state first; add backend only when needed',
737
+ 'Expo web/native deployment',
738
+ ]);
739
+ return [answers.audience, answers.coreFlows, answers.dataNeeds, answers.deploymentTarget].some((value) => genericValues.has(value.trim()));
740
+ }
741
+ async function ensureUniwindMetroConfig(projectPath) {
742
+ const metroPath = path.join(projectPath, 'metro.config.js');
743
+ const existing = await readOptionalText(metroPath);
744
+ if (!existing) {
745
+ await writeFile(metroPath, renderUniwindMetroConfig(), 'utf8');
746
+ return;
747
+ }
748
+ if (existing.includes('withUniwindConfig')) {
749
+ return;
750
+ }
751
+ if (existing.includes('withNativeWind') || existing.includes("require('nativewind/metro')")) {
752
+ await writeFile(metroPath, renderUniwindMetroConfig(), 'utf8');
753
+ return;
754
+ }
755
+ const withImport = existing.includes("require('uniwind/metro')")
756
+ ? existing
757
+ : existing.replace(/const \{ getDefaultConfig \} = require\(['"]expo\/metro-config['"]\);\r?\n/, (match) => `${match}const { withUniwindConfig } = require('uniwind/metro');\n`);
758
+ const updated = withImport.replace(/module\.exports\s*=\s*config;\s*$/m, [
759
+ 'module.exports = withUniwindConfig(config, {',
760
+ " cssEntryFile: './global.css',",
761
+ " dtsFile: './src/uniwind-types.d.ts',",
762
+ '});',
763
+ '',
764
+ ].join('\n'));
765
+ if (updated !== existing) {
766
+ await writeFile(metroPath, updated, 'utf8');
767
+ }
768
+ }
769
+ async function ensureUniwindGlobalCss(projectPath) {
770
+ const globalCssPath = path.join(projectPath, 'global.css');
771
+ const existing = await readOptionalText(globalCssPath);
772
+ if (!existing || existing.includes("@import 'uniwind'") || existing.includes('@import "uniwind"')) {
773
+ return;
774
+ }
775
+ await writeFile(globalCssPath, renderGlobalCss(), 'utf8');
776
+ }
777
+ async function removeNativeWindArtifacts(projectPath) {
778
+ await removeOptionalFile(path.join(projectPath, 'nativewind-env.d.ts'));
779
+ await removeOptionalFileIfContains(path.join(projectPath, 'tailwind.config.js'), 'nativewind');
780
+ await removeNativeWindFromBabelConfig(path.join(projectPath, 'babel.config.js'));
781
+ await removeTailwindPrettierPluginConfig(path.join(projectPath, 'prettier.config.js'));
782
+ await removeTailwindPrettierPluginConfig(path.join(projectPath, 'prettier.config.mjs'));
783
+ }
784
+ async function removeNativeWindFromBabelConfig(filePath) {
785
+ const existing = await readOptionalText(filePath);
786
+ if (!existing || !existing.includes('nativewind')) {
787
+ return;
788
+ }
789
+ const updated = existing
790
+ .replace(/\[\s*(['"])babel-preset-expo\1\s*,\s*\{\s*jsxImportSource:\s*(['"])nativewind\2\s*\}\s*\]/g, "'babel-preset-expo'")
791
+ .replace(/,\s*(['"])nativewind\/babel\1/g, '')
792
+ .replace(/(['"])nativewind\/babel\1\s*,\s*/g, '')
793
+ .replace(/,\s*,/g, ',');
794
+ if (updated !== existing) {
795
+ await writeFile(filePath, updated, 'utf8');
796
+ }
797
+ }
798
+ async function removeTailwindPrettierPluginConfig(filePath) {
799
+ const existing = await readOptionalText(filePath);
800
+ if (!existing || !existing.includes('prettier-plugin-tailwindcss')) {
801
+ return;
802
+ }
803
+ const updated = existing
804
+ .replace(/^\s*plugins:\s*\[\s*require\.resolve\(['"]prettier-plugin-tailwindcss['"]\)\s*\],?\r?\n/m, '')
805
+ .replace(/^\s*tailwindAttributes:\s*\[[^\n]*\],?\r?\n/m, '')
806
+ .replace(/^\s*tailwindFunctions:\s*\[[^\n]*\],?\r?\n/m, '')
807
+ .replace(/\n{3,}/g, '\n\n');
808
+ if (updated !== existing) {
809
+ await writeFile(filePath, updated, 'utf8');
810
+ }
811
+ }
812
+ async function ensureGlobalCssImport(projectPath, appDirectory) {
813
+ const layoutPath = path.join(getExpoRouterAppDir(projectPath, appDirectory), '_layout.tsx');
814
+ const appPath = path.join(projectPath, 'App.tsx');
815
+ const layout = await readOptionalText(layoutPath);
816
+ if (layout) {
817
+ const importStatement = renderGlobalCssImport(layoutPath, projectPath);
818
+ const updated = layout.match(/^\s*import\s+['"][^'"]*global\.css['"];?\r?\n/m)
819
+ ? layout.replace(/^\s*import\s+['"][^'"]*global\.css['"];?\r?\n/m, `${importStatement}\n`)
820
+ : `${importStatement}\n${layout}`;
821
+ if (updated !== layout) {
822
+ await writeFile(layoutPath, updated, 'utf8');
823
+ }
824
+ return;
825
+ }
826
+ const app = await readOptionalText(appPath);
827
+ if (app && !app.includes('global.css')) {
828
+ await writeFile(appPath, `import './global.css';\n${app}`, 'utf8');
829
+ }
830
+ }
831
+ function getExpoRouterAppDir(projectPath, appDirectory) {
832
+ return appDirectory === 'src' ? path.join(projectPath, 'src', 'app') : path.join(projectPath, 'app');
833
+ }
834
+ function renderRouteExport(routeDir, targetModulePath) {
835
+ return `export { default } from '${toRelativeImportPath(routeDir, targetModulePath)}';\n`;
836
+ }
837
+ function renderGlobalCssImport(layoutPath, projectPath) {
838
+ return `import '${toRelativeImportPath(path.dirname(layoutPath), path.join(projectPath, 'global.css'))}';`;
839
+ }
840
+ async function canWriteRichRootLayout(layoutPath) {
841
+ const existing = await readOptionalText(layoutPath);
842
+ if (!existing) {
843
+ return true;
844
+ }
845
+ return !/\b(Tabs|Drawer)\b/.test(existing);
846
+ }
847
+ function toRelativeImportPath(fromDir, toPath) {
848
+ const normalized = path.relative(fromDir, toPath).replace(/\\/g, '/');
849
+ return normalized.startsWith('.') ? normalized : `./${normalized}`;
850
+ }
851
+ async function writeIfAllowed(filePath, contents, force) {
852
+ if (!force && (await fileExists(filePath))) {
853
+ return { filePath, wrote: false };
854
+ }
855
+ await mkdir(path.dirname(filePath), { recursive: true });
856
+ await writeFile(filePath, contents, 'utf8');
857
+ return { filePath, wrote: true };
858
+ }
859
+ async function writeProjectMemoryFile(filePath, contents, force, normalizeExisting) {
860
+ if (force || !(await fileExists(filePath))) {
861
+ await mkdir(path.dirname(filePath), { recursive: true });
862
+ await writeFile(filePath, contents, 'utf8');
863
+ return { filePath, wrote: true };
864
+ }
865
+ const existing = await readOptionalText(filePath);
866
+ if (existing === contents) {
867
+ return { filePath, wrote: false };
868
+ }
869
+ if (!normalizeExisting) {
870
+ return { filePath, wrote: false };
871
+ }
872
+ await mkdir(path.dirname(filePath), { recursive: true });
873
+ await writeFile(filePath, contents, 'utf8');
874
+ return { filePath, wrote: true };
875
+ }
876
+ function shouldGenerateIntakeAgentHandoff(answers, existingInfo, existingStyle) {
877
+ return (hasThinOnboardingAnswers(answers) ||
878
+ hasNonCanonicalContent(existingInfo, INFO_HEADINGS) ||
879
+ hasNonCanonicalContent(existingStyle, STYLE_HEADINGS));
880
+ }
881
+ function renderImportedNotes(existing, headings) {
882
+ const trimmed = existing?.trim();
883
+ if (!trimmed || !hasNonCanonicalContent(trimmed, headings)) {
884
+ return [];
885
+ }
886
+ return [
887
+ '## Imported Notes',
888
+ '',
889
+ 'The following notes existed before MDS normalized this file. An agent should move useful details into the correct sections during project intake.',
890
+ '',
891
+ '```md',
892
+ trimmed,
893
+ '```',
894
+ '',
895
+ ];
896
+ }
897
+ function hasNonCanonicalContent(existing, headings) {
898
+ const trimmed = existing?.trim();
899
+ if (!trimmed) {
900
+ return false;
901
+ }
902
+ return !headings.every((heading) => trimmed.includes(`## ${heading}`));
903
+ }
904
+ async function fileExists(filePath) {
905
+ try {
906
+ await readFile(filePath, 'utf8');
907
+ return true;
908
+ }
909
+ catch {
910
+ return false;
911
+ }
912
+ }
913
+ async function pathExists(filePath) {
914
+ try {
915
+ await access(filePath);
916
+ return true;
917
+ }
918
+ catch {
919
+ return false;
920
+ }
921
+ }
922
+ async function removeOptionalFile(filePath) {
923
+ try {
924
+ await unlink(filePath);
925
+ }
926
+ catch {
927
+ // The NativeWind artifact is optional and may not exist.
928
+ }
929
+ }
930
+ async function removeOptionalFileIfContains(filePath, token) {
931
+ const existing = await readOptionalText(filePath);
932
+ if (existing?.includes(token)) {
933
+ await removeOptionalFile(filePath);
934
+ }
935
+ }
936
+ async function readOptionalText(filePath) {
937
+ try {
938
+ return await readFile(filePath, 'utf8');
939
+ }
940
+ catch {
941
+ return null;
942
+ }
943
+ }
944
+ function renderGlobalCss() {
945
+ return ["@import 'tailwindcss';", "@import 'uniwind';", ''].join('\n');
946
+ }
947
+ function renderUniwindMetroConfig() {
948
+ return [
949
+ "const { getDefaultConfig } = require('expo/metro-config');",
950
+ "const { withUniwindConfig } = require('uniwind/metro');",
951
+ '',
952
+ 'const config = getDefaultConfig(__dirname);',
953
+ '',
954
+ 'module.exports = withUniwindConfig(config, {',
955
+ " cssEntryFile: './global.css',",
956
+ " dtsFile: './src/uniwind-types.d.ts',",
957
+ '});',
958
+ '',
959
+ ].join('\n');
960
+ }
961
+ function renderMockData(answers) {
962
+ return [
963
+ 'export interface AppTask {',
964
+ ' id: string;',
965
+ ' title: string;',
966
+ ' status: "todo" | "doing" | "done";',
967
+ '}',
968
+ '',
969
+ 'export interface AppSnapshot {',
970
+ ' name: string;',
971
+ ' audience: string;',
972
+ ' tasks: AppTask[];',
973
+ '}',
974
+ '',
975
+ 'export const appSnapshot: AppSnapshot = {',
976
+ ` name: ${JSON.stringify(answers.appName)},`,
977
+ ` audience: ${JSON.stringify(answers.audience)},`,
978
+ ' tasks: [',
979
+ " { id: 'task-1', title: 'Shape the first user flow', status: 'doing' },",
980
+ " { id: 'task-2', title: 'Replace mock data with the real data layer', status: 'todo' },",
981
+ " { id: 'task-3', title: 'Run mds doctor before pushing', status: 'todo' },",
982
+ ' ] satisfies AppTask[],',
983
+ '};',
984
+ '',
985
+ ].join('\n');
986
+ }
987
+ function renderLocalDataService(answers) {
988
+ if (answers.dataStart === 'supabase') {
989
+ return [
990
+ "import { appSnapshot } from '../data/mock-app';",
991
+ '',
992
+ 'export async function getLocalAppSnapshot(): Promise<typeof appSnapshot> {',
993
+ ' return appSnapshot;',
994
+ '}',
995
+ '',
996
+ 'export const dataAdapterNotes = [',
997
+ " 'This app is set to start with Supabase.',",
998
+ " 'Use this adapter boundary for reads and writes so feature screens do not import Supabase directly.',",
999
+ " 'Keep separate Supabase projects for test/staging and production.',",
1000
+ '];',
1001
+ '',
1002
+ ].join('\n');
1003
+ }
1004
+ return [
1005
+ "import { appSnapshot } from '../data/mock-app';",
1006
+ '',
1007
+ "import type { AppTask } from '../data/mock-app';",
1008
+ '',
1009
+ 'let tasks: AppTask[] = [...appSnapshot.tasks];',
1010
+ '',
1011
+ 'export async function ensureLocalDataReady(): Promise<void> {',
1012
+ ' tasks = tasks.length > 0 ? tasks : [...appSnapshot.tasks];',
1013
+ '}',
1014
+ '',
1015
+ 'export async function getLocalAppSnapshot(): Promise<typeof appSnapshot> {',
1016
+ ' await ensureLocalDataReady();',
1017
+ ' return {',
1018
+ ' ...appSnapshot,',
1019
+ ' tasks,',
1020
+ ' };',
1021
+ '}',
1022
+ '',
1023
+ "export async function addLocalTask(title = 'Try the local data adapter'): Promise<typeof appSnapshot> {",
1024
+ ' await ensureLocalDataReady();',
1025
+ ' tasks = [',
1026
+ ' ...tasks,',
1027
+ ' {',
1028
+ ' id: `task-${Date.now()}`,',
1029
+ ' title,',
1030
+ " status: 'todo',",
1031
+ ' },',
1032
+ ' ];',
1033
+ ' return getLocalAppSnapshot();',
1034
+ '}',
1035
+ '',
1036
+ 'export const dataAdapterNotes = [',
1037
+ " 'This default adapter is web-safe and keeps generated apps runnable immediately.',",
1038
+ " 'Native builds use local-data.native.ts for the Expo SQLite demo.',",
1039
+ " 'Keep screens behind this adapter boundary so SQLite or Supabase can be swapped later.',",
1040
+ '];',
1041
+ '',
1042
+ ].join('\n');
1043
+ }
1044
+ function renderNativeLocalDataService() {
1045
+ return [
1046
+ "import * as SQLite from 'expo-sqlite';",
1047
+ '',
1048
+ "import { appSnapshot } from '../data/mock-app';",
1049
+ '',
1050
+ "import type { AppTask } from '../data/mock-app';",
1051
+ '',
1052
+ "const dbPromise = SQLite.openDatabaseAsync('exposition.db');",
1053
+ '',
1054
+ 'async function getDb() {',
1055
+ ' return dbPromise;',
1056
+ '}',
1057
+ '',
1058
+ 'export async function ensureLocalDataReady(): Promise<void> {',
1059
+ ' const db = await getDb();',
1060
+ ' await db.execAsync(`',
1061
+ ' CREATE TABLE IF NOT EXISTS exposition_tasks (',
1062
+ ' id TEXT PRIMARY KEY NOT NULL,',
1063
+ ' title TEXT NOT NULL,',
1064
+ ' status TEXT NOT NULL',
1065
+ ' );',
1066
+ ' `);',
1067
+ " const row = await db.getFirstAsync<{ count: number }>('SELECT COUNT(*) as count FROM exposition_tasks');",
1068
+ ' if ((row?.count ?? 0) > 0) {',
1069
+ ' return;',
1070
+ ' }',
1071
+ '',
1072
+ ' for (const task of appSnapshot.tasks) {',
1073
+ ' await db.runAsync(',
1074
+ " 'INSERT INTO exposition_tasks (id, title, status) VALUES (?, ?, ?)',",
1075
+ ' task.id,',
1076
+ ' task.title,',
1077
+ ' task.status',
1078
+ ' );',
1079
+ ' }',
1080
+ '}',
1081
+ '',
1082
+ 'export async function getLocalAppSnapshot(): Promise<typeof appSnapshot> {',
1083
+ ' await ensureLocalDataReady();',
1084
+ ' const db = await getDb();',
1085
+ " const tasks = await db.getAllAsync<AppTask>('SELECT id, title, status FROM exposition_tasks ORDER BY id');",
1086
+ ' return {',
1087
+ ' ...appSnapshot,',
1088
+ ' tasks,',
1089
+ ' };',
1090
+ '}',
1091
+ '',
1092
+ "export async function addLocalTask(title = 'Try the local DB adapter'): Promise<typeof appSnapshot> {",
1093
+ ' await ensureLocalDataReady();',
1094
+ ' const db = await getDb();',
1095
+ ' const id = `task-${Date.now()}`;',
1096
+ " await db.runAsync('INSERT INTO exposition_tasks (id, title, status) VALUES (?, ?, ?)', id, title, 'todo');",
1097
+ ' return getLocalAppSnapshot();',
1098
+ '}',
1099
+ '',
1100
+ ].join('\n');
1101
+ }
1102
+ function renderRichRootLayout(projectPath, appDir) {
1103
+ return [
1104
+ renderGlobalCssImport(path.join(appDir, '_layout.tsx'), projectPath),
1105
+ "import { Stack } from 'expo-router';",
1106
+ "import { SafeAreaProvider } from 'react-native-safe-area-context';",
1107
+ '',
1108
+ 'export default function Layout() {',
1109
+ ' return (',
1110
+ ' <SafeAreaProvider>',
1111
+ ' <Stack>',
1112
+ " <Stack.Screen name=\"index\" options={{ title: 'Home' }} />",
1113
+ " <Stack.Screen name=\"onboarding\" options={{ title: 'Onboarding' }} />",
1114
+ " <Stack.Screen name=\"exposition/index\" options={{ title: 'Exposition' }} />",
1115
+ " <Stack.Screen name=\"exposition/style-guide\" options={{ title: 'Style Guide' }} />",
1116
+ " <Stack.Screen name=\"exposition/data\" options={{ title: 'Data' }} />",
1117
+ " <Stack.Screen name=\"settings\" options={{ presentation: 'modal', title: 'Settings' }} />",
1118
+ ' </Stack>',
1119
+ ' </SafeAreaProvider>',
1120
+ ' );',
1121
+ '}',
1122
+ '',
1123
+ ].join('\n');
1124
+ }
1125
+ function renderSupabaseClient() {
1126
+ return [
1127
+ "import AsyncStorage from '@react-native-async-storage/async-storage';",
1128
+ "import { createClient } from '@supabase/supabase-js';",
1129
+ '',
1130
+ 'const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL;',
1131
+ 'const supabasePublishableKey =',
1132
+ ' process.env.EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY ?? process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY;',
1133
+ '',
1134
+ 'export const supabase = supabaseUrl && supabasePublishableKey',
1135
+ ' ? createClient(supabaseUrl, supabasePublishableKey, {',
1136
+ ' auth: {',
1137
+ ' storage: AsyncStorage,',
1138
+ ' autoRefreshToken: true,',
1139
+ ' persistSession: true,',
1140
+ ' detectSessionInUrl: false,',
1141
+ ' },',
1142
+ ' })',
1143
+ ' : null;',
1144
+ '',
1145
+ 'export function assertSupabaseConfigured(): void {',
1146
+ ' if (!supabase) {',
1147
+ " throw new Error('Set EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY before using Supabase. EXPO_PUBLIC_SUPABASE_ANON_KEY is accepted as a fallback for older projects.');",
1148
+ ' }',
1149
+ '}',
1150
+ '',
1151
+ ].join('\n');
1152
+ }
1153
+ function renderGitHubPrChecksWorkflow() {
1154
+ return [
1155
+ 'name: MDS PR Checks',
1156
+ '',
1157
+ 'on:',
1158
+ ' pull_request:',
1159
+ ' branches: [test, main]',
1160
+ ' push:',
1161
+ ' branches: [test]',
1162
+ '',
1163
+ 'jobs:',
1164
+ ' verify:',
1165
+ ' runs-on: ubuntu-latest',
1166
+ ' steps:',
1167
+ ' - uses: actions/checkout@v4',
1168
+ ' - uses: actions/setup-node@v4',
1169
+ ' with:',
1170
+ ' node-version: 22',
1171
+ ' - name: Install dependencies',
1172
+ ' run: npm install',
1173
+ ' - name: Verify project',
1174
+ ' run: npm run ci:verify --if-present',
1175
+ '',
1176
+ ].join('\n');
1177
+ }
1178
+ function renderReleaseFlow(answers) {
1179
+ return [
1180
+ `# ${answers.appName} Release Flow`,
1181
+ '',
1182
+ '## Test-To-Main Safeguards',
1183
+ '',
1184
+ '- Build features on short-lived feature branches.',
1185
+ '- Open pull requests into `test` first.',
1186
+ '- Require the `MDS PR Checks` workflow to pass before merging into `test`.',
1187
+ '- Smoke test the app from `test` with staging data and staging Supabase keys when Supabase is used.',
1188
+ '- Promote from `test` to `main` only after validation.',
1189
+ '- Protect `main` so direct pushes are blocked and PR checks are required.',
1190
+ '',
1191
+ '## Supabase Environments',
1192
+ '',
1193
+ ...(answers.dataStart === 'supabase'
1194
+ ? [
1195
+ '- Use one Supabase project for test/staging and one Supabase project for production.',
1196
+ '- Keep publishable client keys in environment files for the matching branch/environment.',
1197
+ '- Never commit Supabase service-role or secret keys into the Expo app.',
1198
+ ]
1199
+ : [
1200
+ '- Local dummy data is the starting point.',
1201
+ '- When Supabase is introduced, create separate test/staging and production projects before wiring production data.',
1202
+ ]),
1203
+ '',
1204
+ '## GitHub Setup The User Still Needs To Do',
1205
+ '',
1206
+ '- Create `test` and `main` branches.',
1207
+ '- In GitHub branch protection, require pull requests and status checks for `test` and `main`.',
1208
+ '- Require the generated `MDS PR Checks` workflow before merge.',
1209
+ '',
1210
+ ].join('\n');
1211
+ }
1212
+ function renderAnimatedPressable() {
1213
+ return [
1214
+ "import type { ReactNode } from 'react';",
1215
+ "import { Pressable, StyleSheet, Text } from 'react-native';",
1216
+ "import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';",
1217
+ '',
1218
+ 'const AnimatedPressableBase = Animated.createAnimatedComponent(Pressable);',
1219
+ '',
1220
+ 'interface AnimatedPressableProps {',
1221
+ ' children?: ReactNode;',
1222
+ ' label?: string;',
1223
+ ' onPress?: () => void;',
1224
+ '}',
1225
+ '',
1226
+ "export function AnimatedPressable({ children, label = 'Reanimated press demo', onPress }: AnimatedPressableProps) {",
1227
+ ' const pressed = useSharedValue(0);',
1228
+ ' const animatedStyle = useAnimatedStyle(() => ({',
1229
+ ' transform: [{ scale: withTiming(pressed.value ? 0.97 : 1, { duration: 120 }) }],',
1230
+ ' }));',
1231
+ '',
1232
+ ' return (',
1233
+ ' <AnimatedPressableBase',
1234
+ ' onPress={onPress}',
1235
+ ' onPressIn={() => {',
1236
+ ' pressed.value = 1;',
1237
+ ' }}',
1238
+ ' onPressOut={() => {',
1239
+ ' pressed.value = 0;',
1240
+ ' }}',
1241
+ ' style={[styles.button, animatedStyle]}',
1242
+ ' >',
1243
+ ' {children ?? <Text style={styles.label}>{label}</Text>}',
1244
+ ' </AnimatedPressableBase>',
1245
+ ' );',
1246
+ '}',
1247
+ '',
1248
+ 'const styles = StyleSheet.create({',
1249
+ ' button: {',
1250
+ " backgroundColor: '#111827',",
1251
+ ' borderRadius: 10,',
1252
+ ' paddingHorizontal: 16,',
1253
+ ' paddingVertical: 12,',
1254
+ ' },',
1255
+ ' label: {',
1256
+ " color: '#ffffff',",
1257
+ ' fontSize: 15,',
1258
+ ' fontWeight: "700",',
1259
+ ' textAlign: "center",',
1260
+ ' },',
1261
+ '});',
1262
+ '',
1263
+ ].join('\n');
1264
+ }
1265
+ function renderGestureCard() {
1266
+ return [
1267
+ "import { StyleSheet, Text } from 'react-native';",
1268
+ "import { Gesture, GestureDetector } from 'react-native-gesture-handler';",
1269
+ "import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';",
1270
+ '',
1271
+ 'interface GestureCardProps {',
1272
+ ' title: string;',
1273
+ ' body: string;',
1274
+ '}',
1275
+ '',
1276
+ 'export function GestureCard({ title, body }: GestureCardProps) {',
1277
+ ' const offset = useSharedValue(0);',
1278
+ ' const pan = Gesture.Pan()',
1279
+ ' .onChange((event) => {',
1280
+ ' offset.value = event.translationX;',
1281
+ ' })',
1282
+ ' .onFinalize(() => {',
1283
+ ' offset.value = withSpring(0);',
1284
+ ' });',
1285
+ '',
1286
+ ' const style = useAnimatedStyle(() => ({',
1287
+ ' transform: [{ translateX: offset.value }],',
1288
+ ' }));',
1289
+ '',
1290
+ ' return (',
1291
+ ' <GestureDetector gesture={pan}>',
1292
+ ' <Animated.View style={[styles.card, style]}>',
1293
+ ' <Text style={styles.title}>{title}</Text>',
1294
+ ' <Text style={styles.body}>{body}</Text>',
1295
+ ' </Animated.View>',
1296
+ ' </GestureDetector>',
1297
+ ' );',
1298
+ '}',
1299
+ '',
1300
+ 'const styles = StyleSheet.create({',
1301
+ ' card: {',
1302
+ " backgroundColor: '#ffffff',",
1303
+ " borderColor: '#e5e7eb',",
1304
+ ' borderRadius: 12,',
1305
+ ' borderWidth: 1,',
1306
+ ' padding: 16,',
1307
+ ' shadowColor: "#000000",',
1308
+ ' shadowOpacity: 0.08,',
1309
+ ' shadowRadius: 10,',
1310
+ ' },',
1311
+ ' title: {',
1312
+ " color: '#111827',",
1313
+ ' fontSize: 16,',
1314
+ ' fontWeight: "700",',
1315
+ ' },',
1316
+ ' body: {',
1317
+ " color: '#4b5563',",
1318
+ ' fontSize: 14,',
1319
+ ' lineHeight: 20,',
1320
+ ' marginTop: 8,',
1321
+ ' },',
1322
+ '});',
1323
+ '',
1324
+ ].join('\n');
1325
+ }
1326
+ function renderKeyboardForm() {
1327
+ return [
1328
+ "import { StyleSheet, TextInput } from 'react-native';",
1329
+ "import { KeyboardAwareScrollView, KeyboardToolbar } from 'react-native-keyboard-controller';",
1330
+ '',
1331
+ 'export function KeyboardForm() {',
1332
+ ' return (',
1333
+ ' <>',
1334
+ ' <KeyboardAwareScrollView bottomOffset={72} contentContainerStyle={styles.form} style={styles.scroller}>',
1335
+ ' <TextInput placeholder="Project note" style={styles.input} />',
1336
+ ' <TextInput multiline placeholder="Details" style={[styles.input, styles.multiline]} />',
1337
+ ' </KeyboardAwareScrollView>',
1338
+ ' <KeyboardToolbar />',
1339
+ ' </>',
1340
+ ' );',
1341
+ '}',
1342
+ '',
1343
+ 'const styles = StyleSheet.create({',
1344
+ ' scroller: {',
1345
+ ' maxHeight: 220,',
1346
+ ' },',
1347
+ ' form: {',
1348
+ ' gap: 12,',
1349
+ ' paddingVertical: 8,',
1350
+ ' },',
1351
+ ' input: {',
1352
+ " borderColor: '#d1d5db',",
1353
+ ' borderRadius: 10,',
1354
+ ' borderWidth: 1,',
1355
+ ' minHeight: 44,',
1356
+ ' paddingHorizontal: 12,',
1357
+ ' },',
1358
+ ' multiline: {',
1359
+ ' minHeight: 88,',
1360
+ ' paddingTop: 10,',
1361
+ ' textAlignVertical: "top",',
1362
+ ' },',
1363
+ '});',
1364
+ '',
1365
+ ].join('\n');
1366
+ }
1367
+ function renderSvgMark() {
1368
+ return [
1369
+ "import Svg, { Circle, Path } from 'react-native-svg';",
1370
+ '',
1371
+ 'export function SvgMark() {',
1372
+ ' return (',
1373
+ ' <Svg width={44} height={44} viewBox="0 0 44 44" accessibilityRole="image">',
1374
+ ' <Circle cx={22} cy={22} r={20} fill="#111827" />',
1375
+ ' <Path d="M14 23.5 19.5 29 31 15" stroke="#ffffff" strokeWidth={3} strokeLinecap="round" strokeLinejoin="round" />',
1376
+ ' </Svg>',
1377
+ ' );',
1378
+ '}',
1379
+ '',
1380
+ ].join('\n');
1381
+ }
1382
+ function renderScreensCard() {
1383
+ return [
1384
+ "import { useEffect } from 'react';",
1385
+ "import { StyleSheet, Text, View } from 'react-native';",
1386
+ "import { enableScreens } from 'react-native-screens';",
1387
+ '',
1388
+ 'export function ScreensCard() {',
1389
+ ' useEffect(() => {',
1390
+ ' enableScreens(true);',
1391
+ ' }, []);',
1392
+ '',
1393
+ ' return (',
1394
+ ' <View style={styles.card}>',
1395
+ ' <Text style={styles.title}>Native Screens</Text>',
1396
+ ' <Text style={styles.body}>react-native-screens is enabled so navigation can use native screen primitives for better memory and lifecycle behavior.</Text>',
1397
+ ' </View>',
1398
+ ' );',
1399
+ '}',
1400
+ '',
1401
+ 'const styles = StyleSheet.create({',
1402
+ ' card: {',
1403
+ " backgroundColor: '#eef2ff',",
1404
+ " borderColor: '#c7d2fe',",
1405
+ ' borderRadius: 12,',
1406
+ ' borderWidth: 1,',
1407
+ ' padding: 16,',
1408
+ ' },',
1409
+ ' title: {',
1410
+ " color: '#312e81',",
1411
+ ' fontSize: 16,',
1412
+ ' fontWeight: "700",',
1413
+ ' },',
1414
+ ' body: {',
1415
+ " color: '#4338ca',",
1416
+ ' fontSize: 14,',
1417
+ ' lineHeight: 20,',
1418
+ ' marginTop: 8,',
1419
+ ' },',
1420
+ '});',
1421
+ '',
1422
+ ].join('\n');
1423
+ }
1424
+ function renderExpositionNotice() {
1425
+ return [
1426
+ "import { StyleSheet, Text, View } from 'react-native';",
1427
+ '',
1428
+ 'export function ExpositionNotice() {',
1429
+ ' return (',
1430
+ ' <View style={styles.notice}>',
1431
+ ' <Text style={styles.eyebrow}>Temporary exposition scaffold</Text>',
1432
+ ` <Text style={styles.body}>${EXPOSITION_NOTICE}</Text>`,
1433
+ ' </View>',
1434
+ ' );',
1435
+ '}',
1436
+ '',
1437
+ 'const styles = StyleSheet.create({',
1438
+ ' notice: {',
1439
+ " backgroundColor: '#fff7ed',",
1440
+ " borderColor: '#fed7aa',",
1441
+ ' borderRadius: 12,',
1442
+ ' borderWidth: 1,',
1443
+ ' gap: 6,',
1444
+ ' padding: 14,',
1445
+ ' },',
1446
+ ' eyebrow: {',
1447
+ " color: '#9a3412',",
1448
+ ' fontSize: 12,',
1449
+ ' fontWeight: "800",',
1450
+ ' letterSpacing: 0.4,',
1451
+ ' textTransform: "uppercase",',
1452
+ ' },',
1453
+ ' body: {',
1454
+ " color: '#7c2d12',",
1455
+ ' fontSize: 14,',
1456
+ ' lineHeight: 20,',
1457
+ ' },',
1458
+ '});',
1459
+ '',
1460
+ ].join('\n');
1461
+ }
1462
+ function renderPackageCard() {
1463
+ return [
1464
+ "import type { ReactNode } from 'react';",
1465
+ "import { StyleSheet, Text, View } from 'react-native';",
1466
+ '',
1467
+ 'interface PackageCardProps {',
1468
+ ' title: string;',
1469
+ ' packageName: string;',
1470
+ ' body: string;',
1471
+ ' children?: ReactNode;',
1472
+ '}',
1473
+ '',
1474
+ 'export function PackageCard({ title, packageName, body, children }: PackageCardProps) {',
1475
+ ' return (',
1476
+ ' <View style={styles.card}>',
1477
+ ' <Text style={styles.packageName}>{packageName}</Text>',
1478
+ ' <Text style={styles.title}>{title}</Text>',
1479
+ ' <Text style={styles.body}>{body}</Text>',
1480
+ ' {children ? <View style={styles.demo}>{children}</View> : null}',
1481
+ ' </View>',
1482
+ ' );',
1483
+ '}',
1484
+ '',
1485
+ 'const styles = StyleSheet.create({',
1486
+ ' card: {',
1487
+ " backgroundColor: '#ffffff',",
1488
+ " borderColor: '#e5e7eb',",
1489
+ ' borderRadius: 12,',
1490
+ ' borderWidth: 1,',
1491
+ ' gap: 8,',
1492
+ ' padding: 16,',
1493
+ ' },',
1494
+ ' packageName: {',
1495
+ " color: '#6b7280',",
1496
+ ' fontSize: 12,',
1497
+ ' fontWeight: "700",',
1498
+ ' },',
1499
+ ' title: {',
1500
+ " color: '#111827',",
1501
+ ' fontSize: 17,',
1502
+ ' fontWeight: "800",',
1503
+ ' },',
1504
+ ' body: {',
1505
+ " color: '#4b5563',",
1506
+ ' fontSize: 14,',
1507
+ ' lineHeight: 20,',
1508
+ ' },',
1509
+ ' demo: {',
1510
+ ' marginTop: 6,',
1511
+ ' },',
1512
+ '});',
1513
+ '',
1514
+ ].join('\n');
1515
+ }
1516
+ function renderExpositionComponentIndex() {
1517
+ return [
1518
+ "export { AnimatedPressable } from './animated-pressable';",
1519
+ "export { ExpositionNotice } from './notice';",
1520
+ "export { GestureCard } from './gesture-card';",
1521
+ "export { KeyboardForm } from './keyboard-form';",
1522
+ "export { PackageCard } from './package-card';",
1523
+ "export { ScreensCard } from './screens-card';",
1524
+ "export { SvgMark } from './svg-mark';",
1525
+ '',
1526
+ ].join('\n');
1527
+ }
1528
+ function renderHomeScreen(answers) {
1529
+ return [
1530
+ "import { Link } from 'expo-router';",
1531
+ "import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';",
1532
+ '',
1533
+ "import { GestureCard, SvgMark } from '../../components/exposition';",
1534
+ "import { appSnapshot } from '../../data/mock-app';",
1535
+ '',
1536
+ 'const expositionLinks = [',
1537
+ " { href: '/exposition' as const, title: 'Package exposition', body: 'Review included base packages and decide what stays.' },",
1538
+ " { href: '/exposition/style-guide' as const, title: 'Style guide', body: 'Test colors, type, motion, and component density.' },",
1539
+ " { href: '/exposition/data' as const, title: 'Data adapter', body: 'Try the local data boundary before replacing it.' },",
1540
+ '];',
1541
+ '',
1542
+ 'export default function HomeScreen() {',
1543
+ ' return (',
1544
+ ' <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.content} style={styles.screen}>',
1545
+ ' <View style={styles.header}>',
1546
+ ' <SvgMark />',
1547
+ ' <View style={styles.headerText}>',
1548
+ ` <Text style={styles.title}>${answers.appName}</Text>`,
1549
+ ' <Text style={styles.subtitle}>{appSnapshot.audience}</Text>',
1550
+ ' </View>',
1551
+ ' <Link href="/settings" asChild>',
1552
+ ' <Pressable accessibilityRole="button" style={styles.infoButton}>',
1553
+ ' <Text style={styles.infoButtonText}>i</Text>',
1554
+ ' </Pressable>',
1555
+ ' </Link>',
1556
+ ' </View>',
1557
+ ' <GestureCard',
1558
+ ' title="Rich boilerplate is wired"',
1559
+ ' body="Routes stay thin, feature screens hold UI, and the temporary exposition pages are reachable from this home screen."',
1560
+ ' />',
1561
+ ' <View style={styles.grid}>',
1562
+ ' <Link href="/onboarding" asChild>',
1563
+ ' <Pressable style={styles.primaryCard}>',
1564
+ ' <Text style={styles.primaryTitle}>Onboarding preview</Text>',
1565
+ ' <Text style={styles.primaryBody}>Open the generated onboarding screen before the main product flow replaces it.</Text>',
1566
+ ' </Pressable>',
1567
+ ' </Link>',
1568
+ ' {expositionLinks.map((item) => (',
1569
+ ' <Link key={item.href} href={item.href} asChild>',
1570
+ ' <Pressable style={styles.linkCard}>',
1571
+ ' <Text style={styles.linkTitle}>{item.title}</Text>',
1572
+ ' <Text style={styles.linkBody}>{item.body}</Text>',
1573
+ ' </Pressable>',
1574
+ ' </Link>',
1575
+ ' ))}',
1576
+ ' </View>',
1577
+ ' <View style={styles.taskList}>',
1578
+ ' <Text style={styles.sectionTitle}>Generated next steps</Text>',
1579
+ ' {appSnapshot.tasks.map((task) => (',
1580
+ ' <View key={task.id} style={styles.taskCard}>',
1581
+ ' <Text style={styles.taskTitle}>{task.title}</Text>',
1582
+ ' <Text style={styles.taskStatus}>{task.status}</Text>',
1583
+ ' </View>',
1584
+ ' ))}',
1585
+ ' </View>',
1586
+ ' </ScrollView>',
1587
+ ' );',
1588
+ '}',
1589
+ '',
1590
+ 'const styles = StyleSheet.create({',
1591
+ ' screen: {',
1592
+ " backgroundColor: '#f9fafb',",
1593
+ ' flex: 1,',
1594
+ ' },',
1595
+ ' content: {',
1596
+ ' gap: 16,',
1597
+ ' padding: 20,',
1598
+ ' },',
1599
+ ' header: {',
1600
+ ' alignItems: "center",',
1601
+ ' flexDirection: "row",',
1602
+ ' gap: 12,',
1603
+ ' },',
1604
+ ' headerText: {',
1605
+ ' flex: 1,',
1606
+ ' },',
1607
+ ' infoButton: {',
1608
+ ' alignItems: "center",',
1609
+ " backgroundColor: '#111827',",
1610
+ ' borderRadius: 18,',
1611
+ ' height: 36,',
1612
+ ' justifyContent: "center",',
1613
+ ' width: 36,',
1614
+ ' },',
1615
+ ' infoButtonText: {',
1616
+ " color: '#ffffff',",
1617
+ ' fontSize: 18,',
1618
+ ' fontWeight: "800",',
1619
+ ' },',
1620
+ ' title: {',
1621
+ " color: '#111827',",
1622
+ ' fontSize: 22,',
1623
+ ' fontWeight: "800",',
1624
+ ' },',
1625
+ ' subtitle: {',
1626
+ " color: '#4b5563',",
1627
+ ' fontSize: 14,',
1628
+ ' marginTop: 3,',
1629
+ ' },',
1630
+ ' grid: {',
1631
+ ' gap: 12,',
1632
+ ' },',
1633
+ ' primaryCard: {',
1634
+ " backgroundColor: '#111827',",
1635
+ ' borderRadius: 12,',
1636
+ ' gap: 8,',
1637
+ ' padding: 16,',
1638
+ ' },',
1639
+ ' primaryTitle: {',
1640
+ " color: '#ffffff',",
1641
+ ' fontSize: 18,',
1642
+ ' fontWeight: "800",',
1643
+ ' },',
1644
+ ' primaryBody: {',
1645
+ " color: '#d1d5db',",
1646
+ ' fontSize: 14,',
1647
+ ' lineHeight: 20,',
1648
+ ' },',
1649
+ ' linkCard: {',
1650
+ " backgroundColor: '#ffffff',",
1651
+ " borderColor: '#e5e7eb',",
1652
+ ' borderRadius: 12,',
1653
+ ' borderWidth: 1,',
1654
+ ' gap: 6,',
1655
+ ' padding: 16,',
1656
+ ' },',
1657
+ ' linkTitle: {',
1658
+ " color: '#111827',",
1659
+ ' fontSize: 16,',
1660
+ ' fontWeight: "800",',
1661
+ ' },',
1662
+ ' linkBody: {',
1663
+ " color: '#4b5563',",
1664
+ ' fontSize: 14,',
1665
+ ' lineHeight: 20,',
1666
+ ' },',
1667
+ ' taskList: {',
1668
+ ' gap: 10,',
1669
+ ' },',
1670
+ ' sectionTitle: {',
1671
+ " color: '#111827',",
1672
+ ' fontSize: 18,',
1673
+ ' fontWeight: "800",',
1674
+ ' },',
1675
+ ' taskCard: {',
1676
+ " backgroundColor: '#ffffff',",
1677
+ " borderColor: '#e5e7eb',",
1678
+ ' borderRadius: 10,',
1679
+ ' borderWidth: 1,',
1680
+ ' padding: 12,',
1681
+ ' },',
1682
+ ' taskTitle: {',
1683
+ " color: '#111827',",
1684
+ ' fontWeight: "700",',
1685
+ ' },',
1686
+ ' taskStatus: {',
1687
+ " color: '#6b7280',",
1688
+ ' fontSize: 12,',
1689
+ ' fontWeight: "800",',
1690
+ ' marginTop: 4,',
1691
+ ' textTransform: "uppercase",',
1692
+ ' },',
1693
+ '});',
1694
+ '',
1695
+ ].join('\n');
1696
+ }
1697
+ function renderOnboardingScreen() {
1698
+ return [
1699
+ "import { Link } from 'expo-router';",
1700
+ "import { StyleSheet, Text, View } from 'react-native';",
1701
+ '',
1702
+ "import { AnimatedPressable } from '../../components/exposition';",
1703
+ '',
1704
+ 'export default function OnboardingScreen() {',
1705
+ ' return (',
1706
+ ' <View style={styles.screen}>',
1707
+ ' <Text style={styles.title}>Start with intent</Text>',
1708
+ ' <Text style={styles.body}>',
1709
+ ' Replace this screen with the first real onboarding step once the product flow is settled.',
1710
+ ' </Text>',
1711
+ ' <Link href="/" asChild>',
1712
+ ' <AnimatedPressable label="Continue to home" />',
1713
+ ' </Link>',
1714
+ ' </View>',
1715
+ ' );',
1716
+ '}',
1717
+ '',
1718
+ 'const styles = StyleSheet.create({',
1719
+ ' screen: {',
1720
+ " backgroundColor: '#ffffff',",
1721
+ ' flex: 1,',
1722
+ ' gap: 16,',
1723
+ ' justifyContent: "center",',
1724
+ ' padding: 20,',
1725
+ ' },',
1726
+ ' title: {',
1727
+ " color: '#111827',",
1728
+ ' fontSize: 26,',
1729
+ ' fontWeight: "800",',
1730
+ ' },',
1731
+ ' body: {',
1732
+ " color: '#4b5563',",
1733
+ ' fontSize: 16,',
1734
+ ' lineHeight: 24,',
1735
+ ' },',
1736
+ '});',
1737
+ '',
1738
+ ].join('\n');
1739
+ }
1740
+ function renderSettingsScreen() {
1741
+ return [
1742
+ "import { StyleSheet, Text, View } from 'react-native';",
1743
+ '',
1744
+ "import { KeyboardForm } from '../../components/exposition';",
1745
+ '',
1746
+ 'export default function SettingsScreen() {',
1747
+ ' return (',
1748
+ ' <View style={styles.screen}>',
1749
+ ' <View style={styles.header}>',
1750
+ ' <Text style={styles.title}>Settings</Text>',
1751
+ ' <Text style={styles.body}>Keyboard Controller is ready for form-heavy screens.</Text>',
1752
+ ' </View>',
1753
+ ' <KeyboardForm />',
1754
+ ' </View>',
1755
+ ' );',
1756
+ '}',
1757
+ '',
1758
+ 'const styles = StyleSheet.create({',
1759
+ ' screen: {',
1760
+ " backgroundColor: '#ffffff',",
1761
+ ' flex: 1,',
1762
+ ' padding: 20,',
1763
+ ' },',
1764
+ ' header: {',
1765
+ ' marginBottom: 12,',
1766
+ ' },',
1767
+ ' title: {',
1768
+ " color: '#111827',",
1769
+ ' fontSize: 26,',
1770
+ ' fontWeight: "800",',
1771
+ ' },',
1772
+ ' body: {',
1773
+ " color: '#4b5563',",
1774
+ ' fontSize: 14,',
1775
+ ' lineHeight: 20,',
1776
+ ' marginTop: 4,',
1777
+ ' },',
1778
+ '});',
1779
+ '',
1780
+ ].join('\n');
1781
+ }
1782
+ function renderExpositionScreen(answers) {
1783
+ return [
1784
+ "import { ScrollView, StyleSheet, Text, View } from 'react-native';",
1785
+ '',
1786
+ "import { AnimatedPressable, ExpositionNotice, GestureCard, KeyboardForm, PackageCard, ScreensCard, SvgMark } from '../../components/exposition';",
1787
+ '',
1788
+ 'export default function ExpositionScreen() {',
1789
+ ' return (',
1790
+ ' <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.content} style={styles.screen}>',
1791
+ ` <Text style={styles.title}>${answers.appName} Exposition</Text>`,
1792
+ ' <Text style={styles.intro}>Browse the included base packages, then delete what the app does not need.</Text>',
1793
+ ' <ExpositionNotice />',
1794
+ ' <PackageCard',
1795
+ ' packageName="react-native-reanimated + react-native-worklets"',
1796
+ ' title="Motion that feels native"',
1797
+ ' body="Press the button to see the Reanimated timing demo. Worklets make this kind of UI-thread animation possible."',
1798
+ ' >',
1799
+ ' <AnimatedPressable label="Press and hold" />',
1800
+ ' </PackageCard>',
1801
+ ' <PackageCard',
1802
+ ' packageName="react-native-gesture-handler"',
1803
+ ' title="Gesture-first interactions"',
1804
+ ' body="Drag the card below. If your product does not need touch-heavy interactions, this demo helps you decide what to remove."',
1805
+ ' >',
1806
+ ' <GestureCard title="Drag me" body="This card springs back when the gesture ends." />',
1807
+ ' </PackageCard>',
1808
+ ' <PackageCard',
1809
+ ' packageName="react-native-screens"',
1810
+ ' title="Native navigation primitives"',
1811
+ ' body="Screens support the navigation layer with native lifecycle and memory behavior."',
1812
+ ' >',
1813
+ ' <ScreensCard />',
1814
+ ' </PackageCard>',
1815
+ ' <PackageCard',
1816
+ ' packageName="react-native-svg"',
1817
+ ' title="Portable vector UI"',
1818
+ ' body="Use SVG for marks, badges, charts, and vector states that need to scale cleanly."',
1819
+ ' >',
1820
+ ' <View style={styles.svgDemo}><SvgMark /></View>',
1821
+ ' </PackageCard>',
1822
+ ' <PackageCard',
1823
+ ' packageName="react-native-keyboard-controller"',
1824
+ ' title="Keyboard-heavy screens"',
1825
+ ' body="Use this when forms, chat, notes, or auth flows need better keyboard control than manual offsets."',
1826
+ ' >',
1827
+ ' <KeyboardForm />',
1828
+ ' </PackageCard>',
1829
+ ' </ScrollView>',
1830
+ ' );',
1831
+ '}',
1832
+ '',
1833
+ 'const styles = StyleSheet.create({',
1834
+ ' screen: {',
1835
+ " backgroundColor: '#f9fafb',",
1836
+ ' flex: 1,',
1837
+ ' },',
1838
+ ' content: {',
1839
+ ' gap: 16,',
1840
+ ' padding: 20,',
1841
+ ' },',
1842
+ ' title: {',
1843
+ " color: '#111827',",
1844
+ ' fontSize: 30,',
1845
+ ' fontWeight: "900",',
1846
+ ' },',
1847
+ ' intro: {',
1848
+ " color: '#4b5563',",
1849
+ ' fontSize: 16,',
1850
+ ' lineHeight: 24,',
1851
+ ' },',
1852
+ ' svgDemo: {',
1853
+ ' alignItems: "center",',
1854
+ ' paddingVertical: 8,',
1855
+ ' },',
1856
+ '});',
1857
+ '',
1858
+ ].join('\n');
1859
+ }
1860
+ function renderStyleGuideScreen(answers) {
1861
+ return [
1862
+ "import { ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';",
1863
+ '',
1864
+ "import { AnimatedPressable, ExpositionNotice } from '../../components/exposition';",
1865
+ '',
1866
+ 'const colors = [',
1867
+ " ['Ink', '#111827'],",
1868
+ " ['Cloud', '#f9fafb'],",
1869
+ " ['Accent', '#2563eb'],",
1870
+ " ['Success', '#16a34a'],",
1871
+ " ['Warning', '#f97316'],",
1872
+ '];',
1873
+ '',
1874
+ 'export default function StyleGuideScreen() {',
1875
+ ' return (',
1876
+ ' <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.content} style={styles.screen}>',
1877
+ ` <Text style={styles.title}>${answers.appName} Style Guide</Text>`,
1878
+ ' <Text style={styles.intro}>Use this page to explore type, spacing, color, and component tone with the client before the production UI hardens.</Text>',
1879
+ ' <ExpositionNotice />',
1880
+ ' <View style={styles.section}>',
1881
+ ' <Text style={styles.sectionTitle}>Color Palette</Text>',
1882
+ ' <View style={styles.swatchGrid}>',
1883
+ ' {colors.map(([name, color]) => (',
1884
+ ' <View key={name} style={styles.swatchItem}>',
1885
+ ' <View style={[styles.swatch, { backgroundColor: color }]} />',
1886
+ ' <Text style={styles.swatchLabel}>{name}</Text>',
1887
+ ' <Text style={styles.swatchValue}>{color}</Text>',
1888
+ ' </View>',
1889
+ ' ))}',
1890
+ ' </View>',
1891
+ ' </View>',
1892
+ ' <View style={styles.section}>',
1893
+ ' <Text style={styles.sectionTitle}>Typography</Text>',
1894
+ ' <Text style={styles.display}>Display headline</Text>',
1895
+ ' <Text style={styles.heading}>Section heading</Text>',
1896
+ ' <Text style={styles.body}>Readable body copy for product screens, onboarding, settings, and forms.</Text>',
1897
+ ' <Text style={styles.caption}>Caption and metadata text</Text>',
1898
+ ' </View>',
1899
+ ' <View style={styles.section}>',
1900
+ ' <Text style={styles.sectionTitle}>Controls</Text>',
1901
+ ' <AnimatedPressable label="Primary action" />',
1902
+ ' <TextInput placeholder="Input state" style={styles.input} />',
1903
+ ' </View>',
1904
+ ' <View style={styles.section}>',
1905
+ ' <Text style={styles.sectionTitle}>Card Language</Text>',
1906
+ ' <View style={styles.card}>',
1907
+ ' <Text style={styles.heading}>Decision card</Text>',
1908
+ ' <Text style={styles.body}>Use cards like this to compare concepts during research, then promote only the useful patterns into production components.</Text>',
1909
+ ' </View>',
1910
+ ' </View>',
1911
+ ' </ScrollView>',
1912
+ ' );',
1913
+ '}',
1914
+ '',
1915
+ 'const styles = StyleSheet.create({',
1916
+ ' screen: {',
1917
+ " backgroundColor: '#ffffff',",
1918
+ ' flex: 1,',
1919
+ ' },',
1920
+ ' content: {',
1921
+ ' gap: 18,',
1922
+ ' padding: 20,',
1923
+ ' },',
1924
+ ' title: {',
1925
+ " color: '#111827',",
1926
+ ' fontSize: 30,',
1927
+ ' fontWeight: "900",',
1928
+ ' },',
1929
+ ' intro: {',
1930
+ " color: '#4b5563',",
1931
+ ' fontSize: 16,',
1932
+ ' lineHeight: 24,',
1933
+ ' },',
1934
+ ' section: {',
1935
+ " backgroundColor: '#f9fafb',",
1936
+ ' borderRadius: 12,',
1937
+ ' gap: 12,',
1938
+ ' padding: 16,',
1939
+ ' },',
1940
+ ' sectionTitle: {',
1941
+ " color: '#111827',",
1942
+ ' fontSize: 18,',
1943
+ ' fontWeight: "800",',
1944
+ ' },',
1945
+ ' swatchGrid: {',
1946
+ ' flexDirection: "row",',
1947
+ ' flexWrap: "wrap",',
1948
+ ' gap: 12,',
1949
+ ' },',
1950
+ ' swatchItem: {',
1951
+ ' minWidth: 92,',
1952
+ ' },',
1953
+ ' swatch: {',
1954
+ ' borderRadius: 10,',
1955
+ ' height: 44,',
1956
+ ' },',
1957
+ ' swatchLabel: {',
1958
+ " color: '#111827',",
1959
+ ' fontWeight: "700",',
1960
+ ' marginTop: 6,',
1961
+ ' },',
1962
+ ' swatchValue: {',
1963
+ " color: '#6b7280',",
1964
+ ' fontSize: 12,',
1965
+ ' },',
1966
+ ' display: {',
1967
+ " color: '#111827',",
1968
+ ' fontSize: 32,',
1969
+ ' fontWeight: "900",',
1970
+ ' },',
1971
+ ' heading: {',
1972
+ " color: '#111827',",
1973
+ ' fontSize: 20,',
1974
+ ' fontWeight: "800",',
1975
+ ' },',
1976
+ ' body: {',
1977
+ " color: '#4b5563',",
1978
+ ' fontSize: 15,',
1979
+ ' lineHeight: 22,',
1980
+ ' },',
1981
+ ' caption: {',
1982
+ " color: '#6b7280',",
1983
+ ' fontSize: 12,',
1984
+ ' fontWeight: "700",',
1985
+ ' textTransform: "uppercase",',
1986
+ ' },',
1987
+ ' input: {',
1988
+ " borderColor: '#d1d5db',",
1989
+ ' borderRadius: 10,',
1990
+ ' borderWidth: 1,',
1991
+ ' minHeight: 44,',
1992
+ ' paddingHorizontal: 12,',
1993
+ ' },',
1994
+ ' card: {',
1995
+ " backgroundColor: '#ffffff',",
1996
+ " borderColor: '#e5e7eb',",
1997
+ ' borderRadius: 12,',
1998
+ ' borderWidth: 1,',
1999
+ ' gap: 8,',
2000
+ ' padding: 16,',
2001
+ ' },',
2002
+ '});',
2003
+ '',
2004
+ ].join('\n');
2005
+ }
2006
+ function renderDataScreen(answers) {
2007
+ if (answers.dataStart === 'supabase') {
2008
+ return renderSupabaseDataScreen(answers);
2009
+ }
2010
+ return [
2011
+ "import { useEffect, useState } from 'react';",
2012
+ "import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';",
2013
+ '',
2014
+ "import { ExpositionNotice } from '../../components/exposition';",
2015
+ "import { addLocalTask, getLocalAppSnapshot } from '../../services/local-data';",
2016
+ '',
2017
+ "import type { appSnapshot } from '../../data/mock-app';",
2018
+ '',
2019
+ 'type Snapshot = typeof appSnapshot;',
2020
+ '',
2021
+ 'export default function DataScreen() {',
2022
+ ' const [snapshot, setSnapshot] = useState<Snapshot | null>(null);',
2023
+ '',
2024
+ ' useEffect(() => {',
2025
+ ' void getLocalAppSnapshot().then(setSnapshot);',
2026
+ ' }, []);',
2027
+ '',
2028
+ ' async function addTask() {',
2029
+ ' setSnapshot(await addLocalTask());',
2030
+ ' }',
2031
+ '',
2032
+ ' return (',
2033
+ ' <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.content} style={styles.screen}>',
2034
+ ' <Text style={styles.title}>Data Exposition</Text>',
2035
+ ' <Text style={styles.intro}>This app starts with a web-safe local adapter and a native Expo SQLite adapter. Keep the boundary, then swap implementation details when Supabase is ready.</Text>',
2036
+ ' <ExpositionNotice />',
2037
+ ' <Pressable onPress={addTask} style={styles.button}>',
2038
+ ' <Text style={styles.buttonText}>Insert a local task</Text>',
2039
+ ' </Pressable>',
2040
+ ' {snapshot?.tasks.map((task) => (',
2041
+ ' <View key={task.id} style={styles.taskCard}>',
2042
+ ' <Text style={styles.taskTitle}>{task.title}</Text>',
2043
+ ' <Text style={styles.taskStatus}>{task.status}</Text>',
2044
+ ' </View>',
2045
+ ' ))}',
2046
+ ' <View style={styles.guidance}>',
2047
+ ' <Text style={styles.sectionTitle}>Later Supabase replacement</Text>',
2048
+ ' <Text style={styles.body}>Create matching tables, move reads/writes into this adapter, then keep screens unchanged. Use separate Supabase projects for test/staging and production so test-to-main promotion never writes directly into production data.</Text>',
2049
+ ' </View>',
2050
+ ' </ScrollView>',
2051
+ ' );',
2052
+ '}',
2053
+ '',
2054
+ ...renderDataScreenStyles(),
2055
+ ].join('\n');
2056
+ }
2057
+ function renderSupabaseDataScreen(answers) {
2058
+ return [
2059
+ "import { ScrollView, StyleSheet, Text, View } from 'react-native';",
2060
+ '',
2061
+ "import { ExpositionNotice } from '../../components/exposition';",
2062
+ '',
2063
+ 'export default function DataScreen() {',
2064
+ ' return (',
2065
+ ' <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.content} style={styles.screen}>',
2066
+ ' <Text style={styles.title}>Data Exposition</Text>',
2067
+ ` <Text style={styles.intro}>${answers.appName} is set to start with Supabase. Keep the adapter boundary in src/services so screens stay independent from backend details.</Text>`,
2068
+ ' <ExpositionNotice />',
2069
+ ' <View style={styles.guidance}>',
2070
+ ' <Text style={styles.sectionTitle}>Two Supabase projects</Text>',
2071
+ ' <Text style={styles.body}>Create one Supabase project for test/staging and one for production. Point feature branches and the test branch at the test project, then only promote validated work to the production project from main.</Text>',
2072
+ ' </View>',
2073
+ ' <View style={styles.guidance}>',
2074
+ ' <Text style={styles.sectionTitle}>Client setup</Text>',
2075
+ ' <Text style={styles.body}>Use EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY for client access. Never put service-role or secret keys in Expo client code.</Text>',
2076
+ ' </View>',
2077
+ ' <View style={styles.guidance}>',
2078
+ ' <Text style={styles.sectionTitle}>Migration path</Text>',
2079
+ ' <Text style={styles.body}>Design tables from project/info.md, enable RLS on exposed schemas, and map local demo concepts into Supabase queries inside the service adapter.</Text>',
2080
+ ' </View>',
2081
+ ' </ScrollView>',
2082
+ ' );',
2083
+ '}',
2084
+ '',
2085
+ ...renderDataScreenStyles(),
2086
+ ].join('\n');
2087
+ }
2088
+ function renderDataScreenStyles() {
2089
+ return [
2090
+ 'const styles = StyleSheet.create({',
2091
+ ' screen: {',
2092
+ " backgroundColor: '#f9fafb',",
2093
+ ' flex: 1,',
2094
+ ' },',
2095
+ ' content: {',
2096
+ ' gap: 14,',
2097
+ ' padding: 20,',
2098
+ ' },',
2099
+ ' title: {',
2100
+ " color: '#111827',",
2101
+ ' fontSize: 30,',
2102
+ ' fontWeight: "900",',
2103
+ ' },',
2104
+ ' intro: {',
2105
+ " color: '#4b5563',",
2106
+ ' fontSize: 16,',
2107
+ ' lineHeight: 24,',
2108
+ ' },',
2109
+ ' button: {',
2110
+ " backgroundColor: '#111827',",
2111
+ ' borderRadius: 10,',
2112
+ ' paddingHorizontal: 16,',
2113
+ ' paddingVertical: 12,',
2114
+ ' },',
2115
+ ' buttonText: {',
2116
+ " color: '#ffffff',",
2117
+ ' fontSize: 15,',
2118
+ ' fontWeight: "800",',
2119
+ ' textAlign: "center",',
2120
+ ' },',
2121
+ ' taskCard: {',
2122
+ " backgroundColor: '#ffffff',",
2123
+ " borderColor: '#e5e7eb',",
2124
+ ' borderRadius: 10,',
2125
+ ' borderWidth: 1,',
2126
+ ' padding: 12,',
2127
+ ' },',
2128
+ ' taskTitle: {',
2129
+ " color: '#111827',",
2130
+ ' fontWeight: "700",',
2131
+ ' },',
2132
+ ' taskStatus: {',
2133
+ " color: '#6b7280',",
2134
+ ' fontSize: 12,',
2135
+ ' fontWeight: "800",',
2136
+ ' marginTop: 4,',
2137
+ ' textTransform: "uppercase",',
2138
+ ' },',
2139
+ ' guidance: {',
2140
+ " backgroundColor: '#ffffff',",
2141
+ " borderColor: '#e5e7eb',",
2142
+ ' borderRadius: 12,',
2143
+ ' borderWidth: 1,',
2144
+ ' gap: 8,',
2145
+ ' padding: 16,',
2146
+ ' },',
2147
+ ' sectionTitle: {',
2148
+ " color: '#111827',",
2149
+ ' fontSize: 18,',
2150
+ ' fontWeight: "800",',
2151
+ ' },',
2152
+ ' body: {',
2153
+ " color: '#4b5563',",
2154
+ ' fontSize: 14,',
2155
+ ' lineHeight: 21,',
2156
+ ' },',
2157
+ '});',
2158
+ '',
2159
+ ];
2160
+ }
2161
+ //# sourceMappingURL=project-memory.js.map