commitshow 0.3.14 → 0.3.15

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.
@@ -1,16 +1,125 @@
1
- // Device flow placeholder. Needs the /cli/link web page + token-exchange
2
- // Edge Function on the server side (V1 backend work · see CLAUDE.md §15-C.4
3
- // rollout). Until that lands, we fail fast with a clear message so the user
4
- // knows their audit/status commands still work read-only.
1
+ // commitshow login — device-flow authorization.
2
+ //
3
+ // Default flow:
4
+ // 1. POST /functions/v1/cli-link-init · receive { code, poll_token, verification_url }
5
+ // 2. Print the code + URL · open the URL in the user's browser (unless --no-open)
6
+ // 3. Poll /functions/v1/cli-link-poll every 2s until 'ok' (token returned),
7
+ // 'expired', or timeout (10 min).
8
+ // 4. Save the api_token + member info to ~/.commitshow/config.json
9
+ //
10
+ // --token mode: skip the browser handshake and accept a pre-minted JWT
11
+ // (useful for headless / CI environments).
12
+ //
13
+ // --no-open: don't auto-launch the browser, just print the URL.
14
+ import { readConfig, writeConfig } from '../lib/config.js';
5
15
  import { c } from '../lib/colors.js';
6
- export async function login(_args) {
16
+ const POLL_INTERVAL_MS = 2000;
17
+ const POLL_TIMEOUT_MS = 10 * 60 * 1000;
18
+ const DEFAULT_BASE_URL = 'https://tekemubwihsjdzittoqf.supabase.co';
19
+ const DEFAULT_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InRla2VtdWJ3aWhzamR6aXR0b3FmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzY0MzQ1NzUsImV4cCI6MjA5MjAxMDU3NX0.n2K-3lFVvlXQx-bV9evdNRSQCtG5oC4uQushxB2ja9Y';
20
+ function baseUrl() {
21
+ return readConfig().base_url ?? DEFAULT_BASE_URL;
22
+ }
23
+ function tryOpen(url) {
24
+ // Best-effort cross-platform open. Failure is silent — user still has
25
+ // the URL printed and can copy/paste manually.
26
+ const cmd = process.platform === 'darwin' ? 'open'
27
+ : process.platform === 'win32' ? 'cmd'
28
+ : 'xdg-open';
29
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
30
+ try {
31
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
32
+ const { spawn } = require('node:child_process');
33
+ spawn(cmd, args, { stdio: 'ignore', detached: true }).unref();
34
+ }
35
+ catch { /* ignore */ }
36
+ }
37
+ async function fetchUser(token) {
38
+ try {
39
+ const res = await fetch(`${baseUrl()}/auth/v1/user`, {
40
+ headers: { apikey: DEFAULT_ANON_KEY, Authorization: `Bearer ${token}` },
41
+ });
42
+ if (!res.ok)
43
+ return null;
44
+ const j = await res.json();
45
+ return { id: j.id, email: j.email ?? null };
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ export async function login(args) {
52
+ const noOpen = args.includes('--no-open');
53
+ const tokenIdx = args.indexOf('--token');
54
+ const presetToken = tokenIdx >= 0 ? args[tokenIdx + 1] : null;
55
+ // Headless / CI path · skip the browser handshake.
56
+ if (presetToken) {
57
+ const user = await fetchUser(presetToken);
58
+ if (!user) {
59
+ console.error(c.scarlet('✗ Token rejected · invalid or expired.'));
60
+ return 1;
61
+ }
62
+ writeConfig({ ...readConfig(), token: presetToken, member_id: user.id, display_name: user.email ?? undefined });
63
+ console.log(c.gold('✓ Logged in · token saved to ~/.commitshow/config.json'));
64
+ return 0;
65
+ }
66
+ // 1. Init the device-flow.
67
+ let init;
68
+ try {
69
+ const res = await fetch(`${baseUrl()}/functions/v1/cli-link-init`, {
70
+ method: 'POST',
71
+ headers: { apikey: DEFAULT_ANON_KEY, Authorization: `Bearer ${DEFAULT_ANON_KEY}`, 'Content-Type': 'application/json' },
72
+ body: '{}',
73
+ });
74
+ init = await res.json();
75
+ if (!res.ok || !init.code || !init.poll_token) {
76
+ console.error(c.scarlet(`✗ Init failed: ${init.error ?? `HTTP ${res.status}`}`));
77
+ return 1;
78
+ }
79
+ }
80
+ catch (e) {
81
+ console.error(c.scarlet(`✗ Init network error: ${e?.message ?? e}`));
82
+ return 1;
83
+ }
84
+ console.log('');
85
+ console.log(c.cream(' Authorize commitshow CLI to act on your account.'));
7
86
  console.log('');
8
- console.log(c.cream('Login is not yet available in 0.1.'));
87
+ console.log(` Verification code: ${c.gold(init.code)}`);
88
+ console.log(` Approve at: ${init.verification_url}`);
9
89
  console.log('');
10
- console.log(c.muted(' Read-only commands (audit, status, whoami) already work against public data.'));
11
- console.log(c.muted(' Write commands (submit, re-audit, install) unlock once the device-flow'));
12
- console.log(c.muted(' endpoint ships — tracked as a V1 item in the roadmap.'));
90
+ console.log(c.dim(' Waiting for approval (10 min timeout)'));
13
91
  console.log('');
14
- console.log(c.dim(' Sign in on the web for now → https://commit.show'));
92
+ if (!noOpen && init.verification_url)
93
+ tryOpen(init.verification_url);
94
+ // 2. Poll until approved / expired / timeout.
95
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
96
+ while (Date.now() < deadline) {
97
+ await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
98
+ try {
99
+ const res = await fetch(`${baseUrl()}/functions/v1/cli-link-poll`, {
100
+ method: 'POST',
101
+ headers: { apikey: DEFAULT_ANON_KEY, Authorization: `Bearer ${DEFAULT_ANON_KEY}`, 'Content-Type': 'application/json' },
102
+ body: JSON.stringify({ poll_token: init.poll_token }),
103
+ });
104
+ const resp = await res.json();
105
+ if (resp.status === 'ok' && resp.api_token) {
106
+ writeConfig({ ...readConfig(), token: resp.api_token, member_id: resp.user_id ?? undefined });
107
+ console.log(c.gold(' ✓ Authorized · token saved to ~/.commitshow/config.json'));
108
+ const user = await fetchUser(resp.api_token);
109
+ if (user?.email)
110
+ writeConfig({ ...readConfig(), display_name: user.email });
111
+ return 0;
112
+ }
113
+ if (resp.status === 'expired' || resp.status === 'consumed') {
114
+ console.error(c.scarlet(` ✗ ${resp.message ?? resp.status}`));
115
+ return 1;
116
+ }
117
+ // status === 'pending' · keep polling.
118
+ }
119
+ catch {
120
+ // transient · keep polling.
121
+ }
122
+ }
123
+ console.error(c.scarlet(' ✗ Timed out waiting for approval (10 min). Re-run commitshow login.'));
15
124
  return 1;
16
125
  }
@@ -1,14 +1,56 @@
1
- import { readConfig } from '../lib/config.js';
1
+ import { readConfig, writeConfig } from '../lib/config.js';
2
2
  import { c } from '../lib/colors.js';
3
- export async function whoami(_args) {
3
+ const DEFAULT_BASE_URL = 'https://tekemubwihsjdzittoqf.supabase.co';
4
+ const DEFAULT_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InRla2VtdWJ3aWhzamR6aXR0b3FmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzY0MzQ1NzUsImV4cCI6MjA5MjAxMDU3NX0.n2K-3lFVvlXQx-bV9evdNRSQCtG5oC4uQushxB2ja9Y';
5
+ async function verifyToken(token) {
6
+ try {
7
+ const res = await fetch(`${DEFAULT_BASE_URL}/auth/v1/user`, {
8
+ headers: { apikey: DEFAULT_ANON_KEY, Authorization: `Bearer ${token}` },
9
+ });
10
+ if (!res.ok)
11
+ return null;
12
+ const j = await res.json();
13
+ return { id: j.id, email: j.email ?? null };
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ export async function whoami(args) {
4
20
  const cfg = readConfig();
5
- if (!cfg.token || !cfg.display_name) {
21
+ // --logout · convenience flag · clears token from local config.
22
+ if (args.includes('--logout')) {
23
+ if (!cfg.token) {
24
+ console.log(c.dim('Already signed out.'));
25
+ return 0;
26
+ }
27
+ const next = { ...cfg };
28
+ delete next.token;
29
+ delete next.member_id;
30
+ delete next.display_name;
31
+ delete next.refresh_token;
32
+ writeConfig(next);
33
+ console.log(c.gold('✓ Signed out · token cleared from ~/.commitshow/config.json'));
34
+ return 0;
35
+ }
36
+ if (!cfg.token) {
6
37
  console.log(c.muted('Not signed in.'));
7
- console.log(c.dim(' Read-only commands still work. Login coming in the next CLI release.'));
38
+ console.log(c.dim(' Run `commitshow login` to authorize a 90-day API token.'));
39
+ return 1;
40
+ }
41
+ // Verify the saved token is still valid (not expired or revoked).
42
+ const user = await verifyToken(cfg.token);
43
+ if (!user) {
44
+ console.log(c.scarlet('✗ Token rejected (expired or revoked).'));
45
+ console.log(c.dim(' Re-run `commitshow login`.'));
8
46
  return 1;
9
47
  }
10
- console.log(c.cream(cfg.display_name));
11
- if (cfg.member_id)
12
- console.log(c.muted(` ${cfg.member_id}`));
48
+ console.log('');
49
+ if (cfg.display_name)
50
+ console.log(` ${c.cream(cfg.display_name)}`);
51
+ if (user.email && user.email !== cfg.display_name)
52
+ console.log(` email: ${user.email}`);
53
+ console.log(` member id: ${user.id}`);
54
+ console.log('');
13
55
  return 0;
14
56
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commitshow",
3
- "version": "0.3.14",
3
+ "version": "0.3.15",
4
4
  "description": "commit.show CLI — audit any vibe-coded project from your terminal.",
5
5
  "type": "module",
6
6
  "bin": {