@magnetoagents/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +190 -0
- package/dist/cli.d.ts +22 -0
- package/dist/cli.js +671 -0
- package/dist/client.d.ts +90 -0
- package/dist/client.js +245 -0
- package/dist/credentials.d.ts +14 -0
- package/dist/credentials.js +63 -0
- package/dist/format.d.ts +31 -0
- package/dist/format.js +175 -0
- package/dist/open-url.d.ts +1 -0
- package/dist/open-url.js +15 -0
- package/dist/package-version.d.ts +2 -0
- package/dist/package-version.js +10 -0
- package/package.json +39 -0
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export type MagnetoClientOptions = {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
/** Origin or full API base. Accepts `https://magnetoapp.io` or `.../api/v1`. */
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
fetchImpl?: typeof fetch;
|
|
6
|
+
/** Per-request timeout. `0` disables. Default `MAGNETO_TIMEOUT_MS` or 30s. */
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
/** Injected so tests can assert 429 sleep without waiting. */
|
|
9
|
+
sleepImpl?: (ms: number) => Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
export declare class MagnetoApiError extends Error {
|
|
12
|
+
status: number;
|
|
13
|
+
detail: string;
|
|
14
|
+
retryAfter?: string;
|
|
15
|
+
requestId?: string;
|
|
16
|
+
constructor(status: number, detail: string, retryAfter?: string, requestId?: string);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parse `Retry-After` as delta-seconds or HTTP-date.
|
|
20
|
+
* Missing/invalid → 1000 ms. Cap 60 s. Past HTTP-date → 0.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseRetryAfter(raw: string | null | undefined, now?: number): number;
|
|
23
|
+
/**
|
|
24
|
+
* Normalize a user-supplied base into `…/api/v1` without doubling the prefix.
|
|
25
|
+
*/
|
|
26
|
+
export declare function normalizeApiBase(input?: string | null): string;
|
|
27
|
+
export type ResizeBody = {
|
|
28
|
+
vcpu: number;
|
|
29
|
+
ram_gb: number;
|
|
30
|
+
disk_gb: number;
|
|
31
|
+
};
|
|
32
|
+
export type BashBody = {
|
|
33
|
+
command: string;
|
|
34
|
+
timeout?: number;
|
|
35
|
+
};
|
|
36
|
+
export type BashResult = {
|
|
37
|
+
output: string;
|
|
38
|
+
exit_code: number;
|
|
39
|
+
timed_out?: boolean;
|
|
40
|
+
truncated?: boolean;
|
|
41
|
+
duration_ms?: number;
|
|
42
|
+
};
|
|
43
|
+
export type GatewayTarget = 'desktop' | 'terminal';
|
|
44
|
+
export declare function createClient(opts: MagnetoClientOptions): {
|
|
45
|
+
base: string;
|
|
46
|
+
listComputers: () => Promise<unknown[]>;
|
|
47
|
+
getComputer: (id: string) => Promise<unknown>;
|
|
48
|
+
createComputer: (body: Record<string, unknown>) => Promise<unknown>;
|
|
49
|
+
stopComputer: (id: string) => Promise<unknown>;
|
|
50
|
+
startComputer: (id: string) => Promise<unknown>;
|
|
51
|
+
restartComputer: (id: string) => Promise<unknown>;
|
|
52
|
+
resizeComputer: (id: string, body: ResizeBody) => Promise<unknown>;
|
|
53
|
+
deleteComputer: (id: string, confirm: string) => Promise<unknown>;
|
|
54
|
+
getComputerUptime: (id: string, windowSeconds?: number) => Promise<unknown>;
|
|
55
|
+
mintGatewayUrl: (id: string, target: GatewayTarget) => Promise<{
|
|
56
|
+
url: string;
|
|
57
|
+
}>;
|
|
58
|
+
listRuns: (computerId: string, page?: {
|
|
59
|
+
limit?: number;
|
|
60
|
+
offset?: number;
|
|
61
|
+
}) => Promise<unknown>;
|
|
62
|
+
getRun: (computerId: string, runId: string) => Promise<unknown>;
|
|
63
|
+
exportRun: (computerId: string, runId: string) => Promise<Response>;
|
|
64
|
+
stopRun: (computerId: string, runId: string) => Promise<unknown>;
|
|
65
|
+
bash: (computerId: string, body: BashBody) => Promise<BashResult>;
|
|
66
|
+
listFiles: () => Promise<unknown>;
|
|
67
|
+
uploadFile: (filePath: string) => Promise<unknown>;
|
|
68
|
+
getFileDownloadUrl: (id: string) => Promise<{
|
|
69
|
+
url: string;
|
|
70
|
+
filename?: string;
|
|
71
|
+
}>;
|
|
72
|
+
downloadSignedUrl: (url: string, destPath: string) => Promise<void>;
|
|
73
|
+
listTemplates: ({ skip, limit }?: {
|
|
74
|
+
skip?: number;
|
|
75
|
+
limit?: number;
|
|
76
|
+
}) => Promise<unknown>;
|
|
77
|
+
listSkills: ({ skip, limit }?: {
|
|
78
|
+
skip?: number;
|
|
79
|
+
limit?: number;
|
|
80
|
+
}) => Promise<unknown>;
|
|
81
|
+
uninstallSkill: (computerId: string, skillId: string | number) => Promise<unknown>;
|
|
82
|
+
getAccount: () => Promise<unknown>;
|
|
83
|
+
installSkill: (computerId: string, skillId: number) => Promise<unknown>;
|
|
84
|
+
runComputerUse: (computerId: string, body: {
|
|
85
|
+
instruction: string;
|
|
86
|
+
model?: string;
|
|
87
|
+
max_steps?: number;
|
|
88
|
+
}, onChunk: (chunk: string) => void) => Promise<void>;
|
|
89
|
+
};
|
|
90
|
+
export type MagnetoClient = ReturnType<typeof createClient>;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// Thin fetch client for Magneto public `/api/v1` (#110 / #440).
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export class MagnetoApiError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
detail;
|
|
7
|
+
retryAfter;
|
|
8
|
+
requestId;
|
|
9
|
+
constructor(status, detail, retryAfter, requestId) {
|
|
10
|
+
super(detail);
|
|
11
|
+
this.name = 'MagnetoApiError';
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.detail = detail;
|
|
14
|
+
this.retryAfter = retryAfter;
|
|
15
|
+
this.requestId = requestId;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const RETRY_AFTER_DEFAULT_MS = 1000;
|
|
19
|
+
const RETRY_AFTER_CAP_MS = 60_000;
|
|
20
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
21
|
+
/**
|
|
22
|
+
* Parse `Retry-After` as delta-seconds or HTTP-date.
|
|
23
|
+
* Missing/invalid → 1000 ms. Cap 60 s. Past HTTP-date → 0.
|
|
24
|
+
*/
|
|
25
|
+
export function parseRetryAfter(raw, now = Date.now()) {
|
|
26
|
+
if (raw == null)
|
|
27
|
+
return RETRY_AFTER_DEFAULT_MS;
|
|
28
|
+
const trimmed = raw.trim();
|
|
29
|
+
if (!trimmed)
|
|
30
|
+
return RETRY_AFTER_DEFAULT_MS;
|
|
31
|
+
if (/^\d+$/.test(trimmed)) {
|
|
32
|
+
return Math.min(RETRY_AFTER_CAP_MS, Number(trimmed) * 1000);
|
|
33
|
+
}
|
|
34
|
+
const parsed = Date.parse(trimmed);
|
|
35
|
+
if (Number.isNaN(parsed))
|
|
36
|
+
return RETRY_AFTER_DEFAULT_MS;
|
|
37
|
+
return Math.min(RETRY_AFTER_CAP_MS, Math.max(0, parsed - now));
|
|
38
|
+
}
|
|
39
|
+
function defaultSleep(ms) {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
setTimeout(resolve, ms);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function defaultTimeoutMs() {
|
|
45
|
+
return Number(process.env.MAGNETO_TIMEOUT_MS) || DEFAULT_TIMEOUT_MS;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Normalize a user-supplied base into `…/api/v1` without doubling the prefix.
|
|
49
|
+
*/
|
|
50
|
+
export function normalizeApiBase(input) {
|
|
51
|
+
const fallback = 'https://magnetoapp.io/api/v1';
|
|
52
|
+
const raw = (input ?? process.env.MAGNETO_API_BASE ?? process.env.MAGNETO_BASE_URL ?? fallback)
|
|
53
|
+
.trim()
|
|
54
|
+
.replace(/\/+$/, '');
|
|
55
|
+
if (!raw)
|
|
56
|
+
return fallback;
|
|
57
|
+
if (raw.endsWith('/api/v1'))
|
|
58
|
+
return raw;
|
|
59
|
+
if (raw.endsWith('/api'))
|
|
60
|
+
return `${raw}/v1`;
|
|
61
|
+
return `${raw}/api/v1`;
|
|
62
|
+
}
|
|
63
|
+
export function createClient(opts) {
|
|
64
|
+
const base = normalizeApiBase(opts.baseUrl);
|
|
65
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
66
|
+
const timeoutDefault = opts.timeoutMs ?? defaultTimeoutMs();
|
|
67
|
+
const sleepImpl = opts.sleepImpl ?? defaultSleep;
|
|
68
|
+
function resolveUrl(p) {
|
|
69
|
+
if (p.startsWith('http://') || p.startsWith('https://'))
|
|
70
|
+
return p;
|
|
71
|
+
return `${base}${p.startsWith('/') ? p : `/${p}`}`;
|
|
72
|
+
}
|
|
73
|
+
async function toApiError(res) {
|
|
74
|
+
let detail = res.statusText || `HTTP ${res.status}`;
|
|
75
|
+
try {
|
|
76
|
+
const parsed = (await res.json());
|
|
77
|
+
if (typeof parsed.detail === 'string')
|
|
78
|
+
detail = parsed.detail;
|
|
79
|
+
else if (parsed.detail != null)
|
|
80
|
+
detail = JSON.stringify(parsed.detail);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// ignore non-JSON error bodies
|
|
84
|
+
}
|
|
85
|
+
return new MagnetoApiError(res.status, detail, res.headers.get('Retry-After') ?? undefined, res.headers.get('x-request-id') ?? undefined);
|
|
86
|
+
}
|
|
87
|
+
async function send(sendOpts) {
|
|
88
|
+
const url = resolveUrl(sendOpts.path);
|
|
89
|
+
let attempt = 0;
|
|
90
|
+
for (;;) {
|
|
91
|
+
const headers = {
|
|
92
|
+
Accept: 'application/json',
|
|
93
|
+
...(sendOpts.extraHeaders ?? {}),
|
|
94
|
+
};
|
|
95
|
+
if (sendOpts.auth !== false) {
|
|
96
|
+
headers.Authorization = `Bearer ${opts.apiKey}`;
|
|
97
|
+
}
|
|
98
|
+
let payload;
|
|
99
|
+
if (sendOpts.body instanceof FormData) {
|
|
100
|
+
payload = sendOpts.body;
|
|
101
|
+
}
|
|
102
|
+
else if (sendOpts.body !== undefined) {
|
|
103
|
+
headers['Content-Type'] = 'application/json';
|
|
104
|
+
payload = JSON.stringify(sendOpts.body);
|
|
105
|
+
}
|
|
106
|
+
const init = {
|
|
107
|
+
method: sendOpts.method,
|
|
108
|
+
headers,
|
|
109
|
+
body: payload,
|
|
110
|
+
};
|
|
111
|
+
const timeoutMs = sendOpts.timeoutMs ?? timeoutDefault;
|
|
112
|
+
if (timeoutMs > 0) {
|
|
113
|
+
init.signal = AbortSignal.timeout(timeoutMs);
|
|
114
|
+
}
|
|
115
|
+
const res = await fetchImpl(url, init);
|
|
116
|
+
if (res.status === 429 && attempt === 0) {
|
|
117
|
+
const retryAfter = res.headers.get('Retry-After');
|
|
118
|
+
await res.arrayBuffer().catch(() => undefined);
|
|
119
|
+
await sleepImpl(parseRetryAfter(retryAfter));
|
|
120
|
+
attempt += 1;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
return res;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function request(method, path, body, extra) {
|
|
127
|
+
const res = await send({
|
|
128
|
+
method,
|
|
129
|
+
path,
|
|
130
|
+
body,
|
|
131
|
+
extraHeaders: extra?.extraHeaders,
|
|
132
|
+
timeoutMs: extra?.timeoutMs,
|
|
133
|
+
});
|
|
134
|
+
if (!res.ok)
|
|
135
|
+
throw await toApiError(res);
|
|
136
|
+
if (res.status === 204)
|
|
137
|
+
return undefined;
|
|
138
|
+
const text = await res.text();
|
|
139
|
+
if (!text.trim())
|
|
140
|
+
return undefined;
|
|
141
|
+
try {
|
|
142
|
+
return JSON.parse(text);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return text;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function stream(path, body, onChunk) {
|
|
149
|
+
const res = await send({
|
|
150
|
+
method: 'POST',
|
|
151
|
+
path,
|
|
152
|
+
body,
|
|
153
|
+
timeoutMs: 0,
|
|
154
|
+
extraHeaders: {
|
|
155
|
+
Accept: 'text/plain, application/json, */*',
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
if (!res.ok)
|
|
159
|
+
throw await toApiError(res);
|
|
160
|
+
if (!res.body) {
|
|
161
|
+
const text = await res.text();
|
|
162
|
+
if (text)
|
|
163
|
+
onChunk(text);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const reader = res.body.getReader();
|
|
167
|
+
const decoder = new TextDecoder();
|
|
168
|
+
for (;;) {
|
|
169
|
+
const { done, value } = await reader.read();
|
|
170
|
+
if (done)
|
|
171
|
+
break;
|
|
172
|
+
onChunk(decoder.decode(value, { stream: true }));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
base,
|
|
177
|
+
listComputers: () => request('GET', '/computers'),
|
|
178
|
+
getComputer: (id) => request('GET', `/computers/${encodeURIComponent(id)}`),
|
|
179
|
+
createComputer: (body) => request('POST', '/computers', body),
|
|
180
|
+
stopComputer: (id) => request('POST', `/computers/${encodeURIComponent(id)}/stop`),
|
|
181
|
+
startComputer: (id) => request('POST', `/computers/${encodeURIComponent(id)}/start`),
|
|
182
|
+
restartComputer: (id) => request('POST', `/computers/${encodeURIComponent(id)}/restart`),
|
|
183
|
+
resizeComputer: (id, body) => request('POST', `/computers/${encodeURIComponent(id)}/resize`, body),
|
|
184
|
+
deleteComputer: (id, confirm) => request('DELETE', `/computers/${encodeURIComponent(id)}?confirm=${encodeURIComponent(confirm)}`),
|
|
185
|
+
getComputerUptime: (id, windowSeconds) => {
|
|
186
|
+
const q = windowSeconds != null ? `?window_seconds=${encodeURIComponent(String(windowSeconds))}` : '';
|
|
187
|
+
return request('GET', `/computers/${encodeURIComponent(id)}/uptime${q}`);
|
|
188
|
+
},
|
|
189
|
+
mintGatewayUrl: (id, target) => request('POST', `/computers/${encodeURIComponent(id)}/gateway?target=${encodeURIComponent(target)}`),
|
|
190
|
+
listRuns: (computerId, page) => {
|
|
191
|
+
const parts = [];
|
|
192
|
+
if (page?.limit != null)
|
|
193
|
+
parts.push(`limit=${encodeURIComponent(String(page.limit))}`);
|
|
194
|
+
if (page?.offset != null)
|
|
195
|
+
parts.push(`offset=${encodeURIComponent(String(page.offset))}`);
|
|
196
|
+
const q = parts.length ? `?${parts.join('&')}` : '';
|
|
197
|
+
return request('GET', `/computers/${encodeURIComponent(computerId)}/runs${q}`);
|
|
198
|
+
},
|
|
199
|
+
getRun: (computerId, runId) => request('GET', `/computers/${encodeURIComponent(computerId)}/runs/${encodeURIComponent(runId)}`),
|
|
200
|
+
exportRun: async (computerId, runId) => {
|
|
201
|
+
const res = await send({
|
|
202
|
+
method: 'GET',
|
|
203
|
+
path: `/computers/${encodeURIComponent(computerId)}/runs/${encodeURIComponent(runId)}/export`,
|
|
204
|
+
timeoutMs: 0,
|
|
205
|
+
});
|
|
206
|
+
if (!res.ok)
|
|
207
|
+
throw await toApiError(res);
|
|
208
|
+
return res;
|
|
209
|
+
},
|
|
210
|
+
stopRun: (computerId, runId) => request('POST', `/computers/${encodeURIComponent(computerId)}/computer-use/stop`, {
|
|
211
|
+
run_id: runId,
|
|
212
|
+
}),
|
|
213
|
+
bash: (computerId, body) => request('POST', `/computers/${encodeURIComponent(computerId)}/bash`, body),
|
|
214
|
+
listFiles: () => request('GET', '/files'),
|
|
215
|
+
uploadFile: async (filePath) => {
|
|
216
|
+
const buf = fs.readFileSync(filePath);
|
|
217
|
+
const form = new FormData();
|
|
218
|
+
form.append('file', new Blob([buf]), path.basename(filePath));
|
|
219
|
+
return request('POST', '/files', form);
|
|
220
|
+
},
|
|
221
|
+
getFileDownloadUrl: (id) => request('GET', `/files/${encodeURIComponent(id)}/download`),
|
|
222
|
+
downloadSignedUrl: async (url, destPath) => {
|
|
223
|
+
const res = await send({
|
|
224
|
+
method: 'GET',
|
|
225
|
+
path: url,
|
|
226
|
+
timeoutMs: 0,
|
|
227
|
+
auth: false,
|
|
228
|
+
extraHeaders: { Accept: '*/*' },
|
|
229
|
+
});
|
|
230
|
+
if (!res.ok)
|
|
231
|
+
throw await toApiError(res);
|
|
232
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
233
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
234
|
+
fs.writeFileSync(destPath, buf);
|
|
235
|
+
},
|
|
236
|
+
listTemplates: ({ skip = 0, limit = 100 } = {}) => request('GET', `/templates?skip=${encodeURIComponent(String(skip))}&limit=${encodeURIComponent(String(limit))}`),
|
|
237
|
+
listSkills: ({ skip = 0, limit = 100 } = {}) => request('GET', `/skills?skip=${encodeURIComponent(String(skip))}&limit=${encodeURIComponent(String(limit))}`),
|
|
238
|
+
uninstallSkill: (computerId, skillId) => request('DELETE', `/computers/${encodeURIComponent(computerId)}/skills/${encodeURIComponent(String(skillId))}`),
|
|
239
|
+
getAccount: () => request('GET', '/account'),
|
|
240
|
+
installSkill: (computerId, skillId) => request('POST', `/computers/${encodeURIComponent(computerId)}/skills`, {
|
|
241
|
+
skill_id: skillId,
|
|
242
|
+
}),
|
|
243
|
+
runComputerUse: (computerId, body, onChunk) => stream(`/computers/${encodeURIComponent(computerId)}/computer-use/run`, body, onChunk),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type CredentialsFile = {
|
|
2
|
+
api_key: string;
|
|
3
|
+
};
|
|
4
|
+
export declare function magnetoHomeDir(home?: string): string;
|
|
5
|
+
export declare function credentialsPath(home?: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Write credentials with directory mode 0700 and file mode 0600.
|
|
8
|
+
*/
|
|
9
|
+
export declare function writeCredentials(apiKey: string, home?: string): string;
|
|
10
|
+
export declare function readCredentialsFile(home?: string): string | null;
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the API key: env MAGNETO_API_KEY wins over credentials file.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveApiKey(env?: NodeJS.ProcessEnv, home?: string): string | null;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Resolve and persist Magneto API credentials.
|
|
2
|
+
// Order: MAGNETO_API_KEY env → ~/.magneto/credentials.json
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
export function magnetoHomeDir(home = os.homedir()) {
|
|
7
|
+
return path.join(home, '.magneto');
|
|
8
|
+
}
|
|
9
|
+
export function credentialsPath(home = os.homedir()) {
|
|
10
|
+
return path.join(magnetoHomeDir(home), 'credentials.json');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Write credentials with directory mode 0700 and file mode 0600.
|
|
14
|
+
*/
|
|
15
|
+
export function writeCredentials(apiKey, home = os.homedir()) {
|
|
16
|
+
const dir = magnetoHomeDir(home);
|
|
17
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
18
|
+
// Ensure dir mode even if it already existed.
|
|
19
|
+
try {
|
|
20
|
+
fs.chmodSync(dir, 0o700);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// best-effort on platforms that ignore mode
|
|
24
|
+
}
|
|
25
|
+
const filePath = credentialsPath(home);
|
|
26
|
+
const payload = { api_key: apiKey.trim() };
|
|
27
|
+
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, {
|
|
28
|
+
encoding: 'utf8',
|
|
29
|
+
mode: 0o600,
|
|
30
|
+
});
|
|
31
|
+
try {
|
|
32
|
+
fs.chmodSync(filePath, 0o600);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// best-effort
|
|
36
|
+
}
|
|
37
|
+
return filePath;
|
|
38
|
+
}
|
|
39
|
+
export function readCredentialsFile(home = os.homedir()) {
|
|
40
|
+
const filePath = credentialsPath(home);
|
|
41
|
+
if (!fs.existsSync(filePath))
|
|
42
|
+
return null;
|
|
43
|
+
try {
|
|
44
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
45
|
+
const parsed = JSON.parse(raw);
|
|
46
|
+
if (typeof parsed.api_key === 'string' && parsed.api_key.trim()) {
|
|
47
|
+
return parsed.api_key.trim();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the API key: env MAGNETO_API_KEY wins over credentials file.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveApiKey(env = process.env, home = os.homedir()) {
|
|
59
|
+
const fromEnv = env.MAGNETO_API_KEY?.trim();
|
|
60
|
+
if (fromEnv)
|
|
61
|
+
return fromEnv;
|
|
62
|
+
return readCredentialsFile(home);
|
|
63
|
+
}
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type OutputFormat = 'table' | 'json' | 'quiet';
|
|
2
|
+
export type ColumnSpec = {
|
|
3
|
+
key: string;
|
|
4
|
+
header: string;
|
|
5
|
+
width?: number;
|
|
6
|
+
};
|
|
7
|
+
export type FormatSpec = {
|
|
8
|
+
columns: ColumnSpec[];
|
|
9
|
+
/** Dotted path to the id used by `--output quiet`. */
|
|
10
|
+
idPath: string | string[];
|
|
11
|
+
/** Optional dotted path to the row object (e.g. `run` for run get). */
|
|
12
|
+
rowPath?: string;
|
|
13
|
+
};
|
|
14
|
+
export declare class InvalidOutputFormatError extends Error {
|
|
15
|
+
constructor(raw: string);
|
|
16
|
+
}
|
|
17
|
+
export declare function parseOutputFlag(raw: string): OutputFormat;
|
|
18
|
+
export declare function resolveOutputFormat(opts: {
|
|
19
|
+
explicit?: string | null;
|
|
20
|
+
isTty: boolean;
|
|
21
|
+
}): OutputFormat;
|
|
22
|
+
export declare function getPath(obj: unknown, path: string): unknown;
|
|
23
|
+
export declare function formatOutput(data: unknown, format: OutputFormat, spec: FormatSpec): string;
|
|
24
|
+
export declare const COMPUTERS_SPEC: FormatSpec;
|
|
25
|
+
export declare const UPTIME_SPEC: FormatSpec;
|
|
26
|
+
export declare const RUNS_SPEC: FormatSpec;
|
|
27
|
+
export declare const RUN_GET_SPEC: FormatSpec;
|
|
28
|
+
export declare const FILES_SPEC: FormatSpec;
|
|
29
|
+
export declare const TEMPLATES_SPEC: FormatSpec;
|
|
30
|
+
export declare const SKILLS_SPEC: FormatSpec;
|
|
31
|
+
export declare const ACCOUNT_SPEC: FormatSpec;
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Hand-rolled table | json | quiet formatter. No TUI deps.
|
|
2
|
+
export class InvalidOutputFormatError extends Error {
|
|
3
|
+
constructor(raw) {
|
|
4
|
+
super(`invalid --output '${raw}' (expected table|json|quiet)`);
|
|
5
|
+
this.name = 'InvalidOutputFormatError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function parseOutputFlag(raw) {
|
|
9
|
+
const v = raw.trim().toLowerCase();
|
|
10
|
+
if (v === 'table' || v === 'json' || v === 'quiet')
|
|
11
|
+
return v;
|
|
12
|
+
throw new InvalidOutputFormatError(raw);
|
|
13
|
+
}
|
|
14
|
+
export function resolveOutputFormat(opts) {
|
|
15
|
+
if (opts.explicit != null && opts.explicit !== '') {
|
|
16
|
+
return parseOutputFlag(opts.explicit);
|
|
17
|
+
}
|
|
18
|
+
return opts.isTty ? 'table' : 'json';
|
|
19
|
+
}
|
|
20
|
+
export function getPath(obj, path) {
|
|
21
|
+
if (obj == null)
|
|
22
|
+
return undefined;
|
|
23
|
+
let cur = obj;
|
|
24
|
+
for (const part of path.split('.')) {
|
|
25
|
+
if (cur == null || typeof cur !== 'object')
|
|
26
|
+
return undefined;
|
|
27
|
+
cur = cur[part];
|
|
28
|
+
}
|
|
29
|
+
return cur;
|
|
30
|
+
}
|
|
31
|
+
function asRows(data, spec) {
|
|
32
|
+
if (spec.rowPath) {
|
|
33
|
+
const inner = getPath(data, spec.rowPath);
|
|
34
|
+
if (inner == null)
|
|
35
|
+
return [];
|
|
36
|
+
return [inner];
|
|
37
|
+
}
|
|
38
|
+
if (Array.isArray(data))
|
|
39
|
+
return data;
|
|
40
|
+
if (data && typeof data === 'object' && Array.isArray(data.items)) {
|
|
41
|
+
return data.items;
|
|
42
|
+
}
|
|
43
|
+
if (data && typeof data === 'object')
|
|
44
|
+
return [data];
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
function cellText(value) {
|
|
48
|
+
if (value == null)
|
|
49
|
+
return '';
|
|
50
|
+
return String(value);
|
|
51
|
+
}
|
|
52
|
+
function padTrunc(text, width) {
|
|
53
|
+
const clipped = text.length > width ? text.slice(0, width) : text;
|
|
54
|
+
return clipped.padEnd(width);
|
|
55
|
+
}
|
|
56
|
+
function formatTable(rows, spec) {
|
|
57
|
+
if (rows.length === 0)
|
|
58
|
+
return '(none)\n';
|
|
59
|
+
const widths = spec.columns.map((col) => {
|
|
60
|
+
const headerLen = col.header.length;
|
|
61
|
+
const maxCell = rows.reduce((m, row) => {
|
|
62
|
+
const n = cellText(getPath(row, col.key)).length;
|
|
63
|
+
return n > m ? n : m;
|
|
64
|
+
}, 0);
|
|
65
|
+
const natural = Math.max(headerLen, maxCell);
|
|
66
|
+
return col.width ?? natural;
|
|
67
|
+
});
|
|
68
|
+
const header = spec.columns
|
|
69
|
+
.map((col, i) => padTrunc(col.header, widths[i]))
|
|
70
|
+
.join(' ');
|
|
71
|
+
const lines = rows.map((row) => spec.columns.map((col, i) => padTrunc(cellText(getPath(row, col.key)), widths[i])).join(' '));
|
|
72
|
+
return `${[header, ...lines].join('\n')}\n`;
|
|
73
|
+
}
|
|
74
|
+
function formatQuiet(data, rows, spec) {
|
|
75
|
+
const paths = Array.isArray(spec.idPath) ? spec.idPath : [spec.idPath];
|
|
76
|
+
const ids = [];
|
|
77
|
+
const sources = rows.length > 0 ? rows : data != null ? [data] : [];
|
|
78
|
+
for (const row of sources) {
|
|
79
|
+
let id;
|
|
80
|
+
for (const p of paths) {
|
|
81
|
+
id = getPath(row, p);
|
|
82
|
+
if (id != null && String(id) !== '')
|
|
83
|
+
break;
|
|
84
|
+
// Also try the path against the original envelope (e.g. run.id).
|
|
85
|
+
id = getPath(data, p);
|
|
86
|
+
if (id != null && String(id) !== '')
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
if (id == null || String(id) === '')
|
|
90
|
+
continue;
|
|
91
|
+
ids.push(String(id));
|
|
92
|
+
}
|
|
93
|
+
return ids.length ? `${ids.join('\n')}\n` : '';
|
|
94
|
+
}
|
|
95
|
+
export function formatOutput(data, format, spec) {
|
|
96
|
+
if (format === 'json') {
|
|
97
|
+
return `${JSON.stringify(data, null, 2)}\n`;
|
|
98
|
+
}
|
|
99
|
+
const rows = asRows(data, spec);
|
|
100
|
+
if (format === 'quiet')
|
|
101
|
+
return formatQuiet(data, rows, spec);
|
|
102
|
+
return formatTable(rows, spec);
|
|
103
|
+
}
|
|
104
|
+
export const COMPUTERS_SPEC = {
|
|
105
|
+
columns: [
|
|
106
|
+
{ key: 'id', header: 'id' },
|
|
107
|
+
{ key: 'name', header: 'name' },
|
|
108
|
+
{ key: 'kind', header: 'kind' },
|
|
109
|
+
{ key: 'state', header: 'state' },
|
|
110
|
+
],
|
|
111
|
+
idPath: 'id',
|
|
112
|
+
};
|
|
113
|
+
export const UPTIME_SPEC = {
|
|
114
|
+
columns: [
|
|
115
|
+
{ key: 'computer_id', header: 'computer_id' },
|
|
116
|
+
{ key: 'window_seconds', header: 'window_seconds' },
|
|
117
|
+
{ key: 'uptime_ratio', header: 'uptime_ratio' },
|
|
118
|
+
{ key: 'sample_count', header: 'sample_count' },
|
|
119
|
+
],
|
|
120
|
+
idPath: 'computer_id',
|
|
121
|
+
};
|
|
122
|
+
export const RUNS_SPEC = {
|
|
123
|
+
columns: [
|
|
124
|
+
{ key: 'id', header: 'id' },
|
|
125
|
+
{ key: 'status', header: 'status' },
|
|
126
|
+
{ key: 'model', header: 'model' },
|
|
127
|
+
{ key: 'started_at', header: 'started_at' },
|
|
128
|
+
],
|
|
129
|
+
idPath: 'id',
|
|
130
|
+
};
|
|
131
|
+
export const RUN_GET_SPEC = {
|
|
132
|
+
columns: [
|
|
133
|
+
{ key: 'id', header: 'id' },
|
|
134
|
+
{ key: 'status', header: 'status' },
|
|
135
|
+
{ key: 'model', header: 'model' },
|
|
136
|
+
{ key: 'steps', header: 'steps' },
|
|
137
|
+
],
|
|
138
|
+
idPath: 'run.id',
|
|
139
|
+
rowPath: 'run',
|
|
140
|
+
};
|
|
141
|
+
export const FILES_SPEC = {
|
|
142
|
+
columns: [
|
|
143
|
+
{ key: 'id', header: 'id' },
|
|
144
|
+
{ key: 'filename', header: 'filename' },
|
|
145
|
+
{ key: 'size_bytes', header: 'size_bytes' },
|
|
146
|
+
{ key: 'content_type', header: 'content_type' },
|
|
147
|
+
{ key: 'created_at', header: 'created_at' },
|
|
148
|
+
],
|
|
149
|
+
idPath: 'id',
|
|
150
|
+
};
|
|
151
|
+
export const TEMPLATES_SPEC = {
|
|
152
|
+
columns: [
|
|
153
|
+
{ key: 'id', header: 'id' },
|
|
154
|
+
{ key: 'name', header: 'name' },
|
|
155
|
+
{ key: 'category', header: 'category' },
|
|
156
|
+
],
|
|
157
|
+
idPath: 'id',
|
|
158
|
+
};
|
|
159
|
+
export const SKILLS_SPEC = {
|
|
160
|
+
columns: [
|
|
161
|
+
{ key: 'id', header: 'id' },
|
|
162
|
+
{ key: 'name', header: 'name' },
|
|
163
|
+
{ key: 'price', header: 'price' },
|
|
164
|
+
{ key: 'category', header: 'category' },
|
|
165
|
+
],
|
|
166
|
+
idPath: 'id',
|
|
167
|
+
};
|
|
168
|
+
export const ACCOUNT_SPEC = {
|
|
169
|
+
columns: [
|
|
170
|
+
{ key: 'id', header: 'id' },
|
|
171
|
+
{ key: 'name', header: 'name' },
|
|
172
|
+
],
|
|
173
|
+
idPath: 'workspace.id',
|
|
174
|
+
rowPath: 'workspace',
|
|
175
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function openUrl(url: string): Promise<void>;
|
package/dist/open-url.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Launch the platform default browser. No extra deps; never log the URL.
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
export function openUrl(url) {
|
|
4
|
+
const platform = process.platform;
|
|
5
|
+
if (platform === 'darwin') {
|
|
6
|
+
spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();
|
|
7
|
+
}
|
|
8
|
+
else if (platform === 'win32') {
|
|
9
|
+
spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' }).unref();
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
|
|
13
|
+
}
|
|
14
|
+
return Promise.resolve();
|
|
15
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
/** Read this package's version from package.json (works from src/, dist/, and an installed tarball). */
|
|
3
|
+
export function readPackageVersion() {
|
|
4
|
+
const raw = fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8');
|
|
5
|
+
const parsed = JSON.parse(raw);
|
|
6
|
+
if (typeof parsed.version !== 'string' || parsed.version.length === 0) {
|
|
7
|
+
throw new Error('package.json is missing a version');
|
|
8
|
+
}
|
|
9
|
+
return parsed.version;
|
|
10
|
+
}
|