@agentuity/cli 0.1.36 → 0.1.38
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/bin/cli.ts +20 -0
- package/dist/agent-detection.d.ts +71 -0
- package/dist/agent-detection.d.ts.map +1 -0
- package/dist/agent-detection.js +232 -0
- package/dist/agent-detection.js.map +1 -0
- package/dist/cache/index.d.ts +1 -0
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +1 -0
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/project-cache.d.ts +16 -0
- package/dist/cache/project-cache.d.ts.map +1 -0
- package/dist/cache/project-cache.js +36 -0
- package/dist/cache/project-cache.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +10 -2
- package/dist/cli.js.map +1 -1
- package/dist/cmd/auth/index.d.ts.map +1 -1
- package/dist/cmd/auth/index.js +0 -2
- package/dist/cmd/auth/index.js.map +1 -1
- package/dist/cmd/auth/org/enroll.d.ts +2 -0
- package/dist/cmd/auth/org/enroll.d.ts.map +1 -0
- package/dist/cmd/auth/{machine/setup.js → org/enroll.js} +14 -14
- package/dist/cmd/auth/org/enroll.js.map +1 -0
- package/dist/cmd/auth/org/index.d.ts.map +1 -1
- package/dist/cmd/auth/org/index.js +53 -10
- package/dist/cmd/auth/org/index.js.map +1 -1
- package/dist/cmd/auth/org/status.d.ts +2 -0
- package/dist/cmd/auth/org/status.d.ts.map +1 -0
- package/dist/cmd/auth/org/status.js +60 -0
- package/dist/cmd/auth/org/status.js.map +1 -0
- package/dist/cmd/auth/org/unenroll.d.ts +2 -0
- package/dist/cmd/auth/org/unenroll.d.ts.map +1 -0
- package/dist/cmd/auth/org/unenroll.js +68 -0
- package/dist/cmd/auth/org/unenroll.js.map +1 -0
- package/dist/cmd/build/ast.d.ts.map +1 -1
- package/dist/cmd/build/ast.js +6 -0
- package/dist/cmd/build/ast.js.map +1 -1
- package/dist/cmd/cloud/deploy.d.ts.map +1 -1
- package/dist/cmd/cloud/deploy.js +11 -4
- package/dist/cmd/cloud/deploy.js.map +1 -1
- package/dist/cmd/cloud/keyvalue/get.d.ts.map +1 -1
- package/dist/cmd/cloud/keyvalue/get.js +6 -1
- package/dist/cmd/cloud/keyvalue/get.js.map +1 -1
- package/dist/cmd/cloud/keyvalue/repl.d.ts.map +1 -1
- package/dist/cmd/cloud/keyvalue/repl.js +6 -1
- package/dist/cmd/cloud/keyvalue/repl.js.map +1 -1
- package/dist/cmd/cloud/keyvalue/set.d.ts.map +1 -1
- package/dist/cmd/cloud/keyvalue/set.js +7 -1
- package/dist/cmd/cloud/keyvalue/set.js.map +1 -1
- package/dist/cmd/cloud/vector/get.d.ts.map +1 -1
- package/dist/cmd/cloud/vector/get.js +5 -0
- package/dist/cmd/cloud/vector/get.js.map +1 -1
- package/dist/cmd/cloud/vector/search.d.ts.map +1 -1
- package/dist/cmd/cloud/vector/search.js +2 -0
- package/dist/cmd/cloud/vector/search.js.map +1 -1
- package/dist/cmd/cloud/vector/upsert.d.ts.map +1 -1
- package/dist/cmd/cloud/vector/upsert.js +24 -1
- package/dist/cmd/cloud/vector/upsert.js.map +1 -1
- package/dist/cmd/project/reconcile.d.ts.map +1 -1
- package/dist/cmd/project/reconcile.js +15 -2
- package/dist/cmd/project/reconcile.js.map +1 -1
- package/dist/cmd/support/report.d.ts.map +1 -1
- package/dist/cmd/support/report.js.map +1 -1
- package/dist/cmd/support/system.d.ts.map +1 -1
- package/dist/cmd/support/system.js +5 -0
- package/dist/cmd/support/system.js.map +1 -1
- package/dist/internal-logger.d.ts +4 -0
- package/dist/internal-logger.d.ts.map +1 -1
- package/dist/internal-logger.js +18 -0
- package/dist/internal-logger.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +6 -6
- package/src/agent-detection.ts +262 -0
- package/src/cache/index.ts +2 -0
- package/src/cache/project-cache.ts +41 -0
- package/src/cli.ts +10 -2
- package/src/cmd/auth/index.ts +0 -2
- package/src/cmd/auth/{machine/setup.ts → org/enroll.ts} +13 -13
- package/src/cmd/auth/org/index.ts +54 -10
- package/src/cmd/auth/org/status.ts +64 -0
- package/src/cmd/auth/org/unenroll.ts +80 -0
- package/src/cmd/build/ast.ts +6 -0
- package/src/cmd/cloud/deploy.ts +11 -4
- package/src/cmd/cloud/keyvalue/get.ts +6 -1
- package/src/cmd/cloud/keyvalue/repl.ts +6 -1
- package/src/cmd/cloud/keyvalue/set.ts +7 -1
- package/src/cmd/cloud/vector/get.ts +6 -0
- package/src/cmd/cloud/vector/search.ts +2 -0
- package/src/cmd/cloud/vector/upsert.ts +28 -1
- package/src/cmd/project/reconcile.ts +15 -2
- package/src/cmd/support/report.ts +1 -3
- package/src/cmd/support/system.ts +6 -0
- package/src/internal-logger.ts +18 -0
- package/src/types.ts +13 -0
- package/dist/cmd/auth/machine/index.d.ts +0 -2
- package/dist/cmd/auth/machine/index.d.ts.map +0 -1
- package/dist/cmd/auth/machine/index.js +0 -16
- package/dist/cmd/auth/machine/index.js.map +0 -1
- package/dist/cmd/auth/machine/setup.d.ts +0 -2
- package/dist/cmd/auth/machine/setup.d.ts.map +0 -1
- package/dist/cmd/auth/machine/setup.js.map +0 -1
- package/src/cmd/auth/machine/index.ts +0 -16
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Map of process names to internal agent short names.
|
|
5
|
+
* The key is the process name (or substring) that appears in the parent process command line.
|
|
6
|
+
* The value is the internal short name used to identify the agent.
|
|
7
|
+
*
|
|
8
|
+
* Process names verified via `agentuity cloud sandbox run --runtime <agent>:latest`:
|
|
9
|
+
* - opencode: binary 'opencode' (from bun install -g opencode-ai)
|
|
10
|
+
* - codex: binary 'codex' (from npm install -g @openai/codex)
|
|
11
|
+
* - cursor: binary 'cursor-agent' (from curl installer)
|
|
12
|
+
* - claude-code: binary 'claude', shows as 'node /usr/local/bin/claude'
|
|
13
|
+
* - copilot: binary 'copilot', shows as 'node /usr/local/bin/copilot' and spawns native binary
|
|
14
|
+
* - gemini: binary 'gemini', shows as 'node /usr/local/bin/gemini'
|
|
15
|
+
* - amp: binary 'amp', shows as 'node --no-warnings /usr/local/bin/amp'
|
|
16
|
+
*
|
|
17
|
+
* IMPORTANT: Order matters! More specific patterns should come before less specific ones.
|
|
18
|
+
* For example, 'opencode' must be checked before 'code' to avoid false matches.
|
|
19
|
+
*/
|
|
20
|
+
export const KNOWN_AGENTS: [string, string][] = [
|
|
21
|
+
// Verified via cloud sandbox runtime - most specific patterns first
|
|
22
|
+
['opencode', 'opencode'],
|
|
23
|
+
['codex', 'codex'],
|
|
24
|
+
['cursor-agent', 'cursor'],
|
|
25
|
+
['claude', 'claude-code'],
|
|
26
|
+
['copilot', 'copilot'],
|
|
27
|
+
['gemini', 'gemini'],
|
|
28
|
+
['cline', 'cline'],
|
|
29
|
+
['roo-code', 'roo'],
|
|
30
|
+
['windsurf', 'windsurf'],
|
|
31
|
+
['zed', 'zed'],
|
|
32
|
+
['amp', 'amp'],
|
|
33
|
+
// TODO: VSCode Agent Mode detection - need to find a reliable way to detect
|
|
34
|
+
// when VSCode's built-in agent (Copilot Chat) is running commands vs just
|
|
35
|
+
// running in VSCode's integrated terminal. May need env var detection.
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export type KnownAgent = (typeof KNOWN_AGENTS)[number][1];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Promise for the detection result (set when detection starts)
|
|
42
|
+
*/
|
|
43
|
+
let detectionPromise: Promise<string | undefined> | null = null;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Cached result after detection completes (null = not yet resolved)
|
|
47
|
+
*/
|
|
48
|
+
let cachedResult: string | undefined | null = null;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Callbacks to invoke when agent detection completes
|
|
52
|
+
*/
|
|
53
|
+
const detectionCallbacks: Array<(agent: string | undefined) => void> = [];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get the command line and parent PID for a given PID in a single ps call
|
|
57
|
+
*/
|
|
58
|
+
function getProcessInfo(pid: number): Promise<{ command: string; ppid: number } | undefined> {
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
const ps = spawn('ps', ['-p', String(pid), '-o', 'ppid=,command='], {
|
|
61
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
let output = '';
|
|
65
|
+
|
|
66
|
+
ps.stdout.on('data', (data: Buffer) => {
|
|
67
|
+
output += data.toString();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
ps.on('close', () => {
|
|
71
|
+
const trimmed = output.trim();
|
|
72
|
+
if (!trimmed) {
|
|
73
|
+
resolve(undefined);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Output format: " PPID COMMAND" (ppid is right-aligned, then space, then command)
|
|
78
|
+
const match = trimmed.match(/^\s*(\d+)\s+(.+)$/);
|
|
79
|
+
if (!match) {
|
|
80
|
+
resolve(undefined);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const ppid = parseInt(match[1], 10);
|
|
85
|
+
const command = match[2].toLowerCase();
|
|
86
|
+
|
|
87
|
+
if (isNaN(ppid) || ppid <= 1 || !command) {
|
|
88
|
+
resolve(undefined);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
resolve({ command, ppid });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
ps.on('error', () => {
|
|
96
|
+
resolve(undefined);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if a command matches any known agent
|
|
103
|
+
*/
|
|
104
|
+
function matchAgent(command: string): string | undefined {
|
|
105
|
+
for (const [processName, agentName] of KNOWN_AGENTS) {
|
|
106
|
+
if (command.includes(processName)) {
|
|
107
|
+
return agentName;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Detect the parent process command and check if it matches a known agent.
|
|
115
|
+
* Walks up the process tree to find the first matching agent.
|
|
116
|
+
*/
|
|
117
|
+
function detectParentAgent(): Promise<string | undefined> {
|
|
118
|
+
return new Promise((resolve) => {
|
|
119
|
+
// TODO: Implement Windows support using wmic or PowerShell
|
|
120
|
+
if (process.platform === 'win32') {
|
|
121
|
+
resolve(undefined);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const maxDepth = 10; // Limit how far up we walk the tree
|
|
126
|
+
|
|
127
|
+
async function walkTree(pid: number, depth: number): Promise<string | undefined> {
|
|
128
|
+
if (depth >= maxDepth || pid <= 1) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Get both command and parent PID in a single ps call
|
|
133
|
+
const info = await getProcessInfo(pid);
|
|
134
|
+
if (!info) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check if this process matches a known agent
|
|
139
|
+
const agent = matchAgent(info.command);
|
|
140
|
+
if (agent) {
|
|
141
|
+
return agent;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Walk up to parent
|
|
145
|
+
return walkTree(info.ppid, depth + 1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const ppid = process.ppid;
|
|
149
|
+
if (!ppid) {
|
|
150
|
+
resolve(undefined);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
walkTree(ppid, 0).then(resolve).catch(() => resolve(undefined));
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Start agent detection immediately (non-blocking).
|
|
160
|
+
* Call this early in CLI startup to begin detection in the background.
|
|
161
|
+
*/
|
|
162
|
+
export function startAgentDetection(): void {
|
|
163
|
+
if (detectionPromise !== null) {
|
|
164
|
+
// Already started
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
detectionPromise = detectParentAgent().then((result) => {
|
|
169
|
+
cachedResult = result;
|
|
170
|
+
// Invoke all registered callbacks
|
|
171
|
+
for (const callback of detectionCallbacks) {
|
|
172
|
+
try {
|
|
173
|
+
callback(result);
|
|
174
|
+
} catch {
|
|
175
|
+
// Ignore callback errors
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Register a callback to be invoked when agent detection completes.
|
|
184
|
+
* If detection has already completed, the callback is invoked immediately.
|
|
185
|
+
* This is non-blocking and does not return a promise.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```typescript
|
|
189
|
+
* onAgentDetected((agent) => {
|
|
190
|
+
* if (agent) {
|
|
191
|
+
* console.log(`Detected agent: ${agent}`);
|
|
192
|
+
* }
|
|
193
|
+
* });
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
export function onAgentDetected(callback: (agent: string | undefined) => void): void {
|
|
197
|
+
// If detection already completed, invoke immediately
|
|
198
|
+
if (cachedResult !== null) {
|
|
199
|
+
try {
|
|
200
|
+
callback(cachedResult);
|
|
201
|
+
} catch {
|
|
202
|
+
// Ignore callback errors
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Otherwise, register for later invocation
|
|
208
|
+
detectionCallbacks.push(callback);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get the cached detection result synchronously.
|
|
213
|
+
* Returns undefined if detection hasn't completed yet or no agent was detected.
|
|
214
|
+
* Returns null if detection hasn't started or completed yet.
|
|
215
|
+
*
|
|
216
|
+
* Use this for synchronous access when you don't want to wait for detection.
|
|
217
|
+
*/
|
|
218
|
+
export function getDetectedAgent(): string | undefined | null {
|
|
219
|
+
return cachedResult;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Wait for agent detection to complete and ensure all callbacks have been invoked.
|
|
224
|
+
* Call this before CLI exit to ensure the detected agent is written to session logs.
|
|
225
|
+
*
|
|
226
|
+
* This is a no-op if detection hasn't started or has already completed.
|
|
227
|
+
*/
|
|
228
|
+
export async function flushAgentDetection(): Promise<void> {
|
|
229
|
+
if (detectionPromise !== null) {
|
|
230
|
+
await detectionPromise;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Check if the CLI is being executed from a known coding agent.
|
|
236
|
+
* Returns the agent name if detected, undefined otherwise.
|
|
237
|
+
*
|
|
238
|
+
* This function returns immediately if detection has already completed,
|
|
239
|
+
* otherwise it awaits the detection promise started by startAgentDetection().
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```typescript
|
|
243
|
+
* const agent = await isExecutingFromAgent();
|
|
244
|
+
* if (agent) {
|
|
245
|
+
* logger.debug(`Running from agent: ${agent}`);
|
|
246
|
+
* }
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
export async function isExecutingFromAgent(): Promise<string | undefined> {
|
|
250
|
+
// Return cached result if detection has completed
|
|
251
|
+
if (cachedResult !== null) {
|
|
252
|
+
return cachedResult;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// If detection hasn't started yet, start it now
|
|
256
|
+
if (detectionPromise === null) {
|
|
257
|
+
startAgentDetection();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Wait for detection to complete
|
|
261
|
+
return detectionPromise!;
|
|
262
|
+
}
|
package/src/cache/index.ts
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Project } from '@agentuity/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-memory cache for project data to avoid duplicate API calls within a single CLI command execution.
|
|
5
|
+
* This cache is NOT persisted to disk - it only lives for the duration of the CLI process.
|
|
6
|
+
*
|
|
7
|
+
* The cache key is `{profile}:{projectId}` to ensure proper isolation between profiles.
|
|
8
|
+
*/
|
|
9
|
+
const projectCache = new Map<string, Project>();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate a cache key from profile and project ID
|
|
13
|
+
*/
|
|
14
|
+
function getCacheKey(profile: string, projectId: string): string {
|
|
15
|
+
return `${profile}:${projectId}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get a cached project by profile and project ID.
|
|
20
|
+
* Returns null if not found in cache.
|
|
21
|
+
*/
|
|
22
|
+
export function getCachedProject(profile: string, projectId: string): Project | null {
|
|
23
|
+
const key = getCacheKey(profile, projectId);
|
|
24
|
+
return projectCache.get(key) ?? null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Store a project in the cache.
|
|
29
|
+
*/
|
|
30
|
+
export function setCachedProject(profile: string, projectId: string, project: Project): void {
|
|
31
|
+
const key = getCacheKey(profile, projectId);
|
|
32
|
+
projectCache.set(key, project);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Clear all cached projects.
|
|
37
|
+
* Useful for testing or when switching contexts.
|
|
38
|
+
*/
|
|
39
|
+
export function clearProjectCache(): void {
|
|
40
|
+
projectCache.clear();
|
|
41
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { getCommand } from './command-prefix';
|
|
|
27
27
|
import { isValidateMode, outputValidation, type ValidationResult } from './output';
|
|
28
28
|
import { StructuredError } from '@agentuity/core';
|
|
29
29
|
import { setProgram } from './program-ref';
|
|
30
|
+
import { getCachedProject, setCachedProject } from './cache';
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* Check if an error is a CLI input validation error (Zod error from schema parsing),
|
|
@@ -1052,8 +1053,15 @@ async function registerSubcommand(
|
|
|
1052
1053
|
},
|
|
1053
1054
|
};
|
|
1054
1055
|
const apiClient = createAPIClient(baseCtx, configWithAuth as Config);
|
|
1055
|
-
|
|
1056
|
-
const
|
|
1056
|
+
// Check cache first to avoid duplicate API calls
|
|
1057
|
+
const profile = baseCtx.config?.name ?? 'default';
|
|
1058
|
+
let projectDetails = getCachedProject(profile, projectId);
|
|
1059
|
+
if (!projectDetails) {
|
|
1060
|
+
const { projectGet } = await import('@agentuity/server');
|
|
1061
|
+
// Use keys: false to match other callers and ensure cache consistency
|
|
1062
|
+
projectDetails = await projectGet(apiClient, { id: projectId, keys: false });
|
|
1063
|
+
setCachedProject(profile, projectId, projectDetails);
|
|
1064
|
+
}
|
|
1057
1065
|
project = {
|
|
1058
1066
|
projectId: projectDetails.id,
|
|
1059
1067
|
orgId: projectDetails.orgId,
|
package/src/cmd/auth/index.ts
CHANGED
|
@@ -6,7 +6,6 @@ import { signupCommand } from './signup';
|
|
|
6
6
|
import { whoamiCommand } from './whoami';
|
|
7
7
|
import { sshSubcommand } from './ssh';
|
|
8
8
|
import { orgSubcommand } from './org';
|
|
9
|
-
import { machineSubcommand } from './machine';
|
|
10
9
|
import { getCommand } from '../../command-prefix';
|
|
11
10
|
|
|
12
11
|
export const command = createCommand({
|
|
@@ -26,6 +25,5 @@ export const command = createCommand({
|
|
|
26
25
|
whoamiCommand,
|
|
27
26
|
sshSubcommand,
|
|
28
27
|
orgSubcommand,
|
|
29
|
-
machineSubcommand,
|
|
30
28
|
],
|
|
31
29
|
});
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { createSubcommand } from '../../../types';
|
|
3
3
|
import * as tui from '../../../tui';
|
|
4
|
-
import {
|
|
4
|
+
import { orgAuthEnroll } from '@agentuity/server';
|
|
5
5
|
import { getCommand } from '../../../command-prefix';
|
|
6
6
|
import { ErrorCode } from '../../../errors';
|
|
7
7
|
import { readFileSync } from 'fs';
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
success: z.boolean().describe('Whether the
|
|
9
|
+
const EnrollResponseSchema = z.object({
|
|
10
|
+
success: z.boolean().describe('Whether the enrollment succeeded'),
|
|
11
11
|
orgId: z.string().describe('The organization ID'),
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
export const
|
|
15
|
-
name: '
|
|
14
|
+
export const enrollSubcommand = createSubcommand({
|
|
15
|
+
name: 'enroll',
|
|
16
16
|
description:
|
|
17
|
-
'
|
|
17
|
+
'Configure your organization for self-hosted infrastructure by uploading a public key',
|
|
18
18
|
tags: ['mutating', 'slow', 'requires-auth', 'uses-stdin'],
|
|
19
19
|
examples: [
|
|
20
20
|
{
|
|
21
|
-
command: `${getCommand('auth
|
|
21
|
+
command: `${getCommand('auth org enroll')} --file ./public-key.pem`,
|
|
22
22
|
description: 'Upload ECDSA P-256 public key from file',
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
|
-
command: `cat public-key.pem | ${getCommand('auth
|
|
25
|
+
command: `cat public-key.pem | ${getCommand('auth org enroll')}`,
|
|
26
26
|
description: 'Upload public key from stdin',
|
|
27
27
|
},
|
|
28
28
|
],
|
|
@@ -32,7 +32,7 @@ export const setupSubcommand = createSubcommand({
|
|
|
32
32
|
options: z.object({
|
|
33
33
|
file: z.string().optional().describe('Path to the public key file (PEM format)'),
|
|
34
34
|
}),
|
|
35
|
-
response:
|
|
35
|
+
response: EnrollResponseSchema,
|
|
36
36
|
},
|
|
37
37
|
async handler(ctx) {
|
|
38
38
|
const { apiClient, opts, options, logger, orgId } = ctx;
|
|
@@ -83,13 +83,13 @@ export const setupSubcommand = createSubcommand({
|
|
|
83
83
|
|
|
84
84
|
const result = await tui.spinner({
|
|
85
85
|
type: 'simple',
|
|
86
|
-
message: '
|
|
87
|
-
callback: () =>
|
|
86
|
+
message: 'Enrolling organization...',
|
|
87
|
+
callback: () => orgAuthEnroll(apiClient, orgId, publicKey),
|
|
88
88
|
clearOnSuccess: true,
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
if (!options.json) {
|
|
92
|
-
tui.success(`
|
|
92
|
+
tui.success(`Organization ${result.orgId} enrolled for self-hosted infrastructure.`);
|
|
93
93
|
tui.newline();
|
|
94
94
|
tui.info(
|
|
95
95
|
'Your self-hosted machines can now authenticate using the corresponding private key.'
|
|
@@ -98,7 +98,7 @@ export const setupSubcommand = createSubcommand({
|
|
|
98
98
|
|
|
99
99
|
return { success: true, orgId: result.orgId };
|
|
100
100
|
} catch (ex) {
|
|
101
|
-
tui.fatal(`Failed to
|
|
101
|
+
tui.fatal(`Failed to enroll organization: ${ex}`, ErrorCode.API_ERROR);
|
|
102
102
|
}
|
|
103
103
|
},
|
|
104
104
|
});
|
|
@@ -4,6 +4,9 @@ import { getCommand } from '../../../command-prefix';
|
|
|
4
4
|
import { saveOrgId, clearOrgId } from '../../../config';
|
|
5
5
|
import * as tui from '../../../tui';
|
|
6
6
|
import { listOrganizations } from '@agentuity/server';
|
|
7
|
+
import { enrollSubcommand } from './enroll';
|
|
8
|
+
import { unenrollSubcommand } from './unenroll';
|
|
9
|
+
import { statusSubcommand } from './status';
|
|
7
10
|
|
|
8
11
|
const selectCommand = createSubcommand({
|
|
9
12
|
name: 'select',
|
|
@@ -75,7 +78,8 @@ const selectCommand = createSubcommand({
|
|
|
75
78
|
const unselectCommand = createSubcommand({
|
|
76
79
|
name: 'unselect',
|
|
77
80
|
description: 'Clear the default organization preference',
|
|
78
|
-
tags: ['fast'],
|
|
81
|
+
tags: ['fast', 'requires-auth'],
|
|
82
|
+
requires: { auth: true, apiClient: true },
|
|
79
83
|
examples: [
|
|
80
84
|
{ command: getCommand('auth org unselect'), description: 'Clear default organization' },
|
|
81
85
|
],
|
|
@@ -106,37 +110,77 @@ const unselectCommand = createSubcommand({
|
|
|
106
110
|
const currentCommand = createSubcommand({
|
|
107
111
|
name: 'current',
|
|
108
112
|
description: 'Show the current default organization',
|
|
109
|
-
tags: ['read-only', 'fast'],
|
|
113
|
+
tags: ['read-only', 'fast', 'requires-auth'],
|
|
110
114
|
idempotent: true,
|
|
115
|
+
requires: { auth: true, apiClient: true },
|
|
111
116
|
examples: [
|
|
112
|
-
{ command: getCommand('auth org current'), description: 'Show default organization' },
|
|
117
|
+
{ command: getCommand('auth org current'), description: 'Show default organization ID' },
|
|
118
|
+
{ command: getCommand('auth org current --name'), description: 'Show default organization name' },
|
|
113
119
|
{ command: getCommand('auth org current --json'), description: 'Show output in JSON format' },
|
|
114
120
|
],
|
|
115
121
|
schema: {
|
|
116
|
-
|
|
122
|
+
options: z.object({
|
|
123
|
+
name: z.boolean().optional().describe('Show organization name instead of ID'),
|
|
124
|
+
}),
|
|
125
|
+
response: z
|
|
126
|
+
.object({
|
|
127
|
+
id: z.string().nullable().describe('The current organization ID or null if not set'),
|
|
128
|
+
name: z.string().nullable().describe('The current organization name or null if not set or not found'),
|
|
129
|
+
})
|
|
130
|
+
.describe('The current organization details'),
|
|
117
131
|
},
|
|
118
132
|
|
|
119
133
|
async handler(ctx) {
|
|
120
|
-
const { options, config } = ctx;
|
|
134
|
+
const { options, config, apiClient, opts } = ctx;
|
|
121
135
|
const orgId = config?.preferences?.orgId || null;
|
|
122
136
|
|
|
137
|
+
let orgName: string | null = null;
|
|
138
|
+
|
|
139
|
+
// Fetch org name if we have an orgId and either --name or --json is requested
|
|
140
|
+
if (orgId && (opts.name || options.json)) {
|
|
141
|
+
const orgs = await listOrganizations(apiClient);
|
|
142
|
+
const org = orgs.find((o) => o.id === orgId);
|
|
143
|
+
orgName = org?.name ?? null;
|
|
144
|
+
}
|
|
145
|
+
|
|
123
146
|
if (!options.json) {
|
|
124
|
-
if (
|
|
125
|
-
|
|
147
|
+
if (opts.name) {
|
|
148
|
+
// --name flag: print only the org name
|
|
149
|
+
if (orgName) {
|
|
150
|
+
console.log(orgName);
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
// Default behavior: print only the org ID
|
|
154
|
+
if (orgId) {
|
|
155
|
+
console.log(orgId);
|
|
156
|
+
}
|
|
126
157
|
}
|
|
127
158
|
}
|
|
128
159
|
|
|
129
|
-
return orgId;
|
|
160
|
+
return { id: orgId, name: orgName };
|
|
130
161
|
},
|
|
131
162
|
});
|
|
132
163
|
|
|
133
164
|
export const orgSubcommand = createCommand({
|
|
134
165
|
name: 'org',
|
|
135
|
-
|
|
166
|
+
aliases: ['machine', 'organization'],
|
|
167
|
+
description: 'Manage organization preferences and machine authentication',
|
|
136
168
|
tags: ['fast'],
|
|
137
169
|
examples: [
|
|
138
170
|
{ command: getCommand('auth org select'), description: 'Set default organization' },
|
|
139
171
|
{ command: getCommand('auth org current'), description: 'Show current default' },
|
|
172
|
+
{
|
|
173
|
+
command: getCommand('auth org enroll --file ./public-key.pem'),
|
|
174
|
+
description: 'Enroll an organization',
|
|
175
|
+
},
|
|
176
|
+
{ command: getCommand('auth org status'), description: 'Show org auth status' },
|
|
177
|
+
],
|
|
178
|
+
subcommands: [
|
|
179
|
+
selectCommand,
|
|
180
|
+
unselectCommand,
|
|
181
|
+
currentCommand,
|
|
182
|
+
enrollSubcommand,
|
|
183
|
+
unenrollSubcommand,
|
|
184
|
+
statusSubcommand,
|
|
140
185
|
],
|
|
141
|
-
subcommands: [selectCommand, unselectCommand, currentCommand],
|
|
142
186
|
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createSubcommand } from '../../../types';
|
|
3
|
+
import * as tui from '../../../tui';
|
|
4
|
+
import { orgAuthStatus } from '@agentuity/server';
|
|
5
|
+
import { getCommand } from '../../../command-prefix';
|
|
6
|
+
import { ErrorCode } from '../../../errors';
|
|
7
|
+
|
|
8
|
+
const StatusResponseSchema = z.object({
|
|
9
|
+
publicKey: z.string().nullable().describe('The public key or null if not set'),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const statusSubcommand = createSubcommand({
|
|
13
|
+
name: 'status',
|
|
14
|
+
aliases: ['show'],
|
|
15
|
+
description: 'Get the current public key status for an organization',
|
|
16
|
+
tags: ['read-only', 'fast', 'requires-auth'],
|
|
17
|
+
examples: [
|
|
18
|
+
{
|
|
19
|
+
command: getCommand('auth org status'),
|
|
20
|
+
description: 'Show the current public key for the organization',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
command: getCommand('auth org show'),
|
|
24
|
+
description: 'Show the current public key (alias)',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
requires: { auth: true, apiClient: true, org: true },
|
|
28
|
+
idempotent: true,
|
|
29
|
+
schema: {
|
|
30
|
+
response: StatusResponseSchema,
|
|
31
|
+
},
|
|
32
|
+
async handler(ctx) {
|
|
33
|
+
const { apiClient, options, orgId } = ctx;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const result = await tui.spinner({
|
|
37
|
+
type: 'simple',
|
|
38
|
+
message: 'Fetching organization authentication status...',
|
|
39
|
+
callback: () => orgAuthStatus(apiClient, orgId),
|
|
40
|
+
clearOnSuccess: true,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!options.json) {
|
|
44
|
+
if (result.publicKey) {
|
|
45
|
+
tui.success('Organization authentication is configured');
|
|
46
|
+
tui.newline();
|
|
47
|
+
console.log(tui.bold('Public Key:'));
|
|
48
|
+
tui.newline();
|
|
49
|
+
console.log(result.publicKey);
|
|
50
|
+
} else {
|
|
51
|
+
tui.info('No public key is configured for this organization');
|
|
52
|
+
tui.newline();
|
|
53
|
+
tui.info(
|
|
54
|
+
`Use '${getCommand('auth org enroll --file ./public-key.pem')}' to set up machine authentication.`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { publicKey: result.publicKey };
|
|
60
|
+
} catch (ex) {
|
|
61
|
+
tui.fatal(`Failed to get organization authentication status: ${ex}`, ErrorCode.API_ERROR);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createSubcommand } from '../../../types';
|
|
3
|
+
import * as tui from '../../../tui';
|
|
4
|
+
import { orgAuthUnenroll } from '@agentuity/server';
|
|
5
|
+
import { getCommand } from '../../../command-prefix';
|
|
6
|
+
import { ErrorCode } from '../../../errors';
|
|
7
|
+
|
|
8
|
+
const UnenrollResponseSchema = z.object({
|
|
9
|
+
success: z.boolean().describe('Whether the unenrollment succeeded'),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const unenrollSubcommand = createSubcommand({
|
|
13
|
+
name: 'unenroll',
|
|
14
|
+
description: 'Remove the public key from the organization, disabling self-hosted infrastructure',
|
|
15
|
+
tags: ['mutating', 'destructive', 'slow', 'requires-auth'],
|
|
16
|
+
examples: [
|
|
17
|
+
{
|
|
18
|
+
command: getCommand('auth org unenroll'),
|
|
19
|
+
description: 'Remove the public key from the current organization (with confirmation)',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
command: getCommand('auth org unenroll --confirm'),
|
|
23
|
+
description: 'Remove the public key without confirmation prompt',
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
requires: { auth: true, apiClient: true, org: true },
|
|
27
|
+
idempotent: true,
|
|
28
|
+
schema: {
|
|
29
|
+
options: z.object({
|
|
30
|
+
confirm: z.boolean().optional().default(false).describe('Skip confirmation prompt'),
|
|
31
|
+
}),
|
|
32
|
+
response: UnenrollResponseSchema,
|
|
33
|
+
},
|
|
34
|
+
async handler(ctx) {
|
|
35
|
+
const { apiClient, options, opts, orgId } = ctx;
|
|
36
|
+
|
|
37
|
+
if (!opts.confirm) {
|
|
38
|
+
if (process.stdin.isTTY) {
|
|
39
|
+
tui.newline();
|
|
40
|
+
tui.warning(
|
|
41
|
+
'Removing your public key will immediately stop all network traffic and routing to your machines and all further traffic will immediately fail.'
|
|
42
|
+
);
|
|
43
|
+
tui.newline();
|
|
44
|
+
|
|
45
|
+
const confirmed = await tui.confirm(
|
|
46
|
+
'Are you sure you want to remove organization authentication?',
|
|
47
|
+
false
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (!confirmed) {
|
|
51
|
+
tui.info('Unenrollment cancelled.');
|
|
52
|
+
return { success: false };
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
tui.error(
|
|
56
|
+
'Non-interactive sessions require --confirm flag to unenroll. Use: ' +
|
|
57
|
+
getCommand('auth org unenroll --confirm')
|
|
58
|
+
);
|
|
59
|
+
return { success: false };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await tui.spinner({
|
|
65
|
+
type: 'simple',
|
|
66
|
+
message: 'Removing organization authentication...',
|
|
67
|
+
callback: () => orgAuthUnenroll(apiClient, orgId),
|
|
68
|
+
clearOnSuccess: true,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!options.json) {
|
|
72
|
+
tui.success('Organization authentication removed');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { success: true };
|
|
76
|
+
} catch (ex) {
|
|
77
|
+
tui.fatal(`Failed to unenroll organization: ${ex}`, ErrorCode.API_ERROR);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
});
|