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.
Files changed (85) hide show
  1. package/README.md +29 -256
  2. package/assets/anhedral-cli-init-technical.svg +322 -0
  3. package/assets/anhedral-cli-init.svg +257 -173
  4. package/dist/bin.js +28 -162
  5. package/dist/dependencies.d.ts +7 -56
  6. package/dist/dependencies.js +96 -305
  7. package/dist/index.d.ts +2 -7
  8. package/dist/index.js +1 -4
  9. package/dist/{templates → platforms}/desktop.d.ts +1 -1
  10. package/dist/{templates → platforms}/desktop.js +28 -266
  11. package/dist/{templates → platforms}/extension.d.ts +1 -1
  12. package/dist/platforms/extension.js +401 -0
  13. package/dist/{templates → platforms}/mobile.d.ts +1 -1
  14. package/dist/platforms/mobile.js +232 -0
  15. package/dist/project.d.ts +4 -0
  16. package/dist/project.js +1 -0
  17. package/dist/standard-products.d.ts +2 -0
  18. package/dist/standard-products.js +6 -0
  19. package/dist/standard.d.ts +18 -0
  20. package/dist/standard.js +578 -0
  21. package/dist/version.d.ts +1 -1
  22. package/dist/version.js +1 -1
  23. package/docs/application-stack-standard.md +700 -0
  24. package/package.json +34 -49
  25. package/CHANGELOG.md +0 -132
  26. package/CONTRIBUTING.md +0 -73
  27. package/SECURITY.md +0 -37
  28. package/SKILL.md +0 -244
  29. package/assets/images/svg/ably-symbol-color.svg +0 -8
  30. package/bin/anhedral.js +0 -2
  31. package/dist/architecture/contributions.d.ts +0 -28
  32. package/dist/architecture/contributions.js +0 -125
  33. package/dist/architecture/index.d.ts +0 -12
  34. package/dist/architecture/index.js +0 -6
  35. package/dist/architecture/manifest.d.ts +0 -53
  36. package/dist/architecture/manifest.js +0 -283
  37. package/dist/architecture/modules.d.ts +0 -37
  38. package/dist/architecture/modules.js +0 -184
  39. package/dist/architecture/plan.d.ts +0 -58
  40. package/dist/architecture/plan.js +0 -145
  41. package/dist/architecture/templates.d.ts +0 -10
  42. package/dist/architecture/templates.js +0 -23
  43. package/dist/cli.d.ts +0 -32
  44. package/dist/cli.js +0 -235
  45. package/dist/composers/root.d.ts +0 -6
  46. package/dist/composers/root.js +0 -35
  47. package/dist/print.d.ts +0 -8
  48. package/dist/print.js +0 -48
  49. package/dist/prompts.d.ts +0 -5
  50. package/dist/prompts.js +0 -19
  51. package/dist/scaffold.d.ts +0 -76
  52. package/dist/scaffold.js +0 -1895
  53. package/dist/template-source.d.ts +0 -20
  54. package/dist/template-source.js +0 -180
  55. package/dist/templates/api.d.ts +0 -2
  56. package/dist/templates/api.js +0 -3122
  57. package/dist/templates/assets-private-proxy.d.ts +0 -3
  58. package/dist/templates/assets-private-proxy.js +0 -277
  59. package/dist/templates/electron-updater.d.ts +0 -3
  60. package/dist/templates/electron-updater.js +0 -358
  61. package/dist/templates/extension.js +0 -732
  62. package/dist/templates/mobile.js +0 -720
  63. package/dist/templates/shared.d.ts +0 -2
  64. package/dist/templates/shared.js +0 -520
  65. package/dist/templates/web.d.ts +0 -2
  66. package/dist/templates/web.js +0 -412
  67. package/dist/toolchain.d.ts +0 -3
  68. package/dist/toolchain.js +0 -8
  69. package/dist/ui.d.ts +0 -33
  70. package/dist/ui.js +0 -199
  71. package/dist/workspace-config.d.ts +0 -11
  72. package/dist/workspace-config.js +0 -372
  73. package/docs/conventions.md +0 -92
  74. package/docs/master-stack-map.md +0 -199
  75. package/docs/output-tree-contract.md +0 -482
  76. package/docs/references/manual-scaffolding.md +0 -403
  77. package/docs/references/provisioning.md +0 -220
  78. package/scripts/render-master-stack-map.mjs +0 -510
  79. package/templates/api-fastify/apps/api/tsconfig.json +0 -20
  80. package/templates/catalog.json +0 -23
  81. package/templates/db-drizzle/packages/db/tsconfig.json +0 -14
  82. package/templates/desktop-electron/apps/desktop/tsconfig.json +0 -37
  83. package/templates/extension-wxt/apps/extension/tsconfig.json +0 -41
  84. package/templates/mobile-expo/apps/mobile/expo-env.d.ts +0 -1
  85. package/templates/web-next/apps/web/next-env.d.ts +0 -6
