claude-code

This commit is contained in:
ashutoshpythoncs@gmail.com
2026-03-31 18:58:05 +05:30
parent a2a44a5841
commit b564857c0b
2148 changed files with 564518 additions and 2 deletions

197
scripts/build-bundle.ts Normal file
View File

@@ -0,0 +1,197 @@
// scripts/build-bundle.ts
// Usage: bun scripts/build-bundle.ts [--watch] [--minify] [--no-sourcemap]
//
// Production build: bun scripts/build-bundle.ts --minify
// Dev build: bun scripts/build-bundle.ts
// Watch mode: bun scripts/build-bundle.ts --watch
import * as esbuild from 'esbuild'
import { resolve, dirname } from 'path'
import { chmodSync, readFileSync, existsSync } from 'fs'
import { fileURLToPath } from 'url'
// Bun: import.meta.dir — Node 21+: import.meta.dirname — fallback
const __dir: string =
(import.meta as any).dir ??
(import.meta as any).dirname ??
dirname(fileURLToPath(import.meta.url))
const ROOT = resolve(__dir, '..')
const watch = process.argv.includes('--watch')
const minify = process.argv.includes('--minify')
const noSourcemap = process.argv.includes('--no-sourcemap')
// Read version from package.json for MACRO injection
const pkg = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8'))
const version = pkg.version || '0.0.0-dev'
// ── Plugin: resolve bare 'src/' imports (tsconfig baseUrl: ".") ──
// The codebase uses `import ... from 'src/foo/bar.js'` which relies on
// TypeScript's baseUrl resolution. This plugin maps those to real TS files.
const srcResolverPlugin: esbuild.Plugin = {
name: 'src-resolver',
setup(build) {
build.onResolve({ filter: /^src\// }, (args) => {
const basePath = resolve(ROOT, args.path)
// Already exists as-is
if (existsSync(basePath)) {
return { path: basePath }
}
// Strip .js/.jsx and try TypeScript extensions
const withoutExt = basePath.replace(/\.(js|jsx)$/, '')
for (const ext of ['.ts', '.tsx', '.js', '.jsx']) {
const candidate = withoutExt + ext
if (existsSync(candidate)) {
return { path: candidate }
}
}
// Try as directory with index file
const dirPath = basePath.replace(/\.(js|jsx)$/, '')
for (const ext of ['.ts', '.tsx', '.js', '.jsx']) {
const candidate = resolve(dirPath, 'index' + ext)
if (existsSync(candidate)) {
return { path: candidate }
}
}
// Let esbuild handle it (will error if truly missing)
return undefined
})
},
}
const buildOptions: esbuild.BuildOptions = {
entryPoints: [resolve(ROOT, 'src/entrypoints/cli.tsx')],
bundle: true,
platform: 'node',
target: ['node20', 'es2022'],
format: 'esm',
outdir: resolve(ROOT, 'dist'),
outExtension: { '.js': '.mjs' },
// Single-file output — no code splitting for CLI tools
splitting: false,
plugins: [srcResolverPlugin],
// Use tsconfig for baseUrl / paths resolution (complements plugin above)
tsconfig: resolve(ROOT, 'tsconfig.json'),
// Alias bun:bundle to our runtime shim
alias: {
'bun:bundle': resolve(ROOT, 'src/shims/bun-bundle.ts'),
},
// Don't bundle node built-ins or problematic native packages
external: [
// Node built-ins (with and without node: prefix)
'fs', 'path', 'os', 'crypto', 'child_process', 'http', 'https',
'net', 'tls', 'url', 'util', 'stream', 'events', 'buffer',
'querystring', 'readline', 'zlib', 'assert', 'tty', 'worker_threads',
'perf_hooks', 'async_hooks', 'dns', 'dgram', 'cluster',
'string_decoder', 'module', 'vm', 'constants', 'domain',
'console', 'process', 'v8', 'inspector',
'node:*',
// Native addons that can't be bundled
'fsevents',
'sharp',
'image-processor-napi',
// Anthropic-internal packages (not published externally)
'@anthropic-ai/sandbox-runtime',
'@anthropic-ai/claude-agent-sdk',
// Anthropic-internal (@ant/) packages — gated behind USER_TYPE === 'ant'
'@ant/*',
],
jsx: 'automatic',
// Source maps for production debugging (external .map files)
sourcemap: noSourcemap ? false : 'external',
// Minification for production
minify,
// Tree shaking (on by default, explicit for clarity)
treeShaking: true,
// Define replacements — inline constants at build time
// MACRO.* — originally inlined by Bun's bundler at compile time
// process.env.USER_TYPE — eliminates 'ant' (Anthropic-internal) code branches
define: {
'MACRO.VERSION': JSON.stringify(version),
'MACRO.PACKAGE_URL': JSON.stringify('@anthropic-ai/claude-code'),
'MACRO.ISSUES_EXPLAINER': JSON.stringify(
'report issues at https://github.com/anthropics/claude-code/issues'
),
'process.env.USER_TYPE': '"external"',
'process.env.NODE_ENV': minify ? '"production"' : '"development"',
},
// Banner: shebang for direct CLI execution
banner: {
js: '#!/usr/bin/env node\n',
},
// Handle the .js → .ts resolution that the codebase uses
resolveExtensions: ['.tsx', '.ts', '.jsx', '.js', '.json'],
logLevel: 'info',
// Metafile for bundle analysis
metafile: true,
}
async function main() {
if (watch) {
const ctx = await esbuild.context(buildOptions)
await ctx.watch()
console.log('Watching for changes...')
} else {
const startTime = Date.now()
const result = await esbuild.build(buildOptions)
if (result.errors.length > 0) {
console.error('Build failed')
process.exit(1)
}
// Make the output executable
const outPath = resolve(ROOT, 'dist/cli.mjs')
try {
chmodSync(outPath, 0o755)
} catch {
// chmod may fail on some platforms, non-fatal
}
const elapsed = Date.now() - startTime
// Print bundle size info
if (result.metafile) {
const text = await esbuild.analyzeMetafile(result.metafile, { verbose: false })
const outFiles = Object.entries(result.metafile.outputs)
for (const [file, info] of outFiles) {
if (file.endsWith('.mjs')) {
const sizeMB = ((info as { bytes: number }).bytes / 1024 / 1024).toFixed(2)
console.log(`\n ${file}: ${sizeMB} MB`)
}
}
console.log(`\nBuild complete in ${elapsed}ms → dist/`)
// Write metafile for further analysis
const { writeFileSync } = await import('fs')
writeFileSync(
resolve(ROOT, 'dist/meta.json'),
JSON.stringify(result.metafile),
)
console.log(' Metafile written to dist/meta.json')
}
}
}
main().catch(err => {
console.error(err)
process.exit(1)
})

