oathbound 0.6.0 → 0.7.0

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/auth.ts CHANGED
@@ -1,14 +1,16 @@
1
1
  import { createClient } from '@supabase/supabase-js';
2
+ import { spawn } from 'node:child_process';
2
3
  import {
3
4
  mkdirSync, writeFileSync, readFileSync, unlinkSync, existsSync,
4
5
  } from 'node:fs';
5
6
  import { join } from 'node:path';
6
7
  import { homedir } from 'node:os';
7
- import { intro, outro, text, password, cancel, isCancel } from '@clack/prompts';
8
+ import { intro, outro } from '@clack/prompts';
8
9
  import { BRAND, GREEN, DIM, BOLD, RESET, fail, spinner } from './ui';
9
10
 
10
11
  const SUPABASE_URL = 'https://mjnfqagwuewhgwbtrdgs.supabase.co';
11
12
  const SUPABASE_ANON_KEY = 'sb_publishable_T-rk0azNRqAMLLGCyadyhQ_ulk9685n';
13
+ const API_BASE = process.env.OATHBOUND_API_URL ?? 'https://oathbound.ai';
12
14
 
13
15
  const AUTH_DIR = join(homedir(), '.oathbound');
14
16
  const AUTH_FILE = join(AUTH_DIR, 'auth.json');
@@ -37,51 +39,117 @@ function clearSession(): void {
37
39
  if (existsSync(AUTH_FILE)) unlinkSync(AUTH_FILE);
38
40
  }
39
41
 
42
+ function openBrowser(url: string): void {
43
+ const cmd = process.platform === 'darwin' ? 'open'
44
+ : process.platform === 'win32' ? 'cmd'
45
+ : 'xdg-open';
46
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
47
+ try {
48
+ spawn(cmd, args, { stdio: 'ignore', detached: true }).unref();
49
+ } catch {
50
+ // URL is already printed — user can open manually
51
+ }
52
+ }
53
+
54
+ const SUCCESS_HTML = `<!DOCTYPE html>
55
+ <html><head><title>Oathbound CLI</title></head>
56
+ <body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#0a0a0a;color:#e5e5e5">
57
+ <div style="text-align:center">
58
+ <h1 style="color:#3fa8a4">&#10003; Logged in</h1>
59
+ <p>You can close this tab and return to your terminal.</p>
60
+ </div></body></html>`;
61
+
62
+ const ERROR_HTML = `<!DOCTYPE html>
63
+ <html><head><title>Oathbound CLI</title></head>
64
+ <body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#0a0a0a;color:#e5e5e5">
65
+ <div style="text-align:center">
66
+ <h1 style="color:#ef4444">Login failed</h1>
67
+ <p>Missing session tokens. Please try again.</p>
68
+ </div></body></html>`;
69
+
40
70
  export async function login(): Promise<void> {
41
71
  intro(BRAND);
42
72
 
43
- const email = await text({
44
- message: 'Email:',
45
- validate(value) {
46
- if (!value.includes('@')) return 'Please enter a valid email';
73
+ let resolveSession: (s: StoredSession) => void;
74
+ let rejectSession: (e: Error) => void;
75
+ const sessionPromise = new Promise<StoredSession>((res, rej) => {
76
+ resolveSession = res;
77
+ rejectSession = rej;
78
+ });
79
+
80
+ const server = Bun.serve({
81
+ port: 0,
82
+ fetch(req) {
83
+ const url = new URL(req.url);
84
+ if (url.pathname !== '/callback') {
85
+ return new Response('Not found', { status: 404 });
86
+ }
87
+
88
+ const accessToken = url.searchParams.get('access_token');
89
+ const refreshToken = url.searchParams.get('refresh_token');
90
+ const expiresAt = url.searchParams.get('expires_at');
91
+
92
+ if (!accessToken || !refreshToken || !expiresAt) {
93
+ rejectSession!(new Error('Missing session tokens from callback'));
94
+ setTimeout(() => server.stop(), 500);
95
+ return new Response(ERROR_HTML, { headers: { 'Content-Type': 'text/html' } });
96
+ }
97
+
98
+ resolveSession!({
99
+ access_token: accessToken,
100
+ refresh_token: refreshToken,
101
+ expires_at: Number(expiresAt),
102
+ });
103
+
104
+ setTimeout(() => server.stop(), 500);
105
+ return new Response(SUCCESS_HTML, { headers: { 'Content-Type': 'text/html' } });
47
106
  },
48
107
  });
49
- if (isCancel(email)) { cancel('Login cancelled.'); process.exit(0); }
50
108
 
51
- const pw = await password({ message: 'Password:' });
52
- if (isCancel(pw)) { cancel('Login cancelled.'); process.exit(0); }
109
+ const port = server.port;
110
+ const loginUrl = `${API_BASE}/cli-login?port=${port}`;
53
111
 
54
- const spin = spinner('Signing in...');
112
+ console.log(`${DIM} Opening browser...${RESET}`);
113
+ console.log(`${DIM} If it doesn't open, visit:${RESET}`);
114
+ console.log(`${DIM} ${loginUrl}${RESET}\n`);
55
115
 
56
- const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
57
- auth: { autoRefreshToken: false, persistSession: false },
58
- });
116
+ openBrowser(loginUrl);
59
117
 
60
- const { data, error } = await supabase.auth.signInWithPassword({
61
- email: email as string,
62
- password: pw as string,
63
- });
118
+ const spin = spinner('Waiting for login...');
64
119
 
65
- spin.stop();
120
+ const timeout = new Promise<never>((_, rej) =>
121
+ setTimeout(() => rej(new Error('Login timed out (2 minutes). Please try again.')), 120_000),
122
+ );
66
123
 
67
- if (error || !data.session) {
68
- fail('Login failed', error?.message ?? 'No session returned');
124
+ let session: StoredSession;
125
+ try {
126
+ session = await Promise.race([sessionPromise, timeout]);
127
+ } catch (err) {
128
+ spin.stop();
129
+ server.stop();
130
+ fail('Login failed', err instanceof Error ? err.message : 'Unknown error');
69
131
  }
70
132
 
71
- saveSession({
72
- access_token: data.session.access_token,
73
- refresh_token: data.session.refresh_token,
74
- expires_at: data.session.expires_at!,
75
- });
133
+ spin.stop();
134
+ saveSession(session);
76
135
 
77
136
  // Get username for display
78
- const { data: userRecord } = await supabase
79
- .from('users')
80
- .select('username')
81
- .eq('user_id', data.user.id)
82
- .single();
137
+ const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
138
+ global: { headers: { Authorization: `Bearer ${session.access_token}` } },
139
+ auth: { autoRefreshToken: false, persistSession: false },
140
+ });
141
+
142
+ const { data: { user } } = await supabase.auth.getUser();
143
+ let displayName = user?.email ?? 'unknown';
144
+ if (user) {
145
+ const { data: userRecord } = await supabase
146
+ .from('users')
147
+ .select('username')
148
+ .eq('user_id', user.id)
149
+ .single();
150
+ if (userRecord?.username) displayName = userRecord.username;
151
+ }
83
152
 
84
- const displayName = userRecord?.username ?? data.user.email ?? 'unknown';
85
153
  outro(`Logged in as ${BOLD}${displayName}${RESET}`);
86
154
  }
