anhedral 0.9.0 → 0.10.1
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 +6 -10
- package/assets/security/audit-deps.mjs +6 -1
- package/dist/bin.js +6 -2
- 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/dependencies.d.ts +32 -0
- package/dist/dependencies.js +47 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -1
- package/dist/platforms/desktop.js +4 -5
- package/dist/platforms/extension.js +9 -73
- package/dist/platforms/mobile.js +6 -47
- package/dist/readiness.js +12 -21
- package/dist/registry/add.d.ts +5 -9
- package/dist/registry/add.js +26 -58
- package/dist/registry/commands.js +5 -56
- package/dist/registry/receipt.d.ts +0 -1
- package/dist/registry/receipt.js +2 -22
- package/dist/registry/resolve.js +19 -13
- package/dist/registry/types.d.ts +1 -0
- package/dist/registry/usage.js +2 -3
- 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 +73 -17
- package/dist/scaffold/bootstrap.d.ts +3 -0
- package/dist/scaffold/bootstrap.js +15 -1
- 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 +38 -22
- package/dist/stack-registry.d.ts +2 -2
- package/dist/stack-registry.js +38 -11
- package/dist/standard.js +3 -8
- 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/delivery-workflow.md +1 -1
- package/package.json +1 -1
- package/dist/registry/remote.d.ts +0 -25
- package/dist/registry/remote.js +0 -168
package/dist/registry/add.d.ts
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
import type { StandardRunner } from './types.js';
|
|
2
2
|
export interface AddRequest {
|
|
3
3
|
items: string[];
|
|
4
|
-
registry?: string;
|
|
5
|
-
integrity?: string;
|
|
6
4
|
dryRun?: boolean;
|
|
7
5
|
skipInstall?: boolean;
|
|
8
6
|
}
|
|
9
7
|
/** Add recipes to an initialized workspace, refusing edits to customer-owned source. */
|
|
10
8
|
export declare function addStackItems(root: string, request: AddRequest, run?: StandardRunner): Promise<{
|
|
11
|
-
files: string[];
|
|
12
|
-
conflicts: string[];
|
|
13
|
-
registry?: {
|
|
14
|
-
source: string;
|
|
15
|
-
integrity: string;
|
|
16
|
-
} | undefined;
|
|
17
9
|
schemaVersion: number;
|
|
18
10
|
root: string;
|
|
19
11
|
dryRun: boolean;
|
|
20
12
|
items: string[];
|
|
21
|
-
|
|
13
|
+
bootstrapRequired: boolean;
|
|
14
|
+
filesChecked: boolean;
|
|
15
|
+
targets: string[];
|
|
16
|
+
files: string[];
|
|
17
|
+
conflicts: string[];
|
|
22
18
|
}>;
|
package/dist/registry/add.js
CHANGED
|
@@ -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';
|
|
@@ -16,10 +17,14 @@ import { json, put, readJson } from '../scaffold/files.js';
|
|
|
16
17
|
import { writeStandardRoot } from '../scaffold/workspace.js';
|
|
17
18
|
import { createRecipeContext } from './context.js';
|
|
18
19
|
import { resolveStackRegistry, resolveStandardProducts } from './resolve.js';
|
|
19
|
-
import { sourceFiles
|
|
20
|
-
import { loadSourceRegistry, resolveSourceItems } from './remote.js';
|
|
20
|
+
import { sourceFiles } from './receipt.js';
|
|
21
21
|
/** Add recipes to an initialized workspace, refusing edits to customer-owned source. */
|
|
22
22
|
export async function addStackItems(root, request, run = execFile) {
|
|
23
|
+
for (const key of Object.keys(request))
|
|
24
|
+
if (!['items', 'dryRun', 'skipInstall'].includes(key))
|
|
25
|
+
throw new Error(`Unknown add option: ${key}. Only bundled registry items are supported.`);
|
|
26
|
+
if (!request.dryRun)
|
|
27
|
+
assertWorkspaceRuntime(run);
|
|
23
28
|
root = realpathSync(root);
|
|
24
29
|
if (!request.items.length)
|
|
25
30
|
throw new Error('Select at least one registry item.');
|
|
@@ -34,17 +39,11 @@ export async function addStackItems(root, request, run = execFile) {
|
|
|
34
39
|
throw new Error('This project has no scaffold fingerprints; migrate it deliberately before using add.');
|
|
35
40
|
const baseline = receipt.files;
|
|
36
41
|
const selected = new Set(plan.products);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (!STANDARD_PRODUCTS.includes(id))
|
|
43
|
-
throw new Error(`Unknown stack product: ${id}.`);
|
|
44
|
-
selected.add(id);
|
|
45
|
-
}
|
|
46
|
-
if (request.integrity && (!remote || remote.integrity !== request.integrity))
|
|
47
|
-
throw new Error('Source registry integrity differs from the reviewed payload.');
|
|
42
|
+
for (const id of request.items) {
|
|
43
|
+
if (!STANDARD_PRODUCTS.includes(id))
|
|
44
|
+
throw new Error(`Unknown stack product: ${id}.`);
|
|
45
|
+
selected.add(id);
|
|
46
|
+
}
|
|
48
47
|
resolveStandardProducts(selected, plan.hosting, false);
|
|
49
48
|
const options = {
|
|
50
49
|
root,
|
|
@@ -65,24 +64,8 @@ export async function addStackItems(root, request, run = execFile) {
|
|
|
65
64
|
if (before && before.targets.some((target) => !item.targets.includes(target)))
|
|
66
65
|
throw new Error(`Adding this selection relocates ${item.name}; migrate its existing source explicitly.`);
|
|
67
66
|
}
|
|
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
67
|
const before = sourceFiles(root);
|
|
85
|
-
const receiptText =
|
|
68
|
+
const receiptText = readBoundedFile(path.join(root, 'anhedral.registry.json'), MAX_PROJECT_JSON_BYTES).toString('utf8');
|
|
86
69
|
const commitPaths = [];
|
|
87
70
|
const conflicts = [];
|
|
88
71
|
const result = {
|
|
@@ -92,11 +75,19 @@ export async function addStackItems(root, request, run = execFile) {
|
|
|
92
75
|
items: registry.items
|
|
93
76
|
.filter((item) => !isDeepStrictEqual(oldItems.get(item.name), item))
|
|
94
77
|
.map((item) => item.name),
|
|
95
|
-
|
|
96
|
-
|
|
78
|
+
bootstrapRequired: recipes.some((item) => ['vinext', 'next'].includes(item.name)) &&
|
|
79
|
+
!existsSync(path.join(root, 'apps/web/package.json')),
|
|
80
|
+
filesChecked: true,
|
|
81
|
+
targets: recipes.flatMap((item) => item.targets),
|
|
97
82
|
files: commitPaths,
|
|
98
83
|
conflicts,
|
|
99
84
|
};
|
|
85
|
+
// The upstream web generator requires network access and executes package scripts.
|
|
86
|
+
// A preview reports the resolved selection without invoking that generator.
|
|
87
|
+
if (request.dryRun && result.bootstrapRequired) {
|
|
88
|
+
result.filesChecked = false;
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
100
91
|
await runStagedTransaction(root, {
|
|
101
92
|
dryRun: !!request.dryRun,
|
|
102
93
|
commitPaths,
|
|
@@ -126,11 +117,6 @@ export async function addStackItems(root, request, run = execFile) {
|
|
|
126
117
|
}
|
|
127
118
|
put(stage, file, content);
|
|
128
119
|
}
|
|
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
120
|
writeStandardRoot(stage, options, registry, true);
|
|
135
121
|
if (options.products.includes('next') && options.hosting === 'vercel')
|
|
136
122
|
registerVercelApplication(stage);
|
|
@@ -158,40 +144,22 @@ export async function addStackItems(root, request, run = execFile) {
|
|
|
158
144
|
conflicts.push(file);
|
|
159
145
|
if (conflicts.length && !request.dryRun)
|
|
160
146
|
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
147
|
json(stage, 'anhedral.registry.json', {
|
|
179
148
|
...registry,
|
|
180
149
|
status: 'scaffolded',
|
|
181
150
|
resourcesProvisioned: false,
|
|
182
151
|
workspaces: inspectWorkspaceGraph(stage),
|
|
183
152
|
files,
|
|
184
|
-
...(remoteReceipts.length ? { remoteItems: remoteReceipts } : {}),
|
|
185
153
|
});
|
|
186
154
|
commitPaths.push('anhedral.registry.json');
|
|
187
155
|
// Detect concurrent changes even to source that this addition would otherwise preserve.
|
|
188
156
|
if (!isDeepStrictEqual(sourceFiles(root), before) ||
|
|
189
|
-
|
|
157
|
+
readBoundedFile(path.join(root, 'anhedral.registry.json'), MAX_PROJECT_JSON_BYTES).toString('utf8') !== receiptText)
|
|
190
158
|
throw new Error('Workspace changed during addition; retry after reviewing source.');
|
|
191
159
|
},
|
|
192
160
|
afterCommit: () => {
|
|
193
161
|
if (!request.skipInstall)
|
|
194
|
-
run('pnpm', ['install', '--frozen-lockfile'
|
|
162
|
+
run('pnpm', ['install', '--frozen-lockfile'], root);
|
|
195
163
|
},
|
|
196
164
|
});
|
|
197
165
|
return result;
|
|
@@ -1,42 +1,7 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
1
|
import path from 'node:path';
|
|
3
2
|
import { addStackItems } from './add.js';
|
|
4
|
-
import { buildSourceRegistry, loadSourceRegistry } from './remote.js';
|
|
5
3
|
import { STACK_REGISTRY, getStackItem } from '../stack-registry.js';
|
|
6
4
|
export async function runRegistry(args, json) {
|
|
7
|
-
if (args[0] === 'build') {
|
|
8
|
-
args = args.filter((arg) => arg !== '--json');
|
|
9
|
-
if (args.length !== 4 || args[2] !== '--output' || !args[1] || !args[3])
|
|
10
|
-
throw new Error('Use registry build <manifest> --output <registry.json>.');
|
|
11
|
-
const source = path.resolve(args[1]);
|
|
12
|
-
const output = path.resolve(args[3]);
|
|
13
|
-
if (source === output)
|
|
14
|
-
throw new Error('Registry output must differ from its authored manifest.');
|
|
15
|
-
const registry = buildSourceRegistry(source);
|
|
16
|
-
mkdirSync(path.dirname(output), { recursive: true });
|
|
17
|
-
writeFileSync(output, JSON.stringify(registry, null, 2) + '\n');
|
|
18
|
-
console.log(JSON.stringify({ output, items: registry.items.length }));
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
const registryFlag = args.indexOf('--registry');
|
|
22
|
-
if (registryFlag >= 0) {
|
|
23
|
-
const source = args[registryFlag + 1];
|
|
24
|
-
if (!source || source.startsWith('-'))
|
|
25
|
-
throw new Error('Missing registry source.');
|
|
26
|
-
const positional = args.filter((arg, index) => index !== registryFlag && index !== registryFlag + 1 && arg !== '--json');
|
|
27
|
-
const registry = await loadSourceRegistry(source);
|
|
28
|
-
if (positional.length === 1 && positional[0] === 'list')
|
|
29
|
-
console.log(JSON.stringify(registry, null, 2));
|
|
30
|
-
else if (positional.length === 2 && positional[0] === 'view') {
|
|
31
|
-
const item = registry.registry.items.find((item) => item.name === positional[1]);
|
|
32
|
-
if (!item)
|
|
33
|
-
throw new Error('Unknown remote registry item.');
|
|
34
|
-
console.log(JSON.stringify({ source: registry.source, integrity: registry.integrity, item }, null, 2));
|
|
35
|
-
}
|
|
36
|
-
else
|
|
37
|
-
throw new Error('Use registry list|view [item] --registry <file|https-url>.');
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
5
|
const positional = args.filter((arg) => arg !== '--json');
|
|
41
6
|
if (positional.includes('--help') || positional.includes('-h')) {
|
|
42
7
|
console.log('anhedral registry list|view [item] [--json]');
|
|
@@ -61,38 +26,22 @@ export async function runRegistry(args, json) {
|
|
|
61
26
|
}
|
|
62
27
|
export async function runAdd(args) {
|
|
63
28
|
if (args.includes('--help') || args.includes('-h')) {
|
|
64
|
-
console.log('anhedral add <directory> <items...> [--
|
|
29
|
+
console.log('anhedral add <directory> <items...> [--dry-run] [--skip-install] [--json]');
|
|
65
30
|
return;
|
|
66
31
|
}
|
|
67
|
-
let source;
|
|
68
|
-
let integrity;
|
|
69
32
|
const positional = [];
|
|
70
|
-
for (
|
|
71
|
-
const arg = args[index];
|
|
33
|
+
for (const arg of args) {
|
|
72
34
|
if (['--dry-run', '--skip-install', '--json'].includes(arg))
|
|
73
35
|
continue;
|
|
74
|
-
if (arg
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
integrity = args[++index];
|
|
78
|
-
}
|
|
79
|
-
else if (arg === '--registry') {
|
|
80
|
-
if (source || !args[index + 1] || args[index + 1].startsWith('-'))
|
|
81
|
-
throw new Error('Specify one registry source.');
|
|
82
|
-
source = args[++index];
|
|
83
|
-
}
|
|
84
|
-
else if (arg.startsWith('-'))
|
|
85
|
-
throw new Error('Unknown add option.');
|
|
86
|
-
else
|
|
87
|
-
positional.push(arg);
|
|
36
|
+
if (arg.startsWith('-'))
|
|
37
|
+
throw new Error(`Unknown add option: ${arg}.`);
|
|
38
|
+
positional.push(arg);
|
|
88
39
|
}
|
|
89
40
|
const [directory, ...items] = positional;
|
|
90
41
|
if (!directory || !items.length)
|
|
91
42
|
throw new Error('Use add <directory> <items...>.');
|
|
92
43
|
console.log(JSON.stringify(await addStackItems(path.resolve(directory), {
|
|
93
44
|
items,
|
|
94
|
-
registry: source,
|
|
95
|
-
integrity,
|
|
96
45
|
dryRun: args.includes('--dry-run'),
|
|
97
46
|
skipInstall: args.includes('--skip-install'),
|
|
98
47
|
}), null, 2));
|
package/dist/registry/receipt.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { json, readJson } from '../scaffold/files.js';
|
|
5
5
|
const excluded = new Set([
|
|
@@ -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,24 +51,3 @@ export function recordScaffoldFiles(root) {
|
|
|
50
51
|
files: Object.fromEntries(sourceFiles(root)),
|
|
51
52
|
});
|
|
52
53
|
}
|
|
53
|
-
export function assertSafeTarget(root, relative) {
|
|
54
|
-
if (!relative ||
|
|
55
|
-
path.isAbsolute(relative) ||
|
|
56
|
-
relative.includes('\\') ||
|
|
57
|
-
relative
|
|
58
|
-
.split('/')
|
|
59
|
-
.some((part) => !part ||
|
|
60
|
-
part === '.' ||
|
|
61
|
-
part === '..' ||
|
|
62
|
-
part.includes(':') ||
|
|
63
|
-
/[\x00-\x1f]/.test(part) ||
|
|
64
|
-
/[. ]$/.test(part) ||
|
|
65
|
-
/^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i.test(part)))
|
|
66
|
-
throw new Error(`Unsafe registry target: ${relative}.`);
|
|
67
|
-
let current = root;
|
|
68
|
-
for (const part of relative.split('/')) {
|
|
69
|
-
current = path.join(current, part);
|
|
70
|
-
if (existsSync(current) && lstatSync(current).isSymbolicLink())
|
|
71
|
-
throw new Error(`Registry target traverses a symlink: ${relative}.`);
|
|
72
|
-
}
|
|
73
|
-
}
|
package/dist/registry/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.js
CHANGED
|
@@ -2,7 +2,7 @@ import { STANDARD_PRODUCTS } from '../stack-registry.js';
|
|
|
2
2
|
export const STANDARD_USAGE = `Anhedral Application Stack & Delivery Standard
|
|
3
3
|
|
|
4
4
|
anhedral new <directory> [products...] [--hosting cloudflare|vercel] [--skip-install] [--no-git] [--dry-run] [--json]
|
|
5
|
-
anhedral add <directory> <items...> [--
|
|
5
|
+
anhedral add <directory> <items...> [--dry-run] [--skip-install] [--json]
|
|
6
6
|
anhedral init [products...] [--hosting cloudflare|vercel] [--skip-install] [--git] [--dry-run] [--json]
|
|
7
7
|
|
|
8
8
|
Default: Vinext on Cloudflare Workers, from the official shadcn pnpm/Turborepo monorepo.
|
|
@@ -15,8 +15,7 @@ Examples:
|
|
|
15
15
|
anhedral new website --next --stripe --resend
|
|
16
16
|
anhedral init --wxt
|
|
17
17
|
anhedral cloud register|stage|save|deploy|status [directory] (managed service API required)
|
|
18
|
-
anhedral registry
|
|
19
|
-
anhedral registry list|view [item] [--registry <file|https-url>] [--json]
|
|
18
|
+
anhedral registry list|view [item] [--json]
|
|
20
19
|
anhedral doctor [directory] [--providers] --json
|
|
21
20
|
anhedral repository [directory] [--owner <github-account>] [--name <repo>] [--visibility private|public] [--create]
|
|
22
21
|
anhedral register [directory]
|
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'],
|
package/dist/scaffold/auth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { writeEnvironmentFile } from './environment.js';
|
|
1
2
|
import { STACK_DEPENDENCIES } from '../dependencies.js';
|
|
2
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -9,15 +10,20 @@ export function scaffoldBetterAuth({ root, options, runtimeApp, has }) {
|
|
|
9
10
|
import { drizzleAdapter } from '@better-auth/drizzle-adapter';
|
|
10
11
|
import type { createDatabase } from '@workspace/db';
|
|
11
12
|
export function createAuth(db: ReturnType<typeof createDatabase>, secret: string, baseURL: string) {
|
|
12
|
-
return betterAuth({
|
|
13
|
+
return betterAuth({
|
|
14
|
+
database: drizzleAdapter(db, { provider: '${has('neon') ? 'pg' : 'sqlite'}', transaction: false }),
|
|
15
|
+
secret,
|
|
16
|
+
baseURL,
|
|
17
|
+
emailAndPassword: { enabled: true },
|
|
18
|
+
});
|
|
13
19
|
}
|
|
14
20
|
`, {
|
|
15
|
-
'better-auth': '
|
|
16
|
-
'@better-auth/drizzle-adapter': '
|
|
21
|
+
'better-auth': STACK_DEPENDENCIES['better-auth'],
|
|
22
|
+
'@better-auth/drizzle-adapter': STACK_DEPENDENCIES['@better-auth/drizzle-adapter'],
|
|
17
23
|
'@workspace/db': 'workspace:*',
|
|
18
24
|
});
|
|
19
25
|
patchPackage(root, `apps/${runtimeApp}/package.json`, {
|
|
20
|
-
dependencies: { '@workspace/auth': 'workspace:*' },
|
|
26
|
+
dependencies: { '@workspace/auth': 'workspace:*', '@workspace/db': 'workspace:*' },
|
|
21
27
|
});
|
|
22
28
|
const connection = has('neon') ? 'createDatabase(env.DATABASE_URL)' : 'createDatabase(env.DB)';
|
|
23
29
|
if (has('hono')) {
|
|
@@ -27,7 +33,10 @@ export function createAuth(db: ReturnType<typeof createDatabase>, secret: string
|
|
|
27
33
|
const config = readJson(root, 'apps/api/wrangler.jsonc');
|
|
28
34
|
json(root, 'apps/api/wrangler.jsonc', {
|
|
29
35
|
...config,
|
|
30
|
-
vars: {
|
|
36
|
+
vars: {
|
|
37
|
+
BETTER_AUTH_URL: 'http://localhost:8787',
|
|
38
|
+
...(config.vars ?? {}),
|
|
39
|
+
},
|
|
31
40
|
});
|
|
32
41
|
}
|
|
33
42
|
else {
|
|
@@ -48,14 +57,21 @@ export { handler as GET, handler as POST };
|
|
|
48
57
|
const config = readJson(root, 'apps/web/wrangler.jsonc');
|
|
49
58
|
json(root, 'apps/web/wrangler.jsonc', {
|
|
50
59
|
...config,
|
|
51
|
-
vars: {
|
|
60
|
+
vars: {
|
|
61
|
+
BETTER_AUTH_URL: 'http://localhost:3000',
|
|
62
|
+
...(config.vars ?? {}),
|
|
63
|
+
},
|
|
52
64
|
});
|
|
53
65
|
}
|
|
54
66
|
patchPackage(root, 'apps/web/package.json', {
|
|
55
67
|
dependencies: { 'drizzle-orm': STACK_DEPENDENCIES['drizzle-orm'] },
|
|
56
68
|
});
|
|
57
69
|
}
|
|
58
|
-
|
|
70
|
+
writeEnvironmentFile(root, `apps/${runtimeApp}/${runtimeApp === 'web' && options.hosting === 'vercel' ? '.env.example' : '.dev.vars.example'}`, {
|
|
71
|
+
...(has('neon') ? { DATABASE_URL: '' } : {}),
|
|
72
|
+
BETTER_AUTH_SECRET: '',
|
|
73
|
+
BETTER_AUTH_URL: `http://localhost:${runtimeApp === 'web' ? '3000' : '8787'}`,
|
|
74
|
+
});
|
|
59
75
|
if (has('hono'))
|
|
60
76
|
put(root, 'apps/api/src/secrets.d.ts', 'interface Env { BETTER_AUTH_SECRET: string }\n');
|
|
61
77
|
}
|
|
@@ -74,22 +90,62 @@ export function scaffoldClerk({ root, has }) {
|
|
|
74
90
|
put(root, 'apps/web/app/layout.tsx', 'import { ClerkProvider } from "@clerk/nextjs";\n' + wrapped);
|
|
75
91
|
}
|
|
76
92
|
}
|
|
77
|
-
put(root, 'apps/web/app/sign-in/[[...sign-in]]/page.tsx',
|
|
78
|
-
|
|
93
|
+
put(root, 'apps/web/app/sign-in/[[...sign-in]]/page.tsx', `import { Suspense } from 'react';
|
|
94
|
+
import { SignIn } from '@clerk/nextjs';
|
|
95
|
+
|
|
96
|
+
export default function SignInPage() {
|
|
97
|
+
return (
|
|
98
|
+
<Suspense fallback={<p role="status">Loading sign in…</p>}>
|
|
99
|
+
<SignIn />
|
|
100
|
+
</Suspense>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
`);
|
|
104
|
+
put(root, 'apps/web/app/sign-up/[[...sign-up]]/page.tsx', `import { Suspense } from 'react';
|
|
105
|
+
import { SignUp } from '@clerk/nextjs';
|
|
106
|
+
|
|
107
|
+
export default function SignUpPage() {
|
|
108
|
+
return (
|
|
109
|
+
<Suspense fallback={<p role="status">Loading sign up…</p>}>
|
|
110
|
+
<SignUp />
|
|
111
|
+
</Suspense>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
`);
|
|
79
115
|
put(root, 'apps/web/proxy.ts', `import { clerkMiddleware } from '@clerk/nextjs/server';
|
|
80
|
-
|
|
116
|
+
import type { NextMiddleware } from 'next/server';
|
|
117
|
+
|
|
118
|
+
const proxy: NextMiddleware = clerkMiddleware();
|
|
119
|
+
export default proxy;
|
|
81
120
|
export const config = { matcher: ['/((?!_next|[^?]*\\\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico)).*)', '/(api|trpc)(.*)'] };
|
|
82
121
|
`);
|
|
83
122
|
// Middleware makes auth available; product routes must explicitly require identity and authorization.
|
|
84
123
|
}
|
|
85
124
|
if (has('hono')) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
125
|
+
sharedPackage(root, 'auth', `import { createClerkClient } from '@clerk/backend';
|
|
126
|
+
|
|
127
|
+
export async function authenticate(
|
|
128
|
+
request: Request,
|
|
129
|
+
secretKey: string,
|
|
130
|
+
publishableKey: string,
|
|
131
|
+
authorizedParties: string[],
|
|
132
|
+
) {
|
|
133
|
+
if (!secretKey || !publishableKey || authorizedParties.length === 0) {
|
|
134
|
+
throw new Error('Configure Clerk keys and explicit authorized origins before authenticating.');
|
|
135
|
+
}
|
|
136
|
+
return createClerkClient({ secretKey, publishableKey }).authenticateRequest(request, {
|
|
137
|
+
authorizedParties,
|
|
138
|
+
});
|
|
92
139
|
}
|
|
93
|
-
|
|
140
|
+
`, { '@clerk/backend': STACK_DEPENDENCIES['@clerk/backend'] });
|
|
141
|
+
writeEnvironmentFile(root, 'apps/api/.dev.vars.example', {
|
|
142
|
+
CLERK_SECRET_KEY: '',
|
|
143
|
+
CLERK_PUBLISHABLE_KEY: '',
|
|
144
|
+
});
|
|
94
145
|
}
|
|
146
|
+
if (has('next') || has('vinext'))
|
|
147
|
+
writeEnvironmentFile(root, 'apps/web/.env.example', {
|
|
148
|
+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: '',
|
|
149
|
+
CLERK_SECRET_KEY: '',
|
|
150
|
+
});
|
|
95
151
|
}
|
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
import type { StandardRunner } from '../registry/types.js';
|
|
2
|
+
export declare function assertBootstrapPlatform(run: StandardRunner, platform?: NodeJS.Platform): void;
|
|
3
|
+
/** Planning and diagnostics support older Node releases; generated dependencies do not. */
|
|
4
|
+
export declare function assertWorkspaceRuntime(run: StandardRunner, version?: string): void;
|
|
2
5
|
export declare function bootstrapWorkspace(root: string, name: string, run: StandardRunner, webOnly?: boolean): void;
|
|
@@ -1,8 +1,22 @@
|
|
|
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
|
+
}
|
|
9
|
+
/** Planning and diagnostics support older Node releases; generated dependencies do not. */
|
|
10
|
+
export function assertWorkspaceRuntime(run, version = process.versions.node) {
|
|
11
|
+
if (run !== execFile)
|
|
12
|
+
return;
|
|
13
|
+
const [major, minor] = version.split('.').map(Number);
|
|
14
|
+
if (!((major === 22 && minor >= 13) || major >= 24))
|
|
15
|
+
throw new Error('Generated workspaces require Node.js 22.13+ on the 22.x line or Node.js 24+. Planning and doctor remain available on older CLI-supported versions.');
|
|
16
|
+
}
|
|
5
17
|
export function bootstrapWorkspace(root, name, run, webOnly = false) {
|
|
18
|
+
assertBootstrapPlatform(run);
|
|
19
|
+
assertWorkspaceRuntime(run);
|
|
6
20
|
const temporary = path.join(root, '.bootstrap');
|
|
7
21
|
mkdirSync(temporary);
|
|
8
22
|
try {
|