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,20 @@
|
|
|
1
|
+
import type { ModuleId } from './architecture/modules.js';
|
|
2
|
+
import { type TemplateId, type TemplateProvenanceMap } from './architecture/templates.js';
|
|
3
|
+
export { TEMPLATE_IDS } from './architecture/templates.js';
|
|
4
|
+
export { templateIdsForModules } from './architecture/templates.js';
|
|
5
|
+
export type { TemplateId, TemplateProvenance, TemplateProvenanceMap } from './architecture/templates.js';
|
|
6
|
+
export type TemplateMaterializeRequest = {
|
|
7
|
+
readonly ids: readonly TemplateId[];
|
|
8
|
+
readonly destination: string;
|
|
9
|
+
};
|
|
10
|
+
export interface TemplateSource {
|
|
11
|
+
materialize(request: TemplateMaterializeRequest): TemplateProvenanceMap;
|
|
12
|
+
}
|
|
13
|
+
export declare function hashTemplateDirectory(root: string): string;
|
|
14
|
+
export declare class BundledTemplateSource implements TemplateSource {
|
|
15
|
+
readonly root: string;
|
|
16
|
+
constructor(root?: string);
|
|
17
|
+
materialize(request: TemplateMaterializeRequest): TemplateProvenanceMap;
|
|
18
|
+
}
|
|
19
|
+
export declare function materializeTemplates(destination: string, modules: readonly ModuleId[], source?: TemplateSource): TemplateProvenanceMap;
|
|
20
|
+
export declare function assertTemplateProvenance(modules: readonly ModuleId[], provenance: TemplateProvenanceMap, source?: BundledTemplateSource): void;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { chmodSync, copyFileSync, lstatSync, mkdirSync, readFileSync, readdirSync, } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { isSha256, normalizePlanPath } from './architecture/plan.js';
|
|
6
|
+
import { TEMPLATE_IDS, templateIdsForModules, } from './architecture/templates.js';
|
|
7
|
+
export { TEMPLATE_IDS } from './architecture/templates.js';
|
|
8
|
+
export { templateIdsForModules } from './architecture/templates.js';
|
|
9
|
+
const TEMPLATE_ID_SET = new Set(TEMPLATE_IDS);
|
|
10
|
+
const FORBIDDEN_ENTRY_NAMES = new Set([
|
|
11
|
+
'.git',
|
|
12
|
+
'.next',
|
|
13
|
+
'.output',
|
|
14
|
+
'.turbo',
|
|
15
|
+
'.wxt',
|
|
16
|
+
'dist',
|
|
17
|
+
'node_modules',
|
|
18
|
+
'pnpm-lock.yaml',
|
|
19
|
+
'package-lock.json',
|
|
20
|
+
'yarn.lock',
|
|
21
|
+
]);
|
|
22
|
+
const MAX_TEMPLATE_FILES = 1_000;
|
|
23
|
+
const MAX_TEMPLATE_BYTES = 8 * 1024 * 1024;
|
|
24
|
+
function isRecord(value) {
|
|
25
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
function readCatalog(root) {
|
|
28
|
+
const raw = JSON.parse(readFileSync(path.join(root, 'catalog.json'), 'utf8'));
|
|
29
|
+
if (!isRecord(raw) || raw.version !== 1 || !isRecord(raw.templates)) {
|
|
30
|
+
throw new Error('Invalid bundled template catalog.');
|
|
31
|
+
}
|
|
32
|
+
const actualIds = Object.keys(raw.templates).sort();
|
|
33
|
+
const expectedIds = [...TEMPLATE_IDS].sort();
|
|
34
|
+
if (actualIds.length !== expectedIds.length || actualIds.some((id, index) => id !== expectedIds[index])) {
|
|
35
|
+
throw new Error('Template catalog must define every supported template exactly once.');
|
|
36
|
+
}
|
|
37
|
+
const templates = {};
|
|
38
|
+
for (const id of TEMPLATE_IDS) {
|
|
39
|
+
const entry = raw.templates[id];
|
|
40
|
+
if (!isRecord(entry) || Object.keys(entry).length !== 1 || !isSha256(entry.sha256)) {
|
|
41
|
+
throw new Error(`Invalid template catalog entry: ${id}.`);
|
|
42
|
+
}
|
|
43
|
+
templates[id] = { sha256: entry.sha256 };
|
|
44
|
+
}
|
|
45
|
+
return { version: 1, templates };
|
|
46
|
+
}
|
|
47
|
+
function collectTemplateFiles(root, relativeRoot = '') {
|
|
48
|
+
const absoluteRoot = path.join(root, relativeRoot);
|
|
49
|
+
const files = [];
|
|
50
|
+
for (const entry of readdirSync(absoluteRoot, { withFileTypes: true })) {
|
|
51
|
+
if (FORBIDDEN_ENTRY_NAMES.has(entry.name)) {
|
|
52
|
+
throw new Error(`Forbidden template entry: ${relativeRoot ? `${relativeRoot}/` : ''}${entry.name}`);
|
|
53
|
+
}
|
|
54
|
+
const relativePath = relativeRoot ? `${relativeRoot}/${entry.name}` : entry.name;
|
|
55
|
+
normalizePlanPath(relativePath);
|
|
56
|
+
const absolutePath = path.join(root, relativePath);
|
|
57
|
+
const stat = lstatSync(absolutePath);
|
|
58
|
+
if (stat.isSymbolicLink())
|
|
59
|
+
throw new Error(`Template symbolic links are not supported: ${relativePath}`);
|
|
60
|
+
if (stat.isDirectory())
|
|
61
|
+
files.push(...collectTemplateFiles(root, relativePath));
|
|
62
|
+
else if (stat.isFile())
|
|
63
|
+
files.push(relativePath);
|
|
64
|
+
else
|
|
65
|
+
throw new Error(`Unsupported template entry: ${relativePath}`);
|
|
66
|
+
}
|
|
67
|
+
return files.sort();
|
|
68
|
+
}
|
|
69
|
+
export function hashTemplateDirectory(root) {
|
|
70
|
+
const files = collectTemplateFiles(root);
|
|
71
|
+
if (files.length === 0 || files.length > MAX_TEMPLATE_FILES) {
|
|
72
|
+
throw new Error(`Template file count is outside the supported range: ${files.length}.`);
|
|
73
|
+
}
|
|
74
|
+
const hash = createHash('sha256');
|
|
75
|
+
let totalBytes = 0;
|
|
76
|
+
for (const relativePath of files) {
|
|
77
|
+
const contents = readFileSync(path.join(root, relativePath));
|
|
78
|
+
totalBytes += contents.byteLength;
|
|
79
|
+
if (totalBytes > MAX_TEMPLATE_BYTES)
|
|
80
|
+
throw new Error('Template exceeds the maximum supported size.');
|
|
81
|
+
hash.update(relativePath, 'utf8');
|
|
82
|
+
hash.update('\0');
|
|
83
|
+
hash.update(String(contents.byteLength), 'utf8');
|
|
84
|
+
hash.update('\0');
|
|
85
|
+
hash.update(contents);
|
|
86
|
+
}
|
|
87
|
+
return hash.digest('hex');
|
|
88
|
+
}
|
|
89
|
+
function copyTemplate(sourceRoot, destination) {
|
|
90
|
+
const resolvedDestination = path.resolve(destination);
|
|
91
|
+
try {
|
|
92
|
+
const destinationStat = lstatSync(resolvedDestination);
|
|
93
|
+
if (destinationStat.isSymbolicLink() || !destinationStat.isDirectory()) {
|
|
94
|
+
throw new Error('Template destination must be a regular directory.');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error.code !== 'ENOENT')
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
for (const relativePath of collectTemplateFiles(sourceRoot)) {
|
|
102
|
+
const source = path.join(sourceRoot, relativePath);
|
|
103
|
+
const target = path.resolve(resolvedDestination, relativePath);
|
|
104
|
+
if (!target.startsWith(`${resolvedDestination}${path.sep}`)) {
|
|
105
|
+
throw new Error(`Unsafe template destination: ${relativePath}`);
|
|
106
|
+
}
|
|
107
|
+
const segments = relativePath.split('/');
|
|
108
|
+
let ancestor = resolvedDestination;
|
|
109
|
+
for (const segment of segments.slice(0, -1)) {
|
|
110
|
+
ancestor = path.join(ancestor, segment);
|
|
111
|
+
try {
|
|
112
|
+
const stat = lstatSync(ancestor);
|
|
113
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
114
|
+
throw new Error(`Unsafe template destination ancestor: ${relativePath}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error.code !== 'ENOENT')
|
|
119
|
+
throw error;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
if (lstatSync(target).isSymbolicLink()) {
|
|
125
|
+
throw new Error(`Refusing to overwrite template destination symbolic link: ${relativePath}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (error.code !== 'ENOENT')
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
133
|
+
copyFileSync(source, target);
|
|
134
|
+
chmodSync(target, 0o644);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export class BundledTemplateSource {
|
|
138
|
+
root;
|
|
139
|
+
constructor(root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'templates')) {
|
|
140
|
+
this.root = root;
|
|
141
|
+
}
|
|
142
|
+
materialize(request) {
|
|
143
|
+
const catalog = readCatalog(this.root);
|
|
144
|
+
const result = {};
|
|
145
|
+
for (const id of [...new Set(request.ids)].sort()) {
|
|
146
|
+
if (!TEMPLATE_ID_SET.has(id))
|
|
147
|
+
throw new Error(`Unknown template: ${id}`);
|
|
148
|
+
const sourceRoot = path.join(this.root, id);
|
|
149
|
+
const actualDigest = hashTemplateDirectory(sourceRoot);
|
|
150
|
+
const expectedDigest = catalog.templates[id].sha256;
|
|
151
|
+
if (actualDigest !== expectedDigest) {
|
|
152
|
+
throw new Error(`Template integrity check failed for ${id}.`);
|
|
153
|
+
}
|
|
154
|
+
copyTemplate(sourceRoot, request.destination);
|
|
155
|
+
result[id] = Object.freeze({ version: catalog.version, sha256: actualDigest });
|
|
156
|
+
}
|
|
157
|
+
return Object.freeze(result);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export function materializeTemplates(destination, modules, source = new BundledTemplateSource()) {
|
|
161
|
+
return source.materialize({ ids: templateIdsForModules(modules), destination });
|
|
162
|
+
}
|
|
163
|
+
export function assertTemplateProvenance(modules, provenance, source = new BundledTemplateSource()) {
|
|
164
|
+
const expectedIds = [...templateIdsForModules(modules)].sort();
|
|
165
|
+
const actualIds = Object.keys(provenance).sort();
|
|
166
|
+
if (expectedIds.length !== actualIds.length
|
|
167
|
+
|| expectedIds.some((id, index) => id !== actualIds[index])) {
|
|
168
|
+
throw new Error(`Template provenance must exactly match selected modules: ${expectedIds.join(', ')}.`);
|
|
169
|
+
}
|
|
170
|
+
const catalog = readCatalog(source.root);
|
|
171
|
+
for (const id of expectedIds) {
|
|
172
|
+
const record = provenance[id];
|
|
173
|
+
if (!record || record.version !== catalog.version || record.sha256 !== catalog.templates[id].sha256) {
|
|
174
|
+
throw new Error(`Template provenance differs from the installed catalog: ${id}.`);
|
|
175
|
+
}
|
|
176
|
+
if (hashTemplateDirectory(path.join(source.root, id)) !== record.sha256) {
|
|
177
|
+
throw new Error(`Installed template integrity check failed: ${id}.`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|