anhedral 0.10.0 → 0.11.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 (120) hide show
  1. package/README.md +78 -39
  2. package/assets/{anhedral-cli-init-technical.svg → anhedral-stack-technical.svg} +6 -6
  3. package/assets/{anhedral-cli-init.svg → anhedral-stack.svg} +7 -7
  4. package/assets/security/audit-deps.mjs +6 -1
  5. package/dist/bin.js +271 -154
  6. package/dist/bounded-file.d.ts +1 -0
  7. package/dist/bounded-file.js +1 -0
  8. package/dist/capabilities.d.ts +4 -4
  9. package/dist/capabilities.js +1 -1
  10. package/dist/capability-contract.d.ts +47 -0
  11. package/dist/capability-contract.js +136 -0
  12. package/dist/cli-arguments.d.ts +16 -0
  13. package/dist/cli-arguments.js +115 -0
  14. package/dist/cli-output.d.ts +8 -0
  15. package/dist/cli-output.js +211 -0
  16. package/dist/cloudflare.d.ts +7 -2
  17. package/dist/cloudflare.js +166 -36
  18. package/dist/command-schema.d.ts +21 -0
  19. package/dist/command-schema.js +374 -0
  20. package/dist/delivery.d.ts +2 -2
  21. package/dist/delivery.js +21 -4
  22. package/dist/dependencies.d.ts +32 -0
  23. package/dist/dependencies.js +47 -1
  24. package/dist/index.d.ts +23 -1
  25. package/dist/index.js +15 -1
  26. package/dist/inspector-model.d.ts +115 -0
  27. package/dist/inspector-model.js +1 -0
  28. package/dist/lifecycle-model.d.ts +15 -0
  29. package/dist/lifecycle-model.js +20 -0
  30. package/dist/lifecycle.d.ts +12 -0
  31. package/dist/lifecycle.js +92 -0
  32. package/dist/mobile-delivery.d.ts +100 -0
  33. package/dist/mobile-delivery.js +319 -0
  34. package/dist/platforms/desktop.js +4 -5
  35. package/dist/platforms/extension.js +9 -73
  36. package/dist/platforms/mobile.js +14 -52
  37. package/dist/project-inspector.d.ts +4 -0
  38. package/dist/project-inspector.js +646 -0
  39. package/dist/project-state.d.ts +17 -0
  40. package/dist/project-state.js +239 -11
  41. package/dist/provider-auth.d.ts +41 -0
  42. package/dist/provider-auth.js +210 -0
  43. package/dist/providers/adapters.d.ts +17 -0
  44. package/dist/providers/adapters.js +269 -0
  45. package/dist/providers/api.d.ts +28 -0
  46. package/dist/providers/api.js +252 -0
  47. package/dist/providers/config.d.ts +15 -0
  48. package/dist/providers/config.js +184 -0
  49. package/dist/providers/definitions.d.ts +318 -0
  50. package/dist/providers/definitions.js +127 -0
  51. package/dist/providers/provision.d.ts +24 -0
  52. package/dist/providers/provision.js +76 -0
  53. package/dist/providers/receipts.d.ts +20 -0
  54. package/dist/providers/receipts.js +66 -0
  55. package/dist/providers/recovery-verification.d.ts +7 -0
  56. package/dist/providers/recovery-verification.js +112 -0
  57. package/dist/providers/recovery.d.ts +30 -0
  58. package/dist/providers/recovery.js +34 -0
  59. package/dist/providers/runtime.d.ts +10 -0
  60. package/dist/providers/runtime.js +231 -0
  61. package/dist/providers/schema.d.ts +28 -0
  62. package/dist/providers/schema.js +49 -0
  63. package/dist/providers/state.d.ts +56 -0
  64. package/dist/providers/state.js +329 -0
  65. package/dist/provision.d.ts +1 -1
  66. package/dist/provision.js +61 -3
  67. package/dist/readiness.js +11 -3
  68. package/dist/registry/add.d.ts +6 -0
  69. package/dist/registry/add.js +35 -4
  70. package/dist/registry/commands.d.ts +3 -2
  71. package/dist/registry/commands.js +53 -14
  72. package/dist/registry/options.js +3 -2
  73. package/dist/registry/preview.d.ts +11 -0
  74. package/dist/registry/preview.js +91 -0
  75. package/dist/registry/receipt.js +1 -0
  76. package/dist/registry/resolve.js +19 -13
  77. package/dist/registry/types.d.ts +1 -0
  78. package/dist/registry/usage.d.ts +10 -0
  79. package/dist/registry/usage.js +22 -37
  80. package/dist/render.d.ts +2 -0
  81. package/dist/render.js +10 -0
  82. package/dist/scaffold/api.js +20 -3
  83. package/dist/scaffold/auth.js +68 -28
  84. package/dist/scaffold/bootstrap.d.ts +2 -0
  85. package/dist/scaffold/bootstrap.js +9 -0
  86. package/dist/scaffold/database.js +12 -6
  87. package/dist/scaffold/documentation.js +11 -8
  88. package/dist/scaffold/environment.d.ts +2 -0
  89. package/dist/scaffold/environment.js +18 -1
  90. package/dist/scaffold/files.d.ts +1 -0
  91. package/dist/scaffold/files.js +15 -4
  92. package/dist/scaffold/services.js +37 -13
  93. package/dist/scaffold/stack-documentation.d.ts +3 -0
  94. package/dist/scaffold/stack-documentation.js +52 -0
  95. package/dist/scaffold/styling.d.ts +3 -0
  96. package/dist/scaffold/styling.js +84 -0
  97. package/dist/scaffold/web.js +31 -11
  98. package/dist/scaffold/workspace.js +49 -22
  99. package/dist/ship.d.ts +320 -0
  100. package/dist/ship.js +211 -0
  101. package/dist/stack-registry.d.ts +2 -2
  102. package/dist/stack-registry.js +44 -11
  103. package/dist/standard.d.ts +2 -1
  104. package/dist/standard.js +13 -12
  105. package/dist/vercel.d.ts +3 -3
  106. package/dist/vercel.js +27 -1
  107. package/dist/version.d.ts +1 -1
  108. package/dist/version.js +1 -1
  109. package/dist/vinext.js +9 -18
  110. package/dist/workspace-graph.js +3 -2
  111. package/docs/application-stack-standard.md +51 -80
  112. package/docs/cli-design.md +67 -0
  113. package/docs/delivery-workflow.md +29 -15
  114. package/docs/plugin-ui.md +61 -0
  115. package/docs/provider-configuration.md +117 -0
  116. package/package.json +16 -12
  117. package/dist/managed-hosting.d.ts +0 -49
  118. package/dist/managed-hosting.js +0 -373
  119. package/docs/managed-hosting-architecture.md +0 -87
  120. package/docs/sponsorships.md +0 -25
