@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
JavaScript
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
};