opencode-add-dir 1.3.1 → 1.5.0

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.
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"
3
+ import { join } from "path"
4
+ import { homedir } from "os"
5
+
6
+ const PKG = "opencode-add-dir"
7
+
8
+ try {
9
+ const dir = join(
10
+ process.env.XDG_CONFIG_HOME || join(homedir(), ".config"),
11
+ "opencode",
12
+ )
13
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
14
+
15
+ let filePath = join(dir, "tui.json")
16
+ for (const name of ["tui.jsonc", "tui.json"]) {
17
+ const p = join(dir, name)
18
+ if (existsSync(p)) { filePath = p; break }
19
+ }
20
+
21
+ let config = {}
22
+ if (existsSync(filePath)) {
23
+ const raw = readFileSync(filePath, "utf-8")
24
+ config = JSON.parse(raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""))
25
+ }
26
+
27
+ const plugins = config.plugin || []
28
+ const has = plugins.some((p) => {
29
+ const name = Array.isArray(p) ? p[0] : p
30
+ return name === PKG || (typeof name === "string" && name.startsWith(PKG + "@"))
31
+ })
32
+
33
+ if (!has) {
34
+ config.plugin = [...plugins, PKG]
35
+ writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n")
36
+ }
37
+ } catch {
38
+ // Non-critical — TUI dialog available after manual setup or restart
39
+ }
package/bin/setup.mjs CHANGED
@@ -9,19 +9,9 @@ const isRemove = args.includes("--remove")
9
9
 
10
10
  function configDir() {
11
11
  if (process.env.XDG_CONFIG_HOME) return join(process.env.XDG_CONFIG_HOME, "opencode")
12
- if (process.platform === "darwin") return join(homedir(), ".config", "opencode")
13
12
  return join(homedir(), ".config", "opencode")
14
13
  }
15
14
 