@@ -0,0 +1,319 @@
1
+ import path from 'node:path';
2
+ import { realpathSync, existsSync, writeFileSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { EAS_CLI_VERSION } from './dependencies.js';
6
+ import { readBoundedFile, MAX_PROJECT_JSON_BYTES } from './bounded-file.js';
7
+ import { requiredProviders } from './provider-auth.js';
8
+ import { planProviderProvisioning, provisionSelectedProviders } from './providers/provision.js';
9
+ import { configuredRuntime, runtimeTargets, RUNTIME_KEYS } from './providers/runtime.js';
10
+ import { CREDENTIALS } from './providers/api.js';
11
+ import { atomicJson, defaultRunner } from './project-state.js';
12
+ import { withLifecycleOperation } from './lifecycle.js';
13
+ function artifactUrl(value) {
14
+ if (typeof value !== 'string')
15
+ return undefined;
16
+ try {
17
+ const url = new URL(value);
18
+ if (url.protocol !== 'https:' || url.username || url.password)
19
+ return undefined;
20
+ return url.href;
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
26
+ function pendingBuild(root, commit, options, projectId) {
27
+ const file = path.join(root, '.anhedral/mobile-build.pending.json');
28
+ if (!existsSync(file))
29
+ return undefined;
30
+ let pending;
31
+ try {
32
+ pending = JSON.parse(readBoundedFile(file, MAX_PROJECT_JSON_BYTES).toString('utf8'));
33
+ }
34
+ catch {
35
+ throw new Error('Invalid pending EAS build. Inspect the selected project before retrying.');
36
+ }
37
+ if (!pending ||
38
+ typeof pending !== 'object' ||
39
+ Array.isArray(pending) ||
40
+ pending.schemaVersion !== 1 ||
41
+ Object.keys(pending).some((key) => ![
42
+ 'schemaVersion',
43
+ 'status',
44
+ 'commit',
45
+ 'platform',
46
+ 'profile',
47
+ 'projectId',
48
+ 'buildId',
49
+ ].includes(key)) ||
50
+ ![undefined, 'submitting', 'queued'].includes(pending.status) ||
51
+ pending.commit !== commit ||
52
+ pending.platform !== options.platform ||
53
+ pending.profile !== options.profile ||
54
+ pending.projectId !== projectId ||
55
+ (pending.status === 'submitting'
56
+ ? Object.hasOwn(pending, 'buildId')
57
+ : typeof pending.buildId !== 'string' || !/^[a-zA-Z0-9-]{1,255}$/.test(pending.buildId)))
58
+ throw new Error('Pending EAS build differs from the selected source or profile.');
59
+ if (pending.status === 'submitting')
60
+ throw new Error('EAS submission has an unknown outcome. Reconcile the selected project before retrying.');
61
+ return pending;
62
+ }
63
+ /** EAS builds produce artifacts; store submission is a separate user action. */
64
+ export async function buildMobileApplication(root, options, run = defaultRunner) {
65
+ if (options.dryRun)
66
+ return executeMobileBuild(root, options, run);
67
+ // Validate local build configuration before creating an execution record.
68
+ await executeMobileBuild(root, { ...options, dryRun: true }, run);
69
+ return withLifecycleOperation(root, 'mobile', run, (session) => session.phase('mobile-build', () => executeMobileBuild(root, options, run)));
70
+ }
71
+ async function executeMobileBuild(root, options, run = defaultRunner) {
72
+ root = realpathSync(root);
73
+ if (!['ios', 'android'].includes(options.platform) ||
74
+ !/^[a-z][a-z0-9-]{0,63}$/.test(options.profile))
75
+ throw new Error('Select an explicit mobile platform and build profile.');
76
+ if (!requiredProviders(root).some((provider) => provider.id === 'expo'))
77
+ throw new Error('This project does not select Expo.');
78
+ const directory = realpathSync(path.join(root, 'apps/mobile'));
79
+ if (!directory.startsWith(root + path.sep))
80
+ throw new Error('Mobile app escapes project directory.');
81
+ let configuration;
82
+ try {
83
+ configuration = JSON.parse(readBoundedFile(path.join(directory, 'eas.json'), MAX_PROJECT_JSON_BYTES).toString('utf8'));
84
+ }
85
+ catch {
86
+ throw new Error('A valid EAS build configuration is required.');
87
+ }
88
+ const profile = configuration?.build?.[options.profile];
89
+ if (!profile || typeof profile !== 'object' || Array.isArray(profile))
90
+ throw new Error('Requested EAS profile is not configured.');
91
+ if (profile.extends)
92
+ throw new Error('Resolve inherited EAS profiles before automatic delivery.');
93
+ const selectedEnvironment = profile.environment ??
94
+ (profile.developmentClient
95
+ ? 'development'
96
+ : profile.distribution === 'internal'
97
+ ? 'preview'
98
+ : 'production');
99
+ if (!['development', 'preview', 'production'].includes(selectedEnvironment))
100
+ throw new Error('Select an explicit supported EAS environment.');
101
+ const args = [
102
+ 'dlx',
103
+ `eas-cli@${EAS_CLI_VERSION}`,
104
+ 'build',
105
+ '--platform',
106
+ options.platform,
107
+ '--profile',
108
+ options.profile,
109
+ '--non-interactive',
110
+ '--json',
111
+ '--no-wait',
112
+ ];
113
+ if (options.dryRun)
114
+ return {
115
+ platform: options.platform,
116
+ profile: options.profile,
117
+ command: ['pnpm', ...args],
118
+ mutation: false,
119
+ storeSubmission: false,
120
+ integrations: planProviderProvisioning(root),
121
+ };
122
+ const app = JSON.parse(readBoundedFile(path.join(directory, 'app.json'), MAX_PROJECT_JSON_BYTES).toString('utf8')).expo;
123
+ if (['app.config.js', 'app.config.ts'].some((file) => existsSync(path.join(directory, file))))
124
+ throw new Error('Automatic EAS setup requires a static app.json with explicit owner and slug.');
125
+ if (typeof app?.owner !== 'string' ||
126
+ !/^[a-zA-Z0-9_-]+$/.test(app.owner) ||
127
+ typeof app.slug !== 'string' ||
128
+ !/^[a-zA-Z0-9_-]+$/.test(app.slug))
129
+ throw new Error('Set the intended Expo owner and slug in app.json before shipping.');
130
+ const environment = { ...process.env, CI: '1', NO_COLOR: '1' };
131
+ // Expo keeps its own login credential; server/provider keys never reach native builds.
132
+ for (const key of [...Object.values(CREDENTIALS), ...RUNTIME_KEYS])
133
+ if (!key.startsWith('EXPO_PUBLIC_'))
134
+ delete environment[key];
135
+ const source = async () => {
136
+ if (realpathSync((await run('git', ['rev-parse', '--show-toplevel'], root, environment)).trim()) !== root ||
137
+ (await run('git', ['status', '--porcelain'], root, environment)).trim())
138
+ throw new Error('Mobile delivery requires a clean application Git repository.');
139
+ const commit = (await run('git', ['rev-parse', 'HEAD'], root, environment)).trim();
140
+ if (!/^[a-f0-9]{40,64}$/.test(commit))
141
+ throw new Error('Invalid mobile source commit.');
142
+ return commit;
143
+ };
144
+ const initialCommit = await source();
145
+ // An unresolved queue mutation blocks every subsequent provisioning/configuration mutation.
146
+ pendingBuild(root, initialCommit, options, app.extra?.eas?.projectId);
147
+ await run('pnpm', ['run', 'check'], root, environment);
148
+ if ((await source()) !== initialCommit)
149
+ throw new Error('Source changed during mobile checks.');
150
+ const configured = await provisionSelectedProviders(root, false, run);
151
+ if (!configured.ready)
152
+ return {
153
+ state: 'provider-configuration-required',
154
+ deployed: false,
155
+ integrations: configured,
156
+ storeSubmission: false,
157
+ };
158
+ const runtime = configuredRuntime(root);
159
+ const commit = await source();
160
+ if (commit !== initialCommit)
161
+ throw new Error('Source changed during provider configuration.');
162
+ const cli = ['dlx', `eas-cli@${EAS_CLI_VERSION}`];
163
+ if (!app.extra?.eas?.projectId) {
164
+ await run('pnpm', [...cli, 'project:init', '--non-interactive', '--force'], directory, environment);
165
+ const linked = JSON.parse(readBoundedFile(path.join(directory, 'app.json'), MAX_PROJECT_JSON_BYTES).toString('utf8')).expo;
166
+ if (linked?.owner !== app.owner ||
167
+ linked.slug !== app.slug ||
168
+ typeof linked.extra?.eas?.projectId !== 'string' ||
169
+ !/^[a-f0-9-]{36}$/i.test(linked.extra.eas.projectId))
170
+ throw new Error('EAS did not confirm a project link for the selected owner and slug. Inspect the account before retrying.');
171
+ return {
172
+ state: 'configuration-required',
173
+ deployed: false,
174
+ storeSubmission: false,
175
+ nextAction: 'Review and commit the EAS project link written to app.json, then run ship again.',
176
+ };
177
+ }
178
+ const projectId = app.extra.eas.projectId;
179
+ if (typeof projectId !== 'string' || !/^[a-f0-9-]{36}$/i.test(projectId))
180
+ throw new Error('Invalid EAS project ID.');
181
+ const remote = await run('pnpm', [...cli, 'project:info'], directory, environment);
182
+ const remoteName = remote
183
+ .split(/\r?\n/)
184
+ .find((line) => /^fullName\s+/.test(line.trim()))
185
+ ?.trim()
186
+ .split(/\s+/)[1];
187
+ const remoteId = remote
188
+ .split(/\r?\n/)
189
+ .find((line) => /^ID\s+/.test(line.trim()))
190
+ ?.trim()
191
+ .split(/\s+/)[1];
192
+ if (remoteId !== projectId || remoteName !== `@${app.owner}/${app.slug}`)
193
+ throw new Error('EAS project identity differs from the selected owner and project.');
194
+ const values = runtimeTargets(root, runtime)['apps/mobile/.env.local'];
195
+ if (values && Object.keys(values).length) {
196
+ const file = path.join(tmpdir(), `anhedral-expo-${randomUUID()}.env`);
197
+ try {
198
+ writeFileSync(file, Object.entries(values)
199
+ .map(([key, value]) => `${key}=${JSON.stringify(value)}\n`)
200
+ .join(''), { mode: 0o600, flag: 'wx' });
201
+ await run('pnpm', [...cli, 'env:push', '--environment', selectedEnvironment, '--path', file, '--force'], directory, environment);
202
+ }
203
+ finally {
204
+ rmSync(file, { force: true });
205
+ }
206
+ }
207
+ let builds;
208
+ let expectedBuildId;
209
+ const pendingFile = path.join(root, '.anhedral/mobile-build.pending.json');
210
+ try {
211
+ const pending = pendingBuild(root, commit, options, projectId);
212
+ if (pending) {
213
+ const buildId = pending.buildId;
214
+ if (!buildId)
215
+ throw new Error('Pending EAS build has no verified ID.');
216
+ expectedBuildId = buildId;
217
+ builds = [
218
+ JSON.parse(await run('pnpm', [...cli, 'build:view', buildId, '--json'], directory, environment)),
219
+ ];
220
+ }
221
+ else {
222
+ if ((await source()) !== commit)
223
+ throw new Error('Source changed before EAS build submission.');
224
+ // Record intent before the remote queue mutation, including lost or invalid responses.
225
+ atomicJson(pendingFile, {
226
+ schemaVersion: 1,
227
+ status: 'submitting',
228
+ commit,
229
+ platform: options.platform,
230
+ profile: options.profile,
231
+ projectId,
232
+ });
233
+ builds = JSON.parse(await run('pnpm', args, directory, { ...environment, ...values }));
234
+ }
235
+ }
236
+ catch {
237
+ throw new Error('EAS build did not return verified completion; inspect the selected Expo project before retrying.');
238
+ }
239
+ if (!Array.isArray(builds) || builds.length !== 1)
240
+ throw new Error('EAS returned an unexpected build selection.');
241
+ const build = builds[0];
242
+ const downloadUrl = artifactUrl(build?.artifacts?.applicationArchiveUrl ?? build?.artifacts?.buildUrl);
243
+ if (typeof build?.id !== 'string' || !/^[a-zA-Z0-9-]{1,255}$/.test(build.id))
244
+ throw new Error('EAS did not return a valid build ID.');
245
+ if (build.project?.id !== projectId ||
246
+ build.project?.ownerAccount?.name !== app.owner ||
247
+ build.project?.slug !== app.slug)
248
+ throw new Error('EAS build belongs to a different project.');
249
+ if (build.platform !== options.platform.toUpperCase() ||
250
+ build.buildProfile !== options.profile ||
251
+ (build.environment !== undefined && build.environment !== selectedEnvironment) ||
252
+ (options.platform === 'ios' && build.isForIosSimulator !== (profile.ios?.simulator === true)))
253
+ throw new Error('EAS build platform, profile or environment differs from the selected configuration.');
254
+ if (build.gitCommitHash !== commit)
255
+ throw new Error('EAS build source differs from the selected source commit.');
256
+ if (expectedBuildId && build.id !== expectedBuildId)
257
+ throw new Error('EAS returned a different build than the recorded selection.');
258
+ atomicJson(pendingFile, {
259
+ schemaVersion: 1,
260
+ status: 'queued',
261
+ commit,
262
+ platform: options.platform,
263
+ profile: options.profile,
264
+ projectId,
265
+ buildId: build.id,
266
+ });
267
+ const buildPage = `https://expo.dev/accounts/${app.owner}/projects/${app.slug}/builds/${build.id}`;
268
+ if (['NEW', 'IN_QUEUE', 'IN_PROGRESS', 'PENDING_CANCEL'].includes(build.status))
269
+ return {
270
+ schemaVersion: 1,
271
+ state: 'building',
272
+ buildId: build.id,
273
+ buildPageUrl: buildPage,
274
+ commit,
275
+ platform: options.platform,
276
+ profile: options.profile,
277
+ storeSubmission: false,
278
+ nextAction: 'Run the same ship command again to inspect this build without creating another build.',
279
+ };
280
+ if (build.status !== 'FINISHED' || !downloadUrl)
281
+ throw new Error('EAS has not confirmed a completed downloadable build. Inspect the recorded build before retrying.');
282
+ if ((await source()) !== commit)
283
+ throw new Error('Source changed during mobile build; do not treat this artifact as the current release.');
284
+ const owner = build.project?.ownerAccount?.name;
285
+ const slug = build.project?.slug;
286
+ const buildPageUrl = typeof owner === 'string' &&
287
+ /^[a-zA-Z0-9_-]+$/.test(owner) &&
288
+ typeof slug === 'string' &&
289
+ /^[a-zA-Z0-9_-]+$/.test(slug)
290
+ ? `https://expo.dev/accounts/${owner}/projects/${slug}/builds/${build.id}`
291
+ : undefined;
292
+ const result = {
293
+ schemaVersion: 1,
294
+ projectId,
295
+ owner: app.owner,
296
+ slug: app.slug,
297
+ recordedAt: new Date().toISOString(),
298
+ ...(buildPageUrl ? { buildPageUrl } : {}),
299
+ commit,
300
+ platform: options.platform,
301
+ profile: options.profile,
302
+ buildId: build.id,
303
+ downloadUrl,
304
+ state: 'built',
305
+ storeSubmission: false,
306
+ artifact: options.platform === 'ios'
307
+ ? profile.ios?.simulator === true
308
+ ? 'ios-simulator'
309
+ : 'ipa'
310
+ : profile.android?.buildType === 'apk' ||
311
+ profile.distribution === 'internal' ||
312
+ profile.developmentClient === true
313
+ ? 'apk'
314
+ : 'aab',
315
+ };
316
+ atomicJson(path.join(root, '.anhedral/mobile-build.json'), result);
317
+ rmSync(pendingFile, { force: true });
318
+ return result;
319
+ }
@@ -107,9 +107,6 @@ export default defineConfig({
107
107
  build: {
108
108
  outDir: 'dist/renderer',
109
109
  emptyOutDir: true,
110
- // Clerk is already loaded behind a dynamic import. Its optional account-management
111
- // surface is intentionally a large async chunk and does not inflate initial rendering.
112
- chunkSizeWarningLimit: 1600,
113
110
  },
114
111
  });
115
112
  `);
@@ -261,7 +258,7 @@ function isAllowedAppNavigation(value: string): boolean {
261
258
  }
262
259
 
263
260
  // Product-owned window options and navigation policy belong in this file.
264
- // Anhedral's managed main.ts imports this seam and wires lifecycle integrations around it.
261
+ // main.ts imports this seam and manages application lifecycle.
265
262
  export function createAppWindow(): BrowserWindow {
266
263
  const window = new BrowserWindow({
267
264
  width: 1180,
@@ -306,11 +303,13 @@ export function createAppWindow(): BrowserWindow {
306
303
  writeFile(path.join(dir, 'src/main/main.ts'), `import { app, BrowserWindow, session } from 'electron';
307
304
  import { createAppWindow } from './app-window.js';
308
305
 
309
-
310
306
  app.whenReady().then(() => {
311
307
  session.defaultSession.setPermissionCheckHandler(() => false);
312
308
  session.defaultSession.setPermissionRequestHandler((_webContents, _permission, callback) => callback(false));
313
309
  createAppWindow();
310
+ }).catch((error) => {
311
+ console.error('Unable to start the desktop app:', error);
312
+ app.quit();
314
313
  });
315
314
  app.on('window-all-closed', () => {
316
315
  if (process.platform !== 'darwin') app.quit();
@@ -1,3 +1,4 @@
1
+ import { NATIVE_ESLINT_VERSION } from '../dependencies.js';
1
2
  import path from 'node:path';
2
3
  import { writeReactLintConfig } from './lint.js';
3
4
  import { writeFile } from '../util.js';
@@ -41,11 +42,14 @@ function writePackageJson(dir, projectName) {
41
42
  typecheck: 'tsc --noEmit',
42
43
  lint: 'eslint src --max-warnings 0',
43
44
  },
44
- dependencies: EXTENSION_DEPENDENCIES.dependencies,
45
+ dependencies: {
46
+ ...EXTENSION_DEPENDENCIES.dependencies,
47
+ '@workspace/styling': 'workspace:*',
48
+ },
45
49
  devDependencies: {
46
50
  ...EXTENSION_DEPENDENCIES.devDependencies,
47
51
  '@workspace/eslint-config': 'workspace:*',
48
- eslint: '10.0.3',
52
+ eslint: NATIVE_ESLINT_VERSION,
49
53
  },
50
54
  }, null, 2) + '\n');
51
55
  }
@@ -55,13 +59,12 @@ function writeWxtConfig(dir, displayName) {
55
59
  const actionTitleLiteral = jsString(`Open ${displayName}`);
56
60
  writeFile(path.join(dir, 'wxt.config.ts'), `import { defineConfig } from 'wxt';
57
61
 
58
-
59
62
  export default defineConfig({
60
63
  srcDir: 'src',
64
+ dev: { server: { port: 3001, strictPort: true } },
61
65
  manifest: () => {
62
66
  const crxPublicKey = import.meta.env.VITE_CRX_PUBLIC_KEY || '';
63
67
 
64
-
65
68
  return {
66
69
  name: ${nameLiteral},
67
70
  description: ${descriptionLiteral},
@@ -176,51 +179,7 @@ function writeTailwindConfig(dir) {
176
179
  writeFile(path.join(dir, 'tailwind.config.cjs'), `/** @type {import('tailwindcss').Config} */
177
180
  module.exports = {
178
181
  content: ['./src/**/*.{ts,tsx,html}'],
179
- theme: {
180
- extend: {
181
- colors: {
182
- border: 'hsl(var(--border))',
183
- input: 'hsl(var(--input))',
184
- ring: 'hsl(var(--ring))',
185
- background: 'hsl(var(--background))',
186
- foreground: 'hsl(var(--foreground))',
187
- primary: {
188
- DEFAULT: 'hsl(var(--primary))',
189
- foreground: 'hsl(var(--primary-foreground))',
190
- },
191
- secondary: {
192
- DEFAULT: 'hsl(var(--secondary))',
193
- foreground: 'hsl(var(--secondary-foreground))',
194
- },
195
- destructive: {
196
- DEFAULT: 'hsl(var(--destructive))',
197
- foreground: 'hsl(var(--destructive-foreground))',
198
- },
199
- muted: {
200
- DEFAULT: 'hsl(var(--muted))',
201
- foreground: 'hsl(var(--muted-foreground))',
202
- },
203
- accent: {
204
- DEFAULT: 'hsl(var(--accent))',
205
- foreground: 'hsl(var(--accent-foreground))',
206
- },
207
- popover: {
208
- DEFAULT: 'hsl(var(--popover))',
209
- foreground: 'hsl(var(--popover-foreground))',
210
- },
211
- card: {
212
- DEFAULT: 'hsl(var(--card))',
213
- foreground: 'hsl(var(--card-foreground))',
214
- },
215
- },
216
- borderRadius: {
217
- lg: 'var(--radius)',
218
- md: 'calc(var(--radius) - 2px)',
219
- sm: 'calc(var(--radius) - 4px)',
220
- },
221
- },
222
- },
223
- plugins: [],
182
+ presets: [require('@workspace/styling/tailwind')],
224
183
  };
225
184
  `);
226
185
  }
@@ -259,7 +218,7 @@ export default defineBackground(() => {
259
218
  // Open the side panel when the extension icon is clicked.
260
219
  (chrome as ChromeWithSidePanel).sidePanel
261
220
  .setPanelBehavior({ openPanelOnActionClick: true })
262
- .catch(() => {});
221
+ .catch((error) => console.error('Unable to configure the side panel:', error));
263
222
  });
264
223
  `);
265
224
  }
@@ -328,7 +287,6 @@ export function SidePanelApp() {
328
287
  }
329
288
  }, []);
