anhedral 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +78 -39
  2. package/assets/{anhedral-cli-init-technical.svg → anhedral-stack-technical.svg} +6 -6
  3. package/assets/{anhedral-cli-init.svg → anhedral-stack.svg} +7 -7
  4. package/assets/security/audit-deps.mjs +6 -1
  5. package/dist/bin.js +271 -154
  6. package/dist/bounded-file.d.ts +1 -0
  7. package/dist/bounded-file.js +1 -0
  8. package/dist/capabilities.d.ts +4 -4
  9. package/dist/capabilities.js +1 -1
  10. package/dist/capability-contract.d.ts +47 -0
  11. package/dist/capability-contract.js +136 -0
  12. package/dist/cli-arguments.d.ts +16 -0
  13. package/dist/cli-arguments.js +115 -0
  14. package/dist/cli-output.d.ts +8 -0
  15. package/dist/cli-output.js +211 -0
  16. package/dist/cloudflare.d.ts +7 -2
  17. package/dist/cloudflare.js +166 -36
  18. package/dist/command-schema.d.ts +21 -0
  19. package/dist/command-schema.js +374 -0
  20. package/dist/delivery.d.ts +2 -2
  21. package/dist/delivery.js +21 -4
  22. package/dist/dependencies.d.ts +32 -0
  23. package/dist/dependencies.js +47 -1
  24. package/dist/index.d.ts +23 -1
  25. package/dist/index.js +15 -1
  26. package/dist/inspector-model.d.ts +115 -0
  27. package/dist/inspector-model.js +1 -0
  28. package/dist/lifecycle-model.d.ts +15 -0
  29. package/dist/lifecycle-model.js +20 -0
  30. package/dist/lifecycle.d.ts +12 -0
  31. package/dist/lifecycle.js +92 -0
  32. package/dist/mobile-delivery.d.ts +100 -0
  33. package/dist/mobile-delivery.js +319 -0
  34. package/dist/platforms/desktop.js +4 -5
  35. package/dist/platforms/extension.js +9 -73
  36. package/dist/platforms/mobile.js +14 -52
  37. package/dist/project-inspector.d.ts +4 -0
  38. package/dist/project-inspector.js +646 -0
  39. package/dist/project-state.d.ts +17 -0
  40. package/dist/project-state.js +239 -11
  41. package/dist/provider-auth.d.ts +41 -0
  42. package/dist/provider-auth.js +210 -0
  43. package/dist/providers/adapters.d.ts +17 -0
  44. package/dist/providers/adapters.js +269 -0
  45. package/dist/providers/api.d.ts +28 -0
  46. package/dist/providers/api.js +252 -0
  47. package/dist/providers/config.d.ts +15 -0
  48. package/dist/providers/config.js +184 -0
  49. package/dist/providers/definitions.d.ts +318 -0
  50. package/dist/providers/definitions.js +127 -0
  51. package/dist/providers/provision.d.ts +24 -0
  52. package/dist/providers/provision.js +76 -0
  53. package/dist/providers/receipts.d.ts +20 -0
  54. package/dist/providers/receipts.js +66 -0
  55. package/dist/providers/recovery-verification.d.ts +7 -0
  56. package/dist/providers/recovery-verification.js +112 -0
  57. package/dist/providers/recovery.d.ts +30 -0
  58. package/dist/providers/recovery.js +34 -0
  59. package/dist/providers/runtime.d.ts +10 -0
  60. package/dist/providers/runtime.js +231 -0
  61. package/dist/providers/schema.d.ts +28 -0
  62. package/dist/providers/schema.js +49 -0
  63. package/dist/providers/state.d.ts +56 -0
  64. package/dist/providers/state.js +329 -0
  65. package/dist/provision.d.ts +1 -1
  66. package/dist/provision.js +61 -3
  67. package/dist/readiness.js +11 -3
  68. package/dist/registry/add.d.ts +6 -0
  69. package/dist/registry/add.js +35 -4
  70. package/dist/registry/commands.d.ts +3 -2
  71. package/dist/registry/commands.js +53 -14
  72. package/dist/registry/options.js +3 -2
  73. package/dist/registry/preview.d.ts +11 -0
  74. package/dist/registry/preview.js +91 -0
  75. package/dist/registry/receipt.js +1 -0
  76. package/dist/registry/resolve.js +19 -13
  77. package/dist/registry/types.d.ts +1 -0
  78. package/dist/registry/usage.d.ts +10 -0
  79. package/dist/registry/usage.js +22 -37
  80. package/dist/render.d.ts +2 -0
  81. package/dist/render.js +10 -0
  82. package/dist/scaffold/api.js +20 -3
  83. package/dist/scaffold/auth.js +68 -28
  84. package/dist/scaffold/bootstrap.d.ts +2 -0
  85. package/dist/scaffold/bootstrap.js +9 -0
  86. package/dist/scaffold/database.js +12 -6
  87. package/dist/scaffold/documentation.js +11 -8
  88. package/dist/scaffold/environment.d.ts +2 -0
  89. package/dist/scaffold/environment.js +18 -1
  90. package/dist/scaffold/files.d.ts +1 -0
  91. package/dist/scaffold/files.js +15 -4
  92. package/dist/scaffold/services.js +37 -13
  93. package/dist/scaffold/stack-documentation.d.ts +3 -0
  94. package/dist/scaffold/stack-documentation.js +52 -0
  95. package/dist/scaffold/styling.d.ts +3 -0
  96. package/dist/scaffold/styling.js +84 -0
  97. package/dist/scaffold/web.js +31 -11
  98. package/dist/scaffold/workspace.js +49 -22
  99. package/dist/ship.d.ts +320 -0
  100. package/dist/ship.js +211 -0
  101. package/dist/stack-registry.d.ts +2 -2
  102. package/dist/stack-registry.js +44 -11
  103. package/dist/standard.d.ts +2 -1
  104. package/dist/standard.js +13 -12
  105. package/dist/vercel.d.ts +3 -3
  106. package/dist/vercel.js +27 -1
  107. package/dist/version.d.ts +1 -1
  108. package/dist/version.js +1 -1
  109. package/dist/vinext.js +9 -18
  110. package/dist/workspace-graph.js +3 -2
  111. package/docs/application-stack-standard.md +51 -80
  112. package/docs/cli-design.md +67 -0
  113. package/docs/delivery-workflow.md +29 -15
  114. package/docs/plugin-ui.md +61 -0
  115. package/docs/provider-configuration.md +117 -0
  116. package/package.json +16 -12
  117. package/dist/managed-hosting.d.ts +0 -49
  118. package/dist/managed-hosting.js +0 -373
  119. package/docs/managed-hosting-architecture.md +0 -87
  120. package/docs/sponsorships.md +0 -25
