@gjsify/cli 0.15.1 → 0.16.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.
@@ -0,0 +1,453 @@
1
+ // `gjsify onboard [--dry-run] [--otp <code>] [--registry <url>] [--json] [--yes]`
2
+ //
3
+ // One command that ensures EVERY publishable `@gjsify/*` workspace is both
4
+ // PUBLISHED on npm AND has a Trusted Publisher configured for this repo's
5
+ // `release.yml`. It does the minimum work — publishing/trusting only what's
6
+ // missing — and folds the whole manual bootstrap sequence documented in
7
+ // AGENTS.md ("New @gjsify/* package: first-publish + Trusted Publisher
8
+ // bootstrap") into a single idempotent sweep:
9
+ //
10
+ // 1. Auth gate — `whoami` first. If the token is live, proceed without asking
11
+ // for credentials; only when it is dead/missing do we run the `gjsify
12
+ // login` flow (unless `--yes` on a non-TTY, which fails clearly).
13
+ // 2. Enumerate publishable workspaces (non-private, excluding `@girs/*`).
14
+ // 3. Determine each package's state CONCURRENTLY (bounded) by reading npm's
15
+ // Trusted-Publisher config: unpublished (404) / published-but-untrusted /
16
+ // published-and-trusted. Reads never prompt for 2FA.
17
+ // 4. Act on the gaps, minimally: build+publish+trust an unpublished package;
18
+ // trust a published-but-untrusted one; skip an already-done one.
19
+ // 5. Share ONE OTP across every publish + trust operation — the cached code
20
+ // is tried before prompting, so a whole sweep of N packages typically
21
+ // needs the user to type an OTP only once (see utils/npm-otp.ts).
22
+ //
23
+ // Re-running when everything is already published + trusted does nothing and
24
+ // exits 0. `--dry-run` reports the plan and changes nothing.
25
+ import { spawn, spawnSync } from 'node:child_process';
26
+ import { DEFAULT_REGISTRY, buildHeaders, registryFor, whoami } from '@gjsify/npm-registry';
27
+ import { discoverWorkspaces, filterWorkspaces } from '@gjsify/workspace';
28
+ import { hasAnyCredential, loadNpmrc } from '../utils/load-npmrc.js';
29
+ import { OtpProvider } from '../utils/npm-otp.js';
30
+ import { publishWorkspace } from './publish.js';
31
+ import { createTrustRequester } from './trust.js';
32
+ import { runLogin, LoginError } from './login.js';
33
+ import { detectPackageManager } from './workspace.js';
34
+ import { findWorkspaceRoot } from '../utils/workspace-root.js';
35
+ import { classifyTrustList, githubTrustBody, normalizeWorkflowFile, parseRepoFromGitRemote, trustUrl, validateRepository, } from '../utils/trust-registry.js';
36
+ export const onboardCommand = {
37
+ command: 'onboard',
38
+ description: 'Ensure every publishable @gjsify/* workspace is both published on npm and has a Trusted Publisher configured — publishing/trusting only what is missing, one shared OTP across the whole sweep. Idempotent.',
39
+ builder: (yargs) => yargs
40
+ .option('repository', {
41
+ description: 'GitHub repo as `owner/repo`. Default: inferred from the `origin` git remote.',
42
+ type: 'string',
43
+ })
44
+ .option('workflow', {
45
+ description: 'Workflow filename allowed to publish via OIDC (basename only).',
46
+ type: 'string',
47
+ default: 'release.yml',
48
+ })
49
+ .option('environment', {
50
+ description: 'Optional GitHub Actions environment the workflow must run in.',
51
+ type: 'string',
52
+ })
53
+ .option('registry', {
54
+ description: `Registry override. Default: scope-aware lookup from .npmrc (falls back to ${DEFAULT_REGISTRY}).`,
55
+ type: 'string',
56
+ })
57
+ .option('otp', {
58
+ description: 'npm 2FA one-time code, used as the initial shared code. Prompted on demand (once) if omitted and a challenge occurs.',
59
+ type: 'string',
60
+ })
61
+ .option('concurrency', {
62
+ description: 'How many packages to probe (read state) in parallel.',
63
+ type: 'number',
64
+ default: 8,
65
+ })
66
+ .option('dry-run', {
67
+ description: 'Report the plan (what would be published / trusted) without changing anything.',
68
+ type: 'boolean',
69
+ default: false,
70
+ })
71
+ .option('json', {
72
+ description: 'Emit a machine-readable summary object as the final line of stdout.',
73
+ type: 'boolean',
74
+ default: false,
75
+ })
76
+ .option('yes', {
77
+ description: 'Non-interactive: never prompt. Fail clearly if a login or an OTP is required and not supplied via flags.',
78
+ type: 'boolean',
79
+ default: false,
80
+ }),
81
+ handler: async (args) => {
82
+ const cwd = process.cwd();
83
+ const dryRun = args['dry-run'] === true;
84
+ const asJson = args.json === true;
85
+ const nonInteractive = args.yes === true;
86
+ const log = (msg = '') => {
87
+ // In --json mode keep stdout clean for the final summary object;
88
+ // route progress to stderr.
89
+ if (asJson)
90
+ process.stderr.write(`${msg}\n`);
91
+ else
92
+ process.stdout.write(`${msg}\n`);
93
+ };
94
+ let npmrc = await loadNpmrc(cwd);
95
+ const defaultRegistry = args.registry ?? process.env.npm_config_registry ?? npmrc.registry ?? DEFAULT_REGISTRY;
96
+ // 1. Auth gate — whoami first; only login when the token is dead/missing.
97
+ let username;
98
+ try {
99
+ username = await ensureAuthenticated({
100
+ npmrc,
101
+ registry: defaultRegistry,
102
+ cwd,
103
+ nonInteractive,
104
+ dryRun,
105
+ otp: args.otp,
106
+ log,
107
+ });
108
+ }
109
+ catch (err) {
110
+ const msg = err instanceof Error ? err.message : String(err);
111
+ if (asJson)
112
+ process.stdout.write(`${JSON.stringify({ ok: false, error: 'auth', message: msg })}\n`);
113
+ else
114
+ console.error(msg);
115
+ process.exit(1);
116
+ return;
117
+ }
118
+ // Login may have written a fresh token — reload so the probes/ops use it.
119
+ npmrc = await loadNpmrc(cwd);
120
+ if (username)
121
+ log(`gjsify onboard: authenticated as ${username}`);
122
+ // Repository: flag wins, else infer from the origin remote.
123
+ let repository = args.repository;
124
+ if (!repository) {
125
+ const r = spawnSync('git', ['remote', 'get-url', 'origin'], { cwd, encoding: 'utf8' });
126
+ if (r.status === 0)
127
+ repository = parseRepoFromGitRemote(r.stdout) ?? undefined;
128
+ }
129
+ if (!repository || !validateRepository(repository)) {
130
+ const msg = 'gjsify onboard: could not determine the GitHub repository — pass --repository owner/repo.';
131
+ if (asJson)
132
+ process.stdout.write(`${JSON.stringify({ ok: false, error: 'repository', message: msg })}\n`);
133
+ else
134
+ console.error(msg);
135
+ process.exit(1);
136
+ return;
137
+ }
138
+ const workflow = normalizeWorkflowFile(args.workflow);
139
+ const environment = args.environment;
140
+ // 2. Enumerate publishable workspaces (non-private, exclude @girs/*).
141
+ const root = findWorkspaceRoot(cwd) ?? cwd;
142
+ const all = discoverWorkspaces(root, { includeRoot: true });
143
+ const selected = filterWorkspaces(all, { noPrivate: true, exclude: ['@girs/*'] }).filter((ws) => ws.name);
144
+ if (selected.length === 0) {
145
+ const msg = 'gjsify onboard: no publishable workspace packages found.';
146
+ if (asJson)
147
+ process.stdout.write(`${JSON.stringify({ ok: false, error: 'no-packages', message: msg })}\n`);
148
+ else
149
+ console.error(msg);
150
+ process.exit(1);
151
+ return;
152
+ }
153
+ log(`gjsify onboard: ${selected.length} package(s) | repo=${repository} workflow=${workflow}`);
154
+ if (dryRun)
155
+ log('(dry-run — nothing will be published or configured)');
156
+ log();
157
+ // 3. Determine per-package state CONCURRENTLY. Reads use a plain
158
+ // authenticated GET (no OTP prompting — a 401 becomes `auth-required`).
159
+ const plans = await mapWithConcurrency(selected, Math.max(1, args.concurrency), (ws) => probeTrustState(ws, {
160
+ registryOverride: args.registry,
161
+ npmrc,
162
+ repository,
163
+ workflow,
164
+ }));
165
+ // Summary table (published✓ / trusted✓ vs work to do).
166
+ const toPublish = plans.filter((p) => p.action === 'publish-and-trust');
167
+ const toTrust = plans.filter((p) => p.action === 'trust');
168
+ const done = plans.filter((p) => p.action === 'skip');
169
+ const blocked = plans.filter((p) => p.action === 'blocked');
170
+ for (const p of plans)
171
+ log(` ${statusCell(p)} ${p.ws.name}`);
172
+ log();
173
+ log(`Plan: ${done.length} already done, ${toPublish.length} to publish+trust, ${toTrust.length} to trust, ${blocked.length} unreadable.`);
174
+ const results = [];
175
+ if (dryRun) {
176
+ for (const p of plans) {
177
+ results.push({
178
+ name: p.ws.name,
179
+ result: p.action === 'skip' ? 'already-done' : p.action === 'blocked' ? 'failed' : 'skipped-dry-run',
180
+ detail: p.action === 'publish-and-trust'
181
+ ? 'would publish + trust'
182
+ : p.action === 'trust'
183
+ ? 'would trust'
184
+ : undefined,
185
+ });
186
+ }
187
+ finish(results, plans, { asJson, dryRun });
188
+ return;
189
+ }
190
+ // 4. Act on the gaps, minimally. Publishes + trust POSTs run SERIALLY so
191
+ // the shared OTP is prompted at most once (and reused everywhere).
192
+ const otpProvider = new OtpProvider(args.otp, nonInteractive ? async () => '' : undefined);
193
+ const trustRequest = createTrustRequester({ npmrc, otpProvider });
194
+ const trustBody = githubTrustBody({ repository, workflow, environment });
195
+ let failed = 0;
196
+ for (const p of plans) {
197
+ if (p.action === 'skip') {
198
+ results.push({ name: p.ws.name, result: 'already-done' });
199
+ continue;
200
+ }
201
+ if (p.action === 'blocked') {
202
+ log(`→ ${p.ws.name}: cannot read trust state (HTTP ${p.httpStatus}) — skipped`);
203
+ results.push({ name: p.ws.name, result: 'failed', detail: `state HTTP ${p.httpStatus}` });
204
+ failed++;
205
+ continue;
206
+ }
207
+ let publishedNow = false;
208
+ if (p.action === 'publish-and-trust') {
209
+ log(`→ ${p.ws.name}: building + publishing…`);
210
+ try {
211
+ await buildIfPresent(p.ws, log);
212
+ }
213
+ catch (err) {
214
+ const msg = err instanceof Error ? err.message : String(err);
215
+ log(` build failed: ${msg}`);
216
+ results.push({ name: p.ws.name, result: 'failed', detail: `build: ${msg}` });
217
+ failed++;
218
+ continue;
219
+ }
220
+ const pub = await publishWorkspace({
221
+ wsDir: p.ws.location,
222
+ tag: 'latest',
223
+ access: 'public',
224
+ provenance: false,
225
+ tolerate: true, // a concurrent/racey publish is harmless
226
+ tolerateUntrustedNew: false,
227
+ trustedFlag: undefined,
228
+ registry: p.registry,
229
+ verbose: false,
230
+ json: false,
231
+ otpProvider,
232
+ seedOtpFirst: false,
233
+ });
234
+ if (!pub.ok) {
235
+ const detail = describePublishFailure(pub);
236
+ log(` publish failed: ${detail}`);
237
+ results.push({ name: p.ws.name, result: 'failed', detail: `publish: ${detail}` });
238
+ failed++;
239
+ continue;
240
+ }
241
+ publishedNow = pub.action !== 'skipped-untrusted-new';
242
+ log(` published ${pub.name}@${'version' in pub ? pub.version : ''}`);
243
+ }
244
+ // Configure the Trusted Publisher.
245
+ const created = await trustRequest('POST', p.url, trustBody);
246
+ if (created.status >= 200 && created.status < 300) {
247
+ log(` trusted ${p.ws.name}`);
248
+ results.push({
249
+ name: p.ws.name,
250
+ result: publishedNow ? 'published+trusted' : 'trusted',
251
+ });
252
+ }
253
+ else if (created.status === 409) {
254
+ results.push({ name: p.ws.name, result: publishedNow ? 'published+trusted' : 'trusted' });
255
+ }
256
+ else if (created.status === 404) {
257
+ // Freshly-published package not yet provisioned for trust config.
258
+ log(` ${p.ws.name}: published but not yet trust-configurable (re-run onboard)`);
259
+ results.push({
260
+ name: p.ws.name,
261
+ result: publishedNow ? 'published' : 'failed',
262
+ detail: 'trust config deferred (404)',
263
+ });
264
+ if (!publishedNow)
265
+ failed++;
266
+ }
267
+ else {
268
+ log(` trust failed (HTTP ${created.status})`);
269
+ results.push({
270
+ name: p.ws.name,
271
+ result: 'failed',
272
+ detail: `trust HTTP ${created.status}`,
273
+ });
274
+ failed++;
275
+ }
276
+ }
277
+ finish(results, plans, { asJson, dryRun, failed });
278
+ },
279
+ };
280
+ /** Print the summary + exit with the right code. */
281
+ function finish(results, plans, opts) {
282
+ const failed = opts.failed ?? 0;
283
+ if (opts.asJson) {
284
+ process.stdout.write(`${JSON.stringify({
285
+ ok: failed === 0,
286
+ dryRun: opts.dryRun,
287
+ total: plans.length,
288
+ published: results.filter((r) => r.result === 'published' || r.result === 'published+trusted')
289
+ .length,
290
+ trusted: results.filter((r) => r.result === 'trusted' || r.result === 'published+trusted').length,
291
+ alreadyDone: results.filter((r) => r.result === 'already-done').length,
292
+ failed,
293
+ results,
294
+ }, null, 2)}\n`);
295
+ }
296
+ else {
297
+ const line = opts.dryRun
298
+ ? 'Dry-run complete — nothing changed.'
299
+ : `Done: ${results.filter((r) => r.result.includes('published')).length} published, ` +
300
+ `${results.filter((r) => r.result === 'trusted' || r.result === 'published+trusted').length} trusted, ` +
301
+ `${results.filter((r) => r.result === 'already-done').length} already done, ${failed} failed.`;
302
+ process.stdout.write(`\n${line}\n`);
303
+ }
304
+ process.exit(failed > 0 ? 1 : 0);
305
+ }
306
+ /** A compact per-package status cell for the summary table. */
307
+ function statusCell(p) {
308
+ switch (p.action) {
309
+ case 'skip':
310
+ return 'published✓ trusted✓';
311
+ case 'trust':
312
+ return 'published✓ trust… ';
313
+ case 'publish-and-trust':
314
+ return 'publish… trust… ';
315
+ case 'blocked':
316
+ return `unreadable (${p.httpStatus})`;
317
+ }
318
+ }
319
+ /**
320
+ * Run the whoami liveness check, and only run the login flow when the token is
321
+ * dead/missing. Returns the authenticated username. Throws on a non-interactive
322
+ * gate that would need input.
323
+ */
324
+ async function ensureAuthenticated(ctx) {
325
+ const { npmrc, registry, cwd, nonInteractive, dryRun, otp, log } = ctx;
326
+ // A live token short-circuits — no credential prompt.
327
+ if (hasAnyCredential(npmrc)) {
328
+ try {
329
+ const who = await whoami(registry, npmrc);
330
+ if (who.username)
331
+ return who.username;
332
+ }
333
+ catch {
334
+ /* fall through to the login path */
335
+ }
336
+ }
337
+ // Token dead or missing.
338
+ if (dryRun) {
339
+ log('gjsify onboard: no live npm token — dry-run continues (state reads may be limited).');
340
+ return undefined;
341
+ }
342
+ if (nonInteractive) {
343
+ throw new Error('gjsify onboard: not authenticated and --yes was passed — run `gjsify login` first (or omit --yes to log in interactively).');
344
+ }
345
+ log('gjsify onboard: no live npm token — logging in…');
346
+ try {
347
+ const res = await runLogin({ registry, otp, cwd });
348
+ return res.username;
349
+ }
350
+ catch (err) {
351
+ if (err instanceof LoginError)
352
+ throw new Error(err.message);
353
+ throw err;
354
+ }
355
+ }
356
+ /**
357
+ * Read a package's current published + trust state via a plain authenticated
358
+ * GET on its trust config (no OTP prompting). 404 → unpublished; a matching
359
+ * github entry → trusted; otherwise → published-but-untrusted.
360
+ */
361
+ async function probeTrustState(ws, ctx) {
362
+ const registry = ctx.registryOverride ?? registryFor(ws.name, ctx.npmrc) ?? DEFAULT_REGISTRY;
363
+ const url = trustUrl(registry, ws.name);
364
+ let state = 'error';
365
+ let httpStatus = 0;
366
+ try {
367
+ const headers = buildHeaders(url, { npmrc: ctx.npmrc });
368
+ headers['accept'] = 'application/json';
369
+ const res = await fetch(url, { method: 'GET', headers });
370
+ httpStatus = res.status;
371
+ const text = await res.text().catch(() => '');
372
+ let json = undefined;
373
+ try {
374
+ json = text ? JSON.parse(text) : undefined;
375
+ }
376
+ catch {
377
+ /* non-JSON body */
378
+ }
379
+ state = classifyTrustList(res.status, json, { repository: ctx.repository, workflow: ctx.workflow });
380
+ }
381
+ catch {
382
+ state = 'error';
383
+ }
384
+ let action;
385
+ switch (state) {
386
+ case 'trusted':
387
+ action = 'skip';
388
+ break;
389
+ case 'untrusted':
390
+ action = 'trust';
391
+ break;
392
+ case 'unpublished':
393
+ action = 'publish-and-trust';
394
+ break;
395
+ default:
396
+ action = 'blocked'; // auth-required / error — can't determine
397
+ }
398
+ return { ws, registry, url, state, httpStatus, action };
399
+ }
400
+ /** Run the package's `build` script (if it declares one) before publishing. */
401
+ async function buildIfPresent(ws, log) {
402
+ const scripts = ws.manifest.scripts ?? {};
403
+ if (typeof scripts.build !== 'string')
404
+ return; // nothing to build
405
+ const pm = detectPackageManager();
406
+ const argv = ['run', 'build'];
407
+ log(` $ ${pm} ${argv.join(' ')}`);
408
+ await new Promise((resolve, reject) => {
409
+ const child = spawn(pm, argv, {
410
+ cwd: ws.location,
411
+ stdio: 'inherit',
412
+ env: { ...process.env },
413
+ });
414
+ child.on('close', (code) => {
415
+ if (code === 0)
416
+ resolve();
417
+ else
418
+ reject(new Error(`build exited with code ${code}`));
419
+ });
420
+ child.on('error', reject);
421
+ });
422
+ }
423
+ /** Human-readable reason for a failed publish outcome. */
424
+ function describePublishFailure(pub) {
425
+ switch (pub.action) {
426
+ case 'otp-required':
427
+ return 'npm required a 2FA code (re-run with --otp, or on a TTY)';
428
+ case 'oidc-failed':
429
+ case 'oidc-no-token':
430
+ return 'OIDC/token auth failed';
431
+ case 'diagnostic':
432
+ return `HTTP 404 (${pub.diag.reason})`;
433
+ case 'error':
434
+ return `HTTP ${pub.status} ${pub.statusText}`;
435
+ default:
436
+ return 'unknown';
437
+ }
438
+ }
439
+ /** Map with bounded concurrency, preserving input order in the result. */
440
+ async function mapWithConcurrency(items, limit, fn) {
441
+ const results = Array.from({ length: items.length });
442
+ let next = 0;
443
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
444
+ while (true) {
445
+ const i = next++;
446
+ if (i >= items.length)
447
+ return;
448
+ results[i] = await fn(items[i]);
449
+ }
450
+ });
451
+ await Promise.all(workers);
452
+ return results;
453
+ }
@@ -172,8 +172,18 @@ export async function packWorkspace(wsDir, opts = {}) {
172
172
  * Walk the workspace and return the list of files to include in the tarball,
173
173
  * relative to `wsDir`. Mirrors npm pack's selection rules:
174
174
  *
175
- * - If pkg.files exists: use it as an allowlist (with .npmignore as a
176
- * blacklist within those globs)
175
+ * - If pkg.files exists: the array is an ALLOWLIST that overrides
176
+ * .gitignore / .npmignore entirely. Every file under an allowlisted
177
+ * path/dir ships from disk; only the hardcoded implicit-exclusion set
178
+ * (node_modules, .git, lockfiles, … — applied by `walkAll` /
179
+ * `expandFilesPatterns`) and npm's always-ignore names are dropped.
180
+ * This matches `npm pack`: a `files` entry re-includes a path even when
181
+ * .gitignore/.npmignore would drop it (verified against `npm pack
182
+ * --dry-run`). Trimming `files`-allowlisted content by .gitignore
183
+ * silently dropped build-generated-but-allowlisted files like
184
+ * adwaita-web's `src/styles.generated.ts` (gitignored, allowlisted via
185
+ * `files: ["src"]`) — shipping a broken tarball whose `src/index.ts`
186
+ * imported a file the tarball didn't contain.
177
187
  * - Otherwise: walk everything, apply .npmignore + .gitignore, drop the
