Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
set -euo pipefail
TARBALL=$(ls knowpatch-*.tgz)
CONTENTS=$(tar tzf "${TARBALL}")
echo "${CONTENTS}" | grep -q "package/bin/cli.js" || { echo "FAIL: bin/cli.js missing"; exit 1; }
echo "${CONTENTS}" | grep -q "package/bin/detect.js" || { echo "FAIL: bin/detect.js missing"; exit 1; }
echo "${CONTENTS}" | grep -q "package/skills/" || { echo "FAIL: skills/ missing"; exit 1; }
echo "${CONTENTS}" | grep -q "package/bin/cli.js" || { echo "FAIL: bin/cli.js missing"; exit 1; }
echo "${CONTENTS}" | grep -q "package/skills/knowpatch/bin/detect.js" || { echo "FAIL: skills/knowpatch/bin/detect.js missing"; exit 1; }
echo "${CONTENTS}" | grep -q "package/skills/" || { echo "FAIL: skills/ missing"; exit 1; }
echo "Package contents verified"
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ jobs:
- run: bun run build
- name: Verify build artifacts
run: |
[ -f bin/cli.js ] && [ -f bin/detect.js ] && echo "OK" || exit 1
[ -f bin/cli.js ] && [ -f skills/knowpatch/bin/detect.js ] && echo "OK" || exit 1
- name: Verify package contents
run: npm pack --dry-run
- name: Publish to npm
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ dist/
*.js.map
.DS_Store
.agents/hooks/state/
skills/knowpatch/bin/

