@lifeaitools/clauth 2.1.0 → 2.10.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.
Files changed (98) hide show
  1. package/.clauth-skill/references/keys-guide.md +270 -270
  2. package/cli/api.js +238 -238
  3. package/cli/commands/install.js +413 -396
  4. package/cli/commands/login.test.js +49 -1
  5. package/cli/commands/ops-install.js +11 -10
  6. package/cli/commands/ops.test.js +270 -0
  7. package/cli/commands/scrub.test.js +70 -0
  8. package/cli/commands/serve.js +2614 -11189
  9. package/cli/commands/uninstall.js +164 -164
  10. package/cli/dashboard/dashboard.js +592 -0
  11. package/cli/dashboard/global.css +65 -0
  12. package/cli/dashboard/panel-element.js +79 -0
  13. package/cli/dashboard/panels/build-status-panel.css +12 -0
  14. package/cli/dashboard/panels/build-status-panel.js +98 -0
  15. package/cli/dashboard/panels/panel-registry.js +58 -0
  16. package/cli/dashboard/panels/services-panel.css +95 -0
  17. package/cli/dashboard/panels/services-panel.js +872 -0
  18. package/cli/dashboard/panels/surfaces-panel.css +59 -0
  19. package/cli/dashboard/panels/surfaces-panel.js +474 -0
  20. package/cli/dashboard/panels/toolbar-panel.css +42 -0
  21. package/cli/dashboard/panels/toolbar-panel.js +374 -0
  22. package/cli/dashboard/panels/tunnel-panel.css +61 -0
  23. package/cli/dashboard/panels/tunnel-panel.js +688 -0
  24. package/cli/dashboard/panels/webdav-panel.css +38 -0
  25. package/cli/dashboard/panels/webdav-panel.js +166 -0
  26. package/cli/fingerprint.test.js +95 -0
  27. package/cli/http/components/callagent-terminal-component.js +310 -0
  28. package/cli/http/components/dashboard-component.js +123 -0
  29. package/cli/http/components/inbox-component.js +41 -0
  30. package/cli/http/components/mcp-transport-component.js +275 -0
  31. package/cli/http/components/oauth-component.js +207 -0
  32. package/cli/http/components/service-crud-component.js +288 -0
  33. package/cli/http/components/supervisor-component.js +104 -0
  34. package/cli/http/components/system-component.js +184 -0
  35. package/cli/http/components/tunnel-component.js +508 -0
  36. package/cli/http/components/vault-auth-component.js +264 -0
  37. package/cli/http/components/watchdog-component.js +28 -0
  38. package/cli/http/request-utils.js +21 -0
  39. package/cli/http/servers/ops-server.js +335 -0
  40. package/cli/index.js +5 -91
  41. package/cli/lib/fs-git.test.js +467 -0
  42. package/cli/mcp/providers/call-agent-provider.js +30 -0
  43. package/cli/mcp/providers/clauth-core-provider.js +461 -0
  44. package/cli/mcp/providers/ops-provider.js +42 -0
  45. package/cli/mcp/providers/terminal-provider.js +81 -0
  46. package/cli/mcp/tool-registry.js +48 -0
  47. package/cli/mcp/tool-schemas.js +822 -0
  48. package/cli/mcp/tool-schemas.test.js +49 -0
  49. package/cli/ops/pm2-preflight.js +54 -0
  50. package/cli/recovery.js +6 -0
  51. package/cli/recovery.test.js +179 -0
  52. package/cli/services/agent-pool-instance.js +38 -0
  53. package/cli/services/claude-binary.js +29 -0
  54. package/cli/services/delegation-lane-instance.js +35 -0
  55. package/cli/services/delegation-lane-instance.test.js +37 -0
  56. package/cli/services/file-io.js +38 -0
  57. package/cli/services/file-io.test.js +10 -0
  58. package/cli/services/fs-mount.js +66 -0
  59. package/cli/services/fs-mount.test.js +14 -0
  60. package/cli/services/fs-upload-sessions.js +20 -0
  61. package/cli/services/fs-upload-sessions.test.js +14 -0
  62. package/cli/services/git-exec.js +37 -0
  63. package/cli/services/git-exec.test.js +41 -0
  64. package/cli/services/git-import-guard.js +28 -0
  65. package/cli/services/git-import-guard.test.js +18 -0
  66. package/cli/services/service-status.js +191 -0
  67. package/cli/services/service-status.test.js +39 -0
  68. package/cli/services/terminal-dispatch.js +135 -0
  69. package/cli/services/terminal-dispatch.test.js +9 -0
  70. package/cli/services/terminal-registry.js +39 -0
  71. package/cli/services/terminal-registry.test.js +31 -0
  72. package/cli/services/terminal-sessions.js +100 -0
  73. package/cli/services/terminal-sessions.test.js +26 -0
  74. package/cli/services/terminal-window.js +106 -0
  75. package/cli/services/vault-read.js +14 -0
  76. package/cli/services/vault-read.test.js +35 -0
  77. package/cli/supervisor-registry.js +116 -0
  78. package/cli/supervisor-registry.test.js +165 -0
  79. package/cli/supervisor-ui.test.js +230 -30
  80. package/cli/webdav-config.js +84 -0
  81. package/cli/webdav-config.test.js +79 -0
  82. package/cli/webdav-obscure.js +23 -0
  83. package/cli/webdav-obscure.test.js +41 -0
  84. package/cli/webdav-rclone-discovery.js +44 -0
  85. package/cli/webdav-service.js +30 -118
  86. package/cli/webdav-service.test.js +107 -0
  87. package/install.ps1 +102 -102
  88. package/install.sh +49 -49
  89. package/package.json +2 -2
  90. package/scripts/bootstrap.cjs +121 -121
  91. package/supabase/functions/auth-vault/index.ts +350 -350
  92. package/supabase/migrations/001_clauth_schema.sql +94 -94
  93. package/supabase/migrations/002_vault_helpers.sql +90 -90
  94. package/supabase/migrations/20260317_lockout.sql +26 -26
  95. package/cli/assets/codevelop/launcher-active.cmd.template +0 -20
  96. package/cli/assets/codevelop/launcher-static.cmd.template +0 -7
  97. package/cli/assets/codevelop/windows-terminal.profiles.json +0 -48
  98. package/cli/commands/codevelop.js +0 -1190
