@puzzmo/cli 1.0.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.
Files changed (54) hide show
  1. package/README.md +75 -0
  2. package/lib/commands/game/create.d.ts +2 -0
  3. package/lib/commands/game/create.js +145 -0
  4. package/lib/commands/game/create.js.map +1 -0
  5. package/lib/commands/login.d.ts +2 -0
  6. package/lib/commands/login.js +13 -0
  7. package/lib/commands/login.js.map +1 -0
  8. package/lib/commands/upload.d.ts +2 -0
  9. package/lib/commands/upload.js +87 -0
  10. package/lib/commands/upload.js.map +1 -0
  11. package/lib/download/page-downloader.d.ts +2 -0
  12. package/lib/download/page-downloader.js +94 -0
  13. package/lib/download/page-downloader.js.map +1 -0
  14. package/lib/index.d.ts +2 -0
  15. package/lib/index.js +53 -0
  16. package/lib/index.js.map +1 -0
  17. package/lib/lib/api.d.ts +10 -0
  18. package/lib/lib/api.js +53 -0
  19. package/lib/lib/api.js.map +1 -0
  20. package/lib/lib/config.d.ts +13 -0
  21. package/lib/lib/config.js +29 -0
  22. package/lib/lib/config.js.map +1 -0
  23. package/lib/lib/exec.d.ts +15 -0
  24. package/lib/lib/exec.js +34 -0
  25. package/lib/lib/exec.js.map +1 -0
  26. package/lib/lib/package-manager.d.ts +2 -0
  27. package/lib/lib/package-manager.js +15 -0
  28. package/lib/lib/package-manager.js.map +1 -0
  29. package/lib/skills/registry.d.ts +13 -0
  30. package/lib/skills/registry.js +49 -0
  31. package/lib/skills/registry.js.map +1 -0
  32. package/lib/skills/runner.d.ts +4 -0
  33. package/lib/skills/runner.js +80 -0
  34. package/lib/skills/runner.js.map +1 -0
  35. package/lib/tui/pipeline.d.ts +2 -0
  36. package/lib/tui/pipeline.js +426 -0
  37. package/lib/tui/pipeline.js.map +1 -0
  38. package/lib/wizard/agent-detect.d.ts +4 -0
  39. package/lib/wizard/agent-detect.js +4 -0
  40. package/lib/wizard/agent-detect.js.map +1 -0
  41. package/package.json +20 -0
  42. package/src/commands/game/create.ts +171 -0
  43. package/src/commands/login.ts +15 -0
  44. package/src/commands/upload.ts +94 -0
  45. package/src/download/page-downloader.ts +104 -0
  46. package/src/index.ts +56 -0
  47. package/src/lib/api.ts +75 -0
  48. package/src/lib/config.ts +37 -0
  49. package/src/lib/exec.ts +38 -0
  50. package/src/skills/registry.ts +57 -0
  51. package/src/skills/runner.ts +91 -0
  52. package/src/tui/pipeline.ts +460 -0
  53. package/src/wizard/agent-detect.ts +6 -0
  54. package/tsconfig.json +15 -0
