fix: escape parentheses in paths before bash shell-out, fix __extensionDir fallback (#1872)
Closes #1437
This commit is contained in:
parent
77b220e9e5
commit
8bed02c077
7 changed files with 82 additions and 30 deletions
|
|
@ -25,7 +25,7 @@ import {
|
|||
isDbAvailable,
|
||||
} from "./gsd-db.js";
|
||||
import { atomicWriteSync } from "./atomic-write.js";
|
||||
import { execSync, execFileSync } from "node:child_process";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
|
||||
import { gsdRoot } from "./paths.js";
|
||||
import {
|
||||
|
|
@ -477,7 +477,7 @@ export function runWorktreePostCreateHook(
|
|||
}
|
||||
|
||||
try {
|
||||
execSync(resolved, {
|
||||
execFileSync(resolved, [], {
|
||||
cwd: worktreeDir,
|
||||
env: {
|
||||
...process.env,
|
||||
|
|
@ -1172,7 +1172,7 @@ export function mergeMilestoneToMain(
|
|||
if (prefs.auto_push === true && !nothingToCommit) {
|
||||
const remote = prefs.remote ?? "origin";
|
||||
try {
|
||||
execSync(`git push ${remote} ${mainBranch}`, {
|
||||
execFileSync("git", ["push", remote, mainBranch], {
|
||||
cwd: originalBasePath_,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf-8",
|
||||
|
|
@ -1190,20 +1190,23 @@ export function mergeMilestoneToMain(
|
|||
const prTarget = prefs.pr_target_branch ?? mainBranch;
|
||||
try {
|
||||
// Push the milestone branch to remote first
|
||||
execSync(`git push ${remote} ${milestoneBranch}`, {
|
||||
execFileSync("git", ["push", remote, milestoneBranch], {
|
||||
cwd: originalBasePath_,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf-8",
|
||||
});
|
||||
// Create PR via gh CLI
|
||||
execSync(
|
||||
`gh pr create --base "${prTarget}" --head "${milestoneBranch}" --title "Milestone ${milestoneId} complete" --body "Auto-created by GSD on milestone completion."`,
|
||||
{
|
||||
cwd: originalBasePath_,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf-8",
|
||||
},
|
||||
);
|
||||
execFileSync("gh", [
|
||||
"pr", "create",
|
||||
"--base", prTarget,
|
||||
"--head", milestoneBranch,
|
||||
"--title", `Milestone ${milestoneId} complete`,
|
||||
"--body", "Auto-created by GSD on milestone completion.",
|
||||
], {
|
||||
cwd: originalBasePath_,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf-8",
|
||||
});
|
||||
prCreated = true;
|
||||
} catch {
|
||||
// PR creation failure is non-fatal — gh may not be installed or authenticated
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent
|
|||
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
||||
import { join, dirname, relative } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { homedir } from "node:os";
|
||||
|
||||
import { extractTrace, type ExecutionTrace } from "./session-forensics.js";
|
||||
import { nativeParseJsonlTail } from "./native-parser-bridge.js";
|
||||
|
|
@ -102,9 +103,14 @@ export async function handleForensics(
|
|||
const report = await buildForensicReport(basePath);
|
||||
const savedPath = saveForensicReport(basePath, report, problemDescription);
|
||||
|
||||
// Derive GSD source dir for prompt
|
||||
const __extensionDir = dirname(fileURLToPath(import.meta.url));
|
||||
const gsdSourceDir = __extensionDir;
|
||||
// Derive GSD source dir for prompt — fall back to ~/.gsd/agent/extensions/gsd/
|
||||
// when import.meta.url resolves to the npm-global install path (Windows).
|
||||
let gsdSourceDir = dirname(fileURLToPath(import.meta.url));
|
||||
if (!existsSync(join(gsdSourceDir, "prompts"))) {
|
||||
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
||||
const fallback = join(gsdHome, "agent", "extensions", "gsd");
|
||||
if (existsSync(join(fallback, "prompts"))) gsdSourceDir = fallback;
|
||||
}
|
||||
|
||||
const forensicData = formatReportForPrompt(report);
|
||||
const content = loadPrompt("forensics", {
|
||||
|
|
|
|||
|
|
@ -683,10 +683,11 @@ export function createDraftPR(
|
|||
body: string,
|
||||
): string | null {
|
||||
try {
|
||||
const result = execSync(
|
||||
`gh pr create --draft --title ${JSON.stringify(title)} --body ${JSON.stringify(body)}`,
|
||||
{ cwd: basePath, encoding: "utf8", timeout: 30000, env: GIT_NO_PROMPT_ENV },
|
||||
);
|
||||
const result = execFileSync("gh", [
|
||||
"pr", "create", "--draft",
|
||||
"--title", title,
|
||||
"--body", body,
|
||||
], { cwd: basePath, encoding: "utf8", timeout: 30000, env: GIT_NO_PROMPT_ENV });
|
||||
return result.trim();
|
||||
} catch {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -808,7 +808,7 @@ export function nativeCheckoutBranch(basePath: string, branch: string): void {
|
|||
native.gitCheckoutBranch(basePath, branch);
|
||||
return;
|
||||
}
|
||||
execSync(`git checkout ${branch}`, {
|
||||
execFileSync("git", ["checkout", branch], {
|
||||
cwd: basePath,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf-8",
|
||||
|
|
@ -843,7 +843,7 @@ export function nativeMergeSquash(basePath: string, branch: string): GitMergeRes
|
|||
}
|
||||
|
||||
try {
|
||||
execSync(`git merge --squash ${branch}`, {
|
||||
execFileSync("git", ["merge", "--squash", branch], {
|
||||
cwd: basePath,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf-8",
|
||||
|
|
|
|||
|
|
@ -17,12 +17,36 @@
|
|||
* that aren't read until the end of a long auto-mode run.
|
||||
*/
|
||||
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
||||
import { GSDError, GSD_PARSE_ERROR } from "./errors.js";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { homedir } from "node:os";
|
||||
|
||||
const __extensionDir = dirname(fileURLToPath(import.meta.url));
|
||||
/**
|
||||
* Resolve the GSD extension directory.
|
||||
*
|
||||
* `import.meta.url` resolves to whichever copy of this module is executing.
|
||||
* On Windows (npm global install via MSYS2 / Git Bash) this can resolve to
|
||||
* the npm-global `AppData/Roaming/npm/…` path, which does NOT contain the
|
||||
* prompts/ and templates/ subtrees that initResources() copies to
|
||||
* `~/.gsd/agent/extensions/gsd/`. Detect the mismatch and fall back to
|
||||
* the user-local agent directory.
|
||||
*/
|
||||
function resolveExtensionDir(): string {
|
||||
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
||||
if (existsSync(join(moduleDir, "prompts"))) return moduleDir;
|
||||
|
||||
// Fallback: user-local agent directory
|
||||
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
||||
const agentGsdDir = join(gsdHome, "agent", "extensions", "gsd");
|
||||
if (existsSync(join(agentGsdDir, "prompts"))) return agentGsdDir;
|
||||
|
||||
// Last resort: return the module dir (warmCache will silently handle the miss)
|
||||
return moduleDir;
|
||||
}
|
||||
|
||||
const __extensionDir = resolveExtensionDir();
|
||||
const promptsDir = join(__extensionDir, "prompts");
|
||||
const templatesDir = join(__extensionDir, "templates");
|
||||
|
||||
|
|
@ -45,7 +69,11 @@ function warmCache(): void {
|
|||
}
|
||||
}
|
||||
} catch {
|
||||
// prompts/ may not exist in test environments — lazy loading still works
|
||||
// prompts/ may not exist in test environments — lazy loading still works.
|
||||
// Emit a diagnostic when running outside tests so wrong-path bugs are visible.
|
||||
if (!process.env.VITEST && !process.env.NODE_TEST) {
|
||||
process.stderr.write(`[gsd:prompt-loader] warmCache: prompts dir not found: ${promptsDir}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -57,7 +85,10 @@ function warmCache(): void {
|
|||
}
|
||||
}
|
||||
} catch {
|
||||
// templates/ may not exist in test environments — lazy loading still works
|
||||
// templates/ may not exist in test environments — lazy loading still works.
|
||||
if (!process.env.VITEST && !process.env.NODE_TEST) {
|
||||
process.stderr.write(`[gsd:prompt-loader] warmCache: templates dir not found: ${templatesDir}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,10 +8,21 @@
|
|||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { homedir } from "node:os";
|
||||
|
||||
const __extensionDir = dirname(fileURLToPath(import.meta.url));
|
||||
const __extensionDir = resolveGsdExtensionDir();
|
||||
const registryPath = join(__extensionDir, "workflow-templates", "registry.json");
|
||||
|
||||
/** Resolve the GSD extension dir with fallback to ~/.gsd/agent/extensions/gsd/. */
|
||||
function resolveGsdExtensionDir(): string {
|
||||
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
||||
if (existsSync(join(moduleDir, "workflow-templates"))) return moduleDir;
|
||||
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
||||
const agentGsdDir = join(gsdHome, "agent", "extensions", "gsd");
|
||||
if (existsSync(join(agentGsdDir, "workflow-templates"))) return agentGsdDir;
|
||||
return moduleDir;
|
||||
}
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface TemplateEntry {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent";
|
|||
import { shortcutDesc } from "../shared/mod.js";
|
||||
import type { AssistantMessage } from "@gsd/pi-ai";
|
||||
import { isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@gsd/pi-tui";
|
||||
import { spawn, execSync, type ChildProcess } from "node:child_process";
|
||||
import { spawn, execFileSync, type ChildProcess } from "node:child_process";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
|
|
@ -32,7 +32,7 @@ function linuxPython(): string {
|
|||
function ensureBinary(): boolean {
|
||||
if (fs.existsSync(RECOGNIZER_BIN)) return true;
|
||||
try {
|
||||
execSync(`swiftc "${SWIFT_SRC}" -o "${RECOGNIZER_BIN}" -framework Speech -framework AVFoundation`, {
|
||||
execFileSync("swiftc", [SWIFT_SRC, "-o", RECOGNIZER_BIN, "-framework", "Speech", "-framework", "AVFoundation"], {
|
||||
timeout: 60000,
|
||||
});
|
||||
return true;
|
||||
|
|
@ -54,7 +54,7 @@ function ensureLinuxReady(ctx: ExtensionContext): boolean {
|
|||
|
||||
// Check python3 exists
|
||||
try {
|
||||
execSync("which python3", { stdio: "pipe" });
|
||||
execFileSync("which", ["python3"], { stdio: "pipe" });
|
||||
} catch {
|
||||
ctx.ui.notify("Voice: python3 not found — install with: sudo apt install python3", "error");
|
||||
return false;
|
||||
|
|
@ -63,7 +63,7 @@ function ensureLinuxReady(ctx: ExtensionContext): boolean {
|
|||
// Check that sounddevice is importable
|
||||
const py = linuxPython();
|
||||
try {
|
||||
execSync(`${py} -c "import sounddevice"`, {
|
||||
execFileSync(py, ["-c", "import sounddevice"], {
|
||||
stdio: "pipe",
|
||||
timeout: 10000,
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue