@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.
- package/README.md +75 -0
- package/lib/commands/game/create.d.ts +2 -0
- package/lib/commands/game/create.js +145 -0
- package/lib/commands/game/create.js.map +1 -0
- package/lib/commands/login.d.ts +2 -0
- package/lib/commands/login.js +13 -0
- package/lib/commands/login.js.map +1 -0
- package/lib/commands/upload.d.ts +2 -0
- package/lib/commands/upload.js +87 -0
- package/lib/commands/upload.js.map +1 -0
- package/lib/download/page-downloader.d.ts +2 -0
- package/lib/download/page-downloader.js +94 -0
- package/lib/download/page-downloader.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +53 -0
- package/lib/index.js.map +1 -0
- package/lib/lib/api.d.ts +10 -0
- package/lib/lib/api.js +53 -0
- package/lib/lib/api.js.map +1 -0
- package/lib/lib/config.d.ts +13 -0
- package/lib/lib/config.js +29 -0
- package/lib/lib/config.js.map +1 -0
- package/lib/lib/exec.d.ts +15 -0
- package/lib/lib/exec.js +34 -0
- package/lib/lib/exec.js.map +1 -0
- package/lib/lib/package-manager.d.ts +2 -0
- package/lib/lib/package-manager.js +15 -0
- package/lib/lib/package-manager.js.map +1 -0
- package/lib/skills/registry.d.ts +13 -0
- package/lib/skills/registry.js +49 -0
- package/lib/skills/registry.js.map +1 -0
- package/lib/skills/runner.d.ts +4 -0
- package/lib/skills/runner.js +80 -0
- package/lib/skills/runner.js.map +1 -0
- package/lib/tui/pipeline.d.ts +2 -0
- package/lib/tui/pipeline.js +426 -0
- package/lib/tui/pipeline.js.map +1 -0
- package/lib/wizard/agent-detect.d.ts +4 -0
- package/lib/wizard/agent-detect.js +4 -0
- package/lib/wizard/agent-detect.js.map +1 -0
- package/package.json +20 -0
- package/src/commands/game/create.ts +171 -0
- package/src/commands/login.ts +15 -0
- package/src/commands/upload.ts +94 -0
- package/src/download/page-downloader.ts +104 -0
- package/src/index.ts +56 -0
- package/src/lib/api.ts +75 -0
- package/src/lib/config.ts +37 -0
- package/src/lib/exec.ts +38 -0
- package/src/skills/registry.ts +57 -0
- package/src/skills/runner.ts +91 -0
- package/src/tui/pipeline.ts +460 -0
- package/src/wizard/agent-detect.ts +6 -0
- 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
|
+
}
|
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
|
+
}
|