* fix(#976): detect command+args (wrapped) hook registrations in installer presence checks Add referencesHook() helper that inspects both h.command (standard form) and h.args[] (args-form / wrapped-launcher form) when checking whether a managed hook is already registered. Rewrite all has*Hook predicates and the alreadyHas* guards to use it so args-form registrations suppress the duplicate stock string-command entry that was previously appended on every install/update. Also add an explicit args-form skip to rewriteLegacyManagedNodeHookCommands so entries with a non-empty args[] are left untouched (they are intentional user wrappers, not legacy bare-node commands to migrate). Extend isManagedHookCommand() in shell-command-projection.cts with an optional args: unknown[] parameter that checks whether any arg's basename matches the managed hook surface set — backward compatible; existing callers are unaffected. Regression test added to tests/install-regressions.test.cjs: - two-pass install with an args-form SessionStart entry pre-written to settings.local.json asserts exactly 1 hook entry remains after reinstall (previously 2 — the original args-form + a new stock string-command duplicate) - rewriteLegacyManagedNodeHookCommands test asserts args-form entries unchanged Closes #976 * chore(#976): backfill changeset pr number (994) --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
@@ -219,12 +219,25 @@ function managedHookCommandSurfaceSet(surface: string = 'settings-json', include
|
||||
return new Set([...base, ...aliases]);
|
||||
}
|
||||
|
||||
export function isManagedHookCommand(commandText: unknown, opts: { surface?: string; includeLegacyAliases?: boolean; configDir?: string } = {}): boolean {
|
||||
export function isManagedHookCommand(commandText: unknown, opts: { surface?: string; includeLegacyAliases?: boolean; configDir?: string; args?: unknown[] } = {}): boolean {
|
||||
if (typeof commandText !== 'string') return false;
|
||||
const surface = opts.surface || 'settings-json';
|
||||
const includeLegacyAliases = opts.includeLegacyAliases === true;
|
||||
const managedBasenames = managedHookCommandSurfaceSet(surface, includeLegacyAliases);
|
||||
if (!managedBasenames || managedBasenames.size === 0) return false;
|
||||
|
||||
// args-form check: the managed hook filename may appear in args[] rather than
|
||||
// in command when a windowless launcher wraps the Node invocation. (#976)
|
||||
// Only treat as managed when an arg basename matches the managed hook set —
|
||||
// prevents false-positives for non-GSD entries that happen to share a path segment.
|
||||
if (Array.isArray(opts.args) && opts.args.length > 0) {
|
||||
for (const arg of opts.args) {
|
||||
if (typeof arg !== 'string') continue;
|
||||
const argBasename = arg.replace(/\\/g, '/').split('/').pop() || '';
|
||||
if (isManagedHookBasename(argBasename, { surface })) return true;
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedCommand = commandText.replace(/\\/g, '/');
|
||||
|
||||
if (typeof opts.configDir === 'string' && opts.configDir.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user