87
155
 
package/bin/cli.cjs CHANGED
File without changes
package/cli.ts CHANGED
@@ -25,7 +25,7 @@ export { stripJsoncComments, writeOathboundConfig, mergeClaudeSettings, type Mer
25
25
  export { isNewer } from './update';
26
26
  export { installDevDependency, type InstallResult, setup, addPrepareScript, type PrepareResult };
27
27
 
28
- const VERSION = '0.6.0';
28
+ const VERSION = '0.7.0';
29
29
 
30
30
  // --- Supabase ---
31
31
  const SUPABASE_URL = 'https://mjnfqagwuewhgwbtrdgs.supabase.co';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oathbound",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Install verified Claude Code skills from the Oath Bound registry",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Josh Anderson",
package/push.ts CHANGED
@@ -24,12 +24,20 @@ export async function push(pathArg?: string): Promise<void> {
24
24
 
25
25
  // Parse SKILL.md frontmatter to extract metadata
26
26
  const skillMdFile = rawFiles.find(f => f.path === 'SKILL.md');
27
- if (!skillMdFile) fail('SKILL.md not found in collected files');
27
+ if (!skillMdFile) {
28
+ fail('SKILL.md not found in collected files');
29
+ }
28
30
 
29
31
  const meta = parseFrontmatter(skillMdFile.content.toString('utf-8'));
30
- if (!meta.name) fail('SKILL.md frontmatter missing: name');
31
- if (!meta.description) fail('SKILL.md frontmatter missing: description');
32
- if (!meta.license) fail('SKILL.md frontmatter missing: license');
32
+ if (!meta.name) {
33
+ fail('SKILL.md frontmatter missing: name');
34
+ }
35
+ if (!meta.description) {
36
+ fail('SKILL.md frontmatter missing: description');
37
+ }
38
+ if (!meta.license) {
39
+ fail('SKILL.md frontmatter missing: license');
40
+ }
33
41
 
34
42
  // Build files array with root dir prefix (API expects rootDir/path format)
35
43
  const files = rawFiles.map(f => ({
package/ui.ts CHANGED
@@ -8,7 +8,13 @@ export const DIM = USE_COLOR ? '\x1b[2m' : '';
8
8
  export const BOLD = USE_COLOR ? '\x1b[1m' : '';
9
9
  export const RESET = USE_COLOR ? '\x1b[0m' : '';
10
10
 
11
- export const BRAND = `${TEAL}${BOLD}🛡️ oathbound${RESET}`;
11
+ const BRAND_MARKS = ['✦', '✧', '✿', '⬡'];
12
+
13
+ export function brand(): string {
14
+ return `${TEAL}${BOLD}⬡ oathbound${RESET}`;
15
+ }
16
+
17
+ export const BRAND = brand();
12
18
 
13
19
  export function usage(exitCode = 1): never {
14
20
  console.log(`
@@ -41,13 +47,13 @@ export function fail(message: string, detail?: string): never {
41
47
  }
42
48
 
43
49
  export function spinner(text: string): { stop: () => void } {
44
- const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
45
50
  let i = 0;
46
- process.stdout.write(`${TEAL} ${frames[0]} ${text}${RESET}`);
51
+ const render = () => `\r${TEAL}${BOLD} ${BRAND_MARKS[i % BRAND_MARKS.length]} ${RESET}${TEAL}${text}${RESET}`;
52
+ process.stdout.write(render());
47
53
  const interval = setInterval(() => {
48
- i = (i + 1) % frames.length;
49
- process.stdout.write(`\r${TEAL} ${frames[i]} ${text}${RESET}`);
50
- }, 80);
54
+ i++;
55
+ process.stdout.write(render());
56
+ }, 150);
51
57
  return {
52
58
  stop() {
53
59
  clearInterval(interval);