diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c04f0e0..553dee0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -46,5 +46,8 @@ jobs:
- name: Check package contents
run: pnpm pack:codex-claw
+ - name: Smoke packed CLI
+ run: pnpm smoke:codex-claw:pack
+
- name: Audit dependencies
run: pnpm audit --audit-level low
diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml
index d9d4cc2..7f73d83 100644
--- a/.github/workflows/release-package.yml
+++ b/.github/workflows/release-package.yml
@@ -44,6 +44,9 @@ jobs:
- name: Build package archive
run: pnpm package:codex-claw
+ - name: Smoke packed CLI
+ run: pnpm smoke:codex-claw:pack
+
- name: Write SHA256 checksums
shell: bash
run: |
diff --git a/README.md b/README.md
index 151f92c..5e0bac0 100644
--- a/README.md
+++ b/README.md
@@ -26,6 +26,7 @@ CodexClaw turns your installed codex command into a local web clien
- [Terminal Demo](#terminal-demo)
- [Configuration](#configuration)
- [Common Commands](#common-commands)
+- [Troubleshooting](#troubleshooting)
- [How It Works](#how-it-works)
- [Beta Track](#beta-track)
- [Contributing](#contributing)
@@ -75,13 +76,35 @@ codex exec "Reply with: ready"
The public npm package target is codex-claw@0.1.0-alpha.0.
-After the first npm alpha publish, the install path will be:
+After the first npm alpha publish, start without a global install:
+
+~~~powershell
+# Windows PowerShell
+npx codex-claw@alpha
+npm exec codex-claw@alpha -- doctor
+~~~
~~~bash
+# macOS and Linux
npx codex-claw@alpha
+npm exec codex-claw@alpha -- doctor
+~~~
+
+For a pinned global CLI during alpha:
+
+~~~bash
+npm install -g codex-claw@alpha
+codex-claw doctor
+codex-claw --help
+~~~
+
+Update by re-running npx codex-claw@alpha or reinstalling the alpha tag:
+
+~~~bash
+npm install -g codex-claw@alpha
~~~
-Until then, use the source checkout above. The package is intentionally tagged as alpha so early releases do not claim a stable latest workflow.
+Until the first alpha package exists on npm, use the source checkout above. The package is intentionally tagged as alpha so early releases do not claim a stable latest workflow.
## Terminal Demo
@@ -102,7 +125,16 @@ Local source readiness check:
~~~console
$ node packages/codex-claw/bin/codex-claw.js doctor
-Environment looks good.
+[ok] Node.js: Node.js 20.19.5
+[ok] npm: 10.8.2
+[warn] npm auth: npm auth unavailable. Run `npm login` before publishing codex-claw@alpha.
+[ok] pnpm: 9.15.4
+[ok] git: git version 2.51.0.windows.1
+[ok] git worktree: Current directory is a git worktree.
+[ok] Codex CLI: codex-cli 0.61.0
+[ok] state directory: .codex-claw can be created on first run.
+[ok] port: Port 3000 is available.
+Environment is usable with 1 warning(s).
~~~
Manual source workflow:
@@ -141,9 +173,27 @@ pnpm lint # run ESLint
pnpm landing:dev # start the landing page
pnpm landing:build # build the landing page
pnpm pack:codex-claw # inspect npm package contents
+pnpm smoke:codex-claw # run npx against the packed local tarball
+pnpm smoke:codex-claw:npm # run npx against codex-claw@alpha once published
pnpm release:codex-claw # publish alpha package with the alpha dist-tag
~~~
+## Troubleshooting
+
+| Symptom | What to run |
+| --- | --- |
+| npm auth unavailable | Run npm login, then npm whoami before publishing |
+| codex-claw@alpha was not found on npm | The alpha package has not been published yet; use the source checkout or publish with pnpm release:codex-claw |
+| Port 3000 is already in use | Stop the process using the port or run codex-claw doctor --port 3001 |
+| Codex CLI was not found | Install Codex CLI, run codex login, or pass --codex-command <cmd> |
+
+Package readiness checks:
+
+~~~bash
+pnpm smoke:codex-claw:pack
+pnpm smoke:codex-claw:npm
+~~~
+
## How It Works
~~~text
diff --git a/package.json b/package.json
index 81a1b64..a7018d6 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,9 @@
"format": "pnpm -C apps/codex-claw format",
"check": "pnpm -C apps/codex-claw check",
"pack:codex-claw": "pnpm -C packages/codex-claw exec npm pack --dry-run",
+ "smoke:codex-claw": "node packages/codex-claw/scripts/install-smoke.mjs --source pack",
+ "smoke:codex-claw:pack": "node packages/codex-claw/scripts/install-smoke.mjs --source pack",
+ "smoke:codex-claw:npm": "node packages/codex-claw/scripts/install-smoke.mjs --source npm",
"package:codex-claw": "node -e \"require('node:fs').mkdirSync('dist/release',{recursive:true})\" && pnpm -C packages/codex-claw exec npm pack --pack-destination ../../dist/release",
"release:codex-claw": "pnpm -C packages/codex-claw exec npm publish --tag alpha --access public",
"bump:codex-claw:patch": "pnpm -C packages/codex-claw version patch --no-git-tag-version",
diff --git a/packages/codex-claw/README.md b/packages/codex-claw/README.md
index c84d2c1..543dbd4 100644
--- a/packages/codex-claw/README.md
+++ b/packages/codex-claw/README.md
@@ -20,10 +20,25 @@ The package is designed for an npx codex-claw@alpha first-run workf
## Alpha Install
-After the first public npm publish:
+After the first public npm publish, use npx for the cleanest alpha path:
+
+~~~powershell
+# Windows PowerShell
+npx codex-claw@alpha
+npm exec codex-claw@alpha -- doctor
+~~~
~~~bash
+# macOS and Linux
npx codex-claw@alpha
+npm exec codex-claw@alpha -- doctor
+~~~
+
+Global alpha install:
+
+~~~bash
+npm install -g codex-claw@alpha
+codex-claw doctor
~~~
Useful non-interactive bootstrap:
@@ -35,6 +50,12 @@ pnpm install
pnpm dev
~~~
+Update by re-running npx codex-claw@alpha or reinstalling the alpha tag:
+
+~~~bash
+npm install -g codex-claw@alpha
+~~~
+
## Local Development Usage
From this repository:
@@ -55,7 +76,7 @@ node packages/codex-claw/bin/codex-claw.js doctor
| codex-claw preview | Preview the production build |
| codex-claw test | Run tests |
| codex-claw lint | Run lint |
-| codex-claw doctor | Validate Node.js, pnpm, and Codex CLI |
+| codex-claw doctor | Validate Node.js, npm auth, pnpm, Git, Codex CLI, state directory, and dev port availability |
## Prompts
@@ -84,7 +105,20 @@ Then it creates the project folder, installs dependencies, and starts CodexClaw
npm whoami
npm view codex-claw version dist-tags --json
pnpm pack:codex-claw
+pnpm smoke:codex-claw:pack
+pnpm smoke:codex-claw:npm
pnpm release:codex-claw
~~~
The release script publishes with the alpha dist-tag so early builds stay clearly separated from a future stable channel.
+
+## Troubleshooting
+
+| Symptom | Action |
+| --- | --- |
+| npm auth unavailable | Run npm login, then npm whoami before publishing |
+| codex-claw@alpha was not found on npm | The alpha package is not published yet; use the source checkout or publish with pnpm release:codex-claw |
+| Port 3000 is already in use | Stop the existing process or run codex-claw doctor --port 3001 |
+| Codex CLI was not found | Install Codex CLI, run codex login, or pass --codex-command <cmd> |
+
+The npm smoke test intentionally fails with a package-not-found message until the alpha package is available on npm.
diff --git a/packages/codex-claw/bin/codex-claw.js b/packages/codex-claw/bin/codex-claw.js
index 1a73148..e579cad 100755
--- a/packages/codex-claw/bin/codex-claw.js
+++ b/packages/codex-claw/bin/codex-claw.js
@@ -1,6 +1,7 @@
#!/usr/bin/env node
import fs from 'node:fs'
+import net from 'node:net'
import os from 'node:os'
import path from 'node:path'
import { spawnSync } from 'node:child_process'
@@ -39,8 +40,10 @@ function printHelp() {
process.stdout.write(` --codex-sandbox CODEX_CLI_SANDBOX value\n`)
process.stdout.write(` --codex-workdir CODEX_CLI_WORKDIR value\n`)
process.stdout.write(` --port Dev server port\n`)
+ process.stdout.write(` --state-dir CODEX_CLAW_STATE_DIR value for doctor\n`)
process.stdout.write(` --yes Accept defaults (non-interactive)\n`)
process.stdout.write(` --no-start Do not auto-run install + dev\n`)
+ process.stdout.write(` --no-port-check Skip doctor port availability check\n`)
process.stdout.write(` --force Allow init in non-empty directory\n`)
process.stdout.write(` --skip-env Skip .env.local setup prompts\n`)
process.stdout.write(` -h, --help Show help\n`)
@@ -56,6 +59,7 @@ function parseCliArgs(args) {
'--codex-sandbox',
'--codex-workdir',
'--port',
+ '--state-dir',
])
for (let index = 0; index < args.length; index += 1) {
@@ -377,20 +381,6 @@ function startProject(targetDir) {
runCommand(packageManager, ['run', 'dev'], targetDir)
}
-function hasCommand(command) {
- if (process.platform === 'win32') {
- return spawnSync(`${command} --version`, {
- stdio: 'ignore',
- shell: true,
- }).status === 0
- }
-
- if (spawnSync(command, ['--version'], { stdio: 'ignore' }).status === 0) {
- return true
- }
- return false
-}
-
async function initProject(rawTarget, options, bootstrapConfig) {
if (!bootstrapConfig) {
printBanner()
@@ -519,31 +509,277 @@ async function askBootstrapConfig(defaultProjectName, parsedArgs) {
}
}
-function doctor() {
- const nodeMajor = Number(process.versions.node.split('.')[0] || 0)
- const hasPnpm = hasCommand('pnpm')
- const hasCodex = hasCommand('codex')
- const issues = []
+function quoteShellArg(value) {
+ const text = String(value)
+ if (/^[a-zA-Z0-9_./:@%+=,-]+$/.test(text)) {
+ return text
+ }
+ return `"${text.replace(/"/g, '\\\\"')}"`
+}
+
+function commandNeedsShell(command) {
+ return process.platform === 'win32' || /\s/.test(command)
+}
+
+function runCommandCapture(command, args = [], cwd = process.cwd()) {
+ const useShell = commandNeedsShell(command)
+ const result = useShell
+ ? spawnSync([command, ...args.map(quoteShellArg)].join(' '), {
+ cwd,
+ encoding: 'utf8',
+ env: process.env,
+ shell: true,
+ stdio: 'pipe',
+ })
+ : spawnSync(command, args, {
+ cwd,
+ encoding: 'utf8',
+ env: process.env,
+ stdio: 'pipe',
+ })
+
+ return {
+ error: result.error,
+ status: typeof result.status === 'number' ? result.status : 1,
+ stdout: result.stdout ? result.stdout.trim() : '',
+ stderr: result.stderr ? result.stderr.trim() : '',
+ }
+}
+
+function firstOutputLine(result) {
+ const output = result.stdout || result.stderr
+ return output.split(/\r?\n/).find((line) => line.trim().length > 0) || ''
+}
+
+function createDoctorCheck(status, label, message) {
+ return { status, label, message }
+}
+function checkCommandVersion(label, command, args, missingMessage) {
+ const result = runCommandCapture(command, args)
+ if (result.status === 0) {
+ const version = firstOutputLine(result)
+ return createDoctorCheck(
+ 'ok',
+ label,
+ version.length > 0 ? version : `${command} is available.`,
+ )
+ }
+
+ const reason = result.error?.message || firstOutputLine(result)
+ const suffix = reason.length > 0 ? ` (${reason})` : ''
+ return createDoctorCheck('fail', label, `${missingMessage}${suffix}`)
+}
+
+function checkNodeVersion() {
+ const nodeMajor = Number(process.versions.node.split('.')[0] || 0)
if (nodeMajor < 20) {
- issues.push('Node.js >= 20 is required.')
+ return createDoctorCheck(
+ 'fail',
+ 'Node.js',
+ `Node.js >= 20 is required. Found ${process.versions.node}.`,
+ )
}
- if (!hasPnpm) {
- issues.push('pnpm is recommended but was not found in PATH.')
+
+ return createDoctorCheck('ok', 'Node.js', `Node.js ${process.versions.node}`)
+}
+
+function checkNpmAuth() {
+ const result = runCommandCapture('npm', ['whoami'])
+ if (result.status === 0) {
+ return createDoctorCheck('ok', 'npm auth', `Authenticated as ${result.stdout}.`)
}
- if (!hasCodex) {
- issues.push('Codex CLI was not found in PATH.')
+
+ return createDoctorCheck(
+ 'warn',
+ 'npm auth',
+ 'npm auth unavailable. Run `npm login` before publishing codex-claw@alpha.',
+ )
+}
+
+function checkGitWorktree() {
+ const result = runCommandCapture('git', ['rev-parse', '--is-inside-work-tree'])
+ if (result.status === 0 && result.stdout === 'true') {
+ return createDoctorCheck('ok', 'git worktree', 'Current directory is a git worktree.')
}
- if (issues.length === 0) {
- process.stdout.write('Environment looks good.\n')
- return
+ return createDoctorCheck(
+ 'warn',
+ 'git worktree',
+ 'Current directory is not a git worktree. Bootstrap creates one for new projects.',
+ )
+}
+
+function resolveDoctorStateDir(parsedArgs) {
+ const stateDir =
+ parsedArgs.values.get('--state-dir') ||
+ process.env.CODEX_CLAW_STATE_DIR ||
+ path.join(process.cwd(), '.codex-claw')
+ return path.resolve(process.cwd(), stateDir)
+}
+
+function checkStateDirectory(parsedArgs) {
+ const stateDir = resolveDoctorStateDir(parsedArgs)
+ const parentDir = path.dirname(stateDir)
+
+ try {
+ if (fs.existsSync(stateDir)) {
+ const stat = fs.statSync(stateDir)
+ if (!stat.isDirectory()) {
+ return createDoctorCheck(
+ 'fail',
+ 'state directory',
+ `${stateDir} exists but is not a directory.`,
+ )
+ }
+
+ fs.accessSync(stateDir, fs.constants.R_OK | fs.constants.W_OK)
+ const probePath = path.join(
+ stateDir,
+ `.doctor-${process.pid}-${Date.now()}.tmp`,
+ )
+ fs.writeFileSync(probePath, 'ok\n')
+ fs.rmSync(probePath, { force: true })
+ return createDoctorCheck(
+ 'ok',
+ 'state directory',
+ `${stateDir} is writable.`,
+ )
+ }
+
+ if (!fs.existsSync(parentDir)) {
+ return createDoctorCheck(
+ 'fail',
+ 'state directory',
+ `Parent directory does not exist for ${stateDir}.`,
+ )
+ }
+
+ fs.accessSync(parentDir, fs.constants.W_OK)
+ return createDoctorCheck(
+ 'ok',
+ 'state directory',
+ `${stateDir} can be created on first run.`,
+ )
+ } catch (error) {
+ return createDoctorCheck(
+ 'fail',
+ 'state directory',
+ `${stateDir} is not writable: ${error instanceof Error ? error.message : String(error)}`,
+ )
}
+}
+
+function checkPortAvailable(port) {
+ return new Promise((resolve) => {
+ const server = net.createServer()
+ let settled = false
- for (const issue of issues) {
- process.stderr.write(`- ${issue}\n`)
+ function finish(check) {
+ if (settled) return
+ settled = true
+ resolve(check)
+ }
+
+ server.once('error', (error) => {
+ if (error && error.code === 'EADDRINUSE') {
+ finish(
+ createDoctorCheck(
+ 'fail',
+ 'port',
+ `Port ${port} is already in use. Re-run with --port or stop the existing process.`,
+ ),
+ )
+ return
+ }
+
+ finish(
+ createDoctorCheck(
+ 'fail',
+ 'port',
+ `Port ${port} is not available: ${error instanceof Error ? error.message : String(error)}`,
+ ),
+ )
+ })
+
+ server.once('listening', () => {
+ server.close(() => {
+ finish(createDoctorCheck('ok', 'port', `Port ${port} is available.`))
+ })
+ })
+
+ server.listen(port, '127.0.0.1')
+ })
+}
+
+function printDoctorChecks(checks) {
+ for (const check of checks) {
+ process.stdout.write(`[${check.status}] ${check.label}: ${check.message}\n`)
}
- process.exit(1)
+}
+
+async function doctor(parsedArgs) {
+ const codexCommand =
+ parsedArgs.values.get('--codex-command') ||
+ process.env.CODEX_CLI_COMMAND ||
+ 'codex'
+ const port = parsePort(parsedArgs.values.get('--port') || process.env.PORT || 3000, 3000)
+ const checks = [
+ checkNodeVersion(),
+ checkCommandVersion(
+ 'npm',
+ 'npm',
+ ['--version'],
+ 'npm was not found in PATH. Install Node.js with npm, then retry.',
+ ),
+ checkNpmAuth(),
+ checkCommandVersion(
+ 'pnpm',
+ 'pnpm',
+ ['--version'],
+ 'pnpm was not found in PATH. Install it with `npm install -g pnpm` or Corepack.',
+ ),
+ checkCommandVersion(
+ 'git',
+ 'git',
+ ['--version'],
+ 'git was not found in PATH. Install Git before bootstrapping CodexClaw.',
+ ),
+ checkGitWorktree(),
+ checkCommandVersion(
+ 'Codex CLI',
+ codexCommand,
+ ['--version'],
+ `Codex CLI was not found with command \`${codexCommand}\`. Install Codex CLI, run \`codex login\`, or pass --codex-command .`,
+ ),
+ checkStateDirectory(parsedArgs),
+ ]
+
+ if (parsedArgs.flags.has('--no-port-check')) {
+ checks.push(createDoctorCheck('warn', 'port', 'Port availability check skipped.'))
+ } else {
+ checks.push(await checkPortAvailable(port))
+ }
+
+ printDoctorChecks(checks)
+
+ const failures = checks.filter((check) => check.status === 'fail')
+ const warnings = checks.filter((check) => check.status === 'warn')
+ if (failures.length > 0) {
+ process.stderr.write(
+ `CodexClaw doctor found ${failures.length} blocking issue(s).\n`,
+ )
+ process.exit(1)
+ }
+
+ if (warnings.length > 0) {
+ process.stdout.write(
+ `Environment is usable with ${warnings.length} warning(s).\n`,
+ )
+ return
+ }
+
+ process.stdout.write('Environment looks good.\n')
}
async function main() {
@@ -571,7 +807,7 @@ async function main() {
}
if (command === 'doctor') {
- doctor()
+ await doctor(parsedArgs)
return
}
diff --git a/packages/codex-claw/scripts/install-smoke.mjs b/packages/codex-claw/scripts/install-smoke.mjs
new file mode 100644
index 0000000..6970f63
--- /dev/null
+++ b/packages/codex-claw/scripts/install-smoke.mjs
@@ -0,0 +1,190 @@
+#!/usr/bin/env node
+
+import fs from 'node:fs'
+import os from 'node:os'
+import path from 'node:path'
+import { spawnSync } from 'node:child_process'
+import { fileURLToPath } from 'node:url'
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = path.dirname(__filename)
+const packageDir = path.resolve(__dirname, '..')
+const packageName = 'codex-claw'
+const alphaSpec = packageName + '@alpha'
+
+function parseArgs(args) {
+ let source = 'pack'
+
+ for (let index = 0; index < args.length; index += 1) {
+ const arg = args[index]
+ if (arg === '--source' && args[index + 1]) {
+ source = args[index + 1]
+ index += 1
+ continue
+ }
+
+ if (arg.startsWith('--source=')) {
+ source = arg.slice('--source='.length)
+ continue
+ }
+
+ if (arg === '-h' || arg === '--help') {
+ process.stdout.write('Usage: node scripts/install-smoke.mjs --source \n')
+ process.exit(0)
+ }
+ }
+
+ if (!['pack', 'npm', 'all'].includes(source)) {
+ throw createFailure('Invalid --source value. Use pack, npm, or all.')
+ }
+
+ return { source }
+}
+
+function resolveNpmCliPath() {
+ const nodeDir = path.dirname(process.execPath)
+ const prefixDir = path.resolve(nodeDir, '..')
+ const candidates = [
+ path.join(nodeDir, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
+ path.join(prefixDir, 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
+ path.join(prefixDir, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
+ ]
+
+ return candidates.find((candidate) => fs.existsSync(candidate)) || null
+}
+
+function runNpm(args, cwd) {
+ const npmCliPath = resolveNpmCliPath()
+ if (npmCliPath) {
+ return spawnSync(process.execPath, [npmCliPath, ...args], {
+ cwd,
+ encoding: 'utf8',
+ env: process.env,
+ stdio: 'pipe',
+ })
+ }
+
+ return spawnSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', args, {
+ cwd,
+ encoding: 'utf8',
+ env: process.env,
+ stdio: 'pipe',
+ })
+}
+
+function createFailure(message, result) {
+ return { message, result }
+}
+
+function assertSuccess(result, message) {
+ if (result.status === 0) {
+ return
+ }
+ throw createFailure(message, result)
+}
+
+function printCaptured(result) {
+ if (!result) return
+ if (result.stdout && result.stdout.trim().length > 0) {
+ process.stderr.write(result.stdout.trim() + '\n')
+ }
+ if (result.stderr && result.stderr.trim().length > 0) {
+ process.stderr.write(result.stderr.trim() + '\n')
+ }
+ if (result.error) {
+ process.stderr.write(result.error.message + '\n')
+ }
+}
+
+function findPackedTarball(result, tempDir) {
+ const lines = result.stdout.split(/\r?\n/).map((line) => line.trim())
+ const tarballName = [...lines].reverse().find((line) => line.endsWith('.tgz'))
+ if (!tarballName) {
+ throw createFailure('npm pack did not report a .tgz artifact.', result)
+ }
+
+ const tarballPath = path.resolve(tempDir, tarballName)
+ if (!fs.existsSync(tarballPath)) {
+ throw createFailure('Packed tarball was not written to ' + tarballPath + '.', result)
+ }
+
+ return tarballPath
+}
+
+function runPackSmoke() {
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-claw-pack-'))
+
+ try {
+ process.stdout.write('Packing local codex-claw package...\n')
+ const packResult = runNpm(['pack', '--pack-destination', tempDir], packageDir)
+ assertSuccess(packResult, 'npm pack failed for the local codex-claw package.')
+
+ const tarballPath = findPackedTarball(packResult, tempDir)
+ process.stdout.write('Running npx-compatible smoke test from packed tarball...\n')
+ const npxResult = runNpm(
+ ['exec', '--yes', '--package', tarballPath, '--', packageName, '--help'],
+ packageDir,
+ )
+ assertSuccess(
+ npxResult,
+ 'npx could not run codex-claw from the packed tarball.',
+ )
+ process.stdout.write('Packed tarball smoke test passed.\n')
+ } finally {
+ fs.rmSync(tempDir, { recursive: true, force: true })
+ }
+}
+
+function runNpmSmoke() {
+ process.stdout.write('Checking published codex-claw alpha package...\n')
+ const viewResult = runNpm(['view', alphaSpec, 'version'], packageDir)
+ if (viewResult.status !== 0) {
+ const combinedOutput = ((viewResult.stdout || '') + '\n' + (viewResult.stderr || '')).trim()
+ if (/E404|404|not found|No match found/i.test(combinedOutput)) {
+ throw createFailure(
+ 'codex-claw@alpha was not found on npm. Publish with pnpm release:codex-claw after npm login, then rerun this smoke test.',
+ viewResult,
+ )
+ }
+ if (/ENEEDAUTH|E401|E403|auth/i.test(combinedOutput)) {
+ throw createFailure(
+ 'npm auth unavailable. Run npm login before publishing or checking restricted package metadata.',
+ viewResult,
+ )
+ }
+ throw createFailure('npm could not read codex-claw@alpha metadata.', viewResult)
+ }
+
+ const version = viewResult.stdout.trim()
+ process.stdout.write('Found codex-claw@alpha version ' + version + '.\n')
+ process.stdout.write('Running npx-compatible smoke test from npm alpha...\n')
+ const npxResult = runNpm(
+ ['exec', '--yes', '--package', alphaSpec, '--', packageName, '--help'],
+ packageDir,
+ )
+ assertSuccess(npxResult, 'npx could not run codex-claw@alpha from npm.')
+ process.stdout.write('npm alpha smoke test passed.\n')
+}
+
+function main() {
+ const { source } = parseArgs(process.argv.slice(2))
+
+ if (source === 'pack' || source === 'all') {
+ runPackSmoke()
+ }
+
+ if (source === 'npm' || source === 'all') {
+ runNpmSmoke()
+ }
+}
+
+try {
+ main()
+} catch (error) {
+ const message = error && typeof error === 'object' && 'message' in error
+ ? error.message
+ : String(error)
+ process.stderr.write(message + '\n')
+ printCaptured(error && typeof error === 'object' ? error.result : null)
+ process.exit(1)
+}