@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,118 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises"
|
|
2
|
-
import { dirname } from "node:path"
|
|
3
|
-
|
|
4
|
-
export const defaultBashPermissions = {
|
|
5
|
-
"*": "ask",
|
|
6
|
-
"cat *": "allow",
|
|
7
|
-
"grep *": "allow",
|
|
8
|
-
"find *": "allow",
|
|
9
|
-
"ls *": "allow",
|
|
10
|
-
pwd: "allow",
|
|
11
|
-
"uname *": "allow",
|
|
12
|
-
"df *": "allow",
|
|
13
|
-
"free *": "allow",
|
|
14
|
-
"ps *": "allow",
|
|
15
|
-
"systemctl status *": "allow",
|
|
16
|
-
} as const
|
|
17
|
-
|
|
18
|
-
type OpenCodeConfig = {
|
|
19
|
-
plugin?: string[]
|
|
20
|
-
permission?: {
|
|
21
|
-
edit?: unknown
|
|
22
|
-
bash?: unknown
|
|
23
|
-
[key: string]: unknown
|
|
24
|
-
}
|
|
25
|
-
[key: string]: unknown
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const readConfig = async (opencodeConfigFile: string): Promise<OpenCodeConfig> => {
|
|
29
|
-
try {
|
|
30
|
-
return JSON.parse(await readFile(opencodeConfigFile, "utf8")) as OpenCodeConfig
|
|
31
|
-
} catch (error: unknown) {
|
|
32
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
33
|
-
return {}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
throw error
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const writeConfig = async (opencodeConfigFile: string, config: OpenCodeConfig) => {
|
|
41
|
-
await mkdir(dirname(opencodeConfigFile), { recursive: true })
|
|
42
|
-
await writeFile(opencodeConfigFile, JSON.stringify(config, null, 2) + "\n")
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export const installIntoOpenCodeConfig = async (opencodeConfigFile: string) => {
|
|
46
|
-
const current = await readConfig(opencodeConfigFile)
|
|
47
|
-
const plugins = Array.isArray(current.plugin) ? [...current.plugin] : []
|
|
48
|
-
if (!plugins.includes("@junwu168/openshell")) {
|
|
49
|
-
plugins.push("@junwu168/openshell")
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const currentPermissions =
|
|
53
|
-
typeof current.permission === "object" && current.permission !== null ? current.permission : {}
|
|
54
|
-
const currentBash =
|
|
55
|
-
typeof currentPermissions.bash === "object" && currentPermissions.bash !== null
|
|
56
|
-
? currentPermissions.bash
|
|
57
|
-
: {}
|
|
58
|
-
|
|
59
|
-
await writeConfig(opencodeConfigFile, {
|
|
60
|
-
...current,
|
|
61
|
-
plugin: plugins,
|
|
62
|
-
permission: {
|
|
63
|
-
...currentPermissions,
|
|
64
|
-
edit: currentPermissions.edit ?? "ask",
|
|
65
|
-
bash: {
|
|
66
|
-
...defaultBashPermissions,
|
|
67
|
-
...currentBash,
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
})
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export const uninstallFromOpenCodeConfig = async (opencodeConfigFile: string) => {
|
|
74
|
-
const current = await readConfig(opencodeConfigFile)
|
|
75
|
-
const plugins = Array.isArray(current.plugin)
|
|
76
|
-
? current.plugin.filter((plugin) => plugin !== "@junwu168/openshell")
|
|
77
|
-
: []
|
|
78
|
-
const currentPermissions =
|
|
79
|
-
typeof current.permission === "object" && current.permission !== null ? { ...current.permission } : {}
|
|
80
|
-
const currentBash =
|
|
81
|
-
typeof currentPermissions.bash === "object" && currentPermissions.bash !== null
|
|
82
|
-
? { ...(currentPermissions.bash as Record<string, unknown>) }
|
|
83
|
-
: null
|
|
84
|
-
|
|
85
|
-
if (currentBash) {
|
|
86
|
-
for (const [pattern, permission] of Object.entries(defaultBashPermissions)) {
|
|
87
|
-
if (currentBash[pattern] === permission) {
|
|
88
|
-
delete currentBash[pattern]
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (Object.keys(currentBash).length === 0) {
|
|
93
|
-
delete currentPermissions.bash
|
|
94
|
-
} else {
|
|
95
|
-
currentPermissions.bash = currentBash
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (currentPermissions.edit === "ask") {
|
|
100
|
-
delete currentPermissions.edit
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const nextConfig: OpenCodeConfig = { ...current }
|
|
104
|
-
|
|
105
|
-
if (plugins.length > 0) {
|
|
106
|
-
nextConfig.plugin = plugins
|
|
107
|
-
} else {
|
|
108
|
-
delete nextConfig.plugin
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (Object.keys(currentPermissions).length > 0) {
|
|
112
|
-
nextConfig.permission = currentPermissions
|
|
113
|
-
} else {
|
|
114
|
-
delete nextConfig.permission
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
await writeConfig(opencodeConfigFile, nextConfig)
|
|
118
|
-
}
|
package/src/product/uninstall.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { rm } from "node:fs/promises"
|
|
2
|
-
import { cwd, stdout } from "node:process"
|
|
3
|
-
import { createRuntimePaths } from "../core/paths.js"
|
|
4
|
-
import { uninstallFromOpenCodeConfig } from "./opencode-config.js"
|
|
5
|
-
import { createWorkspaceTracker } from "./workspace-tracker.js"
|
|
6
|
-
|
|
7
|
-
type WritableLike = {
|
|
8
|
-
write(chunk: string): void
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
type RuntimePaths = ReturnType<typeof createRuntimePaths>
|
|
12
|
-
|
|
13
|
-
type UninstallOptions = {
|
|
14
|
-
runtimePaths: RuntimePaths
|
|
15
|
-
stdout: WritableLike
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const uninstallOpenShell = async ({ runtimePaths, stdout }: UninstallOptions) => {
|
|
19
|
-
const tracker = createWorkspaceTracker(runtimePaths.workspaceTrackerFile)
|
|
20
|
-
const trackedWorkspaces = await tracker.list()
|
|
21
|
-
|
|
22
|
-
await uninstallFromOpenCodeConfig(runtimePaths.opencodeConfigFile)
|
|
23
|
-
|
|
24
|
-
for (const entry of trackedWorkspaces) {
|
|
25
|
-
await rm(entry.managedPath, { recursive: true, force: true })
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
await rm(runtimePaths.configDir, { recursive: true, force: true })
|
|
29
|
-
await rm(runtimePaths.dataDir, { recursive: true, force: true })
|
|
30
|
-
|
|
31
|
-
stdout.write(
|
|
32
|
-
[
|
|
33
|
-
"Removed openshell.",
|
|
34
|
-
`OpenShell config: ${runtimePaths.configDir}`,
|
|
35
|
-
`OpenShell data: ${runtimePaths.dataDir}`,
|
|
36
|
-
].join("\n") + "\n",
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export const runUninstallCli = async (_argv: string[] = [], stream: WritableLike = stdout) => {
|
|
41
|
-
await uninstallOpenShell({
|
|
42
|
-
runtimePaths: createRuntimePaths(cwd()),
|
|
43
|
-
stdout: stream,
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
return 0
|
|
47
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises"
|
|
2
|
-
import { dirname } from "node:path"
|
|
3
|
-
|
|
4
|
-
export type WorkspaceTrackerEntry = {
|
|
5
|
-
workspaceRoot: string
|
|
6
|
-
managedPath: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export type WorkspaceTracker = {
|
|
10
|
-
list(): Promise<WorkspaceTrackerEntry[]>
|
|
11
|
-
record(entry: WorkspaceTrackerEntry): Promise<void>
|
|
12
|
-
remove(workspaceRoot: string): Promise<void>
|
|
13
|
-
clear(): Promise<void>
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const readEntries = async (trackerFile: string): Promise<WorkspaceTrackerEntry[]> => {
|
|
17
|
-
try {
|
|
18
|
-
const raw = await readFile(trackerFile, "utf8")
|
|
19
|
-
const parsed = JSON.parse(raw) as unknown
|
|
20
|
-
if (!Array.isArray(parsed)) {
|
|
21
|
-
return []
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return parsed.filter((entry): entry is WorkspaceTrackerEntry => {
|
|
25
|
-
if (typeof entry !== "object" || entry === null) {
|
|
26
|
-
return false
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const candidate = entry as Record<string, unknown>
|
|
30
|
-
return typeof candidate.workspaceRoot === "string" && typeof candidate.managedPath === "string"
|
|
31
|
-
})
|
|
32
|
-
} catch (error: unknown) {
|
|
33
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
34
|
-
return []
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
throw error
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const writeEntries = async (trackerFile: string, entries: WorkspaceTrackerEntry[]) => {
|
|
42
|
-
await mkdir(dirname(trackerFile), { recursive: true })
|
|
43
|
-
await writeFile(trackerFile, JSON.stringify(entries, null, 2) + "\n")
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export const createWorkspaceTracker = (trackerFile: string): WorkspaceTracker => ({
|
|
47
|
-
async list() {
|
|
48
|
-
return readEntries(trackerFile)
|
|
49
|
-
},
|
|
50
|
-
async record(entry) {
|
|
51
|
-
const entries = await readEntries(trackerFile)
|
|
52
|
-
const next = [
|
|
53
|
-
...entries.filter((existing) => existing.workspaceRoot !== entry.workspaceRoot),
|
|
54
|
-
entry,
|
|
55
|
-
]
|
|
56
|
-
|
|
57
|
-
await writeEntries(trackerFile, next)
|
|
58
|
-
},
|
|
59
|
-
async remove(workspaceRoot) {
|
|
60
|
-
const entries = await readEntries(trackerFile)
|
|
61
|
-
await writeEntries(
|
|
62
|
-
trackerFile,
|
|
63
|
-
entries.filter((entry) => entry.workspaceRoot !== workspaceRoot),
|
|
64
|
-
)
|
|
65
|
-
},
|
|
66
|
-
async clear() {
|
|
67
|
-
await writeEntries(trackerFile, [])
|
|
68
|
-
},
|
|
69
|
-
})
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { Client, type ConnectConfig } from "ssh2"
|
|
2
|
-
import { GenericContainer, Wait } from "testcontainers"
|
|
3
|
-
|
|
4
|
-
const SSH_IMAGE = "linuxserver/openssh-server:10.2_p1-r0-ls219"
|
|
5
|
-
|
|
6
|
-
const waitForSshReady = async (connection: ConnectConfig, timeoutMs = 15_000) => {
|
|
7
|
-
const deadline = Date.now() + timeoutMs
|
|
8
|
-
let lastError: unknown
|
|
9
|
-
|
|
10
|
-
while (Date.now() < deadline) {
|
|
11
|
-
try {
|
|
12
|
-
await new Promise<void>((resolve, reject) => {
|
|
13
|
-
const client = new Client()
|
|
14
|
-
let settled = false
|
|
15
|
-
|
|
16
|
-
const finish = (handler: () => void) => {
|
|
17
|
-
if (settled) {
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
settled = true
|
|
22
|
-
handler()
|
|
23
|
-
client.end()
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
client
|
|
27
|
-
.on("ready", () => finish(resolve))
|
|
28
|
-
.on("error", (error) => finish(() => reject(error)))
|
|
29
|
-
.connect({
|
|
30
|
-
...connection,
|
|
31
|
-
readyTimeout: Math.min(2_000, Math.max(1, deadline - Date.now())),
|
|
32
|
-
})
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
return
|
|
36
|
-
} catch (error) {
|
|
37
|
-
lastError = error
|
|
38
|
-
await Bun.sleep(250)
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
throw lastError ?? new Error("ssh readiness timed out")
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export const startFakeSshServer = async () => {
|
|
46
|
-
const container = await new GenericContainer(SSH_IMAGE)
|
|
47
|
-
.withCopyContentToContainer([
|
|
48
|
-
{
|
|
49
|
-
content: [
|
|
50
|
-
"#!/usr/bin/env sh",
|
|
51
|
-
"set -eu",
|
|
52
|
-
"",
|
|
53
|
-
"mkdir -p /tmp/open-code",
|
|
54
|
-
"chmod 1777 /tmp/open-code",
|
|
55
|
-
"cat <<'EOF' >/tmp/open-code/hosts",
|
|
56
|
-
"127.0.0.1 localhost",
|
|
57
|
-
"EOF",
|
|
58
|
-
"cat <<'EOF' >/tmp/open-code/app.conf",
|
|
59
|
-
"port=80",
|
|
60
|
-
"EOF",
|
|
61
|
-
"chown open:open /tmp/open-code/hosts /tmp/open-code/app.conf",
|
|
62
|
-
"chmod 0644 /tmp/open-code/hosts /tmp/open-code/app.conf",
|
|
63
|
-
"",
|
|
64
|
-
].join("\n"),
|
|
65
|
-
target: "/custom-cont-init.d/10-seed-open-code.sh",
|
|
66
|
-
mode: 0o755,
|
|
67
|
-
},
|
|
68
|
-
])
|
|
69
|
-
.withEnvironment({
|
|
70
|
-
USER_NAME: "open",
|
|
71
|
-
USER_PASSWORD: "openpass",
|
|
72
|
-
PASSWORD_ACCESS: "true",
|
|
73
|
-
SUDO_ACCESS: "false",
|
|
74
|
-
})
|
|
75
|
-
.withExposedPorts(2222)
|
|
76
|
-
.withWaitStrategy(Wait.forLogMessage("sshd is listening on port 2222"))
|
|
77
|
-
.start()
|
|
78
|
-
|
|
79
|
-
const connection = {
|
|
80
|
-
host: container.getHost(),
|
|
81
|
-
port: container.getMappedPort(2222),
|
|
82
|
-
username: "open",
|
|
83
|
-
password: "openpass",
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
await waitForSshReady(connection)
|
|
88
|
-
} catch (error) {
|
|
89
|
-
await container.stop().catch(() => undefined)
|
|
90
|
-
throw error
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
connection,
|
|
95
|
-
stop: () => container.stop(),
|
|
96
|
-
}
|
|
97
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { afterEach, 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
|
-
|
|
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
|
-
const createWritable = () => {
|
|
13
|
-
let buffer = ""
|
|
14
|
-
|
|
15
|
-
return {
|
|
16
|
-
write(chunk: string) {
|
|
17
|
-
buffer += chunk
|
|
18
|
-
},
|
|
19
|
-
toString() {
|
|
20
|
-
return buffer
|
|
21
|
-
},
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
describe("openshell install lifecycle", () => {
|
|
26
|
-
test("install creates openshell state and uninstall removes tracked workspaces", async () => {
|
|
27
|
-
const tempDir = await mkdtemp(join(tmpdir(), "openshell-install-lifecycle-"))
|
|
28
|
-
tempDirs.push(tempDir)
|
|
29
|
-
|
|
30
|
-
const configDir = join(tempDir, "config", "openshell")
|
|
31
|
-
const dataDir = join(tempDir, "data", "openshell")
|
|
32
|
-
const opencodeConfigDir = join(tempDir, "config", "opencode")
|
|
33
|
-
const opencodeConfigFile = join(opencodeConfigDir, "opencode.json")
|
|
34
|
-
const workspaceRoot = join(tempDir, "workspace")
|
|
35
|
-
const managedPath = join(workspaceRoot, ".open-code")
|
|
36
|
-
|
|
37
|
-
await mkdir(managedPath, { recursive: true })
|
|
38
|
-
await writeFile(join(managedPath, "servers.json"), "[]")
|
|
39
|
-
await mkdir(opencodeConfigDir, { recursive: true })
|
|
40
|
-
await writeFile(opencodeConfigFile, JSON.stringify({ plugin: ["existing-plugin"] }))
|
|
41
|
-
|
|
42
|
-
const runtimePaths = {
|
|
43
|
-
configDir,
|
|
44
|
-
dataDir,
|
|
45
|
-
globalRegistryFile: join(configDir, "servers.json"),
|
|
46
|
-
workspaceTrackerFile: join(dataDir, "workspaces.json"),
|
|
47
|
-
opencodeConfigDir,
|
|
48
|
-
opencodeConfigFile,
|
|
49
|
-
workspaceRegistryDir: join(workspaceRoot, ".open-code"),
|
|
50
|
-
workspaceRegistryFile: join(workspaceRoot, ".open-code", "servers.json"),
|
|
51
|
-
auditLogFile: join(dataDir, "audit", "actions.jsonl"),
|
|
52
|
-
auditRepoDir: join(dataDir, "audit", "repo"),
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const stdout = createWritable()
|
|
56
|
-
const { installOpenShell } = await import("../../src/product/install")
|
|
57
|
-
await installOpenShell({ runtimePaths, stdout })
|
|
58
|
-
|
|
59
|
-
const installedConfig = JSON.parse(await readFile(opencodeConfigFile, "utf8"))
|
|
60
|
-
expect(installedConfig.plugin).toContain("@junwu168/openshell")
|
|
61
|
-
expect(stdout.toString()).toContain("Installed openshell")
|
|
62
|
-
|
|
63
|
-
const trackerFile = join(dataDir, "workspaces.json")
|
|
64
|
-
await writeFile(
|
|
65
|
-
trackerFile,
|
|
66
|
-
JSON.stringify([
|
|
67
|
-
{
|
|
68
|
-
workspaceRoot,
|
|
69
|
-
managedPath,
|
|
70
|
-
},
|
|
71
|
-
]),
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
const uninstallStdout = createWritable()
|
|
75
|
-
const { uninstallOpenShell } = await import("../../src/product/uninstall")
|
|
76
|
-
await uninstallOpenShell({ runtimePaths, stdout: uninstallStdout })
|
|
77
|
-
|
|
78
|
-
await expect(readFile(opencodeConfigFile, "utf8")).resolves.toContain("existing-plugin")
|
|
79
|
-
await expect(readFile(opencodeConfigFile, "utf8")).resolves.not.toContain("@junwu168/openshell")
|
|
80
|
-
await expect(rm(managedPath, { recursive: false })).rejects.toMatchObject({ code: "ENOENT" })
|
|
81
|
-
await expect(rm(configDir, { recursive: false })).rejects.toMatchObject({ code: "ENOENT" })
|
|
82
|
-
await expect(rm(dataDir, { recursive: false })).rejects.toMatchObject({ code: "ENOENT" })
|
|
83
|
-
expect(uninstallStdout.toString()).toContain("Removed openshell")
|
|
84
|
-
})
|
|
85
|
-
})
|