178
188
  * implicit-exclusion set (node_modules, .git, …)
179
189
  *
@@ -185,18 +195,22 @@ function collectFiles(wsDir, pkg) {
185
195
  const filesField = Array.isArray(pkg.files)
186
196
  ? pkg.files.filter((f) => typeof f === 'string')
187
197
  : null;
188
- let candidates;
198
+ const out = new Set();
189
199
  if (filesField) {
190
- candidates = expandFilesPatterns(wsDir, filesField);
200
+ // A `files` allowlist overrides .gitignore/.npmignore — ship every
201
+ // allowlisted path from disk. `expandFilesPatterns` already excludes
202
+ // the NEVER_INCLUDED_BASENAMES set (node_modules, .git, lockfiles, …)
203
+ // via `walkAll`, so junk is still kept out.
204
+ for (const f of expandFilesPatterns(wsDir, filesField))
205
+ out.add(f);
191
206
  }
192
207
  else {
193
- candidates = walkAll(wsDir);
194
- }
195
- const ignore = loadIgnore(wsDir);
196
- const out = new Set();
197
- for (const f of candidates) {
198
- if (!ignore(f))
199
- out.add(f);
208
+ // No `files` field — walk everything and apply .npmignore/.gitignore.
209
+ const ignore = loadIgnore(wsDir);
210
+ for (const f of walkAll(wsDir)) {
211
+ if (!ignore(f))
212
+ out.add(f);
213
+ }
200
214
  }
201
215
  for (const f of always) {
202
216
  if (existsSync(join(wsDir, f)))
@@ -1,4 +1,6 @@
1
1
  import type { Command } from '../types/index.js';
2
+ import { type Diagnose404Result } from '../utils/publish-diagnose.js';
3
+ import { OtpProvider } from '../utils/npm-otp.js';
2
4
  interface PublishOptions {
3
5
  path?: string;
4
6
  tag?: string;
@@ -13,4 +15,94 @@ interface PublishOptions {
13
15
  'check-trusted'?: boolean;
14
16
  }
15
17
  export declare const publishCommand: Command<unknown, PublishOptions>;
18
+ /** Input to {@link publishWorkspace} — the token-auth-aware, OTP-shareable publish flow. */
19
+ export interface PublishWorkspaceInput {
20
+ /** Workspace directory to pack + publish. */
21
+ wsDir: string;
22
+ tag: string;
23
+ access?: string;
24
+ provenance: boolean;
25
+ /** Treat "already published" as success. */
26
+ tolerate: boolean;
27
+ /** Skip an un-bootstrapped new package on the OIDC path. */
28
+ tolerateUntrustedNew: boolean;
29
+ /** `--trusted` (force OIDC) / `undefined` | `'auto'` (auto-detect). */
30
+ trustedFlag: boolean | 'auto' | undefined;
31
+ /** Registry override. Default: `$npm_config_registry` → scope-aware npmrc → npmjs. */
32
+ registry?: string;
33
+ verbose: boolean;
34
+ /** Redirect lifecycle-script stdout → stderr so `--json` stdout stays clean. */
35
+ json: boolean;
36
+ /** Shared OTP provider — inject ONE across many packages to reuse a single code. */
37
+ otpProvider: OtpProvider;
38
+ /** Send the cached code on the first PUT (publish `--otp`). */
39
+ seedOtpFirst: boolean;
40
+ }
41
+ /** Discriminated result of {@link publishWorkspace}; the caller owns presentation. */
42
+ export type PublishOutcome = {
43
+ ok: true;
44
+ action: 'published';
45
+ name: string;
46
+ version: string;
47
+ filename: string;
48
+ size: number;
49
+ integrity: string;
50
+ tag: string;
51
+ registry: string;
52
+ } | {
53
+ ok: true;
54
+ action: 'republish-tolerated';
55
+ name: string;
56
+ version: string;
57
+ status: number;
58
+ } | {
59
+ ok: true;
60
+ action: 'skipped-untrusted-new';
61
+ name: string;
62
+ version: string;
63
+ } | {
64
+ ok: false;
65
+ action: 'otp-required';
66
+ name: string;
67
+ version: string;
68
+ status: number;
69
+ } | {
70
+ ok: false;
71
+ action: 'oidc-failed';
72
+ name: string;
73
+ version: string;
74
+ error: unknown;
75
+ } | {
76
+ ok: false;
77
+ action: 'oidc-no-token';
78
+ name: string;
79
+ version: string;
80
+ error: unknown;
81
+ } | {
82
+ ok: false;
83
+ action: 'diagnostic';
84
+ name: string;
85
+ version: string;
86
+ status: number;
87
+ diag: Diagnose404Result;
88
+ } | {
89
+ ok: false;
90
+ action: 'error';
91
+ name: string;
92
+ version: string;
93
+ status: number;
94
+ statusText: string;
95
+ text: string;
96
+ };
97
+ /**
98
+ * Pack + PUT a single workspace to its npm registry, returning a structured
99
+ * outcome instead of writing/exiting. Extracted from the `publish` command so
100
+ * `gjsify onboard` can drive the SAME publish flow while sharing one
101
+ * {@link OtpProvider} across every package in a sweep.
102
+ *
103
+ * The OTP dance runs through `withOtpRetry(provider)`: the cached code is tried
104
+ * before prompting, so a shared provider needs only one interactive entry for N
105
+ * packages.
106
+ */
107
+ export declare function publishWorkspace(input: PublishWorkspaceInput): Promise<PublishOutcome>;
16
108
  export {};