backeer-opencode 0.1.0 → 0.1.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to `backeer-opencode` are documented here.
4
4
 
5
+ ## 0.1.1 - 2026-09-01
6
+
7
+ - Add `backeer-opencode configure` for hidden interactive token entry.
8
+ - Preserve existing tokens unless replacement is explicitly confirmed.
9
+ - Keep npm plugin configuration separate from side-loaded installation.
10
+
5
11
  ## 0.1.0 - 2026-09-01
6
12
 
7
13
  - Publish OpenCode permission requests to Backeer.
package/README.md CHANGED
@@ -53,8 +53,13 @@ Install the package in OpenCode's global configuration:
53
53
 
54
54
  ```sh
55
55
  opencode plugin backeer-opencode -g
56
+ npx backeer-opencode@latest configure
56
57
  ```
57
58
 
59
+ `configure` asks for the source ingest token without echoing it, stores it with
60
+ mode `0600`, and does not install a second side-loaded plugin. If a token already
61
+ exists, replacement requires confirmation.
62
+
58
63
  Alternatively, add it to `~/.config/opencode/opencode.json` manually:
59
64
 
60
65
  ```json
@@ -72,7 +77,8 @@ If npm package loading is not available in a particular OpenCode installation,
72
77
  install the package as a side-loaded plugin instead:
73
78
 
74
79
  ```sh
