trustci 0.2.2 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +70 -3
  2. package/dist/cli.js +244 -35
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -10,7 +10,7 @@ Setting up trusted publishing one package at a time is tedious in a monorepo.
10
10
 
11
11
  ## Requirements
12
12
 
13
- - **npm** 11.5.1 or later (for `npm trust`)
13
+ - **npm** 11.15.0 or later (for `npm trust` with `--allow-publish` / `--allow-stage-publish`)
14
14
  - **Node** 22.14.0 or later
15
15
 
16
16
  ## Usage
@@ -32,7 +32,7 @@ npx trustci list
32
32
  | ---------- | ---------------------------------------------------------------------------------------------- | --------------- |
33
33
  | `github` | `--repo <owner/repo>` `--file <workflow>` | `--env <name>` |
34
34
  | `gitlab` | `--project <group/project>` (or `--repo`) `--file <workflow>` | `--env <name>` |
35
- | `circleci` | `--org-id` `--project-id` `--pipeline-definition-id` `--vcs-origin <host/owner/repo>` | `--context-id` (repeatable) |
35
+ | `circleci` | A CircleCI token, or all of `--org-id` `--project-id` `--pipeline-definition-id` | `--vcs-origin <host/owner/repo>` (inferred), `--circle-token`, `--file`, `--context-id` (repeatable) |
36
36
 
37
37
  At least one of `--allow-publish` / `--allow-stage-publish` is required.
38
38
 
@@ -40,7 +40,12 @@ At least one of `--allow-publish` / `--allow-stage-publish` is required.
40
40
  # GitLab
41
41
  npx trustci --provider gitlab --project me/repo --file .gitlab-ci.yml --allow-publish -y
42
42
 
43
- # CircleCI (vcs-origin is host/owner/repo; a leading https:// is stripped automatically)
43
+ # CircleCI — minimal form: UUIDs are resolved via the API and --vcs-origin
44
+ # is inferred, so a token (here via the environment) is all you need to add
45
+ CIRCLE_TOKEN=... npx trustci --provider circleci --allow-publish
46
+
47
+ # CircleCI — fully explicit form
48
+ # (vcs-origin is host/owner/repo; a leading https:// is stripped automatically)
44
49
  npx trustci --provider circleci \
45
50
  --org-id <uuid> --project-id <uuid> --pipeline-definition-id <uuid> \
46
51
  --vcs-origin github.com/me/repo \
@@ -48,6 +53,47 @@ npx trustci --provider circleci \
48
53
  --allow-publish
49
54
  ```
50
55
 
56
+ #### CircleCI `--vcs-origin`
57
+
58
+ When `--vcs-origin` is omitted, trustci takes it from the root `package.json`
59
+ `repository` field (URL, `git+https://…`, `git@host:owner/repo`, or the
60
+ `github:` / `gitlab:` / `bitbucket:` / `owner/repo` shorthands), then from
61
+ `git remote get-url origin`. It prints which one it used, and warns when the two
62
+ disagree (`package.json` wins, since it is committed rather than clone-specific).
63
+
64
+ #### CircleCI token lookup
65
+
66
+ When any of the three UUIDs is missing, trustci resolves it through the CircleCI
67
+ API v2. The token is taken from the first of these that is set, and trustci
68
+ prints which one it used (never the token itself):
69
+
70
+ 1. `--circle-token`
71
+ 2. `CIRCLE_TOKEN` (circleci CLI v1, chunk)
72
+ 3. `CIRCLECI_TOKEN` (chunk)
73
+ 4. `CIRCLE_CLI_TOKEN` (circleci CLI v1, legacy alias)
74
+ 5. `CIRCLECI_CLI_TOKEN` (circleci CLI v0.1)
75
+ 6. `token` in `~/.circleci/cli.yml`, written by `circleci setup` in circleci CLI v0.1.
76
+ It is ignored when its `host` is not `https://circleci.com`.
77
+
78
+ The OS keychain, where circleci CLI v1 and chunk store tokens by default, is not read.
79
+ When all three UUIDs are given, no token is looked up.
80
+
81
+ #### CircleCI pipeline definitions and `--file`
82
+
83
+ A missing `--pipeline-definition-id` is resolved by listing the project's
84
+ pipeline definitions. When there is exactly one, it is used automatically.
85
+ When the project has several — e.g. a config sourced from another repository
86
+ alongside `.circleci/config.yml` — pass `--file` with either the definition's
87
+ config file path or its name; the error message lists the candidates:
88
+
89
+ ```bash
90
+ npx trustci --provider circleci --allow-publish \
91
+ --file workflows/publish/npm-polyrepo-bun-semantic-release.yaml
92
+ ```
93
+
94
+ `--file` is only used for this lookup — it is not passed to `npm trust`, and
95
+ it has no effect when `--pipeline-definition-id` is given explicitly.
96
+
51
97
  ### Common options
