@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,476 +0,0 @@
|
|
|
1
|
-
import { access } from "node:fs/promises"
|
|
2
|
-
import { createInterface } from "node:readline/promises"
|
|
3
|
-
import { stderr, stdin, stdout } from "node:process"
|
|
4
|
-
import { ensureRuntimeDirs, runtimePaths, workspaceRegistryFile } from "../core/paths.js"
|
|
5
|
-
import {
|
|
6
|
-
createServerRegistry,
|
|
7
|
-
type RegistryScope,
|
|
8
|
-
type ResolvedServerRecord,
|
|
9
|
-
type ServerRecord,
|
|
10
|
-
type ServerRegistry,
|
|
11
|
-
} from "../core/registry/server-registry.js"
|
|
12
|
-
import { createWorkspaceTracker, type WorkspaceTracker } from "../product/workspace-tracker.js"
|
|
13
|
-
|
|
14
|
-
type PromptAdapter = {
|
|
15
|
-
text(message: string, defaultValue?: string): Promise<string>
|
|
16
|
-
password(message: string): Promise<string>
|
|
17
|
-
confirm(message: string, defaultValue?: boolean): Promise<boolean>
|
|
18
|
-
close?(): Promise<void> | void
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
type WritableLike = {
|
|
22
|
-
write(chunk: string): void
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
type CliDeps = {
|
|
26
|
-
registry: Pick<ServerRegistry, "list" | "resolve" | "listRaw" | "upsert" | "remove">
|
|
27
|
-
workspaceTracker?: Pick<WorkspaceTracker, "record" | "remove">
|
|
28
|
-
prompt: PromptAdapter
|
|
29
|
-
stdout: WritableLike
|
|
30
|
-
stderr: WritableLike
|
|
31
|
-
workspaceRoot: string
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const usage = [
|
|
35
|
-
"Usage: bun run server-registry <add|list|remove>",
|
|
36
|
-
"",
|
|
37
|
-
"Commands:",
|
|
38
|
-
" add interactively add or update a server across workspace/global scopes",
|
|
39
|
-
" list print configured servers with scope metadata",
|
|
40
|
-
" remove remove a configured server by id and scope",
|
|
41
|
-
].join("\n")
|
|
42
|
-
|
|
43
|
-
const parseList = (input: string) => {
|
|
44
|
-
const values = input
|
|
45
|
-
.split(",")
|
|
46
|
-
.map((value) => value.trim())
|
|
47
|
-
.filter(Boolean)
|
|
48
|
-
|
|
49
|
-
return values.length === 0 ? undefined : values
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const describeServer = (record: Pick<ServerRecord, "id" | "host" | "port" | "username">) =>
|
|
53
|
-
`${record.id} (${record.host}:${record.port} as ${record.username})`
|
|
54
|
-
|
|
55
|
-
const createConsolePrompt = (): PromptAdapter => {
|
|
56
|
-
const askText = async (message: string) => {
|
|
57
|
-
const rl = createInterface({ input: stdin, output: stdout })
|
|
58
|
-
try {
|
|
59
|
-
return await rl.question(message)
|
|
60
|
-
} finally {
|
|
61
|
-
rl.close()
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
async text(message, defaultValue) {
|
|
67
|
-
const suffix = defaultValue === undefined ? "" : ` [${defaultValue}]`
|
|
68
|
-
const answer = (await askText(`${message}${suffix}: `)).trim()
|
|
69
|
-
return answer === "" ? (defaultValue ?? "") : answer
|
|
70
|
-
},
|
|
71
|
-
async password(message) {
|
|
72
|
-
if (!stdin.isTTY || typeof stdin.setRawMode !== "function") {
|
|
73
|
-
return askText(`${message}: `)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
stdout.write(`${message}: `)
|
|
77
|
-
stdin.resume()
|
|
78
|
-
stdin.setEncoding("utf8")
|
|
79
|
-
stdin.setRawMode(true)
|
|
80
|
-
|
|
81
|
-
return await new Promise<string>((resolve, reject) => {
|
|
82
|
-
let value = ""
|
|
83
|
-
|
|
84
|
-
const cleanup = () => {
|
|
85
|
-
stdin.off("data", onData)
|
|
86
|
-
stdin.setRawMode(false)
|
|
87
|
-
stdin.pause()
|
|
88
|
-
stdout.write("\n")
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const onData = (chunk: string | Buffer) => {
|
|
92
|
-
const char = chunk.toString("utf8")
|
|
93
|
-
if (char === "\u0003") {
|
|
94
|
-
cleanup()
|
|
95
|
-
reject(new Error("prompt cancelled"))
|
|
96
|
-
return
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (char === "\r" || char === "\n") {
|
|
100
|
-
cleanup()
|
|
101
|
-
resolve(value)
|
|
102
|
-
return
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (char === "\u007f" || char === "\b") {
|
|
106
|
-
value = value.slice(0, -1)
|
|
107
|
-
return
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
value += char
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
stdin.on("data", onData)
|
|
114
|
-
})
|
|
115
|
-
},
|
|
116
|
-
async confirm(message, defaultValue = false) {
|
|
117
|
-
const suffix = defaultValue ? " [Y/n]" : " [y/N]"
|
|
118
|
-
|
|
119
|
-
while (true) {
|
|
120
|
-
const answer = (await askText(`${message}${suffix}: `)).trim().toLowerCase()
|
|
121
|
-
if (answer === "") {
|
|
122
|
-
return defaultValue
|
|
123
|
-
}
|
|
124
|
-
if (answer === "y" || answer === "yes") {
|
|
125
|
-
return true
|
|
126
|
-
}
|
|
127
|
-
if (answer === "n" || answer === "no") {
|
|
128
|
-
return false
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
},
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const createDefaultDeps = async (): Promise<CliDeps> => {
|
|
136
|
-
await ensureRuntimeDirs()
|
|
137
|
-
const workspaceRoot = process.cwd()
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
registry: createServerRegistry({
|
|
141
|
-
globalRegistryFile: runtimePaths.globalRegistryFile,
|
|
142
|
-
workspaceRegistryFile: workspaceRegistryFile(workspaceRoot),
|
|
143
|
-
workspaceRoot,
|
|
144
|
-
}),
|
|
145
|
-
workspaceTracker: createWorkspaceTracker(runtimePaths.workspaceTrackerFile),
|
|
146
|
-
prompt: createConsolePrompt(),
|
|
147
|
-
stdout: { write: (chunk) => stdout.write(chunk) },
|
|
148
|
-
stderr: { write: (chunk) => stderr.write(chunk) },
|
|
149
|
-
workspaceRoot,
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const getRawRecord = async (registry: CliDeps["registry"], scope: RegistryScope, id: string) =>
|
|
154
|
-
(await registry.listRaw(scope)).find((record) => record.id === id) ?? null
|
|
155
|
-
|
|
156
|
-
const workspaceScopeExists = async (workspaceRoot: string) => {
|
|
157
|
-
try {
|
|
158
|
-
await access(workspaceRegistryFile(workspaceRoot))
|
|
159
|
-
return true
|
|
160
|
-
} catch (error: unknown) {
|
|
161
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
162
|
-
return false
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
throw error
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const promptScope = async (
|
|
170
|
-
deps: CliDeps,
|
|
171
|
-
message: string,
|
|
172
|
-
defaultScope: RegistryScope,
|
|
173
|
-
): Promise<RegistryScope> => {
|
|
174
|
-
while (true) {
|
|
175
|
-
const answer = (await deps.prompt.text(message, defaultScope)).trim().toLowerCase()
|
|
176
|
-
if (answer === "") {
|
|
177
|
-
return defaultScope
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (answer === "global" || answer === "g") {
|
|
181
|
-
return "global"
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (answer === "workspace" || answer === "w") {
|
|
185
|
-
return "workspace"
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
deps.stderr.write(`Invalid scope: ${answer}\n`)
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const promptAuthKind = async (
|
|
193
|
-
deps: CliDeps,
|
|
194
|
-
defaultKind: ServerRecord["auth"]["kind"] = "password",
|
|
195
|
-
): Promise<ServerRecord["auth"]["kind"]> => {
|
|
196
|
-
while (true) {
|
|
197
|
-
const answer = (await deps.prompt.text("Auth kind (password/privateKey/certificate)", defaultKind)).trim()
|
|
198
|
-
if (answer === "") {
|
|
199
|
-
return defaultKind
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
switch (answer.toLowerCase()) {
|
|
203
|
-
case "password":
|
|
204
|
-
return "password"
|
|
205
|
-
case "privatekey":
|
|
206
|
-
return "privateKey"
|
|
207
|
-
case "certificate":
|
|
208
|
-
return "certificate"
|
|
209
|
-
default:
|
|
210
|
-
deps.stderr.write(`Invalid auth kind: ${answer}\n`)
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const promptAuth = async (
|
|
216
|
-
deps: CliDeps,
|
|
217
|
-
kind: ServerRecord["auth"]["kind"],
|
|
218
|
-
existingAuth?: ServerRecord["auth"],
|
|
219
|
-
): Promise<ServerRecord["auth"] | null> => {
|
|
220
|
-
if (kind === "password") {
|
|
221
|
-
const secret = await deps.prompt.password("Password")
|
|
222
|
-
if (!secret) {
|
|
223
|
-
deps.stderr.write("Password is required.\n")
|
|
224
|
-
return null
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
deps.stdout.write("Warning: plain-text password will be stored as-is.\n")
|
|
228
|
-
return { kind, secret }
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (kind === "privateKey") {
|
|
232
|
-
const privateKeyPath = await deps.prompt.text("Private key path", existingAuth?.kind === "privateKey" ? existingAuth.privateKeyPath : undefined)
|
|
233
|
-
if (!privateKeyPath) {
|
|
234
|
-
deps.stderr.write("Private key path is required.\n")
|
|
235
|
-
return null
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const passphrase = (await deps.prompt.text(
|
|
239
|
-
"Passphrase (optional)",
|
|
240
|
-
existingAuth && "passphrase" in existingAuth ? existingAuth.passphrase ?? "" : "",
|
|
241
|
-
)).trim()
|
|
242
|
-
|
|
243
|
-
return {
|
|
244
|
-
kind,
|
|
245
|
-
privateKeyPath,
|
|
246
|
-
...(passphrase ? { passphrase } : {}),
|
|
247
|
-
} as ServerRecord["auth"]
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const certificatePath = await deps.prompt.text(
|
|
251
|
-
"Certificate path",
|
|
252
|
-
existingAuth?.kind === "certificate" ? existingAuth.certificatePath : undefined,
|
|
253
|
-
)
|
|
254
|
-
if (!certificatePath) {
|
|
255
|
-
deps.stderr.write("Certificate path is required.\n")
|
|
256
|
-
return null
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const privateKeyPath = await deps.prompt.text(
|
|
260
|
-
"Private key path",
|
|
261
|
-
existingAuth?.kind === "certificate" ? existingAuth.privateKeyPath : undefined,
|
|
262
|
-
)
|
|
263
|
-
if (!privateKeyPath) {
|
|
264
|
-
deps.stderr.write("Private key path is required.\n")
|
|
265
|
-
return null
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const passphrase = (await deps.prompt.text(
|
|
269
|
-
"Passphrase (optional)",
|
|
270
|
-
existingAuth && "passphrase" in existingAuth ? existingAuth.passphrase ?? "" : "",
|
|
271
|
-
)).trim()
|
|
272
|
-
|
|
273
|
-
return {
|
|
274
|
-
kind,
|
|
275
|
-
certificatePath,
|
|
276
|
-
privateKeyPath,
|
|
277
|
-
...(passphrase ? { passphrase } : {}),
|
|
278
|
-
} as ServerRecord["auth"]
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const handleAdd = async (deps: CliDeps, idArg?: string) => {
|
|
282
|
-
const id = await deps.prompt.text("Server id", idArg)
|
|
283
|
-
if (!id) {
|
|
284
|
-
deps.stderr.write("Server id is required.\n")
|
|
285
|
-
return 1
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const defaultScope = (await workspaceScopeExists(deps.workspaceRoot)) ? "workspace" : "global"
|
|
289
|
-
const scope = await promptScope(deps, "Server scope (global/workspace)", defaultScope)
|
|
290
|
-
|
|
291
|
-
const existing = await getRawRecord(deps.registry, scope, id)
|
|
292
|
-
const resolvedExisting = await deps.registry.resolve(id)
|
|
293
|
-
|
|
294
|
-
if (existing) {
|
|
295
|
-
const overwrite = await deps.prompt.confirm(`Overwrite existing server ${describeServer(existing)}?`)
|
|
296
|
-
if (!overwrite) {
|
|
297
|
-
deps.stdout.write("Cancelled.\n")
|
|
298
|
-
return 0
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (scope === "workspace" && resolvedExisting?.scope === "global") {
|
|
303
|
-
deps.stdout.write(
|
|
304
|
-
`Warning: workspace record ${id} will override global entry ${describeServer(resolvedExisting)}.\n`,
|
|
305
|
-
)
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const host = await deps.prompt.text("Host", existing?.host ?? resolvedExisting?.host)
|
|
309
|
-
if (!host) {
|
|
310
|
-
deps.stderr.write("Host is required.\n")
|
|
311
|
-
return 1
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const portRaw = await deps.prompt.text("Port", String(existing?.port ?? resolvedExisting?.port ?? 22))
|
|
315
|
-
const port = Number.parseInt(portRaw, 10)
|
|
316
|
-
if (!Number.isInteger(port) || port <= 0) {
|
|
317
|
-
deps.stderr.write(`Invalid port: ${portRaw}\n`)
|
|
318
|
-
return 1
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const username = await deps.prompt.text("Username", existing?.username ?? resolvedExisting?.username)
|
|
322
|
-
if (!username) {
|
|
323
|
-
deps.stderr.write("Username is required.\n")
|
|
324
|
-
return 1
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const labels = parseList(
|
|
328
|
-
await deps.prompt.text("Labels (comma-separated)", existing?.labels?.join(",") ?? resolvedExisting?.labels?.join(",") ?? ""),
|
|
329
|
-
)
|
|
330
|
-
const groups = parseList(
|
|
331
|
-
await deps.prompt.text("Groups (comma-separated)", existing?.groups?.join(",") ?? resolvedExisting?.groups?.join(",") ?? ""),
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
const authKind = await promptAuthKind(deps, existing?.auth.kind ?? resolvedExisting?.auth.kind ?? "password")
|
|
335
|
-
const auth = await promptAuth(deps, authKind, existing?.auth ?? resolvedExisting?.auth)
|
|
336
|
-
if (!auth) {
|
|
337
|
-
return 1
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
await deps.registry.upsert(scope, {
|
|
341
|
-
id,
|
|
342
|
-
host,
|
|
343
|
-
port,
|
|
344
|
-
username,
|
|
345
|
-
...(labels ? { labels } : {}),
|
|
346
|
-
...(groups ? { groups } : {}),
|
|
347
|
-
auth,
|
|
348
|
-
} as ServerRecord)
|
|
349
|
-
|
|
350
|
-
if (scope === "workspace") {
|
|
351
|
-
await deps.workspaceTracker?.record({
|
|
352
|
-
workspaceRoot: deps.workspaceRoot,
|
|
353
|
-
managedPath: `${deps.workspaceRoot}/.open-code`,
|
|
354
|
-
})
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
deps.stdout.write(`Saved server ${id} (${host}:${port}).\n`)
|
|
358
|
-
return 0
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const handleList = async (deps: CliDeps) => {
|
|
362
|
-
const records = await deps.registry.list()
|
|
363
|
-
if (records.length === 0) {
|
|
364
|
-
deps.stdout.write("No servers configured.\n")
|
|
365
|
-
return 0
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
deps.stdout.write("ID\tSCOPE\tSTATUS\tHOST\tPORT\tUSERNAME\tLABELS\tGROUPS\n")
|
|
369
|
-
for (const record of records as ResolvedServerRecord[]) {
|
|
370
|
-
deps.stdout.write(
|
|
371
|
-
[
|
|
372
|
-
record.id,
|
|
373
|
-
record.scope,
|
|
374
|
-
record.shadowingGlobal ? "shadowing global" : "",
|
|
375
|
-
record.host,
|
|
376
|
-
String(record.port),
|
|
377
|
-
record.username,
|
|
378
|
-
(record.labels ?? []).join(","),
|
|
379
|
-
(record.groups ?? []).join(","),
|
|
380
|
-
].join("\t") + "\n",
|
|
381
|
-
)
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
return 0
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const handleRemove = async (deps: CliDeps, idArg?: string) => {
|
|
388
|
-
const [globalRecords, workspaceRecords] = await Promise.all([
|
|
389
|
-
deps.registry.listRaw("global"),
|
|
390
|
-
deps.registry.listRaw("workspace"),
|
|
391
|
-
])
|
|
392
|
-
|
|
393
|
-
if (globalRecords.length === 0 && workspaceRecords.length === 0) {
|
|
394
|
-
deps.stdout.write("No servers configured.\n")
|
|
395
|
-
return 0
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const id = idArg ?? (await deps.prompt.text("Server id to remove"))
|
|
399
|
-
if (!id) {
|
|
400
|
-
deps.stderr.write("Server id is required.\n")
|
|
401
|
-
return 1
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
const globalRecord = globalRecords.find((record) => record.id === id) ?? null
|
|
405
|
-
const workspaceRecord = workspaceRecords.find((record) => record.id === id) ?? null
|
|
406
|
-
|
|
407
|
-
if (!globalRecord && !workspaceRecord) {
|
|
408
|
-
deps.stderr.write(`Server ${id} not found.\n`)
|
|
409
|
-
return 1
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
let scope: RegistryScope
|
|
413
|
-
if (globalRecord && workspaceRecord) {
|
|
414
|
-
scope = await promptScope(deps, "Remove from which scope (global/workspace)", "workspace")
|
|
415
|
-
} else {
|
|
416
|
-
scope = workspaceRecord ? "workspace" : "global"
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const existing = scope === "workspace" ? workspaceRecord : globalRecord
|
|
420
|
-
if (!existing) {
|
|
421
|
-
deps.stderr.write(`Server ${id} not found in ${scope}.\n`)
|
|
422
|
-
return 1
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const confirmed = await deps.prompt.confirm(`Remove server ${describeServer(existing)}?`)
|
|
426
|
-
if (!confirmed) {
|
|
427
|
-
deps.stdout.write("Cancelled.\n")
|
|
428
|
-
return 0
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const removed = await deps.registry.remove(scope, id)
|
|
432
|
-
if (!removed) {
|
|
433
|
-
deps.stderr.write(`Server ${id} not found in ${scope}.\n`)
|
|
434
|
-
return 1
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
if (scope === "workspace") {
|
|
438
|
-
const remainingWorkspaceRecords = await deps.registry.listRaw("workspace")
|
|
439
|
-
if (remainingWorkspaceRecords.length === 0) {
|
|
440
|
-
await deps.workspaceTracker?.remove(deps.workspaceRoot)
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
deps.stdout.write(`Removed server ${id} from ${scope}.\n`)
|
|
445
|
-
return 0
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
export const runServerRegistryCli = async (argv: string[], deps?: CliDeps) => {
|
|
449
|
-
let activeDeps: CliDeps
|
|
450
|
-
try {
|
|
451
|
-
activeDeps = deps ?? (await createDefaultDeps())
|
|
452
|
-
} catch (err) {
|
|
453
|
-
console.error(`Failed to initialize: ${err instanceof Error ? err.message : err}`)
|
|
454
|
-
return 1
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
try {
|
|
458
|
-
const [command, arg] = argv
|
|
459
|
-
|
|
460
|
-
switch (command) {
|
|
461
|
-
case "add":
|
|
462
|
-
return await handleAdd(activeDeps, arg)
|
|
463
|
-
case "list":
|
|
464
|
-
return await handleList(activeDeps)
|
|
465
|
-
case "remove":
|
|
466
|
-
return await handleRemove(activeDeps, arg)
|
|
467
|
-
default:
|
|
468
|
-
activeDeps.stderr.write(`${usage}\n`)
|
|
469
|
-
return 1
|
|
470
|
-
}
|
|
471
|
-
} finally {
|
|
472
|
-
await activeDeps.prompt.close?.()
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
export const main = async (argv: string[] = process.argv.slice(2)) => runServerRegistryCli(argv)
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises"
|
|
2
|
-
import { dirname, join } from "node:path"
|
|
3
|
-
|
|
4
|
-
const run = async (cwd: string, args: string[]) => {
|
|
5
|
-
const proc = Bun.spawn(["git", ...args], { cwd, stderr: "pipe", stdout: "pipe" })
|
|
6
|
-
const exitCode = await proc.exited
|
|
7
|
-
if (exitCode !== 0) throw new Error(await new Response(proc.stderr).text())
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const encodeSegment = (segment: string) =>
|
|
11
|
-
`s-${Buffer.from(segment, "utf8").toString("base64url")}`
|
|
12
|
-
|
|
13
|
-
const snapshotParts = (server: string, path: string) => [
|
|
14
|
-
encodeSegment(server),
|
|
15
|
-
...path
|
|
16
|
-
.split("/")
|
|
17
|
-
.filter((segment) => segment.length > 0)
|
|
18
|
-
.map(encodeSegment),
|
|
19
|
-
]
|
|
20
|
-
|
|
21
|
-
export const createGitAuditRepo = (repoDir: string) => ({
|
|
22
|
-
async preflight() {
|
|
23
|
-
await mkdir(repoDir, { recursive: true })
|
|
24
|
-
await run(repoDir, ["init"])
|
|
25
|
-
await run(repoDir, ["config", "user.name", "Open Code"])
|
|
26
|
-
await run(repoDir, ["config", "user.email", "open-code@local"])
|
|
27
|
-
},
|
|
28
|
-
async captureChange(input: { server: string; path: string; before: string; after: string }) {
|
|
29
|
-
const parts = snapshotParts(input.server, input.path)
|
|
30
|
-
const relativeBase = parts.join("/")
|
|
31
|
-
const base = join(repoDir, ...parts)
|
|
32
|
-
await mkdir(dirname(base), { recursive: true })
|
|
33
|
-
await writeFile(`${base}.before`, input.before)
|
|
34
|
-
await writeFile(`${base}.after`, input.after)
|
|
35
|
-
await run(repoDir, ["add", "--", `${relativeBase}.before`, `${relativeBase}.after`])
|
|
36
|
-
await run(repoDir, ["commit", "--allow-empty", "-m", `audit: ${input.server} ${input.path}`])
|
|
37
|
-
},
|
|
38
|
-
async lastCommitMessage() {
|
|
39
|
-
const proc = Bun.spawn(["git", "log", "-1", "--pretty=%s"], { cwd: repoDir, stdout: "pipe" })
|
|
40
|
-
return (await new Response(proc.stdout).text()).trim()
|
|
41
|
-
},
|
|
42
|
-
})
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { appendFile, mkdir } from "node:fs/promises"
|
|
2
|
-
import { dirname } from "node:path"
|
|
3
|
-
import { redactSecrets } from "./redact"
|
|
4
|
-
|
|
5
|
-
export const createAuditLogStore = (file: string) => ({
|
|
6
|
-
async preflight() {
|
|
7
|
-
await mkdir(dirname(file), { recursive: true })
|
|
8
|
-
await appendFile(file, "")
|
|
9
|
-
},
|
|
10
|
-
async append(entry: Record<string, unknown>) {
|
|
11
|
-
const stamped = {
|
|
12
|
-
...entry,
|
|
13
|
-
timestamp: new Date().toISOString(),
|
|
14
|
-
}
|
|
15
|
-
const json = JSON.stringify(stamped, (_key, value) =>
|
|
16
|
-
typeof value === "string" ? redactSecrets(value) : value,
|
|
17
|
-
)
|
|
18
|
-
await appendFile(file, `${json}\n`)
|
|
19
|
-
},
|
|
20
|
-
})
|
package/src/core/audit/redact.ts
DELETED
package/src/core/contracts.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
export type ServerID = string
|
|
2
|
-
|
|
3
|
-
export type ApprovalDecision = "allow" | "deny"
|
|
4
|
-
export type PolicyDecision = "auto-allow" | "approval-required" | "reject"
|
|
5
|
-
export type ToolErrorCode =
|
|
6
|
-
| "APPROVAL_REJECTED"
|
|
7
|
-
| "AUDIT_LOG_PREFLIGHT_FAILED"
|
|
8
|
-
| "AUDIT_SNAPSHOT_PREFLIGHT_FAILED"
|
|
9
|
-
| "AUTH_PATH_INVALID"
|
|
10
|
-
| "AUTH_PATH_UNREADABLE"
|
|
11
|
-
| "CERTIFICATE_PATH_NOT_FOUND"
|
|
12
|
-
| "KEY_PATH_NOT_FOUND"
|
|
13
|
-
| "PATCH_APPLY_FAILED"
|
|
14
|
-
| "POLICY_REJECTED"
|
|
15
|
-
| "REGISTRY_RECORD_INVALID"
|
|
16
|
-
| "REGISTRY_LIST_FAILED"
|
|
17
|
-
| "SERVER_NOT_FOUND"
|
|
18
|
-
| "SERVER_RESOLVE_FAILED"
|
|
19
|
-
| "SSH_EXEC_FAILED"
|
|
20
|
-
| "SSH_FIND_FAILED"
|
|
21
|
-
| "SSH_LIST_FAILED"
|
|
22
|
-
| "SSH_READ_FAILED"
|
|
23
|
-
| "SSH_STAT_FAILED"
|
|
24
|
-
| "SSH_WRITE_FAILED"
|
|
25
|
-
|
|
26
|
-
export type ToolStatus = "ok" | "partial_failure" | "error"
|
|
27
|
-
|
|
28
|
-
export interface ToolPayload<TData = unknown> {
|
|
29
|
-
tool: string
|
|
30
|
-
server?: ServerID
|
|
31
|
-
data?: TData
|
|
32
|
-
message?: string
|
|
33
|
-
code?: ToolErrorCode
|
|
34
|
-
execution?: {
|
|
35
|
-
attempted: boolean
|
|
36
|
-
completed: boolean
|
|
37
|
-
exitCode?: number
|
|
38
|
-
stdoutBytes?: number
|
|
39
|
-
stderrBytes?: number
|
|
40
|
-
stdoutTruncated?: boolean
|
|
41
|
-
stderrTruncated?: boolean
|
|
42
|
-
}
|
|
43
|
-
audit?: {
|
|
44
|
-
logWritten: boolean
|
|
45
|
-
snapshotStatus: "not-applicable" | "written" | "partial-failure"
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface ToolResult<TData = unknown> extends ToolPayload<TData> {
|
|
50
|
-
status: ToolStatus
|
|
51
|
-
}
|