Skip to content

Commit 2f0f9e0

Browse files
cnwangjieclaude
andcommitted
feat(rigup): clack-based prompt flow with curated catalog and npm search
- Replace minimal inline prompts with a full @clack/prompts flow: project name, description, package manager, TypeScript version, server-needed gate, one big multiselect over the curated catalog (server-only items filtered out when no backend), and notes. - Add a curated library catalog (~55 entries across 19 categories, inspired by TanStack/cli's add-on taxonomy) with category prefixes shown in the multiselect for quick scanning. - Add a continuous npm-search loop (clack text → select) so users can pull in libraries beyond the curated set; exits on empty input. - Spawn Claude Code with `--dangerously-skip-permissions --chrome` so the long-running, autonomous integration doesn't stall on prompts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 06edd24 commit 2f0f9e0

5 files changed

Lines changed: 414 additions & 92 deletions

File tree

packages/rigup/src/claude.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,17 @@ export interface SpawnOptions {
2323
* Hand off to Claude Code in interactive mode with the integration prompt as
2424
* the first message. Stdio is inherited so the user sees and interacts with
2525
* Claude directly.
26+
*
27+
* Flags:
28+
* - `--dangerously-skip-permissions`: integration is long, auto-running, and
29+
* would otherwise stall on every file-write / shell-exec permission prompt.
30+
* - `--chrome`: enable Chrome browser automation for any docs / smoke-checks
31+
* the integration may want to do.
2632
*/
33+
export const CLAUDE_FLAGS = ["--dangerously-skip-permissions", "--chrome"] as const;
34+
2735
export async function spawnClaude({ cwd, prompt }: SpawnOptions): Promise<number> {
28-
const child = execa(CLAUDE_BIN, [prompt], {
36+
const child = execa(CLAUDE_BIN, [...CLAUDE_FLAGS, prompt], {
2937
cwd,
3038
stdio: "inherit",
3139
reject: false,

packages/rigup/src/init.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { mkdir, readdir, writeFile } from "node:fs/promises";
22
import { resolve } from "node:path";
3-
import * as p from "@clack/prompts";
43
import pc from "picocolors";
5-
import { isClaudeInstalled, spawnClaude, installHint } from "./claude.js";
4+
import {
5+
CLAUDE_FLAGS,
6+
isClaudeInstalled,
7+
installHint,
8+
spawnClaude,
9+
} from "./claude.js";
610
import { runPrompts } from "./prompts.js";
711
import { renderSpecMarkdown, SPEC_FILENAME, type StackSpec } from "./spec.js";
812

@@ -14,10 +18,8 @@ export interface InitOptions {
1418
}
1519

1620
export async function init(options: InitOptions = {}): Promise<void> {
17-
p.intro(pc.bgCyan(pc.black(" rigup ")));
18-
1921
if (!(await isClaudeInstalled())) {
20-
p.cancel(`Claude Code CLI is required.${installHint}`);
22+
process.stderr.write(`${pc.red("✗")} Claude Code CLI is required.${installHint}`);
2123
process.exit(1);
2224
}
2325

@@ -30,11 +32,10 @@ export async function init(options: InitOptions = {}): Promise<void> {
3032
await ensureEmptyDir(targetDir, options.here ?? false);
3133
await writeSeed(targetDir, spec);
3234

33-
p.note(
34-
`Wrote spec → ${pc.cyan(`${targetDir}/${SPEC_FILENAME}`)}\nHanding off to Claude Code…`,
35-
"Ready",
35+
process.stdout.write(
36+
`\n${pc.green("✓")} Wrote spec → ${pc.cyan(`${targetDir}/${SPEC_FILENAME}`)}\n` +
37+
`${pc.dim(`Handing off to Claude Code (${CLAUDE_FLAGS.join(" ")})…`)}\n\n`,
3638
);
37-
p.outro("Claude is taking over from here.");
3839

3940
const code = await spawnClaude({
4041
cwd: targetDir,
@@ -49,10 +50,12 @@ async function ensureEmptyDir(dir: string, here: boolean): Promise<void> {
4950
const entries = await readdir(dir);
5051
const significant = entries.filter((e) => !e.startsWith("."));
5152
if (significant.length > 0) {
52-
p.cancel(
53-
here
54-
? `Current directory is not empty: ${dir}`
55-
: `Target directory already contains files: ${dir}`,
53+
process.stderr.write(
54+
`${pc.red("✗")} ${
55+
here
56+
? `Current directory is not empty: ${dir}`
57+
: `Target directory already contains files: ${dir}`
58+
}\n`,
5659
);
5760
process.exit(1);
5861
}

packages/rigup/src/libraries.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* Curated list of popular libraries in the TypeScript web ecosystem,
3+
* surfaced as a multi-select checkbox in the prompt flow.
4+
*
5+
* Bias: things people actually reach for in 2026, one or two per slot.
6+
* Not exhaustive — users can always add more via the npm search step.
7+
*/
8+
9+
export type Category =
10+
| "Framework"
11+
| "UI components"
12+
| "Styling"
13+
| "Server framework"
14+
| "API layer"
15+
| "Database / ORM"
16+
| "Auth"
17+
| "State / data"
18+
| "Forms"
19+
| "Validation"
20+
| "Animation"
21+
| "Testing"
22+
| "Lint & format"
23+
| "Email"
24+
| "Payments"
25+
| "AI"
26+
| "i18n"
27+
| "Observability"
28+
| "Tooling";
29+
30+
export interface CuratedLibrary {
31+
/** npm package name — written verbatim into the spec */
32+
name: string;
33+
/** short hint shown next to the name */
34+
description: string;
35+
category: Category;
36+
}
37+
38+
export const CURATED: CuratedLibrary[] = [
39+
// Framework
40+
{ name: "next", description: "React framework, App Router", category: "Framework" },
41+
{ name: "@tanstack/start", description: "TanStack full-stack framework", category: "Framework" },
42+
{ name: "react-router", description: "React Router (Remix successor)", category: "Framework" },
43+
{ name: "astro", description: "Content / island framework", category: "Framework" },
44+
{ name: "nuxt", description: "Vue framework", category: "Framework" },
45+
{ name: "@sveltejs/kit", description: "SvelteKit", category: "Framework" },
46+
{ name: "solid-start", description: "SolidJS framework", category: "Framework" },
47+
{ name: "vite", description: "Plain Vite SPA starter (no meta-framework)", category: "Framework" },
48+
49+
// UI components
50+
{ name: "shadcn-ui", description: "Copy-paste React components on Radix + Tailwind", category: "UI components" },
51+
{ name: "@radix-ui/react", description: "Unstyled accessible primitives", category: "UI components" },
52+
{ name: "@headlessui/react", description: "Unstyled accessible components", category: "UI components" },
53+
{ name: "@mantine/core", description: "Batteries-included React components", category: "UI components" },
54+
55+
// Styling
56+
{ name: "tailwindcss", description: "Utility-first CSS", category: "Styling" },
57+
{ name: "unocss", description: "On-demand atomic CSS", category: "Styling" },
58+
{ name: "@vanilla-extract/css", description: "Type-safe CSS-in-TS, zero runtime", category: "Styling" },
59+
{ name: "@pandacss/dev", description: "Style props at build time", category: "Styling" },
60+
61+
// Server framework
62+
{ name: "hono", description: "Small, fast, edge-friendly server framework", category: "Server framework" },
63+
{ name: "elysia", description: "Bun-native server framework", category: "Server framework" },
64+
{ name: "fastify", description: "Schema-driven Node server framework", category: "Server framework" },
65+
{ name: "express", description: "Classic Node server framework", category: "Server framework" },
66+
{ name: "@nestjs/core", description: "NestJS — opinionated, decorator-based", category: "Server framework" },
67+
68+
// API layer
69+
{ name: "@trpc/server", description: "tRPC — typesafe RPC", category: "API layer" },
70+
{ name: "@orpc/server", description: "oRPC — OpenAPI-compatible typesafe RPC", category: "API layer" },
71+
72+
// Database / ORM
73+
{ name: "drizzle-orm", description: "TypeScript-first ORM / query builder", category: "Database / ORM" },
74+
{ name: "prisma", description: "Schema-first ORM", category: "Database / ORM" },
75+
{ name: "kysely", description: "Type-safe SQL query builder", category: "Database / ORM" },
76+
{ name: "convex", description: "Realtime backend / DB (BaaS)", category: "Database / ORM" },
77+
78+
// Auth
79+
{ name: "better-auth", description: "Modern, framework-agnostic auth", category: "Auth" },
80+
{ name: "next-auth", description: "Auth.js (was NextAuth)", category: "Auth" },
81+
{ name: "@clerk/nextjs", description: "Hosted auth", category: "Auth" },
82+
{ name: "@workos-inc/authkit-nextjs", description: "WorkOS AuthKit", category: "Auth" },
83+
84+
// State / data
85+
{ name: "zustand", description: "Tiny global state", category: "State / data" },
86+
{ name: "jotai", description: "Atomic state", category: "State / data" },
87+
{ name: "@tanstack/react-query", description: "Async / server state", category: "State / data" },
88+
{ name: "swr", description: "Data fetching with cache", category: "State / data" },
89+
{ name: "@reduxjs/toolkit", description: "Redux with less boilerplate", category: "State / data" },
90+
91+
// Forms
92+
{ name: "react-hook-form", description: "Performant React forms", category: "Forms" },
93+
{ name: "@tanstack/react-form", description: "Headless form library", category: "Forms" },
94+
95+
// Validation
96+
{ name: "zod", description: "Schema validation", category: "Validation" },
97+
{ name: "valibot", description: "Modular schema validation", category: "Validation" },
98+
{ name: "arktype", description: "TS-syntax schema validation", category: "Validation" },
99+
100+
// Animation
101+
{ name: "motion", description: "Framer Motion (renamed `motion`)", category: "Animation" },
102+
{ name: "gsap", description: "Animation library", category: "Animation" },
103+
104+
// Testing
105+
{ name: "vitest", description: "Vite-native test runner", category: "Testing" },
106+
{ name: "playwright", description: "Browser end-to-end testing", category: "Testing" },
107+
108+
// Lint & format
109+
{ name: "@biomejs/biome", description: "Fast linter + formatter (Rust)", category: "Lint & format" },
110+
{ name: "eslint", description: "Linter", category: "Lint & format" },
111+
{ name: "prettier", description: "Code formatter", category: "Lint & format" },
112+
113+
// Email
114+
{ name: "react-email", description: "Email components in React", category: "Email" },
115+
{ name: "resend", description: "Transactional email API", category: "Email" },
116+
117+
// Payments
118+
{ name: "stripe", description: "Payments", category: "Payments" },
119+
{ name: "@polar-sh/sdk", description: "Polar payments", category: "Payments" },
120+
121+
// AI
122+
{ name: "ai", description: "Vercel AI SDK", category: "AI" },
123+
{ name: "@anthropic-ai/sdk", description: "Anthropic SDK", category: "AI" },
124+
{ name: "openai", description: "OpenAI SDK", category: "AI" },
125+
126+
// i18n
127+
{ name: "next-intl", description: "i18n for Next.js", category: "i18n" },
128+
{ name: "@inlang/paraglide-js", description: "Type-safe i18n (Paraglide)", category: "i18n" },
129+
130+
// Observability
131+
{ name: "@sentry/node", description: "Sentry error + perf monitoring", category: "Observability" },
132+
{ name: "posthog-js", description: "Product analytics", category: "Observability" },
133+
134+
// Tooling
135+
{ name: "storybook", description: "Component workbench", category: "Tooling" },
136+
{ name: "@t3-oss/env-nextjs", description: "Type-safe env vars (T3 Env)", category: "Tooling" },
137+
];

packages/rigup/src/npm.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
export interface NpmHit {
2+
name: string;
3+
version: string;
4+
description: string;
5+
}
6+
7+
interface NpmSearchResponse {
8+
objects: Array<{
9+
package: {
10+
name: string;
11+
version: string;
12+
description?: string;
13+
};
14+
}>;
15+
}
16+
17+
const SEARCH_ENDPOINT = "https://registry.npmjs.org/-/v1/search";
18+
19+
export async function searchNpm(
20+
query: string,
21+
size = 10,
22+
signal?: AbortSignal,
23+
): Promise<NpmHit[]> {
24+
const trimmed = query.trim();
25+
if (trimmed.length < 2) return [];
26+
27+
const effectiveSignal = signal ?? AbortSignal.timeout(5_000);
28+
const url = `${SEARCH_ENDPOINT}?text=${encodeURIComponent(trimmed)}&size=${size}`;
29+
const res = await fetch(url, {
30+
headers: { Accept: "application/json" },
31+
signal: effectiveSignal,
32+
});
33+
if (!res.ok) return [];
34+
35+
const data = (await res.json()) as NpmSearchResponse;
36+
return data.objects.map(({ package: pkg }) => ({
37+
name: pkg.name,
38+
version: pkg.version,
39+
description: pkg.description ?? "",
40+
}));
41+
}

0 commit comments

Comments
 (0)