@orchagent/cli 0.2.11 → 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.
@@ -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
+ }
@@ -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
+ }
@@ -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
- const error = await initResponse.json().catch(() => ({}));
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
- const error = await exchangeResponse.json().catch(() => ({}));
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
- const error = await initResponse.json().catch(() => ({}));
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
- const error = await exchangeResponse.json().catch(() => ({}));
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
+ }