singularity-forge/src/loader.ts
TÂCHES e93a44d967 feat: add clack-based onboarding wizard and gsd config command (#118)
Replace the plain-text API-key-only wizard with a branded, clack-based
onboarding experience that guides first-launch users through LLM provider
authentication (OAuth or API key), optional tool API keys, and a summary.

- Create src/logo.ts as single source of truth for ASCII logo
- Create src/onboarding.ts with shouldRunOnboarding() and runOnboarding()
- Trim src/wizard.ts to env hydration only (loadStoredEnvKeys)
- Wire onboarding into src/cli.ts, add `gsd config` subcommand
- Remove duplicate first-launch banner from src/loader.ts

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 10:02:00 -06:00

79 lines
4.1 KiB
JavaScript

#!/usr/bin/env node
import { fileURLToPath } from 'url'
import { dirname, resolve, join } from 'path'
import { readFileSync } from 'fs'
import { agentDir } from './app-paths.js'
// pkg/ is a shim directory: contains gsd's piConfig (package.json) and pi's
// theme assets (dist/modes/interactive/theme/) without a src/ directory.
// This allows config.js to:
// 1. Read piConfig.name → "gsd" (branding)
// 2. Resolve themes via dist/ (no src/ present → uses dist path)
const pkgDir = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'pkg')
// MUST be set before any dynamic import of pi SDK fires — this is what config.js
// reads to determine APP_NAME and CONFIG_DIR_NAME
process.env.PI_PACKAGE_DIR = pkgDir
process.env.PI_SKIP_VERSION_CHECK = '1' // GSD ships its own update check — suppress pi's
process.title = 'gsd'
// First-launch branding is handled by the onboarding wizard (src/onboarding.ts)
// GSD_CODING_AGENT_DIR — tells pi's getAgentDir() to return ~/.gsd/agent/ instead of ~/.gsd/agent/
process.env.GSD_CODING_AGENT_DIR = agentDir
// NODE_PATH — make gsd's own node_modules available to extensions loaded via jiti.
// Without this, extensions (e.g. browser-tools) can't resolve dependencies like
// `playwright` because jiti resolves modules from pi-coding-agent's location, not gsd's.
// Prepending gsd's node_modules to NODE_PATH fixes this for all extensions.
const gsdRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const gsdNodeModules = join(gsdRoot, 'node_modules')
process.env.NODE_PATH = process.env.NODE_PATH
? `${gsdNodeModules}:${process.env.NODE_PATH}`
: gsdNodeModules
// Force Node to re-evaluate module search paths with the updated NODE_PATH.
// Must happen synchronously before cli.js imports → extension loading.
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { Module } = await import('module');
(Module as any)._initPaths?.()
// GSD_VERSION — expose package version so extensions can display it
try {
const gsdPkg = JSON.parse(readFileSync(join(gsdRoot, 'package.json'), 'utf-8'))
process.env.GSD_VERSION = gsdPkg.version || '0.0.0'
} catch {
process.env.GSD_VERSION = '0.0.0'
}
// GSD_BIN_PATH — absolute path to this loader (dist/loader.js), used by patched subagent
// to spawn gsd instead of pi when dispatching workflow tasks
process.env.GSD_BIN_PATH = process.argv[1]
// GSD_WORKFLOW_PATH — absolute path to bundled GSD-WORKFLOW.md, used by patched gsd extension
// when dispatching workflow prompts (dist/loader.js → ../src/resources/GSD-WORKFLOW.md)
const resourcesDir = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'src', 'resources')
process.env.GSD_WORKFLOW_PATH = join(resourcesDir, 'GSD-WORKFLOW.md')
// GSD_BUNDLED_EXTENSION_PATHS — colon-joined list of all bundled extension entry point absolute
// paths, used by patched subagent to pass --extension <path> to spawned gsd processes.
// IMPORTANT: paths point to agentDir (~/.gsd/agent/extensions/) NOT src/resources/extensions/.
// initResources() syncs bundled extensions to agentDir before any extension loading occurs,
// so these paths are always valid at runtime. Using agentDir paths matches what buildResourceLoader
// discovers (it scans agentDir), so pi's deduplication works correctly and extensions are not
// double-loaded in subagent child processes.
// Note: shared/ is NOT included — it's a library imported by gsd and ask-user-questions, not an entry point.
process.env.GSD_BUNDLED_EXTENSION_PATHS = [
join(agentDir, 'extensions', 'gsd', 'index.ts'),
join(agentDir, 'extensions', 'bg-shell', 'index.ts'),
join(agentDir, 'extensions', 'browser-tools', 'index.ts'),
join(agentDir, 'extensions', 'context7', 'index.ts'),
join(agentDir, 'extensions', 'search-the-web', 'index.ts'),
join(agentDir, 'extensions', 'slash-commands', 'index.ts'),
join(agentDir, 'extensions', 'subagent', 'index.ts'),
join(agentDir, 'extensions', 'mac-tools', 'index.ts'),
join(agentDir, 'extensions', 'ask-user-questions.ts'),
join(agentDir, 'extensions', 'get-secrets-from-user.ts'),
].join(':')
// Dynamic import defers ESM evaluation — config.js will see PI_PACKAGE_DIR above
await import('./cli.js')