anhedral 0.8.5 → 0.9.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 (126) hide show
  1. package/README.md +140 -34
  2. package/assets/anhedral-cli-init-technical.svg +8 -8
  3. package/assets/anhedral-cli-init.svg +9 -9
  4. package/assets/security/LICENSES.txt +63 -0
  5. package/assets/security/audit-deps.mjs +295 -0
  6. package/assets/security/braces-regression.mjs +102 -0
  7. package/assets/security/braces@3.0.3.patch +285 -0
  8. package/assets/security/decode-uri-component@0.2.2.patch +149 -0
  9. package/assets/security/decoder-regression.mjs +37 -0
  10. package/assets/security/forge-regression.mjs +46 -0
  11. package/assets/security/manifest.json +53 -0
  12. package/assets/security/node-forge@1.4.0.patch +26 -0
  13. package/dist/access.d.ts +10 -0
  14. package/dist/access.js +93 -0
  15. package/dist/auth-schema-snapshot.d.ts +346 -0
  16. package/dist/auth-schema-snapshot.js +347 -0
  17. package/dist/auth-schema.d.ts +1 -0
  18. package/dist/auth-schema.js +57 -0
  19. package/dist/bin.js +353 -10
  20. package/dist/bounded-file.d.ts +2 -0
  21. package/dist/bounded-file.js +30 -0
  22. package/dist/capabilities.d.ts +19 -33
  23. package/dist/capabilities.js +73 -71
  24. package/dist/cloudflare.d.ts +14 -0
  25. package/dist/cloudflare.js +66 -0
  26. package/dist/command.js +12 -3
  27. package/dist/delivery.d.ts +118 -0
  28. package/dist/delivery.js +611 -0
  29. package/dist/dependencies.d.ts +22 -0
  30. package/dist/dependencies.js +109 -67
  31. package/dist/dependency-order.d.ts +2 -0
  32. package/dist/dependency-order.js +24 -0
  33. package/dist/index.d.ts +18 -1
  34. package/dist/index.js +12 -1
  35. package/dist/managed-hosting.d.ts +49 -0
  36. package/dist/managed-hosting.js +373 -0
  37. package/dist/management.d.ts +3 -0
  38. package/dist/management.js +47 -0
  39. package/dist/platforms/desktop.d.ts +2 -2
  40. package/dist/platforms/desktop.js +22 -12
  41. package/dist/platforms/extension.d.ts +2 -2
  42. package/dist/platforms/extension.js +12 -4
  43. package/dist/platforms/mobile.d.ts +2 -2
  44. package/dist/platforms/mobile.js +13 -7
  45. package/dist/pnpm-policy.d.ts +4 -0
  46. package/dist/pnpm-policy.js +66 -0
  47. package/dist/project-state.d.ts +28 -0
  48. package/dist/project-state.js +226 -0
  49. package/dist/provider-readiness.d.ts +30 -0
  50. package/dist/provider-readiness.js +87 -0
  51. package/dist/provision.d.ts +46 -0
  52. package/dist/provision.js +197 -0
  53. package/dist/readiness.d.ts +4 -1
  54. package/dist/readiness.js +80 -8
  55. package/dist/registry/add.d.ts +22 -0
  56. package/dist/registry/add.js +198 -0
  57. package/dist/registry/bootstrap.d.ts +2 -0
  58. package/dist/registry/bootstrap.js +14 -0
  59. package/dist/registry/commands.d.ts +2 -0
  60. package/dist/registry/commands.js +99 -0
  61. package/dist/registry/context.d.ts +3 -0
  62. package/dist/registry/context.js +21 -0
  63. package/dist/registry/options.d.ts +2 -0
  64. package/dist/registry/options.js +65 -0
  65. package/dist/registry/receipt.d.ts +3 -0
  66. package/dist/registry/receipt.js +73 -0
  67. package/dist/registry/remote.d.ts +25 -0
  68. package/dist/registry/remote.js +168 -0
  69. package/dist/registry/resolve.d.ts +10 -0
  70. package/dist/registry/resolve.js +98 -0
  71. package/dist/registry/types.d.ts +64 -0
  72. package/dist/registry/usage.d.ts +1 -0
  73. package/dist/registry/usage.js +38 -0
  74. package/dist/render.js +3 -3
  75. package/dist/repository.d.ts +24 -0
  76. package/dist/repository.js +96 -0
  77. package/dist/scaffold/api.d.ts +2 -0
  78. package/dist/scaffold/api.js +13 -0
  79. package/dist/scaffold/auth.d.ts +3 -0
  80. package/dist/scaffold/auth.js +95 -0
  81. package/dist/scaffold/bootstrap.d.ts +2 -0
  82. package/dist/scaffold/bootstrap.js +34 -0
  83. package/dist/scaffold/compose.d.ts +4 -0
  84. package/dist/scaffold/compose.js +34 -0
  85. package/dist/scaffold/database.d.ts +2 -0
  86. package/dist/scaffold/database.js +43 -0
  87. package/dist/scaffold/documentation.d.ts +3 -0
  88. package/dist/scaffold/documentation.js +91 -0
  89. package/dist/scaffold/environment.d.ts +2 -0
  90. package/dist/scaffold/environment.js +5 -0
  91. package/dist/scaffold/files.d.ts +25 -0
  92. package/dist/scaffold/files.js +108 -0
  93. package/dist/scaffold/integrations.d.ts +3 -0
  94. package/dist/scaffold/integrations.js +7 -0
  95. package/dist/scaffold/services.d.ts +5 -0
  96. package/dist/scaffold/services.js +84 -0
  97. package/dist/scaffold/web.d.ts +2 -0
  98. package/dist/scaffold/web.js +102 -0
  99. package/dist/scaffold/workspace.d.ts +3 -0
  100. package/dist/scaffold/workspace.js +150 -0
  101. package/dist/security-backports.d.ts +3 -0
  102. package/dist/security-backports.js +26 -0
  103. package/dist/stack-registry.d.ts +80 -0
  104. package/dist/stack-registry.js +424 -0
  105. package/dist/standard.d.ts +6 -17
  106. package/dist/standard.js +41 -585
  107. package/dist/transaction.js +46 -26
  108. package/dist/util.js +12 -2
  109. package/dist/vercel.d.ts +98 -0
  110. package/dist/vercel.js +796 -0
  111. package/dist/version.d.ts +1 -1
  112. package/dist/version.js +1 -1
  113. package/dist/vinext.d.ts +1 -0
  114. package/dist/vinext.js +83 -0
  115. package/dist/workspace-graph.d.ts +7 -0
  116. package/dist/workspace-graph.js +73 -0
  117. package/docs/application-stack-standard.md +75 -674
  118. package/docs/delivery-workflow.md +83 -0
  119. package/docs/managed-hosting-architecture.md +87 -0
  120. package/docs/sponsorships.md +25 -0
  121. package/package.json +23 -28
  122. package/dist/project.d.ts +0 -4
  123. package/dist/standard-products.d.ts +0 -2
  124. package/dist/standard-products.js +0 -6
  125. /package/assets/{images/svg/logo-white-subtract.svg → anhedral-mark.svg} +0 -0
  126. /package/dist/{project.js → registry/types.js} +0 -0
