@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,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
- }
@@ -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
- })