opencode-harness-kit 0.3.6 → 0.3.7

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,22 @@ is not happening.
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
+ ## [0.3.7] — 2026-10-20
18
+
19
+ ### Fixed
20
+
21
+ - **`oc-harness trust` no longer reports a failed create as a configured
22
+ one.** It printed "the trusted publisher is configured" after npm refused,
23
+ because it checked the error TEXT rather than the exit code. The user walks
24
+ away believing the publish path is fixed and finds out at the next release.
25
+ The exit code is what decides now, and npm's own words are printed when it
26
+ is not zero.
27
+ - **A failed read is reported as a failed read**, with npm's answer attached,
28
+ rather than as "nothing is configured" - which sent the reader off to
29
+ create a configuration that already existed.
30
+ - **The one-time password prompt goes to stderr, on a line of its own.** A
31
+ question buried in npm's output is a question nobody answers.
32
+
17
33
  ## [0.3.6] — 2026-10-20
18
34
 
19
35
  ### Fixed
@@ -319,7 +335,8 @@ silent in the worst way:
319
335
 
320
336
  ---
321
337
 
322
- [Unreleased]: https://github.com/pham-canh/space-harness/compare/v0.3.6...HEAD
338
+ [Unreleased]: https://github.com/pham-canh/space-harness/compare/v0.3.7...HEAD
339
+ [0.3.7]: https://github.com/pham-canh/space-harness/compare/v0.3.6...v0.3.7
323
340
  [0.3.6]: https://github.com/pham-canh/space-harness/compare/v0.3.5...v0.3.6
324
341
  [0.3.5]: https://github.com/pham-canh/space-harness/compare/v0.3.4...v0.3.5
325
342
  [0.3.4]: https://github.com/pham-canh/space-harness/compare/v0.3.1...v0.3.4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-harness-kit",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
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
@@ -96,7 +96,7 @@ function npm(args, opts = {}) {
96
96
  *
97
97
  * @param {string[]} args
98
98
  * @param {{ otp?: string }} [state]
99
- * @returns {Promise<string>}
99
+ * @returns {Promise<{ out: string, code: number }>}
100
100
  */
101
101
  async function npmWithOtp(args, state = {}) {
102
102
  /** @param {string[]} extra */
@@ -107,28 +107,28 @@ async function npmWithOtp(args, state = {}) {
107
107
  encoding: "utf8",
108
108
  stdio: ["ignore", "pipe", "pipe"],
109
109
  }),
110
- failed: false,
110
+ code: 0,
111
111
  }
112
112
  } catch (err) {
113
113
  const e = /** @type {any} */ (err)
114
- return { out: `${e.stdout ?? ""}${e.stderr ?? ""}`, failed: true }
114
+ return { out: `${e.stdout ?? ""}${e.stderr ?? ""}`, code: e.status ?? 1 }
115
115
  }
116
116
  }
117
117
 
118
118
  const withOtp = state.otp ? [`--otp=${state.otp}`] : []
119
119
  let result = run(withOtp)
120
- if (!result.failed || !/one-time password|EOTP/i.test(result.out)) return result.out
120
+ if (result.code === 0 || !/one-time password|EOTP/i.test(result.out)) return result
121
121
 
122
122
  // npm wants a code. Get one.
123
123
  const code = state.otp ?? (await askForOtp())
124
124
  if (!code) {
125
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)
126
+ console.error(dim(" run it again with --otp=<the code from your authenticator app>"))
127
+ return { out: result.out, code: result.code }
128
128
  }
129
129
  state.otp = code
130
130
  result = run([`--otp=${code}`])
131
- return result.out
131
+ return result
132
132
  }
133
133
 
134
134
  /**
@@ -145,9 +145,15 @@ async function askForOtp() {
145
145
  return null
146
146
  }
147
147
  const { createInterface } = await import("node:readline/promises")
148
- const rl = createInterface({ input: process.stdin, output: process.stdout })
148
+ // The prompt goes to stderr, and it gets a line of its own: a question
149
+ // buried in the middle of npm's output is a question nobody answers.
150
+ process.stderr.write("\n")
151
+ const rl = createInterface({ input: process.stdin, output: process.stderr })
149
152
  try {
150
- const answer = (await rl.question(" one-time password from your authenticator app: ")).trim()
153
+ const answer = (
154
+ await rl.question(" one-time password from your authenticator app: ")
155
+ ).trim()
156
+ process.stderr.write("\n")
151
157
  return answer || null
152
158
  } finally {
153
159
  rl.close()
@@ -163,19 +169,22 @@ async function askForOtp() {
163
169
  *
164
170
  * @param {string} name
165
171
  * @param {{ otp?: string }} [state]
166
- * @returns {Promise<{ configs: any[], failed: boolean }>}
172
+ * @returns {Promise<{ configs: any[], failed: boolean, detail?: string }>}
167
173
  */
