opencode-harness-kit 0.3.5 → 0.3.6

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
@@ -14,6 +14,21 @@ is not happening.
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
+ ## [0.3.6] — 2026-10-20
18
+
19
+ ### Fixed
20
+
21
+ - **`oc-harness trust` asks for the one-time password itself.** npm's own
22
+ helper opens a browser for it, but only when stdin and stdout are both
23
+ terminals — and under `npx` they are not. The result was a failure that
24
+ printed a web URL masked as `auth/cli/***`, which cannot be opened. The code
25
+ is asked for in the terminal now, and `--otp=<code>` works where there is
26
+ none.
27
+ - **A failed read is no longer reported as "nothing is configured".** A
28
+ package with no trusted publisher and a call that could not read one are
29
+ different facts, and treating the second as the first sends you off to
30
+ create a configuration that already exists.
31
+
17
32
  ## [0.3.5] — 2026-10-20
18
33
 
19
34
  ### Added
@@ -304,7 +319,8 @@ silent in the worst way:
304
319
 
305
320
  ---
306
321
 
307
- [Unreleased]: https://github.com/pham-canh/space-harness/compare/v0.3.5...HEAD
322
+ [Unreleased]: https://github.com/pham-canh/space-harness/compare/v0.3.6...HEAD
323
+ [0.3.6]: https://github.com/pham-canh/space-harness/compare/v0.3.5...v0.3.6
308
324
  [0.3.5]: https://github.com/pham-canh/space-harness/compare/v0.3.4...v0.3.5
309
325
  [0.3.4]: https://github.com/pham-canh/space-harness/compare/v0.3.1...v0.3.4
310
326
  [0.3.1]: https://github.com/pham-canh/space-harness/compare/v0.3.0...v0.3.1
@@ -1411,7 +1411,7 @@ try {
1411
1411
  cmdLanes()
1412
1412
  break
1413
1413
  case "trust":
1414
- process.exitCode = trustMain(args.slice(1))
1414
+ process.exitCode = await trustMain(args.slice(1))
1415
1415
  break
1416
1416
  case "doctor":
1417
1417
  await cmdDoctor()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-harness-kit",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "Harness kit for OpenCode: capability profiles, a fleet of job lanes with one free model each, role-based reasoning effort, context pruning, and measured baselines.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/pham-canh/space-harness#readme",
package/src/trust.js CHANGED
@@ -83,22 +83,102 @@ function npm(args, opts = {}) {
83
83
  }
84
84
  }
85
85
 
86
+ /**
87
+ * Run npm, and if it asks for a one-time password, ask for one here and run
88
+ * it again.
89
+ *
90
+ * npm's own helper for this opens a browser - but only when stdin and stdout
91
+ * are both terminals, and under `npx` they are not. The result is a failure
92
+ * that prints a web URL masked as `auth/cli/***`, which cannot be opened.
93
+ *
94
+ * So the code is asked for here, and the same code is passed straight back to
95
+ * npm as `--otp`.
96
+ *
97
+ * @param {string[]} args
98
+ * @param {{ otp?: string }} [state]
99
+ * @returns {Promise<string>}
100
+ */
101
+ async function npmWithOtp(args, state = {}) {
102
+ /** @param {string[]} extra */
103
+ const run = (extra) => {
104
+ try {
105
+ return {
106
+ out: execFileSync("npm", [...args, ...extra], {
107
+ encoding: "utf8",
108
+ stdio: ["ignore", "pipe", "pipe"],
109
+ }),
110
+ failed: false,
111
+ }
112
+ } catch (err) {
113
+ const e = /** @type {any} */ (err)
114
+ return { out: `${e.stdout ?? ""}${e.stderr ?? ""}`, failed: true }
115
+ }
116
+ }
117
+
118
+ const withOtp = state.otp ? [`--otp=${state.otp}`] : []
119
+ let result = run(withOtp)
120
+ if (!result.failed || !/one-time password|EOTP/i.test(result.out)) return result.out
121
+
122
+ // npm wants a code. Get one.
123
+ const code = state.otp ?? (await askForOtp())
124
+ if (!code) {
125
+ console.error(red("npm needs a one-time password and none was given."))
126
+ console.error(dim(` run it again with --otp=<the code from your authenticator app>`))
127
+ process.exit(1)
128
+ }
129
+ state.otp = code
130
+ result = run([`--otp=${code}`])
131
+ return result.out
132
+ }
133
+
134
+ /**
135
+ * Ask for the one-time password.
136
+ *
137
+ * @returns {Promise<string | null>}
138
+ */
139
+ async function askForOtp() {
140
+ if (!process.stdin.isTTY) {
141
+ console.error(
142
+ red("this needs a one-time password, and there is no terminal to ask for it on."),
143
+ )
144
+ console.error(dim(` run it with --otp=<the code from your authenticator app>`))
145
+ return null
146
+ }
147
+ const { createInterface } = await import("node:readline/promises")
148
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
149
+ try {
150
+ const answer = (await rl.question(" one-time password from your authenticator app: ")).trim()
151
+ return answer || null
152
+ } finally {
153
+ rl.close()
154
+ }
155
+ }
156
+
86
157
  /**
87
158
  * The stored trusted publisher configurations for this package.
159
+ *
160
+ * A package with no configuration is not the same as a call that failed: the
161
+ * first is a fact and the second is a question, and treating the second as the
162
+ * first sends you off to create a configuration that already exists.
163
+ *
88
164
  * @param {string} name
89
- * @returns {any[]}
165
+ * @param {{ otp?: string }} [state]
166
+ * @returns {Promise<{ configs: any[], failed: boolean }>}
90
167
  */