58
scripts/build-web.ts Normal file
View File

@@ -0,0 +1,58 @@
// scripts/build-web.ts
// Bundles the browser-side terminal frontend.
//
// Usage:
// bun scripts/build-web.ts # dev build
// bun scripts/build-web.ts --watch # watch mode
// bun scripts/build-web.ts --minify # production (minified)
import * as esbuild from 'esbuild'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
const __dir: string =
(import.meta as any).dir ??
(import.meta as any).dirname ??
dirname(fileURLToPath(import.meta.url))
const ROOT = resolve(__dir, '..')
const ENTRY = resolve(ROOT, 'src/server/web/terminal.ts')
const OUT_DIR = resolve(ROOT, 'src/server/web/public')
const watch = process.argv.includes('--watch')
const minify = process.argv.includes('--minify')
const buildOptions: esbuild.BuildOptions = {
entryPoints: [ENTRY],
bundle: true,
platform: 'browser',
target: ['es2020', 'chrome90', 'firefox90', 'safari14'],
format: 'esm',
outdir: OUT_DIR,
// CSS imported from JS is auto-emitted alongside the JS output
loader: { '.css': 'css' },
minify,
sourcemap: minify ? false : 'inline',
tsconfig: resolve(ROOT, 'src/server/web/tsconfig.json'),
logLevel: 'info',
}
async function main() {
if (watch) {
const ctx = await esbuild.context(buildOptions)
await ctx.watch()
console.log('Watching src/server/web/terminal.ts...')
} else {
const start = Date.now()
const result = await esbuild.build(buildOptions)
if (result.errors.length > 0) {
process.exit(1)
}
console.log(`Web build complete in ${Date.now() - start}ms → ${OUT_DIR}`)
}
}
main().catch(err => {
console.error(err)
process.exit(1)
})

