import chalk from 'chalk'; import { Command } from 'commander'; import { existsSync, readFileSync, statSync } from 'node:fs'; import { join } from 'node:path'; // The facade — see the note in `explainCommand.ts`. This is Story 6.2's second consumer. import { assets } from '@beehexa/hexasync-template-engine'; /** * `hexasync env` — does the tooling agree with its own knowledge? (Story 6.8) * * This file OBSERVES and prints; `assets.checkEnvironment` decides what the observation means. So the CLI, the extension and an * agent cannot reach different conclusions from the same disk — the same reason the engine facade exists. * * ⚠️ Every read is guarded and every failure becomes an ABSENT observation rather than a crash. A check that dies while * checking has answered nothing, and "I could not look" is a result this report knows how to say. */ /** * `undefined` = not there. `null` = there but unreadable. * * ⛔ Both collapsed to `undefined`, so a present-but-unreadable index reported *"Not installed. Run Install * IntelliSense"* — the wrong diagnosis and a remedy that would not help. This repo's own rule says an error is not an * empty result, and `env.ts`'s doctrine is that "could not look" is `n/a`. * * ⚠️ `statSync().isFile()` first, because `readFileSync` on a FIFO blocks forever — a hang is worse than a crash for an * agent, which has no way to tell it apart from slow work. */ const read = (path: string): string | undefined | null => { try { if (!existsSync(path)) return undefined; if (!statSync(path).isFile()) return null; return readFileSync(path, 'utf8'); } catch { return null; } }; function observe(cwd: string): assets.EnvObservation { const installed = join(cwd, assets.CANONICAL_SUBPATH); const index = read(join(installed, 'docs', 'AI-INDEX.md')); const manifest = read(join(installed, assets.MANIFEST_FILENAME)); /** * The bundle the INSTALL recorded, which is the only "tooling" number this command can honestly see. * * ⛔ A first version read the CLI's own package version and compared it against the surface's `Asset bundle NN`. Those * are different numbering systems, so the comparison could never pass — it reported `n/a` with *"could not determine * the version of the installed tooling"* on a perfectly healthy install, which is a check that always abstains. * * What the CLI can compare is the surface's own stamp against the manifest the installer wrote beside it: that catches * a **partially updated install**, where the schemas were refreshed and the knowledge was not. Whether the EDITOR has * since moved on is a question only the extension can answer, because only it knows its own `BUNDLE_VERSION` — so this * command says so rather than pretending. */ const manifestBundle = (() => { if (typeof manifest !== 'string') return undefined; try { const recorded = JSON.parse(manifest).assetBundleVersion; return recorded === undefined || recorded === null ? undefined : String(recorded); } catch { return undefined; // A manifest we cannot parse is one we did not read. } })(); const surfaceBundle = typeof index === 'string' ? /Asset bundle (\S+)/.exec(index)?.[1] : undefined; return { projectFound: existsSync(join(cwd, 'partials', 'main.yaml')) || existsSync(join(cwd, 'main.yaml')), // `null` — present but unreadable — is neither installed nor absent, so it is left undecided rather than reported // as "not installed". ...(index === null ? {} : { surfaceInstalled: index !== undefined }), // The stamp the surface itself carries — `Asset bundle NN`, written by the generator. ...(surfaceBundle ? { surfaceBundle } : {}), // Compared against the install's own record, for the reason above. ...(manifestBundle ? { toolingBundle: manifestBundle, manifestBundle } : {}), }; } export function EnvCommand(): Command { return new Command('env') .description('Report whether the tooling and its installed knowledge agree') .option( '--json', 'Machine-readable output, for an agent gating on one item', ) .action((options: { json?: boolean }) => { const report = assets.checkEnvironment(observe(process.cwd())); if (options.json === true) { console.log(JSON.stringify(report, null, 2)); } else { console.log(''); for (const line of assets.formatEnvReport(report)) { const colour = line.startsWith('✗') ? chalk.red : line.startsWith('✓') ? chalk.green : chalk.gray; console.log( line.startsWith(' ') ? chalk.gray(line) : colour(line), ); } console.log(''); } /** * AC-3 — a failing item is a non-zero exit, so an agent can gate on it. * * ⚠️ `n/a` does NOT set it. "I could not evaluate this" is not a failure of the thing being evaluated, and making * it one would train every caller to ignore the exit code — which is the same as not having one. */ if (report.failed) process.exitCode = 1; }); }