anhedral 0.1.10 → 0.3.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 +97 -0
- package/CONTRIBUTING.md +70 -0
- package/LICENSE +21 -17
- package/README.md +151 -152
- package/SECURITY.md +35 -0
- package/SKILL.md +177 -0
- package/bin/anhedral.js +1 -1
- package/dist/architecture/contributions.d.ts +28 -0
- package/dist/architecture/contributions.js +118 -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 +176 -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 +167 -0
- package/dist/cli.d.ts +24 -2
- package/dist/cli.js +186 -21
- package/dist/composers/root.d.ts +6 -0
- package/dist/composers/root.js +34 -0
- package/dist/dependencies.d.ts +29 -9
- package/dist/dependencies.js +220 -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 +56 -12
- package/dist/scaffold.js +1466 -1158
- 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 +3114 -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 +572 -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 +503 -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 +89 -0
- package/docs/output-tree-contract.md +442 -0
- package/package.json +56 -13
- package/references/manual-scaffolding.md +386 -0
- 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
package/dist/ui.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const UI_TARGETS: readonly ["web", "mobile", "desktop", "extension"];
|
|
2
|
+
export declare const NATIVE_STYLING_LIBRARIES: readonly ["nativewind", "uniwind"];
|
|
3
|
+
export declare const UI_PROVIDERS: readonly ["shadcn", "react-native-reusables"];
|
|
4
|
+
export type UiTarget = (typeof UI_TARGETS)[number];
|
|
5
|
+
export type NativeStylingLibrary = (typeof NATIVE_STYLING_LIBRARIES)[number];
|
|
6
|
+
export type UiProvider = (typeof UI_PROVIDERS)[number];
|
|
7
|
+
export type UiComponentInstall = {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly target: UiTarget;
|
|
10
|
+
readonly provider: UiProvider;
|
|
11
|
+
readonly source: string;
|
|
12
|
+
readonly variant: NativeStylingLibrary | null;
|
|
13
|
+
};
|
|
14
|
+
export type UiInstallCommand = {
|
|
15
|
+
readonly target: UiTarget;
|
|
16
|
+
readonly cwd: string;
|
|
17
|
+
readonly executable: 'pnpm';
|
|
18
|
+
readonly args: readonly string[];
|
|
19
|
+
readonly installs: readonly UiComponentInstall[];
|
|
20
|
+
};
|
|
21
|
+
export declare function reconcileUiWorkspaceDependencies(workspaceRoot: string, targetDirectory: string): void;
|
|
22
|
+
export declare function isUiTarget(value: unknown): value is UiTarget;
|
|
23
|
+
export declare function isNativeStylingLibrary(value: unknown): value is NativeStylingLibrary;
|
|
24
|
+
export declare function isUiProvider(value: unknown): value is UiProvider;
|
|
25
|
+
export declare function normalizeUiComponentName(value: string): string;
|
|
26
|
+
export declare function parseUiComponentList(value: string): string[];
|
|
27
|
+
export declare function providerForTarget(target: UiTarget): UiProvider;
|
|
28
|
+
export declare function registrySourceFor(name: string, target: UiTarget, nativeStyling: NativeStylingLibrary): string;
|
|
29
|
+
export declare function resolveUiInstalls(componentNames: readonly string[], targets: readonly UiTarget[], nativeStyling: NativeStylingLibrary): readonly UiComponentInstall[];
|
|
30
|
+
export declare function uiInstallKey(install: Pick<UiComponentInstall, 'target' | 'name'>): string;
|
|
31
|
+
export declare function mergeUiInstalls(current: readonly UiComponentInstall[], additions: readonly UiComponentInstall[]): readonly UiComponentInstall[];
|
|
32
|
+
export declare function buildUiInstallCommands(root: string, installs: readonly UiComponentInstall[]): readonly UiInstallCommand[];
|
|
33
|
+
export declare function installUiComponents(root: string, installs: readonly UiComponentInstall[]): void;
|
package/dist/ui.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
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
|
+
}
|
package/dist/util.d.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
1
|
export declare function writeFile(filePath: string, content: string): void;
|
|
2
2
|
export declare function appendGitignore(root: string, lines: string[]): void;
|
|
3
|
-
export declare function
|
|
4
|
-
export declare function execCommand(command: string, args: string[], cwd: string): void;
|
|
5
|
-
export declare function execWithInput(cmd: string, cwd: string, stdinInput: string): void;
|
|
6
|
-
export declare function liftNestedProject(root: string, nestedName: string): void;
|
|
7
|
-
export declare function execExpect(cmd: string, cwd: string, prompts: [waitFor: string, answer: string][], timeoutMs?: number): Promise<void>;
|
|
3
|
+
export declare function execFile(executable: string, args: readonly string[], cwd: string): void;
|
package/dist/util.js
CHANGED
|
@@ -1,180 +1,91 @@
|
|
|
1
|
-
import { existsSync, mkdirSync,
|
|
2
|
-
import {
|
|
1
|
+
import { chmodSync, closeSync, existsSync, fstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, rmSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
5
|
+
const FAILURE_TAIL_BYTES = 128 * 1024;
|
|
4
6
|
export function writeFile(filePath, content) {
|
|
7
|
+
const existed = existsSync(filePath);
|
|
5
8
|
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
6
|
-
writeFileSync(filePath, content, '
|
|
9
|
+
writeFileSync(filePath, content, { encoding: 'utf8', mode: 0o644 });
|
|
10
|
+
// Defeat a restrictive caller umask for newly generated files. Existing seeded
|
|
11
|
+
// files retain their user-selected mode across structural merges.
|
|
12
|
+
if (!existed)
|
|
13
|
+
chmodSync(filePath, 0o644);
|
|
7
14
|
}
|
|
8
15
|
export function appendGitignore(root, lines) {
|
|
9
16
|
const filePath = path.join(root, '.gitignore');
|
|
10
|
-
const existing = existsSync(filePath)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
writeFile(filePath, merged.join('\n') + '\n');
|
|
15
|
-
}
|
|
16
|
-
const VERBOSE = process.env.ANHEDRAL_VERBOSE === '1';
|
|
17
|
-
function dumpFailure(error, cmd) {
|
|
18
|
-
const err = error;
|
|
19
|
-
const stdoutText = err.stdout ? err.stdout.toString() : '';
|
|
20
|
-
const stderrText = err.stderr ? err.stderr.toString() : '';
|
|
21
|
-
if (stdoutText)
|
|
22
|
-
process.stderr.write(stdoutText);
|
|
23
|
-
if (stderrText)
|
|
24
|
-
process.stderr.write(stderrText);
|
|
25
|
-
throw new Error(`Command failed (exit ${err.status ?? '?'}): ${cmd}`);
|
|
26
|
-
}
|
|
27
|
-
function commandLabel(command, args) {
|
|
28
|
-
return [command, ...args].join(' ');
|
|
29
|
-
}
|
|
30
|
-
export function exec(cmd, cwd) {
|
|
31
|
-
if (VERBOSE) {
|
|
32
|
-
console.log(` $ ${cmd}`);
|
|
33
|
-
execSync(cmd, { cwd, stdio: 'inherit' });
|
|
17
|
+
const existing = existsSync(filePath) ? readFileSync(filePath, 'utf-8') : '';
|
|
18
|
+
const existingLines = new Set(existing.split(/\r\n|\n|\r/));
|
|
19
|
+
const additions = [...new Set(lines)].filter((line) => !existingLines.has(line));
|
|
20
|
+
if (additions.length === 0)
|
|
34
21
|
return;
|
|
35
|
-
|
|
22
|
+
const newline = existing.includes('\r\n') ? '\r\n' : existing.includes('\n') ? '\n' : existing.includes('\r') ? '\r' : '\n';
|
|
23
|
+
const separator = existing.length > 0 && !/(?:\r\n|\n|\r)$/.test(existing) ? newline : '';
|
|
24
|
+
writeFile(filePath, `${existing}${separator}${additions.join(newline)}${newline}`);
|
|
25
|
+
}
|
|
26
|
+
function readTail(filePath) {
|
|
27
|
+
const file = openSync(filePath, 'r');
|
|
36
28
|
try {
|
|
37
|
-
|
|
29
|
+
const size = fstatSync(file).size;
|
|
30
|
+
const length = Math.min(size, FAILURE_TAIL_BYTES);
|
|
31
|
+
if (length === 0)
|
|
32
|
+
return '';
|
|
33
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
34
|
+
readSync(file, buffer, 0, length, size - length);
|
|
35
|
+
return buffer.toString();
|
|
38
36
|
}
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
finally {
|
|
38
|
+
closeSync(file);
|
|
41
39
|
}
|
|
42
40
|
}
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
stdio: VERBOSE ? 'inherit' : ['ignore', 'pipe', 'pipe'],
|
|
52
|
-
});
|
|
53
|
-
if (result.status === 0) {
|
|
54
|
-
return;
|
|
41
|
+
function dumpFailure(cmd, status, stdoutPath, stderrPath, spawnError) {
|
|
42
|
+
const stdoutText = readTail(stdoutPath);
|
|
43
|
+
const stderrText = readTail(stderrPath);
|
|
44
|
+
if (process.env.ANHEDRAL_QUIET !== '1') {
|
|
45
|
+
if (stdoutText)
|
|
46
|
+
process.stderr.write(stdoutText);
|
|
47
|
+
if (stderrText)
|
|
48
|
+
process.stderr.write(stderrText);
|
|
55
49
|
}
|
|
56
|
-
|
|
57
|
-
if (result.stdout)
|
|
58
|
-
process.stderr.write(result.stdout);
|
|
59
|
-
if (result.stderr)
|
|
60
|
-
process.stderr.write(result.stderr);
|
|
61
|
-
}
|
|
62
|
-
throw new Error(`Command failed (exit ${result.status ?? '?'}): ${label}`);
|
|
50
|
+
throw new Error(`Command failed (exit ${status ?? '?'}): ${cmd}`, spawnError ? { cause: spawnError } : undefined);
|
|
63
51
|
}
|
|
64
|
-
export function
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
52
|
+
export function execFile(executable, args, cwd) {
|
|
53
|
+
const quiet = process.env.ANHEDRAL_QUIET === '1';
|
|
54
|
+
const display = [executable, ...args].join(' ');
|
|
55
|
+
if (process.env.ANHEDRAL_VERBOSE === '1' && !quiet) {
|
|
56
|
+
console.log(` $ ${display}`);
|
|
57
|
+
const result = spawnSync(executable, [...args], { cwd, stdio: 'inherit', shell: false });
|
|
58
|
+
if (result.error || result.status !== 0) {
|
|
59
|
+
throw new Error(`Command failed (exit ${result.status ?? '?'}): ${display}`, result.error ? { cause: result.error } : undefined);
|
|
60
|
+
}
|
|
68
61
|
return;
|
|
69
62
|
}
|
|
63
|
+
const captureRoot = mkdtempSync(path.join(tmpdir(), 'anhedral-exec-'));
|
|
64
|
+
const stdoutPath = path.join(captureRoot, 'stdout.log');
|
|
65
|
+
const stderrPath = path.join(captureRoot, 'stderr.log');
|
|
66
|
+
const stdout = openSync(stdoutPath, 'w');
|
|
67
|
+
const stderr = openSync(stderrPath, 'w');
|
|
70
68
|
try {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const targetPath = path.join(targetDir, entry.name);
|
|
81
|
-
if (existsSync(targetPath)) {
|
|
82
|
-
if (!entry.isDirectory() || !statSync(targetPath).isDirectory()) {
|
|
83
|
-
throw new Error(`Cannot merge scaffold output because ${targetPath} already exists.`);
|
|
84
|
-
}
|
|
85
|
-
moveDirectoryContents(sourcePath, targetPath);
|
|
86
|
-
rmSync(sourcePath, { recursive: true, force: true });
|
|
87
|
-
continue;
|
|
69
|
+
const result = spawnSync(executable, [...args], {
|
|
70
|
+
cwd,
|
|
71
|
+
shell: false,
|
|
72
|
+
stdio: ['ignore', stdout, stderr],
|
|
73
|
+
});
|
|
74
|
+
closeSync(stdout);
|
|
75
|
+
closeSync(stderr);
|
|
76
|
+
if (result.error || result.status !== 0) {
|
|
77
|
+
dumpFailure(display, result.status, stdoutPath, stderrPath, result.error);
|
|
88
78
|
}
|
|
89
|
-
renameSync(sourcePath, targetPath);
|
|
90
79
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (!existsSync(nestedRoot)) {
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
const selfNamedChild = path.join(nestedRoot, nestedName);
|
|
98
|
-
const tempSelfNamedChild = path.join(root, `.${nestedName}.anhedral-tmp`);
|
|
99
|
-
const hasSelfNamedChild = existsSync(selfNamedChild);
|
|
100
|
-
if (hasSelfNamedChild) {
|
|
101
|
-
if (existsSync(tempSelfNamedChild)) {
|
|
102
|
-
throw new Error(`Cannot lift scaffold output because ${tempSelfNamedChild} already exists.`);
|
|
80
|
+
finally {
|
|
81
|
+
try {
|
|
82
|
+
closeSync(stdout);
|
|
103
83
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
function trimOutputTail(output, maxLength = 4000) {
|
|
113
|
-
if (output.length <= maxLength) {
|
|
114
|
-
return output;
|
|
115
|
-
}
|
|
116
|
-
return output.slice(output.length - maxLength);
|
|
117
|
-
}
|
|
118
|
-
export async function execExpect(cmd, cwd, prompts, timeoutMs = 120_000) {
|
|
119
|
-
if (VERBOSE) {
|
|
120
|
-
console.log(` $ ${cmd}`);
|
|
84
|
+
catch { }
|
|
85
|
+
try {
|
|
86
|
+
closeSync(stderr);
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
rmSync(captureRoot, { recursive: true, force: true });
|
|
121
90
|
}
|
|
122
|
-
await new Promise((resolve, reject) => {
|
|
123
|
-
const child = spawn(cmd, {
|
|
124
|
-
cwd,
|
|
125
|
-
env: process.env,
|
|
126
|
-
shell: true,
|
|
127
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
128
|
-
});
|
|
129
|
-
let combinedOutput = '';
|
|
130
|
-
let promptIndex = 0;
|
|
131
|
-
let settled = false;
|
|
132
|
-
const finish = (handler) => {
|
|
133
|
-
if (settled) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
settled = true;
|
|
137
|
-
clearTimeout(timeoutId);
|
|
138
|
-
handler();
|
|
139
|
-
};
|
|
140
|
-
const maybeAnswerPrompt = () => {
|
|
141
|
-
while (promptIndex < prompts.length && combinedOutput.includes(prompts[promptIndex][0])) {
|
|
142
|
-
child.stdin.write(`${prompts[promptIndex][1]}\n`);
|
|
143
|
-
promptIndex += 1;
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
|
-
const handleChunk = (chunk, target) => {
|
|
147
|
-
const text = chunk.toString();
|
|
148
|
-
combinedOutput += text;
|
|
149
|
-
if (VERBOSE)
|
|
150
|
-
target.write(text);
|
|
151
|
-
maybeAnswerPrompt();
|
|
152
|
-
};
|
|
153
|
-
const timeoutId = setTimeout(() => {
|
|
154
|
-
child.kill('SIGTERM');
|
|
155
|
-
finish(() => {
|
|
156
|
-
reject(new Error(`Command timed out after ${timeoutMs}ms: ${cmd}\n` +
|
|
157
|
-
`Answered ${promptIndex}/${prompts.length} prompts.\n` +
|
|
158
|
-
`Recent output:\n${trimOutputTail(combinedOutput)}`));
|
|
159
|
-
});
|
|
160
|
-
}, timeoutMs);
|
|
161
|
-
child.stdout.on('data', (chunk) => handleChunk(chunk, process.stdout));
|
|
162
|
-
child.stderr.on('data', (chunk) => handleChunk(chunk, process.stderr));
|
|
163
|
-
child.on('error', (error) => {
|
|
164
|
-
finish(() => {
|
|
165
|
-
reject(error);
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
child.on('close', (code) => {
|
|
169
|
-
finish(() => {
|
|
170
|
-
if (code === 0) {
|
|
171
|
-
resolve();
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
reject(new Error(`Command failed with exit code ${code}: ${cmd}\n` +
|
|
175
|
-
`Answered ${promptIndex}/${prompts.length} prompts.\n` +
|
|
176
|
-
`Recent output:\n${trimOutputTail(combinedOutput)}`));
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
91
|
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
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;
|