ace-pack 0.1.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.
@@ -0,0 +1,137 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import { stdin as input, stdout as output } from 'node:process'
4
+ import readline from 'node:readline'
5
+ import { pathToFileURL } from 'node:url'
6
+
7
+ export const GENERATED_CONTEXT_PATH = '.ai/generated-context.md'
8
+
9
+ export const HUB_MENU = `[ACE] Agentic Context Engine - Knowledge Hub
10
+ Select the context payload you want to generate:
11
+
12
+ [1] AI Coder Context (For Cursor/VSCode agent: Task, changed files, handoff, reflection)
13
+ [2] AI Architect Context (For Browser AI: Strict rules, tech docs, decisions, roadmap. HIGH DENSITY, LOW TOKEN)
14
+ [3] Business Report (For Humans: Roadmap and recent work log)
15
+ [4] Developer Docs (For Onboarding: Tech docs and devops/setup)
16
+ `
17
+
18
+ const CONTEXT_PAYLOADS = {
19
+ 1: [
20
+ requiredFile('.ai/current-task.md'),
21
+ requiredFile('.ai/session-handoff.md'),
22
+ requiredFile('.ai/changed-files.md'),
23
+ requiredFile('.ai/reflection-log.md'),
24
+ ],
25
+ 2: [
26
+ requiredFile('AGENTS.md'),
27
+ requiredFile('.ai/tech-docs.md'),
28
+ requiredFile('.ai/decisions.md'),
29
+ requiredFile('.ai/product-roadmap.md'),
30
+ ],
31
+ 3: [requiredFile('.ai/product-roadmap.md'), requiredFile('.ai/work-log.md')],
32
+ 4: [requiredFile('.ai/tech-docs.md'), optionalFile('DEVOPS.md')],
33
+ }
34
+
35
+ export async function generateContextPayload(rootDir, selection) {
36
+ const files = getPayloadFiles(selection)
37
+ const sections = []
38
+ const includedFiles = []
39
+
40
+ for (const file of files) {
41
+ const content = await readContextFile(rootDir, file)
42
+
43
+ if (content === null) {
44
+ continue
45
+ }
46
+
47
+ includedFiles.push(file.path)
48
+ sections.push(formatContextSection(file.path, content))
49
+ }
50
+
51
+ const outputPath = path.join(rootDir, GENERATED_CONTEXT_PATH)
52
+ await mkdir(path.dirname(outputPath), { recursive: true })
53
+ await writeFile(outputPath, `${sections.join('\n\n')}\n`, 'utf8')
54
+
55
+ return {
56
+ includedFiles,
57
+ outputPath,
58
+ }
59
+ }
60
+
61
+ function getPayloadFiles(selection) {
62
+ const payload = CONTEXT_PAYLOADS[selection.trim()]
63
+
64
+ if (!payload) {
65
+ throw new Error(`Invalid ACE hub option: ${selection}`)
66
+ }
67
+
68
+ return payload
69
+ }
70
+
71
+ async function readContextFile(rootDir, file) {
72
+ const filePath = path.join(rootDir, file.path)
73
+
74
+ try {
75
+ return await readFile(filePath, 'utf8')
76
+ } catch (error) {
77
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
78
+ if (file.required) {
79
+ throw new Error(`Missing required context file: ${file.path}`)
80
+ }
81
+
82
+ return null
83
+ }
84
+
85
+ throw error
86
+ }
87
+ }
88
+
89
+ function formatContextSection(filePath, content) {
90
+ return `# --- FILE: ${filePath} ---\n\n${content.trimEnd()}`
91
+ }
92
+
93
+ function requiredFile(filePath) {
94
+ return {
95
+ path: filePath,
96
+ required: true,
97
+ }
98
+ }
99
+
100
+ function optionalFile(filePath) {
101
+ return {
102
+ path: filePath,
103
+ required: false,
104
+ }
105
+ }
106
+
107
+ async function promptSelection() {
108
+ const rl = readline.createInterface({ input, output })
109
+
110
+ return new Promise((resolve) => {
111
+ rl.question('Enter option: ', (answer) => {
112
+ rl.close()
113
+ resolve(answer)
114
+ })
115
+ })
116
+ }
117
+
118
+ async function main() {
119
+ process.stdout.write(`${HUB_MENU}\n`)
120
+
121
+ const selection = process.argv[2] ?? (await promptSelection())
122
+
123
+ try {
124
+ await generateContextPayload(process.cwd(), selection)
125
+ process.stdout.write(
126
+ "Context generated and saved to .ai/generated-context.md. Copy this file's content to your AI.\n",
127
+ )
128
+ } catch (error) {
129
+ const message = error instanceof Error ? error.message : String(error)
130
+ process.stderr.write(`${message}\n`)
131
+ process.exit(1)
132
+ }
133
+ }
134
+
135
+ if (import.meta.url === pathToFileURL(process.argv[1]).href) {
136
+ await main()
137
+ }
@@ -0,0 +1,503 @@
1
+ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import { pathToFileURL } from 'node:url'
4
+
5
+ import {
6
+ UNIVERSAL_HIGH_RISK_KEYWORDS,
7
+ UNIVERSAL_HIGH_RISK_PATHS,
8
+ buildMemoryConfig,
9
+ buildPresetMemoryConfig,
10
+ dedupeKeywordRules,
11
+ dedupePathRules,
12
+ getProjectPreset,
13
+ } from './ace-project-presets.mjs'
14
+ import {
15
+ getArgValue,
16
+ nowTimestamp,
17
+ parseCliArgs,
18
+ readTextIfExists,
19
+ writeAceBanner,
20
+ } from './ai-memory-utils.mjs'
21
+
22
+ export const PROJECT_PROFILE_PATH = '.ai/project-profile.md'
23
+ export const RECOMMENDED_CONFIG_PATH = '.ai/memory-config.recommended.json'
24
+
25
+ const MAX_SCANNED_FILES = 5000
26
+ const MAX_SCAN_DEPTH = 8
27
+ const SKIPPED_DIRS = new Set([
28
+ '.git',
29
+ '.next',
30
+ '.turbo',
31
+ 'build',
32
+ 'coverage',
33
+ 'dist',
34
+ 'node_modules',
35
+ 'out',
36
+ 'target',
37
+ 'vendor',
38
+ ])
39
+
40
+ const DETECTION_RULES = [
41
+ {
42
+ ecosystem: 'Next.js / TypeScript',
43
+ paths: [
44
+ { label: 'auth package', pattern: 'packages/auth/**', tier: 'large' },
45
+ { label: 'Next.js app router', pattern: 'app/**', tier: 'standard' },
46
+ { label: 'Next.js app router', pattern: 'apps/*/src/app/**', tier: 'standard' },
47
+ { label: 'Next.js middleware', pattern: 'middleware.ts', tier: 'large' },
48
+ { label: 'Next.js middleware', pattern: '**/middleware.ts', tier: 'large' },
49
+ ],
50
+ signals: ['next.config.ts', 'next.config.js'],
51
+ },
52
+ {
53
+ ecosystem: 'tRPC / API routers',
54
+ paths: [
55
+ { label: 'tRPC router', pattern: 'packages/api/src/routers/**', tier: 'large' },
56
+ { label: 'tRPC router', pattern: 'src/server/api/**', tier: 'large' },
57
+ { label: 'API routes', pattern: '**/api/**', tier: 'standard' },
58
+ ],
59
+ signals: ['@trpc/server'],
60
+ },
61
+ {
62
+ ecosystem: 'Drizzle / database',
63
+ paths: [
64
+ { label: 'database schema', pattern: 'packages/db/src/schema/**', tier: 'large' },
65
+ { label: 'database migration', pattern: 'packages/db/drizzle/**', tier: 'large' },
66
+ { label: 'database migration', pattern: 'packages/db/migrations/**', tier: 'large' },
67
+ { label: 'database schema', pattern: 'src/db/**', tier: 'large' },
68
+ ],
69
+ signals: ['drizzle-orm', 'drizzle-kit'],
70
+ },
71
+ {
72
+ ecosystem: 'Python / FastAPI',
73
+ paths: [
74
+ { label: 'Python security module', pattern: 'app/core/security.py', tier: 'large' },
75
+ { label: 'Python auth module', pattern: 'app/**/auth*.py', tier: 'large' },
76
+ { label: 'FastAPI routers', pattern: 'app/api/**', tier: 'standard' },
77
+ { label: 'Python database migration', pattern: 'alembic/**', tier: 'large' },
78
+ ],
79
+ signals: ['requirements.txt', 'pyproject.toml', 'fastapi'],
80
+ },
81
+ {
82
+ ecosystem: 'Go service',
83
+ paths: [
84
+ { label: 'Go auth package', pattern: 'internal/auth/**', tier: 'large' },
85
+ { label: 'Go middleware', pattern: 'internal/middleware/**', tier: 'large' },
86
+ { label: 'Go API handlers', pattern: 'internal/handlers/**', tier: 'standard' },
87
+ { label: 'Go database migration', pattern: 'migrations/**', tier: 'large' },
88
+ ],
89
+ signals: ['go.mod'],
90
+ },
91
+ {
92
+ ecosystem: '.NET service',
93
+ paths: [
94
+ { label: '.NET auth module', pattern: '**/Auth/**', tier: 'large' },
95
+ { label: '.NET middleware', pattern: '**/Middleware/**', tier: 'large' },
96
+ { label: '.NET migration', pattern: '**/Migrations/**', tier: 'large' },
97
+ ],
98
+ signals: ['.csproj', '.sln'],
99
+ },
100
+ ]
101
+
102
+ export async function onboardRepository(rootDir, options = {}) {
103
+ if (options.check) {
104
+ return checkOnboarding(rootDir)
105
+ }
106
+
107
+ const timestamp = nowTimestamp()
108
+ const profile = await profileRepository(rootDir)
109
+ const recommendedConfig = options.preset
110
+ ? buildPresetMemoryConfig(options.preset, {
111
+ generatedAt: timestamp,
112
+ source: 'ace:onboard',
113
+ status: options.apply ? 'profiled' : 'recommended',
114
+ })
115
+ : buildRecommendedMemoryConfig(profile, timestamp)
116
+
117
+ const profileContent = formatProjectProfile(profile, recommendedConfig, {
118
+ preset: options.preset,
119
+ timestamp,
120
+ })
121
+ const writtenFiles = await writeOnboardingFiles(rootDir, profileContent, recommendedConfig)
122
+
123
+ if (options.apply) {
124
+ const appliedConfig = options.preset
125
+ ? recommendedConfig
126
+ : await mergeRecommendedConfig(rootDir, recommendedConfig, timestamp)
127
+
128
+ await writeJsonFile(path.join(rootDir, '.ai', 'memory-config.json'), appliedConfig)
129
+ writtenFiles.push('.ai/memory-config.json')
130
+ }
131
+
132
+ return {
133
+ applied: Boolean(options.apply),
134
+ detectedEcosystems: profile.ecosystems,
135
+ recommendedConfig,
136
+ recommendedRuleCount:
137
+ recommendedConfig.highRiskPaths.length + recommendedConfig.highRiskKeywords.length,
138
+ status: 'ok',
139
+ writtenFiles,
140
+ }
141
+ }
142
+
143
+ export async function profileRepository(rootDir) {
144
+ const files = await scanRepoFiles(rootDir)
145
+ const fileSet = new Set(files)
146
+ const packageMetadata = await readPackageMetadata(rootDir)
147
+ const contentSignals = await readContentSignals(rootDir, files)
148
+ const signals = new Set([
149
+ ...files.map((file) => path.basename(file)),
150
+ ...packageMetadata.packageSignals,
151
+ ...contentSignals,
152
+ ])
153
+ const ecosystems = []
154
+ const recommendedPaths = [...UNIVERSAL_HIGH_RISK_PATHS]
155
+ const recommendedKeywords = [...UNIVERSAL_HIGH_RISK_KEYWORDS]
156
+
157
+ for (const rule of DETECTION_RULES) {
158
+ if (!rule.signals.some((signal) => signalMatches(signal, signals))) {
159
+ continue
160
+ }
161
+
162
+ ecosystems.push(rule.ecosystem)
163
+
164
+ for (const pathRule of rule.paths) {
165
+ if (files.some((file) => pathRuleMatchesFile(pathRule.pattern, file, fileSet))) {
166
+ recommendedPaths.push(pathRule)
167
+ }
168
+ }
169
+ }
170
+
171
+ return {
172
+ ecosystems: ecosystems.length > 0 ? ecosystems : ['Generic repository'],
173
+ filesScanned: files.length,
174
+ packageManager: detectPackageManager(fileSet),
175
+ recommendedKeywords: dedupeKeywordRules(recommendedKeywords),
176
+ recommendedPaths: dedupePathRules(recommendedPaths),
177
+ signals: [...signals].sort(),
178
+ }
179
+ }
180
+
181
+ export function buildRecommendedMemoryConfig(profile, timestamp = nowTimestamp()) {
182
+ return buildMemoryConfig({
183
+ highRiskKeywords: profile.recommendedKeywords,
184
+ highRiskPaths: profile.recommendedPaths,
185
+ profile: {
186
+ detectedEcosystems: profile.ecosystems,
187
+ generatedAt: timestamp,
188
+ source: 'ace:onboard',
189
+ status: 'recommended',
190
+ },
191
+ })
192
+ }
193
+
194
+ async function checkOnboarding(rootDir) {
195
+ const configContent = await readTextIfExists(path.join(rootDir, '.ai', 'memory-config.json'))
196
+ const profileContent = await readTextIfExists(path.join(rootDir, PROJECT_PROFILE_PATH))
197
+ const issues = []
198
+
199
+ if (configContent === null) {
200
+ issues.push('Missing .ai/memory-config.json.')
201
+ } else {
202
+ const config = JSON.parse(configContent)
203
+
204
+ if (config?._profile?.status !== 'profiled') {
205
+ issues.push('ACE project profile is not applied. Run pnpm ace:onboard -- --apply.')
206
+ }
207
+ }
208
+
209
+ if (profileContent === null) {
210
+ issues.push(`Missing ${PROJECT_PROFILE_PATH}. Run pnpm ace:onboard.`)
211
+ }
212
+
213
+ if (issues.length > 0) {
214
+ return { issues, status: 'failed' }
215
+ }
216
+
217
+ return { issues: [], status: 'ok' }
218
+ }
219
+
220
+ async function mergeRecommendedConfig(rootDir, recommendedConfig, timestamp) {
221
+ const existingContent = await readTextIfExists(path.join(rootDir, '.ai', 'memory-config.json'))
222
+ const existingConfig = existingContent ? JSON.parse(existingContent) : {}
223
+
224
+ return {
225
+ ...existingConfig,
226
+ _name: 'ACE (Agentic Context Engine) Configuration',
227
+ _profile: {
228
+ detectedEcosystems: recommendedConfig._profile.detectedEcosystems,
229
+ profiledAt: timestamp,
230
+ source: 'ace:onboard',
231
+ status: 'profiled',
232
+ },
233
+ highRiskKeywords: dedupeKeywordRules([
234
+ ...(existingConfig.highRiskKeywords ?? []),
235
+ ...recommendedConfig.highRiskKeywords,
236
+ ]),
237
+ highRiskPaths: dedupePathRules([
238
+ ...(existingConfig.highRiskPaths ?? []),
239
+ ...recommendedConfig.highRiskPaths,
240
+ ]),
241
+ thresholds: existingConfig.thresholds ?? recommendedConfig.thresholds,
242
+ version: existingConfig.version ?? recommendedConfig.version,
243
+ }
244
+ }
245
+
246
+ async function writeOnboardingFiles(rootDir, profileContent, recommendedConfig) {
247
+ const writtenFiles = []
248
+ const profilePath = path.join(rootDir, PROJECT_PROFILE_PATH)
249
+ const recommendedPath = path.join(rootDir, RECOMMENDED_CONFIG_PATH)
250
+
251
+ await writeTextFile(profilePath, profileContent)
252
+ writtenFiles.push(PROJECT_PROFILE_PATH)
253
+ await writeJsonFile(recommendedPath, recommendedConfig)
254
+ writtenFiles.push(RECOMMENDED_CONFIG_PATH)
255
+
256
+ return writtenFiles
257
+ }
258
+
259
+ function formatProjectProfile(profile, recommendedConfig, { preset, timestamp }) {
260
+ const ecosystemLines = profile.ecosystems.map((ecosystem) => `- ${ecosystem}`).join('\n')
261
+ const pathLines = recommendedConfig.highRiskPaths
262
+ .slice(0, 20)
263
+ .map((rule) => `- \`${rule.pattern}\` - ${rule.label} (${rule.tier})`)
264
+ .join('\n')
265
+ const keywordLines = recommendedConfig.highRiskKeywords
266
+ .slice(0, 20)
267
+ .map((rule) => `- \`${rule.keyword}\` - ${rule.label} (${rule.tier})`)
268
+ .join('\n')
269
+ const modeLine = preset
270
+ ? `Applied recommendation source: preset \`${preset}\`.`
271
+ : 'Applied recommendation source: repository scan.'
272
+
273
+ return `# ACE Project Profile
274
+
275
+ Generated: ${timestamp}
276
+
277
+ ${modeLine}
278
+
279
+ ## Detected Ecosystems
280
+ ${ecosystemLines}
281
+
282
+ ## Repository Shape
283
+ - Files scanned: ${profile.filesScanned}
284
+ - Package manager: ${profile.packageManager}
285
+
286
+ ## Recommended High-Risk Paths
287
+ ${pathLines}
288
+
289
+ ## Recommended High-Risk Keywords
290
+ ${keywordLines}
291
+
292
+ ## Next Step
293
+ - Review \`.ai/memory-config.recommended.json\`.
294
+ - Run \`pnpm ace:onboard -- --apply\` to apply the recommended profile.
295
+ - For known Next/tRPC/Drizzle SaaS repos, run \`pnpm ace:onboard -- --preset next-trpc-drizzle-saas --apply\`.
296
+ `
297
+ }
298
+
299
+ async function scanRepoFiles(rootDir) {
300
+ const files = []
301
+
302
+ async function visit(directory, depth) {
303
+ if (files.length >= MAX_SCANNED_FILES || depth > MAX_SCAN_DEPTH) {
304
+ return
305
+ }
306
+
307
+ let entries
308
+
309
+ try {
310
+ entries = await readdir(directory, { withFileTypes: true })
311
+ } catch {
312
+ return
313
+ }
314
+
315
+ for (const entry of entries) {
316
+ if (files.length >= MAX_SCANNED_FILES) {
317
+ return
318
+ }
319
+
320
+ const absolutePath = path.join(directory, entry.name)
321
+ const relativePath = normalizeRepoPath(path.relative(rootDir, absolutePath))
322
+
323
+ if (entry.isDirectory()) {
324
+ if (!SKIPPED_DIRS.has(entry.name)) {
325
+ await visit(absolutePath, depth + 1)
326
+ }
327
+
328
+ continue
329
+ }
330
+
331
+ if (entry.isFile()) {
332
+ files.push(relativePath)
333
+ }
334
+ }
335
+ }
336
+
337
+ await visit(rootDir, 0)
338
+
339
+ return files.sort()
340
+ }
341
+
342
+ async function readPackageMetadata(rootDir) {
343
+ const packageSignals = []
344
+ const packageContent = await readTextIfExists(path.join(rootDir, 'package.json'))
345
+
346
+ if (!packageContent) {
347
+ return { packageSignals }
348
+ }
349
+
350
+ try {
351
+ const packageJson = JSON.parse(stripByteOrderMark(packageContent))
352
+ const dependencies = {
353
+ ...(packageJson.dependencies ?? {}),
354
+ ...(packageJson.devDependencies ?? {}),
355
+ }
356
+
357
+ packageSignals.push(...Object.keys(dependencies))
358
+ } catch {
359
+ return { packageSignals }
360
+ }
361
+
362
+ return { packageSignals }
363
+ }
364
+
365
+ async function readContentSignals(rootDir, files) {
366
+ const signals = []
367
+
368
+ for (const file of ['requirements.txt', 'pyproject.toml', 'go.mod']) {
369
+ if (!files.includes(file)) {
370
+ continue
371
+ }
372
+
373
+ const content = await readTextIfExists(path.join(rootDir, file))
374
+
375
+ if (content?.toLowerCase().includes('fastapi')) {
376
+ signals.push('fastapi')
377
+ }
378
+ }
379
+
380
+ return signals
381
+ }
382
+
383
+ function detectPackageManager(fileSet) {
384
+ if (fileSet.has('pnpm-lock.yaml')) {
385
+ return 'pnpm'
386
+ }
387
+
388
+ if (fileSet.has('yarn.lock')) {
389
+ return 'yarn'
390
+ }
391
+
392
+ if (fileSet.has('package-lock.json')) {
393
+ return 'npm'
394
+ }
395
+
396
+ if (fileSet.has('uv.lock')) {
397
+ return 'uv'
398
+ }
399
+
400
+ if (fileSet.has('poetry.lock')) {
401
+ return 'poetry'
402
+ }
403
+
404
+ if (fileSet.has('go.mod')) {
405
+ return 'go'
406
+ }
407
+
408
+ return 'not detected'
409
+ }
410
+
411
+ function signalMatches(signal, signals) {
412
+ if (signal.startsWith('.')) {
413
+ return [...signals].some((value) => value.endsWith(signal))
414
+ }
415
+
416
+ return signals.has(signal)
417
+ }
418
+
419
+ function pathRuleMatchesFile(pattern, file, fileSet) {
420
+ if (pattern.includes('*')) {
421
+ return globToRegExp(pattern).test(file)
422
+ }
423
+
424
+ return file === pattern || fileSet.has(pattern)
425
+ }
426
+
427
+ function globToRegExp(pattern) {
428
+ const escapedPattern = normalizeRepoPath(pattern)
429
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
430
+ .replace(/\*\*/g, '__DOUBLE_STAR__')
431
+ .replace(/\*/g, '[^/]*')
432
+ .replace(/__DOUBLE_STAR__/g, '.*')
433
+
434
+ return new RegExp(`^${escapedPattern}$`, 'u')
435
+ }
436
+
437
+ function normalizeRepoPath(filePath) {
438
+ return filePath.replace(/\\/g, '/').replace(/^\.\//, '')
439
+ }
440
+
441
+ function stripByteOrderMark(content) {
442
+ return content.replace(/^\uFEFF/u, '')
443
+ }
444
+
445
+ async function writeTextFile(filePath, content) {
446
+ await mkdir(path.dirname(filePath), { recursive: true })
447
+ await writeFile(filePath, normalizeTrailingNewline(content), 'utf8')
448
+ }
449
+
450
+ async function writeJsonFile(filePath, value) {
451
+ await writeTextFile(filePath, JSON.stringify(value, null, 2))
452
+ }
453
+
454
+ function normalizeTrailingNewline(content) {
455
+ return content.endsWith('\n') ? content : `${content}\n`
456
+ }
457
+
458
+ async function main() {
459
+ writeAceBanner()
460
+
461
+ const args = parseCliArgs(process.argv.slice(2))
462
+ const rootDir = path.resolve(process.cwd(), getArgValue(args, 'root') ?? '.')
463
+ const preset = getArgValue(args, 'preset')
464
+
465
+ if (preset && !getProjectPreset(preset)) {
466
+ throw new Error(`Unknown ACE project preset: ${preset}`)
467
+ }
468
+
469
+ const result = await onboardRepository(rootDir, {
470
+ apply: getArgValue(args, 'apply') === 'true',
471
+ check: getArgValue(args, 'check') === 'true',
472
+ preset,
473
+ })
474
+
475
+ if (getArgValue(args, 'json') === 'true') {
476
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
477
+ return
478
+ }
479
+
480
+ if (result.status === 'failed') {
481
+ process.stderr.write('ACE onboarding check failed:\n')
482
+
483
+ for (const issue of result.issues) {
484
+ process.stderr.write(`- ${issue}\n`)
485
+ }
486
+
487
+ process.exit(1)
488
+ }
489
+
490
+ process.stdout.write(
491
+ result.applied
492
+ ? `ACE project profile applied. Updated: ${result.writtenFiles.join(', ')}\n`
493
+ : `ACE project profile generated. Review ${RECOMMENDED_CONFIG_PATH}, then run pnpm ace:onboard -- --apply.\n`,
494
+ )
495
+ }
496
+
497
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
498
+ await main().catch((error) => {
499
+ const message = error instanceof Error ? error.message : String(error)
500
+ process.stderr.write(`${message}\n`)
501
+ process.exit(1)
502
+ })
503
+ }