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.
- package/README.md +78 -39
- package/assets/{anhedral-cli-init-technical.svg → anhedral-stack-technical.svg} +6 -6
- package/assets/{anhedral-cli-init.svg → anhedral-stack.svg} +7 -7
- package/assets/security/audit-deps.mjs +6 -1
- package/dist/bin.js +271 -154
- package/dist/bounded-file.d.ts +1 -0
- package/dist/bounded-file.js +1 -0
- package/dist/capabilities.d.ts +4 -4
- package/dist/capabilities.js +1 -1
- package/dist/capability-contract.d.ts +47 -0
- package/dist/capability-contract.js +136 -0
- package/dist/cli-arguments.d.ts +16 -0
- package/dist/cli-arguments.js +115 -0
- package/dist/cli-output.d.ts +8 -0
- package/dist/cli-output.js +211 -0
- package/dist/cloudflare.d.ts +7 -2
- package/dist/cloudflare.js +166 -36
- package/dist/command-schema.d.ts +21 -0
- package/dist/command-schema.js +374 -0
- package/dist/delivery.d.ts +2 -2
- package/dist/delivery.js +21 -4
- package/dist/dependencies.d.ts +32 -0
- package/dist/dependencies.js +47 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.js +15 -1
- package/dist/inspector-model.d.ts +115 -0
- package/dist/inspector-model.js +1 -0
- package/dist/lifecycle-model.d.ts +15 -0
- package/dist/lifecycle-model.js +20 -0
- package/dist/lifecycle.d.ts +12 -0
- package/dist/lifecycle.js +92 -0
- package/dist/mobile-delivery.d.ts +100 -0
- package/dist/mobile-delivery.js +319 -0
- package/dist/platforms/desktop.js +4 -5
- package/dist/platforms/extension.js +9 -73
- package/dist/platforms/mobile.js +14 -52
- package/dist/project-inspector.d.ts +4 -0
- package/dist/project-inspector.js +646 -0
- package/dist/project-state.d.ts +17 -0
- package/dist/project-state.js +239 -11
- package/dist/provider-auth.d.ts +41 -0
- package/dist/provider-auth.js +210 -0
- package/dist/providers/adapters.d.ts +17 -0
- package/dist/providers/adapters.js +269 -0
- package/dist/providers/api.d.ts +28 -0
- package/dist/providers/api.js +252 -0
- package/dist/providers/config.d.ts +15 -0
- package/dist/providers/config.js +184 -0
- package/dist/providers/definitions.d.ts +318 -0
- package/dist/providers/definitions.js +127 -0
- package/dist/providers/provision.d.ts +24 -0
- package/dist/providers/provision.js +76 -0
- package/dist/providers/receipts.d.ts +20 -0
- package/dist/providers/receipts.js +66 -0
- package/dist/providers/recovery-verification.d.ts +7 -0
- package/dist/providers/recovery-verification.js +112 -0
- package/dist/providers/recovery.d.ts +30 -0
- package/dist/providers/recovery.js +34 -0
- package/dist/providers/runtime.d.ts +10 -0
- package/dist/providers/runtime.js +231 -0
- package/dist/providers/schema.d.ts +28 -0
- package/dist/providers/schema.js +49 -0
- package/dist/providers/state.d.ts +56 -0
- package/dist/providers/state.js +329 -0
- package/dist/provision.d.ts +1 -1
- package/dist/provision.js +61 -3
- package/dist/readiness.js +11 -3
- package/dist/registry/add.d.ts +6 -0
- package/dist/registry/add.js +35 -4
- package/dist/registry/commands.d.ts +3 -2
- package/dist/registry/commands.js +53 -14
- package/dist/registry/options.js +3 -2
- package/dist/registry/preview.d.ts +11 -0
- package/dist/registry/preview.js +91 -0
- package/dist/registry/receipt.js +1 -0
- package/dist/registry/resolve.js +19 -13
- package/dist/registry/types.d.ts +1 -0
- package/dist/registry/usage.d.ts +10 -0
- package/dist/registry/usage.js +22 -37
- package/dist/render.d.ts +2 -0
- package/dist/render.js +10 -0
- package/dist/scaffold/api.js +20 -3
- package/dist/scaffold/auth.js +68 -28
- package/dist/scaffold/bootstrap.d.ts +2 -0
- package/dist/scaffold/bootstrap.js +9 -0
- package/dist/scaffold/database.js +12 -6
- package/dist/scaffold/documentation.js +11 -8
- package/dist/scaffold/environment.d.ts +2 -0
- package/dist/scaffold/environment.js +18 -1
- package/dist/scaffold/files.d.ts +1 -0
- package/dist/scaffold/files.js +15 -4
- package/dist/scaffold/services.js +37 -13
- package/dist/scaffold/stack-documentation.d.ts +3 -0
- package/dist/scaffold/stack-documentation.js +52 -0
- package/dist/scaffold/styling.d.ts +3 -0
- package/dist/scaffold/styling.js +84 -0
- package/dist/scaffold/web.js +31 -11
- package/dist/scaffold/workspace.js +49 -22
- package/dist/ship.d.ts +320 -0
- package/dist/ship.js +211 -0
- package/dist/stack-registry.d.ts +2 -2
- package/dist/stack-registry.js +44 -11
- package/dist/standard.d.ts +2 -1
- package/dist/standard.js +13 -12
- package/dist/vercel.d.ts +3 -3
- package/dist/vercel.js +27 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/vinext.js +9 -18
- package/dist/workspace-graph.js +3 -2
- package/docs/application-stack-standard.md +51 -80
- package/docs/cli-design.md +67 -0
- package/docs/delivery-workflow.md +29 -15
- package/docs/plugin-ui.md +61 -0
- package/docs/provider-configuration.md +117 -0
- package/package.json +16 -12
- package/dist/managed-hosting.d.ts +0 -49
- package/dist/managed-hosting.js +0 -373
- package/docs/managed-hosting-architecture.md +0 -87
- package/docs/sponsorships.md +0 -25
package/dist/registry/add.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
export declare function
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
|
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('
|
|
45
|
+
console.log(commandHelp('add'));
|
|
30
46
|
return;
|
|
31
47
|
}
|
|
32
48
|
const positional = [];
|
|
33
|
-
|
|
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 [
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
85
|
+
...(preview ? { preview } : {}),
|
|
86
|
+
}), { ...output, json: args.includes('--json') });
|
|
48
87
|
}
|
package/dist/registry/options.js
CHANGED
|
@@ -46,7 +46,8 @@ function parseStandardFlags(remaining) {
|
|
|
46
46
|
}
|
|
47
47
|
export function parseStandardOptions(command, args) {
|
|
48
48
|
const remaining = [...args];
|
|
49
|
-
const
|
|
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
|
+
}
|
package/dist/registry/receipt.js
CHANGED
package/dist/registry/resolve.js
CHANGED
|
@@ -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 =
|
|
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);
|
package/dist/registry/types.d.ts
CHANGED
|
@@ -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[];
|
package/dist/registry/usage.d.ts
CHANGED
|
@@ -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;
|
package/dist/registry/usage.js
CHANGED
|
@@ -1,37 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
anhedral
|
|
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
|
+
}
|
package/dist/scaffold/api.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
7
|
-
|
|
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'],
|