@meadow-studio/platform 0.0.0-stage → 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 (79) hide show
  1. package/MeadowPlatform.podspec +17 -0
  2. package/Package.swift +23 -0
  3. package/README.md +147 -2
  4. package/android/build.gradle +42 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/java/app/meadow/platform/MeadowAppRatingPlugin.java +38 -0
  7. package/dist/cli/commands.d.ts +74 -0
  8. package/dist/cli/commands.d.ts.map +1 -0
  9. package/dist/cli/commands.js +1062 -0
  10. package/dist/cli/commands.js.map +1 -0
  11. package/dist/cli/config.d.ts +123 -0
  12. package/dist/cli/config.d.ts.map +1 -0
  13. package/dist/cli/config.js +428 -0
  14. package/dist/cli/config.js.map +1 -0
  15. package/dist/cli/meadow.d.ts +8 -0
  16. package/dist/cli/meadow.d.ts.map +1 -0
  17. package/dist/cli/meadow.js +463 -0
  18. package/dist/cli/meadow.js.map +1 -0
  19. package/dist/cli/recipes.d.ts +10 -0
  20. package/dist/cli/recipes.d.ts.map +1 -0
  21. package/dist/cli/recipes.js +575 -0
  22. package/dist/cli/recipes.js.map +1 -0
  23. package/dist/cli/sync.d.ts +14 -0
  24. package/dist/cli/sync.d.ts.map +1 -0
  25. package/dist/cli/sync.js +310 -0
  26. package/dist/cli/sync.js.map +1 -0
  27. package/dist/cli/templates.d.ts +27 -0
  28. package/dist/cli/templates.d.ts.map +1 -0
  29. package/dist/cli/templates.js +1246 -0
  30. package/dist/cli/templates.js.map +1 -0
  31. package/dist/cli/website.d.ts +5 -0
  32. package/dist/cli/website.d.ts.map +1 -0
  33. package/dist/cli/website.js +262 -0
  34. package/dist/cli/website.js.map +1 -0
  35. package/dist/index.d.ts +5 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +5 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/notifications.d.ts +119 -0
  40. package/dist/notifications.d.ts.map +1 -0
  41. package/dist/notifications.js +320 -0
  42. package/dist/notifications.js.map +1 -0
  43. package/dist/platform.d.ts +163 -0
  44. package/dist/platform.d.ts.map +1 -0
  45. package/dist/platform.js +623 -0
  46. package/dist/platform.js.map +1 -0
  47. package/dist/purchases.d.ts +88 -0
  48. package/dist/purchases.d.ts.map +1 -0
  49. package/dist/purchases.js +296 -0
  50. package/dist/purchases.js.map +1 -0
  51. package/dist/ui/app-build.component.d.ts +18 -0
  52. package/dist/ui/app-build.component.d.ts.map +1 -0
  53. package/dist/ui/app-build.component.js +56 -0
  54. package/dist/ui/app-build.component.js.map +1 -0
  55. package/dist/ui/app-build.model.d.ts +11 -0
  56. package/dist/ui/app-build.model.d.ts.map +1 -0
  57. package/dist/ui/app-build.model.js +18 -0
  58. package/dist/ui/app-build.model.js.map +1 -0
  59. package/dist/ui/app-links.component.d.ts +14 -0
  60. package/dist/ui/app-links.component.d.ts.map +1 -0
  61. package/dist/ui/app-links.component.js +77 -0
  62. package/dist/ui/app-links.component.js.map +1 -0
  63. package/dist/ui/app-links.model.d.ts +18 -0
  64. package/dist/ui/app-links.model.d.ts.map +1 -0
  65. package/dist/ui/app-links.model.js +17 -0
  66. package/dist/ui/app-links.model.js.map +1 -0
  67. package/dist/ui/index.d.ts +5 -0
  68. package/dist/ui/index.d.ts.map +1 -0
  69. package/dist/ui/index.js +5 -0
  70. package/dist/ui/index.js.map +1 -0
  71. package/docs/CLI.md +376 -0
  72. package/docs/PLATFORM_GUIDE.md +422 -0
  73. package/docs/POC_FINDINGS.md +218 -0
  74. package/docs/PROJECT_BRIEF.md +501 -0
  75. package/docs/RELEASING.md +49 -0
  76. package/docs/V0.1_PATH.md +310 -0
  77. package/ios/Sources/MeadowPlatformPlugin/MeadowAppRatingPlugin.swift +39 -0
  78. package/package.json +89 -4
  79. package/schema/meadow-config.schema.json +250 -0