58
scripts/build.sh Normal file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────
# build.sh — Minimal build / check script for the leaked source
# ─────────────────────────────────────────────────────────────
# Usage:
# ./scripts/build.sh # install + typecheck + lint
# ./scripts/build.sh install # install deps only
# ./scripts/build.sh check # typecheck + lint only
# ─────────────────────────────────────────────────────────────
set -euo pipefail
STEP="${1:-all}"
install_deps() {
echo "── Installing dependencies ──"
if command -v bun &>/dev/null; then
bun install
elif command -v npm &>/dev/null; then
npm install
else
echo "Error: neither bun nor npm found on PATH" >&2
exit 1
fi
}
typecheck() {
echo "── Running TypeScript type-check ──"
npx tsc --noEmit
}
lint() {
echo "── Running Biome lint ──"
npx @biomejs/biome check src/
}
case "$STEP" in
install)
install_deps
;;
check)
typecheck
lint
;;
all)
install_deps
typecheck
lint
;;
*)
echo "Unknown step: $STEP"
echo "Usage: $0 [install|check|all]"
exit 1
;;
esac
echo "── Done ──"

View File

@@ -0,0 +1,18 @@
// scripts/bun-plugin-shims.ts
// Bun preload plugin — intercepts `bun:bundle` imports at runtime
// and resolves them to our local shim so the CLI can run without
// the production Bun bundler pass.
import { plugin } from 'bun'
import { resolve } from 'path'
plugin({
name: 'bun-bundle-shim',
setup(build) {
const shimPath = resolve(import.meta.dir, '../src/shims/bun-bundle.ts')
build.onResolve({ filter: /^bun:bundle$/ }, () => ({
path: shimPath,
}))
},
})

49
scripts/ci-build.sh Normal file
View File

@@ -0,0 +1,49 @@
#!/bin/bash
# ─────────────────────────────────────────────────────────────
# ci-build.sh — CI/CD build pipeline
# ─────────────────────────────────────────────────────────────
# Runs the full build pipeline: install, typecheck, lint, build,
# and verify the output. Intended for CI environments.
#
# Usage:
# ./scripts/ci-build.sh
# ─────────────────────────────────────────────────────────────
set -euo pipefail
echo "=== Installing dependencies ==="
bun install
echo "=== Type checking ==="
bun run typecheck
echo "=== Linting ==="
bun run lint
echo "=== Building production bundle ==="
bun run build:prod
echo "=== Verifying build output ==="
# Check that the bundle was produced
if [ ! -f dist/cli.mjs ]; then
echo "ERROR: dist/cli.mjs not found"
exit 1
fi
# Print bundle size
SIZE=$(ls -lh dist/cli.mjs | awk '{print $5}')
echo " Bundle size: $SIZE"
# Verify the bundle runs with Node.js
if command -v node &>/dev/null; then
VERSION=$(node dist/cli.mjs --version 2>&1 || true)
echo " node dist/cli.mjs --version → $VERSION"
fi
# Verify the bundle runs with Bun
if command -v bun &>/dev/null; then
VERSION=$(bun dist/cli.mjs --version 2>&1 || true)
echo " bun dist/cli.mjs --version → $VERSION"
fi
echo "=== Done ==="

15
scripts/dev.ts Normal file
View File

@@ -0,0 +1,15 @@
// scripts/dev.ts
// Development launcher — runs the CLI directly via Bun's TS runtime.
//
// Usage:
// bun scripts/dev.ts [args...]
// bun run dev [args...]
//
// The bun:bundle shim is loaded automatically via bunfig.toml preload.
// Bun automatically reads .env files from the project root.
// Load MACRO global (version, package url, etc.) before any app code
import '../src/shims/macro.js'
// Launch the CLI entrypoint
await import('../src/entrypoints/cli.js')

91
scripts/package-npm.ts Normal file
View File