330
289
 
331
-
332
290
  return (
333
291
  <div style={{ padding: 24 }}>
334
292
  <h2>Welcome!</h2>
@@ -351,28 +309,6 @@ function writeStyles(dir) {
351
309
  @tailwind utilities;
352
310
 
353
311
  @layer base {
354
- :root {
355
- --background: 0 0% 100%;
356
- --foreground: 0 0% 9%;
357
- --card: 0 0% 100%;
358
- --card-foreground: 0 0% 9%;
359
- --popover: 0 0% 100%;
360
- --popover-foreground: 0 0% 9%;
361
- --primary: 0 0% 9%;
362
- --primary-foreground: 0 0% 98%;
363
- --secondary: 0 0% 96.1%;
364
- --secondary-foreground: 0 0% 9%;
365
- --muted: 0 0% 96.1%;
366
- --muted-foreground: 0 0% 45.1%;
367
- --accent: 0 0% 96.1%;
368
- --accent-foreground: 0 0% 9%;
369
- --destructive: 0 84.2% 60.2%;
370
- --destructive-foreground: 0 0% 98%;
371
- --border: 0 0% 89.8%;
372
- --input: 0 0% 89.8%;
373
- --ring: 0 0% 63.9%;
374
- --radius: 0.5rem;
375
- }
376
312
 
377
313
  * {
378
314
  border-color: hsl(var(--border));
@@ -1,3 +1,4 @@
1
+ import { NATIVE_ESLINT_VERSION } from '../dependencies.js';
1
2
  import path from 'node:path';
2
3
  import { writeReactLintConfig } from './lint.js';
3
4
  import { appendGitignore, writeFile } from '../util.js';
@@ -62,26 +63,9 @@ module.exports = withNativeWind(config, { input: './global.css', inlineRem: 16 }
62
63
 
63
64
  /** @type {import('tailwindcss').Config} */
64
65
  module.exports = {
65
- darkMode: 'class',
66
66
  content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'],
67
- presets: [require('nativewind/preset')],
68
- theme: {
69
- extend: {
70
- colors: {
71
- border: 'hsl(var(--border))', input: 'hsl(var(--input))', ring: 'hsl(var(--ring))',
72
- background: 'hsl(var(--background))', foreground: 'hsl(var(--foreground))',
73
- primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))' },
74
- secondary: { DEFAULT: 'hsl(var(--secondary))', foreground: 'hsl(var(--secondary-foreground))' },
75
- destructive: { DEFAULT: 'hsl(var(--destructive))', foreground: 'hsl(var(--destructive-foreground))' },
76
- muted: { DEFAULT: 'hsl(var(--muted))', foreground: 'hsl(var(--muted-foreground))' },
77
- accent: { DEFAULT: 'hsl(var(--accent))', foreground: 'hsl(var(--accent-foreground))' },
78
- popover: { DEFAULT: 'hsl(var(--popover))', foreground: 'hsl(var(--popover-foreground))' },
79
- card: { DEFAULT: 'hsl(var(--card))', foreground: 'hsl(var(--card-foreground))' },
80
- },
81
- borderRadius: { lg: 'var(--radius)', md: 'calc(var(--radius) - 2px)', sm: 'calc(var(--radius) - 4px)' },
82
- borderWidth: { hairline: hairlineWidth() },
83
- },
84
- },
67
+ presets: [require('nativewind/preset'), require('@workspace/styling/tailwind')],
68
+ theme: { extend: { borderWidth: { hairline: hairlineWidth() } } },
85
69
  future: { hoverOnlyWhenSupported: true },
86
70
  plugins: [require('tailwindcss-animate')],
87
71
  };
@@ -89,31 +73,6 @@ module.exports = {
89
73
  writeFile(path.join(dir, 'global.css'), `@tailwind base;
90
74
  @tailwind components;
91
75
  @tailwind utilities;
92
-
93
- @layer base {
94
- :root {
95
- --background: 0 0% 100%; --foreground: 0 0% 3.9%;
96
- --card: 0 0% 100%; --card-foreground: 0 0% 3.9%;
97
- --popover: 0 0% 100%; --popover-foreground: 0 0% 3.9%;
98
- --primary: 0 0% 9%; --primary-foreground: 0 0% 98%;
99
- --secondary: 0 0% 96.1%; --secondary-foreground: 0 0% 9%;
100
- --muted: 0 0% 96.1%; --muted-foreground: 0 0% 45.1%;
101
- --accent: 0 0% 96.1%; --accent-foreground: 0 0% 9%;
102
- --destructive: 0 84.2% 60.2%; --destructive-foreground: 0 0% 98%;
103
- --border: 0 0% 89.8%; --input: 0 0% 89.8%; --ring: 0 0% 63%; --radius: 0.625rem;
104
- }
105
- .dark:root {
106
- --background: 0 0% 3.9%; --foreground: 0 0% 98%;
107
- --card: 0 0% 3.9%; --card-foreground: 0 0% 98%;
108
- --popover: 0 0% 3.9%; --popover-foreground: 0 0% 98%;
109
- --primary: 0 0% 98%; --primary-foreground: 0 0% 9%;
110
- --secondary: 0 0% 14.9%; --secondary-foreground: 0 0% 98%;
111
- --muted: 0 0% 14.9%; --muted-foreground: 0 0% 63.9%;
112
- --accent: 0 0% 14.9%; --accent-foreground: 0 0% 98%;
113
- --destructive: 0 70.9% 59.4%; --destructive-foreground: 0 0% 98%;
114
- --border: 0 0% 14.9%; --input: 0 0% 14.9%; --ring: 300 0% 45%;
115
- }
116
- }
117
76
  `);
118
77
  }
119
78
  function expoScheme(projectName) {
@@ -148,23 +107,23 @@ export async function scaffoldExpo({ root, options }) {
148
107
  build: 'pnpm typecheck && pnpm build:web',
149
108
  'build:web': 'expo export --platform web',
150
109
  'eas:login': `pnpm dlx eas-cli@${EAS_CLI_VERSION} login`,
110
+ 'build:simulator:ios': `pnpm dlx eas-cli@${EAS_CLI_VERSION} build --platform ios --profile simulator`,
151
111
  'build:internal:ios': `pnpm dlx eas-cli@${EAS_CLI_VERSION} build --platform ios --profile preview`,
152
112
  'build:internal:android': `pnpm dlx eas-cli@${EAS_CLI_VERSION} build --platform android --profile preview`,
153
113
  'build:production:ios': `pnpm dlx eas-cli@${EAS_CLI_VERSION} build --platform ios --profile production`,
154
114
  'build:production:android': `pnpm dlx eas-cli@${EAS_CLI_VERSION} build --platform android --profile production`,
155
- 'submit:ios': `pnpm dlx eas-cli@${EAS_CLI_VERSION} submit --platform ios --profile production --latest`,
156
- 'submit:android': `pnpm dlx eas-cli@${EAS_CLI_VERSION} submit --platform android --profile production --latest`,
157
115
  typecheck: 'tsc --noEmit',
158
116
  lint: 'eslint app components lib --max-warnings 0',
159
117
  },
160
118
  dependencies: {
161
119
  ...MOBILE_APP_DEPENDENCIES.dependencies,
162
120
  ...MOBILE_NATIVEWIND_DEPENDENCIES.dependencies,
121
+ '@workspace/styling': 'workspace:*',
163
122
  },
164
123
  devDependencies: {
165
124
  ...MOBILE_APP_DEPENDENCIES.devDependencies,
166
125
  '@workspace/eslint-config': 'workspace:*',
167
- eslint: '10.0.3',
126
+ eslint: NATIVE_ESLINT_VERSION,
168
127
  },
169
128
  }, null, 2) + '\n');
170
129
  writeFile(path.join(dir, 'app.json'), JSON.stringify({
@@ -200,7 +159,6 @@ export async function scaffoldExpo({ root, options }) {
200
159
  import { PortalHost } from '@rn-primitives/portal';
201
160
  import { Stack } from 'expo-router';
202
161
 
203
-
204
162
  export default function RootLayout() {
205
163
  return <><Stack /><PortalHost /></>;
206
164
  }
@@ -215,7 +173,7 @@ export default function HomeScreen() {
215
173
  <StatusBar style="auto" />
216
174
  <View style={styles.content}>
217
175
  <Text accessibilityRole="header" style={styles.title}>{${nameLiteral}}</Text>
218
- <Text selectable style={styles.subtitle}>Deterministic Expo application generated by Anhedral.</Text>
176
+ <Text selectable style={styles.subtitle}>Expo application scaffold generated by Anhedral.</Text>
219
177
  <Button onPress={() => void Linking.openURL('https://docs.expo.dev')}>Learn more</Button>
220
178
  </View>
221
179
  </ScrollView>
@@ -235,10 +193,14 @@ const styles = StyleSheet.create({
235
193
  cli: { version: '>= 16.0.0' },
236
194
  build: {
237
195
  development: { developmentClient: true, distribution: 'internal' },
238
- preview: { distribution: 'internal' },
239
- production: { autoIncrement: true },
196
+ preview: {
197
+ environment: 'preview',
198
+ distribution: 'internal',
199
+ android: { buildType: 'apk' },
200
+ },
201
+ simulator: { environment: 'preview', ios: { simulator: true } },
202
+ production: { environment: 'production', autoIncrement: true },
240
203
  },
241
- submit: { production: {} },
242
204
  }, null, 2) + '\n');
243
205
  appendGitignore(dir, ['.env', '.env.*', '!.env.example', '.expo', 'dist', 'node_modules']);
244
206
  }
@@ -0,0 +1,4 @@
1
+ import type { ProjectInspection } from './inspector-model.js';
2
+ /** Local read-only inspection. Each section has bounded allowlisted inputs and an
3
+ * independent error state. No secrets, commands, provider calls, or migrations. */
4
+ export declare function inspectApplication(root: string): ProjectInspection;