@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.
Files changed (61) hide show
  1. package/dist/cli/openshell.js +4 -0
  2. package/dist/core/audit/log-store.js +1 -1
  3. package/dist/core/orchestrator.d.ts +2 -2
  4. package/dist/core/orchestrator.js +3 -3
  5. package/dist/core/result.d.ts +1 -1
  6. package/dist/index.d.ts +3 -3
  7. package/dist/index.js +3 -3
  8. package/dist/opencode/plugin.d.ts +1 -1
  9. package/dist/opencode/plugin.js +8 -8
  10. package/package.json +6 -1
  11. package/.claude/settings.local.json +0 -15
  12. package/bun.lock +0 -368
  13. package/docs/superpowers/notes/2026-03-25-opencode-remote-tools-handoff.md +0 -81
  14. package/docs/superpowers/notes/2026-03-26-openshell-pre-release-review.md +0 -174
  15. package/docs/superpowers/plans/2026-03-25-opencode-remote-tools.md +0 -1656
  16. package/docs/superpowers/plans/2026-03-25-server-registry-cli.md +0 -54
  17. package/docs/superpowers/plans/2026-03-26-config-backed-credential-registry.md +0 -494
  18. package/docs/superpowers/plans/2026-03-26-openshell-release-prep.md +0 -639
  19. package/docs/superpowers/specs/2026-03-25-opencode-remote-tools-design.md +0 -378
  20. package/docs/superpowers/specs/2026-03-26-config-backed-credential-registry-design.md +0 -272
  21. package/docs/superpowers/specs/2026-03-26-openshell-release-prep-design.md +0 -197
  22. package/examples/opencode-local/opencode.json +0 -19
  23. package/scripts/openshell.ts +0 -3
  24. package/scripts/server-registry.ts +0 -3
  25. package/src/cli/openshell.ts +0 -60
  26. package/src/cli/server-registry.ts +0 -476
  27. package/src/core/audit/git-audit-repo.ts +0 -42
  28. package/src/core/audit/log-store.ts +0 -20
  29. package/src/core/audit/redact.ts +0 -4
  30. package/src/core/contracts.ts +0 -51
  31. package/src/core/orchestrator.ts +0 -1082
  32. package/src/core/patch.ts +0 -11
  33. package/src/core/paths.ts +0 -32
  34. package/src/core/policy.ts +0 -30
  35. package/src/core/registry/server-registry.ts +0 -505
  36. package/src/core/result.ts +0 -16
  37. package/src/core/ssh/ssh-runtime.ts +0 -355
  38. package/src/index.ts +0 -3
  39. package/src/opencode/plugin.ts +0 -242
  40. package/src/product/install.ts +0 -43
  41. package/src/product/opencode-config.ts +0 -118
  42. package/src/product/uninstall.ts +0 -47
  43. package/src/product/workspace-tracker.ts +0 -69
  44. package/tests/integration/fake-ssh-server.ts +0 -97
  45. package/tests/integration/install-lifecycle.test.ts +0 -85
  46. package/tests/integration/orchestrator.test.ts +0 -767
  47. package/tests/integration/ssh-runtime.test.ts +0 -122
  48. package/tests/unit/audit.test.ts +0 -221
  49. package/tests/unit/build-layout.test.ts +0 -28
  50. package/tests/unit/opencode-config.test.ts +0 -100
  51. package/tests/unit/opencode-plugin.test.ts +0 -358
  52. package/tests/unit/openshell-cli.test.ts +0 -60
  53. package/tests/unit/paths.test.ts +0 -64
  54. package/tests/unit/plugin-export.test.ts +0 -10
  55. package/tests/unit/policy.test.ts +0 -53
  56. package/tests/unit/release-docs.test.ts +0 -31
  57. package/tests/unit/result.test.ts +0 -28
  58. package/tests/unit/server-registry-cli.test.ts +0 -673
  59. package/tests/unit/server-registry.test.ts +0 -452
  60. package/tests/unit/workspace-tracker.test.ts +0 -57
  61. package/tsconfig.json +0 -14