@@ -1,5 +1,6 @@
1
+ import { readBoundedFile, MAX_PROJECT_JSON_BYTES } from '../bounded-file.js';
1
2
  import { configureSecurityBackports } from '../security-backports.js';
2
- import { bootstrapWorkspace } from '../scaffold/bootstrap.js';
3
+ import { bootstrapWorkspace, assertWorkspaceRuntime } from '../scaffold/bootstrap.js';
3
4
  import { registerApplication } from '../project-state.js';
4
5
  import { registerVercelApplication } from '../vercel.js';
5
6
  import { existsSync, readFileSync, realpathSync } from 'node:fs';
@@ -17,11 +18,22 @@ import { writeStandardRoot } from '../scaffold/workspace.js';
17
18
  import { createRecipeContext } from './context.js';
18
19
  import { resolveStackRegistry, resolveStandardProducts } from './resolve.js';
19
20
  import { sourceFiles } from './receipt.js';
21
+ import { sourcePreview, validatePreviewPath, MAX_PREVIEW_TOTAL_BYTES, } from './preview.js';
20
22
  /** Add recipes to an initialized workspace, refusing edits to customer-owned source. */
21
23
  export async function addStackItems(root, request, run = execFile) {
22
24
  for (const key of Object.keys(request))
23
- if (!['items', 'dryRun', 'skipInstall'].includes(key))
25
+ if (!['items', 'dryRun', 'skipInstall', 'preview'].includes(key))
24
26
  throw new Error(`Unknown add option: ${key}. Only bundled registry items are supported.`);
27
+ if (request.preview) {
28
+ if (!request.dryRun ||
29
+ !['diff', 'view'].includes(request.preview.mode) ||
30
+ Object.keys(request.preview).some((key) => !['mode', 'path'].includes(key)))
31
+ throw new Error('Source previews require dry-run and one mode: diff or view.');
32
+ if (request.preview.path !== undefined)
33
+ validatePreviewPath(request.preview.path);
34
+ }
35
+ if (!request.dryRun)
36
+ assertWorkspaceRuntime(run);
25
37
  root = realpathSync(root);
26
38
  if (!request.items.length)
27
39
  throw new Error('Select at least one registry item.');
@@ -62,7 +74,7 @@ export async function addStackItems(root, request, run = execFile) {
62
74
  throw new Error(`Adding this selection relocates ${item.name}; migrate its existing source explicitly.`);
63
75
  }
64
76
  const before = sourceFiles(root);
65
- const receiptText = readFileSync(path.join(root, 'anhedral.registry.json'), 'utf8');
77
+ const receiptText = readBoundedFile(path.join(root, 'anhedral.registry.json'), MAX_PROJECT_JSON_BYTES).toString('utf8');
66
78
  const commitPaths = [];
67
79
  const conflicts = [];
68
80
  const result = {
@@ -78,11 +90,14 @@ export async function addStackItems(root, request, run = execFile) {
78
90
  targets: recipes.flatMap((item) => item.targets),
79
91
  files: commitPaths,
80
92
  conflicts,
93
+ ...(request.preview ? { previews: [] } : {}),
81
94
  };
82
95
  // The upstream web generator requires network access and executes package scripts.
83
96
  // A preview reports the resolved selection without invoking that generator.
84
97
  if (request.dryRun && result.bootstrapRequired) {
85
98
  result.filesChecked = false;
99
+ if (request.preview)
100
+ throw new Error('Source preview requires a new web bootstrap and cannot run offline. Use add --dry-run for the selection plan.');
86
101
  return result;
87
102
  }
88
103
  await runStagedTransaction(root, {
@@ -149,9 +164,25 @@ export async function addStackItems(root, request, run = execFile) {
149
164
  files,
150
165
  });
151
166
  commitPaths.push('anhedral.registry.json');
167
+ if (request.preview && result.previews) {
168
+ const preview = request.preview;
169
+ const selected = [...commitPaths]
170
+ .filter((file) => !preview.path || preview.path === file)
171
+ .sort();
172
+ if (preview.path && !selected.length)
173
+ throw new Error('Requested path is not a changed generated source file.');
174
+ let total = 0;
175
+ for (const file of selected) {
176
+ const content = sourcePreview(root, stage, file, preview.mode);
177
+ total += Buffer.byteLength(JSON.stringify(content));
178
+ if (total > MAX_PREVIEW_TOTAL_BYTES)
179
+ throw new Error('Source preview exceeds 1 MiB. Select a single file with --diff <path> or --view <path>.');
180
+ result.previews.push(content);
181
+ }
182
+ }
152
183
  // Detect concurrent changes even to source that this addition would otherwise preserve.
153
184
  if (!isDeepStrictEqual(sourceFiles(root), before) ||
154
- readFileSync(path.join(root, 'anhedral.registry.json'), 'utf8') !== receiptText)
185
+ readBoundedFile(path.join(root, 'anhedral.registry.json'), MAX_PROJECT_JSON_BYTES).toString('utf8') !== receiptText)
155
186
  throw new Error('Workspace changed during addition; retry after reviewing source.');
156
187
  },
157
188
  afterCommit: () => {
@@ -1,2 +1,3 @@
1
- export declare function runRegistry(args: string[], json: boolean): Promise<void>;
2
- export declare function runAdd(args: string[]): Promise<void>;
1
+ import { type OutputOptions } from '../cli-output.js';
2
+ export declare function runRegistry(args: string[], json: boolean, output?: OutputOptions): Promise<void>;
3
+ export declare function runAdd(args: string[], output?: OutputOptions): Promise<void>;
@@ -1,16 +1,28 @@
1
1
  import path from 'node:path';
2
+ import { CAPABILITY_CONTRACT, formatCapabilityContract } from '../capability-contract.js';
2
3
  import { addStackItems } from './add.js';
3
- import { STACK_REGISTRY, getStackItem } from '../stack-registry.js';
4
- export async function runRegistry(args, json) {
4
+ import { STACK_REGISTRY, STANDARD_PRODUCTS, getStackItem } from '../stack-registry.js';
5
+ import { printResult } from '../cli-output.js';
6
+ import { commandHelp } from './usage.js';
7
+ import { CliArgumentError, suggest } from '../cli-arguments.js';
8
+ export async function runRegistry(args, json, output = { json }) {
5
9
  const positional = args.filter((arg) => arg !== '--json');
6
10
  if (positional.includes('--help') || positional.includes('-h')) {
7
- console.log('anhedral registry list|view [item] [--json]');
11
+ console.log(commandHelp('registry'));
8
12
  return;
9
13
  }
10
14
  if (positional.some((arg) => arg.startsWith('-')))
11
15
  throw new Error('Unknown registry option.');
12
16
  const [action = 'list', name, ...extra] = positional;
17
+ if (action === 'capabilities' && !name && !extra.length) {
18
+ if (output.quiet && !json)
19
+ return;
20
+ console.log(json ? JSON.stringify(CAPABILITY_CONTRACT, null, 2) : formatCapabilityContract());
21
+ return;
22
+ }
13
23
  if (action === 'list' && !name && !extra.length) {
24
+ if (output.quiet && !json)
25
+ return;
14
26
  console.log(json
15
27
  ? JSON.stringify(STACK_REGISTRY, null, 2)
16
28
  : STACK_REGISTRY.items
@@ -19,30 +31,57 @@ export async function runRegistry(args, json) {
19
31
  return;
20
32
  }
21
33
  if (action === 'view' && name && !extra.length) {
22
- console.log(JSON.stringify(getStackItem(name), null, 2));
34
+ if (!STACK_REGISTRY.items.some((item) => item.name === name)) {
35
+ const correction = suggest(name, STACK_REGISTRY.items.map((item) => item.name));
36
+ throw new CliArgumentError(`Unknown bundled registry item.${correction ? ` Did you mean ${correction}?` : ''}`, 'Run anhedral registry list.');
37
+ }
38
+ printResult('registry', getStackItem(name), { ...output, json });
23
39
  return;
24
40
  }
25
- throw new Error('Use registry list or registry view <item>.');
41
+ throw new Error('Use registry list, registry view <item>, or registry capabilities.');
26
42
  }
27
- export async function runAdd(args) {
43
+ export async function runAdd(args, output = {}) {
28
44
  if (args.includes('--help') || args.includes('-h')) {
29
- console.log('anhedral add <directory> <items...> [--dry-run] [--skip-install] [--json]');
45
+ console.log(commandHelp('add'));
30
46
  return;
31
47
  }
32
48
  const positional = [];
33
- for (const arg of args) {
49
+ let preview;
50
+ for (let index = 0; index < args.length; index++) {
51
+ const arg = args[index];
52
+ if (arg === '--diff' || arg === '--view') {
53
+ if (preview)
54
+ throw new Error('Select one source preview: --diff or --view.');
55
+ preview = { mode: arg === '--diff' ? 'diff' : 'view' };
56
+ const next = args[index + 1];
57
+ if (next &&
58
+ !next.startsWith('-') &&
59
+ !STANDARD_PRODUCTS.includes(next)) {
60
+ preview.path = next;
61
+ index++;
62
+ }
63
+ continue;
64
+ }
34
65
  if (['--dry-run', '--skip-install', '--json'].includes(arg))
35
66
  continue;
36
67
  if (arg.startsWith('-'))
37
68
  throw new Error(`Unknown add option: ${arg}.`);
38
69
  positional.push(arg);
39
70
  }
40
- const [directory, ...items] = positional;
41
- if (!directory || !items.length)
42
- throw new Error('Use add <directory> <items...>.');
43
- console.log(JSON.stringify(await addStackItems(path.resolve(directory), {
71
+ const hasDirectory = positional[0] && !STANDARD_PRODUCTS.includes(positional[0]);
72
+ if (hasDirectory && positional.length === 1) {
73
+ const correction = suggest(positional[0], STANDARD_PRODUCTS);
74
+ if (correction)
75
+ throw new CliArgumentError(`Unknown stack part. Did you mean ${correction}?`, 'Run anhedral registry list.');
76
+ }
77
+ const directory = hasDirectory ? positional.shift() : process.cwd();
78
+ const items = positional;
79
+ if (!items.length)
80
+ throw new Error('Select bundled parts: anhedral add <items...> [--cwd <directory>].');
81
+ printResult('add', await addStackItems(path.resolve(directory), {
44
82
  items,
45
- dryRun: args.includes('--dry-run'),
83
+ dryRun: args.includes('--dry-run') || Boolean(preview),
46
84
  skipInstall: args.includes('--skip-install'),
47
- }), null, 2));
85
+ ...(preview ? { preview } : {}),
86
+ }), { ...output, json: args.includes('--json') });
48
87
  }
@@ -46,7 +46,8 @@ function parseStandardFlags(remaining) {
46
46
  }
47
47
  export function parseStandardOptions(command, args) {
48
48
  const remaining = [...args];
49
- const directory = command === 'new' ? remaining.shift() : process.cwd();
49
+ const hasDirectory = remaining[0] && !remaining[0].startsWith('-');
50
+ const directory = command === 'new' || hasDirectory ? remaining.shift() : process.cwd();
50
51
  if (!directory || directory.startsWith('-'))
51
52
  throw new Error('new requires a destination directory.');
52
53
  const root = path.resolve(directory);
@@ -60,6 +61,6 @@ export function parseStandardOptions(command, args) {
60
61
  skipInstall: remaining.includes('--skip-install'),
61
62
  dryRun: remaining.includes('--dry-run'),
62
63
  json: remaining.includes('--json'),
63
- initializeGit: gitSpecified ?? command === 'new',
64
+ initializeGit: gitSpecified ?? (command === 'new' || Boolean(hasDirectory)),
64
65
  };
65
66
  }
@@ -0,0 +1,11 @@
1
+ export declare const MAX_PREVIEW_TOTAL_BYTES: number;
2
+ export interface SourcePreview {
3
+ path: string;
4
+ content?: string;
5
+ diff?: string;
6
+ unavailable?: string;
7
+ }
8
+ export declare function validatePreviewPath(file: string): void;
9
+ /** A single contextual hunk is linear in file size and remains an applicable unified patch. */
10
+ export declare function sourceDiff(file: string, before: string | undefined, after: string): string;
11
+ export declare function sourcePreview(root: string, stage: string, file: string, mode: 'diff' | 'view'): SourcePreview;
@@ -0,0 +1,91 @@
1
+ import path from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { readBoundedFile } from '../bounded-file.js';
4
+ const MAX_PREVIEW_FILE_BYTES = 256 * 1024;
5
+ export const MAX_PREVIEW_TOTAL_BYTES = 1024 * 1024;
6
+ export function validatePreviewPath(file) {
7
+ if (!file ||
8
+ path.posix.isAbsolute(file) ||
9
+ file.includes('\\') ||
10
+ /[\x00-\x1f\x7f]/.test(file) ||
11
+ file
12
+ .split('/')
13
+ .some((part) => !part ||
14
+ ['.', '..', '.anhedral'].includes(part) ||
15
+ (/^(?:\.env|\.dev\.vars)(?:\.|$)/.test(part) && !part.endsWith('.example'))))
16
+ throw new Error('Select a relative generated source path; private environment and state files cannot be previewed.');
17
+ }
18
+ /** A single contextual hunk is linear in file size and remains an applicable unified patch. */
19
+ export function sourceDiff(file, before, after) {
20
+ const lines = (content) => content === '' ? [] : content.split('\n').slice(0, content.endsWith('\n') ? -1 : undefined);
21
+ const oldLines = lines(before ?? '');
22
+ const newLines = lines(after);
23
+ if (before === after)
24
+ return '';
25
+ let prefix = 0;
26
+ while (prefix < oldLines.length &&
27
+ prefix < newLines.length &&
28
+ oldLines[prefix] === newLines[prefix])
29
+ prefix++;
30
+ // Newline-only changes must still include the final line in the patch.
31
+ if (prefix && oldLines.length === newLines.length && prefix === oldLines.length)
32
+ prefix--;
33
+ let suffix = 0;
34
+ while ((before ?? '').endsWith('\n') === after.endsWith('\n') &&
35
+ suffix < oldLines.length - prefix &&
36
+ suffix < newLines.length - prefix &&
37
+ oldLines.at(-suffix - 1) === newLines.at(-suffix - 1))
38
+ suffix++;
39
+ const start = Math.max(0, prefix - 3);
40
+ const oldEnd = Math.min(oldLines.length, oldLines.length - suffix + 3);
41
+ const newEnd = Math.min(newLines.length, newLines.length - suffix + 3);
42
+ const output = [
43
+ `diff --git a/${file} b/${file}`,
44
+ ...(before === undefined ? ['new file mode 100644'] : []),
45
+ `--- ${before === undefined ? '/dev/null' : `a/${file}`}`,
46
+ `+++ b/${file}`,
47
+ `@@ -${oldEnd - start ? start + 1 : start},${oldEnd - start} +${newEnd - start ? start + 1 : start},${newEnd - start} @@`,
48
+ ];
49
+ if (before === undefined && !after)
50
+ return output.slice(0, 2).join('\n') + '\n';
51
+ const append = (prefix, line, last, content) => {
52
+ output.push(`${prefix}${line}`);
53
+ if (last && !content.endsWith('\n'))
54
+ output.push('\');
55
+ };
56
+ for (let index = start; index < prefix; index++)
57
+ output.push(` ${oldLines[index]}`);
58
+ for (let index = prefix; index < oldLines.length - suffix; index++)
59
+ append('-', oldLines[index], index === oldLines.length - 1, before ?? '');
60
+ for (let index = prefix; index < newLines.length - suffix; index++)
61
+ append('+', newLines[index], index === newLines.length - 1, after);
62
+ for (let index = oldLines.length - suffix; index < oldEnd; index++)
63
+ append(' ', oldLines[index], index === oldLines.length - 1, after);
64
+ return output.join('\n') + '\n';
65
+ }
66
+ export function sourcePreview(root, stage, file, mode) {
67
+ validatePreviewPath(file);
68
+ const decode = (directory) => new TextDecoder('utf-8', { fatal: true }).decode(readBoundedFile(path.join(directory, file), MAX_PREVIEW_FILE_BYTES));
69
+ let after;
70
+ let before;
71
+ try {
72
+ after = decode(stage);
73
+ if (after.includes('\x00'))
74
+ throw new Error('Binary file');
75
+ if (mode === 'diff' && existsSync(path.join(root, file))) {
76
+ before = decode(root);
77
+ if (before.includes('\x00'))
78
+ throw new Error('Binary file');
79
+ }
80
+ }
81
+ catch {
82
+ return {
83
+ path: file,
84
+ unavailable: 'Binary, nonregular or oversized file; source preview is unavailable (256 KiB limit).',
85
+ };
86
+ }
87
+ return {
88
+ path: file,
89
+ ...(mode === 'view' ? { content: after } : { diff: sourceDiff(file, before, after) }),
90
+ };
91
+ }
@@ -16,6 +16,7 @@ const excluded = new Set([
16
16
  '.expo',
17
17
  '.fallow',
18
18
  '.anhedral',
19
+ '.anhedral-audit-report.json',
19
20
  '.anhedral.lock',
20
21
  '.anhedral-txn',
21
22
  'anhedral.registry.json',
@@ -50,6 +50,23 @@ export function resolveStandardProducts(selected, hosting, hostingSpecified) {
50
50
  throw new Error('--hosting applies to the web application.');
51
51
  resolveStackRegistry({ products: [...selected], hosting });
52
52
  }
53
+ function itemDependencies(name, context) {
54
+ const definition = getStackDefinition(name);
55
+ const dependencies = new Set(definition.registryDependencies);
56
+ if (definition.runtime) {
57
+ if (!hasServer(context) && definition.runtime !== 'optional-worker')
58
+ throw new Error(`${name} requires a server runtime.`);
59
+ if (needsWorker(name, context) &&
60
+ !context.has('hono') &&
61
+ !(context.options.hosting === 'cloudflare' && (context.has('vinext') || context.has('next'))))
62
+ throw new Error(`${name} requires a Cloudflare runtime.`);
63
+ if (hasServer(context))
64
+ dependencies.add(context.has('hono') ? 'hono' : context.has('vinext') ? 'vinext' : 'next');
65
+ }
66
+ for (const dependency of definition.resolveDependencies?.(context) ?? [])
67
+ dependencies.add(dependency);
68
+ return dependencies;
69
+ }
53
70
  export function resolveStackRegistry(options) {
54
71
  const selected = new Set(options.products);
55
72
  validateProducts(selected, options.hosting);
@@ -62,20 +79,9 @@ export function resolveStackRegistry(options) {
62
79
  return;
63
80
  const definition = getStackDefinition(name);
64
81
  const metadata = getStackItem(name);
65
- const dependencies = new Set(definition.registryDependencies);
66
- if (definition.runtime) {
67
- if (!hasServer(context) && definition.runtime !== 'optional-worker')
68
- throw new Error(`${name} requires a server runtime.`);
69
- if (needsWorker(name, context) &&
70
- !context.has('hono') &&
71
- !(options.hosting === 'cloudflare' && (context.has('vinext') || context.has('next'))))
72
- throw new Error(`${name} requires a Cloudflare runtime.`);
73
- if (hasServer(context))
74
- dependencies.add(context.has('hono') ? 'hono' : context.has('vinext') ? 'vinext' : 'next');
75
- }
76
- for (const dependency of definition.resolveDependencies?.(context) ?? [])
77
- dependencies.add(dependency);
82
+ const dependencies = itemDependencies(name, context);
78
83
  metadata.registryDependencies = [...dependencies];
84
+ metadata.environment = definition.resolveEnvironment?.(context) ?? metadata.environment;
79
85
  metadata.targets = definition.resolveTargets?.(context) ?? metadata.targets;
80
86
  if (definition.configure)
81
87
  metadata.bindings = definition.configure(context);
@@ -57,6 +57,7 @@ export interface StackRegistryItem {
57
57
  export interface ItemDefinition extends Omit<StackRegistryItem, 'bindings'> {
58
58
  install?: Recipe;
59
59
  configure?: (context: RecipeContext) => Json;
60
+ resolveEnvironment?: (context: RecipeContext) => string[];
60
61
  resolveTargets?: (context: RecipeContext) => string[];
61
62
  resolvePackage?: (context: RecipeContext, recipe: PackageRecipe) => PackageRecipe;
62
63
  resolveDependencies?: (context: RecipeContext) => string[];
@@ -1 +1,11 @@
1
+ export declare const COMMAND_HELP: {
2
+ [k: string]: {
3
+ options: string[];
4
+ description: string;
5
+ usage: string;
6
+ examples: readonly string[];
7
+ note?: string;
8
+ };
9
+ };
10
+ export declare function commandHelp(command: string): string | undefined;
1
11
  export declare const STANDARD_USAGE: string;
@@ -1,37 +1,22 @@
1
- import { STANDARD_PRODUCTS } from '../stack-registry.js';
2
- export const STANDARD_USAGE = `Anhedral Application Stack & Delivery Standard
3
-
4
- anhedral new <directory> [products...] [--hosting cloudflare|vercel] [--skip-install] [--no-git] [--dry-run] [--json]
5
- anhedral add <directory> <items...> [--dry-run] [--skip-install] [--json]
6
- anhedral init [products...] [--hosting cloudflare|vercel] [--skip-install] [--git] [--dry-run] [--json]
7
-
8
- Default: Vinext on Cloudflare Workers, from the official shadcn pnpm/Turborepo monorepo.
9
- Web hosting: Vinext on Cloudflare by default; native Next.js on Vercel is an explicit option.
10
- Products: ${STANDARD_PRODUCTS.join(', ')}
11
- Examples:
12
- anhedral new client-app
13
- anhedral new mobile-app --expo
14
- anhedral new product --next --expo --hono --neon --clerk --r2
15
- anhedral new website --next --stripe --resend
16
- anhedral init --wxt
17
- anhedral cloud register|stage|save|deploy|status [directory] (managed service API required)
18
- anhedral registry list|view [item] [--json]
19
- anhedral doctor [directory] [--providers] --json
20
- anhedral repository [directory] [--owner <github-account>] [--name <repo>] [--visibility private|public] [--create]
21
- anhedral register [directory]
22
- anhedral connect [directory] [--scope <team>] --audience public|private --use personal|commercial [--create] [--git]
23
- anhedral connect [directory] --provider cloudflare [--account <id>] --audience public|private [--allow-email <email>]
24
- anhedral provision [directory] [--dry-run] [--json]
25
- anhedral ship [directory] [--dry-run] [--json]
26
- anhedral adopt [directory] --provider vercel [--json]
27
- anhedral recover [directory] [--provider vercel|cloudflare] [--json]
28
- anhedral status [directory] [--refresh] [--json]
29
- anhedral manage [directory] [--local] [--json]
30
- anhedral verify [directory] [--json]
31
- anhedral rollback [directory] --release <id> --data-compatible [--json]
32
-
33
- --skip-install skips the final workspace install; the official shadcn bootstrap
34
- still requires network access and may install its own dependencies.
35
- --all is intentionally unsupported: select only required capabilities.
36
- Vercel Hobby is personal/non-commercial with limits; commercial apps need a suitable existing plan.
37
- `;
1
+ import { COMMANDS, GLOBAL_OPTIONS, optionHelp } from '../command-schema.js';
2
+ // Preserve the presentation API for consumers; all option text comes from typed definitions.
3
+ export const COMMAND_HELP = Object.fromEntries(Object.entries(COMMANDS).map(([name, entry]) => [
4
+ name,
5
+ { ...entry, options: entry.options.filter((option) => !option.hidden).map(optionHelp) },
6
+ ]));
7
+ const primary = ['init', 'auth', 'ship', 'add', 'registry', 'inspect', 'doctor', 'connect'];
8
+ const common = GLOBAL_OPTIONS.map(optionHelp);
9
+ export function commandHelp(command) {
10
+ if (command === 'new')
11
+ return commandHelp('init')
12
+ ?.replace('Usage: anhedral init', 'Usage: anhedral new')
13
+ .concat('\nCompatibility alias: new requires a destination directory.\n');
14
+ const entry = COMMAND_HELP[command];
15
+ if (!entry)
16
+ return undefined;
17
+ return `${entry.description}\n\nUsage: anhedral ${entry.usage}\n\nOptions:\n${[...entry.options, ...common].map((option) => ` ${option}`).join('\n')}\n\nExamples:\n${entry.examples.map((example) => ` anhedral ${example}`).join('\n')}${entry.note ? `\n\n${entry.note}` : ''}\n`;
18
+ }
19
+ export const STANDARD_USAGE = `Anhedral\nScaffold source, authenticate your providers, and ship your selected stack.\n\nUsage: anhedral <command> [options]\n\nStart here:\n${primary.map((name) => ` ${name.padEnd(12)}${COMMAND_HELP[name].description}`).join('\n')}\n\nDelivery tools:\n${Object.entries(COMMAND_HELP)
20
+ .filter(([name]) => !primary.includes(name))
21
+ .map(([name, entry]) => ` ${name.padEnd(12)}${entry.description}`)
22
+ .join('\n')}\n\nDefault: Vinext on Cloudflare Workers, from the official shadcn pnpm/Turborepo monorepo.\nExamples:\n anhedral init client-app\n anhedral auth client-app\n anhedral ship client-app --dry-run\n\nUse anhedral <command> --help for options and examples.\nUse anhedral help <command> for the same focused help.\nGlobal options:\n${common.map((option) => ` ${option}`).join('\n')}\n -v, --version Show the CLI version.\n\nnew remains a compatibility alias for init with an explicit destination.\n`;
package/dist/render.d.ts CHANGED
@@ -8,3 +8,5 @@ export declare function markdownHeading(value: string): string;
8
8
  export declare function packageNameFromText(value: string): string;
9
9
  export declare function childPackageName(projectName: string, suffix: string): string;
10
10
  export declare function identifierSegment(value: string): string;
11
+ /** Keep provider resource names within the shared Workers/R2/Queues naming limits. */
12
+ export declare function resourceName(projectName: string, suffix: string): string;
package/dist/render.js CHANGED
@@ -72,3 +72,13 @@ export function identifierSegment(value) {
72
72
  .replace(/^-+|-+$/g, '');
73
73
  return segment || 'app';
74
74
  }
75
+ /** Keep provider resource names within the shared Workers/R2/Queues naming limits. */
76
+ export function resourceName(projectName, suffix) {
77
+ const segment = identifierSegment(projectName);
78
+ const qualifier = identifierSegment(suffix);
79
+ if (qualifier.length > 40)
80
+ throw new Error('Resource suffix is too long.');
81
+ const digest = createHash('sha256').update(projectName).digest('hex').slice(0, 8);
82
+ const base = segment === projectName ? segment : `${segment}-${digest}`;
83
+ return `${compactPackageSegment(base, 62 - qualifier.length)}-${qualifier}`;
84
+ }
@@ -1,10 +1,27 @@
1
+ import { resourceName, jsString } from '../render.js';
1
2
  import { STACK_DEPENDENCIES } from '../dependencies.js';
2
3
  import { workerConfig, worker } from './files.js';
3
4
  export function scaffoldApi({ root, options, bindings }) {
4
- worker(root, 'api', { ...workerConfig(`${options.name}-api`), ...bindings }, `import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi';
5
+ worker(root, 'api', { ...workerConfig(resourceName(options.name, 'api')), ...bindings }, `import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi';
6
+
5
7
  const app = new OpenAPIHono<{ Bindings: Env }>();
6
- app.openapi(createRoute({ method: 'get', path: '/health', responses: { 200: { description: 'Healthy', content: { 'application/json': { schema: z.object({ ok: z.literal(true) }) } } } } }), (c) => c.json({ ok: true as const }, 200));
7
- app.doc('/openapi.json', { openapi: '3.0.0', info: { title: '${options.name}', version: '1.0.0' } });
8
+ const health = createRoute({
9
+ method: 'get',
10
+ path: '/health',
11
+ responses: {
12
+ 200: {
13
+ description: 'Healthy',
14
+ content: { 'application/json': { schema: z.object({ ok: z.literal(true) }) } },
15
+ },
16
+ },
17
+ });
18
+
19
+ app.openapi(health, (c) => c.json({ ok: true as const }, 200));
20
+ app.doc('/openapi.json', {
21
+ openapi: '3.0.0',
22
+ info: { title: ${jsString(options.name)}, version: '1.0.0' },
23
+ });
24
+
8
25
  export default app;
9
26
  `, {
10
27
  hono: STACK_DEPENDENCIES['hono'],