@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,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 {};
|
package/lib/lib/exec.js
ADDED
|
@@ -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,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,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
|