@parix/cli 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +123 -0
  2. package/dist/cli.cjs +30 -0
  3. package/dist/cli.d.cts +2 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +28 -0
  6. package/dist/commands/api.cjs +81 -0
  7. package/dist/commands/api.d.cts +2 -0
  8. package/dist/commands/api.d.ts +2 -0
  9. package/dist/commands/api.js +78 -0
  10. package/dist/commands/auth.cjs +128 -0
  11. package/dist/commands/auth.d.cts +2 -0
  12. package/dist/commands/auth.d.ts +2 -0
  13. package/dist/commands/auth.js +125 -0
  14. package/dist/commands/database.cjs +259 -0
  15. package/dist/commands/database.d.cts +2 -0
  16. package/dist/commands/database.d.ts +2 -0
  17. package/dist/commands/database.js +256 -0
  18. package/dist/commands/tb.cjs +242 -0
  19. package/dist/commands/tb.d.cts +2 -0
  20. package/dist/commands/tb.d.ts +2 -0
  21. package/dist/commands/tb.js +239 -0
  22. package/dist/lib/api-client.cjs +44 -0
  23. package/dist/lib/api-client.d.cts +10 -0
  24. package/dist/lib/api-client.d.ts +10 -0
  25. package/dist/lib/api-client.js +40 -0
  26. package/dist/lib/config.cjs +24 -0
  27. package/dist/lib/config.d.cts +10 -0
  28. package/dist/lib/config.d.ts +10 -0
  29. package/dist/lib/config.js +18 -0
  30. package/dist/lib/loopback-server.cjs +158 -0
  31. package/dist/lib/loopback-server.d.cts +20 -0
  32. package/dist/lib/loopback-server.d.ts +20 -0
  33. package/dist/lib/loopback-server.js +155 -0
  34. package/dist/lib/oauth-api.cjs +73 -0
  35. package/dist/lib/oauth-api.d.cts +31 -0
  36. package/dist/lib/oauth-api.d.ts +31 -0
  37. package/dist/lib/oauth-api.js +68 -0
  38. package/dist/lib/oauth-session.cjs +108 -0
  39. package/dist/lib/oauth-session.d.cts +32 -0
  40. package/dist/lib/oauth-session.d.ts +32 -0
  41. package/dist/lib/oauth-session.js +103 -0
  42. package/dist/lib/oauth.cjs +58 -0
  43. package/dist/lib/oauth.d.cts +19 -0
  44. package/dist/lib/oauth.d.ts +19 -0
  45. package/dist/lib/oauth.js +49 -0
  46. package/dist/lib/open-url.cjs +38 -0
  47. package/dist/lib/open-url.d.cts +1 -0
  48. package/dist/lib/open-url.d.ts +1 -0
  49. package/dist/lib/open-url.js +32 -0
  50. package/dist/lib/output.cjs +20 -0
  51. package/dist/lib/output.d.cts +6 -0
  52. package/dist/lib/output.d.ts +6 -0
  53. package/dist/lib/output.js +15 -0
  54. package/dist/lib/parix-api.cjs +59 -0
  55. package/dist/lib/parix-api.d.cts +12 -0
  56. package/dist/lib/parix-api.d.ts +12 -0
  57. package/dist/lib/parix-api.js +55 -0
  58. package/dist/lib/session.cjs +80 -0
  59. package/dist/lib/session.d.cts +28 -0
  60. package/dist/lib/session.d.ts +28 -0
  61. package/dist/lib/session.js +74 -0
  62. package/dist/lib/tb-payloads.cjs +153 -0
  63. package/dist/lib/tb-payloads.d.cts +61 -0
  64. package/dist/lib/tb-payloads.d.ts +61 -0
  65. package/dist/lib/tb-payloads.js +144 -0
  66. package/package.json +74 -0
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # parix
2
+
3
+ `parix` is the Parix command line interface.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @parix/cli
9
+ parix --help
10
+ ```
11
+
12
+ Current commands:
13
+
14
+ - `parix auth login`
15
+ - `parix auth status`
16
+ - `parix auth logout`
17
+ - `parix api <path>`
18
+ - `parix database catalog`
19
+ - `parix database create <database>`
20
+ - `parix database list`
21
+ - `parix database info <database-id>`
22
+ - `parix database remove <database-id>`
23
+ - `parix tb create-accounts <database-id>`
24
+ - `parix tb create-transfers <database-id>`
25
+ - `parix tb lookup-accounts <database-id>`
26
+ - `parix tb lookup-transfers <database-id>`
27
+ - `parix tb get-account-transfers <database-id>`
28
+ - `parix tb get-account-balances <database-id>`
29
+ - `parix tb query-accounts <database-id>`
30
+ - `parix tb query-transfers <database-id>`
31
+
32
+ ## Auth flow
33
+
34
+ - Starts a local loopback callback server on `127.0.0.1`
35
+ - Starts the Better Auth OIDC flow for the first-party `parix` client
36
+ - Opens the browser authorization flow with PKCE
37
+ - Forces the Better Auth consent screen on every `parix auth login`
38
+ - Receives an authorization code through the local callback
39
+ - Redirects the browser to the Parix success page at `/cli-oauth-consent-granted`
40
+ - Exchanges the code for access and refresh tokens
41
+ - Stores the local session at `~/.config/parix/session.json`
42
+
43
+ `parix auth status` validates the stored session, refreshes tokens if needed, and prints the authenticated user plus active organization context.
44
+
45
+ If you want to override the OIDC prompt manually, use `--prompt <value>`.
46
+
47
+ ## Database commands
48
+
49
+ - `parix database catalog`: show the available provider, region, cluster config, cluster size, and storage tier options for database creation
50
+ - `parix database create <database>`: create a database in the active organization, using defaults or explicit `--provider`, `--region`, `--cluster-config-id`, `--cluster-size-id`, `--storage-tier-id`, and `--storage-gb` inputs
51
+ - `parix database list`: list databases in the active organization, including database IDs, with optional `--search`, `--limit`, and `--json`
52
+ - `parix database info <database-id>`: show database metadata, profile state, gateway URL, latest provision job, and metrics summary
53
+ - `parix database remove <database-id>`: queue database removal; use `--yes` for non-interactive runs
54
+
55
+ ## TigerBeetle commands
56
+
57
+ Supported operations:
58
+
59
+ - `create-accounts`
60
+ - `create-transfers`
61
+ - `lookup-accounts`
62
+ - `lookup-transfers`
63
+ - `get-account-transfers`
64
+ - `get-account-balances`
65
+ - `query-accounts`
66
+ - `query-transfers`
67
+
68
+ TigerBeetle commands support both styles:
69
+
70
+ - flag-driven payloads for common workflows such as `--from`, `--to`, `--amount`, `--ledger`, `--code`, repeated `--id`, and repeated `--flag`
71
+ - raw JSON overrides with `--payload '<json>'` or `--file ./payload.json`
72
+
73
+ Operational notes:
74
+
75
+ - database and TB commands use the active organization from the current `parix` token
76
+ - `db:read` and `db:write` OIDC scopes are enforced on the Worker API surface
77
+ - newly provisioned databases may take a short time to become ready for TB operations; the CLI/API includes readiness retries for fresh databases
78
+
79
+ ## Development
80
+
81
+ ```bash
82
+ bun install
83
+ bun run build
84
+ bun run test
85
+ bun run typecheck
86
+ bun run cli -- auth login --base-url http://localhost:5173
87
+ bun run cli -- auth status --base-url http://localhost:5173
88
+ bun run cli -- api /api/v1/session --base-url http://localhost:5173
89
+ bun run cli -- database catalog --json
90
+ bun run cli -- database create demo-ledger --provider gcp --region asia-southeast1 --json
91
+ bun run cli -- database list --json
92
+ bun run cli -- database info <database-id>
93
+ bun run cli -- database remove <database-id> --yes --json
94
+ bun run cli -- tb query-accounts <database-id> --limit 10 --json
95
+ bun run cli -- tb create-transfers <database-id> --from 1000 --to 1001 --amount 1 --ledger 1 --code 1
96
+ ```
97
+
98
+ By default, the CLI targets `https://parix.io`. You can override the target app URL with `--base-url`
99
+ or `PARIX_BASE_URL`.
100
+
101
+ ## Publishing
102
+
103
+ ```bash
104
+ bun run publish:check
105
+ bun run publish:npm
106
+ ```
107
+
108
+ `publish:check` runs formatting, linting, tests, type checking, a fresh build, and `npm pack --dry-run` using a repo-local npm cache directory. `publish:npm` publishes the package publicly to npmjs.com using that same local cache.
109
+
110
+ ## GitHub Actions
111
+
112
+ - `.github/workflows/ci.yml` runs lint, tests, typecheck, build, and `npm pack --dry-run` on pushes and pull requests
113
+ - `.github/workflows/publish.yml` publishes to npm when a GitHub Release is published, using the `NPM_ACCESS_TOKEN` secret, and uploads the published npm tarball to the GitHub Release
114
+
115
+ Recommended release flow:
116
+
117
+ ```bash
118
+ # bump version in package.json
119
+ git tag v0.1.2
120
+ git push origin main --tags
121
+ ```
122
+
123
+ Then publish a GitHub Release from tag `v0.1.2`. The publish workflow verifies that the release tag matches `package.json`, packs the npm tarball, publishes that tarball to npm, and uploads the same `.tgz` as a GitHub Release asset.
package/dist/cli.cjs ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const prompts_1 = require("@clack/prompts");
5
+ const commander_1 = require("commander");
6
+ const node_module_1 = require("node:module");
7
+ const api_1 = require("./commands/api.cjs");
8
+ const auth_1 = require("./commands/auth.cjs");
9
+ const database_1 = require("./commands/database.cjs");
10
+ const tb_1 = require("./commands/tb.cjs");
11
+ const loadPackageJson = (0, node_module_1.createRequire)(require("url").pathToFileURL(__filename));
12
+ const packageJson = loadPackageJson('../package.json');
13
+ async function main() {
14
+ const program = new commander_1.Command();
15
+ program
16
+ .name('parix')
17
+ .version(packageJson.version, '-v, --version')
18
+ .description('Parix command line interface')
19
+ .showHelpAfterError()
20
+ .addCommand((0, auth_1.createAuthCommand)())
21
+ .addCommand((0, api_1.createApiCommand)())
22
+ .addCommand((0, database_1.createDatabaseCommand)())
23
+ .addCommand((0, tb_1.createTbCommand)());
24
+ await program.parseAsync(process.argv);
25
+ }
26
+ main().catch((error) => {
27
+ const message = error instanceof Error ? error.message : String(error);
28
+ prompts_1.log.error(message);
29
+ process.exitCode = 1;
30
+ });
package/dist/cli.d.cts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import { log } from '@clack/prompts';
3
+ import { Command } from 'commander';
4
+ import { createRequire } from 'node:module';
5
+ import { createApiCommand } from "./commands/api.js";
6
+ import { createAuthCommand } from "./commands/auth.js";
7
+ import { createDatabaseCommand } from "./commands/database.js";
8
+ import { createTbCommand } from "./commands/tb.js";
9
+ const loadPackageJson = createRequire(import.meta.url);
10
+ const packageJson = loadPackageJson('../package.json');
11
+ async function main() {
12
+ const program = new Command();
13
+ program
14
+ .name('parix')
15
+ .version(packageJson.version, '-v, --version')
16
+ .description('Parix command line interface')
17
+ .showHelpAfterError()
18
+ .addCommand(createAuthCommand())
19
+ .addCommand(createApiCommand())
20
+ .addCommand(createDatabaseCommand())
21
+ .addCommand(createTbCommand());
22
+ await program.parseAsync(process.argv);
23
+ }
24
+ main().catch((error) => {
25
+ const message = error instanceof Error ? error.message : String(error);
26
+ log.error(message);
27
+ process.exitCode = 1;
28
+ });
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createApiCommand = createApiCommand;
4
+ const prompts_1 = require("@clack/prompts");
5
+ const commander_1 = require("commander");
6
+ const api_client_1 = require("../lib/api-client.cjs");
7
+ const config_1 = require("../lib/config.cjs");
8
+ const session_1 = require("../lib/session.cjs");
9
+ function createApiCommand() {
10
+ return new commander_1.Command('api')
11
+ .description('Make an authenticated API request with the stored bearer token')
12
+ .argument('<path>', 'Relative API path or absolute URL')
13
+ .option('-X, --method <method>', 'HTTP method', 'GET')
14
+ .option('-d, --body <body>', 'Raw request body')
15
+ .option('-H, --header <header>', 'Additional header in key:value form', collectValues, [])
16
+ .option('-b, --base-url <url>', 'Override the stored base URL')
17
+ .action(async (requestPath, options) => {
18
+ await handleApiRequest(requestPath, options);
19
+ });
20
+ }
21
+ async function handleApiRequest(requestPath, options) {
22
+ const storedSession = await (0, session_1.readStoredSession)();
23
+ if (!storedSession) {
24
+ throw new Error('No local session found. Run `parix auth login` first.');
25
+ }
26
+ const baseUrl = (0, config_1.resolveBaseUrl)(options.baseUrl, storedSession);
27
+ const headers = parseHeaders(options.header ?? []);
28
+ const body = options.body ?? undefined;
29
+ if (body && !hasHeader(headers, 'content-type')) {
30
+ headers.set('content-type', 'application/json');
31
+ }
32
+ const client = new api_client_1.ApiClient({
33
+ session: {
34
+ ...storedSession,
35
+ baseUrl,
36
+ },
37
+ });
38
+ const response = await client.request(requestPath, {
39
+ method: options.method ?? 'GET',
40
+ headers,
41
+ body,
42
+ });
43
+ const contentType = response.headers.get('content-type') ?? 'unknown';
44
+ const payload = await response.text();
45
+ (0, prompts_1.note)([`Status: ${response.status} ${response.statusText}`, `Content-Type: ${contentType}`].join('\n'), 'Response');
46
+ if (contentType.includes('application/json') && payload.length > 0) {
47
+ try {
48
+ prompts_1.log.message(JSON.stringify(JSON.parse(payload), null, 2));
49
+ (0, prompts_1.outro)('Request completed');
50
+ return;
51
+ }
52
+ catch { }
53
+ }
54
+ if (payload.length > 0) {
55
+ prompts_1.log.message(payload);
56
+ }
57
+ (0, prompts_1.outro)('Request completed');
58
+ }
59
+ function collectValues(value, previous) {
60
+ previous.push(value);
61
+ return previous;
62
+ }
63
+ function parseHeaders(values) {
64
+ const headers = new Headers();
65
+ for (const value of values) {
66
+ const separatorIndex = value.indexOf(':');
67
+ if (separatorIndex <= 0) {
68
+ throw new Error(`Invalid header format: ${value}. Use key:value.`);
69
+ }
70
+ const key = value.slice(0, separatorIndex).trim();
71
+ const headerValue = value.slice(separatorIndex + 1).trim();
72
+ if (!key) {
73
+ throw new Error(`Invalid header name: ${value}`);
74
+ }
75
+ headers.set(key, headerValue);
76
+ }
77
+ return headers;
78
+ }
79
+ function hasHeader(headers, name) {
80
+ return headers.has(name) || headers.has(name.toLowerCase());
81
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function createApiCommand(): Command;
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function createApiCommand(): Command;
@@ -0,0 +1,78 @@
1
+ import { log, note, outro } from '@clack/prompts';
2
+ import { Command } from 'commander';
3
+ import { ApiClient } from "../lib/api-client.js";
4
+ import { resolveBaseUrl } from "../lib/config.js";
5
+ import { readStoredSession } from "../lib/session.js";
6
+ export function createApiCommand() {
7
+ return new Command('api')
8
+ .description('Make an authenticated API request with the stored bearer token')
9
+ .argument('<path>', 'Relative API path or absolute URL')
10
+ .option('-X, --method <method>', 'HTTP method', 'GET')
11
+ .option('-d, --body <body>', 'Raw request body')
12
+ .option('-H, --header <header>', 'Additional header in key:value form', collectValues, [])
13
+ .option('-b, --base-url <url>', 'Override the stored base URL')
14
+ .action(async (requestPath, options) => {
15
+ await handleApiRequest(requestPath, options);
16
+ });
17
+ }
18
+ async function handleApiRequest(requestPath, options) {
19
+ const storedSession = await readStoredSession();
20
+ if (!storedSession) {
21
+ throw new Error('No local session found. Run `parix auth login` first.');
22
+ }
23
+ const baseUrl = resolveBaseUrl(options.baseUrl, storedSession);
24
+ const headers = parseHeaders(options.header ?? []);
25
+ const body = options.body ?? undefined;
26
+ if (body && !hasHeader(headers, 'content-type')) {
27
+ headers.set('content-type', 'application/json');
28
+ }
29
+ const client = new ApiClient({
30
+ session: {
31
+ ...storedSession,
32
+ baseUrl,
33
+ },
34
+ });
35
+ const response = await client.request(requestPath, {
36
+ method: options.method ?? 'GET',
37
+ headers,
38
+ body,
39
+ });
40
+ const contentType = response.headers.get('content-type') ?? 'unknown';
41
+ const payload = await response.text();
42
+ note([`Status: ${response.status} ${response.statusText}`, `Content-Type: ${contentType}`].join('\n'), 'Response');
43
+ if (contentType.includes('application/json') && payload.length > 0) {
44
+ try {
45
+ log.message(JSON.stringify(JSON.parse(payload), null, 2));
46
+ outro('Request completed');
47
+ return;
48
+ }
49
+ catch { }
50
+ }
51
+ if (payload.length > 0) {
52
+ log.message(payload);
53
+ }
54
+ outro('Request completed');
55
+ }
56
+ function collectValues(value, previous) {
57
+ previous.push(value);
58
+ return previous;
59
+ }
60
+ function parseHeaders(values) {
61
+ const headers = new Headers();
62
+ for (const value of values) {
63
+ const separatorIndex = value.indexOf(':');
64
+ if (separatorIndex <= 0) {
65
+ throw new Error(`Invalid header format: ${value}. Use key:value.`);
66
+ }
67
+ const key = value.slice(0, separatorIndex).trim();
68
+ const headerValue = value.slice(separatorIndex + 1).trim();
69
+ if (!key) {
70
+ throw new Error(`Invalid header name: ${value}`);
71
+ }
72
+ headers.set(key, headerValue);
73
+ }
74
+ return headers;
75
+ }
76
+ function hasHeader(headers, name) {
77
+ return headers.has(name) || headers.has(name.toLowerCase());
78
+ }
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createAuthCommand = createAuthCommand;
4
+ const prompts_1 = require("@clack/prompts");
5
+ const commander_1 = require("commander");
6
+ const config_1 = require("../lib/config.cjs");
7
+ const loopback_server_1 = require("../lib/loopback-server.cjs");
8
+ const oauth_1 = require("../lib/oauth.cjs");
9
+ const oauth_api_1 = require("../lib/oauth-api.cjs");
10
+ const oauth_session_1 = require("../lib/oauth-session.cjs");
11
+ const open_url_1 = require("../lib/open-url.cjs");
12
+ const session_1 = require("../lib/session.cjs");
13
+ const DEFAULT_OIDC_PROMPT = 'consent';
14
+ function createAuthCommand() {
15
+ const auth = new commander_1.Command('auth').description('Authenticate with Parix');
16
+ auth
17
+ .command('login')
18
+ .description('Sign in with OAuth consent and PKCE')
19
+ .option('-b, --base-url <url>', 'Parix base URL')
20
+ .option('-p, --port <port>', 'Loopback callback port', config_1.parsePortOption)
21
+ .option('--prompt <prompt>', 'OAuth prompt override', DEFAULT_OIDC_PROMPT)
22
+ .action(async (options) => {
23
+ await handleLogin(options);
24
+ });
25
+ auth
26
+ .command('status')
27
+ .description('Show the current authenticated session')
28
+ .option('-b, --base-url <url>', 'Override the stored base URL')
29
+ .action(async (options) => {
30
+ await handleStatus(options);
31
+ });
32
+ auth
33
+ .command('logout')
34
+ .description('Remove the local session file')
35
+ .action(async () => {
36
+ await (0, session_1.clearStoredSession)();
37
+ (0, prompts_1.outro)(`Removed local session at ${session_1.SESSION_FILE_PATH}`);
38
+ });
39
+ return auth;
40
+ }
41
+ async function handleLogin(options) {
42
+ (0, prompts_1.intro)('Parix sign in');
43
+ const storedSession = await (0, session_1.readStoredSession)();
44
+ const baseUrl = (0, config_1.resolveBaseUrl)(options.baseUrl, storedSession, { useStoredSession: false });
45
+ const loopbackServer = await (0, loopback_server_1.startLoopbackServer)({
46
+ callbackPath: oauth_1.PARIX_LOCAL_CALLBACK_PATH,
47
+ port: options.port,
48
+ successPage: {
49
+ redirectUrl: new URL(oauth_1.CLI_OAUTH_CONSENT_GRANTED_PATH, baseUrl).toString(),
50
+ },
51
+ timeoutMs: config_1.LOGIN_TIMEOUT_MS,
52
+ });
53
+ try {
54
+ const pkcePair = (0, oauth_1.createPkcePair)();
55
+ const state = (0, oauth_1.buildCliState)(loopbackServer.callbackUrl);
56
+ loopbackServer.setState(state);
57
+ const browserUrl = (0, oauth_1.buildAuthorizeUrl)({
58
+ baseUrl,
59
+ codeChallenge: pkcePair.codeChallenge,
60
+ prompt: options.prompt,
61
+ state,
62
+ });
63
+ const prepareSpinner = (0, prompts_1.spinner)();
64
+ prepareSpinner.start('Preparing browser sign-in');
65
+ prepareSpinner.stop('Browser sign-in ready');
66
+ const opened = await (0, open_url_1.openUrlInBrowser)(browserUrl);
67
+ (0, prompts_1.note)(opened ? browserUrl : `Open this URL manually:\n${browserUrl}`, opened ? 'Opened browser URL' : 'Manual browser URL');
68
+ const waitSpinner = (0, prompts_1.spinner)();
69
+ waitSpinner.start('Waiting for browser callback');
70
+ const callbackResult = await loopbackServer.waitForResult();
71
+ waitSpinner.stop('Received browser callback');
72
+ const tokenSet = await (0, oauth_api_1.exchangeAuthorizationCode)({
73
+ baseUrl,
74
+ code: callbackResult.code,
75
+ codeVerifier: pkcePair.codeVerifier,
76
+ });
77
+ const nextSession = await (0, oauth_session_1.createStoredSession)({
78
+ baseUrl,
79
+ createdAt: storedSession?.createdAt,
80
+ tokenSet,
81
+ });
82
+ await (0, session_1.writeStoredSession)(nextSession);
83
+ (0, prompts_1.note)(session_1.SESSION_FILE_PATH, 'Stored session file');
84
+ (0, prompts_1.outro)(`Signed in as ${formatUserLabel(nextSession.user)}`);
85
+ }
86
+ finally {
87
+ await loopbackServer.close().catch(() => { });
88
+ }
89
+ }
90
+ async function handleStatus(options) {
91
+ (0, prompts_1.intro)('Parix auth status');
92
+ const storedSession = await (0, session_1.readStoredSession)();
93
+ if (!storedSession) {
94
+ prompts_1.log.warn(`No local session found at ${session_1.SESSION_FILE_PATH}`);
95
+ process.exitCode = 1;
96
+ return;
97
+ }
98
+ const baseUrl = (0, config_1.resolveBaseUrl)(options.baseUrl, storedSession);
99
+ try {
100
+ const refreshedSession = await (0, oauth_session_1.hydrateStoredSessionOrganization)(await (0, oauth_session_1.ensureFreshSession)({
101
+ ...storedSession,
102
+ baseUrl,
103
+ }));
104
+ const userInfo = await (0, oauth_api_1.fetchOAuthUserInfo)({
105
+ accessToken: refreshedSession.accessToken,
106
+ baseUrl,
107
+ });
108
+ (0, prompts_1.note)([
109
+ `User: ${userInfo.email ?? formatUserLabel(refreshedSession.user)}`,
110
+ `Base URL: ${baseUrl}`,
111
+ `Expires: ${refreshedSession.accessTokenExpiresAt}`,
112
+ `Scopes: ${refreshedSession.scopes.join(', ')}`,
113
+ `Organization: ${refreshedSession.organization.name ?? 'none'}`,
114
+ `Organization slug: ${refreshedSession.organization.slug ?? 'none'}`,
115
+ `Member role: ${refreshedSession.organization.memberRole ?? 'none'}`,
116
+ `Session file: ${session_1.SESSION_FILE_PATH}`,
117
+ ].join('\n'), 'Authenticated session');
118
+ (0, prompts_1.outro)('Session is valid');
119
+ }
120
+ catch (error) {
121
+ const message = error instanceof Error ? error.message : String(error);
122
+ prompts_1.log.warn(`Stored session is invalid: ${message}`);
123
+ process.exitCode = 1;
124
+ }
125
+ }
126
+ function formatUserLabel(user) {
127
+ return user.email ?? user.name ?? user.id;
128
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function createAuthCommand(): Command;
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function createAuthCommand(): Command;
@@ -0,0 +1,125 @@
1
+ import { intro, log, note, outro, spinner } from '@clack/prompts';
2
+ import { Command } from 'commander';
3
+ import { LOGIN_TIMEOUT_MS, parsePortOption, resolveBaseUrl } from "../lib/config.js";
4
+ import { startLoopbackServer } from "../lib/loopback-server.js";
5
+ import { buildAuthorizeUrl, buildCliState, CLI_OAUTH_CONSENT_GRANTED_PATH, createPkcePair, PARIX_LOCAL_CALLBACK_PATH, } from "../lib/oauth.js";
6
+ import { exchangeAuthorizationCode, fetchOAuthUserInfo } from "../lib/oauth-api.js";
7
+ import { createStoredSession, ensureFreshSession, hydrateStoredSessionOrganization } from "../lib/oauth-session.js";
8
+ import { openUrlInBrowser } from "../lib/open-url.js";
9
+ import { clearStoredSession, readStoredSession, SESSION_FILE_PATH, writeStoredSession } from "../lib/session.js";
10
+ const DEFAULT_OIDC_PROMPT = 'consent';
11
+ export function createAuthCommand() {
12
+ const auth = new Command('auth').description('Authenticate with Parix');
13
+ auth
14
+ .command('login')
15
+ .description('Sign in with OAuth consent and PKCE')
16
+ .option('-b, --base-url <url>', 'Parix base URL')
17
+ .option('-p, --port <port>', 'Loopback callback port', parsePortOption)
18
+ .option('--prompt <prompt>', 'OAuth prompt override', DEFAULT_OIDC_PROMPT)
19
+ .action(async (options) => {
20
+ await handleLogin(options);
21
+ });
22
+ auth
23
+ .command('status')
24
+ .description('Show the current authenticated session')
25
+ .option('-b, --base-url <url>', 'Override the stored base URL')
26
+ .action(async (options) => {
27
+ await handleStatus(options);
28
+ });
29
+ auth
30
+ .command('logout')
31
+ .description('Remove the local session file')
32
+ .action(async () => {
33
+ await clearStoredSession();
34
+ outro(`Removed local session at ${SESSION_FILE_PATH}`);
35
+ });
36
+ return auth;
37
+ }
38
+ async function handleLogin(options) {
39
+ intro('Parix sign in');
40
+ const storedSession = await readStoredSession();
41
+ const baseUrl = resolveBaseUrl(options.baseUrl, storedSession, { useStoredSession: false });
42
+ const loopbackServer = await startLoopbackServer({
43
+ callbackPath: PARIX_LOCAL_CALLBACK_PATH,
44
+ port: options.port,
45
+ successPage: {
46
+ redirectUrl: new URL(CLI_OAUTH_CONSENT_GRANTED_PATH, baseUrl).toString(),
47
+ },
48
+ timeoutMs: LOGIN_TIMEOUT_MS,
49
+ });
50
+ try {
51
+ const pkcePair = createPkcePair();
52
+ const state = buildCliState(loopbackServer.callbackUrl);
53
+ loopbackServer.setState(state);
54
+ const browserUrl = buildAuthorizeUrl({
55
+ baseUrl,
56
+ codeChallenge: pkcePair.codeChallenge,
57
+ prompt: options.prompt,
58
+ state,
59
+ });
60
+ const prepareSpinner = spinner();
61
+ prepareSpinner.start('Preparing browser sign-in');
62
+ prepareSpinner.stop('Browser sign-in ready');
63
+ const opened = await openUrlInBrowser(browserUrl);
64
+ note(opened ? browserUrl : `Open this URL manually:\n${browserUrl}`, opened ? 'Opened browser URL' : 'Manual browser URL');
65
+ const waitSpinner = spinner();
66
+ waitSpinner.start('Waiting for browser callback');
67
+ const callbackResult = await loopbackServer.waitForResult();
68
+ waitSpinner.stop('Received browser callback');
69
+ const tokenSet = await exchangeAuthorizationCode({
70
+ baseUrl,
71
+ code: callbackResult.code,
72
+ codeVerifier: pkcePair.codeVerifier,
73
+ });
74
+ const nextSession = await createStoredSession({
75
+ baseUrl,
76
+ createdAt: storedSession?.createdAt,
77
+ tokenSet,
78
+ });
79
+ await writeStoredSession(nextSession);
80
+ note(SESSION_FILE_PATH, 'Stored session file');
81
+ outro(`Signed in as ${formatUserLabel(nextSession.user)}`);
82
+ }
83
+ finally {
84
+ await loopbackServer.close().catch(() => { });
85
+ }
86
+ }
87
+ async function handleStatus(options) {
88
+ intro('Parix auth status');
89
+ const storedSession = await readStoredSession();
90
+ if (!storedSession) {
91
+ log.warn(`No local session found at ${SESSION_FILE_PATH}`);
92
+ process.exitCode = 1;
93
+ return;
94
+ }
95
+ const baseUrl = resolveBaseUrl(options.baseUrl, storedSession);
96
+ try {
97
+ const refreshedSession = await hydrateStoredSessionOrganization(await ensureFreshSession({
98
+ ...storedSession,
99
+ baseUrl,
100
+ }));
101
+ const userInfo = await fetchOAuthUserInfo({
102
+ accessToken: refreshedSession.accessToken,
103
+ baseUrl,
104
+ });
105
+ note([
106
+ `User: ${userInfo.email ?? formatUserLabel(refreshedSession.user)}`,
107
+ `Base URL: ${baseUrl}`,
108
+ `Expires: ${refreshedSession.accessTokenExpiresAt}`,
109
+ `Scopes: ${refreshedSession.scopes.join(', ')}`,
110
+ `Organization: ${refreshedSession.organization.name ?? 'none'}`,
111
+ `Organization slug: ${refreshedSession.organization.slug ?? 'none'}`,
112
+ `Member role: ${refreshedSession.organization.memberRole ?? 'none'}`,
113
+ `Session file: ${SESSION_FILE_PATH}`,
114
+ ].join('\n'), 'Authenticated session');
115
+ outro('Session is valid');
116
+ }
117
+ catch (error) {
118
+ const message = error instanceof Error ? error.message : String(error);
119
+ log.warn(`Stored session is invalid: ${message}`);
120
+ process.exitCode = 1;
121
+ }
122
+ }
123
+ function formatUserLabel(user) {
124
+ return user.email ?? user.name ?? user.id;
125
+ }