@tanstack/intent 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/{artifact-coverage-DFtI6V_H.mjs → artifact-coverage-DerRKsWw.mjs} +1 -1
- package/dist/artifact-coverage-Dia0ZRPy.mjs +2 -0
- package/dist/{cli-error-DDAO6DIL.mjs → cli-error-BebkXaTJ.mjs} +1 -1
- package/dist/cli.mjs +17 -12
- package/dist/command-BBzoalDz.mjs +388 -0
- package/dist/{install-CTGQvXoB.mjs → command-DFQVnfAz.mjs} +85 -55
- package/dist/{command-runner-B5OofX0E.mjs → command-runner-BFvtjLMh.mjs} +1 -1
- package/dist/{core-BRUBEMwe.mjs → core-BNH_SWxi.mjs} +11 -11
- package/dist/core.d.mts +2 -2
- package/dist/core.mjs +1 -1
- package/dist/{display-CnpA7XuV.mjs → display-Dc1feMcZ.mjs} +2 -2
- package/dist/{edit-package-json-D8xfcy2X.mjs → edit-package-json-CdnA7_TA.mjs} +2 -2
- package/dist/{exclude-DbHwcgQQ.mjs → exclude-IfSv-XI_.mjs} +2 -2
- package/dist/{excludes-ByvSbmmj.mjs → excludes-BEi9N7Ys.mjs} +1 -1
- package/dist/{setup-github-actions-IxZTZihi.mjs → github-actions-D41VZqWh.mjs} +2 -2
- package/dist/index.d.mts +9 -9
- package/dist/index.mjs +8 -8
- package/dist/{list-9SbFGUd5.mjs → list-zVJERMh0.mjs} +4 -4
- package/dist/{load-BY8vh7Gp.mjs → load-BsCRJMxP.mjs} +3 -3
- package/dist/{meta-CF4XIYOo.mjs → meta-Dk7d0N2J.mjs} +2 -2
- package/dist/{package-manager-Dw7lYcI0.mjs → package-manager-C63Zi9q1.mjs} +1 -1
- package/dist/{skill-paths-Bm1P6IYe.mjs → paths-B0KW7rmz.mjs} +2 -2
- package/dist/{project-context-oi_m7paK.mjs → project-context-CALU5-15.mjs} +1 -1
- package/dist/{setup-CdfBc7Oe.d.mts → project-setup-Bvmg5uYy.d.mts} +2 -2
- package/dist/{resolver-Uwx8B5jv.mjs → resolver-6i-WBbh8.mjs} +3 -3
- package/dist/{scanner-qT_M6nV5.mjs → scanner-B1pcLFee.mjs} +82 -68
- package/dist/{setup-Cx1r2y-1.mjs → setup-D5qLjoqf.mjs} +16 -9
- package/dist/setup-DFajGERl.mjs +3 -0
- package/dist/setup.d.mts +1 -1
- package/dist/setup.mjs +2 -2
- package/dist/source-policy-BDNiixOv.mjs +2 -0
- package/dist/{source-policy-DkR80hkL.mjs → source-policy-CXjjpHNc.mjs} +3 -3
- package/dist/{stale-DhjSTIt-.mjs → stale-DlNJHwga.mjs} +2 -2
- package/dist/{staleness-DoZU3lzy.mjs → staleness-I_jAT1Ge.mjs} +3 -3
- package/dist/{staleness-B5Cqe77_.mjs → staleness-bEZ8BeGq.mjs} +1 -1
- package/dist/{cli-support-DK1Kq8Ue.mjs → support-DKP_LLRd.mjs} +1 -1
- package/dist/{cli-support-BQSl7gAE.mjs → support-XEVbBenU.mjs} +9 -9
- package/dist/{types-P6UfPVdp.d.mts → types-Bx6-umBo.d.mts} +3 -2
- package/dist/{skill-use-B2xRF1i9.mjs → use-plp2M918.mjs} +1 -1
- package/dist/{utils-BKBDYbCx.mjs → utils-BpmAIjiN.mjs} +1 -1
- package/dist/{utils-6FtqhOYf.mjs → utils-Bw7HwOo5.mjs} +1 -1
- package/dist/{validate-BSfTOq0v.mjs → validate-D5bt8Q0r.mjs} +5 -5
- package/dist/{workflow-review-Bo2kPVXV.mjs → workflow-review-Bx8x6_uF.mjs} +1 -1
- package/dist/{workflow-review-B4AfwtHH.mjs → workflow-review-CEwwmDdD.mjs} +1 -1
- package/dist/{workspace-patterns-BDoJIWk-.mjs → workspace-patterns-hW0v_meY.mjs} +2 -2
- package/dist/{workspace-patterns-CrL8hAbd.mjs → workspace-patterns-qoXkCfEX.mjs} +1 -1
- package/package.json +1 -1
- package/dist/artifact-coverage-CXX6wav1.mjs +0 -2
- package/dist/source-policy-hMYcpIgm.mjs +0 -2
package/README.md
CHANGED
|
@@ -122,6 +122,7 @@ The real risk with any derived artifact is staleness. `npx @tanstack/intent@late
|
|
|
122
122
|
| Command | Description |
|
|
123
123
|
| -------------------------------------------------- | --------------------------------------------------- |
|
|
124
124
|
| `npx @tanstack/intent@latest install` | Set up skill loading guidance in agent config files |
|
|
125
|
+
| `npx @tanstack/intent@latest hooks install` | Install hook enforcement for supported agents |
|
|
125
126
|
| `npx @tanstack/intent@latest list [--json]` | Discover local intent-enabled packages |
|
|
126
127
|
| `npx @tanstack/intent@latest load <use>` | Load `<package>#<skill>` SKILL.md content |
|
|
127
128
|
| `npx @tanstack/intent@latest meta` | List meta-skills for library maintainers |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { parse } from "yaml";
|
|
4
|
-
//#region src/artifact-coverage.ts
|
|
4
|
+
//#region src/staleness/artifact-coverage.ts
|
|
5
5
|
function isRecord(value) {
|
|
6
6
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
7
7
|
}
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as isCliFailure, t as fail } from "./cli-error-
|
|
2
|
+
import { n as isCliFailure, t as fail } from "./cli-error-BebkXaTJ.mjs";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { cac } from "cac";
|
|
@@ -8,47 +8,52 @@ function createCli() {
|
|
|
8
8
|
const cli = cac("intent");
|
|
9
9
|
cli.usage("<command> [options]");
|
|
10
10
|
cli.command("list", "Discover intent-enabled packages from the project or workspace").usage("list [--json] [--debug] [--global] [--global-only] [--no-notices]").option("--json", "Output JSON").option("--debug", "Print discovery debug details to stderr").option("--global", "Include global packages after project packages").option("--global-only", "List global packages only").option("--no-notices", "Suppress non-critical notices on stderr").example("list").example("list --json").example("list --global").action(async (options) => {
|
|
11
|
-
const { runListCommand } = await import("./list-
|
|
11
|
+
const { runListCommand } = await import("./list-zVJERMh0.mjs");
|
|
12
12
|
await runListCommand(options);
|
|
13
13
|
});
|
|
14
14
|
cli.command("exclude [action] [pattern]", "Manage package.json intent.exclude entries").usage("exclude [list|add|remove] [pattern] [--json]").option("--json", "Output JSON list of configured exclude patterns").example("exclude").example("exclude list --json").example("exclude add @tanstack/router#experimental-*").example("exclude remove @tanstack/router#experimental-*").action(async (action, pattern, options) => {
|
|
15
|
-
const { runExcludeCommand } = await import("./exclude-
|
|
15
|
+
const { runExcludeCommand } = await import("./exclude-IfSv-XI_.mjs");
|
|
16
16
|
await runExcludeCommand(action, pattern, options);
|
|
17
17
|
});
|
|
18
18
|
cli.command("load [use]", "Load a compact skill use and print its SKILL.md").usage("load <use> [--path] [--json] [--debug] [--global] [--global-only]").option("--path", "Print the resolved skill path instead of file content").option("--json", "Output JSON").option("--debug", "Print resolution debug details to stderr").option("--global", "Load from project packages, then global packages").option("--global-only", "Load from global packages only").example("load @tanstack/query#core").example("load @tanstack/query#core --path").action(async (use, options) => {
|
|
19
|
-
const { runLoadCommand } = await import("./load-
|
|
19
|
+
const { runLoadCommand } = await import("./load-BsCRJMxP.mjs");
|
|
20
20
|
await runLoadCommand(use, options);
|
|
21
21
|
});
|
|
22
22
|
cli.command("meta [name]", "List meta-skills, or print one by name").usage("meta [name]").example("meta").example("meta domain-discovery").action(async (name) => {
|
|
23
|
-
const [{ getMetaDir }, { runMetaCommand }] = await Promise.all([import("./
|
|
23
|
+
const [{ getMetaDir }, { runMetaCommand }] = await Promise.all([import("./support-DKP_LLRd.mjs"), import("./meta-Dk7d0N2J.mjs")]);
|
|
24
24
|
await runMetaCommand(name, getMetaDir());
|
|
25
25
|
});
|
|
26
26
|
cli.command("validate [dir]", "Validate skill files").usage("validate [dir] [--github-summary] [--fix] [--check]").option("--github-summary", "Write a GitHub Actions step summary").option("--fix", "Rewrite fixable SKILL.md frontmatter issues").option("--check", "Fail if fixable SKILL.md frontmatter issues would be rewritten").example("validate").example("validate packages/query/skills").action(async (dir, options) => {
|
|
27
|
-
const { runValidateCommand } = await import("./validate-
|
|
27
|
+
const { runValidateCommand } = await import("./validate-D5bt8Q0r.mjs");
|
|
28
28
|
await runValidateCommand(dir, options);
|
|
29
29
|
});
|
|
30
30
|
cli.command("install", "Create or update skill loading guidance in an agent config file").usage("install [--map] [--dry-run] [--print-prompt] [--global] [--global-only] [--no-notices]").option("--map", "Write explicit skill-to-task mappings").option("--dry-run", "Print the generated block without writing").option("--print-prompt", "Print the legacy agent setup prompt instead of writing").option("--global", "Include global packages after project packages").option("--global-only", "Install mappings from global packages only").option("--no-notices", "Suppress non-critical notices on stderr").example("install").example("install --map").example("install --dry-run").example("install --print-prompt").example("install --global").action(async (options) => {
|
|
31
|
-
const [{ scanIntentsOrFail }, { runInstallCommand }] = await Promise.all([import("./
|
|
31
|
+
const [{ scanIntentsOrFail }, { runInstallCommand }] = await Promise.all([import("./support-DKP_LLRd.mjs"), import("./command-DFQVnfAz.mjs")]);
|
|
32
32
|
await runInstallCommand(options, scanIntentsOrFail);
|
|
33
33
|
});
|
|
34
|
+
cli.command("hooks [action]", "Manage agent hooks that enforce skill loading").usage("hooks install [--scope project|user] [--agents copilot,claude,codex|all]").option("--scope <scope>", "Hook install scope: project or user").option("--agents <agents>", "Hook agents: copilot,claude,codex, or all").example("hooks install").example("hooks install --scope user --agents copilot").action(async (action, options) => {
|
|
35
|
+
if (action !== "install") fail("Unknown hooks action: expected install.");
|
|
36
|
+
const { runHooksInstallCommand } = await import("./command-BBzoalDz.mjs");
|
|
37
|
+
runHooksInstallCommand(options);
|
|
38
|
+
});
|
|
34
39
|
cli.command("scaffold", "Print maintainer scaffold prompt").usage("scaffold").action(async () => {
|
|
35
|
-
const [{ getMetaDir }, { runScaffoldCommand }] = await Promise.all([import("./
|
|
40
|
+
const [{ getMetaDir }, { runScaffoldCommand }] = await Promise.all([import("./support-DKP_LLRd.mjs"), import("./scaffold-D8TAMXvs.mjs")]);
|
|
36
41
|
runScaffoldCommand(getMetaDir());
|
|
37
42
|
});
|
|
38
43
|
cli.command("stale [dir]", "Check skills for staleness in the current package or workspace").usage("stale [dir] [--json] [--github-review]").option("--json", "Output JSON").option("--github-review", "Write GitHub Actions review PR files").option("--package-label <label>", "Fallback package label for review PRs").example("stale").example("stale packages/query").example("stale --json").action(async (targetDir, options) => {
|
|
39
|
-
const [{ resolveStaleTargets }, { runStaleCommand }] = await Promise.all([import("./
|
|
44
|
+
const [{ resolveStaleTargets }, { runStaleCommand }] = await Promise.all([import("./support-DKP_LLRd.mjs"), import("./stale-DlNJHwga.mjs")]);
|
|
40
45
|
await runStaleCommand(targetDir, options, resolveStaleTargets);
|
|
41
46
|
});
|
|
42
47
|
cli.command("edit-package-json", "Update package.json files so skills are published").usage("edit-package-json").action(async () => {
|
|
43
|
-
const { runEditPackageJsonCommand } = await import("./edit-package-json-
|
|
48
|
+
const { runEditPackageJsonCommand } = await import("./edit-package-json-CdnA7_TA.mjs");
|
|
44
49
|
await runEditPackageJsonCommand(process.cwd());
|
|
45
50
|
});
|
|
46
51
|
cli.command("setup", "Copy Intent CI workflow templates into .github/workflows/").usage("setup").action(async () => {
|
|
47
|
-
const [{ getMetaDir }, { runSetupGithubActionsCommand }] = await Promise.all([import("./
|
|
52
|
+
const [{ getMetaDir }, { runSetupGithubActionsCommand }] = await Promise.all([import("./support-DKP_LLRd.mjs"), import("./github-actions-D41VZqWh.mjs")]);
|
|
48
53
|
await runSetupGithubActionsCommand(process.cwd(), getMetaDir());
|
|
49
54
|
});
|
|
50
55
|
cli.command("setup-github-actions", "Copy Intent CI workflow templates into .github/workflows/").usage("setup-github-actions").action(async () => {
|
|
51
|
-
const [{ getMetaDir }, { runSetupGithubActionsCommand }] = await Promise.all([import("./
|
|
56
|
+
const [{ getMetaDir }, { runSetupGithubActionsCommand }] = await Promise.all([import("./support-DKP_LLRd.mjs"), import("./github-actions-D41VZqWh.mjs")]);
|
|
52
57
|
await runSetupGithubActionsCommand(process.cwd(), getMetaDir());
|
|
53
58
|
});
|
|
54
59
|
cli.command("help [command]", "Display help for a command").action((commandName) => {
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { t as fail } from "./cli-error-BebkXaTJ.mjs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, relative } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
//#region src/hooks/adapters.ts
|
|
6
|
+
const HOOK_SCRIPT_DIR = ".intent/hooks";
|
|
7
|
+
const HOOK_AGENT_ADAPTERS = {
|
|
8
|
+
claude: {
|
|
9
|
+
agent: "claude",
|
|
10
|
+
configKind: "claude-settings",
|
|
11
|
+
supportedScopes: new Set(["project", "user"]),
|
|
12
|
+
paths: (scope, { homeDir, root }) => {
|
|
13
|
+
const project = scope === "project";
|
|
14
|
+
return {
|
|
15
|
+
configPath: project ? join(root, ".claude", "settings.json") : join(homeDir, ".claude", "settings.json"),
|
|
16
|
+
scriptPath: project ? join(root, HOOK_SCRIPT_DIR, "intent-claude-gate.mjs") : join(homeDir, ".tanstack", "intent", "hooks", "intent-claude-gate.mjs")
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
codex: {
|
|
21
|
+
agent: "codex",
|
|
22
|
+
configKind: "codex-hooks",
|
|
23
|
+
supportedScopes: new Set(["project", "user"]),
|
|
24
|
+
paths: (scope, { homeDir, root }) => {
|
|
25
|
+
const project = scope === "project";
|
|
26
|
+
return {
|
|
27
|
+
configPath: project ? join(root, ".codex", "hooks.json") : join(homeDir, ".codex", "hooks.json"),
|
|
28
|
+
scriptPath: project ? join(root, HOOK_SCRIPT_DIR, "intent-codex-gate.mjs") : join(homeDir, ".tanstack", "intent", "hooks", "intent-codex-gate.mjs")
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
copilot: {
|
|
33
|
+
agent: "copilot",
|
|
34
|
+
configKind: "copilot-hooks",
|
|
35
|
+
supportedScopes: new Set(["user"]),
|
|
36
|
+
paths: (_scope, { copilotHome, homeDir }) => ({
|
|
37
|
+
configPath: join(copilotHome ?? join(homeDir, ".copilot"), "hooks", "hooks.json"),
|
|
38
|
+
scriptPath: join(homeDir, ".tanstack", "intent", "hooks", "intent-copilot-gate.mjs")
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const ALL_HOOK_AGENTS = [
|
|
43
|
+
"copilot",
|
|
44
|
+
"claude",
|
|
45
|
+
"codex"
|
|
46
|
+
];
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/hooks/policy.ts
|
|
49
|
+
const EDIT_TOOLS_BY_AGENT = {
|
|
50
|
+
claude: new Set([
|
|
51
|
+
"Write",
|
|
52
|
+
"Edit",
|
|
53
|
+
"MultiEdit",
|
|
54
|
+
"NotebookEdit"
|
|
55
|
+
]),
|
|
56
|
+
codex: new Set([
|
|
57
|
+
"apply_patch",
|
|
58
|
+
"Write",
|
|
59
|
+
"Edit"
|
|
60
|
+
]),
|
|
61
|
+
copilot: new Set([
|
|
62
|
+
"Write",
|
|
63
|
+
"Edit",
|
|
64
|
+
"MultiEdit",
|
|
65
|
+
"NotebookEdit"
|
|
66
|
+
])
|
|
67
|
+
};
|
|
68
|
+
const GATE_DENY_REASON = "Blocked: load matching TanStack guidance before editing. Follow this repo's TanStack guidance setup, then retry the edit.";
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/hooks/install.ts
|
|
71
|
+
const STATUS_MESSAGE = "Checking Intent guidance";
|
|
72
|
+
function runInstallHooks({ agents, copilotHome, homeDir = homedir(), root, scope }) {
|
|
73
|
+
const resolvedScope = parseScope(scope);
|
|
74
|
+
return parseAgents(agents).map((agent) => installAgentHook({
|
|
75
|
+
agent,
|
|
76
|
+
copilotHome,
|
|
77
|
+
homeDir,
|
|
78
|
+
root,
|
|
79
|
+
scope: resolvedScope
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
function validateHookInstallOptions({ agents, scope }) {
|
|
83
|
+
parseScope(scope);
|
|
84
|
+
parseAgents(agents);
|
|
85
|
+
}
|
|
86
|
+
function buildHookRunnerScript(agent) {
|
|
87
|
+
const editTools = [...EDIT_TOOLS_BY_AGENT[agent]].sort();
|
|
88
|
+
return `#!/usr/bin/env node
|
|
89
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'
|
|
90
|
+
import { tmpdir } from 'node:os'
|
|
91
|
+
import { dirname, join } from 'node:path'
|
|
92
|
+
import { createHash } from 'node:crypto'
|
|
93
|
+
|
|
94
|
+
const AGENT = ${JSON.stringify(agent)}
|
|
95
|
+
const EDIT_TOOLS = new Set(${JSON.stringify(editTools)})
|
|
96
|
+
const GATE_DENY_REASON = ${JSON.stringify(GATE_DENY_REASON)}
|
|
97
|
+
const INTENT_COMMAND_PATTERN = /(?:^|&&|\\|\\||;|\\|)\\s*((?:bunx\\s+@tanstack\\/intent(?:@latest)?)|(?:pnpm\\s+exec\\s+intent)|(?:pnpm\\s+dlx\\s+@tanstack\\/intent(?:@latest)?)|(?:npx\\s+@tanstack\\/intent(?:@latest)?)|(?:yarn\\s+dlx\\s+@tanstack\\/intent(?:@latest)?)|(?:intent))\\s+(list|load)(?:\\s+([^\\s|;&]+))?/i
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const event = readEventFromStdin()
|
|
101
|
+
const stateFile = stateFileForEvent(event)
|
|
102
|
+
const observation = observationFromEvent(event)
|
|
103
|
+
|
|
104
|
+
if (observation) {
|
|
105
|
+
appendObservation(stateFile, observation)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const toolName = event?.tool_name ?? event?.toolName
|
|
109
|
+
if (typeof toolName === 'string' && EDIT_TOOLS.has(toolName) && !hasLoad(stateFile)) {
|
|
110
|
+
process.stdout.write(JSON.stringify(denyOutput()))
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
process.exit(0)
|
|
116
|
+
|
|
117
|
+
function readEventFromStdin() {
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(readFileSync(0, 'utf8'))
|
|
120
|
+
} catch {
|
|
121
|
+
return {}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function stateFileForEvent(event) {
|
|
126
|
+
const sessionId = typeof event?.session_id === 'string' ? event.session_id : 'unknown'
|
|
127
|
+
const cwd = typeof event?.cwd === 'string' ? event.cwd : process.cwd()
|
|
128
|
+
const key = createHash('sha256').update(AGENT + '\\0' + cwd + '\\0' + sessionId).digest('hex')
|
|
129
|
+
return join(tmpdir(), 'tanstack-intent-hooks', key + '.jsonl')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function observationFromEvent(event) {
|
|
133
|
+
if (!event || typeof event !== 'object') return undefined
|
|
134
|
+
const toolName = event.tool_name ?? event.toolName
|
|
135
|
+
const toolInput = event.tool_input ?? event.toolArgs
|
|
136
|
+
if (toolName !== 'Bash') return undefined
|
|
137
|
+
const command = typeof toolInput === 'string' ? safeCommandFromString(toolInput) : commandFromObject(toolInput)
|
|
138
|
+
const parsed = parseIntentInvocation(command)
|
|
139
|
+
if (!parsed || typeof command !== 'string') return undefined
|
|
140
|
+
return { action: parsed.action, skillUse: parsed.skillUse, raw: command }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parseIntentInvocation(command) {
|
|
144
|
+
if (typeof command !== 'string') return undefined
|
|
145
|
+
const match = command.match(INTENT_COMMAND_PATTERN)
|
|
146
|
+
if (!match?.[1] || !match[2]) return undefined
|
|
147
|
+
const action = match[2].toLowerCase()
|
|
148
|
+
if (action !== 'list' && action !== 'load') return undefined
|
|
149
|
+
const skillUse = action === 'load' ? match[3] : undefined
|
|
150
|
+
if (action === 'load' && !skillUse) return undefined
|
|
151
|
+
return action === 'load' ? { action, skillUse } : { action }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function commandFromObject(value) {
|
|
155
|
+
return value && typeof value === 'object' ? value.command : undefined
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function safeCommandFromString(value) {
|
|
159
|
+
try {
|
|
160
|
+
const command = commandFromObject(JSON.parse(value))
|
|
161
|
+
return typeof command === 'string' ? command : value
|
|
162
|
+
} catch {
|
|
163
|
+
return value
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function appendObservation(stateFile, observation) {
|
|
168
|
+
try {
|
|
169
|
+
mkdirSync(dirname(stateFile), { recursive: true })
|
|
170
|
+
appendFileSync(stateFile, JSON.stringify({ ts: new Date().toISOString(), ...observation }) + '\\n')
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function hasLoad(stateFile) {
|
|
176
|
+
if (!existsSync(stateFile)) return false
|
|
177
|
+
try {
|
|
178
|
+
return readFileSync(stateFile, 'utf8')
|
|
179
|
+
.split('\\n')
|
|
180
|
+
.filter(Boolean)
|
|
181
|
+
.some((line) => {
|
|
182
|
+
try {
|
|
183
|
+
return JSON.parse(line).action === 'load'
|
|
184
|
+
} catch {
|
|
185
|
+
return false
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
} catch {
|
|
189
|
+
return false
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function denyOutput() {
|
|
194
|
+
if (AGENT === 'copilot') {
|
|
195
|
+
return { permissionDecision: 'deny', permissionDecisionReason: GATE_DENY_REASON }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
hookSpecificOutput: {
|
|
200
|
+
hookEventName: 'PreToolUse',
|
|
201
|
+
permissionDecision: 'deny',
|
|
202
|
+
permissionDecisionReason: GATE_DENY_REASON,
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
function formatHookInstallResult(result) {
|
|
209
|
+
if (result.status === "skipped") return `Skipped Intent hooks for ${result.agent}: ${result.reason}`;
|
|
210
|
+
const target = result.configPath ? formatPath(result.configPath) : result.agent;
|
|
211
|
+
switch (result.status) {
|
|
212
|
+
case "created": return `Installed Intent hooks for ${result.agent} (${result.scope}) in ${target}.`;
|
|
213
|
+
case "updated": return `Updated Intent hooks for ${result.agent} (${result.scope}) in ${target}.`;
|
|
214
|
+
case "unchanged": return `No changes to Intent hooks for ${result.agent} (${result.scope}); already current.`;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function installAgentHook({ agent, copilotHome, homeDir, root, scope }) {
|
|
218
|
+
const adapter = HOOK_AGENT_ADAPTERS[agent];
|
|
219
|
+
if (!adapter.supportedScopes.has(scope)) return {
|
|
220
|
+
agent,
|
|
221
|
+
configPath: null,
|
|
222
|
+
reason: "project scope is not supported; use --scope user",
|
|
223
|
+
scope,
|
|
224
|
+
scriptPath: null,
|
|
225
|
+
status: "skipped"
|
|
226
|
+
};
|
|
227
|
+
const { configPath, scriptPath } = adapter.paths(scope, {
|
|
228
|
+
copilotHome: copilotHome ?? process.env.COPILOT_HOME,
|
|
229
|
+
homeDir,
|
|
230
|
+
root
|
|
231
|
+
});
|
|
232
|
+
return hookInstallResult({
|
|
233
|
+
agent,
|
|
234
|
+
configPath,
|
|
235
|
+
scope,
|
|
236
|
+
scriptPath,
|
|
237
|
+
scriptStatus: writeIfChanged(scriptPath, buildHookRunnerScript(agent)),
|
|
238
|
+
configStatus: updateJsonConfig(configPath, (config) => upsertAdapterPreToolUseHook({
|
|
239
|
+
config,
|
|
240
|
+
configKind: adapter.configKind,
|
|
241
|
+
project: scope === "project",
|
|
242
|
+
scriptPath
|
|
243
|
+
}))
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function hookInstallResult({ agent, configPath, configStatus, scope, scriptPath, scriptStatus }) {
|
|
247
|
+
return {
|
|
248
|
+
agent,
|
|
249
|
+
configPath,
|
|
250
|
+
scope,
|
|
251
|
+
scriptPath,
|
|
252
|
+
status: scriptStatus === "created" || configStatus === "created" ? "created" : scriptStatus === "updated" || configStatus === "updated" ? "updated" : "unchanged"
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function upsertAdapterPreToolUseHook({ config, configKind, project, scriptPath }) {
|
|
256
|
+
switch (configKind) {
|
|
257
|
+
case "claude-settings": return upsertClaudePreToolUseHook(config, project, scriptPath);
|
|
258
|
+
case "codex-hooks": return upsertCodexPreToolUseHook(config, project, scriptPath);
|
|
259
|
+
case "copilot-hooks": return upsertCopilotPreToolUseHook(config, scriptPath);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function upsertClaudePreToolUseHook(config, project, scriptPath) {
|
|
263
|
+
const hooks = objectValue(config.hooks);
|
|
264
|
+
hooks.PreToolUse = upsertHookGroup(arrayValue(hooks.PreToolUse), {
|
|
265
|
+
matcher: "Bash|Write|Edit|MultiEdit|NotebookEdit",
|
|
266
|
+
hooks: [{
|
|
267
|
+
type: "command",
|
|
268
|
+
command: "node",
|
|
269
|
+
args: [project ? "${CLAUDE_PROJECT_DIR}/.intent/hooks/intent-claude-gate.mjs" : scriptPath],
|
|
270
|
+
timeout: 10,
|
|
271
|
+
statusMessage: STATUS_MESSAGE
|
|
272
|
+
}]
|
|
273
|
+
});
|
|
274
|
+
return {
|
|
275
|
+
...config,
|
|
276
|
+
hooks
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function upsertCodexPreToolUseHook(config, project, scriptPath) {
|
|
280
|
+
const hooks = objectValue(config.hooks);
|
|
281
|
+
hooks.PreToolUse = upsertHookGroup(arrayValue(hooks.PreToolUse), {
|
|
282
|
+
matcher: "Bash|apply_patch|Edit|Write",
|
|
283
|
+
hooks: [{
|
|
284
|
+
type: "command",
|
|
285
|
+
command: project ? "node \"$(git rev-parse --show-toplevel)/.intent/hooks/intent-codex-gate.mjs\"" : `node ${quoteShell(scriptPath)}`,
|
|
286
|
+
timeout: 10,
|
|
287
|
+
statusMessage: STATUS_MESSAGE
|
|
288
|
+
}]
|
|
289
|
+
});
|
|
290
|
+
return {
|
|
291
|
+
...config,
|
|
292
|
+
hooks
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function upsertCopilotPreToolUseHook(config, scriptPath) {
|
|
296
|
+
const hooks = objectValue(config.hooks);
|
|
297
|
+
hooks.PreToolUse = upsertHookGroup(arrayValue(hooks.PreToolUse), { command: `node ${quoteShell(scriptPath)}` });
|
|
298
|
+
return {
|
|
299
|
+
...config,
|
|
300
|
+
hooks
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function upsertHookGroup(groups, nextGroup) {
|
|
304
|
+
return [...groups.flatMap(withoutIntentHooks), nextGroup];
|
|
305
|
+
}
|
|
306
|
+
function withoutIntentHooks(value) {
|
|
307
|
+
if (!value || typeof value !== "object") return [value];
|
|
308
|
+
const hooks = arrayValue(value.hooks);
|
|
309
|
+
if (hooks.length === 0) return isIntentHook(value) ? [] : [value];
|
|
310
|
+
const nextHooks = hooks.filter((hook) => !isIntentHook(hook));
|
|
311
|
+
if (nextHooks.length === hooks.length) return [value];
|
|
312
|
+
if (nextHooks.length === 0) return [];
|
|
313
|
+
return [{
|
|
314
|
+
...value,
|
|
315
|
+
hooks: nextHooks
|
|
316
|
+
}];
|
|
317
|
+
}
|
|
318
|
+
function isIntentHook(value) {
|
|
319
|
+
if (!value || typeof value !== "object") return false;
|
|
320
|
+
const entry = value;
|
|
321
|
+
return [typeof entry.command === "string" ? entry.command : "", ...Array.isArray(entry.args) ? entry.args.filter((arg) => typeof arg === "string") : []].some(isIntentGateScriptReference);
|
|
322
|
+
}
|
|
323
|
+
function isIntentGateScriptReference(value) {
|
|
324
|
+
return /(?:^|[\s"'\\/])(?:old-)?intent-(claude|codex|copilot)-gate\.mjs(?:$|[?#\s"'])/i.test(value);
|
|
325
|
+
}
|
|
326
|
+
function updateJsonConfig(filePath, update) {
|
|
327
|
+
const existed = existsSync(filePath);
|
|
328
|
+
const current = existed ? readFileSync(filePath, "utf8") : "";
|
|
329
|
+
const parsed = current.trim() ? parseJsonObject(filePath, current) : {};
|
|
330
|
+
const next = `${JSON.stringify(update(parsed), null, 2)}\n`;
|
|
331
|
+
if (current === next) return "unchanged";
|
|
332
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
333
|
+
writeFileSync(filePath, next);
|
|
334
|
+
return existed ? "updated" : "created";
|
|
335
|
+
}
|
|
336
|
+
function writeIfChanged(filePath, content) {
|
|
337
|
+
const existed = existsSync(filePath);
|
|
338
|
+
if (existed && readFileSync(filePath, "utf8") === content) return "unchanged";
|
|
339
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
340
|
+
writeFileSync(filePath, content);
|
|
341
|
+
return existed ? "updated" : "created";
|
|
342
|
+
}
|
|
343
|
+
function parseAgents(value) {
|
|
344
|
+
if (!value || value === "all") return ALL_HOOK_AGENTS;
|
|
345
|
+
const agents = value.split(",").map((agent) => agent.trim()).filter(Boolean);
|
|
346
|
+
const invalid = agents.filter((agent) => !ALL_HOOK_AGENTS.includes(agent));
|
|
347
|
+
if (invalid.length > 0) fail(`Unknown hook agent: ${invalid.join(", ")}. Expected copilot, claude, codex, or all.`);
|
|
348
|
+
return [...new Set(agents)];
|
|
349
|
+
}
|
|
350
|
+
function parseScope(value) {
|
|
351
|
+
if (!value) return "project";
|
|
352
|
+
if (value === "project" || value === "user") return value;
|
|
353
|
+
fail(`Unknown hook scope: ${value}. Expected project or user.`);
|
|
354
|
+
}
|
|
355
|
+
function parseJsonObject(filePath, content) {
|
|
356
|
+
try {
|
|
357
|
+
const parsed = JSON.parse(content);
|
|
358
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
359
|
+
} catch (err) {
|
|
360
|
+
fail(`Failed to parse ${formatPath(filePath)}: ${err instanceof Error ? err.message : String(err)}`);
|
|
361
|
+
}
|
|
362
|
+
fail(`Failed to parse ${formatPath(filePath)}: expected a JSON object.`);
|
|
363
|
+
}
|
|
364
|
+
function objectValue(value) {
|
|
365
|
+
return value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
|
|
366
|
+
}
|
|
367
|
+
function arrayValue(value) {
|
|
368
|
+
return Array.isArray(value) ? value : [];
|
|
369
|
+
}
|
|
370
|
+
function quoteShell(value) {
|
|
371
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
372
|
+
}
|
|
373
|
+
function formatPath(filePath) {
|
|
374
|
+
return relative(process.cwd(), filePath) || filePath;
|
|
375
|
+
}
|
|
376
|
+
//#endregion
|
|
377
|
+
//#region src/commands/hooks/command.ts
|
|
378
|
+
function runHooksInstallCommand(options) {
|
|
379
|
+
validateHookInstallOptions(options);
|
|
380
|
+
const results = runInstallHooks({
|
|
381
|
+
agents: options.agents,
|
|
382
|
+
root: process.cwd(),
|
|
383
|
+
scope: options.scope
|
|
384
|
+
});
|
|
385
|
+
for (const result of results) console.log(formatHookInstallResult(result));
|
|
386
|
+
}
|
|
387
|
+
//#endregion
|
|
388
|
+
export { runHooksInstallCommand };
|