@@ -1,28 +0,0 @@
1
- import { type ModuleId } from './modules.js';
2
- export type EnvironmentContribution = {
3
- readonly owner: ModuleId;
4
- readonly name: string;
5
- readonly defaultValue: string;
6
- };
7
- export type CronContribution = {
8
- readonly owner: ModuleId;
9
- readonly id: string;
10
- readonly path: string;
11
- readonly schedule: string;
12
- };
13
- export type ModuleCompositionContribution = {
14
- readonly module: ModuleId;
15
- readonly environment: readonly Omit<EnvironmentContribution, 'owner'>[];
16
- readonly crons: readonly Omit<CronContribution, 'owner'>[];
17
- };
18
- export type CompositionModel = {
19
- readonly modules: readonly ModuleId[];
20
- readonly environment: readonly EnvironmentContribution[];
21
- readonly crons: readonly CronContribution[];
22
- };
23
- export type CompositionErrorCode = 'DUPLICATE_ENVIRONMENT_CONTRIBUTION' | 'DUPLICATE_CRON_CONTRIBUTION';
24
- export declare class CompositionError extends Error {
25
- readonly code: CompositionErrorCode;
26
- constructor(code: CompositionErrorCode, message: string);
27
- }
28
- export declare function collectModuleContributions(requestedModules: readonly ModuleId[], overrides?: Readonly<Partial<Record<ModuleId, ModuleCompositionContribution>>>): CompositionModel;
@@ -1,125 +0,0 @@
1
- import { resolveModules } from './modules.js';
2
- export class CompositionError extends Error {
3
- code;
4
- constructor(code, message) {
5
- super(message);
6
- this.code = code;
7
- this.name = 'CompositionError';
8
- }
9
- }
10
- const EMPTY = Object.freeze([]);
11
- const MODULE_CONTRIBUTIONS = Object.freeze({
12
- web: Object.freeze({ module: 'web', environment: EMPTY, crons: EMPTY }),
13
- mobile: Object.freeze({ module: 'mobile', environment: EMPTY, crons: EMPTY }),
14
- api: Object.freeze({
15
- module: 'api',
16
- environment: Object.freeze([
17
- { name: 'ANHEDRAL_DEMO', defaultValue: 'false' },
18
- { name: 'PORT', defaultValue: '8787' },
19
- { name: 'TRUST_PROXY_HOPS', defaultValue: '0' },
20
- { name: 'CORS_ORIGINS', defaultValue: '' },
21
- ]),
22
- crons: EMPTY,
23
- }),
24
- desktop: Object.freeze({ module: 'desktop', environment: EMPTY, crons: EMPTY }),
25
- extension: Object.freeze({ module: 'extension', environment: EMPTY, crons: EMPTY }),
26
- db: Object.freeze({
27
- module: 'db',
28
- environment: Object.freeze([{ name: 'DATABASE_URL', defaultValue: 'YOUR_NEON_POSTGRES_URL' }]),
29
- crons: EMPTY,
30
- }),
31
- auth: Object.freeze({
32
- module: 'auth',
33
- environment: Object.freeze([
34
- { name: 'CLERK_PUBLISHABLE_KEY', defaultValue: 'pk_test_***' },
35
- { name: 'CLERK_SECRET_KEY', defaultValue: 'sk_test_***' },
36
- ]),
37
- crons: EMPTY,
38
- }),
39
- billing: Object.freeze({
40
- module: 'billing',
41
- environment: Object.freeze([
42
- { name: 'RC_WEBHOOK_SECRET', defaultValue: '' },
43
- { name: 'RC_SECRET_API_KEY', defaultValue: '' },
44
- { name: 'RC_ENTITLEMENT_ID', defaultValue: 'pro' },
45
- { name: 'ABLY_API_KEY', defaultValue: '' },
46
- { name: 'CRON_SECRET', defaultValue: '' },
47
- ]),
48
- crons: Object.freeze([
49
- { id: 'realtime-outbox', path: '/api/internal/realtime/flush', schedule: '*/5 * * * *' },
50
- ]),
51
- }),
52
- storage: Object.freeze({
53
- module: 'storage',
54
- environment: Object.freeze([
55
- { name: 'BASE_URL', defaultValue: 'http://localhost:8787' },
56
- { name: 'R2_ACCOUNT_ID', defaultValue: '' },
57
- { name: 'R2_ACCESS_KEY_ID', defaultValue: '' },
58
- { name: 'R2_SECRET_ACCESS_KEY', defaultValue: '' },
59
- { name: 'R2_BUCKET_NAME', defaultValue: '' },
60
- { name: 'R2_PREFIX', defaultValue: 'storage' },
61
- { name: 'R2_PROXY_READ_URL_TTL_SECONDS', defaultValue: '600' },
62
- { name: 'CLOUDFLARE_API_TOKEN', defaultValue: '' },
63
- { name: 'CRON_SECRET', defaultValue: '' },
64
- ]),
65
- crons: Object.freeze([
66
- { id: 'storage-cleanup', path: '/api/internal/storage/cleanup', schedule: '0 3 * * *' },
67
- ]),
68
- }),
69
- 'native-subscriptions': Object.freeze({
70
- module: 'native-subscriptions',
71
- environment: EMPTY,
72
- crons: EMPTY,
73
- }),
74
- 'electron-updater': Object.freeze({
75
- module: 'electron-updater',
76
- environment: Object.freeze([
77
- { name: 'DESKTOP_UPDATE_BASE_URL', defaultValue: 'https://updates.example.com' },
78
- ]),
79
- crons: EMPTY,
80
- }),
81
- });
82
- function collectEnvironmentFromRegistry(modules, registry) {
83
- const byName = new Map();
84
- for (const module of modules) {
85
- for (const contribution of registry[module].environment) {
86
- const candidate = Object.freeze({ owner: module, ...contribution });
87
- const previous = byName.get(candidate.name);
88
- if (previous && previous.defaultValue !== candidate.defaultValue) {
89
- throw new CompositionError('DUPLICATE_ENVIRONMENT_CONTRIBUTION', `Environment variable ${candidate.name} has incompatible defaults from ${previous.owner} and ${module}.`);
90
- }
91
- if (!previous)
92
- byName.set(candidate.name, candidate);
93
- }
94
- }
95
- return Object.freeze([...byName.values()]);
96
- }
97
- function collectCronsFromRegistry(modules, registry) {
98
- const byId = new Map();
99
- for (const module of modules) {
100
- for (const contribution of registry[module].crons) {
101
- const candidate = Object.freeze({ owner: module, ...contribution });
102
- const previous = byId.get(candidate.id);
103
- if (previous && (previous.path !== candidate.path || previous.schedule !== candidate.schedule)) {
104
- throw new CompositionError('DUPLICATE_CRON_CONTRIBUTION', `Cron ${candidate.id} has incompatible definitions from ${previous.owner} and ${module}.`);
105
- }
106
- if (!previous)
107
- byId.set(candidate.id, candidate);
108
- }
109
- }
110
- return Object.freeze([...byId.values()]);
111
- }
112
- export function collectModuleContributions(requestedModules, overrides = {}) {
113
- const modules = resolveModules(requestedModules).resolvedModules;
114
- const contributions = Object.freeze({ ...MODULE_CONTRIBUTIONS, ...overrides });
115
- for (const module of modules) {
116
- if (contributions[module].module !== module) {
117
- throw new Error(`Composition contribution key ${module} must declare the same module ID.`);
118
- }
119
- }
120
- return Object.freeze({
121
- modules,
122
- environment: collectEnvironmentFromRegistry(modules, contributions),
123
- crons: collectCronsFromRegistry(modules, contributions),
124
- });
125
- }
@@ -1,12 +0,0 @@
1
- export { DEFAULT_MODULE_DEFINITIONS, DEFAULT_MODULE_REGISTRY, MODULE_IDS, ModuleRegistryError, ModuleResolutionError, createModuleRegistry, isModuleId, resolveModules, } from './modules.js';
2
- export type { DependencyEdge, ModuleDefinition, ModuleId, ModuleKind, ModuleRegistry, ModuleRegistryErrorCode, ModuleResolution, ModuleResolutionErrorCode, } from './modules.js';
3
- export { FILE_OWNERSHIP_CLASSES, PlanBuildError, buildGenerationPlan, hashContent, isFileOwnershipClass, isSha256, normalizePlanPath, writePolicyForOwnership, } from './plan.js';
4
- export type { BuildGenerationPlanInput, FileOwner, FileOwnershipClass, FileWritePolicy, GenerationOperation, GenerationPlan, ModulePlanContribution, PlanBuildErrorCode, PlanFileInput, PlannedFile, } from './plan.js';
5
- export { MANIFEST_SCHEMA_VERSION, ManifestValidationError, createManifest, readManifest, serializeManifest, } from './manifest.js';
6
- export { TEMPLATE_IDS, isTemplateId, templateIdsForModules, } from './templates.js';
7
- export type { TemplateId, TemplateProvenance, TemplateProvenanceMap, } from './templates.js';
8
- export { CompositionError, collectModuleContributions, } from './contributions.js';
9
- export type { CompositionErrorCode, CompositionModel, CronContribution, EnvironmentContribution, ModuleCompositionContribution, } from './contributions.js';
10
- export type { CreateManifestInput, ManifestFileRecord, ManifestToolchain, ManifestValidationErrorCode, ProjectManifest, } from './manifest.js';
11
- export { NATIVE_STYLING_LIBRARIES, UI_PROVIDERS, UI_TARGETS, buildUiInstallCommands, isNativeStylingLibrary, isUiProvider, isUiTarget, mergeUiInstalls, normalizeUiComponentName, parseUiComponentList, providerForTarget, registrySourceFor, resolveUiInstalls, uiInstallKey, } from '../ui.js';
12
- export type { NativeStylingLibrary, UiComponentInstall, UiInstallCommand, UiProvider, UiTarget, } from '../ui.js';
@@ -1,6 +0,0 @@
1
- export { DEFAULT_MODULE_DEFINITIONS, DEFAULT_MODULE_REGISTRY, MODULE_IDS, ModuleRegistryError, ModuleResolutionError, createModuleRegistry, isModuleId, resolveModules, } from './modules.js';
2
- export { FILE_OWNERSHIP_CLASSES, PlanBuildError, buildGenerationPlan, hashContent, isFileOwnershipClass, isSha256, normalizePlanPath, writePolicyForOwnership, } from './plan.js';
3
- export { MANIFEST_SCHEMA_VERSION, ManifestValidationError, createManifest, readManifest, serializeManifest, } from './manifest.js';
4
- export { TEMPLATE_IDS, isTemplateId, templateIdsForModules, } from './templates.js';
5
- export { CompositionError, collectModuleContributions, } from './contributions.js';
6
- export { NATIVE_STYLING_LIBRARIES, UI_PROVIDERS, UI_TARGETS, buildUiInstallCommands, isNativeStylingLibrary, isUiProvider, isUiTarget, mergeUiInstalls, normalizeUiComponentName, parseUiComponentList, providerForTarget, registrySourceFor, resolveUiInstalls, uiInstallKey, } from '../ui.js';
@@ -1,53 +0,0 @@
1
- import { type ModuleId, type ModuleRegistry } from './modules.js';
2
- import { type FileOwner, type FileOwnershipClass, type GenerationPlan } from './plan.js';
3
- import { type TemplateProvenanceMap } from './templates.js';
4
- import { type NativeStylingLibrary, type UiComponentInstall } from '../ui.js';
5
- export declare const MANIFEST_SCHEMA_VERSION: 5;
6
- export type ManifestToolchain = 'stable' | 'latest';
7
- export type ManifestFileRecord = {
8
- readonly owner: FileOwner;
9
- readonly ownership: FileOwnershipClass;
10
- readonly hash: string;
11
- /** Normalized Unix permission bits (0o000-0o777), or null on platforms without portable modes. */
12
- readonly mode: number | null;
13
- };
14
- export type ProjectManifest = {
15
- readonly schemaVersion: typeof MANIFEST_SCHEMA_VERSION;
16
- readonly generatorVersion: string;
17
- readonly project: {
18
- readonly name: string;
19
- readonly displayName: string;
20
- };
21
- /** The complete, canonically ordered module dependency closure. */
22
- readonly modules: readonly ModuleId[];
23
- readonly toolchain: ManifestToolchain;
24
- /** Immutable template catalog entries used to seed the generated workspace. */
25
- readonly templates: TemplateProvenanceMap;
26
- readonly ui: {
27
- readonly nativeStyling: NativeStylingLibrary;
28
- readonly components: readonly UiComponentInstall[];
29
- };
30
- readonly files: Readonly<Record<string, ManifestFileRecord>>;
31
- };
32
- export type CreateManifestInput = {
33
- readonly generatorVersion: string;
34
- readonly project: {
35
- readonly name: string;
36
- readonly displayName: string;
37
- };
38
- readonly plan: GenerationPlan;
39
- readonly toolchain: ManifestToolchain;
40
- readonly templates: TemplateProvenanceMap;
41
- readonly nativeStyling?: NativeStylingLibrary;
42
- readonly components?: readonly UiComponentInstall[];
43
- readonly registry?: ModuleRegistry;
44
- };
45
- export type ManifestValidationErrorCode = 'INVALID_JSON' | 'INVALID_MANIFEST' | 'INVALID_SCHEMA_VERSION' | 'FUTURE_SCHEMA_VERSION' | 'UNKNOWN_MODULE' | 'DUPLICATE_MODULE' | 'INVALID_MODULE_CLOSURE' | 'INVALID_FILE_RECORD';
46
- export declare class ManifestValidationError extends Error {
47
- readonly code: ManifestValidationErrorCode;
48
- readonly path: string;
49
- constructor(code: ManifestValidationErrorCode, message: string, path: string);
50
- }
51
- export declare function readManifest(input: string | unknown, registry?: ModuleRegistry): ProjectManifest;
52
- export declare function createManifest(input: CreateManifestInput): ProjectManifest;
53
- export declare function serializeManifest(manifest: ProjectManifest): string;
@@ -1,283 +0,0 @@
1
- import { DEFAULT_MODULE_REGISTRY, isModuleId, resolveModules, } from './modules.js';
2
- import { isFileOwnershipClass, isSha256, normalizePlanPath, } from './plan.js';
3
- import { isTemplateId, templateIdsForModules, } from './templates.js';
4
- import { isNativeStylingLibrary, isUiProvider, isUiTarget, normalizeUiComponentName, providerForTarget, uiInstallKey, } from '../ui.js';
5
- export const MANIFEST_SCHEMA_VERSION = 5;
6
- export class ManifestValidationError extends Error {
7
- code;
8
- path;
9
- constructor(code, message, path) {
10
- super(message);
11
- this.code = code;
12
- this.path = path;
13
- this.name = 'ManifestValidationError';
14
- }
15
- }
16
- function isRecord(value) {
17
- return typeof value === 'object' && value !== null && !Array.isArray(value);
18
- }
19
- function readNonEmptyString(value, path) {
20
- if (typeof value !== 'string' || value.trim().length === 0) {
21
- throw new ManifestValidationError('INVALID_MANIFEST', `${path} must be a non-empty string`, path);
22
- }
23
- return value;
24
- }
25
- function assertExactKeys(value, expected, path, code = 'INVALID_MANIFEST') {
26
- const expectedKeys = new Set(expected);
27
- const actual = Object.keys(value);
28
- const missing = expected.filter((key) => !Object.hasOwn(value, key));
29
- const unknown = actual.filter((key) => !expectedKeys.has(key));
30
- if (missing.length === 0 && unknown.length === 0)
31
- return;
32
- const details = [
33
- missing.length > 0 ? `missing ${missing.join(', ')}` : null,
34
- unknown.length > 0 ? `unknown ${unknown.join(', ')}` : null,
35
- ].filter((entry) => entry !== null).join('; ');
36
- throw new ManifestValidationError(code, `${path} has invalid fields (${details})`, path);
37
- }
38
- function parseModuleArray(value) {
39
- if (!Array.isArray(value)) {
40
- throw new ManifestValidationError('INVALID_MANIFEST', 'modules must be an array', 'modules');
41
- }
42
- const seen = new Set();
43
- const modules = [];
44
- for (let index = 0; index < value.length; index += 1) {
45
- const entry = value[index];
46
- if (!isModuleId(entry)) {
47
- throw new ManifestValidationError('UNKNOWN_MODULE', `modules[${index}] is not a supported module: ${String(entry)}`, `modules[${index}]`);
48
- }
49
- if (seen.has(entry)) {
50
- throw new ManifestValidationError('DUPLICATE_MODULE', `modules contains duplicate module ${entry}`, 'modules');
51
- }
52
- seen.add(entry);
53
- modules.push(entry);
54
- }
55
- return Object.freeze(modules);
56
- }
57
- function assertExactModuleClosure(modules, registry) {
58
- let expected;
59
- try {
60
- expected = resolveModules(modules, registry).resolvedModules;
61
- }
62
- catch (error) {
63
- throw new ManifestValidationError('INVALID_MANIFEST', error instanceof Error ? error.message : 'Manifest modules are invalid', 'modules');
64
- }
65
- if (expected.length !== modules.length || expected.some((moduleId, index) => modules[index] !== moduleId)) {
66
- throw new ManifestValidationError('INVALID_MODULE_CLOSURE', `modules must be the complete, canonically ordered dependency closure: ${expected.join(', ')}`, 'modules');
67
- }
68
- }
69
- function readFileRecord(value, path, modules) {
70
- if (!isRecord(value)) {
71
- throw new ManifestValidationError('INVALID_FILE_RECORD', `${path} must be an object`, path);
72
- }
73
- assertExactKeys(value, ['owner', 'ownership', 'hash', 'mode'], path, 'INVALID_FILE_RECORD');
74
- const owner = value.owner;
75
- if (owner !== 'root' && !isModuleId(owner)) {
76
- throw new ManifestValidationError('INVALID_FILE_RECORD', `${path}.owner is invalid`, `${path}.owner`);
77
- }
78
- if (owner !== 'root' && !modules.has(owner)) {
79
- throw new ManifestValidationError('INVALID_FILE_RECORD', `${path}.owner references an unselected module: ${owner}`, `${path}.owner`);
80
- }
81
- if (!isFileOwnershipClass(value.ownership)) {
82
- throw new ManifestValidationError('INVALID_FILE_RECORD', `${path}.ownership is invalid`, `${path}.ownership`);
83
- }
84
- if (!isSha256(value.hash)) {
85
- throw new ManifestValidationError('INVALID_FILE_RECORD', `${path}.hash must be a SHA-256 hex digest`, `${path}.hash`);
86
- }
87
- const mode = value.mode;
88
- if (mode !== null && (!Number.isInteger(mode) || mode < 0 || mode > 0o777)) {
89
- throw new ManifestValidationError('INVALID_FILE_RECORD', `${path}.mode must be null or normalized Unix permission bits between 0 and 511`, `${path}.mode`);
90
- }
91
- return Object.freeze({
92
- owner,
93
- ownership: value.ownership,
94
- hash: value.hash,
95
- mode: mode,
96
- });
97
- }
98
- function readFiles(value, modules) {
99
- if (!isRecord(value)) {
100
- throw new ManifestValidationError('INVALID_MANIFEST', 'files must be an object', 'files');
101
- }
102
- const moduleSet = new Set(modules);
103
- const entries = [];
104
- for (const rawPath of Object.keys(value).sort()) {
105
- let filePath;
106
- try {
107
- filePath = normalizePlanPath(rawPath);
108
- }
109
- catch {
110
- throw new ManifestValidationError('INVALID_FILE_RECORD', `Invalid manifest file path: ${rawPath}`, `files.${rawPath}`);
111
- }
112
- if (filePath !== rawPath) {
113
- throw new ManifestValidationError('INVALID_FILE_RECORD', `Manifest file path is not canonical: ${rawPath}`, `files.${rawPath}`);
114
- }
115
- entries.push([filePath, readFileRecord(value[rawPath], `files.${rawPath}`, moduleSet)]);
116
- }
117
- return Object.freeze(Object.fromEntries(entries));
118
- }
119
- function readTemplates(value, modules) {
120
- if (!isRecord(value)) {
121
- throw new ManifestValidationError('INVALID_MANIFEST', 'templates must be an object', 'templates');
122
- }
123
- const entries = [];
124
- const expectedIds = [...templateIdsForModules(modules)].sort();
125
- const actualIds = Object.keys(value).sort();
126
- if (expectedIds.length !== actualIds.length
127
- || expectedIds.some((id, index) => id !== actualIds[index])) {
128
- throw new ManifestValidationError('INVALID_MANIFEST', `templates must exactly match selected modules: ${expectedIds.join(', ')}`, 'templates');
129
- }
130
- for (const id of Object.keys(value).sort()) {
131
- if (!isTemplateId(id)) {
132
- throw new ManifestValidationError('INVALID_MANIFEST', `Unknown template: ${id}`, `templates.${id}`);
133
- }
134
- const entry = value[id];
135
- if (!isRecord(entry)) {
136
- throw new ManifestValidationError('INVALID_MANIFEST', `templates.${id} must be an object`, `templates.${id}`);
137
- }
138
- assertExactKeys(entry, ['version', 'sha256'], `templates.${id}`);
139
- if (!Number.isInteger(entry.version) || entry.version < 1) {
140
- throw new ManifestValidationError('INVALID_MANIFEST', `templates.${id}.version must be a positive integer`, `templates.${id}.version`);
141
- }
142
- if (!isSha256(entry.sha256)) {
143
- throw new ManifestValidationError('INVALID_MANIFEST', `templates.${id}.sha256 must be a SHA-256 hex digest`, `templates.${id}.sha256`);
144
- }
145
- entries.push([id, Object.freeze({ version: entry.version, sha256: entry.sha256 })]);
146
- }
147
- return Object.freeze(Object.fromEntries(entries));
148
- }
149
- function readUi(value, modules) {
150
- if (!isRecord(value)) {
151
- throw new ManifestValidationError('INVALID_MANIFEST', 'ui must be an object', 'ui');
152
- }
153
- assertExactKeys(value, ['nativeStyling', 'components'], 'ui');
154
- if (!isNativeStylingLibrary(value.nativeStyling)) {
155
- throw new ManifestValidationError('INVALID_MANIFEST', 'ui.nativeStyling must be nativewind or uniwind', 'ui.nativeStyling');
156
- }
157
- if (!Array.isArray(value.components)) {
158
- throw new ManifestValidationError('INVALID_MANIFEST', 'ui.components must be an array', 'ui.components');
159
- }
160
- const selectedModules = new Set(modules);
161
- const seen = new Set();
162
- const components = [];
163
- for (let index = 0; index < value.components.length; index += 1) {
164
- const itemPath = `ui.components[${index}]`;
165
- const item = value.components[index];
166
- if (!isRecord(item)) {
167
- throw new ManifestValidationError('INVALID_MANIFEST', `${itemPath} must be an object`, itemPath);
168
- }
169
- assertExactKeys(item, ['name', 'target', 'provider', 'source', 'variant'], itemPath);
170
- let name;
171
- try {
172
- name = normalizeUiComponentName(readNonEmptyString(item.name, `${itemPath}.name`));
173
- }
174
- catch (error) {
175
- throw new ManifestValidationError('INVALID_MANIFEST', error instanceof Error ? error.message : `${itemPath}.name is invalid`, `${itemPath}.name`);
176
- }
177
- if (!isUiTarget(item.target) || !selectedModules.has(item.target)) {
178
- throw new ManifestValidationError('INVALID_MANIFEST', `${itemPath}.target is not a selected UI client`, `${itemPath}.target`);
179
- }
180
- if (!isUiProvider(item.provider) || item.provider !== providerForTarget(item.target)) {
181
- throw new ManifestValidationError('INVALID_MANIFEST', `${itemPath}.provider does not match its target`, `${itemPath}.provider`);
182
- }
183
- const source = readNonEmptyString(item.source, `${itemPath}.source`);
184
- const variant = item.variant;
185
- if (item.target === 'mobile') {
186
- if (!isNativeStylingLibrary(variant) || variant !== value.nativeStyling) {
187
- throw new ManifestValidationError('INVALID_MANIFEST', `${itemPath}.variant must match ui.nativeStyling`, `${itemPath}.variant`);
188
- }
189
- }
190
- else if (variant !== null) {
191
- throw new ManifestValidationError('INVALID_MANIFEST', `${itemPath}.variant must be null for DOM clients`, `${itemPath}.variant`);
192
- }
193
- const install = Object.freeze({ name, target: item.target, provider: item.provider, source, variant });
194
- const key = uiInstallKey(install);
195
- if (seen.has(key)) {
196
- throw new ManifestValidationError('INVALID_MANIFEST', `Duplicate UI component installation: ${key}`, itemPath);
197
- }
198
- seen.add(key);
199
- components.push(install);
200
- }
201
- const canonical = [...components].sort((left, right) => uiInstallKey(left).localeCompare(uiInstallKey(right)));
202
- if (canonical.some((component, index) => component !== components[index])) {
203
- throw new ManifestValidationError('INVALID_MANIFEST', 'ui.components must be canonically sorted', 'ui.components');
204
- }
205
- return Object.freeze({ nativeStyling: value.nativeStyling, components: Object.freeze(components) });
206
- }
207
- function parseInput(input) {
208
- if (typeof input !== 'string')
209
- return input;
210
- try {
211
- return JSON.parse(input);
212
- }
213
- catch (error) {
214
- throw new ManifestValidationError('INVALID_JSON', `Manifest is not valid JSON: ${error instanceof Error ? error.message : String(error)}`, '$');
215
- }
216
- }
217
- export function readManifest(input, registry = DEFAULT_MODULE_REGISTRY) {
218
- const value = parseInput(input);
219
- if (!isRecord(value)) {
220
- throw new ManifestValidationError('INVALID_MANIFEST', 'Manifest must be an object', '$');
221
- }
222
- const schemaVersion = value.schemaVersion;
223
- if (typeof schemaVersion !== 'number' || !Number.isInteger(schemaVersion) || schemaVersion < 1) {
224
- throw new ManifestValidationError('INVALID_SCHEMA_VERSION', `schemaVersion must be exactly ${MANIFEST_SCHEMA_VERSION}`, 'schemaVersion');
225
- }
226
- if (schemaVersion > MANIFEST_SCHEMA_VERSION) {
227
- throw new ManifestValidationError('FUTURE_SCHEMA_VERSION', `Manifest schema ${schemaVersion} is newer than supported schema ${MANIFEST_SCHEMA_VERSION}`, 'schemaVersion');
228
- }
229
- if (schemaVersion !== MANIFEST_SCHEMA_VERSION) {
230
- throw new ManifestValidationError('INVALID_SCHEMA_VERSION', `Unsupported manifest schema: ${schemaVersion}; expected ${MANIFEST_SCHEMA_VERSION}`, 'schemaVersion');
231
- }
232
- assertExactKeys(value, ['schemaVersion', 'generatorVersion', 'project', 'modules', 'toolchain', 'templates', 'ui', 'files'], '$');
233
- if (!isRecord(value.project)) {
234
- throw new ManifestValidationError('INVALID_MANIFEST', 'project must be an object', 'project');
235
- }
236
- assertExactKeys(value.project, ['name', 'displayName'], 'project');
237
- const project = Object.freeze({
238
- name: readNonEmptyString(value.project.name, 'project.name'),
239
- displayName: readNonEmptyString(value.project.displayName, 'project.displayName'),
240
- });
241
- const modules = parseModuleArray(value.modules);
242
- assertExactModuleClosure(modules, registry);
243
- if (value.toolchain !== 'stable' && value.toolchain !== 'latest') {
244
- throw new ManifestValidationError('INVALID_MANIFEST', 'toolchain must be either stable or latest', 'toolchain');
245
- }
246
- return Object.freeze({
247
- schemaVersion: MANIFEST_SCHEMA_VERSION,
248
- generatorVersion: readNonEmptyString(value.generatorVersion, 'generatorVersion'),
249
- project,
250
- modules,
251
- toolchain: value.toolchain,
252
- templates: readTemplates(value.templates, modules),
253
- ui: readUi(value.ui, modules),
254
- files: readFiles(value.files, modules),
255
- });
256
- }
257
- export function createManifest(input) {
258
- const files = Object.fromEntries(input.plan.files.map((file) => [
259
- file.path,
260
- {
261
- owner: file.owner,
262
- ownership: file.ownership,
263
- hash: file.contentHash,
264
- mode: null,
265
- },
266
- ]));
267
- return readManifest({
268
- schemaVersion: MANIFEST_SCHEMA_VERSION,
269
- generatorVersion: input.generatorVersion,
270
- project: input.project,
271
- modules: input.plan.resolvedModules,
272
- toolchain: input.toolchain,
273
- templates: input.templates,
274
- ui: {
275
- nativeStyling: input.nativeStyling ?? 'nativewind',
276
- components: input.components ?? [],
277
- },
278
- files,
279
- }, input.registry ?? DEFAULT_MODULE_REGISTRY);
280
- }
281
- export function serializeManifest(manifest) {
282
- return `${JSON.stringify(manifest, null, 2)}\n`;
283
- }
@@ -1,37 +0,0 @@
1
- export declare const MODULE_IDS: readonly ["web", "mobile", "api", "desktop", "extension", "db", "auth", "billing", "storage", "native-subscriptions", "electron-updater"];
2
- export type ModuleId = (typeof MODULE_IDS)[number];
3
- export type ModuleKind = 'app' | 'feature';
4
- export type ModuleDefinition = {
5
- readonly id: ModuleId;
6
- readonly kind: ModuleKind;
7
- readonly requires: readonly ModuleId[];
8
- readonly conflicts: readonly ModuleId[];
9
- };
10
- export type ModuleRegistry = Readonly<Record<ModuleId, ModuleDefinition>>;
11
- export type DependencyEdge = {
12
- readonly module: ModuleId;
13
- readonly requires: ModuleId;
14
- };
15
- export type ModuleResolution = {
16
- readonly requestedModules: readonly ModuleId[];
17
- readonly resolvedModules: readonly ModuleId[];
18
- readonly dependencyAddedModules: readonly ModuleId[];
19
- readonly dependencyEdges: readonly DependencyEdge[];
20
- };
21
- export type ModuleRegistryErrorCode = 'DUPLICATE_MODULE_DEFINITION' | 'MISSING_MODULE_DEFINITION' | 'UNKNOWN_MODULE_REFERENCE' | 'SELF_DEPENDENCY' | 'SELF_CONFLICT' | 'DEPENDENCY_CYCLE';
22
- export declare class ModuleRegistryError extends Error {
23
- readonly code: ModuleRegistryErrorCode;
24
- readonly modules: readonly string[];
25
- constructor(code: ModuleRegistryErrorCode, message: string, modules?: readonly string[]);
26
- }
27
- export type ModuleResolutionErrorCode = 'UNKNOWN_MODULE' | 'MODULE_CONFLICT';
28
- export declare class ModuleResolutionError extends Error {
29
- readonly code: ModuleResolutionErrorCode;
30
- readonly modules: readonly string[];
31
- constructor(code: ModuleResolutionErrorCode, message: string, modules: readonly string[]);
32
- }
33
- export declare function isModuleId(value: unknown): value is ModuleId;
34
- export declare function createModuleRegistry(definitions: readonly ModuleDefinition[]): ModuleRegistry;
35
- export declare const DEFAULT_MODULE_DEFINITIONS: readonly ModuleDefinition[];
36
- export declare const DEFAULT_MODULE_REGISTRY: Readonly<Record<"electron-updater" | "auth" | "billing" | "storage" | "api" | "desktop" | "web" | "mobile" | "extension" | "db" | "native-subscriptions", ModuleDefinition>>;
37
- export declare function resolveModules(requested: readonly string[], registry?: ModuleRegistry): ModuleResolution;