@junwu168/openshell 0.1.2 → 0.1.4
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/dist/cli/openshell.js +4 -0
- package/dist/core/audit/log-store.js +1 -1
- package/dist/core/orchestrator.d.ts +2 -2
- package/dist/core/orchestrator.js +3 -3
- package/dist/core/result.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/opencode/plugin.d.ts +1 -1
- package/dist/opencode/plugin.js +8 -8
- package/package.json +6 -1
- package/.claude/settings.local.json +0 -15
- package/bun.lock +0 -368
- package/docs/superpowers/notes/2026-03-25-opencode-remote-tools-handoff.md +0 -81
- package/docs/superpowers/notes/2026-03-26-openshell-pre-release-review.md +0 -174
- package/docs/superpowers/plans/2026-03-25-opencode-remote-tools.md +0 -1656
- package/docs/superpowers/plans/2026-03-25-server-registry-cli.md +0 -54
- package/docs/superpowers/plans/2026-03-26-config-backed-credential-registry.md +0 -494
- package/docs/superpowers/plans/2026-03-26-openshell-release-prep.md +0 -639
- package/docs/superpowers/specs/2026-03-25-opencode-remote-tools-design.md +0 -378
- package/docs/superpowers/specs/2026-03-26-config-backed-credential-registry-design.md +0 -272
- package/docs/superpowers/specs/2026-03-26-openshell-release-prep-design.md +0 -197
- package/examples/opencode-local/opencode.json +0 -19
- package/scripts/openshell.ts +0 -3
- package/scripts/server-registry.ts +0 -3
- package/src/cli/openshell.ts +0 -60
- package/src/cli/server-registry.ts +0 -476
- package/src/core/audit/git-audit-repo.ts +0 -42
- package/src/core/audit/log-store.ts +0 -20
- package/src/core/audit/redact.ts +0 -4
- package/src/core/contracts.ts +0 -51
- package/src/core/orchestrator.ts +0 -1082
- package/src/core/patch.ts +0 -11
- package/src/core/paths.ts +0 -32
- package/src/core/policy.ts +0 -30
- package/src/core/registry/server-registry.ts +0 -505
- package/src/core/result.ts +0 -16
- package/src/core/ssh/ssh-runtime.ts +0 -355
- package/src/index.ts +0 -3
- package/src/opencode/plugin.ts +0 -242
- package/src/product/install.ts +0 -43
- package/src/product/opencode-config.ts +0 -118
- package/src/product/uninstall.ts +0 -47
- package/src/product/workspace-tracker.ts +0 -69
- package/tests/integration/fake-ssh-server.ts +0 -97
- package/tests/integration/install-lifecycle.test.ts +0 -85
- package/tests/integration/orchestrator.test.ts +0 -767
- package/tests/integration/ssh-runtime.test.ts +0 -122
- package/tests/unit/audit.test.ts +0 -221
- package/tests/unit/build-layout.test.ts +0 -28
- package/tests/unit/opencode-config.test.ts +0 -100
- package/tests/unit/opencode-plugin.test.ts +0 -358
- package/tests/unit/openshell-cli.test.ts +0 -60
- package/tests/unit/paths.test.ts +0 -64
- package/tests/unit/plugin-export.test.ts +0 -10
- package/tests/unit/policy.test.ts +0 -53
- package/tests/unit/release-docs.test.ts +0 -31
- package/tests/unit/result.test.ts +0 -28
- package/tests/unit/server-registry-cli.test.ts +0 -673
- package/tests/unit/server-registry.test.ts +0 -452
- package/tests/unit/workspace-tracker.test.ts +0 -57
- package/tsconfig.json +0 -14
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
import { constants as osConstants } from "node:os"
|
|
2
|
-
import { Shescape } from "shescape"
|
|
3
|
-
import { Client, type ConnectConfig } from "ssh2"
|
|
4
|
-
|
|
5
|
-
const shell = new Shescape({ shell: "zsh" })
|
|
6
|
-
const DEFAULT_OPERATION_TIMEOUT_MS = 30_000
|
|
7
|
-
|
|
8
|
-
type ExecOptions = {
|
|
9
|
-
cwd?: string
|
|
10
|
-
timeout?: number
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
type ExecResult = {
|
|
14
|
-
stdout: string
|
|
15
|
-
stderr: string
|
|
16
|
-
exitCode: number
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
type DirEntry = {
|
|
20
|
-
name: string
|
|
21
|
-
longname: string
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
type PathStat = {
|
|
25
|
-
size: number
|
|
26
|
-
mode: number
|
|
27
|
-
isFile: boolean
|
|
28
|
-
isDirectory: boolean
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
type RuntimeOptions = {
|
|
32
|
-
operationTimeoutMs?: number
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const normalizeShellPath = (path: string) => (path.startsWith("-") ? `./${path}` : path)
|
|
36
|
-
const quoteShellPath = (path: string) => shell.quote(normalizeShellPath(path))
|
|
37
|
-
const clampLimit = (limit: number) => Math.max(0, Math.trunc(limit))
|
|
38
|
-
const joinRemotePath = (parent: string, name: string) => (parent.endsWith("/") ? `${parent}${name}` : `${parent}/${name}`)
|
|
39
|
-
const signalToExitCode = (signal: string | null | undefined) => {
|
|
40
|
-
if (!signal) {
|
|
41
|
-
return 0
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const normalized = signal.startsWith("SIG") ? signal : `SIG${signal}`
|
|
45
|
-
const signalNumber = osConstants.signals[normalized as keyof typeof osConstants.signals]
|
|
46
|
-
|
|
47
|
-
return signalNumber === undefined ? 1 : 128 + signalNumber
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const withClient = <T>(
|
|
51
|
-
connection: ConnectConfig,
|
|
52
|
-
action: (client: Client) => Promise<T>,
|
|
53
|
-
timeoutMs: number | null = null,
|
|
54
|
-
) =>
|
|
55
|
-
new Promise<T>((resolve, reject) => {
|
|
56
|
-
const client = new Client()
|
|
57
|
-
let settled = false
|
|
58
|
-
let timer: ReturnType<typeof setTimeout> | null = null
|
|
59
|
-
|
|
60
|
-
const finish = (handler: () => void, close: () => void = () => client.end()) => {
|
|
61
|
-
if (settled) {
|
|
62
|
-
return
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
settled = true
|
|
66
|
-
if (timer) {
|
|
67
|
-
clearTimeout(timer)
|
|
68
|
-
}
|
|
69
|
-
handler()
|
|
70
|
-
close()
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (timeoutMs !== null) {
|
|
74
|
-
timer = setTimeout(() => {
|
|
75
|
-
finish(() => reject(new Error(`ssh operation timed out after ${timeoutMs}ms`)), () => client.destroy())
|
|
76
|
-
}, timeoutMs)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
client
|
|
80
|
-
.on("ready", () => {
|
|
81
|
-
action(client).then(
|
|
82
|
-
(result) => finish(() => resolve(result)),
|
|
83
|
-
(error) => finish(() => reject(error)),
|
|
84
|
-
)
|
|
85
|
-
})
|
|
86
|
-
.on("error", (error) => finish(() => reject(error)))
|
|
87
|
-
.connect(timeoutMs === null ? connection : { ...connection, readyTimeout: connection.readyTimeout ?? timeoutMs })
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
export const createSshRuntime = (options: RuntimeOptions = {}) => {
|
|
91
|
-
const operationTimeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS
|
|
92
|
-
|
|
93
|
-
const exec = (connection: ConnectConfig, command: string, options: ExecOptions = {}) =>
|
|
94
|
-
withClient<ExecResult>(connection, (client) =>
|
|
95
|
-
new Promise((resolve, reject) => {
|
|
96
|
-
const effective = options.cwd ? `cd -- ${quoteShellPath(options.cwd)} && ${command}` : command
|
|
97
|
-
let settled = false
|
|
98
|
-
let streamRef: {
|
|
99
|
-
signal: (signalName: string) => void
|
|
100
|
-
close: () => void
|
|
101
|
-
} | null = null
|
|
102
|
-
|
|
103
|
-
const finish = (handler: () => void) => {
|
|
104
|
-
if (settled) {
|
|
105
|
-
return
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
settled = true
|
|
109
|
-
|
|
110
|
-
if (timer) {
|
|
111
|
-
clearTimeout(timer)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
handler()
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const timer =
|
|
118
|
-
options.timeout === undefined
|
|
119
|
-
? null
|
|
120
|
-
: setTimeout(() => {
|
|
121
|
-
if (streamRef) {
|
|
122
|
-
try {
|
|
123
|
-
streamRef.signal("SIGKILL")
|
|
124
|
-
} catch {}
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
streamRef.close()
|
|
128
|
-
} catch {}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
finish(() => reject(new Error(`command timed out after ${options.timeout}ms`)))
|
|
132
|
-
}, options.timeout)
|
|
133
|
-
|
|
134
|
-
client.exec(effective, (error, stream) => {
|
|
135
|
-
if (error) {
|
|
136
|
-
finish(() => reject(error))
|
|
137
|
-
return
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
streamRef = stream
|
|
141
|
-
|
|
142
|
-
let stdout = ""
|
|
143
|
-
let stderr = ""
|
|
144
|
-
let exitCode = 0
|
|
145
|
-
|
|
146
|
-
stream.on("data", (chunk: Buffer | string) => {
|
|
147
|
-
stdout += chunk.toString()
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
stream.stderr.on("data", (chunk: Buffer | string) => {
|
|
151
|
-
stderr += chunk.toString()
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
stream.on("exit", (code, signal) => {
|
|
155
|
-
exitCode = code ?? signalToExitCode(signal)
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
stream.on("close", () => {
|
|
159
|
-
finish(() => resolve({ stdout, stderr, exitCode }))
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
stream.on("error", (streamError: Error) => {
|
|
163
|
-
finish(() => reject(streamError))
|
|
164
|
-
})
|
|
165
|
-
})
|
|
166
|
-
}),
|
|
167
|
-
null,
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
const readFile = (connection: ConnectConfig, path: string) =>
|
|
171
|
-
withClient<string>(connection, (client) =>
|
|
172
|
-
new Promise((resolve, reject) => {
|
|
173
|
-
client.sftp((error, sftp) => {
|
|
174
|
-
if (error) {
|
|
175
|
-
reject(error)
|
|
176
|
-
return
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const chunks: Buffer[] = []
|
|
180
|
-
const stream = sftp.createReadStream(path)
|
|
181
|
-
|
|
182
|
-
stream.on("data", (chunk: Buffer | string) => {
|
|
183
|
-
chunks.push(Buffer.from(chunk))
|
|
184
|
-
})
|
|
185
|
-
stream.on("error", reject)
|
|
186
|
-
stream.on("close", () => {
|
|
187
|
-
resolve(Buffer.concat(chunks).toString("utf8"))
|
|
188
|
-
})
|
|
189
|
-
})
|
|
190
|
-
}),
|
|
191
|
-
operationTimeoutMs,
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
const writeFile = (connection: ConnectConfig, path: string, content: string, mode?: number) =>
|
|
195
|
-
withClient<void>(connection, (client) =>
|
|
196
|
-
new Promise((resolve, reject) => {
|
|
197
|
-
client.sftp((error, sftp) => {
|
|
198
|
-
if (error) {
|
|
199
|
-
reject(error)
|
|
200
|
-
return
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const stream = sftp.createWriteStream(path, mode === undefined ? undefined : { mode })
|
|
204
|
-
|
|
205
|
-
stream.on("error", reject)
|
|
206
|
-
stream.on("close", () => resolve())
|
|
207
|
-
stream.end(content)
|
|
208
|
-
})
|
|
209
|
-
}),
|
|
210
|
-
operationTimeoutMs,
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
const listDir = async (connection: ConnectConfig, path: string, recursive = false, limit = 200) => {
|
|
214
|
-
const boundedLimit = clampLimit(limit)
|
|
215
|
-
|
|
216
|
-
if (boundedLimit === 0) {
|
|
217
|
-
return []
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (recursive) {
|
|
221
|
-
return withClient<string[]>(connection, (client) =>
|
|
222
|
-
new Promise((resolve, reject) => {
|
|
223
|
-
client.sftp((error, sftp) => {
|
|
224
|
-
if (error) {
|
|
225
|
-
reject(error)
|
|
226
|
-
return
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const statPath = (target: string) =>
|
|
230
|
-
new Promise<any>((resolvePath, rejectPath) => {
|
|
231
|
-
sftp.stat(target, (statError, stats) => {
|
|
232
|
-
if (statError) {
|
|
233
|
-
rejectPath(statError)
|
|
234
|
-
return
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
resolvePath(stats)
|
|
238
|
-
})
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
const readDir = (target: string) =>
|
|
242
|
-
new Promise<any[]>((resolvePath, rejectPath) => {
|
|
243
|
-
sftp.readdir(target, (readError, entries) => {
|
|
244
|
-
if (readError) {
|
|
245
|
-
rejectPath(readError)
|
|
246
|
-
return
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
resolvePath(entries)
|
|
250
|
-
})
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
const visit = async (target: string, output: string[]) => {
|
|
254
|
-
if (output.length >= boundedLimit) {
|
|
255
|
-
return
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
output.push(target)
|
|
259
|
-
|
|
260
|
-
if (output.length >= boundedLimit) {
|
|
261
|
-
return
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const targetStats = await statPath(target)
|
|
265
|
-
if (!targetStats.isDirectory()) {
|
|
266
|
-
return
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const entries = await readDir(target)
|
|
270
|
-
|
|
271
|
-
for (const entry of entries) {
|
|
272
|
-
if (entry.filename === "." || entry.filename === "..") {
|
|
273
|
-
continue
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const fullPath = joinRemotePath(target, entry.filename)
|
|
277
|
-
|
|
278
|
-
if (entry.attrs.isDirectory()) {
|
|
279
|
-
await visit(fullPath, output)
|
|
280
|
-
} else if (output.length < boundedLimit) {
|
|
281
|
-
output.push(fullPath)
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (output.length >= boundedLimit) {
|
|
285
|
-
return
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
void (async () => {
|
|
291
|
-
try {
|
|
292
|
-
const output: string[] = []
|
|
293
|
-
await visit(path, output)
|
|
294
|
-
resolve(output)
|
|
295
|
-
} catch (visitError) {
|
|
296
|
-
reject(visitError)
|
|
297
|
-
}
|
|
298
|
-
})()
|
|
299
|
-
})
|
|
300
|
-
}),
|
|
301
|
-
operationTimeoutMs,
|
|
302
|
-
)
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return withClient<DirEntry[]>(connection, (client) =>
|
|
306
|
-
new Promise((resolve, reject) => {
|
|
307
|
-
client.sftp((error, sftp) => {
|
|
308
|
-
if (error) {
|
|
309
|
-
reject(error)
|
|
310
|
-
return
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
sftp.readdir(path, (readError, entries) => {
|
|
314
|
-
if (readError) {
|
|
315
|
-
reject(readError)
|
|
316
|
-
return
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
resolve(entries.slice(0, boundedLimit).map((entry) => ({ name: entry.filename, longname: entry.longname })))
|
|
320
|
-
})
|
|
321
|
-
})
|
|
322
|
-
}),
|
|
323
|
-
operationTimeoutMs,
|
|
324
|
-
)
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const stat = (connection: ConnectConfig, path: string) =>
|
|
328
|
-
withClient<PathStat>(connection, (client) =>
|
|
329
|
-
new Promise((resolve, reject) => {
|
|
330
|
-
client.sftp((error, sftp) => {
|
|
331
|
-
if (error) {
|
|
332
|
-
reject(error)
|
|
333
|
-
return
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
sftp.stat(path, (statError, stats) => {
|
|
337
|
-
if (statError) {
|
|
338
|
-
reject(statError)
|
|
339
|
-
return
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
resolve({
|
|
343
|
-
size: stats.size,
|
|
344
|
-
mode: stats.mode,
|
|
345
|
-
isFile: stats.isFile(),
|
|
346
|
-
isDirectory: stats.isDirectory(),
|
|
347
|
-
})
|
|
348
|
-
})
|
|
349
|
-
})
|
|
350
|
-
}),
|
|
351
|
-
operationTimeoutMs,
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
return { exec, readFile, writeFile, listDir, stat }
|
|
355
|
-
}
|
package/src/index.ts
DELETED
package/src/opencode/plugin.ts
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
import { tool, type Plugin, type ToolContext } from "@opencode-ai/plugin"
|
|
2
|
-
import { createAuditLogStore } from "../core/audit/log-store"
|
|
3
|
-
import { createGitAuditRepo } from "../core/audit/git-audit-repo"
|
|
4
|
-
import { createOrchestrator } from "../core/orchestrator"
|
|
5
|
-
import { classifyRemoteExec } from "../core/policy"
|
|
6
|
-
import { createRuntimePaths, ensureRuntimeDirs } from "../core/paths"
|
|
7
|
-
import { createServerRegistry } from "../core/registry/server-registry"
|
|
8
|
-
import { errorResult } from "../core/result"
|
|
9
|
-
import { createSshRuntime } from "../core/ssh/ssh-runtime"
|
|
10
|
-
|
|
11
|
-
const serialize = async <T>(result: Promise<T>) => JSON.stringify(await result)
|
|
12
|
-
type RuntimeDependencies = Parameters<typeof createOrchestrator>[0]
|
|
13
|
-
type OpenCodePluginOptions = {
|
|
14
|
-
ensureRuntimeDirs?: () => Promise<void>
|
|
15
|
-
createRuntimeDependencies?: (workspaceRoot?: string) => RuntimeDependencies
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
type ApprovalRequest = Parameters<ToolContext["ask"]>[0]
|
|
19
|
-
|
|
20
|
-
const approvalRejected = (toolId: string, server: string, error: unknown) =>
|
|
21
|
-
JSON.stringify(
|
|
22
|
-
errorResult({
|
|
23
|
-
tool: toolId,
|
|
24
|
-
server,
|
|
25
|
-
code: "APPROVAL_REJECTED",
|
|
26
|
-
message: error instanceof Error ? error.message : "approval rejected",
|
|
27
|
-
execution: { attempted: false, completed: false },
|
|
28
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
29
|
-
}),
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
const requestApproval = async (
|
|
33
|
-
context: ToolContext,
|
|
34
|
-
toolId: string,
|
|
35
|
-
server: string,
|
|
36
|
-
request: ApprovalRequest | null,
|
|
37
|
-
) => {
|
|
38
|
-
if (!request) {
|
|
39
|
-
return null
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
await context.ask(request)
|
|
44
|
-
return null
|
|
45
|
-
} catch (error) {
|
|
46
|
-
return approvalRejected(toolId, server, error)
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const createEditApproval = (
|
|
51
|
-
toolId: "remote_write_file" | "remote_patch_file",
|
|
52
|
-
input: { server: string; path: string; mode?: number; patch?: string; content?: string },
|
|
53
|
-
): ApprovalRequest => ({
|
|
54
|
-
permission: "edit",
|
|
55
|
-
patterns: [input.path],
|
|
56
|
-
always: [],
|
|
57
|
-
metadata: {
|
|
58
|
-
tool: toolId,
|
|
59
|
-
server: input.server,
|
|
60
|
-
path: input.path,
|
|
61
|
-
mode: input.mode,
|
|
62
|
-
contentBytes: input.content ? Buffer.byteLength(input.content) : undefined,
|
|
63
|
-
patchBytes: input.patch ? Buffer.byteLength(input.patch) : undefined,
|
|
64
|
-
},
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
const createRemoteExecApproval = (input: {
|
|
68
|
-
server: string
|
|
69
|
-
command: string
|
|
70
|
-
cwd?: string
|
|
71
|
-
timeout?: number
|
|
72
|
-
}): ApprovalRequest | null => {
|
|
73
|
-
const classification = classifyRemoteExec(input.command)
|
|
74
|
-
if (classification.decision !== "approval-required") {
|
|
75
|
-
return null
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
permission: "bash",
|
|
80
|
-
patterns: [input.command],
|
|
81
|
-
always: [],
|
|
82
|
-
metadata: {
|
|
83
|
-
tool: "remote_exec",
|
|
84
|
-
server: input.server,
|
|
85
|
-
command: input.command,
|
|
86
|
-
cwd: input.cwd,
|
|
87
|
-
timeout: input.timeout,
|
|
88
|
-
reason: classification.reason,
|
|
89
|
-
},
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const createTools = (orchestrator: ReturnType<typeof createOrchestrator>) => ({
|
|
94
|
-
list_servers: tool({
|
|
95
|
-
description: "List configured remote servers.",
|
|
96
|
-
args: {},
|
|
97
|
-
execute: async () => serialize(orchestrator.listServers()),
|
|
98
|
-
}),
|
|
99
|
-
remote_exec: tool({
|
|
100
|
-
description: "Execute a shell command on a remote server.",
|
|
101
|
-
args: {
|
|
102
|
-
server: tool.schema.string(),
|
|
103
|
-
command: tool.schema.string(),
|
|
104
|
-
cwd: tool.schema.string().optional(),
|
|
105
|
-
timeout: tool.schema.number().int().positive().optional(),
|
|
106
|
-
},
|
|
107
|
-
execute: async ({ server, command, cwd, timeout }, context) => {
|
|
108
|
-
const rejected = await requestApproval(
|
|
109
|
-
context,
|
|
110
|
-
"remote_exec",
|
|
111
|
-
server,
|
|
112
|
-
createRemoteExecApproval({ server, command, cwd, timeout }),
|
|
113
|
-
)
|
|
114
|
-
if (rejected) {
|
|
115
|
-
return rejected
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return serialize(orchestrator.remoteExec({ server, command, cwd, timeout }))
|
|
119
|
-
},
|
|
120
|
-
}),
|
|
121
|
-
remote_read_file: tool({
|
|
122
|
-
description: "Read a remote file.",
|
|
123
|
-
args: {
|
|
124
|
-
server: tool.schema.string(),
|
|
125
|
-
path: tool.schema.string(),
|
|
126
|
-
offset: tool.schema.number().int().nonnegative().optional(),
|
|
127
|
-
length: tool.schema.number().int().positive().optional(),
|
|
128
|
-
},
|
|
129
|
-
execute: async ({ server, path, offset, length }) =>
|
|
130
|
-
serialize(orchestrator.remoteReadFile({ server, path, offset, length })),
|
|
131
|
-
}),
|
|
132
|
-
remote_write_file: tool({
|
|
133
|
-
description: "Write content to a remote file.",
|
|
134
|
-
args: {
|
|
135
|
-
server: tool.schema.string(),
|
|
136
|
-
path: tool.schema.string(),
|
|
137
|
-
content: tool.schema.string(),
|
|
138
|
-
mode: tool.schema.number().int().positive().optional(),
|
|
139
|
-
},
|
|
140
|
-
execute: async ({ server, path, content, mode }, context) => {
|
|
141
|
-
const rejected = await requestApproval(
|
|
142
|
-
context,
|
|
143
|
-
"remote_write_file",
|
|
144
|
-
server,
|
|
145
|
-
createEditApproval("remote_write_file", { server, path, content, mode }),
|
|
146
|
-
)
|
|
147
|
-
if (rejected) {
|
|
148
|
-
return rejected
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return serialize(orchestrator.remoteWriteFile({ server, path, content, mode }))
|
|
152
|
-
},
|
|
153
|
-
}),
|
|
154
|
-
remote_patch_file: tool({
|
|
155
|
-
description: "Apply a unified diff to a remote file.",
|
|
156
|
-
args: {
|
|
157
|
-
server: tool.schema.string(),
|
|
158
|
-
path: tool.schema.string(),
|
|
159
|
-
patch: tool.schema.string(),
|
|
160
|
-
},
|
|
161
|
-
execute: async ({ server, path, patch }, context) => {
|
|
162
|
-
const rejected = await requestApproval(
|
|
163
|
-
context,
|
|
164
|
-
"remote_patch_file",
|
|
165
|
-
server,
|
|
166
|
-
createEditApproval("remote_patch_file", { server, path, patch }),
|
|
167
|
-
)
|
|
168
|
-
if (rejected) {
|
|
169
|
-
return rejected
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return serialize(orchestrator.remotePatchFile({ server, path, patch }))
|
|
173
|
-
},
|
|
174
|
-
}),
|
|
175
|
-
remote_list_dir: tool({
|
|
176
|
-
description: "List a remote directory.",
|
|
177
|
-
args: {
|
|
178
|
-
server: tool.schema.string(),
|
|
179
|
-
path: tool.schema.string(),
|
|
180
|
-
recursive: tool.schema.boolean().optional(),
|
|
181
|
-
limit: tool.schema.number().int().positive().optional(),
|
|
182
|
-
},
|
|
183
|
-
execute: async ({ server, path, recursive, limit }) =>
|
|
184
|
-
serialize(orchestrator.remoteListDir({ server, path, recursive, limit })),
|
|
185
|
-
}),
|
|
186
|
-
remote_stat: tool({
|
|
187
|
-
description: "Stat a remote path.",
|
|
188
|
-
args: {
|
|
189
|
-
server: tool.schema.string(),
|
|
190
|
-
path: tool.schema.string(),
|
|
191
|
-
},
|
|
192
|
-
execute: async ({ server, path }) => serialize(orchestrator.remoteStat({ server, path })),
|
|
193
|
-
}),
|
|
194
|
-
remote_find: tool({
|
|
195
|
-
description: "Search a remote directory for matching files or content.",
|
|
196
|
-
args: {
|
|
197
|
-
server: tool.schema.string(),
|
|
198
|
-
path: tool.schema.string(),
|
|
199
|
-
pattern: tool.schema.string(),
|
|
200
|
-
glob: tool.schema.string().optional(),
|
|
201
|
-
limit: tool.schema.number().int().positive().optional(),
|
|
202
|
-
},
|
|
203
|
-
execute: async ({ server, path, pattern, glob, limit }) =>
|
|
204
|
-
serialize(orchestrator.remoteFind({ server, path, pattern, glob, limit })),
|
|
205
|
-
}),
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
const buildRuntimeDependencies = (workspaceRoot: string): RuntimeDependencies => {
|
|
209
|
-
const paths = createRuntimePaths(workspaceRoot)
|
|
210
|
-
const registry = createServerRegistry({
|
|
211
|
-
globalRegistryFile: paths.globalRegistryFile,
|
|
212
|
-
workspaceRegistryFile: paths.workspaceRegistryFile,
|
|
213
|
-
workspaceRoot,
|
|
214
|
-
})
|
|
215
|
-
const auditLog = createAuditLogStore(paths.auditLogFile)
|
|
216
|
-
const auditRepo = createGitAuditRepo(paths.auditRepoDir)
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
registry,
|
|
220
|
-
ssh: createSshRuntime(),
|
|
221
|
-
audit: {
|
|
222
|
-
preflightLog: () => auditLog.preflight(),
|
|
223
|
-
appendLog: (entry) => auditLog.append(entry),
|
|
224
|
-
preflightSnapshots: () => auditRepo.preflight(),
|
|
225
|
-
captureSnapshots: (input) => auditRepo.captureChange(input),
|
|
226
|
-
},
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export const createOpenCodePlugin = (options: OpenCodePluginOptions = {}): Plugin => async (input) => {
|
|
231
|
-
await (options.ensureRuntimeDirs ?? ensureRuntimeDirs)()
|
|
232
|
-
|
|
233
|
-
const orchestrator = createOrchestrator(
|
|
234
|
-
(options.createRuntimeDependencies ?? buildRuntimeDependencies)(input.worktree),
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
return {
|
|
238
|
-
tool: createTools(orchestrator),
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export const OpenCodePlugin: Plugin = createOpenCodePlugin()
|
package/src/product/install.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { mkdir } from "node:fs/promises"
|
|
2
|
-
import { cwd, stdout } from "node:process"
|
|
3
|
-
import { createRuntimePaths } from "../core/paths.js"
|
|
4
|
-
import { installIntoOpenCodeConfig } from "./opencode-config.js"
|
|
5
|
-
import { createWorkspaceTracker } from "./workspace-tracker.js"
|
|
6
|
-
|
|
7
|
-
type WritableLike = {
|
|
8
|
-
write(chunk: string): void
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
type RuntimePaths = ReturnType<typeof createRuntimePaths>
|
|
12
|
-
|
|
13
|
-
type InstallOptions = {
|
|
14
|
-
runtimePaths: RuntimePaths
|
|
15
|
-
stdout: WritableLike
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const installOpenShell = async ({ runtimePaths, stdout }: InstallOptions) => {
|
|
19
|
-
await mkdir(runtimePaths.configDir, { recursive: true })
|
|
20
|
-
await mkdir(runtimePaths.dataDir, { recursive: true })
|
|
21
|
-
await mkdir(runtimePaths.opencodeConfigDir, { recursive: true })
|
|
22
|
-
|
|
23
|
-
await installIntoOpenCodeConfig(runtimePaths.opencodeConfigFile)
|
|
24
|
-
await createWorkspaceTracker(runtimePaths.workspaceTrackerFile).clear()
|
|
25
|
-
|
|
26
|
-
stdout.write(
|
|
27
|
-
[
|
|
28
|
-
"Installed openshell.",
|
|
29
|
-
`OpenShell config: ${runtimePaths.configDir}`,
|
|
30
|
-
`OpenShell data: ${runtimePaths.dataDir}`,
|
|
31
|
-
`OpenCode config: ${runtimePaths.opencodeConfigFile}`,
|
|
32
|
-
].join("\n") + "\n",
|
|
33
|
-
)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export const runInstallCli = async (_argv: string[] = [], stream: WritableLike = stdout) => {
|
|
37
|
-
await installOpenShell({
|
|
38
|
-
runtimePaths: createRuntimePaths(cwd()),
|
|
39
|
-
stdout: stream,
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
return 0
|
|
43
|
-
}
|