diff --git a/README.md b/README.md index caf2b92..6fd1a35 100644 --- a/README.md +++ b/README.md @@ -100,11 +100,28 @@ Once the agent is done with the edits, it calls `domscribe.annotation.respond` w --- -## Install +## Getting Started + +```bash +npx domscribe init +``` + +The setup wizard walks you through two steps: + +1. **Connect your coding agent** — select your agent (Claude Code, Copilot, Gemini, Kiro, or others) and the wizard installs the plugin automatically. +2. **Add to your app** — select your framework and bundler, the wizard installs the right package and shows you the config snippet to add. + +That's it. Start your dev server and you're ready to go. + +> Prefer to set things up manually, or need finer control? See the [manual setup](#manual-setup) instructions below. + +--- + +## Manual Setup Domscribe has two sides: **app-side** (bundler + framework plugins) and **agent-side** (MCP for your coding agent). Both are needed for the full workflow. -### Step 1 — Add to Your App +### App-Side — Add to Your Bundler
Next.js (15 + 16)npm install -D @domscribe/next @@ -151,12 +168,34 @@ export default defineConfig({ Webpack plugin: -```ts +```js // webpack.config.js const { DomscribeWebpackPlugin } = require('@domscribe/react/webpack'); +const isDevelopment = process.env.NODE_ENV !== 'production'; + module.exports = { - plugins: [new DomscribeWebpackPlugin()], + module: { + rules: [ + { + test: /\.[jt]sx?$/, + exclude: /node_modules/, + enforce: 'pre', + use: [ + { + loader: '@domscribe/transform/webpack-loader', + options: { enabled: isDevelopment }, + }, + ], + }, + ], + }, + plugins: [ + new DomscribeWebpackPlugin({ + enabled: isDevelopment, + overlay: true, + }), + ], }; ``` @@ -180,12 +219,34 @@ export default defineConfig({ Webpack plugin: -```ts +```js // webpack.config.js const { DomscribeWebpackPlugin } = require('@domscribe/vue/webpack'); +const isDevelopment = process.env.NODE_ENV !== 'production'; + module.exports = { - plugins: [new DomscribeWebpackPlugin()], + module: { + rules: [ + { + test: /\.[jt]sx?$/, + exclude: /node_modules/, + enforce: 'pre', + use: [ + { + loader: '@domscribe/transform/webpack-loader', + options: { enabled: isDevelopment }, + }, + ], + }, + ], + }, + plugins: [ + new DomscribeWebpackPlugin({ + enabled: isDevelopment, + overlay: true, + }), + ], }; ``` @@ -214,8 +275,30 @@ const { DomscribeWebpackPlugin, } = require('@domscribe/transform/plugins/webpack'); +const isDevelopment = process.env.NODE_ENV !== 'production'; + module.exports = { - plugins: [new DomscribeWebpackPlugin()], + module: { + rules: [ + { + test: /\.[jt]sx?$/, + exclude: /node_modules/, + enforce: 'pre', + use: [ + { + loader: '@domscribe/transform/webpack-loader', + options: { enabled: isDevelopment }, + }, + ], + }, + ], + }, + plugins: [ + new DomscribeWebpackPlugin({ + enabled: isDevelopment, + overlay: true, + }), + ], }; ``` @@ -223,17 +306,15 @@ module.exports = { > **Working examples:** See [`packages/domscribe-test-fixtures/fixtures/`](./packages/domscribe-test-fixtures/fixtures/) for complete app setups across every supported framework and bundler combination. -### Step 2 — Connect Your Coding Agent - -Domscribe exposes its full tool surface (12 tools + 4 prompts) via MCP. Agent plugins bundle the MCP config and a skill file that teaches the agent how to use the tools effectively. +### Agent-Side — Connect Your Coding Agent -For agents with first-class plugin support, install the plugin and you're done: +Domscribe exposes 12 tools and 4 prompts via MCP. Agent plugins bundle the MCP config and a skill file that teaches the agent how to use the tools effectively. #### Claude Code ```shell -/plugin marketplace add patchorbit/domscribe -/plugin install domscribe@domscribe +claude plugin marketplace add patchorbit/domscribe +claude plugin install domscribe@domscribe ``` #### GitHub Copilot diff --git a/packages/domscribe-relay/package.json b/packages/domscribe-relay/package.json index b8daf78..aae3e76 100644 --- a/packages/domscribe-relay/package.json +++ b/packages/domscribe-relay/package.json @@ -21,11 +21,13 @@ "domscribe-mcp": "./cli/bin/mcp.js" }, "dependencies": { + "@clack/prompts": "^1.1.0", "@domscribe/core": "workspace:*", "@domscribe/manifest": "workspace:*", "@fastify/cors": "^10.0.0", "@fastify/websocket": "^11.0.0", "@modelcontextprotocol/sdk": "^1.0.0", + "cli-highlight": "^2.1.11", "commander": "^14.0.2", "fastify": "^5.0.0", "fastify-type-provider-zod": "^6.1.0", diff --git a/packages/domscribe-relay/src/cli/commands/init.command.ts b/packages/domscribe-relay/src/cli/commands/init.command.ts index fc59e97..416ab0a 100644 --- a/packages/domscribe-relay/src/cli/commands/init.command.ts +++ b/packages/domscribe-relay/src/cli/commands/init.command.ts @@ -1,25 +1,68 @@ import { Command } from 'commander'; -/** - * Init command options - */ +import { + runInitWizard, + AGENT_IDS, + FRAMEWORK_IDS, + PACKAGE_MANAGER_IDS, +} from '../init/index.js'; +import type { InitOptions } from '../init/index.js'; + interface InitCommandOptions { force: boolean; dryRun: boolean; + agent?: string; + framework?: string; + pm?: string; } export const InitCommand = new Command('init') .description('Initialize Domscribe and configure your coding agent') .option('-f, --force', 'Overwrite existing configuration', false) .option('--dry-run', 'Show what would be done without making changes', false) - .action((options: InitCommandOptions) => { - init(options); - }); + .option('--agent ', `Coding agent (${AGENT_IDS.join(', ')})`) + .option( + '--framework ', + `Framework + bundler (${FRAMEWORK_IDS.join(', ')})`, + ) + .option('--pm ', `Package manager (${PACKAGE_MANAGER_IDS.join(', ')})`) + .action(async (options: InitCommandOptions) => { + try { + if (options.agent && !AGENT_IDS.includes(options.agent as never)) { + console.error( + `[domscribe-cli] Invalid agent: ${options.agent}. Valid options: ${AGENT_IDS.join(', ')}`, + ); + process.exit(1); + } + if ( + options.framework && + !FRAMEWORK_IDS.includes(options.framework as never) + ) { + console.error( + `[domscribe-cli] Invalid framework: ${options.framework}. Valid options: ${FRAMEWORK_IDS.join(', ')}`, + ); + process.exit(1); + } -function init( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _options: InitCommandOptions -): void { - console.error('[domscribe-cli] Not implemented'); -} + if (options.pm && !PACKAGE_MANAGER_IDS.includes(options.pm as never)) { + console.error( + `[domscribe-cli] Invalid package manager: ${options.pm}. Valid options: ${PACKAGE_MANAGER_IDS.join(', ')}`, + ); + process.exit(1); + } + + const initOptions: InitOptions = { + force: options.force, + dryRun: options.dryRun, + agent: options.agent as InitOptions['agent'], + framework: options.framework as InitOptions['framework'], + pm: options.pm as InitOptions['pm'], + }; + + await runInitWizard(initOptions); + } catch (error) { + console.error(`[domscribe-cli] Init failed: ${error}`); + process.exit(1); + } + }); diff --git a/packages/domscribe-relay/src/cli/init/agent-step.spec.ts b/packages/domscribe-relay/src/cli/init/agent-step.spec.ts new file mode 100644 index 0000000..4538dc3 --- /dev/null +++ b/packages/domscribe-relay/src/cli/init/agent-step.spec.ts @@ -0,0 +1,213 @@ +import { execFileSync, spawnSync } from 'node:child_process'; + +import * as clack from '@clack/prompts'; + +import { runAgentStep } from './agent-step.js'; +import type { InitOptions } from './types.js'; + +vi.mock('node:child_process', () => ({ + execFileSync: vi.fn(), + spawnSync: vi.fn().mockReturnValue({ status: 0 }), +})); + +vi.mock('@clack/prompts', () => ({ + select: vi.fn(), + isCancel: vi.fn().mockReturnValue(false), + cancel: vi.fn(), + log: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + success: vi.fn(), + message: vi.fn(), + }, + note: vi.fn(), +})); + +const baseOptions: InitOptions = { + force: false, + dryRun: false, +}; + +describe('runAgentStep', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(clack.isCancel).mockReturnValue(false); + vi.mocked(spawnSync).mockReturnValue({ status: 0 } as ReturnType< + typeof spawnSync + >); + }); + + describe('interactive mode', () => { + it('should prompt for agent selection when no --agent flag', async () => { + // Arrange + vi.mocked(clack.select).mockResolvedValue('kiro'); + + // Act + await runAgentStep(baseOptions); + + // Assert + expect(clack.select).toHaveBeenCalledWith( + expect.objectContaining({ message: 'Select your coding agent:' }), + ); + }); + + it('should exit gracefully on cancel', async () => { + // Arrange + vi.mocked(clack.select).mockResolvedValue(Symbol('cancel')); + vi.mocked(clack.isCancel).mockReturnValue(true); + const exitSpy = vi + .spyOn(process, 'exit') + .mockImplementation(() => undefined as never); + + // Act + await runAgentStep(baseOptions); + + // Assert + expect(clack.cancel).toHaveBeenCalledWith('Setup cancelled.'); + expect(exitSpy).toHaveBeenCalledWith(0); + }); + }); + + describe('non-interactive mode', () => { + it('should skip prompt when --agent flag is provided', async () => { + // Arrange — kiro is manual, so no spawn + const options: InitOptions = { ...baseOptions, agent: 'kiro' }; + + // Act + await runAgentStep(options); + + // Assert + expect(clack.select).not.toHaveBeenCalled(); + expect(clack.log.info).toHaveBeenCalledWith('Amazon Kiro'); + }); + }); + + describe('manual agents', () => { + it('should show manual instructions for Kiro', async () => { + // Arrange + vi.mocked(clack.select).mockResolvedValue('kiro'); + const writeSpy = vi.spyOn(process.stdout, 'write').mockReturnValue(true); + + // Act + await runAgentStep(baseOptions); + + // Assert + expect(writeSpy).toHaveBeenCalledWith( + expect.stringContaining('Powers panel'), + ); + }); + + it('should show MCP config for Cursor', async () => { + // Arrange + vi.mocked(clack.select).mockResolvedValue('cursor'); + const writeSpy = vi.spyOn(process.stdout, 'write').mockReturnValue(true); + + // Act + await runAgentStep(baseOptions); + + // Assert + expect(writeSpy).toHaveBeenCalledWith( + expect.stringContaining('coming soon'), + ); + }); + + it('should show MCP config for Other', async () => { + // Arrange + vi.mocked(clack.select).mockResolvedValue('other'); + const writeSpy = vi.spyOn(process.stdout, 'write').mockReturnValue(true); + + // Act + await runAgentStep(baseOptions); + + // Assert + expect(writeSpy).toHaveBeenCalledWith( + expect.stringContaining('mcpServers'), + ); + }); + }); + + describe('command-based agents', () => { + it('should run install commands for Claude Code', async () => { + // Arrange + vi.mocked(clack.select).mockResolvedValue('claude-code'); + vi.mocked(execFileSync).mockReturnValue(Buffer.from('')); + vi.mocked(spawnSync).mockReturnValue({ status: 0 } as ReturnType< + typeof spawnSync + >); + + // Act + await runAgentStep(baseOptions); + + // Assert + expect(spawnSync).toHaveBeenCalledTimes(2); + expect(spawnSync).toHaveBeenCalledWith( + 'claude', + ['plugin', 'marketplace', 'add', 'patchorbit/domscribe'], + { stdio: 'inherit' }, + ); + expect(spawnSync).toHaveBeenCalledWith( + 'claude', + ['plugin', 'install', 'domscribe@domscribe'], + { stdio: 'inherit' }, + ); + expect(clack.log.success).toHaveBeenCalled(); + }); + + it('should fall back to manual when CLI is not on PATH', async () => { + // Arrange + vi.mocked(clack.select).mockResolvedValue('copilot'); + vi.mocked(execFileSync).mockImplementation(() => { + throw new Error('not found'); + }); + + // Act + await runAgentStep(baseOptions); + + // Assert + expect(spawnSync).not.toHaveBeenCalled(); + expect(clack.log.warn).toHaveBeenCalledWith( + expect.stringContaining('not installed'), + ); + expect(clack.log.info).toHaveBeenCalledWith( + expect.stringContaining('Install the GitHub Copilot CLI'), + ); + }); + + it('should warn on command failure and stop', async () => { + // Arrange + vi.mocked(clack.select).mockResolvedValue('claude-code'); + vi.mocked(execFileSync).mockReturnValue(Buffer.from('')); + vi.mocked(spawnSync).mockReturnValue({ status: 1 } as ReturnType< + typeof spawnSync + >); + + // Act + await runAgentStep(baseOptions); + + // Assert + expect(spawnSync).toHaveBeenCalledTimes(1); + expect(clack.log.warn).toHaveBeenCalledWith( + expect.stringContaining('failed'), + ); + }); + }); + + describe('dry-run', () => { + it('should print commands without executing them', async () => { + // Arrange + vi.mocked(clack.select).mockResolvedValue('claude-code'); + const options: InitOptions = { ...baseOptions, dryRun: true }; + + // Act + await runAgentStep(options); + + // Assert + expect(spawnSync).not.toHaveBeenCalled(); + expect(clack.log.info).toHaveBeenCalledWith('Would run:'); + expect(clack.log.message).toHaveBeenCalledWith( + expect.stringContaining('claude plugin marketplace add'), + ); + }); + }); +}); diff --git a/packages/domscribe-relay/src/cli/init/agent-step.ts b/packages/domscribe-relay/src/cli/init/agent-step.ts new file mode 100644 index 0000000..a09caae --- /dev/null +++ b/packages/domscribe-relay/src/cli/init/agent-step.ts @@ -0,0 +1,116 @@ +/** + * Agent selection and installation step for the init wizard. + * @module @domscribe/relay/cli/init/agent-step + */ +import { execFileSync, spawnSync } from 'node:child_process'; + +import * as clack from '@clack/prompts'; + +import type { AgentConfig, InitOptions } from './types.js'; +import { AGENTS } from './types.js'; + +/** + * Check if a CLI binary exists on the system PATH. + */ +function isBinaryAvailable(binary: string): boolean { + try { + execFileSync('which', [binary], { stdio: 'ignore' }); + return true; + } catch { + return false; + } +} + +/** + * Execute a shell command with inherited stdio. + * Returns true if the command succeeded. + */ +function runCommand(command: string): boolean { + const [bin, ...args] = command.split(' '); + const result = spawnSync(bin, args, { stdio: 'inherit' }); + return result.status === 0; +} + +/** + * Get the primary binary name from the first command of an agent config. + */ +function getAgentBinary(agent: AgentConfig): string | undefined { + if (!agent.commands?.length) return undefined; + return agent.commands[0].split(' ')[0]; +} + +/** + * Run the agent selection and installation step. + */ +export async function runAgentStep(options: InitOptions): Promise { + let agentId = options.agent; + + if (!agentId) { + const selected = await clack.select({ + message: 'Select your coding agent:', + options: AGENTS.map((a) => ({ + value: a.id, + label: a.label, + hint: a.hint, + })), + }); + + if (clack.isCancel(selected)) { + clack.cancel('Setup cancelled.'); + return process.exit(0); + } + + agentId = selected; + } + + const agent = AGENTS.find((a) => a.id === agentId); + if (!agent) { + clack.log.error(`Unknown agent: ${agentId}`); + return; + } + + if (agent.installType === 'manual') { + if (agent.manualInstructions) { + clack.log.info(agent.label); + process.stdout.write(agent.manualInstructions + '\n\n'); + } + return; + } + + if (!agent.commands?.length) return; + + // Command-based install + const binary = getAgentBinary(agent); + + if (options.dryRun) { + clack.log.info(`Would run:`); + for (const cmd of agent.commands) { + clack.log.message(` ${cmd}`); + } + return; + } + + if (binary && !isBinaryAvailable(binary)) { + clack.log.warn( + `${binary} is not installed or not on PATH. Showing manual setup instead.`, + ); + clack.log.info(`Install the ${agent.label} CLI, then run:\n`); + process.stdout.write( + agent.commands.map((c) => ` ${c}`).join('\n') + '\n\n', + ); + return; + } + + for (const cmd of agent.commands) { + clack.log.info(`Running: ${cmd}`); + const success = runCommand(cmd); + if (!success) { + clack.log.warn( + `Command failed: ${cmd}\nYou can run it manually after setup.`, + ); + return; + } + } + + clack.log.success(`${agent.label} plugin installed.`); +} diff --git a/packages/domscribe-relay/src/cli/init/detect-package-manager.spec.ts b/packages/domscribe-relay/src/cli/init/detect-package-manager.spec.ts new file mode 100644 index 0000000..10a035b --- /dev/null +++ b/packages/domscribe-relay/src/cli/init/detect-package-manager.spec.ts @@ -0,0 +1,103 @@ +import { existsSync } from 'node:fs'; + +import { detectPackageManager } from './detect-package-manager.js'; + +vi.mock('node:fs', () => ({ + existsSync: vi.fn(), +})); + +describe('detectPackageManager', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should detect pnpm from pnpm-lock.yaml', () => { + // Arrange + vi.mocked(existsSync).mockImplementation((p) => + String(p).endsWith('pnpm-lock.yaml'), + ); + + // Act + const result = detectPackageManager('/project'); + + // Assert + expect(result).toBe('pnpm'); + }); + + it('should detect yarn from yarn.lock', () => { + // Arrange + vi.mocked(existsSync).mockImplementation((p) => + String(p).endsWith('yarn.lock'), + ); + + // Act + const result = detectPackageManager('/project'); + + // Assert + expect(result).toBe('yarn'); + }); + + it('should detect bun from bun.lock', () => { + // Arrange + vi.mocked(existsSync).mockImplementation((p) => + String(p).endsWith('bun.lock'), + ); + + // Act + const result = detectPackageManager('/project'); + + // Assert + expect(result).toBe('bun'); + }); + + it('should detect bun from bun.lockb', () => { + // Arrange + vi.mocked(existsSync).mockImplementation((p) => + String(p).endsWith('bun.lockb'), + ); + + // Act + const result = detectPackageManager('/project'); + + // Assert + expect(result).toBe('bun'); + }); + + it('should detect npm from package-lock.json', () => { + // Arrange + vi.mocked(existsSync).mockImplementation((p) => + String(p).endsWith('package-lock.json'), + ); + + // Act + const result = detectPackageManager('/project'); + + // Assert + expect(result).toBe('npm'); + }); + + it('should fall back to npm when no lockfile is found', () => { + // Arrange + vi.mocked(existsSync).mockReturnValue(false); + + // Act + const result = detectPackageManager('/project'); + + // Assert + expect(result).toBe('npm'); + }); + + it('should prioritize pnpm over yarn when both lockfiles exist', () => { + // Arrange + vi.mocked(existsSync).mockImplementation((p) => { + const s = String(p); + return s.endsWith('pnpm-lock.yaml') || s.endsWith('yarn.lock'); + }); + + // Act + const result = detectPackageManager('/project'); + + // Assert + expect(result).toBe('pnpm'); + }); +}); diff --git a/packages/domscribe-relay/src/cli/init/detect-package-manager.ts b/packages/domscribe-relay/src/cli/init/detect-package-manager.ts new file mode 100644 index 0000000..86c3b00 --- /dev/null +++ b/packages/domscribe-relay/src/cli/init/detect-package-manager.ts @@ -0,0 +1,24 @@ +/** + * Package manager detection from lockfiles. + * @module @domscribe/relay/cli/init/detect-package-manager + */ +import { existsSync } from 'node:fs'; +import path from 'node:path'; + +import type { PackageManagerId } from './types.js'; + +/** + * Detect the package manager by checking for lockfiles in the given directory. + * Falls back to npm if no lockfile is found. + */ +export function detectPackageManager(cwd: string): PackageManagerId { + if (existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'; + if (existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn'; + if ( + existsSync(path.join(cwd, 'bun.lock')) || + existsSync(path.join(cwd, 'bun.lockb')) + ) + return 'bun'; + + return 'npm'; +} diff --git a/packages/domscribe-relay/src/cli/init/framework-step.spec.ts b/packages/domscribe-relay/src/cli/init/framework-step.spec.ts new file mode 100644 index 0000000..836fb3c --- /dev/null +++ b/packages/domscribe-relay/src/cli/init/framework-step.spec.ts @@ -0,0 +1,236 @@ +import { spawnSync } from 'node:child_process'; + +import * as clack from '@clack/prompts'; + +import { runFrameworkStep } from './framework-step.js'; +import type { InitOptions } from './types.js'; + +vi.mock('node:child_process', () => ({ + spawnSync: vi.fn().mockReturnValue({ status: 0 }), +})); + +vi.mock('@clack/prompts', () => ({ + select: vi.fn(), + isCancel: vi.fn().mockReturnValue(false), + cancel: vi.fn(), + spinner: vi.fn().mockReturnValue({ + start: vi.fn(), + stop: vi.fn(), + }), + log: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + success: vi.fn(), + message: vi.fn(), + }, + note: vi.fn(), +})); + +vi.mock('./detect-package-manager.js', () => ({ + detectPackageManager: vi.fn().mockReturnValue('npm'), +})); + +const baseOptions: InitOptions = { + force: false, + dryRun: false, +}; + +describe('runFrameworkStep', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(clack.isCancel).mockReturnValue(false); + vi.mocked(spawnSync).mockReturnValue({ status: 0 } as ReturnType< + typeof spawnSync + >); + }); + + describe('interactive mode', () => { + it('should prompt for framework selection', async () => { + // Arrange + vi.mocked(clack.select) + .mockResolvedValueOnce('next') + .mockResolvedValueOnce('npm'); + + // Act + await runFrameworkStep(baseOptions, '/project'); + + // Assert + expect(clack.select).toHaveBeenCalledWith( + expect.objectContaining({ message: 'Select your framework:' }), + ); + }); + + it('should prompt to confirm detected package manager', async () => { + // Arrange + vi.mocked(clack.select) + .mockResolvedValueOnce('next') + .mockResolvedValueOnce('pnpm'); + + // Act + await runFrameworkStep(baseOptions, '/project'); + + // Assert + expect(clack.select).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.stringContaining('Detected npm'), + }), + ); + }); + + it('should exit gracefully on framework cancel', async () => { + // Arrange + vi.mocked(clack.select).mockResolvedValueOnce(Symbol('cancel')); + vi.mocked(clack.isCancel).mockReturnValueOnce(true); + const exitSpy = vi + .spyOn(process, 'exit') + .mockImplementation(() => undefined as never); + + // Act + await runFrameworkStep(baseOptions, '/project'); + + // Assert + expect(clack.cancel).toHaveBeenCalledWith('Setup cancelled.'); + expect(exitSpy).toHaveBeenCalledWith(0); + }); + }); + + describe('non-interactive mode', () => { + it('should skip prompts when --framework and --pm flags are provided', async () => { + // Arrange + const options: InitOptions = { + ...baseOptions, + framework: 'nuxt', + pm: 'pnpm', + }; + + // Act + await runFrameworkStep(options, '/project'); + + // Assert + expect(clack.select).not.toHaveBeenCalled(); + expect(spawnSync).toHaveBeenCalledWith( + 'pnpm', + ['add', '-D', '@domscribe/nuxt'], + expect.objectContaining({ cwd: '/project' }), + ); + }); + }); + + describe('package installation', () => { + it('should install the correct package for Next.js with npm', async () => { + // Arrange + vi.mocked(clack.select) + .mockResolvedValueOnce('next') + .mockResolvedValueOnce('npm'); + + // Act + await runFrameworkStep(baseOptions, '/project'); + + // Assert + expect(spawnSync).toHaveBeenCalledWith( + 'npm', + ['install', '-D', '@domscribe/next'], + expect.objectContaining({ cwd: '/project' }), + ); + }); + + it('should install the correct package for React + Vite with pnpm', async () => { + // Arrange + vi.mocked(clack.select) + .mockResolvedValueOnce('react-vite') + .mockResolvedValueOnce('pnpm'); + + // Act + await runFrameworkStep(baseOptions, '/project'); + + // Assert + expect(spawnSync).toHaveBeenCalledWith( + 'pnpm', + ['add', '-D', '@domscribe/react'], + expect.objectContaining({ cwd: '/project' }), + ); + }); + + it('should warn on install failure and show manual command', async () => { + // Arrange + vi.mocked(clack.select) + .mockResolvedValueOnce('nuxt') + .mockResolvedValueOnce('npm'); + vi.mocked(spawnSync).mockReturnValue({ + status: 1, + stderr: Buffer.from('ERR'), + } as unknown as ReturnType); + + // Act + await runFrameworkStep(baseOptions, '/project'); + + // Assert + expect(clack.log.warn).toHaveBeenCalledWith( + expect.stringContaining('npm install -D'), + ); + }); + }); + + describe('config snippet', () => { + it('should show config snippet after installation', async () => { + // Arrange + vi.mocked(clack.select) + .mockResolvedValueOnce('next') + .mockResolvedValueOnce('npm'); + const writeSpy = vi.spyOn(process.stdout, 'write').mockReturnValue(true); + + // Act + await runFrameworkStep(baseOptions, '/project'); + + // Assert + expect(clack.log.warn).toHaveBeenCalledWith( + expect.stringContaining('next.config.ts'), + ); + expect(writeSpy).toHaveBeenCalledWith( + expect.stringContaining('withDomscribe'), + ); + }); + + it('should show webpack loader + plugin for React + Webpack', async () => { + // Arrange + vi.mocked(clack.select) + .mockResolvedValueOnce('react-webpack') + .mockResolvedValueOnce('npm'); + const writeSpy = vi.spyOn(process.stdout, 'write').mockReturnValue(true); + + // Act + await runFrameworkStep(baseOptions, '/project'); + + // Assert + expect(writeSpy).toHaveBeenCalledWith( + expect.stringContaining('webpack-loader'), + ); + expect(writeSpy).toHaveBeenCalledWith( + expect.stringContaining('DomscribeWebpackPlugin'), + ); + }); + }); + + describe('dry-run', () => { + it('should print install command and snippet without executing', async () => { + // Arrange + vi.mocked(clack.select) + .mockResolvedValueOnce('nuxt') + .mockResolvedValueOnce('npm'); + const options: InitOptions = { ...baseOptions, dryRun: true }; + + // Act + await runFrameworkStep(options, '/project'); + + // Assert + expect(spawnSync).not.toHaveBeenCalled(); + expect(clack.log.info).toHaveBeenCalledWith( + expect.stringContaining('npm install -D @domscribe/nuxt'), + ); + expect(clack.log.warn).toHaveBeenCalledWith( + expect.stringContaining('to complete setup'), + ); + }); + }); +}); diff --git a/packages/domscribe-relay/src/cli/init/framework-step.ts b/packages/domscribe-relay/src/cli/init/framework-step.ts new file mode 100644 index 0000000..d62a638 --- /dev/null +++ b/packages/domscribe-relay/src/cli/init/framework-step.ts @@ -0,0 +1,145 @@ +/** + * Framework selection, package installation, and config snippet step. + * @module @domscribe/relay/cli/init/framework-step + */ +import { spawnSync } from 'node:child_process'; + +import * as clack from '@clack/prompts'; +import { highlight } from 'cli-highlight'; + +import { detectPackageManager } from './detect-package-manager.js'; +import { CONFIG_SNIPPETS } from './snippets.js'; +import type { + FrameworkConfig, + InitOptions, + PackageManagerId, +} from './types.js'; +import { FRAMEWORKS, PACKAGE_MANAGERS } from './types.js'; + +/** + * Syntax-highlight a config snippet for terminal display. + */ +function highlightSnippet(code: string, configFile: string): string { + const language = configFile.endsWith('.ts') ? 'typescript' : 'javascript'; + try { + return highlight(code, { language }); + } catch { + return code; + } +} + +/** + * Display the config snippet with contextual instructions. + */ +function showConfigSnippet(framework: FrameworkConfig): void { + const snippet = CONFIG_SNIPPETS[framework.id]; + const highlighted = highlightSnippet(snippet, framework.configFile); + + clack.log.warn( + `Add the following to your ${framework.configFile} to complete setup:\n`, + ); + process.stdout.write(highlighted + '\n\n'); +} + +/** + * Build the install command string for a given package manager and package. + */ +function buildInstallCommand(pm: PackageManagerId, pkg: string): string { + const pmConfig = PACKAGE_MANAGERS.find((p) => p.id === pm); + return `${pmConfig?.installCmd ?? 'npm install -D'} ${pkg}`; +} + +/** + * Prompt the user to confirm or override the detected package manager. + */ +async function confirmPackageManager( + detected: PackageManagerId, + options: InitOptions, +): Promise { + if (options.pm) return options.pm; + + // Build options with detected PM first, marked as "(detected)" + const pmOptions = PACKAGE_MANAGERS.map((pm) => ({ + value: pm.id, + label: pm.id === detected ? `${pm.label} (detected)` : pm.label, + })); + + const selected = await clack.select({ + message: `Detected ${detected} — which package manager should we use?`, + options: pmOptions, + initialValue: detected, + }); + + if (clack.isCancel(selected)) { + clack.cancel('Setup cancelled.'); + return process.exit(0) as never; + } + + return selected; +} + +/** + * Run the framework selection, package installation, and config snippet step. + */ +export async function runFrameworkStep( + options: InitOptions, + cwd: string, +): Promise { + let frameworkId = options.framework; + + if (!frameworkId) { + const selected = await clack.select({ + message: 'Select your framework:', + options: FRAMEWORKS.map((f) => ({ + value: f.id, + label: f.label, + })), + }); + + if (clack.isCancel(selected)) { + clack.cancel('Setup cancelled.'); + return process.exit(0); + } + + frameworkId = selected; + } + + const framework = FRAMEWORKS.find((f) => f.id === frameworkId); + if (!framework) { + clack.log.error(`Unknown framework: ${frameworkId}`); + return; + } + + // Detect and confirm package manager + const detected = detectPackageManager(cwd); + const pm = await confirmPackageManager(detected, options); + + const installCmd = buildInstallCommand(pm, framework.package); + + if (options.dryRun) { + clack.log.info(`Would run: ${installCmd}`); + showConfigSnippet(framework); + return; + } + + // Run the install + const spinner = clack.spinner(); + spinner.start(`Installing ${framework.package}...`); + + const [bin, ...args] = installCmd.split(' '); + const result = spawnSync(bin, args, { stdio: 'pipe', cwd }); + + if (result.status !== 0) { + spinner.stop(`Failed to install ${framework.package}.`); + const stderr = result.stderr?.toString().trim(); + if (stderr) { + clack.log.warn(stderr); + } + clack.log.warn(`You can install manually: ${installCmd}`); + } else { + spinner.stop(`Installed ${framework.package}.`); + } + + // Always show the config snippet + showConfigSnippet(framework); +} diff --git a/packages/domscribe-relay/src/cli/init/index.ts b/packages/domscribe-relay/src/cli/init/index.ts new file mode 100644 index 0000000..20ca2ac --- /dev/null +++ b/packages/domscribe-relay/src/cli/init/index.ts @@ -0,0 +1,7 @@ +/** + * Init wizard public API. + * @module @domscribe/relay/cli/init + */ +export { runInitWizard } from './init-wizard.js'; +export type { InitOptions } from './types.js'; +export { AGENT_IDS, FRAMEWORK_IDS, PACKAGE_MANAGER_IDS } from './types.js'; diff --git a/packages/domscribe-relay/src/cli/init/init-wizard.spec.ts b/packages/domscribe-relay/src/cli/init/init-wizard.spec.ts new file mode 100644 index 0000000..650b5fb --- /dev/null +++ b/packages/domscribe-relay/src/cli/init/init-wizard.spec.ts @@ -0,0 +1,78 @@ +import * as clack from '@clack/prompts'; + +import { runInitWizard } from './init-wizard.js'; +import type { InitOptions } from './types.js'; + +vi.mock('@clack/prompts', () => ({ + intro: vi.fn(), + outro: vi.fn(), +})); + +vi.mock('./agent-step.js', () => ({ + runAgentStep: vi.fn().mockResolvedValue(undefined), +})); + +vi.mock('./framework-step.js', () => ({ + runFrameworkStep: vi.fn().mockResolvedValue(undefined), +})); + +const baseOptions: InitOptions = { + force: false, + dryRun: false, +}; + +describe('runInitWizard', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should show intro and outro', async () => { + // Act + await runInitWizard(baseOptions); + + // Assert + expect(clack.intro).toHaveBeenCalledWith('Domscribe Setup'); + expect(clack.outro).toHaveBeenCalledWith( + expect.stringContaining('Domscribe will take care of the rest'), + ); + }); + + it('should call agent step then framework step in order', async () => { + // Arrange + const callOrder: string[] = []; + const { runAgentStep } = await import('./agent-step.js'); + const { runFrameworkStep } = await import('./framework-step.js'); + vi.mocked(runAgentStep).mockImplementation(async () => { + callOrder.push('agent'); + }); + vi.mocked(runFrameworkStep).mockImplementation(async () => { + callOrder.push('framework'); + }); + + // Act + await runInitWizard(baseOptions); + + // Assert + expect(callOrder).toEqual(['agent', 'framework']); + }); + + it('should pass options through to both steps', async () => { + // Arrange + const options: InitOptions = { + force: true, + dryRun: true, + agent: 'claude-code', + framework: 'next', + pm: 'pnpm', + }; + const { runAgentStep } = await import('./agent-step.js'); + const { runFrameworkStep } = await import('./framework-step.js'); + + // Act + await runInitWizard(options); + + // Assert + expect(runAgentStep).toHaveBeenCalledWith(options); + expect(runFrameworkStep).toHaveBeenCalledWith(options, process.cwd()); + }); +}); diff --git a/packages/domscribe-relay/src/cli/init/init-wizard.ts b/packages/domscribe-relay/src/cli/init/init-wizard.ts new file mode 100644 index 0000000..74136a0 --- /dev/null +++ b/packages/domscribe-relay/src/cli/init/init-wizard.ts @@ -0,0 +1,28 @@ +/** + * Init wizard orchestrator. + * @module @domscribe/relay/cli/init/init-wizard + */ +import * as clack from '@clack/prompts'; + +import { runAgentStep } from './agent-step.js'; +import { runFrameworkStep } from './framework-step.js'; +import type { InitOptions } from './types.js'; + +/** + * Run the full init wizard: agent selection → framework selection. + * + * @remarks + * The `.domscribe/` directory is NOT created here — it is created + * automatically by the relay when the dev server first starts + * (via `autoStart: true` in the bundler plugin). + */ +export async function runInitWizard(options: InitOptions): Promise { + clack.intro('Domscribe Setup'); + + await runAgentStep(options); + await runFrameworkStep(options, process.cwd()); + + clack.outro( + 'Add the config above to your project, then start your dev server — Domscribe will take care of the rest.', + ); +} diff --git a/packages/domscribe-relay/src/cli/init/snippets.ts b/packages/domscribe-relay/src/cli/init/snippets.ts new file mode 100644 index 0000000..415375e --- /dev/null +++ b/packages/domscribe-relay/src/cli/init/snippets.ts @@ -0,0 +1,134 @@ +/** + * Config snippets displayed to the user after framework selection. + * @module @domscribe/relay/cli/init/snippets + */ +import type { FrameworkId } from './types.js'; + +/** + * Config snippet for each framework + bundler combination. + * Displayed with syntax highlighting after package installation. + */ +export const CONFIG_SNIPPETS: Record = { + next: `import type { NextConfig } from 'next'; +import { withDomscribe } from '@domscribe/next'; + +const nextConfig: NextConfig = {}; + +export default withDomscribe()(nextConfig);`, + + nuxt: `export default defineNuxtConfig({ + modules: ['@domscribe/nuxt'], +});`, + + 'react-vite': `import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { domscribe } from '@domscribe/react/vite'; + +export default defineConfig({ + plugins: [react(), domscribe()], +});`, + + 'react-webpack': `const { DomscribeWebpackPlugin } = require('@domscribe/react/webpack'); + +const isDevelopment = process.env.NODE_ENV !== 'production'; + +module.exports = { + module: { + rules: [ + { + test: /\\.[jt]sx?$/, + exclude: /node_modules/, + enforce: 'pre', + use: [ + { + loader: '@domscribe/transform/webpack-loader', + options: { enabled: isDevelopment }, + }, + ], + }, + // ... your other loaders + ], + }, + plugins: [ + new DomscribeWebpackPlugin({ + enabled: isDevelopment, + overlay: true, + }), + ], +};`, + + 'vue-vite': `import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import { domscribe } from '@domscribe/vue/vite'; + +export default defineConfig({ + plugins: [vue(), domscribe()], +});`, + + 'vue-webpack': `const { DomscribeWebpackPlugin } = require('@domscribe/vue/webpack'); + +const isDevelopment = process.env.NODE_ENV !== 'production'; + +module.exports = { + module: { + rules: [ + { + test: /\\.[jt]sx?$/, + exclude: /node_modules/, + enforce: 'pre', + use: [ + { + loader: '@domscribe/transform/webpack-loader', + options: { enabled: isDevelopment }, + }, + ], + }, + // ... your other loaders + ], + }, + plugins: [ + new DomscribeWebpackPlugin({ + enabled: isDevelopment, + overlay: true, + }), + ], +};`, + + 'other-vite': `import { defineConfig } from 'vite'; +import { domscribe } from '@domscribe/transform/plugins/vite'; + +export default defineConfig({ + plugins: [domscribe()], +});`, + + 'other-webpack': `const { + DomscribeWebpackPlugin, +} = require('@domscribe/transform/plugins/webpack'); + +const isDevelopment = process.env.NODE_ENV !== 'production'; + +module.exports = { + module: { + rules: [ + { + test: /\\.[jt]sx?$/, + exclude: /node_modules/, + enforce: 'pre', + use: [ + { + loader: '@domscribe/transform/webpack-loader', + options: { enabled: isDevelopment }, + }, + ], + }, + // ... your other loaders + ], + }, + plugins: [ + new DomscribeWebpackPlugin({ + enabled: isDevelopment, + overlay: true, + }), + ], +};`, +}; diff --git a/packages/domscribe-relay/src/cli/init/types.ts b/packages/domscribe-relay/src/cli/init/types.ts new file mode 100644 index 0000000..dc8d4c4 --- /dev/null +++ b/packages/domscribe-relay/src/cli/init/types.ts @@ -0,0 +1,218 @@ +/** + * Types and data definitions for the init wizard. + * @module @domscribe/relay/cli/init/types + */ + +/** + * Supported coding agent identifiers. + */ +export type AgentId = + | 'claude-code' + | 'copilot' + | 'gemini' + | 'kiro' + | 'cursor' + | 'other'; + +/** + * Framework + bundler combination identifiers. + */ +export type FrameworkId = + | 'next' + | 'nuxt' + | 'react-vite' + | 'react-webpack' + | 'vue-vite' + | 'vue-webpack' + | 'other-vite' + | 'other-webpack'; + +/** + * Supported package manager identifiers. + */ +export type PackageManagerId = 'npm' | 'pnpm' | 'yarn' | 'bun'; + +/** + * Agent install strategy. + */ +export type AgentInstallType = 'command' | 'manual'; + +/** + * Configuration for a coding agent in the wizard. + */ +export interface AgentConfig { + readonly id: AgentId; + readonly label: string; + readonly hint?: string; + readonly installType: AgentInstallType; + /** Shell commands to run sequentially (for installType: 'command'). */ + readonly commands?: readonly string[]; + /** Text to display for manual installs. */ + readonly manualInstructions?: string; +} + +/** + * Configuration for a framework + bundler choice in the wizard. + */ +export interface FrameworkConfig { + readonly id: FrameworkId; + readonly label: string; + readonly package: string; + readonly configFile: string; +} + +/** + * Options passed to the init wizard. + */ +export interface InitOptions { + readonly force: boolean; + readonly dryRun: boolean; + readonly agent?: AgentId; + readonly framework?: FrameworkId; + readonly pm?: PackageManagerId; +} + +const MCP_CONFIG = `{ + "mcpServers": { + "domscribe": { + "type": "stdio", + "command": "npx", + "args": ["-y", "@domscribe/mcp"] + } + } +}`; + +/** + * All supported coding agents. + */ +export const AGENTS: readonly AgentConfig[] = [ + { + id: 'claude-code', + label: 'Claude Code', + installType: 'command', + commands: [ + 'claude plugin marketplace add patchorbit/domscribe', + 'claude plugin install domscribe@domscribe', + ], + }, + { + id: 'copilot', + label: 'GitHub Copilot', + installType: 'command', + commands: ['copilot plugin install patchorbit/domscribe'], + }, + { + id: 'gemini', + label: 'Gemini CLI', + installType: 'command', + commands: [ + 'gemini extensions install https://github.com/patchorbit/domscribe', + ], + }, + { + id: 'kiro', + label: 'Amazon Kiro', + installType: 'manual', + manualInstructions: + 'Open the Powers panel → Add power from GitHub → enter https://github.com/patchorbit/domscribe', + }, + { + id: 'cursor', + label: 'Cursor', + hint: 'coming soon', + installType: 'manual', + manualInstructions: `Cursor plugin support is coming soon.\n\nFor now, add this MCP config to .cursor/mcp.json:\n\n${MCP_CONFIG}`, + }, + { + id: 'other', + label: 'Other (manual MCP setup)', + installType: 'manual', + manualInstructions: `Run:\n npx skills add patchorbit/domscribe\n\nThen add this MCP config to your agent:\n\n${MCP_CONFIG}`, + }, +] as const; + +/** + * All supported framework + bundler combinations. + */ +export const FRAMEWORKS: readonly FrameworkConfig[] = [ + { + id: 'next', + label: 'Next.js', + package: '@domscribe/next', + configFile: 'next.config.ts', + }, + { + id: 'nuxt', + label: 'Nuxt', + package: '@domscribe/nuxt', + configFile: 'nuxt.config.ts', + }, + { + id: 'react-vite', + label: 'React + Vite', + package: '@domscribe/react', + configFile: 'vite.config.ts', + }, + { + id: 'react-webpack', + label: 'React + Webpack', + package: '@domscribe/react', + configFile: 'webpack.config.js', + }, + { + id: 'vue-vite', + label: 'Vue + Vite', + package: '@domscribe/vue', + configFile: 'vite.config.ts', + }, + { + id: 'vue-webpack', + label: 'Vue + Webpack', + package: '@domscribe/vue', + configFile: 'webpack.config.js', + }, + { + id: 'other-vite', + label: 'Other (Vite)', + package: '@domscribe/transform', + configFile: 'vite.config.ts', + }, + { + id: 'other-webpack', + label: 'Other (Webpack)', + package: '@domscribe/transform', + configFile: 'webpack.config.js', + }, +] as const; + +/** + * All supported package managers. + */ +export const PACKAGE_MANAGERS: readonly { + readonly id: PackageManagerId; + readonly label: string; + readonly installCmd: string; +}[] = [ + { id: 'npm', label: 'npm', installCmd: 'npm install -D' }, + { id: 'pnpm', label: 'pnpm', installCmd: 'pnpm add -D' }, + { id: 'yarn', label: 'yarn', installCmd: 'yarn add -D' }, + { id: 'bun', label: 'bun', installCmd: 'bun add -D' }, +] as const; + +/** + * Valid agent IDs for flag validation. + */ +export const AGENT_IDS: readonly AgentId[] = AGENTS.map((a) => a.id); + +/** + * Valid framework IDs for flag validation. + */ +export const FRAMEWORK_IDS: readonly FrameworkId[] = FRAMEWORKS.map( + (f) => f.id, +); + +/** + * Valid package manager IDs for flag validation. + */ +export const PACKAGE_MANAGER_IDS: readonly PackageManagerId[] = + PACKAGE_MANAGERS.map((p) => p.id); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95e757d..125df14 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -234,6 +234,9 @@ importers: packages/domscribe-relay: dependencies: + '@clack/prompts': + specifier: ^1.1.0 + version: 1.1.0 '@domscribe/core': specifier: workspace:* version: link:../domscribe-core @@ -249,6 +252,9 @@ importers: '@modelcontextprotocol/sdk': specifier: ^1.0.0 version: 1.25.1(hono@4.11.3)(zod@4.3.6) + cli-highlight: + specifier: ^2.1.11 + version: 2.1.11 commander: specifier: ^14.0.2 version: 14.0.2 @@ -3253,6 +3259,9 @@ packages: resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} engines: {node: '>=14'} + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -3574,6 +3583,11 @@ packages: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} + cli-highlight@2.1.11: + resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + cli-spinners@2.6.1: resolution: {integrity: sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==} engines: {node: '>=6'} @@ -3594,6 +3608,9 @@ packages: resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==} engines: {node: '>=18'} + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -4583,6 +4600,9 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + hono@4.11.3: resolution: {integrity: sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w==} engines: {node: '>=16.9.0'} @@ -5307,6 +5327,9 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -5587,6 +5610,15 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse5-htmlparser2-tree-adapter@6.0.1: + resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + + parse5@5.1.1: + resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + parse5@8.0.0: resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} @@ -6520,6 +6552,13 @@ packages: text-decoder@1.2.7: resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thread-stream@3.1.0: resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} @@ -7223,10 +7262,18 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -10482,6 +10529,8 @@ snapshots: ansis@4.2.0: {} + any-promise@1.3.0: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -10855,6 +10904,15 @@ snapshots: dependencies: restore-cursor: 3.1.0 + cli-highlight@2.1.11: + dependencies: + chalk: 4.1.2 + highlight.js: 10.7.3 + mz: 2.7.0 + parse5: 5.1.1 + parse5-htmlparser2-tree-adapter: 6.0.1 + yargs: 16.2.0 + cli-spinners@2.6.1: {} cli-spinners@2.9.2: {} @@ -10871,6 +10929,12 @@ snapshots: is-wsl: 3.1.1 is64bit: 2.0.0 + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -11954,6 +12018,8 @@ snapshots: he@1.2.0: {} + highlight.js@10.7.3: {} + hono@4.11.3: {} hookable@5.5.3: {} @@ -12685,6 +12751,12 @@ snapshots: muggle-string@0.4.1: {} + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + nanoid@3.3.11: {} nanoid@5.1.6: {} @@ -13267,6 +13339,14 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse5-htmlparser2-tree-adapter@6.0.1: + dependencies: + parse5: 6.0.1 + + parse5@5.1.1: {} + + parse5@6.0.1: {} + parse5@8.0.0: dependencies: entities: 6.0.1 @@ -14263,6 +14343,14 @@ snapshots: transitivePeerDependencies: - react-native-b4a + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + thread-stream@3.1.0: dependencies: real-require: 0.2.0 @@ -14958,8 +15046,20 @@ snapshots: yaml@2.8.2: {} + yargs-parser@20.2.9: {} + yargs-parser@21.1.1: {} + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + yargs@17.7.2: dependencies: cliui: 8.0.1