trustci 0.1.0

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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -0
  3. package/dist/cli.js +636 -0
  4. package/package.json +38 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hidetaka Okamoto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # trustci
2
+
3
+ Bulk-register [npm Trusted Publishers](https://docs.npmjs.com/trusted-publishers/)
4
+ across **all public packages in a monorepo** with a single command. Wraps
5
+ `npm trust` and supports **GitHub Actions**, **GitLab CI/CD**, and **CircleCI**.
6
+
7
+ Setting up trusted publishing one package at a time is tedious in a monorepo.
8
+ `trustci` detects your workspace, finds every public package, and runs the right
9
+ `npm trust <provider> …` for each.
10
+
11
+ ## Requirements
12
+
13
+ - **npm** 11.5.1 or later (for `npm trust`)
14
+ - **Node** 22.14.0 or later
15
+
16
+ ## Usage
17
+
18
+ ```bash
19
+ # Preview the commands without running anything (recommended first step)
20
+ npx trustci --dry-run --provider github --repo me/repo --file release.yml --allow-publish
21
+
22
+ # Actually register (asks for confirmation unless -y)
23
+ npx trustci --provider github --repo me/repo --file release.yml --allow-publish
24
+
25
+ # List current trusted publishers for every public package
26
+ npx trustci list
27
+ ```
28
+
29
+ ### Providers
30
+
31
+ | Provider | Required flags | Optional |
32
+ | ---------- | ---------------------------------------------------------------------------------------------- | --------------- |
33
+ | `github` | `--repo <owner/repo>` `--file <workflow>` | `--env <name>` |
34
+ | `gitlab` | `--project <group/project>` (or `--repo`) `--file <workflow>` | `--env <name>` |
35
+ | `circleci` | `--org-id` `--project-id` `--pipeline-definition-id` `--vcs-origin <url>` | `--context-id` (repeatable) |
36
+
37
+ At least one of `--allow-publish` / `--allow-stage-publish` is required.
38
+
39
+ ```bash
40
+ # GitLab
41
+ npx trustci --provider gitlab --project me/repo --file .gitlab-ci.yml --allow-publish -y
42
+
43
+ # CircleCI (vcs-origin must include the protocol)
44
+ npx trustci --provider circleci \
45
+ --org-id <uuid> --project-id <uuid> --pipeline-definition-id <uuid> \
46
+ --vcs-origin https://github.com/me/repo \
47
+ --context-id <uuid> \
48
+ --allow-publish
49
+ ```
50
+
51
+ ### Common options
52
+
53
+ | Flag | Description |
54
+ | ------------------------ | --------------------------------------------------- |
55
+ | `--dry-run` | Print the `npm trust` commands without executing. |
56
+ | `--only <name-glob>` | Only include packages matching (repeatable). |
57
+ | `--exclude <name-glob>` | Exclude packages matching (repeatable). |
58
+ | `-y`, `--yes` | Skip the confirmation prompt (and npm's). |
59
+ | `-h`, `--help` | Show help. |
60
+ | `-v`, `--version` | Show version. |
61
+
62
+ ## Workspace detection
63
+
64
+ Patterns are read from the first of:
65
+
66
+ 1. `pnpm-workspace.yaml` (`packages:`)
67
+ 2. `package.json` (`workspaces`)
68
+ 3. `lerna.json` (`packages`)
69
+
70
+ Packages marked `"private": true` are always excluded. On a real (non-dry) run,
71
+ packages not yet published to the registry are skipped with a warning.
72
+
73
+ ## Development
74
+
75
+ Built with [Bun](https://bun.sh), ships zero runtime dependencies, and the
76
+ published artifact is plain Node-compatible JS.
77
+
78
+ ```bash
79
+ bun install
80
+ bun test
81
+ bun run build # -> dist/cli.js (npx entry point), via `bun build --target node`
82
+ ```
83
+
84
+ ## License
85
+
86
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,636 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import process2 from "node:process";
5
+ import { createInterface } from "node:readline/promises";
6
+
7
+ // src/args.ts
8
+ import { parseArgs } from "node:util";
9
+
10
+ // src/types.ts
11
+ var PROVIDERS = ["github", "gitlab", "circleci"];
12
+
13
+ // src/args.ts
14
+ class ArgError extends Error {
15
+ }
16
+ var options = {
17
+ provider: { type: "string" },
18
+ repo: { type: "string" },
19
+ project: { type: "string" },
20
+ env: { type: "string" },
21
+ file: { type: "string" },
22
+ "org-id": { type: "string" },
23
+ "project-id": { type: "string" },
24
+ "pipeline-definition-id": { type: "string" },
25
+ "vcs-origin": { type: "string" },
26
+ "context-id": { type: "string", multiple: true },
27
+ "allow-publish": { type: "boolean" },
28
+ "allow-stage-publish": { type: "boolean" },
29
+ "dry-run": { type: "boolean" },
30
+ only: { type: "string", multiple: true },
31
+ exclude: { type: "string", multiple: true },
32
+ yes: { type: "boolean", short: "y" },
33
+ help: { type: "boolean", short: "h" },
34
+ version: { type: "boolean", short: "v" }
35
+ };
36
+ function require_(value, flag, provider) {
37
+ if (!value)
38
+ throw new ArgError(`--${flag} is required for --provider ${provider}.`);
39
+ return value;
40
+ }
41
+ function parseCliArgs(argv) {
42
+ let values;
43
+ let positionals;
44
+ try {
45
+ const parsed = parseArgs({ args: argv, options, allowPositionals: true, strict: true });
46
+ values = parsed.values;
47
+ positionals = parsed.positionals;
48
+ } catch (err) {
49
+ throw new ArgError(err.message);
50
+ }
51
+ if (values.help)
52
+ return { kind: "help" };
53
+ if (values.version)
54
+ return { kind: "version" };
55
+ const dryRun = values["dry-run"] === true;
56
+ const sub = positionals[0];
57
+ if (sub === "list") {
58
+ return { kind: "list", pkg: positionals[1], dryRun };
59
+ }
60
+ if (sub === "trust") {
61
+ if (positionals.length > 1) {
62
+ throw new ArgError(`Unexpected argument "${positionals[1]}" after "trust".`);
63
+ }
64
+ } else if (sub !== undefined) {
65
+ throw new ArgError(`Unknown command "${sub}". Did you mean "list" or "trust", or did you forget --provider?`);
66
+ }
67
+ const provider = values.provider;
68
+ if (!provider)
69
+ throw new ArgError("--provider is required (github | gitlab | circleci).");
70
+ if (!PROVIDERS.includes(provider)) {
71
+ throw new ArgError(`Unknown provider "${provider}". Must be one of: ${PROVIDERS.join(", ")}.`);
72
+ }
73
+ const common = {
74
+ allowPublish: values["allow-publish"] === true,
75
+ allowStagePublish: values["allow-stage-publish"] === true,
76
+ yes: values.yes === true
77
+ };
78
+ if (!common.allowPublish && !common.allowStagePublish) {
79
+ throw new ArgError("At least one of --allow-publish or --allow-stage-publish is required.");
80
+ }
81
+ let providerOptions;
82
+ switch (provider) {
83
+ case "github":
84
+ providerOptions = {
85
+ provider: "github",
86
+ repo: require_(values.repo, "repo", "github"),
87
+ file: require_(values.file, "file", "github"),
88
+ env: values.env,
89
+ ...common
90
+ };
91
+ break;
92
+ case "gitlab": {
93
+ const project = values.project ?? values.repo;
94
+ providerOptions = {
95
+ provider: "gitlab",
96
+ project: require_(project, "project", "gitlab"),
97
+ file: require_(values.file, "file", "gitlab"),
98
+ env: values.env,
99
+ ...common
100
+ };
101
+ break;
102
+ }
103
+ case "circleci": {
104
+ const vcsOrigin = require_(values["vcs-origin"], "vcs-origin", "circleci");
105
+ if (!vcsOrigin.includes("://")) {
106
+ throw new ArgError(`--vcs-origin must include a protocol, e.g. https://github.com/org/repo (got "${vcsOrigin}").`);
107
+ }
108
+ providerOptions = {
109
+ provider: "circleci",
110
+ orgId: require_(values["org-id"], "org-id", "circleci"),
111
+ projectId: require_(values["project-id"], "project-id", "circleci"),
112
+ pipelineDefinitionId: require_(values["pipeline-definition-id"], "pipeline-definition-id", "circleci"),
113
+ vcsOrigin,
114
+ contextIds: values["context-id"] ?? [],
115
+ ...common
116
+ };
117
+ break;
118
+ }
119
+ }
120
+ return {
121
+ kind: "trust",
122
+ options: providerOptions,
123
+ dryRun,
124
+ only: values.only ?? [],
125
+ exclude: values.exclude ?? []
126
+ };
127
+ }
128
+
129
+ // src/exec.ts
130
+ import { spawnSync } from "node:child_process";
131
+ function run(cmd, args) {
132
+ const res = spawnSync(cmd, args, { encoding: "utf8" });
133
+ return {
134
+ status: res.status ?? (res.error || res.signal ? 1 : 0),
135
+ stdout: res.stdout ?? "",
136
+ stderr: res.stderr ?? ""
137
+ };
138
+ }
139
+ function runInherit(cmd, args) {
140
+ const res = spawnSync(cmd, args, { stdio: "inherit" });
141
+ return res.status ?? (res.error || res.signal ? 1 : 0);
142
+ }
143
+
144
+ // src/npm.ts
145
+ var MIN_NODE = "22.14.0";
146
+ var MIN_NPM = "11.5.1";
147
+ function cmpVersion(a, b) {
148
+ const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
149
+ const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
150
+ for (let i = 0;i < 3; i++) {
151
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
152
+ if (d !== 0)
153
+ return d > 0 ? 1 : -1;
154
+ }
155
+ return 0;
156
+ }
157
+ function getNpmVersion() {
158
+ const r = run("npm", ["--version"]);
159
+ if (r.status !== 0)
160
+ return null;
161
+ const v = r.stdout.trim();
162
+ return v || null;
163
+ }
164
+ function checkVersions() {
165
+ const node = process.versions.node;
166
+ if (cmpVersion(node, MIN_NODE) < 0) {
167
+ return `trustci requires Node ${MIN_NODE} or later (found ${node}).`;
168
+ }
169
+ const npm = getNpmVersion();
170
+ if (!npm) {
171
+ return "Could not determine the npm version. Is npm installed and on your PATH?";
172
+ }
173
+ if (cmpVersion(npm, MIN_NPM) < 0) {
174
+ return `trustci requires npm ${MIN_NPM} or later for trusted publishing (found ${npm}).
175
+ ` + "Update with: npm install -g npm@latest";
176
+ }
177
+ return null;
178
+ }
179
+ function isPublished(name) {
180
+ const r = run("npm", ["view", name, "version"]);
181
+ if (r.status === 0) {
182
+ return r.stdout.trim().length > 0;
183
+ }
184
+ const out = `${r.stderr}
185
+ ${r.stdout}`;
186
+ if (out.includes("E404") || out.includes("404 Not Found")) {
187
+ return false;
188
+ }
189
+ throw new Error(`failed to check the registry for ${name}: ${r.stderr.trim() || "unknown error"}`);
190
+ }
191
+ function commonFlags(o) {
192
+ const a = [];
193
+ if (o.allowPublish)
194
+ a.push("--allow-publish");
195
+ if (o.allowStagePublish)
196
+ a.push("--allow-stage-publish");
197
+ if (o.yes)
198
+ a.push("-y");
199
+ return a;
200
+ }
201
+ function buildTrustArgs(o, pkg) {
202
+ const args = ["trust", o.provider, pkg];
203
+ switch (o.provider) {
204
+ case "github":
205
+ args.push("--repository", o.repo, "--file", o.file);
206
+ if (o.env)
207
+ args.push("--environment", o.env);
208
+ break;
209
+ case "gitlab":
210
+ args.push("--project", o.project, "--file", o.file);
211
+ if (o.env)
212
+ args.push("--environment", o.env);
213
+ break;
214
+ case "circleci":
215
+ args.push("--org-id", o.orgId, "--project-id", o.projectId, "--pipeline-definition-id", o.pipelineDefinitionId, "--vcs-origin", o.vcsOrigin);
216
+ for (const c of o.contextIds)
217
+ args.push("--context-id", c);
218
+ break;
219
+ }
220
+ args.push(...commonFlags(o));
221
+ return args;
222
+ }
223
+ function shellQuote(s) {
224
+ return /^[A-Za-z0-9_@%/:.,=+-]+$/.test(s) ? s : `'${s.replace(/'/g, "'\\''")}'`;
225
+ }
226
+ function displayCommand(args) {
227
+ return ["npm", ...args].map(shellQuote).join(" ");
228
+ }
229
+
230
+ // src/workspace.ts
231
+ import { existsSync, readFileSync } from "node:fs";
232
+ import path2 from "node:path";
233
+
234
+ // src/glob.ts
235
+ import { readdirSync } from "node:fs";
236
+ import path from "node:path";
237
+ var ALWAYS_IGNORE = new Set(["node_modules", ".git", "dist", "build", "coverage", ".turbo"]);
238
+ function escape(s) {
239
+ return s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
240
+ }
241
+ function compilePathGlob(pattern) {
242
+ const segments = pattern.replace(/\/+$/, "").split("/");
243
+ const parts = [];
244
+ for (let i = 0;i < segments.length; i++) {
245
+ const seg = segments[i];
246
+ if (seg === "**") {
247
+ parts.push("(?:.*/)?");
248
+ if (i < segments.length - 1)
249
+ continue;
250
+ parts[parts.length - 1] = "(?:.*)?";
251
+ break;
252
+ }
253
+ const segRe = escape(seg).replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
254
+ parts.push(segRe);
255
+ if (i < segments.length - 1)
256
+ parts.push("/");
257
+ }
258
+ let body = parts.join("").replace(/\(\?:\.\*\/\)\?\//g, "(?:.*/)?");
259
+ if (body.endsWith("/(?:.*)?")) {
260
+ body = `${body.slice(0, -"/(?:.*)?".length)}(?:/.*)?`;
261
+ }
262
+ return new RegExp(`^${body}$`);
263
+ }
264
+ function collectDirs(root) {
265
+ const out = [];
266
+ const walk = (abs, rel) => {
267
+ let entries;
268
+ try {
269
+ entries = readdirSync(abs, { withFileTypes: true });
270
+ } catch {
271
+ return;
272
+ }
273
+ for (const entry of entries) {
274
+ if (!entry.isDirectory())
275
+ continue;
276
+ if (ALWAYS_IGNORE.has(entry.name))
277
+ continue;
278
+ const childRel = rel ? `${rel}/${entry.name}` : entry.name;
279
+ out.push(childRel);
280
+ walk(path.join(abs, entry.name), childRel);
281
+ }
282
+ };
283
+ walk(root, "");
284
+ return out;
285
+ }
286
+ function expandPatterns(root, patterns) {
287
+ const positives = [];
288
+ const negatives = [];
289
+ for (const raw of patterns) {
290
+ const pattern = raw.trim();
291
+ if (!pattern)
292
+ continue;
293
+ if (pattern.startsWith("!")) {
294
+ negatives.push(compilePathGlob(pattern.slice(1)));
295
+ } else {
296
+ positives.push(compilePathGlob(pattern));
297
+ }
298
+ }
299
+ if (positives.length === 0)
300
+ return [];
301
+ const dirs = collectDirs(root);
302
+ const matched = new Set;
303
+ for (const rel of dirs) {
304
+ if (positives.some((re) => re.test(rel)) && !negatives.some((re) => re.test(rel))) {
305
+ matched.add(rel);
306
+ }
307
+ }
308
+ return [...matched].sort().map((rel) => path.resolve(root, rel));
309
+ }
310
+ function matchName(pattern, name) {
311
+ const re = new RegExp(`^${escape(pattern).replace(/\*/g, ".*").replace(/\?/g, ".")}$`);
312
+ return re.test(name);
313
+ }
314
+
315
+ // src/workspace.ts
316
+ function stripComment(line) {
317
+ let inSingle = false;
318
+ let inDouble = false;
319
+ for (let i = 0;i < line.length; i++) {
320
+ const c = line[i];
321
+ if (c === "'" && !inDouble)
322
+ inSingle = !inSingle;
323
+ else if (c === '"' && !inSingle)
324
+ inDouble = !inDouble;
325
+ else if (c === "#" && !inSingle && !inDouble)
326
+ return line.slice(0, i);
327
+ }
328
+ return line;
329
+ }
330
+ function unquote(s) {
331
+ const t = s.trim();
332
+ if (t.length >= 2 && (t[0] === '"' && t.at(-1) === '"' || t[0] === "'" && t.at(-1) === "'")) {
333
+ return t.slice(1, -1);
334
+ }
335
+ return t;
336
+ }
337
+ function parseFlowArray(s) {
338
+ const open = s.indexOf("[");
339
+ const close = s.lastIndexOf("]");
340
+ if (open === -1 || close === -1)
341
+ return [];
342
+ return s.slice(open + 1, close).split(",").map((x) => unquote(x)).filter(Boolean);
343
+ }
344
+ function parsePnpmPackages(content) {
345
+ const lines = content.split(/\r?\n/);
346
+ const result = [];
347
+ let inBlock = false;
348
+ for (const rawLine of lines) {
349
+ const line = stripComment(rawLine);
350
+ const trimmed = line.trim();
351
+ if (!trimmed)
352
+ continue;
353
+ if (!inBlock) {
354
+ const m = /^packages\s*:\s*(.*)$/.exec(trimmed);
355
+ if (!m)
356
+ continue;
357
+ const rest = (m[1] ?? "").trim();
358
+ if (rest.startsWith("["))
359
+ return parseFlowArray(rest);
360
+ inBlock = true;
361
+ continue;
362
+ }
363
+ if (trimmed.startsWith("-")) {
364
+ const val = unquote(trimmed.slice(1).trim());
365
+ if (val)
366
+ result.push(val);
367
+ } else {
368
+ break;
369
+ }
370
+ }
371
+ return result;
372
+ }
373
+ function readJson(file) {
374
+ return JSON.parse(readFileSync(file, "utf8"));
375
+ }
376
+ function readJsonSafe(file) {
377
+ try {
378
+ const json = readJson(file);
379
+ return json && typeof json === "object" ? json : null;
380
+ } catch {
381
+ return null;
382
+ }
383
+ }
384
+ function detectPatterns(root) {
385
+ const pnpmFile = path2.join(root, "pnpm-workspace.yaml");
386
+ if (existsSync(pnpmFile)) {
387
+ const patterns = parsePnpmPackages(readFileSync(pnpmFile, "utf8"));
388
+ if (patterns.length > 0)
389
+ return { source: "pnpm-workspace.yaml", patterns };
390
+ }
391
+ const pkgFile = path2.join(root, "package.json");
392
+ if (existsSync(pkgFile)) {
393
+ const json = readJsonSafe(pkgFile);
394
+ if (json) {
395
+ const ws = json.workspaces;
396
+ const patterns = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
397
+ if (patterns.length > 0)
398
+ return { source: "package.json (workspaces)", patterns };
399
+ }
400
+ }
401
+ const lernaFile = path2.join(root, "lerna.json");
402
+ if (existsSync(lernaFile)) {
403
+ const json = readJsonSafe(lernaFile);
404
+ if (json) {
405
+ const patterns = Array.isArray(json.packages) ? json.packages : ["packages/*"];
406
+ if (patterns.length > 0)
407
+ return { source: "lerna.json", patterns };
408
+ }
409
+ }
410
+ return { source: "none", patterns: [] };
411
+ }
412
+ function readPkg(dir) {
413
+ const file = path2.join(dir, "package.json");
414
+ if (!existsSync(file))
415
+ return null;
416
+ const json = readJsonSafe(file);
417
+ if (!json || typeof json.name !== "string" || !json.name)
418
+ return null;
419
+ return {
420
+ name: json.name,
421
+ version: typeof json.version === "string" ? json.version : undefined,
422
+ private: json.private === true,
423
+ dir
424
+ };
425
+ }
426
+ function discoverPackages(root) {
427
+ const { source, patterns } = detectPatterns(root);
428
+ const dirs = expandPatterns(root, patterns);
429
+ const all = [];
430
+ const seen = new Set;
431
+ for (const dir of dirs) {
432
+ const pkg = readPkg(dir);
433
+ if (pkg && !seen.has(pkg.name)) {
434
+ seen.add(pkg.name);
435
+ all.push(pkg);
436
+ }
437
+ }
438
+ return { source, patterns, all };
439
+ }
440
+ function filterPackages(all, only, exclude) {
441
+ return all.filter((pkg) => {
442
+ if (pkg.private)
443
+ return false;
444
+ if (only.length > 0 && !only.some((p) => matchName(p, pkg.name)))
445
+ return false;
446
+ if (exclude.some((p) => matchName(p, pkg.name)))
447
+ return false;
448
+ return true;
449
+ });
450
+ }
451
+
452
+ // src/cli.ts
453
+ var VERSION = "0.1.0";
454
+ var HELP = `trustci — bulk-register npm Trusted Publishers across a monorepo
455
+
456
+ Usage:
457
+ trustci [trust] --provider <github|gitlab|circleci> [provider flags] [options]
458
+ trustci list [package] [--dry-run]
459
+
460
+ Providers:
461
+ github --repo <owner/repo> --file <workflow> [--env <name>]
462
+ gitlab --project <group/project> --file <workflow> [--env <name>]
463
+ (--repo is accepted as an alias for --project)
464
+ circleci --org-id <uuid> --project-id <uuid> --pipeline-definition-id <uuid>
465
+ --vcs-origin <url> [--context-id <uuid> ...]
466
+
467
+ Permissions (at least one required):
468
+ --allow-publish allow npm publish
469
+ --allow-stage-publish allow npm stage publish
470
+
471
+ Options:
472
+ --dry-run print the npm trust commands without executing them
473
+ --only <name-glob> only include packages matching (repeatable)
474
+ --exclude <name-glob> exclude packages matching (repeatable)
475
+ -y, --yes skip the confirmation prompt (and npm's)
476
+ -h, --help show this help
477
+ -v, --version show the trustci version
478
+
479
+ Workspaces are detected from pnpm-workspace.yaml, package.json "workspaces",
480
+ or lerna.json. Private packages ("private": true) are always excluded.
481
+
482
+ Examples:
483
+ trustci --dry-run --provider github --repo me/repo --file release.yml --allow-publish
484
+ trustci --provider gitlab --project me/repo --file .gitlab-ci.yml --allow-publish -y
485
+ trustci --provider circleci --org-id <uuid> --project-id <uuid> \\
486
+ --pipeline-definition-id <uuid> --vcs-origin https://github.com/me/repo --allow-publish
487
+ trustci list
488
+ `;
489
+ async function confirm(question) {
490
+ const rl = createInterface({ input: process2.stdin, output: process2.stderr });
491
+ try {
492
+ const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
493
+ return answer === "y" || answer === "yes";
494
+ } finally {
495
+ rl.close();
496
+ }
497
+ }
498
+ async function runList(pkg, dryRun) {
499
+ const targets = [];
500
+ if (pkg) {
501
+ targets.push(pkg);
502
+ } else {
503
+ const { all, source } = discoverPackages(process2.cwd());
504
+ const pub = filterPackages(all, [], []);
505
+ if (pub.length === 0) {
506
+ console.error(`No public workspace packages found (detected via ${source}).`);
507
+ return 1;
508
+ }
509
+ targets.push(...pub.map((p) => p.name));
510
+ }
511
+ let failures = 0;
512
+ for (const name of targets) {
513
+ const args = ["trust", "list", name];
514
+ if (dryRun) {
515
+ console.log(displayCommand(args));
516
+ } else {
517
+ console.error(`
518
+ $ ${displayCommand(args)}`);
519
+ if (runInherit("npm", args) !== 0)
520
+ failures++;
521
+ }
522
+ }
523
+ return failures > 0 ? 1 : 0;
524
+ }
525
+ async function runTrust(parsed) {
526
+ const root = process2.cwd();
527
+ const { all, source } = discoverPackages(root);
528
+ if (all.length === 0) {
529
+ console.error(`No workspace packages found (detected via ${source}).`);
530
+ return 1;
531
+ }
532
+ const candidates = filterPackages(all, parsed.only, parsed.exclude);
533
+ if (candidates.length === 0) {
534
+ console.error(`No public packages matched (out of ${all.length} discovered via ${source}).`);
535
+ return 1;
536
+ }
537
+ console.error(`Detected ${all.length} package(s) via ${source}; ${candidates.length} public after filters.`);
538
+ const provider = parsed.options.provider;
539
+ if (parsed.dryRun) {
540
+ console.log(`
541
+ # ${candidates.length} command(s) for provider "${provider}" (dry run):
542
+ `);
543
+ for (const pkg of candidates) {
544
+ console.log(displayCommand(buildTrustArgs(parsed.options, pkg.name)));
545
+ }
546
+ console.error(`
547
+ (dry run — registry publish-state was not checked and nothing was executed)`);
548
+ return 0;
549
+ }
550
+ const targets = [];
551
+ for (const pkg of candidates) {
552
+ try {
553
+ if (isPublished(pkg.name)) {
554
+ targets.push(pkg);
555
+ } else {
556
+ console.warn(`skip ${pkg.name}: not published to the registry yet`);
557
+ }
558
+ } catch (err) {
559
+ console.error(`error: ${err.message}`);
560
+ return 1;
561
+ }
562
+ }
563
+ if (targets.length === 0) {
564
+ console.error("Nothing to do: no published packages to register.");
565
+ return 1;
566
+ }
567
+ console.error(`
568
+ Will register ${targets.length} package(s) with ${provider}:`);
569
+ for (const pkg of targets)
570
+ console.error(` - ${pkg.name}`);
571
+ if (!parsed.options.yes) {
572
+ if (!process2.stdin.isTTY) {
573
+ console.error("error: confirmation required but stdin is not a TTY. Pass --yes (-y) to run non-interactively.");
574
+ return 1;
575
+ }
576
+ if (!await confirm(`
577
+ Proceed?`)) {
578
+ console.error("Aborted.");
579
+ return 1;
580
+ }
581
+ }
582
+ let failures = 0;
583
+ for (const pkg of targets) {
584
+ const args = buildTrustArgs(parsed.options, pkg.name);
585
+ console.error(`
586
+ $ ${displayCommand(args)}`);
587
+ const code = runInherit("npm", args);
588
+ if (code !== 0) {
589
+ failures++;
590
+ console.error(` ${pkg.name}: failed (exit ${code})`);
591
+ }
592
+ }
593
+ console.error(`
594
+ Done. ${targets.length - failures} succeeded, ${failures} failed.`);
595
+ return failures > 0 ? 1 : 0;
596
+ }
597
+ async function main() {
598
+ let parsed;
599
+ try {
600
+ parsed = parseCliArgs(process2.argv.slice(2));
601
+ } catch (err) {
602
+ if (err instanceof ArgError) {
603
+ console.error(`error: ${err.message}
604
+ `);
605
+ console.error(HELP);
606
+ return 1;
607
+ }
608
+ throw err;
609
+ }
610
+ if (parsed.kind === "help") {
611
+ console.log(HELP);
612
+ return 0;
613
+ }
614
+ if (parsed.kind === "version") {
615
+ console.log(VERSION);
616
+ return 0;
617
+ }
618
+ const verr = checkVersions();
619
+ if (verr) {
620
+ if (parsed.dryRun) {
621
+ console.error(`warning: ${verr}
622
+ (continuing because --dry-run does not execute npm)
623
+ `);
624
+ } else {
625
+ console.error(`error: ${verr}`);
626
+ return 1;
627
+ }
628
+ }
629
+ return parsed.kind === "list" ? runList(parsed.pkg, parsed.dryRun) : runTrust(parsed);
630
+ }
631
+ main().then((code) => {
632
+ process2.exitCode = code;
633
+ }, (err) => {
634
+ console.error(err);
635
+ process2.exitCode = 1;
636
+ });
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "trustci",
3
+ "version": "0.1.0",
4
+ "description": "Bulk-register npm Trusted Publishers (npm trust) across all public packages in a monorepo. Supports GitHub, GitLab and CircleCI.",
5
+ "type": "module",
6
+ "bin": {
7
+ "trustci": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "engines": {
13
+ "node": ">=22.14.0",
14
+ "npm": ">=11.5.1"
15
+ },
16
+ "scripts": {
17
+ "build": "bun build src/cli.ts --target node --outfile dist/cli.js",
18
+ "test": "bun test",
19
+ "prepublishOnly": "bun run build"
20
+ },
21
+ "keywords": [
22
+ "npm",
23
+ "trusted-publishing",
24
+ "trusted-publisher",
25
+ "oidc",
26
+ "monorepo",
27
+ "cli",
28
+ "npm-trust",
29
+ "github-actions",
30
+ "gitlab",
31
+ "circleci"
32
+ ],
33
+ "license": "MIT",
34
+ "devDependencies": {
35
+ "@types/bun": "latest",
36
+ "typescript": "^5"
37
+ }
38
+ }