@junwu168/openshell 0.1.3 → 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/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 -25
- 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 -65
- 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,673 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from "bun:test"
|
|
2
|
-
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
|
|
3
|
-
import { tmpdir } from "node:os"
|
|
4
|
-
import { join } from "node:path"
|
|
5
|
-
import type { ServerRecord } from "../../src/core/registry/server-registry"
|
|
6
|
-
|
|
7
|
-
type Scope = "global" | "workspace"
|
|
8
|
-
|
|
9
|
-
type PromptCall = {
|
|
10
|
-
kind: "text" | "password" | "confirm"
|
|
11
|
-
message: string
|
|
12
|
-
defaultValue?: string | boolean
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const tempDirs: string[] = []
|
|
16
|
-
|
|
17
|
-
const cleanupTempDirs = async () => {
|
|
18
|
-
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })))
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
afterEach(async () => {
|
|
22
|
-
await cleanupTempDirs()
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
const createWorkspaceRoot = async (withWorkspaceConfig: boolean) => {
|
|
26
|
-
const tempDir = await mkdtemp(join(tmpdir(), "open-code-cli-"))
|
|
27
|
-
tempDirs.push(tempDir)
|
|
28
|
-
|
|
29
|
-
const workspaceRoot = join(tempDir, "repo")
|
|
30
|
-
await mkdir(join(workspaceRoot, ".open-code"), { recursive: true })
|
|
31
|
-
if (withWorkspaceConfig) {
|
|
32
|
-
await writeFile(join(workspaceRoot, ".open-code", "servers.json"), "[]")
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return workspaceRoot
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const mergeRecords = (records: Record<Scope, ServerRecord[]>) => {
|
|
39
|
-
const resolved = new Map<string, ServerRecord & { scope: Scope; shadowingGlobal?: boolean }>()
|
|
40
|
-
const order: string[] = []
|
|
41
|
-
|
|
42
|
-
for (const record of records.global) {
|
|
43
|
-
if (!resolved.has(record.id)) {
|
|
44
|
-
order.push(record.id)
|
|
45
|
-
}
|
|
46
|
-
resolved.set(record.id, { ...record, scope: "global" })
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
for (const record of records.workspace) {
|
|
50
|
-
if (!resolved.has(record.id)) {
|
|
51
|
-
order.push(record.id)
|
|
52
|
-
}
|
|
53
|
-
resolved.set(record.id, {
|
|
54
|
-
...record,
|
|
55
|
-
scope: "workspace",
|
|
56
|
-
...(records.global.some((item) => item.id === record.id) ? { shadowingGlobal: true } : {}),
|
|
57
|
-
})
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return order.map((id) => resolved.get(id)!).filter(Boolean)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const createInMemoryRegistry = (initial: Partial<Record<Scope, ServerRecord[]>> = {}) => {
|
|
64
|
-
const state: Record<Scope, Map<string, ServerRecord>> = {
|
|
65
|
-
global: new Map((initial.global ?? []).map((record) => [record.id, record] as const)),
|
|
66
|
-
workspace: new Map((initial.workspace ?? []).map((record) => [record.id, record] as const)),
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const resolvedList = () =>
|
|
70
|
-
mergeRecords({
|
|
71
|
-
global: [...state.global.values()],
|
|
72
|
-
workspace: [...state.workspace.values()],
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
async list() {
|
|
77
|
-
return resolvedList()
|
|
78
|
-
},
|
|
79
|
-
async resolve(id: string) {
|
|
80
|
-
return resolvedList().find((record) => record.id === id) ?? null
|
|
81
|
-
},
|
|
82
|
-
async upsert(scopeOrRecord: Scope | ServerRecord, maybeRecord?: ServerRecord) {
|
|
83
|
-
if (maybeRecord === undefined) {
|
|
84
|
-
state.global.set(scopeOrRecord.id, scopeOrRecord)
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
state[scopeOrRecord].set(maybeRecord.id, maybeRecord)
|
|
89
|
-
},
|
|
90
|
-
async remove(scopeOrId: Scope | string, maybeId?: string) {
|
|
91
|
-
if (maybeId === undefined) {
|
|
92
|
-
return state.global.delete(scopeOrId)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return state[scopeOrId].delete(maybeId)
|
|
96
|
-
},
|
|
97
|
-
async listRaw(scope: Scope) {
|
|
98
|
-
return [...state[scope].values()]
|
|
99
|
-
},
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const createPrompt = (resolve: (call: PromptCall) => string | boolean) => {
|
|
104
|
-
const calls: PromptCall[] = []
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
calls,
|
|
108
|
-
async text(message: string, defaultValue?: string) {
|
|
109
|
-
const call: PromptCall = { kind: "text", message, defaultValue }
|
|
110
|
-
calls.push(call)
|
|
111
|
-
const answer = resolve(call)
|
|
112
|
-
return typeof answer === "string" ? answer : String(answer)
|
|
113
|
-
},
|
|
114
|
-
async password(message: string) {
|
|
115
|
-
const call: PromptCall = { kind: "password", message }
|
|
116
|
-
calls.push(call)
|
|
117
|
-
const answer = resolve(call)
|
|
118
|
-
return typeof answer === "string" ? answer : String(answer)
|
|
119
|
-
},
|
|
120
|
-
async confirm(message: string, defaultValue = false) {
|
|
121
|
-
const call: PromptCall = { kind: "confirm", message, defaultValue }
|
|
122
|
-
calls.push(call)
|
|
123
|
-
const answer = resolve(call)
|
|
124
|
-
return typeof answer === "boolean" ? answer : Boolean(answer)
|
|
125
|
-
},
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const createWritable = () => {
|
|
130
|
-
let buffer = ""
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
write(chunk: string) {
|
|
134
|
-
buffer += chunk
|
|
135
|
-
},
|
|
136
|
-
toString() {
|
|
137
|
-
return buffer
|
|
138
|
-
},
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const runCli = async (argv: string[], deps: Record<string, unknown>) => {
|
|
143
|
-
const { runServerRegistryCli } = await import("../../src/cli/server-registry")
|
|
144
|
-
return runServerRegistryCli(argv, deps as never)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
describe("server registry cli", () => {
|
|
148
|
-
test("defaults add to workspace scope when a workspace config exists", async () => {
|
|
149
|
-
const workspaceRoot = await createWorkspaceRoot(true)
|
|
150
|
-
const registry = createInMemoryRegistry()
|
|
151
|
-
const prompt = createPrompt((call) => {
|
|
152
|
-
if (call.kind === "text" && call.message.includes("Server id")) return "prod-a"
|
|
153
|
-
if (call.kind === "text" && call.message.includes("Scope")) return ""
|
|
154
|
-
if (call.kind === "text" && call.message.includes("Host")) return "10.0.0.10"
|
|
155
|
-
if (call.kind === "text" && call.message.includes("Port")) return "22"
|
|
156
|
-
if (call.kind === "text" && call.message.includes("Username")) return "root"
|
|
157
|
-
if (call.kind === "text" && call.message.includes("Labels")) return ""
|
|
158
|
-
if (call.kind === "text" && call.message.includes("Groups")) return ""
|
|
159
|
-
if (call.kind === "text" && call.message.includes("Auth kind")) return "password"
|
|
160
|
-
if (call.kind === "password") return "super-secret"
|
|
161
|
-
return ""
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
await expect(
|
|
165
|
-
runCli(["add"], {
|
|
166
|
-
registry,
|
|
167
|
-
prompt,
|
|
168
|
-
stdout: createWritable(),
|
|
169
|
-
stderr: createWritable(),
|
|
170
|
-
workspaceRoot,
|
|
171
|
-
}),
|
|
172
|
-
).resolves.toBe(0)
|
|
173
|
-
|
|
174
|
-
expect(prompt.calls).toContainEqual(
|
|
175
|
-
expect.objectContaining({
|
|
176
|
-
kind: "text",
|
|
177
|
-
message: expect.stringContaining("Server scope"),
|
|
178
|
-
defaultValue: "workspace",
|
|
179
|
-
}),
|
|
180
|
-
)
|
|
181
|
-
expect(await registry.listRaw("workspace")).toHaveLength(1)
|
|
182
|
-
expect(await registry.listRaw("global")).toEqual([])
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
test("defaults add to global scope when no workspace config exists", async () => {
|
|
186
|
-
const workspaceRoot = await createWorkspaceRoot(false)
|
|
187
|
-
const registry = createInMemoryRegistry()
|
|
188
|
-
const prompt = createPrompt((call) => {
|
|
189
|
-
if (call.kind === "text" && call.message.includes("Server id")) return "prod-a"
|
|
190
|
-
if (call.kind === "text" && call.message.includes("Scope")) return ""
|
|
191
|
-
if (call.kind === "text" && call.message.includes("Host")) return "10.0.0.10"
|
|
192
|
-
if (call.kind === "text" && call.message.includes("Port")) return "22"
|
|
193
|
-
if (call.kind === "text" && call.message.includes("Username")) return "root"
|
|
194
|
-
if (call.kind === "text" && call.message.includes("Labels")) return ""
|
|
195
|
-
if (call.kind === "text" && call.message.includes("Groups")) return ""
|
|
196
|
-
if (call.kind === "text" && call.message.includes("Auth kind")) return "password"
|
|
197
|
-
if (call.kind === "password") return "super-secret"
|
|
198
|
-
return ""
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
await expect(
|
|
202
|
-
runCli(["add"], {
|
|
203
|
-
registry,
|
|
204
|
-
prompt,
|
|
205
|
-
stdout: createWritable(),
|
|
206
|
-
stderr: createWritable(),
|
|
207
|
-
workspaceRoot,
|
|
208
|
-
}),
|
|
209
|
-
).resolves.toBe(0)
|
|
210
|
-
|
|
211
|
-
expect(prompt.calls).toContainEqual(
|
|
212
|
-
expect.objectContaining({
|
|
213
|
-
kind: "text",
|
|
214
|
-
message: expect.stringContaining("Server scope"),
|
|
215
|
-
defaultValue: "global",
|
|
216
|
-
}),
|
|
217
|
-
)
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
test("re-prompts for scope until a valid value is entered", async () => {
|
|
221
|
-
const workspaceRoot = await createWorkspaceRoot(true)
|
|
222
|
-
const registry = createInMemoryRegistry()
|
|
223
|
-
const prompt = createPrompt((call) => {
|
|
224
|
-
if (call.kind === "text" && call.message.includes("Server id")) return "prod-a"
|
|
225
|
-
if (call.kind === "text" && call.message.includes("Server scope")) {
|
|
226
|
-
const scopePrompts = prompt.calls.filter(
|
|
227
|
-
(entry) => entry.kind === "text" && entry.message.includes("Server scope"),
|
|
228
|
-
)
|
|
229
|
-
return scopePrompts.length === 1 ? "wrong-scope" : "workspace"
|
|
230
|
-
}
|
|
231
|
-
if (call.kind === "text" && call.message.includes("Host")) return "10.0.0.10"
|
|
232
|
-
if (call.kind === "text" && call.message.includes("Port")) return "22"
|
|
233
|
-
if (call.kind === "text" && call.message.includes("Username")) return "root"
|
|
234
|
-
if (call.kind === "text" && call.message.includes("Labels")) return ""
|
|
235
|
-
if (call.kind === "text" && call.message.includes("Groups")) return ""
|
|
236
|
-
if (call.kind === "text" && call.message.includes("Auth kind")) return "password"
|
|
237
|
-
if (call.kind === "password") return "super-secret"
|
|
238
|
-
return ""
|
|
239
|
-
})
|
|
240
|
-
const stdout = createWritable()
|
|
241
|
-
const stderr = createWritable()
|
|
242
|
-
|
|
243
|
-
await expect(
|
|
244
|
-
runCli(["add"], {
|
|
245
|
-
registry,
|
|
246
|
-
prompt,
|
|
247
|
-
stdout,
|
|
248
|
-
stderr,
|
|
249
|
-
workspaceRoot,
|
|
250
|
-
}),
|
|
251
|
-
).resolves.toBe(0)
|
|
252
|
-
|
|
253
|
-
expect(prompt.calls.filter((entry) => entry.kind === "text" && entry.message.includes("Server scope"))).toHaveLength(2)
|
|
254
|
-
expect(stderr.toString()).toContain("Invalid scope")
|
|
255
|
-
expect(await registry.listRaw("workspace")).toHaveLength(1)
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
test("prompts for password auth kind and warns before storing a plain-text password", async () => {
|
|
259
|
-
const workspaceRoot = await createWorkspaceRoot(true)
|
|
260
|
-
const registry = createInMemoryRegistry()
|
|
261
|
-
const stdout = createWritable()
|
|
262
|
-
const stderr = createWritable()
|
|
263
|
-
const prompt = createPrompt((call) => {
|
|
264
|
-
if (call.kind === "text" && call.message.includes("Server id")) return "prod-a"
|
|
265
|
-
if (call.kind === "text" && call.message.includes("Scope")) return ""
|
|
266
|
-
if (call.kind === "text" && call.message.includes("Host")) return "10.0.0.10"
|
|
267
|
-
if (call.kind === "text" && call.message.includes("Port")) return "22"
|
|
268
|
-
if (call.kind === "text" && call.message.includes("Username")) return "root"
|
|
269
|
-
if (call.kind === "text" && call.message.includes("Labels")) return ""
|
|
270
|
-
if (call.kind === "text" && call.message.includes("Groups")) return ""
|
|
271
|
-
if (call.kind === "text" && call.message.includes("Auth kind")) return "password"
|
|
272
|
-
if (call.kind === "password") return "super-secret"
|
|
273
|
-
return ""
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
await expect(
|
|
277
|
-
runCli(["add"], {
|
|
278
|
-
registry,
|
|
279
|
-
prompt,
|
|
280
|
-
stdout,
|
|
281
|
-
stderr,
|
|
282
|
-
workspaceRoot,
|
|
283
|
-
}),
|
|
284
|
-
).resolves.toBe(0)
|
|
285
|
-
|
|
286
|
-
expect(prompt.calls).toContainEqual(
|
|
287
|
-
expect.objectContaining({
|
|
288
|
-
kind: "text",
|
|
289
|
-
message: expect.stringContaining("Auth kind"),
|
|
290
|
-
}),
|
|
291
|
-
)
|
|
292
|
-
expect(stdout.toString()).toContain("plain-text password")
|
|
293
|
-
expect(await registry.listRaw("workspace")).toEqual([
|
|
294
|
-
{
|
|
295
|
-
id: "prod-a",
|
|
296
|
-
host: "10.0.0.10",
|
|
297
|
-
port: 22,
|
|
298
|
-
username: "root",
|
|
299
|
-
auth: { kind: "password", secret: "super-secret" },
|
|
300
|
-
},
|
|
301
|
-
])
|
|
302
|
-
expect(stderr.toString()).toBe("")
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
test("re-prompts for auth kind until a valid value is entered", async () => {
|
|
306
|
-
const workspaceRoot = await createWorkspaceRoot(true)
|
|
307
|
-
const registry = createInMemoryRegistry()
|
|
308
|
-
const prompt = createPrompt((call) => {
|
|
309
|
-
if (call.kind === "text" && call.message.includes("Server id")) return "prod-a"
|
|
310
|
-
if (call.kind === "text" && call.message.includes("Server scope")) return "workspace"
|
|
311
|
-
if (call.kind === "text" && call.message.includes("Host")) return "10.0.0.10"
|
|
312
|
-
if (call.kind === "text" && call.message.includes("Port")) return "22"
|
|
313
|
-
if (call.kind === "text" && call.message.includes("Username")) return "root"
|
|
314
|
-
if (call.kind === "text" && call.message.includes("Labels")) return ""
|
|
315
|
-
if (call.kind === "text" && call.message.includes("Groups")) return ""
|
|
316
|
-
if (call.kind === "text" && call.message.includes("Auth kind")) {
|
|
317
|
-
const authPrompts = prompt.calls.filter(
|
|
318
|
-
(entry) => entry.kind === "text" && entry.message.includes("Auth kind"),
|
|
319
|
-
)
|
|
320
|
-
return authPrompts.length === 1 ? "wrong-auth" : "privateKey"
|
|
321
|
-
}
|
|
322
|
-
if (call.kind === "text" && call.message.includes("Private key path")) return "./keys/id_rsa"
|
|
323
|
-
if (call.kind === "text" && call.message.includes("Passphrase")) return ""
|
|
324
|
-
return ""
|
|
325
|
-
})
|
|
326
|
-
const stderr = createWritable()
|
|
327
|
-
|
|
328
|
-
await expect(
|
|
329
|
-
runCli(["add"], {
|
|
330
|
-
registry,
|
|
331
|
-
prompt,
|
|
332
|
-
stdout: createWritable(),
|
|
333
|
-
stderr,
|
|
334
|
-
workspaceRoot,
|
|
335
|
-
}),
|
|
336
|
-
).resolves.toBe(0)
|
|
337
|
-
|
|
338
|
-
expect(prompt.calls.filter((entry) => entry.kind === "text" && entry.message.includes("Auth kind"))).toHaveLength(2)
|
|
339
|
-
expect(stderr.toString()).toContain("Invalid auth kind")
|
|
340
|
-
expect(await registry.listRaw("workspace")).toEqual([
|
|
341
|
-
{
|
|
342
|
-
id: "prod-a",
|
|
343
|
-
host: "10.0.0.10",
|
|
344
|
-
port: 22,
|
|
345
|
-
username: "root",
|
|
346
|
-
auth: { kind: "privateKey", privateKeyPath: "./keys/id_rsa" },
|
|
347
|
-
},
|
|
348
|
-
])
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
test("prompts for privateKey auth kind and stores the key path", async () => {
|
|
352
|
-
const workspaceRoot = await createWorkspaceRoot(true)
|
|
353
|
-
const registry = createInMemoryRegistry()
|
|
354
|
-
const prompt = createPrompt((call) => {
|
|
355
|
-
if (call.kind === "text" && call.message.includes("Server id")) return "prod-a"
|
|
356
|
-
if (call.kind === "text" && call.message.includes("Scope")) return ""
|
|
357
|
-
if (call.kind === "text" && call.message.includes("Host")) return "10.0.0.10"
|
|
358
|
-
if (call.kind === "text" && call.message.includes("Port")) return "22"
|
|
359
|
-
if (call.kind === "text" && call.message.includes("Username")) return "root"
|
|
360
|
-
if (call.kind === "text" && call.message.includes("Labels")) return ""
|
|
361
|
-
if (call.kind === "text" && call.message.includes("Groups")) return ""
|
|
362
|
-
if (call.kind === "text" && call.message.includes("Auth kind")) return "privateKey"
|
|
363
|
-
if (call.kind === "text" && call.message.includes("Private key path")) return "./keys/id_rsa"
|
|
364
|
-
if (call.kind === "text" && call.message.includes("Passphrase")) return ""
|
|
365
|
-
return ""
|
|
366
|
-
})
|
|
367
|
-
|
|
368
|
-
await expect(
|
|
369
|
-
runCli(["add"], {
|
|
370
|
-
registry,
|
|
371
|
-
prompt,
|
|
372
|
-
stdout: createWritable(),
|
|
373
|
-
stderr: createWritable(),
|
|
374
|
-
workspaceRoot,
|
|
375
|
-
}),
|
|
376
|
-
).resolves.toBe(0)
|
|
377
|
-
|
|
378
|
-
expect(prompt.calls).toContainEqual(
|
|
379
|
-
expect.objectContaining({
|
|
380
|
-
kind: "text",
|
|
381
|
-
message: expect.stringContaining("Auth kind"),
|
|
382
|
-
}),
|
|
383
|
-
)
|
|
384
|
-
expect(await registry.listRaw("workspace")).toEqual([
|
|
385
|
-
{
|
|
386
|
-
id: "prod-a",
|
|
387
|
-
host: "10.0.0.10",
|
|
388
|
-
port: 22,
|
|
389
|
-
username: "root",
|
|
390
|
-
auth: {
|
|
391
|
-
kind: "privateKey",
|
|
392
|
-
privateKeyPath: "./keys/id_rsa",
|
|
393
|
-
},
|
|
394
|
-
},
|
|
395
|
-
])
|
|
396
|
-
})
|
|
397
|
-
|
|
398
|
-
test("prompts for certificate auth kind and stores both key paths", async () => {
|
|
399
|
-
const workspaceRoot = await createWorkspaceRoot(true)
|
|
400
|
-
const registry = createInMemoryRegistry()
|
|
401
|
-
const prompt = createPrompt((call) => {
|
|
402
|
-
if (call.kind === "text" && call.message.includes("Server id")) return "prod-a"
|
|
403
|
-
if (call.kind === "text" && call.message.includes("Scope")) return ""
|
|
404
|
-
if (call.kind === "text" && call.message.includes("Host")) return "10.0.0.10"
|
|
405
|
-
if (call.kind === "text" && call.message.includes("Port")) return "22"
|
|
406
|
-
if (call.kind === "text" && call.message.includes("Username")) return "root"
|
|
407
|
-
if (call.kind === "text" && call.message.includes("Labels")) return ""
|
|
408
|
-
if (call.kind === "text" && call.message.includes("Groups")) return ""
|
|
409
|
-
if (call.kind === "text" && call.message.includes("Auth kind")) return "certificate"
|
|
410
|
-
if (call.kind === "text" && call.message.includes("Certificate path")) return "./keys/client.pem"
|
|
411
|
-
if (call.kind === "text" && call.message.includes("Private key path")) return "./keys/client-key.pem"
|
|
412
|
-
if (call.kind === "text" && call.message.includes("Passphrase")) return "top-secret"
|
|
413
|
-
return ""
|
|
414
|
-
})
|
|
415
|
-
|
|
416
|
-
await expect(
|
|
417
|
-
runCli(["add"], {
|
|
418
|
-
registry,
|
|
419
|
-
prompt,
|
|
420
|
-
stdout: createWritable(),
|
|
421
|
-
stderr: createWritable(),
|
|
422
|
-
workspaceRoot,
|
|
423
|
-
}),
|
|
424
|
-
).resolves.toBe(0)
|
|
425
|
-
|
|
426
|
-
expect(await registry.listRaw("workspace")).toEqual([
|
|
427
|
-
{
|
|
428
|
-
id: "prod-a",
|
|
429
|
-
host: "10.0.0.10",
|
|
430
|
-
port: 22,
|
|
431
|
-
username: "root",
|
|
432
|
-
auth: {
|
|
433
|
-
kind: "certificate",
|
|
434
|
-
certificatePath: "./keys/client.pem",
|
|
435
|
-
privateKeyPath: "./keys/client-key.pem",
|
|
436
|
-
passphrase: "top-secret",
|
|
437
|
-
},
|
|
438
|
-
},
|
|
439
|
-
])
|
|
440
|
-
})
|
|
441
|
-
|
|
442
|
-
test("warns when a workspace id overrides a global id", async () => {
|
|
443
|
-
const workspaceRoot = await createWorkspaceRoot(true)
|
|
444
|
-
const registry = createInMemoryRegistry({
|
|
445
|
-
global: [
|
|
446
|
-
{
|
|
447
|
-
id: "prod-a",
|
|
448
|
-
host: "10.0.0.10",
|
|
449
|
-
port: 22,
|
|
450
|
-
username: "root",
|
|
451
|
-
auth: { kind: "password", secret: "global-secret" },
|
|
452
|
-
},
|
|
453
|
-
],
|
|
454
|
-
})
|
|
455
|
-
const stdout = createWritable()
|
|
456
|
-
const prompt = createPrompt((call) => {
|
|
457
|
-
if (call.kind === "text" && call.message.includes("Server id")) return "prod-a"
|
|
458
|
-
if (call.kind === "confirm") return true
|
|
459
|
-
if (call.kind === "text" && call.message.includes("Scope")) return "workspace"
|
|
460
|
-
if (call.kind === "text" && call.message.includes("Host")) return "10.0.0.20"
|
|
461
|
-
if (call.kind === "text" && call.message.includes("Port")) return "2222"
|
|
462
|
-
if (call.kind === "text" && call.message.includes("Username")) return "deploy"
|
|
463
|
-
if (call.kind === "text" && call.message.includes("Labels")) return ""
|
|
464
|
-
if (call.kind === "text" && call.message.includes("Groups")) return ""
|
|
465
|
-
if (call.kind === "text" && call.message.includes("Auth kind")) return "password"
|
|
466
|
-
if (call.kind === "password") return "workspace-secret"
|
|
467
|
-
return ""
|
|
468
|
-
})
|
|
469
|
-
|
|
470
|
-
await expect(
|
|
471
|
-
runCli(["add"], {
|
|
472
|
-
registry,
|
|
473
|
-
prompt,
|
|
474
|
-
stdout,
|
|
475
|
-
stderr: createWritable(),
|
|
476
|
-
workspaceRoot,
|
|
477
|
-
}),
|
|
478
|
-
).resolves.toBe(0)
|
|
479
|
-
|
|
480
|
-
expect(stdout.toString()).toContain("will override global entry")
|
|
481
|
-
expect(await registry.listRaw("global")).toEqual([
|
|
482
|
-
{
|
|
483
|
-
id: "prod-a",
|
|
484
|
-
host: "10.0.0.10",
|
|
485
|
-
port: 22,
|
|
486
|
-
username: "root",
|
|
487
|
-
auth: { kind: "password", secret: "global-secret" },
|
|
488
|
-
},
|
|
489
|
-
])
|
|
490
|
-
expect(await registry.listRaw("workspace")).toEqual([
|
|
491
|
-
{
|
|
492
|
-
id: "prod-a",
|
|
493
|
-
host: "10.0.0.20",
|
|
494
|
-
port: 2222,
|
|
495
|
-
username: "deploy",
|
|
496
|
-
auth: { kind: "password", secret: "workspace-secret" },
|
|
497
|
-
},
|
|
498
|
-
])
|
|
499
|
-
})
|
|
500
|
-
|
|
501
|
-
test("adding a workspace-scoped server records the managed workspace path", async () => {
|
|
502
|
-
const workspaceRoot = await createWorkspaceRoot(true)
|
|
503
|
-
const registry = createInMemoryRegistry()
|
|
504
|
-
const trackerCalls: Array<{ workspaceRoot: string; managedPath: string }> = []
|
|
505
|
-
const prompt = createPrompt((call) => {
|
|
506
|
-
if (call.kind === "text" && call.message.includes("Server id")) return "prod-a"
|
|
507
|
-
if (call.kind === "text" && call.message.includes("Scope")) return "workspace"
|
|
508
|
-
if (call.kind === "text" && call.message.includes("Host")) return "10.0.0.10"
|
|
509
|
-
if (call.kind === "text" && call.message.includes("Port")) return "22"
|
|
510
|
-
if (call.kind === "text" && call.message.includes("Username")) return "root"
|
|
511
|
-
if (call.kind === "text" && call.message.includes("Labels")) return ""
|
|
512
|
-
if (call.kind === "text" && call.message.includes("Groups")) return ""
|
|
513
|
-
if (call.kind === "text" && call.message.includes("Auth kind")) return "password"
|
|
514
|
-
if (call.kind === "password") return "super-secret"
|
|
515
|
-
return ""
|
|
516
|
-
})
|
|
517
|
-
|
|
518
|
-
await expect(
|
|
519
|
-
runCli(["add"], {
|
|
520
|
-
registry,
|
|
521
|
-
prompt,
|
|
522
|
-
stdout: createWritable(),
|
|
523
|
-
stderr: createWritable(),
|
|
524
|
-
workspaceRoot,
|
|
525
|
-
workspaceTracker: {
|
|
526
|
-
record: async (entry: { workspaceRoot: string; managedPath: string }) => {
|
|
527
|
-
trackerCalls.push(entry)
|
|
528
|
-
},
|
|
529
|
-
},
|
|
530
|
-
}),
|
|
531
|
-
).resolves.toBe(0)
|
|
532
|
-
|
|
533
|
-
expect(trackerCalls).toEqual([
|
|
534
|
-
{
|
|
535
|
-
workspaceRoot,
|
|
536
|
-
managedPath: `${workspaceRoot}/.open-code`,
|
|
537
|
-
},
|
|
538
|
-
])
|
|
539
|
-
})
|
|
540
|
-
|
|
541
|
-
test("prompts which scope to remove from when the same id exists in both configs", async () => {
|
|
542
|
-
const workspaceRoot = await createWorkspaceRoot(true)
|
|
543
|
-
const registry = createInMemoryRegistry({
|
|
544
|
-
global: [
|
|
545
|
-
{
|
|
546
|
-
id: "prod-a",
|
|
547
|
-
host: "10.0.0.10",
|
|
548
|
-
port: 22,
|
|
549
|
-
username: "root",
|
|
550
|
-
auth: { kind: "password", secret: "global-secret" },
|
|
551
|
-
},
|
|
552
|
-
],
|
|
553
|
-
workspace: [
|
|
554
|
-
{
|
|
555
|
-
id: "prod-a",
|
|
556
|
-
host: "10.0.0.20",
|
|
557
|
-
port: 2222,
|
|
558
|
-
username: "deploy",
|
|
559
|
-
auth: { kind: "password", secret: "workspace-secret" },
|
|
560
|
-
},
|
|
561
|
-
],
|
|
562
|
-
})
|
|
563
|
-
const prompt = createPrompt((call) => {
|
|
564
|
-
if (call.kind === "confirm") return true
|
|
565
|
-
if (call.kind === "text" && call.message.includes("Remove from which scope")) return "global"
|
|
566
|
-
return ""
|
|
567
|
-
})
|
|
568
|
-
|
|
569
|
-
await expect(
|
|
570
|
-
runCli(["remove", "prod-a"], {
|
|
571
|
-
registry,
|
|
572
|
-
prompt,
|
|
573
|
-
stdout: createWritable(),
|
|
574
|
-
stderr: createWritable(),
|
|
575
|
-
workspaceRoot,
|
|
576
|
-
}),
|
|
577
|
-
).resolves.toBe(0)
|
|
578
|
-
|
|
579
|
-
expect(prompt.calls).toContainEqual(
|
|
580
|
-
expect.objectContaining({
|
|
581
|
-
kind: "text",
|
|
582
|
-
message: expect.stringContaining("Remove from which scope"),
|
|
583
|
-
}),
|
|
584
|
-
)
|
|
585
|
-
expect(await registry.listRaw("global")).toEqual([])
|
|
586
|
-
expect(await registry.listRaw("workspace")).toEqual([
|
|
587
|
-
{
|
|
588
|
-
id: "prod-a",
|
|
589
|
-
host: "10.0.0.20",
|
|
590
|
-
port: 2222,
|
|
591
|
-
username: "deploy",
|
|
592
|
-
auth: { kind: "password", secret: "workspace-secret" },
|
|
593
|
-
},
|
|
594
|
-
])
|
|
595
|
-
})
|
|
596
|
-
|
|
597
|
-
test("lists the source scope and shadowing status", async () => {
|
|
598
|
-
const workspaceRoot = await createWorkspaceRoot(true)
|
|
599
|
-
const registry = createInMemoryRegistry({
|
|
600
|
-
global: [
|
|
601
|
-
{
|
|
602
|
-
id: "prod-b",
|
|
603
|
-
host: "10.0.0.11",
|
|
604
|
-
port: 22,
|
|
605
|
-
username: "ops",
|
|
606
|
-
auth: { kind: "privateKey", privateKeyPath: "/keys/prod-b" },
|
|
607
|
-
},
|
|
608
|
-
{
|
|
609
|
-
id: "prod-a",
|
|
610
|
-
host: "10.0.0.10",
|
|
611
|
-
port: 22,
|
|
612
|
-
username: "root",
|
|
613
|
-
auth: { kind: "password", secret: "global-secret" },
|
|
614
|
-
},
|
|
615
|
-
],
|
|
616
|
-
workspace: [
|
|
617
|
-
{
|
|
618
|
-
id: "prod-a",
|
|
619
|
-
host: "10.0.0.99",
|
|
620
|
-
port: 2222,
|
|
621
|
-
username: "deploy",
|
|
622
|
-
auth: {
|
|
623
|
-
kind: "certificate",
|
|
624
|
-
certificatePath: "/certs/prod-a.crt",
|
|
625
|
-
privateKeyPath: "/keys/prod-a",
|
|
626
|
-
},
|
|
627
|
-
},
|
|
628
|
-
],
|
|
629
|
-
})
|
|
630
|
-
const stdout = createWritable()
|
|
631
|
-
|
|
632
|
-
await expect(
|
|
633
|
-
runCli(["list"], {
|
|
634
|
-
registry,
|
|
635
|
-
prompt: createPrompt(() => ""),
|
|
636
|
-
stdout,
|
|
637
|
-
stderr: createWritable(),
|
|
638
|
-
workspaceRoot,
|
|
639
|
-
}),
|
|
640
|
-
).resolves.toBe(0)
|
|
641
|
-
|
|
642
|
-
expect(stdout.toString().split("\n").filter((line) => line.length > 0)).toEqual([
|
|
643
|
-
"ID\tSCOPE\tSTATUS\tHOST\tPORT\tUSERNAME\tLABELS\tGROUPS",
|
|
644
|
-
"prod-b\tglobal\t\t10.0.0.11\t22\tops\t\t",
|
|
645
|
-
"prod-a\tworkspace\tshadowing global\t10.0.0.99\t2222\tdeploy\t\t",
|
|
646
|
-
])
|
|
647
|
-
expect(stdout.toString()).not.toContain("global-secret")
|
|
648
|
-
expect(stdout.toString()).not.toContain("/keys/prod-b")
|
|
649
|
-
expect(stdout.toString()).not.toContain("/certs/prod-a.crt")
|
|
650
|
-
})
|
|
651
|
-
|
|
652
|
-
test("remove preserves the empty-registry fast path", async () => {
|
|
653
|
-
const workspaceRoot = await createWorkspaceRoot(true)
|
|
654
|
-
const registry = createInMemoryRegistry()
|
|
655
|
-
const stdout = createWritable()
|
|
656
|
-
const stderr = createWritable()
|
|
657
|
-
const prompt = createPrompt(() => "prod-a")
|
|
658
|
-
|
|
659
|
-
await expect(
|
|
660
|
-
runCli(["remove"], {
|
|
661
|
-
registry,
|
|
662
|
-
prompt,
|
|
663
|
-
stdout,
|
|
664
|
-
stderr,
|
|
665
|
-
workspaceRoot,
|
|
666
|
-
}),
|
|
667
|
-
).resolves.toBe(0)
|
|
668
|
-
|
|
669
|
-
expect(stdout.toString()).toContain("No servers configured.")
|
|
670
|
-
expect(stderr.toString()).toBe("")
|
|
671
|
-
expect(prompt.calls).toHaveLength(0)
|
|
672
|
-
})
|
|
673
|
-
})
|