anhedral 0.4.1 → 0.6.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 +29 -256
- package/assets/anhedral-cli-init-technical.svg +322 -0
- package/assets/anhedral-cli-init.svg +257 -173
- package/dist/bin.js +28 -162
- package/dist/dependencies.d.ts +7 -56
- package/dist/dependencies.js +96 -305
- package/dist/index.d.ts +2 -7
- package/dist/index.js +1 -4
- package/dist/{templates → platforms}/desktop.d.ts +1 -1
- package/dist/{templates → platforms}/desktop.js +28 -266
- package/dist/{templates → platforms}/extension.d.ts +1 -1
- package/dist/platforms/extension.js +401 -0
- package/dist/{templates → platforms}/mobile.d.ts +1 -1
- package/dist/platforms/mobile.js +232 -0
- package/dist/project.d.ts +4 -0
- package/dist/project.js +1 -0
- package/dist/standard-products.d.ts +2 -0
- package/dist/standard-products.js +6 -0
- package/dist/standard.d.ts +18 -0
- package/dist/standard.js +578 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/application-stack-standard.md +700 -0
- package/package.json +34 -49
- package/CHANGELOG.md +0 -132
- package/CONTRIBUTING.md +0 -73
- package/SECURITY.md +0 -37
- package/SKILL.md +0 -244
- package/assets/images/svg/ably-symbol-color.svg +0 -8
- package/bin/anhedral.js +0 -2
- package/dist/architecture/contributions.d.ts +0 -28
- package/dist/architecture/contributions.js +0 -125
- package/dist/architecture/index.d.ts +0 -12
- package/dist/architecture/index.js +0 -6
- package/dist/architecture/manifest.d.ts +0 -53
- package/dist/architecture/manifest.js +0 -283
- package/dist/architecture/modules.d.ts +0 -37
- package/dist/architecture/modules.js +0 -184
- package/dist/architecture/plan.d.ts +0 -58
- package/dist/architecture/plan.js +0 -145
- package/dist/architecture/templates.d.ts +0 -10
- package/dist/architecture/templates.js +0 -23
- package/dist/cli.d.ts +0 -32
- package/dist/cli.js +0 -235
- package/dist/composers/root.d.ts +0 -6
- package/dist/composers/root.js +0 -35
- package/dist/print.d.ts +0 -8
- package/dist/print.js +0 -48
- package/dist/prompts.d.ts +0 -5
- package/dist/prompts.js +0 -19
- package/dist/scaffold.d.ts +0 -76
- package/dist/scaffold.js +0 -1895
- package/dist/template-source.d.ts +0 -20
- package/dist/template-source.js +0 -180
- package/dist/templates/api.d.ts +0 -2
- package/dist/templates/api.js +0 -3122
- package/dist/templates/assets-private-proxy.d.ts +0 -3
- package/dist/templates/assets-private-proxy.js +0 -277
- package/dist/templates/electron-updater.d.ts +0 -3
- package/dist/templates/electron-updater.js +0 -358
- package/dist/templates/extension.js +0 -732
- package/dist/templates/mobile.js +0 -720
- package/dist/templates/shared.d.ts +0 -2
- package/dist/templates/shared.js +0 -520
- package/dist/templates/web.d.ts +0 -2
- package/dist/templates/web.js +0 -412
- package/dist/toolchain.d.ts +0 -3
- package/dist/toolchain.js +0 -8
- package/dist/ui.d.ts +0 -33
- package/dist/ui.js +0 -199
- package/dist/workspace-config.d.ts +0 -11
- package/dist/workspace-config.js +0 -372
- package/docs/conventions.md +0 -92
- package/docs/master-stack-map.md +0 -199
- package/docs/output-tree-contract.md +0 -482
- package/docs/references/manual-scaffolding.md +0 -403
- package/docs/references/provisioning.md +0 -220
- package/scripts/render-master-stack-map.mjs +0 -510
- package/templates/api-fastify/apps/api/tsconfig.json +0 -20
- package/templates/catalog.json +0 -23
- package/templates/db-drizzle/packages/db/tsconfig.json +0 -14
- package/templates/desktop-electron/apps/desktop/tsconfig.json +0 -37
- package/templates/extension-wxt/apps/extension/tsconfig.json +0 -41
- package/templates/mobile-expo/apps/mobile/expo-env.d.ts +0 -1
- package/templates/web-next/apps/web/next-env.d.ts +0 -6
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
export const MODULE_IDS = [
|
|
2
|
-
'web',
|
|
3
|
-
'mobile',
|
|
4
|
-
'api',
|
|
5
|
-
'desktop',
|
|
6
|
-
'extension',
|
|
7
|
-
'db',
|
|
8
|
-
'auth',
|
|
9
|
-
'billing',
|
|
10
|
-
'storage',
|
|
11
|
-
'native-subscriptions',
|
|
12
|
-
'electron-updater',
|
|
13
|
-
];
|
|
14
|
-
export class ModuleRegistryError extends Error {
|
|
15
|
-
code;
|
|
16
|
-
modules;
|
|
17
|
-
constructor(code, message, modules = []) {
|
|
18
|
-
super(message);
|
|
19
|
-
this.code = code;
|
|
20
|
-
this.modules = modules;
|
|
21
|
-
this.name = 'ModuleRegistryError';
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
export class ModuleResolutionError extends Error {
|
|
25
|
-
code;
|
|
26
|
-
modules;
|
|
27
|
-
constructor(code, message, modules) {
|
|
28
|
-
super(message);
|
|
29
|
-
this.code = code;
|
|
30
|
-
this.modules = modules;
|
|
31
|
-
this.name = 'ModuleResolutionError';
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
const MODULE_ID_SET = new Set(MODULE_IDS);
|
|
35
|
-
const MODULE_ORDER = new Map(MODULE_IDS.map((id, index) => [id, index]));
|
|
36
|
-
export function isModuleId(value) {
|
|
37
|
-
return typeof value === 'string' && MODULE_ID_SET.has(value);
|
|
38
|
-
}
|
|
39
|
-
function compareModules(left, right) {
|
|
40
|
-
return (MODULE_ORDER.get(left) ?? Number.MAX_SAFE_INTEGER)
|
|
41
|
-
- (MODULE_ORDER.get(right) ?? Number.MAX_SAFE_INTEGER);
|
|
42
|
-
}
|
|
43
|
-
function normalizeModuleList(values) {
|
|
44
|
-
return Object.freeze([...new Set(values)].sort(compareModules));
|
|
45
|
-
}
|
|
46
|
-
function freezeDefinition(definition) {
|
|
47
|
-
return Object.freeze({
|
|
48
|
-
id: definition.id,
|
|
49
|
-
kind: definition.kind,
|
|
50
|
-
requires: normalizeModuleList(definition.requires),
|
|
51
|
-
conflicts: normalizeModuleList(definition.conflicts),
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
function assertNoDependencyCycles(registry) {
|
|
55
|
-
const visited = new Set();
|
|
56
|
-
const active = new Set();
|
|
57
|
-
const stack = [];
|
|
58
|
-
const visit = (moduleId) => {
|
|
59
|
-
if (visited.has(moduleId))
|
|
60
|
-
return;
|
|
61
|
-
if (active.has(moduleId)) {
|
|
62
|
-
const cycleStart = stack.indexOf(moduleId);
|
|
63
|
-
const cycle = [...stack.slice(cycleStart), moduleId];
|
|
64
|
-
throw new ModuleRegistryError('DEPENDENCY_CYCLE', `Module dependency cycle detected: ${cycle.join(' -> ')}`, Object.freeze(cycle));
|
|
65
|
-
}
|
|
66
|
-
active.add(moduleId);
|
|
67
|
-
stack.push(moduleId);
|
|
68
|
-
for (const required of registry[moduleId].requires)
|
|
69
|
-
visit(required);
|
|
70
|
-
stack.pop();
|
|
71
|
-
active.delete(moduleId);
|
|
72
|
-
visited.add(moduleId);
|
|
73
|
-
};
|
|
74
|
-
for (const moduleId of MODULE_IDS)
|
|
75
|
-
visit(moduleId);
|
|
76
|
-
}
|
|
77
|
-
export function createModuleRegistry(definitions) {
|
|
78
|
-
const registry = {};
|
|
79
|
-
for (const definition of definitions) {
|
|
80
|
-
if (!isModuleId(definition.id)) {
|
|
81
|
-
throw new ModuleRegistryError('UNKNOWN_MODULE_REFERENCE', `Unknown module definition: ${String(definition.id)}`, Object.freeze([String(definition.id)]));
|
|
82
|
-
}
|
|
83
|
-
if (registry[definition.id]) {
|
|
84
|
-
throw new ModuleRegistryError('DUPLICATE_MODULE_DEFINITION', `Duplicate module definition: ${definition.id}`, Object.freeze([definition.id]));
|
|
85
|
-
}
|
|
86
|
-
for (const required of definition.requires) {
|
|
87
|
-
if (!isModuleId(required)) {
|
|
88
|
-
throw new ModuleRegistryError('UNKNOWN_MODULE_REFERENCE', `Module ${definition.id} requires unknown module ${String(required)}`, Object.freeze([definition.id, String(required)]));
|
|
89
|
-
}
|
|
90
|
-
if (required === definition.id) {
|
|
91
|
-
throw new ModuleRegistryError('SELF_DEPENDENCY', `Module ${definition.id} cannot require itself`, Object.freeze([definition.id]));
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
for (const conflict of definition.conflicts) {
|
|
95
|
-
if (!isModuleId(conflict)) {
|
|
96
|
-
throw new ModuleRegistryError('UNKNOWN_MODULE_REFERENCE', `Module ${definition.id} conflicts with unknown module ${String(conflict)}`, Object.freeze([definition.id, String(conflict)]));
|
|
97
|
-
}
|
|
98
|
-
if (conflict === definition.id) {
|
|
99
|
-
throw new ModuleRegistryError('SELF_CONFLICT', `Module ${definition.id} cannot conflict with itself`, Object.freeze([definition.id]));
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
registry[definition.id] = freezeDefinition(definition);
|
|
103
|
-
}
|
|
104
|
-
const missing = MODULE_IDS.filter((moduleId) => !registry[moduleId]);
|
|
105
|
-
if (missing.length > 0) {
|
|
106
|
-
throw new ModuleRegistryError('MISSING_MODULE_DEFINITION', `Missing module definitions: ${missing.join(', ')}`, Object.freeze(missing));
|
|
107
|
-
}
|
|
108
|
-
const completeRegistry = Object.freeze(registry);
|
|
109
|
-
assertNoDependencyCycles(completeRegistry);
|
|
110
|
-
return completeRegistry;
|
|
111
|
-
}
|
|
112
|
-
const DEFAULT_MODULE_DEFINITION_INPUTS = [
|
|
113
|
-
{ id: 'web', kind: 'app', requires: [], conflicts: [] },
|
|
114
|
-
{ id: 'mobile', kind: 'app', requires: [], conflicts: [] },
|
|
115
|
-
{ id: 'api', kind: 'app', requires: [], conflicts: [] },
|
|
116
|
-
{ id: 'desktop', kind: 'app', requires: [], conflicts: [] },
|
|
117
|
-
{ id: 'extension', kind: 'app', requires: [], conflicts: [] },
|
|
118
|
-
{ id: 'db', kind: 'feature', requires: [], conflicts: [] },
|
|
119
|
-
{ id: 'auth', kind: 'feature', requires: ['api', 'db'], conflicts: [] },
|
|
120
|
-
{ id: 'billing', kind: 'feature', requires: ['auth'], conflicts: [] },
|
|
121
|
-
{ id: 'storage', kind: 'feature', requires: ['auth'], conflicts: [] },
|
|
122
|
-
{ id: 'native-subscriptions', kind: 'feature', requires: ['mobile', 'billing'], conflicts: [] },
|
|
123
|
-
{ id: 'electron-updater', kind: 'feature', requires: ['desktop'], conflicts: [] },
|
|
124
|
-
];
|
|
125
|
-
export const DEFAULT_MODULE_DEFINITIONS = Object.freeze(DEFAULT_MODULE_DEFINITION_INPUTS.map(freezeDefinition));
|
|
126
|
-
export const DEFAULT_MODULE_REGISTRY = createModuleRegistry(DEFAULT_MODULE_DEFINITIONS);
|
|
127
|
-
export function resolveModules(requested, registry = DEFAULT_MODULE_REGISTRY) {
|
|
128
|
-
const unknown = [...new Set(requested.filter((moduleId) => !isModuleId(moduleId)))].sort();
|
|
129
|
-
if (unknown.length > 0) {
|
|
130
|
-
throw new ModuleResolutionError('UNKNOWN_MODULE', `Unknown modules: ${unknown.join(', ')}`, Object.freeze(unknown));
|
|
131
|
-
}
|
|
132
|
-
const requestedModules = normalizeModuleList(requested);
|
|
133
|
-
const resolved = new Set();
|
|
134
|
-
const visiting = new Set();
|
|
135
|
-
const ordered = [];
|
|
136
|
-
const visit = (moduleId) => {
|
|
137
|
-
if (resolved.has(moduleId))
|
|
138
|
-
return;
|
|
139
|
-
if (visiting.has(moduleId)) {
|
|
140
|
-
throw new ModuleRegistryError('DEPENDENCY_CYCLE', `Module dependency cycle reached while resolving ${moduleId}`, Object.freeze([moduleId]));
|
|
141
|
-
}
|
|
142
|
-
visiting.add(moduleId);
|
|
143
|
-
for (const required of registry[moduleId].requires)
|
|
144
|
-
visit(required);
|
|
145
|
-
visiting.delete(moduleId);
|
|
146
|
-
resolved.add(moduleId);
|
|
147
|
-
ordered.push(moduleId);
|
|
148
|
-
};
|
|
149
|
-
for (const moduleId of requestedModules)
|
|
150
|
-
visit(moduleId);
|
|
151
|
-
const conflictPairs = new Set();
|
|
152
|
-
for (const moduleId of ordered) {
|
|
153
|
-
for (const conflict of registry[moduleId].conflicts) {
|
|
154
|
-
if (!resolved.has(conflict))
|
|
155
|
-
continue;
|
|
156
|
-
const pair = [moduleId, conflict].sort(compareModules);
|
|
157
|
-
conflictPairs.add(`${pair[0]}\0${pair[1]}`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
if (conflictPairs.size > 0) {
|
|
161
|
-
const conflicts = [...conflictPairs]
|
|
162
|
-
.map((pair) => pair.split('\0'))
|
|
163
|
-
.sort(([leftA, leftB], [rightA, rightB]) => compareModules(leftA, rightA) || compareModules(leftB, rightB));
|
|
164
|
-
const modules = Object.freeze([...new Set(conflicts.flat())].sort(compareModules));
|
|
165
|
-
throw new ModuleResolutionError('MODULE_CONFLICT', `Conflicting modules: ${conflicts.map(([left, right]) => `${left} + ${right}`).join(', ')}`, modules);
|
|
166
|
-
}
|
|
167
|
-
// Resolution must have one stable representation regardless of whether a
|
|
168
|
-
// dependency was requested directly or discovered while walking another
|
|
169
|
-
// module. Without this final normalization, adding a module to an existing
|
|
170
|
-
// project could produce a different manifest order than creating the same
|
|
171
|
-
// topology directly (for example, adding electron-updater after db/auth).
|
|
172
|
-
const resolvedModules = normalizeModuleList(ordered);
|
|
173
|
-
const requestedSet = new Set(requestedModules);
|
|
174
|
-
const dependencyAddedModules = resolvedModules.filter((moduleId) => !requestedSet.has(moduleId));
|
|
175
|
-
const dependencyEdges = resolvedModules.flatMap((moduleId) => registry[moduleId].requires
|
|
176
|
-
.filter((required) => resolved.has(required))
|
|
177
|
-
.map((required) => Object.freeze({ module: moduleId, requires: required })));
|
|
178
|
-
return Object.freeze({
|
|
179
|
-
requestedModules,
|
|
180
|
-
resolvedModules,
|
|
181
|
-
dependencyAddedModules: Object.freeze(dependencyAddedModules),
|
|
182
|
-
dependencyEdges: Object.freeze(dependencyEdges),
|
|
183
|
-
});
|
|
184
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { type DependencyEdge, type ModuleId, type ModuleRegistry } from './modules.js';
|
|
2
|
-
export declare const FILE_OWNERSHIP_CLASSES: readonly ["managed", "mergeable", "user"];
|
|
3
|
-
export type FileOwnershipClass = (typeof FILE_OWNERSHIP_CLASSES)[number];
|
|
4
|
-
export type FileOwner = ModuleId | 'root';
|
|
5
|
-
export type GenerationOperation = 'init' | 'add';
|
|
6
|
-
export type FileWritePolicy = 'replace-if-unmodified' | 'structural-merge' | 'create-only';
|
|
7
|
-
export type PlanFileInput = {
|
|
8
|
-
readonly path: string;
|
|
9
|
-
readonly ownership: FileOwnershipClass;
|
|
10
|
-
readonly content: string;
|
|
11
|
-
};
|
|
12
|
-
export type ModulePlanContribution = {
|
|
13
|
-
readonly module: FileOwner;
|
|
14
|
-
readonly files: readonly PlanFileInput[];
|
|
15
|
-
};
|
|
16
|
-
export type PlannedFile = {
|
|
17
|
-
readonly path: string;
|
|
18
|
-
readonly owner: FileOwner;
|
|
19
|
-
readonly ownership: FileOwnershipClass;
|
|
20
|
-
readonly writePolicy: FileWritePolicy;
|
|
21
|
-
readonly content: string;
|
|
22
|
-
readonly contentHash: string;
|
|
23
|
-
};
|
|
24
|
-
export type GenerationPlan = {
|
|
25
|
-
readonly planVersion: 1;
|
|
26
|
-
readonly operation: GenerationOperation;
|
|
27
|
-
readonly requestedModules: readonly ModuleId[];
|
|
28
|
-
readonly resolvedModules: readonly ModuleId[];
|
|
29
|
-
readonly dependencyAddedModules: readonly ModuleId[];
|
|
30
|
-
readonly dependencyEdges: readonly DependencyEdge[];
|
|
31
|
-
readonly files: readonly PlannedFile[];
|
|
32
|
-
readonly fingerprint: string;
|
|
33
|
-
};
|
|
34
|
-
export type BuildGenerationPlanInput = {
|
|
35
|
-
readonly operation: GenerationOperation;
|
|
36
|
-
readonly requestedModules: readonly string[];
|
|
37
|
-
readonly contributions?: readonly ModulePlanContribution[];
|
|
38
|
-
readonly registry?: ModuleRegistry;
|
|
39
|
-
};
|
|
40
|
-
export type PlanBuildErrorCode = 'INVALID_OPERATION' | 'INVALID_PLAN_PATH' | 'INVALID_FILE_OWNER' | 'INVALID_FILE_OWNERSHIP' | 'INVALID_FILE_CONTENT' | 'UNRESOLVED_CONTRIBUTION' | 'DUPLICATE_OUTPUT_PATH';
|
|
41
|
-
export declare class PlanBuildError extends Error {
|
|
42
|
-
readonly code: PlanBuildErrorCode;
|
|
43
|
-
readonly paths: readonly string[];
|
|
44
|
-
constructor(code: PlanBuildErrorCode, message: string, paths?: readonly string[]);
|
|
45
|
-
}
|
|
46
|
-
export declare function isFileOwnershipClass(value: unknown): value is FileOwnershipClass;
|
|
47
|
-
export declare function isSha256(value: unknown): value is string;
|
|
48
|
-
/**
|
|
49
|
-
* Hash rendered text or exact filesystem bytes.
|
|
50
|
-
*
|
|
51
|
-
* Strings are encoded as UTF-8 for deterministic generation-plan fingerprints.
|
|
52
|
-
* Callers hashing an existing file must pass its Buffer/Uint8Array so malformed
|
|
53
|
-
* byte sequences cannot collapse to the same Unicode replacement characters.
|
|
54
|
-
*/
|
|
55
|
-
export declare function hashContent(content: string | Uint8Array): string;
|
|
56
|
-
export declare function writePolicyForOwnership(ownership: FileOwnershipClass): FileWritePolicy;
|
|
57
|
-
export declare function normalizePlanPath(value: string): string;
|
|
58
|
-
export declare function buildGenerationPlan(input: BuildGenerationPlanInput): GenerationPlan;
|
|
@@ -1,145 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
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[];
|
|
@@ -1,23 +0,0 @@
|
|
|
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/cli.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
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;
|
|
15
|
-
export type ParsedFlags = {
|
|
16
|
-
toolchain?: string;
|
|
17
|
-
skipInstall?: boolean;
|
|
18
|
-
dryRun?: boolean;
|
|
19
|
-
json?: boolean;
|
|
20
|
-
verbose?: boolean;
|
|
21
|
-
uiComponents: string[];
|
|
22
|
-
nativeStyling?: NativeStylingLibrary;
|
|
23
|
-
modules: Set<SupportedModule>;
|
|
24
|
-
};
|
|
25
|
-
export declare function parseCli(args: string[]): ParsedFlags;
|
|
26
|
-
export declare function normalizeModuleName(raw: string): SupportedModule | null;
|
|
27
|
-
export declare function deriveProjectName(cwd: string): string;
|
|
28
|
-
export declare function deriveDisplayName(cwd: string): string;
|
|
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;
|