neon-init 0.17.2 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/interactive.d.ts.map +1 -1
- package/dist/interactive.js +2 -13
- package/dist/interactive.js.map +1 -1
- package/dist/lib/bootstrap.d.ts +104 -2
- package/dist/lib/bootstrap.d.ts.map +1 -1
- package/dist/lib/bootstrap.js +272 -3
- package/dist/lib/bootstrap.js.map +1 -1
- package/dist/lib/phases/setup.d.ts.map +1 -1
- package/dist/lib/phases/setup.js +4 -13
- package/dist/lib/phases/setup.js.map +1 -1
- package/dist/lib/vsix.js.map +1 -1
- package/package.json +6 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interactive.d.ts","names":[],"sources":["../src/interactive.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"interactive.d.ts","names":[],"sources":["../src/interactive.ts"],"mappings":";;AAkFA;AAIA;;AACU,UALO,sBAAA,CAKP;SACP,CAAA,EAAA,OAAA;AAAO;iBAFY,eAAA,WACZ,yBACP"}
|
package/dist/interactive.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ALL_CONFIGURABLE_AGENTS, getAddMcpAgentId } from "./lib/agents.js";
|
|
2
2
|
import { ensureNeonctlAuth, isAuthenticated } from "./lib/auth.js";
|
|
3
|
-
import { FALLBACK_TEMPLATES, fetchTemplates } from "./lib/bootstrap.js";
|
|
3
|
+
import { FALLBACK_TEMPLATES, fetchTemplates, scaffoldTemplate } from "./lib/bootstrap.js";
|
|
4
4
|
import { detectAgent, detectIde } from "./lib/detect-agent.js";
|
|
5
5
|
import { detectAvailableEditors } from "./lib/editors.js";
|
|
6
6
|
import { installExtension, isExtensionInstalled, usesExtension } from "./lib/extension.js";
|
|
@@ -137,18 +137,7 @@ async function interactiveInitInner(options) {
|
|
|
137
137
|
const bootstrapS = spinner();
|
|
138
138
|
bootstrapS.start(`Scaffolding project from template "${selectedTemplate.title}"...`);
|
|
139
139
|
try {
|
|
140
|
-
await
|
|
141
|
-
"-y",
|
|
142
|
-
"neonctl@latest",
|
|
143
|
-
"bootstrap",
|
|
144
|
-
".",
|
|
145
|
-
"--template",
|
|
146
|
-
selectedTemplate.id,
|
|
147
|
-
"--force"
|
|
148
|
-
], {
|
|
149
|
-
stdio: "pipe",
|
|
150
|
-
timeout: 12e4
|
|
151
|
-
});
|
|
140
|
+
await scaffoldTemplate(selectedTemplate, ".", { onWarn: (message) => log.warn(message) });
|
|
152
141
|
bootstrapS.stop(dim(`Scaffolded project from "${selectedTemplate.title}" ✓`));
|
|
153
142
|
} catch (err) {
|
|
154
143
|
const msg = err instanceof Error ? err.message : "Unknown error";
|
package/dist/interactive.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interactive.js","names":[],"sources":["../src/interactive.ts"],"sourcesContent":["/**\n * Interactive v2 CLI — purpose-built guided flow for humans.\n * Uses the same underlying install functions but with a clean clack-based UX.\n */\n\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport {\n\tconfirm,\n\tisCancel,\n\tlog,\n\tmultiselect,\n\toutro,\n\tselect,\n\tspinner,\n} from \"@clack/prompts\";\nimport { execa } from \"execa\";\nimport { bold, dim } from \"yoctocolors\";\nimport { ALL_CONFIGURABLE_AGENTS, getAddMcpAgentId } from \"./lib/agents.js\";\nimport { ensureNeonctlAuth, isAuthenticated } from \"./lib/auth.js\";\nimport {\n\ttype BootstrapTemplate,\n\tFALLBACK_TEMPLATES,\n\tfetchTemplates,\n\ttype NeonFeature,\n} from \"./lib/bootstrap.js\";\nimport { detectAgent, detectIde } from \"./lib/detect-agent.js\";\nimport { detectAvailableEditors } from \"./lib/editors.js\";\nimport {\n\tinstallExtension,\n\tisExtensionInstalled,\n\tusesExtension,\n} from \"./lib/extension.js\";\nimport { inspectProject } from \"./lib/inspect.js\";\nimport { ensureNeonctl } from \"./lib/neonctl.js\";\nimport { ensureSkillsUpToDate, installAgentSkills } from \"./lib/skills.js\";\nimport type { Editor } from \"./lib/types.js\";\n\nfunction wordWrap(text: string, width: number): string {\n\treturn text\n\t\t.split(\"\\n\")\n\t\t.map((line) => {\n\t\t\tif (line.length <= width) return line;\n\t\t\tconst words = line.split(\" \");\n\t\t\tconst lines: string[] = [];\n\t\t\tlet current = \"\";\n\t\t\tfor (const word of words) {\n\t\t\t\tif (\n\t\t\t\t\tcurrent.length + word.length + 1 > width &&\n\t\t\t\t\tcurrent.length > 0\n\t\t\t\t) {\n\t\t\t\t\tlines.push(current);\n\t\t\t\t\tcurrent = word;\n\t\t\t\t} else {\n\t\t\t\t\tcurrent = current.length > 0 ? `${current} ${word}` : word;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (current.length > 0) lines.push(current);\n\t\t\treturn lines.join(\"\\n\");\n\t\t})\n\t\t.join(\"\\n\");\n}\n\n// Patch picocolors (used by @clack/prompts) to use Neon green instead of cyan/magenta.\n// clack hardcodes picocolors.cyan() with no theme API, so this is the least invasive override.\nimport picocolors from \"picocolors\";\n\nconst neonGreenFn = (s: string) => `\\x1b[38;2;75;181;120m${s}\\x1b[39m`;\nconst originalCyan = picocolors.cyan;\nconst originalMagenta = picocolors.magenta;\n\nfunction patchClackColors(): () => void {\n\tconst pc = picocolors as unknown as Record<string, unknown>;\n\tpc.cyan = neonGreenFn;\n\tpc.magenta = neonGreenFn;\n\treturn () => {\n\t\tpc.cyan = originalCyan;\n\t\tpc.magenta = originalMagenta;\n\t};\n}\n\nexport interface InteractiveInitOptions {\n\tpreview?: boolean;\n}\n\nexport async function interactiveInit(\n\toptions: InteractiveInitOptions = {},\n): Promise<void> {\n\tconst restoreColors = patchClackColors();\n\ttry {\n\t\tawait interactiveInitInner(options);\n\t} finally {\n\t\trestoreColors();\n\t}\n}\n\nasync function interactiveInitInner(\n\toptions: InteractiveInitOptions,\n): Promise<void> {\n\tconsole.log();\n\tconsole.log(\n\t\t\"\\x1b[38;2;75;181;120m\" +\n\t\t\t[\n\t\t\t\t\" ██╗ ██╗██████╗ ██████╗ ██╗ ██╗\",\n\t\t\t\t\" ███╗ ██║██╔═══╝ ██╔═══██╗███╗ ██║\",\n\t\t\t\t\" ████╗██║██████╗ ██║ ██║████╗██║\",\n\t\t\t\t\" ██╔████║██╔═══╝ ██║ ██║██╔████║\",\n\t\t\t\t\" ██║╚███║██████╗ ╚██████╔╝██║╚███║\",\n\t\t\t\t\" ╚═╝ ╚══╝╚═════╝ ╚═════╝ ╚═╝ ╚══╝\",\n\t\t\t].join(\"\\n\") +\n\t\t\t\"\\x1b[0m\",\n\t);\n\tconsole.log(\n\t\tdim(\n\t\t\twordWrap(\n\t\t\t\t\"\\nLet's get your project set up with Neon. We'll install the MCP server, agent skills, and IDE extension, then connect your app to a database.\\n\",\n\t\t\t\tprocess.stdout.columns || 80,\n\t\t\t),\n\t\t),\n\t);\n\n\tconst detectedAgentId = detectAgent();\n\tconst detectedEditor = detectedAgentId\n\t\t? agentIdToEditor(detectedAgentId)\n\t\t: null;\n\n\t// -----------------------------------------------------------------------\n\t// Step 1: Inspect what's already in place\n\t// -----------------------------------------------------------------------\n\tconst inspectSpinner = spinner();\n\tinspectSpinner.start(\"Checking existing configuration...\");\n\tconst inspection = await inspectProject([\n\t\t{ id: \"has_app\", description: \"\", lookFor: [] },\n\t\t{ id: \"mcp_server\", description: \"\", lookFor: [] },\n\t\t{ id: \"skills\", description: \"\", lookFor: [] },\n\t\t{ id: \"connection_string\", description: \"\", lookFor: [] },\n\t\t{ id: \"project_stack\", description: \"\", lookFor: [] },\n\t\t{ id: \"migrations\", description: \"\", lookFor: [] },\n\t\t{ id: \"ide_type\", description: \"\", lookFor: [] },\n\t]);\n\tinspectSpinner.stop(dim(\"Configuration checked ✓\"));\n\n\tconst hasApp = inspection.hasApp === true;\n\tlet selectedFeatures: NeonFeature[] = [];\n\tlet selectedTemplate: BootstrapTemplate | null = null;\n\n\t// Preview mode: bootstrap from template if no app detected\n\tif (options.preview && !hasApp) {\n\t\tlet templates = FALLBACK_TEMPLATES;\n\t\ttry {\n\t\t\tconst fetched = await fetchTemplates();\n\t\t\tif (fetched && fetched.length > 0) templates = fetched;\n\t\t} catch {}\n\n\t\tconst templateResult = await select({\n\t\t\tmessage:\n\t\t\t\t\"No application detected. Would you like to scaffold a new project from a template?\",\n\t\t\toptions: [\n\t\t\t\t...templates.map((t) => ({\n\t\t\t\t\tvalue: t.id,\n\t\t\t\t\tlabel: t.title,\n\t\t\t\t\thint: t.description,\n\t\t\t\t})),\n\t\t\t\t{\n\t\t\t\t\tvalue: \"none\",\n\t\t\t\t\tlabel: \"No thanks — continue without scaffolding\",\n\t\t\t\t},\n\t\t\t],\n\t\t\tinitialValue: templates[0]?.id ?? \"none\",\n\t\t});\n\t\tif (isCancel(templateResult)) {\n\t\t\toutro(\"Setup cancelled.\");\n\t\t\treturn;\n\t\t}\n\t\tif (templateResult !== \"none\") {\n\t\t\tselectedTemplate =\n\t\t\t\ttemplates.find((t) => t.id === templateResult) ?? null;\n\t\t\tif (selectedTemplate) {\n\t\t\t\tselectedFeatures = selectedTemplate.requires;\n\t\t\t\tconst bootstrapS = spinner();\n\t\t\t\tbootstrapS.start(\n\t\t\t\t\t`Scaffolding project from template \"${selectedTemplate.title}\"...`,\n\t\t\t\t);\n\t\t\t\ttry {\n\t\t\t\t\t// Pin @latest (and -y) so a stale globally-installed neonctl\n\t\t\t\t\t// can't be picked up by npx — bootstrap's rate-limit fix lives\n\t\t\t\t\t// in recent neonctl.\n\t\t\t\t\tawait execa(\n\t\t\t\t\t\t\"npx\",\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\"-y\",\n\t\t\t\t\t\t\t\"neonctl@latest\",\n\t\t\t\t\t\t\t\"bootstrap\",\n\t\t\t\t\t\t\t\".\",\n\t\t\t\t\t\t\t\"--template\",\n\t\t\t\t\t\t\tselectedTemplate.id,\n\t\t\t\t\t\t\t\"--force\",\n\t\t\t\t\t\t],\n\t\t\t\t\t\t{ stdio: \"pipe\", timeout: 120000 },\n\t\t\t\t\t);\n\t\t\t\t\tbootstrapS.stop(\n\t\t\t\t\t\tdim(\n\t\t\t\t\t\t\t`Scaffolded project from \"${selectedTemplate.title}\" ✓`,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst msg =\n\t\t\t\t\t\terr instanceof Error ? err.message : \"Unknown error\";\n\t\t\t\t\tbootstrapS.stop(\"Failed to scaffold project\");\n\t\t\t\t\tlog.error(msg);\n\t\t\t\t\toutro(\"Setup failed.\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// For brownfield flows (existing app), ask which features to enable\n\tif (!selectedTemplate && hasApp) {\n\t\tconst featuresResult = await select({\n\t\t\tmessage:\n\t\t\t\t\"Which Neon features would you like to enable for this project?\",\n\t\t\toptions: [\n\t\t\t\t{ value: \"database\", label: \"Database\" },\n\t\t\t\t{\n\t\t\t\t\tvalue: \"database,auth\",\n\t\t\t\t\tlabel: \"Database + Neon Auth (adds authentication via Neon)\",\n\t\t\t\t},\n\t\t\t],\n\t\t\tinitialValue: \"database\",\n\t\t});\n\t\tif (isCancel(featuresResult)) {\n\t\t\toutro(\"Setup cancelled.\");\n\t\t\treturn;\n\t\t}\n\t\tselectedFeatures = (featuresResult as string).split(\n\t\t\t\",\",\n\t\t) as NeonFeature[];\n\t}\n\n\t// Write _init metadata to .neon\n\tif (selectedFeatures.length > 0) {\n\t\tconst neonPath = resolve(process.cwd(), \".neon\");\n\t\tlet existing: Record<string, unknown> = {};\n\t\tif (existsSync(neonPath)) {\n\t\t\ttry {\n\t\t\t\texisting = JSON.parse(readFileSync(neonPath, \"utf-8\"));\n\t\t\t} catch {}\n\t\t}\n\t\texisting._init = { features: selectedFeatures };\n\t\twriteFileSync(neonPath, `${JSON.stringify(existing, null, 2)}\\n`);\n\t}\n\n\tconst mcpAlready = inspection.mcpConfigured === true;\n\t// If we bootstrapped, skills come from the template\n\tconst skillsAlready =\n\t\tinspection.skillsInstalled === true || selectedTemplate !== null;\n\tconst hasNeonConnection = inspection.connectionString === true;\n\tlet needsMcp = !mcpAlready;\n\tconst needsSkills = !skillsAlready;\n\tconst needsInstall = needsMcp || needsSkills;\n\n\t// Check if .neon context file exists\n\tconst neonContextPath = resolve(process.cwd(), \".neon\");\n\tconst hasNeonContext =\n\t\texistsSync(neonContextPath) &&\n\t\t(() => {\n\t\t\ttry {\n\t\t\t\tconst content = JSON.parse(\n\t\t\t\t\treadFileSync(neonContextPath, \"utf-8\"),\n\t\t\t\t);\n\t\t\t\treturn !!content.projectId;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t})();\n\n\t// Check if Neon Auth is configured\n\tconst hasNeonAuth = (() => {\n\t\tfor (const envFile of [\".env\", \".env.local\"]) {\n\t\t\tconst envPath = resolve(process.cwd(), envFile);\n\t\t\tif (existsSync(envPath)) {\n\t\t\t\ttry {\n\t\t\t\t\tconst content = readFileSync(envPath, \"utf-8\");\n\t\t\t\t\tif (/^NEON_AUTH_/m.test(content)) return true;\n\t\t\t\t} catch {}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t})();\n\n\t// Check if extension is installed for the detected editor\n\tlet extensionAlready = false;\n\tif (detectedEditor && usesExtension(detectedEditor)) {\n\t\textensionAlready = await isExtensionInstalled(detectedEditor);\n\t}\n\n\t// If tooling + database are configured, check if there's anything left to do\n\tif (mcpAlready && skillsAlready && hasNeonConnection && hasNeonContext) {\n\t\tlog.step(\n\t\t\tdim(\n\t\t\t\t`Neon MCP server already configured (${inspection.mcpScope || \"detected\"}) ✓`,\n\t\t\t),\n\t\t);\n\t\tlog.step(\n\t\t\tdim(\n\t\t\t\t`Neon agent skills already installed (${inspection.skillsScope || \"detected\"}) ✓`,\n\t\t\t),\n\t\t);\n\t\tif (extensionAlready)\n\t\t\tlog.step(dim(\"Neon editor extension installed ✓\"));\n\t\tlog.step(dim(\"Neon database connected ✓\"));\n\n\t\tif (hasNeonAuth) {\n\t\t\tlog.step(dim(\"Neon Auth configured ✓\"));\n\t\t\toutro(\n\t\t\t\tdim(\n\t\t\t\t\t\"Your project is fully configured with Neon. Nothing to do.\",\n\t\t\t\t),\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\t// Neon Auth not configured — ask if they want it\n\t\tconst authResult = await select({\n\t\t\tmessage:\n\t\t\t\t\"Would you like to set up Neon Auth for user authentication?\",\n\t\t\toptions: [\n\t\t\t\t{ value: \"yes\", label: \"Yes, set up Neon Auth\" },\n\t\t\t\t{ value: \"no\", label: \"No, skip for now\" },\n\t\t\t],\n\t\t\tinitialValue: \"no\",\n\t\t});\n\n\t\tif (isCancel(authResult) || authResult === \"no\") {\n\t\t\toutro(\n\t\t\t\tdim(\n\t\t\t\t\t`Your project is configured with Neon. You can set up Neon Auth later by having your agent run: neonctl init --agent --data '{\"step\":\"neon-auth\"}'`,\n\t\t\t\t),\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\t// Read .neon for project context\n\t\tlet projectId: string | null = null;\n\t\ttry {\n\t\t\tconst neonCtx = JSON.parse(readFileSync(neonContextPath, \"utf-8\"));\n\t\t\tprojectId = neonCtx.projectId ?? null;\n\t\t} catch {}\n\n\t\tlog.step(\"Next steps\");\n\t\tconst promptLines = [\"Set up Neon Auth for this project.\"];\n\t\tif (projectId) promptLines.push(`Project ID: ${projectId}.`);\n\t\tlog.message(dim(\"Copy the following into your agent chat:\"));\n\t\tlog.message(\n\t\t\tpromptLines.map((line) => bold(neonGreenFn(line))).join(\"\\n\"),\n\t\t);\n\t\toutro(dim(\"Have feedback? Email us at feedback@neon.tech\"));\n\t\treturn;\n\t}\n\n\t// Log what's already in place\n\tif (mcpAlready)\n\t\tlog.step(\n\t\t\tdim(\n\t\t\t\t`Neon MCP server already configured (${inspection.mcpScope || \"detected\"}) ✓`,\n\t\t\t),\n\t\t);\n\tif (skillsAlready)\n\t\tlog.step(\n\t\t\tdim(\n\t\t\t\t`Neon agent skills already installed (${inspection.skillsScope || \"detected\"}) ✓`,\n\t\t\t),\n\t\t);\n\n\t// -----------------------------------------------------------------------\n\t// Step 3–5: Install what's missing (skip entirely if everything is configured)\n\t// -----------------------------------------------------------------------\n\tif (needsInstall) {\n\t\tconst homeDir = process.env.HOME || process.env.USERPROFILE;\n\t\tif (!homeDir) {\n\t\t\tlog.error(\"Could not determine home directory.\");\n\t\t\toutro(\"Setup failed.\");\n\t\t\treturn;\n\t\t}\n\n\t\tlet selectedEditors: Editor[];\n\t\tif (detectedEditor) {\n\t\t\tselectedEditors = [detectedEditor];\n\t\t} else {\n\t\t\tconst availableEditors = await detectAvailableEditors(homeDir);\n\t\t\tconst editorResponse = await multiselect({\n\t\t\t\tmessage: \"Which editor(s) would you like to configure?\",\n\t\t\t\toptions: ALL_CONFIGURABLE_AGENTS.map((agent) => ({\n\t\t\t\t\tvalue: agent.editor,\n\t\t\t\t\tlabel: agent.editor,\n\t\t\t\t\thint: agent.hint,\n\t\t\t\t})),\n\t\t\t\tinitialValues: availableEditors,\n\t\t\t\trequired: true,\n\t\t\t});\n\t\t\tif (isCancel(editorResponse)) {\n\t\t\t\toutro(\"Setup cancelled.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tselectedEditors = editorResponse as Editor[];\n\t\t\tif (selectedEditors.length === 0) {\n\t\t\t\tlog.warn(\"No editors selected.\");\n\t\t\t\toutro(\"Setup cancelled.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// Check extension status\n\t\tconst vscodeEditors = selectedEditors.filter(usesExtension);\n\t\tlet extensionAlreadyInstalled = false;\n\t\tif (vscodeEditors.length > 0) {\n\t\t\tconst checks = await Promise.all(\n\t\t\t\tvscodeEditors.map((e) => isExtensionInstalled(e)),\n\t\t\t);\n\t\t\textensionAlreadyInstalled = checks.every(Boolean);\n\t\t\tif (extensionAlreadyInstalled) {\n\t\t\t\tlog.step(dim(\"Neon editor extension already installed ✓\"));\n\t\t\t}\n\t\t}\n\t\tlet doInstallExtension =\n\t\t\tvscodeEditors.length > 0 && !extensionAlreadyInstalled;\n\n\t\t// Build hint showing only what needs installing\n\t\tconst hintParts: string[] = [];\n\t\tif (needsMcp) hintParts.push(\"MCP server (global)\");\n\t\tif (needsSkills) hintParts.push(\"agent skills (project)\");\n\t\tif (doInstallExtension) hintParts.push(\"editor extension\");\n\n\t\t// Installation preferences\n\t\tlet mcpScope: \"global\" | \"project\" | \"none\" = \"global\";\n\t\tlet skillsScope: \"global\" | \"project\" = \"project\";\n\n\t\tlet modeResult: string;\n\t\twhile (true) {\n\t\t\tconst editorName = selectedEditors.join(\", \");\n\t\t\tconst result = await select({\n\t\t\t\tmessage: `Configure ${editorName} for Neon:`,\n\t\t\t\toptions: [\n\t\t\t\t\t{\n\t\t\t\t\t\tvalue: \"defaults\",\n\t\t\t\t\t\tlabel: \"Install with defaults\",\n\t\t\t\t\t\thint: hintParts.join(\", \"),\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tvalue: \"customize\",\n\t\t\t\t\t\tlabel: \"Customize installation\",\n\t\t\t\t\t\thint: \"choose scopes and options\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tvalue: \"change_editor\",\n\t\t\t\t\t\tlabel: \"Configure a different editor\",\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tinitialValue: \"defaults\",\n\t\t\t});\n\n\t\t\tif (isCancel(result)) {\n\t\t\t\toutro(\"Setup cancelled.\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (result === \"change_editor\") {\n\t\t\t\tconst availableEditors = await detectAvailableEditors(homeDir);\n\t\t\t\tconst editorResponse = await multiselect({\n\t\t\t\t\tmessage: \"Which editor(s) would you like to configure?\",\n\t\t\t\t\toptions: ALL_CONFIGURABLE_AGENTS.map((agent) => ({\n\t\t\t\t\t\tvalue: agent.editor,\n\t\t\t\t\t\tlabel: agent.editor,\n\t\t\t\t\t\thint: agent.hint,\n\t\t\t\t\t})),\n\t\t\t\t\tinitialValues: availableEditors,\n\t\t\t\t\trequired: true,\n\t\t\t\t});\n\t\t\t\tif (isCancel(editorResponse)) {\n\t\t\t\t\toutro(\"Setup cancelled.\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tselectedEditors = editorResponse as Editor[];\n\t\t\t\tif (selectedEditors.length === 0) {\n\t\t\t\t\toutro(\"Setup cancelled.\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tmodeResult = result as string;\n\t\t\tbreak;\n\t\t}\n\n\t\tif (modeResult === \"customize\") {\n\t\t\tif (needsMcp) {\n\t\t\t\tconst scopeResult = await select({\n\t\t\t\t\tmessage: \"Where should the Neon MCP server be configured?\",\n\t\t\t\t\toptions: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvalue: \"global\",\n\t\t\t\t\t\t\tlabel: \"Global (available in all projects)\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvalue: \"project\",\n\t\t\t\t\t\t\tlabel: \"Project-level (this project only)\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvalue: \"none\",\n\t\t\t\t\t\t\tlabel: \"Skip — do not install the MCP server\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t});\n\t\t\t\tif (isCancel(scopeResult)) {\n\t\t\t\t\toutro(\"Setup cancelled.\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tmcpScope = scopeResult as \"global\" | \"project\" | \"none\";\n\t\t\t\tif (mcpScope === \"none\") needsMcp = false;\n\t\t\t}\n\n\t\t\tif (needsSkills) {\n\t\t\t\tconst skillsScopeResult = await select({\n\t\t\t\t\tmessage: \"Where should Neon agent skills be installed?\",\n\t\t\t\t\toptions: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvalue: \"global\",\n\t\t\t\t\t\t\tlabel: \"Global (available in all projects)\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvalue: \"project\",\n\t\t\t\t\t\t\tlabel: \"Project-level (this project only)\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tinitialValue: \"project\",\n\t\t\t\t});\n\t\t\t\tif (isCancel(skillsScopeResult)) {\n\t\t\t\t\toutro(\"Setup cancelled.\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tskillsScope = skillsScopeResult as \"global\" | \"project\";\n\t\t\t}\n\n\t\t\tif (doInstallExtension) {\n\t\t\t\tconst extResult = await confirm({\n\t\t\t\t\tmessage: `Install the Neon extension for ${vscodeEditors.join(\", \")}?`,\n\t\t\t\t});\n\t\t\t\tif (isCancel(extResult)) {\n\t\t\t\t\toutro(\"Setup cancelled.\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tdoInstallExtension = extResult;\n\t\t\t}\n\t\t}\n\n\t\t// Auth check before install\n\t\tconst installAuthed = await isAuthenticated();\n\t\tif (!installAuthed) {\n\t\t\tconst authS = spinner();\n\t\t\tauthS.start(\"Authenticating with Neon...\");\n\t\t\tconst authSuccess = await ensureNeonctlAuth();\n\t\t\tif (!authSuccess) {\n\t\t\t\tauthS.stop(\"Authentication failed.\");\n\t\t\t\toutro(\"Run neon-init again after signing in.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauthS.stop(\"Authenticated.\");\n\t\t}\n\n\t\t// Ensure neonctl CLI is installed and up to date\n\t\tconst nctlS = spinner();\n\t\tnctlS.start(\"Checking neonctl CLI...\");\n\t\tconst nctlResult = await ensureNeonctl();\n\t\tswitch (nctlResult.status) {\n\t\t\tcase \"already_current\":\n\t\t\t\tnctlS.stop(\n\t\t\t\t\tdim(`neonctl CLI is up to date (v${nctlResult.version}) ✓`),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"installed\":\n\t\t\t\tnctlS.stop(\n\t\t\t\t\tdim(`Installed neonctl CLI (v${nctlResult.version}) ✓`),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"updated\":\n\t\t\t\tnctlS.stop(\n\t\t\t\t\tdim(`Updated neonctl CLI to v${nctlResult.version} ✓`),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"failed\":\n\t\t\t\tnctlS.stop(\"Failed to install neonctl CLI\");\n\t\t\t\tlog.warn(\n\t\t\t\t\t\"neonctl could not be installed automatically. The setup will continue using npx.\",\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t}\n\n\t\t// Install only what's missing\n\t\tfor (const editor of selectedEditors) {\n\t\t\tif (needsMcp) {\n\t\t\t\tconst mcpAgentId = getAddMcpAgentId(editor);\n\t\t\t\tconst mcpArgs = [\n\t\t\t\t\t\"-y\",\n\t\t\t\t\t\"add-mcp\",\n\t\t\t\t\t\"https://mcp.neon.tech/mcp\",\n\t\t\t\t\t\"-n\",\n\t\t\t\t\t\"Neon\",\n\t\t\t\t\t\"-y\",\n\t\t\t\t\t\"-a\",\n\t\t\t\t\tmcpAgentId,\n\t\t\t\t];\n\t\t\t\tif (mcpScope === \"global\") mcpArgs.splice(5, 0, \"-g\");\n\n\t\t\t\tconst mcpS = spinner();\n\t\t\t\tmcpS.start(`Installing Neon MCP server for ${editor}...`);\n\t\t\t\ttry {\n\t\t\t\t\tawait execa(\"npx\", mcpArgs, {\n\t\t\t\t\t\tstdio: \"pipe\",\n\t\t\t\t\t\ttimeout: 60000,\n\t\t\t\t\t});\n\t\t\t\t\tmcpS.stop(\n\t\t\t\t\t\tdim(\n\t\t\t\t\t\t\t`Neon MCP server configured for ${editor} (${mcpScope}) ✓`,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst msg =\n\t\t\t\t\t\terr instanceof Error ? err.message : \"Unknown error\";\n\t\t\t\t\tmcpS.stop(`Failed to configure MCP server for ${editor}`);\n\t\t\t\t\tlog.error(msg);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (needsSkills) {\n\t\t\t\tawait installAgentSkills([editor], {\n\t\t\t\t\tscope: skillsScope,\n\t\t\t\t\tpreview: options.preview,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (doInstallExtension && usesExtension(editor)) {\n\t\t\t\tconst extS = spinner();\n\t\t\t\textS.start(`Installing Neon extension for ${editor}...`);\n\t\t\t\tconst extOk = await installExtension(editor);\n\t\t\t\tif (extOk) {\n\t\t\t\t\textS.stop(dim(`Neon extension installed for ${editor} ✓`));\n\t\t\t\t} else {\n\t\t\t\t\textS.stop(\n\t\t\t\t\t\t`Extension install failed — install manually from the extensions panel.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Ensure all required skills are present (fills in any missing ones).\n\t// detectAgent() returns null in a human terminal (TTY), so fall back\n\t// to IDE detection which works regardless of TTY.\n\tconst ide = detectIde();\n\tconst agentForSkills =\n\t\tdetectAgent() ??\n\t\t(ide === \"Cursor\"\n\t\t\t? \"cursor\"\n\t\t\t: ide === \"VS Code\"\n\t\t\t\t? \"vscode\"\n\t\t\t\t: ide === \"Windsurf\"\n\t\t\t\t\t? \"windsurf\"\n\t\t\t\t\t: null);\n\tif (agentForSkills) {\n\t\tconst detectedSkillsScope =\n\t\t\tinspection.skillsScope === \"global\" ? \"global\" : undefined;\n\t\tawait ensureSkillsUpToDate(\n\t\t\tagentForSkills,\n\t\t\tdetectedSkillsScope,\n\t\t\toptions.preview,\n\t\t);\n\t}\n\n\t// -----------------------------------------------------------------------\n\t// Step 6: Done — build prompt for the agent to continue\n\t// -----------------------------------------------------------------------\n\n\t// Build the getting-started data payload (same as agent mode)\n\tconst gettingStartedData: Record<string, unknown> = {};\n\tif (hasNeonConnection) gettingStartedData.hasConnectionString = true;\n\tif (inspection.framework && inspection.framework !== \"none\")\n\t\tgettingStartedData.framework = inspection.framework;\n\tif (inspection.orm && inspection.orm !== \"none\")\n\t\tgettingStartedData.orm = inspection.orm;\n\tif (inspection.migrationTool && inspection.migrationTool !== \"none\")\n\t\tgettingStartedData.migrationTool = inspection.migrationTool;\n\tif (inspection.migrationDir && inspection.migrationDir !== \"none\")\n\t\tgettingStartedData.migrationDir = inspection.migrationDir;\n\tif (selectedFeatures.length > 0)\n\t\tgettingStartedData.features = selectedFeatures;\n\tif (options.preview) gettingStartedData.preview = true;\n\n\t// Build a prompt for the user to paste into their agent chat\n\tconst cmd = `neonctl init --agent --data '${JSON.stringify({ step: \"getting-started\", ...gettingStartedData })}'`;\n\t// Account for clack's \"│ \" prefix (3 chars) when wrapping\n\tconst cols = (process.stdout.columns || 80) - 3;\n\tconst promptText = `To finish setting up Neon using Neon's agent-guided onboarding experience, have your agent run this shell command: ${cmd}`;\n\n\tlog.step(\"Next steps\");\n\tlog.message(dim(\"Copy the following into your agent chat:\"));\n\tlog.message(\n\t\twordWrap(promptText, cols)\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => bold(neonGreenFn(line)))\n\t\t\t.join(\"\\n\"),\n\t);\n\toutro(dim(\"Have feedback? Email us at feedback@neon.tech\"));\n}\n\nfunction agentIdToEditor(agentId: string): Editor | null {\n\tswitch (agentId) {\n\t\tcase \"cursor\":\n\t\t\treturn \"Cursor\";\n\t\tcase \"vscode\":\n\t\t\treturn \"VS Code\";\n\t\tcase \"claude-code\":\n\t\t\treturn \"Claude CLI\";\n\t\tcase \"windsurf\":\n\t\t\t// Windsurf not in Editor type yet — fall back to prompt\n\t\t\treturn null;\n\t\tcase \"codex\":\n\t\t\treturn \"Codex\";\n\t\tcase \"cline\":\n\t\t\treturn \"Cline\";\n\t\tdefault:\n\t\t\treturn null;\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"interactive.js","names":[],"sources":["../src/interactive.ts"],"sourcesContent":["/**\n * Interactive v2 CLI — purpose-built guided flow for humans.\n * Uses the same underlying install functions but with a clean clack-based UX.\n */\n\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport {\n\tconfirm,\n\tisCancel,\n\tlog,\n\tmultiselect,\n\toutro,\n\tselect,\n\tspinner,\n} from \"@clack/prompts\";\nimport { execa } from \"execa\";\nimport { bold, dim } from \"yoctocolors\";\nimport { ALL_CONFIGURABLE_AGENTS, getAddMcpAgentId } from \"./lib/agents.js\";\nimport { ensureNeonctlAuth, isAuthenticated } from \"./lib/auth.js\";\nimport {\n\ttype BootstrapTemplate,\n\tFALLBACK_TEMPLATES,\n\tfetchTemplates,\n\ttype NeonFeature,\n\tscaffoldTemplate,\n} from \"./lib/bootstrap.js\";\nimport { detectAgent, detectIde } from \"./lib/detect-agent.js\";\nimport { detectAvailableEditors } from \"./lib/editors.js\";\nimport {\n\tinstallExtension,\n\tisExtensionInstalled,\n\tusesExtension,\n} from \"./lib/extension.js\";\nimport { inspectProject } from \"./lib/inspect.js\";\nimport { ensureNeonctl } from \"./lib/neonctl.js\";\nimport { ensureSkillsUpToDate, installAgentSkills } from \"./lib/skills.js\";\nimport type { Editor } from \"./lib/types.js\";\n\nfunction wordWrap(text: string, width: number): string {\n\treturn text\n\t\t.split(\"\\n\")\n\t\t.map((line) => {\n\t\t\tif (line.length <= width) return line;\n\t\t\tconst words = line.split(\" \");\n\t\t\tconst lines: string[] = [];\n\t\t\tlet current = \"\";\n\t\t\tfor (const word of words) {\n\t\t\t\tif (\n\t\t\t\t\tcurrent.length + word.length + 1 > width &&\n\t\t\t\t\tcurrent.length > 0\n\t\t\t\t) {\n\t\t\t\t\tlines.push(current);\n\t\t\t\t\tcurrent = word;\n\t\t\t\t} else {\n\t\t\t\t\tcurrent = current.length > 0 ? `${current} ${word}` : word;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (current.length > 0) lines.push(current);\n\t\t\treturn lines.join(\"\\n\");\n\t\t})\n\t\t.join(\"\\n\");\n}\n\n// Patch picocolors (used by @clack/prompts) to use Neon green instead of cyan/magenta.\n// clack hardcodes picocolors.cyan() with no theme API, so this is the least invasive override.\nimport picocolors from \"picocolors\";\n\nconst neonGreenFn = (s: string) => `\\x1b[38;2;75;181;120m${s}\\x1b[39m`;\nconst originalCyan = picocolors.cyan;\nconst originalMagenta = picocolors.magenta;\n\nfunction patchClackColors(): () => void {\n\tconst pc = picocolors as unknown as Record<string, unknown>;\n\tpc.cyan = neonGreenFn;\n\tpc.magenta = neonGreenFn;\n\treturn () => {\n\t\tpc.cyan = originalCyan;\n\t\tpc.magenta = originalMagenta;\n\t};\n}\n\nexport interface InteractiveInitOptions {\n\tpreview?: boolean;\n}\n\nexport async function interactiveInit(\n\toptions: InteractiveInitOptions = {},\n): Promise<void> {\n\tconst restoreColors = patchClackColors();\n\ttry {\n\t\tawait interactiveInitInner(options);\n\t} finally {\n\t\trestoreColors();\n\t}\n}\n\nasync function interactiveInitInner(\n\toptions: InteractiveInitOptions,\n): Promise<void> {\n\tconsole.log();\n\tconsole.log(\n\t\t\"\\x1b[38;2;75;181;120m\" +\n\t\t\t[\n\t\t\t\t\" ██╗ ██╗██████╗ ██████╗ ██╗ ██╗\",\n\t\t\t\t\" ███╗ ██║██╔═══╝ ██╔═══██╗███╗ ██║\",\n\t\t\t\t\" ████╗██║██████╗ ██║ ██║████╗██║\",\n\t\t\t\t\" ██╔████║██╔═══╝ ██║ ██║██╔████║\",\n\t\t\t\t\" ██║╚███║██████╗ ╚██████╔╝██║╚███║\",\n\t\t\t\t\" ╚═╝ ╚══╝╚═════╝ ╚═════╝ ╚═╝ ╚══╝\",\n\t\t\t].join(\"\\n\") +\n\t\t\t\"\\x1b[0m\",\n\t);\n\tconsole.log(\n\t\tdim(\n\t\t\twordWrap(\n\t\t\t\t\"\\nLet's get your project set up with Neon. We'll install the MCP server, agent skills, and IDE extension, then connect your app to a database.\\n\",\n\t\t\t\tprocess.stdout.columns || 80,\n\t\t\t),\n\t\t),\n\t);\n\n\tconst detectedAgentId = detectAgent();\n\tconst detectedEditor = detectedAgentId\n\t\t? agentIdToEditor(detectedAgentId)\n\t\t: null;\n\n\t// -----------------------------------------------------------------------\n\t// Step 1: Inspect what's already in place\n\t// -----------------------------------------------------------------------\n\tconst inspectSpinner = spinner();\n\tinspectSpinner.start(\"Checking existing configuration...\");\n\tconst inspection = await inspectProject([\n\t\t{ id: \"has_app\", description: \"\", lookFor: [] },\n\t\t{ id: \"mcp_server\", description: \"\", lookFor: [] },\n\t\t{ id: \"skills\", description: \"\", lookFor: [] },\n\t\t{ id: \"connection_string\", description: \"\", lookFor: [] },\n\t\t{ id: \"project_stack\", description: \"\", lookFor: [] },\n\t\t{ id: \"migrations\", description: \"\", lookFor: [] },\n\t\t{ id: \"ide_type\", description: \"\", lookFor: [] },\n\t]);\n\tinspectSpinner.stop(dim(\"Configuration checked ✓\"));\n\n\tconst hasApp = inspection.hasApp === true;\n\tlet selectedFeatures: NeonFeature[] = [];\n\tlet selectedTemplate: BootstrapTemplate | null = null;\n\n\t// Preview mode: bootstrap from template if no app detected\n\tif (options.preview && !hasApp) {\n\t\tlet templates = FALLBACK_TEMPLATES;\n\t\ttry {\n\t\t\tconst fetched = await fetchTemplates();\n\t\t\tif (fetched && fetched.length > 0) templates = fetched;\n\t\t} catch {}\n\n\t\tconst templateResult = await select({\n\t\t\tmessage:\n\t\t\t\t\"No application detected. Would you like to scaffold a new project from a template?\",\n\t\t\toptions: [\n\t\t\t\t...templates.map((t) => ({\n\t\t\t\t\tvalue: t.id,\n\t\t\t\t\tlabel: t.title,\n\t\t\t\t\thint: t.description,\n\t\t\t\t})),\n\t\t\t\t{\n\t\t\t\t\tvalue: \"none\",\n\t\t\t\t\tlabel: \"No thanks — continue without scaffolding\",\n\t\t\t\t},\n\t\t\t],\n\t\t\tinitialValue: templates[0]?.id ?? \"none\",\n\t\t});\n\t\tif (isCancel(templateResult)) {\n\t\t\toutro(\"Setup cancelled.\");\n\t\t\treturn;\n\t\t}\n\t\tif (templateResult !== \"none\") {\n\t\t\tselectedTemplate =\n\t\t\t\ttemplates.find((t) => t.id === templateResult) ?? null;\n\t\t\tif (selectedTemplate) {\n\t\t\t\tselectedFeatures = selectedTemplate.requires;\n\t\t\t\tconst bootstrapS = spinner();\n\t\t\t\tbootstrapS.start(\n\t\t\t\t\t`Scaffolding project from template \"${selectedTemplate.title}\"...`,\n\t\t\t\t);\n\t\t\t\ttry {\n\t\t\t\t\tawait scaffoldTemplate(selectedTemplate, \".\", {\n\t\t\t\t\t\tonWarn: (message) => log.warn(message),\n\t\t\t\t\t});\n\t\t\t\t\tbootstrapS.stop(\n\t\t\t\t\t\tdim(\n\t\t\t\t\t\t\t`Scaffolded project from \"${selectedTemplate.title}\" ✓`,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst msg =\n\t\t\t\t\t\terr instanceof Error ? err.message : \"Unknown error\";\n\t\t\t\t\tbootstrapS.stop(\"Failed to scaffold project\");\n\t\t\t\t\tlog.error(msg);\n\t\t\t\t\toutro(\"Setup failed.\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// For brownfield flows (existing app), ask which features to enable\n\tif (!selectedTemplate && hasApp) {\n\t\tconst featuresResult = await select({\n\t\t\tmessage:\n\t\t\t\t\"Which Neon features would you like to enable for this project?\",\n\t\t\toptions: [\n\t\t\t\t{ value: \"database\", label: \"Database\" },\n\t\t\t\t{\n\t\t\t\t\tvalue: \"database,auth\",\n\t\t\t\t\tlabel: \"Database + Neon Auth (adds authentication via Neon)\",\n\t\t\t\t},\n\t\t\t],\n\t\t\tinitialValue: \"database\",\n\t\t});\n\t\tif (isCancel(featuresResult)) {\n\t\t\toutro(\"Setup cancelled.\");\n\t\t\treturn;\n\t\t}\n\t\tselectedFeatures = (featuresResult as string).split(\n\t\t\t\",\",\n\t\t) as NeonFeature[];\n\t}\n\n\t// Write _init metadata to .neon\n\tif (selectedFeatures.length > 0) {\n\t\tconst neonPath = resolve(process.cwd(), \".neon\");\n\t\tlet existing: Record<string, unknown> = {};\n\t\tif (existsSync(neonPath)) {\n\t\t\ttry {\n\t\t\t\texisting = JSON.parse(readFileSync(neonPath, \"utf-8\"));\n\t\t\t} catch {}\n\t\t}\n\t\texisting._init = { features: selectedFeatures };\n\t\twriteFileSync(neonPath, `${JSON.stringify(existing, null, 2)}\\n`);\n\t}\n\n\tconst mcpAlready = inspection.mcpConfigured === true;\n\t// If we bootstrapped, skills come from the template\n\tconst skillsAlready =\n\t\tinspection.skillsInstalled === true || selectedTemplate !== null;\n\tconst hasNeonConnection = inspection.connectionString === true;\n\tlet needsMcp = !mcpAlready;\n\tconst needsSkills = !skillsAlready;\n\tconst needsInstall = needsMcp || needsSkills;\n\n\t// Check if .neon context file exists\n\tconst neonContextPath = resolve(process.cwd(), \".neon\");\n\tconst hasNeonContext =\n\t\texistsSync(neonContextPath) &&\n\t\t(() => {\n\t\t\ttry {\n\t\t\t\tconst content = JSON.parse(\n\t\t\t\t\treadFileSync(neonContextPath, \"utf-8\"),\n\t\t\t\t);\n\t\t\t\treturn !!content.projectId;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t})();\n\n\t// Check if Neon Auth is configured\n\tconst hasNeonAuth = (() => {\n\t\tfor (const envFile of [\".env\", \".env.local\"]) {\n\t\t\tconst envPath = resolve(process.cwd(), envFile);\n\t\t\tif (existsSync(envPath)) {\n\t\t\t\ttry {\n\t\t\t\t\tconst content = readFileSync(envPath, \"utf-8\");\n\t\t\t\t\tif (/^NEON_AUTH_/m.test(content)) return true;\n\t\t\t\t} catch {}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t})();\n\n\t// Check if extension is installed for the detected editor\n\tlet extensionAlready = false;\n\tif (detectedEditor && usesExtension(detectedEditor)) {\n\t\textensionAlready = await isExtensionInstalled(detectedEditor);\n\t}\n\n\t// If tooling + database are configured, check if there's anything left to do\n\tif (mcpAlready && skillsAlready && hasNeonConnection && hasNeonContext) {\n\t\tlog.step(\n\t\t\tdim(\n\t\t\t\t`Neon MCP server already configured (${inspection.mcpScope || \"detected\"}) ✓`,\n\t\t\t),\n\t\t);\n\t\tlog.step(\n\t\t\tdim(\n\t\t\t\t`Neon agent skills already installed (${inspection.skillsScope || \"detected\"}) ✓`,\n\t\t\t),\n\t\t);\n\t\tif (extensionAlready)\n\t\t\tlog.step(dim(\"Neon editor extension installed ✓\"));\n\t\tlog.step(dim(\"Neon database connected ✓\"));\n\n\t\tif (hasNeonAuth) {\n\t\t\tlog.step(dim(\"Neon Auth configured ✓\"));\n\t\t\toutro(\n\t\t\t\tdim(\n\t\t\t\t\t\"Your project is fully configured with Neon. Nothing to do.\",\n\t\t\t\t),\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\t// Neon Auth not configured — ask if they want it\n\t\tconst authResult = await select({\n\t\t\tmessage:\n\t\t\t\t\"Would you like to set up Neon Auth for user authentication?\",\n\t\t\toptions: [\n\t\t\t\t{ value: \"yes\", label: \"Yes, set up Neon Auth\" },\n\t\t\t\t{ value: \"no\", label: \"No, skip for now\" },\n\t\t\t],\n\t\t\tinitialValue: \"no\",\n\t\t});\n\n\t\tif (isCancel(authResult) || authResult === \"no\") {\n\t\t\toutro(\n\t\t\t\tdim(\n\t\t\t\t\t`Your project is configured with Neon. You can set up Neon Auth later by having your agent run: neonctl init --agent --data '{\"step\":\"neon-auth\"}'`,\n\t\t\t\t),\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\t// Read .neon for project context\n\t\tlet projectId: string | null = null;\n\t\ttry {\n\t\t\tconst neonCtx = JSON.parse(readFileSync(neonContextPath, \"utf-8\"));\n\t\t\tprojectId = neonCtx.projectId ?? null;\n\t\t} catch {}\n\n\t\tlog.step(\"Next steps\");\n\t\tconst promptLines = [\"Set up Neon Auth for this project.\"];\n\t\tif (projectId) promptLines.push(`Project ID: ${projectId}.`);\n\t\tlog.message(dim(\"Copy the following into your agent chat:\"));\n\t\tlog.message(\n\t\t\tpromptLines.map((line) => bold(neonGreenFn(line))).join(\"\\n\"),\n\t\t);\n\t\toutro(dim(\"Have feedback? Email us at feedback@neon.tech\"));\n\t\treturn;\n\t}\n\n\t// Log what's already in place\n\tif (mcpAlready)\n\t\tlog.step(\n\t\t\tdim(\n\t\t\t\t`Neon MCP server already configured (${inspection.mcpScope || \"detected\"}) ✓`,\n\t\t\t),\n\t\t);\n\tif (skillsAlready)\n\t\tlog.step(\n\t\t\tdim(\n\t\t\t\t`Neon agent skills already installed (${inspection.skillsScope || \"detected\"}) ✓`,\n\t\t\t),\n\t\t);\n\n\t// -----------------------------------------------------------------------\n\t// Step 3–5: Install what's missing (skip entirely if everything is configured)\n\t// -----------------------------------------------------------------------\n\tif (needsInstall) {\n\t\tconst homeDir = process.env.HOME || process.env.USERPROFILE;\n\t\tif (!homeDir) {\n\t\t\tlog.error(\"Could not determine home directory.\");\n\t\t\toutro(\"Setup failed.\");\n\t\t\treturn;\n\t\t}\n\n\t\tlet selectedEditors: Editor[];\n\t\tif (detectedEditor) {\n\t\t\tselectedEditors = [detectedEditor];\n\t\t} else {\n\t\t\tconst availableEditors = await detectAvailableEditors(homeDir);\n\t\t\tconst editorResponse = await multiselect({\n\t\t\t\tmessage: \"Which editor(s) would you like to configure?\",\n\t\t\t\toptions: ALL_CONFIGURABLE_AGENTS.map((agent) => ({\n\t\t\t\t\tvalue: agent.editor,\n\t\t\t\t\tlabel: agent.editor,\n\t\t\t\t\thint: agent.hint,\n\t\t\t\t})),\n\t\t\t\tinitialValues: availableEditors,\n\t\t\t\trequired: true,\n\t\t\t});\n\t\t\tif (isCancel(editorResponse)) {\n\t\t\t\toutro(\"Setup cancelled.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tselectedEditors = editorResponse as Editor[];\n\t\t\tif (selectedEditors.length === 0) {\n\t\t\t\tlog.warn(\"No editors selected.\");\n\t\t\t\toutro(\"Setup cancelled.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// Check extension status\n\t\tconst vscodeEditors = selectedEditors.filter(usesExtension);\n\t\tlet extensionAlreadyInstalled = false;\n\t\tif (vscodeEditors.length > 0) {\n\t\t\tconst checks = await Promise.all(\n\t\t\t\tvscodeEditors.map((e) => isExtensionInstalled(e)),\n\t\t\t);\n\t\t\textensionAlreadyInstalled = checks.every(Boolean);\n\t\t\tif (extensionAlreadyInstalled) {\n\t\t\t\tlog.step(dim(\"Neon editor extension already installed ✓\"));\n\t\t\t}\n\t\t}\n\t\tlet doInstallExtension =\n\t\t\tvscodeEditors.length > 0 && !extensionAlreadyInstalled;\n\n\t\t// Build hint showing only what needs installing\n\t\tconst hintParts: string[] = [];\n\t\tif (needsMcp) hintParts.push(\"MCP server (global)\");\n\t\tif (needsSkills) hintParts.push(\"agent skills (project)\");\n\t\tif (doInstallExtension) hintParts.push(\"editor extension\");\n\n\t\t// Installation preferences\n\t\tlet mcpScope: \"global\" | \"project\" | \"none\" = \"global\";\n\t\tlet skillsScope: \"global\" | \"project\" = \"project\";\n\n\t\tlet modeResult: string;\n\t\twhile (true) {\n\t\t\tconst editorName = selectedEditors.join(\", \");\n\t\t\tconst result = await select({\n\t\t\t\tmessage: `Configure ${editorName} for Neon:`,\n\t\t\t\toptions: [\n\t\t\t\t\t{\n\t\t\t\t\t\tvalue: \"defaults\",\n\t\t\t\t\t\tlabel: \"Install with defaults\",\n\t\t\t\t\t\thint: hintParts.join(\", \"),\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tvalue: \"customize\",\n\t\t\t\t\t\tlabel: \"Customize installation\",\n\t\t\t\t\t\thint: \"choose scopes and options\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tvalue: \"change_editor\",\n\t\t\t\t\t\tlabel: \"Configure a different editor\",\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tinitialValue: \"defaults\",\n\t\t\t});\n\n\t\t\tif (isCancel(result)) {\n\t\t\t\toutro(\"Setup cancelled.\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (result === \"change_editor\") {\n\t\t\t\tconst availableEditors = await detectAvailableEditors(homeDir);\n\t\t\t\tconst editorResponse = await multiselect({\n\t\t\t\t\tmessage: \"Which editor(s) would you like to configure?\",\n\t\t\t\t\toptions: ALL_CONFIGURABLE_AGENTS.map((agent) => ({\n\t\t\t\t\t\tvalue: agent.editor,\n\t\t\t\t\t\tlabel: agent.editor,\n\t\t\t\t\t\thint: agent.hint,\n\t\t\t\t\t})),\n\t\t\t\t\tinitialValues: availableEditors,\n\t\t\t\t\trequired: true,\n\t\t\t\t});\n\t\t\t\tif (isCancel(editorResponse)) {\n\t\t\t\t\toutro(\"Setup cancelled.\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tselectedEditors = editorResponse as Editor[];\n\t\t\t\tif (selectedEditors.length === 0) {\n\t\t\t\t\toutro(\"Setup cancelled.\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tmodeResult = result as string;\n\t\t\tbreak;\n\t\t}\n\n\t\tif (modeResult === \"customize\") {\n\t\t\tif (needsMcp) {\n\t\t\t\tconst scopeResult = await select({\n\t\t\t\t\tmessage: \"Where should the Neon MCP server be configured?\",\n\t\t\t\t\toptions: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvalue: \"global\",\n\t\t\t\t\t\t\tlabel: \"Global (available in all projects)\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvalue: \"project\",\n\t\t\t\t\t\t\tlabel: \"Project-level (this project only)\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvalue: \"none\",\n\t\t\t\t\t\t\tlabel: \"Skip — do not install the MCP server\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t});\n\t\t\t\tif (isCancel(scopeResult)) {\n\t\t\t\t\toutro(\"Setup cancelled.\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tmcpScope = scopeResult as \"global\" | \"project\" | \"none\";\n\t\t\t\tif (mcpScope === \"none\") needsMcp = false;\n\t\t\t}\n\n\t\t\tif (needsSkills) {\n\t\t\t\tconst skillsScopeResult = await select({\n\t\t\t\t\tmessage: \"Where should Neon agent skills be installed?\",\n\t\t\t\t\toptions: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvalue: \"global\",\n\t\t\t\t\t\t\tlabel: \"Global (available in all projects)\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvalue: \"project\",\n\t\t\t\t\t\t\tlabel: \"Project-level (this project only)\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tinitialValue: \"project\",\n\t\t\t\t});\n\t\t\t\tif (isCancel(skillsScopeResult)) {\n\t\t\t\t\toutro(\"Setup cancelled.\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tskillsScope = skillsScopeResult as \"global\" | \"project\";\n\t\t\t}\n\n\t\t\tif (doInstallExtension) {\n\t\t\t\tconst extResult = await confirm({\n\t\t\t\t\tmessage: `Install the Neon extension for ${vscodeEditors.join(\", \")}?`,\n\t\t\t\t});\n\t\t\t\tif (isCancel(extResult)) {\n\t\t\t\t\toutro(\"Setup cancelled.\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tdoInstallExtension = extResult;\n\t\t\t}\n\t\t}\n\n\t\t// Auth check before install\n\t\tconst installAuthed = await isAuthenticated();\n\t\tif (!installAuthed) {\n\t\t\tconst authS = spinner();\n\t\t\tauthS.start(\"Authenticating with Neon...\");\n\t\t\tconst authSuccess = await ensureNeonctlAuth();\n\t\t\tif (!authSuccess) {\n\t\t\t\tauthS.stop(\"Authentication failed.\");\n\t\t\t\toutro(\"Run neon-init again after signing in.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tauthS.stop(\"Authenticated.\");\n\t\t}\n\n\t\t// Ensure neonctl CLI is installed and up to date\n\t\tconst nctlS = spinner();\n\t\tnctlS.start(\"Checking neonctl CLI...\");\n\t\tconst nctlResult = await ensureNeonctl();\n\t\tswitch (nctlResult.status) {\n\t\t\tcase \"already_current\":\n\t\t\t\tnctlS.stop(\n\t\t\t\t\tdim(`neonctl CLI is up to date (v${nctlResult.version}) ✓`),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"installed\":\n\t\t\t\tnctlS.stop(\n\t\t\t\t\tdim(`Installed neonctl CLI (v${nctlResult.version}) ✓`),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"updated\":\n\t\t\t\tnctlS.stop(\n\t\t\t\t\tdim(`Updated neonctl CLI to v${nctlResult.version} ✓`),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"failed\":\n\t\t\t\tnctlS.stop(\"Failed to install neonctl CLI\");\n\t\t\t\tlog.warn(\n\t\t\t\t\t\"neonctl could not be installed automatically. The setup will continue using npx.\",\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t}\n\n\t\t// Install only what's missing\n\t\tfor (const editor of selectedEditors) {\n\t\t\tif (needsMcp) {\n\t\t\t\tconst mcpAgentId = getAddMcpAgentId(editor);\n\t\t\t\tconst mcpArgs = [\n\t\t\t\t\t\"-y\",\n\t\t\t\t\t\"add-mcp\",\n\t\t\t\t\t\"https://mcp.neon.tech/mcp\",\n\t\t\t\t\t\"-n\",\n\t\t\t\t\t\"Neon\",\n\t\t\t\t\t\"-y\",\n\t\t\t\t\t\"-a\",\n\t\t\t\t\tmcpAgentId,\n\t\t\t\t];\n\t\t\t\tif (mcpScope === \"global\") mcpArgs.splice(5, 0, \"-g\");\n\n\t\t\t\tconst mcpS = spinner();\n\t\t\t\tmcpS.start(`Installing Neon MCP server for ${editor}...`);\n\t\t\t\ttry {\n\t\t\t\t\tawait execa(\"npx\", mcpArgs, {\n\t\t\t\t\t\tstdio: \"pipe\",\n\t\t\t\t\t\ttimeout: 60000,\n\t\t\t\t\t});\n\t\t\t\t\tmcpS.stop(\n\t\t\t\t\t\tdim(\n\t\t\t\t\t\t\t`Neon MCP server configured for ${editor} (${mcpScope}) ✓`,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst msg =\n\t\t\t\t\t\terr instanceof Error ? err.message : \"Unknown error\";\n\t\t\t\t\tmcpS.stop(`Failed to configure MCP server for ${editor}`);\n\t\t\t\t\tlog.error(msg);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (needsSkills) {\n\t\t\t\tawait installAgentSkills([editor], {\n\t\t\t\t\tscope: skillsScope,\n\t\t\t\t\tpreview: options.preview,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (doInstallExtension && usesExtension(editor)) {\n\t\t\t\tconst extS = spinner();\n\t\t\t\textS.start(`Installing Neon extension for ${editor}...`);\n\t\t\t\tconst extOk = await installExtension(editor);\n\t\t\t\tif (extOk) {\n\t\t\t\t\textS.stop(dim(`Neon extension installed for ${editor} ✓`));\n\t\t\t\t} else {\n\t\t\t\t\textS.stop(\n\t\t\t\t\t\t`Extension install failed — install manually from the extensions panel.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Ensure all required skills are present (fills in any missing ones).\n\t// detectAgent() returns null in a human terminal (TTY), so fall back\n\t// to IDE detection which works regardless of TTY.\n\tconst ide = detectIde();\n\tconst agentForSkills =\n\t\tdetectAgent() ??\n\t\t(ide === \"Cursor\"\n\t\t\t? \"cursor\"\n\t\t\t: ide === \"VS Code\"\n\t\t\t\t? \"vscode\"\n\t\t\t\t: ide === \"Windsurf\"\n\t\t\t\t\t? \"windsurf\"\n\t\t\t\t\t: null);\n\tif (agentForSkills) {\n\t\tconst detectedSkillsScope =\n\t\t\tinspection.skillsScope === \"global\" ? \"global\" : undefined;\n\t\tawait ensureSkillsUpToDate(\n\t\t\tagentForSkills,\n\t\t\tdetectedSkillsScope,\n\t\t\toptions.preview,\n\t\t);\n\t}\n\n\t// -----------------------------------------------------------------------\n\t// Step 6: Done — build prompt for the agent to continue\n\t// -----------------------------------------------------------------------\n\n\t// Build the getting-started data payload (same as agent mode)\n\tconst gettingStartedData: Record<string, unknown> = {};\n\tif (hasNeonConnection) gettingStartedData.hasConnectionString = true;\n\tif (inspection.framework && inspection.framework !== \"none\")\n\t\tgettingStartedData.framework = inspection.framework;\n\tif (inspection.orm && inspection.orm !== \"none\")\n\t\tgettingStartedData.orm = inspection.orm;\n\tif (inspection.migrationTool && inspection.migrationTool !== \"none\")\n\t\tgettingStartedData.migrationTool = inspection.migrationTool;\n\tif (inspection.migrationDir && inspection.migrationDir !== \"none\")\n\t\tgettingStartedData.migrationDir = inspection.migrationDir;\n\tif (selectedFeatures.length > 0)\n\t\tgettingStartedData.features = selectedFeatures;\n\tif (options.preview) gettingStartedData.preview = true;\n\n\t// Build a prompt for the user to paste into their agent chat\n\tconst cmd = `neonctl init --agent --data '${JSON.stringify({ step: \"getting-started\", ...gettingStartedData })}'`;\n\t// Account for clack's \"│ \" prefix (3 chars) when wrapping\n\tconst cols = (process.stdout.columns || 80) - 3;\n\tconst promptText = `To finish setting up Neon using Neon's agent-guided onboarding experience, have your agent run this shell command: ${cmd}`;\n\n\tlog.step(\"Next steps\");\n\tlog.message(dim(\"Copy the following into your agent chat:\"));\n\tlog.message(\n\t\twordWrap(promptText, cols)\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => bold(neonGreenFn(line)))\n\t\t\t.join(\"\\n\"),\n\t);\n\toutro(dim(\"Have feedback? Email us at feedback@neon.tech\"));\n}\n\nfunction agentIdToEditor(agentId: string): Editor | null {\n\tswitch (agentId) {\n\t\tcase \"cursor\":\n\t\t\treturn \"Cursor\";\n\t\tcase \"vscode\":\n\t\t\treturn \"VS Code\";\n\t\tcase \"claude-code\":\n\t\t\treturn \"Claude CLI\";\n\t\tcase \"windsurf\":\n\t\t\t// Windsurf not in Editor type yet — fall back to prompt\n\t\t\treturn null;\n\t\tcase \"codex\":\n\t\t\treturn \"Codex\";\n\t\tcase \"cline\":\n\t\t\treturn \"Cline\";\n\t\tdefault:\n\t\t\treturn null;\n\t}\n}\n"],"mappings":"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"}
|
package/dist/lib/bootstrap.d.ts
CHANGED
|
@@ -1,19 +1,42 @@
|
|
|
1
1
|
//#region src/lib/bootstrap.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A scaffold template that lives in a subdirectory of a public GitHub repo we
|
|
4
|
+
* control. Bootstrapping copies that subdirectory into a target folder —
|
|
5
|
+
* conceptually the same as `degit user/repo/subdir`, but implemented in-house.
|
|
6
|
+
*
|
|
7
|
+
* The whole template is pulled in a single request: we download the repo's
|
|
8
|
+
* gzipped tarball from `codeload.github.com` and extract only the subdir we
|
|
9
|
+
* want. That endpoint is unauthenticated and is NOT subject to the 60-requests
|
|
10
|
+
* per-hour limit of the REST API (`api.github.com`), so bootstrapping works out
|
|
11
|
+
* of the box on shared/corporate networks without a GITHUB_TOKEN. We lean on
|
|
12
|
+
* `fflate` for gunzip and parse the tar in-house, so we never pull in a heavy
|
|
13
|
+
* dependency tree just to copy a few files.
|
|
14
|
+
*/
|
|
2
15
|
/**
|
|
3
16
|
* Neon features that a template or project may require.
|
|
4
17
|
* Each feature maps to a setup phase that the orchestrator can run.
|
|
5
18
|
*/
|
|
6
19
|
type NeonFeature = "database" | "auth" | "functions" | "ai-gateway" | "object-storage";
|
|
7
20
|
interface BootstrapTemplate {
|
|
21
|
+
/** Stable id used by `--template` and analytics. */
|
|
8
22
|
id: string;
|
|
23
|
+
/** Human label shown in the interactive selector. */
|
|
9
24
|
title: string;
|
|
25
|
+
/** One-line description shown under the title in the selector. */
|
|
10
26
|
description: string;
|
|
11
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Neon services the template uses (e.g. "Postgres", "Functions"). Shown as a
|
|
29
|
+
* badge next to the title in the picker. Optional — older manifests omit it.
|
|
30
|
+
*/
|
|
31
|
+
services?: string[];
|
|
32
|
+
/** Neon features this template needs (defaults to ["database"]). */
|
|
12
33
|
requires: NeonFeature[];
|
|
13
34
|
source: {
|
|
14
35
|
owner: string;
|
|
15
36
|
repo: string;
|
|
37
|
+
/** Branch (or tag) the template is pulled from. */
|
|
16
38
|
ref: string;
|
|
39
|
+
/** Subdirectory within the repo to copy (no leading/trailing slash). */
|
|
17
40
|
subdir: string;
|
|
18
41
|
};
|
|
19
42
|
}
|
|
@@ -24,6 +47,21 @@ interface BootstrapTemplate {
|
|
|
24
47
|
* the full set of starters rather than a single template.
|
|
25
48
|
*/
|
|
26
49
|
declare const FALLBACK_TEMPLATES: BootstrapTemplate[];
|
|
50
|
+
declare const templateIds: (templates: BootstrapTemplate[]) => string;
|
|
51
|
+
declare const findTemplate: (templates: BootstrapTemplate[], id: string) => BootstrapTemplate | undefined;
|
|
52
|
+
/** A single file or symlink to materialize, already resolved with its bytes. */
|
|
53
|
+
type TemplateFile = {
|
|
54
|
+
kind: "file";
|
|
55
|
+
/** Path relative to the target directory (subdir prefix stripped). */
|
|
56
|
+
path: string;
|
|
57
|
+
bytes: Buffer;
|
|
58
|
+
executable: boolean;
|
|
59
|
+
} | {
|
|
60
|
+
kind: "symlink";
|
|
61
|
+
path: string;
|
|
62
|
+
/** The (relative) link target. */
|
|
63
|
+
target: string;
|
|
64
|
+
};
|
|
27
65
|
declare function parseManifest(text: string): BootstrapTemplate[];
|
|
28
66
|
/**
|
|
29
67
|
* Fetch the template manifest, trying each source in {@link manifestUrls} in
|
|
@@ -32,6 +70,70 @@ declare function parseManifest(text: string): BootstrapTemplate[];
|
|
|
32
70
|
* picker never fails just because a host is down.
|
|
33
71
|
*/
|
|
34
72
|
declare function fetchTemplates(): Promise<BootstrapTemplate[]>;
|
|
73
|
+
/** A raw entry decoded from a tar stream, before subdir filtering. */
|
|
74
|
+
type TarEntry = {
|
|
75
|
+
/** Full path as stored in the archive (includes the top-level dir). */
|
|
76
|
+
name: string;
|
|
77
|
+
/** POSIX type flag: '0' file, '5' directory, '2' symlink, etc. */
|
|
78
|
+
type: string;
|
|
79
|
+
/** File permission bits. */
|
|
80
|
+
mode: number;
|
|
81
|
+
/** Symlink target (for type '2'). */
|
|
82
|
+
linkname: string;
|
|
83
|
+
/** File contents (for type '0'). */
|
|
84
|
+
data: Buffer;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Decode a (decompressed) tar archive into its file/symlink entries. Pure and
|
|
88
|
+
* dependency-free so it can be unit tested without touching the network.
|
|
89
|
+
* Handles the ustar `prefix` field, pax extended headers (type 'x'/'g'), and
|
|
90
|
+
* GNU long-name/long-link headers (type 'L'/'K') so deep template paths and
|
|
91
|
+
* long symlink targets round-trip correctly.
|
|
92
|
+
*/
|
|
93
|
+
declare const parseTar: (buf: Buffer) => TarEntry[];
|
|
94
|
+
/**
|
|
95
|
+
* Map decoded tar entries to the files under `subdir`, with the top-level
|
|
96
|
+
* archive directory and the `subdir/` prefix stripped from each path. Pure so
|
|
97
|
+
* it can be unit tested. Directory and other non-regular entries are dropped —
|
|
98
|
+
* writing files re-creates their parent directories.
|
|
99
|
+
*/
|
|
100
|
+
declare const selectTemplateFiles: (entries: TarEntry[], subdir: string) => TemplateFile[];
|
|
101
|
+
/**
|
|
102
|
+
* Download a template and resolve it to the exact set of files to write. The
|
|
103
|
+
* entire subtree is captured in one tarball request, so the copy is atomically
|
|
104
|
+
* consistent: a push to the template repo mid-download cannot produce a
|
|
105
|
+
* mismatched checkout (unlike fetching a file list and then each blob).
|
|
106
|
+
*/
|
|
107
|
+
declare const downloadTemplate: (template: BootstrapTemplate) => Promise<TemplateFile[]>;
|
|
108
|
+
/**
|
|
109
|
+
* A bad caller-supplied input that an agent (or human) can correct: an unknown
|
|
110
|
+
* template id or a non-empty target directory. Carries an `agentCode` so an
|
|
111
|
+
* agent surface can report a precise error code instead of a generic
|
|
112
|
+
* INTERNAL_ERROR, while a human path just surfaces the clear `message`.
|
|
113
|
+
*/
|
|
114
|
+
declare class BootstrapInputError extends Error {
|
|
115
|
+
readonly agentCode: string;
|
|
116
|
+
constructor(message: string, agentCode: string);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Ensure `dir` is safe to scaffold into: it must be missing, or an empty
|
|
120
|
+
* directory (a lone `.git` is ignored so you can scaffold into a freshly
|
|
121
|
+
* `git init`ed folder). `force` allows scaffolding into a non-empty directory,
|
|
122
|
+
* overwriting colliding files. Throws a {@link BootstrapInputError} otherwise.
|
|
123
|
+
*/
|
|
124
|
+
declare const ensureTargetUsable: (dir: string, force: boolean) => void;
|
|
125
|
+
interface ScaffoldOptions {
|
|
126
|
+
/** Called for non-fatal warnings (e.g. a symlink that fell back to a file). */
|
|
127
|
+
onWarn?: (message: string) => void;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Download `template` and materialize its files into `targetDir`, creating
|
|
131
|
+
* parent directories, preserving executable bits, and recreating symlinks
|
|
132
|
+
* (with a graceful regular-file fallback on platforms that disallow them).
|
|
133
|
+
* Returns the number of files written. The caller is responsible for any
|
|
134
|
+
* target validation ({@link ensureTargetUsable}) and user-facing progress.
|
|
135
|
+
*/
|
|
136
|
+
declare const scaffoldTemplate: (template: BootstrapTemplate, targetDir: string, options?: ScaffoldOptions) => Promise<number>;
|
|
35
137
|
//#endregion
|
|
36
|
-
export { BootstrapTemplate, FALLBACK_TEMPLATES, NeonFeature, fetchTemplates, parseManifest };
|
|
138
|
+
export { BootstrapInputError, BootstrapTemplate, FALLBACK_TEMPLATES, NeonFeature, ScaffoldOptions, TemplateFile, downloadTemplate, ensureTargetUsable, fetchTemplates, findTemplate, parseManifest, parseTar, scaffoldTemplate, selectTemplateFiles, templateIds };
|
|
37
139
|
//# sourceMappingURL=bootstrap.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bootstrap.d.ts","names":[],"sources":["../../src/lib/bootstrap.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"bootstrap.d.ts","names":[],"sources":["../../src/lib/bootstrap.ts"],"mappings":";;AAiCA;AAUA;AA8BA;AA6CA;AAGA;;;;AAGoB;AAGpB;AAkEA;AAoDA;;;;AAA+C;AAsB1C,KA1OO,WAAA,GAoPL,UAAM,GAAA,MAAA,GAAA,WAAA,GAAA,YAAA,GAAA,gBAAA;AAuDA,UAjSI,iBAAA,CAqVhB;EAAA;MApD6B,MAAA;;EAAiB,KAAA,EAAA,MAAA;EA4DlC;EA4BZ,WAAA,EAAA,MAAA;;;AAzBc;AAoDf;EAoCC,QAAA,CAAA,EAAA,MAAA,EAAA;;UAlCU,EAzYA,WAyYA,EAAA;QAAR,EAAA;IAAO,KAAA,EAAA,MAAA;IA8CG,IAAA,EAAA,MAAA;IAeA;IA4DI,GAAA,EAAA,MAAA;IAYJ;IAmBZ,MAAA,EAAA,MAAA;;;;AAfS;;;;;cAjgBG,oBAAoB;cA6CpB,yBAA0B;cAG1B,0BACD,oCAET;;KAGS,YAAA;;;;SAKF;;;;;;;;iBA6DM,aAAA,gBAA6B;;;;;;;iBAoDvB,cAAA,CAAA,GAAkB,QAAQ;;KAsB3C,QAAA;;;;;;;;;;QAUE;;;;;;;;;cAuDM,gBAAiB,WAAS;;;;;;;cA4D1B,+BACH,+BAEP;;;;;;;cAoDU,6BACF,sBACR,QAAQ;;;;;;;cA8CE,mBAAA,SAA4B,KAAK;;;;;;;;;;cAejC;UA4DI,eAAA;;;;;;;;;;;cAYJ,6BACF,gDAED,oBACP"}
|
package/dist/lib/bootstrap.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { gunzipSync } from "fflate";
|
|
1
4
|
import YAML from "yaml";
|
|
2
5
|
//#region src/lib/bootstrap.ts
|
|
3
6
|
/** Default features when a template doesn't specify `requires`. */
|
|
@@ -13,6 +16,7 @@ const FALLBACK_TEMPLATES = [
|
|
|
13
16
|
id: "hono",
|
|
14
17
|
title: "Hono API (Drizzle, Neon Postgres) on Neon Functions",
|
|
15
18
|
description: "A Hono API using Drizzle ORM and Neon Postgres, ready to deploy as a Neon Function.",
|
|
19
|
+
services: ["Postgres", "Functions"],
|
|
16
20
|
requires: ["database", "functions"],
|
|
17
21
|
source: {
|
|
18
22
|
owner: "neondatabase",
|
|
@@ -25,6 +29,12 @@ const FALLBACK_TEMPLATES = [
|
|
|
25
29
|
id: "ai-sdk",
|
|
26
30
|
title: "AI SDK agent (AI Gateway, object storage, Drizzle) on Neon Functions",
|
|
27
31
|
description: "A Vercel AI SDK agent on Neon Functions: streams chat through the Neon AI Gateway, generates an image with OpenAI image generation, and stores it in Neon object storage indexed in Postgres via Drizzle.",
|
|
32
|
+
services: [
|
|
33
|
+
"Postgres",
|
|
34
|
+
"Functions",
|
|
35
|
+
"Object Storage",
|
|
36
|
+
"AI Gateway"
|
|
37
|
+
],
|
|
28
38
|
requires: [
|
|
29
39
|
"database",
|
|
30
40
|
"functions",
|
|
@@ -42,6 +52,11 @@ const FALLBACK_TEMPLATES = [
|
|
|
42
52
|
id: "mastra",
|
|
43
53
|
title: "Mastra personal agent (AI Gateway, Mastra Memory) on Neon Functions",
|
|
44
54
|
description: "A Mastra personal-assistant agent on Neon Functions: streams chat through the Neon AI Gateway and uses Mastra Memory — backed by Neon Postgres — to remember the user across conversation threads via resource-scoped working memory.",
|
|
55
|
+
services: [
|
|
56
|
+
"Postgres",
|
|
57
|
+
"Functions",
|
|
58
|
+
"AI Gateway"
|
|
59
|
+
],
|
|
45
60
|
requires: [
|
|
46
61
|
"database",
|
|
47
62
|
"functions",
|
|
@@ -55,6 +70,26 @@ const FALLBACK_TEMPLATES = [
|
|
|
55
70
|
}
|
|
56
71
|
}
|
|
57
72
|
];
|
|
73
|
+
const templateIds = (templates) => templates.map((t) => t.id).join(", ");
|
|
74
|
+
const findTemplate = (templates, id) => templates.find((t) => t.id === id);
|
|
75
|
+
const githubToken = () => process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN ?? "";
|
|
76
|
+
const downloadHeaders = () => ({
|
|
77
|
+
"User-Agent": "neon-init",
|
|
78
|
+
...githubToken() ? { Authorization: `Bearer ${githubToken()}` } : {}
|
|
79
|
+
});
|
|
80
|
+
const codeloadBase = () => process.env.NEON_BOOTSTRAP_GITHUB_CODELOAD ?? "https://codeload.github.com";
|
|
81
|
+
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
82
|
+
/**
|
|
83
|
+
* Normalize a manifest entry's `services` into a clean string list. Tolerant by
|
|
84
|
+
* design: a missing or non-array value yields `undefined`, and non-string items
|
|
85
|
+
* are dropped, so a malformed `services` never sinks an otherwise-valid
|
|
86
|
+
* template (it just renders without its badge).
|
|
87
|
+
*/
|
|
88
|
+
const parseServices = (value) => {
|
|
89
|
+
if (!Array.isArray(value)) return void 0;
|
|
90
|
+
const services = value.filter((item) => typeof item === "string" && item.trim() !== "");
|
|
91
|
+
return services.length > 0 ? services : void 0;
|
|
92
|
+
};
|
|
58
93
|
const NEON_MANIFEST_URL = "https://neon.com/bootstrap/templates.yaml";
|
|
59
94
|
const GITHUB_RAW_MANIFEST_URL = "https://raw.githubusercontent.com/neondatabase/examples/main/bootstrap.yaml";
|
|
60
95
|
function manifestUrls() {
|
|
@@ -62,7 +97,6 @@ function manifestUrls() {
|
|
|
62
97
|
if (override) return [override];
|
|
63
98
|
return [NEON_MANIFEST_URL, GITHUB_RAW_MANIFEST_URL];
|
|
64
99
|
}
|
|
65
|
-
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
66
100
|
function parseManifest(text) {
|
|
67
101
|
const data = YAML.parse(text);
|
|
68
102
|
if (!isRecord(data) || !Array.isArray(data.templates)) throw new Error("Invalid bootstrap manifest: missing \"templates\" array.");
|
|
@@ -70,10 +104,12 @@ function parseManifest(text) {
|
|
|
70
104
|
for (const item of data.templates) {
|
|
71
105
|
if (!isRecord(item) || typeof item.id !== "string" || typeof item.title !== "string" || typeof item.description !== "string" || !isRecord(item.source) || typeof item.source.owner !== "string" || typeof item.source.repo !== "string" || typeof item.source.ref !== "string" || typeof item.source.subdir !== "string") continue;
|
|
72
106
|
const requires = Array.isArray(item.requires) && item.requires.every((r) => typeof r === "string") ? item.requires : DEFAULT_REQUIRES;
|
|
107
|
+
const services = parseServices(item.services);
|
|
73
108
|
templates.push({
|
|
74
109
|
id: item.id,
|
|
75
110
|
title: item.title,
|
|
76
111
|
description: item.description,
|
|
112
|
+
...services ? { services } : {},
|
|
77
113
|
requires,
|
|
78
114
|
source: {
|
|
79
115
|
owner: item.source.owner,
|
|
@@ -93,14 +129,247 @@ function parseManifest(text) {
|
|
|
93
129
|
*/
|
|
94
130
|
async function fetchTemplates() {
|
|
95
131
|
for (const url of manifestUrls()) try {
|
|
96
|
-
const res = await fetch(url, {
|
|
132
|
+
const res = await fetch(url, {
|
|
133
|
+
headers: downloadHeaders(),
|
|
134
|
+
signal: AbortSignal.timeout(1e4)
|
|
135
|
+
});
|
|
97
136
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
98
137
|
const templates = parseManifest(await res.text());
|
|
99
138
|
if (templates.length > 0) return templates;
|
|
100
139
|
} catch {}
|
|
101
140
|
return FALLBACK_TEMPLATES;
|
|
102
141
|
}
|
|
142
|
+
const TAR_BLOCK = 512;
|
|
143
|
+
const readTarString = (buf, offset, length) => {
|
|
144
|
+
let end = offset;
|
|
145
|
+
const max = offset + length;
|
|
146
|
+
while (end < max && buf[end] !== 0) end++;
|
|
147
|
+
return buf.toString("utf8", offset, end);
|
|
148
|
+
};
|
|
149
|
+
const readTarOctal = (buf, offset, length) => {
|
|
150
|
+
const text = readTarString(buf, offset, length).trim();
|
|
151
|
+
if (text === "") return 0;
|
|
152
|
+
const value = parseInt(text, 8);
|
|
153
|
+
return Number.isNaN(value) ? 0 : value;
|
|
154
|
+
};
|
|
155
|
+
const isZeroBlock = (buf, offset) => {
|
|
156
|
+
for (let i = offset; i < offset + TAR_BLOCK; i++) if (buf[i] !== 0) return false;
|
|
157
|
+
return true;
|
|
158
|
+
};
|
|
159
|
+
/**
|
|
160
|
+
* Parse pax extended-header records ("<len> <key>=<value>\n"). GitHub uses
|
|
161
|
+
* these for the global header and for any path that doesn't fit the legacy
|
|
162
|
+
* 100-byte name field, so we must honor at least `path` and `linkpath`.
|
|
163
|
+
*/
|
|
164
|
+
const parsePaxRecords = (data) => {
|
|
165
|
+
const records = {};
|
|
166
|
+
let pos = 0;
|
|
167
|
+
const text = data.toString("utf8");
|
|
168
|
+
while (pos < text.length) {
|
|
169
|
+
const space = text.indexOf(" ", pos);
|
|
170
|
+
if (space === -1) break;
|
|
171
|
+
const len = parseInt(text.slice(pos, space), 10);
|
|
172
|
+
if (Number.isNaN(len) || len <= 0) break;
|
|
173
|
+
const record = text.slice(space + 1, pos + len - 1);
|
|
174
|
+
const eq = record.indexOf("=");
|
|
175
|
+
if (eq !== -1) records[record.slice(0, eq)] = record.slice(eq + 1);
|
|
176
|
+
pos += len;
|
|
177
|
+
}
|
|
178
|
+
return records;
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* Decode a (decompressed) tar archive into its file/symlink entries. Pure and
|
|
182
|
+
* dependency-free so it can be unit tested without touching the network.
|
|
183
|
+
* Handles the ustar `prefix` field, pax extended headers (type 'x'/'g'), and
|
|
184
|
+
* GNU long-name/long-link headers (type 'L'/'K') so deep template paths and
|
|
185
|
+
* long symlink targets round-trip correctly.
|
|
186
|
+
*/
|
|
187
|
+
const parseTar = (buf) => {
|
|
188
|
+
const entries = [];
|
|
189
|
+
let overridePath;
|
|
190
|
+
let overrideLink;
|
|
191
|
+
let offset = 0;
|
|
192
|
+
while (offset + TAR_BLOCK <= buf.length) {
|
|
193
|
+
if (isZeroBlock(buf, offset)) break;
|
|
194
|
+
let name = readTarString(buf, offset, 100);
|
|
195
|
+
const mode = readTarOctal(buf, offset + 100, 8);
|
|
196
|
+
const size = readTarOctal(buf, offset + 124, 12);
|
|
197
|
+
const typeByte = buf[offset + 156];
|
|
198
|
+
const type = typeByte === 0 ? "0" : String.fromCharCode(typeByte);
|
|
199
|
+
let linkname = readTarString(buf, offset + 157, 100);
|
|
200
|
+
if (readTarString(buf, offset + 257, 6).startsWith("ustar")) {
|
|
201
|
+
const prefix = readTarString(buf, offset + 345, 155);
|
|
202
|
+
if (prefix !== "") name = `${prefix}/${name}`;
|
|
203
|
+
}
|
|
204
|
+
offset += TAR_BLOCK;
|
|
205
|
+
const data = buf.subarray(offset, offset + size);
|
|
206
|
+
offset += Math.ceil(size / TAR_BLOCK) * TAR_BLOCK;
|
|
207
|
+
if (type === "x") {
|
|
208
|
+
const records = parsePaxRecords(data);
|
|
209
|
+
if (records.path !== void 0) overridePath = records.path;
|
|
210
|
+
if (records.linkpath !== void 0) overrideLink = records.linkpath;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (type === "g") continue;
|
|
214
|
+
if (type === "L" || type === "K") {
|
|
215
|
+
const longValue = data.toString("utf8").replace(/\0+$/, "");
|
|
216
|
+
if (type === "L") overridePath = longValue;
|
|
217
|
+
else overrideLink = longValue;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (overridePath !== void 0) name = overridePath;
|
|
221
|
+
if (overrideLink !== void 0) linkname = overrideLink;
|
|
222
|
+
overridePath = void 0;
|
|
223
|
+
overrideLink = void 0;
|
|
224
|
+
entries.push({
|
|
225
|
+
name,
|
|
226
|
+
type,
|
|
227
|
+
mode,
|
|
228
|
+
linkname,
|
|
229
|
+
data: Buffer.from(data)
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return entries;
|
|
233
|
+
};
|
|
234
|
+
/**
|
|
235
|
+
* Map decoded tar entries to the files under `subdir`, with the top-level
|
|
236
|
+
* archive directory and the `subdir/` prefix stripped from each path. Pure so
|
|
237
|
+
* it can be unit tested. Directory and other non-regular entries are dropped —
|
|
238
|
+
* writing files re-creates their parent directories.
|
|
239
|
+
*/
|
|
240
|
+
const selectTemplateFiles = (entries, subdir) => {
|
|
241
|
+
const prefix = `${subdir.replace(/^\/+|\/+$/g, "")}/`;
|
|
242
|
+
const files = [];
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
const slash = entry.name.indexOf("/");
|
|
245
|
+
if (slash === -1) continue;
|
|
246
|
+
const repoPath = entry.name.slice(slash + 1);
|
|
247
|
+
if (!repoPath.startsWith(prefix)) continue;
|
|
248
|
+
const path = repoPath.slice(prefix.length);
|
|
249
|
+
if (path === "") continue;
|
|
250
|
+
if (entry.type === "2") files.push({
|
|
251
|
+
kind: "symlink",
|
|
252
|
+
path,
|
|
253
|
+
target: entry.linkname
|
|
254
|
+
});
|
|
255
|
+
else if (entry.type === "0" || entry.type === "7") files.push({
|
|
256
|
+
kind: "file",
|
|
257
|
+
path,
|
|
258
|
+
bytes: entry.data,
|
|
259
|
+
executable: (entry.mode & 73) !== 0
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return files;
|
|
263
|
+
};
|
|
264
|
+
const tarballUrl = (template) => {
|
|
265
|
+
const { owner, repo, ref } = template.source;
|
|
266
|
+
return `${codeloadBase()}/${owner}/${repo}/tar.gz/${ref}`;
|
|
267
|
+
};
|
|
268
|
+
const friendlyGithubError = (status, url) => {
|
|
269
|
+
if (status === 404) return /* @__PURE__ */ new Error(`GitHub returned 404 for ${url}. The template repo or ref may have moved.`);
|
|
270
|
+
if (status === 403 || status === 429) return /* @__PURE__ */ new Error(`GitHub rate limited the template download (${url}). Set a GITHUB_TOKEN environment variable to raise the limit, then retry.`);
|
|
271
|
+
return /* @__PURE__ */ new Error(`GitHub returned HTTP ${status} for ${url}.`);
|
|
272
|
+
};
|
|
273
|
+
/**
|
|
274
|
+
* Download a template and resolve it to the exact set of files to write. The
|
|
275
|
+
* entire subtree is captured in one tarball request, so the copy is atomically
|
|
276
|
+
* consistent: a push to the template repo mid-download cannot produce a
|
|
277
|
+
* mismatched checkout (unlike fetching a file list and then each blob).
|
|
278
|
+
*/
|
|
279
|
+
const downloadTemplate = async (template) => {
|
|
280
|
+
const url = tarballUrl(template);
|
|
281
|
+
let gzipped;
|
|
282
|
+
try {
|
|
283
|
+
const res = await fetch(url, {
|
|
284
|
+
headers: downloadHeaders(),
|
|
285
|
+
signal: AbortSignal.timeout(3e4)
|
|
286
|
+
});
|
|
287
|
+
if (!res.ok) throw friendlyGithubError(res.status, url);
|
|
288
|
+
gzipped = Buffer.from(await res.arrayBuffer());
|
|
289
|
+
} catch (err) {
|
|
290
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
291
|
+
}
|
|
292
|
+
let tar;
|
|
293
|
+
try {
|
|
294
|
+
tar = Buffer.from(gunzipSync(new Uint8Array(gzipped)));
|
|
295
|
+
} catch (err) {
|
|
296
|
+
throw new Error(`Failed to decompress the template archive from ${url}: ${err instanceof Error ? err.message : String(err)}`);
|
|
297
|
+
}
|
|
298
|
+
const { owner, repo, ref, subdir } = template.source;
|
|
299
|
+
const files = selectTemplateFiles(parseTar(tar), subdir);
|
|
300
|
+
if (files.length === 0) throw new Error(`Template subdirectory "${subdir}" was not found in ${owner}/${repo}@${ref}.`);
|
|
301
|
+
return files;
|
|
302
|
+
};
|
|
303
|
+
/**
|
|
304
|
+
* A bad caller-supplied input that an agent (or human) can correct: an unknown
|
|
305
|
+
* template id or a non-empty target directory. Carries an `agentCode` so an
|
|
306
|
+
* agent surface can report a precise error code instead of a generic
|
|
307
|
+
* INTERNAL_ERROR, while a human path just surfaces the clear `message`.
|
|
308
|
+
*/
|
|
309
|
+
var BootstrapInputError = class extends Error {
|
|
310
|
+
agentCode;
|
|
311
|
+
constructor(message, agentCode) {
|
|
312
|
+
super(message);
|
|
313
|
+
this.name = "BootstrapInputError";
|
|
314
|
+
this.agentCode = agentCode;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
/**
|
|
318
|
+
* Ensure `dir` is safe to scaffold into: it must be missing, or an empty
|
|
319
|
+
* directory (a lone `.git` is ignored so you can scaffold into a freshly
|
|
320
|
+
* `git init`ed folder). `force` allows scaffolding into a non-empty directory,
|
|
321
|
+
* overwriting colliding files. Throws a {@link BootstrapInputError} otherwise.
|
|
322
|
+
*/
|
|
323
|
+
const ensureTargetUsable = (dir, force) => {
|
|
324
|
+
if (!existsSync(dir)) return;
|
|
325
|
+
if (!statSync(dir).isDirectory()) throw new BootstrapInputError(`Target ${dir} already exists and is not a directory.`, "TARGET_NOT_DIRECTORY");
|
|
326
|
+
if (readdirSync(dir).filter((name) => name !== ".git").length > 0 && !force) throw new BootstrapInputError(`Target directory ${dir} is not empty. Use --force to scaffold into it anyway (colliding files will be overwritten), or choose an empty directory.`, "TARGET_NOT_EMPTY");
|
|
327
|
+
};
|
|
328
|
+
const isSymlink = (path) => {
|
|
329
|
+
try {
|
|
330
|
+
return lstatSync(path).isSymbolicLink();
|
|
331
|
+
} catch {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
const errnoCode = (err) => {
|
|
336
|
+
if (typeof err === "object" && err !== null && "code" in err && typeof err.code === "string") return err.code;
|
|
337
|
+
};
|
|
338
|
+
const writeSymlink = (dest, target, onWarn) => {
|
|
339
|
+
if (isSymlink(dest)) rmSync(dest, { force: true });
|
|
340
|
+
try {
|
|
341
|
+
symlinkSync(target, dest);
|
|
342
|
+
} catch (err) {
|
|
343
|
+
if (errnoCode(err) === "EPERM" || process.platform === "win32") {
|
|
344
|
+
onWarn?.(`Could not create symlink ${dest} -> ${target}; wrote it as a regular file instead.`);
|
|
345
|
+
writeFileSync(dest, target);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
throw err;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
/**
|
|
352
|
+
* Download `template` and materialize its files into `targetDir`, creating
|
|
353
|
+
* parent directories, preserving executable bits, and recreating symlinks
|
|
354
|
+
* (with a graceful regular-file fallback on platforms that disallow them).
|
|
355
|
+
* Returns the number of files written. The caller is responsible for any
|
|
356
|
+
* target validation ({@link ensureTargetUsable}) and user-facing progress.
|
|
357
|
+
*/
|
|
358
|
+
const scaffoldTemplate = async (template, targetDir, options = {}) => {
|
|
359
|
+
const files = await downloadTemplate(template);
|
|
360
|
+
mkdirSync(targetDir, { recursive: true });
|
|
361
|
+
for (const file of files) {
|
|
362
|
+
const dest = join(targetDir, file.path);
|
|
363
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
364
|
+
if (file.kind === "symlink") writeSymlink(dest, file.target, options.onWarn);
|
|
365
|
+
else {
|
|
366
|
+
writeFileSync(dest, file.bytes);
|
|
367
|
+
if (file.executable) chmodSync(dest, 493);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return files.length;
|
|
371
|
+
};
|
|
103
372
|
//#endregion
|
|
104
|
-
export { FALLBACK_TEMPLATES, fetchTemplates, parseManifest };
|
|
373
|
+
export { BootstrapInputError, FALLBACK_TEMPLATES, downloadTemplate, ensureTargetUsable, fetchTemplates, findTemplate, parseManifest, parseTar, scaffoldTemplate, selectTemplateFiles, templateIds };
|
|
105
374
|
|
|
106
375
|
//# sourceMappingURL=bootstrap.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bootstrap.js","names":[],"sources":["../../src/lib/bootstrap.ts"],"sourcesContent":["import YAML from \"yaml\";\n\n/**\n * Neon features that a template or project may require.\n * Each feature maps to a setup phase that the orchestrator can run.\n */\nexport type NeonFeature =\n\t| \"database\"\n\t| \"auth\"\n\t| \"functions\"\n\t| \"ai-gateway\"\n\t| \"object-storage\";\n\n/** Default features when a template doesn't specify `requires`. */\nconst DEFAULT_REQUIRES: NeonFeature[] = [\"database\"];\n\nexport interface BootstrapTemplate {\n\tid: string;\n\ttitle: string;\n\tdescription: string;\n\t/** Neon features this template needs (defaults to [\"database\"]) */\n\trequires: NeonFeature[];\n\tsource: {\n\t\towner: string;\n\t\trepo: string;\n\t\tref: string;\n\t\tsubdir: string;\n\t};\n}\n\n/**\n * Hardcoded fallback used when every remote manifest source is unreachable.\n * Kept in sync with `neondatabase/examples/bootstrap.yaml` (the source of\n * truth) so that, even fully offline from the manifest, the picker still offers\n * the full set of starters rather than a single template.\n */\nexport const FALLBACK_TEMPLATES: BootstrapTemplate[] = [\n\t{\n\t\tid: \"hono\",\n\t\ttitle: \"Hono API (Drizzle, Neon Postgres) on Neon Functions\",\n\t\tdescription:\n\t\t\t\"A Hono API using Drizzle ORM and Neon Postgres, ready to deploy as a Neon Function.\",\n\t\trequires: [\"database\", \"functions\"],\n\t\tsource: {\n\t\t\towner: \"neondatabase\",\n\t\t\trepo: \"examples\",\n\t\t\tref: \"main\",\n\t\t\tsubdir: \"with-hono\",\n\t\t},\n\t},\n\t{\n\t\tid: \"ai-sdk\",\n\t\ttitle: \"AI SDK agent (AI Gateway, object storage, Drizzle) on Neon Functions\",\n\t\tdescription:\n\t\t\t\"A Vercel AI SDK agent on Neon Functions: streams chat through the Neon AI Gateway, generates an image with OpenAI image generation, and stores it in Neon object storage indexed in Postgres via Drizzle.\",\n\t\trequires: [\"database\", \"functions\", \"object-storage\", \"ai-gateway\"],\n\t\tsource: {\n\t\t\towner: \"neondatabase\",\n\t\t\trepo: \"examples\",\n\t\t\tref: \"main\",\n\t\t\tsubdir: \"with-ai-sdk\",\n\t\t},\n\t},\n\t{\n\t\tid: \"mastra\",\n\t\ttitle: \"Mastra personal agent (AI Gateway, Mastra Memory) on Neon Functions\",\n\t\tdescription:\n\t\t\t\"A Mastra personal-assistant agent on Neon Functions: streams chat through the Neon AI Gateway and uses Mastra Memory — backed by Neon Postgres — to remember the user across conversation threads via resource-scoped working memory.\",\n\t\trequires: [\"database\", \"functions\", \"ai-gateway\"],\n\t\tsource: {\n\t\t\towner: \"neondatabase\",\n\t\t\trepo: \"examples\",\n\t\t\tref: \"main\",\n\t\t\tsubdir: \"with-mastra\",\n\t\t},\n\t},\n];\n\n// Primary manifest host is neon.com (CDN-backed, no GitHub rate limiting), with\n// the raw GitHub copy as a fallback and the hardcoded list as the last resort.\n// A single env override (used by tests) short-circuits the chain.\nconst NEON_MANIFEST_URL = \"https://neon.com/bootstrap/templates.yaml\";\nconst GITHUB_RAW_MANIFEST_URL =\n\t\"https://raw.githubusercontent.com/neondatabase/examples/main/bootstrap.yaml\";\n\nfunction manifestUrls(): string[] {\n\tconst override = process.env.NEON_BOOTSTRAP_MANIFEST_URL;\n\tif (override) return [override];\n\treturn [NEON_MANIFEST_URL, GITHUB_RAW_MANIFEST_URL];\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n\ttypeof value === \"object\" && value !== null;\n\nexport function parseManifest(text: string): BootstrapTemplate[] {\n\tconst data: unknown = YAML.parse(text);\n\tif (!isRecord(data) || !Array.isArray(data.templates)) {\n\t\tthrow new Error(\n\t\t\t'Invalid bootstrap manifest: missing \"templates\" array.',\n\t\t);\n\t}\n\tconst templates: BootstrapTemplate[] = [];\n\tfor (const item of data.templates) {\n\t\tif (\n\t\t\t!isRecord(item) ||\n\t\t\ttypeof item.id !== \"string\" ||\n\t\t\ttypeof item.title !== \"string\" ||\n\t\t\ttypeof item.description !== \"string\" ||\n\t\t\t!isRecord(item.source) ||\n\t\t\ttypeof item.source.owner !== \"string\" ||\n\t\t\ttypeof item.source.repo !== \"string\" ||\n\t\t\ttypeof item.source.ref !== \"string\" ||\n\t\t\ttypeof item.source.subdir !== \"string\"\n\t\t) {\n\t\t\tcontinue;\n\t\t}\n\t\t// Parse requires — accept string array, default to [\"database\"]\n\t\tconst requires: NeonFeature[] =\n\t\t\tArray.isArray(item.requires) &&\n\t\t\titem.requires.every((r: unknown) => typeof r === \"string\")\n\t\t\t\t? (item.requires as NeonFeature[])\n\t\t\t\t: DEFAULT_REQUIRES;\n\n\t\ttemplates.push({\n\t\t\tid: item.id,\n\t\t\ttitle: item.title,\n\t\t\tdescription: item.description,\n\t\t\trequires,\n\t\t\tsource: {\n\t\t\t\towner: item.source.owner,\n\t\t\t\trepo: item.source.repo,\n\t\t\t\tref: item.source.ref,\n\t\t\t\tsubdir: item.source.subdir,\n\t\t\t},\n\t\t});\n\t}\n\treturn templates;\n}\n\n/**\n * Fetch the template manifest, trying each source in {@link manifestUrls} in\n * order and returning the first that yields a non-empty template list. Falls\n * back to the hardcoded list when every source is unreachable or empty, so the\n * picker never fails just because a host is down.\n */\nexport async function fetchTemplates(): Promise<BootstrapTemplate[]> {\n\tfor (const url of manifestUrls()) {\n\t\ttry {\n\t\t\tconst res = await fetch(url, {\n\t\t\t\tsignal: AbortSignal.timeout(10_000),\n\t\t\t});\n\t\t\tif (!res.ok) throw new Error(`HTTP ${res.status}`);\n\t\t\tconst templates = parseManifest(await res.text());\n\t\t\tif (templates.length > 0) return templates;\n\t\t} catch {\n\t\t\t// Try the next source.\n\t\t}\n\t}\n\treturn FALLBACK_TEMPLATES;\n}\n"],"mappings":";;;AAcA,MAAM,mBAAkC,CAAC,UAAU;;;;;;;AAsBnD,MAAa,qBAA0C;CACtD;EACC,IAAI;EACJ,OAAO;EACP,aACC;EACD,UAAU,CAAC,YAAY,WAAW;EAClC,QAAQ;GACP,OAAO;GACP,MAAM;GACN,KAAK;GACL,QAAQ;EACT;CACD;CACA;EACC,IAAI;EACJ,OAAO;EACP,aACC;EACD,UAAU;GAAC;GAAY;GAAa;GAAkB;EAAY;EAClE,QAAQ;GACP,OAAO;GACP,MAAM;GACN,KAAK;GACL,QAAQ;EACT;CACD;CACA;EACC,IAAI;EACJ,OAAO;EACP,aACC;EACD,UAAU;GAAC;GAAY;GAAa;EAAY;EAChD,QAAQ;GACP,OAAO;GACP,MAAM;GACN,KAAK;GACL,QAAQ;EACT;CACD;AACD;AAKA,MAAM,oBAAoB;AAC1B,MAAM,0BACL;AAED,SAAS,eAAyB;CACjC,MAAM,WAAW,QAAQ,IAAI;CAC7B,IAAI,UAAU,OAAO,CAAC,QAAQ;CAC9B,OAAO,CAAC,mBAAmB,uBAAuB;AACnD;AAEA,MAAM,YAAY,UACjB,OAAO,UAAU,YAAY,UAAU;AAExC,SAAgB,cAAc,MAAmC;CAChE,MAAM,OAAgB,KAAK,MAAM,IAAI;CACrC,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC,MAAM,QAAQ,KAAK,SAAS,GACnD,MAAM,IAAI,MACT,0DACD;CAED,MAAM,YAAiC,CAAC;CACxC,KAAK,MAAM,QAAQ,KAAK,WAAW;EAClC,IACC,CAAC,SAAS,IAAI,KACd,OAAO,KAAK,OAAO,YACnB,OAAO,KAAK,UAAU,YACtB,OAAO,KAAK,gBAAgB,YAC5B,CAAC,SAAS,KAAK,MAAM,KACrB,OAAO,KAAK,OAAO,UAAU,YAC7B,OAAO,KAAK,OAAO,SAAS,YAC5B,OAAO,KAAK,OAAO,QAAQ,YAC3B,OAAO,KAAK,OAAO,WAAW,UAE9B;EAGD,MAAM,WACL,MAAM,QAAQ,KAAK,QAAQ,KAC3B,KAAK,SAAS,OAAO,MAAe,OAAO,MAAM,QAAQ,IACrD,KAAK,WACN;EAEJ,UAAU,KAAK;GACd,IAAI,KAAK;GACT,OAAO,KAAK;GACZ,aAAa,KAAK;GAClB;GACA,QAAQ;IACP,OAAO,KAAK,OAAO;IACnB,MAAM,KAAK,OAAO;IAClB,KAAK,KAAK,OAAO;IACjB,QAAQ,KAAK,OAAO;GACrB;EACD,CAAC;CACF;CACA,OAAO;AACR;;;;;;;AAQA,eAAsB,iBAA+C;CACpE,KAAK,MAAM,OAAO,aAAa,GAC9B,IAAI;EACH,MAAM,MAAM,MAAM,MAAM,KAAK,EAC5B,QAAQ,YAAY,QAAQ,GAAM,EACnC,CAAC;EACD,IAAI,CAAC,IAAI,IAAI,MAAM,IAAI,MAAM,QAAQ,IAAI,QAAQ;EACjD,MAAM,YAAY,cAAc,MAAM,IAAI,KAAK,CAAC;EAChD,IAAI,UAAU,SAAS,GAAG,OAAO;CAClC,QAAQ,CAER;CAED,OAAO;AACR"}
|
|
1
|
+
{"version":3,"file":"bootstrap.js","names":[],"sources":["../../src/lib/bootstrap.ts"],"sourcesContent":["import {\n\tchmodSync,\n\texistsSync,\n\tlstatSync,\n\tmkdirSync,\n\treaddirSync,\n\trmSync,\n\tstatSync,\n\tsymlinkSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { gunzipSync } from \"fflate\";\nimport YAML from \"yaml\";\n\n/**\n * A scaffold template that lives in a subdirectory of a public GitHub repo we\n * control. Bootstrapping copies that subdirectory into a target folder —\n * conceptually the same as `degit user/repo/subdir`, but implemented in-house.\n *\n * The whole template is pulled in a single request: we download the repo's\n * gzipped tarball from `codeload.github.com` and extract only the subdir we\n * want. That endpoint is unauthenticated and is NOT subject to the 60-requests\n * per-hour limit of the REST API (`api.github.com`), so bootstrapping works out\n * of the box on shared/corporate networks without a GITHUB_TOKEN. We lean on\n * `fflate` for gunzip and parse the tar in-house, so we never pull in a heavy\n * dependency tree just to copy a few files.\n */\n\n/**\n * Neon features that a template or project may require.\n * Each feature maps to a setup phase that the orchestrator can run.\n */\nexport type NeonFeature =\n\t| \"database\"\n\t| \"auth\"\n\t| \"functions\"\n\t| \"ai-gateway\"\n\t| \"object-storage\";\n\n/** Default features when a template doesn't specify `requires`. */\nconst DEFAULT_REQUIRES: NeonFeature[] = [\"database\"];\n\nexport interface BootstrapTemplate {\n\t/** Stable id used by `--template` and analytics. */\n\tid: string;\n\t/** Human label shown in the interactive selector. */\n\ttitle: string;\n\t/** One-line description shown under the title in the selector. */\n\tdescription: string;\n\t/**\n\t * Neon services the template uses (e.g. \"Postgres\", \"Functions\"). Shown as a\n\t * badge next to the title in the picker. Optional — older manifests omit it.\n\t */\n\tservices?: string[];\n\t/** Neon features this template needs (defaults to [\"database\"]). */\n\trequires: NeonFeature[];\n\tsource: {\n\t\towner: string;\n\t\trepo: string;\n\t\t/** Branch (or tag) the template is pulled from. */\n\t\tref: string;\n\t\t/** Subdirectory within the repo to copy (no leading/trailing slash). */\n\t\tsubdir: string;\n\t};\n}\n\n/**\n * Hardcoded fallback used when every remote manifest source is unreachable.\n * Kept in sync with `neondatabase/examples/bootstrap.yaml` (the source of\n * truth) so that, even fully offline from the manifest, the picker still offers\n * the full set of starters rather than a single template.\n */\nexport const FALLBACK_TEMPLATES: BootstrapTemplate[] = [\n\t{\n\t\tid: \"hono\",\n\t\ttitle: \"Hono API (Drizzle, Neon Postgres) on Neon Functions\",\n\t\tdescription:\n\t\t\t\"A Hono API using Drizzle ORM and Neon Postgres, ready to deploy as a Neon Function.\",\n\t\tservices: [\"Postgres\", \"Functions\"],\n\t\trequires: [\"database\", \"functions\"],\n\t\tsource: {\n\t\t\towner: \"neondatabase\",\n\t\t\trepo: \"examples\",\n\t\t\tref: \"main\",\n\t\t\tsubdir: \"with-hono\",\n\t\t},\n\t},\n\t{\n\t\tid: \"ai-sdk\",\n\t\ttitle: \"AI SDK agent (AI Gateway, object storage, Drizzle) on Neon Functions\",\n\t\tdescription:\n\t\t\t\"A Vercel AI SDK agent on Neon Functions: streams chat through the Neon AI Gateway, generates an image with OpenAI image generation, and stores it in Neon object storage indexed in Postgres via Drizzle.\",\n\t\tservices: [\"Postgres\", \"Functions\", \"Object Storage\", \"AI Gateway\"],\n\t\trequires: [\"database\", \"functions\", \"object-storage\", \"ai-gateway\"],\n\t\tsource: {\n\t\t\towner: \"neondatabase\",\n\t\t\trepo: \"examples\",\n\t\t\tref: \"main\",\n\t\t\tsubdir: \"with-ai-sdk\",\n\t\t},\n\t},\n\t{\n\t\tid: \"mastra\",\n\t\ttitle: \"Mastra personal agent (AI Gateway, Mastra Memory) on Neon Functions\",\n\t\tdescription:\n\t\t\t\"A Mastra personal-assistant agent on Neon Functions: streams chat through the Neon AI Gateway and uses Mastra Memory — backed by Neon Postgres — to remember the user across conversation threads via resource-scoped working memory.\",\n\t\tservices: [\"Postgres\", \"Functions\", \"AI Gateway\"],\n\t\trequires: [\"database\", \"functions\", \"ai-gateway\"],\n\t\tsource: {\n\t\t\towner: \"neondatabase\",\n\t\t\trepo: \"examples\",\n\t\t\tref: \"main\",\n\t\t\tsubdir: \"with-mastra\",\n\t\t},\n\t},\n];\n\nexport const templateIds = (templates: BootstrapTemplate[]): string =>\n\ttemplates.map((t) => t.id).join(\", \");\n\nexport const findTemplate = (\n\ttemplates: BootstrapTemplate[],\n\tid: string,\n): BootstrapTemplate | undefined => templates.find((t) => t.id === id);\n\n/** A single file or symlink to materialize, already resolved with its bytes. */\nexport type TemplateFile =\n\t| {\n\t\t\tkind: \"file\";\n\t\t\t/** Path relative to the target directory (subdir prefix stripped). */\n\t\t\tpath: string;\n\t\t\tbytes: Buffer;\n\t\t\texecutable: boolean;\n\t }\n\t| {\n\t\t\tkind: \"symlink\";\n\t\t\tpath: string;\n\t\t\t/** The (relative) link target. */\n\t\t\ttarget: string;\n\t };\n\nconst githubToken = (): string =>\n\tprocess.env.GITHUB_TOKEN ?? process.env.GH_TOKEN ?? \"\";\n\n// A token is never required for public templates, but we forward it when\n// present so the same code path works behind proxies that authenticate, and\n// (in future) for private template repos.\nconst downloadHeaders = (): Record<string, string> => ({\n\t\"User-Agent\": \"neon-init\",\n\t...(githubToken() ? { Authorization: `Bearer ${githubToken()}` } : {}),\n});\n\n// The codeload host is overridable so the e2e tests can point the downloader at\n// a local server (the same trick `--api-host` uses to redirect the Neon API).\nconst codeloadBase = (): string =>\n\tprocess.env.NEON_BOOTSTRAP_GITHUB_CODELOAD ?? \"https://codeload.github.com\";\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n\ttypeof value === \"object\" && value !== null;\n\n/**\n * Normalize a manifest entry's `services` into a clean string list. Tolerant by\n * design: a missing or non-array value yields `undefined`, and non-string items\n * are dropped, so a malformed `services` never sinks an otherwise-valid\n * template (it just renders without its badge).\n */\nconst parseServices = (value: unknown): string[] | undefined => {\n\tif (!Array.isArray(value)) return undefined;\n\tconst services = value.filter(\n\t\t(item): item is string =>\n\t\t\ttypeof item === \"string\" && item.trim() !== \"\",\n\t);\n\treturn services.length > 0 ? services : undefined;\n};\n\n// ---------------------------------------------------------------------------\n// Remote template manifest\n// ---------------------------------------------------------------------------\n\n// Primary manifest host is neon.com (CDN-backed, no GitHub rate limiting), with\n// the raw GitHub copy as a fallback and the hardcoded list as the last resort.\n// A single env override (used by tests) short-circuits the chain.\nconst NEON_MANIFEST_URL = \"https://neon.com/bootstrap/templates.yaml\";\nconst GITHUB_RAW_MANIFEST_URL =\n\t\"https://raw.githubusercontent.com/neondatabase/examples/main/bootstrap.yaml\";\n\nfunction manifestUrls(): string[] {\n\tconst override = process.env.NEON_BOOTSTRAP_MANIFEST_URL;\n\tif (override) return [override];\n\treturn [NEON_MANIFEST_URL, GITHUB_RAW_MANIFEST_URL];\n}\n\nexport function parseManifest(text: string): BootstrapTemplate[] {\n\tconst data: unknown = YAML.parse(text);\n\tif (!isRecord(data) || !Array.isArray(data.templates)) {\n\t\tthrow new Error(\n\t\t\t'Invalid bootstrap manifest: missing \"templates\" array.',\n\t\t);\n\t}\n\tconst templates: BootstrapTemplate[] = [];\n\tfor (const item of data.templates) {\n\t\tif (\n\t\t\t!isRecord(item) ||\n\t\t\ttypeof item.id !== \"string\" ||\n\t\t\ttypeof item.title !== \"string\" ||\n\t\t\ttypeof item.description !== \"string\" ||\n\t\t\t!isRecord(item.source) ||\n\t\t\ttypeof item.source.owner !== \"string\" ||\n\t\t\ttypeof item.source.repo !== \"string\" ||\n\t\t\ttypeof item.source.ref !== \"string\" ||\n\t\t\ttypeof item.source.subdir !== \"string\"\n\t\t) {\n\t\t\tcontinue;\n\t\t}\n\t\t// Parse requires — accept a string array, default to [\"database\"].\n\t\tconst requires: NeonFeature[] =\n\t\t\tArray.isArray(item.requires) &&\n\t\t\titem.requires.every((r: unknown) => typeof r === \"string\")\n\t\t\t\t? (item.requires as NeonFeature[])\n\t\t\t\t: DEFAULT_REQUIRES;\n\t\tconst services = parseServices(item.services);\n\t\ttemplates.push({\n\t\t\tid: item.id,\n\t\t\ttitle: item.title,\n\t\t\tdescription: item.description,\n\t\t\t...(services ? { services } : {}),\n\t\t\trequires,\n\t\t\tsource: {\n\t\t\t\towner: item.source.owner,\n\t\t\t\trepo: item.source.repo,\n\t\t\t\tref: item.source.ref,\n\t\t\t\tsubdir: item.source.subdir,\n\t\t\t},\n\t\t});\n\t}\n\treturn templates;\n}\n\n/**\n * Fetch the template manifest, trying each source in {@link manifestUrls} in\n * order and returning the first that yields a non-empty template list. Falls\n * back to the hardcoded list when every source is unreachable or empty, so the\n * picker never fails just because a host is down.\n */\nexport async function fetchTemplates(): Promise<BootstrapTemplate[]> {\n\tfor (const url of manifestUrls()) {\n\t\ttry {\n\t\t\tconst res = await fetch(url, {\n\t\t\t\theaders: downloadHeaders(),\n\t\t\t\tsignal: AbortSignal.timeout(10_000),\n\t\t\t});\n\t\t\tif (!res.ok) throw new Error(`HTTP ${res.status}`);\n\t\t\tconst templates = parseManifest(await res.text());\n\t\t\tif (templates.length > 0) return templates;\n\t\t} catch {\n\t\t\t// Try the next source.\n\t\t}\n\t}\n\treturn FALLBACK_TEMPLATES;\n}\n\n// ---------------------------------------------------------------------------\n// Tar parsing\n// ---------------------------------------------------------------------------\n\n/** A raw entry decoded from a tar stream, before subdir filtering. */\ntype TarEntry = {\n\t/** Full path as stored in the archive (includes the top-level dir). */\n\tname: string;\n\t/** POSIX type flag: '0' file, '5' directory, '2' symlink, etc. */\n\ttype: string;\n\t/** File permission bits. */\n\tmode: number;\n\t/** Symlink target (for type '2'). */\n\tlinkname: string;\n\t/** File contents (for type '0'). */\n\tdata: Buffer;\n};\n\nconst TAR_BLOCK = 512;\n\nconst readTarString = (buf: Buffer, offset: number, length: number): string => {\n\tlet end = offset;\n\tconst max = offset + length;\n\twhile (end < max && buf[end] !== 0) end++;\n\treturn buf.toString(\"utf8\", offset, end);\n};\n\nconst readTarOctal = (buf: Buffer, offset: number, length: number): number => {\n\tconst text = readTarString(buf, offset, length).trim();\n\tif (text === \"\") return 0;\n\tconst value = parseInt(text, 8);\n\treturn Number.isNaN(value) ? 0 : value;\n};\n\nconst isZeroBlock = (buf: Buffer, offset: number): boolean => {\n\tfor (let i = offset; i < offset + TAR_BLOCK; i++) {\n\t\tif (buf[i] !== 0) return false;\n\t}\n\treturn true;\n};\n\n/**\n * Parse pax extended-header records (\"<len> <key>=<value>\\n\"). GitHub uses\n * these for the global header and for any path that doesn't fit the legacy\n * 100-byte name field, so we must honor at least `path` and `linkpath`.\n */\nconst parsePaxRecords = (data: Buffer): Record<string, string> => {\n\tconst records: Record<string, string> = {};\n\tlet pos = 0;\n\tconst text = data.toString(\"utf8\");\n\twhile (pos < text.length) {\n\t\tconst space = text.indexOf(\" \", pos);\n\t\tif (space === -1) break;\n\t\tconst len = parseInt(text.slice(pos, space), 10);\n\t\tif (Number.isNaN(len) || len <= 0) break;\n\t\tconst record = text.slice(space + 1, pos + len - 1); // drop trailing \"\\n\"\n\t\tconst eq = record.indexOf(\"=\");\n\t\tif (eq !== -1) records[record.slice(0, eq)] = record.slice(eq + 1);\n\t\tpos += len;\n\t}\n\treturn records;\n};\n\n/**\n * Decode a (decompressed) tar archive into its file/symlink entries. Pure and\n * dependency-free so it can be unit tested without touching the network.\n * Handles the ustar `prefix` field, pax extended headers (type 'x'/'g'), and\n * GNU long-name/long-link headers (type 'L'/'K') so deep template paths and\n * long symlink targets round-trip correctly.\n */\nexport const parseTar = (buf: Buffer): TarEntry[] => {\n\tconst entries: TarEntry[] = [];\n\t// Overrides carried from a preceding pax/GNU header to the next real entry.\n\tlet overridePath: string | undefined;\n\tlet overrideLink: string | undefined;\n\tlet offset = 0;\n\n\twhile (offset + TAR_BLOCK <= buf.length) {\n\t\tif (isZeroBlock(buf, offset)) break;\n\n\t\tlet name = readTarString(buf, offset, 100);\n\t\tconst mode = readTarOctal(buf, offset + 100, 8);\n\t\tconst size = readTarOctal(buf, offset + 124, 12);\n\t\tconst typeByte = buf[offset + 156];\n\t\tconst type = typeByte === 0 ? \"0\" : String.fromCharCode(typeByte);\n\t\tlet linkname = readTarString(buf, offset + 157, 100);\n\t\tconst magic = readTarString(buf, offset + 257, 6);\n\t\tif (magic.startsWith(\"ustar\")) {\n\t\t\tconst prefix = readTarString(buf, offset + 345, 155);\n\t\t\tif (prefix !== \"\") name = `${prefix}/${name}`;\n\t\t}\n\n\t\toffset += TAR_BLOCK;\n\t\tconst data = buf.subarray(offset, offset + size);\n\t\toffset += Math.ceil(size / TAR_BLOCK) * TAR_BLOCK;\n\n\t\tif (type === \"x\") {\n\t\t\tconst records = parsePaxRecords(data);\n\t\t\tif (records.path !== undefined) overridePath = records.path;\n\t\t\tif (records.linkpath !== undefined) overrideLink = records.linkpath;\n\t\t\tcontinue;\n\t\t}\n\t\tif (type === \"g\") {\n\t\t\t// Global pax header (e.g. GitHub's comment block): not per-entry state.\n\t\t\tcontinue;\n\t\t}\n\t\tif (type === \"L\" || type === \"K\") {\n\t\t\tconst longValue = data.toString(\"utf8\").replace(/\\0+$/, \"\");\n\t\t\tif (type === \"L\") overridePath = longValue;\n\t\t\telse overrideLink = longValue;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (overridePath !== undefined) name = overridePath;\n\t\tif (overrideLink !== undefined) linkname = overrideLink;\n\t\toverridePath = undefined;\n\t\toverrideLink = undefined;\n\n\t\tentries.push({ name, type, mode, linkname, data: Buffer.from(data) });\n\t}\n\n\treturn entries;\n};\n\n/**\n * Map decoded tar entries to the files under `subdir`, with the top-level\n * archive directory and the `subdir/` prefix stripped from each path. Pure so\n * it can be unit tested. Directory and other non-regular entries are dropped —\n * writing files re-creates their parent directories.\n */\nexport const selectTemplateFiles = (\n\tentries: TarEntry[],\n\tsubdir: string,\n): TemplateFile[] => {\n\tconst prefix = `${subdir.replace(/^\\/+|\\/+$/g, \"\")}/`;\n\tconst files: TemplateFile[] = [];\n\tfor (const entry of entries) {\n\t\t// codeload wraps everything in a single top-level dir (\"<repo>-<ref>/\");\n\t\t// strip that first segment to get the repo-relative path.\n\t\tconst slash = entry.name.indexOf(\"/\");\n\t\tif (slash === -1) continue;\n\t\tconst repoPath = entry.name.slice(slash + 1);\n\t\tif (!repoPath.startsWith(prefix)) continue;\n\t\tconst path = repoPath.slice(prefix.length);\n\t\tif (path === \"\") continue;\n\t\tif (entry.type === \"2\") {\n\t\t\tfiles.push({ kind: \"symlink\", path, target: entry.linkname });\n\t\t} else if (entry.type === \"0\" || entry.type === \"7\") {\n\t\t\tfiles.push({\n\t\t\t\tkind: \"file\",\n\t\t\t\tpath,\n\t\t\t\tbytes: entry.data,\n\t\t\t\texecutable: (entry.mode & 0o111) !== 0,\n\t\t\t});\n\t\t}\n\t\t// Directories ('5') and any other node types are intentionally skipped.\n\t}\n\treturn files;\n};\n\nconst tarballUrl = (template: BootstrapTemplate): string => {\n\tconst { owner, repo, ref } = template.source;\n\treturn `${codeloadBase()}/${owner}/${repo}/tar.gz/${ref}`;\n};\n\nconst friendlyGithubError = (status: number, url: string): Error => {\n\tif (status === 404) {\n\t\treturn new Error(\n\t\t\t`GitHub returned 404 for ${url}. The template repo or ref may have moved.`,\n\t\t);\n\t}\n\tif (status === 403 || status === 429) {\n\t\treturn new Error(\n\t\t\t`GitHub rate limited the template download (${url}). Set a GITHUB_TOKEN environment variable to raise the limit, then retry.`,\n\t\t);\n\t}\n\treturn new Error(`GitHub returned HTTP ${status} for ${url}.`);\n};\n\n/**\n * Download a template and resolve it to the exact set of files to write. The\n * entire subtree is captured in one tarball request, so the copy is atomically\n * consistent: a push to the template repo mid-download cannot produce a\n * mismatched checkout (unlike fetching a file list and then each blob).\n */\nexport const downloadTemplate = async (\n\ttemplate: BootstrapTemplate,\n): Promise<TemplateFile[]> => {\n\tconst url = tarballUrl(template);\n\n\tlet gzipped: Buffer;\n\ttry {\n\t\tconst res = await fetch(url, {\n\t\t\theaders: downloadHeaders(),\n\t\t\tsignal: AbortSignal.timeout(30_000),\n\t\t});\n\t\tif (!res.ok) throw friendlyGithubError(res.status, url);\n\t\tgzipped = Buffer.from(await res.arrayBuffer());\n\t} catch (err) {\n\t\tthrow err instanceof Error ? err : new Error(String(err));\n\t}\n\n\tlet tar: Buffer;\n\ttry {\n\t\ttar = Buffer.from(gunzipSync(new Uint8Array(gzipped)));\n\t} catch (err) {\n\t\tthrow new Error(\n\t\t\t`Failed to decompress the template archive from ${url}: ${\n\t\t\t\terr instanceof Error ? err.message : String(err)\n\t\t\t}`,\n\t\t);\n\t}\n\n\tconst { owner, repo, ref, subdir } = template.source;\n\tconst files = selectTemplateFiles(parseTar(tar), subdir);\n\tif (files.length === 0) {\n\t\tthrow new Error(\n\t\t\t`Template subdirectory \"${subdir}\" was not found in ${owner}/${repo}@${ref}.`,\n\t\t);\n\t}\n\treturn files;\n};\n\n// ---------------------------------------------------------------------------\n// Target validation + scaffolding to disk\n// ---------------------------------------------------------------------------\n\n/**\n * A bad caller-supplied input that an agent (or human) can correct: an unknown\n * template id or a non-empty target directory. Carries an `agentCode` so an\n * agent surface can report a precise error code instead of a generic\n * INTERNAL_ERROR, while a human path just surfaces the clear `message`.\n */\nexport class BootstrapInputError extends Error {\n\treadonly agentCode: string;\n\tconstructor(message: string, agentCode: string) {\n\t\tsuper(message);\n\t\tthis.name = \"BootstrapInputError\";\n\t\tthis.agentCode = agentCode;\n\t}\n}\n\n/**\n * Ensure `dir` is safe to scaffold into: it must be missing, or an empty\n * directory (a lone `.git` is ignored so you can scaffold into a freshly\n * `git init`ed folder). `force` allows scaffolding into a non-empty directory,\n * overwriting colliding files. Throws a {@link BootstrapInputError} otherwise.\n */\nexport const ensureTargetUsable = (dir: string, force: boolean): void => {\n\tif (!existsSync(dir)) return;\n\tif (!statSync(dir).isDirectory()) {\n\t\tthrow new BootstrapInputError(\n\t\t\t`Target ${dir} already exists and is not a directory.`,\n\t\t\t\"TARGET_NOT_DIRECTORY\",\n\t\t);\n\t}\n\tconst contents = readdirSync(dir).filter((name) => name !== \".git\");\n\tif (contents.length > 0 && !force) {\n\t\tthrow new BootstrapInputError(\n\t\t\t`Target directory ${dir} is not empty. Use --force to scaffold into it anyway (colliding files will be overwritten), or choose an empty directory.`,\n\t\t\t\"TARGET_NOT_EMPTY\",\n\t\t);\n\t}\n};\n\nconst isSymlink = (path: string): boolean => {\n\ttry {\n\t\treturn lstatSync(path).isSymbolicLink();\n\t} catch {\n\t\treturn false;\n\t}\n};\n\nconst errnoCode = (err: unknown): string | undefined => {\n\tif (\n\t\ttypeof err === \"object\" &&\n\t\terr !== null &&\n\t\t\"code\" in err &&\n\t\ttypeof err.code === \"string\"\n\t) {\n\t\treturn err.code;\n\t}\n\treturn undefined;\n};\n\nconst writeSymlink = (\n\tdest: string,\n\ttarget: string,\n\tonWarn?: (message: string) => void,\n): void => {\n\tif (isSymlink(dest)) rmSync(dest, { force: true });\n\ttry {\n\t\tsymlinkSync(target, dest);\n\t} catch (err) {\n\t\t// Windows refuses symlinks without elevated rights / developer mode. The\n\t\t// template still works for most tooling if we drop a regular file holding\n\t\t// the link target, so we degrade gracefully instead of failing the copy.\n\t\tif (errnoCode(err) === \"EPERM\" || process.platform === \"win32\") {\n\t\t\tonWarn?.(\n\t\t\t\t`Could not create symlink ${dest} -> ${target}; wrote it as a regular file instead.`,\n\t\t\t);\n\t\t\twriteFileSync(dest, target);\n\t\t\treturn;\n\t\t}\n\t\tthrow err;\n\t}\n};\n\nexport interface ScaffoldOptions {\n\t/** Called for non-fatal warnings (e.g. a symlink that fell back to a file). */\n\tonWarn?: (message: string) => void;\n}\n\n/**\n * Download `template` and materialize its files into `targetDir`, creating\n * parent directories, preserving executable bits, and recreating symlinks\n * (with a graceful regular-file fallback on platforms that disallow them).\n * Returns the number of files written. The caller is responsible for any\n * target validation ({@link ensureTargetUsable}) and user-facing progress.\n */\nexport const scaffoldTemplate = async (\n\ttemplate: BootstrapTemplate,\n\ttargetDir: string,\n\toptions: ScaffoldOptions = {},\n): Promise<number> => {\n\tconst files = await downloadTemplate(template);\n\n\tmkdirSync(targetDir, { recursive: true });\n\tfor (const file of files) {\n\t\tconst dest = join(targetDir, file.path);\n\t\tmkdirSync(dirname(dest), { recursive: true });\n\t\tif (file.kind === \"symlink\") {\n\t\t\twriteSymlink(dest, file.target, options.onWarn);\n\t\t} else {\n\t\t\twriteFileSync(dest, file.bytes);\n\t\t\tif (file.executable) chmodSync(dest, 0o755);\n\t\t}\n\t}\n\treturn files.length;\n};\n"],"mappings":";;;;;;AAyCA,MAAM,mBAAkC,CAAC,UAAU;;;;;;;AAgCnD,MAAa,qBAA0C;CACtD;EACC,IAAI;EACJ,OAAO;EACP,aACC;EACD,UAAU,CAAC,YAAY,WAAW;EAClC,UAAU,CAAC,YAAY,WAAW;EAClC,QAAQ;GACP,OAAO;GACP,MAAM;GACN,KAAK;GACL,QAAQ;EACT;CACD;CACA;EACC,IAAI;EACJ,OAAO;EACP,aACC;EACD,UAAU;GAAC;GAAY;GAAa;GAAkB;EAAY;EAClE,UAAU;GAAC;GAAY;GAAa;GAAkB;EAAY;EAClE,QAAQ;GACP,OAAO;GACP,MAAM;GACN,KAAK;GACL,QAAQ;EACT;CACD;CACA;EACC,IAAI;EACJ,OAAO;EACP,aACC;EACD,UAAU;GAAC;GAAY;GAAa;EAAY;EAChD,UAAU;GAAC;GAAY;GAAa;EAAY;EAChD,QAAQ;GACP,OAAO;GACP,MAAM;GACN,KAAK;GACL,QAAQ;EACT;CACD;AACD;AAEA,MAAa,eAAe,cAC3B,UAAU,KAAK,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI;AAErC,MAAa,gBACZ,WACA,OACmC,UAAU,MAAM,MAAM,EAAE,OAAO,EAAE;AAkBrE,MAAM,oBACL,QAAQ,IAAI,gBAAgB,QAAQ,IAAI,YAAY;AAKrD,MAAM,yBAAiD;CACtD,cAAc;CACd,GAAI,YAAY,IAAI,EAAE,eAAe,UAAU,YAAY,IAAI,IAAI,CAAC;AACrE;AAIA,MAAM,qBACL,QAAQ,IAAI,kCAAkC;AAE/C,MAAM,YAAY,UACjB,OAAO,UAAU,YAAY,UAAU;;;;;;;AAQxC,MAAM,iBAAiB,UAAyC;CAC/D,IAAI,CAAC,MAAM,QAAQ,KAAK,GAAG,OAAO,KAAA;CAClC,MAAM,WAAW,MAAM,QACrB,SACA,OAAO,SAAS,YAAY,KAAK,KAAK,MAAM,EAC9C;CACA,OAAO,SAAS,SAAS,IAAI,WAAW,KAAA;AACzC;AASA,MAAM,oBAAoB;AAC1B,MAAM,0BACL;AAED,SAAS,eAAyB;CACjC,MAAM,WAAW,QAAQ,IAAI;CAC7B,IAAI,UAAU,OAAO,CAAC,QAAQ;CAC9B,OAAO,CAAC,mBAAmB,uBAAuB;AACnD;AAEA,SAAgB,cAAc,MAAmC;CAChE,MAAM,OAAgB,KAAK,MAAM,IAAI;CACrC,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC,MAAM,QAAQ,KAAK,SAAS,GACnD,MAAM,IAAI,MACT,0DACD;CAED,MAAM,YAAiC,CAAC;CACxC,KAAK,MAAM,QAAQ,KAAK,WAAW;EAClC,IACC,CAAC,SAAS,IAAI,KACd,OAAO,KAAK,OAAO,YACnB,OAAO,KAAK,UAAU,YACtB,OAAO,KAAK,gBAAgB,YAC5B,CAAC,SAAS,KAAK,MAAM,KACrB,OAAO,KAAK,OAAO,UAAU,YAC7B,OAAO,KAAK,OAAO,SAAS,YAC5B,OAAO,KAAK,OAAO,QAAQ,YAC3B,OAAO,KAAK,OAAO,WAAW,UAE9B;EAGD,MAAM,WACL,MAAM,QAAQ,KAAK,QAAQ,KAC3B,KAAK,SAAS,OAAO,MAAe,OAAO,MAAM,QAAQ,IACrD,KAAK,WACN;EACJ,MAAM,WAAW,cAAc,KAAK,QAAQ;EAC5C,UAAU,KAAK;GACd,IAAI,KAAK;GACT,OAAO,KAAK;GACZ,aAAa,KAAK;GAClB,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;GAC/B;GACA,QAAQ;IACP,OAAO,KAAK,OAAO;IACnB,MAAM,KAAK,OAAO;IAClB,KAAK,KAAK,OAAO;IACjB,QAAQ,KAAK,OAAO;GACrB;EACD,CAAC;CACF;CACA,OAAO;AACR;;;;;;;AAQA,eAAsB,iBAA+C;CACpE,KAAK,MAAM,OAAO,aAAa,GAC9B,IAAI;EACH,MAAM,MAAM,MAAM,MAAM,KAAK;GAC5B,SAAS,gBAAgB;GACzB,QAAQ,YAAY,QAAQ,GAAM;EACnC,CAAC;EACD,IAAI,CAAC,IAAI,IAAI,MAAM,IAAI,MAAM,QAAQ,IAAI,QAAQ;EACjD,MAAM,YAAY,cAAc,MAAM,IAAI,KAAK,CAAC;EAChD,IAAI,UAAU,SAAS,GAAG,OAAO;CAClC,QAAQ,CAER;CAED,OAAO;AACR;AAoBA,MAAM,YAAY;AAElB,MAAM,iBAAiB,KAAa,QAAgB,WAA2B;CAC9E,IAAI,MAAM;CACV,MAAM,MAAM,SAAS;CACrB,OAAO,MAAM,OAAO,IAAI,SAAS,GAAG;CACpC,OAAO,IAAI,SAAS,QAAQ,QAAQ,GAAG;AACxC;AAEA,MAAM,gBAAgB,KAAa,QAAgB,WAA2B;CAC7E,MAAM,OAAO,cAAc,KAAK,QAAQ,MAAM,CAAC,CAAC,KAAK;CACrD,IAAI,SAAS,IAAI,OAAO;CACxB,MAAM,QAAQ,SAAS,MAAM,CAAC;CAC9B,OAAO,OAAO,MAAM,KAAK,IAAI,IAAI;AAClC;AAEA,MAAM,eAAe,KAAa,WAA4B;CAC7D,KAAK,IAAI,IAAI,QAAQ,IAAI,SAAS,WAAW,KAC5C,IAAI,IAAI,OAAO,GAAG,OAAO;CAE1B,OAAO;AACR;;;;;;AAOA,MAAM,mBAAmB,SAAyC;CACjE,MAAM,UAAkC,CAAC;CACzC,IAAI,MAAM;CACV,MAAM,OAAO,KAAK,SAAS,MAAM;CACjC,OAAO,MAAM,KAAK,QAAQ;EACzB,MAAM,QAAQ,KAAK,QAAQ,KAAK,GAAG;EACnC,IAAI,UAAU,IAAI;EAClB,MAAM,MAAM,SAAS,KAAK,MAAM,KAAK,KAAK,GAAG,EAAE;EAC/C,IAAI,OAAO,MAAM,GAAG,KAAK,OAAO,GAAG;EACnC,MAAM,SAAS,KAAK,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC;EAClD,MAAM,KAAK,OAAO,QAAQ,GAAG;EAC7B,IAAI,OAAO,IAAI,QAAQ,OAAO,MAAM,GAAG,EAAE,KAAK,OAAO,MAAM,KAAK,CAAC;EACjE,OAAO;CACR;CACA,OAAO;AACR;;;;;;;;AASA,MAAa,YAAY,QAA4B;CACpD,MAAM,UAAsB,CAAC;CAE7B,IAAI;CACJ,IAAI;CACJ,IAAI,SAAS;CAEb,OAAO,SAAS,aAAa,IAAI,QAAQ;EACxC,IAAI,YAAY,KAAK,MAAM,GAAG;EAE9B,IAAI,OAAO,cAAc,KAAK,QAAQ,GAAG;EACzC,MAAM,OAAO,aAAa,KAAK,SAAS,KAAK,CAAC;EAC9C,MAAM,OAAO,aAAa,KAAK,SAAS,KAAK,EAAE;EAC/C,MAAM,WAAW,IAAI,SAAS;EAC9B,MAAM,OAAO,aAAa,IAAI,MAAM,OAAO,aAAa,QAAQ;EAChE,IAAI,WAAW,cAAc,KAAK,SAAS,KAAK,GAAG;EAEnD,IADc,cAAc,KAAK,SAAS,KAAK,CACvC,CAAC,CAAC,WAAW,OAAO,GAAG;GAC9B,MAAM,SAAS,cAAc,KAAK,SAAS,KAAK,GAAG;GACnD,IAAI,WAAW,IAAI,OAAO,GAAG,OAAO,GAAG;EACxC;EAEA,UAAU;EACV,MAAM,OAAO,IAAI,SAAS,QAAQ,SAAS,IAAI;EAC/C,UAAU,KAAK,KAAK,OAAO,SAAS,IAAI;EAExC,IAAI,SAAS,KAAK;GACjB,MAAM,UAAU,gBAAgB,IAAI;GACpC,IAAI,QAAQ,SAAS,KAAA,GAAW,eAAe,QAAQ;GACvD,IAAI,QAAQ,aAAa,KAAA,GAAW,eAAe,QAAQ;GAC3D;EACD;EACA,IAAI,SAAS,KAEZ;EAED,IAAI,SAAS,OAAO,SAAS,KAAK;GACjC,MAAM,YAAY,KAAK,SAAS,MAAM,CAAC,CAAC,QAAQ,QAAQ,EAAE;GAC1D,IAAI,SAAS,KAAK,eAAe;QAC5B,eAAe;GACpB;EACD;EAEA,IAAI,iBAAiB,KAAA,GAAW,OAAO;EACvC,IAAI,iBAAiB,KAAA,GAAW,WAAW;EAC3C,eAAe,KAAA;EACf,eAAe,KAAA;EAEf,QAAQ,KAAK;GAAE;GAAM;GAAM;GAAM;GAAU,MAAM,OAAO,KAAK,IAAI;EAAE,CAAC;CACrE;CAEA,OAAO;AACR;;;;;;;AAQA,MAAa,uBACZ,SACA,WACoB;CACpB,MAAM,SAAS,GAAG,OAAO,QAAQ,cAAc,EAAE,EAAE;CACnD,MAAM,QAAwB,CAAC;CAC/B,KAAK,MAAM,SAAS,SAAS;EAG5B,MAAM,QAAQ,MAAM,KAAK,QAAQ,GAAG;EACpC,IAAI,UAAU,IAAI;EAClB,MAAM,WAAW,MAAM,KAAK,MAAM,QAAQ,CAAC;EAC3C,IAAI,CAAC,SAAS,WAAW,MAAM,GAAG;EAClC,MAAM,OAAO,SAAS,MAAM,OAAO,MAAM;EACzC,IAAI,SAAS,IAAI;EACjB,IAAI,MAAM,SAAS,KAClB,MAAM,KAAK;GAAE,MAAM;GAAW;GAAM,QAAQ,MAAM;EAAS,CAAC;OACtD,IAAI,MAAM,SAAS,OAAO,MAAM,SAAS,KAC/C,MAAM,KAAK;GACV,MAAM;GACN;GACA,OAAO,MAAM;GACb,aAAa,MAAM,OAAO,QAAW;EACtC,CAAC;CAGH;CACA,OAAO;AACR;AAEA,MAAM,cAAc,aAAwC;CAC3D,MAAM,EAAE,OAAO,MAAM,QAAQ,SAAS;CACtC,OAAO,GAAG,aAAa,EAAE,GAAG,MAAM,GAAG,KAAK,UAAU;AACrD;AAEA,MAAM,uBAAuB,QAAgB,QAAuB;CACnE,IAAI,WAAW,KACd,uBAAO,IAAI,MACV,2BAA2B,IAAI,2CAChC;CAED,IAAI,WAAW,OAAO,WAAW,KAChC,uBAAO,IAAI,MACV,8CAA8C,IAAI,2EACnD;CAED,uBAAO,IAAI,MAAM,wBAAwB,OAAO,OAAO,IAAI,EAAE;AAC9D;;;;;;;AAQA,MAAa,mBAAmB,OAC/B,aAC6B;CAC7B,MAAM,MAAM,WAAW,QAAQ;CAE/B,IAAI;CACJ,IAAI;EACH,MAAM,MAAM,MAAM,MAAM,KAAK;GAC5B,SAAS,gBAAgB;GACzB,QAAQ,YAAY,QAAQ,GAAM;EACnC,CAAC;EACD,IAAI,CAAC,IAAI,IAAI,MAAM,oBAAoB,IAAI,QAAQ,GAAG;EACtD,UAAU,OAAO,KAAK,MAAM,IAAI,YAAY,CAAC;CAC9C,SAAS,KAAK;EACb,MAAM,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;CACzD;CAEA,IAAI;CACJ,IAAI;EACH,MAAM,OAAO,KAAK,WAAW,IAAI,WAAW,OAAO,CAAC,CAAC;CACtD,SAAS,KAAK;EACb,MAAM,IAAI,MACT,kDAAkD,IAAI,IACrD,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,GAEjD;CACD;CAEA,MAAM,EAAE,OAAO,MAAM,KAAK,WAAW,SAAS;CAC9C,MAAM,QAAQ,oBAAoB,SAAS,GAAG,GAAG,MAAM;CACvD,IAAI,MAAM,WAAW,GACpB,MAAM,IAAI,MACT,0BAA0B,OAAO,qBAAqB,MAAM,GAAG,KAAK,GAAG,IAAI,EAC5E;CAED,OAAO;AACR;;;;;;;AAYA,IAAa,sBAAb,cAAyC,MAAM;CAC9C;CACA,YAAY,SAAiB,WAAmB;EAC/C,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,YAAY;CAClB;AACD;;;;;;;AAQA,MAAa,sBAAsB,KAAa,UAAyB;CACxE,IAAI,CAAC,WAAW,GAAG,GAAG;CACtB,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC,YAAY,GAC9B,MAAM,IAAI,oBACT,UAAU,IAAI,0CACd,sBACD;CAGD,IADiB,YAAY,GAAG,CAAC,CAAC,QAAQ,SAAS,SAAS,MACjD,CAAC,CAAC,SAAS,KAAK,CAAC,OAC3B,MAAM,IAAI,oBACT,oBAAoB,IAAI,6HACxB,kBACD;AAEF;AAEA,MAAM,aAAa,SAA0B;CAC5C,IAAI;EACH,OAAO,UAAU,IAAI,CAAC,CAAC,eAAe;CACvC,QAAQ;EACP,OAAO;CACR;AACD;AAEA,MAAM,aAAa,QAAqC;CACvD,IACC,OAAO,QAAQ,YACf,QAAQ,QACR,UAAU,OACV,OAAO,IAAI,SAAS,UAEpB,OAAO,IAAI;AAGb;AAEA,MAAM,gBACL,MACA,QACA,WACU;CACV,IAAI,UAAU,IAAI,GAAG,OAAO,MAAM,EAAE,OAAO,KAAK,CAAC;CACjD,IAAI;EACH,YAAY,QAAQ,IAAI;CACzB,SAAS,KAAK;EAIb,IAAI,UAAU,GAAG,MAAM,WAAW,QAAQ,aAAa,SAAS;GAC/D,SACC,4BAA4B,KAAK,MAAM,OAAO,sCAC/C;GACA,cAAc,MAAM,MAAM;GAC1B;EACD;EACA,MAAM;CACP;AACD;;;;;;;;AAcA,MAAa,mBAAmB,OAC/B,UACA,WACA,UAA2B,CAAC,MACP;CACrB,MAAM,QAAQ,MAAM,iBAAiB,QAAQ;CAE7C,UAAU,WAAW,EAAE,WAAW,KAAK,CAAC;CACxC,KAAK,MAAM,QAAQ,OAAO;EACzB,MAAM,OAAO,KAAK,WAAW,KAAK,IAAI;EACtC,UAAU,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;EAC5C,IAAI,KAAK,SAAS,WACjB,aAAa,MAAM,KAAK,QAAQ,QAAQ,MAAM;OACxC;GACN,cAAc,MAAM,KAAK,KAAK;GAC9B,IAAI,KAAK,YAAY,UAAU,MAAM,GAAK;EAC3C;CACD;CACA,OAAO,MAAM;AACd"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.d.ts","names":[],"sources":["../../../src/lib/phases/setup.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"setup.d.ts","names":[],"sources":["../../../src/lib/phases/setup.ts"],"mappings":";;;;UAwBiB,iBAAA;;EAAA;EAAiB,GAAA,CAAA,EAAA,MAAA;;SAatB,CAAA,EAAA,OAAA;EAAW;EA2BD,MAAA,CAAA,EAAA,OAAA;EAAgB;UAC5B,CAAA,EAAA,MAAA;;kBACP,CAAA,EA/BiB,WA+BjB,EAAA;EAAO;aA7BE;;;;;;;;;;;;;;;;;;;;;;;iBA2BU,gBAAA,UACZ,oBACP,QAAQ"}
|
package/dist/lib/phases/setup.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveAddMcpAgentId } from "../agents.js";
|
|
2
|
-
import { FALLBACK_TEMPLATES, fetchTemplates } from "../bootstrap.js";
|
|
2
|
+
import { FALLBACK_TEMPLATES, fetchTemplates, findTemplate, scaffoldTemplate } from "../bootstrap.js";
|
|
3
3
|
import { detectIde, isCursorInstalled, isVSCodeInstalled } from "../detect-agent.js";
|
|
4
4
|
import { NEON_EXTENSION_ID, downloadVsix } from "../vsix.js";
|
|
5
5
|
import { findEditorCommand } from "../extension.js";
|
|
@@ -266,18 +266,9 @@ async function executeBatchedInstallation(options) {
|
|
|
266
266
|
const results = [];
|
|
267
267
|
const isBootstrap = !!options.template;
|
|
268
268
|
if (isBootstrap && options.template) try {
|
|
269
|
-
await
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
"bootstrap",
|
|
273
|
-
".",
|
|
274
|
-
"--template",
|
|
275
|
-
options.template,
|
|
276
|
-
"--force"
|
|
277
|
-
], {
|
|
278
|
-
stdio: "pipe",
|
|
279
|
-
timeout: 12e4
|
|
280
|
-
});
|
|
269
|
+
const template = findTemplate(await fetchTemplates(), options.template) ?? findTemplate(FALLBACK_TEMPLATES, options.template);
|
|
270
|
+
if (!template) throw new Error(`Unknown template "${options.template}".`);
|
|
271
|
+
await scaffoldTemplate(template, ".");
|
|
281
272
|
results.push({
|
|
282
273
|
id: "bootstrap",
|
|
283
274
|
description: `Scaffolded project from template "${options.template}"`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.js","names":[],"sources":["../../../src/lib/phases/setup.ts"],"sourcesContent":["import { writeFileSync } from \"node:fs\";\nimport { unlink } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { execa } from \"execa\";\nimport { resolveAddMcpAgentId } from \"../agents.js\";\nimport {\n\tFALLBACK_TEMPLATES,\n\tfetchTemplates,\n\ttype NeonFeature,\n} from \"../bootstrap.js\";\nimport {\n\tdetectIde,\n\tisCursorInstalled,\n\tisVSCodeInstalled,\n} from \"../detect-agent.js\";\nimport { findEditorCommand } from \"../extension.js\";\nimport { inspectProject } from \"../inspect.js\";\nimport { ensureNeonctl } from \"../neonctl.js\";\nimport { ensureSkillsUpToDate } from \"../skills.js\";\nimport type { Editor, PhaseResponse } from \"../types.js\";\nimport { downloadVsix, NEON_EXTENSION_ID } from \"../vsix.js\";\n\nexport interface SetupPhaseOptions {\n\tagent?: string;\n\t/** The IDE/editor the user is running in (e.g. \"cursor\", \"vscode\") — reported by agent */\n\tide?: string;\n\t/** Enable preview skills (neon-object-storage, neon-functions, neon-ai-gateway) */\n\tpreview?: boolean;\n\t/** Whether the directory already contains an application */\n\thasApp?: boolean;\n\t/** Template ID to scaffold (when bootstrapping a new project) */\n\ttemplate?: string;\n\t/** Neon features required by the selected template */\n\ttemplateRequires?: NeonFeature[];\n\t/** Neon features selected by the user (brownfield flows) */\n\tfeatures?: NeonFeature[];\n\t// Inspection results — pre-filled by orchestrator or reported by agent\n\tmcpConfigured?: boolean | null;\n\tskillsInstalled?: boolean | null;\n\tconnectionString?: boolean | null;\n\tconnectionParams?: string; // JSON with host/dbname/etc if found\n\tframework?: string;\n\torm?: string;\n\tmigrationTool?: string;\n\tmigrationDir?: string;\n\tisVscodeIde?: boolean | null;\n\t// User preferences (also used for pre-detected scope from inspection)\n\tmode?: string;\n\tmcpScope?: string;\n\tskillsScope?: string;\n\tinstallExtension?: boolean;\n\t// Execution flags\n\texecute?: boolean;\n}\n\n/**\n * Comprehensive setup phase: inspects repo state, collects user preferences,\n * then batches all installation commands together.\n *\n * With --data JSON, the agent sends inspection results AND user preferences\n * in a single call, so the CLI can go straight to installation.\n */\nexport async function handleSetupPhase(\n\toptions: SetupPhaseOptions,\n): Promise<PhaseResponse> {\n\t// Parse features from comma-separated string (e.g. \"database,auth\" from agent --data)\n\tif (typeof options.features === \"string\") {\n\t\toptions.features = (options.features as unknown as string)\n\t\t\t.split(\",\")\n\t\t\t.map((f) => f.trim()) as NeonFeature[];\n\t}\n\n\t// Treat \"none\" as no template selected\n\tconst templateWasAnswered = options.template !== undefined;\n\tif (options.template === \"none\") {\n\t\toptions.template = undefined;\n\t}\n\n\t// Resolve template requirements if a template was selected but requires not yet populated\n\tif (options.template && !options.templateRequires) {\n\t\tconst templates = await fetchTemplates();\n\t\tconst selected = templates.find((t) => t.id === options.template);\n\t\tif (selected) {\n\t\t\toptions.templateRequires = selected.requires;\n\t\t}\n\t}\n\n\t// --execute: run the batched installation (legacy path)\n\tif (options.execute) {\n\t\treturn executeBatchedInstallation(await mergeCliInspection(options));\n\t}\n\n\t// Treat any explicit mode value that isn't \"customize\"/\"custom\" as defaults.\n\t// Also treat as defaults when mode is missing but agent reported back\n\t// (template was answered, nothing left to customize).\n\tconst hasReportedBack = options.mode || templateWasAnswered;\n\tif (\n\t\thasReportedBack &&\n\t\toptions.mode !== \"customize\" &&\n\t\toptions.mode !== \"custom\"\n\t) {\n\t\tconst merged = await mergeCliInspection(options);\n\t\tconst shouldInstallExt =\n\t\t\tmerged.installExtension ?? isVscodeBasedIde(merged);\n\t\treturn executeBatchedInstallation({\n\t\t\t...merged,\n\t\t\tmcpScope: merged.mcpScope ?? \"global\",\n\t\t\tskillsScope: merged.skillsScope ?? \"project\",\n\t\t\tinstallExtension: shouldInstallExt,\n\t\t});\n\t}\n\n\t// User chose \"customize\" (also accept \"custom\" — agents sometimes truncate)\n\tif (options.mode === \"customize\" || options.mode === \"custom\") {\n\t\tconst merged = await mergeCliInspection(options);\n\t\tconst shouldInstallExt =\n\t\t\tmerged.installExtension ?? isVscodeBasedIde(merged);\n\t\treturn executeBatchedInstallation({\n\t\t\t...merged,\n\t\t\tmcpScope: merged.mcpScope ?? \"global\",\n\t\t\tskillsScope: merged.skillsScope ?? \"project\",\n\t\t\tinstallExtension: shouldInstallExt,\n\t\t});\n\t}\n\n\t// Default: send inspection checks with user preferences (all in one response)\n\treturn buildBulkInspection(options);\n}\n\nfunction buildTemplatePreference(\n\ttemplates: { id: string; title: string; description: string }[],\n) {\n\treturn [\n\t\t{\n\t\t\tid: \"template\",\n\t\t\tquestion:\n\t\t\t\t\"No application was detected in this directory. Would you like to scaffold a new project from a template?\",\n\t\t\tphase: \"before_checks\" as const,\n\t\t\toptions: [\n\t\t\t\t...templates.map((t) => ({\n\t\t\t\t\tvalue: t.id,\n\t\t\t\t\tlabel: `${t.title} — ${t.description}`,\n\t\t\t\t})),\n\t\t\t\t{\n\t\t\t\t\tvalue: \"none\",\n\t\t\t\t\tlabel: \"No thanks — continue without scaffolding\",\n\t\t\t\t},\n\t\t\t],\n\t\t\tdefault: \"none\",\n\t\t},\n\t];\n}\n\nasync function buildBulkInspection(\n\toptions: SetupPhaseOptions,\n): Promise<PhaseResponse> {\n\tconst hasApp = options.hasApp !== false;\n\tconst detectedIde = detectIde();\n\n\t// If no IDE detected (e.g. standalone terminal), check what's installed\n\tconst installedEditors: string[] = [];\n\tif (!detectedIde) {\n\t\tif (isCursorInstalled()) installedEditors.push(\"cursor\");\n\t\tif (isVSCodeInstalled()) installedEditors.push(\"vscode\");\n\t}\n\n\t// Fetch available templates when no app is detected\n\tlet templatePreferences: ReturnType<typeof buildTemplatePreference> = [];\n\tif (!hasApp) {\n\t\tlet templates = FALLBACK_TEMPLATES;\n\t\ttry {\n\t\t\tconst fetched = await fetchTemplates();\n\t\t\tif (fetched && fetched.length > 0) templates = fetched;\n\t\t} catch {}\n\t\ttemplatePreferences = buildTemplatePreference(templates);\n\t}\n\n\treturn {\n\t\tphase: \"setup\",\n\t\tstatus: hasApp ? \"pending\" : \"bootstrap_needed\",\n\t\tdetectedIde: detectedIde?.toLowerCase() ?? null,\n\t\tinstalledEditors: installedEditors.length > 0 ? installedEditors : null,\n\t\t// Pre-detected state from filesystem inspection\n\t\tmcpConfigured: options.mcpConfigured ?? null,\n\t\tmcpScope: options.mcpScope ?? null,\n\t\tskillsInstalled: options.skillsInstalled ?? null,\n\t\tskillsScope: options.skillsScope ?? null,\n\t\tnextAction: {\n\t\t\ttype: \"agent_check\",\n\t\t\tinstructions: [\n\t\t\t\t\"IMPORTANT: Do NOT summarize this response or ask the user for consent/confirmation before starting. Proceed IMMEDIATELY with the first userPreference question.\",\n\t\t\t\t\"\",\n\t\t\t\thasApp\n\t\t\t\t\t? \"Perform the agent checks listed above (MCP server status and your agent identity), then present each userPreference question to the user ONE AT A TIME, in order. Wait for the user's answer before showing the next question. Respect the `condition` field — only show a question if its condition is met.\"\n\t\t\t\t\t: \"No application was detected in this directory. Ask the user if they'd like to scaffold a new project from a template (the `template` preference). Present ALL template options and the 'No thanks' option — do NOT auto-select even if there is only one template. If the user selects a template, the scaffolded template includes agent skills so skills installation will be skipped. If the user chooses 'none', continue with the remaining setup preferences normally. Then perform the agent checks and present the remaining preferences ONE AT A TIME.\",\n\t\t\t\t\"\",\n\t\t\t\t`The CLI has pre-detected the following from the filesystem: MCP server: ${options.mcpConfigured ? `configured (${options.mcpScope})` : \"not configured\"}. Agent skills: ${options.skillsInstalled ? `installed (${options.skillsScope})` : String(options.skillsScope ?? \"\").includes(\"partial\") ? `partially installed (${options.skillsScope}) — missing skills will be auto-installed to the same scope` : \"not installed\"}. Report these findings to the user before asking preferences. Only ask about scope/options for components that are NOT already configured. Do NOT ask about skills scope if skills are partially installed — they will be completed automatically.`,\n\t\t\t\t\"\",\n\t\t\t\t\"IMPORTANT (Cursor users): Cursor disables project-level MCP servers by default as a security measure. If the user is in Cursor and chooses project-level MCP scope, warn them that they will need to manually enable the Neon server in Cursor Settings > MCP after installation. Recommend global scope for Cursor to avoid this extra step.\",\n\t\t\t\t\"\",\n\t\t\t\t\"GROUPING: Preferences that share the same `group` field should be presented together in a single message (e.g. list all customize options at once and let the user answer them together). Preferences without a `group` must be asked individually.\",\n\t\t\t\t\"\",\n\t\t\t\tdetectedIde\n\t\t\t\t\t? `The CLI has detected the IDE as: ${detectedIde.toLowerCase()}. Include this as the \"ide\" field in your reportBack data. IMPORTANT: The IDE and the agent are different — you may be Claude Code (agent) running inside Cursor (IDE). The extension installs into the IDE, so if the IDE is Cursor/VS Code/Windsurf, the extension IS applicable even if you are Claude Code.`\n\t\t\t\t\t: installedEditors.length > 0\n\t\t\t\t\t\t? `No IDE detected, but the following editors are installed: ${installedEditors.join(\", \")}. The \"installedEditors\" field in this response lists them. If the user wants the extension installed, ask which editor to install it for and include that as the \"ide\" field in your reportBack data. If not, set \"ide\" to \"none\".`\n\t\t\t\t\t\t: `No IDE or supported editors detected. Set \"ide\" to \"none\" in your reportBack data.`,\n\t\t\t\t\"\",\n\t\t\t\t\"After all questions are answered, call reportBack with a single --data JSON containing: agent, ide, mcpConfigured, and all preference answers. The CLI will inspect the project and merge results automatically.\",\n\t\t\t].join(\"\\n\"),\n\t\t\tchecks: [\n\t\t\t\t{\n\t\t\t\t\tid: \"neonctl\",\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t\"The neonctl CLI will be installed or updated automatically (no action needed from the agent)\",\n\t\t\t\t\tlookFor: [],\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tid: \"mcp_server\",\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t\"Check if the Neon MCP server is already configured in your MCP server list\",\n\t\t\t\t\tlookFor: [\n\t\t\t\t\t\t\"An MCP server entry named 'Neon' or with URL containing 'mcp.neon.tech'\",\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tid: \"agent_type\",\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t\"Identify which coding agent is running this command\",\n\t\t\t\t\tlookFor: [\n\t\t\t\t\t\t\"Determine which agent you are: cursor, claude-code, copilot, vscode, windsurf, codex, cline, gemini-cli, goose, opencode, or antigravity\",\n\t\t\t\t\t\t\"Report your own agent identifier — this is used to configure the MCP server for the correct tool\",\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\t...(detectedIde\n\t\t\t\t\t? [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tid: \"extension_installed\",\n\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t\"Check if the Neon editor extension (databricks.neon-local-connect) is already installed in the IDE (NOT the agent — e.g. if you are Claude Code running inside Cursor, check Cursor's extensions)\",\n\t\t\t\t\t\t\t\tlookFor: [\n\t\t\t\t\t\t\t\t\t\"Run the IDE's --list-extensions command or check installed extensions for 'databricks.neon-local-connect' or 'Neon Local Connect'\",\n\t\t\t\t\t\t\t\t\t\"If the extension is found, set installExtension to false in your reportBack data and SKIP the installExtension question\",\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t]\n\t\t\t\t\t: []),\n\t\t\t],\n\t\t\tuserPreferences: [\n\t\t\t\t...templatePreferences,\n\t\t\t\t// For brownfield flows, ask which Neon features to enable\n\t\t\t\t...(hasApp\n\t\t\t\t\t? [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tid: \"features\",\n\t\t\t\t\t\t\t\tquestion:\n\t\t\t\t\t\t\t\t\t\"Which Neon features would you like to enable for this project?\",\n\t\t\t\t\t\t\t\tphase: \"after_checks\" as const,\n\t\t\t\t\t\t\t\toptions: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tvalue: \"database\",\n\t\t\t\t\t\t\t\t\t\tlabel: \"Database (always included)\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tvalue: \"database,auth\",\n\t\t\t\t\t\t\t\t\t\tlabel: \"Database + Neon Auth (adds authentication via Neon)\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\tdefault: \"database\",\n\t\t\t\t\t\t\t\tcontext:\n\t\t\t\t\t\t\t\t\t\"Database connectivity is always set up. Neon Auth adds user authentication powered by Neon. More features (Functions, AI Gateway, Object Storage) will be available soon.\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t]\n\t\t\t\t\t: []),\n\t\t\t\t// Only show defaults/customize when there's something to customize:\n\t\t\t\t// MCP not configured, skills need scope choice, or extension not detected.\n\t\t\t\t...(() => {\n\t\t\t\t\tconst isPartialSkills = String(\n\t\t\t\t\t\toptions.skillsScope ?? \"\",\n\t\t\t\t\t).includes(\"partial\");\n\t\t\t\t\tconst needsMcpChoice = !options.mcpConfigured;\n\t\t\t\t\tconst needsSkillsChoice =\n\t\t\t\t\t\t!options.skillsInstalled && !isPartialSkills;\n\t\t\t\t\tconst hasCustomizableOptions =\n\t\t\t\t\t\tneedsMcpChoice || needsSkillsChoice;\n\t\t\t\t\tif (!hasCustomizableOptions) return [];\n\t\t\t\t\treturn [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tid: \"mode\",\n\t\t\t\t\t\t\tquestion: \"Use default settings or customize?\",\n\t\t\t\t\t\t\tphase: \"after_checks\" as const,\n\t\t\t\t\t\t\toptions: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tvalue: \"defaults\",\n\t\t\t\t\t\t\t\t\tlabel: hasApp\n\t\t\t\t\t\t\t\t\t\t? \"Use defaults (neonctl CLI, MCP: global, skills: project-level, extension if applicable — already-configured components will be skipped)\"\n\t\t\t\t\t\t\t\t\t\t: \"Use defaults (neonctl CLI, MCP: global, extension if applicable — skills included in template)\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tvalue: \"customize\",\n\t\t\t\t\t\t\t\t\tlabel: \"Customize installation settings\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tdefault: \"defaults\",\n\t\t\t\t\t\t},\n\t\t\t\t\t];\n\t\t\t\t})(),\n\t\t\t\t{\n\t\t\t\t\tid: \"mcpScope\",\n\t\t\t\t\tquestion: \"Where should the Neon MCP server be configured?\",\n\t\t\t\t\tcontext:\n\t\t\t\t\t\t\"SKIP this question entirely if the mcp_server check found it is already configured. Only ask if MCP is NOT yet configured. NOTE: Cursor disables project-level MCP servers by default — if the user is in Cursor, recommend global scope or warn that they will need to manually enable the server in Cursor Settings > MCP.\",\n\t\t\t\t\tphase: \"after_checks\",\n\t\t\t\t\toptions: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvalue: \"global\",\n\t\t\t\t\t\t\tlabel: \"Global (available in all projects)\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvalue: \"project\",\n\t\t\t\t\t\t\tlabel: \"Project-level (scoped to this project only)\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvalue: \"none\",\n\t\t\t\t\t\t\tlabel: \"Skip — do not install the MCP server\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdefault: \"global\",\n\t\t\t\t\tcondition: { preferenceId: \"mode\", equals: \"customize\" },\n\t\t\t\t\tgroup: \"customize\",\n\t\t\t\t},\n\t\t\t\t// Show skills scope when skills aren't detected and no partial install exists.\n\t\t\t\t// Partial installations are auto-completed to the same scope silently.\n\t\t\t\t...(!options.skillsInstalled &&\n\t\t\t\t!String(options.skillsScope ?? \"\").includes(\"partial\")\n\t\t\t\t\t? [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tid: \"skillsScope\",\n\t\t\t\t\t\t\t\tquestion:\n\t\t\t\t\t\t\t\t\t\"Where should Neon agent skills be installed?\",\n\t\t\t\t\t\t\t\tcontext:\n\t\t\t\t\t\t\t\t\t\"Only ask if skills are not already installed.\",\n\t\t\t\t\t\t\t\tphase: \"after_checks\" as const,\n\t\t\t\t\t\t\t\toptions: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tvalue: \"global\",\n\t\t\t\t\t\t\t\t\t\tlabel: \"Global (available in all projects)\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tvalue: \"project\",\n\t\t\t\t\t\t\t\t\t\tlabel: \"Project-level (scoped to this project only)\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\tdefault: \"project\",\n\t\t\t\t\t\t\t\tcondition: {\n\t\t\t\t\t\t\t\t\tpreferenceId: \"mode\",\n\t\t\t\t\t\t\t\t\tequals: \"customize\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tgroup: \"customize\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t]\n\t\t\t\t\t: []),\n\t\t\t\t{\n\t\t\t\t\tid: \"installExtension\",\n\t\t\t\t\tquestion:\n\t\t\t\t\t\t\"Install the Neon editor extension for local database browsing?\",\n\t\t\t\t\tphase: \"after_checks\",\n\t\t\t\t\toptions: [\n\t\t\t\t\t\t{ value: \"true\", label: \"Yes\" },\n\t\t\t\t\t\t{ value: \"false\", label: \"No\" },\n\t\t\t\t\t],\n\t\t\t\t\tdefault: \"true\",\n\t\t\t\t\tcontext:\n\t\t\t\t\t\t\"The extension installs into the IDE, NOT the agent. If the CLI detected the IDE (see detectedIde field), use that — e.g. Claude Code running inside Cursor means the IDE is Cursor and the extension IS applicable. Only applicable for VS Code-based IDEs (VS Code, Cursor, Windsurf). SKIP this question if the user is NOT in a VS Code-based IDE, or if the extension_installed check found it is already installed. Set installExtension to false in reportBack if skipped.\",\n\t\t\t\t\tcondition: { preferenceId: \"mode\", equals: \"customize\" },\n\t\t\t\t\tgroup: \"customize\",\n\t\t\t\t},\n\t\t\t],\n\t\t\treportBack: {\n\t\t\t\ttype: \"run_neon_init\",\n\t\t\t\targs: [\n\t\t\t\t\t\"setup\",\n\t\t\t\t\t\"--json\",\n\t\t\t\t\t\"--data\",\n\t\t\t\t\t(() => {\n\t\t\t\t\t\tconst partialScope = String(\n\t\t\t\t\t\t\toptions.skillsScope ?? \"\",\n\t\t\t\t\t\t).replace(\"-partial\", \"\");\n\t\t\t\t\t\tconst hasPartial = String(\n\t\t\t\t\t\t\toptions.skillsScope ?? \"\",\n\t\t\t\t\t\t).includes(\"partial\");\n\t\t\t\t\t\tconst previewFlag = options.preview\n\t\t\t\t\t\t\t? \", preview: true\"\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\t\tconst needsMcpChoice = !options.mcpConfigured;\n\t\t\t\t\t\tconst needsSkillsChoice =\n\t\t\t\t\t\t\t!options.skillsInstalled && !hasPartial;\n\t\t\t\t\t\tconst hasModeQuestion =\n\t\t\t\t\t\t\tneedsMcpChoice || needsSkillsChoice;\n\t\t\t\t\t\tconst modeField = hasModeQuestion\n\t\t\t\t\t\t\t? \", mode: string\"\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\t\tconst mcpField = hasModeQuestion\n\t\t\t\t\t\t\t? \", mcpScope?: 'global'|'project'|'none'\"\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\t\tconst skillsField = needsSkillsChoice\n\t\t\t\t\t\t\t? \", skillsScope?: string\"\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\t\tconst extField = hasModeQuestion\n\t\t\t\t\t\t\t? \", installExtension?: bool\"\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\t\tconst prefilledSkills =\n\t\t\t\t\t\t\toptions.skillsInstalled || hasPartial\n\t\t\t\t\t\t\t\t? `, skillsScope: \"${options.skillsInstalled ? options.skillsScope || \"project\" : partialScope}\"`\n\t\t\t\t\t\t\t\t: skillsField;\n\t\t\t\t\t\treturn `<json: { agent: string, ide: string, mcpConfigured: bool${prefilledSkills}${previewFlag}${modeField}${mcpField}${extField}${hasApp ? \", features?: string\" : \", template: string\"} }>`;\n\t\t\t\t\t})(),\n\t\t\t\t],\n\t\t\t},\n\t\t},\n\t};\n}\n\nfunction _buildModeQuestion(options: SetupPhaseOptions): PhaseResponse {\n\tconst agentArgs = options.agent ? [\"--agent\", options.agent] : [];\n\n\t// Build a context summary from what the agent found\n\tconst findings: string[] = [];\n\tif (options.mcpConfigured) {\n\t\tfindings.push(\n\t\t\t\"Neon MCP server is already configured (will be upgraded to evergreen)\",\n\t\t);\n\t} else {\n\t\tfindings.push(\"Neon MCP server is not configured\");\n\t}\n\tif (options.connectionString) {\n\t\tfindings.push(\"A Neon connection string was found in the project\");\n\t} else {\n\t\tfindings.push(\"No Neon connection string found — will need to add one\");\n\t}\n\tif (options.framework && options.framework !== \"none\") {\n\t\tfindings.push(`Framework detected: ${options.framework}`);\n\t}\n\tif (options.orm && options.orm !== \"none\") {\n\t\tfindings.push(`ORM detected: ${options.orm}`);\n\t}\n\tif (options.migrationTool && options.migrationTool !== \"none\") {\n\t\tfindings.push(`Migration tool detected: ${options.migrationTool}`);\n\t}\n\tif (options.isVscodeIde) {\n\t\tfindings.push(\"VS Code-based IDE detected — Neon extension available\");\n\t}\n\n\tconst inspectionArgs = buildInspectionArgs(options);\n\n\t// Build defaults label showing only what will be installed\n\tconst defaultsParts: string[] = [\"neonctl CLI\"];\n\tif (!options.mcpConfigured) defaultsParts.push(\"MCP global\");\n\tdefaultsParts.push(\"skills in project\");\n\tif (options.isVscodeIde) defaultsParts.push(\"install extension\");\n\tconst defaultsLabel =\n\t\tdefaultsParts.length > 0\n\t\t\t? `Use defaults (${defaultsParts.join(\", \")})`\n\t\t\t: \"Use defaults\";\n\n\treturn {\n\t\tphase: \"setup\",\n\t\tstatus: \"preferences_needed\",\n\t\tinspection: {\n\t\t\tmcpConfigured: options.mcpConfigured,\n\t\t\tconnectionString: options.connectionString,\n\t\t\tframework: options.framework,\n\t\t\torm: options.orm,\n\t\t\tmigrationTool: options.migrationTool,\n\t\t\tmigrationDir: options.migrationDir,\n\t\t\tisVscodeIde: options.isVscodeIde,\n\t\t},\n\t\tnextAction: {\n\t\t\ttype: \"ask_user\",\n\t\t\tquestion: \"Use default settings or customize?\",\n\t\t\toptions: [\n\t\t\t\t{\n\t\t\t\t\tvalue: \"defaults\",\n\t\t\t\t\tlabel: defaultsLabel,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tvalue: \"customize\",\n\t\t\t\t\tlabel: \"Customize installation settings\",\n\t\t\t\t},\n\t\t\t],\n\t\t\tcontext: `Project inspection results:\\n${findings.map((f) => `- ${f}`).join(\"\\n\")}`,\n\t\t\tresponseMapping: {\n\t\t\t\tdefaults: {\n\t\t\t\t\targs: [\n\t\t\t\t\t\t\"setup\",\n\t\t\t\t\t\t\"--json\",\n\t\t\t\t\t\t...agentArgs,\n\t\t\t\t\t\t...inspectionArgs,\n\t\t\t\t\t\t\"--mode\",\n\t\t\t\t\t\t\"defaults\",\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\tcustomize: {\n\t\t\t\t\targs: [\n\t\t\t\t\t\t\"setup\",\n\t\t\t\t\t\t\"--json\",\n\t\t\t\t\t\t...agentArgs,\n\t\t\t\t\t\t...inspectionArgs,\n\t\t\t\t\t\t\"--mode\",\n\t\t\t\t\t\t\"customize\",\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t};\n}\n\nfunction _buildCustomizeQuestions(options: SetupPhaseOptions): PhaseResponse {\n\tconst agentArgs = options.agent ? [\"--agent\", options.agent] : [];\n\tconst inspectionArgs = buildInspectionArgs(options);\n\n\tconst needsMcp = !options.mcpConfigured;\n\tconst mcpScopes = needsMcp ? [\"global\", \"project\", \"none\"] : [\"skip\"];\n\tconst skillsScopes = [\"global\", \"project\"];\n\tconst extOptions = options.isVscodeIde ? [\"ext\", \"noext\"] : [\"ext\"];\n\n\t// Build all combinations of configurable options\n\tconst customOptions: { value: string; label: string }[] = [];\n\tfor (const mcp of mcpScopes) {\n\t\tfor (const skills of skillsScopes) {\n\t\t\tfor (const ext of extOptions) {\n\t\t\t\tconst parts: string[] = [];\n\t\t\t\tif (mcp === \"none\") parts.push(\"Skip MCP\");\n\t\t\t\telse if (mcp !== \"skip\") parts.push(`MCP: ${mcp}`);\n\t\t\t\tif (skills !== \"skip\")\n\t\t\t\t\tparts.push(\n\t\t\t\t\t\t`Skills: ${skills === \"project\" ? \"project-level\" : skills}`,\n\t\t\t\t\t);\n\t\t\t\tif (options.isVscodeIde) {\n\t\t\t\t\tparts.push(\n\t\t\t\t\t\text === \"ext\" ? \"Install extension\" : \"Skip extension\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tcustomOptions.push({\n\t\t\t\t\tvalue: `${mcp}_${skills}_${ext}`,\n\t\t\t\t\tlabel: parts.join(\", \"),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\tconst responseMapping: Record<string, { args: string[] }> = {};\n\n\tfor (const opt of customOptions) {\n\t\tconst parts = opt.value.split(\"_\");\n\t\tconst mcpScope = parts[0] === \"skip\" ? \"global\" : parts[0]; // \"none\" passes through\n\t\tconst skillsScope = parts[1] === \"skip\" ? \"project\" : parts[1];\n\t\tconst installExt = parts[2] === \"ext\";\n\n\t\tresponseMapping[opt.value] = {\n\t\t\targs: [\n\t\t\t\t\"setup\",\n\t\t\t\t\"--json\",\n\t\t\t\t...agentArgs,\n\t\t\t\t...inspectionArgs,\n\t\t\t\t\"--mode\",\n\t\t\t\t\"customize\",\n\t\t\t\t\"--mcp-scope\",\n\t\t\t\tmcpScope,\n\t\t\t\t\"--skills-scope\",\n\t\t\t\tskillsScope,\n\t\t\t\t...(options.isVscodeIde\n\t\t\t\t\t? [\"--install-extension\", installExt ? \"true\" : \"false\"]\n\t\t\t\t\t: []),\n\t\t\t\t\"--execute\",\n\t\t\t],\n\t\t};\n\t}\n\n\treturn {\n\t\tphase: \"setup\",\n\t\tstatus: \"customizing\",\n\t\tnextAction: {\n\t\t\ttype: \"ask_user\",\n\t\t\tquestion: \"Choose your installation configuration:\",\n\t\t\toptions: customOptions,\n\t\t\tcontext:\n\t\t\t\t\"Global scope means settings apply across all your projects. Project-level means settings are scoped to this project only.\" +\n\t\t\t\t(options.mcpConfigured\n\t\t\t\t\t? \"\\nSince Neon tools are already installed, they will be upgraded to the latest evergreen version.\"\n\t\t\t\t\t: \"\") +\n\t\t\t\t(isCursorAgent(options)\n\t\t\t\t\t? \"\\nNote: Cursor disables project-level MCP servers by default. If you choose project scope, you will need to manually enable the Neon server in Cursor Settings > MCP.\"\n\t\t\t\t\t: \"\"),\n\t\t\tresponseMapping,\n\t\t},\n\t};\n}\n\ninterface InstallResult {\n\tid: string;\n\tdescription: string;\n\tstatus: \"success\" | \"failed\";\n\terror?: string;\n\t/** True when the step wasn't automated — the description contains manual instructions for the user */\n\tmanualAction?: boolean;\n\t/** Shell commands the agent can run to complete this step manually */\n\tcommands?: string[];\n}\n\n/**\n * Executes the batched installation of MCP server, skills, and extension.\n * Runs commands directly in the CLI process — the agent does NOT run these.\n * Returns results and chains to the getting-started phase.\n */\nasync function executeBatchedInstallation(\n\toptions: SetupPhaseOptions,\n): Promise<PhaseResponse> {\n\tconst mcpScope = options.mcpScope ?? \"global\";\n\tconst agentId = options.agent ?? \"cursor\";\n\tconst mcpAgentId = resolveAddMcpAgentId(agentId);\n\tconst installExt = options.installExtension === true;\n\n\tconst results: InstallResult[] = [];\n\tconst isBootstrap = !!options.template;\n\n\t// Step 0: Bootstrap project from template if specified\n\tif (isBootstrap && options.template) {\n\t\ttry {\n\t\t\t// Pin @latest (and -y) so a stale globally-installed neonctl can't be\n\t\t\t// picked up by npx — bootstrap's rate-limit fix lives in recent\n\t\t\t// neonctl, and this runs before ensureNeonctl() updates the global.\n\t\t\tawait execa(\n\t\t\t\t\"npx\",\n\t\t\t\t[\n\t\t\t\t\t\"-y\",\n\t\t\t\t\t\"neonctl@latest\",\n\t\t\t\t\t\"bootstrap\",\n\t\t\t\t\t\".\",\n\t\t\t\t\t\"--template\",\n\t\t\t\t\toptions.template,\n\t\t\t\t\t\"--force\",\n\t\t\t\t],\n\t\t\t\t{ stdio: \"pipe\", timeout: 120000 },\n\t\t\t);\n\t\t\tresults.push({\n\t\t\t\tid: \"bootstrap\",\n\t\t\t\tdescription: `Scaffolded project from template \"${options.template}\"`,\n\t\t\t\tstatus: \"success\",\n\t\t\t});\n\n\t\t\t// Write template features to .neon under _init (ephemeral, cleaned up when init completes)\n\t\t\tif (options.templateRequires) {\n\t\t\t\tconst neonContextPath = resolve(process.cwd(), \".neon\");\n\t\t\t\tconst context: Record<string, unknown> = {\n\t\t\t\t\t_init: { features: options.templateRequires },\n\t\t\t\t};\n\t\t\t\twriteFileSync(\n\t\t\t\t\tneonContextPath,\n\t\t\t\t\t`${JSON.stringify(context, null, 2)}\\n`,\n\t\t\t\t);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tresults.push({\n\t\t\t\tid: \"bootstrap\",\n\t\t\t\tdescription: `Failed to scaffold project from template \"${options.template}\"`,\n\t\t\t\tstatus: \"failed\",\n\t\t\t\terror: err instanceof Error ? err.message : \"Unknown error\",\n\t\t\t});\n\t\t}\n\t}\n\n\t// Step 1: Ensure neonctl CLI is installed and up to date\n\tconst neonctlResult = await ensureNeonctl();\n\tswitch (neonctlResult.status) {\n\t\tcase \"already_current\":\n\t\t\tresults.push({\n\t\t\t\tid: \"neonctl\",\n\t\t\t\tdescription: `neonctl CLI is up to date (v${neonctlResult.version})`,\n\t\t\t\tstatus: \"success\",\n\t\t\t});\n\t\t\tbreak;\n\t\tcase \"installed\":\n\t\t\tresults.push({\n\t\t\t\tid: \"neonctl\",\n\t\t\t\tdescription: `Installed neonctl CLI (v${neonctlResult.version})`,\n\t\t\t\tstatus: \"success\",\n\t\t\t});\n\t\t\tbreak;\n\t\tcase \"updated\":\n\t\t\tresults.push({\n\t\t\t\tid: \"neonctl\",\n\t\t\t\tdescription: `Updated neonctl CLI to v${neonctlResult.version}`,\n\t\t\t\tstatus: \"success\",\n\t\t\t});\n\t\t\tbreak;\n\t\tcase \"failed\":\n\t\t\tresults.push({\n\t\t\t\tid: \"neonctl\",\n\t\t\t\tdescription: \"Failed to install neonctl CLI\",\n\t\t\t\tstatus: \"failed\",\n\t\t\t\terror: neonctlResult.error,\n\t\t\t});\n\t\t\tbreak;\n\t}\n\n\t// Step 2: Install MCP server (skip if already configured)\n\tconst isCursor =\n\t\tmcpAgentId === \"cursor\" ||\n\t\toptions.ide?.toLowerCase() === \"cursor\" ||\n\t\toptions.agent?.toLowerCase() === \"cursor\";\n\n\tif (mcpScope === \"none\") {\n\t\tresults.push({\n\t\t\tid: \"skip_mcp\",\n\t\t\tdescription: \"Neon MCP server installation skipped by user\",\n\t\t\tstatus: \"success\",\n\t\t});\n\t} else if (options.mcpConfigured) {\n\t\tresults.push({\n\t\t\tid: \"skip_mcp\",\n\t\t\tdescription: \"Neon MCP server already configured\",\n\t\t\tstatus: \"success\",\n\t\t});\n\t} else {\n\t\tconst mcpArgs = [\n\t\t\t\"-y\",\n\t\t\t\"add-mcp\",\n\t\t\t\"https://mcp.neon.tech/mcp\",\n\t\t\t...(mcpScope === \"global\" ? [\"-g\"] : []),\n\t\t\t\"-n\",\n\t\t\t\"Neon\",\n\t\t\t\"-y\",\n\t\t\t\"-a\",\n\t\t\tmcpAgentId,\n\t\t];\n\t\ttry {\n\t\t\tawait execa(\"npx\", mcpArgs, { stdio: \"pipe\", timeout: 60000 });\n\t\t\tresults.push({\n\t\t\t\tid: \"install_mcp\",\n\t\t\t\tdescription: `Installed Neon MCP server (${mcpScope} scope)`,\n\t\t\t\tstatus: \"success\",\n\t\t\t});\n\n\t\t\t// Some editors disable newly added MCP servers by default.\n\t\t\t// Cursor: project-level servers are always disabled initially.\n\t\t\t// Claude Code: newly added servers require user approval.\n\t\t\tconst isClaudeCode =\n\t\t\t\tmcpAgentId === \"claude-code\" ||\n\t\t\t\toptions.agent?.toLowerCase() === \"claude-code\";\n\n\t\t\tif (isCursor && mcpScope === \"project\") {\n\t\t\t\tresults.push({\n\t\t\t\t\tid: \"enable_mcp\",\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t'Cursor disables project-level MCP servers by default. Open Cursor Settings > MCP and toggle the \"Neon\" server on.',\n\t\t\t\t\tstatus: \"success\",\n\t\t\t\t\tmanualAction: true,\n\t\t\t\t});\n\t\t\t} else if (isClaudeCode) {\n\t\t\t\tresults.push({\n\t\t\t\t\tid: \"enable_mcp\",\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t'Claude Code requires approval for newly added MCP servers. When prompted, approve the \"Neon\" MCP server to enable it. You can check MCP server status with /mcp in Claude Code.',\n\t\t\t\t\tstatus: \"success\",\n\t\t\t\t\tmanualAction: true,\n\t\t\t\t});\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tresults.push({\n\t\t\t\tid: \"install_mcp\",\n\t\t\t\tdescription: \"Failed to install Neon MCP server\",\n\t\t\t\tstatus: \"failed\",\n\t\t\t\terror: err instanceof Error ? err.message : \"Unknown error\",\n\t\t\t});\n\t\t}\n\t}\n\n\t// Step 3: Install/update skills (skip when bootstrapping — templates bundle skills)\n\tif (isBootstrap) {\n\t\tresults.push({\n\t\t\tid: \"install_skills\",\n\t\t\tdescription: \"Neon agent skills included in template\",\n\t\t\tstatus: \"success\",\n\t\t});\n\t} else {\n\t\tconst skillsScope = (options.skillsScope ?? \"project\") as\n\t\t\t| \"global\"\n\t\t\t| \"project\";\n\t\tconst skillsOk = await ensureSkillsUpToDate(\n\t\t\tagentId,\n\t\t\tskillsScope,\n\t\t\toptions.preview,\n\t\t);\n\t\tif (skillsOk) {\n\t\t\tresults.push({\n\t\t\t\tid: \"install_skills\",\n\t\t\t\tdescription: \"Neon agent skills installed\",\n\t\t\t\tstatus: \"success\",\n\t\t\t});\n\t\t} else {\n\t\t\t// Build the install commands for the agent to run directly\n\t\t\t// (sandboxed environments may block child process writes)\n\t\t\tconst { getSkillList } = await import(\"../skills.js\");\n\t\t\tconst skillList = getSkillList(options.preview);\n\t\t\tconst cmds = skillList.map(\n\t\t\t\t(s) =>\n\t\t\t\t\t`skills add neondatabase/agent-skills --skill ${s} --agent ${agentId}${skillsScope === \"global\" ? \" -g\" : \"\"} -y`,\n\t\t\t);\n\t\t\tresults.push({\n\t\t\t\tid: \"install_skills\",\n\t\t\t\tdescription:\n\t\t\t\t\t\"Failed to install Neon agent skills automatically. Run these commands to install manually:\",\n\t\t\t\tstatus: \"failed\",\n\t\t\t\tcommands: cmds,\n\t\t\t});\n\t\t}\n\t}\n\n\t// Step 4: Install editor extension if requested\n\t// Use the agent-reported IDE (not agent identity) — e.g. Claude Code running in\n\t// Cursor should install the extension for Cursor, not skip it.\n\tif (installExt) {\n\t\tconst extResult = await installExtensionForIde(options.ide ?? agentId);\n\t\tresults.push(extResult);\n\t}\n\n\t// Step 5: Write selected features to .neon under _init for brownfield flows\n\t// (Bootstrap flows already wrote _init in step 0)\n\tif (!isBootstrap && options.features && options.features.length > 0) {\n\t\tconst neonContextPath = resolve(process.cwd(), \".neon\");\n\t\tconst context: Record<string, unknown> = {\n\t\t\t_init: { features: options.features },\n\t\t};\n\t\twriteFileSync(neonContextPath, `${JSON.stringify(context, null, 2)}\\n`);\n\t}\n\n\tconst allSucceeded = results.every((r) => r.status === \"success\");\n\n\t// Build args to chain to the getting-started phase as a separate CLI call.\n\t// This ensures the agent gets a clean response with ONLY the getting-started\n\t// action — no competing \"results\" array to distract it.\n\tconst gettingStartedData: Record<string, unknown> = {};\n\tif (options.connectionString) gettingStartedData.hasConnectionString = true;\n\tif (options.framework) gettingStartedData.framework = options.framework;\n\tif (options.orm) gettingStartedData.orm = options.orm;\n\tif (options.migrationTool)\n\t\tgettingStartedData.migrationTool = options.migrationTool;\n\tif (options.migrationDir)\n\t\tgettingStartedData.migrationDir = options.migrationDir;\n\t// Pass features so getting-started knows which phases to chain to\n\tconst resolvedFeatures = options.templateRequires ?? options.features;\n\tif (resolvedFeatures && resolvedFeatures.length > 0)\n\t\tgettingStartedData.features = resolvedFeatures;\n\t// Bootstrap implies preview mode (new project in us-east required)\n\tif (isBootstrap) gettingStartedData.preview = true;\n\tconst gettingStartedArgs = [\n\t\t\"getting-started\",\n\t\t\"--json\",\n\t\t\"--data\",\n\t\tJSON.stringify(gettingStartedData),\n\t];\n\n\treturn {\n\t\tphase: \"setup\",\n\t\tstatus: allSucceeded ? \"installed\" : \"partial\",\n\t\tresults,\n\t\tnextAction: {\n\t\t\ttype: \"run_neon_init\",\n\t\t\targs: gettingStartedArgs,\n\t\t},\n\t};\n}\n\nfunction buildInspectionArgs(options: SetupPhaseOptions): string[] {\n\tconst args: string[] = [];\n\tif (options.mcpConfigured !== null && options.mcpConfigured !== undefined) {\n\t\targs.push(\"--mcp-configured\", options.mcpConfigured ? \"true\" : \"false\");\n\t}\n\tif (\n\t\toptions.connectionString !== null &&\n\t\toptions.connectionString !== undefined\n\t) {\n\t\targs.push(\n\t\t\t\"--connection-string\",\n\t\t\toptions.connectionString ? \"true\" : \"false\",\n\t\t);\n\t}\n\tif (options.framework) {\n\t\targs.push(\"--framework\", options.framework);\n\t}\n\tif (options.orm) {\n\t\targs.push(\"--orm\", options.orm);\n\t}\n\tif (options.migrationTool) {\n\t\targs.push(\"--migration-tool\", options.migrationTool);\n\t}\n\tif (options.migrationDir) {\n\t\targs.push(\"--migration-dir\", options.migrationDir);\n\t}\n\tif (options.isVscodeIde !== null && options.isVscodeIde !== undefined) {\n\t\targs.push(\"--is-vscode-ide\", options.isVscodeIde ? \"true\" : \"false\");\n\t}\n\treturn args;\n}\n\n/**\n * Fills in missing filesystem inspection fields by running inspectProject().\n * Agent-reported data (mcpConfigured, agent, mode, scopes) is preserved.\n * CLI-detectable fields (framework, orm, migrations, connectionString, isVscodeIde)\n * are filled in only if not already present.\n */\nasync function mergeCliInspection(\n\toptions: SetupPhaseOptions,\n): Promise<SetupPhaseOptions> {\n\t// If the agent already provided these, no need to re-inspect\n\tif (options.framework !== undefined && options.orm !== undefined) {\n\t\treturn options;\n\t}\n\n\tconst inspection = await inspectProject([\n\t\t{ id: \"connection_string\", description: \"\", lookFor: [] },\n\t\t{ id: \"project_stack\", description: \"\", lookFor: [] },\n\t\t{ id: \"migrations\", description: \"\", lookFor: [] },\n\t\t{ id: \"ide_type\", description: \"\", lookFor: [] },\n\t]);\n\n\t// Also detect IDE if not already reported by the agent\n\tconst ide =\n\t\toptions.ide?.toLowerCase().replace(/\\s+/g, \"-\") ||\n\t\tdetectIde()?.toLowerCase().replace(/\\s+/g, \"-\") ||\n\t\tundefined;\n\n\treturn {\n\t\t...options,\n\t\tide,\n\t\tconnectionString:\n\t\t\toptions.connectionString ??\n\t\t\t(inspection.connectionString as boolean | undefined),\n\t\tframework:\n\t\t\toptions.framework ?? (inspection.framework as string | undefined),\n\t\torm: options.orm ?? (inspection.orm as string | undefined),\n\t\tmigrationTool:\n\t\t\toptions.migrationTool ??\n\t\t\t(inspection.migrationTool as string | undefined),\n\t\tmigrationDir:\n\t\t\toptions.migrationDir ??\n\t\t\t(inspection.migrationDir as string | undefined),\n\t\tisVscodeIde:\n\t\t\toptions.isVscodeIde ??\n\t\t\t(inspection.isVscodeIde as boolean | undefined),\n\t};\n}\n\n/**\n * Checks whether the user is in a VS Code-based IDE that supports extensions.\n * Uses agent-reported `ide` field first, then falls back to `isVscodeIde` from inspection.\n */\nfunction isCursorAgent(options: SetupPhaseOptions): boolean {\n\tconst ide = options.ide?.toLowerCase();\n\tif (ide === \"cursor\") return true;\n\tconst agent = options.agent?.toLowerCase();\n\tif (agent === \"cursor\") return true;\n\treturn false;\n}\n\nfunction isVscodeBasedIde(options: SetupPhaseOptions): boolean {\n\tif (options.ide) {\n\t\tconst ide = options.ide.toLowerCase();\n\t\treturn (\n\t\t\tide === \"cursor\" ||\n\t\t\tide === \"vscode\" ||\n\t\t\tide === \"vs-code\" ||\n\t\t\tide === \"windsurf\"\n\t\t);\n\t}\n\treturn options.isVscodeIde === true;\n}\n\n/**\n * Resolves which IDE to install the extension for.\n * Accepts the agent-reported IDE value (preferred), the agent ID, or\n * falls back to env-var detection.\n */\nfunction resolveEditorForExtension(ideOrAgentId: string): Editor | null {\n\t// Map known IDE/agent identifiers to Editor types\n\tswitch (ideOrAgentId.toLowerCase()) {\n\t\tcase \"cursor\":\n\t\t\treturn \"Cursor\";\n\t\tcase \"vscode\":\n\t\tcase \"vs-code\":\n\t\tcase \"copilot\":\n\t\tcase \"github-copilot\":\n\t\tcase \"github-copilot-cli\":\n\t\t\treturn \"VS Code\";\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\n\t// Fall back to env-var detection\n\tconst ide = detectIde();\n\tif (ide === \"Cursor\" || ide === \"VS Code\") return ide;\n\n\treturn null;\n}\n\nconst MANUAL_INSTALL_MSG = `Search for \"Neon\" in the extensions panel (Cmd+Shift+X / Ctrl+Shift+X) and install \"Neon Local Connect\" by Databricks.`;\n\n/**\n * Installs the Neon extension for the detected IDE.\n *\n * Uses env-var detection to determine the IDE (not the agent identity),\n * so Claude Code running in Cursor correctly installs for Cursor.\n *\n * Strategy:\n * 1. Try `<editor> --install-extension <id>` directly (uses editor's configured marketplace)\n * 2. If that fails, download .vsix (from proxy or Open VSX) and install via local file\n * 3. If all else fails: return manual install instructions\n */\nasync function installExtensionForIde(agentId: string): Promise<InstallResult> {\n\tconst editorType = resolveEditorForExtension(agentId);\n\tif (!editorType) {\n\t\treturn {\n\t\t\tid: \"install_extension\",\n\t\t\tdescription: MANUAL_INSTALL_MSG,\n\t\t\tstatus: \"success\",\n\t\t\tmanualAction: true,\n\t\t};\n\t}\n\n\tconst editorCmd = await findEditorCommand(editorType);\n\tif (!editorCmd) {\n\t\treturn {\n\t\t\tid: \"install_extension\",\n\t\t\tdescription: MANUAL_INSTALL_MSG,\n\t\t\tstatus: \"success\",\n\t\t\tmanualAction: true,\n\t\t};\n\t}\n\n\t// Try direct marketplace install first (works if editor has marketplace configured)\n\ttry {\n\t\tawait execa(editorCmd, [\"--install-extension\", NEON_EXTENSION_ID], {\n\t\t\tstdio: \"pipe\",\n\t\t\ttimeout: 60000,\n\t\t});\n\t\treturn {\n\t\t\tid: \"install_extension\",\n\t\t\tdescription: `Installed Neon extension for ${editorType}`,\n\t\t\tstatus: \"success\",\n\t\t};\n\t} catch {\n\t\t// Fall through to VSIX download approach\n\t}\n\n\t// Download .vsix and install locally\n\tconst vsixPath = await downloadVsix();\n\tif (!vsixPath) {\n\t\treturn {\n\t\t\tid: \"install_extension\",\n\t\t\tdescription: MANUAL_INSTALL_MSG,\n\t\t\tstatus: \"success\",\n\t\t\tmanualAction: true,\n\t\t};\n\t}\n\n\ttry {\n\t\tawait execa(editorCmd, [\"--install-extension\", vsixPath], {\n\t\t\tstdio: \"pipe\",\n\t\t\ttimeout: 60000,\n\t\t});\n\t\treturn {\n\t\t\tid: \"install_extension\",\n\t\t\tdescription: `Installed Neon extension for ${editorType}`,\n\t\t\tstatus: \"success\",\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tid: \"install_extension\",\n\t\t\tdescription: MANUAL_INSTALL_MSG,\n\t\t\tstatus: \"success\",\n\t\t\tmanualAction: true,\n\t\t};\n\t} finally {\n\t\ttry {\n\t\t\tawait unlink(vsixPath);\n\t\t} catch {}\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"setup.js","names":[],"sources":["../../../src/lib/phases/setup.ts"],"sourcesContent":["import { writeFileSync } from \"node:fs\";\nimport { unlink } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { execa } from \"execa\";\nimport { resolveAddMcpAgentId } from \"../agents.js\";\nimport {\n\tFALLBACK_TEMPLATES,\n\tfetchTemplates,\n\tfindTemplate,\n\ttype NeonFeature,\n\tscaffoldTemplate,\n} from \"../bootstrap.js\";\nimport {\n\tdetectIde,\n\tisCursorInstalled,\n\tisVSCodeInstalled,\n} from \"../detect-agent.js\";\nimport { findEditorCommand } from \"../extension.js\";\nimport { inspectProject } from \"../inspect.js\";\nimport { ensureNeonctl } from \"../neonctl.js\";\nimport { ensureSkillsUpToDate } from \"../skills.js\";\nimport type { Editor, PhaseResponse } from \"../types.js\";\nimport { downloadVsix, NEON_EXTENSION_ID } from \"../vsix.js\";\n\nexport interface SetupPhaseOptions {\n\tagent?: string;\n\t/** The IDE/editor the user is running in (e.g. \"cursor\", \"vscode\") — reported by agent */\n\tide?: string;\n\t/** Enable preview skills (neon-object-storage, neon-functions, neon-ai-gateway) */\n\tpreview?: boolean;\n\t/** Whether the directory already contains an application */\n\thasApp?: boolean;\n\t/** Template ID to scaffold (when bootstrapping a new project) */\n\ttemplate?: string;\n\t/** Neon features required by the selected template */\n\ttemplateRequires?: NeonFeature[];\n\t/** Neon features selected by the user (brownfield flows) */\n\tfeatures?: NeonFeature[];\n\t// Inspection results — pre-filled by orchestrator or reported by agent\n\tmcpConfigured?: boolean | null;\n\tskillsInstalled?: boolean | null;\n\tconnectionString?: boolean | null;\n\tconnectionParams?: string; // JSON with host/dbname/etc if found\n\tframework?: string;\n\torm?: string;\n\tmigrationTool?: string;\n\tmigrationDir?: string;\n\tisVscodeIde?: boolean | null;\n\t// User preferences (also used for pre-detected scope from inspection)\n\tmode?: string;\n\tmcpScope?: string;\n\tskillsScope?: string;\n\tinstallExtension?: boolean;\n\t// Execution flags\n\texecute?: boolean;\n}\n\n/**\n * Comprehensive setup phase: inspects repo state, collects user preferences,\n * then batches all installation commands together.\n *\n * With --data JSON, the agent sends inspection results AND user preferences\n * in a single call, so the CLI can go straight to installation.\n */\nexport async function handleSetupPhase(\n\toptions: SetupPhaseOptions,\n): Promise<PhaseResponse> {\n\t// Parse features from comma-separated string (e.g. \"database,auth\" from agent --data)\n\tif (typeof options.features === \"string\") {\n\t\toptions.features = (options.features as unknown as string)\n\t\t\t.split(\",\")\n\t\t\t.map((f) => f.trim()) as NeonFeature[];\n\t}\n\n\t// Treat \"none\" as no template selected\n\tconst templateWasAnswered = options.template !== undefined;\n\tif (options.template === \"none\") {\n\t\toptions.template = undefined;\n\t}\n\n\t// Resolve template requirements if a template was selected but requires not yet populated\n\tif (options.template && !options.templateRequires) {\n\t\tconst templates = await fetchTemplates();\n\t\tconst selected = templates.find((t) => t.id === options.template);\n\t\tif (selected) {\n\t\t\toptions.templateRequires = selected.requires;\n\t\t}\n\t}\n\n\t// --execute: run the batched installation (legacy path)\n\tif (options.execute) {\n\t\treturn executeBatchedInstallation(await mergeCliInspection(options));\n\t}\n\n\t// Treat any explicit mode value that isn't \"customize\"/\"custom\" as defaults.\n\t// Also treat as defaults when mode is missing but agent reported back\n\t// (template was answered, nothing left to customize).\n\tconst hasReportedBack = options.mode || templateWasAnswered;\n\tif (\n\t\thasReportedBack &&\n\t\toptions.mode !== \"customize\" &&\n\t\toptions.mode !== \"custom\"\n\t) {\n\t\tconst merged = await mergeCliInspection(options);\n\t\tconst shouldInstallExt =\n\t\t\tmerged.installExtension ?? isVscodeBasedIde(merged);\n\t\treturn executeBatchedInstallation({\n\t\t\t...merged,\n\t\t\tmcpScope: merged.mcpScope ?? \"global\",\n\t\t\tskillsScope: merged.skillsScope ?? \"project\",\n\t\t\tinstallExtension: shouldInstallExt,\n\t\t});\n\t}\n\n\t// User chose \"customize\" (also accept \"custom\" — agents sometimes truncate)\n\tif (options.mode === \"customize\" || options.mode === \"custom\") {\n\t\tconst merged = await mergeCliInspection(options);\n\t\tconst shouldInstallExt =\n\t\t\tmerged.installExtension ?? isVscodeBasedIde(merged);\n\t\treturn executeBatchedInstallation({\n\t\t\t...merged,\n\t\t\tmcpScope: merged.mcpScope ?? \"global\",\n\t\t\tskillsScope: merged.skillsScope ?? \"project\",\n\t\t\tinstallExtension: shouldInstallExt,\n\t\t});\n\t}\n\n\t// Default: send inspection checks with user preferences (all in one response)\n\treturn buildBulkInspection(options);\n}\n\nfunction buildTemplatePreference(\n\ttemplates: { id: string; title: string; description: string }[],\n) {\n\treturn [\n\t\t{\n\t\t\tid: \"template\",\n\t\t\tquestion:\n\t\t\t\t\"No application was detected in this directory. Would you like to scaffold a new project from a template?\",\n\t\t\tphase: \"before_checks\" as const,\n\t\t\toptions: [\n\t\t\t\t...templates.map((t) => ({\n\t\t\t\t\tvalue: t.id,\n\t\t\t\t\tlabel: `${t.title} — ${t.description}`,\n\t\t\t\t})),\n\t\t\t\t{\n\t\t\t\t\tvalue: \"none\",\n\t\t\t\t\tlabel: \"No thanks — continue without scaffolding\",\n\t\t\t\t},\n\t\t\t],\n\t\t\tdefault: \"none\",\n\t\t},\n\t];\n}\n\nasync function buildBulkInspection(\n\toptions: SetupPhaseOptions,\n): Promise<PhaseResponse> {\n\tconst hasApp = options.hasApp !== false;\n\tconst detectedIde = detectIde();\n\n\t// If no IDE detected (e.g. standalone terminal), check what's installed\n\tconst installedEditors: string[] = [];\n\tif (!detectedIde) {\n\t\tif (isCursorInstalled()) installedEditors.push(\"cursor\");\n\t\tif (isVSCodeInstalled()) installedEditors.push(\"vscode\");\n\t}\n\n\t// Fetch available templates when no app is detected\n\tlet templatePreferences: ReturnType<typeof buildTemplatePreference> = [];\n\tif (!hasApp) {\n\t\tlet templates = FALLBACK_TEMPLATES;\n\t\ttry {\n\t\t\tconst fetched = await fetchTemplates();\n\t\t\tif (fetched && fetched.length > 0) templates = fetched;\n\t\t} catch {}\n\t\ttemplatePreferences = buildTemplatePreference(templates);\n\t}\n\n\treturn {\n\t\tphase: \"setup\",\n\t\tstatus: hasApp ? \"pending\" : \"bootstrap_needed\",\n\t\tdetectedIde: detectedIde?.toLowerCase() ?? null,\n\t\tinstalledEditors: installedEditors.length > 0 ? installedEditors : null,\n\t\t// Pre-detected state from filesystem inspection\n\t\tmcpConfigured: options.mcpConfigured ?? null,\n\t\tmcpScope: options.mcpScope ?? null,\n\t\tskillsInstalled: options.skillsInstalled ?? null,\n\t\tskillsScope: options.skillsScope ?? null,\n\t\tnextAction: {\n\t\t\ttype: \"agent_check\",\n\t\t\tinstructions: [\n\t\t\t\t\"IMPORTANT: Do NOT summarize this response or ask the user for consent/confirmation before starting. Proceed IMMEDIATELY with the first userPreference question.\",\n\t\t\t\t\"\",\n\t\t\t\thasApp\n\t\t\t\t\t? \"Perform the agent checks listed above (MCP server status and your agent identity), then present each userPreference question to the user ONE AT A TIME, in order. Wait for the user's answer before showing the next question. Respect the `condition` field — only show a question if its condition is met.\"\n\t\t\t\t\t: \"No application was detected in this directory. Ask the user if they'd like to scaffold a new project from a template (the `template` preference). Present ALL template options and the 'No thanks' option — do NOT auto-select even if there is only one template. If the user selects a template, the scaffolded template includes agent skills so skills installation will be skipped. If the user chooses 'none', continue with the remaining setup preferences normally. Then perform the agent checks and present the remaining preferences ONE AT A TIME.\",\n\t\t\t\t\"\",\n\t\t\t\t`The CLI has pre-detected the following from the filesystem: MCP server: ${options.mcpConfigured ? `configured (${options.mcpScope})` : \"not configured\"}. Agent skills: ${options.skillsInstalled ? `installed (${options.skillsScope})` : String(options.skillsScope ?? \"\").includes(\"partial\") ? `partially installed (${options.skillsScope}) — missing skills will be auto-installed to the same scope` : \"not installed\"}. Report these findings to the user before asking preferences. Only ask about scope/options for components that are NOT already configured. Do NOT ask about skills scope if skills are partially installed — they will be completed automatically.`,\n\t\t\t\t\"\",\n\t\t\t\t\"IMPORTANT (Cursor users): Cursor disables project-level MCP servers by default as a security measure. If the user is in Cursor and chooses project-level MCP scope, warn them that they will need to manually enable the Neon server in Cursor Settings > MCP after installation. Recommend global scope for Cursor to avoid this extra step.\",\n\t\t\t\t\"\",\n\t\t\t\t\"GROUPING: Preferences that share the same `group` field should be presented together in a single message (e.g. list all customize options at once and let the user answer them together). Preferences without a `group` must be asked individually.\",\n\t\t\t\t\"\",\n\t\t\t\tdetectedIde\n\t\t\t\t\t? `The CLI has detected the IDE as: ${detectedIde.toLowerCase()}. Include this as the \"ide\" field in your reportBack data. IMPORTANT: The IDE and the agent are different — you may be Claude Code (agent) running inside Cursor (IDE). The extension installs into the IDE, so if the IDE is Cursor/VS Code/Windsurf, the extension IS applicable even if you are Claude Code.`\n\t\t\t\t\t: installedEditors.length > 0\n\t\t\t\t\t\t? `No IDE detected, but the following editors are installed: ${installedEditors.join(\", \")}. The \"installedEditors\" field in this response lists them. If the user wants the extension installed, ask which editor to install it for and include that as the \"ide\" field in your reportBack data. If not, set \"ide\" to \"none\".`\n\t\t\t\t\t\t: `No IDE or supported editors detected. Set \"ide\" to \"none\" in your reportBack data.`,\n\t\t\t\t\"\",\n\t\t\t\t\"After all questions are answered, call reportBack with a single --data JSON containing: agent, ide, mcpConfigured, and all preference answers. The CLI will inspect the project and merge results automatically.\",\n\t\t\t].join(\"\\n\"),\n\t\t\tchecks: [\n\t\t\t\t{\n\t\t\t\t\tid: \"neonctl\",\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t\"The neonctl CLI will be installed or updated automatically (no action needed from the agent)\",\n\t\t\t\t\tlookFor: [],\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tid: \"mcp_server\",\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t\"Check if the Neon MCP server is already configured in your MCP server list\",\n\t\t\t\t\tlookFor: [\n\t\t\t\t\t\t\"An MCP server entry named 'Neon' or with URL containing 'mcp.neon.tech'\",\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tid: \"agent_type\",\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t\"Identify which coding agent is running this command\",\n\t\t\t\t\tlookFor: [\n\t\t\t\t\t\t\"Determine which agent you are: cursor, claude-code, copilot, vscode, windsurf, codex, cline, gemini-cli, goose, opencode, or antigravity\",\n\t\t\t\t\t\t\"Report your own agent identifier — this is used to configure the MCP server for the correct tool\",\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\t...(detectedIde\n\t\t\t\t\t? [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tid: \"extension_installed\",\n\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t\"Check if the Neon editor extension (databricks.neon-local-connect) is already installed in the IDE (NOT the agent — e.g. if you are Claude Code running inside Cursor, check Cursor's extensions)\",\n\t\t\t\t\t\t\t\tlookFor: [\n\t\t\t\t\t\t\t\t\t\"Run the IDE's --list-extensions command or check installed extensions for 'databricks.neon-local-connect' or 'Neon Local Connect'\",\n\t\t\t\t\t\t\t\t\t\"If the extension is found, set installExtension to false in your reportBack data and SKIP the installExtension question\",\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t]\n\t\t\t\t\t: []),\n\t\t\t],\n\t\t\tuserPreferences: [\n\t\t\t\t...templatePreferences,\n\t\t\t\t// For brownfield flows, ask which Neon features to enable\n\t\t\t\t...(hasApp\n\t\t\t\t\t? [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tid: \"features\",\n\t\t\t\t\t\t\t\tquestion:\n\t\t\t\t\t\t\t\t\t\"Which Neon features would you like to enable for this project?\",\n\t\t\t\t\t\t\t\tphase: \"after_checks\" as const,\n\t\t\t\t\t\t\t\toptions: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tvalue: \"database\",\n\t\t\t\t\t\t\t\t\t\tlabel: \"Database (always included)\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tvalue: \"database,auth\",\n\t\t\t\t\t\t\t\t\t\tlabel: \"Database + Neon Auth (adds authentication via Neon)\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\tdefault: \"database\",\n\t\t\t\t\t\t\t\tcontext:\n\t\t\t\t\t\t\t\t\t\"Database connectivity is always set up. Neon Auth adds user authentication powered by Neon. More features (Functions, AI Gateway, Object Storage) will be available soon.\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t]\n\t\t\t\t\t: []),\n\t\t\t\t// Only show defaults/customize when there's something to customize:\n\t\t\t\t// MCP not configured, skills need scope choice, or extension not detected.\n\t\t\t\t...(() => {\n\t\t\t\t\tconst isPartialSkills = String(\n\t\t\t\t\t\toptions.skillsScope ?? \"\",\n\t\t\t\t\t).includes(\"partial\");\n\t\t\t\t\tconst needsMcpChoice = !options.mcpConfigured;\n\t\t\t\t\tconst needsSkillsChoice =\n\t\t\t\t\t\t!options.skillsInstalled && !isPartialSkills;\n\t\t\t\t\tconst hasCustomizableOptions =\n\t\t\t\t\t\tneedsMcpChoice || needsSkillsChoice;\n\t\t\t\t\tif (!hasCustomizableOptions) return [];\n\t\t\t\t\treturn [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tid: \"mode\",\n\t\t\t\t\t\t\tquestion: \"Use default settings or customize?\",\n\t\t\t\t\t\t\tphase: \"after_checks\" as const,\n\t\t\t\t\t\t\toptions: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tvalue: \"defaults\",\n\t\t\t\t\t\t\t\t\tlabel: hasApp\n\t\t\t\t\t\t\t\t\t\t? \"Use defaults (neonctl CLI, MCP: global, skills: project-level, extension if applicable — already-configured components will be skipped)\"\n\t\t\t\t\t\t\t\t\t\t: \"Use defaults (neonctl CLI, MCP: global, extension if applicable — skills included in template)\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tvalue: \"customize\",\n\t\t\t\t\t\t\t\t\tlabel: \"Customize installation settings\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tdefault: \"defaults\",\n\t\t\t\t\t\t},\n\t\t\t\t\t];\n\t\t\t\t})(),\n\t\t\t\t{\n\t\t\t\t\tid: \"mcpScope\",\n\t\t\t\t\tquestion: \"Where should the Neon MCP server be configured?\",\n\t\t\t\t\tcontext:\n\t\t\t\t\t\t\"SKIP this question entirely if the mcp_server check found it is already configured. Only ask if MCP is NOT yet configured. NOTE: Cursor disables project-level MCP servers by default — if the user is in Cursor, recommend global scope or warn that they will need to manually enable the server in Cursor Settings > MCP.\",\n\t\t\t\t\tphase: \"after_checks\",\n\t\t\t\t\toptions: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvalue: \"global\",\n\t\t\t\t\t\t\tlabel: \"Global (available in all projects)\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvalue: \"project\",\n\t\t\t\t\t\t\tlabel: \"Project-level (scoped to this project only)\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvalue: \"none\",\n\t\t\t\t\t\t\tlabel: \"Skip — do not install the MCP server\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdefault: \"global\",\n\t\t\t\t\tcondition: { preferenceId: \"mode\", equals: \"customize\" },\n\t\t\t\t\tgroup: \"customize\",\n\t\t\t\t},\n\t\t\t\t// Show skills scope when skills aren't detected and no partial install exists.\n\t\t\t\t// Partial installations are auto-completed to the same scope silently.\n\t\t\t\t...(!options.skillsInstalled &&\n\t\t\t\t!String(options.skillsScope ?? \"\").includes(\"partial\")\n\t\t\t\t\t? [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tid: \"skillsScope\",\n\t\t\t\t\t\t\t\tquestion:\n\t\t\t\t\t\t\t\t\t\"Where should Neon agent skills be installed?\",\n\t\t\t\t\t\t\t\tcontext:\n\t\t\t\t\t\t\t\t\t\"Only ask if skills are not already installed.\",\n\t\t\t\t\t\t\t\tphase: \"after_checks\" as const,\n\t\t\t\t\t\t\t\toptions: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tvalue: \"global\",\n\t\t\t\t\t\t\t\t\t\tlabel: \"Global (available in all projects)\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tvalue: \"project\",\n\t\t\t\t\t\t\t\t\t\tlabel: \"Project-level (scoped to this project only)\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\tdefault: \"project\",\n\t\t\t\t\t\t\t\tcondition: {\n\t\t\t\t\t\t\t\t\tpreferenceId: \"mode\",\n\t\t\t\t\t\t\t\t\tequals: \"customize\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tgroup: \"customize\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t]\n\t\t\t\t\t: []),\n\t\t\t\t{\n\t\t\t\t\tid: \"installExtension\",\n\t\t\t\t\tquestion:\n\t\t\t\t\t\t\"Install the Neon editor extension for local database browsing?\",\n\t\t\t\t\tphase: \"after_checks\",\n\t\t\t\t\toptions: [\n\t\t\t\t\t\t{ value: \"true\", label: \"Yes\" },\n\t\t\t\t\t\t{ value: \"false\", label: \"No\" },\n\t\t\t\t\t],\n\t\t\t\t\tdefault: \"true\",\n\t\t\t\t\tcontext:\n\t\t\t\t\t\t\"The extension installs into the IDE, NOT the agent. If the CLI detected the IDE (see detectedIde field), use that — e.g. Claude Code running inside Cursor means the IDE is Cursor and the extension IS applicable. Only applicable for VS Code-based IDEs (VS Code, Cursor, Windsurf). SKIP this question if the user is NOT in a VS Code-based IDE, or if the extension_installed check found it is already installed. Set installExtension to false in reportBack if skipped.\",\n\t\t\t\t\tcondition: { preferenceId: \"mode\", equals: \"customize\" },\n\t\t\t\t\tgroup: \"customize\",\n\t\t\t\t},\n\t\t\t],\n\t\t\treportBack: {\n\t\t\t\ttype: \"run_neon_init\",\n\t\t\t\targs: [\n\t\t\t\t\t\"setup\",\n\t\t\t\t\t\"--json\",\n\t\t\t\t\t\"--data\",\n\t\t\t\t\t(() => {\n\t\t\t\t\t\tconst partialScope = String(\n\t\t\t\t\t\t\toptions.skillsScope ?? \"\",\n\t\t\t\t\t\t).replace(\"-partial\", \"\");\n\t\t\t\t\t\tconst hasPartial = String(\n\t\t\t\t\t\t\toptions.skillsScope ?? \"\",\n\t\t\t\t\t\t).includes(\"partial\");\n\t\t\t\t\t\tconst previewFlag = options.preview\n\t\t\t\t\t\t\t? \", preview: true\"\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\t\tconst needsMcpChoice = !options.mcpConfigured;\n\t\t\t\t\t\tconst needsSkillsChoice =\n\t\t\t\t\t\t\t!options.skillsInstalled && !hasPartial;\n\t\t\t\t\t\tconst hasModeQuestion =\n\t\t\t\t\t\t\tneedsMcpChoice || needsSkillsChoice;\n\t\t\t\t\t\tconst modeField = hasModeQuestion\n\t\t\t\t\t\t\t? \", mode: string\"\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\t\tconst mcpField = hasModeQuestion\n\t\t\t\t\t\t\t? \", mcpScope?: 'global'|'project'|'none'\"\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\t\tconst skillsField = needsSkillsChoice\n\t\t\t\t\t\t\t? \", skillsScope?: string\"\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\t\tconst extField = hasModeQuestion\n\t\t\t\t\t\t\t? \", installExtension?: bool\"\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\t\tconst prefilledSkills =\n\t\t\t\t\t\t\toptions.skillsInstalled || hasPartial\n\t\t\t\t\t\t\t\t? `, skillsScope: \"${options.skillsInstalled ? options.skillsScope || \"project\" : partialScope}\"`\n\t\t\t\t\t\t\t\t: skillsField;\n\t\t\t\t\t\treturn `<json: { agent: string, ide: string, mcpConfigured: bool${prefilledSkills}${previewFlag}${modeField}${mcpField}${extField}${hasApp ? \", features?: string\" : \", template: string\"} }>`;\n\t\t\t\t\t})(),\n\t\t\t\t],\n\t\t\t},\n\t\t},\n\t};\n}\n\nfunction _buildModeQuestion(options: SetupPhaseOptions): PhaseResponse {\n\tconst agentArgs = options.agent ? [\"--agent\", options.agent] : [];\n\n\t// Build a context summary from what the agent found\n\tconst findings: string[] = [];\n\tif (options.mcpConfigured) {\n\t\tfindings.push(\n\t\t\t\"Neon MCP server is already configured (will be upgraded to evergreen)\",\n\t\t);\n\t} else {\n\t\tfindings.push(\"Neon MCP server is not configured\");\n\t}\n\tif (options.connectionString) {\n\t\tfindings.push(\"A Neon connection string was found in the project\");\n\t} else {\n\t\tfindings.push(\"No Neon connection string found — will need to add one\");\n\t}\n\tif (options.framework && options.framework !== \"none\") {\n\t\tfindings.push(`Framework detected: ${options.framework}`);\n\t}\n\tif (options.orm && options.orm !== \"none\") {\n\t\tfindings.push(`ORM detected: ${options.orm}`);\n\t}\n\tif (options.migrationTool && options.migrationTool !== \"none\") {\n\t\tfindings.push(`Migration tool detected: ${options.migrationTool}`);\n\t}\n\tif (options.isVscodeIde) {\n\t\tfindings.push(\"VS Code-based IDE detected — Neon extension available\");\n\t}\n\n\tconst inspectionArgs = buildInspectionArgs(options);\n\n\t// Build defaults label showing only what will be installed\n\tconst defaultsParts: string[] = [\"neonctl CLI\"];\n\tif (!options.mcpConfigured) defaultsParts.push(\"MCP global\");\n\tdefaultsParts.push(\"skills in project\");\n\tif (options.isVscodeIde) defaultsParts.push(\"install extension\");\n\tconst defaultsLabel =\n\t\tdefaultsParts.length > 0\n\t\t\t? `Use defaults (${defaultsParts.join(\", \")})`\n\t\t\t: \"Use defaults\";\n\n\treturn {\n\t\tphase: \"setup\",\n\t\tstatus: \"preferences_needed\",\n\t\tinspection: {\n\t\t\tmcpConfigured: options.mcpConfigured,\n\t\t\tconnectionString: options.connectionString,\n\t\t\tframework: options.framework,\n\t\t\torm: options.orm,\n\t\t\tmigrationTool: options.migrationTool,\n\t\t\tmigrationDir: options.migrationDir,\n\t\t\tisVscodeIde: options.isVscodeIde,\n\t\t},\n\t\tnextAction: {\n\t\t\ttype: \"ask_user\",\n\t\t\tquestion: \"Use default settings or customize?\",\n\t\t\toptions: [\n\t\t\t\t{\n\t\t\t\t\tvalue: \"defaults\",\n\t\t\t\t\tlabel: defaultsLabel,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tvalue: \"customize\",\n\t\t\t\t\tlabel: \"Customize installation settings\",\n\t\t\t\t},\n\t\t\t],\n\t\t\tcontext: `Project inspection results:\\n${findings.map((f) => `- ${f}`).join(\"\\n\")}`,\n\t\t\tresponseMapping: {\n\t\t\t\tdefaults: {\n\t\t\t\t\targs: [\n\t\t\t\t\t\t\"setup\",\n\t\t\t\t\t\t\"--json\",\n\t\t\t\t\t\t...agentArgs,\n\t\t\t\t\t\t...inspectionArgs,\n\t\t\t\t\t\t\"--mode\",\n\t\t\t\t\t\t\"defaults\",\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\tcustomize: {\n\t\t\t\t\targs: [\n\t\t\t\t\t\t\"setup\",\n\t\t\t\t\t\t\"--json\",\n\t\t\t\t\t\t...agentArgs,\n\t\t\t\t\t\t...inspectionArgs,\n\t\t\t\t\t\t\"--mode\",\n\t\t\t\t\t\t\"customize\",\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t};\n}\n\nfunction _buildCustomizeQuestions(options: SetupPhaseOptions): PhaseResponse {\n\tconst agentArgs = options.agent ? [\"--agent\", options.agent] : [];\n\tconst inspectionArgs = buildInspectionArgs(options);\n\n\tconst needsMcp = !options.mcpConfigured;\n\tconst mcpScopes = needsMcp ? [\"global\", \"project\", \"none\"] : [\"skip\"];\n\tconst skillsScopes = [\"global\", \"project\"];\n\tconst extOptions = options.isVscodeIde ? [\"ext\", \"noext\"] : [\"ext\"];\n\n\t// Build all combinations of configurable options\n\tconst customOptions: { value: string; label: string }[] = [];\n\tfor (const mcp of mcpScopes) {\n\t\tfor (const skills of skillsScopes) {\n\t\t\tfor (const ext of extOptions) {\n\t\t\t\tconst parts: string[] = [];\n\t\t\t\tif (mcp === \"none\") parts.push(\"Skip MCP\");\n\t\t\t\telse if (mcp !== \"skip\") parts.push(`MCP: ${mcp}`);\n\t\t\t\tif (skills !== \"skip\")\n\t\t\t\t\tparts.push(\n\t\t\t\t\t\t`Skills: ${skills === \"project\" ? \"project-level\" : skills}`,\n\t\t\t\t\t);\n\t\t\t\tif (options.isVscodeIde) {\n\t\t\t\t\tparts.push(\n\t\t\t\t\t\text === \"ext\" ? \"Install extension\" : \"Skip extension\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tcustomOptions.push({\n\t\t\t\t\tvalue: `${mcp}_${skills}_${ext}`,\n\t\t\t\t\tlabel: parts.join(\", \"),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\tconst responseMapping: Record<string, { args: string[] }> = {};\n\n\tfor (const opt of customOptions) {\n\t\tconst parts = opt.value.split(\"_\");\n\t\tconst mcpScope = parts[0] === \"skip\" ? \"global\" : parts[0]; // \"none\" passes through\n\t\tconst skillsScope = parts[1] === \"skip\" ? \"project\" : parts[1];\n\t\tconst installExt = parts[2] === \"ext\";\n\n\t\tresponseMapping[opt.value] = {\n\t\t\targs: [\n\t\t\t\t\"setup\",\n\t\t\t\t\"--json\",\n\t\t\t\t...agentArgs,\n\t\t\t\t...inspectionArgs,\n\t\t\t\t\"--mode\",\n\t\t\t\t\"customize\",\n\t\t\t\t\"--mcp-scope\",\n\t\t\t\tmcpScope,\n\t\t\t\t\"--skills-scope\",\n\t\t\t\tskillsScope,\n\t\t\t\t...(options.isVscodeIde\n\t\t\t\t\t? [\"--install-extension\", installExt ? \"true\" : \"false\"]\n\t\t\t\t\t: []),\n\t\t\t\t\"--execute\",\n\t\t\t],\n\t\t};\n\t}\n\n\treturn {\n\t\tphase: \"setup\",\n\t\tstatus: \"customizing\",\n\t\tnextAction: {\n\t\t\ttype: \"ask_user\",\n\t\t\tquestion: \"Choose your installation configuration:\",\n\t\t\toptions: customOptions,\n\t\t\tcontext:\n\t\t\t\t\"Global scope means settings apply across all your projects. Project-level means settings are scoped to this project only.\" +\n\t\t\t\t(options.mcpConfigured\n\t\t\t\t\t? \"\\nSince Neon tools are already installed, they will be upgraded to the latest evergreen version.\"\n\t\t\t\t\t: \"\") +\n\t\t\t\t(isCursorAgent(options)\n\t\t\t\t\t? \"\\nNote: Cursor disables project-level MCP servers by default. If you choose project scope, you will need to manually enable the Neon server in Cursor Settings > MCP.\"\n\t\t\t\t\t: \"\"),\n\t\t\tresponseMapping,\n\t\t},\n\t};\n}\n\ninterface InstallResult {\n\tid: string;\n\tdescription: string;\n\tstatus: \"success\" | \"failed\";\n\terror?: string;\n\t/** True when the step wasn't automated — the description contains manual instructions for the user */\n\tmanualAction?: boolean;\n\t/** Shell commands the agent can run to complete this step manually */\n\tcommands?: string[];\n}\n\n/**\n * Executes the batched installation of MCP server, skills, and extension.\n * Runs commands directly in the CLI process — the agent does NOT run these.\n * Returns results and chains to the getting-started phase.\n */\nasync function executeBatchedInstallation(\n\toptions: SetupPhaseOptions,\n): Promise<PhaseResponse> {\n\tconst mcpScope = options.mcpScope ?? \"global\";\n\tconst agentId = options.agent ?? \"cursor\";\n\tconst mcpAgentId = resolveAddMcpAgentId(agentId);\n\tconst installExt = options.installExtension === true;\n\n\tconst results: InstallResult[] = [];\n\tconst isBootstrap = !!options.template;\n\n\t// Step 0: Bootstrap project from template if specified\n\tif (isBootstrap && options.template) {\n\t\ttry {\n\t\t\tconst templates = await fetchTemplates();\n\t\t\tconst template =\n\t\t\t\tfindTemplate(templates, options.template) ??\n\t\t\t\tfindTemplate(FALLBACK_TEMPLATES, options.template);\n\t\t\tif (!template) {\n\t\t\t\tthrow new Error(`Unknown template \"${options.template}\".`);\n\t\t\t}\n\t\t\tawait scaffoldTemplate(template, \".\");\n\t\t\tresults.push({\n\t\t\t\tid: \"bootstrap\",\n\t\t\t\tdescription: `Scaffolded project from template \"${options.template}\"`,\n\t\t\t\tstatus: \"success\",\n\t\t\t});\n\n\t\t\t// Write template features to .neon under _init (ephemeral, cleaned up when init completes)\n\t\t\tif (options.templateRequires) {\n\t\t\t\tconst neonContextPath = resolve(process.cwd(), \".neon\");\n\t\t\t\tconst context: Record<string, unknown> = {\n\t\t\t\t\t_init: { features: options.templateRequires },\n\t\t\t\t};\n\t\t\t\twriteFileSync(\n\t\t\t\t\tneonContextPath,\n\t\t\t\t\t`${JSON.stringify(context, null, 2)}\\n`,\n\t\t\t\t);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tresults.push({\n\t\t\t\tid: \"bootstrap\",\n\t\t\t\tdescription: `Failed to scaffold project from template \"${options.template}\"`,\n\t\t\t\tstatus: \"failed\",\n\t\t\t\terror: err instanceof Error ? err.message : \"Unknown error\",\n\t\t\t});\n\t\t}\n\t}\n\n\t// Step 1: Ensure neonctl CLI is installed and up to date\n\tconst neonctlResult = await ensureNeonctl();\n\tswitch (neonctlResult.status) {\n\t\tcase \"already_current\":\n\t\t\tresults.push({\n\t\t\t\tid: \"neonctl\",\n\t\t\t\tdescription: `neonctl CLI is up to date (v${neonctlResult.version})`,\n\t\t\t\tstatus: \"success\",\n\t\t\t});\n\t\t\tbreak;\n\t\tcase \"installed\":\n\t\t\tresults.push({\n\t\t\t\tid: \"neonctl\",\n\t\t\t\tdescription: `Installed neonctl CLI (v${neonctlResult.version})`,\n\t\t\t\tstatus: \"success\",\n\t\t\t});\n\t\t\tbreak;\n\t\tcase \"updated\":\n\t\t\tresults.push({\n\t\t\t\tid: \"neonctl\",\n\t\t\t\tdescription: `Updated neonctl CLI to v${neonctlResult.version}`,\n\t\t\t\tstatus: \"success\",\n\t\t\t});\n\t\t\tbreak;\n\t\tcase \"failed\":\n\t\t\tresults.push({\n\t\t\t\tid: \"neonctl\",\n\t\t\t\tdescription: \"Failed to install neonctl CLI\",\n\t\t\t\tstatus: \"failed\",\n\t\t\t\terror: neonctlResult.error,\n\t\t\t});\n\t\t\tbreak;\n\t}\n\n\t// Step 2: Install MCP server (skip if already configured)\n\tconst isCursor =\n\t\tmcpAgentId === \"cursor\" ||\n\t\toptions.ide?.toLowerCase() === \"cursor\" ||\n\t\toptions.agent?.toLowerCase() === \"cursor\";\n\n\tif (mcpScope === \"none\") {\n\t\tresults.push({\n\t\t\tid: \"skip_mcp\",\n\t\t\tdescription: \"Neon MCP server installation skipped by user\",\n\t\t\tstatus: \"success\",\n\t\t});\n\t} else if (options.mcpConfigured) {\n\t\tresults.push({\n\t\t\tid: \"skip_mcp\",\n\t\t\tdescription: \"Neon MCP server already configured\",\n\t\t\tstatus: \"success\",\n\t\t});\n\t} else {\n\t\tconst mcpArgs = [\n\t\t\t\"-y\",\n\t\t\t\"add-mcp\",\n\t\t\t\"https://mcp.neon.tech/mcp\",\n\t\t\t...(mcpScope === \"global\" ? [\"-g\"] : []),\n\t\t\t\"-n\",\n\t\t\t\"Neon\",\n\t\t\t\"-y\",\n\t\t\t\"-a\",\n\t\t\tmcpAgentId,\n\t\t];\n\t\ttry {\n\t\t\tawait execa(\"npx\", mcpArgs, { stdio: \"pipe\", timeout: 60000 });\n\t\t\tresults.push({\n\t\t\t\tid: \"install_mcp\",\n\t\t\t\tdescription: `Installed Neon MCP server (${mcpScope} scope)`,\n\t\t\t\tstatus: \"success\",\n\t\t\t});\n\n\t\t\t// Some editors disable newly added MCP servers by default.\n\t\t\t// Cursor: project-level servers are always disabled initially.\n\t\t\t// Claude Code: newly added servers require user approval.\n\t\t\tconst isClaudeCode =\n\t\t\t\tmcpAgentId === \"claude-code\" ||\n\t\t\t\toptions.agent?.toLowerCase() === \"claude-code\";\n\n\t\t\tif (isCursor && mcpScope === \"project\") {\n\t\t\t\tresults.push({\n\t\t\t\t\tid: \"enable_mcp\",\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t'Cursor disables project-level MCP servers by default. Open Cursor Settings > MCP and toggle the \"Neon\" server on.',\n\t\t\t\t\tstatus: \"success\",\n\t\t\t\t\tmanualAction: true,\n\t\t\t\t});\n\t\t\t} else if (isClaudeCode) {\n\t\t\t\tresults.push({\n\t\t\t\t\tid: \"enable_mcp\",\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t'Claude Code requires approval for newly added MCP servers. When prompted, approve the \"Neon\" MCP server to enable it. You can check MCP server status with /mcp in Claude Code.',\n\t\t\t\t\tstatus: \"success\",\n\t\t\t\t\tmanualAction: true,\n\t\t\t\t});\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tresults.push({\n\t\t\t\tid: \"install_mcp\",\n\t\t\t\tdescription: \"Failed to install Neon MCP server\",\n\t\t\t\tstatus: \"failed\",\n\t\t\t\terror: err instanceof Error ? err.message : \"Unknown error\",\n\t\t\t});\n\t\t}\n\t}\n\n\t// Step 3: Install/update skills (skip when bootstrapping — templates bundle skills)\n\tif (isBootstrap) {\n\t\tresults.push({\n\t\t\tid: \"install_skills\",\n\t\t\tdescription: \"Neon agent skills included in template\",\n\t\t\tstatus: \"success\",\n\t\t});\n\t} else {\n\t\tconst skillsScope = (options.skillsScope ?? \"project\") as\n\t\t\t| \"global\"\n\t\t\t| \"project\";\n\t\tconst skillsOk = await ensureSkillsUpToDate(\n\t\t\tagentId,\n\t\t\tskillsScope,\n\t\t\toptions.preview,\n\t\t);\n\t\tif (skillsOk) {\n\t\t\tresults.push({\n\t\t\t\tid: \"install_skills\",\n\t\t\t\tdescription: \"Neon agent skills installed\",\n\t\t\t\tstatus: \"success\",\n\t\t\t});\n\t\t} else {\n\t\t\t// Build the install commands for the agent to run directly\n\t\t\t// (sandboxed environments may block child process writes)\n\t\t\tconst { getSkillList } = await import(\"../skills.js\");\n\t\t\tconst skillList = getSkillList(options.preview);\n\t\t\tconst cmds = skillList.map(\n\t\t\t\t(s) =>\n\t\t\t\t\t`skills add neondatabase/agent-skills --skill ${s} --agent ${agentId}${skillsScope === \"global\" ? \" -g\" : \"\"} -y`,\n\t\t\t);\n\t\t\tresults.push({\n\t\t\t\tid: \"install_skills\",\n\t\t\t\tdescription:\n\t\t\t\t\t\"Failed to install Neon agent skills automatically. Run these commands to install manually:\",\n\t\t\t\tstatus: \"failed\",\n\t\t\t\tcommands: cmds,\n\t\t\t});\n\t\t}\n\t}\n\n\t// Step 4: Install editor extension if requested\n\t// Use the agent-reported IDE (not agent identity) — e.g. Claude Code running in\n\t// Cursor should install the extension for Cursor, not skip it.\n\tif (installExt) {\n\t\tconst extResult = await installExtensionForIde(options.ide ?? agentId);\n\t\tresults.push(extResult);\n\t}\n\n\t// Step 5: Write selected features to .neon under _init for brownfield flows\n\t// (Bootstrap flows already wrote _init in step 0)\n\tif (!isBootstrap && options.features && options.features.length > 0) {\n\t\tconst neonContextPath = resolve(process.cwd(), \".neon\");\n\t\tconst context: Record<string, unknown> = {\n\t\t\t_init: { features: options.features },\n\t\t};\n\t\twriteFileSync(neonContextPath, `${JSON.stringify(context, null, 2)}\\n`);\n\t}\n\n\tconst allSucceeded = results.every((r) => r.status === \"success\");\n\n\t// Build args to chain to the getting-started phase as a separate CLI call.\n\t// This ensures the agent gets a clean response with ONLY the getting-started\n\t// action — no competing \"results\" array to distract it.\n\tconst gettingStartedData: Record<string, unknown> = {};\n\tif (options.connectionString) gettingStartedData.hasConnectionString = true;\n\tif (options.framework) gettingStartedData.framework = options.framework;\n\tif (options.orm) gettingStartedData.orm = options.orm;\n\tif (options.migrationTool)\n\t\tgettingStartedData.migrationTool = options.migrationTool;\n\tif (options.migrationDir)\n\t\tgettingStartedData.migrationDir = options.migrationDir;\n\t// Pass features so getting-started knows which phases to chain to\n\tconst resolvedFeatures = options.templateRequires ?? options.features;\n\tif (resolvedFeatures && resolvedFeatures.length > 0)\n\t\tgettingStartedData.features = resolvedFeatures;\n\t// Bootstrap implies preview mode (new project in us-east required)\n\tif (isBootstrap) gettingStartedData.preview = true;\n\tconst gettingStartedArgs = [\n\t\t\"getting-started\",\n\t\t\"--json\",\n\t\t\"--data\",\n\t\tJSON.stringify(gettingStartedData),\n\t];\n\n\treturn {\n\t\tphase: \"setup\",\n\t\tstatus: allSucceeded ? \"installed\" : \"partial\",\n\t\tresults,\n\t\tnextAction: {\n\t\t\ttype: \"run_neon_init\",\n\t\t\targs: gettingStartedArgs,\n\t\t},\n\t};\n}\n\nfunction buildInspectionArgs(options: SetupPhaseOptions): string[] {\n\tconst args: string[] = [];\n\tif (options.mcpConfigured !== null && options.mcpConfigured !== undefined) {\n\t\targs.push(\"--mcp-configured\", options.mcpConfigured ? \"true\" : \"false\");\n\t}\n\tif (\n\t\toptions.connectionString !== null &&\n\t\toptions.connectionString !== undefined\n\t) {\n\t\targs.push(\n\t\t\t\"--connection-string\",\n\t\t\toptions.connectionString ? \"true\" : \"false\",\n\t\t);\n\t}\n\tif (options.framework) {\n\t\targs.push(\"--framework\", options.framework);\n\t}\n\tif (options.orm) {\n\t\targs.push(\"--orm\", options.orm);\n\t}\n\tif (options.migrationTool) {\n\t\targs.push(\"--migration-tool\", options.migrationTool);\n\t}\n\tif (options.migrationDir) {\n\t\targs.push(\"--migration-dir\", options.migrationDir);\n\t}\n\tif (options.isVscodeIde !== null && options.isVscodeIde !== undefined) {\n\t\targs.push(\"--is-vscode-ide\", options.isVscodeIde ? \"true\" : \"false\");\n\t}\n\treturn args;\n}\n\n/**\n * Fills in missing filesystem inspection fields by running inspectProject().\n * Agent-reported data (mcpConfigured, agent, mode, scopes) is preserved.\n * CLI-detectable fields (framework, orm, migrations, connectionString, isVscodeIde)\n * are filled in only if not already present.\n */\nasync function mergeCliInspection(\n\toptions: SetupPhaseOptions,\n): Promise<SetupPhaseOptions> {\n\t// If the agent already provided these, no need to re-inspect\n\tif (options.framework !== undefined && options.orm !== undefined) {\n\t\treturn options;\n\t}\n\n\tconst inspection = await inspectProject([\n\t\t{ id: \"connection_string\", description: \"\", lookFor: [] },\n\t\t{ id: \"project_stack\", description: \"\", lookFor: [] },\n\t\t{ id: \"migrations\", description: \"\", lookFor: [] },\n\t\t{ id: \"ide_type\", description: \"\", lookFor: [] },\n\t]);\n\n\t// Also detect IDE if not already reported by the agent\n\tconst ide =\n\t\toptions.ide?.toLowerCase().replace(/\\s+/g, \"-\") ||\n\t\tdetectIde()?.toLowerCase().replace(/\\s+/g, \"-\") ||\n\t\tundefined;\n\n\treturn {\n\t\t...options,\n\t\tide,\n\t\tconnectionString:\n\t\t\toptions.connectionString ??\n\t\t\t(inspection.connectionString as boolean | undefined),\n\t\tframework:\n\t\t\toptions.framework ?? (inspection.framework as string | undefined),\n\t\torm: options.orm ?? (inspection.orm as string | undefined),\n\t\tmigrationTool:\n\t\t\toptions.migrationTool ??\n\t\t\t(inspection.migrationTool as string | undefined),\n\t\tmigrationDir:\n\t\t\toptions.migrationDir ??\n\t\t\t(inspection.migrationDir as string | undefined),\n\t\tisVscodeIde:\n\t\t\toptions.isVscodeIde ??\n\t\t\t(inspection.isVscodeIde as boolean | undefined),\n\t};\n}\n\n/**\n * Checks whether the user is in a VS Code-based IDE that supports extensions.\n * Uses agent-reported `ide` field first, then falls back to `isVscodeIde` from inspection.\n */\nfunction isCursorAgent(options: SetupPhaseOptions): boolean {\n\tconst ide = options.ide?.toLowerCase();\n\tif (ide === \"cursor\") return true;\n\tconst agent = options.agent?.toLowerCase();\n\tif (agent === \"cursor\") return true;\n\treturn false;\n}\n\nfunction isVscodeBasedIde(options: SetupPhaseOptions): boolean {\n\tif (options.ide) {\n\t\tconst ide = options.ide.toLowerCase();\n\t\treturn (\n\t\t\tide === \"cursor\" ||\n\t\t\tide === \"vscode\" ||\n\t\t\tide === \"vs-code\" ||\n\t\t\tide === \"windsurf\"\n\t\t);\n\t}\n\treturn options.isVscodeIde === true;\n}\n\n/**\n * Resolves which IDE to install the extension for.\n * Accepts the agent-reported IDE value (preferred), the agent ID, or\n * falls back to env-var detection.\n */\nfunction resolveEditorForExtension(ideOrAgentId: string): Editor | null {\n\t// Map known IDE/agent identifiers to Editor types\n\tswitch (ideOrAgentId.toLowerCase()) {\n\t\tcase \"cursor\":\n\t\t\treturn \"Cursor\";\n\t\tcase \"vscode\":\n\t\tcase \"vs-code\":\n\t\tcase \"copilot\":\n\t\tcase \"github-copilot\":\n\t\tcase \"github-copilot-cli\":\n\t\t\treturn \"VS Code\";\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\n\t// Fall back to env-var detection\n\tconst ide = detectIde();\n\tif (ide === \"Cursor\" || ide === \"VS Code\") return ide;\n\n\treturn null;\n}\n\nconst MANUAL_INSTALL_MSG = `Search for \"Neon\" in the extensions panel (Cmd+Shift+X / Ctrl+Shift+X) and install \"Neon Local Connect\" by Databricks.`;\n\n/**\n * Installs the Neon extension for the detected IDE.\n *\n * Uses env-var detection to determine the IDE (not the agent identity),\n * so Claude Code running in Cursor correctly installs for Cursor.\n *\n * Strategy:\n * 1. Try `<editor> --install-extension <id>` directly (uses editor's configured marketplace)\n * 2. If that fails, download .vsix (from proxy or Open VSX) and install via local file\n * 3. If all else fails: return manual install instructions\n */\nasync function installExtensionForIde(agentId: string): Promise<InstallResult> {\n\tconst editorType = resolveEditorForExtension(agentId);\n\tif (!editorType) {\n\t\treturn {\n\t\t\tid: \"install_extension\",\n\t\t\tdescription: MANUAL_INSTALL_MSG,\n\t\t\tstatus: \"success\",\n\t\t\tmanualAction: true,\n\t\t};\n\t}\n\n\tconst editorCmd = await findEditorCommand(editorType);\n\tif (!editorCmd) {\n\t\treturn {\n\t\t\tid: \"install_extension\",\n\t\t\tdescription: MANUAL_INSTALL_MSG,\n\t\t\tstatus: \"success\",\n\t\t\tmanualAction: true,\n\t\t};\n\t}\n\n\t// Try direct marketplace install first (works if editor has marketplace configured)\n\ttry {\n\t\tawait execa(editorCmd, [\"--install-extension\", NEON_EXTENSION_ID], {\n\t\t\tstdio: \"pipe\",\n\t\t\ttimeout: 60000,\n\t\t});\n\t\treturn {\n\t\t\tid: \"install_extension\",\n\t\t\tdescription: `Installed Neon extension for ${editorType}`,\n\t\t\tstatus: \"success\",\n\t\t};\n\t} catch {\n\t\t// Fall through to VSIX download approach\n\t}\n\n\t// Download .vsix and install locally\n\tconst vsixPath = await downloadVsix();\n\tif (!vsixPath) {\n\t\treturn {\n\t\t\tid: \"install_extension\",\n\t\t\tdescription: MANUAL_INSTALL_MSG,\n\t\t\tstatus: \"success\",\n\t\t\tmanualAction: true,\n\t\t};\n\t}\n\n\ttry {\n\t\tawait execa(editorCmd, [\"--install-extension\", vsixPath], {\n\t\t\tstdio: \"pipe\",\n\t\t\ttimeout: 60000,\n\t\t});\n\t\treturn {\n\t\t\tid: \"install_extension\",\n\t\t\tdescription: `Installed Neon extension for ${editorType}`,\n\t\t\tstatus: \"success\",\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tid: \"install_extension\",\n\t\t\tdescription: MANUAL_INSTALL_MSG,\n\t\t\tstatus: \"success\",\n\t\t\tmanualAction: true,\n\t\t};\n\t} finally {\n\t\ttry {\n\t\t\tawait unlink(vsixPath);\n\t\t} catch {}\n\t}\n}\n"],"mappings":"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"}
|
package/dist/lib/vsix.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vsix.js","names":[],"sources":["../../src/lib/vsix.ts"],"sourcesContent":["/**\n * Shared VSIX download utilities for extension installation.\n * Supports corporate proxy via NEON_VSX_GALLERY_URL env var.\n */\nimport { createWriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { pipeline } from \"node:stream/promises\";\n\nexport const NEON_EXTENSION_ID = \"databricks.neon-local-connect\";\nconst OPEN_VSX_API = \"https://open-vsx.org/api\";\n\n/**\n * Downloads a .vsix file for the Neon extension.\n *\n * Strategy:\n * 1. If NEON_VSX_GALLERY_URL is set, download from the corporate proxy gallery\n * 2. Otherwise, download from the public Open VSX API\n *\n * Returns the path to the temp .vsix file, or null on failure.\n */\nexport async function downloadVsix(): Promise<string | null> {\n\t// Runtime env var takes priority, then build-time baked value\n\tconst { INTERNAL_VSX_GALLERY } = await import(\"./build-config.js\");\n\tconst proxyGallery =\n\t\tprocess.env.NEON_VSX_GALLERY_URL || INTERNAL_VSX_GALLERY || \"\";\n\tif (proxyGallery) {\n\t\treturn downloadFromGallery(proxyGallery);\n\t}\n\treturn downloadFromOpenVsx();\n}\n\n/**\n * Downloads from a VS Code marketplace-compatible gallery API (corporate proxy).\n * Uses the VS Code extensionquery POST API to find the VSIX download URL.\n */\nasync function downloadFromGallery(galleryUrl: string): Promise<string | null> {\n\tconst [publisher, name] = NEON_EXTENSION_ID.split(\".\");\n\tconst baseUrl = galleryUrl.replace(/\\/+$/, \"\");\n\tconst queryUrl = `${baseUrl}/extensionquery`;\n\n\ttry {\n\t\t// Query the marketplace API for the extension\n\t\tconst queryRes = await fetch(queryUrl, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\tAccept: \"application/json;api-version=6.1-preview.1\",\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tfilters: [\n\t\t\t\t\t{\n\t\t\t\t\t\tcriteria: [\n\t\t\t\t\t\t\t{ filterType: 7, value: `${publisher}.${name}` },\n\t\t\t\t\t\t],\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tflags: 914,\n\t\t\t}),\n\t\t\tsignal: AbortSignal.timeout(15000),\n\t\t});\n\n\t\tif (!queryRes.ok) return null;\n\n\t\tconst data = (await queryRes.json()) as {\n\t\t\tresults?: {\n\t\t\t\textensions?: {\n\t\t\t\t\tversions?: {\n\t\t\t\t\t\tfiles?: { assetType: string; source: string }[];\n\t\t\t\t\t}[];\n\t\t\t\t}[];\n\t\t\t}[];\n\t\t};\n\n\t\t// Find the VSIX download URL from the response\n\t\tconst extension = data.results?.[0]?.extensions?.[0];\n\t\tconst latestVersion = extension?.versions?.[0];\n\t\tconst vsixFile = latestVersion?.files?.find(\n\t\t\t(f) =>\n\t\t\t\tf.assetType === \"Microsoft.VisualStudio.Services.VSIXPackage\",\n\t\t);\n\n\t\tif (!vsixFile?.source) return null;\n\n\t\t// Download the VSIX\n\t\tconst vsixRes = await fetch(vsixFile.source, {\n\t\t\tsignal: AbortSignal.timeout(30000),\n\t\t\tredirect: \"follow\",\n\t\t});\n\t\tif (!vsixRes.ok || !vsixRes.body) return null;\n\n\t\tconst tmpPath = join(tmpdir(), `${NEON_EXTENSION_ID}-proxy.vsix`);\n\t\tconst fileStream = createWriteStream(tmpPath);\n\t\tawait pipeline(\n\t\t\tvsixRes.body as unknown as NodeJS.ReadableStream,\n\t\t\tfileStream,\n\t\t);\n\t\treturn tmpPath;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Downloads from the public Open VSX API.\n */\nasync function downloadFromOpenVsx(): Promise<string | null> {\n\tconst [publisher, name] = NEON_EXTENSION_ID.split(\".\");\n\tconst metaUrl = `${OPEN_VSX_API}/${publisher}/${name}/latest`;\n\n\ttry {\n\t\tconst metaRes = await fetch(metaUrl, {\n\t\t\tsignal: AbortSignal.timeout(10000),\n\t\t});\n\t\tif (!metaRes.ok) return null;\n\n\t\tconst meta = (await metaRes.json()) as {\n\t\t\tfiles?: { download?: string };\n\t\t\tversion?: string;\n\t\t};\n\t\tconst downloadUrl = meta.files?.download;\n\t\tif (!downloadUrl) return null;\n\n\t\tconst vsixRes = await fetch(downloadUrl, {\n\t\t\tsignal: AbortSignal.timeout(30000),\n\t\t});\n\t\tif (!vsixRes.ok || !vsixRes.body) return null;\n\n\t\tconst tmpPath = join(\n\t\t\ttmpdir(),\n\t\t\t`${NEON_EXTENSION_ID}-${meta.version ?? \"latest\"}.vsix`,\n\t\t);\n\t\tconst fileStream = createWriteStream(tmpPath);\n\t\tawait pipeline(\n\t\t\tvsixRes.body as unknown as NodeJS.ReadableStream,\n\t\t\tfileStream,\n\t\t);\n\t\treturn tmpPath;\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":";;;;;;;;;AASA,MAAa,oBAAoB;AACjC,MAAM,eAAe;;;;;;;;;;AAWrB,eAAsB,eAAuC;CAE5D,MAAM,EAAE,yBAAyB,MAAM,OAAO;CAC9C,MAAM,eACL,QAAQ,IAAI,wBAAwB,wBAAwB;CAC7D,IAAI,cACH,OAAO,oBAAoB,YAAY;CAExC,OAAO,oBAAoB;AAC5B;;;;;AAMA,eAAe,oBAAoB,YAA4C;CAC9E,MAAM,CAAC,WAAW,QAAQ,kBAAkB,MAAM,GAAG;CAErD,MAAM,WAAW,GADD,WAAW,QAAQ,QAAQ,EACjB,EAAE;CAE5B,IAAI;EAEH,MAAM,WAAW,MAAM,MAAM,UAAU;GACtC,QAAQ;GACR,SAAS;IACR,gBAAgB;IAChB,QAAQ;GACT;GACA,MAAM,KAAK,UAAU;IACpB,SAAS,CACR,EACC,UAAU,CACT;KAAE,YAAY;KAAG,OAAO,GAAG,UAAU,GAAG;IAAO,CAChD,EACD,CACD;IACA,OAAO;GACR,CAAC;GACD,QAAQ,YAAY,QAAQ,IAAK;EAClC,CAAC;EAED,IAAI,CAAC,SAAS,IAAI,OAAO;EAezB,MAAM,cAFY,MAXE,SAAS,KAAK,EAAA,CAWX,UAAU,EAAE,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"vsix.js","names":[],"sources":["../../src/lib/vsix.ts"],"sourcesContent":["/**\n * Shared VSIX download utilities for extension installation.\n * Supports corporate proxy via NEON_VSX_GALLERY_URL env var.\n */\nimport { createWriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { pipeline } from \"node:stream/promises\";\n\nexport const NEON_EXTENSION_ID = \"databricks.neon-local-connect\";\nconst OPEN_VSX_API = \"https://open-vsx.org/api\";\n\n/**\n * Downloads a .vsix file for the Neon extension.\n *\n * Strategy:\n * 1. If NEON_VSX_GALLERY_URL is set, download from the corporate proxy gallery\n * 2. Otherwise, download from the public Open VSX API\n *\n * Returns the path to the temp .vsix file, or null on failure.\n */\nexport async function downloadVsix(): Promise<string | null> {\n\t// Runtime env var takes priority, then build-time baked value\n\tconst { INTERNAL_VSX_GALLERY } = await import(\"./build-config.js\");\n\tconst proxyGallery =\n\t\tprocess.env.NEON_VSX_GALLERY_URL || INTERNAL_VSX_GALLERY || \"\";\n\tif (proxyGallery) {\n\t\treturn downloadFromGallery(proxyGallery);\n\t}\n\treturn downloadFromOpenVsx();\n}\n\n/**\n * Downloads from a VS Code marketplace-compatible gallery API (corporate proxy).\n * Uses the VS Code extensionquery POST API to find the VSIX download URL.\n */\nasync function downloadFromGallery(galleryUrl: string): Promise<string | null> {\n\tconst [publisher, name] = NEON_EXTENSION_ID.split(\".\");\n\tconst baseUrl = galleryUrl.replace(/\\/+$/, \"\");\n\tconst queryUrl = `${baseUrl}/extensionquery`;\n\n\ttry {\n\t\t// Query the marketplace API for the extension\n\t\tconst queryRes = await fetch(queryUrl, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\tAccept: \"application/json;api-version=6.1-preview.1\",\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tfilters: [\n\t\t\t\t\t{\n\t\t\t\t\t\tcriteria: [\n\t\t\t\t\t\t\t{ filterType: 7, value: `${publisher}.${name}` },\n\t\t\t\t\t\t],\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tflags: 914,\n\t\t\t}),\n\t\t\tsignal: AbortSignal.timeout(15000),\n\t\t});\n\n\t\tif (!queryRes.ok) return null;\n\n\t\tconst data = (await queryRes.json()) as {\n\t\t\tresults?: {\n\t\t\t\textensions?: {\n\t\t\t\t\tversions?: {\n\t\t\t\t\t\tfiles?: { assetType: string; source: string }[];\n\t\t\t\t\t}[];\n\t\t\t\t}[];\n\t\t\t}[];\n\t\t};\n\n\t\t// Find the VSIX download URL from the response\n\t\tconst extension = data.results?.[0]?.extensions?.[0];\n\t\tconst latestVersion = extension?.versions?.[0];\n\t\tconst vsixFile = latestVersion?.files?.find(\n\t\t\t(f) =>\n\t\t\t\tf.assetType === \"Microsoft.VisualStudio.Services.VSIXPackage\",\n\t\t);\n\n\t\tif (!vsixFile?.source) return null;\n\n\t\t// Download the VSIX\n\t\tconst vsixRes = await fetch(vsixFile.source, {\n\t\t\tsignal: AbortSignal.timeout(30000),\n\t\t\tredirect: \"follow\",\n\t\t});\n\t\tif (!vsixRes.ok || !vsixRes.body) return null;\n\n\t\tconst tmpPath = join(tmpdir(), `${NEON_EXTENSION_ID}-proxy.vsix`);\n\t\tconst fileStream = createWriteStream(tmpPath);\n\t\tawait pipeline(\n\t\t\tvsixRes.body as unknown as NodeJS.ReadableStream,\n\t\t\tfileStream,\n\t\t);\n\t\treturn tmpPath;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Downloads from the public Open VSX API.\n */\nasync function downloadFromOpenVsx(): Promise<string | null> {\n\tconst [publisher, name] = NEON_EXTENSION_ID.split(\".\");\n\tconst metaUrl = `${OPEN_VSX_API}/${publisher}/${name}/latest`;\n\n\ttry {\n\t\tconst metaRes = await fetch(metaUrl, {\n\t\t\tsignal: AbortSignal.timeout(10000),\n\t\t});\n\t\tif (!metaRes.ok) return null;\n\n\t\tconst meta = (await metaRes.json()) as {\n\t\t\tfiles?: { download?: string };\n\t\t\tversion?: string;\n\t\t};\n\t\tconst downloadUrl = meta.files?.download;\n\t\tif (!downloadUrl) return null;\n\n\t\tconst vsixRes = await fetch(downloadUrl, {\n\t\t\tsignal: AbortSignal.timeout(30000),\n\t\t});\n\t\tif (!vsixRes.ok || !vsixRes.body) return null;\n\n\t\tconst tmpPath = join(\n\t\t\ttmpdir(),\n\t\t\t`${NEON_EXTENSION_ID}-${meta.version ?? \"latest\"}.vsix`,\n\t\t);\n\t\tconst fileStream = createWriteStream(tmpPath);\n\t\tawait pipeline(\n\t\t\tvsixRes.body as unknown as NodeJS.ReadableStream,\n\t\t\tfileStream,\n\t\t);\n\t\treturn tmpPath;\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":";;;;;;;;;AASA,MAAa,oBAAoB;AACjC,MAAM,eAAe;;;;;;;;;;AAWrB,eAAsB,eAAuC;CAE5D,MAAM,EAAE,yBAAyB,MAAM,OAAO;CAC9C,MAAM,eACL,QAAQ,IAAI,wBAAwB,wBAAwB;CAC7D,IAAI,cACH,OAAO,oBAAoB,YAAY;CAExC,OAAO,oBAAoB;AAC5B;;;;;AAMA,eAAe,oBAAoB,YAA4C;CAC9E,MAAM,CAAC,WAAW,QAAQ,kBAAkB,MAAM,GAAG;CAErD,MAAM,WAAW,GADD,WAAW,QAAQ,QAAQ,EACjB,EAAE;CAE5B,IAAI;EAEH,MAAM,WAAW,MAAM,MAAM,UAAU;GACtC,QAAQ;GACR,SAAS;IACR,gBAAgB;IAChB,QAAQ;GACT;GACA,MAAM,KAAK,UAAU;IACpB,SAAS,CACR,EACC,UAAU,CACT;KAAE,YAAY;KAAG,OAAO,GAAG,UAAU,GAAG;IAAO,CAChD,EACD,CACD;IACA,OAAO;GACR,CAAC;GACD,QAAQ,YAAY,QAAQ,IAAK;EAClC,CAAC;EAED,IAAI,CAAC,SAAS,IAAI,OAAO;EAezB,MAAM,cAFY,MAXE,SAAS,KAAK,EAAA,CAWX,UAAU,EAAE,EAAE,aAAa,GAAA,EACjB,WAAW,GAAA,EACZ,OAAO,MACrC,MACA,EAAE,cAAc,6CAClB;EAEA,IAAI,CAAC,UAAU,QAAQ,OAAO;EAG9B,MAAM,UAAU,MAAM,MAAM,SAAS,QAAQ;GAC5C,QAAQ,YAAY,QAAQ,GAAK;GACjC,UAAU;EACX,CAAC;EACD,IAAI,CAAC,QAAQ,MAAM,CAAC,QAAQ,MAAM,OAAO;EAEzC,MAAM,UAAU,KAAK,OAAO,GAAG,GAAG,kBAAkB,YAAY;EAChE,MAAM,aAAa,kBAAkB,OAAO;EAC5C,MAAM,SACL,QAAQ,MACR,UACD;EACA,OAAO;CACR,QAAQ;EACP,OAAO;CACR;AACD;;;;AAKA,eAAe,sBAA8C;CAC5D,MAAM,CAAC,WAAW,QAAQ,kBAAkB,MAAM,GAAG;CACrD,MAAM,UAAU,GAAG,aAAa,GAAG,UAAU,GAAG,KAAK;CAErD,IAAI;EACH,MAAM,UAAU,MAAM,MAAM,SAAS,EACpC,QAAQ,YAAY,QAAQ,GAAK,EAClC,CAAC;EACD,IAAI,CAAC,QAAQ,IAAI,OAAO;EAExB,MAAM,OAAQ,MAAM,QAAQ,KAAK;EAIjC,MAAM,cAAc,KAAK,OAAO;EAChC,IAAI,CAAC,aAAa,OAAO;EAEzB,MAAM,UAAU,MAAM,MAAM,aAAa,EACxC,QAAQ,YAAY,QAAQ,GAAK,EAClC,CAAC;EACD,IAAI,CAAC,QAAQ,MAAM,CAAC,QAAQ,MAAM,OAAO;EAEzC,MAAM,UAAU,KACf,OAAO,GACP,GAAG,kBAAkB,GAAG,KAAK,WAAW,SAAS,MAClD;EACA,MAAM,aAAa,kBAAkB,OAAO;EAC5C,MAAM,SACL,QAAQ,MACR,UACD;EACA,OAAO;CACR,QAAQ;EACP,OAAO;CACR;AACD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neon-init",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Initialize Neon projects",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"neon",
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
".": {
|
|
27
27
|
"import": "./dist/index.js",
|
|
28
28
|
"types": "./dist/index.d.ts"
|
|
29
|
+
},
|
|
30
|
+
"./bootstrap": {
|
|
31
|
+
"import": "./dist/lib/bootstrap.js",
|
|
32
|
+
"types": "./dist/lib/bootstrap.d.ts"
|
|
29
33
|
}
|
|
30
34
|
},
|
|
31
35
|
"files": [
|
|
@@ -46,6 +50,7 @@
|
|
|
46
50
|
"@clack/prompts": "0.10.1",
|
|
47
51
|
"add-mcp": "^1.5.1",
|
|
48
52
|
"execa": "^9.5.2",
|
|
53
|
+
"fflate": "^0.8.3",
|
|
49
54
|
"picocolors": "^1.1.1",
|
|
50
55
|
"yaml": "^2.9.0",
|
|
51
56
|
"yargs": "^18.0.0",
|