@@ -0,0 +1,91 @@
// scripts/package-npm.ts
// Generate a publishable npm package in dist/npm/
//
// Usage: bun scripts/package-npm.ts
//
// Prerequisites: run `bun run build:prod` first to generate dist/cli.mjs
import { readFileSync, writeFileSync, mkdirSync, copyFileSync, existsSync, chmodSync } from 'fs'
import { resolve } from 'path'
// Bun: import.meta.dir — Node 21+: import.meta.dirname — fallback
const __dir: string =
(import.meta as ImportMeta & { dir?: string; dirname?: string }).dir ??
(import.meta as ImportMeta & { dir?: string; dirname?: string }).dirname ??
new URL('.', import.meta.url).pathname
const ROOT = resolve(__dir, '..')
const DIST = resolve(ROOT, 'dist')
const NPM_DIR = resolve(DIST, 'npm')
const CLI_BUNDLE = resolve(DIST, 'cli.mjs')
function main() {
// Verify the bundle exists
if (!existsSync(CLI_BUNDLE)) {
console.error('Error: dist/cli.mjs not found. Run `bun run build:prod` first.')
process.exit(1)
}
// Read source package.json
const srcPkg = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8'))
// Create npm output directory
mkdirSync(NPM_DIR, { recursive: true })
// Copy the bundled CLI
copyFileSync(CLI_BUNDLE, resolve(NPM_DIR, 'cli.mjs'))
chmodSync(resolve(NPM_DIR, 'cli.mjs'), 0o755)
// Copy source map if it exists
const sourceMap = resolve(DIST, 'cli.mjs.map')
if (existsSync(sourceMap)) {
copyFileSync(sourceMap, resolve(NPM_DIR, 'cli.mjs.map'))
}
// Generate a publishable package.json
const npmPkg = {
name: srcPkg.name || '@anthropic-ai/claude-code',
version: srcPkg.version || '0.0.0',
description: srcPkg.description || 'Anthropic Claude Code CLI',
license: 'MIT',
type: 'module',
main: './cli.mjs',
bin: {
claude: './cli.mjs',
},
engines: {
node: '>=20.0.0',
},
os: ['darwin', 'linux', 'win32'],
files: [
'cli.mjs',
'cli.mjs.map',
'README.md',
],
}
writeFileSync(
resolve(NPM_DIR, 'package.json'),
JSON.stringify(npmPkg, null, 2) + '\n',
)
// Copy README if it exists
const readme = resolve(ROOT, 'README.md')
if (existsSync(readme)) {
copyFileSync(readme, resolve(NPM_DIR, 'README.md'))
}
// Summary
const bundleSize = readFileSync(CLI_BUNDLE).byteLength
const sizeMB = (bundleSize / 1024 / 1024).toFixed(2)
console.log('npm package generated in dist/npm/')
console.log(` package: ${npmPkg.name}@${npmPkg.version}`)
console.log(` bundle: cli.mjs (${sizeMB} MB)`)
console.log(` bin: claude → ./cli.mjs`)
console.log('')
console.log('To publish:')
console.log(' cd dist/npm && npm publish')
}
main()

26
scripts/test-auth.ts Normal file
View File

@@ -0,0 +1,26 @@
// scripts/test-auth.ts
// Quick test that the API key is configured and can reach Anthropic
// Usage: bun scripts/test-auth.ts
import Anthropic from '@anthropic-ai/sdk'
const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
})
async function main() {
try {
const msg = await client.messages.create({
model: process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-20250514',
max_tokens: 50,
messages: [{ role: 'user', content: 'Say "hello" and nothing else.' }],
})
console.log('✅ API connection successful!')
console.log('Response:', msg.content[0].type === 'text' ? msg.content[0].text : msg.content[0])
} catch (err: any) {
console.error('❌ API connection failed:', err.message)
process.exit(1)
}
}
main()

64
scripts/test-commands.ts Normal file
View File