52
98
 
53
99
  | Flag | Description |
@@ -70,6 +116,18 @@ Patterns are read from the first of:
70
116
  Packages marked `"private": true` are always excluded. On a real (non-dry) run,
71
117
  packages not yet published to the registry are skipped with a warning.
72
118
 
119
+ ## Agent skill
120
+
121
+ The repository ships an [Agent Skill](https://agentskills.io/) at
122
+ [`skills/trustci/SKILL.md`](https://github.com/hideokamoto/trustci/blob/main/skills/trustci/SKILL.md)
123
+ that teaches coding agents (Claude Code, Codex, Cursor, …) to run trustci safely:
124
+ dry-run first, then a real run only after you confirm. Install it with the
125
+ [`skills`](https://github.com/vercel-labs/skills) CLI:
126
+
127
+ ```bash
128
+ npx skills add hideokamoto/trustci
129
+ ```
130
+
73
131
  ## Development
74
132
 
75
133
  Built with [Bun](https://bun.sh), ships zero runtime dependencies, and the
@@ -82,6 +140,15 @@ bun run typecheck
82
140
  bun run build # -> dist/cli.js (npx entry point), via `bun build --target node`
83
141
  ```
84
142
 
143
+ The documentation site lives in `docs/` ([Astro Starlight](https://starlight.astro.build/)):
144
+
145
+ ```bash
146
+ cd docs
147
+ bun install
148
+ bun run dev # local preview
149
+ bun run build # -> docs/dist
150
+ ```
151
+
85
152
  `bun install` also enables a husky `commit-msg` hook that runs
86
153
  [commitlint](https://commitlint.js.org/) with `@commitlint/config-conventional`,
87
154
  so every commit must be a [Conventional Commit](https://www.conventionalcommits.org/).
package/dist/cli.js CHANGED
@@ -37,13 +37,24 @@ function projectSlugFromVcsOrigin(origin) {
37
37
  }
38
38
  return `${vcsType}/${parts[0]}/${parts[1]}`;
39
39
  }
40
+
41
+ class CircleApiError extends Error {
42
+ status;
43
+ path;
44
+ body;
45
+ constructor(status, path, body) {
46
+ super(`CircleCI API error (${status}) for ${path}: ${body}`);
47
+ this.status = status;
48
+ this.path = path;
49
+ this.body = body;
50
+ }
51
+ }
40
52
  async function circleGet(token, path) {
41
53
  const response = await fetch(`${BASE_URL}${path}`, {
42
54
  headers: { "Circle-Token": token, Accept: "application/json" }
43
55
  });
44
56
  if (!response.ok) {
45
- const body = await response.text();
46
- throw new Error(`CircleCI API error (${response.status}) for ${path}: ${body}`);
57
+ throw new CircleApiError(response.status, path, await response.text());
47
58
  }
48
59
  return response.json();
49
60
  }
@@ -168,26 +179,17 @@ function parseCliArgs(argv) {
168
179
  break;
169
180
  }
170
181
  case "circleci": {
182
+ const rawVcsOrigin = values["vcs-origin"];
171
183
  let vcsOrigin;
172
184
  try {
173
- vcsOrigin = normalizeVcsOrigin(require_(values["vcs-origin"], "vcs-origin", "circleci"));
185
+ vcsOrigin = rawVcsOrigin === undefined ? undefined : normalizeVcsOrigin(rawVcsOrigin);
174
186
  } catch (e) {
175
- if (e instanceof ArgError)
176
- throw e;
177
187
  throw new ArgError(`--${e.message}`);
178
188
  }
179
189
  const circleToken = values["circle-token"];
180
190
  const orgId = values["org-id"];
181
191
  const projectId = values["project-id"];
182
192
  const pipelineDefinitionId = values["pipeline-definition-id"];
183
- if (!circleToken) {
184
- if (!orgId)
185
- throw new ArgError("--org-id is required for --provider circleci (or provide --circle-token for auto-resolution).");
186
- if (!projectId)
187
- throw new ArgError("--project-id is required for --provider circleci (or provide --circle-token for auto-resolution).");
188
- if (!pipelineDefinitionId)
189
- throw new ArgError("--pipeline-definition-id is required for --provider circleci (or provide --circle-token for auto-resolution).");
190
- }
191
193
  providerOptions = {
192
194
  provider: "circleci",
193
195
  circleToken,
@@ -226,33 +228,217 @@ function runInherit(cmd, args) {
226
228
  return res.status ?? (res.error || res.signal ? 1 : 0);
227
229
  }
228
230
 
231
+ // src/resolve.ts
232
+ import { readFileSync } from "node:fs";
233
+ import { homedir } from "node:os";
234
+ import { join as join2 } from "node:path";
235
+
236
+ // src/token.ts
237
+ import { join } from "node:path";
238
+ var DEFAULT_HOST = "https://circleci.com";
239
+ var LEGACY_CLI_YML = "~/.circleci/cli.yml";
240
+ var TOKEN_ENV_VARS = ["CIRCLE_TOKEN", "CIRCLECI_TOKEN", "CIRCLE_CLI_TOKEN", "CIRCLECI_CLI_TOKEN"];
241
+ var TOKEN_SOURCES = ["--circle-token", ...TOKEN_ENV_VARS, LEGACY_CLI_YML];
242
+ function parseLegacyCliYml(content) {
243
+ const out = {};
244
+ for (const raw of content.split(/\r?\n/)) {
245
+ const m = /^(token|host):(.*)$/.exec(raw);
246
+ if (!m)
247
+ continue;
248
+ const value = parseScalar(m[2].trim());
249
+ if (value)
250
+ out[m[1]] = value;
251
+ }
252
+ return out;
253
+ }
254
+ function parseScalar(v) {
255
+ const quoted = /^(["'])(.*)\1(\s+#.*)?$/.exec(v);
256
+ if (quoted)
257
+ return quoted[2];
258
+ return v.replace(/\s+#.*$/, "").trim();
259
+ }
260
+ function resolveCircleToken(input) {
261
+ if (input.flag)
262
+ return { token: input.flag, source: "--circle-token", notes: [] };
263
+ for (const name of TOKEN_ENV_VARS) {
264
+ const value = input.env[name];
265
+ if (value)
266
+ return { token: value, source: name, notes: [] };
267
+ }
268
+ const content = input.readFile(join(input.homedir, ".circleci", "cli.yml"));
269
+ if (content === null)
270
+ return { token: undefined, notes: [] };
271
+ const { token, host } = parseLegacyCliYml(content);
272
+ if (!token)
273
+ return { token: undefined, notes: [] };
274
+ if (token === "|" || token === ">" || /^[|>][-+0-9]*$/.test(token)) {
275
+ return { token: undefined, notes: [`skipped ${LEGACY_CLI_YML}: its token is not a single-line value trustci can read`] };
276
+ }
277
+ if (host && host.replace(/\/+$/, "") !== DEFAULT_HOST) {
278
+ return { token: undefined, notes: [`skipped ${LEGACY_CLI_YML}: its token is for ${host}, not ${DEFAULT_HOST}`] };
279
+ }
280
+ return { token, source: LEGACY_CLI_YML, notes: [] };
281
+ }
282
+
283
+ // src/vcs.ts
284
+ var SHORTHAND_HOSTS = {
285
+ github: "github.com",
286
+ gitlab: "gitlab.com",
287
+ bitbucket: "bitbucket.org"
288
+ };
289
+ var PACKAGE_JSON = "package.json repository";
290
+ var GIT_REMOTE = "git remote origin";
291
+ function parseRepositoryUrl(input) {
292
+ const s = input.trim();
293
+ let host;
294
+ let path;
295
+ const shorthand = /^(github|gitlab|bitbucket):(.+)$/.exec(s);
296
+ const scp = /^[^@/\s]+@([^:/\s]+):(.+)$/.exec(s);
297
+ if (shorthand) {
298
+ host = SHORTHAND_HOSTS[shorthand[1]];
299
+ path = shorthand[2];
300
+ } else if (/^[^/:\s]+\/[^/:\s]+$/.test(s)) {
301
+ host = "github.com";
302
+ path = s;
303
+ } else if (scp) {
304
+ host = scp[1];
305
+ path = scp[2];
306
+ } else {
307
+ let url;
308
+ try {
309
+ url = new URL(s.replace(/^git\+/, ""));
310
+ } catch {
311
+ return null;
312
+ }
313
+ if (!["https:", "http:", "ssh:", "git:"].includes(url.protocol) || !url.hostname)
314
+ return null;
315
+ host = url.hostname;
316
+ path = url.pathname;
317
+ }
318
+ const parts = path.replace(/#.*$/, "").replace(/^\/+|\/+$/g, "").replace(/\.git$/, "").split("/");
319
+ if (parts.length < 2 || parts.some((p) => !p))
320
+ return null;
321
+ return `${host}/${parts.join("/")}`;
322
+ }
323
+ function repositoryFromPackageJson(repository) {
324
+ if (typeof repository === "string")
325
+ return repository;
326
+ if (repository && typeof repository === "object" && "url" in repository) {
327
+ const url = repository.url;
328
+ if (typeof url === "string")
329
+ return url;
330
+ }
331
+ return;
332
+ }
333
+ function resolveVcsOrigin(input) {
334
+ if (input.flag)
335
+ return { value: input.flag, source: "--vcs-origin", warnings: [] };
336
+ const warnings = [];
337
+ const fromGit = input.gitRemote ? parseRepositoryUrl(input.gitRemote) : null;
338
+ if (input.pkgRepository !== undefined) {
339
+ const fromPkg = parseRepositoryUrl(input.pkgRepository);
340
+ if (fromPkg) {
341
+ if (fromGit && fromGit !== fromPkg) {
342
+ warnings.push(`${PACKAGE_JSON} (${fromPkg}) differs from ${GIT_REMOTE} (${fromGit}); using ${fromPkg}`);
343
+ }
344
+ return { value: fromPkg, source: PACKAGE_JSON, warnings };
345
+ }
346
+ warnings.push(`could not read a repository from ${PACKAGE_JSON} ("${input.pkgRepository}")`);
347
+ }
348
+ if (fromGit)
349
+ return { value: fromGit, source: GIT_REMOTE, warnings };
350
+ return { value: undefined, warnings };
351
+ }
352
+
229
353
  // src/resolve.ts
230
354
  var defaultResolveDeps = {
231
355
  resolveOrgAndProjectId,
232
356
  resolvePipelineDefinitionId,
233
- log: (message) => console.error(message)
357
+ log: (message) => console.error(message),
358
+ env: process.env,
359
+ homedir: homedir(),
360
+ readFile: (path) => {
361
+ try {
362
+ return readFileSync(path, "utf8");
363
+ } catch {
364
+ return null;
365
+ }
366
+ },
367
+ cwd: process.cwd(),
368
+ gitRemoteUrl: () => {
369
+ const r = run("git", ["remote", "get-url", "origin"]);
370
+ return r.status === 0 && r.stdout.trim() ? r.stdout.trim() : null;
371
+ }
234
372
  };
373
+ function resolveMissingVcsOrigin(opts, deps) {
374
+ if (opts.vcsOrigin)
375
+ return opts.vcsOrigin;
376
+ let pkgRepository;
377
+ const pkgText = deps.readFile(join2(deps.cwd, "package.json"));
378
+ if (pkgText !== null) {
379
+ try {
380
+ pkgRepository = repositoryFromPackageJson(JSON.parse(pkgText).repository);
381
+ } catch {
382
+ deps.log("warning: could not parse package.json while looking for a repository");
383
+ }
384
+ }
385
+ const found = resolveVcsOrigin({ pkgRepository, gitRemote: deps.gitRemoteUrl() });
386
+ for (const w of found.warnings)
387
+ deps.log(`warning: ${w}`);
388
+ if (found.value === undefined) {
389
+ throw new Error("--vcs-origin is required for --provider circleci and could not be inferred " + "(looked in: package.json repository, git remote origin). Pass e.g. --vcs-origin github.com/org/repo.");
390
+ }
391
+ deps.log(`vcs-origin: ${found.value} (from ${found.source})`);
392
+ opts.vcsOrigin = found.value;
393
+ return found.value;
394
+ }
235
395
  async function resolveCircleciOptions(parsed, deps = defaultResolveDeps) {
236
396
  const opts = parsed.options;
237
397
  if (opts.provider !== "circleci")
238
398
  return;
239
- if (!opts.circleToken)
399
+ const vcsOrigin = resolveMissingVcsOrigin(opts, deps);
400
+ const missing = [
401
+ !opts.orgId && "--org-id",
402
+ !opts.projectId && "--project-id",
403
+ !opts.pipelineDefinitionId && "--pipeline-definition-id"
404
+ ].filter((m) => Boolean(m));
405
+ if (missing.length === 0)
240
406
  return;
241
- if (!opts.orgId || !opts.projectId) {
242
- deps.log("Resolving org-id and project-id via CircleCI API...");
243
- const resolved = await deps.resolveOrgAndProjectId(opts.circleToken, opts.vcsOrigin);
244
- opts.orgId ??= resolved.orgId;
245
- opts.projectId ??= resolved.projectId;
407
+ const found = resolveCircleToken({
408
+ flag: opts.circleToken,
409
+ env: deps.env,
410
+ homedir: deps.homedir,
411
+ readFile: deps.readFile
412
+ });
413
+ for (const note of found.notes)
414
+ deps.log(`note: ${note}`);
415
+ if (found.token === undefined) {
416
+ throw new Error(`missing ${missing.join(", ")} for --provider circleci. Pass them explicitly, ` + `or provide a CircleCI API token so they can be resolved (looked in: ${TOKEN_SOURCES.join(", ")}).`);
246
417
  }
247
- if (!opts.pipelineDefinitionId) {
248
- deps.log("Resolving pipeline-definition-id via CircleCI API...");
249
- opts.pipelineDefinitionId = await deps.resolvePipelineDefinitionId(opts.circleToken, opts.projectId, opts.file);
418
+ const { token, source } = found;
419
+ deps.log(`Using CircleCI token from ${source}`);
420
+ try {
421
+ if (!opts.orgId || !opts.projectId) {
422
+ deps.log("Resolving org-id and project-id via CircleCI API...");
423
+ const resolved = await deps.resolveOrgAndProjectId(token, vcsOrigin);
424
+ opts.orgId ??= resolved.orgId;
425
+ opts.projectId ??= resolved.projectId;
426
+ }
427
+ if (!opts.pipelineDefinitionId) {
428
+ deps.log("Resolving pipeline-definition-id via CircleCI API...");
429
+ opts.pipelineDefinitionId = await deps.resolvePipelineDefinitionId(token, opts.projectId, opts.file);
430
+ }
431
+ } catch (err) {
432
+ if (err instanceof CircleApiError && (err.status === 401 || err.status === 403)) {
433
+ throw new Error(`CircleCI rejected the token from ${source} (${err.status}). ` + `Pass a valid token with --circle-token or the CIRCLE_TOKEN environment variable.`);
434
+ }
435
+ throw err;
250
436
  }
251
437
  }
252
438
 
253
439
  // src/npm.ts
254
440
  var MIN_NODE = "22.14.0";
255
- var MIN_NPM = "11.5.1";
441
+ var MIN_NPM = "11.15.0";
256
442
  function cmpVersion(a, b) {
257
443
  const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
258
444
  const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
@@ -337,7 +523,7 @@ function displayCommand(args) {
337
523
  }
338
524
 
339
525
  // src/workspace.ts
340
- import { existsSync, readFileSync } from "node:fs";
526
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
341
527
  import path2 from "node:path";
342
528
 
343
529
  // src/glob.ts
@@ -480,7 +666,7 @@ function parsePnpmPackages(content) {
480
666
  return result;
481
667
  }
482
668
  function readJson(file) {
483
- return JSON.parse(readFileSync(file, "utf8"));
669
+ return JSON.parse(readFileSync2(file, "utf8"));
484
670
  }
485
671
  function readJsonSafe(file) {
486
672
  try {
@@ -493,7 +679,7 @@ function readJsonSafe(file) {
493
679
  function detectPatterns(root) {
494
680
  const pnpmFile = path2.join(root, "pnpm-workspace.yaml");
495
681
  if (existsSync(pnpmFile)) {
496
- const patterns = parsePnpmPackages(readFileSync(pnpmFile, "utf8"));
682
+ const patterns = parsePnpmPackages(readFileSync2(pnpmFile, "utf8"));
497
683
  if (patterns.length > 0)
498
684
  return { source: "pnpm-workspace.yaml", patterns };
499
685
  }
@@ -537,6 +723,9 @@ function discoverPackages(root) {
537
723
  const dirs = expandPatterns(root, patterns);
538
724
  const all = [];
539
725
  const seen = new Set;
726
+ if (existsSync(path2.join(root, "pnpm-workspace.yaml"))) {
727
+ dirs.unshift(path2.resolve(root));
728
+ }
540
729
  for (const dir of dirs) {
541
730
  const pkg = readPkg(dir);
542
731
  if (pkg && !seen.has(pkg.name)) {
@@ -564,7 +753,7 @@ function filterPackages(all, only, exclude) {
564
753
  });
565
754
  }
566
755
  // package.json
567
- var version = "0.2.2";
756
+ var version = "0.3.1";
568
757
 
569
758
  // src/cli.ts
570
759
  var VERSION = version;
@@ -574,40 +763,57 @@ Usage:
574
763
  trustci [trust] --provider <github|gitlab|circleci> [provider flags] [options]
575
764
  trustci list [package] [--dry-run]
576
765
 
766
+ Commands:
767
+ trust register a trusted publisher for every public workspace package
768
+ (the default; "trust" may be omitted)
769
+ list run \`npm trust list\` for <package>, or for every public workspace package
770
+
577
771
  Providers:
578
772
  github --repo <owner/repo> --file <workflow> [--env <name>]
579
773
  gitlab --project <group/project> --file <workflow> [--env <name>]
580
774
  (--repo is accepted as an alias for --project)
581
- circleci --vcs-origin <host/owner/repo> --circle-token <token> [--file <config>]
775
+ circleci [--vcs-origin <host/owner/repo>] [--circle-token <token>] [--file <config>]
582
776
  [--org-id <uuid>] [--project-id <uuid>] [--pipeline-definition-id <uuid>]
583
777
  [--context-id <uuid> ...]
584
- UUIDs are auto-resolved via CircleCI API v2 when --circle-token is provided.
585
- Explicit UUIDs override auto-resolution and work without a token.
778
+ --vcs-origin defaults to the root package.json "repository", then
779
+ \`git remote get-url origin\`.
780
+ Missing UUIDs are resolved via CircleCI API v2 (also under --dry-run)
781
+ using a token from --circle-token, $CIRCLE_TOKEN, $CIRCLECI_TOKEN,
782
+ $CIRCLE_CLI_TOKEN, $CIRCLECI_CLI_TOKEN, or ~/.circleci/cli.yml (in that order).
783
+ Explicit UUIDs override auto-resolution; with all three, no token is needed.
784
+ --file selects the pipeline definition by config path or name (needed
785
+ when the project has more than one); it is not passed to npm trust.
586
786
 
587
787
  Permissions (at least one required):
588
788
  --allow-publish allow npm publish
589
789
  --allow-stage-publish allow npm stage publish
590
790
 
591
791
  Options:
592
- --dry-run print the npm trust commands without executing them
792
+ --dry-run print the npm commands without executing them
593
793
  --only <name-glob> only include packages matching (repeatable)
594
794
  --exclude <name-glob> exclude packages matching (repeatable)
595
- -y, --yes skip the confirmation prompt (and npm's)
795
+ globs match the whole name; * also matches "/"
796
+ -y, --yes skip the confirmation prompt (and npm's);
797
+ required when stdin is not a terminal
596
798
  -h, --help show this help
597
799
  -v, --version show the trustci version
598
800
 
599
801
  Workspaces are detected from pnpm-workspace.yaml, package.json "workspaces",
600
802
  or lerna.json. If no workspace config is found, the root package.json is used
601
803
  as a single-package fallback. Private packages ("private": true) are always excluded.
804
+ On a real run, packages not yet published to the registry are skipped.
602
805
 
603
806
  Examples:
604
807
  trustci --dry-run --provider github --repo me/repo --file release.yml --allow-publish
605
808
  trustci --provider gitlab --project me/repo --file .gitlab-ci.yml --allow-publish -y
606
- trustci --provider circleci --vcs-origin github.com/me/repo \\
607
- --circle-token $CIRCLECI_TOKEN --allow-publish
809
+ CIRCLE_TOKEN=... trustci --provider circleci --allow-publish
810
+ trustci --provider circleci --file .circleci/config.yml --allow-publish
608
811
  trustci --provider circleci --org-id <uuid> --project-id <uuid> \\
609
812
  --pipeline-definition-id <uuid> --vcs-origin github.com/me/repo --allow-publish
610
813
  trustci list
814
+
815
+ Requires npm >= 11.15.0 and Node >= 22.14.0.
816
+ Documentation: https://trustci.hidetaka.dev
611
817
  `;
612
818
  async function confirm(question) {
613
819
  const rl = createInterface({ input: process2.stdin, output: process2.stderr });
@@ -655,6 +861,9 @@ async function runTrust(parsed) {
655
861
  const candidates = filterPackages(all, parsed.only, parsed.exclude);
656
862
  if (candidates.length === 0) {
657
863
  console.error(`No public packages matched (out of ${all.length} discovered via ${source}).`);
864
+ for (const pkg of all) {
865
+ console.error(` - ${pkg.name}${pkg.private ? " (private)" : ""}`);
866
+ }
658
867
  return 1;
659
868
  }
660
869
  console.error(`Detected ${all.length} package(s) via ${source}; ${candidates.length} public after filters.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trustci",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "Bulk-register npm Trusted Publishers (npm trust) across all public packages in a monorepo. Supports GitHub, GitLab and CircleCI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,7 +11,7 @@
11
11
  ],
12
12
  "engines": {
13
13
  "node": ">=22.14.0",
14
- "npm": ">=11.5.1"
14
+ "npm": ">=11.15.0"
15
15
  },
16
16
  "scripts": {
17
17
  "build": "bun build src/cli.ts --target node --outfile dist/cli.js",