@orchagent/cli 0.2.10 → 0.2.12
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/dist/commands/delete.js +90 -0
- package/dist/commands/doctor.js +27 -0
- package/dist/commands/github.js +543 -0
- package/dist/commands/index.js +6 -0
- package/dist/commands/run.js +10 -3
- package/dist/commands/workspace.js +133 -0
- package/dist/lib/api.js +15 -0
- package/dist/lib/auth-errors.js +27 -0
- package/dist/lib/browser-auth.js +5 -8
- package/dist/lib/doctor/checks/auth.js +115 -0
- package/dist/lib/doctor/checks/config.js +119 -0
- package/dist/lib/doctor/checks/connectivity.js +109 -0
- package/dist/lib/doctor/checks/environment.js +151 -0
- package/dist/lib/doctor/checks/llm.js +108 -0
- package/dist/lib/doctor/index.js +19 -0
- package/dist/lib/doctor/output.js +105 -0
- package/dist/lib/doctor/runner.js +68 -0
- package/dist/lib/doctor/types.js +2 -0
- package/dist/lib/llm-errors.js +79 -0
- package/dist/lib/llm.js +4 -3
- package/package.json +1 -1
package/dist/commands/run.js
CHANGED
|
@@ -371,7 +371,9 @@ async function unzipBundle(zipPath, destDir) {
|
|
|
371
371
|
});
|
|
372
372
|
proc.on('close', (code) => {
|
|
373
373
|
if (code !== 0) {
|
|
374
|
-
|
|
374
|
+
const detail = stderr.trim() || `exit code ${code}`;
|
|
375
|
+
reject(new errors_1.CliError(`Failed to extract bundle: ${detail}\n` +
|
|
376
|
+
`Ensure the bundle is valid. Try re-publishing the agent.`));
|
|
375
377
|
}
|
|
376
378
|
else {
|
|
377
379
|
resolve();
|
|
@@ -566,9 +568,14 @@ async function executeBundleAgent(config, org, agentName, version, agentData, ar
|
|
|
566
568
|
else if (exitCode !== 0) {
|
|
567
569
|
// No stdout, check stderr
|
|
568
570
|
if (stderr.trim()) {
|
|
569
|
-
throw new errors_1.CliError(`Agent
|
|
571
|
+
throw new errors_1.CliError(`Agent exited with code ${exitCode}\n\nError output:\n${stderr.trim()}`);
|
|
570
572
|
}
|
|
571
|
-
throw new errors_1.CliError(`Agent exited with code ${exitCode} (no output)`
|
|
573
|
+
throw new errors_1.CliError(`Agent exited with code ${exitCode} (no output)\n\n` +
|
|
574
|
+
`Common causes:\n` +
|
|
575
|
+
` - Missing dependency (check requirements.txt)\n` +
|
|
576
|
+
` - Syntax error in entrypoint\n` +
|
|
577
|
+
` - Agent crashed before writing output\n\n` +
|
|
578
|
+
`Run with --verbose or check logs in dashboard.`);
|
|
572
579
|
}
|
|
573
580
|
}
|
|
574
581
|
finally {
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerWorkspaceCommand = registerWorkspaceCommand;
|
|
4
|
+
const config_1 = require("../lib/config");
|
|
5
|
+
const api_1 = require("../lib/api");
|
|
6
|
+
function registerWorkspaceCommand(program) {
|
|
7
|
+
const workspace = program
|
|
8
|
+
.command('workspace')
|
|
9
|
+
.description('Manage workspaces');
|
|
10
|
+
// List workspaces
|
|
11
|
+
workspace
|
|
12
|
+
.command('list')
|
|
13
|
+
.description('List all workspaces')
|
|
14
|
+
.action(async () => {
|
|
15
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
16
|
+
const fileConfig = await (0, config_1.loadConfig)();
|
|
17
|
+
const workspaces = await (0, api_1.listWorkspaces)(config);
|
|
18
|
+
if (workspaces.length === 0) {
|
|
19
|
+
process.stdout.write('No workspaces found.\n');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const currentId = fileConfig.current_workspace;
|
|
23
|
+
for (const ws of workspaces) {
|
|
24
|
+
const isCurrent = ws.id === currentId || ws.slug === currentId;
|
|
25
|
+
const marker = isCurrent ? '* ' : ' ';
|
|
26
|
+
const type = ws.type === 'team' ? '(team)' : '(personal)';
|
|
27
|
+
const members = ws.type === 'team' ? ` - ${ws.member_count} members` : '';
|
|
28
|
+
process.stdout.write(`${marker}${ws.slug} - ${ws.name} ${type}${members}\n`);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
// Switch workspace
|
|
32
|
+
workspace
|
|
33
|
+
.command('use <slug>')
|
|
34
|
+
.description('Switch to a different workspace')
|
|
35
|
+
.action(async (slug) => {
|
|
36
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
37
|
+
const workspaces = await (0, api_1.listWorkspaces)(config);
|
|
38
|
+
const target = workspaces.find(ws => ws.slug === slug || ws.id === slug);
|
|
39
|
+
if (!target) {
|
|
40
|
+
process.stderr.write(`Workspace "${slug}" not found.\n`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const fileConfig = await (0, config_1.loadConfig)();
|
|
44
|
+
fileConfig.current_workspace = target.id;
|
|
45
|
+
await (0, config_1.saveConfig)(fileConfig);
|
|
46
|
+
process.stdout.write(`Switched to workspace: ${target.name} (${target.slug})\n`);
|
|
47
|
+
});
|
|
48
|
+
// Create workspace
|
|
49
|
+
workspace
|
|
50
|
+
.command('create <name>')
|
|
51
|
+
.description('Create a new team workspace')
|
|
52
|
+
.option('--slug <slug>', 'Workspace URL slug')
|
|
53
|
+
.action(async (name, options) => {
|
|
54
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
55
|
+
// Auto-generate slug from name if not provided
|
|
56
|
+
const slug = options.slug || name
|
|
57
|
+
.toLowerCase()
|
|
58
|
+
.replace(/\s+/g, '-')
|
|
59
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
60
|
+
.replace(/-+/g, '-')
|
|
61
|
+
.slice(0, 30);
|
|
62
|
+
try {
|
|
63
|
+
const workspace = await (0, api_1.createWorkspace)(config, { name, slug });
|
|
64
|
+
// Switch to the new workspace
|
|
65
|
+
const fileConfig = await (0, config_1.loadConfig)();
|
|
66
|
+
fileConfig.current_workspace = workspace.id;
|
|
67
|
+
await (0, config_1.saveConfig)(fileConfig);
|
|
68
|
+
process.stdout.write(`Created workspace: ${workspace.name} (${workspace.slug})\n`);
|
|
69
|
+
process.stdout.write(`Switched to new workspace.\n`);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
process.stderr.write(`Failed to create workspace: ${err instanceof Error ? err.message : 'Unknown error'}\n`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
// Invite member
|
|
77
|
+
workspace
|
|
78
|
+
.command('invite <email>')
|
|
79
|
+
.description('Invite a member to the current workspace')
|
|
80
|
+
.option('--role <role>', 'Role: owner or member', 'member')
|
|
81
|
+
.action(async (email, options) => {
|
|
82
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
83
|
+
const fileConfig = await (0, config_1.loadConfig)();
|
|
84
|
+
if (!fileConfig.current_workspace) {
|
|
85
|
+
process.stderr.write('No workspace selected. Run `orch workspace use <slug>` first.\n');
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
const role = options.role;
|
|
89
|
+
if (role !== 'owner' && role !== 'member') {
|
|
90
|
+
process.stderr.write('Role must be "owner" or "member".\n');
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
await (0, api_1.inviteToWorkspace)(config, fileConfig.current_workspace, { email, role });
|
|
95
|
+
process.stdout.write(`Invited ${email} as ${role}.\n`);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
process.stderr.write(`Failed to send invite: ${err instanceof Error ? err.message : 'Unknown error'}\n`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
// List members
|
|
103
|
+
workspace
|
|
104
|
+
.command('members')
|
|
105
|
+
.description('List members of the current workspace')
|
|
106
|
+
.action(async () => {
|
|
107
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
108
|
+
const fileConfig = await (0, config_1.loadConfig)();
|
|
109
|
+
if (!fileConfig.current_workspace) {
|
|
110
|
+
process.stderr.write('No workspace selected. Run `orch workspace use <slug>` first.\n');
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const { members, invites } = await (0, api_1.getWorkspaceMembers)(config, fileConfig.current_workspace);
|
|
115
|
+
process.stdout.write('Members:\n');
|
|
116
|
+
for (const member of members) {
|
|
117
|
+
const roleLabel = member.role === 'owner' ? '[owner]' : '[member]';
|
|
118
|
+
process.stdout.write(` ${member.clerk_user_id} ${roleLabel}\n`);
|
|
119
|
+
}
|
|
120
|
+
if (invites.length > 0) {
|
|
121
|
+
process.stdout.write('\nPending invites:\n');
|
|
122
|
+
for (const invite of invites) {
|
|
123
|
+
const expires = new Date(invite.expires_at).toLocaleDateString();
|
|
124
|
+
process.stdout.write(` ${invite.email} (${invite.role}) - expires ${expires}\n`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
process.stderr.write(`Failed to list members: ${err instanceof Error ? err.message : 'Unknown error'}\n`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
package/dist/lib/api.js
CHANGED
|
@@ -52,6 +52,8 @@ exports.uploadCodeBundle = uploadCodeBundle;
|
|
|
52
52
|
exports.getMyAgent = getMyAgent;
|
|
53
53
|
exports.getAgentWithFallback = getAgentWithFallback;
|
|
54
54
|
exports.downloadCodeBundleAuthenticated = downloadCodeBundleAuthenticated;
|
|
55
|
+
exports.checkAgentDelete = checkAgentDelete;
|
|
56
|
+
exports.deleteAgent = deleteAgent;
|
|
55
57
|
class ApiError extends Error {
|
|
56
58
|
status;
|
|
57
59
|
payload;
|
|
@@ -261,3 +263,16 @@ async function downloadCodeBundleAuthenticated(config, agentId) {
|
|
|
261
263
|
const arrayBuffer = await response.arrayBuffer();
|
|
262
264
|
return Buffer.from(arrayBuffer);
|
|
263
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* Check if an agent requires confirmation for deletion.
|
|
268
|
+
*/
|
|
269
|
+
async function checkAgentDelete(config, agentId) {
|
|
270
|
+
return request(config, 'GET', `/agents/${agentId}/delete-check`);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Soft delete an agent.
|
|
274
|
+
*/
|
|
275
|
+
async function deleteAgent(config, agentId, confirmationName) {
|
|
276
|
+
const params = confirmationName ? `?confirmation_name=${encodeURIComponent(confirmationName)}` : '';
|
|
277
|
+
return request(config, 'DELETE', `/agents/${agentId}${params}`);
|
|
278
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseAuthError = parseAuthError;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
const AUTH_MESSAGES = {
|
|
6
|
+
400: 'Invalid authentication request. Please try again.',
|
|
7
|
+
401: 'Authentication failed. Run `orchagent login` again.',
|
|
8
|
+
403: 'Access denied. You may not have permission for this organization.',
|
|
9
|
+
429: 'Too many attempts. Wait a moment and try again.',
|
|
10
|
+
500: 'Server error. Please try again later.',
|
|
11
|
+
502: 'Server temporarily unavailable. Try again later.',
|
|
12
|
+
};
|
|
13
|
+
async function parseAuthError(response, context) {
|
|
14
|
+
try {
|
|
15
|
+
const json = await response.json();
|
|
16
|
+
const msg = json?.error?.message || json?.message;
|
|
17
|
+
if (msg && typeof msg === 'string' && msg.length < 200) {
|
|
18
|
+
return new errors_1.CliError(msg);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch { }
|
|
22
|
+
const ctx = context === 'init'
|
|
23
|
+
? 'Failed to start authentication'
|
|
24
|
+
: 'Failed to complete authentication';
|
|
25
|
+
const action = AUTH_MESSAGES[response.status] || 'Please try again.';
|
|
26
|
+
return new errors_1.CliError(`${ctx}: ${action}`);
|
|
27
|
+
}
|
package/dist/lib/browser-auth.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.startBrowserAuth = startBrowserAuth;
|
|
|
8
8
|
const http_1 = __importDefault(require("http"));
|
|
9
9
|
const open_1 = __importDefault(require("open"));
|
|
10
10
|
const errors_1 = require("./errors");
|
|
11
|
+
const auth_errors_1 = require("./auth-errors");
|
|
11
12
|
const DEFAULT_PORT = 8374;
|
|
12
13
|
const AUTH_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
13
14
|
/**
|
|
@@ -26,8 +27,7 @@ async function browserAuthFlow(apiUrl, port = DEFAULT_PORT) {
|
|
|
26
27
|
body: JSON.stringify({ redirect_port: port }),
|
|
27
28
|
});
|
|
28
29
|
if (!initResponse.ok) {
|
|
29
|
-
|
|
30
|
-
throw new errors_1.CliError(error?.error?.message || `Failed to initialize auth: ${initResponse.statusText}`);
|
|
30
|
+
throw await (0, auth_errors_1.parseAuthError)(initResponse, 'init');
|
|
31
31
|
}
|
|
32
32
|
const { auth_url } = await initResponse.json();
|
|
33
33
|
// Step 2: Start local server and wait for callback
|
|
@@ -39,8 +39,7 @@ async function browserAuthFlow(apiUrl, port = DEFAULT_PORT) {
|
|
|
39
39
|
body: JSON.stringify({ token }),
|
|
40
40
|
});
|
|
41
41
|
if (!exchangeResponse.ok) {
|
|
42
|
-
|
|
43
|
-
throw new errors_1.CliError(error?.error?.message || `Failed to exchange token: ${exchangeResponse.statusText}`);
|
|
42
|
+
throw await (0, auth_errors_1.parseAuthError)(exchangeResponse, 'exchange');
|
|
44
43
|
}
|
|
45
44
|
const result = await exchangeResponse.json();
|
|
46
45
|
// Step 4: Open browser (do this after server is ready but before waiting)
|
|
@@ -125,8 +124,7 @@ async function startBrowserAuth(apiUrl, port = DEFAULT_PORT) {
|
|
|
125
124
|
body: JSON.stringify({ redirect_port: port }),
|
|
126
125
|
});
|
|
127
126
|
if (!initResponse.ok) {
|
|
128
|
-
|
|
129
|
-
throw new errors_1.CliError(error?.error?.message || `Failed to initialize auth: ${initResponse.statusText}`);
|
|
127
|
+
throw await (0, auth_errors_1.parseAuthError)(initResponse, 'init');
|
|
130
128
|
}
|
|
131
129
|
const { auth_url } = await initResponse.json();
|
|
132
130
|
// Step 2: Start local server to receive callback
|
|
@@ -148,8 +146,7 @@ async function startBrowserAuth(apiUrl, port = DEFAULT_PORT) {
|
|
|
148
146
|
body: JSON.stringify({ token }),
|
|
149
147
|
});
|
|
150
148
|
if (!exchangeResponse.ok) {
|
|
151
|
-
|
|
152
|
-
throw new errors_1.CliError(error?.error?.message || `Failed to complete authentication: ${exchangeResponse.statusText}`);
|
|
149
|
+
throw await (0, auth_errors_1.parseAuthError)(exchangeResponse, 'exchange');
|
|
153
150
|
}
|
|
154
151
|
const result = await exchangeResponse.json();
|
|
155
152
|
return {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkApiKeyPresent = checkApiKeyPresent;
|
|
4
|
+
exports.checkApiKeyValid = checkApiKeyValid;
|
|
5
|
+
exports.runAuthChecks = runAuthChecks;
|
|
6
|
+
const config_1 = require("../../config");
|
|
7
|
+
const api_1 = require("../../api");
|
|
8
|
+
/**
|
|
9
|
+
* Check if API key is configured (in config file or env var).
|
|
10
|
+
*/
|
|
11
|
+
async function checkApiKeyPresent() {
|
|
12
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
13
|
+
const fileConfig = await (0, config_1.loadConfig)();
|
|
14
|
+
if (config.apiKey) {
|
|
15
|
+
// Determine source of the API key
|
|
16
|
+
let source = 'unknown';
|
|
17
|
+
if (process.env.ORCHAGENT_API_KEY) {
|
|
18
|
+
source = 'ORCHAGENT_API_KEY environment variable';
|
|
19
|
+
}
|
|
20
|
+
else if (fileConfig.api_key) {
|
|
21
|
+
source = '~/.orchagent/config.json';
|
|
22
|
+
}
|
|
23
|
+
// Get key prefix for verbose output (mask most of the key)
|
|
24
|
+
const keyPrefix = config.apiKey.slice(0, 12) + '...';
|
|
25
|
+
return {
|
|
26
|
+
category: 'authentication',
|
|
27
|
+
name: 'api_key_present',
|
|
28
|
+
status: 'success',
|
|
29
|
+
message: 'API key configured',
|
|
30
|
+
details: { source, keyPrefix },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
category: 'authentication',
|
|
35
|
+
name: 'api_key_present',
|
|
36
|
+
status: 'error',
|
|
37
|
+
message: 'No API key configured',
|
|
38
|
+
fix: 'Run `orch login` or set ORCHAGENT_API_KEY environment variable',
|
|
39
|
+
details: { configured: false },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Check if API key is valid by calling the /org endpoint.
|
|
44
|
+
*/
|
|
45
|
+
async function checkApiKeyValid() {
|
|
46
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
47
|
+
if (!config.apiKey) {
|
|
48
|
+
return {
|
|
49
|
+
category: 'authentication',
|
|
50
|
+
name: 'api_key_valid',
|
|
51
|
+
status: 'error',
|
|
52
|
+
message: 'Cannot validate API key (not configured)',
|
|
53
|
+
details: { reason: 'no api key' },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const org = await (0, api_1.getOrg)(config);
|
|
58
|
+
return {
|
|
59
|
+
category: 'authentication',
|
|
60
|
+
name: 'api_key_valid',
|
|
61
|
+
status: 'success',
|
|
62
|
+
message: `API key valid (logged in as: ${org.name})`,
|
|
63
|
+
details: {
|
|
64
|
+
orgName: org.name,
|
|
65
|
+
orgSlug: org.slug,
|
|
66
|
+
apiUrl: config.apiUrl,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (err instanceof api_1.ApiError) {
|
|
72
|
+
if (err.status === 401) {
|
|
73
|
+
return {
|
|
74
|
+
category: 'authentication',
|
|
75
|
+
name: 'api_key_valid',
|
|
76
|
+
status: 'error',
|
|
77
|
+
message: 'API key is invalid or expired',
|
|
78
|
+
fix: 'Run `orch login` to get a new key',
|
|
79
|
+
details: { error: err.message, status: err.status },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
category: 'authentication',
|
|
84
|
+
name: 'api_key_valid',
|
|
85
|
+
status: 'error',
|
|
86
|
+
message: `API key validation failed (${err.status})`,
|
|
87
|
+
fix: 'Check your network connection and try again',
|
|
88
|
+
details: { error: err.message, status: err.status },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
category: 'authentication',
|
|
93
|
+
name: 'api_key_valid',
|
|
94
|
+
status: 'error',
|
|
95
|
+
message: 'Could not validate API key',
|
|
96
|
+
fix: 'Check your network connection and try again',
|
|
97
|
+
details: { error: err instanceof Error ? err.message : 'unknown error' },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Run all auth checks.
|
|
103
|
+
*/
|
|
104
|
+
async function runAuthChecks() {
|
|
105
|
+
const results = [];
|
|
106
|
+
// First check if key is present
|
|
107
|
+
const keyPresent = await checkApiKeyPresent();
|
|
108
|
+
results.push(keyPresent);
|
|
109
|
+
// Only validate key if it's present
|
|
110
|
+
if (keyPresent.status === 'success') {
|
|
111
|
+
const keyValid = await checkApiKeyValid();
|
|
112
|
+
results.push(keyValid);
|
|
113
|
+
}
|
|
114
|
+
return results;
|
|
115
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.checkConfigExists = checkConfigExists;
|
|
7
|
+
exports.checkConfigPermissions = checkConfigPermissions;
|
|
8
|
+
exports.runConfigChecks = runConfigChecks;
|
|
9
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
12
|
+
const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.orchagent');
|
|
13
|
+
const CONFIG_PATH = path_1.default.join(CONFIG_DIR, 'config.json');
|
|
14
|
+
/**
|
|
15
|
+
* Check if config file exists.
|
|
16
|
+
*/
|
|
17
|
+
async function checkConfigExists() {
|
|
18
|
+
try {
|
|
19
|
+
await promises_1.default.access(CONFIG_PATH);
|
|
20
|
+
return {
|
|
21
|
+
category: 'configuration',
|
|
22
|
+
name: 'config_exists',
|
|
23
|
+
status: 'success',
|
|
24
|
+
message: `Config file exists (~/.orchagent/config.json)`,
|
|
25
|
+
details: { path: CONFIG_PATH },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return {
|
|
30
|
+
category: 'configuration',
|
|
31
|
+
name: 'config_exists',
|
|
32
|
+
status: 'warning',
|
|
33
|
+
message: 'Config file not found',
|
|
34
|
+
fix: 'Run `orch login` to create config',
|
|
35
|
+
details: { path: CONFIG_PATH, exists: false },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Check config file permissions (should be 600 for security).
|
|
41
|
+
*/
|
|
42
|
+
async function checkConfigPermissions() {
|
|
43
|
+
try {
|
|
44
|
+
const stats = await promises_1.default.stat(CONFIG_PATH);
|
|
45
|
+
// On Windows, file permissions work differently
|
|
46
|
+
if (process.platform === 'win32') {
|
|
47
|
+
return {
|
|
48
|
+
category: 'configuration',
|
|
49
|
+
name: 'config_permissions',
|
|
50
|
+
status: 'success',
|
|
51
|
+
message: 'Config file permissions (Windows)',
|
|
52
|
+
details: { platform: 'win32', note: 'permissions check skipped on Windows' },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Check if permissions are 600 (owner read/write only)
|
|
56
|
+
// mode & 0o777 gives us the permission bits
|
|
57
|
+
const mode = stats.mode & 0o777;
|
|
58
|
+
if (mode === 0o600) {
|
|
59
|
+
return {
|
|
60
|
+
category: 'configuration',
|
|
61
|
+
name: 'config_permissions',
|
|
62
|
+
status: 'success',
|
|
63
|
+
message: 'Config file permissions (600)',
|
|
64
|
+
details: { mode: mode.toString(8), expected: '600' },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Check if it's too permissive (world or group readable)
|
|
68
|
+
const worldReadable = (mode & 0o004) !== 0;
|
|
69
|
+
const groupReadable = (mode & 0o040) !== 0;
|
|
70
|
+
if (worldReadable || groupReadable) {
|
|
71
|
+
return {
|
|
72
|
+
category: 'configuration',
|
|
73
|
+
name: 'config_permissions',
|
|
74
|
+
status: 'warning',
|
|
75
|
+
message: `Config file permissions too open (${mode.toString(8)})`,
|
|
76
|
+
fix: 'Run `chmod 600 ~/.orchagent/config.json`',
|
|
77
|
+
details: {
|
|
78
|
+
mode: mode.toString(8),
|
|
79
|
+
expected: '600',
|
|
80
|
+
worldReadable,
|
|
81
|
+
groupReadable,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
category: 'configuration',
|
|
87
|
+
name: 'config_permissions',
|
|
88
|
+
status: 'success',
|
|
89
|
+
message: `Config file permissions (${mode.toString(8)})`,
|
|
90
|
+
details: { mode: mode.toString(8), expected: '600' },
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
if (err.code === 'ENOENT') {
|
|
95
|
+
// Config doesn't exist, skip permissions check
|
|
96
|
+
return {
|
|
97
|
+
category: 'configuration',
|
|
98
|
+
name: 'config_permissions',
|
|
99
|
+
status: 'warning',
|
|
100
|
+
message: 'Config file permissions (file not found)',
|
|
101
|
+
details: { error: 'file not found' },
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
category: 'configuration',
|
|
106
|
+
name: 'config_permissions',
|
|
107
|
+
status: 'warning',
|
|
108
|
+
message: 'Could not check config permissions',
|
|
109
|
+
details: { error: err instanceof Error ? err.message : 'unknown error' },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Run all config checks.
|
|
115
|
+
*/
|
|
116
|
+
async function runConfigChecks() {
|
|
117
|
+
const results = await Promise.all([checkConfigExists(), checkConfigPermissions()]);
|
|
118
|
+
return results;
|
|
119
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkGatewayHealth = checkGatewayHealth;
|
|
4
|
+
exports.runConnectivityChecks = runConnectivityChecks;
|
|
5
|
+
const config_1 = require("../../config");
|
|
6
|
+
const LATENCY_WARNING_MS = 2000;
|
|
7
|
+
/**
|
|
8
|
+
* Check if gateway is reachable by pinging /health endpoint.
|
|
9
|
+
* Also measures response time.
|
|
10
|
+
*/
|
|
11
|
+
async function checkGatewayHealth() {
|
|
12
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
13
|
+
const healthUrl = `${config.apiUrl.replace(/\/$/, '')}/health`;
|
|
14
|
+
const startTime = Date.now();
|
|
15
|
+
try {
|
|
16
|
+
const response = await fetch(healthUrl, {
|
|
17
|
+
signal: AbortSignal.timeout(10000),
|
|
18
|
+
});
|
|
19
|
+
const latency = Date.now() - startTime;
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
category: 'connectivity',
|
|
24
|
+
name: 'gateway_reachable',
|
|
25
|
+
status: 'error',
|
|
26
|
+
message: `Gateway returned ${response.status}`,
|
|
27
|
+
fix: 'Check if OrchAgent API is operational at https://status.orchagent.io',
|
|
28
|
+
details: {
|
|
29
|
+
url: healthUrl,
|
|
30
|
+
status: response.status,
|
|
31
|
+
latency,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
// Parse hostname for display
|
|
37
|
+
const apiHost = new URL(config.apiUrl).host;
|
|
38
|
+
const results = [
|
|
39
|
+
{
|
|
40
|
+
category: 'connectivity',
|
|
41
|
+
name: 'gateway_reachable',
|
|
42
|
+
status: 'success',
|
|
43
|
+
message: `Gateway reachable (${apiHost})`,
|
|
44
|
+
details: {
|
|
45
|
+
url: healthUrl,
|
|
46
|
+
status: response.status,
|
|
47
|
+
host: apiHost,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
// Add latency check
|
|
52
|
+
if (latency > LATENCY_WARNING_MS) {
|
|
53
|
+
results.push({
|
|
54
|
+
category: 'connectivity',
|
|
55
|
+
name: 'response_time',
|
|
56
|
+
status: 'warning',
|
|
57
|
+
message: `Response time: ${latency}ms (high latency)`,
|
|
58
|
+
fix: 'High latency detected. Check network or try a different region.',
|
|
59
|
+
details: { latency, threshold: LATENCY_WARNING_MS },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
results.push({
|
|
64
|
+
category: 'connectivity',
|
|
65
|
+
name: 'response_time',
|
|
66
|
+
status: 'success',
|
|
67
|
+
message: `Response time: ${latency}ms`,
|
|
68
|
+
details: { latency, threshold: LATENCY_WARNING_MS },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
const latency = Date.now() - startTime;
|
|
75
|
+
const isTimeout = err instanceof Error && (err.name === 'AbortError' || err.name === 'TimeoutError');
|
|
76
|
+
if (isTimeout) {
|
|
77
|
+
return [
|
|
78
|
+
{
|
|
79
|
+
category: 'connectivity',
|
|
80
|
+
name: 'gateway_reachable',
|
|
81
|
+
status: 'error',
|
|
82
|
+
message: 'Gateway connection timed out',
|
|
83
|
+
fix: 'Check network, firewall, or proxy settings',
|
|
84
|
+
details: { url: healthUrl, error: 'timeout', latency },
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
return [
|
|
89
|
+
{
|
|
90
|
+
category: 'connectivity',
|
|
91
|
+
name: 'gateway_reachable',
|
|
92
|
+
status: 'error',
|
|
93
|
+
message: 'Cannot reach gateway',
|
|
94
|
+
fix: 'Check network, firewall, or proxy settings',
|
|
95
|
+
details: {
|
|
96
|
+
url: healthUrl,
|
|
97
|
+
error: err instanceof Error ? err.message : 'unknown error',
|
|
98
|
+
latency,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Run all connectivity checks.
|
|
106
|
+
*/
|
|
107
|
+
async function runConnectivityChecks() {
|
|
108
|
+
return checkGatewayHealth();
|
|
109
|
+
}
|