@@ -1,406 +1,423 @@
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 0: Upgrade detection ──────────────
89
- const config0 = new Conf(getConfOptions());
90
- if (config0.get('supabase_url')) {
91
- console.log(chalk.cyan(' Existing installation detected.\n'));
92
- console.log(chalk.gray(` Vault: ${config0.get('supabase_url')}`));
93
- console.log(chalk.gray(` Config: ${config0.path}\n`));
94
-
95
- const inquirerMod = await import('inquirer');
96
- const { action } = await inquirerMod.default.prompt([{
97
- type: 'list',
98
- name: 'action',
99
- message: 'What would you like to do?',
100
- choices: [
101
- { name: 'Upgrade (keep config, redeploy Edge Function + migrations)', value: 'upgrade' },
102
- { name: 'Fresh install (new vault)', value: 'fresh' },
103
- { name: 'Cancel', value: 'cancel' },
104
- ],
105
- }]);
106
-
107
- if (action === 'cancel') {
108
- console.log(chalk.gray('\n Cancelled.\n'));
109
- return;
110
- }
111
-
112
- if (action === 'upgrade') {
113
- // Upgrade path: skip provisioning, just redeploy Edge Function and run migrations
114
- console.log(chalk.cyan('\n Running upgrade...\n'));
115
-
116
- let ref = opts.ref;
117
- let pat = opts.pat;
118
- if (!ref || !pat) {
119
- console.log(chalk.gray(' Need Supabase PAT to redeploy Edge Function + run migrations.\n'));
120
- if (!ref) ref = await ask(chalk.white('Supabase project ref: '));
121
- if (!pat) pat = await askSecret(chalk.white('Supabase PAT: '));
122
- closeRL();
123
- }
124
- if (!ref || !pat) {
125
- console.log(chalk.red('\n Both required.\n')); process.exit(1);
126
- }
127
-
128
- // Run migrations
129
- const s4 = ora('Running database migrations...').start();
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
+ import { ensurePm2 } from '../ops/pm2-preflight.js';
28
+
29
+ const __dirname = dirname(fileURLToPath(import.meta.url));
30
+ const ROOT = join(__dirname, '..', '..');
31
+ const MGMT = 'https://api.supabase.com/v1';
32
+ const SKILLS_DIR = process.env.CLAUTH_SKILLS_DIR ||
33
+ (process.platform === 'win32'
34
+ ? join(process.env.USERPROFILE || '', '.claude', 'skills')
35
+ : join(process.env.HOME || '', '.claude', 'skills'));
36
+
37
+ // ─────────────────────────────────────────────
38
+ // Prompt helpers single shared readline to
39
+ // avoid losing buffered stdin between prompts
40
+ // ─────────────────────────────────────────────
41
+
42
+ let _rl;
43
+ function getRL() {
44
+ if (!_rl) {
45
+ _rl = createInterface({ input: process.stdin, output: process.stdout });
46
+ }
47
+ return _rl;
48
+ }
49
+ function closeRL() {
50
+ if (_rl) { _rl.close(); _rl = null; }
51
+ }
52
+
53
+ function ask(question) {
54
+ return new Promise(res => getRL().question(question, a => res(a.trim())));
55
+ }
56
+
57
+ function askSecret(label) {
58
+ // In non-TTY (piped input, CI, Git Bash on Windows), just read a line
59
+ return ask(label);
60
+ }
61
+
62
+ // ─────────────────────────────────────────────
63
+ // Supabase Management API
64
+ // ─────────────────────────────────────────────
65
+
66
+ async function mgmt(pat, method, path, body) {
67
+ const res = await fetch(`${MGMT}${path}`, {
68
+ method,
69
+ headers: { 'Authorization': `Bearer ${pat}`, 'Content-Type': 'application/json' },
70
+ body: body ? JSON.stringify(body) : undefined,
71
+ });
72
+ if (!res.ok) {
73
+ const text = await res.text().catch(() => res.statusText);
74
+ throw new Error(`${method} ${path} → HTTP ${res.status}: ${text}`);
75
+ }
76
+ if (res.status === 204) return {};
77
+ const text = await res.text();
78
+ if (!text) return {};
79
+ return JSON.parse(text);
80
+ }
81
+
82
+ // ─────────────────────────────────────────────
83
+ // Main install command
84
+ // ─────────────────────────────────────────────
85
+
86
+ export async function runInstall(opts = {}) {
87
+ console.log(chalk.cyan('\n🔐 clauth install\n'));
88
+
89
+ // ── Step 0: Upgrade detection ──────────────
90
+ const config0 = new Conf(getConfOptions());
91
+ if (config0.get('supabase_url')) {
92
+ console.log(chalk.cyan(' Existing installation detected.\n'));
93
+ console.log(chalk.gray(` Vault: ${config0.get('supabase_url')}`));
94
+ console.log(chalk.gray(` Config: ${config0.path}\n`));
95
+
96
+ const inquirerMod = await import('inquirer');
97
+ const { action } = await inquirerMod.default.prompt([{
98
+ type: 'list',
99
+ name: 'action',
100
+ message: 'What would you like to do?',
101
+ choices: [
102
+ { name: 'Upgrade (keep config, redeploy Edge Function + migrations)', value: 'upgrade' },
103
+ { name: 'Fresh install (new vault)', value: 'fresh' },
104
+ { name: 'Cancel', value: 'cancel' },
105
+ ],
106
+ }]);
107
+
108
+ if (action === 'cancel') {
109
+ console.log(chalk.gray('\n Cancelled.\n'));
110
+ return;
111
+ }
112
+
113
+ if (action === 'upgrade') {
114
+ // Upgrade path: skip provisioning, just redeploy Edge Function and run migrations
115
+ console.log(chalk.cyan('\n Running upgrade...\n'));
116
+
117
+ let ref = opts.ref;
118
+ let pat = opts.pat;
119
+ if (!ref || !pat) {
120
+ console.log(chalk.gray(' Need Supabase PAT to redeploy Edge Function + run migrations.\n'));
121
+ if (!ref) ref = await ask(chalk.white('Supabase project ref: '));
122
+ if (!pat) pat = await askSecret(chalk.white('Supabase PAT: '));
123
+ closeRL();
124
+ }
125
+ if (!ref || !pat) {
126
+ console.log(chalk.red('\n Both required.\n')); process.exit(1);
127
+ }
128
+
129
+ // Run migrations
130
+ const s4 = ora('Running database migrations...').start();
130
131
  const migrations = [
131
132
  { name: '001_clauth_schema', file: 'supabase/migrations/001_clauth_schema.sql' },
132
133
  { name: '002_vault_helpers', file: 'supabase/migrations/002_vault_helpers.sql' },
133
134
  { name: '003_machine_enrollments', file: 'supabase/migrations/003_machine_enrollments.sql' },
134
135
  ];
135
- for (const m of migrations) {
136
- const sql = readFileSync(join(ROOT, m.file), 'utf8');
137
- try {
138
- await mgmt(pat, 'POST', `/projects/${ref}/database/query`, { query: sql });
139
- s4.text = `Migrations: ${m.name} ✓`;
140
- } catch (e) {
141
- s4.fail(`Migration ${m.name} failed: ${e.message}`);
142
- process.exit(1);
143
- }
144
- }
145
- s4.succeed('Database migrations applied');
146
-
147
- // Redeploy Edge Function
148
- const s5 = ora('Redeploying auth-vault Edge Function...').start();
149
- const fnSource = readFileSync(join(ROOT, 'supabase/functions/auth-vault/index.ts'), 'utf8');
150
- try {
151
- const formData = new FormData();
152
- const metadata = {
153
- name: 'auth-vault',
154
- entrypoint_path: 'index.ts',
155
- verify_jwt: true,
156
- };
157
- formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
158
- formData.append('file', new Blob([fnSource], { type: 'application/typescript' }), 'index.ts');
159
-
160
- const deployRes = await fetch(
161
- `${MGMT}/projects/${ref}/functions/deploy?slug=auth-vault`,
162
- {
163
- method: 'POST',
164
- headers: { 'Authorization': `Bearer ${pat}` },
165
- body: formData,
166
- }
167
- );
168
- if (!deployRes.ok) {
169
- const errText = await deployRes.text().catch(() => deployRes.statusText);
170
- throw new Error(`HTTP ${deployRes.status}: ${errText}`);
171
- }
172
- s5.succeed('auth-vault Edge Function redeployed');
173
- } catch (e) {
174
- s5.warn(`Edge Function deploy failed: ${e.message}`);
175
- console.log(chalk.yellow(' Run manually: supabase functions deploy auth-vault'));
176
- }
177
-
178
- // Reinstall Claude skill
179
- const s9 = ora('Updating Claude skill...').start();
180
- const skillSrc = join(ROOT, '.clauth-skill');
181
- if (existsSync(skillSrc)) {
182
- try {
183
- const skillDest = join(SKILLS_DIR, 'clauth');
184
- mkdirSync(skillDest, { recursive: true });
185
- cpSync(skillSrc, skillDest, { recursive: true, force: true });
186
- s9.succeed(`Claude skill updated → ${skillDest}`);
187
- } catch (e) {
188
- s9.warn(`Skill update skipped: ${e.message}`);
189
- }
190
- } else {
191
- s9.warn('Skill directory not found — skipping');
192
- }
193
-
194
- console.log('');
195
- console.log(chalk.green(' Upgrade complete.'));
196
- console.log(chalk.gray(' Run clauth test to verify.\n'));
197
- return;
198
- }
199
- // action === 'fresh' — continue with normal install below
200
- }
201
-
202
- // ── Step 1: Check internet ────────────────
203
- const s1 = ora('Checking connectivity...').start();
204
- try {
205
- await fetch('https://api.supabase.com/v1/projects', {
206
- method: 'GET', headers: { 'Authorization': 'Bearer test' }
207
- });
208
- s1.succeed('Connected');
209
- } catch {
210
- s1.fail('No internet connection. Check your network and retry.');
211
- process.exit(1);
212
- }
213
-
214
- // ── Step 2: Collect credentials ───────────
215
- let ref = opts.ref;
216
- let pat = opts.pat;
217
-
218
- if (!ref || !pat) {
219
- console.log(chalk.cyan('\nYou need two things from Supabase:\n'));
220
- console.log(chalk.gray(' Project ref: last segment of your project URL'));
221
- console.log(chalk.gray(' e.g. supabase.com/dashboard/project/') + chalk.white('uvojezuorjgqzmhhgluu'));
222
- console.log('');
223
- console.log(chalk.gray(' Personal Access Token (PAT):'));
224
- console.log(chalk.gray(' supabase.com/dashboard/account/tokens Generate new token'));
225
- console.log(chalk.gray(' (This is NOT your anon key or service_role key)\n'));
226
-
227
- if (!ref) ref = await ask(chalk.white('Supabase project ref: '));
228
- if (!pat) pat = await askSecret(chalk.white('Supabase PAT: '));
229
- closeRL();
230
- }
231
-
232
- if (!ref || !pat) {
233
- console.log(chalk.red('\n✗ Both required.\n')); process.exit(1);
234
- }
235
-
236
- // ── Step 3: Verify + fetch keys ──────────
237
- const s3 = ora('Verifying Supabase credentials...').start();
238
- let anon, serviceRole;
239
- try {
240
- const keys = await mgmt(pat, 'GET', `/projects/${ref}/api-keys`);
241
- anon = keys.find(k => k.name === 'anon')?.api_key;
242
- serviceRole = keys.find(k => k.name === 'service_role')?.api_key;
243
- if (!anon || !serviceRole) throw new Error('API keys not found in response');
244
- s3.succeed('Supabase project verified');
245
- } catch (e) {
246
- s3.fail(`Could not connect: ${e.message}`);
247
- console.log(chalk.gray(' Check your project ref and PAT.'));
248
- process.exit(1);
249
- }
250
-
251
- const projectUrl = `https://${ref}.supabase.co`;
252
-
253
- // ── Step 4: Run migrations ────────────────
254
- const s4 = ora('Running database migrations...').start();
136
+ for (const m of migrations) {
137
+ const sql = readFileSync(join(ROOT, m.file), 'utf8');
138
+ try {
139
+ await mgmt(pat, 'POST', `/projects/${ref}/database/query`, { query: sql });
140
+ s4.text = `Migrations: ${m.name} ✓`;
141
+ } catch (e) {
142
+ s4.fail(`Migration ${m.name} failed: ${e.message}`);
143
+ process.exit(1);
144
+ }
145
+ }
146
+ s4.succeed('Database migrations applied');
147
+
148
+ // Redeploy Edge Function
149
+ const s5 = ora('Redeploying auth-vault Edge Function...').start();
150
+ const fnSource = readFileSync(join(ROOT, 'supabase/functions/auth-vault/index.ts'), 'utf8');
151
+ try {
152
+ const formData = new FormData();
153
+ const metadata = {
154
+ name: 'auth-vault',
155
+ entrypoint_path: 'index.ts',
156
+ verify_jwt: true,
157
+ };
158
+ formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
159
+ formData.append('file', new Blob([fnSource], { type: 'application/typescript' }), 'index.ts');
160
+
161
+ const deployRes = await fetch(
162
+ `${MGMT}/projects/${ref}/functions/deploy?slug=auth-vault`,
163
+ {
164
+ method: 'POST',
165
+ headers: { 'Authorization': `Bearer ${pat}` },
166
+ body: formData,
167
+ }
168
+ );
169
+ if (!deployRes.ok) {
170
+ const errText = await deployRes.text().catch(() => deployRes.statusText);
171
+ throw new Error(`HTTP ${deployRes.status}: ${errText}`);
172
+ }
173
+ s5.succeed('auth-vault Edge Function redeployed');
174
+ } catch (e) {
175
+ s5.warn(`Edge Function deploy failed: ${e.message}`);
176
+ console.log(chalk.yellow(' Run manually: supabase functions deploy auth-vault'));
177
+ }
178
+
179
+ // Reinstall Claude skill
180
+ const s9 = ora('Updating Claude skill...').start();
181
+ const skillSrc = join(ROOT, '.clauth-skill');
182
+ if (existsSync(skillSrc)) {
183
+ try {
184
+ const skillDest = join(SKILLS_DIR, 'clauth');
185
+ mkdirSync(skillDest, { recursive: true });
186
+ cpSync(skillSrc, skillDest, { recursive: true, force: true });
187
+ s9.succeed(`Claude skill updated → ${skillDest}`);
188
+ } catch (e) {
189
+ s9.warn(`Skill update skipped: ${e.message}`);
190
+ }
191
+ } else {
192
+ s9.warn('Skill directory not found — skipping');
193
+ }
194
+
195
+ console.log('');
196
+ console.log(chalk.green(' Upgrade complete.'));
197
+ console.log(chalk.gray(' Run clauth test to verify.\n'));
198
+ return;
199
+ }
200
+ // action === 'fresh' — continue with normal install below
201
+ }
202
+
203
+ // ── Step 1: Check internet ────────────────
204
+ const s1 = ora('Checking connectivity...').start();
205
+ try {
206
+ await fetch('https://api.supabase.com/v1/projects', {
207
+ method: 'GET', headers: { 'Authorization': 'Bearer test' }
208
+ });
209
+ s1.succeed('Connected');
210
+ } catch {
211
+ s1.fail('No internet connection. Check your network and retry.');
212
+ process.exit(1);
213
+ }
214
+
215
+ // ── Step 1.5: Check PM2 (the ops/deployment fabric) ──
216
+ // PM2 is a real declared dependency (package.json), not optional: the ops
217
+ // server (clauth's PM2/Coolify/deployment operations plane, its own port)
218
+ // cannot start without it, and a missing/corrupted install used to crash
219
+ // the whole daemon at import time with no recovery. Repairing it here,
220
+ // proactively, means `clauth serve` never has to discover the gap live.
221
+ const s15 = ora('Checking pm2 (deployment fabric)...').start();
222
+ try {
223
+ await ensurePm2();
224
+ s15.succeed('pm2 available');
225
+ } catch (e) {
226
+ s15.fail(`pm2 unavailable: ${e.message}`);
227
+ console.log(chalk.yellow(' Run manually: npm install (from the clauth repo root)'));
228
+ process.exit(1);
229
+ }
230
+
231
+ // ── Step 2: Collect credentials ───────────
232
+ let ref = opts.ref;
233
+ let pat = opts.pat;
234
+
235
+ if (!ref || !pat) {
236
+ console.log(chalk.cyan('\nYou need two things from Supabase:\n'));
237
+ console.log(chalk.gray(' Project ref: last segment of your project URL'));
238
+ console.log(chalk.gray(' e.g. supabase.com/dashboard/project/') + chalk.white('uvojezuorjgqzmhhgluu'));
239
+ console.log('');
240
+ console.log(chalk.gray(' Personal Access Token (PAT):'));
241
+ console.log(chalk.gray(' supabase.com/dashboard/account/tokens Generate new token'));
242
+ console.log(chalk.gray(' (This is NOT your anon key or service_role key)\n'));
243
+
244
+ if (!ref) ref = await ask(chalk.white('Supabase project ref: '));
245
+ if (!pat) pat = await askSecret(chalk.white('Supabase PAT: '));
246
+ closeRL();
247
+ }
248
+
249
+ if (!ref || !pat) {
250
+ console.log(chalk.red('\n✗ Both required.\n')); process.exit(1);
251
+ }
252
+
253
+ // ── Step 3: Verify + fetch keys ──────────
254
+ const s3 = ora('Verifying Supabase credentials...').start();
255
+ let anon, serviceRole;
256
+ try {
257
+ const keys = await mgmt(pat, 'GET', `/projects/${ref}/api-keys`);
258
+ anon = keys.find(k => k.name === 'anon')?.api_key;
259
+ serviceRole = keys.find(k => k.name === 'service_role')?.api_key;
260
+ if (!anon || !serviceRole) throw new Error('API keys not found in response');
261
+ s3.succeed('Supabase project verified');
262
+ } catch (e) {
263
+ s3.fail(`Could not connect: ${e.message}`);
264
+ console.log(chalk.gray(' Check your project ref and PAT.'));
265
+ process.exit(1);
266
+ }
267
+
268
+ const projectUrl = `https://${ref}.supabase.co`;
269
+
270
+ // ── Step 4: Run migrations ────────────────
271
+ const s4 = ora('Running database migrations...').start();
255
272
  const migrations = [
256
273
  { name: '001_clauth_schema', file: 'supabase/migrations/001_clauth_schema.sql' },
257
274
  { name: '002_vault_helpers', file: 'supabase/migrations/002_vault_helpers.sql' },
258
275
  { name: '003_machine_enrollments', file: 'supabase/migrations/003_machine_enrollments.sql' },
259
276
  ];
260
- for (const m of migrations) {
261
- const sql = readFileSync(join(ROOT, m.file), 'utf8');
262
- try {
263
- await mgmt(pat, 'POST', `/projects/${ref}/database/query`, { query: sql });
264
- s4.text = `Migrations: ${m.name} ✓`;
265
- } catch (e) {
266
- s4.fail(`Migration ${m.name} failed: ${e.message}`);
267
- process.exit(1);
268
- }
269
- }
270
- s4.succeed('Database migrations applied');
271
-
272
- // ── Step 5: Deploy Edge Function ─────────
273
- const s5 = ora('Deploying auth-vault Edge Function...').start();
274
- const fnSource = readFileSync(join(ROOT, 'supabase/functions/auth-vault/index.ts'), 'utf8');
275
- try {
276
- // Use the /deploy endpoint with multipart/form-data (not the old /functions endpoint)
277
- const formData = new FormData();
278
-
279
- const metadata = {
280
- name: 'auth-vault',
281
- entrypoint_path: 'index.ts',
282
- verify_jwt: true,
283
- };
284
- formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
285
- formData.append('file', new Blob([fnSource], { type: 'application/typescript' }), 'index.ts');
286
-
287
- const deployRes = await fetch(
288
- `${MGMT}/projects/${ref}/functions/deploy?slug=auth-vault`,
289
- {
290
- method: 'POST',
291
- headers: { 'Authorization': `Bearer ${pat}` },
292
- body: formData,
293
- }
294
- );
295
-
296
- if (!deployRes.ok) {
297
- const errText = await deployRes.text().catch(() => deployRes.statusText);
298
- throw new Error(`HTTP ${deployRes.status}: ${errText}`);
299
- }
300
-
301
- s5.succeed('auth-vault Edge Function deployed');
302
- } catch (e) {
303
- s5.warn(`Edge Function deploy failed: ${e.message}`);
304
- console.log(chalk.yellow(' Run manually: supabase functions deploy auth-vault'));
305
- }
306
-
307
- // ── Step 6: Generate + store secrets ─────
308
- const s6 = ora('Generating secrets...').start();
309
- const hmacSalt = randomBytes(32).toString('hex');
310
- const bootstrapToken = randomBytes(16).toString('hex');
311
- try {
312
- await mgmt(pat, 'POST', `/projects/${ref}/secrets`, [
313
- { name: 'CLAUTH_HMAC_SALT', value: hmacSalt },
314
- { name: 'CLAUTH_ADMIN_BOOTSTRAP_TOKEN', value: bootstrapToken },
315
- ]);
316
- s6.succeed('Secrets generated and stored');
317
- } catch (e) {
318
- s6.warn(`Could not store secrets via API: ${e.message}`);
319
- console.log(chalk.yellow('\n Set these manually in Supabase → Settings → Edge Functions → Secrets:'));
320
- console.log(chalk.white(` CLAUTH_HMAC_SALT = ${hmacSalt}`));
321
- console.log(chalk.white(` CLAUTH_ADMIN_BOOTSTRAP_TOKEN = ${bootstrapToken}\n`));
322
- }
323
-
324
- // ── Step 7: Save local config ─────────────
325
- const config = new Conf(getConfOptions());
326
- config.set('supabase_url', projectUrl);
327
- config.set('supabase_anon_key', anon);
328
-
329
- // ── Step 8: End-to-end test ───────────────
330
- const s8 = ora('Running end-to-end test...').start();
331
- try {
332
- // Register a test machine
333
- const testMachine = `install-test-${randomBytes(4).toString('hex')}`;
334
- const regRes = await fetch(`${projectUrl}/functions/v1/auth-vault/register-machine`, {
335
- method: 'POST',
336
- headers: { 'Authorization': `Bearer ${anon}`, 'Content-Type': 'application/json' },
337
- body: JSON.stringify({
338
- machine_hash: testMachine, hmac_seed_hash: 'test',
339
- label: 'install-test', admin_token: bootstrapToken,
340
- }),
341
- });
342
- const reg = await regRes.json();
343
- if (!reg.success) throw new Error(`register-machine: ${JSON.stringify(reg)}`);
344
- s8.succeed('End-to-end test passed — Edge Function is live');
345
- } catch (e) {
346
- s8.warn(`Test failed (Edge Function may still be deploying): ${e.message}`);
347
- console.log(chalk.yellow(' Run clauth test after a minute to verify.'));
348
- }
349
-
350
- // ── Step 9: Install Claude skill ──────────
351
- const s9 = ora('Installing Claude skill...').start();
352
- const skillSrc = join(ROOT, '.clauth-skill');
353
- if (existsSync(skillSrc)) {
354
- try {
355
- const skillDest = join(SKILLS_DIR, 'clauth');
356
- mkdirSync(skillDest, { recursive: true });
357
- cpSync(skillSrc, skillDest, { recursive: true, force: true });
358
- s9.succeed(`Claude skill installed → ${skillDest}`);
359
- } catch (e) {
360
- s9.warn(`Skill install skipped: ${e.message}`);
361
- }
362
- } else {
363
- s9.warn('Skill directory not found — skipping');
364
- }
365
-
366
- // ── Step 9.5: Install MCP server config ───
367
- const s95 = ora('Configuring MCP server for Claude Code...').start();
368
- try {
369
- const mcpJsonPath = join(
370
- process.platform === 'win32' ? process.env.USERPROFILE : process.env.HOME,
371
- '.mcp.json'
372
- );
373
- const cliEntry = join(ROOT, 'cli', 'index.js').replace(/\\/g, '/');
374
- let mcpConfig = {};
375
- if (existsSync(mcpJsonPath)) {
376
- try { mcpConfig = JSON.parse(readFileSync(mcpJsonPath, 'utf8')); } catch {}
377
- }
378
- if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
379
- mcpConfig.mcpServers.clauth = {
380
- command: 'node',
381
- args: [cliEntry, 'serve', '--action', 'mcp'],
382
- };
383
- const { writeFileSync } = await import('fs');
384
- writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + '\n', 'utf8');
385
- s95.succeed(`MCP server config → ${mcpJsonPath}`);
386
- } catch (e) {
387
- s95.warn(`MCP config skipped: ${e.message}`);
388
- console.log(chalk.yellow(' Add manually to ~/.mcp.json:'));
389
- console.log(chalk.gray(` { "mcpServers": { "clauth": { "command": "node", "args": ["${join(ROOT, 'cli', 'index.js').replace(/\\/g, '/')}", "serve", "--action", "mcp"] } } }`));
390
- }
391
-
392
- // ── Step 10: Done ─────────────────────────
393
- console.log('');
394
- console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
395
- console.log(chalk.green(' ✓ clauth installed and ready'));
396
- console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
397
- console.log('');
398
- console.log(chalk.yellow(' Save this bootstrap token — you need it once for clauth setup:'));
399
- console.log(chalk.white(' ' + bootstrapToken));
400
- console.log('');
401
- console.log(chalk.gray(' (Also stored in Supabase → Settings → Edge Functions → Secrets)'));
402
- console.log('');
403
- console.log(chalk.white(' Next: run') + chalk.cyan(' clauth setup'));
404
- console.log(chalk.gray(' Then:') + chalk.cyan(' clauth test'));
405
- console.log('');
406
- }
277
+ for (const m of migrations) {
278
+ const sql = readFileSync(join(ROOT, m.file), 'utf8');
279
+ try {
280
+ await mgmt(pat, 'POST', `/projects/${ref}/database/query`, { query: sql });
281
+ s4.text = `Migrations: ${m.name} ✓`;
282
+ } catch (e) {
283
+ s4.fail(`Migration ${m.name} failed: ${e.message}`);
284
+ process.exit(1);
285
+ }
286
+ }
287
+ s4.succeed('Database migrations applied');
288
+
289
+ // ── Step 5: Deploy Edge Function ─────────
290
+ const s5 = ora('Deploying auth-vault Edge Function...').start();
291
+ const fnSource = readFileSync(join(ROOT, 'supabase/functions/auth-vault/index.ts'), 'utf8');
292
+ try {
293
+ // Use the /deploy endpoint with multipart/form-data (not the old /functions endpoint)
294
+ const formData = new FormData();
295
+
296
+ const metadata = {
297
+ name: 'auth-vault',
298
+ entrypoint_path: 'index.ts',
299
+ verify_jwt: true,
300
+ };
301
+ formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
302
+ formData.append('file', new Blob([fnSource], { type: 'application/typescript' }), 'index.ts');
303
+
304
+ const deployRes = await fetch(
305
+ `${MGMT}/projects/${ref}/functions/deploy?slug=auth-vault`,
306
+ {
307
+ method: 'POST',
308
+ headers: { 'Authorization': `Bearer ${pat}` },
309
+ body: formData,
310
+ }
311
+ );
312
+
313
+ if (!deployRes.ok) {
314
+ const errText = await deployRes.text().catch(() => deployRes.statusText);
315
+ throw new Error(`HTTP ${deployRes.status}: ${errText}`);
316
+ }
317
+
318
+ s5.succeed('auth-vault Edge Function deployed');
319
+ } catch (e) {
320
+ s5.warn(`Edge Function deploy failed: ${e.message}`);
321
+ console.log(chalk.yellow(' Run manually: supabase functions deploy auth-vault'));
322
+ }
323
+
324
+ // ── Step 6: Generate + store secrets ─────
325
+ const s6 = ora('Generating secrets...').start();
326
+ const hmacSalt = randomBytes(32).toString('hex');
327
+ const bootstrapToken = randomBytes(16).toString('hex');
328
+ try {
329
+ await mgmt(pat, 'POST', `/projects/${ref}/secrets`, [
330
+ { name: 'CLAUTH_HMAC_SALT', value: hmacSalt },
331
+ { name: 'CLAUTH_ADMIN_BOOTSTRAP_TOKEN', value: bootstrapToken },
332
+ ]);
333
+ s6.succeed('Secrets generated and stored');
334
+ } catch (e) {
335
+ s6.warn(`Could not store secrets via API: ${e.message}`);
336
+ console.log(chalk.yellow('\n Set these manually in Supabase → Settings → Edge Functions → Secrets:'));
337
+ console.log(chalk.white(` CLAUTH_HMAC_SALT = ${hmacSalt}`));
338
+ console.log(chalk.white(` CLAUTH_ADMIN_BOOTSTRAP_TOKEN = ${bootstrapToken}\n`));
339
+ }
340
+
341
+ // ── Step 7: Save local config ─────────────
342
+ const config = new Conf(getConfOptions());
343
+ config.set('supabase_url', projectUrl);
344
+ config.set('supabase_anon_key', anon);
345
+
346
+ // ── Step 8: End-to-end test ───────────────
347
+ const s8 = ora('Running end-to-end test...').start();
348
+ try {
349
+ // Register a test machine
350
+ const testMachine = `install-test-${randomBytes(4).toString('hex')}`;
351
+ const regRes = await fetch(`${projectUrl}/functions/v1/auth-vault/register-machine`, {
352
+ method: 'POST',
353
+ headers: { 'Authorization': `Bearer ${anon}`, 'Content-Type': 'application/json' },
354
+ body: JSON.stringify({
355
+ machine_hash: testMachine, hmac_seed_hash: 'test',
356
+ label: 'install-test', admin_token: bootstrapToken,
357
+ }),
358
+ });
359
+ const reg = await regRes.json();
360
+ if (!reg.success) throw new Error(`register-machine: ${JSON.stringify(reg)}`);
361
+ s8.succeed('End-to-end test passed — Edge Function is live');
362
+ } catch (e) {
363
+ s8.warn(`Test failed (Edge Function may still be deploying): ${e.message}`);
364
+ console.log(chalk.yellow(' Run clauth test after a minute to verify.'));
365
+ }
366
+
367
+ // ── Step 9: Install Claude skill ──────────
368
+ const s9 = ora('Installing Claude skill...').start();
369
+ const skillSrc = join(ROOT, '.clauth-skill');
370
+ if (existsSync(skillSrc)) {
371
+ try {
372
+ const skillDest = join(SKILLS_DIR, 'clauth');
373
+ mkdirSync(skillDest, { recursive: true });
374
+ cpSync(skillSrc, skillDest, { recursive: true, force: true });
375
+ s9.succeed(`Claude skill installed → ${skillDest}`);
376
+ } catch (e) {
377
+ s9.warn(`Skill install skipped: ${e.message}`);
378
+ }
379
+ } else {
380
+ s9.warn('Skill directory not found — skipping');
381
+ }
382
+
383
+ // ── Step 9.5: Install MCP server config ───
384
+ const s95 = ora('Configuring MCP server for Claude Code...').start();
385
+ try {
386
+ const mcpJsonPath = join(
387
+ process.platform === 'win32' ? process.env.USERPROFILE : process.env.HOME,
388
+ '.mcp.json'
389
+ );
390
+ const cliEntry = join(ROOT, 'cli', 'index.js').replace(/\\/g, '/');
391
+ let mcpConfig = {};
392
+ if (existsSync(mcpJsonPath)) {
393
+ try { mcpConfig = JSON.parse(readFileSync(mcpJsonPath, 'utf8')); } catch {}
394
+ }
395
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
396
+ mcpConfig.mcpServers.clauth = {
397
+ command: 'node',
398
+ args: [cliEntry, 'serve', '--action', 'mcp'],
399
+ };
400
+ const { writeFileSync } = await import('fs');
401
+ writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + '\n', 'utf8');
402
+ s95.succeed(`MCP server config → ${mcpJsonPath}`);
403
+ } catch (e) {
404
+ s95.warn(`MCP config skipped: ${e.message}`);
405
+ console.log(chalk.yellow(' Add manually to ~/.mcp.json:'));
406
+ console.log(chalk.gray(` { "mcpServers": { "clauth": { "command": "node", "args": ["${join(ROOT, 'cli', 'index.js').replace(/\\/g, '/')}", "serve", "--action", "mcp"] } } }`));
407
+ }
408
+
409
+ // ── Step 10: Done ─────────────────────────
410
+ console.log('');
411
+ console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
412
+ console.log(chalk.green(' ✓ clauth installed and ready'));
413
+ console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
414
+ console.log('');
415
+ console.log(chalk.yellow(' Save this bootstrap token — you need it once for clauth setup:'));
416
+ console.log(chalk.white(' ' + bootstrapToken));
417
+ console.log('');
418
+ console.log(chalk.gray(' (Also stored in Supabase → Settings → Edge Functions → Secrets)'));
419
+ console.log('');
420
+ console.log(chalk.white(' Next: run') + chalk.cyan(' clauth setup'));
421
+ console.log(chalk.gray(' Then:') + chalk.cyan(' clauth test'));
422
+ console.log('');
423
+ }