@propper-ai/cli 0.2.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,1354 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ApiError,
4
+ AuthError,
5
+ UsageError,
6
+ callOperation,
7
+ manifest
8
+ } from "../chunk-CBIH6V3I.js";
9
+
10
+ // src/bin/propper.ts
11
+ import { CommanderError } from "commander";
12
+
13
+ // src/output/printer.ts
14
+ import ora from "ora";
15
+ import pc from "picocolors";
16
+
17
+ // src/output/format.ts
18
+ import Table from "cli-table3";
19
+ import { stringify as yamlStringify } from "yaml";
20
+ var OUTPUT_FORMATS = ["json", "yaml", "table", "text"];
21
+ function isOutputFormat(value) {
22
+ return OUTPUT_FORMATS.includes(value);
23
+ }
24
+ function isRecordArray(data) {
25
+ return Array.isArray(data) && data.length > 0 && data.every((d) => d !== null && typeof d === "object" && !Array.isArray(d));
26
+ }
27
+ function scalar(value) {
28
+ if (value === null || value === void 0) return "";
29
+ if (typeof value === "object") return JSON.stringify(value);
30
+ return String(value);
31
+ }
32
+ function toTable(data) {
33
+ const columns = [...new Set(data.flatMap((row) => Object.keys(row)))];
34
+ const table = new Table({ head: columns });
35
+ for (const row of data) table.push(columns.map((c) => scalar(row[c])));
36
+ return table.toString();
37
+ }
38
+ function toText(data) {
39
+ if (isRecordArray(data)) {
40
+ const columns = [...new Set(data.flatMap((row) => Object.keys(row)))];
41
+ return data.map((row) => columns.map((c) => scalar(row[c])).join(" ")).join("\n");
42
+ }
43
+ if (Array.isArray(data)) {
44
+ return data.map((item) => scalar(item)).join("\n");
45
+ }
46
+ if (data !== null && typeof data === "object") {
47
+ return Object.entries(data).map(([k, v]) => `${k} ${scalar(v)}`).join("\n");
48
+ }
49
+ return scalar(data);
50
+ }
51
+ function formatOutput(data, format) {
52
+ switch (format) {
53
+ case "yaml":
54
+ return yamlStringify(data).trimEnd();
55
+ case "table":
56
+ return isRecordArray(data) ? toTable(data) : JSON.stringify(data, null, 2);
57
+ case "text":
58
+ return toText(data);
59
+ default:
60
+ return JSON.stringify(data, null, 2);
61
+ }
62
+ }
63
+
64
+ // src/output/query.ts
65
+ import * as jmespath from "jmespath";
66
+ function applyQuery(data, expression) {
67
+ if (!expression) return data;
68
+ return jmespath.search(data, expression);
69
+ }
70
+
71
+ // src/output/printer.ts
72
+ var identity = (s) => s;
73
+ function palette(enabled) {
74
+ if (!enabled)
75
+ return { red: identity, green: identity, yellow: identity, dim: identity, bold: identity };
76
+ return { red: pc.red, green: pc.green, yellow: pc.yellow, dim: pc.dim, bold: pc.bold };
77
+ }
78
+ function renderResult(data, opts) {
79
+ const filtered = applyQuery(data, opts.query);
80
+ const format = isOutputFormat(opts.output) ? opts.output : "json";
81
+ return formatOutput(filtered, format);
82
+ }
83
+ function printResult(data, opts, out = process.stdout) {
84
+ out.write(`${renderResult(data, opts)}
85
+ `);
86
+ }
87
+ var STATUS_TEXT = {
88
+ 400: "Bad Request",
89
+ 401: "Unauthorized",
90
+ 403: "Forbidden",
91
+ 404: "Not Found",
92
+ 405: "Method Not Allowed",
93
+ 409: "Conflict",
94
+ 410: "Gone",
95
+ 422: "Unprocessable Entity",
96
+ 429: "Too Many Requests",
97
+ 500: "Internal Server Error",
98
+ 502: "Bad Gateway",
99
+ 503: "Service Unavailable",
100
+ 504: "Gateway Timeout"
101
+ };
102
+ function indent(text, pad = " ") {
103
+ return text.split("\n").map((line) => `${pad}${line}`).join("\n");
104
+ }
105
+ function extraBody(body, message) {
106
+ if (body == null) return void 0;
107
+ if (typeof body === "string") {
108
+ const t = body.trim();
109
+ return t && t !== message ? body : void 0;
110
+ }
111
+ if (Array.isArray(body)) return body.length ? body : void 0;
112
+ if (typeof body === "object") {
113
+ const rest = { ...body };
114
+ for (const k of ["message", "error", "code"]) {
115
+ if (typeof rest[k] === "string") delete rest[k];
116
+ }
117
+ return Object.keys(rest).length ? body : void 0;
118
+ }
119
+ return void 0;
120
+ }
121
+ function formatApiError(err, colors) {
122
+ const statusText = STATUS_TEXT[err.status];
123
+ const head = err.status ? `HTTP ${err.status}${statusText ? ` ${statusText}` : ""}` : "Request failed";
124
+ const where = err.method && err.path ? ` on ${colors.bold(`${err.method} ${err.path}`)}` : "";
125
+ const lines = [`${colors.red("error")}: ${head}${where}`];
126
+ const msg = err.message?.trim() ?? "";
127
+ const generic = `${err.status} ${statusText ?? ""}`.trim();
128
+ if (msg && msg !== generic && msg !== head) lines.push(` ${msg}`);
129
+ const details = extraBody(err.body, msg);
130
+ if (details !== void 0) {
131
+ const rendered = typeof details === "string" ? details : JSON.stringify(details, null, 2);
132
+ lines.push(colors.dim(indent(rendered)));
133
+ }
134
+ if (err.code) lines.push(colors.dim(` code: ${err.code}`));
135
+ if (err.requestId) lines.push(colors.dim(` request id: ${err.requestId}`));
136
+ if (err.operation) {
137
+ lines.push(
138
+ colors.dim(
139
+ ` hint: run \`propper ${err.operation} --help\` for required parameters, or re-run with --debug`
140
+ )
141
+ );
142
+ }
143
+ return lines.join("\n");
144
+ }
145
+ function printError(err, opts = {}, out = process.stderr) {
146
+ const colors = palette(opts.color ?? true);
147
+ if (err instanceof ApiError) {
148
+ out.write(`${formatApiError(err, colors)}
149
+ `);
150
+ if (opts.debug && err.stack) out.write(`${colors.dim(err.stack)}
151
+ `);
152
+ return;
153
+ }
154
+ const message = err instanceof Error ? err.message : String(err);
155
+ out.write(`${colors.red("error")}: ${message}
156
+ `);
157
+ if (opts.debug && err instanceof Error && err.stack) out.write(`${colors.dim(err.stack)}
158
+ `);
159
+ }
160
+
161
+ // src/runtime/program.ts
162
+ import { Command } from "commander";
163
+
164
+ // src/commands/auth/login.ts
165
+ import { randomBytes as randomBytes2 } from "crypto";
166
+ import { createServer } from "http";
167
+
168
+ // src/auth/oidc.ts
169
+ import { createHash, randomBytes } from "crypto";
170
+ function base64url(buf) {
171
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
172
+ }
173
+ function generatePkce() {
174
+ const verifier = base64url(randomBytes(32));
175
+ const challenge = base64url(createHash("sha256").update(verifier).digest());
176
+ return { verifier, challenge, method: "S256" };
177
+ }
178
+ function fallbackEndpoints(authBaseUrl) {
179
+ const base = authBaseUrl.replace(/\/$/, "");
180
+ return {
181
+ authorization_endpoint: `${base}/oauth2/authorize`,
182
+ token_endpoint: `${base}/oauth2/token`,
183
+ userinfo_endpoint: `${base}/oauth2/userinfo`,
184
+ introspection_endpoint: `${base}/oauth2/introspect`,
185
+ revocation_endpoint: `${base}/oauth2/revoke`
186
+ };
187
+ }
188
+ async function discover(authBaseUrl, fetchFn = fetch) {
189
+ const base = authBaseUrl.replace(/\/$/, "");
190
+ const fallback = fallbackEndpoints(base);
191
+ try {
192
+ const res = await fetchFn(`${base}/.well-known/openid-configuration`);
193
+ if (!res.ok) return fallback;
194
+ const doc = await res.json();
195
+ return {
196
+ authorization_endpoint: doc.authorization_endpoint ?? fallback.authorization_endpoint,
197
+ token_endpoint: doc.token_endpoint ?? fallback.token_endpoint,
198
+ userinfo_endpoint: doc.userinfo_endpoint ?? fallback.userinfo_endpoint,
199
+ introspection_endpoint: doc.introspection_endpoint ?? fallback.introspection_endpoint,
200
+ revocation_endpoint: doc.revocation_endpoint ?? fallback.revocation_endpoint
201
+ };
202
+ } catch {
203
+ return fallback;
204
+ }
205
+ }
206
+ async function postForm(url, form, fetchFn) {
207
+ const body = new URLSearchParams();
208
+ for (const [k, v] of Object.entries(form)) if (v !== void 0) body.set(k, v);
209
+ return fetchFn(url, {
210
+ method: "POST",
211
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
212
+ body
213
+ });
214
+ }
215
+ async function tokenCall(res) {
216
+ const text = await res.text();
217
+ if (!res.ok) {
218
+ throw new Error(`Token request failed (${res.status}): ${text.slice(0, 300)}`);
219
+ }
220
+ return JSON.parse(text);
221
+ }
222
+ function buildAuthorizeUrl(opts) {
223
+ const url = new URL(opts.authorizationEndpoint);
224
+ url.searchParams.set("response_type", "code");
225
+ url.searchParams.set("client_id", opts.clientId);
226
+ url.searchParams.set("redirect_uri", opts.redirectUri);
227
+ url.searchParams.set("scope", opts.scope);
228
+ url.searchParams.set("state", opts.state);
229
+ url.searchParams.set("code_challenge", opts.codeChallenge);
230
+ url.searchParams.set("code_challenge_method", "S256");
231
+ return url.toString();
232
+ }
233
+ function exchangeCode(opts, fetchFn = fetch) {
234
+ return postForm(
235
+ opts.tokenEndpoint,
236
+ {
237
+ grant_type: "authorization_code",
238
+ code: opts.code,
239
+ redirect_uri: opts.redirectUri,
240
+ client_id: opts.clientId,
241
+ code_verifier: opts.codeVerifier,
242
+ client_secret: opts.clientSecret
243
+ },
244
+ fetchFn
245
+ ).then(tokenCall);
246
+ }
247
+ function refresh(opts, fetchFn = fetch) {
248
+ return postForm(
249
+ opts.tokenEndpoint,
250
+ {
251
+ grant_type: "refresh_token",
252
+ refresh_token: opts.refreshToken,
253
+ client_id: opts.clientId,
254
+ client_secret: opts.clientSecret
255
+ },
256
+ fetchFn
257
+ ).then(tokenCall);
258
+ }
259
+ function clientCredentials(opts, fetchFn = fetch) {
260
+ return postForm(
261
+ opts.tokenEndpoint,
262
+ {
263
+ grant_type: "client_credentials",
264
+ client_id: opts.clientId,
265
+ client_secret: opts.clientSecret,
266
+ scope: opts.scope
267
+ },
268
+ fetchFn
269
+ ).then(tokenCall);
270
+ }
271
+ async function revoke(opts, fetchFn = fetch) {
272
+ await postForm(
273
+ opts.revocationEndpoint,
274
+ { token: opts.token, client_id: opts.clientId, client_secret: opts.clientSecret },
275
+ fetchFn
276
+ );
277
+ }
278
+ async function userinfo(opts, fetchFn = fetch) {
279
+ const res = await fetchFn(opts.userinfoEndpoint, {
280
+ headers: { authorization: `Bearer ${opts.accessToken}`, accept: "application/json" }
281
+ });
282
+ if (!res.ok) throw new Error(`userinfo failed (${res.status})`);
283
+ return await res.json();
284
+ }
285
+ async function introspect(opts, fetchFn = fetch) {
286
+ const res = await postForm(
287
+ opts.introspectionEndpoint,
288
+ { token: opts.token, client_id: opts.clientId, client_secret: opts.clientSecret },
289
+ fetchFn
290
+ );
291
+ if (!res.ok) return { active: false };
292
+ return await res.json();
293
+ }
294
+
295
+ // src/auth/token-store.ts
296
+ import { chmodSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
297
+
298
+ // src/config/paths.ts
299
+ import { homedir } from "os";
300
+ import { join } from "path";
301
+
302
+ // package.json
303
+ var package_default = {
304
+ name: "@propper-ai/cli",
305
+ version: "0.2.1",
306
+ description: "Propper CLI \u2014 an AWS-style, OpenAPI-generated command-line interface for the Propper Sign + Auth APIs",
307
+ type: "module",
308
+ license: "MIT",
309
+ repository: {
310
+ type: "git",
311
+ url: "git+https://github.com/mypropper/propper-cli.git"
312
+ },
313
+ homepage: "https://github.com/mypropper/propper-cli#readme",
314
+ bugs: {
315
+ url: "https://github.com/mypropper/propper-cli/issues"
316
+ },
317
+ keywords: [
318
+ "propper",
319
+ "cli",
320
+ "e-signature",
321
+ "esignature",
322
+ "esign",
323
+ "sign",
324
+ "signing",
325
+ "docgen",
326
+ "document-generation",
327
+ "openapi",
328
+ "sdk",
329
+ "api"
330
+ ],
331
+ bin: {
332
+ propper: "./dist/bin/propper.js"
333
+ },
334
+ exports: {
335
+ "./generated/client": "./dist/generated/client.js",
336
+ "./package.json": "./package.json"
337
+ },
338
+ files: [
339
+ "dist",
340
+ "README.md"
341
+ ],
342
+ engines: {
343
+ node: ">=20"
344
+ },
345
+ scripts: {
346
+ build: "tsup",
347
+ dev: "tsx src/bin/propper.ts",
348
+ "dev:setup-local": "npm install && npm run build && npm install -g .",
349
+ "dev:link-local": "npm install && npm run build && npm link",
350
+ "dev:uninstall-local": "npm uninstall -g @propper-ai/cli",
351
+ "dev:reinstall-local": "npm run dev:uninstall-local; npm run dev:link-local && node scripts/install-completion.mjs",
352
+ test: "vitest run",
353
+ "test:watch": "vitest",
354
+ lint: "biome check .",
355
+ "lint:fix": "biome check --write .",
356
+ typecheck: "tsc --noEmit",
357
+ generate: "tsx scripts/generate.ts",
358
+ "sync-spec": "tsx scripts/sync-spec.ts",
359
+ "spec-diff": "tsx scripts/spec-diff.ts"
360
+ },
361
+ dependencies: {
362
+ "cli-table3": "^0.6.5",
363
+ commander: "^12.1.0",
364
+ jmespath: "^0.16.0",
365
+ open: "^10.1.0",
366
+ ora: "^8.1.1",
367
+ picocolors: "^1.1.1",
368
+ yaml: "^2.6.1"
369
+ },
370
+ devDependencies: {
371
+ "@biomejs/biome": "^1.9.4",
372
+ "@types/jmespath": "^0.15.2",
373
+ "@types/node": "^20.17.6",
374
+ "openapi-types": "^12.1.3",
375
+ tsup: "^8.3.5",
376
+ tsx: "^4.19.2",
377
+ typescript: "^5.7.2",
378
+ vitest: "^3.2.6"
379
+ }
380
+ };
381
+
382
+ // src/constants.ts
383
+ var CLI_VERSION = package_default.version;
384
+ var USER_AGENT = `propper-cli/${CLI_VERSION}`;
385
+ var DEFAULT_API_BASE_URL = "https://api.propper.ai";
386
+ var DEFAULT_AUTH_BASE_URL = "https://auth.propper.ai";
387
+ var OAUTH_CLIENT_ID = "propper-cli";
388
+ var DEFAULT_PROFILE = "default";
389
+ var DEFAULT_OUTPUT = "json";
390
+ var ENV = {
391
+ token: "PROPPER_API_TOKEN",
392
+ profile: "PROPPER_PROFILE",
393
+ apiBaseUrl: "PROPPER_BASE_URL",
394
+ authBaseUrl: "PROPPER_AUTH_BASE_URL",
395
+ /** Client-credentials (M2M) client id. */
396
+ clientId: "PROPPER_CLIENT_ID",
397
+ clientSecret: "PROPPER_CLIENT_SECRET",
398
+ configDir: "PROPPER_CONFIG_DIR"
399
+ };
400
+
401
+ // src/config/paths.ts
402
+ function configDir() {
403
+ return process.env[ENV.configDir] || join(homedir(), ".propper");
404
+ }
405
+ function configFile() {
406
+ return join(configDir(), "config.json");
407
+ }
408
+ function credentialsFile() {
409
+ return join(configDir(), "credentials.json");
410
+ }
411
+
412
+ // src/config/store.ts
413
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
414
+ import { dirname } from "path";
415
+ var PROFILE_KEYS = ["api_base_url", "auth_base_url", "client_id", "output"];
416
+ function loadConfig() {
417
+ try {
418
+ const raw = readFileSync(configFile(), "utf8");
419
+ const parsed = JSON.parse(raw);
420
+ return { default_profile: parsed.default_profile, profiles: parsed.profiles ?? {} };
421
+ } catch {
422
+ return { profiles: {} };
423
+ }
424
+ }
425
+ function saveConfig(config) {
426
+ mkdirSync(configDir(), { recursive: true, mode: 448 });
427
+ writeFileSync(configFile(), `${JSON.stringify(config, null, 2)}
428
+ `, { mode: 420 });
429
+ }
430
+ function getProfile(name) {
431
+ return loadConfig().profiles[name] ?? {};
432
+ }
433
+ function listProfiles() {
434
+ return Object.keys(loadConfig().profiles).sort();
435
+ }
436
+ function setProfileValue(name, key, value) {
437
+ const config = loadConfig();
438
+ const profile = config.profiles[name] ?? {};
439
+ profile[key] = value;
440
+ config.profiles[name] = profile;
441
+ if (!config.default_profile) config.default_profile = name;
442
+ saveConfig(config);
443
+ }
444
+ function ensureDirFor(filePath) {
445
+ mkdirSync(dirname(filePath), { recursive: true, mode: 448 });
446
+ }
447
+
448
+ // src/auth/token-store.ts
449
+ function readAll() {
450
+ try {
451
+ return JSON.parse(readFileSync2(credentialsFile(), "utf8"));
452
+ } catch {
453
+ return {};
454
+ }
455
+ }
456
+ function writeAll(all) {
457
+ const file = credentialsFile();
458
+ ensureDirFor(file);
459
+ writeFileSync2(file, `${JSON.stringify(all, null, 2)}
460
+ `, { mode: 384 });
461
+ chmodSync(file, 384);
462
+ }
463
+ function readCredentials(profile) {
464
+ return readAll()[profile];
465
+ }
466
+ function writeCredentials(profile, creds) {
467
+ const all = readAll();
468
+ all[profile] = creds;
469
+ writeAll(all);
470
+ }
471
+ function clearCredentials(profile) {
472
+ const all = readAll();
473
+ delete all[profile];
474
+ writeAll(all);
475
+ }
476
+ function isExpired(creds, skewSec = 60, now = Date.now()) {
477
+ if (!creds.expires_at) return false;
478
+ return now >= creds.expires_at - skewSec * 1e3;
479
+ }
480
+ function expiresAtFrom(expiresIn, now = Date.now()) {
481
+ return typeof expiresIn === "number" ? now + expiresIn * 1e3 : void 0;
482
+ }
483
+
484
+ // src/auth/chain.ts
485
+ var defaultDeps = {
486
+ readCredentials,
487
+ writeCredentials,
488
+ discover,
489
+ refresh,
490
+ clientCredentials
491
+ };
492
+ var explicitProvider = async (ctx) => ctx.explicitToken ?? null;
493
+ var envProvider = async (ctx) => (ctx.env ?? process.env)[ENV.token] ?? null;
494
+ var storedProvider = async (ctx, deps) => {
495
+ const creds = deps.readCredentials(ctx.profile);
496
+ if (!creds?.access_token) return null;
497
+ if (!isExpired(creds)) return creds.access_token;
498
+ if (!creds.refresh_token) return null;
499
+ try {
500
+ const endpoints = await deps.discover(ctx.authBaseUrl, ctx.fetchFn);
501
+ const tok = await deps.refresh(
502
+ {
503
+ tokenEndpoint: endpoints.token_endpoint,
504
+ refreshToken: creds.refresh_token,
505
+ clientId: OAUTH_CLIENT_ID
506
+ },
507
+ ctx.fetchFn
508
+ );
509
+ const updated = {
510
+ access_token: tok.access_token,
511
+ refresh_token: tok.refresh_token ?? creds.refresh_token,
512
+ expires_at: expiresAtFrom(tok.expires_in),
513
+ scope: tok.scope ?? creds.scope,
514
+ client_secret: creds.client_secret
515
+ };
516
+ deps.writeCredentials(ctx.profile, updated);
517
+ return tok.access_token;
518
+ } catch {
519
+ return null;
520
+ }
521
+ };
522
+ var clientCredentialsProvider = async (ctx, deps) => {
523
+ if (!ctx.clientSecret || !ctx.clientId) return null;
524
+ const endpoints = await deps.discover(ctx.authBaseUrl, ctx.fetchFn);
525
+ const tok = await deps.clientCredentials(
526
+ {
527
+ tokenEndpoint: endpoints.token_endpoint,
528
+ clientId: ctx.clientId,
529
+ clientSecret: ctx.clientSecret,
530
+ scope: ctx.scope
531
+ },
532
+ ctx.fetchFn
533
+ );
534
+ deps.writeCredentials(ctx.profile, {
535
+ access_token: tok.access_token,
536
+ expires_at: expiresAtFrom(tok.expires_in),
537
+ scope: tok.scope,
538
+ client_secret: ctx.clientSecret
539
+ });
540
+ return tok.access_token;
541
+ };
542
+ var PROVIDERS = [
543
+ explicitProvider,
544
+ envProvider,
545
+ storedProvider,
546
+ clientCredentialsProvider
547
+ ];
548
+ async function resolveToken(ctx) {
549
+ const deps = { ...defaultDeps, ...ctx.deps };
550
+ for (const provider of PROVIDERS) {
551
+ const token = await provider(ctx, deps);
552
+ if (token) return token;
553
+ }
554
+ throw new AuthError(
555
+ "Not authenticated. Run `propper auth login`, or set PROPPER_API_TOKEN / client credentials."
556
+ );
557
+ }
558
+
559
+ // src/commands/auth/deps.ts
560
+ var defaultAuthDeps = {
561
+ resolveToken,
562
+ discover,
563
+ exchangeCode,
564
+ clientCredentials,
565
+ revoke,
566
+ userinfo,
567
+ introspect,
568
+ readCredentials,
569
+ writeCredentials,
570
+ clearCredentials,
571
+ openBrowser: async (url) => {
572
+ const { default: open } = await import("open");
573
+ return open(url);
574
+ }
575
+ };
576
+
577
+ // src/commands/auth/login.ts
578
+ var DEFAULT_SCOPE = [
579
+ "openid",
580
+ "email",
581
+ "profile",
582
+ "offline_access",
583
+ "sign:read",
584
+ "sign:write",
585
+ "click:read",
586
+ "click:write",
587
+ "docgen:read",
588
+ "docgen:write",
589
+ "locker:read",
590
+ "locker:write",
591
+ "org:read"
592
+ ].join(" ");
593
+ var PKCE_TIMEOUT_MS = 12e4;
594
+ function rememberProfile(ctx) {
595
+ if (ctx.clientId) setProfileValue(ctx.profile, "client_id", ctx.clientId);
596
+ setProfileValue(ctx.profile, "auth_base_url", ctx.authBaseUrl);
597
+ setProfileValue(ctx.profile, "api_base_url", ctx.apiBaseUrl);
598
+ }
599
+ function loginWithToken(ctx, token, deps = defaultAuthDeps) {
600
+ const creds = { access_token: token };
601
+ deps.writeCredentials(ctx.profile, creds);
602
+ rememberProfile(ctx);
603
+ return creds;
604
+ }
605
+ async function loginClientCredentials(ctx, clientSecret, scope = DEFAULT_SCOPE, deps = defaultAuthDeps) {
606
+ if (!ctx.clientId) {
607
+ throw new UsageError(
608
+ "No client id configured. Set one via --client-id, PROPPER_CLIENT_ID, or `propper configure`."
609
+ );
610
+ }
611
+ const endpoints = await deps.discover(ctx.authBaseUrl);
612
+ const tok = await deps.clientCredentials({
613
+ tokenEndpoint: endpoints.token_endpoint,
614
+ clientId: ctx.clientId,
615
+ clientSecret,
616
+ scope
617
+ });
618
+ const creds = {
619
+ access_token: tok.access_token,
620
+ expires_at: expiresAtFrom(tok.expires_in),
621
+ scope: tok.scope,
622
+ client_secret: clientSecret
623
+ };
624
+ deps.writeCredentials(ctx.profile, creds);
625
+ rememberProfile(ctx);
626
+ return creds;
627
+ }
628
+ function awaitCallback(server, expectedState) {
629
+ return new Promise((resolve, reject) => {
630
+ const timer = setTimeout(() => {
631
+ server.close();
632
+ reject(new Error("Timed out waiting for the browser callback."));
633
+ }, PKCE_TIMEOUT_MS);
634
+ server.on("request", (req, res) => {
635
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
636
+ if (url.pathname !== "/callback") {
637
+ res.writeHead(404).end();
638
+ return;
639
+ }
640
+ const code = url.searchParams.get("code");
641
+ const state = url.searchParams.get("state");
642
+ const error = url.searchParams.get("error");
643
+ res.writeHead(200, { "content-type": "text/html" });
644
+ res.end(
645
+ error ? `<h1>Login failed</h1><p>${error}</p>` : "<h1>Login complete</h1><p>You can close this tab and return to the terminal.</p>"
646
+ );
647
+ clearTimeout(timer);
648
+ server.close();
649
+ if (error || !code || !state) {
650
+ reject(new Error(`Authorization failed: ${error ?? "missing code/state"}`));
651
+ } else if (state !== expectedState) {
652
+ reject(new Error("State mismatch (possible CSRF); aborting."));
653
+ } else {
654
+ resolve({ code, state });
655
+ }
656
+ });
657
+ });
658
+ }
659
+ async function loginPkce(ctx, scope = DEFAULT_SCOPE, deps = defaultAuthDeps) {
660
+ const endpoints = await deps.discover(ctx.authBaseUrl);
661
+ const pkce = generatePkce();
662
+ const state = randomBytes2(16).toString("hex");
663
+ const server = createServer();
664
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
665
+ const address = server.address();
666
+ if (!address || typeof address === "string") {
667
+ server.close();
668
+ throw new Error("Could not bind a loopback port for the OAuth callback.");
669
+ }
670
+ const redirectUri = `http://127.0.0.1:${address.port}/callback`;
671
+ const authorizeUrl = buildAuthorizeUrl({
672
+ authorizationEndpoint: endpoints.authorization_endpoint,
673
+ clientId: OAUTH_CLIENT_ID,
674
+ redirectUri,
675
+ scope,
676
+ state,
677
+ codeChallenge: pkce.challenge
678
+ });
679
+ const callbackPromise = awaitCallback(server, state);
680
+ await deps.openBrowser(authorizeUrl);
681
+ const { code } = await callbackPromise;
682
+ const tok = await deps.exchangeCode({
683
+ tokenEndpoint: endpoints.token_endpoint,
684
+ code,
685
+ redirectUri,
686
+ clientId: OAUTH_CLIENT_ID,
687
+ codeVerifier: pkce.verifier
688
+ });
689
+ const creds = {
690
+ access_token: tok.access_token,
691
+ refresh_token: tok.refresh_token,
692
+ expires_at: expiresAtFrom(tok.expires_in),
693
+ scope: tok.scope
694
+ };
695
+ deps.writeCredentials(ctx.profile, creds);
696
+ rememberProfile(ctx);
697
+ return { creds, authorizeUrl };
698
+ }
699
+
700
+ // src/commands/auth/logout.ts
701
+ async function logout(ctx, deps = defaultAuthDeps) {
702
+ const creds = deps.readCredentials(ctx.profile);
703
+ if (!creds) return false;
704
+ if (creds.access_token) {
705
+ try {
706
+ const endpoints = await deps.discover(ctx.authBaseUrl);
707
+ await deps.revoke({
708
+ revocationEndpoint: endpoints.revocation_endpoint,
709
+ token: creds.refresh_token ?? creds.access_token,
710
+ clientId: creds.client_secret && ctx.clientId ? ctx.clientId : OAUTH_CLIENT_ID,
711
+ clientSecret: creds.client_secret
712
+ });
713
+ } catch {
714
+ }
715
+ }
716
+ deps.clearCredentials(ctx.profile);
717
+ return true;
718
+ }
719
+
720
+ // src/commands/auth/status.ts
721
+ function maskToken(token) {
722
+ return token.length > 12 ? `${token.slice(0, 4)}\u2026${token.slice(-4)}` : "****";
723
+ }
724
+ function detectSource(ctx, deps) {
725
+ if (ctx.explicitToken) return "flag";
726
+ if (ctx.env[ENV.token]) return "env";
727
+ if (deps.readCredentials(ctx.profile)?.access_token) return "profile";
728
+ if (ctx.clientSecret) return "client-credentials";
729
+ return "none";
730
+ }
731
+ function toScopes(scope) {
732
+ if (!scope) return void 0;
733
+ const parts = scope.split(/\s+/).filter(Boolean);
734
+ return parts.length ? parts : void 0;
735
+ }
736
+ async function authStatus(ctx, deps = defaultAuthDeps) {
737
+ let token;
738
+ try {
739
+ token = await deps.resolveToken({
740
+ explicitToken: ctx.explicitToken,
741
+ profile: ctx.profile,
742
+ authBaseUrl: ctx.authBaseUrl,
743
+ clientId: ctx.clientId,
744
+ clientSecret: ctx.clientSecret,
745
+ env: ctx.env
746
+ });
747
+ } catch {
748
+ throw new AuthError(
749
+ `Not authenticated for profile "${ctx.profile}". Run \`propper auth login\` to sign in.`
750
+ );
751
+ }
752
+ const result = {
753
+ profile: ctx.profile,
754
+ source: detectSource(ctx, deps),
755
+ apiBaseUrl: ctx.apiBaseUrl,
756
+ authBaseUrl: ctx.authBaseUrl,
757
+ tokenMasked: maskToken(token)
758
+ };
759
+ let expiresAtMs = deps.readCredentials(ctx.profile)?.expires_at;
760
+ const endpoints = await deps.discover(ctx.authBaseUrl);
761
+ try {
762
+ const info = await deps.userinfo({
763
+ userinfoEndpoint: endpoints.userinfo_endpoint,
764
+ accessToken: token
765
+ });
766
+ result.user = {
767
+ sub: info.sub,
768
+ email: info.email,
769
+ name: info.name,
770
+ roles: info.roles,
771
+ org: info.org
772
+ };
773
+ result.scopes = toScopes(info.scope);
774
+ } catch {
775
+ }
776
+ try {
777
+ const intro = await deps.introspect({
778
+ introspectionEndpoint: endpoints.introspection_endpoint,
779
+ token,
780
+ // Authenticate the introspection call with whichever client minted the
781
+ // token: the M2M client when there's a secret, else the public OAuth client.
782
+ clientId: ctx.clientSecret && ctx.clientId ? ctx.clientId : OAUTH_CLIENT_ID,
783
+ clientSecret: ctx.clientSecret
784
+ });
785
+ if (intro.active) {
786
+ result.scopes = result.scopes ?? toScopes(intro.scope);
787
+ if (typeof intro.exp === "number") expiresAtMs = expiresAtMs ?? intro.exp * 1e3;
788
+ }
789
+ } catch {
790
+ }
791
+ if (typeof expiresAtMs === "number") result.tokenExpiresAt = new Date(expiresAtMs).toISOString();
792
+ return result;
793
+ }
794
+
795
+ // src/commands/completion.ts
796
+ var SUPPORTED_SHELLS = ["bash", "zsh", "fish"];
797
+ var GLOBAL_FLAGS = [
798
+ "--profile",
799
+ "--output",
800
+ "--query",
801
+ "--base-url",
802
+ "--auth-base-url",
803
+ "--client-id",
804
+ "--token",
805
+ "--no-color",
806
+ "--quiet",
807
+ "--debug",
808
+ "--help"
809
+ ];
810
+ var STATIC_GROUPS = {
811
+ auth: ["login", "logout", "status", "whoami"],
812
+ configure: ["set", "get", "list-profiles"],
813
+ completion: [...SUPPORTED_SHELLS]
814
+ };
815
+ function operationFlags(entry) {
816
+ const flags = [
817
+ ...entry.pathParams.map((p) => p.flag),
818
+ ...entry.queryParams.map((p) => p.flag),
819
+ ...entry.bodyFields.map((f) => f.flag)
820
+ ];
821
+ if (entry.hasBody) flags.push("--input-json");
822
+ if (entry.fileField) flags.push("--file");
823
+ if (entry.produces === "binary") flags.push("--output-file");
824
+ if (entry.queryParams.some((p) => p.name === "page")) flags.push("--all");
825
+ return flags;
826
+ }
827
+ function uniq(values) {
828
+ return [...new Set(values)].sort();
829
+ }
830
+ function candidatesAt(path, manifest2) {
831
+ const apiNames = manifest2.apis.map((a) => a.name);
832
+ if (path.length === 0) return uniq([...apiNames, ...Object.keys(STATIC_GROUPS)]);
833
+ const head = path[0] ?? "";
834
+ if (head in STATIC_GROUPS) return path.length === 1 ? STATIC_GROUPS[head] ?? [] : [];
835
+ if (!apiNames.includes(head)) return [];
836
+ if (path.length === 1) {
837
+ return uniq(manifest2.operations.filter((o) => o.api === head).map((o) => o.topic));
838
+ }
839
+ if (path.length === 2) {
840
+ return uniq(
841
+ manifest2.operations.filter((o) => o.api === head && o.topic === path[1]).map((o) => o.command)
842
+ );
843
+ }
844
+ const entry = manifest2.operations.find(
845
+ (o) => o.api === head && o.topic === path[1] && o.command === path[2]
846
+ );
847
+ return uniq([...entry ? operationFlags(entry) : [], ...GLOBAL_FLAGS]);
848
+ }
849
+ function completeArgs(completedWords, manifest2) {
850
+ const path = completedWords.filter((w) => !w.startsWith("-"));
851
+ return candidatesAt(path, manifest2);
852
+ }
853
+ function completionScript(shell) {
854
+ if (shell === "bash") {
855
+ return `# propper bash completion
856
+ _propper_complete() {
857
+ local cur completed candidates
858
+ cur="\${COMP_WORDS[COMP_CWORD]}"
859
+ completed=("\${COMP_WORDS[@]:1:COMP_CWORD-1}")
860
+ candidates="$(propper __complete "\${completed[@]}" 2>/dev/null)"
861
+ local IFS=$'\\n'
862
+ COMPREPLY=( $(compgen -W "\${candidates}" -- "\${cur}") )
863
+ }
864
+ complete -F _propper_complete propper
865
+ `;
866
+ }
867
+ if (shell === "zsh") {
868
+ return `#compdef propper
869
+ _propper() {
870
+ local -a candidates
871
+ candidates=("\${(@f)$(propper __complete \${words[2,CURRENT-1]} 2>/dev/null)}")
872
+ compadd -- $candidates
873
+ }
874
+ compdef _propper propper
875
+ `;
876
+ }
877
+ return `# propper fish completion
878
+ function __propper_complete
879
+ set -l tokens (commandline -opc)
880
+ propper __complete $tokens[2..-1]
881
+ end
882
+ complete -c propper -f -a '(__propper_complete)'
883
+ `;
884
+ }
885
+
886
+ // src/commands/configure/index.ts
887
+ import { createInterface } from "readline/promises";
888
+ var SECRET_KEYS = ["client_secret"];
889
+ var ALL_KEYS = [...PROFILE_KEYS, ...SECRET_KEYS];
890
+ var CREDENTIAL_KEYS = /* @__PURE__ */ new Set(["client_id", "client_secret"]);
891
+ function cleanCredential(value) {
892
+ return value.replace(/\s+/g, "");
893
+ }
894
+ function maskSecret(secret) {
895
+ const n = secret.length;
896
+ if (n >= 14) return `${secret.slice(0, 5)}\u2026${secret.slice(-5)}`;
897
+ if (n > 6) return `${secret.slice(0, 3)}\u2026${secret.slice(-3)}`;
898
+ return "****";
899
+ }
900
+ function isSecretKey(key) {
901
+ return SECRET_KEYS.includes(key);
902
+ }
903
+ function assertKey(key) {
904
+ if (!PROFILE_KEYS.includes(key)) {
905
+ throw new UsageError(`Unknown config key "${key}". Valid keys: ${ALL_KEYS.join(", ")}`);
906
+ }
907
+ }
908
+ function writeClientSecret(profile, secret) {
909
+ const existing = readCredentials(profile) ?? {};
910
+ writeCredentials(profile, { ...existing, client_secret: cleanCredential(secret) });
911
+ }
912
+ function configureSet(profile, key, value) {
913
+ const v = CREDENTIAL_KEYS.has(key) ? cleanCredential(value) : value;
914
+ if (isSecretKey(key)) {
915
+ writeClientSecret(profile, v);
916
+ return;
917
+ }
918
+ assertKey(key);
919
+ setProfileValue(profile, key, v);
920
+ }
921
+ function configureGet(profile, key) {
922
+ if (isSecretKey(key)) {
923
+ return readCredentials(profile)?.client_secret;
924
+ }
925
+ assertKey(key);
926
+ return getProfile(profile)[key];
927
+ }
928
+ function configureListProfiles() {
929
+ return listProfiles();
930
+ }
931
+ async function runConfigureWizard(ctx, input = process.stdin, output = process.stdout) {
932
+ const rl = createInterface({ input, output });
933
+ const written = [];
934
+ try {
935
+ const ask = async (label, current2) => {
936
+ const suffix = current2 ? ` [${current2}]` : "";
937
+ const answer = (await rl.question(`${label}${suffix}: `)).trim();
938
+ return answer || current2 || "";
939
+ };
940
+ const current = getProfile(ctx.profile);
941
+ const apiBaseUrl = await ask("API base URL", current.api_base_url ?? ctx.apiBaseUrl);
942
+ const authBaseUrl = await ask("Auth base URL", current.auth_base_url ?? ctx.authBaseUrl);
943
+ const clientId = cleanCredential(
944
+ await ask("Client id (client-credentials / M2M)", current.client_id ?? ctx.clientId)
945
+ );
946
+ const existingSecret = readCredentials(ctx.profile)?.client_secret;
947
+ const secretLabel = existingSecret ? `Client secret [${maskSecret(existingSecret)}]` : "Client secret (optional, blank to skip)";
948
+ const secretAnswer = cleanCredential(await rl.question(`${secretLabel}: `));
949
+ const token = (await rl.question("API token (optional, blank to skip): ")).trim();
950
+ const out = await ask("Default output (json/table/yaml/text)", current.output ?? ctx.output);
951
+ for (const [key, value] of [
952
+ ["api_base_url", apiBaseUrl],
953
+ ["auth_base_url", authBaseUrl],
954
+ ["client_id", clientId],
955
+ ["output", out]
956
+ ]) {
957
+ if (value) {
958
+ setProfileValue(ctx.profile, key, value);
959
+ written.push(key);
960
+ }
961
+ }
962
+ if (secretAnswer) {
963
+ writeClientSecret(ctx.profile, secretAnswer);
964
+ written.push("client_secret");
965
+ }
966
+ if (token) {
967
+ const existing = readCredentials(ctx.profile) ?? {};
968
+ writeCredentials(ctx.profile, { ...existing, access_token: token });
969
+ written.push("token");
970
+ }
971
+ return written;
972
+ } finally {
973
+ rl.close();
974
+ }
975
+ }
976
+
977
+ // src/runtime/dispatch.ts
978
+ import { Buffer } from "buffer";
979
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
980
+
981
+ // src/manifest/types.ts
982
+ function kebabCase(input) {
983
+ return input.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").replace(/-+/g, "-").toLowerCase();
984
+ }
985
+ function camelCase(input) {
986
+ const parts = kebabCase(input).split("-").filter(Boolean);
987
+ return parts.map((p, i) => i === 0 ? p : p.charAt(0).toUpperCase() + p.slice(1)).join("");
988
+ }
989
+
990
+ // src/runtime/options.ts
991
+ function buildContext(globals, env = process.env) {
992
+ const profile = globals.profile || env[ENV.profile] || loadConfig().default_profile || DEFAULT_PROFILE;
993
+ const cfg = getProfile(profile);
994
+ return {
995
+ profile,
996
+ apiBaseUrl: globals.baseUrl || env[ENV.apiBaseUrl] || cfg.api_base_url || manifest.apiBaseUrl || DEFAULT_API_BASE_URL,
997
+ authBaseUrl: globals.authBaseUrl || env[ENV.authBaseUrl] || cfg.auth_base_url || DEFAULT_AUTH_BASE_URL,
998
+ // Client-credentials (M2M) client id: flag > env > profile. Optional.
999
+ clientId: globals.clientId || env[ENV.clientId] || cfg.client_id || void 0,
1000
+ clientSecret: env[ENV.clientSecret] || readCredentials(profile)?.client_secret || void 0,
1001
+ output: globals.output || cfg.output || DEFAULT_OUTPUT,
1002
+ query: globals.query,
1003
+ color: globals.color !== false && !env.NO_COLOR,
1004
+ quiet: Boolean(globals.quiet),
1005
+ debug: Boolean(globals.debug),
1006
+ explicitToken: globals.token,
1007
+ env
1008
+ };
1009
+ }
1010
+ function makeRequestContext(ctx, token) {
1011
+ return {
1012
+ apiBaseUrl: ctx.apiBaseUrl,
1013
+ token,
1014
+ userAgent: USER_AGENT,
1015
+ debug: ctx.debug
1016
+ };
1017
+ }
1018
+
1019
+ // src/runtime/dispatch.ts
1020
+ function flagKey(flag) {
1021
+ return camelCase(flag.replace(/^--/, ""));
1022
+ }
1023
+ var defaultDispatchDeps = {
1024
+ resolveToken,
1025
+ call: (entry, req, ctx) => callOperation(entry.api, entry.operationId, req, ctx),
1026
+ readFile: (path) => readFileSync3(path),
1027
+ writeFile: (path, data) => writeFileSync3(path, data),
1028
+ stdout: process.stdout
1029
+ };
1030
+ function coerce(value, type) {
1031
+ if (type === "boolean") return value === true || value === "true";
1032
+ if (type === "number" || type === "integer") return Number(value);
1033
+ return String(value);
1034
+ }
1035
+ function readInputJson(raw, deps) {
1036
+ const text = raw === "-" ? readFileSync3(0, "utf8") : raw.startsWith("@") ? deps.readFile(raw.slice(1)).toString("utf8") : raw;
1037
+ try {
1038
+ return JSON.parse(text);
1039
+ } catch (err) {
1040
+ throw new UsageError(`--input-json is not valid JSON: ${err.message}`);
1041
+ }
1042
+ }
1043
+ function buildRequest(entry, flags, deps) {
1044
+ const pathParams = {};
1045
+ for (const p of entry.pathParams) {
1046
+ const value = flags[flagKey(p.flag)];
1047
+ if (value === void 0 || value === "") {
1048
+ throw new UsageError(`Missing required option ${p.flag}`);
1049
+ }
1050
+ pathParams[p.name] = String(value);
1051
+ }
1052
+ const query = {};
1053
+ for (const p of entry.queryParams) {
1054
+ const value = flags[flagKey(p.flag)];
1055
+ if (value !== void 0) query[p.name] = value;
1056
+ }
1057
+ let body;
1058
+ if (entry.hasBody) {
1059
+ body = {};
1060
+ const inputJson = flags.inputJson;
1061
+ if (typeof inputJson === "string") Object.assign(body, readInputJson(inputJson, deps));
1062
+ for (const field of entry.bodyFields) {
1063
+ const value = flags[flagKey(field.flag)];
1064
+ if (value !== void 0) body[field.name] = coerce(value, field.type);
1065
+ }
1066
+ if (entry.fileField && typeof flags.file === "string") {
1067
+ body[entry.fileField] = deps.readFile(flags.file).toString("base64");
1068
+ }
1069
+ if (Object.keys(body).length === 0) body = void 0;
1070
+ }
1071
+ return { pathParams, query, body };
1072
+ }
1073
+ function pageItems(data) {
1074
+ if (Array.isArray(data)) return data;
1075
+ if (data && typeof data === "object") {
1076
+ for (const key of ["data", "items", "results", "agreements", "templates", "recipients"]) {
1077
+ const value = data[key];
1078
+ if (Array.isArray(value)) return value;
1079
+ }
1080
+ }
1081
+ return void 0;
1082
+ }
1083
+ var MAX_PAGES = 50;
1084
+ async function dispatch(entry, flags, ctx, deps = defaultDispatchDeps) {
1085
+ const token = await deps.resolveToken({
1086
+ explicitToken: ctx.explicitToken,
1087
+ profile: ctx.profile,
1088
+ authBaseUrl: ctx.authBaseUrl,
1089
+ clientId: ctx.clientId,
1090
+ clientSecret: ctx.clientSecret,
1091
+ env: ctx.env
1092
+ });
1093
+ const reqCtx = makeRequestContext(ctx, token);
1094
+ const req = buildRequest(entry, flags, deps);
1095
+ const canPaginate = entry.queryParams.some((p) => p.name === "page");
1096
+ if (flags.all && canPaginate && entry.produces === "json") {
1097
+ const collected = [];
1098
+ for (let page = 1; page <= MAX_PAGES; page++) {
1099
+ const res2 = await deps.call(entry, { ...req, query: { ...req.query, page } }, reqCtx);
1100
+ const items = pageItems(res2.data);
1101
+ if (!items || items.length === 0) break;
1102
+ collected.push(...items);
1103
+ const limit = Number(req.query?.limit ?? items.length);
1104
+ if (items.length < limit) break;
1105
+ }
1106
+ printResult(collected, { output: ctx.output, query: ctx.query }, deps.stdout);
1107
+ return;
1108
+ }
1109
+ const res = await deps.call(entry, req, reqCtx);
1110
+ if (entry.produces === "binary") {
1111
+ const bytes = res.bytes ?? Buffer.alloc(0);
1112
+ if (typeof flags.outputFile === "string") {
1113
+ deps.writeFile(flags.outputFile, bytes);
1114
+ deps.stdout.write(`Saved ${bytes.length} bytes to ${flags.outputFile}
1115
+ `);
1116
+ } else {
1117
+ deps.stdout.write(bytes);
1118
+ }
1119
+ return;
1120
+ }
1121
+ printResult(res.data, { output: ctx.output, query: ctx.query }, deps.stdout);
1122
+ }
1123
+
1124
+ // src/runtime/program.ts
1125
+ var defaultProgramDeps = {
1126
+ buildContext: (globals) => buildContext(globals),
1127
+ dispatch,
1128
+ auth: {
1129
+ loginWithToken,
1130
+ loginClientCredentials,
1131
+ loginPkce,
1132
+ logout,
1133
+ authStatus
1134
+ },
1135
+ configure: {
1136
+ configureSet,
1137
+ configureGet,
1138
+ configureListProfiles,
1139
+ runConfigureWizard
1140
+ }
1141
+ };
1142
+ function addGlobalOptions(cmd) {
1143
+ return cmd.option("--profile <name>", "Config profile to use").option("--output <format>", "Output format: json|table|yaml|text").option("--query <jmespath>", "JMESPath filter expression (like AWS --query)").option("--base-url <url>", "Override the API base URL").option("--auth-base-url <url>", "Override the auth base URL").option("--client-id <id>", "Client-credentials (M2M) client id").option("--token <token>", "Bearer token (overrides stored credentials)").option("--no-color", "Disable colored output").option("--quiet", "Suppress spinners and non-essential output").option("--debug", "Print request/response debug info");
1144
+ }
1145
+ function renderEnvHelp() {
1146
+ const rows = [
1147
+ { name: ENV.token, desc: "Bearer token (overrides stored credentials)" },
1148
+ { name: ENV.profile, desc: "Active profile", flag: "--profile" },
1149
+ { name: ENV.apiBaseUrl, desc: "API base URL", flag: "--base-url" },
1150
+ { name: ENV.authBaseUrl, desc: "Auth base URL", flag: "--auth-base-url" },
1151
+ { name: ENV.clientId, desc: "Client-credentials (M2M) client id", flag: "--client-id" },
1152
+ { name: ENV.clientSecret, desc: "Client-credentials (M2M) client secret" },
1153
+ { name: ENV.configDir, desc: "Config directory (default ~/.propper)" },
1154
+ { name: "NO_COLOR", desc: "Disable colored output", flag: "--no-color" }
1155
+ ];
1156
+ const nameWidth = Math.max(...rows.map((r) => r.name.length));
1157
+ const descWidth = Math.max(...rows.map((r) => r.desc.length));
1158
+ const lines = rows.map((r) => {
1159
+ const left = ` ${r.name.padEnd(nameWidth)} ${r.desc.padEnd(descWidth)}`;
1160
+ return r.flag ? `${left} (${r.flag})` : left.trimEnd();
1161
+ });
1162
+ return [
1163
+ "Environment variables:",
1164
+ ...lines,
1165
+ "",
1166
+ "Precedence: command flags > environment variables > profile config > built-in defaults."
1167
+ ].join("\n");
1168
+ }
1169
+ function showHelpWhenEmpty(cmd) {
1170
+ return cmd.action(() => cmd.help());
1171
+ }
1172
+ function addApiCommand(topicCmd, entry, deps) {
1173
+ const cmd = topicCmd.command(entry.command);
1174
+ if (entry.summary) cmd.description(entry.summary);
1175
+ const added = /* @__PURE__ */ new Set();
1176
+ const add = (flag, desc, required = false) => {
1177
+ const key = flag.split(" ")[0] ?? flag;
1178
+ if (added.has(key)) return;
1179
+ added.add(key);
1180
+ if (required) cmd.requiredOption(flag, desc);
1181
+ else cmd.option(flag, desc);
1182
+ };
1183
+ for (const p of entry.pathParams) {
1184
+ add(`${p.flag} <value>`, p.description ?? `path parameter '${p.name}'`, true);
1185
+ }
1186
+ for (const p of entry.queryParams) {
1187
+ add(`${p.flag} <value>`, p.description ?? `query parameter '${p.name}'`);
1188
+ }
1189
+ for (const f of entry.bodyFields) {
1190
+ const desc = `${f.description ?? `body field '${f.name}'`}${f.required ? " (required)" : ""}`;
1191
+ add(f.type === "boolean" ? f.flag : `${f.flag} <value>`, desc);
1192
+ }
1193
+ if (entry.hasBody) {
1194
+ add("--input-json <json|@file>", "Raw JSON body: a string, @file.json, or '-' for stdin");
1195
+ }
1196
+ if (entry.fileField) {
1197
+ add("--file <path>", `Local file for the '${entry.fileField}' field (base64-encoded)`);
1198
+ }
1199
+ if (entry.produces === "binary") {
1200
+ add("--output-file <path>", "Write the binary response to a file (else stdout)");
1201
+ }
1202
+ if (entry.queryParams.some((p) => p.name === "page")) {
1203
+ add("--all", "Auto-paginate and return all pages");
1204
+ }
1205
+ addGlobalOptions(cmd);
1206
+ cmd.action(async (_opts, c) => {
1207
+ const ctx = deps.buildContext(c.optsWithGlobals());
1208
+ await deps.dispatch(entry, c.opts(), ctx);
1209
+ });
1210
+ }
1211
+ function registerApiTopics(program, manifest2, deps) {
1212
+ const apiCmds = /* @__PURE__ */ new Map();
1213
+ const topicCmds = /* @__PURE__ */ new Map();
1214
+ for (const api of manifest2.apis) {
1215
+ const title = api.title.replace(/\s*API$/i, "");
1216
+ const apiCmd = program.command(api.name).description(`${title} API`);
1217
+ showHelpWhenEmpty(apiCmd);
1218
+ apiCmds.set(api.name, apiCmd);
1219
+ }
1220
+ for (const entry of manifest2.operations) {
1221
+ const apiCmd = apiCmds.get(entry.api);
1222
+ if (!apiCmd) continue;
1223
+ const topicKey = `${entry.api}:${entry.topic}`;
1224
+ let topicCmd = topicCmds.get(topicKey);
1225
+ if (!topicCmd) {
1226
+ topicCmd = apiCmd.command(entry.topic).description(`Manage ${entry.topic}`);
1227
+ showHelpWhenEmpty(topicCmd);
1228
+ topicCmds.set(topicKey, topicCmd);
1229
+ }
1230
+ addApiCommand(topicCmd, entry, deps);
1231
+ }
1232
+ }
1233
+ function registerAuth(program, deps) {
1234
+ const auth = program.command("auth").description("Authenticate with Propper");
1235
+ showHelpWhenEmpty(auth);
1236
+ const login = addGlobalOptions(auth.command("login")).description("Log in via browser (PKCE), --client-credentials, or --token").option("--client-credentials", "Use the OAuth client-credentials grant (CI)").option(
1237
+ "--scope <scope>",
1238
+ "Space-separated OAuth scopes to request (defaults to the full propper-cli set)"
1239
+ ).action(async (_opts, c) => {
1240
+ const ctx = deps.buildContext(c.optsWithGlobals());
1241
+ const o = c.opts();
1242
+ if (ctx.explicitToken) {
1243
+ deps.auth.loginWithToken(ctx, ctx.explicitToken);
1244
+ } else if (o.clientCredentials) {
1245
+ if (!ctx.clientId || !ctx.clientSecret) {
1246
+ throw new UsageError(
1247
+ `Set a client id (--client-id / ${ENV.clientId} / \`propper configure\`) and ${ENV.clientSecret} to use --client-credentials.`
1248
+ );
1249
+ }
1250
+ await deps.auth.loginClientCredentials(ctx, ctx.clientSecret, o.scope);
1251
+ } else {
1252
+ const { authorizeUrl } = await deps.auth.loginPkce(ctx, o.scope);
1253
+ process.stderr.write(`Opening browser to authorize...
1254
+ ${authorizeUrl}
1255
+ `);
1256
+ }
1257
+ process.stdout.write(`\u2713 Logged in to profile "${ctx.profile}".
1258
+ `);
1259
+ });
1260
+ void login;
1261
+ addGlobalOptions(auth.command("logout")).description("Revoke and clear stored credentials").action(async (_opts, c) => {
1262
+ const ctx = deps.buildContext(c.optsWithGlobals());
1263
+ const ok = await deps.auth.logout(ctx);
1264
+ process.stdout.write(ok ? `\u2713 Logged out of "${ctx.profile}".
1265
+ ` : "Nothing to log out of.\n");
1266
+ });
1267
+ addGlobalOptions(auth.command("status")).alias("whoami").description("Show the current identity and token status").action(async (_opts, c) => {
1268
+ const ctx = deps.buildContext(c.optsWithGlobals());
1269
+ const status = await deps.auth.authStatus(ctx);
1270
+ printResult(status, { output: ctx.output, query: ctx.query });
1271
+ });
1272
+ }
1273
+ function registerConfigure(program, deps) {
1274
+ const configure = addGlobalOptions(program.command("configure")).description(
1275
+ "Configure profiles (base URLs, client id + secret, default output, token)"
1276
+ );
1277
+ addGlobalOptions(configure.command("set")).argument("<key>", "Config key").argument("<value>", "Value").description("Set a profile value").action((key, value, _o, c) => {
1278
+ const ctx = deps.buildContext(c.optsWithGlobals());
1279
+ deps.configure.configureSet(ctx.profile, key, value);
1280
+ process.stdout.write(`\u2713 Set ${key} for profile "${ctx.profile}".
1281
+ `);
1282
+ });
1283
+ addGlobalOptions(configure.command("get")).argument("<key>", "Config key").description("Get a profile value").action((key, _o, c) => {
1284
+ const ctx = deps.buildContext(c.optsWithGlobals());
1285
+ const value = deps.configure.configureGet(ctx.profile, key);
1286
+ process.stdout.write(`${value ?? ""}
1287
+ `);
1288
+ });
1289
+ addGlobalOptions(configure.command("list-profiles")).description("List configured profiles").action(() => {
1290
+ printResult(deps.configure.configureListProfiles(), { output: "json" });
1291
+ });
1292
+ configure.action(async (_opts, c) => {
1293
+ const ctx = deps.buildContext(c.optsWithGlobals());
1294
+ await deps.configure.runConfigureWizard(ctx);
1295
+ });
1296
+ }
1297
+ function registerCompletion(program, manifest2) {
1298
+ program.command("completion").argument("<shell>", `shell to emit a completion script for (${SUPPORTED_SHELLS.join(" | ")})`).description("Output a shell completion script").action((shell) => {
1299
+ if (!SUPPORTED_SHELLS.includes(shell)) {
1300
+ throw new UsageError(
1301
+ `Unsupported shell "${shell}". Use one of: ${SUPPORTED_SHELLS.join(", ")}`
1302
+ );
1303
+ }
1304
+ process.stdout.write(completionScript(shell));
1305
+ });
1306
+ const complete = new Command("__complete").argument("[words...]", "tokens typed after `propper`").action((words = []) => {
1307
+ for (const candidate of completeArgs(words, manifest2)) process.stdout.write(`${candidate}
1308
+ `);
1309
+ });
1310
+ program.addCommand(complete, { hidden: true });
1311
+ }
1312
+ function buildProgram(manifest2, deps = defaultProgramDeps) {
1313
+ const program = new Command();
1314
+ program.name("propper").description("Propper CLI \u2014 AWS-style, OpenAPI-generated interface for the Propper APIs").version(CLI_VERSION, "-V, --version").showHelpAfterError("(add --help for usage)");
1315
+ addGlobalOptions(program);
1316
+ program.addHelpText("after", `
1317
+ ${renderEnvHelp()}`);
1318
+ registerApiTopics(program, manifest2, deps);
1319
+ registerAuth(program, deps);
1320
+ registerConfigure(program, deps);
1321
+ registerCompletion(program, manifest2);
1322
+ return program;
1323
+ }
1324
+
1325
+ // src/bin/propper.ts
1326
+ function exitCodeFor(err) {
1327
+ if (err instanceof UsageError) return 2;
1328
+ if (err instanceof AuthError) return 3;
1329
+ if (err instanceof ApiError) return 4;
1330
+ return 1;
1331
+ }
1332
+ async function main() {
1333
+ const argv = process.argv;
1334
+ const debug = argv.includes("--debug");
1335
+ try {
1336
+ const program = buildProgram(manifest);
1337
+ program.exitOverride();
1338
+ if (argv.slice(2).length === 0) {
1339
+ program.outputHelp();
1340
+ return;
1341
+ }
1342
+ await program.parseAsync(argv);
1343
+ } catch (err) {
1344
+ if (err instanceof CommanderError) {
1345
+ process.exitCode = err.exitCode === 0 ? 0 : 2;
1346
+ return;
1347
+ }
1348
+ const color = !argv.includes("--no-color") && !process.env.NO_COLOR;
1349
+ printError(err, { debug, color });
1350
+ process.exitCode = exitCodeFor(err);
1351
+ }
1352
+ }
1353
+ void main();
1354
+ //# sourceMappingURL=propper.js.map