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 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 items and versioned source registries from local files or HTTPS. Additions refuse to overwrite edited or untracked source; runtime relocation requires a deliberate migration.
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 --registry https://example.com/registry.json --json
64
- anhedral add my-app feature --registry https://example.com/registry.json --dry-run --json
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`. For external registries, pass the returned SHA-256 value with `--integrity` to require the reviewed payload. `--skip-install` still resolves the lockfile. Addition installs disable dependency lifecycle scripts; enable only reviewed native build scripts separately when required. Projects must have matching CLI versions and scaffold fingerprints; older projects need a deliberate migration. Existing provider identities stay intact, and no provider resources are created.
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
- : 'UNKNOWN_COMMAND';
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, remoteItems, resourcesProvisioned, ...snapshot } = installed;
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 {
@@ -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
- remoteItems: string[];
13
+ bootstrapRequired: boolean;
14
+ filesChecked: boolean;
15
+ targets: string[];
16
+ files: string[];
17
+ conflicts: string[];
22
18
  }>;
@@ -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, assertSafeTarget } from './receipt.js';
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
- let remote;
38
- if (request.registry)
39
- remote = await loadSourceRegistry(request.registry);
40
- else
41
- for (const id of request.items) {
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
- remoteItems: remoteItems.map((item) => item.name),
96
- ...(remote ? { registry: { source: remote.source, integrity: remote.integrity } } : {}),
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', '--ignore-scripts'], root);
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...> [--registry <file|https-url>] [--integrity sha256-...] [--dry-run] [--skip-install] [--json]');
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 (let index = 0; index < args.length; index++) {
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 === '--integrity') {
75
- if (integrity || !args[index + 1] || !/^sha256-[A-Za-z0-9+/]{43}=$/.test(args[index + 1]))
76
- throw new Error('Specify one SHA-256 registry integrity.');
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));
@@ -1,3 +1,2 @@
1
1
  export declare function sourceFiles(root: string): Map<string, string>;
2
2
  export declare function recordScaffoldFiles(root: string): void;
3
- export declare function assertSafeTarget(root: string, relative: string): void;
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
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
- }
@@ -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...> [--registry <file|https-url>] [--integrity sha256-...] [--dry-run] [--skip-install] [--json]
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 build <manifest> --output <registry.json>
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
- const defaultRunner = (command, args, cwd) => execFile(command, args, cwd);
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.9.0";
1
+ export declare const GENERATOR_VERSION = "0.10.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const GENERATOR_VERSION = '0.9.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. Use `--registry <file|https-url>` for reviewed declarative registries. Older projects and runtime relocation require explicit migration. `doctor --providers --json` checks access without configuring infrastructure or certifying production.
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,6 +1,6 @@
1
1
  {
2
2
  "name": "anhedral",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Build and deliver applications with Cloudflare-first Vinext, selected services, optional Next.js on Vercel, and verified release evidence.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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;
@@ -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
- }