@@ -0,0 +1,64 @@
// scripts/test-commands.ts
// Verify all commands load without errors
// Usage: bun scripts/test-commands.ts
//
// The bun:bundle shim is loaded automatically via bunfig.toml preload.
// Load MACRO global before any app code
import '../src/shims/macro.js'
async function main() {
const { getCommands } = await import('../src/commands.js')
const cwd = process.cwd()
const commands = await getCommands(cwd)
console.log(`Loaded ${commands.length} commands:\n`)
// Group commands by type for readability
const byType: Record<string, typeof commands> = {}
for (const cmd of commands) {
const t = cmd.type
if (!byType[t]) byType[t] = []
byType[t]!.push(cmd)
}
for (const [type, cmds] of Object.entries(byType)) {
console.log(` [${type}] (${cmds.length} commands)`)
for (const cmd of cmds) {
const aliases = cmd.aliases?.length ? ` (aliases: ${cmd.aliases.join(', ')})` : ''
const hidden = cmd.isHidden ? ' [hidden]' : ''
const source = cmd.type === 'prompt' ? ` (source: ${cmd.source})` : ''
console.log(` /${cmd.name}${cmd.description || '(no description)'}${aliases}${hidden}${source}`)
}
console.log()
}
// Verify essential commands are present
const essential = ['help', 'config', 'init', 'commit', 'review']
const commandNames = new Set(commands.map(c => c.name))
const missing = essential.filter(n => !commandNames.has(n))
if (missing.length > 0) {
console.error(`❌ Missing essential commands: ${missing.join(', ')}`)
process.exit(1)
}
console.log(`✅ All ${essential.length} essential commands present: ${essential.join(', ')}`)
// Check moved-to-plugin commands
const movedToPlugin = commands.filter(
c => c.type === 'prompt' && c.description && c.name
).filter(c => ['security-review', 'pr-comments'].includes(c.name))
if (movedToPlugin.length > 0) {
console.log(`✅ Moved-to-plugin commands present and loadable: ${movedToPlugin.map(c => c.name).join(', ')}`)
}
console.log(`\n✅ Command system loaded successfully (${commands.length} commands)`)
}
main().catch(err => {
console.error('❌ Command loading failed:', err)
process.exit(1)
})

180
scripts/test-mcp.ts Normal file
View File

@@ -0,0 +1,180 @@
#!/usr/bin/env npx tsx
/**
* scripts/test-mcp.ts
* Test MCP client/server roundtrip using the standalone mcp-server sub-project.
*
* Usage:
* cd mcp-server && npm install && npm run build && cd ..
* npx tsx scripts/test-mcp.ts
*
* What it does:
* 1. Spawns mcp-server/dist/index.js as a child process (stdio transport)
* 2. Creates an MCP client using @modelcontextprotocol/sdk
* 3. Connects client to server
* 4. Lists available tools
* 5. Calls list_tools and read_source_file tools
* 6. Lists resources and reads one
* 7. Prints results and exits
*/
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const PROJECT_ROOT = resolve(__dirname, "..");
// ── Helpers ───────────────────────────────────────────────────────────────
function section(title: string) {
console.log(`\n${"─".repeat(60)}`);
console.log(` ${title}`);
console.log(`${"─".repeat(60)}`);
}
function jsonPretty(obj: unknown): string {
return JSON.stringify(obj, null, 2);
}
// ── Main ──────────────────────────────────────────────────────────────────
async function main() {
const serverScript = resolve(PROJECT_ROOT, "mcp-server", "dist", "index.js");
const srcRoot = resolve(PROJECT_ROOT, "src");
section("1. Spawning MCP server (stdio transport)");
console.log(` Server: ${serverScript}`);
console.log(` SRC_ROOT: ${srcRoot}`);
const transport = new StdioClientTransport({
command: "node",
args: [serverScript],
env: {
...process.env,
CLAUDE_CODE_SRC_ROOT: srcRoot,
} as Record<string, string>,
stderr: "pipe",
});
// Log stderr from the server process
if (transport.stderr) {
transport.stderr.on("data", (data: Buffer) => {
const msg = data.toString().trim();
if (msg) console.log(` [server stderr] ${msg}`);
});
}
section("2. Creating MCP client");
const client = new Client(
{
name: "test-mcp-client",
version: "1.0.0",
},
{
capabilities: {},
}
);
section("3. Connecting client → server");
await client.connect(transport);
console.log(" ✓ Connected successfully");
// ── List Tools ──────────────────────────────────────────────────────────
section("4. Listing available tools");
const toolsResult = await client.listTools();
console.log(` Found ${toolsResult.tools.length} tool(s):`);
for (const tool of toolsResult.tools) {
console.log(`${tool.name}${tool.description?.slice(0, 80)}`);
}
// ── Call list_tools ─────────────────────────────────────────────────────
section("5. Calling tool: list_tools");
const listToolsResult = await client.callTool({
name: "list_tools",
arguments: {},
});
const listToolsContent = listToolsResult.content as Array<{
type: string;
text: string;
}>;
const listToolsText = listToolsContent
.filter((c) => c.type === "text")
.map((c) => c.text)
.join("\n");
// Show first 500 chars
console.log(
` Result (first 500 chars):\n${listToolsText.slice(0, 500)}${listToolsText.length > 500 ? "\n …(truncated)" : ""}`
);
// ── Call read_source_file ───────────────────────────────────────────────
section("6. Calling tool: read_source_file (path: 'main.tsx', lines 1-20)");
const readResult = await client.callTool({
name: "read_source_file",
arguments: { path: "main.tsx", startLine: 1, endLine: 20 },
});
const readContent = readResult.content as Array<{
type: string;
text: string;
}>;
const readText = readContent
.filter((c) => c.type === "text")
.map((c) => c.text)
.join("\n");
console.log(` Result:\n${readText.slice(0, 600)}`);
// ── List Resources ──────────────────────────────────────────────────────
section("7. Listing resources");
try {
const resourcesResult = await client.listResources();
console.log(` Found ${resourcesResult.resources.length} resource(s):`);
for (const res of resourcesResult.resources.slice(0, 10)) {
console.log(`${res.name} (${res.uri})`);
}
if (resourcesResult.resources.length > 10) {
console.log(
` …and ${resourcesResult.resources.length - 10} more`
);
}
// Read the first resource
if (resourcesResult.resources.length > 0) {
const firstRes = resourcesResult.resources[0]!;
section(`8. Reading resource: ${firstRes.name}`);
const resContent = await client.readResource({ uri: firstRes.uri });
const resText = resContent.contents
.filter((c): c is { uri: string; text: string; mimeType?: string } => "text" in c)
.map((c) => c.text)
.join("\n");
console.log(
` Content (first 400 chars):\n${resText.slice(0, 400)}${resText.length > 400 ? "\n …(truncated)" : ""}`
);
}
} catch (err) {
console.log(` Resources not supported or error: ${err}`);
}
// ── List Prompts ────────────────────────────────────────────────────────
section("9. Listing prompts");
try {
const promptsResult = await client.listPrompts();
console.log(` Found ${promptsResult.prompts.length} prompt(s):`);
for (const p of promptsResult.prompts) {
console.log(`${p.name}${p.description?.slice(0, 80)}`);
}
} catch (err) {
console.log(` Prompts not supported or error: ${err}`);
}
// ── Cleanup ─────────────────────────────────────────────────────────────
section("Done ✓");
console.log(" All tests passed. Closing connection.");
await client.close();
process.exit(0);
}
main().catch((err) => {
console.error("\n✗ Test failed:", err);
process.exit(1);
});

