anhedral 0.8.6 → 0.10.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.
- package/README.md +137 -38
- package/assets/anhedral-cli-init-technical.svg +15 -15
- package/assets/anhedral-cli-init.svg +16 -16
- package/dist/access.d.ts +10 -0
- package/dist/access.js +93 -0
- package/dist/auth-schema-snapshot.d.ts +346 -0
- package/dist/auth-schema-snapshot.js +347 -0
- package/dist/auth-schema.d.ts +1 -0
- package/dist/auth-schema.js +57 -0
- package/dist/bin.js +349 -39
- package/dist/bounded-file.d.ts +2 -0
- package/dist/bounded-file.js +30 -0
- package/dist/capabilities.d.ts +24 -42
- package/dist/capabilities.js +73 -75
- package/dist/cloudflare.d.ts +14 -0
- package/dist/cloudflare.js +66 -0
- package/dist/command.js +12 -3
- package/dist/delivery.d.ts +118 -0
- package/dist/delivery.js +611 -0
- package/dist/dependencies.d.ts +24 -3
- package/dist/dependencies.js +112 -74
- package/dist/dependency-order.d.ts +2 -0
- package/dist/dependency-order.js +24 -0
- package/dist/index.d.ts +16 -3
- package/dist/index.js +11 -2
- package/dist/managed-hosting.d.ts +49 -0
- package/dist/managed-hosting.js +373 -0
- package/dist/management.d.ts +3 -0
- package/dist/management.js +47 -0
- package/dist/platforms/desktop.d.ts +2 -2
- package/dist/platforms/desktop.js +25 -15
- package/dist/platforms/extension.d.ts +2 -2
- package/dist/platforms/extension.js +104 -66
- package/dist/platforms/mobile.d.ts +2 -2
- package/dist/platforms/mobile.js +13 -7
- package/dist/pnpm-policy.d.ts +0 -2
- package/dist/pnpm-policy.js +18 -13
- package/dist/project-state.d.ts +28 -0
- package/dist/project-state.js +226 -0
- package/dist/provider-readiness.d.ts +30 -0
- package/dist/provider-readiness.js +87 -0
- package/dist/provision.d.ts +46 -0
- package/dist/provision.js +197 -0
- package/dist/readiness.d.ts +4 -2
- package/dist/readiness.js +64 -14
- package/dist/registry/add.d.ts +18 -0
- package/dist/registry/add.js +163 -0
- package/dist/registry/bootstrap.d.ts +2 -0
- package/dist/registry/bootstrap.js +14 -0
- package/dist/registry/commands.d.ts +2 -0
- package/dist/registry/commands.js +48 -0
- package/dist/registry/context.d.ts +3 -0
- package/dist/registry/context.js +21 -0
- package/dist/registry/options.d.ts +2 -0
- package/dist/registry/options.js +65 -0
- package/dist/registry/receipt.d.ts +2 -0
- package/dist/registry/receipt.js +52 -0
- package/dist/registry/resolve.d.ts +10 -0
- package/dist/registry/resolve.js +98 -0
- package/dist/registry/types.d.ts +64 -0
- package/dist/registry/usage.d.ts +1 -0
- package/dist/registry/usage.js +37 -0
- package/dist/render.d.ts +0 -4
- package/dist/render.js +3 -16
- package/dist/repository.d.ts +24 -0
- package/dist/repository.js +96 -0
- package/dist/scaffold/api.d.ts +2 -0
- package/dist/scaffold/api.js +13 -0
- package/dist/scaffold/auth.d.ts +3 -0
- package/dist/scaffold/auth.js +95 -0
- package/dist/scaffold/bootstrap.d.ts +3 -0
- package/dist/scaffold/bootstrap.js +39 -0
- package/dist/scaffold/compose.d.ts +4 -0
- package/dist/scaffold/compose.js +34 -0
- package/dist/scaffold/database.d.ts +2 -0
- package/dist/scaffold/database.js +43 -0
- package/dist/scaffold/documentation.d.ts +3 -0
- package/dist/scaffold/documentation.js +91 -0
- package/dist/scaffold/environment.d.ts +2 -0
- package/dist/scaffold/environment.js +5 -0
- package/dist/scaffold/files.d.ts +25 -0
- package/dist/scaffold/files.js +108 -0
- package/dist/scaffold/integrations.d.ts +3 -0
- package/dist/scaffold/integrations.js +7 -0
- package/dist/scaffold/services.d.ts +5 -0
- package/dist/scaffold/services.js +84 -0
- package/dist/scaffold/web.d.ts +2 -0
- package/dist/scaffold/web.js +102 -0
- package/dist/scaffold/workspace.d.ts +3 -0
- package/dist/scaffold/workspace.js +150 -0
- package/dist/security-backports.js +7 -2
- package/dist/stack-registry.d.ts +80 -0
- package/dist/stack-registry.js +424 -0
- package/dist/standard.d.ts +6 -19
- package/dist/standard.js +45 -658
- package/dist/transaction.js +47 -56
- package/dist/util.js +12 -2
- package/dist/vercel.d.ts +98 -0
- package/dist/vercel.js +796 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/vinext.d.ts +1 -0
- package/dist/vinext.js +83 -0
- package/dist/workspace-graph.d.ts +7 -0
- package/dist/workspace-graph.js +73 -0
- package/docs/application-stack-standard.md +76 -91
- package/docs/delivery-workflow.md +83 -0
- package/docs/managed-hosting-architecture.md +87 -0
- package/docs/sponsorships.md +25 -0
- package/package.json +23 -29
- package/dist/evidence-validation.d.ts +0 -2
- package/dist/evidence-validation.js +0 -18
- package/dist/foundation.d.ts +0 -6
- package/dist/foundation.js +0 -49
- package/dist/progress-view.d.ts +0 -57
- package/dist/progress-view.js +0 -69
- package/dist/progress.d.ts +0 -95
- package/dist/progress.js +0 -327
- package/dist/project.d.ts +0 -5
- package/dist/standard-products.d.ts +0 -2
- package/dist/standard-products.js +0 -6
- /package/assets/{images/svg/logo-white-subtract.svg → anhedral-mark.svg} +0 -0
- /package/dist/{project.js → registry/types.js} +0 -0
|
@@ -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 {};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { applyEdits, modify } from 'jsonc-parser';
|
|
4
|
+
import { atomicJson, authenticate, defaultRunner, environment, readApplication, readConfig, targetDirectory, withApplicationLock, } from './project-state.js';
|
|
5
|
+
import { cloudflareRequest, listCloudflare } from './cloudflare.js';
|
|
6
|
+
const resourceTypes = {
|
|
7
|
+
d1: { endpoint: '/d1/database', nameField: 'name', idField: 'uuid' },
|
|
8
|
+
r2: { endpoint: '/r2/buckets', nameField: 'name', idField: 'name' },
|
|
9
|
+
kv: { endpoint: '/storage/kv/namespaces', nameField: 'title', idField: 'id' },
|
|
10
|
+
queue: { endpoint: '/queues', nameField: 'queue_name', idField: 'queue_id' },
|
|
11
|
+
};
|
|
12
|
+
async function listResources(request, kind) {
|
|
13
|
+
const { endpoint } = resourceTypes[kind];
|
|
14
|
+
if (kind !== 'r2')
|
|
15
|
+
return listCloudflare(request, endpoint);
|
|
16
|
+
const value = (await request('GET', endpoint)).result;
|
|
17
|
+
if (!Array.isArray(value?.buckets) || value.truncated)
|
|
18
|
+
throw new Error('Unsupported or truncated R2 bucket listing; select resources explicitly with provider tooling.');
|
|
19
|
+
return value.buckets;
|
|
20
|
+
}
|
|
21
|
+
/** Provisioning and recovery use the same independently verified private-storage policy. */
|
|
22
|
+
async function assertPrivateBucket(request, name) {
|
|
23
|
+
const endpoint = `${resourceTypes.r2.endpoint}/${encodeURIComponent(name)}/domains`;
|
|
24
|
+
const managed = (await request('GET', `${endpoint}/managed`)).result;
|
|
25
|
+
const custom = (await request('GET', `${endpoint}/custom`)).result;
|
|
26
|
+
if (managed?.enabled !== false ||
|
|
27
|
+
!Array.isArray(custom?.domains) ||
|
|
28
|
+
custom.domains.some((domain) => domain.enabled !== false))
|
|
29
|
+
throw new Error('Selected R2 bucket has public or unknown domain access; resolve it before using private storage.');
|
|
30
|
+
}
|
|
31
|
+
function resourceName(value) {
|
|
32
|
+
if (typeof value !== 'string' || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value))
|
|
33
|
+
throw new Error('Resources require explicit lowercase names of at most 63 characters.');
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
export function planProvisioning(root) {
|
|
37
|
+
const project = readApplication(root);
|
|
38
|
+
const resources = [];
|
|
39
|
+
for (const target of project.targets) {
|
|
40
|
+
const config = readConfig(targetDirectory(root, target));
|
|
41
|
+
if ((config.env && Object.keys(config.env).length) ||
|
|
42
|
+
(config.account_id && config.account_id !== project.accountId))
|
|
43
|
+
throw new Error('Reconcile target account/environment before provisioning.');
|
|
44
|
+
for (const [kind, field] of [
|
|
45
|
+
['d1', 'd1_databases'],
|
|
46
|
+
['r2', 'r2_buckets'],
|
|
47
|
+
['kv', 'kv_namespaces'],
|
|
48
|
+
]) {
|
|
49
|
+
const entries = config[field];
|
|
50
|
+
if (entries === undefined)
|
|
51
|
+
continue;
|
|
52
|
+
if (!Array.isArray(entries))
|
|
53
|
+
throw new Error(`Invalid ${field} configuration.`);
|
|
54
|
+
entries.forEach((entry, index) => {
|
|
55
|
+
if (!entry || typeof entry.binding !== 'string' || entry.jurisdiction)
|
|
56
|
+
throw new Error('Use an explicit provider workflow for jurisdiction-specific resources.');
|
|
57
|
+
const name = resourceName(kind === 'd1'
|
|
58
|
+
? entry.database_name
|
|
59
|
+
: kind === 'r2'
|
|
60
|
+
? entry.bucket_name
|
|
61
|
+
: (entry.anhedral_name ?? `${project.name}-${entry.binding.toLowerCase()}`));
|
|
62
|
+
const id = kind === 'd1' ? entry.database_id : kind === 'kv' ? entry.id : undefined;
|
|
63
|
+
resources.push({
|
|
64
|
+
kind,
|
|
65
|
+
name,
|
|
66
|
+
target: target.id,
|
|
67
|
+
id: typeof id === 'string' && !id.startsWith('REPLACE_WITH_') ? id : undefined,
|
|
68
|
+
field: kind === 'r2' ? undefined : [field, index, kind === 'd1' ? 'database_id' : 'id'],
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const queues = config.queues;
|
|
73
|
+
for (const entry of [...(queues?.producers ?? []), ...(queues?.consumers ?? [])]) {
|
|
74
|
+
resources.push({ kind: 'queue', name: resourceName(entry.queue), target: target.id });
|
|
75
|
+
if ('dead_letter_queue' in entry && entry.dead_letter_queue)
|
|
76
|
+
resources.push({
|
|
77
|
+
kind: 'queue',
|
|
78
|
+
name: resourceName(entry.dead_letter_queue),
|
|
79
|
+
target: target.id,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
projectId: project.id,
|
|
85
|
+
accountId: project.accountId,
|
|
86
|
+
resources,
|
|
87
|
+
externalIntegrations: ['hyperdrive', 'pipelines'].filter((key) => project.targets.some((target) => key in readConfig(targetDirectory(root, target)))),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export async function provisionApplication(root, dryRun = false, run = defaultRunner, suppliedRequest) {
|
|
91
|
+
root = realpathSync(root);
|
|
92
|
+
const plan = planProvisioning(root);
|
|
93
|
+
if (dryRun)
|
|
94
|
+
return { ...plan, mutation: false };
|
|
95
|
+
return withApplicationLock(root, async () => {
|
|
96
|
+
const project = readApplication(root);
|
|
97
|
+
const plan = planProvisioning(root);
|
|
98
|
+
const configured = new Map(project.targets.map((target) => {
|
|
99
|
+
const file = path.join(targetDirectory(root, target), 'wrangler.jsonc');
|
|
100
|
+
return [file, readFileSync(file, 'utf8')];
|
|
101
|
+
}));
|
|
102
|
+
const assertConfiguration = () => {
|
|
103
|
+
if (JSON.stringify(readApplication(root)) !== JSON.stringify(project) ||
|
|
104
|
+
[...configured].some(([file, content]) => readFileSync(file, 'utf8') !== content))
|
|
105
|
+
throw new Error('Configuration changed during provisioning; inspect resource receipts before retrying.');
|
|
106
|
+
};
|
|
107
|
+
await authenticate(root, project, run);
|
|
108
|
+
const request = suppliedRequest ??
|
|
109
|
+
(await cloudflareRequest(project, targetDirectory(root, project.targets[0]), run, environment(project)));
|
|
110
|
+
const receipts = [];
|
|
111
|
+
const resources = new Map();
|
|
112
|
+
for (const resource of plan.resources) {
|
|
113
|
+
assertConfiguration();
|
|
114
|
+
const key = `${resource.kind}:${resource.name}`;
|
|
115
|
+
let receipt = resources.get(key);
|
|
116
|
+
if (!receipt) {
|
|
117
|
+
const { endpoint, nameField, idField } = resourceTypes[resource.kind];
|
|
118
|
+
const existing = await listResources(request, resource.kind);
|
|
119
|
+
const matches = existing.filter((item) => item[nameField] === resource.name);
|
|
120
|
+
if (matches.length > 1)
|
|
121
|
+
throw new Error('Ambiguous provider resource name.');
|
|
122
|
+
let value = matches[0];
|
|
123
|
+
const reused = !!value;
|
|
124
|
+
if (resource.id && (!value || value[idField] !== resource.id))
|
|
125
|
+
throw new Error('Configured resource ID does not match its account/name; no replacement was created.');
|
|
126
|
+
assertConfiguration();
|
|
127
|
+
if (!value)
|
|
128
|
+
value = (await request('POST', endpoint, { [nameField]: resource.name }))
|
|
129
|
+
.result;
|
|
130
|
+
if (!value || typeof value[idField] !== 'string' || !value[idField])
|
|
131
|
+
throw new Error('Provider did not return a resource identity; inspect provider state before retrying.');
|
|
132
|
+
receipt = {
|
|
133
|
+
kind: resource.kind,
|
|
134
|
+
name: resource.name,
|
|
135
|
+
id: value[idField],
|
|
136
|
+
reused,
|
|
137
|
+
};
|
|
138
|
+
resources.set(key, receipt);
|
|
139
|
+
receipts.push(receipt);
|
|
140
|
+
// Save identities immediately: a subsequent operation can fail without losing creation evidence.
|
|
141
|
+
atomicJson(path.join(root, '.anhedral/resources.json'), {
|
|
142
|
+
schemaVersion: 1,
|
|
143
|
+
projectId: project.id,
|
|
144
|
+
accountId: project.accountId,
|
|
145
|
+
checkedAt: new Date().toISOString(),
|
|
146
|
+
resources: receipts,
|
|
147
|
+
state: 'reconciling',
|
|
148
|
+
});
|
|
149
|
+
if (resource.kind === 'r2')
|
|
150
|
+
await assertPrivateBucket(request, resource.name);
|
|
151
|
+
}
|
|
152
|
+
assertConfiguration();
|
|
153
|
+
if (resource.id && receipt.id !== resource.id)
|
|
154
|
+
throw new Error('Targets disagree about a shared resource ID.');
|
|
155
|
+
if (resource.field) {
|
|
156
|
+
const target = project.targets.find((item) => item.id === resource.target);
|
|
157
|
+
const file = path.join(targetDirectory(root, target), 'wrangler.jsonc');
|
|
158
|
+
const before = readFileSync(file, 'utf8');
|
|
159
|
+
const after = applyEdits(before, modify(before, resource.field, receipt.id, {
|
|
160
|
+
formattingOptions: { insertSpaces: true, tabSize: 2 },
|
|
161
|
+
}));
|
|
162
|
+
writeFileSync(file, after);
|
|
163
|
+
configured.set(file, after);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const result = {
|
|
167
|
+
schemaVersion: 1,
|
|
168
|
+
projectId: project.id,
|
|
169
|
+
accountId: project.accountId,
|
|
170
|
+
checkedAt: new Date().toISOString(),
|
|
171
|
+
resources: receipts,
|
|
172
|
+
state: 'reconciled',
|
|
173
|
+
externalIntegrations: plan.externalIntegrations,
|
|
174
|
+
};
|
|
175
|
+
atomicJson(path.join(root, '.anhedral/resources.json'), result);
|
|
176
|
+
return result;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/** Reconcile committed identities against the customer account without creating resources or rewriting configuration. */
|
|
180
|
+
export async function inspectApplicationResources(root, request) {
|
|
181
|
+
const plan = planProvisioning(root);
|
|
182
|
+
const receipts = [];
|
|
183
|
+
for (const resource of plan.resources) {
|
|
184
|
+
const { nameField, idField } = resourceTypes[resource.kind];
|
|
185
|
+
const existing = await listResources(request, resource.kind);
|
|
186
|
+
const matches = existing.filter((item) => item[nameField] === resource.name);
|
|
187
|
+
if (matches.length !== 1 || typeof matches[0][idField] !== 'string' || !matches[0][idField])
|
|
188
|
+
throw new Error('Committed resource is missing or ambiguous in the registered account.');
|
|
189
|
+
const id = matches[0][idField];
|
|
190
|
+
if ((resource.field && !resource.id) || (resource.id && resource.id !== id))
|
|
191
|
+
throw new Error('Committed resource identity differs from the registered account; recovery will not replace it.');
|
|
192
|
+
if (resource.kind === 'r2')
|
|
193
|
+
await assertPrivateBucket(request, resource.name);
|
|
194
|
+
receipts.push({ kind: resource.kind, name: resource.name, id, target: resource.target });
|
|
195
|
+
}
|
|
196
|
+
return { resources: receipts, externalIntegrations: plan.externalIntegrations };
|
|
197
|
+
}
|
package/dist/readiness.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ResolvedStackRegistry } from './registry/resolve.js';
|
|
2
|
+
import { type StandardProduct } from './stack-registry.js';
|
|
2
3
|
export declare function inspectProject(root: string): {
|
|
3
4
|
schemaVersion: number;
|
|
4
5
|
root: string;
|
|
@@ -17,5 +18,6 @@ export declare function validateSetupPlan(plan: unknown): asserts plan is {
|
|
|
17
18
|
registryVersion: 1;
|
|
18
19
|
products: StandardProduct[];
|
|
19
20
|
hosting: 'cloudflare' | 'vercel';
|
|
20
|
-
|
|
21
|
+
name?: string;
|
|
22
|
+
registry: ResolvedStackRegistry;
|
|
21
23
|
};
|
package/dist/readiness.js
CHANGED
|
@@ -1,52 +1,100 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
4
|
+
import { inspectWorkspaceGraph } from './workspace-graph.js';
|
|
5
|
+
import { resolveStackRegistry, resolveStandardProducts } from './registry/resolve.js';
|
|
6
|
+
import { CAPABILITY_REGISTRY } from './capabilities.js';
|
|
7
|
+
import { STANDARD_PRODUCTS } from './stack-registry.js';
|
|
5
8
|
export function inspectProject(root) {
|
|
6
9
|
const checks = [];
|
|
7
10
|
const record = (id, ok, detail) => checks.push({ id, ok, detail });
|
|
8
11
|
const read = (file) => JSON.parse(readFileSync(path.join(root, file), 'utf8'));
|
|
12
|
+
record('workspace', existsSync(path.join(root, 'pnpm-workspace.yaml')), 'pnpm workspace manifest');
|
|
9
13
|
record('lockfile', existsSync(path.join(root, 'pnpm-lock.yaml')), 'Committed dependency resolution required');
|
|
10
14
|
try {
|
|
11
15
|
const plan = read('anhedral.setup.json');
|
|
12
16
|
validateSetupPlan(plan);
|
|
13
17
|
const standard = read('anhedral.standard.json');
|
|
14
|
-
const layout = plan.layout ?? 'workspace';
|
|
15
|
-
record('foundation', layout === 'single' ? existsSync(path.join(root, 'package.json')) && existsSync(path.join(root, 'src/index.ts')) : existsSync(path.join(root, 'pnpm-workspace.yaml')), layout === 'single' ? 'Standalone Hono application' : 'pnpm workspace manifest');
|
|
16
18
|
record('setup-plan', true, `Requirements recorded by CLI ${plan.cliVersion}`);
|
|
19
|
+
try {
|
|
20
|
+
const installed = read('anhedral.registry.json');
|
|
21
|
+
const { status, workspaces: _workspaces, files: _files, resourcesProvisioned, ...snapshot } = installed;
|
|
22
|
+
if (status !== 'scaffolded' ||
|
|
23
|
+
resourcesProvisioned !== false ||
|
|
24
|
+
!isDeepStrictEqual(snapshot, plan.registry))
|
|
25
|
+
throw new Error('Registry provenance differs from the setup plan.');
|
|
26
|
+
for (const item of plan.registry.items)
|
|
27
|
+
for (const target of item.targets) {
|
|
28
|
+
if (!existsSync(path.join(root, target)))
|
|
29
|
+
throw new Error(`Registry target missing: ${item.name}.`);
|
|
30
|
+
}
|
|
31
|
+
record('stack-registry', true, 'Registry provenance and resolved targets match the setup plan');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
record('stack-registry', false, 'Missing registry provenance or targets; inspect before regenerating');
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const workspaces = inspectWorkspaceGraph(root);
|
|
38
|
+
record('workspace-graph', workspaces.length > 0, 'Workspace names and dependencies resolve without cycles');
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
record('workspace-graph', false, 'Invalid workspace manifests, references or dependency cycles');
|
|
42
|
+
}
|
|
17
43
|
record('cli-compatibility', plan.cliVersion === CAPABILITY_REGISTRY.cliVersion, 'Use the project CLI version or review upgrade differences');
|
|
18
|
-
record('selection', standard.schemaVersion === 1 &&
|
|
44
|
+
record('selection', standard.schemaVersion === 1 &&
|
|
45
|
+
JSON.stringify(standard.products) === JSON.stringify(plan.products) &&
|
|
46
|
+
standard.hosting ===
|
|
47
|
+
(plan.products.includes('next') || plan.products.includes('vinext')
|
|
48
|
+
? plan.hosting
|
|
49
|
+
: null), 'Setup plan matches project selection');
|
|
19
50
|
}
|
|
20
51
|
catch {
|
|
21
52
|
record('setup-plan', false, 'Missing, invalid or unsupported setup plan; inspect the project before changing it');
|
|
22
53
|
}
|
|
23
|
-
return {
|
|
54
|
+
return {
|
|
55
|
+
schemaVersion: 1,
|
|
56
|
+
root,
|
|
57
|
+
checks,
|
|
58
|
+
localReady: checks.every((check) => check.ok),
|
|
59
|
+
providerVerification: 'unverified',
|
|
60
|
+
productionReady: false,
|
|
61
|
+
};
|
|
24
62
|
}
|
|
25
63
|
export function validateSetupPlan(plan) {
|
|
26
64
|
if (!plan || typeof plan !== 'object')
|
|
27
65
|
throw new Error('Invalid setup plan.');
|
|
28
66
|
const value = plan;
|
|
29
|
-
if (value.schemaVersion !== 1 ||
|
|
67
|
+
if (value.schemaVersion !== 1 ||
|
|
68
|
+
value.registryVersion !== 1 ||
|
|
69
|
+
typeof value.cliVersion !== 'string')
|
|
30
70
|
throw new Error('Unsupported setup plan version.');
|
|
31
|
-
if (!Array.isArray(value.products) ||
|
|
71
|
+
if (!Array.isArray(value.products) ||
|
|
72
|
+
!value.products.length ||
|
|
73
|
+
!value.products.every((id) => STANDARD_PRODUCTS.includes(id)))
|
|
32
74
|
throw new Error('Unknown setup capabilities.');
|
|
33
75
|
if (value.hosting !== 'cloudflare' && value.hosting !== 'vercel')
|
|
34
76
|
throw new Error('Invalid hosting.');
|
|
35
|
-
if (value.layout !== undefined && value.layout !== 'workspace' && value.layout !== 'single')
|
|
36
|
-
throw new Error('Invalid layout.');
|
|
37
|
-
if (value.layout === 'single' && (value.products.length !== 1 || value.products[0] !== 'hono'))
|
|
38
|
-
throw new Error('Unsupported standalone recipe.');
|
|
39
77
|
validateRequirements(value.setup, value.products);
|
|
40
78
|
const selected = new Set(value.products);
|
|
41
79
|
resolveStandardProducts(selected, value.hosting, false);
|
|
42
80
|
if (selected.size !== value.products.length)
|
|
43
81
|
throw new Error('Invalid or incomplete setup selection.');
|
|
82
|
+
if (value.name !== undefined && typeof value.name !== 'string')
|
|
83
|
+
throw new Error('Invalid setup project name.');
|
|
84
|
+
if (!isDeepStrictEqual(value.registry, resolveStackRegistry({
|
|
85
|
+
products: value.products,
|
|
86
|
+
hosting: value.hosting,
|
|
87
|
+
name: value.name,
|
|
88
|
+
})))
|
|
89
|
+
throw new Error('Invalid or inconsistent stack registry.');
|
|
44
90
|
}
|
|
45
91
|
function validateRequirements(setup, products) {
|
|
46
92
|
if (!setup || typeof setup !== 'object')
|
|
47
93
|
throw new Error('Missing setup requirements.');
|
|
48
94
|
const value = setup;
|
|
49
|
-
if (value.resourcesProvisioned !== false ||
|
|
95
|
+
if (value.resourcesProvisioned !== false ||
|
|
96
|
+
!Array.isArray(value.capabilities) ||
|
|
97
|
+
value.capabilities.length !== products.length)
|
|
50
98
|
throw new Error('Invalid capability requirements.');
|
|
51
99
|
const ids = value.capabilities.map((item) => validateCapability(item));
|
|
52
100
|
if (new Set(ids).size !== products.length || !products.every((id) => ids.includes(id)))
|
|
@@ -56,7 +104,9 @@ function validateCapability(item) {
|
|
|
56
104
|
if (!item || typeof item !== 'object')
|
|
57
105
|
throw new Error('Invalid capability.');
|
|
58
106
|
const value = item;
|
|
59
|
-
if (typeof value.id !== 'string' ||
|
|
107
|
+
if (typeof value.id !== 'string' ||
|
|
108
|
+
typeof value.purpose !== 'string' ||
|
|
109
|
+
!['planned', 'starter'].includes(String(value.status)))
|
|
60
110
|
throw new Error('Invalid capability metadata.');
|
|
61
111
|
for (const key of ['tools', 'access', 'credentials', 'resources', 'verify']) {
|
|
62
112
|
if (!Array.isArray(value[key]) || !value[key].every((entry) => typeof entry === 'string'))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { StandardRunner } from './types.js';
|
|
2
|
+
export interface AddRequest {
|
|
3
|
+
items: string[];
|
|
4
|
+
dryRun?: boolean;
|
|
5
|
+
skipInstall?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** Add recipes to an initialized workspace, refusing edits to customer-owned source. */
|
|
8
|
+
export declare function addStackItems(root: string, request: AddRequest, run?: StandardRunner): Promise<{
|
|
9
|
+
schemaVersion: number;
|
|
10
|
+
root: string;
|
|
11
|
+
dryRun: boolean;
|
|
12
|
+
items: string[];
|
|
13
|
+
bootstrapRequired: boolean;
|
|
14
|
+
filesChecked: boolean;
|
|
15
|
+
targets: string[];
|
|
16
|
+
files: string[];
|
|
17
|
+
conflicts: string[];
|
|
18
|
+
}>;
|