Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 0 additions & 11 deletions packages/cli/src/commands/bundled-version.ts

This file was deleted.

18 changes: 11 additions & 7 deletions packages/cli/src/commands/version.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
/**
* Version command - displays version info
*
* For compiled binaries, version and git commit are embedded via bundled-version.ts
* For development (Bun), reads from package.json and retrieves git commit at runtime
* For compiled binaries, version and git commit are embedded via `@archon/paths`
* build-time constants (rewritten by `scripts/build-binaries.sh`).
* For development (Bun), reads from package.json and retrieves git commit at runtime.
*/
import { readFile } from 'fs/promises';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { execFileAsync } from '@archon/git';
import { createLogger } from '@archon/paths';
import {
BUNDLED_GIT_COMMIT,
BUNDLED_IS_BINARY,
BUNDLED_VERSION,
createLogger,
} from '@archon/paths';
import { getDatabaseType } from '@archon/core';
import { isBinaryBuild } from '@archon/workflows/defaults';
import { BUNDLED_VERSION, BUNDLED_GIT_COMMIT } from './bundled-version';

const log = createLogger('cli:version');

Expand Down Expand Up @@ -72,7 +76,7 @@ export async function versionCommand(): Promise<void> {
let version: string;
let gitCommit: string;

if (isBinaryBuild()) {
if (BUNDLED_IS_BINARY) {
// Compiled binary: use embedded version and commit
version = BUNDLED_VERSION;
gitCommit = BUNDLED_GIT_COMMIT;
Expand All @@ -86,7 +90,7 @@ export async function versionCommand(): Promise<void> {
const platform = process.platform;
const arch = process.arch;
const dbType = getDatabaseType();
const buildType = isBinaryBuild() ? 'binary' : 'source (bun)';
const buildType = BUNDLED_IS_BINARY ? 'binary' : 'source (bun)';

console.log(`Archon CLI v${version}`);
console.log(` Platform: ${platform}-${arch}`);
Expand Down
4 changes: 1 addition & 3 deletions packages/paths/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@
"type-check": "bun x tsc --noEmit"
},
"dependencies": {
"pino": "^9"
},
"optionalDependencies": {
"pino": "^9",
"pino-pretty": "^13"
},
"peerDependencies": {
Expand Down
19 changes: 19 additions & 0 deletions packages/paths/src/bundled-build.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, expect, it } from 'bun:test';
import { BUNDLED_GIT_COMMIT, BUNDLED_IS_BINARY, BUNDLED_VERSION } from './bundled-build';

describe('bundled-build', () => {
// In dev/test mode the placeholders must be the dev defaults.
// `scripts/build-binaries.sh` rewrites this file only during binary
// compilation and restores it afterwards via an EXIT trap.
it('BUNDLED_IS_BINARY is false in dev mode', () => {
expect(BUNDLED_IS_BINARY).toBe(false);
});

it('BUNDLED_VERSION is the dev placeholder', () => {
expect(BUNDLED_VERSION).toBe('dev');
});

it('BUNDLED_GIT_COMMIT is the dev placeholder', () => {
expect(BUNDLED_GIT_COMMIT).toBe('unknown');
});
});
18 changes: 18 additions & 0 deletions packages/paths/src/bundled-build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Build-time constants embedded into compiled binaries.
*
* In dev/test mode, the placeholders below are used and BUNDLED_IS_BINARY
* is `false`. Compiled binaries get this file overwritten by
* `scripts/build-binaries.sh` before `bun build --compile` is invoked,
* and restored afterwards via an EXIT trap.
*
* Lives in `@archon/paths` (the bottom of the dep graph) so any package
* can import these constants without creating dependency cycles.
*
* See GitHub issue #979 for the rationale (replaces runtime detection
* heuristics that were brittle across Bun's ESM/CJS compile modes).
*/

export const BUNDLED_IS_BINARY = false;
export const BUNDLED_VERSION = 'dev';
export const BUNDLED_GIT_COMMIT = 'unknown';
3 changes: 3 additions & 0 deletions packages/paths/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ export {
// Logger
export { createLogger, setLogLevel, getLogLevel, rootLogger } from './logger';
export type { Logger } from './logger';

// Build-time constants (rewritten by scripts/build-binaries.sh)
export { BUNDLED_IS_BINARY, BUNDLED_VERSION, BUNDLED_GIT_COMMIT } from './bundled-build';
47 changes: 30 additions & 17 deletions packages/paths/src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import pino from 'pino';
import type { Logger } from 'pino';
import pretty from 'pino-pretty';

export type { Logger } from 'pino';

Expand All @@ -44,36 +45,48 @@ function getInitialLevel(): string {
}

/**
* Uses pino-pretty when stdout is a TTY and NODE_ENV !== 'production';
* outputs newline-delimited JSON otherwise.
* Build the root Pino logger.
*
* Uses `pino-pretty` as a **destination stream** (not a worker-thread transport)
* when stdout is a TTY and NODE_ENV !== 'production'. Running pino-pretty as a
* destination stream keeps the formatter on the main thread, which avoids the
* `require.resolve('pino-pretty')` lookup that crashes inside Bun's `/$bunfs/`
* virtual filesystem in compiled binaries (see GitHub issue #960 / #979).
*
* The same code path runs in dev and compiled binaries — no environment
* detection required.
*/
function buildLoggerOptions(): pino.LoggerOptions {
function buildLogger(): Logger {
const level = getInitialLevel();
const usePretty = process.stdout.isTTY && process.env.NODE_ENV !== 'production';

if (usePretty) {
return {
level,
transport: {
target: 'pino-pretty',
options: {
colorize: true,
levelFirst: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname',
},
},
};
try {
const stream = pretty({
colorize: true,
levelFirst: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname',
});
return pino({ level }, stream);
} catch (err) {
// pino-pretty failed to initialize (missing peer, broken TTY descriptor,
// or incompatible runtime). Fall back to plain JSON so logging keeps
// working instead of crashing the entire process at module import time.
console.warn(
`[logger] pino-pretty failed to initialize, falling back to JSON output: ${(err as Error).message}`
);
}
}

return { level };
return pino({ level });
}

/**
* Root Pino logger instance.
* Children inherit the root's level at creation time (not dynamically updated).
*/
export const rootLogger: Logger = pino(buildLoggerOptions());
export const rootLogger: Logger = buildLogger();

/**
* Create a child logger with a module binding.
Expand Down
39 changes: 6 additions & 33 deletions packages/workflows/src/defaults/bundled-defaults.test.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,13 @@
import { describe, it, expect } from 'bun:test';
import {
isBinaryBuild,
isBunVirtualFs,
BUNDLED_COMMANDS,
BUNDLED_WORKFLOWS,
} from './bundled-defaults';
import { isBinaryBuild, BUNDLED_COMMANDS, BUNDLED_WORKFLOWS } from './bundled-defaults';

describe('bundled-defaults', () => {
describe('isBunVirtualFs', () => {
it('should detect Linux/macOS virtual filesystem paths', () => {
expect(isBunVirtualFs('/$bunfs/root/bundled-defaults')).toBe(true);
expect(isBunVirtualFs('/$bunfs/root/')).toBe(true);
});

it('should detect Windows virtual filesystem paths (backslash)', () => {
expect(isBunVirtualFs('B:\\~BUN\\root\\bundled-defaults')).toBe(true);
expect(isBunVirtualFs('B:\\~BUN\\root')).toBe(true);
});

it('should detect Windows virtual filesystem paths (forward slash)', () => {
expect(isBunVirtualFs('B:/~BUN/root/bundled-defaults')).toBe(true);
expect(isBunVirtualFs('B:/~BUN/root')).toBe(true);
});

it('should return false for real filesystem paths', () => {
expect(isBunVirtualFs('/home/user/project/src')).toBe(false);
expect(isBunVirtualFs('C:\\Users\\user\\project\\src')).toBe(false);
expect(isBunVirtualFs('/tmp/test')).toBe(false);
});
});

describe('isBinaryBuild', () => {
it('should return false when running in test environment (not compiled)', () => {
// The true path requires an actual compiled binary (import.meta.dir points to
// Bun's virtual FS only inside compiled binaries). Coverage of the true branch
// relies on isBunVirtualFs tests above + manual binary smoke testing in CI.
it('should return false in dev/test mode', () => {
// `isBinaryBuild()` reads the build-time constant `BUNDLED_IS_BINARY` from
// `@archon/paths`. In dev/test mode it is `false`. It is only rewritten to
// `true` by `scripts/build-binaries.sh` before `bun build --compile`.
// Coverage of the `true` branch is via local binary smoke testing (see #979).
expect(isBinaryBuild()).toBe(false);
});
});
Expand Down
26 changes: 12 additions & 14 deletions packages/workflows/src/defaults/bundled-defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
* Import syntax uses `with { type: 'text' }` to import file contents as strings.
*/

import { BUNDLED_IS_BINARY } from '@archon/paths';

// =============================================================================
// Default Commands (21 total)
// =============================================================================
Expand Down Expand Up @@ -102,23 +104,19 @@ export const BUNDLED_WORKFLOWS: Record<string, string> = {
'archon-workflow-builder': archonWorkflowBuilderWf,
};

/**
* Check if a given module directory path belongs to a compiled Bun binary.
*
* Compiled Bun binaries use a virtual filesystem for bundled modules:
* - Linux/macOS: `/$bunfs/root/`
* - Windows: `B:\~BUN\root\` or `B:/~BUN/root/`
*/
export function isBunVirtualFs(dir: string): boolean {
return dir.startsWith('/$bunfs/') || dir.startsWith('B:\\~BUN\\') || dir.startsWith('B:/~BUN/');
}

/**
* Check if the current process is running as a compiled binary (not via Bun CLI).
*
* Note: `process.versions.bun` is still set in compiled binaries as of Bun 1.3.5,
* so we use the virtual filesystem path prefix for detection instead.
* Reads the build-time constant `BUNDLED_IS_BINARY` from `@archon/paths`.
* `scripts/build-binaries.sh` rewrites that file to set it to `true` before
* `bun build --compile` and restores it afterwards. See GitHub issue #979.
*
* Kept as a function (rather than a direct re-export of `BUNDLED_IS_BINARY`)
* so tests can use `spyOn(bundledDefaults, 'isBinaryBuild').mockReturnValue(...)`
* without resorting to `mock.module('@archon/paths', ...)` — which is
* process-global and irreversible in Bun and would pollute other test files.
* See `.claude/rules/dx-quirks.md` and `loader.test.ts` for context.
*/
export function isBinaryBuild(): boolean {
return isBunVirtualFs(import.meta.dir);
return BUNDLED_IS_BINARY;
}
22 changes: 13 additions & 9 deletions scripts/build-binaries.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,23 @@ VERSION="${VERSION:-$(grep '"version"' package.json | head -1 | cut -d'"' -f4)}"
GIT_COMMIT="${GIT_COMMIT:-$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')}"
echo "Building Archon CLI v${VERSION} (commit: ${GIT_COMMIT})"

# Update bundled version in source before compiling
BUNDLED_VERSION_FILE="packages/cli/src/commands/bundled-version.ts"
echo "Updating bundled version to ${VERSION}..."
cat > "$BUNDLED_VERSION_FILE" << EOF
# Update build-time constants in source before compiling.
# The file is restored via an EXIT trap so the dev tree is never left dirty,
# even if `bun build --compile` fails mid-way. See GitHub issue #979.
BUNDLED_BUILD_FILE="packages/paths/src/bundled-build.ts"
trap 'echo "Restoring ${BUNDLED_BUILD_FILE}..."; git checkout -- "${BUNDLED_BUILD_FILE}"' EXIT

echo "Updating build-time constants (version=${VERSION}, is_binary=true)..."
cat > "$BUNDLED_BUILD_FILE" << EOF
/**
* Bundled version for compiled binaries
*
* This file is updated by scripts/build-binaries.sh before compilation.
* The version is read from package.json at build time and embedded here.
* Build-time constants embedded into compiled binaries.
*
* For development, the version command reads directly from package.json instead.
* This file is rewritten by scripts/build-binaries.sh before \`bun build --compile\`
* and restored afterwards via an EXIT trap. Do not edit these values by hand
* outside the build script — the dev defaults live in the committed copy.
*/

export const BUNDLED_IS_BINARY = true;
export const BUNDLED_VERSION = '${VERSION}';
export const BUNDLED_GIT_COMMIT = '${GIT_COMMIT}';
EOF
Expand Down
Loading