refactor: make spike a standalone script

- Remove spike command from extension registration
- Add run-spike.sh wrapper script
- Add npm script 'spike' to run the PoC
- Spike can be run via: npm run spike or ./run-spike.sh
This commit is contained in:
jay 2026-05-15 03:44:13 +02:00
parent befb1fc98b
commit 4aab59947f
4 changed files with 41 additions and 36 deletions

View File

@ -24,7 +24,6 @@ import {
} from "./config.js"; } from "./config.js";
import { type RawMessage, serializeMessage } from "./messages.js"; import { type RawMessage, serializeMessage } from "./messages.js";
import { type RemoteServer, startServer } from "./server.js"; import { type RemoteServer, startServer } from "./server.js";
import { registerSpikeCommand } from "./spike.js";
// ── Extension entry point ──────────────────────────────────────────────────── // ── Extension entry point ────────────────────────────────────────────────────
@ -34,9 +33,6 @@ const QRCode = _require("qrcode") as {
}; };
export default function remoteControl(pi: ExtensionAPI) { export default function remoteControl(pi: ExtensionAPI) {
// Register spike command for Phase 0 PoC
registerSpikeCommand(pi);
let server: RemoteServer | undefined; let server: RemoteServer | undefined;
let pendingSyncTimer: ReturnType<typeof setTimeout> | undefined; let pendingSyncTimer: ReturnType<typeof setTimeout> | undefined;

View File

@ -14,9 +14,8 @@
import * as fs from "node:fs"; import * as fs from "node:fs";
import * as path from "node:path"; import * as path from "node:path";
import * as os from "node:os"; import * as os from "node:os";
import { spawn, execSync } from "node:child_process"; import { execSync } from "node:child_process";
import { WebSocketServer } from "ws"; import { WebSocketServer } from "ws";
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
const SPIKE_SESSION = "pi-spike"; const SPIKE_SESSION = "pi-spike";
const WS_PORT = 7799; const WS_PORT = 7799;
@ -113,23 +112,24 @@ function startWebSocketServer(fifoPath: string): { wss: WebSocketServer, cleanup
} }
/** /**
* Attach to the tmux session in the current terminal * Print instructions for connecting to the session
*/ */
function attachToSession(sessionName: string): void { function printInstructions(sessionName: string): void {
console.log(`[spike] Attaching to tmux session: ${sessionName}`); console.log("");
console.log(`[spike] To detach: Ctrl+B, then D`); console.log("=== Spike Server Running ===");
console.log(`[spike] WebSocket available at: ws://127.0.0.1:${WS_PORT}/spike`); console.log("");
console.log("To attach to the tmux session (in another terminal):");
console.log(` tmux attach -t ${sessionName}`);
console.log("");
console.log("WebSocket endpoint:");
console.log(` ws://127.0.0.1:${WS_PORT}/spike`);
console.log("");
console.log("To test with the HTML client:");
const clientPath = path.join(path.dirname(new URL(import.meta.url).pathname), "spike-client.html");
console.log(` open ${clientPath}`);
console.log("");
console.log("To stop: Ctrl+C in this terminal");
console.log(""); console.log("");
// Spawn tmux attach in the foreground
// This will take over the terminal until the user detaches
const attach = spawn("tmux", ["attach", "-t", sessionName], {
stdio: "inherit",
});
attach.on("exit", (code) => {
console.log(`\n[spike] Detached from session (exit code: ${code})`);
});
} }
/** /**
@ -159,14 +159,15 @@ function cleanup(cleanupFn: (() => void) | null): void {
/** /**
* Main spike entry point * Main spike entry point
*/ */
export async function runSpike(_ctx: ExtensionContext): Promise<void> { export async function runSpike(): Promise<void> {
console.log("=== Phase 0 Spike: tmux Stream PoC ===\n"); console.log("=== Phase 0 Spike: tmux Stream PoC ===\n");
let cleanupFn: (() => void) | null = null; let cleanupFn: (() => void) | null = null;
// Setup cleanup handlers // Setup cleanup handlers
process.on("SIGINT", () => cleanup(cleanupFn)); const cleanupHandler = () => cleanup(cleanupFn);
process.on("SIGTERM", () => cleanup(cleanupFn)); process.on("SIGINT", cleanupHandler);
process.on("SIGTERM", cleanupHandler);
try { try {
// Step 1: Create or reuse tmux session // Step 1: Create or reuse tmux session
@ -186,8 +187,12 @@ export async function runSpike(_ctx: ExtensionContext): Promise<void> {
// Give the server a moment to start // Give the server a moment to start
await new Promise(resolve => setTimeout(resolve, 500)); await new Promise(resolve => setTimeout(resolve, 500));
// Step 4: Attach to session // Step 4: Print instructions
attachToSession(SPIKE_SESSION); printInstructions(SPIKE_SESSION);
// Keep the process alive
// User can Ctrl+C to stop
await new Promise(() => {}); // Never resolves
} catch (err) { } catch (err) {
console.error("[spike] Error:", err); console.error("[spike] Error:", err);
@ -195,14 +200,7 @@ export async function runSpike(_ctx: ExtensionContext): Promise<void> {
} }
} }
/** // Run if invoked directly
* Register the spike command with pi if (import.meta.url === `file://${process.argv[1]}`) {
*/ runSpike();
export function registerSpikeCommand(pi: ExtensionAPI): void {
pi.registerCommand("spike", {
description: "Phase 0 Spike: Start tmux stream PoC (ws://127.0.0.1:7799/spike)",
handler: async (_args, ctx) => {
await runSpike(ctx);
},
});
} }

View File

@ -15,6 +15,7 @@
"@earendil-works/pi-tui": "*" "@earendil-works/pi-tui": "*"
}, },
"scripts": { "scripts": {
"spike": "./run-spike.sh",
"lint": "biome check --write .", "lint": "biome check --write .",
"lint:check": "biome check .", "lint:check": "biome check .",
"prepare": "node .husky/install.mjs" "prepare": "node .husky/install.mjs"

10
run-spike.sh Executable file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env bash
# Phase 0 Spike runner
# Transpiles and runs the spike PoC
set -e
cd "$(dirname "$0")"
echo "=== Building spike.ts ==="
npx --yes tsx extensions/remote-control/spike.ts