@@ -0,0 +1,66 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { writeFile } from './util.js';
4
+ function policyBlock(key) {
5
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
6
+ return new RegExp(`^(?:${escaped}|"${escaped}"|'${escaped}')[ \\t]*:.*(?:\\r?\\n|$)(?:[ \\t]+[^\\n]*(?:\\n|$)|#[^\\n]*(?:\\n|$)|[ \\t]*\\n)*`, 'gm');
7
+ }
8
+ /** Preserve unrelated starter YAML while replacing the settings this initializer owns. */
9
+ export function setPnpmPolicy(root, policy) {
10
+ const file = path.join(root, 'pnpm-workspace.yaml');
11
+ let source = existsSync(file) ? readFileSync(file, 'utf8') : '';
12
+ for (const [key, value] of Object.entries(policy)) {
13
+ source = source.replace(policyBlock(key), '');
14
+ if (value === undefined)
15
+ continue;
16
+ source += `${source.endsWith('\n') || !source ? '' : '\n'}${key}: ${JSON.stringify(value)}\n`;
17
+ }
18
+ writeFile(file, source);
19
+ }
20
+ /** Merge reviewed patches without deleting starter-maintained mappings. */
21
+ export function mergePnpmPatches(root, patches) {
22
+ const file = path.join(root, 'pnpm-workspace.yaml');
23
+ const source = existsSync(file) ? readFileSync(file, 'utf8') : '';
24
+ const blocks = [...source.matchAll(policyBlock('patchedDependencies'))];
25
+ if (blocks.length > 1)
26
+ throw new Error('Duplicate patchedDependencies configuration');
27
+ const block = blocks[0]?.[0] ?? '';
28
+ const existing = patchRows(block, patches);
29
+ const replacement = 'patchedDependencies:\n' +
30
+ existing +
31
+ Object.entries(patches)
32
+ .map(([key, value]) => ` ${JSON.stringify(key)}: ${JSON.stringify(value)}\n`)
33
+ .join('');
34
+ const remaining = source.replace(policyBlock('patchedDependencies'), '');
35
+ writeFile(file, remaining + `${remaining.endsWith('\n') || !remaining ? '' : '\n'}${replacement}`);
36
+ }
37
+ function patchRows(block, patches) {
38
+ if (!block)
39
+ return '';
40
+ const [header, ...rows] = block.split(/\r?\n/);
41
+ const value = header.slice(header.indexOf(':') + 1).trim();
42
+ if (value && !value.startsWith('#')) {
43
+ const object = JSON.parse(value);
44
+ if (!object ||
45
+ Array.isArray(object) ||
46
+ typeof object !== 'object' ||
47
+ Object.values(object).some((entry) => typeof entry !== 'string'))
48
+ throw new Error('Unsupported patchedDependencies mapping');
49
+ return Object.entries(object)
50
+ .filter(([key]) => !Object.hasOwn(patches, key))
51
+ .map(([key, entry]) => ` ${JSON.stringify(key)}: ${JSON.stringify(entry)}\n`)
52
+ .join('');
53
+ }
54
+ return rows
55
+ .filter((row) => {
56
+ if (!row.trim() || row.trimStart().startsWith('#'))
57
+ return true;
58
+ const match = row.match(/^ (?:"([^"\\]+)"|'([^']+)'|([^\s:'"]+)):\s*(?:"[^"\\]+"|'[^']+'|[^\s{}[\]&*!|>]+)(?:\s+#.*)?\s*$/);
59
+ if (!match)
60
+ throw new Error('Unsupported patchedDependencies mapping');
61
+ return !Object.hasOwn(patches, match[1] ?? match[2] ?? match[3]);
62
+ })
63
+ .filter((row, index, rows) => row || index < rows.length - 1)
64
+ .map((row) => `${row}\n`)
65
+ .join('');
66
+ }
@@ -0,0 +1,28 @@
1
+ export interface DeploymentTarget {
2
+ id: string;
3
+ directory: string;
4
+ workerName: string;
5
+ }
6
+ export interface ApplicationProject {
7
+ schemaVersion: 1;
8
+ id: string;
9
+ name: string;
10
+ ownership: 'customer';
11
+ provider: 'cloudflare';
12
+ accountId: string | null;
13
+ audience: 'unconfigured' | 'public' | 'private';
14
+ access?: {
15
+ emails: string[];
16
+ };
17
+ targets: DeploymentTarget[];
18
+ }
19
+ export type DeliveryRunner = (command: string, args: string[], cwd: string, env: NodeJS.ProcessEnv) => Promise<string>;
20
+ export declare function atomicJson(file: string, value: unknown): void;
21
+ export declare function readConfig(directory: string): Record<string, unknown>;
22
+ export declare function targetDirectory(root: string, target: DeploymentTarget): string;
23
+ export declare function registerApplication(root: string, extendTargets?: boolean): ApplicationProject;
24
+ export declare function readApplication(root: string): ApplicationProject;
25
+ export declare const defaultRunner: DeliveryRunner;
26
+ export declare function environment(project: ApplicationProject): NodeJS.ProcessEnv;
27
+ export declare function authenticate(root: string, project: ApplicationProject, run: DeliveryRunner): Promise<void>;
28
+ export declare function withApplicationLock<T>(root: string, operation: () => Promise<T>): Promise<T>;
@@ -0,0 +1,226 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
3
+ import { spawn } from 'node:child_process';
4
+ import path from 'node:path';
5
+ import { parse } from 'jsonc-parser';
6
+ import { resolveCommand } from './command.js';
7
+ const manifestName = 'anhedral.project.json';
8
+ const stateDirectory = '.anhedral';
9
+ const accountPattern = /^[a-f0-9]{32}$/i;
10
+ const workerPattern = /^[a-z0-9][a-z0-9-]{0,62}$/;
11
+ export function atomicJson(file, value) {
12
+ const temporary = `${file}.${randomUUID()}.tmp`;
13
+ try {
14
+ writeFileSync(temporary, JSON.stringify(value, null, 2) + '\n', { mode: 0o600 });
15
+ renameSync(temporary, file);
16
+ }
17
+ finally {
18
+ rmSync(temporary, { force: true });
19
+ }
20
+ }
21
+ export function readConfig(directory) {
22
+ const errors = [];
23
+ const value = parse(readFileSync(path.join(directory, 'wrangler.jsonc'), 'utf8'), errors);
24
+ if (errors.length || !value || typeof value !== 'object' || Array.isArray(value))
25
+ throw new Error('Invalid Wrangler JSONC configuration.');
26
+ return value;
27
+ }
28
+ export function targetDirectory(root, target) {
29
+ if (!/^apps\/[a-z0-9-]+$/.test(target.directory))
30
+ throw new Error('Deployment targets must be direct apps/* directories.');
31
+ const directory = realpathSync(path.join(root, target.directory));
32
+ if (!directory.startsWith(realpathSync(root) + path.sep))
33
+ throw new Error('Deployment target escapes the project.');
34
+ return directory;
35
+ }
36
+ export function registerApplication(root, extendTargets = false) {
37
+ root = realpathSync(root);
38
+ const existing = existsSync(path.join(root, manifestName)) ? readApplication(root) : undefined;
39
+ if (existing && !extendTargets)
40
+ return existing;
41
+ const setup = JSON.parse(readFileSync(path.join(root, 'anhedral.setup.json'), 'utf8'));
42
+ if (!['cloudflare', 'vercel'].includes(setup.hosting))
43
+ throw new Error('Automatic delivery currently supports Cloudflare projects.');
44
+ const targets = readdirSync(path.join(root, 'apps'), { withFileTypes: true })
45
+ .filter((entry) => entry.isDirectory())
46
+ .filter((entry) => existsSync(path.join(root, 'apps', entry.name, 'wrangler.jsonc')))
47
+ .map((entry) => {
48
+ const workerName = readConfig(path.join(root, 'apps', entry.name)).name;
49
+ if (typeof workerName !== 'string' || !workerPattern.test(workerName))
50
+ throw new Error('Invalid Worker name.');
51
+ return { id: entry.name, directory: `apps/${entry.name}`, workerName };
52
+ })
53
+ .sort((a, b) => (a.id === 'web' ? 1 : b.id === 'web' ? -1 : a.id.localeCompare(b.id)));
54
+ if (existing) {
55
+ if (existing.targets.some((target) => !targets.some((candidate) => candidate.id === target.id &&
56
+ candidate.directory === target.directory &&
57
+ candidate.workerName === target.workerName)))
58
+ throw new Error('Addition cannot replace existing delivery targets.');
59
+ const extended = { ...existing, targets };
60
+ atomicJson(path.join(root, manifestName), extended);
61
+ return extended;
62
+ }
63
+ const project = {
64
+ schemaVersion: 1,
65
+ id: randomUUID(),
66
+ name: JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8')).name ?? path.basename(root),
67
+ ownership: 'customer',
68
+ provider: 'cloudflare',
69
+ accountId: null,
70
+ audience: 'unconfigured',
71
+ targets,
72
+ };
73
+ const ignoreFile = path.join(root, '.gitignore');
74
+ const ignored = existsSync(ignoreFile) ? readFileSync(ignoreFile, 'utf8') : '';
75
+ if (!ignored.split(/\r?\n/).includes('.anhedral/'))
76
+ writeFileSync(ignoreFile, ignored + (ignored.endsWith('\n') || !ignored ? '' : '\n') + '.anhedral/\n');
77
+ atomicJson(path.join(root, manifestName), project);
78
+ return project;
79
+ }
80
+ export function readApplication(root) {
81
+ const project = JSON.parse(readFileSync(path.join(root, manifestName), 'utf8'));
82
+ if (project.schemaVersion !== 1 ||
83
+ typeof project.id !== 'string' ||
84
+ !/^[a-f0-9-]{36}$/i.test(project.id) ||
85
+ project.ownership !== 'customer' ||
86
+ project.provider !== 'cloudflare' ||
87
+ !(project.accountId === null || accountPattern.test(project.accountId)) ||
88
+ !['unconfigured', 'public', 'private'].includes(project.audience) ||
89
+ !Array.isArray(project.targets))
90
+ throw new Error('Invalid application manifest.');
91
+ if (project.access &&
92
+ (!Array.isArray(project.access.emails) ||
93
+ project.access.emails.some((email) => typeof email !== 'string')))
94
+ throw new Error('Invalid private audience configuration.');
95
+ if (project.audience === 'private' && !project.access?.emails.length)
96
+ throw new Error('Private delivery requires explicit allowed members.');
97
+ const ids = new Set();
98
+ const workers = new Set();
99
+ for (const target of project.targets) {
100
+ if (!target ||
101
+ typeof target.id !== 'string' ||
102
+ typeof target.workerName !== 'string' ||
103
+ !/^[a-z0-9-]+$/.test(target.id) ||
104
+ ids.has(target.id) ||
105
+ workers.has(target.workerName) ||
106
+ !workerPattern.test(target.workerName))
107
+ throw new Error('Invalid or duplicate deployment target.');
108
+ ids.add(target.id);
109
+ workers.add(target.workerName);
110
+ targetDirectory(root, target);
111
+ }
112
+ return project;
113
+ }
114
+ export const defaultRunner = (command, args, cwd, env) => new Promise((resolve, reject) => {
115
+ const invocation = resolveCommand(command, args);
116
+ const child = spawn(invocation.command, invocation.args, {
117
+ cwd,
118
+ env,
119
+ detached: process.platform !== 'win32',
120
+ stdio: ['ignore', 'pipe', 'pipe'],
121
+ });
122
+ let output = '';
123
+ let oversized = false;
124
+ let interrupted = false;
125
+ let timedOut = false;
126
+ let forceTimer;
127
+ const kill = (signal) => {
128
+ if (!child.pid)
129
+ return;
130
+ if (process.platform === 'win32')
131
+ spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore' });
132
+ else {
133
+ try {
134
+ process.kill(-child.pid, signal);
135
+ }
136
+ catch {
137
+ /* The process group already exited. */
138
+ }
139
+ }
140
+ };
141
+ const stop = () => {
142
+ kill('SIGTERM');
143
+ forceTimer ??= setTimeout(() => kill('SIGKILL'), 2000);
144
+ forceTimer.unref();
145
+ };
146
+ const interrupt = () => {
147
+ interrupted = true;
148
+ stop();
149
+ };
150
+ process.once('SIGINT', interrupt);
151
+ process.once('SIGTERM', interrupt);
152
+ child.stdout.on('data', (chunk) => {
153
+ if (!oversized)
154
+ output += chunk.toString();
155
+ if (output.length > 8 * 1024 * 1024) {
156
+ oversized = true;
157
+ stop();
158
+ }
159
+ });
160
+ // Do not persist raw build/provider output: it can contain credentials or customer data.
161
+ child.stderr.resume();
162
+ const timer = setTimeout(() => {
163
+ timedOut = true;
164
+ stop();
165
+ }, 20 * 60 * 1000);
166
+ const clean = () => {
167
+ clearTimeout(timer);
168
+ if (forceTimer)
169
+ clearTimeout(forceTimer);
170
+ process.removeListener('SIGINT', interrupt);
171
+ process.removeListener('SIGTERM', interrupt);
172
+ };
173
+ child.on('error', () => {
174
+ clean();
175
+ reject(new Error(`Cannot run ${command}; check local tooling.`));
176
+ });
177
+ child.on('close', (code) => {
178
+ clean();
179
+ if (code === 0 && !oversized && !interrupted && !timedOut)
180
+ resolve(output);
181
+ else
182
+ reject(new Error(`${command} ${args.slice(0, 3).join(' ')} ${interrupted ? 'was interrupted' : timedOut ? 'timed out' : 'failed'}; inspect the project command locally.`));
183
+ });
184
+ });
185
+ export function environment(project) {
186
+ if (!project.accountId)
187
+ throw new Error('Connect the intended Cloudflare account first.');
188
+ if (process.env.CLOUDFLARE_ACCOUNT_ID && process.env.CLOUDFLARE_ACCOUNT_ID !== project.accountId)
189
+ throw new Error('Cloudflare environment account conflicts with the registered account.');
190
+ if (process.env.CLOUDFLARE_ENV)
191
+ throw new Error('Register a separate project configuration for the intended environment; CLOUDFLARE_ENV overrides are unsupported.');
192
+ return {
193
+ ...process.env,
194
+ CLOUDFLARE_ACCOUNT_ID: project.accountId,
195
+ CI: 'true',
196
+ NO_COLOR: '1',
197
+ FORCE_COLOR: '0',
198
+ };
199
+ }
200
+ export async function authenticate(root, project, run) {
201
+ const identity = JSON.parse(await run('pnpm', ['exec', 'wrangler', 'whoami', '--json'], targetDirectory(root, project.targets[0]), environment(project)));
202
+ if (identity.loggedIn !== true ||
203
+ !Array.isArray(identity.accounts) ||
204
+ !identity.accounts.some((account) => account.id === project.accountId))
205
+ throw new Error('Wrangler cannot access the registered Cloudflare account. Run wrangler login or configure scoped credentials.');
206
+ }
207
+ export async function withApplicationLock(root, operation) {
208
+ root = realpathSync(root);
209
+ const state = path.join(root, stateDirectory);
210
+ mkdirSync(state, { recursive: true });
211
+ if (realpathSync(state) !== path.join(root, stateDirectory))
212
+ throw new Error('Delivery state directory must not be a symbolic link.');
213
+ const lock = path.join(state, 'ship.lock');
214
+ try {
215
+ mkdirSync(lock);
216
+ }
217
+ catch {
218
+ throw new Error('Another delivery is running or was interrupted. Inspect provider status and remove .anhedral/ship.lock only after confirming no delivery is active.');
219
+ }
220
+ try {
221
+ return await operation();
222
+ }
223
+ finally {
224
+ rmSync(lock, { recursive: true, force: true });
225
+ }
226
+ }
@@ -0,0 +1,30 @@
1
+ import { type DeliveryRunner } from './project-state.js';
2
+ /** Explicit opt-in: checks local tools and existing provider context without setup or deployment. */
3
+ export declare function inspectProviderReadiness(root: string, run?: DeliveryRunner): Promise<{
4
+ prerequisiteChecks: {
5
+ id: string;
6
+ ok: boolean;
7
+ detail: string;
8
+ nextAction?: string;
9
+ }[];
10
+ prerequisitesReady: boolean;
11
+ providers: {
12
+ provider: "vercel" | "cloudflare";
13
+ connection: "verified" | "blocked";
14
+ deployment: "active-recorded" | "unverified";
15
+ nextAction?: string;
16
+ }[];
17
+ providerVerification: string;
18
+ connectionEvidence: string;
19
+ externalIntegrations: string;
20
+ productionReady: boolean;
21
+ mutation: boolean;
22
+ schemaVersion: number;
23
+ root: string;
24
+ checks: {
25
+ id: string;
26
+ ok: boolean;
27
+ detail: string;
28
+ }[];
29
+ localReady: boolean;
30
+ }>;
@@ -0,0 +1,87 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { inspectProject } from './readiness.js';
4
+ import { defaultRunner } from './project-state.js';
5
+ import { vercelApplicationStatus } from './vercel.js';
6
+ import { applicationStatus } from './delivery.js';
7
+ /** Explicit opt-in: checks local tools and existing provider context without setup or deployment. */
8
+ export async function inspectProviderReadiness(root, run = defaultRunner) {
9
+ root = path.resolve(root);
10
+ const local = inspectProject(root);
11
+ const checks = [];
12
+ const environment = {
13
+ ...process.env,
14
+ CI: '1',
15
+ GH_PROMPT_DISABLED: '1',
16
+ GIT_TERMINAL_PROMPT: '0',
17
+ NO_COLOR: '1',
18
+ };
19
+ const check = async (id, operation, detail, nextAction) => {
20
+ try {
21
+ checks.push({ id, ok: await operation(), detail, nextAction });
22
+ }
23
+ catch {
24
+ checks.push({ id, ok: false, detail, nextAction });
25
+ }
26
+ };
27
+ const [major, minor] = process.versions.node.split('.').map(Number);
28
+ checks.push({
29
+ id: 'node',
30
+ ok: major > 22 || (major === 22 && minor >= 13),
31
+ detail: 'Generated framework apps require Node 22.13 or newer; the CLI itself supports older Node versions.',
32
+ });
33
+ await check('pnpm', async () => /^\d+\.\d+\.\d+/.test((await run('pnpm', ['--version'], root, environment)).trim()), 'pnpm is available', 'Install the project-pinned pnpm version.');
34
+ await check('git', async () => (await run('git', ['--version'], root, environment)).startsWith('git version '), 'Git is available', 'Install Git.');
35
+ await check('git-identity', async () => Boolean((await run('git', ['var', 'GIT_AUTHOR_IDENT'], root, environment)).trim()), 'Git author identity is configured; its value is not recorded', 'Configure your own Git name and email locally.');
36
+ await check('github', async () => {
37
+ if (process.env.GH_HOST && process.env.GH_HOST !== 'github.com')
38
+ return false;
39
+ const user = JSON.parse(await run('gh', ['api', 'user'], root, { ...environment, GH_HOST: 'github.com' }));
40
+ return typeof user.login === 'string' && /^[a-zA-Z0-9-]+$/.test(user.login);
41
+ }, 'GitHub CLI can identify its authenticated account', 'Complete gh auth login locally for the intended GitHub account.');
42
+ const providers = [];
43
+ for (const provider of ['vercel', 'cloudflare']) {
44
+ const file = path.join(root, provider === 'vercel' ? 'anhedral.web.json' : 'anhedral.project.json');
45
+ if (!existsSync(file))
46
+ continue;
47
+ try {
48
+ const manifest = JSON.parse(readFileSync(file, 'utf8'));
49
+ const connected = provider === 'vercel' ? manifest.projectId && manifest.teamId : manifest.accountId;
50
+ if (!connected || manifest.audience === 'unconfigured')
51
+ throw new Error('unconfigured');
52
+ const status = provider === 'vercel'
53
+ ? await vercelApplicationStatus(root, true, run)
54
+ : await applicationStatus(root, true, run);
55
+ const active = 'activeRecordedVersion' in status
56
+ ? status.activeRecordedVersion === true
57
+ : status.provider.length > 0 &&
58
+ status.provider.every((target) => target.activeRecordedVersion);
59
+ providers.push({
60
+ provider,
61
+ connection: 'verified',
62
+ deployment: active ? 'active-recorded' : 'unverified',
63
+ });
64
+ }
65
+ catch {
66
+ providers.push({
67
+ provider,
68
+ connection: 'blocked',
69
+ deployment: 'unverified',
70
+ nextAction: `Inspect ${path.basename(file)} and sign in to its recorded account. Connect an unconfigured app or run recover for a fresh clone; do not change the audience to bypass access checks.`,
71
+ });
72
+ }
73
+ }
74
+ return {
75
+ ...local,
76
+ prerequisiteChecks: checks,
77
+ prerequisitesReady: checks.every((item) => item.ok),
78
+ providers,
79
+ providerVerification: providers.length && providers.every((item) => item.connection === 'verified')
80
+ ? 'verified-existing-context'
81
+ : 'unverified',
82
+ connectionEvidence: 'Account, configuration and deployment context only; resource IDs and bindings require separate recovery/provisioning checks.',
83
+ externalIntegrations: 'Require separate actual service setup and product tests; this report does not inspect secrets or certify integrations.',
84
+ productionReady: false,
85
+ mutation: false,
86
+ };
87
+ }
@@ -0,0 +1,46 @@
1
+ import { type DeliveryRunner } from './project-state.js';
2
+ import { type CloudflareRequest } from './cloudflare.js';
3
+ interface Resource {
4
+ kind: 'd1' | 'r2' | 'kv' | 'queue';
5
+ name: string;
6
+ id?: string;
7
+ target: string;
8
+ field?: (string | number)[];
9
+ }
10
+ export declare function planProvisioning(root: string): {
11
+ projectId: string;
12
+ accountId: string | null;
13
+ resources: Resource[];
14
+ externalIntegrations: string[];
15
+ };
16
+ export declare function provisionApplication(root: string, dryRun?: boolean, run?: DeliveryRunner, suppliedRequest?: CloudflareRequest): Promise<{
17
+ schemaVersion: number;
18
+ projectId: string;
19
+ accountId: string | null;
20
+ checkedAt: string;
21
+ resources: {
22
+ kind: string;
23
+ name: string;
24
+ id: string;
25
+ reused: boolean;
26
+ }[];
27
+ state: string;
28
+ externalIntegrations: string[];
29
+ } | {
30
+ mutation: boolean;
31
+ projectId: string;
32
+ accountId: string | null;
33
+ resources: Resource[];
34
+ externalIntegrations: string[];
35
+ }>;
36
+ /** Reconcile committed identities against the customer account without creating resources or rewriting configuration. */
37
+ export declare function inspectApplicationResources(root: string, request: CloudflareRequest): Promise<{
38
+ resources: {
39
+ kind: string;
40
+ name: string;
41
+ id: string;
42
+ target: string;
43
+ }[];
44
+ externalIntegrations: string[];
45
+ }>;
46
+ export {};