anhedral 0.10.1 → 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 (89) hide show
  1. package/README.md +77 -38
  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/dist/bin.js +271 -154
  5. package/dist/capabilities.d.ts +3 -3
  6. package/dist/capability-contract.d.ts +47 -0
  7. package/dist/capability-contract.js +136 -0
  8. package/dist/cli-arguments.d.ts +16 -0
  9. package/dist/cli-arguments.js +115 -0
  10. package/dist/cli-output.d.ts +8 -0
  11. package/dist/cli-output.js +211 -0
  12. package/dist/cloudflare.d.ts +7 -2
  13. package/dist/cloudflare.js +166 -36
  14. package/dist/command-schema.d.ts +21 -0
  15. package/dist/command-schema.js +374 -0
  16. package/dist/delivery.d.ts +2 -2
  17. package/dist/delivery.js +21 -4
  18. package/dist/index.d.ts +23 -1
  19. package/dist/index.js +15 -1
  20. package/dist/inspector-model.d.ts +115 -0
  21. package/dist/inspector-model.js +1 -0
  22. package/dist/lifecycle-model.d.ts +15 -0
  23. package/dist/lifecycle-model.js +20 -0
  24. package/dist/lifecycle.d.ts +12 -0
  25. package/dist/lifecycle.js +92 -0
  26. package/dist/mobile-delivery.d.ts +100 -0
  27. package/dist/mobile-delivery.js +319 -0
  28. package/dist/platforms/mobile.js +8 -5
  29. package/dist/project-inspector.d.ts +4 -0
  30. package/dist/project-inspector.js +646 -0
  31. package/dist/project-state.d.ts +17 -0
  32. package/dist/project-state.js +239 -11
  33. package/dist/provider-auth.d.ts +41 -0
  34. package/dist/provider-auth.js +210 -0
  35. package/dist/providers/adapters.d.ts +17 -0
  36. package/dist/providers/adapters.js +269 -0
  37. package/dist/providers/api.d.ts +28 -0
  38. package/dist/providers/api.js +252 -0
  39. package/dist/providers/config.d.ts +15 -0
  40. package/dist/providers/config.js +184 -0
  41. package/dist/providers/definitions.d.ts +318 -0
  42. package/dist/providers/definitions.js +127 -0
  43. package/dist/providers/provision.d.ts +24 -0
  44. package/dist/providers/provision.js +76 -0
  45. package/dist/providers/receipts.d.ts +20 -0
  46. package/dist/providers/receipts.js +66 -0
  47. package/dist/providers/recovery-verification.d.ts +7 -0
  48. package/dist/providers/recovery-verification.js +112 -0
  49. package/dist/providers/recovery.d.ts +30 -0
  50. package/dist/providers/recovery.js +34 -0
  51. package/dist/providers/runtime.d.ts +10 -0
  52. package/dist/providers/runtime.js +231 -0
  53. package/dist/providers/schema.d.ts +28 -0
  54. package/dist/providers/schema.js +49 -0
  55. package/dist/providers/state.d.ts +56 -0
  56. package/dist/providers/state.js +329 -0
  57. package/dist/provision.d.ts +1 -1
  58. package/dist/provision.js +61 -3
  59. package/dist/registry/add.d.ts +6 -0
  60. package/dist/registry/add.js +29 -1
  61. package/dist/registry/commands.d.ts +3 -2
  62. package/dist/registry/commands.js +53 -14
  63. package/dist/registry/options.js +3 -2
  64. package/dist/registry/preview.d.ts +11 -0
  65. package/dist/registry/preview.js +91 -0
  66. package/dist/registry/usage.d.ts +10 -0
  67. package/dist/registry/usage.js +22 -37
  68. package/dist/scaffold/auth.js +3 -19
  69. package/dist/scaffold/workspace.js +11 -0
  70. package/dist/ship.d.ts +320 -0
  71. package/dist/ship.js +211 -0
  72. package/dist/stack-registry.d.ts +2 -2
  73. package/dist/stack-registry.js +7 -1
  74. package/dist/standard.d.ts +2 -1
  75. package/dist/standard.js +11 -11
  76. package/dist/vercel.d.ts +3 -3
  77. package/dist/vercel.js +27 -1
  78. package/dist/version.d.ts +1 -1
  79. package/dist/version.js +1 -1
  80. package/docs/application-stack-standard.md +51 -80
  81. package/docs/cli-design.md +67 -0
  82. package/docs/delivery-workflow.md +29 -15
  83. package/docs/plugin-ui.md +61 -0
  84. package/docs/provider-configuration.md +117 -0
  85. package/package.json +16 -12
  86. package/dist/managed-hosting.d.ts +0 -49
  87. package/dist/managed-hosting.js +0 -373
  88. package/docs/managed-hosting-architecture.md +0 -87
  89. package/docs/sponsorships.md +0 -25
