opencode-harness-kit 0.3.4 → 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 +28 -1
- package/bin/oc-harness.mjs +5 -0
- package/package.json +2 -2
- package/src/trust.js +336 -0
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,31 @@ 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
|
+
|
|
32
|
+
## [0.3.5] — 2026-10-20
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **`oc-harness trust`** configures npm trusted publishing from the command
|
|
37
|
+
line, and says which field is wrong when it is wrong. It works from any
|
|
38
|
+
directory: a release tool that only runs from one place is a release tool
|
|
39
|
+
that people run from the wrong one, and the failure is `Cannot read
|
|
40
|
+
package.json`, which says nothing about what the command was for.
|
|
41
|
+
|
|
17
42
|
## [0.3.4] — 2026-10-20
|
|
18
43
|
|
|
19
44
|
> 0.3.2 and 0.3.3 were never published. 0.3.2 carried the fix below but the
|
|
@@ -294,7 +319,9 @@ silent in the worst way:
|
|
|
294
319
|
|
|
295
320
|
---
|
|
296
321
|
|
|
297
|
-
[Unreleased]: https://github.com/pham-canh/space-harness/compare/v0.3.
|
|
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
|
|
324
|
+
[0.3.5]: https://github.com/pham-canh/space-harness/compare/v0.3.4...v0.3.5
|
|
298
325
|
[0.3.4]: https://github.com/pham-canh/space-harness/compare/v0.3.1...v0.3.4
|
|
299
326
|
[0.3.1]: https://github.com/pham-canh/space-harness/compare/v0.3.0...v0.3.1
|
|
300
327
|
[0.3.0]: https://github.com/pham-canh/space-harness/compare/v0.2.1...v0.3.0
|
package/bin/oc-harness.mjs
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
import { generateConfig, globalInstructionsPath } from "../src/config.js"
|
|
47
47
|
import { planEffort } from "../src/effort.js"
|
|
48
48
|
import { findTelemetryFile, readLaneUsage, averageMs } from "../src/lane-usage.js"
|
|
49
|
+
import { main as trustMain } from "../src/trust.js"
|
|
49
50
|
|
|
50
51
|
const CONFIG_DIR =
|
|
51
52
|
process.env.OPENCODE_CONFIG_DIR ?? join(homedir(), ".config/opencode")
|
|
@@ -1409,6 +1410,9 @@ try {
|
|
|
1409
1410
|
case "lanes":
|
|
1410
1411
|
cmdLanes()
|
|
1411
1412
|
break
|
|
1413
|
+
case "trust":
|
|
1414
|
+
process.exitCode = await trustMain(args.slice(1))
|
|
1415
|
+
break
|
|
1412
1416
|
case "doctor":
|
|
1413
1417
|
await cmdDoctor()
|
|
1414
1418
|
break
|
|
@@ -1433,6 +1437,7 @@ try {
|
|
|
1433
1437
|
link symlink the built plugin into ~/.config/opencode/plugins
|
|
1434
1438
|
agents report agents the effort tiering does not reach
|
|
1435
1439
|
lanes report which lanes the fleet actually used
|
|
1440
|
+
trust set up npm trusted publishing for this package
|
|
1436
1441
|
doctor re-check profiles against models.dev and the installed config
|
|
1437
1442
|
baseline measure the current opencode install
|
|
1438
1443
|
run drive .space/tasks.md to done with the build agent
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-harness-kit",
|
|
3
|
-
"version": "0.3.
|
|
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",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"check": "npm run test:all && npm run test:install && node bin/oc-harness.mjs verify",
|
|
54
54
|
"check:online": "npm run check && node bin/oc-harness.mjs doctor",
|
|
55
55
|
"baseline": "node bin/baseline.mjs",
|
|
56
|
-
"release:trust": "node
|
|
56
|
+
"release:trust": "node bin/oc-harness.mjs trust",
|
|
57
57
|
"eval": "node eval/run.mjs",
|
|
58
58
|
"measure": "node eval/measure-install.mjs",
|
|
59
59
|
"measure:check": "node eval/measure-install.mjs --min-narrow 45 --min-ctx-drop 20",
|
package/src/trust.js
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// Set up npm trusted publishing for this package, and say what is wrong with
|
|
2
|
+
// it if it is wrong.
|
|
3
|
+
//
|
|
4
|
+
// Why this exists: npm's web form for a trusted publisher has four fields and
|
|
5
|
+
// npm does not validate what you type - "npm does not verify your trusted
|
|
6
|
+
// publisher configuration when you save it". A typo in any of them is
|
|
7
|
+
// discovered at publish time, and npm reports it as:
|
|
8
|
+
//
|
|
9
|
+
// OIDC token exchange error - package not found
|
|
10
|
+
//
|
|
11
|
+
// which reads like a missing package. The package is there. The configuration
|
|
12
|
+
// is not matching, and npm does not say which field.
|
|
13
|
+
//
|
|
14
|
+
// So this compares what npm has stored against what the workflow will
|
|
15
|
+
// actually present, and says which field differs. That is the whole job.
|
|
16
|
+
//
|
|
17
|
+
// Requirements npm itself states for these commands:
|
|
18
|
+
// - npm 11.15.0 or later
|
|
19
|
+
// - write access to the package
|
|
20
|
+
// - two-factor authentication enabled on the account
|
|
21
|
+
// - the package must already exist on the registry
|
|
22
|
+
//
|
|
23
|
+
// The root is resolved from THIS file, not from the working directory, so the
|
|
24
|
+
// command works from anywhere - which matters, because a release tool that
|
|
25
|
+
// only runs from one directory is a release tool that people run from the
|
|
26
|
+
// wrong one.
|
|
27
|
+
|
|
28
|
+
import { execFileSync } from "node:child_process"
|
|
29
|
+
import { readFileSync } from "node:fs"
|
|
30
|
+
import { dirname, join } from "node:path"
|
|
31
|
+
import { fileURLToPath } from "node:url"
|
|
32
|
+
|
|
33
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..")
|
|
34
|
+
|
|
35
|
+
/** @param {string} s */
|
|
36
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`
|
|
37
|
+
/** @param {string} s */
|
|
38
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`
|
|
39
|
+
/** @param {string} s */
|
|
40
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`
|
|
41
|
+
/** @param {string} s */
|
|
42
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`
|
|
43
|
+
/** @param {string} s */
|
|
44
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The repository in the `owner/repo` form npm wants.
|
|
48
|
+
*
|
|
49
|
+
* npm matches the trusted publisher's repository against the OIDC token's
|
|
50
|
+
* `repository` claim, which is `owner/repo` with no scheme and no `.git`. The
|
|
51
|
+
* `repository` field in package.json is a git URL, so it has to be reduced to
|
|
52
|
+
* that form - and getting it wrong is one of the ways a configuration ends up
|
|
53
|
+
* not matching.
|
|
54
|
+
*
|
|
55
|
+
* @param {any} repository
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
export function repoOf(repository) {
|
|
59
|
+
const url = typeof repository === "string" ? repository : repository?.url ?? ""
|
|
60
|
+
const m = url.match(/github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?$/)
|
|
61
|
+
if (!m) throw new Error(`cannot read a GitHub repository out of ${JSON.stringify(repository)}`)
|
|
62
|
+
return `${m[1]}/${m[2]}`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Run npm and return its output.
|
|
67
|
+
*
|
|
68
|
+
* @param {string[]} args
|
|
69
|
+
* @param {{ allowFail?: boolean }} [opts]
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
function npm(args, opts = {}) {
|
|
73
|
+
const allowFail = opts.allowFail === true
|
|
74
|
+
try {
|
|
75
|
+
return execFileSync("npm", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (allowFail) return ""
|
|
78
|
+
const e = /** @type {any} */ (err)
|
|
79
|
+
const detail = `${e.stdout ?? ""}${e.stderr ?? ""}`.trim()
|
|
80
|
+
console.error(red(`npm ${args.join(" ")} failed`))
|
|
81
|
+
if (detail) console.error(detail)
|
|
82
|
+
process.exit(1)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
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
|
+
|
|
157
|
+
/**
|
|
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
|
+
*
|
|
164
|
+
* @param {string} name
|
|
165
|
+
* @param {{ otp?: string }} [state]
|
|
166
|
+
* @returns {Promise<{ configs: any[], failed: boolean }>}
|
|
167
|
+
*/
|
|
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 }
|
|
174
|
+
let parsed
|
|
175
|
+
try {
|
|
176
|
+
parsed = JSON.parse(out)
|
|
177
|
+
} catch {
|
|
178
|
+
return { configs: [], failed: true }
|
|
179
|
+
}
|
|
180
|
+
const items = Array.isArray(parsed) ? parsed : parsed.configs ?? [parsed]
|
|
181
|
+
return { configs: items.filter(Boolean), failed: false }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The fields npm actually matches, in the shape npm stores them.
|
|
186
|
+
* @param {any} config
|
|
187
|
+
*/
|
|
188
|
+
function describe(config) {
|
|
189
|
+
const claims = config.claims ?? {}
|
|
190
|
+
const file = config.file ?? claims.workflow_ref?.file
|
|
191
|
+
const repository = config.repository ?? claims.repository
|
|
192
|
+
const environment = config.environment ?? claims.environment
|
|
193
|
+
const permissions = config.permissions ?? []
|
|
194
|
+
return { file, repository, environment, permissions }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @param {string[]} argv
|
|
199
|
+
* @returns {Promise<number>} the exit code
|
|
200
|
+
*/
|
|
201
|
+
export async function main(argv = []) {
|
|
202
|
+
const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"))
|
|
203
|
+
const name = pkg.name
|
|
204
|
+
const repo = repoOf(pkg.repository)
|
|
205
|
+
const file = process.env.TRUSTED_PUBLISHER_FILE ?? "ci.yml"
|
|
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) }
|
|
210
|
+
|
|
211
|
+
console.log(`\n${bold("npm trusted publishing")} ${dim(`${name} · ${repo} · ${file}`)}\n`)
|
|
212
|
+
|
|
213
|
+
// 0. The preconditions npm states, checked before anything else.
|
|
214
|
+
const version = npm(["--version"]).trim()
|
|
215
|
+
const [major, minor] = version.split(".").map(Number)
|
|
216
|
+
if (major < 11 || (major === 11 && minor < 15)) {
|
|
217
|
+
console.error(red(`npm ${version} is too old for \`npm trust\`, which needs 11.15.0 or later.`))
|
|
218
|
+
console.error(" npm install -g npm@latest")
|
|
219
|
+
return 1
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
npm(["whoami"])
|
|
223
|
+
} catch {
|
|
224
|
+
console.error(red("not logged in to the npm registry."))
|
|
225
|
+
console.error(" npm login")
|
|
226
|
+
return 1
|
|
227
|
+
}
|
|
228
|
+
|
|
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
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!configs.length) {
|
|
238
|
+
console.log(yellow(" no trusted publisher is configured for this package."))
|
|
239
|
+
console.log(dim(" The registry answers a publish from an unconfigured package with"))
|
|
240
|
+
console.log(dim(" 'OIDC token exchange error - package not found', which reads like a"))
|
|
241
|
+
console.log(dim(" missing package. The package is there; nothing is configured.\n"))
|
|
242
|
+
if (checkOnly) {
|
|
243
|
+
console.log(dim(" run without --check to create it\n"))
|
|
244
|
+
return 1
|
|
245
|
+
}
|
|
246
|
+
console.log(` creating it: ${file}, no environment, publish and stage\n`)
|
|
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
|
+
}
|
|
267
|
+
console.log(`\n ${green("✓")} the trusted publisher is configured\n`)
|
|
268
|
+
return 0
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 1. npm allows exactly one configuration per package, and it cannot be
|
|
272
|
+
// edited - only revoked and replaced. So the comparison is against the
|
|
273
|
+
// one that is stored.
|
|
274
|
+
for (const config of configs) {
|
|
275
|
+
const got = describe(config)
|
|
276
|
+
const problems = []
|
|
277
|
+
if (got.repository !== repo) {
|
|
278
|
+
problems.push(`repository is ${JSON.stringify(got.repository)}, expected ${JSON.stringify(repo)}`)
|
|
279
|
+
}
|
|
280
|
+
if (got.file !== file) {
|
|
281
|
+
problems.push(`workflow file is ${JSON.stringify(got.file)}, expected ${JSON.stringify(file)}`)
|
|
282
|
+
}
|
|
283
|
+
if (got.environment) {
|
|
284
|
+
problems.push(
|
|
285
|
+
`environment is ${JSON.stringify(got.environment)}, expected none - the workflow declares no environment, so a claim that is never sent can never match`,
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
if (!got.permissions.includes("createPackage")) {
|
|
289
|
+
problems.push(
|
|
290
|
+
`permissions are [${got.permissions.join(", ")}], expected createPackage - without it the configuration cannot publish`,
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (!problems.length) {
|
|
295
|
+
console.log(` ${green("✓")} the stored configuration matches the workflow`)
|
|
296
|
+
console.log(dim(` repository ${got.repository} · file ${got.file} · no environment`))
|
|
297
|
+
console.log(dim(` permissions ${got.permissions.join(", ")}`))
|
|
298
|
+
console.log(`\n ${green("nothing to do")}\n`)
|
|
299
|
+
return 0
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
console.log(yellow(` the stored configuration does not match the workflow:`))
|
|
303
|
+
for (const p of problems) console.log(` - ${p}`)
|
|
304
|
+
console.log("")
|
|
305
|
+
if (checkOnly) {
|
|
306
|
+
console.log(dim(" run without --check to replace it\n"))
|
|
307
|
+
return 1
|
|
308
|
+
}
|
|
309
|
+
console.log(dim(" npm allows one configuration per package and cannot edit it,"))
|
|
310
|
+
console.log(dim(" so it is revoked and replaced.\n"))
|
|
311
|
+
const id = config.id ?? config.trustId ?? config._id
|
|
312
|
+
if (!id) {
|
|
313
|
+
console.error(red(" the stored configuration has no id, so it cannot be revoked from here."))
|
|
314
|
+
console.error(" revoke it on npmjs.com and run this again.")
|
|
315
|
+
return 1
|
|
316
|
+
}
|
|
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
|
+
)
|
|
333
|
+
console.log(`\n ${green("✓")} the trusted publisher is configured\n`)
|
|
334
|
+
}
|
|
335
|
+
return 0
|
|
336
|
+
}
|