aegisnode 0.0.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.
@@ -0,0 +1,72 @@
1
+ import crypto from 'crypto';
2
+ import path from 'path';
3
+ import { ensureDir, ensureValidName, exists, isDirectoryEmpty, writeFile } from '../utils/fs.js';
4
+ import {
5
+ renderEnvExample,
6
+ renderProjectEnv,
7
+ renderProjectAppJs,
8
+ renderProjectGitIgnore,
9
+ renderProjectLoaderCjs,
10
+ renderProjectPackageJson,
11
+ renderProjectRoutes,
12
+ renderProjectSettings,
13
+ } from '../utils/scaffolds.js';
14
+
15
+ async function createSecret() {
16
+ try {
17
+ const jliveModule = await import('jlive');
18
+ const JliveEncrypt = jliveModule?.JliveEncrypt;
19
+ if (JliveEncrypt && typeof JliveEncrypt.generate === 'function') {
20
+ return JliveEncrypt.generate(64);
21
+ }
22
+ } catch {
23
+ // Fallback when jlive is not installed or not compatible.
24
+ }
25
+
26
+ return crypto.randomBytes(32).toString('hex');
27
+ }
28
+
29
+ async function assertCanCreateProject(projectDir) {
30
+ if (!(await exists(projectDir))) {
31
+ return;
32
+ }
33
+
34
+ const empty = await isDirectoryEmpty(projectDir);
35
+ if (!empty) {
36
+ throw new Error(`Directory already exists and is not empty: ${projectDir}`);
37
+ }
38
+ }
39
+
40
+ async function createBaseProjectFiles(projectRoot, projectName) {
41
+ const apps = [];
42
+ const appSecret = await createSecret();
43
+
44
+ await ensureDir(projectRoot);
45
+ await Promise.all([
46
+ ensureDir(path.join(projectRoot, 'apps')),
47
+ ]);
48
+
49
+ await writeFile(path.join(projectRoot, 'app.js'), renderProjectAppJs());
50
+ await writeFile(path.join(projectRoot, 'loader.cjs'), renderProjectLoaderCjs());
51
+ await writeFile(path.join(projectRoot, 'package.json'), renderProjectPackageJson(projectName));
52
+ await writeFile(path.join(projectRoot, '.gitignore'), renderProjectGitIgnore());
53
+ await writeFile(path.join(projectRoot, '.env'), renderProjectEnv(appSecret));
54
+ await writeFile(path.join(projectRoot, '.env.example'), renderEnvExample());
55
+
56
+ await writeFile(path.join(projectRoot, 'settings.js'), renderProjectSettings(projectName, apps));
57
+ await writeFile(path.join(projectRoot, 'routes.js'), renderProjectRoutes());
58
+ }
59
+
60
+ export async function startProject({ projectName, cwd }) {
61
+ ensureValidName(projectName, 'project');
62
+
63
+ const projectRoot = path.resolve(cwd, projectName);
64
+ await assertCanCreateProject(projectRoot);
65
+ await createBaseProjectFiles(projectRoot, projectName);
66
+
67
+ console.log(`AegisNode project created at ${projectRoot}`);
68
+ console.log('Next steps:');
69
+ console.log(` cd ${projectName}`);
70
+ console.log(' npm install');
71
+ console.log(' aegisnode runserver');
72
+ }
@@ -0,0 +1,355 @@
1
+ import fs from 'fs/promises';
2
+ import http from 'http';
3
+ import https from 'https';
4
+ import path from 'path';
5
+ import { spawn } from 'child_process';
6
+ import { resolveProjectRoot } from '../utils/project.js';
7
+ import { exists, writeFile } from '../utils/fs.js';
8
+
9
+ const DEFAULT_REGISTRY_BASE_URL = 'https://registry.npmjs.org/';
10
+ const DEPENDENCY_SECTIONS = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'];
11
+ const UNSUPPORTED_SPEC_PREFIXES = [
12
+ 'file:',
13
+ 'link:',
14
+ 'workspace:',
15
+ 'git:',
16
+ 'git+',
17
+ 'github:',
18
+ 'http:',
19
+ 'https:',
20
+ ];
21
+ const MAX_REDIRECTS = 5;
22
+
23
+ function ensureTrailingSlash(value) {
24
+ return value.endsWith('/') ? value : `${value}/`;
25
+ }
26
+
27
+ function encodePackageName(name) {
28
+ return name.replace(/\//g, '%2f');
29
+ }
30
+
31
+ function getVersionPrefix(spec) {
32
+ if (typeof spec !== 'string' || spec.length === 0) {
33
+ return '';
34
+ }
35
+
36
+ if (spec.startsWith('^') || spec.startsWith('~')) {
37
+ return spec[0];
38
+ }
39
+
40
+ return '';
41
+ }
42
+
43
+ function isRegistryDependencySpec(spec) {
44
+ if (typeof spec !== 'string' || spec.trim().length === 0) {
45
+ return false;
46
+ }
47
+
48
+ const normalized = spec.trim();
49
+ if (normalized.startsWith('npm:')) {
50
+ return parseNpmAliasSpec(normalized) !== null;
51
+ }
52
+
53
+ return !UNSUPPORTED_SPEC_PREFIXES.some((prefix) => normalized.startsWith(prefix));
54
+ }
55
+
56
+ function parseNpmAliasSpec(spec) {
57
+ if (typeof spec !== 'string' || !spec.startsWith('npm:')) {
58
+ return null;
59
+ }
60
+
61
+ const remainder = spec.slice(4);
62
+ const versionDelimiter = remainder.lastIndexOf('@');
63
+ if (versionDelimiter <= 0) {
64
+ return null;
65
+ }
66
+
67
+ const targetPackageName = remainder.slice(0, versionDelimiter);
68
+ const targetVersionSpec = remainder.slice(versionDelimiter + 1);
69
+ if (targetPackageName.trim().length === 0 || targetVersionSpec.trim().length === 0) {
70
+ return null;
71
+ }
72
+
73
+ return {
74
+ targetPackageName,
75
+ targetVersionSpec,
76
+ };
77
+ }
78
+
79
+ function resolveRegistryPackageName(packageName, versionSpec) {
80
+ const aliasSpec = parseNpmAliasSpec(versionSpec);
81
+ if (aliasSpec) {
82
+ return aliasSpec.targetPackageName;
83
+ }
84
+
85
+ return packageName;
86
+ }
87
+
88
+ function buildNextVersionSpec(versionSpec, latestVersion) {
89
+ const aliasSpec = parseNpmAliasSpec(versionSpec);
90
+ if (aliasSpec) {
91
+ return `npm:${aliasSpec.targetPackageName}@${getVersionPrefix(aliasSpec.targetVersionSpec)}${latestVersion}`;
92
+ }
93
+
94
+ return `${getVersionPrefix(String(versionSpec))}${latestVersion}`;
95
+ }
96
+
97
+ function parsePackageManager(packageJson) {
98
+ const declared = packageJson?.packageManager;
99
+ if (typeof declared === 'string' && declared.includes('@')) {
100
+ const [manager] = declared.split('@');
101
+ if (manager === 'npm' || manager === 'pnpm' || manager === 'yarn' || manager === 'bun') {
102
+ return manager;
103
+ }
104
+ }
105
+
106
+ return null;
107
+ }
108
+
109
+ async function detectPackageManager(projectRoot, packageJson) {
110
+ const declared = parsePackageManager(packageJson);
111
+ if (declared) {
112
+ return declared;
113
+ }
114
+
115
+ if (await exists(path.join(projectRoot, 'pnpm-lock.yaml'))) {
116
+ return 'pnpm';
117
+ }
118
+
119
+ if (await exists(path.join(projectRoot, 'yarn.lock'))) {
120
+ return 'yarn';
121
+ }
122
+
123
+ if (await exists(path.join(projectRoot, 'bun.lockb')) || await exists(path.join(projectRoot, 'bun.lock'))) {
124
+ return 'bun';
125
+ }
126
+
127
+ return 'npm';
128
+ }
129
+
130
+ async function requestJson(url, redirectCount = 0) {
131
+ const target = new URL(url);
132
+ const transport = target.protocol === 'https:' ? https : http;
133
+
134
+ return new Promise((resolve, reject) => {
135
+ const request = transport.get(target, {
136
+ headers: {
137
+ accept: 'application/json',
138
+ },
139
+ }, (response) => {
140
+ const status = response.statusCode || 0;
141
+
142
+ if (status >= 300 && status < 400 && response.headers.location) {
143
+ if (redirectCount >= MAX_REDIRECTS) {
144
+ reject(new Error(`Registry request exceeded redirect limit for ${url}.`));
145
+ return;
146
+ }
147
+
148
+ response.resume();
149
+ const nextUrl = new URL(response.headers.location, target).toString();
150
+ requestJson(nextUrl, redirectCount + 1).then(resolve, reject);
151
+ return;
152
+ }
153
+
154
+ let body = '';
155
+ response.setEncoding('utf8');
156
+ response.on('data', (chunk) => {
157
+ body += chunk;
158
+ });
159
+ response.on('end', () => {
160
+ if (status < 200 || status >= 300) {
161
+ const detail = body.trim().slice(0, 200);
162
+ reject(new Error(`Registry request failed for ${url}: ${status}${detail ? ` ${detail}` : ''}`));
163
+ return;
164
+ }
165
+
166
+ try {
167
+ resolve(JSON.parse(body));
168
+ } catch (error) {
169
+ reject(new Error(`Registry returned invalid JSON for ${url}: ${error.message}`));
170
+ }
171
+ });
172
+ });
173
+
174
+ request.on('error', reject);
175
+ });
176
+ }
177
+
178
+ function createLatestVersionResolver({
179
+ registryBaseUrl = DEFAULT_REGISTRY_BASE_URL,
180
+ fetchJson = requestJson,
181
+ } = {}) {
182
+ const cache = new Map();
183
+ const baseUrl = ensureTrailingSlash(registryBaseUrl);
184
+
185
+ return async function resolveLatestVersion(packageName) {
186
+ if (!cache.has(packageName)) {
187
+ cache.set(packageName, (async () => {
188
+ const url = new URL(encodePackageName(packageName), baseUrl).toString();
189
+ const metadata = await fetchJson(url);
190
+ const latestVersion = metadata?.['dist-tags']?.latest;
191
+
192
+ if (typeof latestVersion !== 'string' || latestVersion.trim().length === 0) {
193
+ throw new Error(`Package "${packageName}" is missing dist-tags.latest in ${url}.`);
194
+ }
195
+
196
+ return latestVersion.trim();
197
+ })());
198
+ }
199
+
200
+ return cache.get(packageName);
201
+ };
202
+ }
203
+
204
+ function getInstallCommand(packageManager) {
205
+ const binary = process.platform === 'win32' ? `${packageManager}.cmd` : packageManager;
206
+ return {
207
+ command: binary,
208
+ args: ['install'],
209
+ };
210
+ }
211
+
212
+ async function runInstall(projectRoot, packageManager, output) {
213
+ const { command, args } = getInstallCommand(packageManager);
214
+ output.log(`Running ${packageManager} install...`);
215
+
216
+ await new Promise((resolve, reject) => {
217
+ const child = spawn(command, args, {
218
+ cwd: projectRoot,
219
+ stdio: 'inherit',
220
+ });
221
+
222
+ child.on('error', reject);
223
+ child.on('exit', (code, signal) => {
224
+ if (code === 0) {
225
+ resolve();
226
+ return;
227
+ }
228
+
229
+ if (signal) {
230
+ reject(new Error(`${packageManager} install terminated by signal ${signal}.`));
231
+ return;
232
+ }
233
+
234
+ reject(new Error(`${packageManager} install failed with exit code ${code}.`));
235
+ });
236
+ });
237
+ }
238
+
239
+ export async function runUpdateDependencies({
240
+ projectRoot,
241
+ registryBaseUrl = DEFAULT_REGISTRY_BASE_URL,
242
+ installDependencies = true,
243
+ output = console,
244
+ } = {}) {
245
+ const resolvedRoot = await resolveProjectRoot(projectRoot || process.cwd());
246
+ const packageJsonPath = path.join(resolvedRoot, 'package.json');
247
+ let packageJsonRaw;
248
+
249
+ try {
250
+ packageJsonRaw = await fs.readFile(packageJsonPath, 'utf8');
251
+ } catch (error) {
252
+ throw new Error(`Could not read package.json from ${packageJsonPath}: ${error.message}`);
253
+ }
254
+
255
+ let packageJson;
256
+ try {
257
+ packageJson = JSON.parse(packageJsonRaw);
258
+ } catch (error) {
259
+ throw new Error(`Invalid package.json at ${packageJsonPath}: ${error.message}`);
260
+ }
261
+
262
+ const resolveLatestVersion = createLatestVersionResolver({ registryBaseUrl });
263
+ const updatedEntries = [];
264
+ const unchangedEntries = [];
265
+ const skippedEntries = [];
266
+ let totalDependencyEntries = 0;
267
+
268
+ for (const section of DEPENDENCY_SECTIONS) {
269
+ const dependencies = packageJson?.[section];
270
+ if (!dependencies || typeof dependencies !== 'object' || Array.isArray(dependencies)) {
271
+ continue;
272
+ }
273
+
274
+ const entries = Object.entries(dependencies);
275
+
276
+ await Promise.all(entries.map(async ([packageName, versionSpec]) => {
277
+ totalDependencyEntries += 1;
278
+
279
+ if (!isRegistryDependencySpec(versionSpec)) {
280
+ skippedEntries.push({
281
+ section,
282
+ packageName,
283
+ versionSpec,
284
+ reason: `unsupported source spec "${String(versionSpec)}"`,
285
+ });
286
+ return;
287
+ }
288
+
289
+ const latestVersion = await resolveLatestVersion(resolveRegistryPackageName(packageName, versionSpec));
290
+ const nextVersionSpec = buildNextVersionSpec(versionSpec, latestVersion);
291
+
292
+ if (String(versionSpec) === nextVersionSpec) {
293
+ unchangedEntries.push({
294
+ section,
295
+ packageName,
296
+ versionSpec: String(versionSpec),
297
+ });
298
+ return;
299
+ }
300
+
301
+ dependencies[packageName] = nextVersionSpec;
302
+ updatedEntries.push({
303
+ section,
304
+ packageName,
305
+ from: String(versionSpec),
306
+ to: nextVersionSpec,
307
+ });
308
+ }));
309
+ }
310
+
311
+ if (totalDependencyEntries === 0) {
312
+ output.log(`No dependencies found in ${packageJsonPath}.`);
313
+ return {
314
+ rootDir: resolvedRoot,
315
+ packageManager: null,
316
+ updatedEntries,
317
+ unchangedEntries,
318
+ skippedEntries,
319
+ };
320
+ }
321
+
322
+ if (updatedEntries.length > 0) {
323
+ await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
324
+
325
+ for (const entry of updatedEntries) {
326
+ output.log(`Updated ${entry.section}.${entry.packageName}: ${entry.from} -> ${entry.to}`);
327
+ }
328
+ } else {
329
+ output.log('All registry-backed dependency specs are already on the current latest version.');
330
+ }
331
+
332
+ for (const entry of skippedEntries) {
333
+ output.log(`Skipped ${entry.section}.${entry.packageName}: ${entry.reason}`);
334
+ }
335
+
336
+ const packageManager = updatedEntries.length > 0 && installDependencies
337
+ ? await detectPackageManager(resolvedRoot, packageJson)
338
+ : null;
339
+
340
+ if (packageManager) {
341
+ await runInstall(resolvedRoot, packageManager, output);
342
+ }
343
+
344
+ output.log(
345
+ `updatedeps summary: ${updatedEntries.length} updated, ${unchangedEntries.length} unchanged, ${skippedEntries.length} skipped.`,
346
+ );
347
+
348
+ return {
349
+ rootDir: resolvedRoot,
350
+ packageManager,
351
+ updatedEntries,
352
+ unchangedEntries,
353
+ skippedEntries,
354
+ };
355
+ }
@@ -0,0 +1,151 @@
1
+ import { startProject } from './commands/startproject.js';
2
+ import { createApp } from './commands/createapp.js';
3
+ import { runServer } from './commands/runserver.js';
4
+ import { generateArtifact } from './commands/generate.js';
5
+ import { runDoctor } from './commands/doctor.js';
6
+ import { runUpdateDependencies } from './commands/updatedeps.js';
7
+
8
+ function printHelp() {
9
+ console.log(`AegisNode CLI
10
+
11
+ Usage:
12
+ aegisnode startproject <project-name>
13
+ aegisnode createapp <app-name> [--project <path>] [--mount </path>]
14
+ aegisnode generate <type> <name> --app <app-name> [--project <path>]
15
+ aegisnode runserver [--project <path>] [--port <number>]
16
+ aegisnode doctor [--project <path>]
17
+ aegisnode updatedeps [--project <path>]
18
+
19
+ Examples:
20
+ aegisnode startproject blog
21
+ cd blog
22
+ npm install
23
+ aegisnode runserver
24
+ aegisnode createapp users
25
+ aegisnode generate view user --app users
26
+ aegisnode generate validator user --app users
27
+ aegisnode updatedeps --project blog
28
+ `);
29
+ }
30
+
31
+ function parseFlags(tokens) {
32
+ const flags = {};
33
+ const positional = [];
34
+
35
+ for (let index = 0; index < tokens.length; index += 1) {
36
+ const token = tokens[index];
37
+ if (!token.startsWith('-')) {
38
+ positional.push(token);
39
+ continue;
40
+ }
41
+
42
+ if (token === '-p' || token === '--port') {
43
+ flags.port = tokens[index + 1];
44
+ index += 1;
45
+ continue;
46
+ }
47
+
48
+ if (token === '--project') {
49
+ flags.project = tokens[index + 1];
50
+ index += 1;
51
+ continue;
52
+ }
53
+
54
+ if (token === '--mount') {
55
+ flags.mount = tokens[index + 1];
56
+ index += 1;
57
+ continue;
58
+ }
59
+
60
+ if (token === '--app') {
61
+ flags.app = tokens[index + 1];
62
+ index += 1;
63
+ continue;
64
+ }
65
+
66
+ if (token === '-h' || token === '--help') {
67
+ flags.help = true;
68
+ continue;
69
+ }
70
+
71
+ throw new Error(`Unknown flag: ${token}`);
72
+ }
73
+
74
+ return { flags, positional };
75
+ }
76
+
77
+ export async function runCli(argv) {
78
+ if (!argv.length) {
79
+ printHelp();
80
+ return;
81
+ }
82
+
83
+ const [command, ...rest] = argv;
84
+ const { flags, positional } = parseFlags(rest);
85
+
86
+ if (flags.help || command === 'help') {
87
+ printHelp();
88
+ return;
89
+ }
90
+
91
+ switch (command) {
92
+ case 'startproject': {
93
+ const [projectName] = positional;
94
+ if (!projectName) {
95
+ throw new Error('Missing project name. Usage: aegisnode startproject <project-name>');
96
+ }
97
+ await startProject({ projectName, cwd: process.cwd() });
98
+ return;
99
+ }
100
+
101
+ case 'createapp': {
102
+ const [appName] = positional;
103
+ if (!appName) {
104
+ throw new Error('Missing app name. Usage: aegisnode createapp <app-name>');
105
+ }
106
+ await createApp({
107
+ appName,
108
+ projectRoot: flags.project ? String(flags.project) : process.cwd(),
109
+ mount: flags.mount ? String(flags.mount) : undefined,
110
+ });
111
+ return;
112
+ }
113
+
114
+ case 'runserver': {
115
+ await runServer({
116
+ projectRoot: flags.project ? String(flags.project) : process.cwd(),
117
+ port: flags.port ? Number(flags.port) : undefined,
118
+ });
119
+ return;
120
+ }
121
+
122
+ case 'doctor': {
123
+ await runDoctor({
124
+ projectRoot: flags.project ? String(flags.project) : process.cwd(),
125
+ });
126
+ return;
127
+ }
128
+
129
+ case 'updatedeps': {
130
+ await runUpdateDependencies({
131
+ projectRoot: flags.project ? String(flags.project) : process.cwd(),
132
+ });
133
+ return;
134
+ }
135
+
136
+ case 'generate':
137
+ case 'g': {
138
+ const [type, name] = positional;
139
+ await generateArtifact({
140
+ type,
141
+ name,
142
+ appName: flags.app ? String(flags.app) : undefined,
143
+ projectRoot: flags.project ? String(flags.project) : process.cwd(),
144
+ });
145
+ return;
146
+ }
147
+
148
+ default:
149
+ throw new Error(`Unknown command: ${command}`);
150
+ }
151
+ }
@@ -0,0 +1,53 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ export async function exists(targetPath) {
5
+ try {
6
+ await fs.access(targetPath);
7
+ return true;
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ export async function isDirectoryEmpty(targetPath) {
14
+ const entries = await fs.readdir(targetPath);
15
+ return entries.length === 0;
16
+ }
17
+
18
+ export async function ensureDir(targetPath) {
19
+ await fs.mkdir(targetPath, { recursive: true });
20
+ }
21
+
22
+ export async function writeFile(targetPath, content) {
23
+ await ensureDir(path.dirname(targetPath));
24
+ await fs.writeFile(targetPath, content, 'utf8');
25
+ }
26
+
27
+ export function normalizeMountPath(input) {
28
+ if (!input || input === '/') {
29
+ return '/';
30
+ }
31
+
32
+ const normalized = `/${String(input).trim().replace(/^\/+/, '').replace(/\/+$/, '')}`;
33
+ const segments = normalized.split('/').filter(Boolean);
34
+
35
+ if (segments.length === 0) {
36
+ return '/';
37
+ }
38
+
39
+ const invalidSegment = segments.find((segment) => !/^[a-zA-Z0-9:_-]+$/.test(segment));
40
+ if (invalidSegment) {
41
+ throw new Error(
42
+ `Invalid mount path segment "${invalidSegment}". Use letters, numbers, "_", "-", and optional ":" params only.`,
43
+ );
44
+ }
45
+
46
+ return `/${segments.join('/')}`;
47
+ }
48
+
49
+ export function ensureValidName(input, type) {
50
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(input)) {
51
+ throw new Error(`Invalid ${type} name \"${input}\". Use letters, numbers, _ and - only.`);
52
+ }
53
+ }
@@ -0,0 +1,67 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { exists } from './fs.js';
4
+
5
+ export async function hasRoutesFile(projectRoot) {
6
+ return (await exists(path.join(projectRoot, 'routes.js')))
7
+ || (await exists(path.join(projectRoot, 'routes', 'index.js')));
8
+ }
9
+
10
+ export async function hasSettingsFile(projectRoot) {
11
+ return (await exists(path.join(projectRoot, 'settings.js')))
12
+ || (await exists(path.join(projectRoot, 'settings', 'index.js')))
13
+ || (await exists(path.join(projectRoot, 'settings', 'apps.js')));
14
+ }
15
+
16
+ export async function isProjectRoot(projectRoot) {
17
+ const [hasRoutes, hasSettings] = await Promise.all([
18
+ hasRoutesFile(projectRoot),
19
+ hasSettingsFile(projectRoot),
20
+ ]);
21
+ return hasRoutes && hasSettings;
22
+ }
23
+
24
+ export async function resolveProjectRoot(projectRootHint) {
25
+ const startDir = path.resolve(projectRootHint);
26
+
27
+ let cursor = startDir;
28
+ while (true) {
29
+ if (await isProjectRoot(cursor)) {
30
+ return cursor;
31
+ }
32
+ const parent = path.dirname(cursor);
33
+ if (parent === cursor) {
34
+ break;
35
+ }
36
+ cursor = parent;
37
+ }
38
+
39
+ let entries = [];
40
+ try {
41
+ entries = await fs.readdir(startDir, { withFileTypes: true });
42
+ } catch {
43
+ entries = [];
44
+ }
45
+
46
+ const matches = [];
47
+ for (const entry of entries) {
48
+ if (!entry.isDirectory()) {
49
+ continue;
50
+ }
51
+ const candidate = path.join(startDir, entry.name);
52
+ if (await isProjectRoot(candidate)) {
53
+ matches.push(candidate);
54
+ }
55
+ }
56
+
57
+ if (matches.length === 1) {
58
+ return matches[0];
59
+ }
60
+
61
+ if (matches.length > 1) {
62
+ const names = matches.map((item) => path.basename(item)).join(', ');
63
+ throw new Error(`Multiple AegisNode projects found in ${startDir}: ${names}. Use --project <path>.`);
64
+ }
65
+
66
+ throw new Error(`Could not find an AegisNode project from ${startDir}. Run inside project or use --project <path>.`);
67
+ }