sf snapshot: pre-dispatch, uncommitted changes after 31m inactivity
This commit is contained in:
parent
51202225ec
commit
9843425836
9 changed files with 155 additions and 42 deletions
|
|
@ -20,11 +20,11 @@ import {
|
|||
parseCliArgs,
|
||||
runWebCliBranch,
|
||||
} from "./cli-web-branch.js";
|
||||
import { error, formatStructuredError } from "./errors.js";
|
||||
import { printHelp, printSubcommandHelp } from "./help-text.js";
|
||||
import { runOnboarding, shouldRunOnboarding } from "./onboarding.js";
|
||||
import { migratePiCredentials } from "./pi-migration.js";
|
||||
import { getProjectSessionsDir } from "./project-sessions.js";
|
||||
import { error, formatStructuredError } from "./errors.js";
|
||||
import {
|
||||
buildResourceLoader,
|
||||
getNewerManagedResourceVersion,
|
||||
|
|
@ -129,7 +129,7 @@ function printExtensionErrors(errors: ReadonlyArray<{ error: string }>): void {
|
|||
process.stderr.write(
|
||||
formatStructuredError(
|
||||
error(err.error, { operation: "loadExtension", guidance }),
|
||||
"[sf]",
|
||||
`[sf] ${prefix}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import type {
|
|||
ExtensionAPI,
|
||||
ExtensionCommandContext,
|
||||
} from "@singularity-forge/pi-coding-agent";
|
||||
import { importExtensionModule } from "@singularity-forge/pi-coding-agent";
|
||||
|
||||
interface Route {
|
||||
keywords: string[];
|
||||
|
|
@ -160,13 +161,17 @@ export async function handleDo(
|
|||
ctx.ui.notify(`→ /sf ${fullCommand}`, "info");
|
||||
|
||||
// Re-dispatch through the main dispatcher
|
||||
const { handleSFCommand } = await import("./commands/dispatcher.js");
|
||||
const { handleSFCommand } = await importExtensionModule<
|
||||
typeof import("./commands/dispatcher.js")
|
||||
>(import.meta.url, "./commands/dispatcher.js");
|
||||
await handleSFCommand(fullCommand, ctx, pi);
|
||||
return;
|
||||
}
|
||||
|
||||
// No keyword match → treat as quick task
|
||||
ctx.ui.notify(`→ /sf quick ${args}`, "info");
|
||||
const { handleQuick } = await import("./quick.js");
|
||||
const { handleQuick } = await importExtensionModule<
|
||||
typeof import("./quick.js")
|
||||
>(import.meta.url, "./quick.js");
|
||||
await handleQuick(args, ctx, pi);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { importExtensionModule } from "@singularity-forge/pi-coding-agent";
|
||||
|
||||
export { registerSFCommand } from "./commands/index.js";
|
||||
|
||||
export async function handleSFCommand(
|
||||
...args: Parameters<typeof import("./commands/dispatcher.js").handleSFCommand>
|
||||
) {
|
||||
const { handleSFCommand: dispatch } = await import(
|
||||
"./commands/dispatcher.js"
|
||||
);
|
||||
const { handleSFCommand: dispatch } = await importExtensionModule<
|
||||
typeof import("./commands/dispatcher.js")
|
||||
>(import.meta.url, "./commands/dispatcher.js");
|
||||
return dispatch(...args);
|
||||
}
|
||||
|
||||
|
|
@ -14,8 +16,8 @@ export async function fireStatusViaCommand(
|
|||
typeof import("./commands/handlers/core.js").fireStatusViaCommand
|
||||
>
|
||||
) {
|
||||
const { fireStatusViaCommand: fireStatus } = await import(
|
||||
"./commands/handlers/core.js"
|
||||
);
|
||||
const { fireStatusViaCommand: fireStatus } = await importExtensionModule<
|
||||
typeof import("./commands/handlers/core.js")
|
||||
>(import.meta.url, "./commands/handlers/core.js");
|
||||
return fireStatus(...args);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,19 +2,21 @@ import type {
|
|||
ExtensionAPI,
|
||||
ExtensionCommandContext,
|
||||
} from "@singularity-forge/pi-coding-agent";
|
||||
import { importExtensionModule } from "@singularity-forge/pi-coding-agent";
|
||||
|
||||
import {
|
||||
getSfArgumentCompletions,
|
||||
SF_COMMAND_DESCRIPTION,
|
||||
} from "./catalog.js";
|
||||
import { getSfArgumentCompletions, SF_COMMAND_DESCRIPTION } from "./catalog.js";
|
||||
|
||||
export function registerSFCommand(pi: ExtensionAPI): void {
|
||||
pi.registerCommand("sf", {
|
||||
description: SF_COMMAND_DESCRIPTION,
|
||||
getArgumentCompletions: getSfArgumentCompletions,
|
||||
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
||||
const { handleSFCommand } = await import("./dispatcher.js");
|
||||
const { setStderrLoggingEnabled } = await import("../workflow-logger.js");
|
||||
const { handleSFCommand } = await importExtensionModule<
|
||||
typeof import("./dispatcher.js")
|
||||
>(import.meta.url, "./dispatcher.js");
|
||||
const { setStderrLoggingEnabled } = await importExtensionModule<
|
||||
typeof import("../workflow-logger.js")
|
||||
>(import.meta.url, "../workflow-logger.js");
|
||||
const previousStderrSetting = setStderrLoggingEnabled(false);
|
||||
try {
|
||||
await handleSFCommand(args, ctx, pi);
|
||||
|
|
|
|||
|
|
@ -682,18 +682,41 @@ function detectXcodePlatforms(basePath: string): XcodePlatform[] {
|
|||
// ─── Package Manager Detection ──────────────────────────────────────────────────
|
||||
|
||||
function detectPackageManager(basePath: string): string | undefined {
|
||||
const declared = readPackageJsonPackageManager(basePath);
|
||||
if (declared) return declared;
|
||||
|
||||
if (existsSync(join(basePath, "pnpm-lock.yaml"))) return "pnpm";
|
||||
if (existsSync(join(basePath, "yarn.lock"))) return "yarn";
|
||||
if (
|
||||
existsSync(join(basePath, "bun.lockb")) ||
|
||||
existsSync(join(basePath, "bun.lock"))
|
||||
)
|
||||
return "bun";
|
||||
return existsSync(join(basePath, "package.json")) ? "npm" : undefined;
|
||||
if (existsSync(join(basePath, "package-lock.json"))) return "npm";
|
||||
if (existsSync(join(basePath, "package.json"))) return "npm";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function readPackageJsonPackageManager(basePath: string): string | undefined {
|
||||
try {
|
||||
const raw = readFileSync(join(basePath, "package.json"), "utf-8");
|
||||
const pkg = JSON.parse(raw);
|
||||
if (typeof pkg.packageManager !== "string") return undefined;
|
||||
const name = pkg.packageManager.split("@")[0];
|
||||
if (
|
||||
name === "npm" ||
|
||||
name === "pnpm" ||
|
||||
name === "yarn"
|
||||
) {
|
||||
return name;
|
||||
}
|
||||
if (name === "bun") return "npm";
|
||||
return undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Verification Command Detection ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
|
@ -712,9 +735,7 @@ function detectVerificationCommands(
|
|||
? "npm run"
|
||||
: pm === "yarn"
|
||||
? "yarn"
|
||||
: pm === "bun"
|
||||
? "bun run"
|
||||
: `${pm} run`;
|
||||
: `${pm} run`;
|
||||
|
||||
if (detectedFiles.includes("package.json")) {
|
||||
const scripts = readPackageJsonScripts(basePath);
|
||||
|
|
|
|||
|
|
@ -303,7 +303,40 @@ test("detectProjectSignals: package manager detection", (t) => {
|
|||
|
||||
writeFileSync(join(dir3, "bun.lockb"), "", "utf-8");
|
||||
writeFileSync(join(dir3, "package.json"), "{}", "utf-8");
|
||||
assert.equal(detectProjectSignals(dir3).packageManager, "bun");
|
||||
assert.equal(detectProjectSignals(dir3).packageManager, "npm");
|
||||
});
|
||||
|
||||
test("detectProjectSignals: packageManager field overrides lockfile heuristic", (t) => {
|
||||
const dir = makeTempDir("pm-declared-npm");
|
||||
t.after(() => cleanup(dir));
|
||||
|
||||
writeFileSync(join(dir, "bun.lockb"), "", "utf-8");
|
||||
writeFileSync(
|
||||
join(dir, "package.json"),
|
||||
JSON.stringify({ packageManager: "npm@10.9.3" }),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
assert.equal(detectProjectSignals(dir).packageManager, "npm");
|
||||
});
|
||||
|
||||
test("detectProjectSignals: bun packageManager is normalized to npm for verification", (t) => {
|
||||
const dir = makeTempDir("pm-declared-bun");
|
||||
t.after(() => cleanup(dir));
|
||||
|
||||
writeFileSync(
|
||||
join(dir, "package.json"),
|
||||
JSON.stringify({
|
||||
packageManager: "bun@1.3.3",
|
||||
scripts: { test: "node --test" },
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const signals = detectProjectSignals(dir);
|
||||
assert.equal(signals.packageManager, "npm");
|
||||
assert.ok(signals.verificationCommands.includes("npm test"));
|
||||
assert.ok(!signals.verificationCommands.some((cmd) => cmd.includes("bun")));
|
||||
});
|
||||
|
||||
test("detectProjectSignals: skips default npm test script", (t) => {
|
||||
|
|
|
|||
|
|
@ -166,8 +166,6 @@ const KNOWN_COMMAND_PREFIXES = new Set([
|
|||
"npx",
|
||||
"yarn",
|
||||
"pnpm",
|
||||
"bun",
|
||||
"bunx",
|
||||
"deno",
|
||||
"node",
|
||||
"ts-node",
|
||||
|
|
|
|||
|
|
@ -2354,44 +2354,78 @@ export default function (pi: ExtensionAPI) {
|
|||
return fs.existsSync(homeBin) ? homeBin : "sift";
|
||||
})();
|
||||
|
||||
const args = [
|
||||
"search",
|
||||
"--strategy",
|
||||
strategy,
|
||||
"--agent",
|
||||
query,
|
||||
scope,
|
||||
];
|
||||
const args = ["search", "--strategy", strategy, "--agent", query, scope];
|
||||
|
||||
const stderr: string[] = [];
|
||||
const stdout: string[] = [];
|
||||
let wasAborted = false;
|
||||
|
||||
const proc = spawn(siftBin, args, {
|
||||
cwd: scope,
|
||||
shell: false,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
liveSubagentProcesses.add(proc);
|
||||
|
||||
// Collect output
|
||||
proc.stdout.on("data", (chunk) => stdout.push(chunk.toString()));
|
||||
proc.stderr.on("data", (chunk) => stderr.push(chunk.toString()));
|
||||
|
||||
// Handle abort signal
|
||||
if (signal) {
|
||||
signal.addEventListener("abort", () => {
|
||||
try {
|
||||
proc.kill("SIGTERM");
|
||||
} catch {
|
||||
// ignore
|
||||
const killProc = () => {
|
||||
wasAborted = true;
|
||||
try {
|
||||
proc.kill("SIGTERM");
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (proc.exitCode === null) {
|
||||
try {
|
||||
proc.kill("SIGKILL");
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 5000).unref?.();
|
||||
};
|
||||
if (signal) {
|
||||
if (signal.aborted) killProc();
|
||||
else signal.addEventListener("abort", killProc, { once: true });
|
||||
}
|
||||
|
||||
const exitCode = await new Promise<number>((resolve) => {
|
||||
proc.on("close", (code) => resolve(code ?? 0));
|
||||
proc.on("error", () => resolve(1));
|
||||
proc.on("close", (code) => {
|
||||
liveSubagentProcesses.delete(proc);
|
||||
if (signal) signal.removeEventListener("abort", killProc);
|
||||
resolve(code ?? 0);
|
||||
});
|
||||
proc.on("error", () => {
|
||||
liveSubagentProcesses.delete(proc);
|
||||
if (signal) signal.removeEventListener("abort", killProc);
|
||||
resolve(1);
|
||||
});
|
||||
});
|
||||
|
||||
if (wasAborted) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "call_scout aborted.",
|
||||
},
|
||||
],
|
||||
details: {
|
||||
operation: "call_scout",
|
||||
aborted: true,
|
||||
siftBin,
|
||||
query,
|
||||
scope,
|
||||
strategy,
|
||||
} as Record<string, unknown>,
|
||||
};
|
||||
}
|
||||
|
||||
const out = stdout.join("");
|
||||
const err = stderr.join("").trim();
|
||||
|
||||
|
|
@ -2409,7 +2443,14 @@ export default function (pi: ExtensionAPI) {
|
|||
text: `call_scout failed (exit ${exitCode}). Is sift installed?${hint}`,
|
||||
},
|
||||
],
|
||||
details: { operation: "call_scout", exitCode, siftBin, query, scope, strategy } as Record<string, unknown>,
|
||||
details: {
|
||||
operation: "call_scout",
|
||||
exitCode,
|
||||
siftBin,
|
||||
query,
|
||||
scope,
|
||||
strategy,
|
||||
} as Record<string, unknown>,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,11 +19,22 @@ test("subagent launcher resolves Node command specs instead of shelling through
|
|||
test("normal subagent execution spawns the resolved Node command with argv array", () => {
|
||||
assert.match(
|
||||
subagentSrc,
|
||||
/spawn\(\s*launchSpec\.command,\s*\[\.\.\.extensionArgs,\s*\.\.\.launchSpec\.args\]/,
|
||||
/spawn\(\s*launchSpec\.command,\s*launchSpec\.args,/,
|
||||
);
|
||||
assert.match(subagentSrc, /shell:\s*false/);
|
||||
});
|
||||
|
||||
test("call_scout subprocesses are tracked and killed on abort", () => {
|
||||
const scoutIdx = subagentSrc.indexOf('name: "call_scout"');
|
||||
assert.ok(scoutIdx > 0, "call_scout tool must be registered");
|
||||
const scoutSrc = subagentSrc.slice(scoutIdx);
|
||||
|
||||
assert.match(scoutSrc, /liveSubagentProcesses\.add\(proc\)/);
|
||||
assert.match(scoutSrc, /liveSubagentProcesses\.delete\(proc\)/);
|
||||
assert.match(scoutSrc, /signal\.addEventListener\("abort", killProc, \{ once: true \}\)/);
|
||||
assert.match(scoutSrc, /proc\.kill\("SIGKILL"\)/);
|
||||
});
|
||||
|
||||
test("cmux launcher writes only explicit environment patch, not the full process env", () => {
|
||||
assert.match(subagentSrc, /function writeNodeSubagentLauncher\(/);
|
||||
assert.match(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue