@lifeaitools/clauth 1.30.26 → 1.31.1
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/.clauth-skill/SKILL.md +0 -31
- package/.clauth-skill/references/keys-guide.md +270 -270
- package/.clauth-skill/references/operator-guide.md +0 -27
- package/README.md +2 -48
- package/cli/api.js +238 -238
- package/cli/commands/agent-pool.js +51 -15
- package/cli/commands/install.js +396 -396
- package/cli/commands/login.js +135 -0
- package/cli/commands/login.test.js +73 -0
- package/cli/commands/serve.js +16 -846
- package/cli/commands/uninstall.js +164 -164
- package/cli/commands/watchdog.js +1 -1
- package/cli/index.js +29 -20
- package/cli/supervisor-registry.js +1 -6
- package/cli/watchdog-registry.js +2 -30
- package/cli/watchdog-registry.test.js +5 -28
- package/cli/webdav-service.js +339 -339
- package/install.ps1 +102 -102
- package/install.sh +49 -49
- package/package.json +4 -6
- package/scripts/bin/bootstrap-linux +0 -0
- package/scripts/bin/bootstrap-macos +0 -0
- package/scripts/bin/bootstrap-win.exe +0 -0
- package/scripts/bootstrap.cjs +121 -121
- package/scripts/build.mjs +66 -0
- package/scripts/build.sh +5 -45
- package/scripts/postinstall.js +189 -189
- package/supabase/functions/auth-vault/index.ts +350 -350
- package/supabase/migrations/001_clauth_schema.sql +94 -94
- package/supabase/migrations/002_vault_helpers.sql +90 -90
- package/supabase/migrations/20260317_lockout.sql +26 -26
- package/cli/commands/ops-install.js +0 -211
- package/cli/commands/ops.js +0 -69
- package/cli/ops/coolify-adapter.js +0 -80
- package/cli/ops/deployment-adapter.js +0 -63
- package/cli/ops/job-store.js +0 -116
- package/cli/ops/operation-policy.js +0 -51
- package/cli/ops/pm2-adapter.js +0 -128
- package/cli/ops/serialized-executor.js +0 -9
|
@@ -1,164 +1,164 @@
|
|
|
1
|
-
// cli/commands/uninstall.js
|
|
2
|
-
// clauth uninstall — full teardown: DB objects, Edge Function, secrets, skill, local config
|
|
3
|
-
//
|
|
4
|
-
// Reverses everything `clauth install` does:
|
|
5
|
-
// 1. Drops clauth tables, policies, triggers, functions from Supabase
|
|
6
|
-
// 2. Deletes auth-vault Edge Function
|
|
7
|
-
// 3. Removes CLAUTH_* secrets
|
|
8
|
-
// 4. Removes Claude skill directory
|
|
9
|
-
// 5. Clears local config (Conf store)
|
|
10
|
-
|
|
11
|
-
import { existsSync, rmSync } from 'fs';
|
|
12
|
-
import { join } from 'path';
|
|
13
|
-
import Conf from 'conf';
|
|
14
|
-
import chalk from 'chalk';
|
|
15
|
-
import ora from 'ora';
|
|
16
|
-
|
|
17
|
-
const MGMT = 'https://api.supabase.com/v1';
|
|
18
|
-
const SKILLS_DIR = process.env.CLAUTH_SKILLS_DIR ||
|
|
19
|
-
(process.platform === 'win32'
|
|
20
|
-
? join(process.env.USERPROFILE || '', '.claude', 'skills')
|
|
21
|
-
: join(process.env.HOME || '', '.claude', 'skills'));
|
|
22
|
-
|
|
23
|
-
// ─────────────────────────────────────────────
|
|
24
|
-
// Supabase Management API helper
|
|
25
|
-
// ─────────────────────────────────────────────
|
|
26
|
-
async function mgmt(pat, method, path, body) {
|
|
27
|
-
const res = await fetch(`${MGMT}${path}`, {
|
|
28
|
-
method,
|
|
29
|
-
headers: { 'Authorization': `Bearer ${pat}`, 'Content-Type': 'application/json' },
|
|
30
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
31
|
-
});
|
|
32
|
-
if (!res.ok) {
|
|
33
|
-
const text = await res.text().catch(() => res.statusText);
|
|
34
|
-
throw new Error(`${method} ${path} → HTTP ${res.status}: ${text}`);
|
|
35
|
-
}
|
|
36
|
-
if (res.status === 204) return {};
|
|
37
|
-
const text = await res.text();
|
|
38
|
-
if (!text) return {};
|
|
39
|
-
return JSON.parse(text);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ─────────────────────────────────────────────
|
|
43
|
-
// Main uninstall command
|
|
44
|
-
// ─────────────────────────────────────────────
|
|
45
|
-
export async function runUninstall(opts = {}) {
|
|
46
|
-
console.log(chalk.red('\n🗑️ clauth uninstall\n'));
|
|
47
|
-
|
|
48
|
-
const config = new Conf({ projectName: 'clauth' });
|
|
49
|
-
|
|
50
|
-
// ── Collect credentials ────────────────────
|
|
51
|
-
const ref = opts.ref || config.get('supabase_url')?.match(/https:\/\/(.+)\.supabase\.co/)?.[1];
|
|
52
|
-
const pat = opts.pat;
|
|
53
|
-
|
|
54
|
-
if (!ref) {
|
|
55
|
-
console.log(chalk.red(' Cannot determine Supabase project ref.'));
|
|
56
|
-
console.log(chalk.gray(' Use: clauth uninstall --ref <project-ref> --pat <personal-access-token>'));
|
|
57
|
-
process.exit(1);
|
|
58
|
-
}
|
|
59
|
-
if (!pat) {
|
|
60
|
-
console.log(chalk.red(' Supabase PAT required for teardown.'));
|
|
61
|
-
console.log(chalk.gray(' Use: clauth uninstall --ref <project-ref> --pat <personal-access-token>'));
|
|
62
|
-
process.exit(1);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
console.log(chalk.gray(` Project: ${ref}\n`));
|
|
66
|
-
|
|
67
|
-
// ── Step 1: Drop database objects ──────────
|
|
68
|
-
const s1 = ora('Dropping clauth database objects...').start();
|
|
69
|
-
const teardownSQL = `
|
|
70
|
-
-- Drop triggers
|
|
71
|
-
DROP TRIGGER IF EXISTS clauth_services_updated ON public.clauth_services;
|
|
72
|
-
|
|
73
|
-
-- Drop tables (CASCADE drops policies automatically)
|
|
74
|
-
DROP TABLE IF EXISTS public.clauth_audit CASCADE;
|
|
75
|
-
DROP TABLE IF EXISTS public.clauth_machines CASCADE;
|
|
76
|
-
DROP TABLE IF EXISTS public.clauth_services CASCADE;
|
|
77
|
-
|
|
78
|
-
-- Drop functions
|
|
79
|
-
DROP FUNCTION IF EXISTS public.clauth_touch_updated() CASCADE;
|
|
80
|
-
DROP FUNCTION IF EXISTS public.clauth_upsert_vault_secret(text, text) CASCADE;
|
|
81
|
-
DROP FUNCTION IF EXISTS public.clauth_get_vault_secret(text) CASCADE;
|
|
82
|
-
DROP FUNCTION IF EXISTS public.clauth_delete_vault_secret(text) CASCADE;
|
|
83
|
-
`;
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
await mgmt(pat, 'POST', `/projects/${ref}/database/query`, { query: teardownSQL });
|
|
87
|
-
s1.succeed('Database objects dropped (tables, triggers, functions, policies)');
|
|
88
|
-
} catch (e) {
|
|
89
|
-
s1.fail(`Database teardown failed: ${e.message}`);
|
|
90
|
-
console.log(chalk.yellow(' You may need to drop objects manually via SQL editor.'));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ── Step 2: Delete Edge Function ───────────
|
|
94
|
-
const s2 = ora('Deleting auth-vault Edge Function...').start();
|
|
95
|
-
try {
|
|
96
|
-
const res = await fetch(`${MGMT}/projects/${ref}/functions/auth-vault`, {
|
|
97
|
-
method: 'DELETE',
|
|
98
|
-
headers: { 'Authorization': `Bearer ${pat}` },
|
|
99
|
-
});
|
|
100
|
-
if (res.ok || res.status === 404) {
|
|
101
|
-
s2.succeed(res.status === 404
|
|
102
|
-
? 'Edge Function not found (already deleted)'
|
|
103
|
-
: 'Edge Function deleted');
|
|
104
|
-
} else {
|
|
105
|
-
const text = await res.text().catch(() => res.statusText);
|
|
106
|
-
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
107
|
-
}
|
|
108
|
-
} catch (e) {
|
|
109
|
-
s2.fail(`Edge Function delete failed: ${e.message}`);
|
|
110
|
-
console.log(chalk.yellow(' Delete manually: Supabase Dashboard → Edge Functions → auth-vault → Delete'));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ── Step 3: Remove secrets ─────────────────
|
|
114
|
-
const s3 = ora('Removing clauth secrets...').start();
|
|
115
|
-
try {
|
|
116
|
-
// Supabase Management API: DELETE /projects/{ref}/secrets with body listing secret names
|
|
117
|
-
const res = await fetch(`${MGMT}/projects/${ref}/secrets`, {
|
|
118
|
-
method: 'DELETE',
|
|
119
|
-
headers: { 'Authorization': `Bearer ${pat}`, 'Content-Type': 'application/json' },
|
|
120
|
-
body: JSON.stringify(['CLAUTH_HMAC_SALT', 'CLAUTH_ADMIN_BOOTSTRAP_TOKEN']),
|
|
121
|
-
});
|
|
122
|
-
if (res.ok) {
|
|
123
|
-
s3.succeed('Secrets removed (CLAUTH_HMAC_SALT, CLAUTH_ADMIN_BOOTSTRAP_TOKEN)');
|
|
124
|
-
} else {
|
|
125
|
-
const text = await res.text().catch(() => res.statusText);
|
|
126
|
-
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
127
|
-
}
|
|
128
|
-
} catch (e) {
|
|
129
|
-
s3.warn(`Secret removal failed: ${e.message}`);
|
|
130
|
-
console.log(chalk.yellow(' Remove manually: Supabase → Settings → Edge Functions → Secrets'));
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ── Step 4: Remove Claude skill ────────────
|
|
134
|
-
const s4 = ora('Removing Claude skill...').start();
|
|
135
|
-
const skillDir = join(SKILLS_DIR, 'clauth');
|
|
136
|
-
if (existsSync(skillDir)) {
|
|
137
|
-
try {
|
|
138
|
-
rmSync(skillDir, { recursive: true, force: true });
|
|
139
|
-
s4.succeed(`Skill removed: ${skillDir}`);
|
|
140
|
-
} catch (e) {
|
|
141
|
-
s4.warn(`Could not remove skill: ${e.message}`);
|
|
142
|
-
}
|
|
143
|
-
} else {
|
|
144
|
-
s4.succeed('Skill directory not found (already removed)');
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// ── Step 5: Clear local config ─────────────
|
|
148
|
-
const s5 = ora('Clearing local config...').start();
|
|
149
|
-
try {
|
|
150
|
-
config.clear();
|
|
151
|
-
s5.succeed('Local config cleared');
|
|
152
|
-
} catch (e) {
|
|
153
|
-
s5.warn(`Could not clear config: ${e.message}`);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ── Done ───────────────────────────────────
|
|
157
|
-
console.log('');
|
|
158
|
-
console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
159
|
-
console.log(chalk.yellow(' ✓ clauth fully uninstalled'));
|
|
160
|
-
console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
161
|
-
console.log('');
|
|
162
|
-
console.log(chalk.gray(' To reinstall: npx @lifeaitools/clauth install'));
|
|
163
|
-
console.log('');
|
|
164
|
-
}
|
|
1
|
+
// cli/commands/uninstall.js
|
|
2
|
+
// clauth uninstall — full teardown: DB objects, Edge Function, secrets, skill, local config
|
|
3
|
+
//
|
|
4
|
+
// Reverses everything `clauth install` does:
|
|
5
|
+
// 1. Drops clauth tables, policies, triggers, functions from Supabase
|
|
6
|
+
// 2. Deletes auth-vault Edge Function
|
|
7
|
+
// 3. Removes CLAUTH_* secrets
|
|
8
|
+
// 4. Removes Claude skill directory
|
|
9
|
+
// 5. Clears local config (Conf store)
|
|
10
|
+
|
|
11
|
+
import { existsSync, rmSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import Conf from 'conf';
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import ora from 'ora';
|
|
16
|
+
|
|
17
|
+
const MGMT = 'https://api.supabase.com/v1';
|
|
18
|
+
const SKILLS_DIR = process.env.CLAUTH_SKILLS_DIR ||
|
|
19
|
+
(process.platform === 'win32'
|
|
20
|
+
? join(process.env.USERPROFILE || '', '.claude', 'skills')
|
|
21
|
+
: join(process.env.HOME || '', '.claude', 'skills'));
|
|
22
|
+
|
|
23
|
+
// ─────────────────────────────────────────────
|
|
24
|
+
// Supabase Management API helper
|
|
25
|
+
// ─────────────────────────────────────────────
|
|
26
|
+
async function mgmt(pat, method, path, body) {
|
|
27
|
+
const res = await fetch(`${MGMT}${path}`, {
|
|
28
|
+
method,
|
|
29
|
+
headers: { 'Authorization': `Bearer ${pat}`, 'Content-Type': 'application/json' },
|
|
30
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
const text = await res.text().catch(() => res.statusText);
|
|
34
|
+
throw new Error(`${method} ${path} → HTTP ${res.status}: ${text}`);
|
|
35
|
+
}
|
|
36
|
+
if (res.status === 204) return {};
|
|
37
|
+
const text = await res.text();
|
|
38
|
+
if (!text) return {};
|
|
39
|
+
return JSON.parse(text);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─────────────────────────────────────────────
|
|
43
|
+
// Main uninstall command
|
|
44
|
+
// ─────────────────────────────────────────────
|
|
45
|
+
export async function runUninstall(opts = {}) {
|
|
46
|
+
console.log(chalk.red('\n🗑️ clauth uninstall\n'));
|
|
47
|
+
|
|
48
|
+
const config = new Conf({ projectName: 'clauth' });
|
|
49
|
+
|
|
50
|
+
// ── Collect credentials ────────────────────
|
|
51
|
+
const ref = opts.ref || config.get('supabase_url')?.match(/https:\/\/(.+)\.supabase\.co/)?.[1];
|
|
52
|
+
const pat = opts.pat;
|
|
53
|
+
|
|
54
|
+
if (!ref) {
|
|
55
|
+
console.log(chalk.red(' Cannot determine Supabase project ref.'));
|
|
56
|
+
console.log(chalk.gray(' Use: clauth uninstall --ref <project-ref> --pat <personal-access-token>'));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
if (!pat) {
|
|
60
|
+
console.log(chalk.red(' Supabase PAT required for teardown.'));
|
|
61
|
+
console.log(chalk.gray(' Use: clauth uninstall --ref <project-ref> --pat <personal-access-token>'));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(chalk.gray(` Project: ${ref}\n`));
|
|
66
|
+
|
|
67
|
+
// ── Step 1: Drop database objects ──────────
|
|
68
|
+
const s1 = ora('Dropping clauth database objects...').start();
|
|
69
|
+
const teardownSQL = `
|
|
70
|
+
-- Drop triggers
|
|
71
|
+
DROP TRIGGER IF EXISTS clauth_services_updated ON public.clauth_services;
|
|
72
|
+
|
|
73
|
+
-- Drop tables (CASCADE drops policies automatically)
|
|
74
|
+
DROP TABLE IF EXISTS public.clauth_audit CASCADE;
|
|
75
|
+
DROP TABLE IF EXISTS public.clauth_machines CASCADE;
|
|
76
|
+
DROP TABLE IF EXISTS public.clauth_services CASCADE;
|
|
77
|
+
|
|
78
|
+
-- Drop functions
|
|
79
|
+
DROP FUNCTION IF EXISTS public.clauth_touch_updated() CASCADE;
|
|
80
|
+
DROP FUNCTION IF EXISTS public.clauth_upsert_vault_secret(text, text) CASCADE;
|
|
81
|
+
DROP FUNCTION IF EXISTS public.clauth_get_vault_secret(text) CASCADE;
|
|
82
|
+
DROP FUNCTION IF EXISTS public.clauth_delete_vault_secret(text) CASCADE;
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await mgmt(pat, 'POST', `/projects/${ref}/database/query`, { query: teardownSQL });
|
|
87
|
+
s1.succeed('Database objects dropped (tables, triggers, functions, policies)');
|
|
88
|
+
} catch (e) {
|
|
89
|
+
s1.fail(`Database teardown failed: ${e.message}`);
|
|
90
|
+
console.log(chalk.yellow(' You may need to drop objects manually via SQL editor.'));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Step 2: Delete Edge Function ───────────
|
|
94
|
+
const s2 = ora('Deleting auth-vault Edge Function...').start();
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(`${MGMT}/projects/${ref}/functions/auth-vault`, {
|
|
97
|
+
method: 'DELETE',
|
|
98
|
+
headers: { 'Authorization': `Bearer ${pat}` },
|
|
99
|
+
});
|
|
100
|
+
if (res.ok || res.status === 404) {
|
|
101
|
+
s2.succeed(res.status === 404
|
|
102
|
+
? 'Edge Function not found (already deleted)'
|
|
103
|
+
: 'Edge Function deleted');
|
|
104
|
+
} else {
|
|
105
|
+
const text = await res.text().catch(() => res.statusText);
|
|
106
|
+
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
107
|
+
}
|
|
108
|
+
} catch (e) {
|
|
109
|
+
s2.fail(`Edge Function delete failed: ${e.message}`);
|
|
110
|
+
console.log(chalk.yellow(' Delete manually: Supabase Dashboard → Edge Functions → auth-vault → Delete'));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Step 3: Remove secrets ─────────────────
|
|
114
|
+
const s3 = ora('Removing clauth secrets...').start();
|
|
115
|
+
try {
|
|
116
|
+
// Supabase Management API: DELETE /projects/{ref}/secrets with body listing secret names
|
|
117
|
+
const res = await fetch(`${MGMT}/projects/${ref}/secrets`, {
|
|
118
|
+
method: 'DELETE',
|
|
119
|
+
headers: { 'Authorization': `Bearer ${pat}`, 'Content-Type': 'application/json' },
|
|
120
|
+
body: JSON.stringify(['CLAUTH_HMAC_SALT', 'CLAUTH_ADMIN_BOOTSTRAP_TOKEN']),
|
|
121
|
+
});
|
|
122
|
+
if (res.ok) {
|
|
123
|
+
s3.succeed('Secrets removed (CLAUTH_HMAC_SALT, CLAUTH_ADMIN_BOOTSTRAP_TOKEN)');
|
|
124
|
+
} else {
|
|
125
|
+
const text = await res.text().catch(() => res.statusText);
|
|
126
|
+
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
s3.warn(`Secret removal failed: ${e.message}`);
|
|
130
|
+
console.log(chalk.yellow(' Remove manually: Supabase → Settings → Edge Functions → Secrets'));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Step 4: Remove Claude skill ────────────
|
|
134
|
+
const s4 = ora('Removing Claude skill...').start();
|
|
135
|
+
const skillDir = join(SKILLS_DIR, 'clauth');
|
|
136
|
+
if (existsSync(skillDir)) {
|
|
137
|
+
try {
|
|
138
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
139
|
+
s4.succeed(`Skill removed: ${skillDir}`);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
s4.warn(`Could not remove skill: ${e.message}`);
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
s4.succeed('Skill directory not found (already removed)');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Step 5: Clear local config ─────────────
|
|
148
|
+
const s5 = ora('Clearing local config...').start();
|
|
149
|
+
try {
|
|
150
|
+
config.clear();
|
|
151
|
+
s5.succeed('Local config cleared');
|
|
152
|
+
} catch (e) {
|
|
153
|
+
s5.warn(`Could not clear config: ${e.message}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Done ───────────────────────────────────
|
|
157
|
+
console.log('');
|
|
158
|
+
console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
159
|
+
console.log(chalk.yellow(' ✓ clauth fully uninstalled'));
|
|
160
|
+
console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
161
|
+
console.log('');
|
|
162
|
+
console.log(chalk.gray(' To reinstall: npx @lifeaitools/clauth install'));
|
|
163
|
+
console.log('');
|
|
164
|
+
}
|
package/cli/commands/watchdog.js
CHANGED
|
@@ -131,7 +131,7 @@ export async function runWatchdog(action, opts = {}) {
|
|
|
131
131
|
console.log("Usage: clauth watchdog restart <service-id>");
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
134
|
-
const result =
|
|
134
|
+
const result = restartWatchdogService(serviceId);
|
|
135
135
|
console.log(JSON.stringify(result, null, 2));
|
|
136
136
|
return;
|
|
137
137
|
}
|
package/cli/index.js
CHANGED
|
@@ -150,10 +150,37 @@ import { runInstall } from './commands/install.js';
|
|
|
150
150
|
import { runUninstall } from './commands/uninstall.js';
|
|
151
151
|
import { runScrub } from './commands/scrub.js';
|
|
152
152
|
import { runServe } from './commands/serve.js';
|
|
153
|
-
import { runOps } from './commands/ops.js';
|
|
154
|
-
import { runOpsInstall } from './commands/ops-install.js';
|
|
155
153
|
import { runCodevelop } from './commands/codevelop.js';
|
|
156
154
|
import { runNpm, runPublish } from './commands/npm.js';
|
|
155
|
+
import { runLogin } from './commands/login.js';
|
|
156
|
+
|
|
157
|
+
// ──────────────────────────────────────────────
|
|
158
|
+
// clauth login <target>
|
|
159
|
+
// THE global login directive — the one sanctioned way to SSH into any
|
|
160
|
+
// clauth-managed box. Never hand-fetch a private key (clauth get <ssh-service>)
|
|
161
|
+
// and hand-roll ssh-agent/ssh yourself; add the box to LOGIN_TARGETS in
|
|
162
|
+
// cli/commands/login.js instead and use this command.
|
|
163
|
+
// ──────────────────────────────────────────────
|
|
164
|
+
program
|
|
165
|
+
.command('login [target]')
|
|
166
|
+
.description('SSH into a clauth-managed box by name (e.g. vultr) — run with no target to list them')
|
|
167
|
+
.option('-c, --command <cmd>', 'Run one command instead of opening an interactive shell')
|
|
168
|
+
.option('--host <user@host>', 'Override the target\'s default host')
|
|
169
|
+
.option('--native-windows', 'Human-only: use native Windows OpenSSH instead of Git Bash (blocked for agent sessions)')
|
|
170
|
+
.addHelpText('after', `
|
|
171
|
+
Examples:
|
|
172
|
+
clauth login List known login targets
|
|
173
|
+
clauth login vultr Interactive shell on the Vultr dev box
|
|
174
|
+
clauth login vultr -c "pm2 list" Run one command, non-interactive
|
|
175
|
+
clauth login vultr --host root@1.2.3.4 Override the default host for this target
|
|
176
|
+
`)
|
|
177
|
+
.action(async (target, opts) => {
|
|
178
|
+
await runLogin(target, {
|
|
179
|
+
command: opts.command,
|
|
180
|
+
host: opts.host,
|
|
181
|
+
nativeWindows: opts.nativeWindows,
|
|
182
|
+
});
|
|
183
|
+
});
|
|
157
184
|
|
|
158
185
|
program
|
|
159
186
|
.command('install')
|
|
@@ -1052,22 +1079,4 @@ Examples:
|
|
|
1052
1079
|
await runServe({ ...opts, action: resolvedAction });
|
|
1053
1080
|
});
|
|
1054
1081
|
|
|
1055
|
-
program
|
|
1056
|
-
.command("ops <action>")
|
|
1057
|
-
.description("Call the bearer-authenticated PM2 and Coolify operations control plane")
|
|
1058
|
-
.option("--endpoint <url>", "HTTPS control-plane endpoint (or CLAUTH_OPS_ENDPOINT)")
|
|
1059
|
-
.option("--target <name>", "PM2 process name or id")
|
|
1060
|
-
.option("--script <path>", "PM2 script path for start")
|
|
1061
|
-
.option("--instances <n>", "PM2 scale target")
|
|
1062
|
-
.option("--operation <name>", "PM2 operation for run")
|
|
1063
|
-
.option("--args-json <json>", "JSON positional arguments for a raw pm2_* operation")
|
|
1064
|
-
.option("--options-json <json>", "JSON PM2 options for a typed operation")
|
|
1065
|
-
.option("--application <uuid>", "registered Coolify application UUID for promote")
|
|
1066
|
-
.option("--ref <name>", "registered Git ref for deploy")
|
|
1067
|
-
.option("--job <id>", "job id for status lookup")
|
|
1068
|
-
.option("--config <path>", "server-side JSON policy for ops install")
|
|
1069
|
-
.option("--dry-run", "validate and print ops install configuration without changing PM2")
|
|
1070
|
-
.addHelpText("after", `\nActions: catalog | list | describe | run | deploy | promote | job | install\n\nInstall: clauth ops install --config /etc/clauth/ops-control-plane.json\nThe installer creates or updates a PM2-managed local control plane, then proves /health and the bearer gate without reading a token.\n\nThe bearer is retrieved only from local clauth service vultr-ops-api-token and is never printed.\n`)
|
|
1071
|
-
.action(async (action, opts) => { if (action === "install") await runOpsInstall(opts); else await runOps(action, opts); });
|
|
1072
|
-
|
|
1073
1082
|
program.parse(process.argv);
|
|
@@ -7,12 +7,7 @@ import { spawnSync } from "node:child_process";
|
|
|
7
7
|
const SCHEMA = "lifeai.plugin.v1";
|
|
8
8
|
const DEFAULT_TIMEOUT_MS = 3000;
|
|
9
9
|
const DEFAULT_SUPERVISOR_PORT = 52439;
|
|
10
|
-
|
|
11
|
-
// process at all, but literally embedded in the clauth daemon itself (e.g.
|
|
12
|
-
// fs-mcp) — distinct from local/clauth/pm2 (a separate local process clauth
|
|
13
|
-
// manages via pm2) because there is nothing for a surface action to
|
|
14
|
-
// start/stop/restart; the daemon's own lifecycle IS the surface's lifecycle.
|
|
15
|
-
const DESTINATIONS = new Set(["local/clauth/pm2", "local/clauth/daemon", "vultr/clauth/pm2", "coolify/clauth/docker"]);
|
|
10
|
+
const DESTINATIONS = new Set(["local/clauth/pm2", "vultr/clauth/pm2", "coolify/clauth/docker"]);
|
|
16
11
|
const OWNERS = new Set(["clauth", "plugin", "external"]);
|
|
17
12
|
const ACTIONS = new Set(["start", "stop", "restart", "reconcile", "test", "promote", "rollback"]);
|
|
18
13
|
const DEFAULT_HEALTH_RECONCILE_INTERVAL_MS = 10000;
|
package/cli/watchdog-registry.js
CHANGED
|
@@ -178,30 +178,7 @@ export function readWatchdogEvents(limit = 100) {
|
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
|
|
182
|
-
if (!service.health?.url) return { ok: true, health_status: "not_configured" };
|
|
183
|
-
|
|
184
|
-
const attempts = Number(service.health.readyAttempts || 20);
|
|
185
|
-
const delayMs = Number(service.health.readyDelayMs || 250);
|
|
186
|
-
let observed;
|
|
187
|
-
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
188
|
-
observed = await evaluateWatchdogService(service);
|
|
189
|
-
if (observed.status === "healthy") {
|
|
190
|
-
return { ok: true, health_status: observed.status, health_http_status: observed.httpStatus, attempts: attempt };
|
|
191
|
-
}
|
|
192
|
-
if (attempt < attempts) await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
193
|
-
}
|
|
194
|
-
return {
|
|
195
|
-
ok: false,
|
|
196
|
-
error: "restart_health_unreachable",
|
|
197
|
-
health_status: observed?.status || "unknown",
|
|
198
|
-
health_http_status: observed?.httpStatus,
|
|
199
|
-
health_error: observed?.error,
|
|
200
|
-
attempts,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export async function restartWatchdogService(id) {
|
|
181
|
+
export function restartWatchdogService(id) {
|
|
205
182
|
const service = loadRegistry().services.find((candidate) => candidate.id === id);
|
|
206
183
|
if (!service) return { ok: false, error: "service_not_registered" };
|
|
207
184
|
if (!service.restart) return { ok: false, error: "restart_not_configured" };
|
|
@@ -215,23 +192,18 @@ export async function restartWatchdogService(id) {
|
|
|
215
192
|
encoding: "utf8",
|
|
216
193
|
timeout: Number(service.restart.timeoutMs || 30000),
|
|
217
194
|
});
|
|
218
|
-
const health = result.status === 0 ? await verifyRestartHealth(service) : { ok: false, health_status: "not_checked" };
|
|
219
195
|
const event = {
|
|
220
196
|
kind: "restart",
|
|
221
197
|
service_id: id,
|
|
222
198
|
status: result.status,
|
|
223
|
-
health_status: health.health_status,
|
|
224
|
-
health_http_status: health.health_http_status,
|
|
225
|
-
health_error: health.health_error,
|
|
226
199
|
error: result.error ? result.error.message : undefined,
|
|
227
200
|
};
|
|
228
201
|
appendEvent(event);
|
|
229
202
|
return {
|
|
230
|
-
ok: result.status === 0
|
|
203
|
+
ok: result.status === 0,
|
|
231
204
|
status: result.status,
|
|
232
205
|
stdout: result.stdout,
|
|
233
206
|
stderr: result.stderr,
|
|
234
207
|
error: result.error ? result.error.message : undefined,
|
|
235
|
-
...health,
|
|
236
208
|
};
|
|
237
209
|
}
|
|
@@ -13,12 +13,12 @@ import {
|
|
|
13
13
|
validateWatchdogService,
|
|
14
14
|
} from "./watchdog-registry.js";
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
function withTempRegistry(fn) {
|
|
17
17
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-watchdog-"));
|
|
18
18
|
const old = process.env.CLAUTH_WATCHDOG_DIR;
|
|
19
19
|
process.env.CLAUTH_WATCHDOG_DIR = dir;
|
|
20
20
|
try {
|
|
21
|
-
return
|
|
21
|
+
return fn(dir);
|
|
22
22
|
} finally {
|
|
23
23
|
if (old === undefined) delete process.env.CLAUTH_WATCHDOG_DIR;
|
|
24
24
|
else process.env.CLAUTH_WATCHDOG_DIR = old;
|
|
@@ -78,35 +78,12 @@ test("registerWatchdogManifest upserts services by id", () => withTempRegistry((
|
|
|
78
78
|
assert.equal(registry.services.find((service) => service.id === "codeflow").label, "CodeFlow Updated");
|
|
79
79
|
}));
|
|
80
80
|
|
|
81
|
-
test("restartWatchdogService rejects missing and unapproved services",
|
|
82
|
-
assert.deepEqual(
|
|
81
|
+
test("restartWatchdogService rejects missing and unapproved services", () => withTempRegistry(() => {
|
|
82
|
+
assert.deepEqual(restartWatchdogService("missing"), { ok: false, error: "service_not_registered" });
|
|
83
83
|
registerWatchdogManifest({
|
|
84
84
|
services: [
|
|
85
85
|
{ id: "dev-center", label: "Dev Center", kind: "process", restart: { cmd: "node", args: ["--version"] } },
|
|
86
86
|
],
|
|
87
87
|
});
|
|
88
|
-
assert.deepEqual(
|
|
89
|
-
}));
|
|
90
|
-
|
|
91
|
-
test("restartWatchdogService requires registered health after launching", async () => withTempRegistry(async () => {
|
|
92
|
-
registerWatchdogManifest({
|
|
93
|
-
services: [{
|
|
94
|
-
id: "health-gated",
|
|
95
|
-
label: "Health gated",
|
|
96
|
-
kind: "http",
|
|
97
|
-
health: { url: "http://127.0.0.1:3109/health", readyAttempts: 2, readyDelayMs: 0 },
|
|
98
|
-
restart: { cmd: process.execPath, args: ["--version"] },
|
|
99
|
-
approvalRequired: false,
|
|
100
|
-
}],
|
|
101
|
-
});
|
|
102
|
-
const originalFetch = globalThis.fetch;
|
|
103
|
-
globalThis.fetch = async () => ({ ok: false, status: 503 });
|
|
104
|
-
try {
|
|
105
|
-
const result = await restartWatchdogService("health-gated");
|
|
106
|
-
assert.equal(result.ok, false);
|
|
107
|
-
assert.equal(result.error, "restart_health_unreachable");
|
|
108
|
-
assert.equal(result.health_status, "degraded");
|
|
109
|
-
} finally {
|
|
110
|
-
globalThis.fetch = originalFetch;
|
|
111
|
-
}
|
|
88
|
+
assert.deepEqual(restartWatchdogService("dev-center"), { ok: false, error: "approval_required" });
|
|
112
89
|
}));
|