@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,122 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, test } from "bun:test"
|
|
2
|
-
import { startFakeSshServer } from "./fake-ssh-server"
|
|
3
|
-
|
|
4
|
-
let createSshRuntime: typeof import("../../src/core/ssh/ssh-runtime").createSshRuntime
|
|
5
|
-
|
|
6
|
-
describe("ssh runtime", () => {
|
|
7
|
-
let server: Awaited<ReturnType<typeof startFakeSshServer>> | undefined
|
|
8
|
-
let runtime: ReturnType<typeof createSshRuntime>
|
|
9
|
-
|
|
10
|
-
beforeAll(async () => {
|
|
11
|
-
server = await startFakeSshServer()
|
|
12
|
-
createSshRuntime = (await import("../../src/core/ssh/ssh-runtime?integration-real")).createSshRuntime
|
|
13
|
-
runtime = createSshRuntime()
|
|
14
|
-
}, { timeout: 60_000 })
|
|
15
|
-
|
|
16
|
-
afterAll(async () => {
|
|
17
|
-
await server?.stop()
|
|
18
|
-
}, { timeout: 30_000 })
|
|
19
|
-
|
|
20
|
-
test("executes a safe remote command", async () => {
|
|
21
|
-
const result = await runtime.exec(server.connection, "cat /tmp/open-code/hosts")
|
|
22
|
-
expect(result.stdout).toContain("localhost")
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
test("executes commands from a cwd that contains spaces", async () => {
|
|
26
|
-
await runtime.exec(server.connection, "mkdir -p '/tmp/open code'")
|
|
27
|
-
|
|
28
|
-
const result = await runtime.exec(server.connection, "pwd", { cwd: "/tmp/open code" })
|
|
29
|
-
|
|
30
|
-
expect(result.stdout.trim()).toBe("/tmp/open code")
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
test("supports relative cwd values that start with a dash", async () => {
|
|
34
|
-
const home = (await runtime.exec(server.connection, "pwd")).stdout.trim()
|
|
35
|
-
await runtime.exec(server.connection, "mkdir -- -cwd")
|
|
36
|
-
|
|
37
|
-
const result = await runtime.exec(server.connection, "pwd", { cwd: "-cwd" })
|
|
38
|
-
|
|
39
|
-
expect(result.stdout.trim()).toBe(`${home}/-cwd`)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test("times out long-running commands", async () => {
|
|
43
|
-
await expect(runtime.exec(server.connection, "sleep 2", { timeout: 50 })).rejects.toThrow(
|
|
44
|
-
"command timed out after 50ms",
|
|
45
|
-
)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
test("reports signal-terminated commands as non-zero exits", async () => {
|
|
49
|
-
const result = await runtime.exec(server.connection, "sh -lc 'kill -TERM $$'")
|
|
50
|
-
|
|
51
|
-
expect(result.exitCode).toBe(143)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
test("does not allow timed out commands to mutate remote state later", async () => {
|
|
55
|
-
await runtime.writeFile(server.connection, "/tmp/open-code/app.conf", "port=80\n")
|
|
56
|
-
|
|
57
|
-
await expect(
|
|
58
|
-
runtime.exec(
|
|
59
|
-
server.connection,
|
|
60
|
-
"sh -lc \"sleep 1; echo late >> /tmp/open-code/app.conf\"",
|
|
61
|
-
{ timeout: 100 },
|
|
62
|
-
),
|
|
63
|
-
).rejects.toThrow("command timed out after 100ms")
|
|
64
|
-
|
|
65
|
-
await Bun.sleep(1_500)
|
|
66
|
-
|
|
67
|
-
expect(await runtime.readFile(server.connection, "/tmp/open-code/app.conf")).toBe("port=80\n")
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
test("writes and reads a remote file through sftp", async () => {
|
|
71
|
-
await runtime.writeFile(server.connection, "/tmp/open-code/app.conf", "port=80\n")
|
|
72
|
-
|
|
73
|
-
expect(await runtime.readFile(server.connection, "/tmp/open-code/app.conf")).toBe("port=80\n")
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
test("lists and stats remote paths", async () => {
|
|
77
|
-
const entries = await runtime.listDir(server.connection, "/tmp/open-code", false, 50)
|
|
78
|
-
const recursiveEntries = await runtime.listDir(server.connection, "/tmp/open-code", true, 50)
|
|
79
|
-
|
|
80
|
-
expect(entries.some((entry) => entry.name === "hosts")).toBe(true)
|
|
81
|
-
expect(entries.some((entry) => entry.name === "app.conf")).toBe(true)
|
|
82
|
-
expect(recursiveEntries).toContain("/tmp/open-code/hosts")
|
|
83
|
-
await expect(runtime.listDir(server.connection, "/tmp/does-not-exist", true, 50)).rejects.toThrow()
|
|
84
|
-
expect(await runtime.stat(server.connection, "/tmp/open-code/hosts")).toMatchObject({ isFile: true })
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
test("enforces list limits in both listing modes", async () => {
|
|
88
|
-
const entries = await runtime.listDir(server.connection, "/tmp/open-code", false, 1)
|
|
89
|
-
const recursiveEntries = await runtime.listDir(server.connection, "/tmp/open-code", true, 1)
|
|
90
|
-
|
|
91
|
-
expect(entries).toHaveLength(1)
|
|
92
|
-
expect(recursiveEntries).toHaveLength(1)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
test("times out stalled sftp operations", async () => {
|
|
96
|
-
const impatientRuntime = createSshRuntime({ operationTimeoutMs: 0 })
|
|
97
|
-
|
|
98
|
-
await expect(impatientRuntime.readFile(server.connection, "/tmp/open-code/app.conf")).rejects.toThrow(
|
|
99
|
-
"ssh operation timed out after 0ms",
|
|
100
|
-
)
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
test("applies a unified patch before writing the updated file", async () => {
|
|
104
|
-
const { applyUnifiedPatch } = await import("../../src/core/patch")
|
|
105
|
-
const current = await runtime.readFile(server.connection, "/tmp/open-code/app.conf")
|
|
106
|
-
const next = applyUnifiedPatch(
|
|
107
|
-
current,
|
|
108
|
-
[
|
|
109
|
-
"--- app.conf",
|
|
110
|
-
"+++ app.conf",
|
|
111
|
-
"@@ -1 +1 @@",
|
|
112
|
-
"-port=80",
|
|
113
|
-
"+port=8080",
|
|
114
|
-
"",
|
|
115
|
-
].join("\n"),
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
await runtime.writeFile(server.connection, "/tmp/open-code/app.conf", next)
|
|
119
|
-
|
|
120
|
-
expect(await runtime.readFile(server.connection, "/tmp/open-code/app.conf")).toBe("port=8080\n")
|
|
121
|
-
})
|
|
122
|
-
})
|
package/tests/unit/audit.test.ts
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import { mkdtemp, readFile, readdir, stat } from "node:fs/promises"
|
|
3
|
-
import { join } from "node:path"
|
|
4
|
-
import { tmpdir } from "node:os"
|
|
5
|
-
import { createAuditLogStore } from "../../src/core/audit/log-store"
|
|
6
|
-
import { createGitAuditRepo } from "../../src/core/audit/git-audit-repo"
|
|
7
|
-
import { redactSecrets } from "../../src/core/audit/redact"
|
|
8
|
-
|
|
9
|
-
const runGit = async (cwd: string, args: string[]) => {
|
|
10
|
-
const proc = Bun.spawn(["git", ...args], { cwd, stdout: "pipe", stderr: "pipe" })
|
|
11
|
-
const exitCode = await proc.exited
|
|
12
|
-
if (exitCode !== 0) throw new Error(await new Response(proc.stderr).text())
|
|
13
|
-
return (await new Response(proc.stdout).text()).trim()
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const exists = async (path: string) => {
|
|
17
|
-
try {
|
|
18
|
-
await stat(path)
|
|
19
|
-
return true
|
|
20
|
-
} catch {
|
|
21
|
-
return false
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
describe("audit engine", () => {
|
|
26
|
-
test("preflight prepares the audit log target", async () => {
|
|
27
|
-
const dir = await mkdtemp(join(tmpdir(), "open-code-audit-preflight-"))
|
|
28
|
-
const file = join(dir, "actions.jsonl")
|
|
29
|
-
const store = createAuditLogStore(file)
|
|
30
|
-
|
|
31
|
-
await store.preflight()
|
|
32
|
-
|
|
33
|
-
const pathStat = await stat(file)
|
|
34
|
-
expect(pathStat.isFile()).toBe(true)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test("redacts secret-looking values before writing JSONL entries", async () => {
|
|
38
|
-
const dir = await mkdtemp(join(tmpdir(), "open-code-audit-"))
|
|
39
|
-
const store = createAuditLogStore(join(dir, "actions.jsonl"))
|
|
40
|
-
await store.preflight()
|
|
41
|
-
await store.append({ command: "psql postgresql://user:secret@db/app" })
|
|
42
|
-
const disk = await readFile(join(dir, "actions.jsonl"), "utf8")
|
|
43
|
-
expect(disk.includes("secret")).toBe(false)
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
test("preserves trailing non-secret query data when redacting key/value values", () => {
|
|
47
|
-
expect(redactSecrets("token=abc&mode=ro")).toBe("token=[REDACTED]&mode=ro")
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
test("stamps a fresh timestamp even when the entry includes one", async () => {
|
|
51
|
-
const dir = await mkdtemp(join(tmpdir(), "open-code-audit-timestamp-"))
|
|
52
|
-
const file = join(dir, "actions.jsonl")
|
|
53
|
-
const store = createAuditLogStore(file)
|
|
54
|
-
|
|
55
|
-
await store.preflight()
|
|
56
|
-
await store.append({ timestamp: "1999-01-01T00:00:00.000Z", command: "whoami" })
|
|
57
|
-
|
|
58
|
-
const [disk] = (await readFile(file, "utf8")).trim().split("\n")
|
|
59
|
-
const entry = JSON.parse(disk) as { timestamp: string; command: string }
|
|
60
|
-
|
|
61
|
-
expect(entry.timestamp).not.toBe("1999-01-01T00:00:00.000Z")
|
|
62
|
-
expect(entry.command).toBe("whoami")
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
test("keeps traversal inputs inside the audit repo", async () => {
|
|
66
|
-
const tempRoot = await mkdtemp(join(tmpdir(), "open-code-git-audit-traversal-"))
|
|
67
|
-
const repoDir = join(tempRoot, "repo")
|
|
68
|
-
const repo = createGitAuditRepo(repoDir)
|
|
69
|
-
|
|
70
|
-
await repo.preflight()
|
|
71
|
-
await repo.captureChange({
|
|
72
|
-
server: "../escape",
|
|
73
|
-
path: "/loot",
|
|
74
|
-
before: "before\n",
|
|
75
|
-
after: "after\n",
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
expect(await exists(join(tempRoot, "escape", "loot.before"))).toBe(false)
|
|
79
|
-
expect(await exists(join(tempRoot, "escape", "loot.after"))).toBe(false)
|
|
80
|
-
|
|
81
|
-
let foundAuditArtifact = false
|
|
82
|
-
const walk = async (dir: string) => {
|
|
83
|
-
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
84
|
-
const child = join(dir, entry.name)
|
|
85
|
-
if (entry.isDirectory()) {
|
|
86
|
-
await walk(child)
|
|
87
|
-
} else if (entry.name.endsWith(".before") || entry.name.endsWith(".after")) {
|
|
88
|
-
foundAuditArtifact = true
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
await walk(repoDir)
|
|
93
|
-
|
|
94
|
-
expect(foundAuditArtifact).toBe(true)
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
test("stages only the current snapshot artifacts", async () => {
|
|
98
|
-
const tempRoot = await mkdtemp(join(tmpdir(), "open-code-git-audit-stage-"))
|
|
99
|
-
const repoDir = join(tempRoot, "repo")
|
|
100
|
-
const repo = createGitAuditRepo(repoDir)
|
|
101
|
-
|
|
102
|
-
await repo.preflight()
|
|
103
|
-
await Bun.write(join(repoDir, "unrelated.txt"), "keep-out\n")
|
|
104
|
-
await repo.captureChange({
|
|
105
|
-
server: "prod-a",
|
|
106
|
-
path: "/etc/app.conf",
|
|
107
|
-
before: "port=80\n",
|
|
108
|
-
after: "port=81\n",
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
const files = await runGit(repoDir, ["show", "--pretty=format:", "--name-only", "HEAD"])
|
|
112
|
-
expect(files.includes("unrelated.txt")).toBe(false)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
test("encodes distinct logical targets into distinct repo paths", async () => {
|
|
116
|
-
const tempRoot = await mkdtemp(join(tmpdir(), "open-code-git-audit-unique-"))
|
|
117
|
-
const repoDir = join(tempRoot, "repo")
|
|
118
|
-
const repo = createGitAuditRepo(repoDir)
|
|
119
|
-
|
|
120
|
-
await repo.preflight()
|
|
121
|
-
await repo.captureChange({
|
|
122
|
-
server: "prod:a",
|
|
123
|
-
path: "/etc/a?b",
|
|
124
|
-
before: "one\n",
|
|
125
|
-
after: "two\n",
|
|
126
|
-
})
|
|
127
|
-
await repo.captureChange({
|
|
128
|
-
server: "prod/a",
|
|
129
|
-
path: "/etc/a:b",
|
|
130
|
-
before: "three\n",
|
|
131
|
-
after: "four\n",
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
let artifactCount = 0
|
|
135
|
-
const walk = async (dir: string) => {
|
|
136
|
-
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
137
|
-
const child = join(dir, entry.name)
|
|
138
|
-
if (entry.isDirectory()) {
|
|
139
|
-
await walk(child)
|
|
140
|
-
} else if (entry.name.endsWith(".before") || entry.name.endsWith(".after")) {
|
|
141
|
-
artifactCount++
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
await walk(repoDir)
|
|
146
|
-
|
|
147
|
-
expect(artifactCount).toBe(4)
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
test("keeps hostile path segments inside the repo and distinct", async () => {
|
|
151
|
-
const tempRoot = await mkdtemp(join(tmpdir(), "open-code-git-audit-hostile-"))
|
|
152
|
-
const repoDir = join(tempRoot, "repo")
|
|
153
|
-
const repo = createGitAuditRepo(repoDir)
|
|
154
|
-
|
|
155
|
-
await repo.preflight()
|
|
156
|
-
await repo.captureChange({
|
|
157
|
-
server: "../escape",
|
|
158
|
-
path: "/../loot?mode=ro",
|
|
159
|
-
before: "before\n",
|
|
160
|
-
after: "after\n",
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
expect(await exists(join(tempRoot, "escape"))).toBe(false)
|
|
164
|
-
|
|
165
|
-
let artifactCount = 0
|
|
166
|
-
const walk = async (dir: string) => {
|
|
167
|
-
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
168
|
-
const child = join(dir, entry.name)
|
|
169
|
-
if (entry.isDirectory()) {
|
|
170
|
-
await walk(child)
|
|
171
|
-
} else if (entry.name.endsWith(".before") || entry.name.endsWith(".after")) {
|
|
172
|
-
artifactCount++
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
await walk(repoDir)
|
|
177
|
-
|
|
178
|
-
expect(artifactCount).toBe(2)
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
test("records repeated identical captures as distinct commits", async () => {
|
|
182
|
-
const tempRoot = await mkdtemp(join(tmpdir(), "open-code-git-audit-repeat-"))
|
|
183
|
-
const repoDir = join(tempRoot, "repo")
|
|
184
|
-
const repo = createGitAuditRepo(repoDir)
|
|
185
|
-
|
|
186
|
-
await repo.preflight()
|
|
187
|
-
await repo.captureChange({
|
|
188
|
-
server: "prod-a",
|
|
189
|
-
path: "/etc/app.conf",
|
|
190
|
-
before: "port=80\n",
|
|
191
|
-
after: "port=81\n",
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
const firstCount = await runGit(repoDir, ["rev-list", "--count", "HEAD"])
|
|
195
|
-
|
|
196
|
-
await repo.captureChange({
|
|
197
|
-
server: "prod-a",
|
|
198
|
-
path: "/etc/app.conf",
|
|
199
|
-
before: "port=80\n",
|
|
200
|
-
after: "port=81\n",
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
const secondCount = await runGit(repoDir, ["rev-list", "--count", "HEAD"])
|
|
204
|
-
|
|
205
|
-
expect(firstCount).toBe("1")
|
|
206
|
-
expect(secondCount).toBe("2")
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
test("creates a git commit for before and after snapshots", async () => {
|
|
210
|
-
const dir = await mkdtemp(join(tmpdir(), "open-code-git-audit-"))
|
|
211
|
-
const repo = createGitAuditRepo(dir)
|
|
212
|
-
await repo.preflight()
|
|
213
|
-
await repo.captureChange({
|
|
214
|
-
server: "prod-a",
|
|
215
|
-
path: "/etc/app.conf",
|
|
216
|
-
before: "port=80\n",
|
|
217
|
-
after: "port=81\n",
|
|
218
|
-
})
|
|
219
|
-
expect(await repo.lastCommitMessage()).toContain("prod-a")
|
|
220
|
-
})
|
|
221
|
-
})
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import { readFile } from "node:fs/promises"
|
|
3
|
-
|
|
4
|
-
describe("build layout", () => {
|
|
5
|
-
test("package metadata no longer declares keytar", async () => {
|
|
6
|
-
const packageJson = JSON.parse(await readFile(new URL("../../package.json", import.meta.url), "utf8"))
|
|
7
|
-
|
|
8
|
-
expect(packageJson.dependencies.keytar).toBeUndefined()
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
test("package metadata publishes openshell with a bin entry", async () => {
|
|
12
|
-
const packageJson = JSON.parse(await readFile(new URL("../../package.json", import.meta.url), "utf8"))
|
|
13
|
-
|
|
14
|
-
expect(packageJson.name).toBe("@junwu168/openshell")
|
|
15
|
-
expect(packageJson.private).not.toBe(true)
|
|
16
|
-
expect(packageJson.bin).toEqual({
|
|
17
|
-
openshell: "./dist/cli/openshell.js",
|
|
18
|
-
})
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
test("emits the package entry at dist/index.js", async () => {
|
|
22
|
-
const packageJson = JSON.parse(await readFile(new URL("../../package.json", import.meta.url), "utf8"))
|
|
23
|
-
const tsconfig = JSON.parse(await readFile(new URL("../../tsconfig.json", import.meta.url), "utf8"))
|
|
24
|
-
|
|
25
|
-
expect(packageJson.exports["."].default).toBe("./dist/index.js")
|
|
26
|
-
expect(tsconfig.compilerOptions.rootDir).toBe("src")
|
|
27
|
-
})
|
|
28
|
-
})
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from "bun:test"
|
|
2
|
-
import { mkdtemp, readFile, rm, writeFile } 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("opencode config integration", () => {
|
|
13
|
-
test("install merges openshell into the global OpenCode plugin list", async () => {
|
|
14
|
-
const tempDir = await mkdtemp(join(tmpdir(), "openshell-opencode-config-"))
|
|
15
|
-
tempDirs.push(tempDir)
|
|
16
|
-
const opencodeConfigFile = join(tempDir, "opencode.json")
|
|
17
|
-
await writeFile(
|
|
18
|
-
opencodeConfigFile,
|
|
19
|
-
JSON.stringify({
|
|
20
|
-
plugin: ["existing-plugin"],
|
|
21
|
-
permission: {
|
|
22
|
-
edit: "ask",
|
|
23
|
-
bash: {
|
|
24
|
-
"kubectl *": "ask",
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
}),
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
const { installIntoOpenCodeConfig, defaultBashPermissions } = await import("../../src/product/opencode-config")
|
|
31
|
-
await installIntoOpenCodeConfig(opencodeConfigFile)
|
|
32
|
-
|
|
33
|
-
const merged = JSON.parse(await readFile(opencodeConfigFile, "utf8"))
|
|
34
|
-
expect(merged).toMatchObject({
|
|
35
|
-
plugin: ["existing-plugin", "@junwu168/openshell"],
|
|
36
|
-
permission: {
|
|
37
|
-
edit: "ask",
|
|
38
|
-
bash: expect.objectContaining({
|
|
39
|
-
...defaultBashPermissions,
|
|
40
|
-
"kubectl *": "ask",
|
|
41
|
-
}),
|
|
42
|
-
},
|
|
43
|
-
})
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
test("uninstall removes openshell registration while preserving unrelated plugins", async () => {
|
|
47
|
-
const tempDir = await mkdtemp(join(tmpdir(), "openshell-opencode-config-"))
|
|
48
|
-
tempDirs.push(tempDir)
|
|
49
|
-
const opencodeConfigFile = join(tempDir, "opencode.json")
|
|
50
|
-
await writeFile(
|
|
51
|
-
opencodeConfigFile,
|
|
52
|
-
JSON.stringify({
|
|
53
|
-
plugin: ["existing-plugin", "@junwu168/openshell"],
|
|
54
|
-
permission: {
|
|
55
|
-
edit: "ask",
|
|
56
|
-
bash: {
|
|
57
|
-
"kubectl *": "ask",
|
|
58
|
-
"cat *": "allow",
|
|
59
|
-
"grep *": "allow",
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
}),
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
const { uninstallFromOpenCodeConfig } = await import("../../src/product/opencode-config")
|
|
66
|
-
await uninstallFromOpenCodeConfig(opencodeConfigFile)
|
|
67
|
-
|
|
68
|
-
const merged = JSON.parse(await readFile(opencodeConfigFile, "utf8"))
|
|
69
|
-
expect(merged.plugin).toEqual(["existing-plugin"])
|
|
70
|
-
expect(merged.permission.bash).toEqual({
|
|
71
|
-
"kubectl *": "ask",
|
|
72
|
-
})
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
test("uninstall removes the plugin key when openshell was the only registered plugin", async () => {
|
|
76
|
-
const tempDir = await mkdtemp(join(tmpdir(), "openshell-opencode-config-"))
|
|
77
|
-
tempDirs.push(tempDir)
|
|
78
|
-
const opencodeConfigFile = join(tempDir, "opencode.json")
|
|
79
|
-
await writeFile(
|
|
80
|
-
opencodeConfigFile,
|
|
81
|
-
JSON.stringify({
|
|
82
|
-
plugin: ["@junwu168/openshell"],
|
|
83
|
-
permission: {
|
|
84
|
-
edit: "ask",
|
|
85
|
-
bash: {
|
|
86
|
-
"*": "ask",
|
|
87
|
-
"cat *": "allow",
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
}),
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
const { uninstallFromOpenCodeConfig } = await import("../../src/product/opencode-config")
|
|
94
|
-
await uninstallFromOpenCodeConfig(opencodeConfigFile)
|
|
95
|
-
|
|
96
|
-
const merged = JSON.parse(await readFile(opencodeConfigFile, "utf8"))
|
|
97
|
-
expect(merged.plugin).toBeUndefined()
|
|
98
|
-
expect(merged.permission).toBeUndefined()
|
|
99
|
-
})
|
|
100
|
-
})
|