putitoutthere 0.1.8 → 0.1.10

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/dist/auth.d.ts ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `auth login/logout/status` — OAuth Device Flow against the
3
+ * `putitoutthere-cli` GitHub App (client ID committed to
4
+ * docs/guide/auth.md).
5
+ *
6
+ * Scope: optional. The rest of the CLI authenticates to registries
7
+ * via env vars; this subcommand exists so `token list` (and later
8
+ * features) can read repo-secret metadata from GitHub on behalf of
9
+ * the user. If `auth login` has never been run, those features
10
+ * degrade gracefully — see #98.
11
+ *
12
+ * Token values are never logged or written to stdout. They flow
13
+ * through the `Keyring` abstraction (see `src/keyring.ts`); in logs
14
+ * a token is identified by the first 8 hex chars of its SHA-256.
15
+ */
16
+ import { type Keyring } from './keyring.js';
17
+ /** Public client ID of the `putitoutthere-cli` GitHub App (#104). */
18
+ export declare const CLIENT_ID = "Iv23lio0NtN1koa0Rwle";
19
+ export interface LoginOptions {
20
+ /** Override the App client ID. Tests inject a fake. */
21
+ clientId?: string;
22
+ fetchFn?: typeof fetch;
23
+ keyring?: Keyring;
24
+ /** Epoch-seconds clock. Defaults to `Date.now()/1000`. */
25
+ now?: () => number;
26
+ /** Awaitable delay between polls. Defaults to real `setTimeout`. */
27
+ sleep?: (ms: number) => Promise<void>;
28
+ /**
29
+ * Surface the verification prompt to the user. Callers that want
30
+ * human-readable output (the CLI) provide one that writes to stderr;
31
+ * tests pass a capture. Defaults to a no-op.
32
+ */
33
+ onPrompt?: (prompt: DevicePrompt) => void;
34
+ }
35
+ export interface DevicePrompt {
36
+ userCode: string;
37
+ verificationUri: string;
38
+ /** Raw seconds-from-now until the device_code expires. */
39
+ expiresInSeconds: number;
40
+ }
41
+ export interface LoginResult {
42
+ account: string;
43
+ /** Epoch seconds at which the *access* token expires. */
44
+ expiresAt: number;
45
+ }
46
+ export interface LogoutOptions {
47
+ keyring?: Keyring;
48
+ }
49
+ export interface StatusOptions {
50
+ fetchFn?: typeof fetch;
51
+ keyring?: Keyring;
52
+ now?: () => number;
53
+ }
54
+ export type StatusResult = {
55
+ authenticated: true;
56
+ account: string;
57
+ expiresAt: number;
58
+ } | {
59
+ authenticated: false;
60
+ reason: 'not_logged_in' | 'refresh_failed' | 'revoked';
61
+ message: string;
62
+ };
63
+ export declare function login(opts?: LoginOptions): Promise<LoginResult>;
64
+ export declare function logout(opts?: LogoutOptions): Promise<{
65
+ wiped: boolean;
66
+ }>;
67
+ export declare function status(opts?: StatusOptions): Promise<StatusResult>;
68
+ /** SHA-256 prefix for identifying a token in logs without leaking it. */
69
+ export declare function tokenDigest(token: string): string;
70
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,EAAkB,KAAK,OAAO,EAAmB,MAAM,cAAc,CAAC;AAE7E,qEAAqE;AACrE,eAAO,MAAM,SAAS,yBAAyB,CAAC;AAehD,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0DAA0D;IAC1D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,oEAAoE;IACpE,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAC;CAC3C;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,0DAA0D;IAC1D,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,aAAa,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC3D;IACE,aAAa,EAAE,KAAK,CAAC;IACrB,MAAM,EAAE,eAAe,GAAG,gBAAgB,GAAG,SAAS,CAAC;IACvD,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAIN,wBAAsB,KAAK,CAAC,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CA6BzE;AAID,wBAAsB,MAAM,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC,CAMlF;AAID,wBAAsB,MAAM,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAmE5E;AAsMD,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjD"}
package/dist/auth.js ADDED
@@ -0,0 +1,290 @@
1
+ /**
2
+ * `auth login/logout/status` — OAuth Device Flow against the
3
+ * `putitoutthere-cli` GitHub App (client ID committed to
4
+ * docs/guide/auth.md).
5
+ *
6
+ * Scope: optional. The rest of the CLI authenticates to registries
7
+ * via env vars; this subcommand exists so `token list` (and later
8
+ * features) can read repo-secret metadata from GitHub on behalf of
9
+ * the user. If `auth login` has never been run, those features
10
+ * degrade gracefully — see #98.
11
+ *
12
+ * Token values are never logged or written to stdout. They flow
13
+ * through the `Keyring` abstraction (see `src/keyring.ts`); in logs
14
+ * a token is identified by the first 8 hex chars of its SHA-256.
15
+ */
16
+ import { createHash } from 'node:crypto';
17
+ import { defaultKeyring } from './keyring.js';
18
+ /** Public client ID of the `putitoutthere-cli` GitHub App (#104). */
19
+ export const CLIENT_ID = 'Iv23lio0NtN1koa0Rwle';
20
+ const GITHUB_DEVICE_URL = 'https://github.com/login/device/code';
21
+ const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token';
22
+ const GITHUB_API_USER = 'https://api.github.com/user';
23
+ const GRANT_DEVICE = 'urn:ietf:params:oauth:grant-type:device_code';
24
+ const GRANT_REFRESH = 'refresh_token';
25
+ /** Seconds of slack to refresh a token before it actually expires. */
26
+ const REFRESH_SKEW_SECONDS = 60;
27
+ /** Bumped when GitHub returns `slow_down`. */
28
+ const SLOW_DOWN_BUMP_SECONDS = 5;
29
+ // ---- login -------------------------------------------------------------
30
+ export async function login(opts = {}) {
31
+ const clientId = opts.clientId ?? CLIENT_ID;
32
+ const fetchFn = opts.fetchFn ?? fetch;
33
+ const keyring = opts.keyring ?? defaultKeyring();
34
+ const now = opts.now ?? epochSeconds;
35
+ const sleep = opts.sleep ?? realSleep;
36
+ const onPrompt = opts.onPrompt ?? (() => undefined);
37
+ const device = await requestDeviceCode(fetchFn, clientId);
38
+ onPrompt({
39
+ userCode: device.user_code,
40
+ verificationUri: device.verification_uri,
41
+ expiresInSeconds: device.expires_in,
42
+ });
43
+ const tokens = await pollForToken(fetchFn, sleep, clientId, device);
44
+ const account = await fetchLogin(fetchFn, tokens.access_token);
45
+ const issuedAt = now();
46
+ const stored = {
47
+ account,
48
+ access_token: tokens.access_token,
49
+ refresh_token: tokens.refresh_token,
50
+ access_token_expires_at: issuedAt + tokens.expires_in,
51
+ refresh_token_expires_at: issuedAt + tokens.refresh_token_expires_in,
52
+ };
53
+ await keyring.set(stored);
54
+ return { account, expiresAt: stored.access_token_expires_at };
55
+ }
56
+ // ---- logout ------------------------------------------------------------
57
+ export async function logout(opts = {}) {
58
+ const keyring = opts.keyring ?? defaultKeyring();
59
+ const existing = await keyring.get();
60
+ if (!existing)
61
+ return { wiped: false };
62
+ await keyring.delete();
63
+ return { wiped: true };
64
+ }
65
+ // ---- status ------------------------------------------------------------
66
+ export async function status(opts = {}) {
67
+ const fetchFn = opts.fetchFn ?? fetch;
68
+ const keyring = opts.keyring ?? defaultKeyring();
69
+ const now = opts.now ?? epochSeconds;
70
+ const stored = await keyring.get();
71
+ if (!stored) {
72
+ return {
73
+ authenticated: false,
74
+ reason: 'not_logged_in',
75
+ message: 'Not logged in. Run `putitoutthere auth login`.',
76
+ };
77
+ }
78
+ let working = stored;
79
+ const soonExpired = now() >= stored.access_token_expires_at - REFRESH_SKEW_SECONDS;
80
+ if (soonExpired) {
81
+ const refreshed = await tryRefresh(fetchFn, keyring, working, now);
82
+ if (!refreshed) {
83
+ return {
84
+ authenticated: false,
85
+ reason: 'refresh_failed',
86
+ message: 'Session expired. Run `putitoutthere auth login`.',
87
+ };
88
+ }
89
+ working = refreshed;
90
+ }
91
+ const probe = await probeUser(fetchFn, working.access_token);
92
+ if (probe.ok) {
93
+ return {
94
+ authenticated: true,
95
+ account: probe.login,
96
+ expiresAt: working.access_token_expires_at,
97
+ };
98
+ }
99
+ if (probe.status !== 401) {
100
+ // Network or 5xx — don't wipe credentials over a transient blip.
101
+ return {
102
+ authenticated: false,
103
+ reason: 'refresh_failed',
104
+ message: `GitHub /user probe failed (${probe.status}).`,
105
+ };
106
+ }
107
+ // 401 with a still-unexpired token => credential actually revoked.
108
+ // Try one refresh before giving up, in case the server clock drifted.
109
+ const refreshed = await tryRefresh(fetchFn, keyring, working, now);
110
+ if (!refreshed) {
111
+ return {
112
+ authenticated: false,
113
+ reason: 'revoked',
114
+ message: 'Token rejected by GitHub. Run `putitoutthere auth login`.',
115
+ };
116
+ }
117
+ const reprobe = await probeUser(fetchFn, refreshed.access_token);
118
+ if (!reprobe.ok) {
119
+ return {
120
+ authenticated: false,
121
+ reason: 'revoked',
122
+ message: 'Token rejected by GitHub. Run `putitoutthere auth login`.',
123
+ };
124
+ }
125
+ return {
126
+ authenticated: true,
127
+ account: reprobe.login,
128
+ expiresAt: refreshed.access_token_expires_at,
129
+ };
130
+ }
131
+ async function requestDeviceCode(fetchFn, clientId) {
132
+ const res = await fetchFn(GITHUB_DEVICE_URL, {
133
+ method: 'POST',
134
+ headers: {
135
+ accept: 'application/json',
136
+ 'content-type': 'application/x-www-form-urlencoded',
137
+ },
138
+ body: new URLSearchParams({ client_id: clientId }).toString(),
139
+ });
140
+ if (!res.ok) {
141
+ throw new Error(`device code request failed: HTTP ${res.status}`);
142
+ }
143
+ const body = await res.json();
144
+ /* v8 ignore next 3 -- defensive; GitHub always returns the documented fields on 2xx. */
145
+ if (!isDeviceCodeResponse(body)) {
146
+ throw new Error('device code response missing expected fields');
147
+ }
148
+ return body;
149
+ }
150
+ async function pollForToken(fetchFn, sleep, clientId, device) {
151
+ let intervalSec = device.interval;
152
+ const deadline = Date.now() + device.expires_in * 1000;
153
+ while (Date.now() < deadline) {
154
+ await sleep(intervalSec * 1000);
155
+ const res = await fetchFn(GITHUB_TOKEN_URL, {
156
+ method: 'POST',
157
+ headers: {
158
+ accept: 'application/json',
159
+ 'content-type': 'application/x-www-form-urlencoded',
160
+ },
161
+ body: new URLSearchParams({
162
+ client_id: clientId,
163
+ device_code: device.device_code,
164
+ grant_type: GRANT_DEVICE,
165
+ }).toString(),
166
+ });
167
+ const body = (await res.json());
168
+ if (isTokenResponse(body))
169
+ return body;
170
+ const error = typeof body.error === 'string' ? body.error : 'unknown_error';
171
+ if (error === 'authorization_pending')
172
+ continue;
173
+ if (error === 'slow_down') {
174
+ intervalSec += SLOW_DOWN_BUMP_SECONDS;
175
+ continue;
176
+ }
177
+ if (error === 'access_denied') {
178
+ throw new Error('authorization denied by user');
179
+ }
180
+ if (error === 'expired_token') {
181
+ throw new Error('device code expired before approval');
182
+ }
183
+ /* v8 ignore start -- unknown error codes + wall-clock deadline are both defensive; GitHub's error set is exhaustive and the server's expired_token fires first. */
184
+ throw new Error(`device flow error: ${error}`);
185
+ }
186
+ throw new Error('device code expired before approval');
187
+ }
188
+ /* v8 ignore stop */
189
+ async function tryRefresh(fetchFn, keyring, current, now) {
190
+ const res = await fetchFn(GITHUB_TOKEN_URL, {
191
+ method: 'POST',
192
+ headers: {
193
+ accept: 'application/json',
194
+ 'content-type': 'application/x-www-form-urlencoded',
195
+ },
196
+ body: new URLSearchParams({
197
+ client_id: CLIENT_ID,
198
+ refresh_token: current.refresh_token,
199
+ grant_type: GRANT_REFRESH,
200
+ }).toString(),
201
+ });
202
+ if (!res.ok)
203
+ return null;
204
+ const body = (await res.json());
205
+ if (!isTokenResponse(body))
206
+ return null;
207
+ const issuedAt = now();
208
+ const next = {
209
+ account: current.account,
210
+ access_token: body.access_token,
211
+ refresh_token: body.refresh_token,
212
+ access_token_expires_at: issuedAt + body.expires_in,
213
+ refresh_token_expires_at: issuedAt + body.refresh_token_expires_in,
214
+ };
215
+ await keyring.set(next);
216
+ return next;
217
+ }
218
+ async function fetchLogin(fetchFn, accessToken) {
219
+ const res = await fetchFn(GITHUB_API_USER, {
220
+ headers: {
221
+ authorization: `Bearer ${accessToken}`,
222
+ accept: 'application/vnd.github+json',
223
+ 'user-agent': 'putitoutthere',
224
+ },
225
+ });
226
+ /* v8 ignore next 3 -- fetchLogin runs only after a successful token grant; the happy-path is covered. */
227
+ if (!res.ok) {
228
+ throw new Error(`GitHub /user failed: HTTP ${res.status}`);
229
+ }
230
+ const body = (await res.json());
231
+ /* v8 ignore next 3 -- defensive: GitHub has never shipped /user without a login field. */
232
+ if (typeof body.login !== 'string') {
233
+ throw new Error('GitHub /user response missing login');
234
+ }
235
+ return body.login;
236
+ }
237
+ async function probeUser(fetchFn, accessToken) {
238
+ try {
239
+ const res = await fetchFn(GITHUB_API_USER, {
240
+ headers: {
241
+ authorization: `Bearer ${accessToken}`,
242
+ accept: 'application/vnd.github+json',
243
+ 'user-agent': 'putitoutthere',
244
+ },
245
+ });
246
+ if (!res.ok)
247
+ return { ok: false, status: res.status };
248
+ const body = (await res.json());
249
+ /* v8 ignore next -- defensive: GitHub has never shipped /user without a login field. */
250
+ if (typeof body.login !== 'string')
251
+ return { ok: false, status: res.status };
252
+ return { ok: true, login: body.login };
253
+ /* v8 ignore next 3 -- network-throw catch; msw can't model a pre-HTTP throw cleanly. */
254
+ }
255
+ catch {
256
+ return { ok: false, status: 0 };
257
+ }
258
+ }
259
+ // ---- helpers -----------------------------------------------------------
260
+ function isDeviceCodeResponse(v) {
261
+ if (!v || typeof v !== 'object')
262
+ return false;
263
+ const o = v;
264
+ return (typeof o.device_code === 'string' &&
265
+ typeof o.user_code === 'string' &&
266
+ typeof o.verification_uri === 'string' &&
267
+ typeof o.expires_in === 'number' &&
268
+ typeof o.interval === 'number');
269
+ }
270
+ function isTokenResponse(v) {
271
+ if (!v || typeof v !== 'object')
272
+ return false;
273
+ const o = v;
274
+ return (typeof o.access_token === 'string' &&
275
+ typeof o.expires_in === 'number' &&
276
+ typeof o.refresh_token === 'string' &&
277
+ typeof o.refresh_token_expires_in === 'number');
278
+ }
279
+ function epochSeconds() {
280
+ return Math.floor(Date.now() / 1000);
281
+ }
282
+ /* v8 ignore next 3 -- real timer; every test injects `sleep`. */
283
+ function realSleep(ms) {
284
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
285
+ }
286
+ /** SHA-256 prefix for identifying a token in logs without leaking it. */
287
+ export function tokenDigest(token) {
288
+ return createHash('sha256').update(token).digest('hex').slice(0, 8);
289
+ }
290
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;GAaG;AAuGH,wBAAsB,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA6LlE"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;GAaG;AAiHH,wBAAsB,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAsPlE"}
package/dist/cli.js CHANGED
@@ -14,14 +14,15 @@
14
14
  * --json for plan; emit JSON instead of a table
