@plimeor/harness 0.1.0 → 0.1.2

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.
@@ -0,0 +1,182 @@
1
+ import { rm } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+
4
+ import type { HarnessContext, HookResource } from '../types'
5
+ import type { HookExtensionDriver, InstalledHook, JsonObject, McpExtensionDriver } from './extensions'
6
+ import {
7
+ createJsonMcpDriver,
8
+ jsonFingerprint,
9
+ mcpServerRecord,
10
+ pathExists,
11
+ readJsonFileFingerprint,
12
+ safeName,
13
+ writeJsonFile
14
+ } from './extensions'
15
+
16
+ export function createKiroMcpDriver(input: {
17
+ configDirectory: string
18
+ configFile: string
19
+ context?: HarnessContext
20
+ }): McpExtensionDriver {
21
+ const jsonDriver = createJsonMcpDriver(input.configFile)
22
+
23
+ return {
24
+ configFile: input.configFile,
25
+ currentFingerprint: jsonDriver.currentFingerprint,
26
+ async install({ name, server }) {
27
+ const args = ['mcp', 'add', '--scope', 'global', '--name', name, '--command', server.command]
28
+
29
+ if (server.args && server.args.length > 0) {
30
+ args.push('--args', JSON.stringify(server.args))
31
+ }
32
+
33
+ for (const [key, value] of Object.entries(server.env ?? {})) {
34
+ args.push('--env', `${key}=${value}`)
35
+ }
36
+
37
+ await runKiroCommand(input, args)
38
+
39
+ const fingerprint = await jsonDriver.currentFingerprint(name)
40
+ if (!fingerprint) {
41
+ await removeKiroMcpServer(input, name)
42
+ throw new Error(`Kiro MCP server ${name} was not written to ${input.configFile}.`)
43
+ }
44
+
45
+ return { fingerprint, name, server: mcpServerRecord(server) }
46
+ },
47
+ async remove(name: string) {
48
+ await removeKiroMcpServer(input, name)
49
+ }
50
+ }
51
+ }
52
+
53
+ export function createKiroHookDriver(hooksDirectory: string, events: readonly string[]): HookExtensionDriver {
54
+ return {
55
+ events,
56
+ async conflicts({ extensionId, hooks }) {
57
+ const issues: Awaited<ReturnType<HookExtensionDriver['conflicts']>> = []
58
+
59
+ for (const hook of hooks) {
60
+ const targetPath = kiroHookTargetPath(hooksDirectory, extensionId, hook)
61
+ if (await pathExists(targetPath)) {
62
+ issues.push({
63
+ kind: 'conflict',
64
+ reason: `Hook install target already exists: ${targetPath}.`,
65
+ resourceKind: 'hooks',
66
+ resourceName: hook.name
67
+ })
68
+ }
69
+ }
70
+
71
+ return issues
72
+ },
73
+ async currentFingerprint(hook: InstalledHook) {
74
+ if (!hook.targetPath) {
75
+ return undefined
76
+ }
77
+
78
+ return readJsonFileFingerprint(hook.targetPath)
79
+ },
80
+ async install({ extensionId, hooks }) {
81
+ const installed: InstalledHook[] = []
82
+
83
+ for (const hook of hooks) {
84
+ const targetPath = kiroHookTargetPath(hooksDirectory, extensionId, hook)
85
+ const hookConfig = kiroHookConfig(hook)
86
+ await writeJsonFile(targetPath, hookConfig)
87
+ installed.push({
88
+ command: hook.command,
89
+ event: hook.event,
90
+ fingerprint: jsonFingerprint(hookConfig),
91
+ name: hook.name,
92
+ targetPath
93
+ })
94
+ }
95
+
96
+ return installed
97
+ },
98
+ async restore(hooks: InstalledHook[]) {
99
+ for (const hook of hooks) {
100
+ if (!hook.targetPath || !hook.name || (await pathExists(hook.targetPath))) {
101
+ continue
102
+ }
103
+
104
+ await writeJsonFile(
105
+ hook.targetPath,
106
+ kiroHookConfig({ command: hook.command, event: hook.event, name: hook.name })
107
+ )
108
+ }
109
+ },
110
+ async uninstall(hooks: InstalledHook[]) {
111
+ for (const hook of hooks) {
112
+ if (!hook.targetPath) {
113
+ continue
114
+ }
115
+
116
+ const current = await readJsonFileFingerprint(hook.targetPath)
117
+ if (current !== hook.fingerprint) {
118
+ continue
119
+ }
120
+
121
+ await rm(hook.targetPath, { force: true })
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ async function removeKiroMcpServer(
128
+ config: { configDirectory: string; context?: HarnessContext },
129
+ name: string
130
+ ): Promise<void> {
131
+ await runKiroCommand(config, ['mcp', 'remove', '--scope', 'global', '--name', name])
132
+ }
133
+
134
+ async function runKiroCommand(
135
+ config: { configDirectory: string; context?: HarnessContext },
136
+ args: string[]
137
+ ): Promise<{ exitCode: number; stderr: string; stdout: string }> {
138
+ const subprocess = Bun.spawn({
139
+ cmd: ['kiro-cli', ...args],
140
+ cwd: config.context?.cwd ?? process.cwd(),
141
+ env: Object.fromEntries(
142
+ Object.entries({
143
+ ...process.env,
144
+ ...(config.context?.home ? { KIRO_HOME: config.configDirectory } : {}),
145
+ ...(config.context?.env ?? {})
146
+ }).filter((entry): entry is [string, string] => {
147
+ return typeof entry[1] === 'string'
148
+ })
149
+ ),
150
+ stderr: 'pipe',
151
+ stdout: 'pipe'
152
+ })
153
+ const [exitCode, stdout, stderr] = await Promise.all([
154
+ subprocess.exited,
155
+ new Response(subprocess.stdout).text(),
156
+ new Response(subprocess.stderr).text()
157
+ ])
158
+
159
+ if (exitCode !== 0) {
160
+ throw new Error(`kiro-cli ${args.join(' ')} failed: ${stderr || stdout}`)
161
+ }
162
+
163
+ return { exitCode, stderr, stdout }
164
+ }
165
+
166
+ function kiroHookTargetPath(hooksDirectory: string, extensionId: string, hook: HookResource): string {
167
+ return join(hooksDirectory, `${safeName(extensionId)}__${safeName(hook.name)}.json`)
168
+ }
169
+
170
+ function kiroHookConfig(hook: HookResource): JsonObject {
171
+ return {
172
+ version: 'v1',
173
+ hooks: [
174
+ {
175
+ action: { command: hook.command, type: 'command' },
176
+ enabled: true,
177
+ name: hook.name,
178
+ trigger: hook.event
179
+ }
180
+ ]
181
+ }
182
+ }
@@ -1,6 +1,7 @@
1
1
  import { harness } from '../registry'
2
2
  import type { HarnessContext, RunOutputRequest, RunRequest, TextOutputRequest } from '../types'
3
3
  import { configDirectory, createExtensionFacet } from './extensions'
4
+ import { createKiroHookDriver, createKiroMcpDriver } from './kiro-extensions'
4
5
  import { createBuiltInAdapter, planTextCommand, unsupportedOutputMode } from './shared'
5
6
 
6
7
  const HARNESS_ID = 'kiro'
@@ -16,25 +17,25 @@ export const kiroAdapter = createBuiltInAdapter({
16
17
  configDirectory: directory,
17
18
  context,
18
19
  harnessId: HARNESS_ID,
19
- mcp: { configFile: `${directory}/settings/mcp.json`, kind: 'kiro-cli' },
20
- skillsDirectory: `${directory}/skills`,
21
- hooks: {
22
- hooksDirectory: `${directory}/hooks`,
23
- kind: 'kiro-hook-files',
24
- events: [
25
- 'Manual',
26
- 'PostFileCreate',
27
- 'PostFileDelete',
28
- 'PostFileSave',
29
- 'PostTaskExec',
30
- 'PostToolUse',
31
- 'PreTaskExec',
32
- 'PreToolUse',
33
- 'SessionStart',
34
- 'Stop',
35
- 'UserPromptSubmit'
36
- ]
37
- }
20
+ hooks: createKiroHookDriver(`${directory}/hooks`, [
21
+ 'Manual',
22
+ 'PostFileCreate',
23
+ 'PostFileDelete',
24
+ 'PostFileSave',
25
+ 'PostTaskExec',
26
+ 'PostToolUse',
27
+ 'PreTaskExec',
28
+ 'PreToolUse',
29
+ 'SessionStart',
30
+ 'Stop',
31
+ 'UserPromptSubmit'
32
+ ]),
33
+ mcp: createKiroMcpDriver({
34
+ configDirectory: directory,
35
+ configFile: `${directory}/settings/mcp.json`,
36
+ context
37
+ }),
38
+ skillsDirectory: `${directory}/skills`
38
39
  })
39
40
  },
40
41
  plan(request: RunRequest<RunOutputRequest>, command: string, cwd: string) {