UNPKG

@stackmemoryai/stackmemory

Version:

Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a

124 lines (123 loc) 4.46 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { Command } from "commander"; import { execSync } from "child_process"; import chalk from "chalk"; function parseSeconds(value) { const match = value.match(/^(\d+)(s|m|h)?$/); if (!match) return parseInt(value, 10) || 10; const num = parseInt(match[1], 10); switch (match[2]) { case "h": return num * 3600; case "m": return num * 60; default: return num; } } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function checkCondition(output, exitCode, opts) { if (opts.untilExit) return exitCode === 0; if (opts.untilEmpty) return output.trim().length === 0; if (opts.untilNonEmpty) return output.trim().length > 0; if (opts.until) return output.includes(opts.until); if (opts.untilNot) return !output.includes(opts.untilNot); return exitCode === 0; } function createLoopCommand() { return new Command("loop").alias("watch").description( "Run a command repeatedly until a condition is met (monitor CI, deploys, logs)" ).argument("<command>", "Shell command to run on each iteration").option("--until <pattern>", "Stop when output contains this string").option( "--until-not <pattern>", "Stop when output no longer contains this string" ).option("--until-empty", "Stop when output is empty", false).option("--until-non-empty", "Stop when output is non-empty", false).option( "--until-exit", "Stop when command exits with code 0 (ignore output)", false ).option("-i, --interval <duration>", "Check interval (e.g. 10s, 1m)", "10s").option("-t, --timeout <duration>", "Max wait time (e.g. 30m, 1h)", "30m").option("-q, --quiet", "Only show final result", false).option("--json", "Output result as JSON", false).option("-l, --label <name>", "Label for this loop (shown in output)").option("--shell <path>", "Shell to use", "/bin/sh").action(async (command, opts) => { const intervalSec = parseSeconds(opts.interval); const timeoutSec = parseSeconds(opts.timeout); const label = opts.label || command.slice(0, 40); const startTime = Date.now(); const deadline = startTime + timeoutSec * 1e3; let iterations = 0; let lastOutput = ""; if (!opts.quiet && !opts.json) { console.log( chalk.cyan(`[loop] ${label}`) + chalk.gray(` (every ${intervalSec}s, timeout ${timeoutSec}s)`) ); } while (Date.now() < deadline) { iterations++; let output = ""; let exitCode = 0; try { output = execSync(command, { encoding: "utf8", timeout: Math.min(intervalSec * 1e3, 6e4), shell: opts.shell, stdio: ["pipe", "pipe", "pipe"] }); } catch (error) { const e = error; exitCode = e.status ?? 1; output = (e.stdout || "") + (e.stderr || ""); } lastOutput = output; if (!opts.quiet && !opts.json) { const elapsed = Math.round((Date.now() - startTime) / 1e3); const preview = output.trim().split("\n").slice(-3).join("\n"); console.log( chalk.gray(`[${elapsed}s #${iterations}]`) + (preview ? ` ${preview}` : chalk.gray(" (no output)")) ); } if (checkCondition(output, exitCode, opts)) { const result2 = { ok: true, reason: "matched", iterations, elapsed: Date.now() - startTime, lastOutput: lastOutput.trim() }; if (opts.json) { console.log(JSON.stringify(result2)); } else if (!opts.quiet) { console.log( chalk.green(`[loop] Condition met after ${iterations} iterations`) ); } return; } const remaining = deadline - Date.now(); if (remaining > 0) { await sleep(Math.min(intervalSec * 1e3, remaining)); } } const result = { ok: false, reason: "timeout", iterations, elapsed: Date.now() - startTime, lastOutput: lastOutput.trim() }; if (opts.json) { console.log(JSON.stringify(result)); } else { console.log( chalk.yellow( `[loop] Timed out after ${timeoutSec}s (${iterations} iterations)` ) ); } process.exit(1); }); } export { createLoopCommand };