anhedral 0.1.10 → 0.4.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/CHANGELOG.md +119 -0
- package/CONTRIBUTING.md +73 -0
- package/LICENSE +196 -17
- package/README.md +202 -145
- package/SECURITY.md +37 -0
- package/SKILL.md +244 -0
- package/bin/anhedral.js +1 -1
- package/dist/architecture/contributions.d.ts +28 -0
- package/dist/architecture/contributions.js +125 -0
- package/dist/architecture/index.d.ts +12 -0
- package/dist/architecture/index.js +6 -0
- package/dist/architecture/manifest.d.ts +53 -0
- package/dist/architecture/manifest.js +283 -0
- package/dist/architecture/modules.d.ts +37 -0
- package/dist/architecture/modules.js +184 -0
- package/dist/architecture/plan.d.ts +58 -0
- package/dist/architecture/plan.js +145 -0
- package/dist/architecture/templates.d.ts +10 -0
- package/dist/architecture/templates.js +23 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +179 -0
- package/dist/cli.d.ts +24 -2
- package/dist/cli.js +189 -21
- package/dist/composers/root.d.ts +6 -0
- package/dist/composers/root.js +35 -0
- package/dist/dependencies.d.ts +31 -9
- package/dist/dependencies.js +227 -60
- package/dist/index.d.ts +8 -2
- package/dist/index.js +5 -31
- package/dist/print.js +15 -0
- package/dist/prompts.d.ts +5 -0
- package/dist/prompts.js +19 -0
- package/dist/render.d.ts +11 -0
- package/dist/render.js +74 -0
- package/dist/scaffold.d.ts +64 -12
- package/dist/scaffold.js +1726 -1150
- package/dist/template-source.d.ts +20 -0
- package/dist/template-source.js +180 -0
- package/dist/templates/api.d.ts +2 -0
- package/dist/templates/api.js +3122 -0
- package/dist/templates/assets-private-proxy.d.ts +3 -0
- package/dist/templates/assets-private-proxy.js +277 -0
- package/dist/templates/desktop.d.ts +2 -0
- package/dist/templates/desktop.js +639 -0
- package/dist/templates/electron-updater.d.ts +3 -0
- package/dist/templates/electron-updater.js +358 -0
- package/dist/templates/extension.d.ts +1 -1
- package/dist/templates/extension.js +322 -248
- package/dist/templates/mobile.d.ts +2 -0
- package/dist/templates/mobile.js +720 -0
- package/dist/templates/shared.d.ts +2 -0
- package/dist/templates/shared.js +520 -0
- package/dist/templates/web.d.ts +2 -0
- package/dist/templates/web.js +412 -0
- package/dist/toolchain.d.ts +0 -8
- package/dist/toolchain.js +0 -21
- package/dist/transaction.d.ts +22 -0
- package/dist/transaction.js +822 -0
- package/dist/ui.d.ts +33 -0
- package/dist/ui.js +199 -0
- package/dist/util.d.ts +1 -5
- package/dist/util.js +70 -159
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/dist/workspace-config.d.ts +11 -0
- package/dist/workspace-config.js +372 -0
- package/docs/conventions.md +92 -0
- package/docs/master-stack-map.md +192 -0
- package/docs/output-tree-contract.md +482 -0
- package/docs/references/manual-scaffolding.md +403 -0
- package/docs/references/provisioning.md +220 -0
- package/package.json +57 -13
- package/templates/api-fastify/apps/api/tsconfig.json +20 -0
- package/templates/catalog.json +23 -0
- package/templates/db-drizzle/packages/db/tsconfig.json +14 -0
- package/templates/desktop-electron/apps/desktop/tsconfig.json +37 -0
- package/templates/extension-wxt/apps/extension/tsconfig.json +41 -0
- package/templates/mobile-expo/apps/mobile/expo-env.d.ts +1 -0
- package/templates/web-next/apps/web/next-env.d.ts +6 -0
- package/dist/branding.d.ts +0 -5
- package/dist/branding.js +0 -17
- package/dist/commands.d.ts +0 -7
- package/dist/commands.js +0 -24
- package/dist/templates/backend.d.ts +0 -2
- package/dist/templates/backend.js +0 -2115
- package/dist/templates/frontend.d.ts +0 -2
- package/dist/templates/frontend.js +0 -1268
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { DEFAULT_MODULE_REGISTRY, isModuleId, resolveModules, } from './modules.js';
|
|
3
|
+
export const FILE_OWNERSHIP_CLASSES = [
|
|
4
|
+
'managed',
|
|
5
|
+
'mergeable',
|
|
6
|
+
'user',
|
|
7
|
+
];
|
|
8
|
+
export class PlanBuildError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
paths;
|
|
11
|
+
constructor(code, message, paths = []) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.paths = paths;
|
|
15
|
+
this.name = 'PlanBuildError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const OWNERSHIP_SET = new Set(FILE_OWNERSHIP_CLASSES);
|
|
19
|
+
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
20
|
+
function compareStrings(left, right) {
|
|
21
|
+
if (left < right)
|
|
22
|
+
return -1;
|
|
23
|
+
if (left > right)
|
|
24
|
+
return 1;
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
export function isFileOwnershipClass(value) {
|
|
28
|
+
return typeof value === 'string' && OWNERSHIP_SET.has(value);
|
|
29
|
+
}
|
|
30
|
+
export function isSha256(value) {
|
|
31
|
+
return typeof value === 'string' && SHA256_PATTERN.test(value);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Hash rendered text or exact filesystem bytes.
|
|
35
|
+
*
|
|
36
|
+
* Strings are encoded as UTF-8 for deterministic generation-plan fingerprints.
|
|
37
|
+
* Callers hashing an existing file must pass its Buffer/Uint8Array so malformed
|
|
38
|
+
* byte sequences cannot collapse to the same Unicode replacement characters.
|
|
39
|
+
*/
|
|
40
|
+
export function hashContent(content) {
|
|
41
|
+
const hash = createHash('sha256');
|
|
42
|
+
if (typeof content === 'string')
|
|
43
|
+
hash.update(content, 'utf8');
|
|
44
|
+
else
|
|
45
|
+
hash.update(content);
|
|
46
|
+
return hash.digest('hex');
|
|
47
|
+
}
|
|
48
|
+
export function writePolicyForOwnership(ownership) {
|
|
49
|
+
switch (ownership) {
|
|
50
|
+
case 'managed': return 'replace-if-unmodified';
|
|
51
|
+
case 'mergeable': return 'structural-merge';
|
|
52
|
+
case 'user': return 'create-only';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function normalizePlanPath(value) {
|
|
56
|
+
if (typeof value !== 'string') {
|
|
57
|
+
throw new PlanBuildError('INVALID_PLAN_PATH', `Plan paths must be strings: ${String(value)}`, Object.freeze([String(value)]));
|
|
58
|
+
}
|
|
59
|
+
const pathValue = value.trim();
|
|
60
|
+
if (pathValue.length === 0
|
|
61
|
+
|| pathValue !== value
|
|
62
|
+
|| pathValue === '.'
|
|
63
|
+
|| pathValue.startsWith('/')
|
|
64
|
+
|| /^[a-zA-Z]:\//.test(pathValue)
|
|
65
|
+
|| pathValue.includes('\\')
|
|
66
|
+
|| pathValue.includes('\0')) {
|
|
67
|
+
throw new PlanBuildError('INVALID_PLAN_PATH', `Plan paths must be non-empty relative POSIX paths: ${JSON.stringify(value)}`, Object.freeze([value]));
|
|
68
|
+
}
|
|
69
|
+
const segments = pathValue.split('/');
|
|
70
|
+
if (segments.some((segment) => segment.length === 0 || segment === '.' || segment === '..')) {
|
|
71
|
+
throw new PlanBuildError('INVALID_PLAN_PATH', `Plan paths cannot contain empty, current-directory, or parent-directory segments: ${JSON.stringify(value)}`, Object.freeze([value]));
|
|
72
|
+
}
|
|
73
|
+
return segments.join('/');
|
|
74
|
+
}
|
|
75
|
+
function normalizeOwner(value) {
|
|
76
|
+
if (value === 'root' || isModuleId(value))
|
|
77
|
+
return value;
|
|
78
|
+
throw new PlanBuildError('INVALID_FILE_OWNER', `Unknown plan contribution owner: ${String(value)}`);
|
|
79
|
+
}
|
|
80
|
+
function canonicalFingerprintPayload(operation, requestedModules, resolvedModules, dependencyAddedModules, dependencyEdges, files) {
|
|
81
|
+
return JSON.stringify({
|
|
82
|
+
planVersion: 1,
|
|
83
|
+
operation,
|
|
84
|
+
requestedModules,
|
|
85
|
+
resolvedModules,
|
|
86
|
+
dependencyAddedModules,
|
|
87
|
+
dependencyEdges,
|
|
88
|
+
files: files.map((file) => ({
|
|
89
|
+
path: file.path,
|
|
90
|
+
owner: file.owner,
|
|
91
|
+
ownership: file.ownership,
|
|
92
|
+
writePolicy: file.writePolicy,
|
|
93
|
+
contentHash: file.contentHash,
|
|
94
|
+
})),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
export function buildGenerationPlan(input) {
|
|
98
|
+
if (!['init', 'add'].includes(input.operation)) {
|
|
99
|
+
throw new PlanBuildError('INVALID_OPERATION', `Unknown generation operation: ${String(input.operation)}`);
|
|
100
|
+
}
|
|
101
|
+
const registry = input.registry ?? DEFAULT_MODULE_REGISTRY;
|
|
102
|
+
const resolution = resolveModules(input.requestedModules, registry);
|
|
103
|
+
const resolvedSet = new Set(resolution.resolvedModules);
|
|
104
|
+
const plannedByPath = new Map();
|
|
105
|
+
for (const contribution of input.contributions ?? []) {
|
|
106
|
+
const owner = normalizeOwner(contribution.module);
|
|
107
|
+
if (owner !== 'root' && !resolvedSet.has(owner)) {
|
|
108
|
+
throw new PlanBuildError('UNRESOLVED_CONTRIBUTION', `Module ${owner} contributed files but is not in the resolved module plan`);
|
|
109
|
+
}
|
|
110
|
+
for (const file of contribution.files) {
|
|
111
|
+
const filePath = normalizePlanPath(file.path);
|
|
112
|
+
if (typeof file.content !== 'string') {
|
|
113
|
+
throw new PlanBuildError('INVALID_FILE_CONTENT', `Planned file content must be a string: ${filePath}`, Object.freeze([filePath]));
|
|
114
|
+
}
|
|
115
|
+
if (!isFileOwnershipClass(file.ownership)) {
|
|
116
|
+
throw new PlanBuildError('INVALID_FILE_OWNERSHIP', `Unknown ownership class for ${filePath}: ${String(file.ownership)}`, Object.freeze([filePath]));
|
|
117
|
+
}
|
|
118
|
+
const existing = plannedByPath.get(filePath);
|
|
119
|
+
if (existing) {
|
|
120
|
+
throw new PlanBuildError('DUPLICATE_OUTPUT_PATH', `Output path ${filePath} is owned by both ${existing.owner} and ${owner}; compose it before planning`, Object.freeze([filePath]));
|
|
121
|
+
}
|
|
122
|
+
const plannedFile = Object.freeze({
|
|
123
|
+
path: filePath,
|
|
124
|
+
owner,
|
|
125
|
+
ownership: file.ownership,
|
|
126
|
+
writePolicy: writePolicyForOwnership(file.ownership),
|
|
127
|
+
content: file.content,
|
|
128
|
+
contentHash: hashContent(file.content),
|
|
129
|
+
});
|
|
130
|
+
plannedByPath.set(filePath, plannedFile);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const files = Object.freeze([...plannedByPath.values()].sort((left, right) => compareStrings(left.path, right.path)));
|
|
134
|
+
const fingerprint = hashContent(canonicalFingerprintPayload(input.operation, resolution.requestedModules, resolution.resolvedModules, resolution.dependencyAddedModules, resolution.dependencyEdges, files));
|
|
135
|
+
return Object.freeze({
|
|
136
|
+
planVersion: 1,
|
|
137
|
+
operation: input.operation,
|
|
138
|
+
requestedModules: resolution.requestedModules,
|
|
139
|
+
resolvedModules: resolution.resolvedModules,
|
|
140
|
+
dependencyAddedModules: resolution.dependencyAddedModules,
|
|
141
|
+
dependencyEdges: resolution.dependencyEdges,
|
|
142
|
+
files,
|
|
143
|
+
fingerprint,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ModuleId } from './modules.js';
|
|
2
|
+
export declare const TEMPLATE_IDS: readonly ["web-next", "mobile-expo", "api-fastify", "desktop-electron", "extension-wxt", "db-drizzle"];
|
|
3
|
+
export type TemplateId = (typeof TEMPLATE_IDS)[number];
|
|
4
|
+
export type TemplateProvenance = {
|
|
5
|
+
readonly version: number;
|
|
6
|
+
readonly sha256: string;
|
|
7
|
+
};
|
|
8
|
+
export type TemplateProvenanceMap = Readonly<Partial<Record<TemplateId, TemplateProvenance>>>;
|
|
9
|
+
export declare function isTemplateId(value: unknown): value is TemplateId;
|
|
10
|
+
export declare function templateIdsForModules(modules: readonly ModuleId[]): readonly TemplateId[];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const TEMPLATE_IDS = [
|
|
2
|
+
'web-next',
|
|
3
|
+
'mobile-expo',
|
|
4
|
+
'api-fastify',
|
|
5
|
+
'desktop-electron',
|
|
6
|
+
'extension-wxt',
|
|
7
|
+
'db-drizzle',
|
|
8
|
+
];
|
|
9
|
+
const TEMPLATE_ID_SET = new Set(TEMPLATE_IDS);
|
|
10
|
+
export function isTemplateId(value) {
|
|
11
|
+
return typeof value === 'string' && TEMPLATE_ID_SET.has(value);
|
|
12
|
+
}
|
|
13
|
+
export function templateIdsForModules(modules) {
|
|
14
|
+
const selected = new Set(modules);
|
|
15
|
+
return Object.freeze([
|
|
16
|
+
selected.has('web') ? 'web-next' : null,
|
|
17
|
+
selected.has('mobile') ? 'mobile-expo' : null,
|
|
18
|
+
selected.has('api') ? 'api-fastify' : null,
|
|
19
|
+
selected.has('desktop') ? 'desktop-electron' : null,
|
|
20
|
+
selected.has('extension') ? 'extension-wxt' : null,
|
|
21
|
+
selected.has('db') ? 'db-drizzle' : null,
|
|
22
|
+
].filter((id) => id !== null));
|
|
23
|
+
}
|
package/dist/bin.d.ts
ADDED
package/dist/bin.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
|
+
import { argv, stdin, stdout } from 'node:process';
|
|
4
|
+
import { buildAddOptions, buildOptions, buildOptionsForRoot, parseCli, parseNewProjectRequest, parseUiAddOptions, USAGE } from './cli.js';
|
|
5
|
+
import { doctorProject, scaffoldAddModules, scaffoldProject, scaffoldUiComponents, scaffoldUpgradeProject } from './scaffold.js';
|
|
6
|
+
import { DEFAULT_PROMPT_APP_MODULES, DEFAULT_PROMPT_FEATURE_MODULES, hasUiSelection, parsePromptModules, shouldPromptForInitModules, } from './prompts.js';
|
|
7
|
+
import { GENERATOR_VERSION } from './version.js';
|
|
8
|
+
import { PostCommitError } from './transaction.js';
|
|
9
|
+
function writeCliError(error, code, json) {
|
|
10
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
11
|
+
if (json)
|
|
12
|
+
console.error(JSON.stringify({ error: message, code }));
|
|
13
|
+
else
|
|
14
|
+
console.error('Error:', message);
|
|
15
|
+
}
|
|
16
|
+
function splitAddArgs(args) {
|
|
17
|
+
const moduleNames = [];
|
|
18
|
+
const flagArgs = [];
|
|
19
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
20
|
+
const arg = args[index];
|
|
21
|
+
if (arg === '--toolchain') {
|
|
22
|
+
flagArgs.push(arg);
|
|
23
|
+
if (args[index + 1] != null) {
|
|
24
|
+
flagArgs.push(args[index + 1]);
|
|
25
|
+
index += 1;
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (arg.startsWith('--')) {
|
|
30
|
+
flagArgs.push(arg);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
moduleNames.push(arg);
|
|
34
|
+
}
|
|
35
|
+
return { moduleNames, flagArgs };
|
|
36
|
+
}
|
|
37
|
+
async function promptForInitModules(args) {
|
|
38
|
+
if (!shouldPromptForInitModules(args, stdin.isTTY === true))
|
|
39
|
+
return args;
|
|
40
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
41
|
+
try {
|
|
42
|
+
console.log('Select app surfaces: web, mobile, api, desktop, extension');
|
|
43
|
+
const appAnswer = await rl.question('App surfaces [all]: ');
|
|
44
|
+
console.log('Select backend features: db, auth, billing, storage, native-subscriptions, electron-updater');
|
|
45
|
+
const featureAnswer = await rl.question('Backend features [all]: ');
|
|
46
|
+
const selected = [
|
|
47
|
+
...parsePromptModules(appAnswer, DEFAULT_PROMPT_APP_MODULES),
|
|
48
|
+
...parsePromptModules(featureAnswer, DEFAULT_PROMPT_FEATURE_MODULES),
|
|
49
|
+
];
|
|
50
|
+
const result = [...args, ...selected.map((moduleName) => `--${moduleName}`)];
|
|
51
|
+
const hasUiClient = selected.some((moduleName) => ['web', 'mobile', 'desktop', 'extension'].includes(moduleName));
|
|
52
|
+
if (hasUiClient && !hasUiSelection(args)) {
|
|
53
|
+
console.log('Optionally add starter UI components to each selected client.');
|
|
54
|
+
const componentAnswer = await rl.question('Starter components [none]: ');
|
|
55
|
+
if (componentAnswer.trim())
|
|
56
|
+
result.push(`--ui=${componentAnswer}`);
|
|
57
|
+
}
|
|
58
|
+
if (selected.includes('mobile') && !args.some((arg) => arg.startsWith('--native-styling'))) {
|
|
59
|
+
const stylingAnswer = (await rl.question('Expo styling [nativewind]: ')).trim();
|
|
60
|
+
result.push(`--native-styling=${stylingAnswer || 'nativewind'}`);
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
rl.close();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function main() {
|
|
69
|
+
const args = argv.slice(2);
|
|
70
|
+
const command = args[0];
|
|
71
|
+
const rawArgs = args.slice(1);
|
|
72
|
+
const json = rawArgs.includes('--json');
|
|
73
|
+
const verbose = rawArgs.includes('--verbose');
|
|
74
|
+
if (json)
|
|
75
|
+
process.env.ANHEDRAL_QUIET = '1';
|
|
76
|
+
if (verbose)
|
|
77
|
+
process.env.ANHEDRAL_VERBOSE = '1';
|
|
78
|
+
if (command === '--version' || command === '-v') {
|
|
79
|
+
if (json)
|
|
80
|
+
console.log(JSON.stringify({ version: GENERATOR_VERSION }));
|
|
81
|
+
else
|
|
82
|
+
console.log(GENERATOR_VERSION);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!command || command === '--help' || command === '-h') {
|
|
86
|
+
if (json)
|
|
87
|
+
console.log(JSON.stringify({ usage: USAGE }));
|
|
88
|
+
else
|
|
89
|
+
console.log(USAGE);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!['new', 'init', 'add', 'ui', 'upgrade', 'doctor'].includes(command)) {
|
|
93
|
+
if (json)
|
|
94
|
+
writeCliError(new Error(`Unknown command: ${command}`), 'UNKNOWN_COMMAND', true);
|
|
95
|
+
else {
|
|
96
|
+
console.error(`Unknown command: ${command}`);
|
|
97
|
+
console.log(USAGE);
|
|
98
|
+
}
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
|
|
103
|
+
if (json)
|
|
104
|
+
console.log(JSON.stringify({ usage: USAGE }));
|
|
105
|
+
else
|
|
106
|
+
console.log(USAGE);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
let phase = 'arguments';
|
|
110
|
+
try {
|
|
111
|
+
if (command === 'new') {
|
|
112
|
+
const request = parseNewProjectRequest(rawArgs);
|
|
113
|
+
const promptedArgs = await promptForInitModules([...request.moduleArgs]);
|
|
114
|
+
const options = buildOptionsForRoot(parseCli(promptedArgs), request.directory);
|
|
115
|
+
phase = 'execute';
|
|
116
|
+
await scaffoldProject(options);
|
|
117
|
+
if (!options.json && !options.dryRun) {
|
|
118
|
+
console.log(`\nCreated ${options.rootDirectory}\nNext: cd ${options.rootDirectory}\nThen follow README.md to configure the selected providers and start the app.`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else if (command === 'doctor') {
|
|
122
|
+
const unknown = rawArgs.filter((arg) => !['--json', '--verbose'].includes(arg));
|
|
123
|
+
if (unknown.length)
|
|
124
|
+
throw new Error(`Unknown doctor option: ${unknown[0]}`);
|
|
125
|
+
phase = 'execute';
|
|
126
|
+
const report = doctorProject();
|
|
127
|
+
if (json)
|
|
128
|
+
console.log(JSON.stringify(report, null, 2));
|
|
129
|
+
else {
|
|
130
|
+
console.log(report.ok ? 'Anhedral project is healthy.' : 'Anhedral project has drift.');
|
|
131
|
+
for (const issue of report.issues)
|
|
132
|
+
console.log(` ${issue.severity}: ${issue.path}: ${issue.message}`);
|
|
133
|
+
}
|
|
134
|
+
if (!report.ok)
|
|
135
|
+
process.exitCode = 1;
|
|
136
|
+
}
|
|
137
|
+
else if (command === 'add') {
|
|
138
|
+
const { moduleNames, flagArgs } = splitAddArgs(rawArgs);
|
|
139
|
+
const options = buildAddOptions(moduleNames, parseCli(flagArgs));
|
|
140
|
+
phase = 'execute';
|
|
141
|
+
await scaffoldAddModules(options);
|
|
142
|
+
}
|
|
143
|
+
else if (command === 'ui') {
|
|
144
|
+
if (rawArgs[0] !== 'add')
|
|
145
|
+
throw new Error('Unknown UI command. Use: anhedral ui add <component...>');
|
|
146
|
+
const options = parseUiAddOptions(rawArgs.slice(1));
|
|
147
|
+
phase = 'execute';
|
|
148
|
+
await scaffoldUiComponents(options);
|
|
149
|
+
}
|
|
150
|
+
else if (command === 'upgrade') {
|
|
151
|
+
const unknown = rawArgs.filter((arg) => !['--skip-install', '--dry-run', '--json', '--verbose'].includes(arg));
|
|
152
|
+
if (unknown.length)
|
|
153
|
+
throw new Error(`Unknown upgrade option: ${unknown[0]}`);
|
|
154
|
+
phase = 'execute';
|
|
155
|
+
await scaffoldUpgradeProject({
|
|
156
|
+
skipInstall: rawArgs.includes('--skip-install'),
|
|
157
|
+
dryRun: rawArgs.includes('--dry-run'),
|
|
158
|
+
json,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const options = buildOptions(parseCli(await promptForInitModules(rawArgs)));
|
|
163
|
+
phase = 'execute';
|
|
164
|
+
await scaffoldProject(options);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
const code = error instanceof PostCommitError
|
|
169
|
+
? 'POST_COMMIT_FAILED'
|
|
170
|
+
: phase === 'arguments'
|
|
171
|
+
? 'INVALID_ARGUMENT'
|
|
172
|
+
: command === 'doctor'
|
|
173
|
+
? 'DOCTOR_FAILED'
|
|
174
|
+
: 'GENERATION_FAILED';
|
|
175
|
+
writeCliError(error, code, json);
|
|
176
|
+
process.exitCode = 1;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
void main();
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,10 +1,32 @@
|
|
|
1
|
-
import type { InitOptions } from './scaffold.js';
|
|
2
|
-
|
|
1
|
+
import type { AddOptions, InitOptions } from './scaffold.js';
|
|
2
|
+
import { type NativeStylingLibrary } from './ui.js';
|
|
3
|
+
import type { UiAddOptions } from './scaffold.js';
|
|
4
|
+
export declare const USAGE = "\nanhedral new <directory> [modules...] [--ui <components>] [--native-styling <nativewind|uniwind>] [--toolchain <latest|stable>] [--skip-install] [--dry-run] [--json] [--verbose]\nanhedral init [modules...] [--ui <components>] [--native-styling <nativewind|uniwind>] [--toolchain <latest|stable>] [--skip-install] [--dry-run] [--json] [--verbose]\nanhedral add <module...> [--skip-install] [--dry-run] [--json] [--verbose]\nanhedral ui add <component...> [--target <client>] [--skip-install] [--dry-run] [--json] [--verbose]\nanhedral upgrade [--skip-install] [--dry-run] [--json] [--verbose]\nanhedral doctor [--json] [--verbose]\nanhedral --version\n\nCommands:\n anhedral new my-app\n Generate the complete TypeScript stack in a new directory. With no module flags, every module is included.\n anhedral new my-app --web --api --db --auth\n Generate a web app, Fastify API, shared database package, and auth wiring.\n anhedral init --web --api --db --auth\n Generate the same readable workspace in the current empty directory.\n anhedral add mobile extension\n Add missing modules to an existing Anhedral project.\n anhedral ui add button dialog --target mobile\n Add React Native Reusables components to Expo. DOM clients use shadcn/ui.\n anhedral upgrade\n Transactionally upgrade a supported older Anhedral project before adding modules.\n";
|
|
5
|
+
export type NewProjectRequest = {
|
|
6
|
+
readonly directory: string;
|
|
7
|
+
readonly moduleArgs: readonly string[];
|
|
8
|
+
};
|
|
9
|
+
export declare function parseNewProjectRequest(args: readonly string[]): NewProjectRequest;
|
|
10
|
+
export declare const APP_MODULES: readonly ["web", "mobile", "api", "desktop", "extension"];
|
|
11
|
+
export declare const FEATURE_MODULES: readonly ["db", "auth", "billing", "storage", "native-subscriptions", "electron-updater"];
|
|
12
|
+
export type AppModule = (typeof APP_MODULES)[number];
|
|
13
|
+
export type FeatureModule = (typeof FEATURE_MODULES)[number];
|
|
14
|
+
export type SupportedModule = AppModule | FeatureModule;
|
|
3
15
|
export type ParsedFlags = {
|
|
4
16
|
toolchain?: string;
|
|
5
17
|
skipInstall?: boolean;
|
|
18
|
+
dryRun?: boolean;
|
|
19
|
+
json?: boolean;
|
|
20
|
+
verbose?: boolean;
|
|
21
|
+
uiComponents: string[];
|
|
22
|
+
nativeStyling?: NativeStylingLibrary;
|
|
23
|
+
modules: Set<SupportedModule>;
|
|
6
24
|
};
|
|
7
25
|
export declare function parseCli(args: string[]): ParsedFlags;
|
|
26
|
+
export declare function normalizeModuleName(raw: string): SupportedModule | null;
|
|
8
27
|
export declare function deriveProjectName(cwd: string): string;
|
|
9
28
|
export declare function deriveDisplayName(cwd: string): string;
|
|
10
29
|
export declare function buildOptions(flags: ParsedFlags): InitOptions;
|
|
30
|
+
export declare function buildOptionsForRoot(flags: ParsedFlags, root: string): InitOptions;
|
|
31
|
+
export declare function parseUiAddOptions(args: readonly string[]): UiAddOptions;
|
|
32
|
+
export declare function buildAddOptions(modules: string[], flags: ParsedFlags): AddOptions;
|
package/dist/cli.js
CHANGED
|
@@ -1,24 +1,95 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { env } from 'node:process';
|
|
3
3
|
import { TOOLCHAIN_CHANNELS, resolveToolchainChannel } from './toolchain.js';
|
|
4
|
+
import { resolveModules } from './architecture/modules.js';
|
|
5
|
+
import { packageNameFromText } from './render.js';
|
|
6
|
+
import { isNativeStylingLibrary, isUiTarget, parseUiComponentList, } from './ui.js';
|
|
4
7
|
export const USAGE = `
|
|
5
|
-
anhedral
|
|
8
|
+
anhedral new <directory> [modules...] [--ui <components>] [--native-styling <nativewind|uniwind>] [--toolchain <latest|stable>] [--skip-install] [--dry-run] [--json] [--verbose]
|
|
9
|
+
anhedral init [modules...] [--ui <components>] [--native-styling <nativewind|uniwind>] [--toolchain <latest|stable>] [--skip-install] [--dry-run] [--json] [--verbose]
|
|
10
|
+
anhedral add <module...> [--skip-install] [--dry-run] [--json] [--verbose]
|
|
11
|
+
anhedral ui add <component...> [--target <client>] [--skip-install] [--dry-run] [--json] [--verbose]
|
|
12
|
+
anhedral upgrade [--skip-install] [--dry-run] [--json] [--verbose]
|
|
13
|
+
anhedral doctor [--json] [--verbose]
|
|
14
|
+
anhedral --version
|
|
6
15
|
|
|
7
16
|
Commands:
|
|
8
|
-
anhedral
|
|
9
|
-
|
|
17
|
+
anhedral new my-app
|
|
18
|
+
Generate the complete TypeScript stack in a new directory. With no module flags, every module is included.
|
|
19
|
+
anhedral new my-app --web --api --db --auth
|
|
20
|
+
Generate a web app, Fastify API, shared database package, and auth wiring.
|
|
21
|
+
anhedral init --web --api --db --auth
|
|
22
|
+
Generate the same readable workspace in the current empty directory.
|
|
23
|
+
anhedral add mobile extension
|
|
24
|
+
Add missing modules to an existing Anhedral project.
|
|
25
|
+
anhedral ui add button dialog --target mobile
|
|
26
|
+
Add React Native Reusables components to Expo. DOM clients use shadcn/ui.
|
|
27
|
+
anhedral upgrade
|
|
28
|
+
Transactionally upgrade a supported older Anhedral project before adding modules.
|
|
10
29
|
`;
|
|
30
|
+
export function parseNewProjectRequest(args) {
|
|
31
|
+
const [directory, ...moduleArgs] = args;
|
|
32
|
+
if (!directory || directory.startsWith('--'))
|
|
33
|
+
throw new Error('anhedral new requires a destination directory before module flags');
|
|
34
|
+
return Object.freeze({ directory, moduleArgs: Object.freeze(moduleArgs) });
|
|
35
|
+
}
|
|
36
|
+
export const APP_MODULES = ['web', 'mobile', 'api', 'desktop', 'extension'];
|
|
37
|
+
export const FEATURE_MODULES = ['db', 'auth', 'billing', 'storage', 'native-subscriptions', 'electron-updater'];
|
|
11
38
|
export function parseCli(args) {
|
|
12
|
-
const flags = {};
|
|
39
|
+
const flags = { modules: new Set(), uiComponents: [] };
|
|
13
40
|
for (let index = 0; index < args.length; index += 1) {
|
|
14
41
|
const token = args[index];
|
|
15
42
|
if (!token.startsWith('--')) {
|
|
16
|
-
|
|
43
|
+
const moduleName = normalizeModuleName(token);
|
|
44
|
+
if (moduleName) {
|
|
45
|
+
flags.modules.add(moduleName);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`Unexpected argument: ${token}. Use module names, module flags, --toolchain, or --skip-install`);
|
|
17
49
|
}
|
|
18
50
|
if (token === '--skip-install') {
|
|
19
51
|
flags.skipInstall = true;
|
|
20
52
|
continue;
|
|
21
53
|
}
|
|
54
|
+
if (token === '--dry-run') {
|
|
55
|
+
flags.dryRun = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (token === '--json') {
|
|
59
|
+
flags.json = true;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (token === '--verbose') {
|
|
63
|
+
flags.verbose = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (token === '--ui') {
|
|
67
|
+
const value = args[index + 1];
|
|
68
|
+
if (!value || value.startsWith('--'))
|
|
69
|
+
throw new Error('Missing value for --ui');
|
|
70
|
+
flags.uiComponents.push(...parseUiComponentList(value));
|
|
71
|
+
index += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (token.startsWith('--ui=')) {
|
|
75
|
+
flags.uiComponents.push(...parseUiComponentList(token.slice('--ui='.length)));
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (token === '--native-styling') {
|
|
79
|
+
const value = args[index + 1];
|
|
80
|
+
if (!isNativeStylingLibrary(value))
|
|
81
|
+
throw new Error('--native-styling must be nativewind or uniwind');
|
|
82
|
+
flags.nativeStyling = value;
|
|
83
|
+
index += 1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (token.startsWith('--native-styling=')) {
|
|
87
|
+
const value = token.slice('--native-styling='.length);
|
|
88
|
+
if (!isNativeStylingLibrary(value))
|
|
89
|
+
throw new Error('--native-styling must be nativewind or uniwind');
|
|
90
|
+
flags.nativeStyling = value;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
22
93
|
if (token === '--toolchain') {
|
|
23
94
|
const value = args[index + 1];
|
|
24
95
|
if (!value || value.startsWith('--')) {
|
|
@@ -28,40 +99,137 @@ export function parseCli(args) {
|
|
|
28
99
|
index += 1;
|
|
29
100
|
continue;
|
|
30
101
|
}
|
|
102
|
+
if (token.startsWith('--toolchain=')) {
|
|
103
|
+
const value = token.slice('--toolchain='.length);
|
|
104
|
+
if (!value) {
|
|
105
|
+
throw new Error('Missing value for --toolchain');
|
|
106
|
+
}
|
|
107
|
+
flags.toolchain = value;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const moduleName = normalizeModuleName(token.slice(2));
|
|
111
|
+
if (moduleName) {
|
|
112
|
+
flags.modules.add(moduleName);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
31
115
|
throw new Error(`Unknown flag: ${token}`);
|
|
32
116
|
}
|
|
33
117
|
return flags;
|
|
34
118
|
}
|
|
119
|
+
export function normalizeModuleName(raw) {
|
|
120
|
+
if (APP_MODULES.includes(raw))
|
|
121
|
+
return raw;
|
|
122
|
+
if (FEATURE_MODULES.includes(raw))
|
|
123
|
+
return raw;
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
35
126
|
export function deriveProjectName(cwd) {
|
|
36
|
-
|
|
37
|
-
const sanitized = base
|
|
38
|
-
.trim()
|
|
39
|
-
.toLowerCase()
|
|
40
|
-
.replace(/[^a-z0-9_-]+/g, '-')
|
|
41
|
-
.replace(/^-+|-+$/g, '');
|
|
42
|
-
return sanitized || 'anhedral-app';
|
|
127
|
+
return packageNameFromText(path.basename(cwd));
|
|
43
128
|
}
|
|
44
129
|
export function deriveDisplayName(cwd) {
|
|
45
130
|
const base = path.basename(cwd).trim();
|
|
46
131
|
return base || 'Anhedral App';
|
|
47
132
|
}
|
|
48
133
|
export function buildOptions(flags) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
134
|
+
return buildOptionsForRoot(flags, process.cwd());
|
|
135
|
+
}
|
|
136
|
+
export function buildOptionsForRoot(flags, root) {
|
|
137
|
+
const resolvedRoot = path.resolve(root);
|
|
138
|
+
const projectName = deriveProjectName(resolvedRoot);
|
|
139
|
+
const displayName = deriveDisplayName(resolvedRoot);
|
|
52
140
|
if (flags.toolchain != null && !TOOLCHAIN_CHANNELS.includes(flags.toolchain)) {
|
|
53
141
|
throw new Error(`--toolchain must be one of: ${TOOLCHAIN_CHANNELS.join(', ')}`);
|
|
54
142
|
}
|
|
143
|
+
const requestedModules = flags.modules.size === 0
|
|
144
|
+
? [...APP_MODULES, ...FEATURE_MODULES]
|
|
145
|
+
: [...flags.modules];
|
|
146
|
+
const resolution = resolveModules(requestedModules);
|
|
55
147
|
return {
|
|
56
148
|
projectName,
|
|
57
149
|
displayName,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
orm: 'drizzle',
|
|
62
|
-
storage: 'r2',
|
|
63
|
-
api: 'fastify',
|
|
150
|
+
modules: [...resolution.requestedModules],
|
|
151
|
+
uiComponents: Array.from(new Set(flags.uiComponents)),
|
|
152
|
+
nativeStyling: flags.nativeStyling ?? 'nativewind',
|
|
64
153
|
skipInstall: flags.skipInstall === true || env.ANHEDRAL_SKIP_INSTALL === '1',
|
|
154
|
+
dryRun: flags.dryRun === true,
|
|
155
|
+
json: flags.json === true,
|
|
65
156
|
toolchainChannel: resolveToolchainChannel(flags.toolchain ?? env.ANHEDRAL_TOOLCHAIN),
|
|
157
|
+
rootDirectory: resolvedRoot,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
export function parseUiAddOptions(args) {
|
|
161
|
+
const components = [];
|
|
162
|
+
const targets = [];
|
|
163
|
+
let skipInstall = env.ANHEDRAL_SKIP_INSTALL === '1';
|
|
164
|
+
let dryRun = false;
|
|
165
|
+
let json = false;
|
|
166
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
167
|
+
const token = args[index];
|
|
168
|
+
if (!token.startsWith('--')) {
|
|
169
|
+
components.push(...parseUiComponentList(token));
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (token === '--target') {
|
|
173
|
+
const value = args[index + 1];
|
|
174
|
+
if (!isUiTarget(value))
|
|
175
|
+
throw new Error(`--target must be one of: web, mobile, desktop, extension`);
|
|
176
|
+
targets.push(value);
|
|
177
|
+
index += 1;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (token.startsWith('--target=')) {
|
|
181
|
+
const value = token.slice('--target='.length);
|
|
182
|
+
if (!isUiTarget(value))
|
|
183
|
+
throw new Error(`--target must be one of: web, mobile, desktop, extension`);
|
|
184
|
+
targets.push(value);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (token === '--skip-install') {
|
|
188
|
+
skipInstall = true;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (token === '--dry-run') {
|
|
192
|
+
dryRun = true;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (token === '--json') {
|
|
196
|
+
json = true;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (token === '--verbose') {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
throw new Error(`Unknown UI option: ${token}`);
|
|
203
|
+
}
|
|
204
|
+
if (components.length === 0)
|
|
205
|
+
throw new Error('anhedral ui add requires at least one component');
|
|
206
|
+
return {
|
|
207
|
+
components: Array.from(new Set(components)),
|
|
208
|
+
targets: Array.from(new Set(targets)),
|
|
209
|
+
skipInstall,
|
|
210
|
+
dryRun,
|
|
211
|
+
json,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
export function buildAddOptions(modules, flags) {
|
|
215
|
+
const requestedModules = [...modules, ...flags.modules];
|
|
216
|
+
if (requestedModules.length === 0) {
|
|
217
|
+
throw new Error('anhedral add requires at least one module');
|
|
218
|
+
}
|
|
219
|
+
const normalizedModules = requestedModules.map((moduleName) => {
|
|
220
|
+
const normalized = normalizeModuleName(moduleName);
|
|
221
|
+
if (!normalized) {
|
|
222
|
+
throw new Error(`Unknown module: ${moduleName}`);
|
|
223
|
+
}
|
|
224
|
+
return normalized;
|
|
225
|
+
});
|
|
226
|
+
return {
|
|
227
|
+
modules: Array.from(new Set(normalizedModules)),
|
|
228
|
+
skipInstall: flags.skipInstall === true || env.ANHEDRAL_SKIP_INSTALL === '1',
|
|
229
|
+
dryRun: flags.dryRun === true,
|
|
230
|
+
json: flags.json === true,
|
|
231
|
+
...((flags.toolchain ?? env.ANHEDRAL_TOOLCHAIN) != null
|
|
232
|
+
? { toolchainChannel: resolveToolchainChannel(flags.toolchain ?? env.ANHEDRAL_TOOLCHAIN) }
|
|
233
|
+
: {}),
|
|
66
234
|
};
|
|
67
235
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CompositionModel } from '../architecture/contributions.js';
|
|
2
|
+
export type RootEnvironmentContext = {
|
|
3
|
+
readonly corsOrigins: readonly string[];
|
|
4
|
+
};
|
|
5
|
+
export declare function composeRootEnvironment(model: CompositionModel, context: RootEnvironmentContext): readonly string[];
|
|
6
|
+
export declare function composeVercelCrons(model: CompositionModel): readonly Record<string, string>[];
|