168
174
  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 }
175
+ const { out, code } = await npmWithOtp(["trust", "list", name, "--json"], state)
176
+ if (code !== 0) {
177
+ // A package with no configuration answers 404, and a failed call answers
178
+ // with something else. They are different facts.
179
+ if (/E404|not found/i.test(out)) return { configs: [], failed: false }
180
+ return { configs: [], failed: true, detail: out }
172
181
  }
173
182
  if (!out.trim()) return { configs: [], failed: false }
174
183
  let parsed
175
184
  try {
176
185
  parsed = JSON.parse(out)
177
186
  } catch {
178
- return { configs: [], failed: true }
187
+ return { configs: [], failed: true, detail: out }
179
188
  }
180
189
  const items = Array.isArray(parsed) ? parsed : parsed.configs ?? [parsed]
181
190
  return { configs: items.filter(Boolean), failed: false }
@@ -226,10 +235,13 @@ export async function main(argv = []) {
226
235
  return 1
227
236
  }
228
237
 
229
- const { configs, failed } = await listConfigs(name, state)
238
+ const { configs, failed, detail } = await listConfigs(name, state)
230
239
  if (failed && !configs.length) {
231
240
  console.error(red("could not read the trusted publisher configuration."))
232
- console.error(dim(" npm answered with something that is not a configuration list."))
241
+ if (detail) {
242
+ console.error(dim(" npm said:"))
243
+ for (const line of detail.trim().split("\n").slice(-12)) console.error(dim(" " + line))
244
+ }
233
245
  console.error(dim(` run \`npm trust list ${name}\` to see the raw answer.`))
234
246
  return 1
235
247
  }
@@ -244,7 +256,7 @@ export async function main(argv = []) {
244
256
  return 1
245
257
  }
246
258
  console.log(` creating it: ${file}, no environment, publish and stage\n`)
247
- const out = await npmWithOtp(
259
+ const { out, code } = await npmWithOtp(
248
260
  [
249
261
  "trust",
250
262
  "github",
@@ -259,9 +271,19 @@ export async function main(argv = []) {
259
271
  ],
260
272
  state,
261
273
  )
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>"))
274
+ if (code !== 0) {
275
+ // Printing "configured" after a failed create is the worst thing this
276
+ // command could do: the user walks away believing the publish path is
277
+ // fixed, and finds out at the next release.
278
+ console.error(red("npm did not create the trusted publisher."))
279
+ if (/one-time password|EOTP/i.test(out)) {
280
+ console.error(dim(" run it again with --otp=<the code from your authenticator app>"))
281
+ }
282
+ const detail = out.trim()
283
+ if (detail) {
284
+ console.error(dim(" npm said:"))
285
+ for (const line of detail.split("\n").slice(-12)) console.error(dim(" " + line))
286
+ }
265
287
  return 1
266
288
  }
267
289
  console.log(`\n ${green("✓")} the trusted publisher is configured\n`)
@@ -314,8 +336,12 @@ export async function main(argv = []) {
314
336
  console.error(" revoke it on npmjs.com and run this again.")
315
337
  return 1
316
338
  }
317
- await npmWithOtp(["trust", "revoke", name, "--id", id, "-y"], state)
318
- await npmWithOtp(
339
+ const revoked = await npmWithOtp(["trust", "revoke", name, "--id", id, "-y"], state)
340
+ if (revoked.code !== 0) {
341
+ console.error(red("npm did not revoke the old configuration, so nothing was replaced."))
342
+ return 1
343
+ }
344
+ const created = await npmWithOtp(
319
345
  [
320
346
  "trust",
321
347
  "github",
@@ -330,6 +356,15 @@ export async function main(argv = []) {
330
356
  ],
331
357
  state,
332
358
  )
359
+ if (created.code !== 0) {
360
+ console.error(red("npm did not create the replacement configuration."))
361
+ const detail = created.out.trim()
362
+ if (detail) {
363
+ console.error(dim(" npm said:"))
364
+ for (const line of detail.split("\n").slice(-12)) console.error(dim(" " + line))
365
+ }
366
+ return 1
367
+ }
333
368
  console.log(`\n ${green("✓")} the trusted publisher is configured\n`)
334
369
  }
335
370
  return 0