@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.
Files changed (60) hide show
  1. package/dist/core/audit/log-store.js +1 -1
  2. package/dist/core/orchestrator.d.ts +2 -2
  3. package/dist/core/orchestrator.js +3 -3
  4. package/dist/core/result.d.ts +1 -1
  5. package/dist/index.d.ts +3 -3
  6. package/dist/index.js +3 -3
  7. package/dist/opencode/plugin.d.ts +1 -1
  8. package/dist/opencode/plugin.js +8 -8
  9. package/package.json +6 -1
  10. package/.claude/settings.local.json +0 -25
  11. package/bun.lock +0 -368
  12. package/docs/superpowers/notes/2026-03-25-opencode-remote-tools-handoff.md +0 -81
  13. package/docs/superpowers/notes/2026-03-26-openshell-pre-release-review.md +0 -174
  14. package/docs/superpowers/plans/2026-03-25-opencode-remote-tools.md +0 -1656
  15. package/docs/superpowers/plans/2026-03-25-server-registry-cli.md +0 -54
  16. package/docs/superpowers/plans/2026-03-26-config-backed-credential-registry.md +0 -494
  17. package/docs/superpowers/plans/2026-03-26-openshell-release-prep.md +0 -639
  18. package/docs/superpowers/specs/2026-03-25-opencode-remote-tools-design.md +0 -378
  19. package/docs/superpowers/specs/2026-03-26-config-backed-credential-registry-design.md +0 -272
  20. package/docs/superpowers/specs/2026-03-26-openshell-release-prep-design.md +0 -197
  21. package/examples/opencode-local/opencode.json +0 -19
  22. package/scripts/openshell.ts +0 -3
  23. package/scripts/server-registry.ts +0 -3
  24. package/src/cli/openshell.ts +0 -65
  25. package/src/cli/server-registry.ts +0 -476
  26. package/src/core/audit/git-audit-repo.ts +0 -42
  27. package/src/core/audit/log-store.ts +0 -20
  28. package/src/core/audit/redact.ts +0 -4
  29. package/src/core/contracts.ts +0 -51
  30. package/src/core/orchestrator.ts +0 -1082
  31. package/src/core/patch.ts +0 -11
  32. package/src/core/paths.ts +0 -32
  33. package/src/core/policy.ts +0 -30
  34. package/src/core/registry/server-registry.ts +0 -505
  35. package/src/core/result.ts +0 -16
  36. package/src/core/ssh/ssh-runtime.ts +0 -355
  37. package/src/index.ts +0 -3
  38. package/src/opencode/plugin.ts +0 -242
  39. package/src/product/install.ts +0 -43
  40. package/src/product/opencode-config.ts +0 -118
  41. package/src/product/uninstall.ts +0 -47
  42. package/src/product/workspace-tracker.ts +0 -69
  43. package/tests/integration/fake-ssh-server.ts +0 -97
  44. package/tests/integration/install-lifecycle.test.ts +0 -85
  45. package/tests/integration/orchestrator.test.ts +0 -767
  46. package/tests/integration/ssh-runtime.test.ts +0 -122
  47. package/tests/unit/audit.test.ts +0 -221
  48. package/tests/unit/build-layout.test.ts +0 -28
  49. package/tests/unit/opencode-config.test.ts +0 -100
  50. package/tests/unit/opencode-plugin.test.ts +0 -358
  51. package/tests/unit/openshell-cli.test.ts +0 -60
  52. package/tests/unit/paths.test.ts +0 -64
  53. package/tests/unit/plugin-export.test.ts +0 -10
  54. package/tests/unit/policy.test.ts +0 -53
  55. package/tests/unit/release-docs.test.ts +0 -31
  56. package/tests/unit/result.test.ts +0 -28
  57. package/tests/unit/server-registry-cli.test.ts +0 -673
  58. package/tests/unit/server-registry.test.ts +0 -452
  59. package/tests/unit/workspace-tracker.test.ts +0 -57
  60. 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
- }