@lifeaitools/clauth 0.4.0 → 0.5.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.
@@ -1,265 +1,291 @@
1
- // cli/commands/install.js
2
- // clauth install — full provisioning command
3
- // Called automatically by the bootstrap installer, or manually: clauth install
4
- //
5
- // Does:
6
- // 1. Checks prerequisites (git, node, internet)
7
- // 2. Collects Supabase project ref + PAT
8
- // 3. Runs SQL migrations
9
- // 4. Deploys auth-vault Edge Function
10
- // 5. Generates HMAC salt + bootstrap token, stores as project secrets
11
- // 6. Saves local config (Supabase URL + anon key)
12
- // 7. Runs end-to-end test
13
- // 8. Installs Claude skill
14
- // 9. Cleans up
15
- // 10. Prints: "clauth ready. Type clauth --help"
16
-
17
- import { createInterface } from 'readline';
18
- import { randomBytes } from 'crypto';
19
- import { readFileSync, existsSync, mkdirSync, cpSync, rmSync } from 'fs';
20
- import { join, dirname } from 'path';
21
- import { fileURLToPath } from 'url';
22
- import { execSync } from 'child_process';
23
- import Conf from 'conf';
24
- import { getConfOptions } from '../conf-path.js';
25
- import chalk from 'chalk';
26
- import ora from 'ora';
27
-
28
- const __dirname = dirname(fileURLToPath(import.meta.url));
29
- const ROOT = join(__dirname, '..', '..');
30
- const MGMT = 'https://api.supabase.com/v1';
31
- const SKILLS_DIR = process.env.CLAUTH_SKILLS_DIR ||
32
- (process.platform === 'win32'
33
- ? join(process.env.USERPROFILE || '', '.claude', 'skills')
34
- : join(process.env.HOME || '', '.claude', 'skills'));
35
-
36
- // ─────────────────────────────────────────────
37
- // Prompt helpers — single shared readline to
38
- // avoid losing buffered stdin between prompts
39
- // ─────────────────────────────────────────────
40
-
41
- let _rl;
42
- function getRL() {
43
- if (!_rl) {
44
- _rl = createInterface({ input: process.stdin, output: process.stdout });
45
- }
46
- return _rl;
47
- }
48
- function closeRL() {
49
- if (_rl) { _rl.close(); _rl = null; }
50
- }
51
-
52
- function ask(question) {
53
- return new Promise(res => getRL().question(question, a => res(a.trim())));
54
- }
55
-
56
- function askSecret(label) {
57
- // In non-TTY (piped input, CI, Git Bash on Windows), just read a line
58
- return ask(label);
59
- }
60
-
61
- // ─────────────────────────────────────────────
62
- // Supabase Management API
63
- // ─────────────────────────────────────────────
64
-
65
- async function mgmt(pat, method, path, body) {
66
- const res = await fetch(`${MGMT}${path}`, {
67
- method,
68
- headers: { 'Authorization': `Bearer ${pat}`, 'Content-Type': 'application/json' },
69
- body: body ? JSON.stringify(body) : undefined,
70
- });
71
- if (!res.ok) {
72
- const text = await res.text().catch(() => res.statusText);
73
- throw new Error(`${method} ${path} → HTTP ${res.status}: ${text}`);
74
- }
75
- if (res.status === 204) return {};
76
- const text = await res.text();
77
- if (!text) return {};
78
- return JSON.parse(text);
79
- }
80
-
81
- // ─────────────────────────────────────────────
82
- // Main install command
83
- // ─────────────────────────────────────────────
84
-
85
- export async function runInstall(opts = {}) {
86
- console.log(chalk.cyan('\n🔐 clauth install\n'));
87
-
88
- // ── Step 1: Check internet ────────────────
89
- const s1 = ora('Checking connectivity...').start();
90
- try {
91
- await fetch('https://api.supabase.com/v1/projects', {
92
- method: 'GET', headers: { 'Authorization': 'Bearer test' }
93
- });
94
- s1.succeed('Connected');
95
- } catch {
96
- s1.fail('No internet connection. Check your network and retry.');
97
- process.exit(1);
98
- }
99
-
100
- // ── Step 2: Collect credentials ───────────
101
- let ref = opts.ref;
102
- let pat = opts.pat;
103
-
104
- if (!ref || !pat) {
105
- console.log(chalk.cyan('\nYou need two things from Supabase:\n'));
106
- console.log(chalk.gray(' Project ref: last segment of your project URL'));
107
- console.log(chalk.gray(' e.g. supabase.com/dashboard/project/') + chalk.white('uvojezuorjgqzmhhgluu'));
108
- console.log('');
109
- console.log(chalk.gray(' Personal Access Token (PAT):'));
110
- console.log(chalk.gray(' supabase.com/dashboard/account/tokens → Generate new token'));
111
- console.log(chalk.gray(' (This is NOT your anon key or service_role key)\n'));
112
-
113
- if (!ref) ref = await ask(chalk.white('Supabase project ref: '));
114
- if (!pat) pat = await askSecret(chalk.white('Supabase PAT: '));
115
- closeRL();
116
- }
117
-
118
- if (!ref || !pat) {
119
- console.log(chalk.red('\n✗ Both required.\n')); process.exit(1);
120
- }
121
-
122
- // ── Step 3: Verify + fetch keys ──────────
123
- const s3 = ora('Verifying Supabase credentials...').start();
124
- let anon, serviceRole;
125
- try {
126
- const keys = await mgmt(pat, 'GET', `/projects/${ref}/api-keys`);
127
- anon = keys.find(k => k.name === 'anon')?.api_key;
128
- serviceRole = keys.find(k => k.name === 'service_role')?.api_key;
129
- if (!anon || !serviceRole) throw new Error('API keys not found in response');
130
- s3.succeed('Supabase project verified');
131
- } catch (e) {
132
- s3.fail(`Could not connect: ${e.message}`);
133
- console.log(chalk.gray(' Check your project ref and PAT.'));
134
- process.exit(1);
135
- }
136
-
137
- const projectUrl = `https://${ref}.supabase.co`;
138
-
139
- // ── Step 4: Run migrations ────────────────
140
- const s4 = ora('Running database migrations...').start();
141
- const migrations = [
142
- { name: '001_clauth_schema', file: 'supabase/migrations/001_clauth_schema.sql' },
143
- { name: '002_vault_helpers', file: 'supabase/migrations/002_vault_helpers.sql' },
144
- ];
145
- for (const m of migrations) {
146
- const sql = readFileSync(join(ROOT, m.file), 'utf8');
147
- try {
148
- await mgmt(pat, 'POST', `/projects/${ref}/database/query`, { query: sql });
149
- s4.text = `Migrations: ${m.name} ✓`;
150
- } catch (e) {
151
- s4.fail(`Migration ${m.name} failed: ${e.message}`);
152
- process.exit(1);
153
- }
154
- }
155
- s4.succeed('Database migrations applied');
156
-
157
- // ── Step 5: Deploy Edge Function ─────────
158
- const s5 = ora('Deploying auth-vault Edge Function...').start();
159
- const fnSource = readFileSync(join(ROOT, 'supabase/functions/auth-vault/index.ts'), 'utf8');
160
- try {
161
- // Use the /deploy endpoint with multipart/form-data (not the old /functions endpoint)
162
- const formData = new FormData();
163
-
164
- const metadata = {
165
- name: 'auth-vault',
166
- entrypoint_path: 'index.ts',
167
- verify_jwt: true,
168
- };
169
- formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
170
- formData.append('file', new Blob([fnSource], { type: 'application/typescript' }), 'index.ts');
171
-
172
- const deployRes = await fetch(
173
- `${MGMT}/projects/${ref}/functions/deploy?slug=auth-vault`,
174
- {
175
- method: 'POST',
176
- headers: { 'Authorization': `Bearer ${pat}` },
177
- body: formData,
178
- }
179
- );
180
-
181
- if (!deployRes.ok) {
182
- const errText = await deployRes.text().catch(() => deployRes.statusText);
183
- throw new Error(`HTTP ${deployRes.status}: ${errText}`);
184
- }
185
-
186
- s5.succeed('auth-vault Edge Function deployed');
187
- } catch (e) {
188
- s5.warn(`Edge Function deploy failed: ${e.message}`);
189
- console.log(chalk.yellow(' Run manually: supabase functions deploy auth-vault'));
190
- }
191
-
192
- // ── Step 6: Generate + store secrets ─────
193
- const s6 = ora('Generating secrets...').start();
194
- const hmacSalt = randomBytes(32).toString('hex');
195
- const bootstrapToken = randomBytes(16).toString('hex');
196
- try {
197
- await mgmt(pat, 'POST', `/projects/${ref}/secrets`, [
198
- { name: 'CLAUTH_HMAC_SALT', value: hmacSalt },
199
- { name: 'CLAUTH_ADMIN_BOOTSTRAP_TOKEN', value: bootstrapToken },
200
- ]);
201
- s6.succeed('Secrets generated and stored');
202
- } catch (e) {
203
- s6.warn(`Could not store secrets via API: ${e.message}`);
204
- console.log(chalk.yellow('\n Set these manually in Supabase → Settings → Edge Functions → Secrets:'));
205
- console.log(chalk.white(` CLAUTH_HMAC_SALT = ${hmacSalt}`));
206
- console.log(chalk.white(` CLAUTH_ADMIN_BOOTSTRAP_TOKEN = ${bootstrapToken}\n`));
207
- }
208
-
209
- // ── Step 7: Save local config ─────────────
210
- const config = new Conf(getConfOptions());
211
- config.set('supabase_url', projectUrl);
212
- config.set('supabase_anon_key', anon);
213
-
214
- // ── Step 8: End-to-end test ───────────────
215
- const s8 = ora('Running end-to-end test...').start();
216
- try {
217
- // Register a test machine
218
- const testMachine = `install-test-${randomBytes(4).toString('hex')}`;
219
- const regRes = await fetch(`${projectUrl}/functions/v1/auth-vault/register-machine`, {
220
- method: 'POST',
221
- headers: { 'Authorization': `Bearer ${anon}`, 'Content-Type': 'application/json' },
222
- body: JSON.stringify({
223
- machine_hash: testMachine, hmac_seed_hash: 'test',
224
- label: 'install-test', admin_token: bootstrapToken,
225
- }),
226
- });
227
- const reg = await regRes.json();
228
- if (!reg.success) throw new Error(`register-machine: ${JSON.stringify(reg)}`);
229
- s8.succeed('End-to-end test passed — Edge Function is live');
230
- } catch (e) {
231
- s8.warn(`Test failed (Edge Function may still be deploying): ${e.message}`);
232
- console.log(chalk.yellow(' Run clauth test after a minute to verify.'));
233
- }
234
-
235
- // ── Step 9: Install Claude skill ──────────
236
- const s9 = ora('Installing Claude skill...').start();
237
- const skillSrc = join(ROOT, '.clauth-skill');
238
- if (existsSync(skillSrc)) {
239
- try {
240
- const skillDest = join(SKILLS_DIR, 'clauth');
241
- mkdirSync(skillDest, { recursive: true });
242
- cpSync(skillSrc, skillDest, { recursive: true, force: true });
243
- s9.succeed(`Claude skill installed → ${skillDest}`);
244
- } catch (e) {
245
- s9.warn(`Skill install skipped: ${e.message}`);
246
- }
247
- } else {
248
- s9.warn('Skill directory not found — skipping');
249
- }
250
-
251
- // ── Step 10: Done ─────────────────────────
252
- console.log('');
253
- console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
254
- console.log(chalk.green(' ✓ clauth installed and ready'));
255
- console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
256
- console.log('');
257
- console.log(chalk.yellow(' Save this bootstrap token — you need it once for clauth setup:'));
258
- console.log(chalk.white(' ' + bootstrapToken));
259
- console.log('');
260
- console.log(chalk.gray(' (Also stored in Supabase → Settings → Edge Functions → Secrets)'));
261
- console.log('');
262
- console.log(chalk.white(' Next: run') + chalk.cyan(' clauth setup'));
263
- console.log(chalk.gray(' Then:') + chalk.cyan(' clauth test'));
264
- console.log('');
265
- }
1
+ // cli/commands/install.js
2
+ // clauth install — full provisioning command
3
+ // Called automatically by the bootstrap installer, or manually: clauth install
4
+ //
5
+ // Does:
6
+ // 1. Checks prerequisites (git, node, internet)
7
+ // 2. Collects Supabase project ref + PAT
8
+ // 3. Runs SQL migrations
9
+ // 4. Deploys auth-vault Edge Function
10
+ // 5. Generates HMAC salt + bootstrap token, stores as project secrets
11
+ // 6. Saves local config (Supabase URL + anon key)
12
+ // 7. Runs end-to-end test
13
+ // 8. Installs Claude skill
14
+ // 9. Cleans up
15
+ // 10. Prints: "clauth ready. Type clauth --help"
16
+
17
+ import { createInterface } from 'readline';
18
+ import { randomBytes } from 'crypto';
19
+ import { readFileSync, existsSync, mkdirSync, cpSync, rmSync } from 'fs';
20
+ import { join, dirname } from 'path';
21
+ import { fileURLToPath } from 'url';
22
+ import { execSync } from 'child_process';
23
+ import Conf from 'conf';
24
+ import { getConfOptions } from '../conf-path.js';
25
+ import chalk from 'chalk';
26
+ import ora from 'ora';
27
+
28
+ const __dirname = dirname(fileURLToPath(import.meta.url));
29
+ const ROOT = join(__dirname, '..', '..');
30
+ const MGMT = 'https://api.supabase.com/v1';
31
+ const SKILLS_DIR = process.env.CLAUTH_SKILLS_DIR ||
32
+ (process.platform === 'win32'
33
+ ? join(process.env.USERPROFILE || '', '.claude', 'skills')
34
+ : join(process.env.HOME || '', '.claude', 'skills'));
35
+
36
+ // ─────────────────────────────────────────────
37
+ // Prompt helpers — single shared readline to
38
+ // avoid losing buffered stdin between prompts
39
+ // ─────────────────────────────────────────────
40
+
41
+ let _rl;
42
+ function getRL() {
43
+ if (!_rl) {
44
+ _rl = createInterface({ input: process.stdin, output: process.stdout });
45
+ }
46
+ return _rl;
47
+ }
48
+ function closeRL() {
49
+ if (_rl) { _rl.close(); _rl = null; }
50
+ }
51
+
52
+ function ask(question) {
53
+ return new Promise(res => getRL().question(question, a => res(a.trim())));
54
+ }
55
+
56
+ function askSecret(label) {
57
+ // In non-TTY (piped input, CI, Git Bash on Windows), just read a line
58
+ return ask(label);
59
+ }
60
+
61
+ // ─────────────────────────────────────────────
62
+ // Supabase Management API
63
+ // ─────────────────────────────────────────────
64
+
65
+ async function mgmt(pat, method, path, body) {
66
+ const res = await fetch(`${MGMT}${path}`, {
67
+ method,
68
+ headers: { 'Authorization': `Bearer ${pat}`, 'Content-Type': 'application/json' },
69
+ body: body ? JSON.stringify(body) : undefined,
70
+ });
71
+ if (!res.ok) {
72
+ const text = await res.text().catch(() => res.statusText);
73
+ throw new Error(`${method} ${path} → HTTP ${res.status}: ${text}`);
74
+ }
75
+ if (res.status === 204) return {};
76
+ const text = await res.text();
77
+ if (!text) return {};
78
+ return JSON.parse(text);
79
+ }
80
+
81
+ // ─────────────────────────────────────────────
82
+ // Main install command
83
+ // ─────────────────────────────────────────────
84
+
85
+ export async function runInstall(opts = {}) {
86
+ console.log(chalk.cyan('\n🔐 clauth install\n'));
87
+
88
+ // ── Step 1: Check internet ────────────────
89
+ const s1 = ora('Checking connectivity...').start();
90
+ try {
91
+ await fetch('https://api.supabase.com/v1/projects', {
92
+ method: 'GET', headers: { 'Authorization': 'Bearer test' }
93
+ });
94
+ s1.succeed('Connected');
95
+ } catch {
96
+ s1.fail('No internet connection. Check your network and retry.');
97
+ process.exit(1);
98
+ }
99
+
100
+ // ── Step 2: Collect credentials ───────────
101
+ let ref = opts.ref;
102
+ let pat = opts.pat;
103
+
104
+ if (!ref || !pat) {
105
+ console.log(chalk.cyan('\nYou need two things from Supabase:\n'));
106
+ console.log(chalk.gray(' Project ref: last segment of your project URL'));
107
+ console.log(chalk.gray(' e.g. supabase.com/dashboard/project/') + chalk.white('uvojezuorjgqzmhhgluu'));
108
+ console.log('');
109
+ console.log(chalk.gray(' Personal Access Token (PAT):'));
110
+ console.log(chalk.gray(' supabase.com/dashboard/account/tokens → Generate new token'));
111
+ console.log(chalk.gray(' (This is NOT your anon key or service_role key)\n'));
112
+
113
+ if (!ref) ref = await ask(chalk.white('Supabase project ref: '));
114
+ if (!pat) pat = await askSecret(chalk.white('Supabase PAT: '));
115
+ closeRL();
116
+ }
117
+
118
+ if (!ref || !pat) {
119
+ console.log(chalk.red('\n✗ Both required.\n')); process.exit(1);
120
+ }
121
+
122
+ // ── Step 3: Verify + fetch keys ──────────
123
+ const s3 = ora('Verifying Supabase credentials...').start();
124
+ let anon, serviceRole;
125
+ try {
126
+ const keys = await mgmt(pat, 'GET', `/projects/${ref}/api-keys`);
127
+ anon = keys.find(k => k.name === 'anon')?.api_key;
128
+ serviceRole = keys.find(k => k.name === 'service_role')?.api_key;
129
+ if (!anon || !serviceRole) throw new Error('API keys not found in response');
130
+ s3.succeed('Supabase project verified');
131
+ } catch (e) {
132
+ s3.fail(`Could not connect: ${e.message}`);
133
+ console.log(chalk.gray(' Check your project ref and PAT.'));
134
+ process.exit(1);
135
+ }
136
+
137
+ const projectUrl = `https://${ref}.supabase.co`;
138
+
139
+ // ── Step 4: Run migrations ────────────────
140
+ const s4 = ora('Running database migrations...').start();
141
+ const migrations = [
142
+ { name: '001_clauth_schema', file: 'supabase/migrations/001_clauth_schema.sql' },
143
+ { name: '002_vault_helpers', file: 'supabase/migrations/002_vault_helpers.sql' },
144
+ ];
145
+ for (const m of migrations) {
146
+ const sql = readFileSync(join(ROOT, m.file), 'utf8');
147
+ try {
148
+ await mgmt(pat, 'POST', `/projects/${ref}/database/query`, { query: sql });
149
+ s4.text = `Migrations: ${m.name} ✓`;
150
+ } catch (e) {
151
+ s4.fail(`Migration ${m.name} failed: ${e.message}`);
152
+ process.exit(1);
153
+ }
154
+ }
155
+ s4.succeed('Database migrations applied');
156
+
157
+ // ── Step 5: Deploy Edge Function ─────────
158
+ const s5 = ora('Deploying auth-vault Edge Function...').start();
159
+ const fnSource = readFileSync(join(ROOT, 'supabase/functions/auth-vault/index.ts'), 'utf8');
160
+ try {
161
+ // Use the /deploy endpoint with multipart/form-data (not the old /functions endpoint)
162
+ const formData = new FormData();
163
+
164
+ const metadata = {
165
+ name: 'auth-vault',
166
+ entrypoint_path: 'index.ts',
167
+ verify_jwt: true,
168
+ };
169
+ formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
170
+ formData.append('file', new Blob([fnSource], { type: 'application/typescript' }), 'index.ts');
171
+
172
+ const deployRes = await fetch(
173
+ `${MGMT}/projects/${ref}/functions/deploy?slug=auth-vault`,
174
+ {
175
+ method: 'POST',
176
+ headers: { 'Authorization': `Bearer ${pat}` },
177
+ body: formData,
178
+ }
179
+ );
180
+
181
+ if (!deployRes.ok) {
182
+ const errText = await deployRes.text().catch(() => deployRes.statusText);
183
+ throw new Error(`HTTP ${deployRes.status}: ${errText}`);
184
+ }
185
+
186
+ s5.succeed('auth-vault Edge Function deployed');
187
+ } catch (e) {
188
+ s5.warn(`Edge Function deploy failed: ${e.message}`);
189
+ console.log(chalk.yellow(' Run manually: supabase functions deploy auth-vault'));
190
+ }
191
+
192
+ // ── Step 6: Generate + store secrets ─────
193
+ const s6 = ora('Generating secrets...').start();
194
+ const hmacSalt = randomBytes(32).toString('hex');
195
+ const bootstrapToken = randomBytes(16).toString('hex');
196
+ try {
197
+ await mgmt(pat, 'POST', `/projects/${ref}/secrets`, [
198
+ { name: 'CLAUTH_HMAC_SALT', value: hmacSalt },
199
+ { name: 'CLAUTH_ADMIN_BOOTSTRAP_TOKEN', value: bootstrapToken },
200
+ ]);
201
+ s6.succeed('Secrets generated and stored');
202
+ } catch (e) {
203
+ s6.warn(`Could not store secrets via API: ${e.message}`);
204
+ console.log(chalk.yellow('\n Set these manually in Supabase → Settings → Edge Functions → Secrets:'));
205
+ console.log(chalk.white(` CLAUTH_HMAC_SALT = ${hmacSalt}`));
206
+ console.log(chalk.white(` CLAUTH_ADMIN_BOOTSTRAP_TOKEN = ${bootstrapToken}\n`));
207
+ }
208
+
209
+ // ── Step 7: Save local config ─────────────
210
+ const config = new Conf(getConfOptions());
211
+ config.set('supabase_url', projectUrl);
212
+ config.set('supabase_anon_key', anon);
213
+
214
+ // ── Step 8: End-to-end test ───────────────
215
+ const s8 = ora('Running end-to-end test...').start();
216
+ try {
217
+ // Register a test machine
218
+ const testMachine = `install-test-${randomBytes(4).toString('hex')}`;
219
+ const regRes = await fetch(`${projectUrl}/functions/v1/auth-vault/register-machine`, {
220
+ method: 'POST',
221
+ headers: { 'Authorization': `Bearer ${anon}`, 'Content-Type': 'application/json' },
222
+ body: JSON.stringify({
223
+ machine_hash: testMachine, hmac_seed_hash: 'test',
224
+ label: 'install-test', admin_token: bootstrapToken,
225
+ }),
226
+ });
227
+ const reg = await regRes.json();
228
+ if (!reg.success) throw new Error(`register-machine: ${JSON.stringify(reg)}`);
229
+ s8.succeed('End-to-end test passed — Edge Function is live');
230
+ } catch (e) {
231
+ s8.warn(`Test failed (Edge Function may still be deploying): ${e.message}`);
232
+ console.log(chalk.yellow(' Run clauth test after a minute to verify.'));
233
+ }
234
+
235
+ // ── Step 9: Install Claude skill ──────────
236
+ const s9 = ora('Installing Claude skill...').start();
237
+ const skillSrc = join(ROOT, '.clauth-skill');
238
+ if (existsSync(skillSrc)) {
239
+ try {
240
+ const skillDest = join(SKILLS_DIR, 'clauth');
241
+ mkdirSync(skillDest, { recursive: true });
242
+ cpSync(skillSrc, skillDest, { recursive: true, force: true });
243
+ s9.succeed(`Claude skill installed → ${skillDest}`);
244
+ } catch (e) {
245
+ s9.warn(`Skill install skipped: ${e.message}`);
246
+ }
247
+ } else {
248
+ s9.warn('Skill directory not found — skipping');
249
+ }
250
+
251
+ // ── Step 9.5: Install MCP server config ───
252
+ const s95 = ora('Configuring MCP server for Claude Code...').start();
253
+ try {
254
+ const mcpJsonPath = join(
255
+ process.platform === 'win32' ? process.env.USERPROFILE : process.env.HOME,
256
+ '.mcp.json'
257
+ );
258
+ const cliEntry = join(ROOT, 'cli', 'index.js').replace(/\\/g, '/');
259
+ let mcpConfig = {};
260
+ if (existsSync(mcpJsonPath)) {
261
+ try { mcpConfig = JSON.parse(readFileSync(mcpJsonPath, 'utf8')); } catch {}
262
+ }
263
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
264
+ mcpConfig.mcpServers.clauth = {
265
+ command: 'node',
266
+ args: [cliEntry, 'serve', '--action', 'mcp'],
267
+ };
268
+ const { writeFileSync } = await import('fs');
269
+ writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + '\n', 'utf8');
270
+ s95.succeed(`MCP server config → ${mcpJsonPath}`);
271
+ } catch (e) {
272
+ s95.warn(`MCP config skipped: ${e.message}`);
273
+ console.log(chalk.yellow(' Add manually to ~/.mcp.json:'));
274
+ console.log(chalk.gray(` { "mcpServers": { "clauth": { "command": "node", "args": ["${join(ROOT, 'cli', 'index.js').replace(/\\/g, '/')}", "serve", "--action", "mcp"] } } }`));
275
+ }
276
+
277
+ // ── Step 10: Done ─────────────────────────
278
+ console.log('');
279
+ console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
280
+ console.log(chalk.green(' ✓ clauth installed and ready'));
281
+ console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
282
+ console.log('');
283
+ console.log(chalk.yellow(' Save this bootstrap token — you need it once for clauth setup:'));
284
+ console.log(chalk.white(' ' + bootstrapToken));
285
+ console.log('');
286
+ console.log(chalk.gray(' (Also stored in Supabase → Settings → Edge Functions → Secrets)'));
287
+ console.log('');
288
+ console.log(chalk.white(' Next: run') + chalk.cyan(' clauth setup'));
289
+ console.log(chalk.gray(' Then:') + chalk.cyan(' clauth test'));
290
+ console.log('');
291
+ }