@@ -0,0 +1,1062 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { createProjectConfig, readProjectConfig, validateProjectConfig, writeProjectConfig, } from './config.js';
5
+ import { generatedFiles, meadowAgentSection, MEADOW_AGENT_SECTION_END, MEADOW_AGENT_SECTION_START, TOOLCHAIN, } from './templates.js';
6
+ import { addLocalData, addNotifications, JEEP_SQLITE_PACKAGE_VERSION, LOCAL_DATA_PACKAGE_VERSION, LOCAL_NOTIFICATIONS_PACKAGE_VERSION, SQL_JS_PACKAGE_VERSION, } from './recipes.js';
7
+ import { syncProject } from './sync.js';
8
+ import { MEADOW_WEBSITE_MARKER, websiteConfig, websiteFiles } from './website.js';
9
+ export const SYSTEM_COMMAND_RUNNER = Object.freeze({
10
+ run(command, args, directory) {
11
+ const result = spawnSync(command, args, spawnOptions(directory, 'inherit'));
12
+ if (result.error)
13
+ throw result.error;
14
+ if (result.status !== 0) {
15
+ throw new Error(`${command} ${args.join(' ')} exited with status ${result.status ?? 'unknown'}.`);
16
+ }
17
+ },
18
+ succeeds(command, args, directory) {
19
+ const result = spawnSync(command, args, spawnOptions(directory, 'ignore'));
20
+ return !result.error && result.status === 0;
21
+ },
22
+ });
23
+ export function createProject(options, runner = SYSTEM_COMMAND_RUNNER) {
24
+ const config = createProjectConfig(options.answers);
25
+ const target = resolve(options.directory, config.app.slug);
26
+ if (existsSync(target))
27
+ throw new Error(`Target already exists: ${target}`);
28
+ mkdirSync(options.directory, { recursive: true });
29
+ const scratch = mkdtempSync(join(options.directory, `.${config.app.slug}.meadow-`));
30
+ let completed = false;
31
+ try {
32
+ writeGeneratedProject(scratch, generatedFiles(config, options.platformVersion, options.platformSpec));
33
+ if (options.install) {
34
+ runner.run('npm', ['install'], scratch);
35
+ runner.run('npm', ['run', 'format'], scratch);
36
+ runner.run('npm', ['run', 'build:device'], scratch);
37
+ for (const platform of config.app.platforms) {
38
+ runner.run(localBinary(scratch, 'cap'), ['add', platform], scratch);
39
+ }
40
+ const syncResult = syncProject(scratch);
41
+ if (!syncResult.valid)
42
+ throw new Error('Generated project could not be synchronized.');
43
+ runner.run('npm', ['run', 'check'], scratch);
44
+ if (options.visualBaselines) {
45
+ runner.run(localBinary(scratch, 'playwright'), ['install', 'chromium'], scratch);
46
+ runner.run('npm', ['run', 'test:visual:update'], scratch);
47
+ }
48
+ }
49
+ if (options.git)
50
+ initializeGit(scratch, config, runner);
51
+ renameSync(scratch, target);
52
+ completed = true;
53
+ if (options.github)
54
+ createGithubRepository(target, config, options.github, runner);
55
+ return target;
56
+ }
57
+ finally {
58
+ if (!completed && existsSync(scratch))
59
+ rmSync(scratch, { recursive: true, force: true });
60
+ }
61
+ }
62
+ export function isCleanGitRepository(directory) {
63
+ const root = resolve(directory);
64
+ return existsSync(root) && statSync(root).isDirectory() &&
65
+ existsSync(resolve(root, '.git')) &&
66
+ readdirSync(root).every((entry) => entry === '.git');
67
+ }
68
+ export function initCleanRepository(options, runner = SYSTEM_COMMAND_RUNNER) {
69
+ const root = resolve(options.directory);
70
+ if (!isCleanGitRepository(root)) {
71
+ throw new Error('Clean-repository init requires an existing Git checkout containing only .git.');
72
+ }
73
+ const config = createProjectConfig(options.answers);
74
+ const files = generatedFiles(config, options.platformVersion, options.platformSpec);
75
+ const actions = [...files.keys()].map((target) => ({
76
+ kind: 'create', target, detail: 'Create the Meadow app and companion website in this repository.',
77
+ }));
78
+ if (options.install)
79
+ actions.push({ kind: 'run', target: 'npm install and project checks', detail: 'Install and validate the generated app.' });
80
+ if (options.dryRun)
81
+ return { config, actions, diagnostics: [], changed: true };
82
+ const stagingParent = mkdtempSync(join(dirname(root), '.meadow-init-'));
83
+ try {
84
+ const staged = createProject({
85
+ directory: stagingParent,
86
+ answers: options.answers,
87
+ platformVersion: options.platformVersion,
88
+ platformSpec: options.platformSpec,
89
+ install: options.install,
90
+ git: false,
91
+ visualBaselines: options.visualBaselines,
92
+ }, runner);
93
+ for (const entry of readdirSync(staged)) {
94
+ renameSync(resolve(staged, entry), resolve(root, entry));
95
+ }
96
+ }
97
+ finally {
98
+ rmSync(stagingParent, { recursive: true, force: true });
99
+ }
100
+ return { config, actions, diagnostics: doctor(root), changed: true };
101
+ }
102
+ export function writeGeneratedProject(directory, files) {
103
+ for (const [relativePath, contents] of files) {
104
+ const path = resolve(directory, relativePath);
105
+ mkdirSync(dirname(path), { recursive: true });
106
+ writeFileSync(path, contents);
107
+ }
108
+ }
109
+ export function inspectExistingProject(directory = process.cwd()) {
110
+ const root = resolve(directory);
111
+ if (!existsSync(root) || !statSync(root).isDirectory()) {
112
+ throw new Error(`Project directory does not exist: ${root}`);
113
+ }
114
+ const packageJson = readPackageJson(root);
115
+ if (!existsSync(resolve(root, 'angular.json'))) {
116
+ throw new Error('meadow init currently supports Angular projects; angular.json is missing.');
117
+ }
118
+ if (!findCapacitorConfig(root)) {
119
+ throw new Error('meadow init currently supports Capacitor projects; capacitor.config.ts or capacitor.config.json is missing.');
120
+ }
121
+ const dependencies = packageDependencies(packageJson);
122
+ const angularVersion = dependencyVersion(dependencies, '@angular/core', 'Angular');
123
+ const capacitorVersion = dependencyVersion(dependencies, '@capacitor/core', 'Capacitor');
124
+ requireSupportedMajor(angularVersion, 22, 'Angular');
125
+ requireSupportedMajor(capacitorVersion, 8, 'Capacitor');
126
+ const existingConfig = existsSync(resolve(root, 'meadow.config.json'))
127
+ ? readProjectConfig(root)
128
+ : undefined;
129
+ const packageName = typeof packageJson.name === 'string' ? packageJson.name : '';
130
+ const packageVersion = semanticVersion(packageJson.version) ?? '0.1.0';
131
+ const capacitorSource = readCapacitorConfig(root);
132
+ const inferredName = existingConfig?.app.name ??
133
+ capacitorStringValue(capacitorSource, 'appName') ??
134
+ titleCase(packageName.replace(/^@[^/]+\//, '')) ??
135
+ 'Meadow App';
136
+ const inferredSlug = existingConfig?.app.slug ?? slugFromPackageName(packageName, inferredName);
137
+ const inferredId = existingConfig?.app.identifiers.android ??
138
+ capacitorStringValue(capacitorSource, 'appId') ??
139
+ `app.meadow.${inferredSlug.replaceAll('-', '')}`;
140
+ const revenuecat = existingConfig?.services.revenuecat.enabled ??
141
+ Object.keys(dependencies).some((name) => name.includes('revenuecat'));
142
+ const supabase = existingConfig?.services.supabase.enabled ?? dependencies['@supabase/supabase-js'] !== undefined;
143
+ const warnings = [];
144
+ if (angularVersion !== TOOLCHAIN.angular) {
145
+ warnings.push(`Angular ${angularVersion} is supported by major version but differs from Meadow's ${TOOLCHAIN.angular} pin; meadow doctor will report it.`);
146
+ }
147
+ if (capacitorVersion !== TOOLCHAIN.capacitor) {
148
+ warnings.push(`Capacitor ${capacitorVersion} is supported by major version but differs from Meadow's ${TOOLCHAIN.capacitor} pin; meadow doctor will report it.`);
149
+ }
150
+ const ionic = Object.keys(dependencies).filter((name) => name.startsWith('@ionic/'));
151
+ if (ionic.length) {
152
+ warnings.push(`Ionic remains app-owned and will be preserved (${ionic.join(', ')}).`);
153
+ }
154
+ return {
155
+ directory: root,
156
+ answers: {
157
+ name: inferredName,
158
+ slug: inferredSlug,
159
+ id: inferredId,
160
+ description: existingConfig?.app.description,
161
+ supportEmail: existingConfig?.links.supportEmail,
162
+ supportUrl: existingConfig?.links.supportUrl,
163
+ privacyUrl: existingConfig?.links.privacyUrl,
164
+ termsUrl: existingConfig?.links.termsUrl,
165
+ websiteUrl: existingConfig?.links.websiteUrl,
166
+ platforms: existingConfig?.app.platforms,
167
+ orientation: existingConfig?.app.orientation ?? 'any',
168
+ devices: existingConfig?.app.devices ?? ['phone', 'tablet'],
169
+ revenuecat,
170
+ monetizationMode: existingConfig?.services.revenuecat.mode ??
171
+ (revenuecat ? 'subscription-and-lifetime' : 'none'),
172
+ supabase,
173
+ },
174
+ angularVersion,
175
+ capacitorVersion,
176
+ packageVersion,
177
+ alreadyInitialized: existingConfig !== undefined,
178
+ warnings,
179
+ };
180
+ }
181
+ export function initProject(options, runner = SYSTEM_COMMAND_RUNNER) {
182
+ const inspection = inspectExistingProject(options.directory);
183
+ const root = inspection.directory;
184
+ const existingConfig = existsSync(resolve(root, 'meadow.config.json'))
185
+ ? readProjectConfig(root)
186
+ : undefined;
187
+ const requestedWebsiteUrl = options.answers.websiteBaseDomain
188
+ ? createProjectConfig(options.answers).links.websiteUrl
189
+ : undefined;
190
+ const config = existingConfig
191
+ ? requestedWebsiteUrl && !existingConfig.links.websiteUrl
192
+ ? validateProjectConfig({
193
+ ...existingConfig,
194
+ links: { ...existingConfig.links, websiteUrl: requestedWebsiteUrl },
195
+ })
196
+ : existingConfig
197
+ : initializedConfig(createProjectConfig(options.answers), root, inspection.packageVersion);
198
+ const packageJson = readPackageJson(root);
199
+ const writes = new Map();
200
+ const actions = [];
201
+ if (!existingConfig || config !== existingConfig) {
202
+ scheduleWrite(writes, actions, 'meadow.config.json', `${JSON.stringify(config, null, 2)}\n`, root, 'Create or update validated Meadow project configuration.');
203
+ }
204
+ else {
205
+ actions.push({
206
+ kind: 'preserve',
207
+ target: 'meadow.config.json',
208
+ detail: 'Keep the existing valid Meadow project configuration.',
209
+ });
210
+ }
211
+ const websiteRoot = resolve(root, 'website');
212
+ const meadowWebsite = !existsSync(websiteRoot) || existsSync(resolve(root, MEADOW_WEBSITE_MARKER));
213
+ if (!existsSync(websiteRoot)) {
214
+ for (const [path, contents] of websiteFiles(config)) {
215
+ scheduleWrite(writes, actions, path, contents, root, 'Create the companion website project.');
216
+ }
217
+ }
218
+ else if (existsSync(resolve(root, MEADOW_WEBSITE_MARKER))) {
219
+ scheduleWrite(writes, actions, 'website/site.config.json', websiteConfig(config), root, 'Synchronize companion website metadata from meadow.config.json.');
220
+ }
221
+ else {
222
+ actions.push({
223
+ kind: 'preserve',
224
+ target: 'website/',
225
+ detail: 'Keep the existing app-owned website project.',
226
+ });
227
+ }
228
+ const requestedPlatformSpec = options.platformSpec ??
229
+ options.platformVersion;
230
+ const packageUpdate = initializedPackageJson(packageJson, config, requestedPlatformSpec, options.platformSpec !== undefined, meadowWebsite);
231
+ scheduleWrite(writes, actions, 'package.json', `${JSON.stringify(packageUpdate, null, 2)}\n`, root, 'Add the pinned Meadow dependency, shared provider dependencies, and non-conflicting Meadow scripts.');
232
+ const agentPath = resolve(root, 'AGENTS.md');
233
+ const existingAgents = existsSync(agentPath) ? readFileSync(agentPath, 'utf8') : '';
234
+ scheduleWrite(writes, actions, 'AGENTS.md', mergeManagedSection(existingAgents, meadowAgentSection(config), MEADOW_AGENT_SECTION_START, MEADOW_AGENT_SECTION_END, `# ${config.app.name} working context`), root, 'Add or refresh the managed Meadow agent guidance without replacing app-owned instructions.');
235
+ const ignoreBlock = `# meadow-platform:start\n/.env.meadow.local\n/.build\n/website/dist\n/android/.gradle\n/android/build\n/android/app/build\n/ios/App/DerivedData\n/playwright-report\n/test-results\n*.jks\n*.keystore\n*.p12\n*.mobileprovision\n*.xcarchive\n# meadow-platform:end`;
236
+ const gitignorePath = resolve(root, '.gitignore');
237
+ const existingGitignore = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf8') : '';
238
+ scheduleWrite(writes, actions, '.gitignore', mergeManagedSection(existingGitignore, ignoreBlock, '# meadow-platform:start', '# meadow-platform:end'), root, 'Add Meadow-managed local configuration, build output, and signing exclusions.');
239
+ if (!existsSync(resolve(root, '.nvmrc'))) {
240
+ scheduleWrite(writes, actions, '.nvmrc', `${TOOLCHAIN.node}\n`, root, 'Pin the supported Node runtime.');
241
+ }
242
+ else {
243
+ actions.push({ kind: 'preserve', target: '.nvmrc', detail: 'Keep the existing Node version file.' });
244
+ }
245
+ const envBlock = environmentExampleSection(config);
246
+ if (envBlock) {
247
+ const envPath = resolve(root, '.env.example');
248
+ const existingEnv = existsSync(envPath) ? readFileSync(envPath, 'utf8') : '';
249
+ scheduleWrite(writes, actions, '.env.example', mergeManagedSection(existingEnv, envBlock, '# meadow-platform:start', '# meadow-platform:end'), root, 'Document app-owned public provider configuration names without adding credentials.');
250
+ }
251
+ if (options.install && writes.has('package.json')) {
252
+ actions.push({ kind: 'run', target: 'npm install', detail: 'Install the updated dependency set.' });
253
+ }
254
+ if (!options.dryRun) {
255
+ writeScheduledFiles(root, writes);
256
+ if (options.install && writes.has('package.json'))
257
+ runner.run('npm', ['install'], root);
258
+ }
259
+ return {
260
+ config,
261
+ actions,
262
+ diagnostics: options.dryRun ? [] : doctor(root),
263
+ changed: writes.size > 0,
264
+ };
265
+ }
266
+ export function configureIntegration(integration, options, runner = SYSTEM_COMMAND_RUNNER) {
267
+ const directory = resolve(options.directory);
268
+ const config = readProjectConfig(directory);
269
+ switch (integration) {
270
+ case 'firebase':
271
+ return configureFirebase(config, directory, options.values, runner);
272
+ case 'revenuecat':
273
+ return configureRevenueCat(config, directory, options.values, runner);
274
+ case 'supabase':
275
+ return configureSupabase(config, directory, options.values);
276
+ case 'assets':
277
+ return configureAssets(config, directory, options.values, runner);
278
+ case 'signing':
279
+ return configureSigning(directory);
280
+ case 'store':
281
+ return configureStore(config, directory, options.values);
282
+ case 'github':
283
+ createGithubRepository(directory, config, {
284
+ owner: textValue(options.values, 'owner', false),
285
+ visibility: visibilityValue(options.values.visibility),
286
+ }, runner);
287
+ return ['GitHub repository created and main pushed.'];
288
+ default:
289
+ throw new Error('Configure target must be firebase, revenuecat, supabase, assets, signing, store, or github.');
290
+ }
291
+ }
292
+ export function addCapability(capability, options, runner = SYSTEM_COMMAND_RUNNER) {
293
+ switch (capability) {
294
+ case 'local-data':
295
+ return addLocalData(options.directory, runner);
296
+ case 'notifications':
297
+ return addNotifications(options.directory, runner);
298
+ default:
299
+ throw new Error('Add target must be local-data or notifications.');
300
+ }
301
+ }
302
+ export function doctor(directory = process.cwd()) {
303
+ const root = resolve(directory);
304
+ const diagnostics = [];
305
+ let config;
306
+ try {
307
+ config = readProjectConfig(root);
308
+ diagnostics.push(pass('config', 'meadow.config.json is valid.'));
309
+ }
310
+ catch (error) {
311
+ diagnostics.push(failure('config', errorMessage(error)));
312
+ return diagnostics;
313
+ }
314
+ const major = Number(process.versions.node.split('.')[0]);
315
+ const minor = Number(process.versions.node.split('.')[1]);
316
+ diagnostics.push(major === 24 && minor >= 15
317
+ ? pass('node', `Node ${process.versions.node} satisfies the Meadow toolchain.`)
318
+ : failure('node', `Node 24.15 or newer within Node 24 is required; found ${process.versions.node}.`));
319
+ const packagePath = resolve(root, 'package.json');
320
+ if (!existsSync(packagePath)) {
321
+ diagnostics.push(failure('package', 'package.json is missing.'));
322
+ }
323
+ else {
324
+ try {
325
+ const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
326
+ const allDependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
327
+ diagnostics.push(allDependencies['@angular/core'] === TOOLCHAIN.angular
328
+ ? pass('angular', `Angular ${TOOLCHAIN.angular} is pinned.`)
329
+ : failure('angular', `@angular/core must be pinned to ${TOOLCHAIN.angular}.`));
330
+ diagnostics.push(allDependencies['@capacitor/core'] === TOOLCHAIN.capacitor
331
+ ? pass('capacitor', `Capacitor ${TOOLCHAIN.capacitor} is pinned.`)
332
+ : failure('capacitor', `@capacitor/core must be pinned to ${TOOLCHAIN.capacitor}.`));
333
+ if (config.services.localData.enabled) {
334
+ for (const [name, version] of [
335
+ ['@capacitor-community/sqlite', LOCAL_DATA_PACKAGE_VERSION],
336
+ ['jeep-sqlite', JEEP_SQLITE_PACKAGE_VERSION],
337
+ ['sql.js', SQL_JS_PACKAGE_VERSION],
338
+ ]) {
339
+ diagnostics.push(allDependencies[name] === version
340
+ ? pass(`local-data:dependency:${name}`, `${name} ${version} is pinned.`)
341
+ : failure(`local-data:dependency:${name}`, `${name} must be pinned to ${version}.`));
342
+ }
343
+ }
344
+ if (config.services.notifications.enabled) {
345
+ diagnostics.push(allDependencies['@capacitor/local-notifications'] ===
346
+ LOCAL_NOTIFICATIONS_PACKAGE_VERSION
347
+ ? pass('notifications:dependency', `@capacitor/local-notifications ${LOCAL_NOTIFICATIONS_PACKAGE_VERSION} is pinned.`)
348
+ : failure('notifications:dependency', `@capacitor/local-notifications must be pinned to ${LOCAL_NOTIFICATIONS_PACKAGE_VERSION}.`));
349
+ }
350
+ if (config.services.firebase.enabled) {
351
+ for (const [name, version] of [
352
+ ['@capacitor-firebase/analytics', TOOLCHAIN.capacitorFirebase],
353
+ ['@capacitor-firebase/crashlytics', TOOLCHAIN.capacitorFirebase],
354
+ ['firebase', TOOLCHAIN.firebase],
355
+ ]) {
356
+ diagnostics.push(allDependencies[name] === version
357
+ ? pass(`firebase:dependency:${name}`, `${name} ${version} is pinned.`)
358
+ : failure(`firebase:dependency:${name}`, `${name} must be pinned to ${version}.`));
359
+ }
360
+ }
361
+ if (config.services.revenuecat.enabled) {
362
+ diagnostics.push(allDependencies['@revenuecat/purchases-capacitor'] === TOOLCHAIN.revenuecat
363
+ ? pass('revenuecat:dependency', `@revenuecat/purchases-capacitor ${TOOLCHAIN.revenuecat} is pinned.`)
364
+ : failure('revenuecat:dependency', `@revenuecat/purchases-capacitor must be pinned to ${TOOLCHAIN.revenuecat}.`));
365
+ }
366
+ diagnostics.push(allDependencies.protractor
367
+ ? failure('stack:protractor', 'Protractor is end-of-life; use the generated Playwright setup.')
368
+ : pass('stack:protractor', 'No Protractor dependency is present.'));
369
+ const ionic = Object.keys(allDependencies).filter((name) => name.startsWith('@ionic/'));
370
+ diagnostics.push(ionic.length
371
+ ? warning('stack:ionic', `Ionic is app-owned (${ionic.join(', ')}) and must not be imported by @meadow-studio/platform.`)
372
+ : pass('stack:ionic', 'The application does not use Ionic.'));
373
+ const requiredScripts = [
374
+ 'check',
375
+ ...(existsSync(resolve(root, MEADOW_WEBSITE_MARKER)) ? ['website:check'] : []),
376
+ 'sync:check',
377
+ 'test:unit',
378
+ 'test:visual',
379
+ 'build:web',
380
+ 'services:config',
381
+ ...(config.app.platforms.includes('android') ? ['build:android:ci'] : []),
382
+ ...(config.app.platforms.includes('ios') ? ['build:ios:ci'] : []),
383
+ ];
384
+ for (const script of requiredScripts) {
385
+ if (!packageJson.scripts?.[script]) {
386
+ diagnostics.push(failure(`script:${script}`, `Required npm script ${script} is missing.`));
387
+ }
388
+ }
389
+ }
390
+ catch (error) {
391
+ diagnostics.push(failure('package', `package.json is invalid: ${errorMessage(error)}`));
392
+ }
393
+ }
394
+ for (const path of [
395
+ '.github/workflows/ci.yml',
396
+ 'playwright.config.ts',
397
+ 'e2e/visual/app.visual.spec.ts',
398
+ 'docs/RELEASE_CHECKLIST.md',
399
+ ]) {
400
+ diagnostics.push(existsSync(resolve(root, path)) ? pass(`file:${path}`, `${path} exists.`) : failure(`file:${path}`, `${path} is missing.`));
401
+ }
402
+ if (existsSync(resolve(root, MEADOW_WEBSITE_MARKER))) {
403
+ for (const path of [
404
+ 'website/package.json',
405
+ 'website/site.config.json',
406
+ 'website/src/index.html',
407
+ 'website/src/support/index.html',
408
+ 'website/src/privacy/index.html',
409
+ 'website/src/terms/index.html',
410
+ ]) {
411
+ diagnostics.push(existsSync(resolve(root, path))
412
+ ? pass(`website:${path}`, `${path} exists.`)
413
+ : failure(`website:${path}`, `${path} is missing.`));
414
+ }
415
+ }
416
+ else {
417
+ diagnostics.push(existsSync(resolve(root, 'website'))
418
+ ? pass('website', 'An app-owned website project exists.')
419
+ : warning('website', 'The companion website is missing; run meadow init to add it.'));
420
+ }
421
+ if (config.services.localData.enabled) {
422
+ for (const path of [
423
+ 'src/app/data/local-data/local-data.migrations.ts',
424
+ 'src/app/data/local-data/local-database.ts',
425
+ 'src/app/data/local-data/local-document.store.ts',
426
+ 'public/assets/sql-wasm.wasm',
427
+ ]) {
428
+ diagnostics.push(existsSync(resolve(root, path))
429
+ ? pass(`local-data:file:${path}`, `${path} exists.`)
430
+ : failure(`local-data:file:${path}`, `${path} is missing.`));
431
+ }
432
+ const migrationsPath = resolve(root, 'src/app/data/local-data/local-data.migrations.ts');
433
+ if (existsSync(migrationsPath)) {
434
+ const versions = [...readFileSync(migrationsPath, 'utf8').matchAll(/toVersion:\s*(\d+)/g)].map((match) => Number(match[1]));
435
+ const generatedVersion = versions.length ? Math.max(...versions) : 0;
436
+ diagnostics.push(generatedVersion === config.services.localData.databaseVersion
437
+ ? pass('local-data:version', `Local-data migration version ${generatedVersion} matches meadow.config.json.`)
438
+ : failure('local-data:version', `Local-data migrations end at ${generatedVersion}, but meadow.config.json records ${config.services.localData.databaseVersion}.`));
439
+ }
440
+ diagnostics.push(existsSync(resolve(root, 'src/app/data/local-data/.meadow-recipe-pending'))
441
+ ? failure('local-data:pending', 'The local-data recipe is still marked pending; rerun meadow add local-data.')
442
+ : pass('local-data:pending', 'The local-data recipe completed.'));
443
+ const androidManifest = resolve(root, 'android/app/src/main/AndroidManifest.xml');
444
+ if (existsSync(androidManifest)) {
445
+ const manifest = readFileSync(androidManifest, 'utf8');
446
+ diagnostics.push(manifest.includes('android:dataExtractionRules="@xml/data_extraction_rules"') &&
447
+ manifest.includes('android:allowBackup="false"')
448
+ ? pass('local-data:android-backup', 'Android local-data backup exclusions are configured.')
449
+ : failure('local-data:android-backup', 'Android local-data backup exclusions are missing.'));
450
+ }
451
+ }
452
+ if (config.services.notifications.enabled) {
453
+ const androidRoot = resolve(root, 'android');
454
+ if (config.app.platforms.includes('android') && existsSync(androidRoot)) {
455
+ diagnostics.push(filesContainText(root, ['android/capacitor.settings.gradle', 'android/app/capacitor.build.gradle'], 'capacitor-local-notifications')
456
+ ? pass('notifications:android-native', 'Android local-notifications plugin is synchronized.')
457
+ : failure('notifications:android-native', 'Android local-notifications plugin is not synchronized; rerun meadow add notifications.'));
458
+ }
459
+ const iosRoot = resolve(root, 'ios');
460
+ if (config.app.platforms.includes('ios') && existsSync(iosRoot)) {
461
+ diagnostics.push(filesContainText(root, ['ios/App/CapApp-SPM/Package.swift', 'ios/App/Podfile.lock'], 'CapacitorLocalNotifications')
462
+ ? pass('notifications:ios-native', 'iOS local-notifications plugin is synchronized.')
463
+ : failure('notifications:ios-native', 'iOS local-notifications plugin is not synchronized; rerun meadow add notifications.'));
464
+ }
465
+ }
466
+ for (const platform of config.app.platforms) {
467
+ diagnostics.push(existsSync(resolve(root, platform))
468
+ ? pass(`native:${platform}`, `${platform} project exists.`)
469
+ : warning(`native:${platform}`, `${platform} project is not generated yet; run npm run native:init.`));
470
+ }
471
+ if (config.services.firebase.enabled) {
472
+ if (config.app.platforms.includes('android') && existsSync(resolve(root, 'android'))) {
473
+ const googleServices = resolve(root, 'android/app/google-services.json');
474
+ diagnostics.push(existsSync(googleServices)
475
+ ? pass('firebase:android-config', 'Android Firebase configuration exists.')
476
+ : warning('firebase:android-config', 'android/app/google-services.json is missing; run meadow configure firebase.'));
477
+ const manifestPath = resolve(root, 'android/app/src/main/AndroidManifest.xml');
478
+ const projectGradlePath = resolve(root, 'android/build.gradle');
479
+ const appGradlePath = resolve(root, 'android/app/build.gradle');
480
+ if (existsSync(manifestPath) && existsSync(projectGradlePath) && existsSync(appGradlePath)) {
481
+ const manifest = readFileSync(manifestPath, 'utf8');
482
+ const projectGradle = readFileSync(projectGradlePath, 'utf8');
483
+ const appGradle = readFileSync(appGradlePath, 'utf8');
484
+ const optInDefaults = manifest.includes('firebase_analytics_collection_enabled') &&
485
+ manifest.includes('firebase_crashlytics_collection_enabled');
486
+ const configuredPlugins = projectGradle.includes('com.google.gms:google-services') &&
487
+ projectGradle.includes('com.google.firebase:firebase-crashlytics-gradle') &&
488
+ appGradle.includes("apply plugin: 'com.google.gms.google-services'") &&
489
+ appGradle.includes("apply plugin: 'com.google.firebase.crashlytics'");
490
+ diagnostics.push(optInDefaults && (!config.services.firebase.configured || configuredPlugins)
491
+ ? pass('firebase:android-native', 'Android Firebase plugins and opt-in defaults are configured.')
492
+ : failure('firebase:android-native', 'Android Firebase build plugins or opt-in collection defaults are missing; run meadow sync.'));
493
+ }
494
+ }
495
+ if (config.app.platforms.includes('ios') && existsSync(resolve(root, 'ios'))) {
496
+ const googleServiceInfo = resolve(root, 'ios/App/App/GoogleService-Info.plist');
497
+ diagnostics.push(existsSync(googleServiceInfo)
498
+ ? pass('firebase:ios-config', 'iOS Firebase configuration exists.')
499
+ : warning('firebase:ios-config', 'ios/App/App/GoogleService-Info.plist is missing; run meadow configure firebase.'));
500
+ const infoPath = resolve(root, 'ios/App/App/Info.plist');
501
+ const projectPath = resolve(root, 'ios/App/App.xcodeproj/project.pbxproj');
502
+ if (existsSync(infoPath) && existsSync(projectPath)) {
503
+ const info = readFileSync(infoPath, 'utf8');
504
+ const project = readFileSync(projectPath, 'utf8');
505
+ const bundledFirebaseConfig = !config.services.firebase.configured ||
506
+ (project.includes('GoogleService-Info.plist') &&
507
+ project.includes('GoogleService-Info.plist in Resources'));
508
+ diagnostics.push(info.includes('FIREBASE_ANALYTICS_COLLECTION_ENABLED') &&
509
+ info.includes('FirebaseCrashlyticsCollectionEnabled') &&
510
+ bundledFirebaseConfig
511
+ ? pass('firebase:ios-native', 'iOS Firebase configuration resource and opt-in defaults are configured.')
512
+ : failure('firebase:ios-native', 'iOS Firebase configuration resource or opt-in collection defaults are missing; run meadow sync.'));
513
+ }
514
+ }
515
+ }
516
+ if (config.services.revenuecat.enabled) {
517
+ const environment = readLocalEnvironment(root);
518
+ for (const platform of config.app.platforms) {
519
+ const variable = platform === 'ios'
520
+ ? 'MEADOW_REVENUECAT_IOS_API_KEY'
521
+ : 'MEADOW_REVENUECAT_ANDROID_API_KEY';
522
+ const value = process.env[variable] ?? environment[variable];
523
+ diagnostics.push(!config.services.revenuecat.configured
524
+ ? warning(`revenuecat:${platform}-key`, `RevenueCat is not configured; run meadow configure revenuecat.`)
525
+ : isRevenueCatPublicKey(value, platform)
526
+ ? pass(`revenuecat:${platform}-key`, `${platform} RevenueCat public SDK key is available.`)
527
+ : failure(`revenuecat:${platform}-key`, `${variable} is missing or is not a ${platform} public SDK key.`));
528
+ }
529
+ diagnostics.push(config.services.revenuecat.configured &&
530
+ Boolean(config.services.revenuecat.entitlementId) &&
531
+ Boolean(config.services.revenuecat.offeringId)
532
+ ? pass('revenuecat:catalog', 'RevenueCat offering and entitlement identifiers are configured.')
533
+ : config.services.revenuecat.configured
534
+ ? failure('revenuecat:catalog', 'RevenueCat offering or entitlement identifier is missing; rerun meadow configure revenuecat.')
535
+ : warning('revenuecat:catalog', 'RevenueCat catalog identifiers are not configured.'));
536
+ }
537
+ const screenshots = resolve(root, 'e2e/visual/__screenshots__');
538
+ diagnostics.push(existsSync(screenshots) && containsFile(screenshots, '.png')
539
+ ? pass('visual-baselines', 'Visual baselines exist.')
540
+ : warning('visual-baselines', 'Visual baselines are missing; run npm run test:visual:update in the CI-compatible environment.'));
541
+ try {
542
+ const sync = syncProject(root, true);
543
+ diagnostics.push(sync.valid
544
+ ? pass('sync', 'Generated project files match meadow.config.json.')
545
+ : failure('sync', 'Generated project files have drifted from meadow.config.json; run meadow sync and review the changes.'));
546
+ }
547
+ catch (error) {
548
+ diagnostics.push(failure('sync', `Could not check generated files: ${errorMessage(error)}`));
549
+ }
550
+ for (const [service, state] of Object.entries(config.services)) {
551
+ if (state.enabled && !state.configured) {
552
+ diagnostics.push(warning(`service:${service}`, `${service} is enabled but has not been configured.`));
553
+ }
554
+ else {
555
+ diagnostics.push(pass(`service:${service}`, `${service} configuration state is consistent.`));
556
+ }
557
+ }
558
+ return diagnostics;
559
+ }
560
+ export function releaseCheck(platform, directory = process.cwd(), runner = SYSTEM_COMMAND_RUNNER) {
561
+ const diagnostics = doctor(directory);
562
+ const config = readProjectConfig(directory);
563
+ if (platform !== 'all' && !config.app.platforms.includes(platform)) {
564
+ return [
565
+ ...diagnostics,
566
+ failure('release:platform', `${platform} is not enabled in meadow.config.json.`),
567
+ ];
568
+ }
569
+ const promoted = diagnostics.map((item) => item.level === 'warning' &&
570
+ (item.id === 'visual-baselines' ||
571
+ item.id.startsWith('native:') ||
572
+ item.id.startsWith('service:'))
573
+ ? failure(item.id, item.message)
574
+ : item);
575
+ if (config.release.channel === 'production') {
576
+ for (const [label, value] of [
577
+ ['support email', config.links.supportEmail],
578
+ ['website URL', config.links.websiteUrl],
579
+ ['privacy URL', config.links.privacyUrl],
580
+ ['terms URL', config.links.termsUrl],
581
+ ]) {
582
+ if (!value)
583
+ promoted.push(failure(`release:${label}`, `${label} is required for production.`));
584
+ }
585
+ }
586
+ if (promoted.some((item) => item.level === 'failure'))
587
+ return promoted;
588
+ runner.run('npm', ['run', 'check'], directory);
589
+ runner.run('npm', ['run', 'test:visual'], directory);
590
+ if ((platform === 'android' || platform === 'all') && config.app.platforms.includes('android')) {
591
+ runner.run('npm', ['run', 'build:android:ci'], directory);
592
+ }
593
+ if ((platform === 'ios' || platform === 'all') && config.app.platforms.includes('ios')) {
594
+ runner.run('npm', ['run', 'build:ios:ci'], directory);
595
+ }
596
+ return [...promoted, pass('release-check', `Release checks passed for ${platform}.`)];
597
+ }
598
+ export function setApplicationVersion(requested, channel, directory = process.cwd()) {
599
+ const root = resolve(directory);
600
+ const config = readProjectConfig(root);
601
+ const version = nextVersion(config.release.version, requested);
602
+ const releaseChannel = channel ?? config.release.channel;
603
+ if (!['development', 'test', 'production'].includes(releaseChannel)) {
604
+ throw new Error('Release channel must be development, test, or production.');
605
+ }
606
+ const next = {
607
+ ...config,
608
+ release: {
609
+ version,
610
+ build: config.release.build + 1,
611
+ channel: releaseChannel,
612
+ },
613
+ };
614
+ writeProjectConfig(next, root);
615
+ const sync = syncProject(root);
616
+ if (!sync.valid) {
617
+ throw new Error('Application version changed, but generated files could not be synchronized.');
618
+ }
619
+ return next;
620
+ }
621
+ export function nextVersion(current, requested) {
622
+ if (/^\d+\.\d+\.\d+$/.test(requested))
623
+ return requested;
624
+ if (!['major', 'minor', 'patch'].includes(requested)) {
625
+ throw new Error('Version must be major, minor, patch, or x.y.z.');
626
+ }
627
+ const [major, minor, patch] = current.split('.').map(Number);
628
+ if (requested === 'major')
629
+ return `${major + 1}.0.0`;
630
+ if (requested === 'minor')
631
+ return `${major}.${minor + 1}.0`;
632
+ return `${major}.${minor}.${patch + 1}`;
633
+ }
634
+ function configureFirebase(config, directory, values, runner) {
635
+ const configuredPlatforms = [];
636
+ if (config.app.platforms.includes('android')) {
637
+ const androidSource = textValue(values, 'google-services', true);
638
+ validateGoogleServices(androidSource);
639
+ const androidTarget = resolve(directory, 'android/app/google-services.json');
640
+ mkdirSync(dirname(androidTarget), { recursive: true });
641
+ cpSync(androidSource, androidTarget);
642
+ configuredPlatforms.push('Android');
643
+ }
644
+ if (config.app.platforms.includes('ios')) {
645
+ const iosSource = textValue(values, 'google-service-info', true);
646
+ validateGoogleServiceInfo(iosSource);
647
+ const iosTarget = resolve(directory, 'ios/App/App/GoogleService-Info.plist');
648
+ mkdirSync(dirname(iosTarget), { recursive: true });
649
+ cpSync(iosSource, iosTarget);
650
+ configuredPlatforms.push('iOS');
651
+ }
652
+ writeProjectConfigAndSync({
653
+ ...config,
654
+ services: {
655
+ ...config.services,
656
+ firebase: { ...config.services.firebase, enabled: true, configured: true },
657
+ },
658
+ }, directory);
659
+ for (const platform of config.app.platforms) {
660
+ runner.run(resolve(directory, 'node_modules/.bin/cap'), ['sync', platform], directory);
661
+ }
662
+ return [
663
+ `Firebase observability configured for ${configuredPlatforms.join(' and ')} with collection disabled until explicit consent.`,
664
+ ];
665
+ }
666
+ function configureRevenueCat(config, directory, values, runner) {
667
+ const iosKey = config.app.platforms.includes('ios')
668
+ ? textValue(values, 'ios-key', true)
669
+ : undefined;
670
+ const androidKey = config.app.platforms.includes('android')
671
+ ? textValue(values, 'android-key', true)
672
+ : undefined;
673
+ const entitlementId = textValue(values, 'entitlement', true);
674
+ const offeringId = textValue(values, 'offering', true);
675
+ if (iosKey && !isRevenueCatPublicKey(iosKey, 'ios')) {
676
+ throw new Error('RevenueCat iOS key must be an Apple public SDK key (appl_ or test_).');
677
+ }
678
+ if (androidKey && !isRevenueCatPublicKey(androidKey, 'android')) {
679
+ throw new Error('RevenueCat Android key must be a Google public SDK key (goog_ or test_).');
680
+ }
681
+ updateEnvFile(directory, {
682
+ ...(iosKey ? { MEADOW_REVENUECAT_IOS_API_KEY: iosKey } : {}),
683
+ ...(androidKey ? { MEADOW_REVENUECAT_ANDROID_API_KEY: androidKey } : {}),
684
+ });
685
+ writeProjectConfigAndSync({
686
+ ...config,
687
+ services: {
688
+ ...config.services,
689
+ revenuecat: {
690
+ ...config.services.revenuecat,
691
+ enabled: true,
692
+ configured: true,
693
+ mode: config.services.revenuecat.mode === 'none'
694
+ ? 'subscription-and-lifetime'
695
+ : config.services.revenuecat.mode,
696
+ entitlementId,
697
+ offeringId,
698
+ },
699
+ },
700
+ }, directory);
701
+ runner.run('npm', ['install'], directory);
702
+ for (const platform of config.app.platforms) {
703
+ runner.run(resolve(directory, 'node_modules/.bin/cap'), ['sync', platform], directory);
704
+ }
705
+ return [
706
+ `RevenueCat purchases configured for ${config.app.platforms.join(' and ')}; public SDK keys remain outside source control.`,
707
+ ];
708
+ }
709
+ function configureSupabase(config, directory, values) {
710
+ const url = textValue(values, 'url', true);
711
+ const key = textValue(values, 'publishable-key', true);
712
+ let parsed;
713
+ try {
714
+ parsed = new URL(url);
715
+ }
716
+ catch {
717
+ throw new Error('Supabase URL must be an absolute URL.');
718
+ }
719
+ if (parsed.protocol !== 'https:')
720
+ throw new Error('Supabase URL must use https.');
721
+ if (/service[_-]?role|secret/i.test(key) || key.startsWith('sb_secret_')) {
722
+ throw new Error('Refusing to store a Supabase service-role or secret key in a client app.');
723
+ }
724
+ if (!key.startsWith('sb_publishable_') && !key.startsWith('eyJ')) {
725
+ throw new Error('Expected a Supabase publishable key (or legacy anon key).');
726
+ }
727
+ updateEnvFile(directory, { MEADOW_SUPABASE_URL: url, MEADOW_SUPABASE_PUBLISHABLE_KEY: key });
728
+ const projectRef = parsed.hostname.endsWith('.supabase.co')
729
+ ? parsed.hostname.slice(0, -'.supabase.co'.length)
730
+ : config.services.supabase.projectRef;
731
+ writeProjectConfigAndSync({
732
+ ...config,
733
+ services: {
734
+ ...config.services,
735
+ supabase: {
736
+ ...config.services.supabase,
737
+ enabled: true,
738
+ configured: true,
739
+ auth: 'email-otp',
740
+ storage: true,
741
+ ...(projectRef ? { projectRef } : {}),
742
+ },
743
+ },
744
+ }, directory);
745
+ return [
746
+ 'Supabase client configuration saved to .env.meadow.local.',
747
+ 'Before release, add explicit grants, RLS policies and database tests for every exposed table.',
748
+ ];
749
+ }
750
+ function configureAssets(_config, directory, values, runner) {
751
+ const source = textValue(values, 'source', true);
752
+ if (!existsSync(source) || !statSync(source).isFile())
753
+ throw new Error(`Asset source not found: ${source}`);
754
+ const target = resolve(directory, 'resources/icon.png');
755
+ mkdirSync(dirname(target), { recursive: true });
756
+ cpSync(source, target);
757
+ runner.run(localBinary(directory, 'capacitor-assets'), ['generate'], directory);
758
+ return ['Native icons and splash assets generated from resources/icon.png.'];
759
+ }
760
+ function configureSigning(directory) {
761
+ const path = resolve(directory, 'docs/SIGNING.md');
762
+ mkdirSync(dirname(path), { recursive: true });
763
+ writeFileSync(path, `# Signing\n\nKeep signing files and passwords outside Git.\n\n- Android: provide the keystore and Gradle properties through the release environment.\n- iOS: use the Apple team, certificates, and provisioning profiles supplied by Xcode or CI secrets.\n- Never commit .jks, .keystore, .p12, .mobileprovision, or credentials.\n`);
764
+ return ['Signing guide created at docs/SIGNING.md.'];
765
+ }
766
+ function configureStore(config, directory, values) {
767
+ const supportEmail = textValue(values, 'support-email', false) ?? config.links.supportEmail;
768
+ const supportUrl = textValue(values, 'support-url', false) ?? config.links.supportUrl;
769
+ const privacyUrl = textValue(values, 'privacy-url', false) ?? config.links.privacyUrl;
770
+ const termsUrl = textValue(values, 'terms-url', false) ?? config.links.termsUrl;
771
+ const websiteUrl = textValue(values, 'website-url', false) ?? config.links.websiteUrl;
772
+ const appleAppId = textValue(values, 'apple-app-id', false) ?? config.store.appleAppId;
773
+ const appStoreUrl = textValue(values, 'app-store-url', false) ?? config.store.appStoreUrl;
774
+ const playStoreUrl = textValue(values, 'play-store-url', false) ?? config.store.playStoreUrl;
775
+ const next = validateProjectConfig({
776
+ ...config,
777
+ links: { ...config.links, supportEmail, supportUrl, privacyUrl, termsUrl, websiteUrl },
778
+ store: { ...config.store, appleAppId, appStoreUrl, playStoreUrl },
779
+ });
780
+ writeProjectConfigAndSync(next, directory);
781
+ return ['Store support and legal metadata updated.'];
782
+ }
783
+ function initializeGit(directory, config, runner) {
784
+ runner.run('git', ['init', '-b', 'main'], directory);
785
+ runner.run('git', ['add', '.'], directory);
786
+ runner.run('git', ['commit', '-m', `🎉 chore: initialize ${config.app.name}`], directory);
787
+ }
788
+ function createGithubRepository(directory, config, github, runner) {
789
+ if (!runner.succeeds('gh', ['auth', 'status'], directory)) {
790
+ throw new Error('GitHub CLI is not authenticated. Run gh auth login and try meadow configure github.');
791
+ }
792
+ const name = github.owner ? `${github.owner}/${config.app.slug}` : config.app.slug;
793
+ runner.run('gh', ['repo', 'create', name, `--${github.visibility}`, '--source', '.', '--remote', 'origin', '--push'], directory);
794
+ }
795
+ function updateEnvFile(directory, values) {
796
+ const path = resolve(directory, '.env.meadow.local');
797
+ const existing = existsSync(path) ? readFileSync(path, 'utf8').split(/\r?\n/) : [];
798
+ const retained = existing.filter((line) => !Object.keys(values).some((key) => line.startsWith(`${key}=`)));
799
+ const next = [...retained.filter(Boolean), ...Object.entries(values).map(([key, value]) => `${key}=${value}`)];
800
+ writeFileSync(path, `${next.join('\n')}\n`, { mode: 0o600 });
801
+ }
802
+ function readLocalEnvironment(directory) {
803
+ const path = resolve(directory, '.env.meadow.local');
804
+ if (!existsSync(path))
805
+ return {};
806
+ return Object.fromEntries(readFileSync(path, 'utf8')
807
+ .split(/\r?\n/)
808
+ .map((line) => line.trim())
809
+ .filter((line) => line && !line.startsWith('#') && line.includes('='))
810
+ .map((line) => {
811
+ const separator = line.indexOf('=');
812
+ const key = line.slice(0, separator).trim();
813
+ const raw = line.slice(separator + 1).trim();
814
+ const quoted = (raw.startsWith('"') && raw.endsWith('"')) ||
815
+ (raw.startsWith("'") && raw.endsWith("'"));
816
+ return [key, quoted ? raw.slice(1, -1) : raw];
817
+ }));
818
+ }
819
+ function isRevenueCatPublicKey(value, platform) {
820
+ if (!value)
821
+ return false;
822
+ const normalized = value.trim();
823
+ return normalized.startsWith('test_') || normalized.startsWith(platform === 'ios' ? 'appl_' : 'goog_');
824
+ }
825
+ function writeProjectConfigAndSync(config, directory) {
826
+ writeProjectConfig(config, directory);
827
+ const result = syncProject(directory);
828
+ if (!result.valid) {
829
+ throw new Error('Configuration was saved, but generated files could not be synchronized.');
830
+ }
831
+ }
832
+ function nextFileEntries(directory) {
833
+ return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
834
+ const path = join(directory, entry.name);
835
+ return entry.isDirectory() ? nextFileEntries(path) : [path];
836
+ });
837
+ }
838
+ function containsFile(directory, suffix) {
839
+ try {
840
+ return nextFileEntries(directory).some((path) => path.endsWith(suffix));
841
+ }
842
+ catch {
843
+ return false;
844
+ }
845
+ }
846
+ function filesContainText(root, paths, value) {
847
+ return paths.some((path) => {
848
+ try {
849
+ return readFileSync(resolve(root, path), 'utf8').includes(value);
850
+ }
851
+ catch {
852
+ return false;
853
+ }
854
+ });
855
+ }
856
+ function validateGoogleServices(path) {
857
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
858
+ if (!parsed.project_info || !Array.isArray(parsed.client)) {
859
+ throw new Error('google-services.json does not look like a Firebase Android config.');
860
+ }
861
+ }
862
+ function validateGoogleServiceInfo(path) {
863
+ const source = readFileSync(path, 'utf8');
864
+ if (!source.includes('<plist') || !source.includes('GOOGLE_APP_ID')) {
865
+ throw new Error('GoogleService-Info.plist does not look like a Firebase iOS config.');
866
+ }
867
+ }
868
+ function textValue(values, key, required) {
869
+ const value = values[key];
870
+ if (typeof value === 'string' && value.trim())
871
+ return value.trim();
872
+ if (required)
873
+ throw new Error(`--${key} is required.`);
874
+ return undefined;
875
+ }
876
+ function visibilityValue(value) {
877
+ const visibility = typeof value === 'string' ? value : 'private';
878
+ if (!['private', 'public', 'internal'].includes(visibility)) {
879
+ throw new Error('Visibility must be private, public, or internal.');
880
+ }
881
+ return visibility;
882
+ }
883
+ function readPackageJson(directory) {
884
+ const path = resolve(directory, 'package.json');
885
+ let parsed;
886
+ try {
887
+ parsed = JSON.parse(readFileSync(path, 'utf8'));
888
+ }
889
+ catch (error) {
890
+ throw new Error(`Could not read ${path}: ${errorMessage(error)}`);
891
+ }
892
+ if (!isRecord(parsed))
893
+ throw new Error(`${path} must contain a JSON object.`);
894
+ return parsed;
895
+ }
896
+ function packageDependencies(packageJson) {
897
+ return {
898
+ ...stringRecord(packageJson.dependencies, 'package.json dependencies'),
899
+ ...stringRecord(packageJson.devDependencies, 'package.json devDependencies'),
900
+ };
901
+ }
902
+ function stringRecord(value, label) {
903
+ if (value === undefined)
904
+ return {};
905
+ if (!isRecord(value) || Object.values(value).some((entry) => typeof entry !== 'string')) {
906
+ throw new Error(`${label} must contain dependency names and version strings.`);
907
+ }
908
+ return value;
909
+ }
910
+ function dependencyVersion(dependencies, name, label) {
911
+ const version = dependencies[name];
912
+ if (!version)
913
+ throw new Error(`meadow init requires ${label}; ${name} is missing from package.json.`);
914
+ return version;
915
+ }
916
+ function requireSupportedMajor(version, major, label) {
917
+ const detected = version.match(/(?:^|[^0-9])(\d+)(?:\.\d+)?/);
918
+ if (!detected || Number(detected[1]) !== major) {
919
+ throw new Error(`meadow init supports ${label} ${major}; package.json uses ${version}.`);
920
+ }
921
+ }
922
+ function findCapacitorConfig(directory) {
923
+ return ['capacitor.config.ts', 'capacitor.config.json'].find((name) => existsSync(resolve(directory, name)));
924
+ }
925
+ function readCapacitorConfig(directory) {
926
+ const relativePath = findCapacitorConfig(directory);
927
+ return relativePath ? readFileSync(resolve(directory, relativePath), 'utf8') : '';
928
+ }
929
+ function capacitorStringValue(source, key) {
930
+ const jsonMatch = source.match(new RegExp(`"${key}"\\s*:\\s*"([^"]+)"`));
931
+ if (jsonMatch?.[1])
932
+ return jsonMatch[1].trim();
933
+ const sourceMatch = source.match(new RegExp(key + "\\s*:\\s*['\"`]([^'\"`]+)['\"`]"));
934
+ return sourceMatch?.[1]?.trim() || undefined;
935
+ }
936
+ function slugFromPackageName(packageName, fallbackName) {
937
+ const candidate = packageName.replace(/^@[^/]+\//, '').trim();
938
+ return createProjectConfig({ name: candidate || fallbackName }).app.slug;
939
+ }
940
+ function titleCase(value) {
941
+ const title = value
942
+ .split(/[-_\s]+/)
943
+ .filter(Boolean)
944
+ .map((part) => `${part[0]?.toUpperCase() ?? ''}${part.slice(1)}`)
945
+ .join(' ');
946
+ return title || undefined;
947
+ }
948
+ function semanticVersion(value) {
949
+ return typeof value === 'string' && /^\d+\.\d+\.\d+$/.test(value) ? value : undefined;
950
+ }
951
+ function initializedConfig(base, directory, packageVersion) {
952
+ const firebaseConfigured = base.app.platforms.every((platform) => existsSync(resolve(directory, platform === 'android'
953
+ ? 'android/app/google-services.json'
954
+ : 'ios/App/App/GoogleService-Info.plist')));
955
+ return validateProjectConfig({
956
+ ...base,
957
+ services: {
958
+ ...base.services,
959
+ firebase: { ...base.services.firebase, configured: firebaseConfigured },
960
+ },
961
+ release: { ...base.release, version: packageVersion },
962
+ });
963
+ }
964
+ function initializedPackageJson(packageJson, config, requestedPlatformSpec, replacePlatformSpec, meadowWebsite) {
965
+ const dependencies = stringRecord(packageJson.dependencies, 'package.json dependencies');
966
+ const nextDependencies = { ...dependencies };
967
+ if (!nextDependencies['@meadow-studio/platform'] || replacePlatformSpec) {
968
+ nextDependencies['@meadow-studio/platform'] = requestedPlatformSpec;
969
+ }
970
+ nextDependencies['@capacitor/app'] ??= TOOLCHAIN.capacitorApp;
971
+ nextDependencies['@capacitor-firebase/analytics'] ??= TOOLCHAIN.capacitorFirebase;
972
+ nextDependencies['@capacitor-firebase/crashlytics'] ??= TOOLCHAIN.capacitorFirebase;
973
+ nextDependencies.firebase ??= TOOLCHAIN.firebase;
974
+ if (config.services.revenuecat.enabled) {
975
+ nextDependencies['@revenuecat/purchases-capacitor'] ??= TOOLCHAIN.revenuecat;
976
+ }
977
+ if (config.services.supabase.enabled) {
978
+ nextDependencies['@supabase/supabase-js'] ??= TOOLCHAIN.supabase;
979
+ }
980
+ const scripts = stringRecord(packageJson.scripts, 'package.json scripts');
981
+ const nextScripts = { ...scripts };
982
+ nextScripts['meadow:doctor'] ??= 'meadow doctor';
983
+ nextScripts['release:check'] ??= 'meadow release-check all';
984
+ nextScripts['cap:sync'] ??= 'npx cap sync';
985
+ if (meadowWebsite) {
986
+ nextScripts['website:start'] ??= 'npm start --prefix website';
987
+ nextScripts['website:build'] ??= 'npm run build --prefix website';
988
+ nextScripts['website:check'] ??= 'npm run build --prefix website';
989
+ }
990
+ const engines = isRecord(packageJson.engines) ? { ...packageJson.engines } : {};
991
+ engines.node ??= '>=24.15 <25';
992
+ engines.npm ??= '>=11 <12';
993
+ return {
994
+ ...packageJson,
995
+ private: packageJson.private ?? true,
996
+ engines,
997
+ scripts: nextScripts,
998
+ dependencies: nextDependencies,
999
+ };
1000
+ }
1001
+ function scheduleWrite(writes, actions, relativePath, contents, directory, detail) {
1002
+ const path = resolve(directory, relativePath);
1003
+ const current = existsSync(path) ? readFileSync(path, 'utf8') : undefined;
1004
+ if (current === contents) {
1005
+ actions.push({ kind: 'preserve', target: relativePath, detail: 'Already matches the Meadow plan.' });
1006
+ return;
1007
+ }
1008
+ writes.set(relativePath, contents);
1009
+ actions.push({ kind: current === undefined ? 'create' : 'update', target: relativePath, detail });
1010
+ }
1011
+ function writeScheduledFiles(directory, writes) {
1012
+ for (const [relativePath, contents] of writes) {
1013
+ const path = resolve(directory, relativePath);
1014
+ mkdirSync(dirname(path), { recursive: true });
1015
+ writeFileSync(path, contents);
1016
+ }
1017
+ }
1018
+ function mergeManagedSection(source, section, startMarker, endMarker, initialHeading) {
1019
+ const start = source.indexOf(startMarker);
1020
+ const end = source.indexOf(endMarker);
1021
+ if ((start >= 0) !== (end >= 0) || (start >= 0 && end < start)) {
1022
+ throw new Error(`Cannot update managed section: expected both ${startMarker} and ${endMarker}.`);
1023
+ }
1024
+ const before = start >= 0 ? source.slice(0, start).trimEnd() : source.trimEnd();
1025
+ const after = end >= 0 ? source.slice(end + endMarker.length).trimStart().trimEnd() : '';
1026
+ const parts = [before || initialHeading, section.trim(), after].filter((part) => Boolean(part));
1027
+ return `${parts.join('\n\n')}\n`;
1028
+ }
1029
+ function environmentExampleSection(config) {
1030
+ const values = [];
1031
+ if (config.services.revenuecat.enabled) {
1032
+ values.push('MEADOW_REVENUECAT_IOS_API_KEY=', 'MEADOW_REVENUECAT_ANDROID_API_KEY=');
1033
+ }
1034
+ if (config.services.supabase.enabled) {
1035
+ values.push('MEADOW_SUPABASE_URL=', 'MEADOW_SUPABASE_PUBLISHABLE_KEY=');
1036
+ }
1037
+ if (!values.length)
1038
+ return undefined;
1039
+ return `# meadow-platform:start\n# Public client configuration only. Never add secrets.\n${values.join('\n')}\n# meadow-platform:end`;
1040
+ }
1041
+ function isRecord(value) {
1042
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
1043
+ }
1044
+ function localBinary(directory, name) {
1045
+ return resolve(directory, 'node_modules/.bin', name);
1046
+ }
1047
+ function spawnOptions(directory, stdio) {
1048
+ return { cwd: directory, stdio, env: process.env, encoding: 'utf8' };
1049
+ }
1050
+ function pass(id, message) {
1051
+ return { level: 'pass', id, message };
1052
+ }
1053
+ function warning(id, message) {
1054
+ return { level: 'warning', id, message };
1055
+ }
1056
+ function failure(id, message) {
1057
+ return { level: 'failure', id, message };
1058
+ }
1059
+ function errorMessage(error) {
1060
+ return error instanceof Error ? error.message : String(error);
1061
+ }
1062
+ //# sourceMappingURL=commands.js.map