233
scripts/test-services.ts Normal file
View File

@@ -0,0 +1,233 @@
// scripts/test-services.ts
// Test that all services initialize without crashing
// Usage: bun scripts/test-services.ts
import '../src/shims/preload.js'
// Ensure we don't accidentally talk to real servers
process.env.NODE_ENV = process.env.NODE_ENV || 'test'
type TestResult = { name: string; status: 'pass' | 'fail' | 'skip'; detail?: string }
const results: TestResult[] = []
function pass(name: string, detail?: string) {
results.push({ name, status: 'pass', detail })
console.log(`${name}${detail ? `${detail}` : ''}`)
}
function fail(name: string, detail: string) {
results.push({ name, status: 'fail', detail })
console.log(`${name}${detail}`)
}
function skip(name: string, detail: string) {
results.push({ name, status: 'skip', detail })
console.log(` ⏭️ ${name}${detail}`)
}
async function testGrowthBook() {
console.log('\n--- GrowthBook (Feature Flags) ---')
try {
const gb = await import('../src/services/analytics/growthbook.js')
// Test cached feature value returns default when GrowthBook is unavailable
const boolResult = gb.getFeatureValue_CACHED_MAY_BE_STALE('nonexistent_feature', false)
if (boolResult === false) {
pass('getFeatureValue_CACHED_MAY_BE_STALE (bool)', 'returns default false')
} else {
fail('getFeatureValue_CACHED_MAY_BE_STALE (bool)', `expected false, got ${boolResult}`)
}
const strResult = gb.getFeatureValue_CACHED_MAY_BE_STALE('nonexistent_str', 'default_val')
if (strResult === 'default_val') {
pass('getFeatureValue_CACHED_MAY_BE_STALE (str)', 'returns default string')
} else {
fail('getFeatureValue_CACHED_MAY_BE_STALE (str)', `expected "default_val", got "${strResult}"`)
}
// Test Statsig gate check returns false
const gateResult = gb.checkStatsigFeatureGate_CACHED_MAY_BE_STALE('nonexistent_gate')
if (gateResult === false) {
pass('checkStatsigFeatureGate_CACHED_MAY_BE_STALE', 'returns false for unknown gate')
} else {
fail('checkStatsigFeatureGate_CACHED_MAY_BE_STALE', `expected false, got ${gateResult}`)
}
} catch (err: any) {
fail('GrowthBook import', err.message)
}
}
async function testAnalyticsSink() {
console.log('\n--- Analytics Sink ---')
try {
const analytics = await import('../src/services/analytics/index.js')
// logEvent should queue without crashing when no sink is attached
analytics.logEvent('test_event', { test_key: 1 })
pass('logEvent (no sink)', 'queues without crash')
await analytics.logEventAsync('test_async_event', { test_key: 2 })
pass('logEventAsync (no sink)', 'queues without crash')
} catch (err: any) {
fail('Analytics sink', err.message)
}
}
async function testPolicyLimits() {
console.log('\n--- Policy Limits ---')
try {
const pl = await import('../src/services/policyLimits/index.js')
// isPolicyAllowed should return true (fail open) when no restrictions loaded
const result = pl.isPolicyAllowed('allow_remote_sessions')
if (result === true) {
pass('isPolicyAllowed (no cache)', 'fails open — returns true')
} else {
fail('isPolicyAllowed (no cache)', `expected true (fail open), got ${result}`)
}
// isPolicyLimitsEligible should return false without valid auth
const eligible = pl.isPolicyLimitsEligible()
pass('isPolicyLimitsEligible', `returns ${eligible} (expected false in test env)`)
} catch (err: any) {
fail('Policy limits', err.message)
}
}
async function testRemoteManagedSettings() {
console.log('\n--- Remote Managed Settings ---')
try {
const rms = await import('../src/services/remoteManagedSettings/index.js')
// isEligibleForRemoteManagedSettings should return false without auth
const eligible = rms.isEligibleForRemoteManagedSettings()
pass('isEligibleForRemoteManagedSettings', `returns ${eligible} (expected false in test env)`)
// waitForRemoteManagedSettingsToLoad should resolve immediately if not eligible
await rms.waitForRemoteManagedSettingsToLoad()
pass('waitForRemoteManagedSettingsToLoad', 'resolves immediately when not eligible')
} catch (err: any) {
fail('Remote managed settings', err.message)
}
}
async function testBootstrapData() {
console.log('\n--- Bootstrap Data ---')
try {
const bootstrap = await import('../src/services/api/bootstrap.js')
// fetchBootstrapData should not crash — just skip when no auth
await bootstrap.fetchBootstrapData()
pass('fetchBootstrapData', 'completes without crash (skips when no auth)')
} catch (err: any) {
// fetchBootstrapData catches its own errors, so this means an import-level issue
fail('Bootstrap data', err.message)
}
}
async function testSessionMemoryUtils() {
console.log('\n--- Session Memory ---')
try {
const smUtils = await import('../src/services/SessionMemory/sessionMemoryUtils.js')
// Default config should be sensible
const config = smUtils.DEFAULT_SESSION_MEMORY_CONFIG
if (config.minimumMessageTokensToInit > 0 && config.minimumTokensBetweenUpdate > 0) {
pass('DEFAULT_SESSION_MEMORY_CONFIG', `init=${config.minimumMessageTokensToInit} tokens, update=${config.minimumTokensBetweenUpdate} tokens`)
} else {
fail('DEFAULT_SESSION_MEMORY_CONFIG', 'unexpected config values')
}
// getLastSummarizedMessageId should return undefined initially
const lastId = smUtils.getLastSummarizedMessageId()
if (lastId === undefined) {
pass('getLastSummarizedMessageId', 'returns undefined initially')
} else {
fail('getLastSummarizedMessageId', `expected undefined, got ${lastId}`)
}
} catch (err: any) {
fail('Session memory utils', err.message)
}
}
async function testCostTracker() {
console.log('\n--- Cost Tracking ---')
try {
const ct = await import('../src/cost-tracker.js')
// Total cost should start at 0
const cost = ct.getTotalCost()
if (cost === 0) {
pass('getTotalCost', 'starts at $0.00')
} else {
pass('getTotalCost', `current: $${cost.toFixed(4)} (non-zero means restored session)`)
}
// Duration should be available
const duration = ct.getTotalDuration()
pass('getTotalDuration', `${duration}ms`)
// Token counters should be available
const inputTokens = ct.getTotalInputTokens()
const outputTokens = ct.getTotalOutputTokens()
pass('Token counters', `input=${inputTokens}, output=${outputTokens}`)
// Lines changed
const added = ct.getTotalLinesAdded()
const removed = ct.getTotalLinesRemoved()
pass('Lines changed', `+${added} -${removed}`)
} catch (err: any) {
fail('Cost tracker', err.message)
}
}
async function testInit() {
console.log('\n--- Init (entrypoint) ---')
try {
const { init } = await import('../src/entrypoints/init.js')
await init()
pass('init()', 'completed successfully')
} catch (err: any) {
fail('init()', err.message)
}
}
async function main() {
console.log('=== Services Layer Smoke Test ===')
console.log(`Environment: NODE_ENV=${process.env.NODE_ENV}`)
console.log(`Auth: ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY ? '(set)' : '(not set)'}`)
// Test individual services first (order: least-dependent → most-dependent)
await testAnalyticsSink()
await testGrowthBook()
await testPolicyLimits()
await testRemoteManagedSettings()
await testBootstrapData()
await testSessionMemoryUtils()
await testCostTracker()
// Then test the full init sequence
await testInit()
// Summary
console.log('\n=== Summary ===')
const passed = results.filter(r => r.status === 'pass').length
const failed = results.filter(r => r.status === 'fail').length
const skipped = results.filter(r => r.status === 'skip').length
console.log(` ${passed} passed, ${failed} failed, ${skipped} skipped`)
if (failed > 0) {
console.log('\nFailed tests:')
for (const r of results.filter(r => r.status === 'fail')) {
console.log(`${r.name}: ${r.detail}`)
}
process.exit(1)
}
console.log('\n✅ All services handle graceful degradation correctly')
}
main().catch(err => {
console.error('Fatal error in smoke test:', err)
process.exit(1)
})

