mirror of
https://github.com/codeaashu/claude-code.git
synced 2026-04-08 14:18:50 +03:00
claude-code
This commit is contained in:
197
scripts/build-bundle.ts
Normal file
197
scripts/build-bundle.ts
Normal 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
58
scripts/build-web.ts
Normal 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
58
scripts/build.sh
Normal 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 ──"
|
||||
|
||||
|
||||
18
scripts/bun-plugin-shims.ts
Normal file
18
scripts/bun-plugin-shims.ts
Normal 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
49
scripts/ci-build.sh
Normal 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
15
scripts/dev.ts
Normal 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
91
scripts/package-npm.ts
Normal 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
26
scripts/test-auth.ts
Normal 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
64
scripts/test-commands.ts
Normal 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
180
scripts/test-mcp.ts
Normal 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
233
scripts/test-services.ts
Normal 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
15
scripts/tsconfig.json
Normal 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
86
scripts/types.d.ts
vendored
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user