anhedral 0.9.0 → 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 +4 -8
- package/dist/bin.js +6 -2
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -1
- package/dist/readiness.js +1 -18
- package/dist/registry/add.d.ts +5 -9
- package/dist/registry/add.js +20 -55
- package/dist/registry/commands.js +5 -56
- package/dist/registry/receipt.d.ts +0 -1
- package/dist/registry/receipt.js +1 -22
- package/dist/registry/usage.js +2 -3
- package/dist/scaffold/bootstrap.d.ts +1 -0
- package/dist/scaffold/bootstrap.js +6 -1
- package/dist/standard.js +2 -8
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/delivery-workflow.md +1 -1
- package/package.json +1 -1
- package/dist/registry/remote.d.ts +0 -25
- package/dist/registry/remote.js +0 -168
package/README.md
CHANGED
|
@@ -53,22 +53,18 @@ Anhedral Init is free and open source. Sponsorship helps fund ongoing developmen
|
|
|
53
53
|
|
|
54
54
|
The bundled stack registry drives composition of every supported stack part. Run `anhedral registry list` or `anhedral registry view better-auth --json` to inspect items, destinations, dependencies and selection constraints. `new --dry-run --json` includes the resolved dependency graph. Scaffolding installs recipes in dependency order into `apps/*` and `packages/*`, validates local workspace references and cycles, and writes `anhedral.registry.json` with recipe versions and the resulting workspace graph. The same catalog is bundled with the plugin at build time.
|
|
55
55
|
|
|
56
|
-
Registry items are trusted recipes shipped in the CLI release: foundations, apps, services, shared packages and runtime integrations. They follow shadcn's source ownership and dependency composition practices; this catalog is an Anhedral schema, not a shadcn registry payload. Runtime-aware recipes coordinate bindings, manifests and auth routes across workspaces. Each registry entry owns its recipe, provider requirements, environment variables, bindings and composition rules. Resolved plans name the exact generated destinations, including runtime-dependent packages. Recipe versions follow the CLI release. The official shadcn bootstrap is pinned to 4.21.4; commit the generated source and lockfile to preserve the application artifact. Incremental `add` supports bundled
|
|
56
|
+
Registry items are trusted recipes shipped in the CLI release: foundations, apps, services, shared packages and runtime integrations. They follow shadcn's source ownership and dependency composition practices; this catalog is an Anhedral schema, not a shadcn registry payload. Runtime-aware recipes coordinate bindings, manifests and auth routes across workspaces. Each registry entry owns its recipe, provider requirements, environment variables, bindings and composition rules. Resolved plans name the exact generated destinations, including runtime-dependent packages. Recipe versions follow the CLI release. The official shadcn bootstrap is pinned to 4.21.4; commit the generated source and lockfile to preserve the application artifact. Incremental `add` supports only the bundled catalog maintained and tested by Anhedral. External registries and customer-authored registry payloads are unsupported. Additions refuse to overwrite edited or untracked source; runtime relocation requires a deliberate migration.
|
|
57
57
|
|
|
58
58
|
Use additions after initialization:
|
|
59
59
|
|
|
60
60
|
```sh
|
|
61
61
|
anhedral add my-app resend stripe --dry-run --json
|
|
62
62
|
anhedral add my-app resend stripe
|
|
63
|
-
anhedral registry list --
|
|
64
|
-
anhedral
|
|
63
|
+
anhedral registry list --json
|
|
64
|
+
anhedral registry view resend --json
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
Review the dry-run file list and conflicts, then run the same addition without `--dry-run`.
|
|
68
|
-
|
|
69
|
-
Author registries with `anhedral registry build registry.source.json --output registry.json`: each file can declare a repository-relative `path` instead of embedded `content`. Build validates and embeds source before producing the distributable payload.
|
|
70
|
-
|
|
71
|
-
External registries use Anhedral's declarative source schema: `schemaVersion: 1`, a registry `name`, and `items`. Each item declares `name`, semver `version`, `registryDependencies` (item names in the same registry), and `files` containing explicit `target` and UTF-8 `content`. Targets are confined to `apps/<name>/` and `packages/<name>/`; manifests must be private `@workspace` packages with versioned npm/workspace dependencies and no install hooks. Registries are limited to 2 MiB and 100 items. The receipt records the source, SHA-256 integrity, item versions, targets and generated-file hashes. This schema does not accept executable recipes or shadcn payloads. Review registry source as application code before using it.
|
|
67
|
+
Review the dry-run file list and conflicts, then run the same addition without `--dry-run`. A preview that needs a new web foundation reports `bootstrapRequired: true` and `filesChecked: false`; it describes the selection and targets without running the upstream generator or checking generated-file conflicts. The actual addition checks conflicts in an isolated transaction before committing files. `--skip-install` still resolves the lockfile; run `pnpm install --frozen-lockfile` afterward to prepare dependencies. Final installs run the bundled workspace's dependency preparation hooks, including WXT and Electron setup. Projects must have matching CLI versions and scaffold fingerprints; older projects and receipts containing external items need a deliberate migration. Existing provider identities stay intact, and no provider resources are created.
|
|
72
68
|
|
|
73
69
|
Application recipes start from the official **shadcn pnpm/Turborepo monorepo**. The default creates **Vinext + React + TypeScript + Tailwind + shadcn/ui + Lucide**, targeting **Cloudflare Workers**. Select additional apps and services only when needed.
|
|
74
70
|
|
package/dist/bin.js
CHANGED
|
@@ -110,7 +110,7 @@ async function main() {
|
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
112
112
|
if (command !== 'new' && command !== 'init') {
|
|
113
|
-
throw new Error(`Unknown command: ${command}. Use new, init, registry, doctor, repository, cloud, register, connect, provision, ship, status, manage, verify, rollback, adopt, recover, --help, or --version.`);
|
|
113
|
+
throw new Error(`Unknown command: ${command}. Use new, init, add, registry, doctor, repository, cloud, register, connect, provision, ship, status, manage, verify, rollback, adopt, recover, --help, or --version.`);
|
|
114
114
|
}
|
|
115
115
|
if (args.some((arg) => ['--help', '-h'].includes(arg))) {
|
|
116
116
|
console.log(json ? JSON.stringify({ usage: STANDARD_USAGE }) : STANDARD_USAGE);
|
|
@@ -401,7 +401,11 @@ catch (error) {
|
|
|
401
401
|
? 'INVALID_ARGUMENT'
|
|
402
402
|
: command === 'new' || command === 'init'
|
|
403
403
|
? 'GENERATION_FAILED'
|
|
404
|
-
: '
|
|
404
|
+
: command === 'add'
|
|
405
|
+
? 'ADDITION_FAILED'
|
|
406
|
+
: command === 'registry'
|
|
407
|
+
? 'REGISTRY_FAILED'
|
|
408
|
+
: 'UNKNOWN_COMMAND';
|
|
405
409
|
console.error(json ? JSON.stringify({ error: message, code }) : `Error: ${message}`);
|
|
406
410
|
process.exitCode = 1;
|
|
407
411
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,4 @@ export { inspectWorkspaceGraph } from './workspace-graph.js';
|
|
|
18
18
|
export { resolveStackRegistry } from './registry/resolve.js';
|
|
19
19
|
export type { ResolvedStackRegistry } from './registry/resolve.js';
|
|
20
20
|
export { addStackItems } from './registry/add.js';
|
|
21
|
-
export { buildSourceRegistry, loadSourceRegistry, validateSourceRegistry, resolveSourceItems, } from './registry/remote.js';
|
|
22
|
-
export type { SourceRegistry, SourceItem, RemoteRegistry } from './registry/remote.js';
|
|
23
21
|
export type { AddRequest } from './registry/add.js';
|
package/dist/index.js
CHANGED
|
@@ -12,4 +12,3 @@ export { STACK_REGISTRY, STACK_ITEMS, getStackItem } from './stack-registry.js';
|
|
|
12
12
|
export { inspectWorkspaceGraph } from './workspace-graph.js';
|
|
13
13
|
export { resolveStackRegistry } from './registry/resolve.js';
|
|
14
14
|
export { addStackItems } from './registry/add.js';
|
|
15
|
-
export { buildSourceRegistry, loadSourceRegistry, validateSourceRegistry, resolveSourceItems, } from './registry/remote.js';
|
package/dist/readiness.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { assertSafeTarget } from './registry/receipt.js';
|
|
2
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { isDeepStrictEqual } from 'node:util';
|
|
@@ -19,7 +18,7 @@ export function inspectProject(root) {
|
|
|
19
18
|
record('setup-plan', true, `Requirements recorded by CLI ${plan.cliVersion}`);
|
|
20
19
|
try {
|
|
21
20
|
const installed = read('anhedral.registry.json');
|
|
22
|
-
const { status, workspaces: _workspaces, files: _files,
|
|
21
|
+
const { status, workspaces: _workspaces, files: _files, resourcesProvisioned, ...snapshot } = installed;
|
|
23
22
|
if (status !== 'scaffolded' ||
|
|
24
23
|
resourcesProvisioned !== false ||
|
|
25
24
|
!isDeepStrictEqual(snapshot, plan.registry))
|
|
@@ -29,22 +28,6 @@ export function inspectProject(root) {
|
|
|
29
28
|
if (!existsSync(path.join(root, target)))
|
|
30
29
|
throw new Error(`Registry target missing: ${item.name}.`);
|
|
31
30
|
}
|
|
32
|
-
if (remoteItems !== undefined) {
|
|
33
|
-
if (!Array.isArray(remoteItems))
|
|
34
|
-
throw new Error('Invalid remote registry receipt.');
|
|
35
|
-
for (const item of remoteItems) {
|
|
36
|
-
if (!item ||
|
|
37
|
-
typeof item.source !== 'string' ||
|
|
38
|
-
typeof item.integrity !== 'string' ||
|
|
39
|
-
!Array.isArray(item.targets))
|
|
40
|
-
throw new Error('Invalid remote registry provenance.');
|
|
41
|
-
for (const target of item.targets) {
|
|
42
|
-
assertSafeTarget(root, target);
|
|
43
|
-
if (!existsSync(path.join(root, target)))
|
|
44
|
-
throw new Error('Missing remote registry target.');
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
31
|
record('stack-registry', true, 'Registry provenance and resolved targets match the setup plan');
|
|
49
32
|
}
|
|
50
33
|
catch {
|
package/dist/registry/add.d.ts
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
import type { StandardRunner } from './types.js';
|
|
2
2
|
export interface AddRequest {
|
|
3
3
|
items: string[];
|
|
4
|
-
registry?: string;
|
|
5
|
-
integrity?: string;
|
|
6
4
|
dryRun?: boolean;
|
|
7
5
|
skipInstall?: boolean;
|
|
8
6
|
}
|
|
9
7
|
/** Add recipes to an initialized workspace, refusing edits to customer-owned source. */
|
|
10
8
|
export declare function addStackItems(root: string, request: AddRequest, run?: StandardRunner): Promise<{
|
|
11
|
-
files: string[];
|
|
12
|
-
conflicts: string[];
|
|
13
|
-
registry?: {
|
|
14
|
-
source: string;
|
|
15
|
-
integrity: string;
|
|
16
|
-
} | undefined;
|
|
17
9
|
schemaVersion: number;
|
|
18
10
|
root: string;
|
|
19
11
|
dryRun: boolean;
|
|
20
12
|
items: string[];
|
|
21
|
-
|
|
13
|
+
bootstrapRequired: boolean;
|
|
14
|
+
filesChecked: boolean;
|
|
15
|
+
targets: string[];
|
|
16
|
+
files: string[];
|
|
17
|
+
conflicts: string[];
|
|
22
18
|
}>;
|
package/dist/registry/add.js
CHANGED
|
@@ -16,10 +16,12 @@ import { json, put, readJson } from '../scaffold/files.js';
|
|
|
16
16
|
import { writeStandardRoot } from '../scaffold/workspace.js';
|
|
17
17
|
import { createRecipeContext } from './context.js';
|
|
18
18
|
import { resolveStackRegistry, resolveStandardProducts } from './resolve.js';
|
|
19
|
-
import { sourceFiles
|
|
20
|
-
import { loadSourceRegistry, resolveSourceItems } from './remote.js';
|
|
19
|
+
import { sourceFiles } from './receipt.js';
|
|
21
20
|
/** Add recipes to an initialized workspace, refusing edits to customer-owned source. */
|
|
22
21
|
export async function addStackItems(root, request, run = execFile) {
|
|
22
|
+
for (const key of Object.keys(request))
|
|
23
|
+
if (!['items', 'dryRun', 'skipInstall'].includes(key))
|
|
24
|
+
throw new Error(`Unknown add option: ${key}. Only bundled registry items are supported.`);
|
|
23
25
|
root = realpathSync(root);
|
|
24
26
|
if (!request.items.length)
|
|
25
27
|
throw new Error('Select at least one registry item.');
|
|
@@ -34,17 +36,11 @@ export async function addStackItems(root, request, run = execFile) {
|
|
|
34
36
|
throw new Error('This project has no scaffold fingerprints; migrate it deliberately before using add.');
|
|
35
37
|
const baseline = receipt.files;
|
|
36
38
|
const selected = new Set(plan.products);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (!STANDARD_PRODUCTS.includes(id))
|
|
43
|
-
throw new Error(`Unknown stack product: ${id}.`);
|
|
44
|
-
selected.add(id);
|
|
45
|
-
}
|
|
46
|
-
if (request.integrity && (!remote || remote.integrity !== request.integrity))
|
|
47
|
-
throw new Error('Source registry integrity differs from the reviewed payload.');
|
|
39
|
+
for (const id of request.items) {
|
|
40
|
+
if (!STANDARD_PRODUCTS.includes(id))
|
|
41
|
+
throw new Error(`Unknown stack product: ${id}.`);
|
|
42
|
+
selected.add(id);
|
|
43
|
+
}
|
|
48
44
|
resolveStandardProducts(selected, plan.hosting, false);
|
|
49
45
|
const options = {
|
|
50
46
|
root,
|
|
@@ -65,22 +61,6 @@ export async function addStackItems(root, request, run = execFile) {
|
|
|
65
61
|
if (before && before.targets.some((target) => !item.targets.includes(target)))
|
|
66
62
|
throw new Error(`Adding this selection relocates ${item.name}; migrate its existing source explicitly.`);
|
|
67
63
|
}
|
|
68
|
-
const remoteItems = remote ? resolveSourceItems(remote.registry, request.items) : [];
|
|
69
|
-
if (remote) {
|
|
70
|
-
const previous = Array.isArray(receipt.remoteItems) ? receipt.remoteItems : [];
|
|
71
|
-
for (const item of remoteItems) {
|
|
72
|
-
const identity = `${remote.source}#${item.name}`;
|
|
73
|
-
const targets = item.files.map((file) => file.target);
|
|
74
|
-
for (const installed of previous) {
|
|
75
|
-
if (installed.identity === identity &&
|
|
76
|
-
installed.targets.some((target) => !targets.includes(target)))
|
|
77
|
-
throw new Error(`Remote update relocates ${item.name}; migrate its source explicitly.`);
|
|
78
|
-
if (installed.identity !== identity &&
|
|
79
|
-
installed.targets.some((target) => targets.includes(target)))
|
|
80
|
-
throw new Error(`Remote target is already owned by ${installed.name}.`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
64
|
const before = sourceFiles(root);
|
|
85
65
|
const receiptText = readFileSync(path.join(root, 'anhedral.registry.json'), 'utf8');
|
|
86
66
|
const commitPaths = [];
|
|
@@ -92,11 +72,19 @@ export async function addStackItems(root, request, run = execFile) {
|
|
|
92
72
|
items: registry.items
|
|
93
73
|
.filter((item) => !isDeepStrictEqual(oldItems.get(item.name), item))
|
|
94
74
|
.map((item) => item.name),
|
|
95
|
-
|
|
96
|
-
|
|
75
|
+
bootstrapRequired: recipes.some((item) => ['vinext', 'next'].includes(item.name)) &&
|
|
76
|
+
!existsSync(path.join(root, 'apps/web/package.json')),
|
|
77
|
+
filesChecked: true,
|
|
78
|
+
targets: recipes.flatMap((item) => item.targets),
|
|
97
79
|
files: commitPaths,
|
|
98
80
|
conflicts,
|
|
99
81
|
};
|
|
82
|
+
// The upstream web generator requires network access and executes package scripts.
|
|
83
|
+
// A preview reports the resolved selection without invoking that generator.
|
|
84
|
+
if (request.dryRun && result.bootstrapRequired) {
|
|
85
|
+
result.filesChecked = false;
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
100
88
|
await runStagedTransaction(root, {
|
|
101
89
|
dryRun: !!request.dryRun,
|
|
102
90
|
commitPaths,
|
|
@@ -126,11 +114,6 @@ export async function addStackItems(root, request, run = execFile) {
|
|
|
126
114
|
}
|
|
127
115
|
put(stage, file, content);
|
|
128
116
|
}
|
|
129
|
-
for (const item of remoteItems)
|
|
130
|
-
for (const file of item.files) {
|
|
131
|
-
assertSafeTarget(stage, file.target);
|
|
132
|
-
put(stage, file.target, file.content);
|
|
133
|
-
}
|
|
134
117
|
writeStandardRoot(stage, options, registry, true);
|
|
135
118
|
if (options.products.includes('next') && options.hosting === 'vercel')
|
|
136
119
|
registerVercelApplication(stage);
|
|
@@ -158,30 +141,12 @@ export async function addStackItems(root, request, run = execFile) {
|
|
|
158
141
|
conflicts.push(file);
|
|
159
142
|
if (conflicts.length && !request.dryRun)
|
|
160
143
|
throw new Error(`Addition conflicts with edited or untracked source: ${conflicts.join(', ')}. No files were installed.`);
|
|
161
|
-
const remoteReceipts = Array.isArray(receipt.remoteItems) ? [...receipt.remoteItems] : [];
|
|
162
|
-
for (const item of remoteItems) {
|
|
163
|
-
const identity = `${remote.source}#${item.name}`;
|
|
164
|
-
const previous = remoteReceipts.findIndex((entry) => entry.identity === identity);
|
|
165
|
-
const entry = {
|
|
166
|
-
identity,
|
|
167
|
-
name: item.name,
|
|
168
|
-
version: item.version,
|
|
169
|
-
source: remote.source,
|
|
170
|
-
integrity: remote.integrity,
|
|
171
|
-
targets: item.files.map((file) => file.target),
|
|
172
|
-
};
|
|
173
|
-
if (previous >= 0)
|
|
174
|
-
remoteReceipts[previous] = entry;
|
|
175
|
-
else
|
|
176
|
-
remoteReceipts.push(entry);
|
|
177
|
-
}
|
|
178
144
|
json(stage, 'anhedral.registry.json', {
|
|
179
145
|
...registry,
|
|
180
146
|
status: 'scaffolded',
|
|
181
147
|
resourcesProvisioned: false,
|
|
182
148
|
workspaces: inspectWorkspaceGraph(stage),
|
|
183
149
|
files,
|
|
184
|
-
...(remoteReceipts.length ? { remoteItems: remoteReceipts } : {}),
|
|
185
150
|
});
|
|
186
151
|
commitPaths.push('anhedral.registry.json');
|
|
187
152
|
// Detect concurrent changes even to source that this addition would otherwise preserve.
|
|
@@ -191,7 +156,7 @@ export async function addStackItems(root, request, run = execFile) {
|
|
|
191
156
|
},
|
|
192
157
|
afterCommit: () => {
|
|
193
158
|
if (!request.skipInstall)
|
|
194
|
-
run('pnpm', ['install', '--frozen-lockfile'
|
|
159
|
+
run('pnpm', ['install', '--frozen-lockfile'], root);
|
|
195
160
|
},
|
|
196
161
|
});
|
|
197
162
|
return result;
|
|
@@ -1,42 +1,7 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
1
|
import path from 'node:path';
|
|
3
2
|
import { addStackItems } from './add.js';
|
|
4
|
-
import { buildSourceRegistry, loadSourceRegistry } from './remote.js';
|
|
5
3
|
import { STACK_REGISTRY, getStackItem } from '../stack-registry.js';
|
|
6
4
|
export async function runRegistry(args, json) {
|
|
7
|
-
if (args[0] === 'build') {
|
|
8
|
-
args = args.filter((arg) => arg !== '--json');
|
|
9
|
-
if (args.length !== 4 || args[2] !== '--output' || !args[1] || !args[3])
|
|
10
|
-
throw new Error('Use registry build <manifest> --output <registry.json>.');
|
|
11
|
-
const source = path.resolve(args[1]);
|
|
12
|
-
const output = path.resolve(args[3]);
|
|
13
|
-
if (source === output)
|
|
14
|
-
throw new Error('Registry output must differ from its authored manifest.');
|
|
15
|
-
const registry = buildSourceRegistry(source);
|
|
16
|
-
mkdirSync(path.dirname(output), { recursive: true });
|
|
17
|
-
writeFileSync(output, JSON.stringify(registry, null, 2) + '\n');
|
|
18
|
-
console.log(JSON.stringify({ output, items: registry.items.length }));
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
const registryFlag = args.indexOf('--registry');
|
|
22
|
-
if (registryFlag >= 0) {
|
|
23
|
-
const source = args[registryFlag + 1];
|
|
24
|
-
if (!source || source.startsWith('-'))
|
|
25
|
-
throw new Error('Missing registry source.');
|
|
26
|
-
const positional = args.filter((arg, index) => index !== registryFlag && index !== registryFlag + 1 && arg !== '--json');
|
|
27
|
-
const registry = await loadSourceRegistry(source);
|
|
28
|
-
if (positional.length === 1 && positional[0] === 'list')
|
|
29
|
-
console.log(JSON.stringify(registry, null, 2));
|
|
30
|
-
else if (positional.length === 2 && positional[0] === 'view') {
|
|
31
|
-
const item = registry.registry.items.find((item) => item.name === positional[1]);
|
|
32
|
-
if (!item)
|
|
33
|
-
throw new Error('Unknown remote registry item.');
|
|
34
|
-
console.log(JSON.stringify({ source: registry.source, integrity: registry.integrity, item }, null, 2));
|
|
35
|
-
}
|
|
36
|
-
else
|
|
37
|
-
throw new Error('Use registry list|view [item] --registry <file|https-url>.');
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
5
|
const positional = args.filter((arg) => arg !== '--json');
|
|
41
6
|
if (positional.includes('--help') || positional.includes('-h')) {
|
|
42
7
|
console.log('anhedral registry list|view [item] [--json]');
|
|
@@ -61,38 +26,22 @@ export async function runRegistry(args, json) {
|
|
|
61
26
|
}
|
|
62
27
|
export async function runAdd(args) {
|
|
63
28
|
if (args.includes('--help') || args.includes('-h')) {
|
|
64
|
-
console.log('anhedral add <directory> <items...> [--
|
|
29
|
+
console.log('anhedral add <directory> <items...> [--dry-run] [--skip-install] [--json]');
|
|
65
30
|
return;
|
|
66
31
|
}
|
|
67
|
-
let source;
|
|
68
|
-
let integrity;
|
|
69
32
|
const positional = [];
|
|
70
|
-
for (
|
|
71
|
-
const arg = args[index];
|
|
33
|
+
for (const arg of args) {
|
|
72
34
|
if (['--dry-run', '--skip-install', '--json'].includes(arg))
|
|
73
35
|
continue;
|
|
74
|
-
if (arg
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
integrity = args[++index];
|
|
78
|
-
}
|
|
79
|
-
else if (arg === '--registry') {
|
|
80
|
-
if (source || !args[index + 1] || args[index + 1].startsWith('-'))
|
|
81
|
-
throw new Error('Specify one registry source.');
|
|
82
|
-
source = args[++index];
|
|
83
|
-
}
|
|
84
|
-
else if (arg.startsWith('-'))
|
|
85
|
-
throw new Error('Unknown add option.');
|
|
86
|
-
else
|
|
87
|
-
positional.push(arg);
|
|
36
|
+
if (arg.startsWith('-'))
|
|
37
|
+
throw new Error(`Unknown add option: ${arg}.`);
|
|
38
|
+
positional.push(arg);
|
|
88
39
|
}
|
|
89
40
|
const [directory, ...items] = positional;
|
|
90
41
|
if (!directory || !items.length)
|
|
91
42
|
throw new Error('Use add <directory> <items...>.');
|
|
92
43
|
console.log(JSON.stringify(await addStackItems(path.resolve(directory), {
|
|
93
44
|
items,
|
|
94
|
-
registry: source,
|
|
95
|
-
integrity,
|
|
96
45
|
dryRun: args.includes('--dry-run'),
|
|
97
46
|
skipInstall: args.includes('--skip-install'),
|
|
98
47
|
}), null, 2));
|
package/dist/registry/receipt.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { json, readJson } from '../scaffold/files.js';
|
|
5
5
|
const excluded = new Set([
|
|
@@ -50,24 +50,3 @@ export function recordScaffoldFiles(root) {
|
|
|
50
50
|
files: Object.fromEntries(sourceFiles(root)),
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
|
-
export function assertSafeTarget(root, relative) {
|
|
54
|
-
if (!relative ||
|
|
55
|
-
path.isAbsolute(relative) ||
|
|
56
|
-
relative.includes('\\') ||
|
|
57
|
-
relative
|
|
58
|
-
.split('/')
|
|
59
|
-
.some((part) => !part ||
|
|
60
|
-
part === '.' ||
|
|
61
|
-
part === '..' ||
|
|
62
|
-
part.includes(':') ||
|
|
63
|
-
/[\x00-\x1f]/.test(part) ||
|
|
64
|
-
/[. ]$/.test(part) ||
|
|
65
|
-
/^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i.test(part)))
|
|
66
|
-
throw new Error(`Unsafe registry target: ${relative}.`);
|
|
67
|
-
let current = root;
|
|
68
|
-
for (const part of relative.split('/')) {
|
|
69
|
-
current = path.join(current, part);
|
|
70
|
-
if (existsSync(current) && lstatSync(current).isSymbolicLink())
|
|
71
|
-
throw new Error(`Registry target traverses a symlink: ${relative}.`);
|
|
72
|
-
}
|
|
73
|
-
}
|
package/dist/registry/usage.js
CHANGED
|
@@ -2,7 +2,7 @@ import { STANDARD_PRODUCTS } from '../stack-registry.js';
|
|
|
2
2
|
export const STANDARD_USAGE = `Anhedral Application Stack & Delivery Standard
|
|
3
3
|
|
|
4
4
|
anhedral new <directory> [products...] [--hosting cloudflare|vercel] [--skip-install] [--no-git] [--dry-run] [--json]
|
|
5
|
-
anhedral add <directory> <items...> [--
|
|
5
|
+
anhedral add <directory> <items...> [--dry-run] [--skip-install] [--json]
|
|
6
6
|
anhedral init [products...] [--hosting cloudflare|vercel] [--skip-install] [--git] [--dry-run] [--json]
|
|
7
7
|
|
|
8
8
|
Default: Vinext on Cloudflare Workers, from the official shadcn pnpm/Turborepo monorepo.
|
|
@@ -15,8 +15,7 @@ Examples:
|
|
|
15
15
|
anhedral new website --next --stripe --resend
|
|
16
16
|
anhedral init --wxt
|
|
17
17
|
anhedral cloud register|stage|save|deploy|status [directory] (managed service API required)
|
|
18
|
-
anhedral registry
|
|
19
|
-
anhedral registry list|view [item] [--registry <file|https-url>] [--json]
|
|
18
|
+
anhedral registry list|view [item] [--json]
|
|
20
19
|
anhedral doctor [directory] [--providers] --json
|
|
21
20
|
anhedral repository [directory] [--owner <github-account>] [--name <repo>] [--visibility private|public] [--create]
|
|
22
21
|
anhedral register [directory]
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { StandardRunner } from '../registry/types.js';
|
|
2
|
+
export declare function assertBootstrapPlatform(run: StandardRunner, platform?: NodeJS.Platform): void;
|
|
2
3
|
export declare function bootstrapWorkspace(root: string, name: string, run: StandardRunner, webOnly?: boolean): void;
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { appendGitignore } from '../util.js';
|
|
3
|
+
import { execFile, appendGitignore } from '../util.js';
|
|
4
4
|
import { bootstrapArguments } from '../registry/bootstrap.js';
|
|
5
|
+
export function assertBootstrapPlatform(run, platform = process.platform) {
|
|
6
|
+
if (platform === 'win32' && run === execFile)
|
|
7
|
+
throw new Error('Native Windows initialization is blocked because the upstream shadcn monorepo bootstrap writes component files outside the project. Run Anhedral inside WSL. Planning and doctor remain available on Windows.');
|
|
8
|
+
}
|
|
5
9
|
export function bootstrapWorkspace(root, name, run, webOnly = false) {
|
|
10
|
+
assertBootstrapPlatform(run);
|
|
6
11
|
const temporary = path.join(root, '.bootstrap');
|
|
7
12
|
mkdirSync(temporary);
|
|
8
13
|
try {
|
package/dist/standard.js
CHANGED
|
@@ -8,19 +8,13 @@ import { registerVercelApplication } from './vercel.js';
|
|
|
8
8
|
import { registerApplication } from './delivery.js';
|
|
9
9
|
import { runStagedTransaction } from './transaction.js';
|
|
10
10
|
import { createSetupPlan } from './capabilities.js';
|
|
11
|
-
import { bootstrapWorkspace } from './scaffold/bootstrap.js';
|
|
11
|
+
import { bootstrapWorkspace, assertBootstrapPlatform } from './scaffold/bootstrap.js';
|
|
12
12
|
import { composeStack } from './scaffold/compose.js';
|
|
13
13
|
import { parseStandardOptions } from './registry/options.js';
|
|
14
14
|
import { STANDARD_USAGE } from './registry/usage.js';
|
|
15
15
|
export { parseStandardOptions, STANDARD_USAGE };
|
|
16
16
|
export { STANDARD_PRODUCTS } from './stack-registry.js';
|
|
17
|
-
|
|
18
|
-
function assertBootstrapPlatform(run) {
|
|
19
|
-
if (process.platform === 'win32' && run === defaultRunner) {
|
|
20
|
-
throw new Error('Native Windows initialization is blocked because the upstream shadcn monorepo bootstrap writes component files outside the project. Run Anhedral inside WSL. Planning and doctor remain available on Windows.');
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
export async function scaffoldStandardProject(options, run = defaultRunner) {
|
|
17
|
+
export async function scaffoldStandardProject(options, run = execFile) {
|
|
24
18
|
const plan = createSetupPlan(options);
|
|
25
19
|
if (options.dryRun) {
|
|
26
20
|
console.log(options.json ? JSON.stringify(plan, null, 2) : JSON.stringify(plan));
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const GENERATOR_VERSION = "0.
|
|
1
|
+
export declare const GENERATOR_VERSION = "0.10.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const GENERATOR_VERSION = '0.
|
|
1
|
+
export const GENERATOR_VERSION = '0.10.0';
|
|
@@ -6,7 +6,7 @@ Codex implements the product; the CLI supplies scaffolding, selected resource re
|
|
|
6
6
|
|
|
7
7
|
1. Choose only needed components from the [stack standard](application-stack-standard.md). Record decisions, ownership, environment/cost assumptions and actual connections in the application's `docs/architecture.md`.
|
|
8
8
|
2. Match installed plugin, registry `cliVersion` and executable versions. Check Node/pnpm, Git identity, toolchains and customer provider access. For unpublished development use `pnpm build` and `node dist/bin.js`; never silently substitute `@latest`.
|
|
9
|
-
3. Inspect `new <unused-directory> <flags> --dry-run --json`, then generate with those choices. Review README, AGENTS, setup manifests and resolved lockfile. For initialized apps, preview `add <directory> <items...> --dry-run --json`; additions protect edited source and record registry provenance.
|
|
9
|
+
3. Inspect `new <unused-directory> <flags> --dry-run --json`, then generate with those choices. Review README, AGENTS, setup manifests and resolved lockfile. For initialized apps, preview `add <directory> <items...> --dry-run --json`; additions protect edited source and record registry provenance. Select only items from the bundled Anhedral catalog. If a web addition reports `filesChecked: false`, the preview covers selection and targets only; the actual transaction checks generated-file conflicts before committing. Older projects and runtime relocation require explicit migration. `doctor --providers --json` checks access without configuring infrastructure or certifying production.
|
|
10
10
|
4. Implement screens, APIs, persistence, authorization and integration behavior. Replace placeholders with working flows. Configure real secrets, origins and reviewed migrations using provider tools; starters do not establish setup completion. Keep credentials out of source, logs and handoffs.
|
|
11
11
|
|
|
12
12
|
## Source and release authority
|
package/package.json
CHANGED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export interface SourceItem {
|
|
2
|
-
name: string;
|
|
3
|
-
version: string;
|
|
4
|
-
registryDependencies: string[];
|
|
5
|
-
files: {
|
|
6
|
-
target: string;
|
|
7
|
-
content: string;
|
|
8
|
-
}[];
|
|
9
|
-
}
|
|
10
|
-
export interface SourceRegistry {
|
|
11
|
-
schemaVersion: 1;
|
|
12
|
-
name: string;
|
|
13
|
-
items: SourceItem[];
|
|
14
|
-
}
|
|
15
|
-
export interface RemoteRegistry {
|
|
16
|
-
source: string;
|
|
17
|
-
integrity: string;
|
|
18
|
-
registry: SourceRegistry;
|
|
19
|
-
}
|
|
20
|
-
/** Declarative source only: no executable recipe callbacks, hooks or destination templates. */
|
|
21
|
-
export declare function validateSourceRegistry(value: unknown): SourceRegistry;
|
|
22
|
-
export declare function loadSourceRegistry(source: string, fetcher?: typeof fetch): Promise<RemoteRegistry>;
|
|
23
|
-
export declare function resolveSourceItems(registry: SourceRegistry, selected: readonly string[]): SourceItem[];
|
|
24
|
-
/** Compile repository-relative source paths into a distributable JSON payload. */
|
|
25
|
-
export declare function buildSourceRegistry(manifestFile: string): SourceRegistry;
|
package/dist/registry/remote.js
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { readBoundedFile } from '../bounded-file.js';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { dependencyOrder } from '../dependency-order.js';
|
|
5
|
-
import { assertSafeTarget } from './receipt.js';
|
|
6
|
-
const identifier = /^[a-z][a-z0-9-]{0,62}$/;
|
|
7
|
-
const limit = 2 * 1024 * 1024;
|
|
8
|
-
/** Declarative source only: no executable recipe callbacks, hooks or destination templates. */
|
|
9
|
-
export function validateSourceRegistry(value) {
|
|
10
|
-
if (!value || typeof value !== 'object')
|
|
11
|
-
throw new Error('Invalid source registry.');
|
|
12
|
-
const registry = value;
|
|
13
|
-
if (registry.schemaVersion !== 1 ||
|
|
14
|
-
typeof registry.name !== 'string' ||
|
|
15
|
-
!identifier.test(registry.name) ||
|
|
16
|
-
!Array.isArray(registry.items) ||
|
|
17
|
-
!registry.items.length ||
|
|
18
|
-
registry.items.length > 100)
|
|
19
|
-
throw new Error('Unsupported source registry schema.');
|
|
20
|
-
const names = new Set();
|
|
21
|
-
const files = new Set();
|
|
22
|
-
for (const item of registry.items) {
|
|
23
|
-
if (!item ||
|
|
24
|
-
typeof item.name !== 'string' ||
|
|
25
|
-
!identifier.test(item.name) ||
|
|
26
|
-
names.has(item.name) ||
|
|
27
|
-
typeof item.version !== 'string' ||
|
|
28
|
-
!/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?$/.test(item.version) ||
|
|
29
|
-
!Array.isArray(item.registryDependencies) ||
|
|
30
|
-
!item.registryDependencies.every((id) => typeof id === 'string' && identifier.test(id)) ||
|
|
31
|
-
!Array.isArray(item.files) ||
|
|
32
|
-
!item.files.length)
|
|
33
|
-
throw new Error('Invalid or duplicate source registry item.');
|
|
34
|
-
names.add(item.name);
|
|
35
|
-
for (const file of item.files) {
|
|
36
|
-
if (!file || typeof file.target !== 'string' || typeof file.content !== 'string')
|
|
37
|
-
throw new Error('Invalid registry file.');
|
|
38
|
-
assertSafeTarget('/registry-validation', file.target);
|
|
39
|
-
if (!/^(?:apps|packages)\/[a-z][a-z0-9-]*\//.test(file.target) ||
|
|
40
|
-
file.target
|
|
41
|
-
.split('/')
|
|
42
|
-
.some((part) => part.startsWith('.') ||
|
|
43
|
-
['node_modules', 'dist', 'release', 'anhedral.registry.json'].includes(part)) ||
|
|
44
|
-
files.has(file.target))
|
|
45
|
-
throw new Error(`Unsafe or duplicate source target: ${file.target}.`);
|
|
46
|
-
files.add(file.target);
|
|
47
|
-
if (path.basename(file.target) === 'package.json') {
|
|
48
|
-
const manifest = JSON.parse(file.content);
|
|
49
|
-
if (!manifest ||
|
|
50
|
-
typeof manifest !== 'object' ||
|
|
51
|
-
Array.isArray(manifest) ||
|
|
52
|
-
manifest.private !== true ||
|
|
53
|
-
typeof manifest.name !== 'string' ||
|
|
54
|
-
!/^@workspace\/[a-z][a-z0-9-]*$/.test(manifest.name))
|
|
55
|
-
throw new Error('Remote workspace manifests must be private @workspace packages.');
|
|
56
|
-
if (manifest.scripts &&
|
|
57
|
-
Object.keys(manifest.scripts).some((key) => /^(?:preinstall|install|postinstall|preprepare|prepare|postprepare|prepublish|prepublishOnly|pnpm:devPreinstall)$/.test(key)))
|
|
58
|
-
throw new Error('Remote installation hooks are unsupported.');
|
|
59
|
-
for (const field of [
|
|
60
|
-
'dependencies',
|
|
61
|
-
'devDependencies',
|
|
62
|
-
'optionalDependencies',
|
|
63
|
-
'peerDependencies',
|
|
64
|
-
]) {
|
|
65
|
-
if (manifest[field] &&
|
|
66
|
-
(typeof manifest[field] !== 'object' ||
|
|
67
|
-
Array.isArray(manifest[field]) ||
|
|
68
|
-
Object.values(manifest[field]).some((version) => typeof version !== 'string' ||
|
|
69
|
-
!/^(?:workspace:[*^~]|[~^]?\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?)$/.test(version))))
|
|
70
|
-
throw new Error('Remote dependencies require versioned npm or workspace references.');
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
dependencyOrder(new Map(registry.items.map((item) => [item.name, item.registryDependencies])), 'Remote registry');
|
|
76
|
-
return structuredClone(registry);
|
|
77
|
-
}
|
|
78
|
-
export async function loadSourceRegistry(source, fetcher = fetch) {
|
|
79
|
-
let bytes;
|
|
80
|
-
if (/^https?:/.test(source)) {
|
|
81
|
-
const url = new URL(source);
|
|
82
|
-
if (url.protocol !== 'https:' ||
|
|
83
|
-
url.username ||
|
|
84
|
-
url.password ||
|
|
85
|
-
url.hash ||
|
|
86
|
-
url.search ||
|
|
87
|
-
!/^[a-z0-9.-]+$/i.test(url.hostname) ||
|
|
88
|
-
url.hostname === 'localhost' ||
|
|
89
|
-
!url.hostname.includes('.') ||
|
|
90
|
-
/^\d+(?:\.\d+){3}$/.test(url.hostname))
|
|
91
|
-
throw new Error('Remote registries require a public HTTPS URL without credentials or query parameters.');
|
|
92
|
-
const response = await fetcher(url, { redirect: 'error', signal: AbortSignal.timeout(15_000) });
|
|
93
|
-
if (!response.ok || !response.body)
|
|
94
|
-
throw new Error('Cannot retrieve source registry.');
|
|
95
|
-
const reader = response.body.getReader();
|
|
96
|
-
const chunks = [];
|
|
97
|
-
let size = 0;
|
|
98
|
-
try {
|
|
99
|
-
while (true) {
|
|
100
|
-
const result = await reader.read();
|
|
101
|
-
if (result.done)
|
|
102
|
-
break;
|
|
103
|
-
size += result.value.byteLength;
|
|
104
|
-
if (size > limit)
|
|
105
|
-
throw new Error('Source registry exceeds 2 MiB.');
|
|
106
|
-
chunks.push(result.value);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
finally {
|
|
110
|
-
await reader.cancel();
|
|
111
|
-
}
|
|
112
|
-
bytes = Buffer.concat(chunks);
|
|
113
|
-
source = url.href;
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
source = path.resolve(source);
|
|
117
|
-
bytes = readBoundedFile(source, limit);
|
|
118
|
-
}
|
|
119
|
-
if (bytes.length > limit)
|
|
120
|
-
throw new Error('Source registry exceeds 2 MiB.');
|
|
121
|
-
return {
|
|
122
|
-
source,
|
|
123
|
-
integrity: `sha256-${createHash('sha256').update(bytes).digest('base64')}`,
|
|
124
|
-
registry: validateSourceRegistry(JSON.parse(bytes.toString('utf8'))),
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
export function resolveSourceItems(registry, selected) {
|
|
128
|
-
const catalog = new Map(registry.items.map((item) => [item.name, item]));
|
|
129
|
-
const graph = new Map();
|
|
130
|
-
const collect = (name) => {
|
|
131
|
-
if (graph.has(name))
|
|
132
|
-
return;
|
|
133
|
-
const item = catalog.get(name);
|
|
134
|
-
if (!item)
|
|
135
|
-
throw new Error(`Unknown remote registry item: ${name}.`);
|
|
136
|
-
graph.set(name, item.registryDependencies);
|
|
137
|
-
item.registryDependencies.forEach(collect);
|
|
138
|
-
};
|
|
139
|
-
selected.forEach(collect);
|
|
140
|
-
return dependencyOrder(graph, 'Remote registry').map((name) => catalog.get(name));
|
|
141
|
-
}
|
|
142
|
-
/** Compile repository-relative source paths into a distributable JSON payload. */
|
|
143
|
-
export function buildSourceRegistry(manifestFile) {
|
|
144
|
-
const root = path.dirname(path.resolve(manifestFile));
|
|
145
|
-
const registry = JSON.parse(readBoundedFile(manifestFile, limit).toString('utf8'));
|
|
146
|
-
if (!Array.isArray(registry.items))
|
|
147
|
-
throw new Error('Invalid source registry manifest.');
|
|
148
|
-
for (const item of registry.items) {
|
|
149
|
-
if (!Array.isArray(item.files))
|
|
150
|
-
throw new Error('Invalid registry file list.');
|
|
151
|
-
for (const file of item.files) {
|
|
152
|
-
if (file.path === undefined)
|
|
153
|
-
continue;
|
|
154
|
-
if (typeof file.path !== 'string' || file.content !== undefined)
|
|
155
|
-
throw new Error('Declare one source path or embedded content.');
|
|
156
|
-
assertSafeTarget(root, file.path);
|
|
157
|
-
if (!/^(?:apps|packages)\/[a-z][a-z0-9-]*\//.test(file.path) ||
|
|
158
|
-
file.path.split('/').some((part) => part.startsWith('.') || part === 'node_modules'))
|
|
159
|
-
throw new Error('Registry source paths must stay in application or package source.');
|
|
160
|
-
const source = path.join(root, file.path);
|
|
161
|
-
file.content = readBoundedFile(source, limit).toString('utf8');
|
|
162
|
-
delete file.path;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
if (Buffer.byteLength(JSON.stringify(registry)) > limit)
|
|
166
|
-
throw new Error('Source registry exceeds 2 MiB.');
|
|
167
|
-
return validateSourceRegistry(registry);
|
|
168
|
-
}
|