75
- npx backeer-opencode@latest --token-file /path/from/your/secret-manager/backeer-token
80
+ npx backeer-opencode@latest install \
81
+ --token-file /path/from/your/secret-manager/backeer-token
76
82
  ```
77
83
 
78
84
  Use either the configured npm plugin or the side-loaded form, not both. Loading
@@ -3,39 +3,61 @@
3
3
  import {
4
4
  chmodSync,
5
5
  copyFileSync,
6
+ existsSync,
6
7
  mkdirSync,
7
8
  readFileSync,
8
9
  renameSync,
10
+ statSync,
9
11
  writeFileSync,
10
12
  } from "node:fs"
11
13
  import { homedir } from "node:os"
12
14
  import { dirname, join, resolve } from "node:path"
15
+ import { createInterface } from "node:readline/promises"
13
16
  import { fileURLToPath } from "node:url"
14
17
 
15
18
  function usage(exitCode = 0) {
16
- console.log(`Usage: backeer-opencode [options]
19
+ console.log(`Usage: backeer-opencode [command] [options]
20
+
21
+ Commands:
22
+ configure Securely prompt for and store the Backeer ingest token
23
+ install Install the side-loaded plugin (default)
17
24
 
18
25
  Options:
19
26
  --config-dir PATH OpenCode config directory (default: XDG_CONFIG_HOME/opencode)
20
- --token-file PATH Read the Backeer ingest token from PATH
21
- --token-stdin Read the Backeer ingest token from standard input
27
+ --token-file PATH Read the token from PATH when using install
28
+ --token-stdin Read the token from standard input when using install
29
+ --force Replace an existing token without confirmation
22
30
  --help Show this help
23
31
 
24
- If no token option is supplied, an existing token is preserved.`)
32
+ Examples:
33
+ backeer-opencode configure
34
+ backeer-opencode install --token-file /run/secrets/backeer-token
35
+
36
+ If install has no token option, an existing token is preserved.`)
25
37
  process.exit(exitCode)
26
38
  }
27
39
 
28
40
  function parseArguments(arguments_) {
29
- const result = {}
30
- for (let index = 0; index < arguments_.length; index++) {
31
- const argument = arguments_[index]
41
+ const remaining = [...arguments_]
42
+ let command = "install"
43
+ if (remaining[0] === "configure" || remaining[0] === "install") {
44
+ command = remaining.shift()
45
+ }
46
+
47
+ const result = { command }
48
+ for (let index = 0; index < remaining.length; index++) {
49
+ const argument = remaining[index]
32
50
  if (argument === "--help") usage()
33
51
  if (argument === "--token-stdin") {
34
52
  result.tokenStdin = true
35
53
  continue
36
54
  }
55
+ if (argument === "--force") {
56
+ result.force = true
57
+ continue
58
+ }
37
59
  if (argument === "--config-dir" || argument === "--token-file") {
38
- const value = arguments_[++index]
60
+ const value = remaining[++index]
39
61
  if (!value) usage(1)
40
62
  result[argument === "--config-dir" ? "configDir" : "tokenFile"] = value
41
63
  continue
@@ -46,6 +68,12 @@ function parseArguments(arguments_) {
46
68
  if (result.tokenStdin && result.tokenFile) {
47
69
  throw new Error("Use either --token-stdin or --token-file, not both")
48
70
  }
71
+ if (command === "configure" && (result.tokenStdin || result.tokenFile)) {
72
+ throw new Error("configure prompts securely; do not use token input options")
73
+ }
74
+ if (command === "install" && result.force) {
75
+ throw new Error("--force is supported only by configure")
76
+ }
49
77
  return result
50
78
  }
51
79
 
@@ -63,6 +91,83 @@ function writeSecretAtomically(path, token) {
63
91
  renameSync(temporaryPath, path)
64
92
  }
65
93
 
94
+ async function confirmReplacement(secretPath) {
95
+ if (!existsSync(secretPath) || statSync(secretPath).size === 0) return true
96
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
97
+ throw new Error(
98
+ `A token already exists at ${secretPath}; rerun in a terminal or use --force`,
99
+ )
100
+ }
101
+
102
+ const prompt = createInterface({ input: process.stdin, output: process.stdout })
103
+ try {
104
+ const answer = await prompt.question(
105
+ `A Backeer token already exists at ${secretPath}. Replace it? [y/N] `,
106
+ )
107
+ return ["y", "yes"].includes(answer.trim().toLowerCase())
108
+ } finally {
109
+ prompt.close()
110
+ }
111
+ }
112
+
113
+ function readHiddenToken() {
114
+ const input = process.stdin
115
+ const output = process.stdout
116
+ if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function") {
117
+ throw new Error("Interactive token entry requires a terminal")
118
+ }
119
+
120
+ return new Promise((resolvePromise, rejectPromise) => {
121
+ let token = ""
122
+ const wasRaw = input.isRaw === true
123
+ output.write("Backeer source ingest token: ")
124
+ input.setEncoding("utf8")
125
+ input.setRawMode(true)
126
+ input.resume()
127
+
128
+ function cleanup() {
129
+ input.removeListener("data", onData)
130
+ input.setRawMode(wasRaw)
131
+ if (!wasRaw) input.pause()
132
+ output.write("\n")
133
+ }
134
+
135
+ function onData(chunk) {
136
+ for (const character of chunk) {
137
+ if (character === "\u0003") {
138
+ cleanup()
139
+ rejectPromise(new Error("Token configuration cancelled"))
140
+ return
141
+ }
142
+ if (character === "\r" || character === "\n") {
143
+ cleanup()
144
+ resolvePromise(token.trim())
145
+ return
146
+ }
147
+ if (character === "\u007f" || character === "\b") {
148
+ token = token.slice(0, -1)
149
+ continue
150
+ }
151
+ if (character >= " ") token += character
152
+ }
153
+ }
154
+
155
+ input.on("data", onData)
156
+ })
157
+ }
158
+
159
+ async function configureToken(secretPath, force) {
160
+ if (!force && !(await confirmReplacement(secretPath))) {
161
+ console.log("Backeer token unchanged.")
162
+ return
163
+ }
164
+
165
+ const token = await readHiddenToken()
166
+ writeSecretAtomically(secretPath, token)
167
+ console.log(`Configured protected Backeer token: ${secretPath}`)
168
+ console.log("Restart OpenCode to use the new token.")
169
+ }
170
+
66
171
  const arguments_ = parseArguments(process.argv.slice(2))
67
172
  const configDirectory = resolve(arguments_.configDir ?? defaultConfigDirectory())
68
173
  const pluginDirectory = join(configDirectory, "plugins")
@@ -73,20 +178,32 @@ const sourcePath = join(packageDirectory, "src", "index.js")
73
178
  // replaces it instead of loading two copies of the same plugin.
74
179
  const installedPath = join(pluginDirectory, "backeer-permission.js")
75
180
 
76
- mkdirSync(pluginDirectory, { recursive: true })
77
- copyFileSync(sourcePath, installedPath)
78
- chmodSync(installedPath, 0o644)
181
+ try {
182
+ if (arguments_.command === "configure") {
183
+ await configureToken(secretPath, arguments_.force)
184
+ } else {
185
+ mkdirSync(pluginDirectory, { recursive: true })
186
+ copyFileSync(sourcePath, installedPath)
187
+ chmodSync(installedPath, 0o644)
79
188
 
80
- if (arguments_.tokenFile) {
81
- writeSecretAtomically(secretPath, readFileSync(resolve(arguments_.tokenFile), "utf8").trim())
82
- } else if (arguments_.tokenStdin) {
83
- writeSecretAtomically(secretPath, readFileSync(0, "utf8").trim())
84
- }
189
+ if (arguments_.tokenFile) {
190
+ writeSecretAtomically(
191
+ secretPath,
192
+ readFileSync(resolve(arguments_.tokenFile), "utf8").trim(),
193
+ )
194
+ } else if (arguments_.tokenStdin) {
195
+ writeSecretAtomically(secretPath, readFileSync(0, "utf8").trim())
196
+ }
85
197
 
86
- console.log(`Installed Backeer OpenCode plugin: ${installedPath}`)
87
- console.log(
88
- arguments_.tokenFile || arguments_.tokenStdin
89
- ? `Installed protected ingest token: ${secretPath}`
90
- : `Token unchanged; expected at ${secretPath} or in BACKEER_INGEST_TOKEN`,
91
- )
92
- console.log("Restart OpenCode to load the plugin.")
198
+ console.log(`Installed Backeer OpenCode plugin: ${installedPath}`)
199
+ console.log(
200
+ arguments_.tokenFile || arguments_.tokenStdin
201
+ ? `Installed protected ingest token: ${secretPath}`
202
+ : `Token unchanged; expected at ${secretPath} or in BACKEER_INGEST_TOKEN`,
203
+ )
204
+ console.log("Restart OpenCode to load the plugin.")
205
+ }
206
+ } catch (error) {
207
+ console.error(`Backeer configuration failed: ${error.message}`)
208
+ process.exitCode = 1
209
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backeer-opencode",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Backeer notifications and remote permission decisions for OpenCode",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",