mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Sync generated provider output
This commit is contained in:
@@ -215,12 +215,57 @@ function collectHookCommands(value, out = []) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Pull the script path out of a hook command line. Commands look like
|
||||
// `node .claude/skills/impeccable/scripts/hook.mjs` and may be quoted or carry
|
||||
// trailing arguments.
|
||||
function hookScriptPathFrom(command) {
|
||||
const match = String(command).match(/(\S*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return match ? match[1].replace(/^['"]|['"]$/g, '') : null;
|
||||
const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
|
||||
// Pull the script-path token out of a hook command line, placeholders intact.
|
||||
// The forms our manifests ship:
|
||||
// * bare: node "${CLAUDE_PROJECT_DIR}/.../hook.mjs"
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
// delimited token that ends at the marker, so we don't absorb `node`, `[`, `!`
|
||||
// or `||`. Returns the token verbatim; resolution happens separately.
|
||||
function hookScriptTokenFrom(command) {
|
||||
const str = String(command);
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
// Resolve a script token to an absolute path the doctor can existsSync, or null
|
||||
// when the doctor cannot know where it points — in which case the caller must
|
||||
// NOT report it missing (a doctor never asserts a negative it cannot verify).
|
||||
//
|
||||
// Per-placeholder policy, mirroring what each runtime actually expands:
|
||||
// ${CLAUDE_PROJECT_DIR} → the project root being scanned. This is exactly the
|
||||
// runtime mapping (Claude Code sets it to the project
|
||||
// dir at hook time), so we EXPAND it against `root`.
|
||||
// Not doing so was the #402 bug: the literal
|
||||
// `${CLAUDE_PROJECT_DIR}/...` string never exists.
|
||||
// ${CLAUDE_PLUGIN_ROOT} → plugin-package install dir, set by the harness to
|
||||
// ${PLUGIN_ROOT} wherever the plugin/codex/grok bundle was unpacked
|
||||
// ${GROK_PLUGIN_ROOT} (grok aliases CLAUDE_PLUGIN_ROOT). The doctor has no
|
||||
// way to know that location → SKIP (return null).
|
||||
// $(...) / backticks → command substitution, e.g. GitHub's
|
||||
// `$(git rev-parse --show-toplevel)`. Not statically
|
||||
// resolvable → SKIP.
|
||||
// any other ${VAR}/$VAR → unknown to the doctor → SKIP.
|
||||
// A token with no placeholder is a literal path: absolute as-is, else relative
|
||||
// to `root`.
|
||||
function resolveHookScriptPath(token, root) {
|
||||
if (!token) return null;
|
||||
// Command substitution or backtick expansion we can't evaluate.
|
||||
if (token.includes('$(') || token.includes('`')) return null;
|
||||
const expanded = token.replace(/\$\{CLAUDE_PROJECT_DIR\}/g, root);
|
||||
// Any placeholder or shell variable still present is one we can't map.
|
||||
if (/\$\{[^}]*\}|\$[A-Za-z_]/.test(expanded)) return null;
|
||||
return path.isAbsolute(expanded) ? expanded : path.join(root, expanded);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,9 +291,11 @@ export function checkHookInstallation({ projectRoot, repoRoot, providerId }) {
|
||||
installedAt = toRelative(manifestPath, projectRoot || root);
|
||||
|
||||
const broken = commands.filter((command) => {
|
||||
const scriptPath = hookScriptPathFrom(command);
|
||||
if (!scriptPath) return false;
|
||||
const abs = path.isAbsolute(scriptPath) ? scriptPath : path.join(root, scriptPath);
|
||||
const token = hookScriptTokenFrom(command);
|
||||
if (!token) return false;
|
||||
const abs = resolveHookScriptPath(token, root);
|
||||
// Unresolvable placeholder or command substitution: never assert missing.
|
||||
if (!abs) return false;
|
||||
return !fs.existsSync(abs);
|
||||
});
|
||||
if (broken.length) {
|
||||
|
||||
@@ -215,12 +215,57 @@ function collectHookCommands(value, out = []) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Pull the script path out of a hook command line. Commands look like
|
||||
// `node .claude/skills/impeccable/scripts/hook.mjs` and may be quoted or carry
|
||||
// trailing arguments.
|
||||
function hookScriptPathFrom(command) {
|
||||
const match = String(command).match(/(\S*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return match ? match[1].replace(/^['"]|['"]$/g, '') : null;
|
||||
const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
|
||||
// Pull the script-path token out of a hook command line, placeholders intact.
|
||||
// The forms our manifests ship:
|
||||
// * bare: node "${CLAUDE_PROJECT_DIR}/.../hook.mjs"
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
// delimited token that ends at the marker, so we don't absorb `node`, `[`, `!`
|
||||
// or `||`. Returns the token verbatim; resolution happens separately.
|
||||
function hookScriptTokenFrom(command) {
|
||||
const str = String(command);
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
// Resolve a script token to an absolute path the doctor can existsSync, or null
|
||||
// when the doctor cannot know where it points — in which case the caller must
|
||||
// NOT report it missing (a doctor never asserts a negative it cannot verify).
|
||||
//
|
||||
// Per-placeholder policy, mirroring what each runtime actually expands:
|
||||
// ${CLAUDE_PROJECT_DIR} → the project root being scanned. This is exactly the
|
||||
// runtime mapping (Claude Code sets it to the project
|
||||
// dir at hook time), so we EXPAND it against `root`.
|
||||
// Not doing so was the #402 bug: the literal
|
||||
// `${CLAUDE_PROJECT_DIR}/...` string never exists.
|
||||
// ${CLAUDE_PLUGIN_ROOT} → plugin-package install dir, set by the harness to
|
||||
// ${PLUGIN_ROOT} wherever the plugin/codex/grok bundle was unpacked
|
||||
// ${GROK_PLUGIN_ROOT} (grok aliases CLAUDE_PLUGIN_ROOT). The doctor has no
|
||||
// way to know that location → SKIP (return null).
|
||||
// $(...) / backticks → command substitution, e.g. GitHub's
|
||||
// `$(git rev-parse --show-toplevel)`. Not statically
|
||||
// resolvable → SKIP.
|
||||
// any other ${VAR}/$VAR → unknown to the doctor → SKIP.
|
||||
// A token with no placeholder is a literal path: absolute as-is, else relative
|
||||
// to `root`.
|
||||
function resolveHookScriptPath(token, root) {
|
||||
if (!token) return null;
|
||||
// Command substitution or backtick expansion we can't evaluate.
|
||||
if (token.includes('$(') || token.includes('`')) return null;
|
||||
const expanded = token.replace(/\$\{CLAUDE_PROJECT_DIR\}/g, root);
|
||||
// Any placeholder or shell variable still present is one we can't map.
|
||||
if (/\$\{[^}]*\}|\$[A-Za-z_]/.test(expanded)) return null;
|
||||
return path.isAbsolute(expanded) ? expanded : path.join(root, expanded);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,9 +291,11 @@ export function checkHookInstallation({ projectRoot, repoRoot, providerId }) {
|
||||
installedAt = toRelative(manifestPath, projectRoot || root);
|
||||
|
||||
const broken = commands.filter((command) => {
|
||||
const scriptPath = hookScriptPathFrom(command);
|
||||
if (!scriptPath) return false;
|
||||
const abs = path.isAbsolute(scriptPath) ? scriptPath : path.join(root, scriptPath);
|
||||
const token = hookScriptTokenFrom(command);
|
||||
if (!token) return false;
|
||||
const abs = resolveHookScriptPath(token, root);
|
||||
// Unresolvable placeholder or command substitution: never assert missing.
|
||||
if (!abs) return false;
|
||||
return !fs.existsSync(abs);
|
||||
});
|
||||
if (broken.length) {
|
||||
|
||||
@@ -215,12 +215,57 @@ function collectHookCommands(value, out = []) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Pull the script path out of a hook command line. Commands look like
|
||||
// `node .claude/skills/impeccable/scripts/hook.mjs` and may be quoted or carry
|
||||
// trailing arguments.
|
||||
function hookScriptPathFrom(command) {
|
||||
const match = String(command).match(/(\S*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return match ? match[1].replace(/^['"]|['"]$/g, '') : null;
|
||||
const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
|
||||
// Pull the script-path token out of a hook command line, placeholders intact.
|
||||
// The forms our manifests ship:
|
||||
// * bare: node "${CLAUDE_PROJECT_DIR}/.../hook.mjs"
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
// delimited token that ends at the marker, so we don't absorb `node`, `[`, `!`
|
||||
// or `||`. Returns the token verbatim; resolution happens separately.
|
||||
function hookScriptTokenFrom(command) {
|
||||
const str = String(command);
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
// Resolve a script token to an absolute path the doctor can existsSync, or null
|
||||
// when the doctor cannot know where it points — in which case the caller must
|
||||
// NOT report it missing (a doctor never asserts a negative it cannot verify).
|
||||
//
|
||||
// Per-placeholder policy, mirroring what each runtime actually expands:
|
||||
// ${CLAUDE_PROJECT_DIR} → the project root being scanned. This is exactly the
|
||||
// runtime mapping (Claude Code sets it to the project
|
||||
// dir at hook time), so we EXPAND it against `root`.
|
||||
// Not doing so was the #402 bug: the literal
|
||||
// `${CLAUDE_PROJECT_DIR}/...` string never exists.
|
||||
// ${CLAUDE_PLUGIN_ROOT} → plugin-package install dir, set by the harness to
|
||||
// ${PLUGIN_ROOT} wherever the plugin/codex/grok bundle was unpacked
|
||||
// ${GROK_PLUGIN_ROOT} (grok aliases CLAUDE_PLUGIN_ROOT). The doctor has no
|
||||
// way to know that location → SKIP (return null).
|
||||
// $(...) / backticks → command substitution, e.g. GitHub's
|
||||
// `$(git rev-parse --show-toplevel)`. Not statically
|
||||
// resolvable → SKIP.
|
||||
// any other ${VAR}/$VAR → unknown to the doctor → SKIP.
|
||||
// A token with no placeholder is a literal path: absolute as-is, else relative
|
||||
// to `root`.
|
||||
function resolveHookScriptPath(token, root) {
|
||||
if (!token) return null;
|
||||
// Command substitution or backtick expansion we can't evaluate.
|
||||
if (token.includes('$(') || token.includes('`')) return null;
|
||||
const expanded = token.replace(/\$\{CLAUDE_PROJECT_DIR\}/g, root);
|
||||
// Any placeholder or shell variable still present is one we can't map.
|
||||
if (/\$\{[^}]*\}|\$[A-Za-z_]/.test(expanded)) return null;
|
||||
return path.isAbsolute(expanded) ? expanded : path.join(root, expanded);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,9 +291,11 @@ export function checkHookInstallation({ projectRoot, repoRoot, providerId }) {
|
||||
installedAt = toRelative(manifestPath, projectRoot || root);
|
||||
|
||||
const broken = commands.filter((command) => {
|
||||
const scriptPath = hookScriptPathFrom(command);
|
||||
if (!scriptPath) return false;
|
||||
const abs = path.isAbsolute(scriptPath) ? scriptPath : path.join(root, scriptPath);
|
||||
const token = hookScriptTokenFrom(command);
|
||||
if (!token) return false;
|
||||
const abs = resolveHookScriptPath(token, root);
|
||||
// Unresolvable placeholder or command substitution: never assert missing.
|
||||
if (!abs) return false;
|
||||
return !fs.existsSync(abs);
|
||||
});
|
||||
if (broken.length) {
|
||||
|
||||
@@ -215,12 +215,57 @@ function collectHookCommands(value, out = []) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Pull the script path out of a hook command line. Commands look like
|
||||
// `node .claude/skills/impeccable/scripts/hook.mjs` and may be quoted or carry
|
||||
// trailing arguments.
|
||||
function hookScriptPathFrom(command) {
|
||||
const match = String(command).match(/(\S*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return match ? match[1].replace(/^['"]|['"]$/g, '') : null;
|
||||
const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
|
||||
// Pull the script-path token out of a hook command line, placeholders intact.
|
||||
// The forms our manifests ship:
|
||||
// * bare: node "${CLAUDE_PROJECT_DIR}/.../hook.mjs"
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
// delimited token that ends at the marker, so we don't absorb `node`, `[`, `!`
|
||||
// or `||`. Returns the token verbatim; resolution happens separately.
|
||||
function hookScriptTokenFrom(command) {
|
||||
const str = String(command);
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
// Resolve a script token to an absolute path the doctor can existsSync, or null
|
||||
// when the doctor cannot know where it points — in which case the caller must
|
||||
// NOT report it missing (a doctor never asserts a negative it cannot verify).
|
||||
//
|
||||
// Per-placeholder policy, mirroring what each runtime actually expands:
|
||||
// ${CLAUDE_PROJECT_DIR} → the project root being scanned. This is exactly the
|
||||
// runtime mapping (Claude Code sets it to the project
|
||||
// dir at hook time), so we EXPAND it against `root`.
|
||||
// Not doing so was the #402 bug: the literal
|
||||
// `${CLAUDE_PROJECT_DIR}/...` string never exists.
|
||||
// ${CLAUDE_PLUGIN_ROOT} → plugin-package install dir, set by the harness to
|
||||
// ${PLUGIN_ROOT} wherever the plugin/codex/grok bundle was unpacked
|
||||
// ${GROK_PLUGIN_ROOT} (grok aliases CLAUDE_PLUGIN_ROOT). The doctor has no
|
||||
// way to know that location → SKIP (return null).
|
||||
// $(...) / backticks → command substitution, e.g. GitHub's
|
||||
// `$(git rev-parse --show-toplevel)`. Not statically
|
||||
// resolvable → SKIP.
|
||||
// any other ${VAR}/$VAR → unknown to the doctor → SKIP.
|
||||
// A token with no placeholder is a literal path: absolute as-is, else relative
|
||||
// to `root`.
|
||||
function resolveHookScriptPath(token, root) {
|
||||
if (!token) return null;
|
||||
// Command substitution or backtick expansion we can't evaluate.
|
||||
if (token.includes('$(') || token.includes('`')) return null;
|
||||
const expanded = token.replace(/\$\{CLAUDE_PROJECT_DIR\}/g, root);
|
||||
// Any placeholder or shell variable still present is one we can't map.
|
||||
if (/\$\{[^}]*\}|\$[A-Za-z_]/.test(expanded)) return null;
|
||||
return path.isAbsolute(expanded) ? expanded : path.join(root, expanded);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,9 +291,11 @@ export function checkHookInstallation({ projectRoot, repoRoot, providerId }) {
|
||||
installedAt = toRelative(manifestPath, projectRoot || root);
|
||||
|
||||
const broken = commands.filter((command) => {
|
||||
const scriptPath = hookScriptPathFrom(command);
|
||||
if (!scriptPath) return false;
|
||||
const abs = path.isAbsolute(scriptPath) ? scriptPath : path.join(root, scriptPath);
|
||||
const token = hookScriptTokenFrom(command);
|
||||
if (!token) return false;
|
||||
const abs = resolveHookScriptPath(token, root);
|
||||
// Unresolvable placeholder or command substitution: never assert missing.
|
||||
if (!abs) return false;
|
||||
return !fs.existsSync(abs);
|
||||
});
|
||||
if (broken.length) {
|
||||
|
||||
@@ -215,12 +215,57 @@ function collectHookCommands(value, out = []) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Pull the script path out of a hook command line. Commands look like
|
||||
// `node .claude/skills/impeccable/scripts/hook.mjs` and may be quoted or carry
|
||||
// trailing arguments.
|
||||
function hookScriptPathFrom(command) {
|
||||
const match = String(command).match(/(\S*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return match ? match[1].replace(/^['"]|['"]$/g, '') : null;
|
||||
const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
|
||||
// Pull the script-path token out of a hook command line, placeholders intact.
|
||||
// The forms our manifests ship:
|
||||
// * bare: node "${CLAUDE_PROJECT_DIR}/.../hook.mjs"
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
// delimited token that ends at the marker, so we don't absorb `node`, `[`, `!`
|
||||
// or `||`. Returns the token verbatim; resolution happens separately.
|
||||
function hookScriptTokenFrom(command) {
|
||||
const str = String(command);
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
// Resolve a script token to an absolute path the doctor can existsSync, or null
|
||||
// when the doctor cannot know where it points — in which case the caller must
|
||||
// NOT report it missing (a doctor never asserts a negative it cannot verify).
|
||||
//
|
||||
// Per-placeholder policy, mirroring what each runtime actually expands:
|
||||
// ${CLAUDE_PROJECT_DIR} → the project root being scanned. This is exactly the
|
||||
// runtime mapping (Claude Code sets it to the project
|
||||
// dir at hook time), so we EXPAND it against `root`.
|
||||
// Not doing so was the #402 bug: the literal
|
||||
// `${CLAUDE_PROJECT_DIR}/...` string never exists.
|
||||
// ${CLAUDE_PLUGIN_ROOT} → plugin-package install dir, set by the harness to
|
||||
// ${PLUGIN_ROOT} wherever the plugin/codex/grok bundle was unpacked
|
||||
// ${GROK_PLUGIN_ROOT} (grok aliases CLAUDE_PLUGIN_ROOT). The doctor has no
|
||||
// way to know that location → SKIP (return null).
|
||||
// $(...) / backticks → command substitution, e.g. GitHub's
|
||||
// `$(git rev-parse --show-toplevel)`. Not statically
|
||||
// resolvable → SKIP.
|
||||
// any other ${VAR}/$VAR → unknown to the doctor → SKIP.
|
||||
// A token with no placeholder is a literal path: absolute as-is, else relative
|
||||
// to `root`.
|
||||
function resolveHookScriptPath(token, root) {
|
||||
if (!token) return null;
|
||||
// Command substitution or backtick expansion we can't evaluate.
|
||||
if (token.includes('$(') || token.includes('`')) return null;
|
||||
const expanded = token.replace(/\$\{CLAUDE_PROJECT_DIR\}/g, root);
|
||||
// Any placeholder or shell variable still present is one we can't map.
|
||||
if (/\$\{[^}]*\}|\$[A-Za-z_]/.test(expanded)) return null;
|
||||
return path.isAbsolute(expanded) ? expanded : path.join(root, expanded);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,9 +291,11 @@ export function checkHookInstallation({ projectRoot, repoRoot, providerId }) {
|
||||
installedAt = toRelative(manifestPath, projectRoot || root);
|
||||
|
||||
const broken = commands.filter((command) => {
|
||||
const scriptPath = hookScriptPathFrom(command);
|
||||
if (!scriptPath) return false;
|
||||
const abs = path.isAbsolute(scriptPath) ? scriptPath : path.join(root, scriptPath);
|
||||
const token = hookScriptTokenFrom(command);
|
||||
if (!token) return false;
|
||||
const abs = resolveHookScriptPath(token, root);
|
||||
// Unresolvable placeholder or command substitution: never assert missing.
|
||||
if (!abs) return false;
|
||||
return !fs.existsSync(abs);
|
||||
});
|
||||
if (broken.length) {
|
||||
|
||||
@@ -215,12 +215,57 @@ function collectHookCommands(value, out = []) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Pull the script path out of a hook command line. Commands look like
|
||||
// `node .claude/skills/impeccable/scripts/hook.mjs` and may be quoted or carry
|
||||
// trailing arguments.
|
||||
function hookScriptPathFrom(command) {
|
||||
const match = String(command).match(/(\S*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return match ? match[1].replace(/^['"]|['"]$/g, '') : null;
|
||||
const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
|
||||
// Pull the script-path token out of a hook command line, placeholders intact.
|
||||
// The forms our manifests ship:
|
||||
// * bare: node "${CLAUDE_PROJECT_DIR}/.../hook.mjs"
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
// delimited token that ends at the marker, so we don't absorb `node`, `[`, `!`
|
||||
// or `||`. Returns the token verbatim; resolution happens separately.
|
||||
function hookScriptTokenFrom(command) {
|
||||
const str = String(command);
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
// Resolve a script token to an absolute path the doctor can existsSync, or null
|
||||
// when the doctor cannot know where it points — in which case the caller must
|
||||
// NOT report it missing (a doctor never asserts a negative it cannot verify).
|
||||
//
|
||||
// Per-placeholder policy, mirroring what each runtime actually expands:
|
||||
// ${CLAUDE_PROJECT_DIR} → the project root being scanned. This is exactly the
|
||||
// runtime mapping (Claude Code sets it to the project
|
||||
// dir at hook time), so we EXPAND it against `root`.
|
||||
// Not doing so was the #402 bug: the literal
|
||||
// `${CLAUDE_PROJECT_DIR}/...` string never exists.
|
||||
// ${CLAUDE_PLUGIN_ROOT} → plugin-package install dir, set by the harness to
|
||||
// ${PLUGIN_ROOT} wherever the plugin/codex/grok bundle was unpacked
|
||||
// ${GROK_PLUGIN_ROOT} (grok aliases CLAUDE_PLUGIN_ROOT). The doctor has no
|
||||
// way to know that location → SKIP (return null).
|
||||
// $(...) / backticks → command substitution, e.g. GitHub's
|
||||
// `$(git rev-parse --show-toplevel)`. Not statically
|
||||
// resolvable → SKIP.
|
||||
// any other ${VAR}/$VAR → unknown to the doctor → SKIP.
|
||||
// A token with no placeholder is a literal path: absolute as-is, else relative
|
||||
// to `root`.
|
||||
function resolveHookScriptPath(token, root) {
|
||||
if (!token) return null;
|
||||
// Command substitution or backtick expansion we can't evaluate.
|
||||
if (token.includes('$(') || token.includes('`')) return null;
|
||||
const expanded = token.replace(/\$\{CLAUDE_PROJECT_DIR\}/g, root);
|
||||
// Any placeholder or shell variable still present is one we can't map.
|
||||
if (/\$\{[^}]*\}|\$[A-Za-z_]/.test(expanded)) return null;
|
||||
return path.isAbsolute(expanded) ? expanded : path.join(root, expanded);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,9 +291,11 @@ export function checkHookInstallation({ projectRoot, repoRoot, providerId }) {
|
||||
installedAt = toRelative(manifestPath, projectRoot || root);
|
||||
|
||||
const broken = commands.filter((command) => {
|
||||
const scriptPath = hookScriptPathFrom(command);
|
||||
if (!scriptPath) return false;
|
||||
const abs = path.isAbsolute(scriptPath) ? scriptPath : path.join(root, scriptPath);
|
||||
const token = hookScriptTokenFrom(command);
|
||||
if (!token) return false;
|
||||
const abs = resolveHookScriptPath(token, root);
|
||||
// Unresolvable placeholder or command substitution: never assert missing.
|
||||
if (!abs) return false;
|
||||
return !fs.existsSync(abs);
|
||||
});
|
||||
if (broken.length) {
|
||||
|
||||
@@ -215,12 +215,57 @@ function collectHookCommands(value, out = []) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Pull the script path out of a hook command line. Commands look like
|
||||
// `node .claude/skills/impeccable/scripts/hook.mjs` and may be quoted or carry
|
||||
// trailing arguments.
|
||||
function hookScriptPathFrom(command) {
|
||||
const match = String(command).match(/(\S*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return match ? match[1].replace(/^['"]|['"]$/g, '') : null;
|
||||
const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
|
||||
// Pull the script-path token out of a hook command line, placeholders intact.
|
||||
// The forms our manifests ship:
|
||||
// * bare: node "${CLAUDE_PROJECT_DIR}/.../hook.mjs"
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
// delimited token that ends at the marker, so we don't absorb `node`, `[`, `!`
|
||||
// or `||`. Returns the token verbatim; resolution happens separately.
|
||||
function hookScriptTokenFrom(command) {
|
||||
const str = String(command);
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
// Resolve a script token to an absolute path the doctor can existsSync, or null
|
||||
// when the doctor cannot know where it points — in which case the caller must
|
||||
// NOT report it missing (a doctor never asserts a negative it cannot verify).
|
||||
//
|
||||
// Per-placeholder policy, mirroring what each runtime actually expands:
|
||||
// ${CLAUDE_PROJECT_DIR} → the project root being scanned. This is exactly the
|
||||
// runtime mapping (Claude Code sets it to the project
|
||||
// dir at hook time), so we EXPAND it against `root`.
|
||||
// Not doing so was the #402 bug: the literal
|
||||
// `${CLAUDE_PROJECT_DIR}/...` string never exists.
|
||||
// ${CLAUDE_PLUGIN_ROOT} → plugin-package install dir, set by the harness to
|
||||
// ${PLUGIN_ROOT} wherever the plugin/codex/grok bundle was unpacked
|
||||
// ${GROK_PLUGIN_ROOT} (grok aliases CLAUDE_PLUGIN_ROOT). The doctor has no
|
||||
// way to know that location → SKIP (return null).
|
||||
// $(...) / backticks → command substitution, e.g. GitHub's
|
||||
// `$(git rev-parse --show-toplevel)`. Not statically
|
||||
// resolvable → SKIP.
|
||||
// any other ${VAR}/$VAR → unknown to the doctor → SKIP.
|
||||
// A token with no placeholder is a literal path: absolute as-is, else relative
|
||||
// to `root`.
|
||||
function resolveHookScriptPath(token, root) {
|
||||
if (!token) return null;
|
||||
// Command substitution or backtick expansion we can't evaluate.
|
||||
if (token.includes('$(') || token.includes('`')) return null;
|
||||
const expanded = token.replace(/\$\{CLAUDE_PROJECT_DIR\}/g, root);
|
||||
// Any placeholder or shell variable still present is one we can't map.
|
||||
if (/\$\{[^}]*\}|\$[A-Za-z_]/.test(expanded)) return null;
|
||||
return path.isAbsolute(expanded) ? expanded : path.join(root, expanded);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,9 +291,11 @@ export function checkHookInstallation({ projectRoot, repoRoot, providerId }) {
|
||||
installedAt = toRelative(manifestPath, projectRoot || root);
|
||||
|
||||
const broken = commands.filter((command) => {
|
||||
const scriptPath = hookScriptPathFrom(command);
|
||||
if (!scriptPath) return false;
|
||||
const abs = path.isAbsolute(scriptPath) ? scriptPath : path.join(root, scriptPath);
|
||||
const token = hookScriptTokenFrom(command);
|
||||
if (!token) return false;
|
||||
const abs = resolveHookScriptPath(token, root);
|
||||
// Unresolvable placeholder or command substitution: never assert missing.
|
||||
if (!abs) return false;
|
||||
return !fs.existsSync(abs);
|
||||
});
|
||||
if (broken.length) {
|
||||
|
||||
@@ -215,12 +215,57 @@ function collectHookCommands(value, out = []) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Pull the script path out of a hook command line. Commands look like
|
||||
// `node .claude/skills/impeccable/scripts/hook.mjs` and may be quoted or carry
|
||||
// trailing arguments.
|
||||
function hookScriptPathFrom(command) {
|
||||
const match = String(command).match(/(\S*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return match ? match[1].replace(/^['"]|['"]$/g, '') : null;
|
||||
const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
|
||||
// Pull the script-path token out of a hook command line, placeholders intact.
|
||||
// The forms our manifests ship:
|
||||
// * bare: node "${CLAUDE_PROJECT_DIR}/.../hook.mjs"
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
// delimited token that ends at the marker, so we don't absorb `node`, `[`, `!`
|
||||
// or `||`. Returns the token verbatim; resolution happens separately.
|
||||
function hookScriptTokenFrom(command) {
|
||||
const str = String(command);
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
// Resolve a script token to an absolute path the doctor can existsSync, or null
|
||||
// when the doctor cannot know where it points — in which case the caller must
|
||||
// NOT report it missing (a doctor never asserts a negative it cannot verify).
|
||||
//
|
||||
// Per-placeholder policy, mirroring what each runtime actually expands:
|
||||
// ${CLAUDE_PROJECT_DIR} → the project root being scanned. This is exactly the
|
||||
// runtime mapping (Claude Code sets it to the project
|
||||
// dir at hook time), so we EXPAND it against `root`.
|
||||
// Not doing so was the #402 bug: the literal
|
||||
// `${CLAUDE_PROJECT_DIR}/...` string never exists.
|
||||
// ${CLAUDE_PLUGIN_ROOT} → plugin-package install dir, set by the harness to
|
||||
// ${PLUGIN_ROOT} wherever the plugin/codex/grok bundle was unpacked
|
||||
// ${GROK_PLUGIN_ROOT} (grok aliases CLAUDE_PLUGIN_ROOT). The doctor has no
|
||||
// way to know that location → SKIP (return null).
|
||||
// $(...) / backticks → command substitution, e.g. GitHub's
|
||||
// `$(git rev-parse --show-toplevel)`. Not statically
|
||||
// resolvable → SKIP.
|
||||
// any other ${VAR}/$VAR → unknown to the doctor → SKIP.
|
||||
// A token with no placeholder is a literal path: absolute as-is, else relative
|
||||
// to `root`.
|
||||
function resolveHookScriptPath(token, root) {
|
||||
if (!token) return null;
|
||||
// Command substitution or backtick expansion we can't evaluate.
|
||||
if (token.includes('$(') || token.includes('`')) return null;
|
||||
const expanded = token.replace(/\$\{CLAUDE_PROJECT_DIR\}/g, root);
|
||||
// Any placeholder or shell variable still present is one we can't map.
|
||||
if (/\$\{[^}]*\}|\$[A-Za-z_]/.test(expanded)) return null;
|
||||
return path.isAbsolute(expanded) ? expanded : path.join(root, expanded);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,9 +291,11 @@ export function checkHookInstallation({ projectRoot, repoRoot, providerId }) {
|
||||
installedAt = toRelative(manifestPath, projectRoot || root);
|
||||
|
||||
const broken = commands.filter((command) => {
|
||||
const scriptPath = hookScriptPathFrom(command);
|
||||
if (!scriptPath) return false;
|
||||
const abs = path.isAbsolute(scriptPath) ? scriptPath : path.join(root, scriptPath);
|
||||
const token = hookScriptTokenFrom(command);
|
||||
if (!token) return false;
|
||||
const abs = resolveHookScriptPath(token, root);
|
||||
// Unresolvable placeholder or command substitution: never assert missing.
|
||||
if (!abs) return false;
|
||||
return !fs.existsSync(abs);
|
||||
});
|
||||
if (broken.length) {
|
||||
|
||||
@@ -215,12 +215,57 @@ function collectHookCommands(value, out = []) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Pull the script path out of a hook command line. Commands look like
|
||||
// `node .claude/skills/impeccable/scripts/hook.mjs` and may be quoted or carry
|
||||
// trailing arguments.
|
||||
function hookScriptPathFrom(command) {
|
||||
const match = String(command).match(/(\S*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return match ? match[1].replace(/^['"]|['"]$/g, '') : null;
|
||||
const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
|
||||
// Pull the script-path token out of a hook command line, placeholders intact.
|
||||
// The forms our manifests ship:
|
||||
// * bare: node "${CLAUDE_PROJECT_DIR}/.../hook.mjs"
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
// delimited token that ends at the marker, so we don't absorb `node`, `[`, `!`
|
||||
// or `||`. Returns the token verbatim; resolution happens separately.
|
||||
function hookScriptTokenFrom(command) {
|
||||
const str = String(command);
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
// Resolve a script token to an absolute path the doctor can existsSync, or null
|
||||
// when the doctor cannot know where it points — in which case the caller must
|
||||
// NOT report it missing (a doctor never asserts a negative it cannot verify).
|
||||
//
|
||||
// Per-placeholder policy, mirroring what each runtime actually expands:
|
||||
// ${CLAUDE_PROJECT_DIR} → the project root being scanned. This is exactly the
|
||||
// runtime mapping (Claude Code sets it to the project
|
||||
// dir at hook time), so we EXPAND it against `root`.
|
||||
// Not doing so was the #402 bug: the literal
|
||||
// `${CLAUDE_PROJECT_DIR}/...` string never exists.
|
||||
// ${CLAUDE_PLUGIN_ROOT} → plugin-package install dir, set by the harness to
|
||||
// ${PLUGIN_ROOT} wherever the plugin/codex/grok bundle was unpacked
|
||||
// ${GROK_PLUGIN_ROOT} (grok aliases CLAUDE_PLUGIN_ROOT). The doctor has no
|
||||
// way to know that location → SKIP (return null).
|
||||
// $(...) / backticks → command substitution, e.g. GitHub's
|
||||
// `$(git rev-parse --show-toplevel)`. Not statically
|
||||
// resolvable → SKIP.
|
||||
// any other ${VAR}/$VAR → unknown to the doctor → SKIP.
|
||||
// A token with no placeholder is a literal path: absolute as-is, else relative
|
||||
// to `root`.
|
||||
function resolveHookScriptPath(token, root) {
|
||||
if (!token) return null;
|
||||
// Command substitution or backtick expansion we can't evaluate.
|
||||
if (token.includes('$(') || token.includes('`')) return null;
|
||||
const expanded = token.replace(/\$\{CLAUDE_PROJECT_DIR\}/g, root);
|
||||
// Any placeholder or shell variable still present is one we can't map.
|
||||
if (/\$\{[^}]*\}|\$[A-Za-z_]/.test(expanded)) return null;
|
||||
return path.isAbsolute(expanded) ? expanded : path.join(root, expanded);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,9 +291,11 @@ export function checkHookInstallation({ projectRoot, repoRoot, providerId }) {
|
||||
installedAt = toRelative(manifestPath, projectRoot || root);
|
||||
|
||||
const broken = commands.filter((command) => {
|
||||
const scriptPath = hookScriptPathFrom(command);
|
||||
if (!scriptPath) return false;
|
||||
const abs = path.isAbsolute(scriptPath) ? scriptPath : path.join(root, scriptPath);
|
||||
const token = hookScriptTokenFrom(command);
|
||||
if (!token) return false;
|
||||
const abs = resolveHookScriptPath(token, root);
|
||||
// Unresolvable placeholder or command substitution: never assert missing.
|
||||
if (!abs) return false;
|
||||
return !fs.existsSync(abs);
|
||||
});
|
||||
if (broken.length) {
|
||||
|
||||
@@ -215,12 +215,57 @@ function collectHookCommands(value, out = []) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Pull the script path out of a hook command line. Commands look like
|
||||
// `node .claude/skills/impeccable/scripts/hook.mjs` and may be quoted or carry
|
||||
// trailing arguments.
|
||||
function hookScriptPathFrom(command) {
|
||||
const match = String(command).match(/(\S*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return match ? match[1].replace(/^['"]|['"]$/g, '') : null;
|
||||
const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
|
||||
// Pull the script-path token out of a hook command line, placeholders intact.
|
||||
// The forms our manifests ship:
|
||||
// * bare: node "${CLAUDE_PROJECT_DIR}/.../hook.mjs"
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
// delimited token that ends at the marker, so we don't absorb `node`, `[`, `!`
|
||||
// or `||`. Returns the token verbatim; resolution happens separately.
|
||||
function hookScriptTokenFrom(command) {
|
||||
const str = String(command);
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
// Resolve a script token to an absolute path the doctor can existsSync, or null
|
||||
// when the doctor cannot know where it points — in which case the caller must
|
||||
// NOT report it missing (a doctor never asserts a negative it cannot verify).
|
||||
//
|
||||
// Per-placeholder policy, mirroring what each runtime actually expands:
|
||||
// ${CLAUDE_PROJECT_DIR} → the project root being scanned. This is exactly the
|
||||
// runtime mapping (Claude Code sets it to the project
|
||||
// dir at hook time), so we EXPAND it against `root`.
|
||||
// Not doing so was the #402 bug: the literal
|
||||
// `${CLAUDE_PROJECT_DIR}/...` string never exists.
|
||||
// ${CLAUDE_PLUGIN_ROOT} → plugin-package install dir, set by the harness to
|
||||
// ${PLUGIN_ROOT} wherever the plugin/codex/grok bundle was unpacked
|
||||
// ${GROK_PLUGIN_ROOT} (grok aliases CLAUDE_PLUGIN_ROOT). The doctor has no
|
||||
// way to know that location → SKIP (return null).
|
||||
// $(...) / backticks → command substitution, e.g. GitHub's
|
||||
// `$(git rev-parse --show-toplevel)`. Not statically
|
||||
// resolvable → SKIP.
|
||||
// any other ${VAR}/$VAR → unknown to the doctor → SKIP.
|
||||
// A token with no placeholder is a literal path: absolute as-is, else relative
|
||||
// to `root`.
|
||||
function resolveHookScriptPath(token, root) {
|
||||
if (!token) return null;
|
||||
// Command substitution or backtick expansion we can't evaluate.
|
||||
if (token.includes('$(') || token.includes('`')) return null;
|
||||
const expanded = token.replace(/\$\{CLAUDE_PROJECT_DIR\}/g, root);
|
||||
// Any placeholder or shell variable still present is one we can't map.
|
||||
if (/\$\{[^}]*\}|\$[A-Za-z_]/.test(expanded)) return null;
|
||||
return path.isAbsolute(expanded) ? expanded : path.join(root, expanded);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,9 +291,11 @@ export function checkHookInstallation({ projectRoot, repoRoot, providerId }) {
|
||||
installedAt = toRelative(manifestPath, projectRoot || root);
|
||||
|
||||
const broken = commands.filter((command) => {
|
||||
const scriptPath = hookScriptPathFrom(command);
|
||||
if (!scriptPath) return false;
|
||||
const abs = path.isAbsolute(scriptPath) ? scriptPath : path.join(root, scriptPath);
|
||||
const token = hookScriptTokenFrom(command);
|
||||
if (!token) return false;
|
||||
const abs = resolveHookScriptPath(token, root);
|
||||
// Unresolvable placeholder or command substitution: never assert missing.
|
||||
if (!abs) return false;
|
||||
return !fs.existsSync(abs);
|
||||
});
|
||||
if (broken.length) {
|
||||
|
||||
@@ -215,12 +215,57 @@ function collectHookCommands(value, out = []) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Pull the script path out of a hook command line. Commands look like
|
||||
// `node .claude/skills/impeccable/scripts/hook.mjs` and may be quoted or carry
|
||||
// trailing arguments.
|
||||
function hookScriptPathFrom(command) {
|
||||
const match = String(command).match(/(\S*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return match ? match[1].replace(/^['"]|['"]$/g, '') : null;
|
||||
const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
|
||||
// Pull the script-path token out of a hook command line, placeholders intact.
|
||||
// The forms our manifests ship:
|
||||
// * bare: node "${CLAUDE_PROJECT_DIR}/.../hook.mjs"
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
// delimited token that ends at the marker, so we don't absorb `node`, `[`, `!`
|
||||
// or `||`. Returns the token verbatim; resolution happens separately.
|
||||
function hookScriptTokenFrom(command) {
|
||||
const str = String(command);
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
// Resolve a script token to an absolute path the doctor can existsSync, or null
|
||||
// when the doctor cannot know where it points — in which case the caller must
|
||||
// NOT report it missing (a doctor never asserts a negative it cannot verify).
|
||||
//
|
||||
// Per-placeholder policy, mirroring what each runtime actually expands:
|
||||
// ${CLAUDE_PROJECT_DIR} → the project root being scanned. This is exactly the
|
||||
// runtime mapping (Claude Code sets it to the project
|
||||
// dir at hook time), so we EXPAND it against `root`.
|
||||
// Not doing so was the #402 bug: the literal
|
||||
// `${CLAUDE_PROJECT_DIR}/...` string never exists.
|
||||
// ${CLAUDE_PLUGIN_ROOT} → plugin-package install dir, set by the harness to
|
||||
// ${PLUGIN_ROOT} wherever the plugin/codex/grok bundle was unpacked
|
||||
// ${GROK_PLUGIN_ROOT} (grok aliases CLAUDE_PLUGIN_ROOT). The doctor has no
|
||||
// way to know that location → SKIP (return null).
|
||||
// $(...) / backticks → command substitution, e.g. GitHub's
|
||||
// `$(git rev-parse --show-toplevel)`. Not statically
|
||||
// resolvable → SKIP.
|
||||
// any other ${VAR}/$VAR → unknown to the doctor → SKIP.
|
||||
// A token with no placeholder is a literal path: absolute as-is, else relative
|
||||
// to `root`.
|
||||
function resolveHookScriptPath(token, root) {
|
||||
if (!token) return null;
|
||||
// Command substitution or backtick expansion we can't evaluate.
|
||||
if (token.includes('$(') || token.includes('`')) return null;
|
||||
const expanded = token.replace(/\$\{CLAUDE_PROJECT_DIR\}/g, root);
|
||||
// Any placeholder or shell variable still present is one we can't map.
|
||||
if (/\$\{[^}]*\}|\$[A-Za-z_]/.test(expanded)) return null;
|
||||
return path.isAbsolute(expanded) ? expanded : path.join(root, expanded);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,9 +291,11 @@ export function checkHookInstallation({ projectRoot, repoRoot, providerId }) {
|
||||
installedAt = toRelative(manifestPath, projectRoot || root);
|
||||
|
||||
const broken = commands.filter((command) => {
|
||||
const scriptPath = hookScriptPathFrom(command);
|
||||
if (!scriptPath) return false;
|
||||
const abs = path.isAbsolute(scriptPath) ? scriptPath : path.join(root, scriptPath);
|
||||
const token = hookScriptTokenFrom(command);
|
||||
if (!token) return false;
|
||||
const abs = resolveHookScriptPath(token, root);
|
||||
// Unresolvable placeholder or command substitution: never assert missing.
|
||||
if (!abs) return false;
|
||||
return !fs.existsSync(abs);
|
||||
});
|
||||
if (broken.length) {
|
||||
|
||||
@@ -215,12 +215,57 @@ function collectHookCommands(value, out = []) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Pull the script path out of a hook command line. Commands look like
|
||||
// `node .claude/skills/impeccable/scripts/hook.mjs` and may be quoted or carry
|
||||
// trailing arguments.
|
||||
function hookScriptPathFrom(command) {
|
||||
const match = String(command).match(/(\S*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return match ? match[1].replace(/^['"]|['"]$/g, '') : null;
|
||||
const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
|
||||
// Pull the script-path token out of a hook command line, placeholders intact.
|
||||
// The forms our manifests ship:
|
||||
// * bare: node "${CLAUDE_PROJECT_DIR}/.../hook.mjs"
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
// delimited token that ends at the marker, so we don't absorb `node`, `[`, `!`
|
||||
// or `||`. Returns the token verbatim; resolution happens separately.
|
||||
function hookScriptTokenFrom(command) {
|
||||
const str = String(command);
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
// Resolve a script token to an absolute path the doctor can existsSync, or null
|
||||
// when the doctor cannot know where it points — in which case the caller must
|
||||
// NOT report it missing (a doctor never asserts a negative it cannot verify).
|
||||
//
|
||||
// Per-placeholder policy, mirroring what each runtime actually expands:
|
||||
// ${CLAUDE_PROJECT_DIR} → the project root being scanned. This is exactly the
|
||||
// runtime mapping (Claude Code sets it to the project
|
||||
// dir at hook time), so we EXPAND it against `root`.
|
||||
// Not doing so was the #402 bug: the literal
|
||||
// `${CLAUDE_PROJECT_DIR}/...` string never exists.
|
||||
// ${CLAUDE_PLUGIN_ROOT} → plugin-package install dir, set by the harness to
|
||||
// ${PLUGIN_ROOT} wherever the plugin/codex/grok bundle was unpacked
|
||||
// ${GROK_PLUGIN_ROOT} (grok aliases CLAUDE_PLUGIN_ROOT). The doctor has no
|
||||
// way to know that location → SKIP (return null).
|
||||
// $(...) / backticks → command substitution, e.g. GitHub's
|
||||
// `$(git rev-parse --show-toplevel)`. Not statically
|
||||
// resolvable → SKIP.
|
||||
// any other ${VAR}/$VAR → unknown to the doctor → SKIP.
|
||||
// A token with no placeholder is a literal path: absolute as-is, else relative
|
||||
// to `root`.
|
||||
function resolveHookScriptPath(token, root) {
|
||||
if (!token) return null;
|
||||
// Command substitution or backtick expansion we can't evaluate.
|
||||
if (token.includes('$(') || token.includes('`')) return null;
|
||||
const expanded = token.replace(/\$\{CLAUDE_PROJECT_DIR\}/g, root);
|
||||
// Any placeholder or shell variable still present is one we can't map.
|
||||
if (/\$\{[^}]*\}|\$[A-Za-z_]/.test(expanded)) return null;
|
||||
return path.isAbsolute(expanded) ? expanded : path.join(root, expanded);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,9 +291,11 @@ export function checkHookInstallation({ projectRoot, repoRoot, providerId }) {
|
||||
installedAt = toRelative(manifestPath, projectRoot || root);
|
||||
|
||||
const broken = commands.filter((command) => {
|
||||
const scriptPath = hookScriptPathFrom(command);
|
||||
if (!scriptPath) return false;
|
||||
const abs = path.isAbsolute(scriptPath) ? scriptPath : path.join(root, scriptPath);
|
||||
const token = hookScriptTokenFrom(command);
|
||||
if (!token) return false;
|
||||
const abs = resolveHookScriptPath(token, root);
|
||||
// Unresolvable placeholder or command substitution: never assert missing.
|
||||
if (!abs) return false;
|
||||
return !fs.existsSync(abs);
|
||||
});
|
||||
if (broken.length) {
|
||||
|
||||
@@ -215,12 +215,57 @@ function collectHookCommands(value, out = []) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Pull the script path out of a hook command line. Commands look like
|
||||
// `node .claude/skills/impeccable/scripts/hook.mjs` and may be quoted or carry
|
||||
// trailing arguments.
|
||||
function hookScriptPathFrom(command) {
|
||||
const match = String(command).match(/(\S*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return match ? match[1].replace(/^['"]|['"]$/g, '') : null;
|
||||
const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
|
||||
// Pull the script-path token out of a hook command line, placeholders intact.
|
||||
// The forms our manifests ship:
|
||||
// * bare: node "${CLAUDE_PROJECT_DIR}/.../hook.mjs"
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
// delimited token that ends at the marker, so we don't absorb `node`, `[`, `!`
|
||||
// or `||`. Returns the token verbatim; resolution happens separately.
|
||||
function hookScriptTokenFrom(command) {
|
||||
const str = String(command);
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
// Resolve a script token to an absolute path the doctor can existsSync, or null
|
||||
// when the doctor cannot know where it points — in which case the caller must
|
||||
// NOT report it missing (a doctor never asserts a negative it cannot verify).
|
||||
//
|
||||
// Per-placeholder policy, mirroring what each runtime actually expands:
|
||||
// ${CLAUDE_PROJECT_DIR} → the project root being scanned. This is exactly the
|
||||
// runtime mapping (Claude Code sets it to the project
|
||||
// dir at hook time), so we EXPAND it against `root`.
|
||||
// Not doing so was the #402 bug: the literal
|
||||
// `${CLAUDE_PROJECT_DIR}/...` string never exists.
|
||||
// ${CLAUDE_PLUGIN_ROOT} → plugin-package install dir, set by the harness to
|
||||
// ${PLUGIN_ROOT} wherever the plugin/codex/grok bundle was unpacked
|
||||
// ${GROK_PLUGIN_ROOT} (grok aliases CLAUDE_PLUGIN_ROOT). The doctor has no
|
||||
// way to know that location → SKIP (return null).
|
||||
// $(...) / backticks → command substitution, e.g. GitHub's
|
||||
// `$(git rev-parse --show-toplevel)`. Not statically
|
||||
// resolvable → SKIP.
|
||||
// any other ${VAR}/$VAR → unknown to the doctor → SKIP.
|
||||
// A token with no placeholder is a literal path: absolute as-is, else relative
|
||||
// to `root`.
|
||||
function resolveHookScriptPath(token, root) {
|
||||
if (!token) return null;
|
||||
// Command substitution or backtick expansion we can't evaluate.
|
||||
if (token.includes('$(') || token.includes('`')) return null;
|
||||
const expanded = token.replace(/\$\{CLAUDE_PROJECT_DIR\}/g, root);
|
||||
// Any placeholder or shell variable still present is one we can't map.
|
||||
if (/\$\{[^}]*\}|\$[A-Za-z_]/.test(expanded)) return null;
|
||||
return path.isAbsolute(expanded) ? expanded : path.join(root, expanded);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,9 +291,11 @@ export function checkHookInstallation({ projectRoot, repoRoot, providerId }) {
|
||||
installedAt = toRelative(manifestPath, projectRoot || root);
|
||||
|
||||
const broken = commands.filter((command) => {
|
||||
const scriptPath = hookScriptPathFrom(command);
|
||||
if (!scriptPath) return false;
|
||||
const abs = path.isAbsolute(scriptPath) ? scriptPath : path.join(root, scriptPath);
|
||||
const token = hookScriptTokenFrom(command);
|
||||
if (!token) return false;
|
||||
const abs = resolveHookScriptPath(token, root);
|
||||
// Unresolvable placeholder or command substitution: never assert missing.
|
||||
if (!abs) return false;
|
||||
return !fs.existsSync(abs);
|
||||
});
|
||||
if (broken.length) {
|
||||
|
||||
@@ -215,12 +215,57 @@ function collectHookCommands(value, out = []) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Pull the script path out of a hook command line. Commands look like
|
||||
// `node .claude/skills/impeccable/scripts/hook.mjs` and may be quoted or carry
|
||||
// trailing arguments.
|
||||
function hookScriptPathFrom(command) {
|
||||
const match = String(command).match(/(\S*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return match ? match[1].replace(/^['"]|['"]$/g, '') : null;
|
||||
const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
|
||||
// Pull the script-path token out of a hook command line, placeholders intact.
|
||||
// The forms our manifests ship:
|
||||
// * bare: node "${CLAUDE_PROJECT_DIR}/.../hook.mjs"
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
// delimited token that ends at the marker, so we don't absorb `node`, `[`, `!`
|
||||
// or `||`. Returns the token verbatim; resolution happens separately.
|
||||
function hookScriptTokenFrom(command) {
|
||||
const str = String(command);
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
// Resolve a script token to an absolute path the doctor can existsSync, or null
|
||||
// when the doctor cannot know where it points — in which case the caller must
|
||||
// NOT report it missing (a doctor never asserts a negative it cannot verify).
|
||||
//
|
||||
// Per-placeholder policy, mirroring what each runtime actually expands:
|
||||
// ${CLAUDE_PROJECT_DIR} → the project root being scanned. This is exactly the
|
||||
// runtime mapping (Claude Code sets it to the project
|
||||
// dir at hook time), so we EXPAND it against `root`.
|
||||
// Not doing so was the #402 bug: the literal
|
||||
// `${CLAUDE_PROJECT_DIR}/...` string never exists.
|
||||
// ${CLAUDE_PLUGIN_ROOT} → plugin-package install dir, set by the harness to
|
||||
// ${PLUGIN_ROOT} wherever the plugin/codex/grok bundle was unpacked
|
||||
// ${GROK_PLUGIN_ROOT} (grok aliases CLAUDE_PLUGIN_ROOT). The doctor has no
|
||||
// way to know that location → SKIP (return null).
|
||||
// $(...) / backticks → command substitution, e.g. GitHub's
|
||||
// `$(git rev-parse --show-toplevel)`. Not statically
|
||||
// resolvable → SKIP.
|
||||
// any other ${VAR}/$VAR → unknown to the doctor → SKIP.
|
||||
// A token with no placeholder is a literal path: absolute as-is, else relative
|
||||
// to `root`.
|
||||
function resolveHookScriptPath(token, root) {
|
||||
if (!token) return null;
|
||||
// Command substitution or backtick expansion we can't evaluate.
|
||||
if (token.includes('$(') || token.includes('`')) return null;
|
||||
const expanded = token.replace(/\$\{CLAUDE_PROJECT_DIR\}/g, root);
|
||||
// Any placeholder or shell variable still present is one we can't map.
|
||||
if (/\$\{[^}]*\}|\$[A-Za-z_]/.test(expanded)) return null;
|
||||
return path.isAbsolute(expanded) ? expanded : path.join(root, expanded);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,9 +291,11 @@ export function checkHookInstallation({ projectRoot, repoRoot, providerId }) {
|
||||
installedAt = toRelative(manifestPath, projectRoot || root);
|
||||
|
||||
const broken = commands.filter((command) => {
|
||||
const scriptPath = hookScriptPathFrom(command);
|
||||
if (!scriptPath) return false;
|
||||
const abs = path.isAbsolute(scriptPath) ? scriptPath : path.join(root, scriptPath);
|
||||
const token = hookScriptTokenFrom(command);
|
||||
if (!token) return false;
|
||||
const abs = resolveHookScriptPath(token, root);
|
||||
// Unresolvable placeholder or command substitution: never assert missing.
|
||||
if (!abs) return false;
|
||||
return !fs.existsSync(abs);
|
||||
});
|
||||
if (broken.length) {
|
||||
|
||||
@@ -215,12 +215,57 @@ function collectHookCommands(value, out = []) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Pull the script path out of a hook command line. Commands look like
|
||||
// `node .claude/skills/impeccable/scripts/hook.mjs` and may be quoted or carry
|
||||
// trailing arguments.
|
||||
function hookScriptPathFrom(command) {
|
||||
const match = String(command).match(/(\S*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return match ? match[1].replace(/^['"]|['"]$/g, '') : null;
|
||||
const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
|
||||
// Pull the script-path token out of a hook command line, placeholders intact.
|
||||
// The forms our manifests ship:
|
||||
// * bare: node "${CLAUDE_PROJECT_DIR}/.../hook.mjs"
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
// delimited token that ends at the marker, so we don't absorb `node`, `[`, `!`
|
||||
// or `||`. Returns the token verbatim; resolution happens separately.
|
||||
function hookScriptTokenFrom(command) {
|
||||
const str = String(command);
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
// Resolve a script token to an absolute path the doctor can existsSync, or null
|
||||
// when the doctor cannot know where it points — in which case the caller must
|
||||
// NOT report it missing (a doctor never asserts a negative it cannot verify).
|
||||
//
|
||||
// Per-placeholder policy, mirroring what each runtime actually expands:
|
||||
// ${CLAUDE_PROJECT_DIR} → the project root being scanned. This is exactly the
|
||||
// runtime mapping (Claude Code sets it to the project
|
||||
// dir at hook time), so we EXPAND it against `root`.
|
||||
// Not doing so was the #402 bug: the literal
|
||||
// `${CLAUDE_PROJECT_DIR}/...` string never exists.
|
||||
// ${CLAUDE_PLUGIN_ROOT} → plugin-package install dir, set by the harness to
|
||||
// ${PLUGIN_ROOT} wherever the plugin/codex/grok bundle was unpacked
|
||||
// ${GROK_PLUGIN_ROOT} (grok aliases CLAUDE_PLUGIN_ROOT). The doctor has no
|
||||
// way to know that location → SKIP (return null).
|
||||
// $(...) / backticks → command substitution, e.g. GitHub's
|
||||
// `$(git rev-parse --show-toplevel)`. Not statically
|
||||
// resolvable → SKIP.
|
||||
// any other ${VAR}/$VAR → unknown to the doctor → SKIP.
|
||||
// A token with no placeholder is a literal path: absolute as-is, else relative
|
||||
// to `root`.
|
||||
function resolveHookScriptPath(token, root) {
|
||||
if (!token) return null;
|
||||
// Command substitution or backtick expansion we can't evaluate.
|
||||
if (token.includes('$(') || token.includes('`')) return null;
|
||||
const expanded = token.replace(/\$\{CLAUDE_PROJECT_DIR\}/g, root);
|
||||
// Any placeholder or shell variable still present is one we can't map.
|
||||
if (/\$\{[^}]*\}|\$[A-Za-z_]/.test(expanded)) return null;
|
||||
return path.isAbsolute(expanded) ? expanded : path.join(root, expanded);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,9 +291,11 @@ export function checkHookInstallation({ projectRoot, repoRoot, providerId }) {
|
||||
installedAt = toRelative(manifestPath, projectRoot || root);
|
||||
|
||||
const broken = commands.filter((command) => {
|
||||
const scriptPath = hookScriptPathFrom(command);
|
||||
if (!scriptPath) return false;
|
||||
const abs = path.isAbsolute(scriptPath) ? scriptPath : path.join(root, scriptPath);
|
||||
const token = hookScriptTokenFrom(command);
|
||||
if (!token) return false;
|
||||
const abs = resolveHookScriptPath(token, root);
|
||||
// Unresolvable placeholder or command substitution: never assert missing.
|
||||
if (!abs) return false;
|
||||
return !fs.existsSync(abs);
|
||||
});
|
||||
if (broken.length) {
|
||||
|
||||
Reference in New Issue
Block a user