@@ -0,0 +1,57 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import { fileURLToPath } from "node:url"
4
+
5
+ export type SkillDefinition = {
6
+ name: string
7
+ optional: boolean
8
+ }
9
+
10
+ /** Ordered list of skills for the migration pipeline */
11
+ export const skillsPipeline: SkillDefinition[] = [
12
+ { name: "convert-to-vite", optional: false },
13
+ { name: "introduce-puzzmo-sdk", optional: false },
14
+ { name: "game-completion", optional: false },
15
+ { name: "puzzmo-theme", optional: true },
16
+ { name: "add-deeds", optional: false },
17
+ { name: "setup-augmentations", optional: false },
18
+ { name: "create-app-bundle", optional: false },
19
+ { name: "setup-deploy", optional: false },
20
+ ]
21
+
22
+ /** Returns the skills directory for a given agent inside the game dir */
23
+ export const agentSkillsDir = (agent: string): string => {
24
+ if (agent === "claude") return ".claude/skills"
25
+ if (agent === "codex") return ".codex/skills"
26
+ if (agent === "gemini") return ".gemini/skills"
27
+ return `.${agent}/skills`
28
+ }
29
+
30
+ /**
31
+ * Copies SKILL.md files from the bundled skills source into the game
32
+ * directory under the agent-specific skills path.
33
+ */
34
+ export const installSkills = (agent: string, gameDir: string): number => {
35
+ const skillsRoot = agentSkillsDir(agent)
36
+
37
+ // Locate the bundled skills source (packages/skills/ relative to this file)
38
+ // This file is at packages/cli/src/skills/registry.ts (or lib/skills/registry.js when built)
39
+ const thisDir = path.dirname(fileURLToPath(import.meta.url))
40
+ // Walk up to packages/cli, then over to packages/skills
41
+ const packagesDir = path.resolve(thisDir, "..", "..", "..")
42
+ const skillsSrcDir = path.join(packagesDir, "skills")
43
+
44
+ let installed = 0
45
+
46
+ for (const skill of skillsPipeline) {
47
+ const srcFile = path.join(skillsSrcDir, skill.name, "SKILL.md")
48
+ if (!fs.existsSync(srcFile)) continue
49
+
50
+ const destDir = path.join(gameDir, skillsRoot, skill.name)
51
+ fs.mkdirSync(destDir, { recursive: true })
52
+ fs.copyFileSync(srcFile, path.join(destDir, "SKILL.md"))
53
+ installed++
54
+ }
55
+
56
+ return installed
57
+ }
@@ -0,0 +1,91 @@
1
+ import { spawnSync } from "node:child_process"
2
+
3
+ import { skillsPipeline, agentSkillsDir } from "./registry.js"
4
+ import { verifyBuild, runCommand, gitCommit } from "../lib/exec.js"
5
+
6
+ /** Builds the agent prompt for a given skill */
7
+ const buildPrompt = (skillName: string, agent: string): string => {
8
+ const skillDir = agentSkillsDir(agent)
9
+ return `Run the skill ${skillName}. Follow the instructions in ${skillDir}/${skillName}/SKILL.md. The game source is in the current directory.`
10
+ }
11
+
12
+ /** Invokes an LLM agent with a prompt (plain mode, stdio inherited) */
13
+ const invokeAgent = (agent: string, prompt: string, cwd: string): { success: boolean; output: string } => {
14
+ const env = { ...process.env }
15
+ delete (env as Record<string, string | undefined>).CLAUDECODE
16
+ const result = spawnSync(agent, [prompt], {
17
+ cwd,
18
+ env,
19
+ stdio: ["inherit", "pipe", "pipe"],
20
+ encoding: "utf-8",
21
+ timeout: 300000, // 5 minute timeout per skill
22
+ })
23
+
24
+ const output = (result.stdout ?? "") + (result.stderr ?? "")
25
+ return {
26
+ success: result.status === 0,
27
+ output,
28
+ }
29
+ }
30
+
31
+ /** Runs the skills pipeline with plain console output (no TUI) */
32
+ export const runSkillsPipeline = async (agent: string, gameDir: string) => {
33
+ const total = skillsPipeline.length
34
+
35
+ for (let i = 0; i < total; i++) {
36
+ const skill = skillsPipeline[i]
37
+ const step = i + 1
38
+ console.log(`[${step}/${total}] Running skill: ${skill.name}${skill.optional ? " (optional)" : ""}`)
39
+
40
+ const prompt = buildPrompt(skill.name, agent)
41
+ let result = invokeAgent(agent, prompt, gameDir)
42
+
43
+ if (!result.success) {
44
+ if (skill.optional) {
45
+ console.log(` Skipped (optional skill failed)`)
46
+ continue
47
+ }
48
+ console.log(` Agent failed, retrying...`)
49
+ result = invokeAgent(agent, prompt, gameDir)
50
+ if (!result.success) {
51
+ console.error(` Skill ${skill.name} failed after retry. Stopping pipeline.`)
52
+ process.exit(1)
53
+ }
54
+ }
55
+
56
+ // Verify build
57
+ const buildResult = verifyBuild(gameDir)
58
+ if (!buildResult.success) {
59
+ console.log(` Build failed after ${skill.name}, asking agent to fix...`)
60
+ const fixPrompt = `The vite build failed after running skill ${skill.name}. Fix the build errors:\n\n${buildResult.error}`
61
+ invokeAgent(agent, fixPrompt, gameDir)
62
+
63
+ const retryBuild = verifyBuild(gameDir)
64
+ if (!retryBuild.success) {
65
+ if (skill.optional) {
66
+ console.log(` Skipped (build still failing after fix attempt)`)
67
+ continue
68
+ }
69
+ console.error(` Build still failing after fix attempt. Stopping.`)
70
+ process.exit(1)
71
+ }
72
+ }
73
+
74
+ // Commit
75
+ try {
76
+ runCommand("git add -A", { cwd: gameDir })
77
+ gitCommit(`skill: ${skill.name}`, { cwd: gameDir })
78
+ console.log(` Committed.`)
79
+ } catch {
80
+ console.log(` No changes to commit.`)
81
+ }
82
+ }
83
+
84
+ console.log(`\nAll skills completed!`)
85
+ }
86
+
87
+ /** Runs the skills pipeline with a split-pane TUI */
88
+ export const runSkillsPipelineTUI = async (agent: string, gameDir: string) => {
89
+ const { runPipelineTUI } = await import("../tui/pipeline.js")
90
+ await runPipelineTUI(agent, gameDir)
91
+ }
@@ -0,0 +1,460 @@
1
+ import { spawn, type ChildProcess } from "node:child_process"
2
+ import fs from "node:fs"
3
+ import path from "node:path"
4
+
5
+ import { skillsPipeline, agentSkillsDir } from "../skills/registry.js"
6
+ import { verifyBuild, runCommand, gitCommit } from "../lib/exec.js"
7
+
8
+ type StepState = "pending" | "running" | "success" | "failed" | "skipped"
9
+ type Phase = "agent" | "build" | "commit" | "idle"
10
+
11
+ // ANSI escape helpers
12
+ const ESC = "\x1b"
13
+ const CSI = `${ESC}[`
14
+ const moveTo = (row: number, col: number) => `${CSI}${row};${col}H`
15
+ const clearScreen = () => `${CSI}2J`
16
+ const hideCursor = () => `${CSI}?25l`
17
+ const showCursor = () => `${CSI}?25h`
18
+ const bold = (s: string) => `${CSI}1m${s}${CSI}0m`
19
+ const dim = (s: string) => `${CSI}2m${s}${CSI}0m`
20
+ const fg = (code: number, s: string) => `${CSI}${code}m${s}${CSI}0m`
21
+
22
+ const green = (s: string) => fg(32, s)
23
+ const yellow = (s: string) => fg(33, s)
24
+ const red = (s: string) => fg(31, s)
25
+ const cyan = (s: string) => fg(36, s)
26
+ const gray = (s: string) => dim(s)
27
+ const magenta = (s: string) => fg(35, s)
28
+
29
+ const stateIcon: Record<StepState, string> = {
30
+ pending: "○",
31
+ running: "●",
32
+ success: "✓",
33
+ failed: "✗",
34
+ skipped: "–",
35
+ }
36
+
37
+ const stateColor: Record<StepState, (s: string) => string> = {
38
+ pending: gray,
39
+ running: yellow,
40
+ success: green,
41
+ failed: red,
42
+ skipped: gray,
43
+ }
44
+
45
+ const phaseColor: Record<Phase, (s: string) => string> = {
46
+ agent: yellow,
47
+ build: cyan,
48
+ commit: magenta,
49
+ idle: gray,
50
+ }
51
+
52
+ /** Build agent-specific command for non-interactive mode */
53
+ const buildAgentCmd = (agent: string, prompt: string): { cmd: string; args: string[] } => {
54
+ if (agent === "claude") return { cmd: "claude", args: ["-p", "--verbose", "--output-format", "stream-json", prompt] }
55
+ if (agent === "codex") return { cmd: "codex", args: ["--quiet", prompt] }
56
+ return { cmd: agent, args: [prompt] }
57
+ }
58
+
59
+ const buildPrompt = (skillName: string, agent: string): string => {
60
+ const skillDir = agentSkillsDir(agent)
61
+ return `Run the skill ${skillName}. Follow the instructions in ${skillDir}/${skillName}/SKILL.md. The game source is in the current directory.`
62
+ }
63
+
64
+ class PipelineTUI {
65
+ private states: StepState[]
66
+ private currentStep = 0
67
+ private outputLines: string[] = []
68
+ private phase: Phase = "idle"
69
+ private done = false
70
+ private activeProc: ChildProcess | null = null
71
+ private sidebarWidth = 30
72
+
73
+ constructor(
74
+ private agent: string,
75
+ private gameDir: string,
76
+ ) {
77
+ this.states = skillsPipeline.map(() => "pending")
78
+ }
79
+
80
+ private get rows(): number {
81
+ return process.stdout.rows || 24
82
+ }
83
+
84
+ private get cols(): number {
85
+ return process.stdout.columns || 80
86
+ }
87
+
88
+ private get maxOutputLines(): number {
89
+ return Math.max(this.rows - 4, 8)
90
+ }
91
+
92
+ private write(s: string) {
93
+ process.stdout.write(s)
94
+ }
95
+
96
+ /** Draw the entire screen */
97
+ private render() {
98
+ const { rows, cols, sidebarWidth } = this
99
+ const outputWidth = cols - sidebarWidth - 1
100
+ let buf = ""
101
+
102
+ buf += clearScreen()
103
+ buf += moveTo(1, 1)
104
+
105
+ // Top border
106
+ buf += "╭" + "─".repeat(sidebarWidth - 2) + "┬" + "─".repeat(outputWidth - 1) + "╮"
107
+
108
+ // Sidebar header
109
+ buf += moveTo(2, 1)
110
+ buf += "│ " + bold("Migration Pipeline") + " ".repeat(Math.max(0, sidebarWidth - 21)) + "│"
111
+
112
+ // Output panel header
113
+ const headerLabel = this.done
114
+ ? "Done"
115
+ : `${skillsPipeline[this.currentStep]?.name ?? ""} ${dim(`(${this.phase})`)}`
116
+ const headerColor = phaseColor[this.phase]
117
+ buf += " " + headerColor(bold(headerLabel))
118
+ buf += " ".repeat(Math.max(0, outputWidth - this.stripAnsi(headerLabel).length - 2)) + "│"
119
+
120
+ // Separator
121
+ buf += moveTo(3, 1)
122
+ buf += "├" + "─".repeat(sidebarWidth - 2) + "┼" + "─".repeat(outputWidth - 1) + "┤"
123
+
124
+ // Content rows
125
+ const contentRows = rows - 4 // top border + header + separator + bottom border
126
+ for (let row = 0; row < contentRows; row++) {
127
+ buf += moveTo(row + 4, 1)
128
+
129
+ // Sidebar column
130
+ const skillIndex = row
131
+ if (skillIndex < skillsPipeline.length) {
132
+ const skill = skillsPipeline[skillIndex]
133
+ const state = this.states[skillIndex]
134
+ const isActive = skillIndex === this.currentStep && !this.done
135
+ const icon = stateIcon[state]
136
+ const colorFn = stateColor[state]
137
+ const label = `${icon} ${skill.name}${skill.optional ? " *" : ""}`
138
+ const styled = isActive ? bold(colorFn(label)) : colorFn(label)
139
+ const rawLen = this.stripAnsi(styled).length
140
+ buf += "│ " + styled + " ".repeat(Math.max(0, sidebarWidth - rawLen - 3)) + "│"
141
+ } else if (skillIndex === skillsPipeline.length + 1) {
142
+ // Status line
143
+ const completedCount = this.states.filter((s) => s === "success").length
144
+ const failedCount = this.states.filter((s) => s === "failed").length
145
+ const status = this.done
146
+ ? failedCount > 0
147
+ ? red(`${failedCount} failure(s)`)
148
+ : green("All complete!")
149
+ : dim(`${completedCount}/${skillsPipeline.length} done`)
150
+ const rawLen = this.stripAnsi(status).length
151
+ buf += "│ " + status + " ".repeat(Math.max(0, sidebarWidth - rawLen - 3)) + "│"
152
+ } else if (skillIndex === skillsPipeline.length + 2) {
153
+ buf += "│ " + dim("* = optional") + " ".repeat(Math.max(0, sidebarWidth - 15)) + "│"
154
+ } else {
155
+ buf += "│" + " ".repeat(sidebarWidth - 2) + "│"
156
+ }
157
+
158
+ // Output column
159
+ const lineIndex = this.outputLines.length - contentRows + row
160
+ const line = lineIndex >= 0 ? this.outputLines[lineIndex] ?? "" : ""
161
+ const truncated = this.truncateVisible(line, outputWidth - 2)
162
+ const visibleLen = this.stripAnsi(truncated).length
163
+ buf += " " + truncated + " ".repeat(Math.max(0, outputWidth - visibleLen - 1)) + "│"
164
+ }
165
+
166
+ // Bottom border
167
+ buf += moveTo(rows, 1)
168
+ buf += "╰" + "─".repeat(sidebarWidth - 2) + "┴" + "─".repeat(outputWidth - 1) + "╯"
169
+
170
+ this.write(buf)
171
+ }
172
+
173
+ /** Strip ANSI escape codes for length calculation */
174
+ private stripAnsi(s: string): string {
175
+ // oxlint-disable-next-line no-control-regex
176
+ return s.replace(/\x1b\[[0-9;]*m/g, "")
177
+ }
178
+
179
+ /** Truncate a string to a visible width, preserving ANSI codes */
180
+ private truncateVisible(s: string, maxWidth: number): string {
181
+ let visible = 0
182
+ let i = 0
183
+ while (i < s.length && visible < maxWidth) {
184
+ if (s[i] === "\x1b" && s[i + 1] === "[") {
185
+ const end = s.indexOf("m", i)
186
+ if (end !== -1) {
187
+ i = end + 1
188
+ continue
189
+ }
190
+ }
191
+ visible++
192
+ i++
193
+ }
194
+ // Include any trailing ANSI sequences (like reset codes) right after the cut point
195
+ while (i < s.length && s[i] === "\x1b" && s[i + 1] === "[") {
196
+ const end = s.indexOf("m", i)
197
+ if (end !== -1) {
198
+ i = end + 1
199
+ } else break
200
+ }
201
+ return s.slice(0, i)
202
+ }
203
+
204
+ /** Parse stream-json output from claude, or plain text from other agents */
205
+ private appendOutput(text: string) {
206
+ const rawLines = text.split("\n").filter((l) => l.trim())
207
+ for (const line of rawLines) {
208
+ const parsed = this.parseStreamLine(line)
209
+ if (parsed) {
210
+ // Split multi-line parsed output into separate display lines
211
+ for (const displayLine of parsed.split("\n")) {
212
+ this.outputLines.push(displayLine)
213
+ }
214
+ }
215
+ }
216
+ // Keep buffer bounded
217
+ if (this.outputLines.length > 500) {
218
+ this.outputLines = this.outputLines.slice(-this.maxOutputLines)
219
+ }
220
+ this.render()
221
+ }
222
+
223
+ /** Format a tool use block into a display line */
224
+ private formatToolUse(t: any): string {
225
+ const name = t.name ?? "unknown"
226
+ if (name === "Edit") return `${yellow("Edit")} ${t.input?.file_path ?? ""}`
227
+ if (name === "Write") return `${yellow("Write")} ${t.input?.file_path ?? ""}`
228
+ if (name === "Read") return `${yellow("Read")} ${t.input?.file_path ?? ""}`
229
+ if (name === "Bash") return `${yellow("Bash")} ${t.input?.command ?? ""}`
230
+ if (name === "Glob") return `${yellow("Glob")} ${t.input?.pattern ?? ""}`
231
+ if (name === "Grep") return `${yellow("Grep")} ${t.input?.pattern ?? ""}`
232
+ return `${yellow(name)}`
233
+ }
234
+
235
+ /** Extract human-readable text from a stream-json line, or pass through raw text */
236
+ private parseStreamLine(line: string): string | null {
237
+ try {
238
+ const obj = JSON.parse(line)
239
+
240
+ // Assistant messages - may contain text, tool_use, thinking, or a mix
241
+ if (obj.type === "assistant" && obj.message?.content) {
242
+ const content = obj.message.content as any[]
243
+ const parts: string[] = []
244
+
245
+ const texts = content.filter((c) => c.type === "text").map((c) => c.text)
246
+ if (texts.length > 0) parts.push(texts.join(""))
247
+
248
+ const toolUses = content.filter((c) => c.type === "tool_use")
249
+ if (toolUses.length > 0) parts.push(toolUses.map((t) => this.formatToolUse(t)).join("\n"))
250
+
251
+ // Thinking blocks - skip silently, they're just internal reasoning
252
+ if (parts.length > 0) return parts.join("\n")
253
+ if (content.some((c) => c.type === "thinking")) return null
254
+ return null
255
+ }
256
+
257
+ // Content block delta (streaming text chunks)
258
+ if (obj.type === "content_block_delta" && obj.delta?.text) {
259
+ return obj.delta.text
260
+ }
261
+
262
+ // Standalone tool use event
263
+ if (obj.type === "tool_use") {
264
+ return this.formatToolUse(obj)
265
+ }
266
+
267
+ // User messages (tool results coming back)
268
+ if (obj.type === "user") {
269
+ const content = obj.message?.content as any[] | undefined
270
+ if (!content) return null
271
+ // Show file create/update results
272
+ const result = obj.tool_use_result
273
+ if (result?.type === "create") return dim(`Created ${result.filePath}`)
274
+ if (result?.type === "update") return dim(`Updated ${result.filePath}`)
275
+ return null
276
+ }
277
+
278
+ // Rate limit events - suppress
279
+ if (obj.type === "rate_limit_event") return null
280
+
281
+ // Tool results
282
+ if (obj.type === "result") {
283
+ const cost = obj.cost_usd ? ` ($${obj.cost_usd.toFixed(4)})` : ""
284
+ return green(`Done${cost}`)
285
+ }
286
+
287
+ // System messages
288
+ if (obj.type === "system") {
289
+ if (obj.subtype === "init") return dim(`Session started`)
290
+ if (obj.subtype === "hook_started") return null
291
+ if (obj.subtype === "hook_response") return null
292
+ return dim(`[system:${obj.subtype}]`)
293
+ }
294
+
295
+ // Show unrecognized types so we can add support
296
+ return dim(`[${obj.type}${obj.subtype ? ":" + obj.subtype : ""}]`)
297
+ } catch {
298
+ // Not JSON - show as-is
299
+ return line
300
+ }
301
+ }
302
+
303
+ /** Spawn agent and stream output */
304
+ private runAgent(prompt: string): Promise<boolean> {
305
+ return new Promise((resolve) => {
306
+ const { cmd, args } = buildAgentCmd(this.agent, prompt)
307
+ const env: Record<string, string | undefined> = { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" }
308
+ // Remove env vars that prevent nested claude sessions
309
+ delete env.CLAUDECODE
310
+ const proc = spawn(cmd, args, {
311
+ cwd: this.gameDir,
312
+ env,
313
+ stdio: ["ignore", "pipe", "pipe"],
314
+ })
315
+
316
+ this.activeProc = proc
317
+ const debugLog = path.join(this.gameDir, "pipeline-debug.log")
318
+
319
+ proc.stdout?.on("data", (data: Buffer) => {
320
+ fs.appendFileSync(debugLog, data.toString())
321
+ this.appendOutput(data.toString())
322
+ })
323
+ proc.stderr?.on("data", (data: Buffer) => {
324
+ fs.appendFileSync(debugLog, `[stderr] ${data.toString()}`)
325
+ this.appendOutput(data.toString())
326
+ })
327
+
328
+ proc.on("close", (code) => {
329
+ fs.appendFileSync(debugLog, `\n[close] code=${code}\n`)
330
+
331
+ this.activeProc = null
332
+ resolve(code === 0)
333
+ })
334
+ proc.on("error", (err) => {
335
+ this.appendOutput(`Error: ${err.message}`)
336
+ this.activeProc = null
337
+ resolve(false)
338
+ })
339
+ })
340
+ }
341
+
342
+ /** Run one pipeline step */
343
+ private async runStep(stepIndex: number): Promise<boolean> {
344
+ const skill = skillsPipeline[stepIndex]
345
+ this.states[stepIndex] = "running"
346
+ this.currentStep = stepIndex
347
+ this.outputLines = []
348
+ this.phase = "agent"
349
+ this.render()
350
+
351
+ const prompt = buildPrompt(skill.name, this.agent)
352
+ let success = await this.runAgent(prompt)
353
+
354
+ if (!success) {
355
+ if (skill.optional) {
356
+ this.states[stepIndex] = "skipped"
357
+ this.render()
358
+ return true
359
+ }
360
+ this.appendOutput("\n--- Retrying ---\n")
361
+ success = await this.runAgent(prompt)
362
+ if (!success) {
363
+ this.states[stepIndex] = "failed"
364
+ this.render()
365
+ return false
366
+ }
367
+ }
368
+
369
+ // Verify build
370
+ this.phase = "build"
371
+ this.render()
372
+ this.appendOutput("Verifying build...")
373
+ const buildResult = verifyBuild(this.gameDir)
374
+ if (!buildResult.success) {
375
+ this.appendOutput(`Build failed: ${buildResult.error}\nAsking agent to fix...`)
376
+ const fixPrompt = `The vite build failed after running skill ${skill.name}. Fix the build errors:\n\n${buildResult.error}`
377
+ await this.runAgent(fixPrompt)
378
+
379
+ const retry = verifyBuild(this.gameDir)
380
+ if (!retry.success) {
381
+ if (skill.optional) {
382
+ this.states[stepIndex] = "skipped"
383
+ this.render()
384
+ return true
385
+ }
386
+ this.states[stepIndex] = "failed"
387
+ this.render()
388
+ return false
389
+ }
390
+ }
391
+
392
+ // Commit
393
+ this.phase = "commit"
394
+ this.render()
395
+ try {
396
+ runCommand("git add -A", { cwd: this.gameDir })
397
+ gitCommit(`skill: ${skill.name}`, { cwd: this.gameDir })
398
+ this.appendOutput("Committed.")
399
+ } catch {
400
+ this.appendOutput("No changes to commit.")
401
+ }
402
+
403
+ this.states[stepIndex] = "success"
404
+ this.render()
405
+ return true
406
+ }
407
+
408
+ /** Set up keyboard handling (Ctrl+C to quit) */
409
+ private setupInput() {
410
+ if (process.stdin.isTTY) {
411
+ process.stdin.setRawMode(true)
412
+ }
413
+ process.stdin.resume()
414
+ process.stdin.on("data", (data: Buffer) => {
415
+ // Ctrl+C
416
+ if (data[0] === 3) {
417
+ if (this.activeProc) this.activeProc.kill()
418
+ this.cleanup()
419
+ process.exit(0)
420
+ }
421
+ })
422
+ }
423
+
424
+ private cleanup() {
425
+ this.write(showCursor())
426
+ if (process.stdin.isTTY) {
427
+ process.stdin.setRawMode(false)
428
+ }
429
+ process.stdin.pause()
430
+ }
431
+
432
+ /** Run the full pipeline */
433
+ async run() {
434
+ this.write(hideCursor())
435
+ this.setupInput()
436
+ this.render()
437
+
438
+ // Redraw on terminal resize
439
+ process.stdout.on("resize", () => this.render())
440
+
441
+ for (let i = 0; i < skillsPipeline.length; i++) {
442
+ const ok = await this.runStep(i)
443
+ if (!ok) {
444
+ this.appendOutput("\nPipeline stopped due to failure.")
445
+ break
446
+ }
447
+ }
448
+
449
+ this.phase = "idle"
450
+ this.done = true
451
+ this.render()
452
+ this.cleanup()
453
+ }
454
+ }
455
+
456
+ /** Render the pipeline TUI and wait for it to complete */
457
+ export const runPipelineTUI = async (agent: string, gameDir: string): Promise<void> => {
458
+ const tui = new PipelineTUI(agent, gameDir)
459
+ await tui.run()
460
+ }
@@ -0,0 +1,6 @@
1
+ import { detectAgentCLIs, type DetectedAgentCLI } from "@puzzmo/agent-cli-detect"
2
+
3
+ export type { DetectedAgentCLI as AgentInfo }
4
+
5
+ /** Detects installed LLM agent CLIs on the system */
6
+ export const detectAgent = (): DetectedAgentCLI[] => detectAgentCLIs()
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "declaration": true,
4
+ "esModuleInterop": true,
5
+ "module": "Node16",
6
+ "moduleResolution": "node16",
7
+ "outDir": "lib",
8
+ "skipLibCheck": true,
9
+ "sourceMap": true,
10
+ "strict": true,
11
+ "target": "ES2022"
12
+ },
13
+ "exclude": ["node_modules"],
14
+ "include": ["src"]
15
+ }