__pycache__
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"knowpatch": "bin/cli.js"
},
"scripts": {
"build": "bun build src/cli.ts --outdir bin --target node --format esm && bun build src/hooks/detect.ts --outdir bin --target node --format esm",
"build": "bun build src/cli.ts --outdir bin --target node --format esm && bun build src/hooks/detect.ts --outdir skills/knowpatch/bin --target node --format esm",
"dev": "bun run src/cli.ts",
"test": "bun test",
"lint": "biome check src/ tests/",
Expand Down
59 changes: 47 additions & 12 deletions src/commands/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,34 +67,54 @@ async function syncInstallation(scope: Scope): Promise<void> {
if (ps.implicitlyLinked) {
const hookStatus = ps.platform.supportsHooks
? ps.hookInstalled
? "hook up-to-date"
? ps.hookUpToDate
? "hook up-to-date"
: "hook outdated"
: "hook missing"
: "";
console.log(
` ${ICONS.ok} ${COLORS.success(`${ps.platform.displayName}:`)} shared via parent symlink${hookStatus ? `, ${hookStatus}` : ""}`,
);

if (ps.platform.supportsHooks && !ps.hookInstalled) {
await installPlatformHook(ps.platform, scope);
console.log(
` ${ICONS.ok} ${COLORS.success(`${ps.platform.displayName}:`)} hook restored`,
);
if (ps.platform.supportsHooks) {
if (!ps.hookInstalled) {
await installPlatformHook(ps.platform, scope);
console.log(
` ${ICONS.ok} ${COLORS.success(`${ps.platform.displayName}:`)} hook restored`,
);
} else if (!ps.hookUpToDate) {
await uninstallPlatformHook(ps.platform, scope);
await installPlatformHook(ps.platform, scope);
console.log(
` ${ICONS.ok} ${COLORS.success(`${ps.platform.displayName}:`)} hook updated`,
);
}
}
} else if (ps.symlinkValid) {
const hookStatus = ps.platform.supportsHooks
? ps.hookInstalled
? "hook up-to-date"
? ps.hookUpToDate
? "hook up-to-date"
: "hook outdated"
: "hook missing"
: "";
console.log(
` ${ICONS.ok} ${COLORS.success(`${ps.platform.displayName}:`)} symlink valid${hookStatus ? `, ${hookStatus}` : ""}`,
);

if (ps.platform.supportsHooks && !ps.hookInstalled) {
await installPlatformHook(ps.platform, scope);
console.log(
` ${ICONS.ok} ${COLORS.success(`${ps.platform.displayName}:`)} hook restored`,
);
if (ps.platform.supportsHooks) {
if (!ps.hookInstalled) {
await installPlatformHook(ps.platform, scope);
console.log(
` ${ICONS.ok} ${COLORS.success(`${ps.platform.displayName}:`)} hook restored`,
);
} else if (!ps.hookUpToDate) {
await uninstallPlatformHook(ps.platform, scope);
await installPlatformHook(ps.platform, scope);
console.log(
` ${ICONS.ok} ${COLORS.success(`${ps.platform.displayName}:`)} hook updated`,
);
}
}
} else {
console.log(
Expand All @@ -107,6 +127,21 @@ async function syncInstallation(scope: Scope): Promise<void> {
console.log(
` ${ICONS.ok} ${COLORS.success(`${ps.platform.displayName}:`)} symlink restored`,
);

if (ps.platform.supportsHooks) {
if (!ps.hookInstalled) {
await installPlatformHook(ps.platform, scope);
console.log(
` ${ICONS.ok} ${COLORS.success(`${ps.platform.displayName}:`)} hook restored`,
);
} else if (!ps.hookUpToDate) {
await uninstallPlatformHook(ps.platform, scope);
await installPlatformHook(ps.platform, scope);
console.log(
` ${ICONS.ok} ${COLORS.success(`${ps.platform.displayName}:`)} hook updated`,
);
}
}
}
}

Expand Down
24 changes: 21 additions & 3 deletions src/core/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,29 @@ export async function isPlatformHookInstalled(
if (!settingsPath) return false;

const settings = await readSettings(settingsPath);
const hookCmd = getHookCommand();
const hookCmd = getHookCommand(scope);
const entries = settings.hooks?.UserPromptSubmit ?? [];
return entries.some((m) => isOurHook(m, hookCmd));
}

/** Check if the installed hook command matches the expected command for this scope */
export async function isPlatformHookUpToDate(
platform: PlatformConfig,
scope: Scope,
): Promise<boolean> {
if (platform.hookType === "none") return true;
const settingsPath = getPlatformSettingsPath(platform, scope);
if (!settingsPath) return true;

const hookCmd = getHookCommand(scope);
const settings = await readSettings(settingsPath);
const entries = settings.hooks?.UserPromptSubmit ?? [];

return entries.some((m) =>
m.hooks.some((h) => h.type === "command" && h.command === hookCmd),
);
}

/** Add the knowpatch hook to a platform's settings.json */
export async function installPlatformHook(
platform: PlatformConfig,
Expand All @@ -74,7 +92,7 @@ export async function installPlatformHook(
const settingsPath = getPlatformSettingsPath(platform, scope);
if (!settingsPath) return false;

const hookCmd = getHookCommand();
const hookCmd = getHookCommand(scope);
const settings = await readSettings(settingsPath);

if (!settings.hooks) {
Expand Down Expand Up @@ -106,7 +124,7 @@ export async function uninstallPlatformHook(
const settingsPath = getPlatformSettingsPath(platform, scope);
if (!settingsPath) return false;

const hookCmd = getHookCommand();
const hookCmd = getHookCommand(scope);
const settings = await readSettings(settingsPath);
const hooks = settings.hooks;
const entries = hooks?.UserPromptSubmit;
Expand Down
10 changes: 7 additions & 3 deletions src/core/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,13 @@ export function getPlatformSettingsPath(
return resolve(getScopeBase(scope), platform.configDir, "settings.json");
}

/** Absolute path to the built hook detect script */
export function getHookCommand(): string {
return `node ${resolve(getPackageRoot(), "bin/detect.js")}`;
/** Hook command for settings.json — relative for project scope, absolute for user */
export function getHookCommand(scope: Scope): string {
const detectScript = ".agents/skills/knowpatch/bin/detect.js";
if (scope === "project") {
return `node ${detectScript}`;
}
return `node ${resolve(homedir(), detectScript)}`;
}

/** Safe lstat that returns null instead of throwing */
Expand Down
7 changes: 6 additions & 1 deletion src/core/status.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { lstat, readlink } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { isPlatformHookInstalled } from "./hooks.js";
import { isPlatformHookInstalled, isPlatformHookUpToDate } from "./hooks.js";
import {
getAgentsSkillPath,
getPlatformSkillPath,
Expand All @@ -22,6 +22,7 @@ export interface PlatformStatus {
symlinkValid: boolean;
implicitlyLinked: boolean;
hookInstalled: boolean;
hookUpToDate: boolean;
}

export interface InstallationStatus {
Expand Down Expand Up @@ -85,13 +86,17 @@ export async function detectInstallation(
}

const hookInstalled = await isPlatformHookInstalled(platform, scope);
const hookUpToDate = hookInstalled
? await isPlatformHookUpToDate(platform, scope)
: false;

platforms.push({
platform,
installed,
symlinkValid,
implicitlyLinked,
hookInstalled,
hookUpToDate,
});
}

Expand Down
29 changes: 7 additions & 22 deletions src/hooks/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
* is derived from correction files.
*/

import { accessSync, readdirSync, readFileSync } from "node:fs";
import { readdirSync, readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

export interface CorrectionEntry {
header: string;
Expand Down Expand Up @@ -73,21 +74,10 @@ export function buildMessage(
].join("\n");
}

/** Find the package root by walking up from this file */
function findPackageRoot(): string {
let dir = dirname(new URL(import.meta.url).pathname);
while (true) {
try {
accessSync(resolve(dir, "package.json"));
return dir;
} catch {
const parent = dirname(dir);
if (parent === dir) {
throw new Error("Could not find knowpatch package root");
}
dir = parent;
}
}
/** Resolve corrections directory relative to this script's location */
function getCorrectionsDir(): string {
const scriptDir = dirname(fileURLToPath(import.meta.url));
return resolve(scriptDir, "..", "corrections");
}

/** Load all correction files, returning parsed tags and entries */
Expand Down Expand Up @@ -147,12 +137,7 @@ async function main() {
return;
}

let correctionsDir: string;
try {
correctionsDir = resolve(findPackageRoot(), "skills/knowpatch/corrections");
} catch {
return;
}
const correctionsDir = getCorrectionsDir();

// Load corrections and derive keywords from tags
const corrections = loadCorrections(correctionsDir);
Expand Down
21 changes: 17 additions & 4 deletions tests/paths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,23 @@ describe("getPlatformSettingsPath", () => {
});

describe("getHookCommand", () => {
test("includes bin/detect.js", () => {
const cmd = getHookCommand();
expect(cmd).toContain("bin/detect.js");
expect(cmd).toStartWith("node ");
test("project scope returns relative path", () => {
const cmd = getHookCommand("project");
expect(cmd).toBe("node .agents/skills/knowpatch/bin/detect.js");
});

test("user scope returns absolute path with homedir", () => {
const { homedir } = require("node:os");
const { resolve } = require("node:path");
const cmd = getHookCommand("user");
expect(cmd).toBe(
`node ${resolve(homedir(), ".agents/skills/knowpatch/bin/detect.js")}`,
);
});

test("both scopes start with node prefix", () => {
expect(getHookCommand("project")).toStartWith("node ");
expect(getHookCommand("user")).toStartWith("node ");
});
});

Expand Down