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