anhedral 0.4.2 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +35 -265
  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 -191
  5. package/dist/dependencies.d.ts +7 -57
  6. package/dist/dependencies.js +96 -308
  7. package/dist/index.d.ts +2 -7
  8. package/dist/index.js +1 -4
  9. package/dist/platforms/desktop.js +401 -0
  10. package/dist/platforms/extension.js +401 -0
  11. package/dist/platforms/mobile.js +232 -0
  12. package/dist/project.d.ts +0 -26
  13. package/dist/standard-products.d.ts +2 -0
  14. package/dist/standard-products.js +6 -0
  15. package/dist/standard.d.ts +18 -0
  16. package/dist/standard.js +595 -0
  17. package/dist/version.d.ts +1 -1
  18. package/dist/version.js +1 -1
  19. package/docs/application-stack-standard.md +700 -0
  20. package/package.json +29 -51
  21. package/CHANGELOG.md +0 -178
  22. package/CONTRIBUTING.md +0 -77
  23. package/SECURITY.md +0 -37
  24. package/SKILL.md +0 -339
  25. package/assets/images/svg/ably-symbol-color.svg +0 -8
  26. package/bin/anhedral.js +0 -2
  27. package/dist/architecture/contributions.d.ts +0 -28
  28. package/dist/architecture/contributions.js +0 -125
  29. package/dist/architecture/index.d.ts +0 -12
  30. package/dist/architecture/index.js +0 -6
  31. package/dist/architecture/manifest.d.ts +0 -53
  32. package/dist/architecture/manifest.js +0 -283
  33. package/dist/architecture/modules.d.ts +0 -41
  34. package/dist/architecture/modules.js +0 -187
  35. package/dist/architecture/plan.d.ts +0 -58
  36. package/dist/architecture/plan.js +0 -145
  37. package/dist/architecture/templates.d.ts +0 -10
  38. package/dist/architecture/templates.js +0 -23
  39. package/dist/cli.d.ts +0 -32
  40. package/dist/cli.js +0 -281
  41. package/dist/composers/root.d.ts +0 -6
  42. package/dist/composers/root.js +0 -35
  43. package/dist/print.d.ts +0 -8
  44. package/dist/print.js +0 -48
  45. package/dist/prompts.d.ts +0 -6
  46. package/dist/prompts.js +0 -41
  47. package/dist/scaffold.d.ts +0 -64
  48. package/dist/scaffold.js +0 -2102
  49. package/dist/template-source.d.ts +0 -20
  50. package/dist/template-source.js +0 -180
  51. package/dist/templates/api.d.ts +0 -2
  52. package/dist/templates/api.js +0 -3187
  53. package/dist/templates/assets-private-proxy.d.ts +0 -3
  54. package/dist/templates/assets-private-proxy.js +0 -277
  55. package/dist/templates/desktop.js +0 -792
  56. package/dist/templates/electron-updater.d.ts +0 -3
  57. package/dist/templates/electron-updater.js +0 -358
  58. package/dist/templates/extension.js +0 -856
  59. package/dist/templates/first-run.d.ts +0 -2
  60. package/dist/templates/first-run.js +0 -167
  61. package/dist/templates/mobile.js +0 -861
  62. package/dist/templates/shared.d.ts +0 -2
  63. package/dist/templates/shared.js +0 -577
  64. package/dist/templates/web.d.ts +0 -2
  65. package/dist/templates/web.js +0 -569
  66. package/dist/toolchain.d.ts +0 -3
  67. package/dist/toolchain.js +0 -8
  68. package/dist/ui.d.ts +0 -33
  69. package/dist/ui.js +0 -199
  70. package/dist/workspace-config.d.ts +0 -11
  71. package/dist/workspace-config.js +0 -372
  72. package/docs/NORTHSTAR.md +0 -114
  73. package/docs/conventions.md +0 -94
  74. package/docs/engineering-principles.md +0 -88
  75. package/docs/master-stack-map.md +0 -199
  76. package/docs/output-tree-contract.md +0 -493
  77. package/docs/references/manual-scaffolding.md +0 -421
  78. package/docs/references/provisioning.md +0 -577
  79. package/scripts/render-master-stack-map.mjs +0 -510
  80. package/templates/api-fastify/apps/api/tsconfig.json +0 -20
  81. package/templates/catalog.json +0 -23
  82. package/templates/db-drizzle/packages/db/tsconfig.json +0 -14
  83. package/templates/desktop-electron/apps/desktop/tsconfig.json +0 -37
  84. package/templates/extension-wxt/apps/extension/tsconfig.json +0 -41
  85. package/templates/mobile-expo/apps/mobile/expo-env.d.ts +0 -1
  86. package/templates/web-next/apps/web/next-env.d.ts +0 -6
  87. /package/dist/{templates → platforms}/desktop.d.ts +0 -0
  88. /package/dist/{templates → platforms}/extension.d.ts +0 -0
  89. /package/dist/{templates → platforms}/mobile.d.ts +0 -0