16
- function findConfig() {
17
- const dir = configDir()
18
- for (const name of ["opencode.jsonc", "opencode.json"]) {
19
- const p = join(dir, name)
20
- if (existsSync(p)) return p
21
- }
22
- return join(dir, "opencode.json")
23
- }
24
-
25
15
  function stripJsonComments(text) {
26
16
  let result = ""
27
17
  let inString = false
@@ -39,48 +29,73 @@ function stripJsonComments(text) {
39
29
  return result
40
30
  }
41
31
 
42
- function run() {
43
- const configPath = findConfig()
44
- const dir = configDir()
45
-
46
- let config = {}
47
- if (existsSync(configPath)) {
48
- const raw = readFileSync(configPath, "utf-8")
49
- config = JSON.parse(stripJsonComments(raw))
50
- } else {
51
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
32
+ function findConfigFile(dir, baseName) {
33
+ for (const ext of [".jsonc", ".json"]) {
34
+ const p = join(dir, baseName + ext)
35
+ if (existsSync(p)) return p
52
36
  }
37
+ return join(dir, baseName + ".json")
38
+ }
53
39
 
54
- config.plugin = config.plugin || []
55
- const has = config.plugin.some((p) => {
40
+ function readConfig(filePath) {
41
+ if (!existsSync(filePath)) return {}
42
+ return JSON.parse(stripJsonComments(readFileSync(filePath, "utf-8")))
43
+ }
44
+
45
+ function hasPlugin(plugins) {
46
+ return (plugins || []).some((p) => {
56
47
  const name = Array.isArray(p) ? p[0] : p
57
48
  return name === PKG || name.startsWith(PKG + "@")
58
49
  })
50
+ }
59
51
 
60
- if (isRemove) {
61
- if (!has) {
62
- console.log(`${PKG} is not in ${configPath}`)
63
- return
64
- }
65
- config.plugin = config.plugin.filter((p) => {
66
- const name = Array.isArray(p) ? p[0] : p
67
- return name !== PKG && !name.startsWith(PKG + "@")
68
- })
69
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n")
70
- console.log(`Removed ${PKG} from ${configPath}`)
71
- return
72
- }
52
+ function withoutPlugin(plugins) {
53
+ return (plugins || []).filter((p) => {
54
+ const name = Array.isArray(p) ? p[0] : p
55
+ return name !== PKG && !name.startsWith(PKG + "@")
56
+ })
57
+ }
58
+
59
+ function patchConfig(filePath, config, schemaUrl) {
60
+ config.plugin = config.plugin || []
73
61
 
74
- if (has) {
75
- console.log(`${PKG} is already in ${configPath}`)
76
- return
62
+ if (isRemove) {
63
+ if (!hasPlugin(config.plugin)) return false
64
+ config.plugin = withoutPlugin(config.plugin)
65
+ writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n")
66
+ return true
77
67
  }
78
68
 
69
+ if (hasPlugin(config.plugin)) return false
79
70
  config.plugin.push(PKG)
80
- if (!config.$schema) config.$schema = "https://opencode.ai/config.json"
81
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n")
82
- console.log(`Added ${PKG} to ${configPath}`)
83
- console.log("Restart OpenCode to activate the plugin.")
71
+ if (schemaUrl && !config.$schema) config.$schema = schemaUrl
72
+ writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n")
73
+ return true
74
+ }
75
+
76
+ function run() {
77
+ const dir = configDir()
78
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
79
+
80
+ const serverPath = findConfigFile(dir, "opencode")
81
+ const tuiPath = findConfigFile(dir, "tui")
82
+
83
+ const serverConfig = readConfig(serverPath)
84
+ const tuiConfig = readConfig(tuiPath)
85
+
86
+ const verb = isRemove ? "Removed" : "Added"
87
+ const serverChanged = patchConfig(serverPath, serverConfig, "https://opencode.ai/config.json")
88
+ const tuiChanged = patchConfig(tuiPath, tuiConfig)
89
+
90
+ if (serverChanged) console.log(`${verb} ${PKG} in ${serverPath}`)
91
+ else console.log(`${PKG} already ${isRemove ? "absent from" : "in"} ${serverPath}`)
92
+
93
+ if (tuiChanged) console.log(`${verb} ${PKG} in ${tuiPath}`)
94
+ else console.log(`${PKG} already ${isRemove ? "absent from" : "in"} ${tuiPath}`)
95
+
96
+ if (serverChanged || tuiChanged) {
97
+ console.log("Restart OpenCode to activate the plugin.")
98
+ }
84
99
  }
85
100
 
86
101
  run()
package/dist/index.d.ts CHANGED
@@ -1,2 +1,5 @@
1
- import { AddDirPlugin } from "./plugin.js";
2
- export default AddDirPlugin;
1
+ import type { PluginModule } from "@opencode-ai/plugin";
2
+ declare const plugin: PluginModule & {
3
+ id: string;
4
+ };
5
+ export default plugin;
package/dist/index.js CHANGED
@@ -35,6 +35,83 @@ function matchesDirs(dirs, filepath) {
35
35
  }
36
36
  return false;
37
37
  }
38
+ var PKG = "opencode-add-dir";
39
+ function stripJsonComments(text) {
40
+ let result = "";
41
+ let inString = false;
42
+ let escape = false;
43
+ for (let i = 0;i < text.length; i++) {
44
+ const ch = text[i];
45
+ if (escape) {
46
+ result += ch;
47
+ escape = false;
48
+ continue;
49
+ }
50
+ if (ch === "\\" && inString) {
51
+ result += ch;
52
+ escape = true;
53
+ continue;
54
+ }
55
+ if (ch === '"') {
56
+ inString = !inString;
57
+ result += ch;
58
+ continue;
59
+ }
60
+ if (inString) {
61
+ result += ch;
62
+ continue;
63
+ }
64
+ if (ch === "/" && text[i + 1] === "/") {
65
+ while (i < text.length && text[i] !== `
66
+ `)
67
+ i++;
68
+ continue;
69
+ }
70
+ if (ch === "/" && text[i + 1] === "*") {
71
+ i += 2;
72
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/"))
73
+ i++;
74
+ i++;
75
+ continue;
76
+ }
77
+ result += ch;
78
+ }
79
+ return result;
80
+ }
81
+ function configDir() {
82
+ return join(process.env["XDG_CONFIG_HOME"] || join(process.env["HOME"] || "~", ".config"), "opencode");
83
+ }
84
+ function findTuiConfig() {
85
+ const dir = configDir();
86
+ for (const name of ["tui.jsonc", "tui.json"]) {
87
+ const p = join(dir, name);
88
+ if (existsSync(p))
89
+ return p;
90
+ }
91
+ return join(dir, "tui.json");
92
+ }
93
+ function ensureTuiConfig() {
94
+ try {
95
+ const dir = configDir();
96
+ if (!existsSync(dir))
97
+ mkdirSync(dir, { recursive: true });
98
+ const filePath = findTuiConfig();
99
+ let config = {};
100
+ if (existsSync(filePath)) {
101
+ config = JSON.parse(stripJsonComments(readFileSync(filePath, "utf-8")));
102
+ }
103
+ const plugins = config.plugin ?? [];
104
+ const hasEntry = plugins.some((p) => {
105
+ const name = Array.isArray(p) ? p[0] : p;
106
+ return name === PKG || typeof name === "string" && name.startsWith(PKG + "@");
107
+ });
108
+ if (hasEntry)
109
+ return;
110
+ config.plugin = [...plugins, PKG];
111
+ writeFileSync(filePath, JSON.stringify(config, null, 2) + `
112
+ `);
113
+ } catch {}
114
+ }
38
115
 
39
116
  // src/validate.ts
40
117
  import { statSync } from "fs";
@@ -138,14 +215,11 @@ ${content}`);
138
215
 
139
216
  // src/plugin.ts
140
217
  var SENTINEL = Object.assign(new Error("__ADD_DIR_HANDLED__"), { stack: "" });
141
- function log(msg, data) {
142
- console.error(`[add-dir] ${msg}`, data !== undefined ? JSON.stringify(data) : "");
143
- }
218
+ ensureTuiConfig();
144
219
  var AddDirPlugin = async ({ client, worktree, directory }) => {
145
220
  const root = worktree || directory;
146
221
  const dirs = loadDirs();
147
222
  const sdk = client;
148
- log("init", { root, persistedDirs: [...dirs.keys()] });
149
223
  function add(dirPath, persist) {
150
224
  const result = validateDir(dirPath, root, [...dirs.values()].map((d) => d.path));
151
225
  if (!result.ok)
@@ -184,24 +258,15 @@ var AddDirPlugin = async ({ client, worktree, directory }) => {
184
258
  notify(sdk, sessionID, result.message);
185
259
  }
186
260
  const commands = {
187
- "add-dir": (args, sid) => {
188
- log("add-dir", { args, sid });
189
- handleAdd(args, sid);
190
- },
191
- "list-dir": (_, sid) => {
192
- log("list-dir", { sid });
193
- notify(sdk, sid, list());
194
- },
195
- "remove-dir": (args, sid) => {
196
- log("remove-dir", { args, sid });
197
- notify(sdk, sid, remove(args));
198
- }
261
+ __adddir: (args, sid) => handleAdd(args, sid),
262
+ "list-dir": (_, sid) => notify(sdk, sid, list()),
263
+ "remove-dir": (args, sid) => notify(sdk, sid, remove(args))
199
264
  };
200
265
  return {
201
266
  config: async (cfg) => {
202
267
  cfg.command ??= {};
203
268
  const cmd = cfg.command;
204
- cmd["add-dir"] = { template: "/add-dir", description: "Add a working directory" };
269
+ cmd["__adddir"] = { template: "/__adddir", description: "Internal: add a working directory" };
205
270
  cmd["list-dir"] = { template: "/list-dir", description: "List added working directories" };
206
271
  cmd["remove-dir"] = { template: "/remove-dir", description: "Remove a working directory" };
207
272
  if (!dirs.size)
@@ -234,7 +299,11 @@ var AddDirPlugin = async ({ client, worktree, directory }) => {
234
299
  };
235
300
 
236
301
  // src/index.ts
237
- var src_default = AddDirPlugin;
302
+ var plugin = {
303
+ id: "opencode-add-dir",
304
+ server: AddDirPlugin
305
+ };
306
+ var src_default = plugin;
238
307
  export {
239
308
  src_default as default
240
309
  };
package/dist/state.d.ts CHANGED
@@ -7,3 +7,4 @@ export declare function loadDirs(): Map<string, DirEntry>;
7
7
  export declare function saveDirs(dirs: Map<string, DirEntry>): void;
8
8
  export declare function isChildOf(parent: string, child: string): boolean;
9
9
  export declare function matchesDirs(dirs: Map<string, DirEntry>, filepath: string): boolean;
10
+ export declare function ensureTuiConfig(): void;
@@ -0,0 +1,5 @@
1
+ import type { TuiPluginModule } from "@opencode-ai/plugin/tui";
2
+ declare const plugin: TuiPluginModule & {
3
+ id: string;
4
+ };
5
+ export default plugin;
package/dist/tui.tsx ADDED
@@ -0,0 +1,110 @@
1
+ import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
2
+ import { createSignal } from "solid-js"
3
+
4
+ const PLUGIN_ID = "opencode-add-dir"
5
+ const INTERNAL_COMMAND = "__adddir"
6
+
7
+ function activeSessionID(api: TuiPluginApi): string | undefined {
8
+ const route = api.route.current
9
+ if (route.name !== "session" || !route.params) return
10
+ return route.params.sessionID as string
11
+ }
12
+
13
+ async function ensureSession(api: TuiPluginApi): Promise<string | undefined> {
14
+ const existing = activeSessionID(api)
15
+ if (existing) return existing
16
+
17
+ const res = await api.client.session.create({})
18
+ if (res.error) return
19
+
20
+ api.route.navigate("session", { sessionID: res.data.id })
21
+ return res.data.id
22
+ }
23
+
24
+ async function executeAddDir(api: TuiPluginApi, dirPath: string) {
25
+ const sessionID = await ensureSession(api)
26
+ if (!sessionID) {
27
+ api.ui.toast({ variant: "error", message: "Failed to create session" })
28
+ return
29
+ }
30
+
31
+ // The server plugin intercepts via command.execute.before, handles the logic,
32
+ // sends feedback through the session event stream, then throws SENTINEL to
33
+ // prevent the command template from reaching the LLM. This rejection is expected.
34
+ api.client.session.command({
35
+ sessionID,
36
+ command: INTERNAL_COMMAND,
37
+ arguments: dirPath,
38
+ }).catch(() => {})
39
+ }
40
+
41
+ function AddDirDialog(props: { api: TuiPluginApi }) {
42
+ const [busy, setBusy] = createSignal(false)
43
+
44
+ async function handleConfirm(value: string) {
45
+ const dirPath = value.trim()
46
+ if (!dirPath) {
47
+ props.api.ui.toast({ variant: "error", message: "Please enter a directory path" })
48
+ return
49
+ }
50
+
51
+ setBusy(true)
52
+ try {
53
+ await executeAddDir(props.api, dirPath)
54
+ props.api.ui.dialog.clear()
55
+ } finally {
56
+ setBusy(false)
57
+ }
58
+ }
59
+
60
+ return (
61
+ <props.api.ui.DialogPrompt
62
+ title="Add directory"
63
+ placeholder="/path/to/directory"
64
+ busy={busy()}
65
+ busyText="Adding directory..."
66
+ description={() => (
67
+ <box gap={0}>
68
+ <text fg={props.api.theme.current.textMuted}>
69
+ To get the full path of a project:
70
+ </text>
71
+ <text fg={props.api.theme.current.textMuted}>
72
+ {" "}1. Move to the project in your terminal
73
+ </text>
74
+ <text fg={props.api.theme.current.textMuted}>
75
+ {" "}2. Run "pwd" and copy the output
76
+ </text>
77
+ <text fg={props.api.theme.current.textMuted}>
78
+ {" "}3. Paste below
79
+ </text>
80
+ </box>
81
+ )}
82
+ onConfirm={handleConfirm}
83
+ onCancel={() => props.api.ui.dialog.clear()}
84
+ />
85
+ )
86
+ }
87
+
88
+ function showDialog(api: TuiPluginApi) {
89
+ api.ui.dialog.replace(() => <AddDirDialog api={api} />)
90
+ }
91
+
92
+ const tui: TuiPlugin = async (api) => {
93
+ api.command.register(() => [
94
+ {
95
+ title: "Add directory",
96
+ value: "add-dir.dialog",
97
+ description: "Add a working directory to the session",
98
+ category: "Directories",
99
+ slash: { name: "add-dir" },
100
+ onSelect: () => showDialog(api),
101
+ },
102
+ ])
103
+ }
104
+
105
+ const plugin: TuiPluginModule & { id: string } = {
106
+ id: PLUGIN_ID,
107
+ tui,
108
+ }
109
+
110
+ export default plugin
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-add-dir",
3
- "version": "1.3.1",
3
+ "version": "1.5.0",
4
4
  "description": "Add working directories to your OpenCode session with auto-approved permissions",
5
5
  "author": "Cristian Fonseca <cfonsecacomas@gmail.com>",
6
6
  "type": "module",
@@ -8,22 +8,36 @@
8
8
  ".": {
9
9
  "types": "./dist/index.d.ts",
10
10
  "import": "./dist/index.js"
11
+ },
12
+ "./server": {
13
+ "import": "./dist/index.js"
14
+ },
15
+ "./tui": {
16
+ "import": "./dist/tui.tsx"
11
17
  }
12
18
  },
13
19
  "main": "./dist/index.js",
14
20
  "types": "./dist/index.d.ts",
21
+ "oc-plugin": [
22
+ "server",
23
+ "tui"
24
+ ],
15
25
  "bin": {
16
26
  "opencode-add-dir-setup": "./bin/setup.mjs"
17
27
  },
18
28
  "files": [
19
29
  "dist/",
20
- "bin/",
30
+ "bin/ensure-tui.mjs",
31
+ "bin/setup.mjs",
21
32
  "README.md",
22
33
  "LICENSE"
23
34
  ],
24
35
  "scripts": {
25
- "build": "bun build ./src/index.ts --outdir ./dist --target node --format esm --external @opencode-ai/plugin && bun x tsc --emitDeclarationOnly",
26
- "deploy": "bun build ./src/plugin.ts --outfile ~/.config/opencode/plugins/add-dir.js --target node --format esm --external @opencode-ai/plugin",
36
+ "build": "bun run build:server && bun run build:tui && bun x tsc --emitDeclarationOnly",
37
+ "build:server": "bun build ./src/index.ts --outdir ./dist --target node --format esm --external @opencode-ai/plugin",
38
+ "build:tui": "cp ./src/tui-plugin.tsx ./dist/tui.tsx",
39
+ "postinstall": "node ./bin/ensure-tui.mjs",
40
+ "deploy": "bun run build:server && bun run build:tui",
27
41
  "test": "bun test",
28
42
  "typecheck": "bun x tsc --noEmit",
29
43
  "prepublishOnly": "bun run typecheck && bun test && bun run build"
@@ -41,10 +55,22 @@
41
55
  },
42
56
  "license": "MIT",
43
57
  "peerDependencies": {
44
- "@opencode-ai/plugin": ">=1.0.0"
58
+ "@opencode-ai/plugin": ">=1.0.0",
59
+ "@opentui/core": ">=0.1.92",
60
+ "@opentui/solid": ">=0.1.92"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "@opentui/core": {
64
+ "optional": true
65
+ },
66
+ "@opentui/solid": {
67
+ "optional": true
68
+ }
45
69
  },
46
70
  "devDependencies": {
47
71
  "@opencode-ai/plugin": "^1.1.14",
72
+ "@opentui/core": "0.1.95",
73
+ "@opentui/solid": "0.1.95",
48
74
  "@semantic-release/changelog": "^6.0.3",
49
75
  "@semantic-release/commit-analyzer": "^13.0.1",
50
76
  "@semantic-release/git": "^10.0.1",
@@ -53,6 +79,7 @@
53
79
  "@semantic-release/release-notes-generator": "^14.1.0",
54
80
  "@types/bun": "latest",
55
81
  "semantic-release": "^25.0.3",
82
+ "solid-js": "^1.9.12",
56
83
  "typescript": "^5.8.3"
57
84
  }
58
85
  }