15
15
  */
16
16
  import pkg from '../package.json' with { type: 'json' };
17
+ import { login, logout, status } from './auth.js';
17
18
  import { doctor } from './doctor.js';
18
19
  import { init } from './init.js';
19
20
  import { plan } from './plan.js';
20
21
  import { runPreflight } from './preflight-run.js';
21
22
  import { publish } from './publish.js';
22
- import { inspect } from './token.js';
23
+ import { inspect, tokenList } from './token.js';
23
24
  const VERSION = pkg.version;
24
- const COMMANDS = ['init', 'plan', 'publish', 'doctor', 'preflight', 'token', 'version'];
25
+ const COMMANDS = ['init', 'plan', 'publish', 'doctor', 'preflight', 'token', 'auth', 'version'];
25
26
  const REGISTRIES = ['crates', 'npm', 'pypi'];
26
27
  function isRegistry(v) {
27
28
  return REGISTRIES.includes(v);
@@ -39,7 +40,8 @@ function printUsage() {
39
40
  ' publish Execute the plan',
40
41
  ' doctor Validate config + handlers + auth (#23)',
41
42
  ' preflight Run every pre-publish check without side effects (#93)',
42
- ' token Inspect registry tokens (pypi/npm/crates)',
43
+ ' token Inspect or list registry tokens (pypi/npm/crates)',
44
+ ' auth Optional: sign in to GitHub for `token list --secrets`',
43
45
  ' version Print CLI version',
44
46
  '',
45
47
  'Options:',
@@ -49,6 +51,8 @@ function printUsage() {
49
51
  ' --json emit machine-readable output (plan only)',
50
52
  ' --force overwrite putitoutthere.toml on init',
51
53
  ' --artifacts doctor: also check artifact completeness',
54
+ ' --deep doctor: also inspect each token\'s publish scope',
55
+ ' --preflight-check publish: refuse on token scope mismatch (pypi/npm)',
52
56
  ' --all preflight: include non-cascaded packages too',
53
57
  ' --cadence <mode> init: immediate (default) or scheduled',
54
58
  ' --token <value> token inspect: token value (else read from env)',
@@ -66,6 +70,8 @@ function parseFlags(argv) {
66
70
  force: false,
67
71
  artifacts: false,
68
72
  all: false,
73
+ deep: false,
74
+ preflightCheck: false,
69
75
  };
70
76
  for (let i = 0; i < argv.length; i++) {
71
77
  const a = argv[i];
@@ -84,6 +90,10 @@ function parseFlags(argv) {
84
90
  out.artifacts = true;
85
91
  else if (a === '--all')
86
92
  out.all = true;
93
+ else if (a === '--deep')
94
+ out.deep = true;
95
+ else if (a === '--preflight-check')
96
+ out.preflightCheck = true;
87
97
  else if (a === '--cadence') {
88
98
  const v = argv[++i];
89
99
  /* v8 ignore next -- invalid cadence is caught by the type system for legit callers */
@@ -153,6 +163,7 @@ export async function run(argv) {
153
163
  /* v8 ignore next -- --config test covered in plan arm; publish shares the same plumbing */
154
164
  ...(flags.config !== undefined ? { configPath: flags.config } : {}),
155
165
  dryRun: flags.dryRun,
166
+ preflightCheck: flags.preflightCheck,
156
167
  });
157
168
  if (flags.json) {
158
169
  process.stdout.write(JSON.stringify(result) + '\n');
@@ -175,6 +186,7 @@ export async function run(argv) {
175
186
  /* v8 ignore next -- --config test covered via plan arm */
176
187
  ...(flags.config !== undefined ? { configPath: flags.config } : {}),
177
188
  checkArtifacts: flags.artifacts,
189
+ deep: flags.deep,
178
190
  });
179
191
  if (flags.json) {
180
192
  process.stdout.write(JSON.stringify(report) + '\n');
@@ -182,7 +194,10 @@ export async function run(argv) {
182
194
  else {
183
195
  for (const p of report.packages) {
184
196
  const badge = p.auth === 'missing' ? '✗' : '✓';
185
- process.stdout.write(` ${badge} ${p.name} (${p.kind}) — auth: ${p.auth}\n`);
197
+ const scopeSuffix = p.scope !== undefined
198
+ ? ` scope: ${p.scope}${p.scope_match && p.scope_match !== 'ok' ? ` [${p.scope_match}]` : ''}`
199
+ : '';
200
+ process.stdout.write(` ${badge} ${p.name} (${p.kind}) — auth: ${p.auth}${scopeSuffix}\n`);
186
201
  }
187
202
  if (report.artifacts && report.artifacts.length > 0) {
188
203
  process.stdout.write('\nArtifacts:\n');
@@ -237,9 +252,23 @@ export async function run(argv) {
237
252
  }
238
253
  case 'token': {
239
254
  const [sub, ...subRest] = rest;
255
+ if (sub === 'list') {
256
+ const subFlags = parseFlags(subRest);
257
+ const rows = tokenList({
258
+ cwd: subFlags.cwd,
259
+ ...(subFlags.config !== undefined ? { configPath: subFlags.config } : {}),
260
+ });
261
+ if (subFlags.json) {
262
+ process.stdout.write(JSON.stringify({ tokens: rows }) + '\n');
263
+ }
264
+ else {
265
+ printTokenList(rows);
266
+ }
267
+ return 0;
268
+ }
240
269
  if (sub !== 'inspect') {
241
270
  process.stderr.write(sub === undefined
242
- ? 'putitoutthere token: missing subcommand (expected "inspect")\n'
271
+ ? 'putitoutthere token: missing subcommand (expected "inspect" or "list")\n'
243
272
  : `putitoutthere token: unknown subcommand: ${sub}\n`);
244
273
  return 1;
245
274
  }
@@ -261,6 +290,46 @@ export async function run(argv) {
261
290
  }
262
291
  return 'error' in result ? 1 : 0;
263
292
  }
293
+ case 'auth': {
294
+ const [sub, ...subRest] = rest;
295
+ const subFlags = parseFlags(subRest);
296
+ /* v8 ignore start -- `auth login` requires live Device Flow against GitHub; the login() function itself is covered in auth.test.ts */
297
+ if (sub === 'login') {
298
+ const result = await login({
299
+ onPrompt: (p) => printDevicePrompt(p),
300
+ });
301
+ if (subFlags.json) {
302
+ process.stdout.write(JSON.stringify(result) + '\n');
303
+ }
304
+ else {
305
+ process.stdout.write(`Logged in as ${result.account} (access token expires ${new Date(result.expiresAt * 1000).toISOString()}).\n`);
306
+ }
307
+ return 0;
308
+ }
309
+ /* v8 ignore stop */
310
+ if (sub === 'logout') {
311
+ const result = await logout();
312
+ /* v8 ignore next -- --json branch is a trivial stringify; covered via auth.test.ts for `logout()` */
313
+ if (subFlags.json)
314
+ process.stdout.write(JSON.stringify(result) + '\n');
315
+ else
316
+ process.stdout.write(result.wiped ? 'Logged out.\n' : 'Not logged in.\n');
317
+ return 0;
318
+ }
319
+ if (sub === 'status') {
320
+ const result = await status();
321
+ /* v8 ignore next -- --json branch is a trivial stringify; covered via auth.test.ts for `status()` */
322
+ if (subFlags.json)
323
+ process.stdout.write(JSON.stringify(result) + '\n');
324
+ else
325
+ printAuthStatus(result);
326
+ return result.authenticated ? 0 : 1;
327
+ }
328
+ process.stderr.write(sub === undefined
329
+ ? 'putitoutthere auth: missing subcommand (expected "login", "logout", or "status")\n'
330
+ : `putitoutthere auth: unknown subcommand: ${sub}\n`);
331
+ return 1;
332
+ }
264
333
  case 'init': {
265
334
  const r = init({
266
335
  cwd: flags.cwd,
@@ -319,6 +388,26 @@ function envTokenFor(registry) {
319
388
  const filtered = matches.filter((m) => m.registry === registry);
320
389
  return filtered[0]?.value;
321
390
  }
391
+ function printTokenList(rows) {
392
+ if (rows.length === 0) {
393
+ process.stdout.write('no registry tokens found in environment\n');
394
+ return;
395
+ }
396
+ const headers = ['REGISTRY', 'SOURCE', 'ENV/NAME', 'DETAILS'];
397
+ const widths = headers.map((h) => h.length);
398
+ for (const r of rows) {
399
+ widths[0] = Math.max(widths[0], r.registry.length);
400
+ widths[1] = Math.max(widths[1], r.source.length);
401
+ widths[2] = Math.max(widths[2], r.name.length);
402
+ widths[3] = Math.max(widths[3], r.details.length);
403
+ }
404
+ const pad = (s, w) => s + ' '.repeat(Math.max(0, w - s.length));
405
+ const line = (cells) => cells.map((c, i) => pad(c, widths[i])).join(' ').trimEnd() + '\n';
406
+ process.stdout.write(line(headers));
407
+ for (const r of rows) {
408
+ process.stdout.write(line([r.registry, r.source, r.name, r.details]));
409
+ }
410
+ }
322
411
  function printInspectHuman(result) {
323
412
  if ('error' in result) {
324
413
  process.stderr.write(`inspect failed (${result.registry}, digest=${result.source_digest}): ${result.error}\n`);
@@ -372,6 +461,19 @@ function printInspectHuman(result) {
372
461
  }
373
462
  process.stdout.write(lines.join('\n') + '\n');
374
463
  }
464
+ /* v8 ignore next 6 -- only called from the `auth login` arm above, which is itself v8-ignored */
465
+ function printDevicePrompt(p) {
466
+ process.stderr.write(`Visit ${p.verificationUri} and enter code: ${p.userCode}\n` +
467
+ `(code expires in ${Math.round(p.expiresInSeconds / 60)} min)\n`);
468
+ }
469
+ function printAuthStatus(r) {
470
+ /* v8 ignore next 5 -- `authenticated: true` requires a live GitHub /user response; status() success path is covered in auth.test.ts */
471
+ if (r.authenticated) {
472
+ process.stdout.write(`Logged in as ${r.account} (access token expires ${new Date(r.expiresAt * 1000).toISOString()}).\n`);
473
+ return;
474
+ }
475
+ process.stderr.write(`${r.message}\n`);
476
+ }
375
477
  // Entry point when invoked as `putitoutthere` or `node dist/cli.js`.
376
478
  /* v8 ignore start -- entry-point guard; only reachable when invoked as a binary */
377
479
  if (import.meta.url === `file://${process.argv[1]}`) {