package/dist/ui.js DELETED
@@ -1,199 +0,0 @@
1
- import path from 'node:path';
2
- import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
3
- import { TOOLCHAIN_DEPENDENCIES } from './dependencies.js';
4
- import { execFile } from './util.js';
5
- export const UI_TARGETS = ['web', 'mobile', 'desktop', 'extension'];
6
- export const NATIVE_STYLING_LIBRARIES = ['nativewind', 'uniwind'];
7
- export const UI_PROVIDERS = ['shadcn', 'react-native-reusables'];
8
- const COMPONENT_NAME_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
9
- const SOURCE_EXTENSION_PATTERN = /\.(?:[cm]?[jt]sx?)$/;
10
- const IMPORT_SPECIFIER_PATTERN = /(?:\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*)['"]([^'"]+)['"]/g;
11
- function collectSourcePackageImports(root) {
12
- const packages = new Set();
13
- const visit = (directory) => {
14
- for (const entry of readdirSync(directory, { withFileTypes: true })) {
15
- if (entry.name === 'node_modules' || entry.name === '.git' || entry.name.startsWith('.next'))
16
- continue;
17
- const absolutePath = path.join(directory, entry.name);
18
- if (entry.isDirectory()) {
19
- visit(absolutePath);
20
- continue;
21
- }
22
- if (!entry.isFile() || !SOURCE_EXTENSION_PATTERN.test(entry.name))
23
- continue;
24
- const source = readFileSync(absolutePath, 'utf8');
25
- for (const match of source.matchAll(IMPORT_SPECIFIER_PATTERN)) {
26
- const specifier = match[1];
27
- if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('#')
28
- || specifier.startsWith('@/') || specifier.startsWith('node:') || specifier.startsWith('virtual:'))
29
- continue;
30
- const segments = specifier.split('/');
31
- packages.add(specifier.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0]);
32
- }
33
- }
34
- };
35
- visit(root);
36
- return packages;
37
- }
38
- function workspacePackageManifests(workspaceRoot) {
39
- const manifests = [];
40
- for (const directory of [workspaceRoot, path.join(workspaceRoot, 'apps'), path.join(workspaceRoot, 'packages')]) {
41
- if (!existsSync(directory))
42
- continue;
43
- for (const entry of readdirSync(directory, { withFileTypes: true })) {
44
- const packagePath = entry.isDirectory()
45
- ? path.join(directory, entry.name, 'package.json')
46
- : entry.name === 'package.json' ? path.join(directory, entry.name) : '';
47
- if (!packagePath)
48
- continue;
49
- try {
50
- manifests.push(JSON.parse(readFileSync(packagePath, 'utf8')));
51
- }
52
- catch (error) {
53
- if (error.code !== 'ENOENT')
54
- throw error;
55
- }
56
- }
57
- }
58
- return manifests;
59
- }
60
- export function reconcileUiWorkspaceDependencies(workspaceRoot, targetDirectory) {
61
- const packagePath = path.join(targetDirectory, 'package.json');
62
- const target = JSON.parse(readFileSync(packagePath, 'utf8'));
63
- const declared = new Set([
64
- ...Object.keys(target.dependencies ?? {}),
65
- ...Object.keys(target.devDependencies ?? {}),
66
- ...Object.keys(target.optionalDependencies ?? {}),
67
- ...Object.keys(target.peerDependencies ?? {}),
68
- ]);
69
- const manifests = workspacePackageManifests(workspaceRoot);
70
- const dependencies = { ...(target.dependencies ?? {}) };
71
- let changed = false;
72
- for (const packageName of collectSourcePackageImports(targetDirectory)) {
73
- if (declared.has(packageName))
74
- continue;
75
- const version = manifests
76
- .map((manifest) => manifest.dependencies?.[packageName])
77
- .find((candidate) => typeof candidate === 'string');
78
- if (!version)
79
- continue;
80
- dependencies[packageName] = version;
81
- declared.add(packageName);
82
- changed = true;
83
- }
84
- if (!changed)
85
- return;
86
- target.dependencies = Object.fromEntries(Object.entries(dependencies).sort(([left], [right]) => left.localeCompare(right)));
87
- writeFileSync(packagePath, JSON.stringify(target, null, 2) + '\n');
88
- }
89
- export function isUiTarget(value) {
90
- return typeof value === 'string' && UI_TARGETS.includes(value);
91
- }
92
- export function isNativeStylingLibrary(value) {
93
- return typeof value === 'string' && NATIVE_STYLING_LIBRARIES.includes(value);
94
- }
95
- export function isUiProvider(value) {
96
- return typeof value === 'string' && UI_PROVIDERS.includes(value);
97
- }
98
- export function normalizeUiComponentName(value) {
99
- const normalized = value.trim().toLowerCase();
100
- if (!COMPONENT_NAME_PATTERN.test(normalized)) {
101
- throw new Error(`Invalid UI component name: ${value}. Use lowercase letters, numbers, and hyphens.`);
102
- }
103
- return normalized;
104
- }
105
- export function parseUiComponentList(value) {
106
- return Array.from(new Set(value
107
- .split(/[\s,]+/)
108
- .map((entry) => entry.trim())
109
- .filter(Boolean)
110
- .map(normalizeUiComponentName)));
111
- }
112
- export function providerForTarget(target) {
113
- return target === 'mobile' ? 'react-native-reusables' : 'shadcn';
114
- }
115
- export function registrySourceFor(name, target, nativeStyling) {
116
- const component = normalizeUiComponentName(name);
117
- return target === 'mobile'
118
- ? `https://reactnativereusables.com/r/${nativeStyling}/${component}.json`
119
- : component;
120
- }
121
- export function resolveUiInstalls(componentNames, targets, nativeStyling) {
122
- const components = Array.from(new Set(componentNames.map(normalizeUiComponentName))).sort();
123
- const uniqueTargets = Array.from(new Set(targets)).sort();
124
- return Object.freeze(uniqueTargets.flatMap((target) => components.map((name) => Object.freeze({
125
- name,
126
- target,
127
- provider: providerForTarget(target),
128
- source: registrySourceFor(name, target, nativeStyling),
129
- variant: target === 'mobile' ? nativeStyling : null,
130
- }))));
131
- }
132
- export function uiInstallKey(install) {
133
- return `${install.target}:${install.name}`;
134
- }
135
- export function mergeUiInstalls(current, additions) {
136
- const byKey = new Map(current.map((entry) => [uiInstallKey(entry), entry]));
137
- for (const entry of additions)
138
- byKey.set(uiInstallKey(entry), entry);
139
- return Object.freeze([...byKey.values()].sort((left, right) => uiInstallKey(left).localeCompare(uiInstallKey(right))));
140
- }
141
- export function buildUiInstallCommands(root, installs) {
142
- const commands = [];
143
- for (const target of UI_TARGETS) {
144
- const targetInstalls = installs.filter((entry) => entry.target === target);
145
- if (targetInstalls.length === 0)
146
- continue;
147
- commands.push(Object.freeze({
148
- target,
149
- cwd: path.join(root, 'apps', target),
150
- executable: 'pnpm',
151
- args: Object.freeze([
152
- 'dlx',
153
- `shadcn@${TOOLCHAIN_DEPENDENCIES.shadcn}`,
154
- 'add',
155
- '--yes',
156
- '--overwrite',
157
- '--silent',
158
- ...targetInstalls.map((entry) => entry.source),
159
- ]),
160
- installs: Object.freeze(targetInstalls),
161
- }));
162
- }
163
- return Object.freeze(commands);
164
- }
165
- export function installUiComponents(root, installs) {
166
- for (const command of buildUiInstallCommands(root, installs)) {
167
- if (command.target !== 'mobile') {
168
- execFile(command.executable, command.args, command.cwd);
169
- reconcileUiWorkspaceDependencies(root, command.cwd);
170
- continue;
171
- }
172
- // shadcn switches to `npx expo install` whenever it sees an Expo dependency.
173
- // Component generation runs in Anhedral's isolated stage before node_modules
174
- // exists, so Expo CLI cannot determine the SDK. Temporarily mask only that
175
- // detector field, let shadcn merge registry dependencies with pnpm, then
176
- // restore Anhedral's exact Expo pin before the staged diff is committed.
177
- const packagePath = path.join(command.cwd, 'package.json');
178
- const originalText = readFileSync(packagePath, 'utf8');
179
- const original = JSON.parse(originalText);
180
- const expoVersion = original.dependencies?.expo;
181
- if (!expoVersion)
182
- throw new Error(`Expo dependency is missing from ${packagePath}`);
183
- const masked = structuredClone(original);
184
- delete masked.dependencies?.expo;
185
- writeFileSync(packagePath, JSON.stringify(masked, null, 2) + '\n');
186
- try {
187
- execFile(command.executable, command.args, command.cwd);
188
- const updated = JSON.parse(readFileSync(packagePath, 'utf8'));
189
- updated.dependencies = { ...(updated.dependencies ?? {}), expo: expoVersion };
190
- updated.dependencies = Object.fromEntries(Object.entries(updated.dependencies).sort(([left], [right]) => left.localeCompare(right)));
191
- writeFileSync(packagePath, JSON.stringify(updated, null, 2) + '\n');
192
- reconcileUiWorkspaceDependencies(root, command.cwd);
193
- }
194
- catch (error) {
195
- writeFileSync(packagePath, originalText);
196
- throw error;
197
- }
198
- }
199
- }
@@ -1,11 +0,0 @@
1
- export type WorkspacePolicy = {
2
- overrides: Record<string, string>;
3
- onlyBuiltDependencies: string[];
4
- ignoredBuiltDependencies: string[];
5
- peerDependencyRules: {
6
- ignoreMissing: string[];
7
- allowedVersions: Record<string, string>;
8
- };
9
- };
10
- export declare function desiredWorkspacePolicy(): WorkspacePolicy;
11
- export declare function mergeWorkspaceFile(root: string, desiredPackages: string[], policy: WorkspacePolicy, allowGeneratedUpdates: boolean): void;
@@ -1,372 +0,0 @@
1
- import { lstatSync, readFileSync } from 'node:fs';
2
- import path from 'node:path';
3
- import { SECURITY_OVERRIDES } from './dependencies.js';
4
- import { writeFile } from './util.js';
5
- function pathEntryExists(target) {
6
- try {
7
- lstatSync(target);
8
- return true;
9
- }
10
- catch (error) {
11
- if (error.code === 'ENOENT')
12
- return false;
13
- throw error;
14
- }
15
- }
16
- export function desiredWorkspacePolicy() {
17
- return {
18
- overrides: { ...SECURITY_OVERRIDES },
19
- onlyBuiltDependencies: ['electron', 'esbuild', 'sharp'],
20
- ignoredBuiltDependencies: [
21
- 'browser-tabs-lock',
22
- 'bufferutil',
23
- 'core-js',
24
- 'electron-winstaller',
25
- 'spawn-sync',
26
- 'utf-8-validate',
27
- ],
28
- peerDependencyRules: {
29
- ignoreMissing: ['@solana/web3.js', 'bs58', 'react-native'],
30
- allowedVersions: {
31
- esbuild: '>=0.25.0',
32
- 'utf-8-validate': '>=5.0.2',
33
- },
34
- },
35
- };
36
- }
37
- // Managed pnpm policy intentionally supports only scalar maps and string lists. More complex YAML
38
- // is left untouched outside managed sections and rejected inside them instead of being guessed at.
39
- function sourceLines(content) {
40
- const lines = [];
41
- let start = 0;
42
- while (start < content.length) {
43
- let cursor = start;
44
- while (cursor < content.length && content[cursor] !== '\n' && content[cursor] !== '\r')
45
- cursor += 1;
46
- const text = content.slice(start, cursor);
47
- if (content[cursor] === '\r' && content[cursor + 1] === '\n')
48
- cursor += 2;
49
- else if (cursor < content.length)
50
- cursor += 1;
51
- lines.push({ text, start, end: cursor });
52
- start = cursor;
53
- }
54
- return lines;
55
- }
56
- function workspaceNewline(content) {
57
- return content.includes('\r\n') ? '\r\n' : content.includes('\r') ? '\r' : '\n';
58
- }
59
- function parseWorkspaceStringScalar(raw) {
60
- const leading = /^[ \t]*/.exec(raw)?.[0].length ?? 0;
61
- const valueAndComment = raw.slice(leading);
62
- if (!valueAndComment)
63
- return null;
64
- const singleQuoted = /^'((?:[^']|'')*)'([ \t]*(?:#.*)?)$/.exec(valueAndComment);
65
- if (singleQuoted) {
66
- return {
67
- value: singleQuoted[1].replaceAll("''", "'"),
68
- tokenStart: leading,
69
- tokenEnd: leading + singleQuoted[0].length - singleQuoted[2].length,
70
- };
71
- }
72
- if (valueAndComment.startsWith("'"))
73
- return null;
74
- const doubleQuoted = /^("(?:[^"\\]|\\.)*")([ \t]*(?:#.*)?)$/.exec(valueAndComment);
75
- if (doubleQuoted) {
76
- try {
77
- const value = JSON.parse(doubleQuoted[1]);
78
- return typeof value === 'string'
79
- ? { value, tokenStart: leading, tokenEnd: leading + doubleQuoted[1].length }
80
- : null;
81
- }
82
- catch {
83
- return null;
84
- }
85
- }
86
- if (valueAndComment.startsWith('"'))
87
- return null;
88
- const comment = /[ \t]+#/.exec(valueAndComment);
89
- const withoutComment = valueAndComment.slice(0, comment?.index ?? valueAndComment.length).trimEnd();
90
- if (!withoutComment || /^[\[{|>&*!]/.test(withoutComment))
91
- return null;
92
- if (/^(?:null|~|true|false|[-+]?(?:\.inf|\.nan|0x[\da-f]+|0o[0-7]+|\d+(?:\.\d+)?(?:e[-+]?\d+)?))$/i.test(withoutComment)) {
93
- return null;
94
- }
95
- return { value: withoutComment, tokenStart: leading, tokenEnd: leading + withoutComment.length };
96
- }
97
- function parseWorkspaceMapLine(line) {
98
- const indent = /^([ \t]*)/.exec(line)?.[1] ?? '';
99
- const body = line.slice(indent.length);
100
- let match = /^'((?:[^']|'')*)'[ \t]*:/.exec(body);
101
- let key;
102
- if (match)
103
- key = match[1].replaceAll("''", "'");
104
- else {
105
- match = /^("(?:[^"\\]|\\.)*")[ \t]*:/.exec(body);
106
- if (match) {
107
- try {
108
- const parsed = JSON.parse(match[1]);
109
- if (typeof parsed !== 'string')
110
- return null;
111
- key = parsed;
112
- }
113
- catch {
114
- return null;
115
- }
116
- }
117
- else {
118
- match = /^([^:#][^:]*?)[ \t]*:/.exec(body);
119
- if (!match)
120
- return null;
121
- key = match[1].trim();
122
- if (!key || key.includes('#'))
123
- return null;
124
- }
125
- }
126
- const restOffset = indent.length + match[0].length;
127
- return { indent, key, rest: line.slice(restOffset), restOffset };
128
- }
129
- function parseWorkspaceListLine(line) {
130
- const match = /^([ \t]+)-[ \t]+(.+)$/.exec(line);
131
- if (!match)
132
- return null;
133
- const scalar = parseWorkspaceStringScalar(match[2]);
134
- return scalar ? { indent: match[1], value: scalar.value } : null;
135
- }
136
- function quoteWorkspaceString(value) {
137
- return `'${value.replaceAll("'", "''")}'`;
138
- }
139
- function workspaceLineIndent(line) {
140
- if (!line.trim() || line.trimStart().startsWith('#'))
141
- return null;
142
- return /^([ \t]*)/.exec(line)?.[1] ?? '';
143
- }
144
- function workspaceBlockAt(lines, keyLineIndex) {
145
- const keyLine = lines[keyLineIndex];
146
- const key = parseWorkspaceMapLine(keyLine.text);
147
- if (!key)
148
- throw new Error('pnpm-workspace.yaml contains an unsupported mapping key.');
149
- if (key.indent.includes('\t'))
150
- throw new Error('pnpm-workspace.yaml managed sections must use spaces for indentation.');
151
- let sectionEndIndex = lines.length;
152
- for (let index = keyLineIndex + 1; index < lines.length; index += 1) {
153
- const indent = workspaceLineIndent(lines[index].text);
154
- if (indent !== null && indent.length <= key.indent.length) {
155
- sectionEndIndex = index;
156
- break;
157
- }
158
- }
159
- return { lines, keyLineIndex, sectionEndIndex, keyLine, key };
160
- }
161
- function findTopLevelWorkspaceBlock(content, key) {
162
- const lines = sourceLines(content);
163
- const matches = lines.flatMap((line, index) => {
164
- const parsed = parseWorkspaceMapLine(line.text);
165
- return parsed?.indent === '' && parsed.key === key ? [index] : [];
166
- });
167
- if (matches.length > 1)
168
- throw new Error(`pnpm-workspace.yaml contains duplicate top-level ${key} keys.`);
169
- return matches.length === 1 ? workspaceBlockAt(lines, matches[0]) : null;
170
- }
171
- function appendWorkspaceLines(content, offset, additions) {
172
- if (additions.length === 0)
173
- return content;
174
- const newline = workspaceNewline(content);
175
- const leading = offset > 0 && !/(?:\r\n|\n|\r)$/.test(content.slice(0, offset)) ? newline : '';
176
- return `${content.slice(0, offset)}${leading}${additions.join(newline)}${newline}${content.slice(offset)}`;
177
- }
178
- function appendTopLevelWorkspaceKey(content, key) {
179
- return appendWorkspaceLines(content, content.length, [`${key}:`]);
180
- }
181
- function requireWorkspaceBlockValue(block, fieldPath) {
182
- if (!/^[ \t]*(?:#.*)?$/.test(block.key.rest)) {
183
- throw new Error(`Conflict in ${fieldPath}: expected a block value that can be merged safely.`);
184
- }
185
- }
186
- function mergeWorkspaceListBlock(content, block, desiredValues, fieldPath) {
187
- requireWorkspaceBlockValue(block, fieldPath);
188
- const existing = new Set();
189
- let indent = `${block.key.indent} `;
190
- let insertionOffset = block.keyLine.end;
191
- let foundItem = false;
192
- for (let index = block.keyLineIndex + 1; index < block.sectionEndIndex; index += 1) {
193
- const line = block.lines[index];
194
- if (workspaceLineIndent(line.text) === null)
195
- continue;
196
- const parsed = parseWorkspaceListLine(line.text);
197
- if (!parsed || parsed.indent.includes('\t') || parsed.indent.length <= block.key.indent.length) {
198
- throw new Error(`Conflict in ${fieldPath}: expected a simple block list of strings.`);
199
- }
200
- if (foundItem && parsed.indent !== indent) {
201
- throw new Error(`Conflict in ${fieldPath}: list items must use consistent indentation.`);
202
- }
203
- foundItem = true;
204
- indent = parsed.indent;
205
- existing.add(parsed.value);
206
- insertionOffset = line.end;
207
- }
208
- const additions = [...new Set(desiredValues)].filter((value) => !existing.has(value));
209
- return appendWorkspaceLines(content, insertionOffset, additions.map((value) => `${indent}- ${quoteWorkspaceString(value)}`));
210
- }
211
- function ensureTopLevelWorkspaceBlock(content, key) {
212
- const existing = findTopLevelWorkspaceBlock(content, key);
213
- if (existing)
214
- return { content, block: existing };
215
- const appended = appendTopLevelWorkspaceKey(content, key);
216
- return { content: appended, block: findTopLevelWorkspaceBlock(appended, key) };
217
- }
218
- function mergeTopLevelWorkspaceList(content, key, desiredValues) {
219
- const ensured = ensureTopLevelWorkspaceBlock(content, key);
220
- return mergeWorkspaceListBlock(ensured.content, ensured.block, desiredValues, `pnpm-workspace.yaml ${key}`);
221
- }
222
- function mergeWorkspaceMapBlock(content, block, desiredValues, fieldPath, allowGeneratedUpdates) {
223
- requireWorkspaceBlockValue(block, fieldPath);
224
- const entries = new Map();
225
- let indent = `${block.key.indent} `;
226
- let insertionOffset = block.keyLine.end;
227
- let foundEntry = false;
228
- for (let index = block.keyLineIndex + 1; index < block.sectionEndIndex; index += 1) {
229
- const line = block.lines[index];
230
- if (workspaceLineIndent(line.text) === null)
231
- continue;
232
- const parsed = parseWorkspaceMapLine(line.text);
233
- if (!parsed || parsed.indent.includes('\t') || parsed.indent.length <= block.key.indent.length) {
234
- throw new Error(`Conflict in ${fieldPath}: expected a simple string-to-string mapping.`);
235
- }
236
- if (foundEntry && parsed.indent !== indent) {
237
- throw new Error(`Conflict in ${fieldPath}: mapping entries must use consistent indentation.`);
238
- }
239
- const scalar = parseWorkspaceStringScalar(parsed.rest);
240
- if (!scalar)
241
- throw new Error(`Conflict in ${fieldPath}: mapping values must be strings.`);
242
- if (entries.has(parsed.key))
243
- throw new Error(`Conflict in ${fieldPath}: duplicate mapping entry ${parsed.key}.`);
244
- foundEntry = true;
245
- indent = parsed.indent;
246
- insertionOffset = line.end;
247
- entries.set(parsed.key, { line, parsed, scalar });
248
- }
249
- const additions = [];
250
- const replacements = [];
251
- for (const [key, desiredValue] of Object.entries(desiredValues)) {
252
- const existing = entries.get(key);
253
- if (!existing) {
254
- additions.push(`${indent}${quoteWorkspaceString(key)}: ${quoteWorkspaceString(desiredValue)}`);
255
- continue;
256
- }
257
- if (existing.scalar.value === desiredValue)
258
- continue;
259
- if (!allowGeneratedUpdates) {
260
- throw new Error(`Conflict in ${fieldPath}: mapping entry ${key} differs from the generated value.`);
261
- }
262
- replacements.push({
263
- start: existing.line.start + existing.parsed.restOffset + existing.scalar.tokenStart,
264
- end: existing.line.start + existing.parsed.restOffset + existing.scalar.tokenEnd,
265
- value: quoteWorkspaceString(desiredValue),
266
- });
267
- }
268
- let merged = appendWorkspaceLines(content, insertionOffset, additions);
269
- for (const replacement of replacements.sort((left, right) => right.start - left.start)) {
270
- merged = `${merged.slice(0, replacement.start)}${replacement.value}${merged.slice(replacement.end)}`;
271
- }
272
- return merged;
273
- }
274
- function mergeTopLevelWorkspaceMap(content, key, desiredValues, allowGeneratedUpdates) {
275
- const ensured = ensureTopLevelWorkspaceBlock(content, key);
276
- return mergeWorkspaceMapBlock(ensured.content, ensured.block, desiredValues, `pnpm-workspace.yaml ${key}`, allowGeneratedUpdates);
277
- }
278
- function directChildIndent(block, fieldPath) {
279
- let childIndent = null;
280
- for (let index = block.keyLineIndex + 1; index < block.sectionEndIndex; index += 1) {
281
- const indent = workspaceLineIndent(block.lines[index].text);
282
- if (indent === null)
283
- continue;
284
- if (indent.includes('\t') || indent.length <= block.key.indent.length) {
285
- throw new Error(`Conflict in ${fieldPath}: expected an indented mapping.`);
286
- }
287
- if (childIndent === null || indent.length < childIndent.length)
288
- childIndent = indent;
289
- }
290
- return childIndent ?? `${block.key.indent} `;
291
- }
292
- function findDirectWorkspaceBlock(parent, key, fieldPath) {
293
- const childIndent = directChildIndent(parent, fieldPath);
294
- const matches = [];
295
- for (let index = parent.keyLineIndex + 1; index < parent.sectionEndIndex; index += 1) {
296
- const line = parent.lines[index];
297
- const indent = workspaceLineIndent(line.text);
298
- if (indent === null || indent.length !== childIndent.length)
299
- continue;
300
- const parsed = parseWorkspaceMapLine(line.text);
301
- if (!parsed || parsed.indent !== childIndent) {
302
- throw new Error(`Conflict in ${fieldPath}: expected a mapping of named policies.`);
303
- }
304
- if (parsed.key === key)
305
- matches.push(index);
306
- }
307
- if (matches.length > 1)
308
- throw new Error(`Conflict in ${fieldPath}: duplicate ${key} keys.`);
309
- return matches.length === 1 ? workspaceBlockAt(parent.lines, matches[0]) : null;
310
- }
311
- function ensureDirectWorkspaceBlock(content, parentKey, key, fieldPath) {
312
- let parent = findTopLevelWorkspaceBlock(content, parentKey);
313
- if (!parent) {
314
- content = appendTopLevelWorkspaceKey(content, parentKey);
315
- parent = findTopLevelWorkspaceBlock(content, parentKey);
316
- }
317
- requireWorkspaceBlockValue(parent, `pnpm-workspace.yaml ${parentKey}`);
318
- const existing = findDirectWorkspaceBlock(parent, key, fieldPath);
319
- if (existing)
320
- return { content, block: existing };
321
- const indent = directChildIndent(parent, fieldPath);
322
- let insertionOffset = parent.keyLine.end;
323
- for (let index = parent.keyLineIndex + 1; index < parent.sectionEndIndex; index += 1) {
324
- const line = parent.lines[index];
325
- if (workspaceLineIndent(line.text) !== null)
326
- insertionOffset = line.end;
327
- }
328
- content = appendWorkspaceLines(content, insertionOffset, [`${indent}${key}:`]);
329
- parent = findTopLevelWorkspaceBlock(content, parentKey);
330
- return { content, block: findDirectWorkspaceBlock(parent, key, fieldPath) };
331
- }
332
- function applyWorkspaceAutoInstallPeers(content, allowGeneratedUpdates) {
333
- const block = findTopLevelWorkspaceBlock(content, 'autoInstallPeers');
334
- if (!block) {
335
- const newline = workspaceNewline(content);
336
- const separator = content.length > 0 && !/(?:\r\n|\n|\r)$/.test(content) ? newline : '';
337
- return `${content}${separator}autoInstallPeers: false${newline}`;
338
- }
339
- const scalar = /^([ \t]*)(true|false)([ \t]*(?:#.*)?)$/i.exec(block.key.rest);
340
- if (!scalar)
341
- throw new Error('Conflict in pnpm-workspace.yaml: autoInstallPeers must be the boolean false.');
342
- for (let index = block.keyLineIndex + 1; index < block.sectionEndIndex; index += 1) {
343
- if (workspaceLineIndent(block.lines[index].text) !== null) {
344
- throw new Error('Conflict in pnpm-workspace.yaml: autoInstallPeers must be a scalar boolean.');
345
- }
346
- }
347
- if (scalar[2].toLowerCase() === 'false')
348
- return content;
349
- if (!allowGeneratedUpdates) {
350
- throw new Error('Conflict in pnpm-workspace.yaml: autoInstallPeers must be false to avoid unsafe optional peer resolution.');
351
- }
352
- const start = block.keyLine.start + block.key.restOffset + scalar[1].length;
353
- return `${content.slice(0, start)}false${content.slice(start + scalar[2].length)}`;
354
- }
355
- function applyWorkspacePolicy(content, policy, allowGeneratedUpdates) {
356
- let merged = applyWorkspaceAutoInstallPeers(content, allowGeneratedUpdates);
357
- merged = mergeTopLevelWorkspaceMap(merged, 'overrides', policy.overrides, allowGeneratedUpdates);
358
- merged = mergeTopLevelWorkspaceList(merged, 'onlyBuiltDependencies', policy.onlyBuiltDependencies);
359
- merged = mergeTopLevelWorkspaceList(merged, 'ignoredBuiltDependencies', policy.ignoredBuiltDependencies);
360
- const ignoreMissing = ensureDirectWorkspaceBlock(merged, 'peerDependencyRules', 'ignoreMissing', 'pnpm-workspace.yaml peerDependencyRules');
361
- merged = mergeWorkspaceListBlock(ignoreMissing.content, ignoreMissing.block, policy.peerDependencyRules.ignoreMissing, 'pnpm-workspace.yaml peerDependencyRules.ignoreMissing');
362
- const allowedVersions = ensureDirectWorkspaceBlock(merged, 'peerDependencyRules', 'allowedVersions', 'pnpm-workspace.yaml peerDependencyRules');
363
- return mergeWorkspaceMapBlock(allowedVersions.content, allowedVersions.block, policy.peerDependencyRules.allowedVersions, 'pnpm-workspace.yaml peerDependencyRules.allowedVersions', allowGeneratedUpdates);
364
- }
365
- export function mergeWorkspaceFile(root, desiredPackages, policy, allowGeneratedUpdates) {
366
- const filePath = path.join(root, 'pnpm-workspace.yaml');
367
- const current = pathEntryExists(filePath) ? readFileSync(filePath, 'utf8') : '';
368
- let merged = mergeTopLevelWorkspaceList(current, 'packages', desiredPackages);
369
- merged = applyWorkspacePolicy(merged, policy, allowGeneratedUpdates);
370
- if (merged !== current || !pathEntryExists(filePath))
371
- writeFile(filePath, merged);
372
- }