91
- function listConfigs(name) {
92
- const out = npm(["trust", "list", name, "--json"], { allowFail: true })
93
- if (!out.trim()) return []
168
+ async function listConfigs(name, state = {}) {
169
+ const out = await npmWithOtp(["trust", "list", name, "--json"], state)
170
+ if (/one-time password|EOTP/i.test(out) && !state.otp) {
171
+ return { configs: [], failed: true }
172
+ }
173
+ if (!out.trim()) return { configs: [], failed: false }
94
174
  let parsed
95
175
  try {
96
176
  parsed = JSON.parse(out)
97
177
  } catch {
98
- return []
178
+ return { configs: [], failed: true }
99
179
  }
100
180
  const items = Array.isArray(parsed) ? parsed : parsed.configs ?? [parsed]
101
- return items.filter(Boolean)
181
+ return { configs: items.filter(Boolean), failed: false }
102
182
  }
103
183
 
104
184
  /**
@@ -116,14 +196,17 @@ function describe(config) {
116
196
 
117
197
  /**
118
198
  * @param {string[]} argv
119
- * @returns {number} the exit code
199
+ * @returns {Promise<number>} the exit code
120
200
  */
121
- export function main(argv = []) {
201
+ export async function main(argv = []) {
122
202
  const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"))
123
203
  const name = pkg.name
124
204
  const repo = repoOf(pkg.repository)
125
205
  const file = process.env.TRUSTED_PUBLISHER_FILE ?? "ci.yml"
126
206
  const checkOnly = argv.includes("--check")
207
+ // A code given on the command line is used as-is. A code given at the
208
+ // prompt is remembered for the rest of the run, so the user types it once.
209
+ const state = { otp: argv.find((a) => a.startsWith("--otp="))?.slice("--otp=".length) }
127
210
 
128
211
  console.log(`\n${bold("npm trusted publishing")} ${dim(`${name} · ${repo} · ${file}`)}\n`)
129
212
 
@@ -143,7 +226,13 @@ export function main(argv = []) {
143
226
  return 1
144
227
  }
145
228
 
146
- const configs = listConfigs(name)
229
+ const { configs, failed } = await listConfigs(name, state)
230
+ if (failed && !configs.length) {
231
+ console.error(red("could not read the trusted publisher configuration."))
232
+ console.error(dim(" npm answered with something that is not a configuration list."))
233
+ console.error(dim(` run \`npm trust list ${name}\` to see the raw answer.`))
234
+ return 1
235
+ }
147
236
 
148
237
  if (!configs.length) {
149
238
  console.log(yellow(" no trusted publisher is configured for this package."))
@@ -155,18 +244,26 @@ export function main(argv = []) {
155
244
  return 1
156
245
  }
157
246
  console.log(` creating it: ${file}, no environment, publish and stage\n`)
158
- npm([
159
- "trust",
160
- "github",
161
- name,
162
- "--file",
163
- file,
164
- "--repo",
165
- repo,
166
- "--allow-publish",
167
- "--allow-stage-publish",
168
- "-y",
169
- ])
247
+ const out = await npmWithOtp(
248
+ [
249
+ "trust",
250
+ "github",
251
+ name,
252
+ "--file",
253
+ file,
254
+ "--repo",
255
+ repo,
256
+ "--allow-publish",
257
+ "--allow-stage-publish",
258
+ "-y",
259
+ ],
260
+ state,
261
+ )
262
+ if (/one-time password|EOTP/i.test(out)) {
263
+ console.error(red("npm still wants a one-time password."))
264
+ console.error(dim(" run it again with --otp=<the code from your authenticator app>"))
265
+ return 1
266
+ }
170
267
  console.log(`\n ${green("✓")} the trusted publisher is configured\n`)
171
268
  return 0
172
269
  }
@@ -217,19 +314,22 @@ export function main(argv = []) {
217
314
  console.error(" revoke it on npmjs.com and run this again.")
218
315
  return 1
219
316
  }
220
- npm(["trust", "revoke", name, "--id", id, "-y"])
221
- npm([
222
- "trust",
223
- "github",
224
- name,
225
- "--file",
226
- file,
227
- "--repo",
228
- repo,
229
- "--allow-publish",
230
- "--allow-stage-publish",
231
- "-y",
232
- ])
317
+ await npmWithOtp(["trust", "revoke", name, "--id", id, "-y"], state)
318
+ await npmWithOtp(
319
+ [
320
+ "trust",
321
+ "github",
322
+ name,
323
+ "--file",
324
+ file,
325
+ "--repo",
326
+ repo,
327
+ "--allow-publish",
328
+ "--allow-stage-publish",
329
+ "-y",
330
+ ],
331
+ state,
332
+ )
233
333
  console.log(`\n ${green("✓")} the trusted publisher is configured\n`)
234
334
  }
235
335
  return 0