@@ -1,355 +0,0 @@
1
- import { constants as osConstants } from "node:os"
2
- import { Shescape } from "shescape"
3
- import { Client, type ConnectConfig } from "ssh2"
4
-
5
- const shell = new Shescape({ shell: "zsh" })
6
- const DEFAULT_OPERATION_TIMEOUT_MS = 30_000
7
-
8
- type ExecOptions = {
9
- cwd?: string
10
- timeout?: number
11
- }
12
-
13
- type ExecResult = {
14
- stdout: string
15
- stderr: string
16
- exitCode: number
17
- }
18
-
19
- type DirEntry = {
20
- name: string
21
- longname: string
22
- }
23
-
24
- type PathStat = {
25
- size: number
26
- mode: number
27
- isFile: boolean
28
- isDirectory: boolean
29
- }
30
-
31
- type RuntimeOptions = {
32
- operationTimeoutMs?: number
33
- }
34
-
35
- const normalizeShellPath = (path: string) => (path.startsWith("-") ? `./${path}` : path)
36
- const quoteShellPath = (path: string) => shell.quote(normalizeShellPath(path))
37
- const clampLimit = (limit: number) => Math.max(0, Math.trunc(limit))
38
- const joinRemotePath = (parent: string, name: string) => (parent.endsWith("/") ? `${parent}${name}` : `${parent}/${name}`)
39
- const signalToExitCode = (signal: string | null | undefined) => {
40
- if (!signal) {
41
- return 0
42
- }
43
-
44
- const normalized = signal.startsWith("SIG") ? signal : `SIG${signal}`
45
- const signalNumber = osConstants.signals[normalized as keyof typeof osConstants.signals]
46
-
47
- return signalNumber === undefined ? 1 : 128 + signalNumber
48
- }
49
-
50
- const withClient = <T>(
51
- connection: ConnectConfig,
52
- action: (client: Client) => Promise<T>,
53
- timeoutMs: number | null = null,
54
- ) =>
55
- new Promise<T>((resolve, reject) => {
56
- const client = new Client()
57
- let settled = false
58
- let timer: ReturnType<typeof setTimeout> | null = null
59
-
60
- const finish = (handler: () => void, close: () => void = () => client.end()) => {
61
- if (settled) {
62
- return
63
- }
64
-
65
- settled = true
66
- if (timer) {
67
- clearTimeout(timer)
68
- }
69
- handler()
70
- close()
71
- }
72
-
73
- if (timeoutMs !== null) {
74
- timer = setTimeout(() => {
75
- finish(() => reject(new Error(`ssh operation timed out after ${timeoutMs}ms`)), () => client.destroy())
76
- }, timeoutMs)
77
- }
78
-
79
- client
80
- .on("ready", () => {
81
- action(client).then(
82
- (result) => finish(() => resolve(result)),
83
- (error) => finish(() => reject(error)),
84
- )
85
- })
86
- .on("error", (error) => finish(() => reject(error)))
87
- .connect(timeoutMs === null ? connection : { ...connection, readyTimeout: connection.readyTimeout ?? timeoutMs })
88
- })
89
-
90
- export const createSshRuntime = (options: RuntimeOptions = {}) => {
91
- const operationTimeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS
92
-
93
- const exec = (connection: ConnectConfig, command: string, options: ExecOptions = {}) =>
94
- withClient<ExecResult>(connection, (client) =>
95
- new Promise((resolve, reject) => {
96
- const effective = options.cwd ? `cd -- ${quoteShellPath(options.cwd)} && ${command}` : command
97
- let settled = false
98
- let streamRef: {
99
- signal: (signalName: string) => void
100
- close: () => void
101
- } | null = null
102
-
103
- const finish = (handler: () => void) => {
104
- if (settled) {
105
- return
106
- }
107
-
108
- settled = true
109
-
110
- if (timer) {
111
- clearTimeout(timer)
112
- }
113
-
114
- handler()
115
- }
116
-
117
- const timer =
118
- options.timeout === undefined
119
- ? null
120
- : setTimeout(() => {
121
- if (streamRef) {
122
- try {
123
- streamRef.signal("SIGKILL")
124
- } catch {}
125
-
126
- try {
127
- streamRef.close()
128
- } catch {}
129
- }
130
-
131
- finish(() => reject(new Error(`command timed out after ${options.timeout}ms`)))
132
- }, options.timeout)
133
-
134
- client.exec(effective, (error, stream) => {
135
- if (error) {
136
- finish(() => reject(error))
137
- return
138
- }
139
-
140
- streamRef = stream
141
-
142
- let stdout = ""
143
- let stderr = ""
144
- let exitCode = 0
145
-
146
- stream.on("data", (chunk: Buffer | string) => {
147
- stdout += chunk.toString()
148
- })
149
-
150
- stream.stderr.on("data", (chunk: Buffer | string) => {
151
- stderr += chunk.toString()
152
- })
153
-
154
- stream.on("exit", (code, signal) => {
155
- exitCode = code ?? signalToExitCode(signal)
156
- })
157
-
158
- stream.on("close", () => {
159
- finish(() => resolve({ stdout, stderr, exitCode }))
160
- })
161
-
162
- stream.on("error", (streamError: Error) => {
163
- finish(() => reject(streamError))
164
- })
165
- })
166
- }),
167
- null,
168
- )
169
-
170
- const readFile = (connection: ConnectConfig, path: string) =>
171
- withClient<string>(connection, (client) =>
172
- new Promise((resolve, reject) => {
173
- client.sftp((error, sftp) => {
174
- if (error) {
175
- reject(error)
176
- return
177
- }
178
-
179
- const chunks: Buffer[] = []
180
- const stream = sftp.createReadStream(path)
181
-
182
- stream.on("data", (chunk: Buffer | string) => {
183
- chunks.push(Buffer.from(chunk))
184
- })
185
- stream.on("error", reject)
186
- stream.on("close", () => {
187
- resolve(Buffer.concat(chunks).toString("utf8"))
188
- })
189
- })
190
- }),
191
- operationTimeoutMs,
192
- )
193
-
194
- const writeFile = (connection: ConnectConfig, path: string, content: string, mode?: number) =>
195
- withClient<void>(connection, (client) =>
196
- new Promise((resolve, reject) => {
197
- client.sftp((error, sftp) => {
198
- if (error) {
199
- reject(error)
200
- return
201
- }
202
-
203
- const stream = sftp.createWriteStream(path, mode === undefined ? undefined : { mode })
204
-
205
- stream.on("error", reject)
206
- stream.on("close", () => resolve())
207
- stream.end(content)
208
- })
209
- }),
210
- operationTimeoutMs,
211
- )
212
-
213
- const listDir = async (connection: ConnectConfig, path: string, recursive = false, limit = 200) => {
214
- const boundedLimit = clampLimit(limit)
215
-
216
- if (boundedLimit === 0) {
217
- return []
218
- }
219
-
220
- if (recursive) {
221
- return withClient<string[]>(connection, (client) =>
222
- new Promise((resolve, reject) => {
223
- client.sftp((error, sftp) => {
224
- if (error) {
225
- reject(error)
226
- return
227
- }
228
-
229
- const statPath = (target: string) =>
230
- new Promise<any>((resolvePath, rejectPath) => {
231
- sftp.stat(target, (statError, stats) => {
232
- if (statError) {
233
- rejectPath(statError)
234
- return
235
- }
236
-
237
- resolvePath(stats)
238
- })
239
- })
240
-
241
- const readDir = (target: string) =>
242
- new Promise<any[]>((resolvePath, rejectPath) => {
243
- sftp.readdir(target, (readError, entries) => {
244
- if (readError) {
245
- rejectPath(readError)
246
- return
247
- }
248
-
249
- resolvePath(entries)
250
- })
251
- })
252
-
253
- const visit = async (target: string, output: string[]) => {
254
- if (output.length >= boundedLimit) {
255
- return
256
- }
257
-
258
- output.push(target)
259
-
260
- if (output.length >= boundedLimit) {
261
- return
262
- }
263
-
264
- const targetStats = await statPath(target)
265
- if (!targetStats.isDirectory()) {
266
- return
267
- }
268
-
269
- const entries = await readDir(target)
270
-
271
- for (const entry of entries) {
272
- if (entry.filename === "." || entry.filename === "..") {
273
- continue
274
- }
275
-
276
- const fullPath = joinRemotePath(target, entry.filename)
277
-
278
- if (entry.attrs.isDirectory()) {
279
- await visit(fullPath, output)
280
- } else if (output.length < boundedLimit) {
281
- output.push(fullPath)
282
- }
283
-
284
- if (output.length >= boundedLimit) {
285
- return
286
- }
287
- }
288
- }
289
-
290
- void (async () => {
291
- try {
292
- const output: string[] = []
293
- await visit(path, output)
294
- resolve(output)
295
- } catch (visitError) {
296
- reject(visitError)
297
- }
298
- })()
299
- })
300
- }),
301
- operationTimeoutMs,
302
- )
303
- }
304
-
305
- return withClient<DirEntry[]>(connection, (client) =>
306
- new Promise((resolve, reject) => {
307
- client.sftp((error, sftp) => {
308
- if (error) {
309
- reject(error)
310
- return
311
- }
312
-
313
- sftp.readdir(path, (readError, entries) => {
314
- if (readError) {
315
- reject(readError)
316
- return
317
- }
318
-
319
- resolve(entries.slice(0, boundedLimit).map((entry) => ({ name: entry.filename, longname: entry.longname })))
320
- })
321
- })
322
- }),
323
- operationTimeoutMs,
324
- )
325
- }
326
-
327
- const stat = (connection: ConnectConfig, path: string) =>
328
- withClient<PathStat>(connection, (client) =>
329
- new Promise((resolve, reject) => {
330
- client.sftp((error, sftp) => {
331
- if (error) {
332
- reject(error)
333
- return
334
- }
335
-
336
- sftp.stat(path, (statError, stats) => {
337
- if (statError) {
338
- reject(statError)
339
- return
340
- }
341
-
342
- resolve({
343
- size: stats.size,
344
- mode: stats.mode,
345
- isFile: stats.isFile(),
346
- isDirectory: stats.isDirectory(),
347
- })
348
- })
349
- })
350
- }),
351
- operationTimeoutMs,
352
- )
353
-
354
- return { exec, readFile, writeFile, listDir, stat }
355
- }
package/src/index.ts DELETED
@@ -1,3 +0,0 @@
1
- export { OpenCodePlugin, OpenCodePlugin as OpenShellPlugin } from "./opencode/plugin"
2
- export { OpenCodePlugin as default } from "./opencode/plugin"
3
- export * from "./core/contracts"
@@ -1,242 +0,0 @@
1
- import { tool, type Plugin, type ToolContext } from "@opencode-ai/plugin"
2
- import { createAuditLogStore } from "../core/audit/log-store"
3
- import { createGitAuditRepo } from "../core/audit/git-audit-repo"
4
- import { createOrchestrator } from "../core/orchestrator"
5
- import { classifyRemoteExec } from "../core/policy"
6
- import { createRuntimePaths, ensureRuntimeDirs } from "../core/paths"
7
- import { createServerRegistry } from "../core/registry/server-registry"
8
- import { errorResult } from "../core/result"
9
- import { createSshRuntime } from "../core/ssh/ssh-runtime"
10
-
11
- const serialize = async <T>(result: Promise<T>) => JSON.stringify(await result)
12
- type RuntimeDependencies = Parameters<typeof createOrchestrator>[0]
13
- type OpenCodePluginOptions = {
14
- ensureRuntimeDirs?: () => Promise<void>
15
- createRuntimeDependencies?: (workspaceRoot?: string) => RuntimeDependencies
16
- }
17
-
18
- type ApprovalRequest = Parameters<ToolContext["ask"]>[0]
19
-
20
- const approvalRejected = (toolId: string, server: string, error: unknown) =>
21
- JSON.stringify(
22
- errorResult({
23
- tool: toolId,
24
- server,
25
- code: "APPROVAL_REJECTED",
26
- message: error instanceof Error ? error.message : "approval rejected",
27
- execution: { attempted: false, completed: false },
28
- audit: { logWritten: false, snapshotStatus: "not-applicable" },
29
- }),
30
- )
31
-
32
- const requestApproval = async (
33
- context: ToolContext,
34
- toolId: string,
35
- server: string,
36
- request: ApprovalRequest | null,
37
- ) => {
38
- if (!request) {
39
- return null
40
- }
41
-
42
- try {
43
- await context.ask(request)
44
- return null
45
- } catch (error) {
46
- return approvalRejected(toolId, server, error)
47
- }
48
- }
49
-
50
- const createEditApproval = (
51
- toolId: "remote_write_file" | "remote_patch_file",
52
- input: { server: string; path: string; mode?: number; patch?: string; content?: string },
53
- ): ApprovalRequest => ({
54
- permission: "edit",
55
- patterns: [input.path],
56
- always: [],
57
- metadata: {
58
- tool: toolId,
59
- server: input.server,
60
- path: input.path,
61
- mode: input.mode,
62
- contentBytes: input.content ? Buffer.byteLength(input.content) : undefined,
63
- patchBytes: input.patch ? Buffer.byteLength(input.patch) : undefined,
64
- },
65
- })
66
-
67
- const createRemoteExecApproval = (input: {
68
- server: string
69
- command: string
70
- cwd?: string
71
- timeout?: number
72
- }): ApprovalRequest | null => {
73
- const classification = classifyRemoteExec(input.command)
74
- if (classification.decision !== "approval-required") {
75
- return null
76
- }
77
-
78
- return {
79
- permission: "bash",
80
- patterns: [input.command],
81
- always: [],
82
- metadata: {
83
- tool: "remote_exec",
84
- server: input.server,
85
- command: input.command,
86
- cwd: input.cwd,
87
- timeout: input.timeout,
88
- reason: classification.reason,
89
- },
90
- }
91
- }
92
-
93
- const createTools = (orchestrator: ReturnType<typeof createOrchestrator>) => ({
94
- list_servers: tool({
95
- description: "List configured remote servers.",
96
- args: {},
97
- execute: async () => serialize(orchestrator.listServers()),
98
- }),
99
- remote_exec: tool({
100
- description: "Execute a shell command on a remote server.",
101
- args: {
102
- server: tool.schema.string(),
103
- command: tool.schema.string(),
104
- cwd: tool.schema.string().optional(),
105
- timeout: tool.schema.number().int().positive().optional(),
106
- },
107
- execute: async ({ server, command, cwd, timeout }, context) => {
108
- const rejected = await requestApproval(
109
- context,
110
- "remote_exec",
111
- server,
112
- createRemoteExecApproval({ server, command, cwd, timeout }),
113
- )
114
- if (rejected) {
115
- return rejected
116
- }
117
-
118
- return serialize(orchestrator.remoteExec({ server, command, cwd, timeout }))
119
- },
120
- }),
121
- remote_read_file: tool({
122
- description: "Read a remote file.",
123
- args: {
124
- server: tool.schema.string(),
125
- path: tool.schema.string(),
126
- offset: tool.schema.number().int().nonnegative().optional(),
127
- length: tool.schema.number().int().positive().optional(),
128
- },
129
- execute: async ({ server, path, offset, length }) =>
130
- serialize(orchestrator.remoteReadFile({ server, path, offset, length })),
131
- }),
132
- remote_write_file: tool({
133
- description: "Write content to a remote file.",
134
- args: {
135
- server: tool.schema.string(),
136
- path: tool.schema.string(),
137
- content: tool.schema.string(),
138
- mode: tool.schema.number().int().positive().optional(),
139
- },
140
- execute: async ({ server, path, content, mode }, context) => {
141
- const rejected = await requestApproval(
142
- context,
143
- "remote_write_file",
144
- server,
145
- createEditApproval("remote_write_file", { server, path, content, mode }),
146
- )
147
- if (rejected) {
148
- return rejected
149
- }
150
-
151
- return serialize(orchestrator.remoteWriteFile({ server, path, content, mode }))
152
- },
153
- }),
154
- remote_patch_file: tool({
155
- description: "Apply a unified diff to a remote file.",
156
- args: {
157
- server: tool.schema.string(),
158
- path: tool.schema.string(),
159
- patch: tool.schema.string(),
160
- },
161
- execute: async ({ server, path, patch }, context) => {
162
- const rejected = await requestApproval(
163
- context,
164
- "remote_patch_file",
165
- server,
166
- createEditApproval("remote_patch_file", { server, path, patch }),
167
- )
168
- if (rejected) {
169
- return rejected
170
- }
171
-
172
- return serialize(orchestrator.remotePatchFile({ server, path, patch }))
173
- },
174
- }),
175
- remote_list_dir: tool({
176
- description: "List a remote directory.",
177
- args: {
178
- server: tool.schema.string(),
179
- path: tool.schema.string(),
180
- recursive: tool.schema.boolean().optional(),
181
- limit: tool.schema.number().int().positive().optional(),
182
- },
183
- execute: async ({ server, path, recursive, limit }) =>
184
- serialize(orchestrator.remoteListDir({ server, path, recursive, limit })),
185
- }),
186
- remote_stat: tool({
187
- description: "Stat a remote path.",
188
- args: {
189
- server: tool.schema.string(),
190
- path: tool.schema.string(),
191
- },
192
- execute: async ({ server, path }) => serialize(orchestrator.remoteStat({ server, path })),
193
- }),
194
- remote_find: tool({
195
- description: "Search a remote directory for matching files or content.",
196
- args: {
197
- server: tool.schema.string(),
198
- path: tool.schema.string(),
199
- pattern: tool.schema.string(),
200
- glob: tool.schema.string().optional(),
201
- limit: tool.schema.number().int().positive().optional(),
202
- },
203
- execute: async ({ server, path, pattern, glob, limit }) =>
204
- serialize(orchestrator.remoteFind({ server, path, pattern, glob, limit })),
205
- }),
206
- })
207
-
208
- const buildRuntimeDependencies = (workspaceRoot: string): RuntimeDependencies => {
209
- const paths = createRuntimePaths(workspaceRoot)
210
- const registry = createServerRegistry({
211
- globalRegistryFile: paths.globalRegistryFile,
212
- workspaceRegistryFile: paths.workspaceRegistryFile,
213
- workspaceRoot,
214
- })
215
- const auditLog = createAuditLogStore(paths.auditLogFile)
216
- const auditRepo = createGitAuditRepo(paths.auditRepoDir)
217
-
218
- return {
219
- registry,
220
- ssh: createSshRuntime(),
221
- audit: {
222
- preflightLog: () => auditLog.preflight(),
223
- appendLog: (entry) => auditLog.append(entry),
224
- preflightSnapshots: () => auditRepo.preflight(),
225
- captureSnapshots: (input) => auditRepo.captureChange(input),
226
- },
227
- }
228
- }
229
-
230
- export const createOpenCodePlugin = (options: OpenCodePluginOptions = {}): Plugin => async (input) => {
231
- await (options.ensureRuntimeDirs ?? ensureRuntimeDirs)()
232
-
233
- const orchestrator = createOrchestrator(
234
- (options.createRuntimeDependencies ?? buildRuntimeDependencies)(input.worktree),
235
- )
236
-
237
- return {
238
- tool: createTools(orchestrator),
239
- }
240
- }
241
-
242
- export const OpenCodePlugin: Plugin = createOpenCodePlugin()
@@ -1,43 +0,0 @@
1
- import { mkdir } from "node:fs/promises"
2
- import { cwd, stdout } from "node:process"
3
- import { createRuntimePaths } from "../core/paths.js"
4
- import { installIntoOpenCodeConfig } 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 InstallOptions = {
14
- runtimePaths: RuntimePaths
15
- stdout: WritableLike
16
- }
17
-
18
- export const installOpenShell = async ({ runtimePaths, stdout }: InstallOptions) => {
19
- await mkdir(runtimePaths.configDir, { recursive: true })
20
- await mkdir(runtimePaths.dataDir, { recursive: true })
21
- await mkdir(runtimePaths.opencodeConfigDir, { recursive: true })
22
-
23
- await installIntoOpenCodeConfig(runtimePaths.opencodeConfigFile)
24
- await createWorkspaceTracker(runtimePaths.workspaceTrackerFile).clear()
25
-
26
- stdout.write(
27
- [
28
- "Installed openshell.",
29
- `OpenShell config: ${runtimePaths.configDir}`,
30
- `OpenShell data: ${runtimePaths.dataDir}`,
31
- `OpenCode config: ${runtimePaths.opencodeConfigFile}`,
32
- ].join("\n") + "\n",
33
- )
34
- }
35
-
36
- export const runInstallCli = async (_argv: string[] = [], stream: WritableLike = stdout) => {
37
- await installOpenShell({
38
- runtimePaths: createRuntimePaths(cwd()),
39
- stdout: stream,
40
- })
41
-
42
- return 0
43
- }