15
scripts/tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"resolveJsonModule": true,
"isolatedModules": true,
"types": ["node"]
},
"include": ["./**/*.ts", "./types.d.ts"]
}

86
scripts/types.d.ts vendored Normal file
View File

@@ -0,0 +1,86 @@
// Local type declarations for scripts/ — avoids depending on installed packages
// for type checking in build scripts.
// ── esbuild (minimal surface used by build-bundle.ts) ──
declare module 'esbuild' {
export interface Plugin {
name: string
setup(build: PluginBuild): void
}
export interface PluginBuild {
onResolve(
options: { filter: RegExp },
callback: (args: OnResolveArgs) => OnResolveResult | undefined | null,
): void
}
export interface OnResolveArgs {
path: string
importer: string
namespace: string
resolveDir: string
kind: string
pluginData: unknown
}
export interface OnResolveResult {
path?: string
external?: boolean
namespace?: string
pluginData?: unknown
}
export interface BuildOptions {
entryPoints?: string[]
bundle?: boolean
platform?: string
target?: string[]
format?: string
outdir?: string
outExtension?: Record<string, string>
splitting?: boolean
plugins?: Plugin[]
tsconfig?: string
alias?: Record<string, string>
external?: string[]
jsx?: string
sourcemap?: boolean | string
minify?: boolean
treeShaking?: boolean
define?: Record<string, string>
banner?: Record<string, string>
resolveExtensions?: string[]
logLevel?: string
metafile?: boolean
[key: string]: unknown
}
export interface Metafile {
inputs: Record<string, { bytes: number; imports: unknown[] }>
outputs: Record<string, { bytes: number; inputs: unknown[]; exports: string[] }>
}
export interface BuildResult {
errors: { text: string }[]
warnings: { text: string }[]
metafile?: Metafile
}
export interface BuildContext {
watch(): Promise<void>
serve(options?: unknown): Promise<unknown>
rebuild(): Promise<BuildResult>
dispose(): Promise<void>
}
export function build(options: BuildOptions): Promise<BuildResult>
export function context(options: BuildOptions): Promise<BuildContext>
export function analyzeMetafile(metafile: Metafile, options?: { verbose?: boolean }): Promise<string>
}
// ── Bun's ImportMeta extensions ──
interface ImportMeta {
dir: string
dirname: string
}