@@ -0,0 +1,646 @@
1
+ import path from 'node:path';
2
+ import { existsSync, lstatSync, readdirSync, realpathSync } from 'node:fs';
3
+ import { parse } from 'jsonc-parser';
4
+ import { readBoundedFile } from './bounded-file.js';
5
+ import { inspectProject, validateSetupPlan } from './readiness.js';
6
+ import { inspectWorkspaceGraph } from './workspace-graph.js';
7
+ import { readLifecycleOperation } from './lifecycle.js';
8
+ import { readProviderStateStatus } from './providers/state.js';
9
+ import { CAPABILITY_CONTRACT } from './capability-contract.js';
10
+ import { PROVIDER_DEFINITIONS } from './providers/definitions.js';
11
+ const MAX_FILE = 256 * 1024;
12
+ const MAX_INPUT = 4 * 1024 * 1024;
13
+ const fields = [
14
+ 'dependencies',
15
+ 'devDependencies',
16
+ 'peerDependencies',
17
+ 'optionalDependencies',
18
+ ];
19
+ function object(value) {
20
+ if (!value || typeof value !== 'object' || Array.isArray(value))
21
+ throw new Error('Invalid object.');
22
+ return value;
23
+ }
24
+ function array(value, limit = 200) {
25
+ if (!Array.isArray(value) || value.length > limit)
26
+ throw new Error('Invalid collection.');
27
+ return value;
28
+ }
29
+ function name(value, maximum = 128) {
30
+ if (typeof value !== 'string' ||
31
+ !value ||
32
+ value.length > maximum ||
33
+ !/^[a-zA-Z0-9@][a-zA-Z0-9_.@/-]*$/.test(value) ||
34
+ /^(?:sk[-_]|rk[-_]|whsec[-_]|re_|Bearer)/i.test(value))
35
+ throw new Error('Invalid identity.');
36
+ return value;
37
+ }
38
+ function identity(value) {
39
+ const id = typeof value === 'number' && Number.isSafeInteger(value) && value > 0
40
+ ? String(value)
41
+ : name(value, 255);
42
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,254}$/.test(id))
43
+ throw new Error('Invalid resource identity.');
44
+ return id;
45
+ }
46
+ function directory(value) {
47
+ const relative = name(value);
48
+ if (!/^(apps|packages)\/[a-z0-9][a-z0-9-]*$/.test(relative))
49
+ throw new Error('Invalid application directory.');
50
+ return relative;
51
+ }
52
+ function time(value) {
53
+ if (value === undefined || value === '')
54
+ return null;
55
+ if (typeof value !== 'string' || value.length > 64)
56
+ throw new Error('Invalid recorded time.');
57
+ const parts = value.match(/^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(?:\.\d+)?(Z|[+-](\d\d):(\d\d))$/);
58
+ if (!parts)
59
+ throw new Error('Invalid recorded time.');
60
+ const [, year, month, day, hour, minute, second, , zoneHour = '0', zoneMinute = '0'] = parts;
61
+ const days = new Date(Date.UTC(Number(year), Number(month), 0)).getUTCDate();
62
+ if (Number(month) < 1 ||
63
+ Number(month) > 12 ||
64
+ Number(day) < 1 ||
65
+ Number(day) > days ||
66
+ Number(hour) > 23 ||
67
+ Number(minute) > 59 ||
68
+ Number(second) > 59 ||
69
+ Number(zoneHour) > 23 ||
70
+ Number(zoneMinute) > 59 ||
71
+ !Number.isFinite(Date.parse(value)))
72
+ throw new Error('Invalid recorded time.');
73
+ return value;
74
+ }
75
+ function commit(value) {
76
+ if (typeof value !== 'string' || !/^[a-f0-9]{40,64}$/.test(value))
77
+ throw new Error('Invalid source identity.');
78
+ return value;
79
+ }
80
+ function publicUrl(value) {
81
+ if (value === undefined || value === null)
82
+ return null;
83
+ if (typeof value !== 'string' || value.length > 2048)
84
+ throw new Error('Invalid URL.');
85
+ const url = new URL(value);
86
+ if (url.protocol !== 'https:' ||
87
+ url.username ||
88
+ url.password ||
89
+ url.search ||
90
+ url.hash ||
91
+ /[\s\\]/.test(value))
92
+ throw new Error('Unsafe URL.');
93
+ return url.href;
94
+ }
95
+ function section(empty, message, read) {
96
+ try {
97
+ return { state: 'available', data: read() };
98
+ }
99
+ catch {
100
+ return { state: 'unavailable', data: empty, error: message };
101
+ }
102
+ }
103
+ /** Local read-only inspection. Each section has bounded allowlisted inputs and an
104
+ * independent error state. No secrets, commands, provider calls, or migrations. */
105
+ export function inspectApplication(root) {
106
+ root = realpathSync(root);
107
+ if (!lstatSync(root).isDirectory())
108
+ throw new Error('Select an application directory.');
109
+ let bytes = 0;
110
+ const contents = new Map();
111
+ const documents = new Map();
112
+ function localFile(relative) {
113
+ if (relative.includes('..') || relative.startsWith('/') || relative.includes('\\'))
114
+ throw new Error('Invalid source path.');
115
+ const file = path.join(root, relative);
116
+ if (!existsSync(file))
117
+ return null;
118
+ if (realpathSync(file) !== file || !file.startsWith(root + path.sep))
119
+ throw new Error('Source symlink is unsupported.');
120
+ return file;
121
+ }
122
+ function content(relative, maximum = MAX_FILE) {
123
+ if (contents.has(relative))
124
+ return contents.get(relative);
125
+ const file = localFile(relative);
126
+ if (!file) {
127
+ contents.set(relative, null);
128
+ return null;
129
+ }
130
+ const value = readBoundedFile(file, maximum);
131
+ bytes += value.length;
132
+ if (bytes > MAX_INPUT)
133
+ throw new Error('Inspection input budget exceeded.');
134
+ const source = value.toString('utf8');
135
+ contents.set(relative, source);
136
+ return source;
137
+ }
138
+ function document(relative, required = false) {
139
+ if (documents.has(relative)) {
140
+ const saved = documents.get(relative);
141
+ if (required && saved === null)
142
+ throw new Error('Missing source.');
143
+ return saved;
144
+ }
145
+ const source = content(relative);
146
+ if (source === null) {
147
+ if (required)
148
+ throw new Error('Missing source.');
149
+ documents.set(relative, null);
150
+ return null;
151
+ }
152
+ const errors = [];
153
+ const value = parse(source, errors);
154
+ if (errors.length)
155
+ throw new Error('Invalid source JSON.');
156
+ const result = object(value);
157
+ documents.set(relative, result);
158
+ return result;
159
+ }
160
+ function apps() {
161
+ const parent = localFile('apps');
162
+ if (!parent)
163
+ return [];
164
+ const entries = readdirSync(parent, { withFileTypes: true });
165
+ if (entries.length > 100 || entries.some((entry) => entry.isSymbolicLink()))
166
+ throw new Error('Invalid apps collection.');
167
+ return entries
168
+ .filter((entry) => entry.isDirectory())
169
+ .map((entry) => directory(`apps/${entry.name}`));
170
+ }
171
+ let applicationName = 'Application';
172
+ try {
173
+ applicationName = name(document('package.json', true).name, 214);
174
+ }
175
+ catch {
176
+ /* Root identity is optional; sections remain inspectable. */
177
+ }
178
+ const selection = section({ cliVersion: null, products: [], hosting: null }, 'Project selection or scaffold provenance is unavailable. Use the recorded CLI version and inspect source.', () => {
179
+ const setup = document('anhedral.setup.json', true);
180
+ if (typeof setup?.cliVersion !== 'string' || !/^\d+\.\d+\.\d+$/.test(setup.cliVersion))
181
+ throw new Error('Invalid CLI version.');
182
+ validateSetupPlan(setup);
183
+ return { cliVersion: setup.cliVersion, products: setup.products, hosting: setup.hosting };
184
+ });
185
+ const setupProviders = section([], 'Provider requirements are unavailable until the selected stack is validated.', () => {
186
+ if (selection.state !== 'available')
187
+ throw new Error('Unknown stack.');
188
+ const products = selection.data.products;
189
+ const providers = [];
190
+ const add = (id, managementCredential, ownerSteps) => providers.push({
191
+ id,
192
+ managementCredential,
193
+ ownerSteps: [...ownerSteps],
194
+ authentication: 'not-checked',
195
+ permissions: 'unknown',
196
+ billing: 'unknown',
197
+ });
198
+ if (products.includes('hono') ||
199
+ products.includes('vinext') ||
200
+ (products.includes('next') && selection.data.hosting === 'cloudflare'))
201
+ add('cloudflare', null, [
202
+ 'Authenticate and connect the intended account and audience; verify service eligibility and required scopes.',
203
+ ]);
204
+ if (products.includes('next') && selection.data.hosting === 'vercel')
205
+ add('vercel', null, [
206
+ 'Authenticate and connect the intended team, project, plan, and audience.',
207
+ ]);
208
+ for (const [id, definition] of Object.entries(PROVIDER_DEFINITIONS))
209
+ if (products.includes(id) || (id === 'openai' && products.includes('ai-sdk')))
210
+ add(id, definition.credential, definition.ownerSteps);
211
+ if (products.includes('expo'))
212
+ add('expo', null, [
213
+ 'Authenticate the intended Expo owner; choose a build profile and supply signing prerequisites. No store submission.',
214
+ ]);
215
+ return providers;
216
+ });
217
+ const workspaces = section([], 'Workspace manifests or local dependency references are unavailable.', () => {
218
+ for (const group of ['apps', 'packages']) {
219
+ const parent = localFile(group);
220
+ if (!parent)
221
+ continue;
222
+ const entries = readdirSync(parent, { withFileTypes: true });
223
+ if (entries.length > 100 || entries.some((entry) => entry.isSymbolicLink()))
224
+ throw new Error('Invalid workspace collection.');
225
+ for (const entry of entries.filter((entry) => entry.isDirectory()))
226
+ document(`${directory(`${group}/${entry.name}`)}/package.json`, true);
227
+ }
228
+ const graph = inspectWorkspaceGraph(root);
229
+ if (graph.length > 100)
230
+ throw new Error('Workspace output exceeds limits.');
231
+ const evidence = graph.map((node) => {
232
+ const manifest = document(`${node.directory}/package.json`, true);
233
+ return {
234
+ name: name(node.name, 214),
235
+ directory: directory(node.directory),
236
+ dependencies: fields.flatMap((kind) => {
237
+ const dependencies = manifest[kind] === undefined ? {} : object(manifest[kind]);
238
+ return node.dependencies
239
+ .filter((dependency) => typeof dependencies[dependency] === 'string' &&
240
+ dependencies[dependency].startsWith('workspace:'))
241
+ .map((dependency) => ({ name: name(dependency, 214), kind }));
242
+ }),
243
+ };
244
+ });
245
+ if (evidence.some((entry) => entry.dependencies.length > 500))
246
+ throw new Error('Dependency output exceeds limits.');
247
+ return evidence;
248
+ });
249
+ const configuration = section({ bindings: [], schedules: [], environment: [] }, 'Configured bindings, schedules, or environment requirements are unavailable.', () => {
250
+ const bindings = [], schedules = [], environment = [];
251
+ const add = (application, value, kind, idField) => {
252
+ const entry = object(value);
253
+ const resource = idField ? entry[idField] : undefined;
254
+ bindings.push({
255
+ application,
256
+ name: name(entry.binding ?? entry.name),
257
+ kind,
258
+ identifierRequired: Boolean(idField),
259
+ configured: !idField ||
260
+ (resource !== undefined &&
261
+ resource !== '' &&
262
+ !String(resource).startsWith('REPLACE_WITH_')),
263
+ resourceId: resource === undefined ||
264
+ resource === '' ||
265
+ String(resource).startsWith('REPLACE_WITH_')
266
+ ? null
267
+ : identity(resource),
268
+ });
269
+ };
270
+ for (const application of apps()) {
271
+ const config = document(`${application}/wrangler.jsonc`);
272
+ if (config) {
273
+ for (const [field, kind, idField] of [
274
+ ['d1_databases', 'D1', 'database_id'],
275
+ ['r2_buckets', 'R2', 'bucket_name'],
276
+ ['kv_namespaces', 'KV', 'id'],
277
+ ['pipelines', 'Basin stream', 'stream'],
278
+ ['hyperdrive', 'Hyperdrive', 'id'],
279
+ ])
280
+ if (config[field] !== undefined)
281
+ for (const entry of array(config[field], 500))
282
+ add(application, entry, kind, idField);
283
+ if (config.queues) {
284
+ const queues = object(config.queues);
285
+ for (const entry of array(queues.producers ?? [], 500))
286
+ add(application, entry, 'Queue producer', 'queue');
287
+ for (const value of array(queues.consumers ?? [], 500)) {
288
+ const entry = object(value);
289
+ add(application, { binding: entry.queue, queue: entry.queue }, 'Queue consumer', 'queue');
290
+ }
291
+ }
292
+ if (config.durable_objects)
293
+ for (const value of array(object(config.durable_objects).bindings ?? [], 500)) {
294
+ const entry = object(value);
295
+ add(application, { binding: entry.name }, 'Durable Object namespace');
296
+ }
297
+ if (config.workflows)
298
+ for (const entry of array(config.workflows, 500))
299
+ add(application, entry, 'Workflow', 'name');
300
+ if (config.ai)
301
+ add(application, config.ai, 'Workers AI');
302
+ if (config.triggers)
303
+ for (const expression of array(object(config.triggers).crons ?? [], 200)) {
304
+ if (typeof expression !== 'string' ||
305
+ expression.length > 120 ||
306
+ /[\x00-\x1f\x7f]/.test(expression) ||
307
+ expression.trim().split(/\s+/).length !== 5 ||
308
+ !expression
309
+ .trim()
310
+ .split(/\s+/)
311
+ .every((field) => /^(?:(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC|SUN|MON|TUE|WED|THU|FRI|SAT)|[0-9*\/,?LW#-])+$/.test(field)))
312
+ throw new Error('Invalid configured schedule.');
313
+ schedules.push({ application, expression });
314
+ }
315
+ }
316
+ for (const example of ['.env.example', '.dev.vars.example']) {
317
+ const source = content(`${application}/${example}`);
318
+ if (!source)
319
+ continue;
320
+ for (const line of source.split(/\r?\n/)) {
321
+ const key = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/)?.[1];
322
+ if (key &&
323
+ key.length <= 128 &&
324
+ !environment.some((entry) => entry.application === application && entry.name === key))
325
+ environment.push({ application, name: key });
326
+ }
327
+ }
328
+ }
329
+ if (bindings.length > 500 || schedules.length > 200 || environment.length > 500)
330
+ throw new Error('Configuration output exceeds limits.');
331
+ return { bindings, schedules, environment };
332
+ });
333
+ function history(filename, project) {
334
+ const value = document(filename);
335
+ if (!value)
336
+ return [];
337
+ if (value.schemaVersion !== 1 || value.projectId !== project.id)
338
+ throw new Error('Foreign release history.');
339
+ return array(value.releases).map(object).slice(-50);
340
+ }
341
+ function release(value, url) {
342
+ if (!['running', 'checked', 'deployed', 'failed'].includes(String(value.state)))
343
+ throw new Error('Invalid release state.');
344
+ return {
345
+ id: identity(value.id),
346
+ state: String(value.state),
347
+ commit: commit(value.commit),
348
+ recordedAt: time(value.recordedAt ?? value.finishedAt ?? value.startedAt),
349
+ url: publicUrl(url),
350
+ kind: 'deployment',
351
+ };
352
+ }
353
+ function attempts(target, values, project) {
354
+ const known = target.provider === 'cloudflare'
355
+ ? array(project.targets).map((entry) => name(object(entry).id))
356
+ : [];
357
+ const seen = new Set();
358
+ for (const value of values) {
359
+ try {
360
+ let entry;
361
+ if (target.provider === 'vercel')
362
+ entry = release(value, value.url);
363
+ else {
364
+ const members = array(value.targets ?? []).map(object);
365
+ const ids = members.map((member) => name(member.target));
366
+ if (new Set(ids).size !== ids.length || ids.some((id) => !known.includes(id)))
367
+ throw new Error('Conflicting target identities.');
368
+ const member = members.find((item) => item.target === target.id.slice('cloudflare:'.length));
369
+ if (value.state === 'deployed' &&
370
+ (!member || member.providerConfirmed !== true || !member.versionId))
371
+ throw new Error('Target deployment evidence is missing.');
372
+ if (member?.versionId)
373
+ identity(member.versionId);
374
+ entry = release(value, member ? array(member.urls ?? [], 10)[0] : undefined);
375
+ if (member?.providerConfirmed === true &&
376
+ member.versionId &&
377
+ entry.state !== 'deployed') {
378
+ entry.operationState = entry.state;
379
+ entry.state = 'deployed';
380
+ }
381
+ }
382
+ if (seen.has(entry.id))
383
+ throw new Error('Repeated release identity.');
384
+ seen.add(entry.id);
385
+ target.latestAttempt = entry;
386
+ if (entry.state === 'deployed')
387
+ target.lastSuccessful = entry;
388
+ }
389
+ catch {
390
+ target.latestAttempt = null;
391
+ target.error =
392
+ 'Some recorded release evidence is unavailable or conflicts with this target. Valid earlier releases remain historical evidence.';
393
+ }
394
+ }
395
+ }
396
+ function hostingTargets() {
397
+ const targets = [];
398
+ for (const provider of ['cloudflare', 'vercel']) {
399
+ const manifest = provider === 'cloudflare' ? 'anhedral.project.json' : 'anhedral.web.json';
400
+ if (!localFile(manifest))
401
+ continue;
402
+ try {
403
+ const project = document(manifest, true);
404
+ if (project.schemaVersion !== 1 ||
405
+ project.provider !== provider ||
406
+ project.ownership !== 'customer' ||
407
+ typeof project.id !== 'string' ||
408
+ !/^[a-f0-9-]{36}$/.test(project.id))
409
+ throw new Error('Invalid hosting identity.');
410
+ if (provider === 'cloudflare' &&
411
+ project.accountId !== null &&
412
+ (typeof project.accountId !== 'string' || !/^[a-f0-9]{32}$/i.test(project.accountId)))
413
+ throw new Error('Invalid Cloudflare account identity.');
414
+ const audience = project.audience;
415
+ if (!['public', 'private', 'unconfigured'].includes(String(audience)))
416
+ throw new Error('Invalid audience.');
417
+ const applications = provider === 'cloudflare'
418
+ ? array(project.targets, 100).map(object)
419
+ : [{ id: 'web', directory: 'apps/web' }];
420
+ for (const app of applications) {
421
+ const target = {
422
+ id: `${provider}:${name(app.id)}`,
423
+ provider,
424
+ application: directory(app.directory),
425
+ audience: audience,
426
+ accountReference: project[provider === 'cloudflare' ? 'accountId' : 'teamId']
427
+ ? identity(project[provider === 'cloudflare' ? 'accountId' : 'teamId'])
428
+ : null,
429
+ latestAttempt: null,
430
+ lastSuccessful: null,
431
+ delivery: 'implemented',
432
+ liveStatus: 'unknown',
433
+ };
434
+ try {
435
+ attempts(target, history(provider === 'cloudflare'
436
+ ? '.anhedral/releases.json'
437
+ : '.anhedral/web-releases.json', project), project);
438
+ }
439
+ catch {
440
+ target.error = 'Recorded release history is unavailable for this target.';
441
+ }
442
+ targets.push(target);
443
+ }
444
+ }
445
+ catch {
446
+ targets.push({
447
+ id: `${provider}:unavailable`,
448
+ provider,
449
+ application: 'unknown',
450
+ audience: 'unconfigured',
451
+ accountReference: null,
452
+ latestAttempt: null,
453
+ lastSuccessful: null,
454
+ delivery: 'implemented',
455
+ liveStatus: 'unknown',
456
+ error: 'Connected target configuration is unavailable.',
457
+ });
458
+ }
459
+ }
460
+ return targets;
461
+ }
462
+ const targets = section([], 'Delivery targets are unavailable.', () => {
463
+ const values = hostingTargets();
464
+ const applications = apps();
465
+ for (const [app, provider] of [
466
+ ['apps/desktop', 'desktop'],
467
+ ['apps/extension', 'extension'],
468
+ ])
469
+ if (applications.includes(app))
470
+ values.push({
471
+ id: `${provider}:application`,
472
+ provider,
473
+ application: app,
474
+ audience: 'not-applicable',
475
+ accountReference: null,
476
+ latestAttempt: null,
477
+ lastSuccessful: null,
478
+ delivery: 'scaffold-only',
479
+ liveStatus: 'unknown',
480
+ });
481
+ if (applications.includes('apps/mobile')) {
482
+ for (const platform of ['ios', 'android']) {
483
+ const target = {
484
+ id: `expo:${platform}`,
485
+ provider: 'expo',
486
+ application: 'apps/mobile',
487
+ audience: 'not-applicable',
488
+ accountReference: null,
489
+ latestAttempt: null,
490
+ lastSuccessful: null,
491
+ delivery: 'artifact-only',
492
+ liveStatus: 'unknown',
493
+ };
494
+ try {
495
+ const app = object(document('apps/mobile/app.json', true).expo);
496
+ const extra = app.extra === undefined ? {} : object(app.extra);
497
+ const eas = extra.eas === undefined ? {} : object(extra.eas);
498
+ const selectedProject = eas.projectId === undefined ? null : identity(eas.projectId);
499
+ const owner = app.owner === undefined ? null : identity(app.owner);
500
+ const slug = app.slug === undefined ? null : identity(app.slug);
501
+ target.accountReference = owner;
502
+ const built = document('.anhedral/mobile-build.json');
503
+ const pending = document('.anhedral/mobile-build.pending.json');
504
+ function build(value, state) {
505
+ const buildId = typeof value.buildId === 'string' ? identity(value.buildId) : 'submission-unknown';
506
+ return {
507
+ id: buildId,
508
+ state,
509
+ commit: commit(value.commit),
510
+ recordedAt: time(value.recordedAt ?? value.finishedAt ?? value.startedAt),
511
+ url: owner && slug && buildId !== 'submission-unknown'
512
+ ? `https://expo.dev/accounts/${owner}/projects/${slug}/builds/${buildId}`
513
+ : null,
514
+ kind: 'build',
515
+ platform,
516
+ profile: name(value.profile),
517
+ ...(typeof value.artifact === 'string' &&
518
+ ['ipa', 'apk', 'aab', 'ios-simulator'].includes(value.artifact)
519
+ ? { artifact: value.artifact }
520
+ : {}),
521
+ };
522
+ }
523
+ if (built && built.platform === platform) {
524
+ if (built.schemaVersion !== 1 || built.state !== 'built')
525
+ throw new Error('Invalid build receipt.');
526
+ if (built.projectId === undefined || !selectedProject) {
527
+ target.latestAttempt = build(built, 'identity-unknown');
528
+ target.error =
529
+ 'Historical build has no matching recorded project identity; it does not establish delivery to the current Expo project.';
530
+ }
531
+ else {
532
+ if (identity(built.projectId) !== selectedProject)
533
+ throw new Error('Foreign build project.');
534
+ target.lastSuccessful = build(built, 'built');
535
+ target.latestAttempt = target.lastSuccessful;
536
+ }
537
+ }
538
+ if (pending && pending.platform === platform) {
539
+ if (pending.schemaVersion !== 1 ||
540
+ !selectedProject ||
541
+ identity(pending.projectId) !== selectedProject ||
542
+ !['submitting', 'queued', undefined].includes(pending.status))
543
+ throw new Error('Foreign or invalid pending project.');
544
+ target.latestAttempt = build(pending, pending.status === 'submitting' ? 'outcome-unknown' : 'building');
545
+ }
546
+ }
547
+ catch {
548
+ target.latestAttempt = null;
549
+ target.error =
550
+ 'Recorded mobile build evidence is unavailable or conflicts with the selected project; identity and time may be unknown.';
551
+ }
552
+ values.push(target);
553
+ }
554
+ }
555
+ if (values.length > 100)
556
+ throw new Error('Target output exceeds limits.');
557
+ return values;
558
+ });
559
+ const integrations = section([], 'Private provider receipt status is unavailable. No credentials were inspected.', () => {
560
+ localFile('.anhedral');
561
+ localFile('.anhedral/providers.json');
562
+ if (content('.anhedral/providers.json') === null)
563
+ return [];
564
+ return readProviderStateStatus(path.join(root, '.anhedral')).providers.map((provider) => ({
565
+ provider: provider.provider,
566
+ operations: array(provider.operations, 500).map((raw) => {
567
+ const operation = raw;
568
+ const resources = {};
569
+ for (const [key, value] of Object.entries(operation.resources ?? {}))
570
+ resources[name(key)] = key === 'project' ? identity(object(value).id) : identity(value);
571
+ return {
572
+ operation: operation.operation,
573
+ status: operation.status,
574
+ resources,
575
+ recovery: operation.recovery,
576
+ kind: operation.descriptor?.key ?? null,
577
+ };
578
+ }),
579
+ }));
580
+ });
581
+ const cloudflareResources = section([], 'Recorded Cloudflare resources are unavailable or belong to another account/project.', () => {
582
+ const value = document('.anhedral/resources.json');
583
+ if (!value)
584
+ return [];
585
+ const project = document('anhedral.project.json', true);
586
+ if (value.schemaVersion !== 1 ||
587
+ value.projectId !== project.id ||
588
+ value.accountId !== project.accountId)
589
+ throw new Error('Foreign Cloudflare resources.');
590
+ return array(value.resources, 500).map((raw) => {
591
+ const entry = object(raw);
592
+ return {
593
+ kind: name(entry.kind),
594
+ name: name(entry.name),
595
+ id: identity(entry.id),
596
+ reused: typeof entry.reused === 'boolean' ? entry.reused : null,
597
+ recordedAt: time(value.checkedAt),
598
+ };
599
+ });
600
+ });
601
+ const readiness = section({ localReady: false, checks: [] }, 'Local prerequisite checks are unavailable. Provider and product readiness are unknown.', () => {
602
+ for (const file of [
603
+ 'anhedral.setup.json',
604
+ 'anhedral.standard.json',
605
+ 'anhedral.registry.json',
606
+ ])
607
+ content(file);
608
+ if (workspaces.state === 'unavailable')
609
+ throw new Error('Unsafe workspace graph.');
610
+ const result = inspectProject(root);
611
+ return { localReady: result.localReady, checks: result.checks };
612
+ });
613
+ const operation = section(null, 'The recorded delivery operation is unavailable.', () => {
614
+ localFile('.anhedral');
615
+ if (content('.anhedral/operation.json') === null)
616
+ return null;
617
+ const value = readLifecycleOperation(root);
618
+ if (value) {
619
+ time(value.startedAt);
620
+ time(value.updatedAt);
621
+ }
622
+ return value;
623
+ });
624
+ return {
625
+ schemaVersion: 1,
626
+ name: applicationName,
627
+ inspectedAt: new Date().toISOString(),
628
+ authentication: { state: 'not-checked' },
629
+ liveStatus: { state: 'unknown' },
630
+ limits: [
631
+ ...CAPABILITY_CONTRACT.limits,
632
+ 'Workspace dependency fields do not establish runtime communication.',
633
+ 'Configured bindings and environment names do not prove resource existence or credential presence.',
634
+ 'Operation phases are recorded diagnostics, not process liveness or automatic resume.',
635
+ ],
636
+ selection,
637
+ setupProviders,
638
+ workspaces,
639
+ configuration,
640
+ targets,
641
+ integrations,
642
+ cloudflareResources,
643
+ readiness,
644
+ operation,
645
+ };
646
+ }
@@ -25,4 +25,21 @@ export declare function readApplication(root: string): ApplicationProject;
25
25
  export declare const defaultRunner: DeliveryRunner;
26
26
  export declare function environment(project: ApplicationProject): NodeJS.ProcessEnv;
27
27
  export declare function authenticate(root: string, project: ApplicationProject, run: DeliveryRunner): Promise<void>;
28
+ /** Explicit local cleanup only. A dead same-host PID is required; live, foreign,
29
+ * malformed and legacy locks remain intact. Provider operations are never replayed. */
30
+ export declare function recoverDeliveryLock(root: string): {
31
+ schemaVersion: number;
32
+ state: string;
33
+ recovered: boolean;
34
+ mutation: string;
35
+ providerReplay: boolean;
36
+ nextAction?: undefined;
37
+ } | {
38
+ schemaVersion: number;
39
+ state: string;
40
+ recovered: boolean;
41
+ mutation: string;
42
+ providerReplay: boolean;
43
+ nextAction: string;
44
+ };
28
45
  export declare function withApplicationLock<T>(root: string, operation: () => Promise<T>): Promise<T>;