@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,452 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
-
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
|
|
3
|
-
import { tmpdir } from "node:os"
|
|
4
|
-
import { join } from "node:path"
|
|
5
|
-
import { createServerRegistry } from "../../src/core/registry/server-registry"
|
|
6
|
-
|
|
7
|
-
describe("server registry", () => {
|
|
8
|
-
let tempDir: string
|
|
9
|
-
let workspaceRoot: string
|
|
10
|
-
let globalRegistryFile: string
|
|
11
|
-
let workspaceRegistryFile: string
|
|
12
|
-
|
|
13
|
-
beforeEach(async () => {
|
|
14
|
-
tempDir = await mkdtemp(join(tmpdir(), "open-code-registry-"))
|
|
15
|
-
workspaceRoot = join(tempDir, "repo")
|
|
16
|
-
globalRegistryFile = join(tempDir, "config", "servers.json")
|
|
17
|
-
workspaceRegistryFile = join(workspaceRoot, ".open-code", "servers.json")
|
|
18
|
-
|
|
19
|
-
await mkdir(join(tempDir, "config"), { recursive: true })
|
|
20
|
-
await mkdir(join(workspaceRoot, ".open-code"), { recursive: true })
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
afterEach(async () => {
|
|
24
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
const createRegistry = () =>
|
|
28
|
-
createServerRegistry({
|
|
29
|
-
globalRegistryFile,
|
|
30
|
-
workspaceRegistryFile,
|
|
31
|
-
workspaceRoot,
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
test("plain-text password is written as plain JSON", async () => {
|
|
35
|
-
const registry = createRegistry()
|
|
36
|
-
const record = {
|
|
37
|
-
id: "prod-a",
|
|
38
|
-
host: "10.0.0.10",
|
|
39
|
-
port: 22,
|
|
40
|
-
username: "root",
|
|
41
|
-
auth: { kind: "password", secret: "super-secret" },
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
await registry.upsert("workspace", record)
|
|
45
|
-
|
|
46
|
-
const disk = await readFile(workspaceRegistryFile, "utf8")
|
|
47
|
-
expect(JSON.parse(disk)).toEqual([record])
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
test("workspace records override global records by id", async () => {
|
|
51
|
-
const registry = createRegistry()
|
|
52
|
-
const globalRecord = {
|
|
53
|
-
id: "prod-a",
|
|
54
|
-
host: "10.0.0.10",
|
|
55
|
-
port: 22,
|
|
56
|
-
username: "root",
|
|
57
|
-
auth: { kind: "password", secret: "global-secret" },
|
|
58
|
-
}
|
|
59
|
-
const workspaceRecord = {
|
|
60
|
-
id: "prod-a",
|
|
61
|
-
host: "10.0.0.99",
|
|
62
|
-
port: 2222,
|
|
63
|
-
username: "deploy",
|
|
64
|
-
auth: { kind: "password", secret: "workspace-secret" },
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
await writeFile(globalRegistryFile, JSON.stringify([globalRecord], null, 2))
|
|
68
|
-
await writeFile(workspaceRegistryFile, JSON.stringify([workspaceRecord], null, 2))
|
|
69
|
-
|
|
70
|
-
expect(await registry.resolve("prod-a")).toMatchObject({
|
|
71
|
-
id: "prod-a",
|
|
72
|
-
host: "10.0.0.99",
|
|
73
|
-
port: 2222,
|
|
74
|
-
username: "deploy",
|
|
75
|
-
scope: "workspace",
|
|
76
|
-
shadowingGlobal: true,
|
|
77
|
-
workspaceRoot,
|
|
78
|
-
auth: { kind: "password", secret: "workspace-secret" },
|
|
79
|
-
})
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
test("duplicate ids resolve to the last record in a scope", async () => {
|
|
83
|
-
const registry = createRegistry()
|
|
84
|
-
const firstRecord = {
|
|
85
|
-
id: "prod-a",
|
|
86
|
-
host: "10.0.0.10",
|
|
87
|
-
port: 22,
|
|
88
|
-
username: "root",
|
|
89
|
-
auth: { kind: "password", secret: "first-secret" },
|
|
90
|
-
}
|
|
91
|
-
const lastRecord = {
|
|
92
|
-
id: "prod-a",
|
|
93
|
-
host: "10.0.0.20",
|
|
94
|
-
port: 2222,
|
|
95
|
-
username: "deploy",
|
|
96
|
-
auth: { kind: "password", secret: "last-secret" },
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
await writeFile(workspaceRegistryFile, JSON.stringify([firstRecord, lastRecord], null, 2))
|
|
100
|
-
|
|
101
|
-
expect(await registry.list()).toEqual([
|
|
102
|
-
{
|
|
103
|
-
...lastRecord,
|
|
104
|
-
scope: "workspace",
|
|
105
|
-
workspaceRoot,
|
|
106
|
-
},
|
|
107
|
-
])
|
|
108
|
-
expect(await registry.resolve("prod-a")).toEqual({
|
|
109
|
-
...lastRecord,
|
|
110
|
-
scope: "workspace",
|
|
111
|
-
workspaceRoot,
|
|
112
|
-
})
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
test("list returns effective merged records with scope metadata", async () => {
|
|
116
|
-
const registry = createRegistry()
|
|
117
|
-
const globalOnly = {
|
|
118
|
-
id: "prod-b",
|
|
119
|
-
host: "10.0.0.11",
|
|
120
|
-
port: 22,
|
|
121
|
-
username: "ops",
|
|
122
|
-
auth: { kind: "privateKey", privateKeyPath: "/keys/prod-b" },
|
|
123
|
-
}
|
|
124
|
-
const globalShadowed = {
|
|
125
|
-
id: "prod-a",
|
|
126
|
-
host: "10.0.0.10",
|
|
127
|
-
port: 22,
|
|
128
|
-
username: "root",
|
|
129
|
-
auth: { kind: "password", secret: "global-secret" },
|
|
130
|
-
}
|
|
131
|
-
const workspaceShadow = {
|
|
132
|
-
id: "prod-a",
|
|
133
|
-
host: "10.0.0.99",
|
|
134
|
-
port: 2222,
|
|
135
|
-
username: "deploy",
|
|
136
|
-
auth: { kind: "certificate", certificatePath: "/certs/prod-a.crt", privateKeyPath: "/keys/prod-a" },
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
await writeFile(globalRegistryFile, JSON.stringify([globalShadowed, globalOnly], null, 2))
|
|
140
|
-
await writeFile(workspaceRegistryFile, JSON.stringify([workspaceShadow], null, 2))
|
|
141
|
-
|
|
142
|
-
expect(await registry.list()).toEqual([
|
|
143
|
-
{
|
|
144
|
-
...workspaceShadow,
|
|
145
|
-
scope: "workspace",
|
|
146
|
-
shadowingGlobal: true,
|
|
147
|
-
workspaceRoot,
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
...globalOnly,
|
|
151
|
-
scope: "global",
|
|
152
|
-
},
|
|
153
|
-
])
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
test("listRaw returns unmerged records for each scope", async () => {
|
|
157
|
-
const registry = createRegistry()
|
|
158
|
-
const globalRecord = {
|
|
159
|
-
id: "prod-b",
|
|
160
|
-
host: "10.0.0.11",
|
|
161
|
-
port: 22,
|
|
162
|
-
username: "ops",
|
|
163
|
-
auth: { kind: "privateKey", privateKeyPath: "/keys/prod-b" },
|
|
164
|
-
}
|
|
165
|
-
const workspaceRecord = {
|
|
166
|
-
id: "prod-a",
|
|
167
|
-
host: "10.0.0.99",
|
|
168
|
-
port: 2222,
|
|
169
|
-
username: "deploy",
|
|
170
|
-
auth: { kind: "password", secret: "workspace-secret" },
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
await writeFile(globalRegistryFile, JSON.stringify([globalRecord], null, 2))
|
|
174
|
-
await writeFile(workspaceRegistryFile, JSON.stringify([workspaceRecord], null, 2))
|
|
175
|
-
|
|
176
|
-
expect(await registry.listRaw("global")).toEqual([globalRecord])
|
|
177
|
-
expect(await registry.listRaw("workspace")).toEqual([workspaceRecord])
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
test("rejects malformed privateKey auth records during load", async () => {
|
|
181
|
-
const registry = createRegistry()
|
|
182
|
-
|
|
183
|
-
await writeFile(
|
|
184
|
-
workspaceRegistryFile,
|
|
185
|
-
JSON.stringify(
|
|
186
|
-
[
|
|
187
|
-
{
|
|
188
|
-
id: "prod-a",
|
|
189
|
-
host: "10.0.0.10",
|
|
190
|
-
port: 22,
|
|
191
|
-
username: "root",
|
|
192
|
-
auth: { kind: "privateKey" },
|
|
193
|
-
},
|
|
194
|
-
],
|
|
195
|
-
null,
|
|
196
|
-
2,
|
|
197
|
-
),
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
await expect(registry.list()).rejects.toMatchObject({
|
|
201
|
-
code: "REGISTRY_RECORD_INVALID",
|
|
202
|
-
message: expect.stringContaining("auth.privateKeyPath"),
|
|
203
|
-
})
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
test("reads wait for pending writes from the same registry instance", async () => {
|
|
207
|
-
const lockFile = `${workspaceRegistryFile}.lock`
|
|
208
|
-
const workspaceRecord = {
|
|
209
|
-
id: "prod-a",
|
|
210
|
-
host: "10.0.0.10",
|
|
211
|
-
port: 22,
|
|
212
|
-
username: "root",
|
|
213
|
-
auth: { kind: "password", secret: "super-secret" },
|
|
214
|
-
}
|
|
215
|
-
let releaseProcessStartTime!: () => void
|
|
216
|
-
let writeBlocked!: () => void
|
|
217
|
-
const blocked = new Promise<void>((resolve) => {
|
|
218
|
-
writeBlocked = resolve
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
const registry = createServerRegistry({
|
|
222
|
-
globalRegistryFile,
|
|
223
|
-
workspaceRegistryFile,
|
|
224
|
-
workspaceRoot,
|
|
225
|
-
lockOptions: {
|
|
226
|
-
getProcessStartTime: async (pid) => {
|
|
227
|
-
if (pid === process.pid) {
|
|
228
|
-
await new Promise<void>((resolve) => {
|
|
229
|
-
releaseProcessStartTime = resolve
|
|
230
|
-
writeBlocked()
|
|
231
|
-
})
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return Date.now()
|
|
235
|
-
},
|
|
236
|
-
},
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
await writeFile(lockFile, JSON.stringify({ pid: process.pid, createdAt: new Date(0).toISOString() }))
|
|
240
|
-
|
|
241
|
-
const pendingUpsert = registry.upsert("workspace", workspaceRecord)
|
|
242
|
-
await blocked
|
|
243
|
-
|
|
244
|
-
const pendingResolve = registry.resolve("prod-a")
|
|
245
|
-
const pendingList = registry.list()
|
|
246
|
-
|
|
247
|
-
releaseProcessStartTime()
|
|
248
|
-
|
|
249
|
-
await pendingUpsert
|
|
250
|
-
expect(await pendingResolve).toEqual({
|
|
251
|
-
...workspaceRecord,
|
|
252
|
-
scope: "workspace",
|
|
253
|
-
workspaceRoot,
|
|
254
|
-
})
|
|
255
|
-
expect(await pendingList).toEqual([
|
|
256
|
-
{
|
|
257
|
-
...workspaceRecord,
|
|
258
|
-
scope: "workspace",
|
|
259
|
-
workspaceRoot,
|
|
260
|
-
},
|
|
261
|
-
])
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
test("serializes overlapping upserts without losing updates", async () => {
|
|
265
|
-
const lockFile = `${workspaceRegistryFile}.lock`
|
|
266
|
-
let releaseProcessStartTime!: () => void
|
|
267
|
-
let writeBlocked!: () => void
|
|
268
|
-
const blocked = new Promise<void>((resolve) => {
|
|
269
|
-
writeBlocked = resolve
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
const firstRegistry = createServerRegistry({
|
|
273
|
-
globalRegistryFile,
|
|
274
|
-
workspaceRegistryFile,
|
|
275
|
-
workspaceRoot,
|
|
276
|
-
lockOptions: {
|
|
277
|
-
getProcessStartTime: async (pid) => {
|
|
278
|
-
if (pid === process.pid) {
|
|
279
|
-
await new Promise<void>((resolve) => {
|
|
280
|
-
releaseProcessStartTime = resolve
|
|
281
|
-
writeBlocked()
|
|
282
|
-
})
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return Date.now()
|
|
286
|
-
},
|
|
287
|
-
},
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
await writeFile(lockFile, JSON.stringify({ pid: process.pid, createdAt: new Date(0).toISOString() }))
|
|
291
|
-
|
|
292
|
-
const firstRecord = {
|
|
293
|
-
id: "prod-a",
|
|
294
|
-
host: "10.0.0.10",
|
|
295
|
-
port: 22,
|
|
296
|
-
username: "root",
|
|
297
|
-
auth: { kind: "password", secret: "super-secret" },
|
|
298
|
-
}
|
|
299
|
-
const secondRecord = {
|
|
300
|
-
id: "prod-b",
|
|
301
|
-
host: "10.0.0.11",
|
|
302
|
-
port: 22,
|
|
303
|
-
username: "deploy",
|
|
304
|
-
auth: {
|
|
305
|
-
kind: "privateKey",
|
|
306
|
-
privateKeyPath: "/keys/prod-b",
|
|
307
|
-
},
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const firstUpsert = firstRegistry.upsert("workspace", firstRecord)
|
|
311
|
-
await blocked
|
|
312
|
-
const secondUpsert = firstRegistry.upsert("workspace", secondRecord)
|
|
313
|
-
releaseProcessStartTime()
|
|
314
|
-
|
|
315
|
-
await firstUpsert
|
|
316
|
-
await secondUpsert
|
|
317
|
-
|
|
318
|
-
const reloadedRegistry = createRegistry()
|
|
319
|
-
expect(await reloadedRegistry.list()).toEqual([
|
|
320
|
-
{
|
|
321
|
-
...firstRecord,
|
|
322
|
-
scope: "workspace",
|
|
323
|
-
workspaceRoot,
|
|
324
|
-
},
|
|
325
|
-
{
|
|
326
|
-
...secondRecord,
|
|
327
|
-
scope: "workspace",
|
|
328
|
-
workspaceRoot,
|
|
329
|
-
},
|
|
330
|
-
])
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
test("two registry instances contend for the same file without losing records", async () => {
|
|
334
|
-
const lockFile = `${workspaceRegistryFile}.lock`
|
|
335
|
-
let releaseProcessStartTime!: () => void
|
|
336
|
-
let firstBlocked!: () => void
|
|
337
|
-
let blockedOnce = false
|
|
338
|
-
const blocked = new Promise<void>((resolve) => {
|
|
339
|
-
firstBlocked = resolve
|
|
340
|
-
})
|
|
341
|
-
|
|
342
|
-
const firstRegistry = createServerRegistry({
|
|
343
|
-
globalRegistryFile,
|
|
344
|
-
workspaceRegistryFile,
|
|
345
|
-
workspaceRoot,
|
|
346
|
-
lockOptions: {
|
|
347
|
-
getProcessStartTime: async (pid) => {
|
|
348
|
-
if (pid === process.pid && !blockedOnce) {
|
|
349
|
-
blockedOnce = true
|
|
350
|
-
await new Promise<void>((resolve) => {
|
|
351
|
-
releaseProcessStartTime = resolve
|
|
352
|
-
firstBlocked()
|
|
353
|
-
})
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
return Date.now()
|
|
357
|
-
},
|
|
358
|
-
},
|
|
359
|
-
})
|
|
360
|
-
const secondRegistry = createRegistry()
|
|
361
|
-
|
|
362
|
-
await writeFile(lockFile, JSON.stringify({ pid: process.pid, createdAt: new Date(0).toISOString() }))
|
|
363
|
-
|
|
364
|
-
const firstRecord = {
|
|
365
|
-
id: "prod-a",
|
|
366
|
-
host: "10.0.0.10",
|
|
367
|
-
port: 22,
|
|
368
|
-
username: "root",
|
|
369
|
-
auth: { kind: "password", secret: "super-secret" },
|
|
370
|
-
}
|
|
371
|
-
const secondRecord = {
|
|
372
|
-
id: "prod-b",
|
|
373
|
-
host: "10.0.0.11",
|
|
374
|
-
port: 22,
|
|
375
|
-
username: "deploy",
|
|
376
|
-
auth: {
|
|
377
|
-
kind: "privateKey",
|
|
378
|
-
privateKeyPath: "/keys/prod-b",
|
|
379
|
-
},
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const firstUpsert = firstRegistry.upsert("workspace", firstRecord)
|
|
383
|
-
await blocked
|
|
384
|
-
const secondUpsert = secondRegistry.upsert("workspace", secondRecord)
|
|
385
|
-
releaseProcessStartTime()
|
|
386
|
-
|
|
387
|
-
await firstUpsert
|
|
388
|
-
await secondUpsert
|
|
389
|
-
|
|
390
|
-
const reloadedRegistry = createRegistry()
|
|
391
|
-
const ids = (await reloadedRegistry.list()).map((record) => record.id).sort()
|
|
392
|
-
expect(ids).toEqual(["prod-a", "prod-b"])
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
test("reclaims a lock when the pid now belongs to a newer process", async () => {
|
|
396
|
-
const lockFile = `${workspaceRegistryFile}.lock`
|
|
397
|
-
const registry = createServerRegistry({
|
|
398
|
-
globalRegistryFile,
|
|
399
|
-
workspaceRegistryFile,
|
|
400
|
-
workspaceRoot,
|
|
401
|
-
lockOptions: {
|
|
402
|
-
getProcessStartTime: async (pid) => (pid === process.pid ? Date.now() : null),
|
|
403
|
-
},
|
|
404
|
-
})
|
|
405
|
-
|
|
406
|
-
await writeFile(lockFile, JSON.stringify({ pid: process.pid, createdAt: new Date(0).toISOString() }))
|
|
407
|
-
|
|
408
|
-
const record = {
|
|
409
|
-
id: "prod-a",
|
|
410
|
-
host: "10.0.0.10",
|
|
411
|
-
port: 22,
|
|
412
|
-
username: "root",
|
|
413
|
-
auth: { kind: "password", secret: "super-secret" },
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
await registry.upsert("workspace", record)
|
|
417
|
-
|
|
418
|
-
expect(await registry.list()).toEqual([
|
|
419
|
-
{
|
|
420
|
-
...record,
|
|
421
|
-
scope: "workspace",
|
|
422
|
-
workspaceRoot,
|
|
423
|
-
},
|
|
424
|
-
])
|
|
425
|
-
})
|
|
426
|
-
|
|
427
|
-
test("times out when a live lock owner keeps the registry busy", async () => {
|
|
428
|
-
const lockFile = `${workspaceRegistryFile}.lock`
|
|
429
|
-
const registry = createServerRegistry({
|
|
430
|
-
globalRegistryFile,
|
|
431
|
-
workspaceRegistryFile,
|
|
432
|
-
workspaceRoot,
|
|
433
|
-
lockOptions: {
|
|
434
|
-
getProcessStartTime: async (pid) => (pid === process.pid ? Date.now() - 1_000 : null),
|
|
435
|
-
retryMs: 5,
|
|
436
|
-
timeoutMs: 40,
|
|
437
|
-
},
|
|
438
|
-
})
|
|
439
|
-
|
|
440
|
-
await writeFile(lockFile, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }))
|
|
441
|
-
|
|
442
|
-
await expect(
|
|
443
|
-
registry.upsert("workspace", {
|
|
444
|
-
id: "prod-a",
|
|
445
|
-
host: "10.0.0.10",
|
|
446
|
-
port: 22,
|
|
447
|
-
username: "root",
|
|
448
|
-
auth: { kind: "password", secret: "super-secret" },
|
|
449
|
-
}),
|
|
450
|
-
).rejects.toThrow("Timed out waiting for registry lock")
|
|
451
|
-
})
|
|
452
|
-
})
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from "bun:test"
|
|
2
|
-
import { mkdtemp, readFile, rm } from "node:fs/promises"
|
|
3
|
-
import { tmpdir } from "node:os"
|
|
4
|
-
import { join } from "node:path"
|
|
5
|
-
|
|
6
|
-
const tempDirs: string[] = []
|
|
7
|
-
|
|
8
|
-
afterEach(async () => {
|
|
9
|
-
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })))
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
describe("workspace tracker", () => {
|
|
13
|
-
test("records and deduplicates managed workspaces", async () => {
|
|
14
|
-
const tempDir = await mkdtemp(join(tmpdir(), "openshell-workspace-tracker-"))
|
|
15
|
-
tempDirs.push(tempDir)
|
|
16
|
-
const trackerFile = join(tempDir, "workspaces.json")
|
|
17
|
-
const { createWorkspaceTracker } = await import("../../src/product/workspace-tracker")
|
|
18
|
-
const tracker = createWorkspaceTracker(trackerFile)
|
|
19
|
-
|
|
20
|
-
await tracker.record({
|
|
21
|
-
workspaceRoot: "/repo",
|
|
22
|
-
managedPath: "/repo/.open-code",
|
|
23
|
-
})
|
|
24
|
-
await tracker.record({
|
|
25
|
-
workspaceRoot: "/repo",
|
|
26
|
-
managedPath: "/repo/.open-code",
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
expect(await tracker.list()).toEqual([
|
|
30
|
-
expect.objectContaining({
|
|
31
|
-
workspaceRoot: "/repo",
|
|
32
|
-
managedPath: "/repo/.open-code",
|
|
33
|
-
}),
|
|
34
|
-
])
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test("persists tracker state as json", async () => {
|
|
38
|
-
const tempDir = await mkdtemp(join(tmpdir(), "openshell-workspace-tracker-"))
|
|
39
|
-
tempDirs.push(tempDir)
|
|
40
|
-
const trackerFile = join(tempDir, "workspaces.json")
|
|
41
|
-
const { createWorkspaceTracker } = await import("../../src/product/workspace-tracker")
|
|
42
|
-
const tracker = createWorkspaceTracker(trackerFile)
|
|
43
|
-
|
|
44
|
-
await tracker.record({
|
|
45
|
-
workspaceRoot: "/repo",
|
|
46
|
-
managedPath: "/repo/.open-code",
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
const raw = JSON.parse(await readFile(trackerFile, "utf8"))
|
|
50
|
-
expect(raw).toEqual([
|
|
51
|
-
expect.objectContaining({
|
|
52
|
-
workspaceRoot: "/repo",
|
|
53
|
-
managedPath: "/repo/.open-code",
|
|
54
|
-
}),
|
|
55
|
-
])
|
|
56
|
-
})
|
|
57
|
-
})
|
package/tsconfig.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "Bundler",
|
|
6
|
-
"types": ["bun"],
|
|
7
|
-
"outDir": "dist",
|
|
8
|
-
"rootDir": "src",
|
|
9
|
-
"strict": true,
|
|
10
|
-
"declaration": true,
|
|
11
|
-
"skipLibCheck": true
|
|
12
|
-
},
|
|
13
|
-
"include": ["src/**/*.ts"]
|
|
14
|
-
}
|