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 +6 -0
- package/README.md +7 -1
- package/bin/backeer-opencode.js +140 -23
- package/package.json +1 -1
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
|
|
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
|
package/bin/backeer-opencode.js
CHANGED
|
@@ -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
|
|
21
|
-
--token-stdin Read the
|
|
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
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
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 =
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
}
|