@openma/cli 0.1.0-beta.0 → 0.1.0-beta.2
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/index.js +270 -11
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,16 +3,76 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { createServer } from "node:http";
|
|
5
5
|
import { execSync } from "node:child_process";
|
|
6
|
+
import { homedir, hostname } from "node:os";
|
|
7
|
+
import { join, dirname } from "node:path";
|
|
8
|
+
import { mkdirSync, readFileSync, writeFileSync, unlinkSync, existsSync, chmodSync } from "node:fs";
|
|
9
|
+
import { randomBytes } from "node:crypto";
|
|
10
|
+
function credentialsPath() {
|
|
11
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
12
|
+
const base = xdg && xdg.length > 0 ? xdg : join(homedir(), ".config");
|
|
13
|
+
return join(base, "oma", "credentials.json");
|
|
14
|
+
}
|
|
15
|
+
function readCredentials() {
|
|
16
|
+
const path = credentialsPath();
|
|
17
|
+
try {
|
|
18
|
+
if (!existsSync(path)) return null;
|
|
19
|
+
const raw = readFileSync(path, "utf8");
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function writeCredentials(creds) {
|
|
26
|
+
const path = credentialsPath();
|
|
27
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
28
|
+
writeFileSync(path, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
29
|
+
try {
|
|
30
|
+
chmodSync(path, 384);
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function clearCredentials() {
|
|
35
|
+
const path = credentialsPath();
|
|
36
|
+
if (!existsSync(path)) return false;
|
|
37
|
+
unlinkSync(path);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
6
40
|
function loadConfig() {
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
41
|
+
const envBase = process.env.OMA_BASE_URL;
|
|
42
|
+
const envKey = process.env.OMA_API_KEY;
|
|
43
|
+
const stored = readCredentials();
|
|
44
|
+
if (envKey) {
|
|
45
|
+
return {
|
|
46
|
+
baseUrl: envBase || stored?.base_url || "https://openma.dev",
|
|
47
|
+
apiKey: envKey,
|
|
48
|
+
json: false,
|
|
49
|
+
source: "env"
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (stored) {
|
|
53
|
+
return {
|
|
54
|
+
baseUrl: envBase || stored.base_url,
|
|
55
|
+
apiKey: stored.token,
|
|
56
|
+
json: false,
|
|
57
|
+
source: "stored"
|
|
58
|
+
};
|
|
14
59
|
}
|
|
15
|
-
|
|
60
|
+
console.error("Error: not authenticated.");
|
|
61
|
+
console.error(" Run: oma auth login");
|
|
62
|
+
console.error(" Or: export OMA_API_KEY=<your-key> (mint at /api-keys page)");
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
function loadConfigOptional() {
|
|
66
|
+
const envBase = process.env.OMA_BASE_URL;
|
|
67
|
+
const envKey = process.env.OMA_API_KEY;
|
|
68
|
+
const stored = readCredentials();
|
|
69
|
+
if (envKey) {
|
|
70
|
+
return { baseUrl: envBase || stored?.base_url || "https://openma.dev", apiKey: envKey, json: false, source: "env" };
|
|
71
|
+
}
|
|
72
|
+
if (stored) {
|
|
73
|
+
return { baseUrl: envBase || stored.base_url, apiKey: stored.token, json: false, source: "stored" };
|
|
74
|
+
}
|
|
75
|
+
return { baseUrl: envBase || "https://openma.dev", apiKey: "", json: false, source: "missing" };
|
|
16
76
|
}
|
|
17
77
|
async function apiFetch(config, path, init) {
|
|
18
78
|
const url = `${config.baseUrl}${path}`;
|
|
@@ -62,7 +122,201 @@ function isPubliclyReachable(url) {
|
|
|
62
122
|
return true;
|
|
63
123
|
}
|
|
64
124
|
}
|
|
125
|
+
async function authLogin(baseUrl, requestedTenant) {
|
|
126
|
+
const state = randomBytes(16).toString("hex");
|
|
127
|
+
const port = 19500 + Math.floor(Math.random() * 500);
|
|
128
|
+
const callback = `http://127.0.0.1:${port}/callback`;
|
|
129
|
+
const params = new URLSearchParams({
|
|
130
|
+
callback,
|
|
131
|
+
state,
|
|
132
|
+
hostname: hostname()
|
|
133
|
+
});
|
|
134
|
+
if (requestedTenant) params.set("tenant", requestedTenant);
|
|
135
|
+
const loginUrl = `${baseUrl}/cli/login?${params.toString()}`;
|
|
136
|
+
const result = await new Promise((resolve, reject) => {
|
|
137
|
+
const timeout = setTimeout(() => {
|
|
138
|
+
server.close();
|
|
139
|
+
reject(new Error("Timed out after 5 minutes waiting for browser approval."));
|
|
140
|
+
}, 5 * 6e4);
|
|
141
|
+
const server = createServer((req, res) => {
|
|
142
|
+
try {
|
|
143
|
+
const u = new URL(req.url || "/", `http://127.0.0.1:${port}`);
|
|
144
|
+
if (u.pathname !== "/callback") {
|
|
145
|
+
res.writeHead(404).end();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const got = u.searchParams;
|
|
149
|
+
if (got.get("error")) {
|
|
150
|
+
const err = String(got.get("error"));
|
|
151
|
+
res.writeHead(400, { "Content-Type": "text/html" }).end(approvalPage("Cancelled", `Login was cancelled: ${err}. You can close this tab.`));
|
|
152
|
+
clearTimeout(timeout);
|
|
153
|
+
server.close();
|
|
154
|
+
reject(new Error(`Login cancelled: ${err}`));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (got.get("state") !== state) {
|
|
158
|
+
res.writeHead(400, { "Content-Type": "text/html" }).end(approvalPage("State mismatch", "The login response didn't match what this CLI session expected. Please try again."));
|
|
159
|
+
clearTimeout(timeout);
|
|
160
|
+
server.close();
|
|
161
|
+
reject(new Error("State mismatch \u2014 refusing the callback"));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const token = got.get("token");
|
|
165
|
+
const tenant = got.get("tenant");
|
|
166
|
+
const user = got.get("user");
|
|
167
|
+
const key_id = got.get("key_id");
|
|
168
|
+
if (!token || !tenant || !user || !key_id) {
|
|
169
|
+
res.writeHead(400, { "Content-Type": "text/html" }).end(approvalPage("Incomplete callback", "The browser handoff is missing required fields."));
|
|
170
|
+
clearTimeout(timeout);
|
|
171
|
+
server.close();
|
|
172
|
+
reject(new Error("Callback missing required fields"));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
res.writeHead(200, { "Content-Type": "text/html" }).end(approvalPage("Signed in", "You can close this tab and return to your terminal."));
|
|
176
|
+
clearTimeout(timeout);
|
|
177
|
+
server.close();
|
|
178
|
+
resolve({ token, tenant, user, key_id });
|
|
179
|
+
} catch (err) {
|
|
180
|
+
res.writeHead(500).end();
|
|
181
|
+
clearTimeout(timeout);
|
|
182
|
+
server.close();
|
|
183
|
+
reject(err);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
server.listen(port, "127.0.0.1");
|
|
187
|
+
console.log(`Opening browser to authorize CLI\u2026`);
|
|
188
|
+
console.log(` ${loginUrl}
|
|
189
|
+
`);
|
|
190
|
+
console.log(`(If the browser doesn't open, copy the URL above into one manually.)`);
|
|
191
|
+
openBrowser(loginUrl);
|
|
192
|
+
});
|
|
193
|
+
const tempConfig = { baseUrl, apiKey: result.token, json: false, source: "stored" };
|
|
194
|
+
const me = await apiFetch(tempConfig, "/v1/me");
|
|
195
|
+
writeCredentials({
|
|
196
|
+
version: 1,
|
|
197
|
+
base_url: baseUrl,
|
|
198
|
+
user: me.user ?? { id: result.user, email: "", name: null },
|
|
199
|
+
tenant: me.tenant,
|
|
200
|
+
tenants: me.tenants,
|
|
201
|
+
token: result.token,
|
|
202
|
+
key_id: result.key_id,
|
|
203
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
204
|
+
});
|
|
205
|
+
console.log(`\u2713 Signed in as ${me.user?.email ?? me.user?.id}`);
|
|
206
|
+
console.log(` Tenant : ${me.tenant.name || me.tenant.id} (${me.tenant.id})`);
|
|
207
|
+
console.log(` Stored : ${credentialsPath()}`);
|
|
208
|
+
if (me.tenants.length > 1) {
|
|
209
|
+
console.log(` ${me.tenants.length} tenants available \u2014 switch with: oma auth tenant use <id>`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function openBrowser(url) {
|
|
213
|
+
try {
|
|
214
|
+
const p = process.platform;
|
|
215
|
+
if (p === "darwin") execSync(`open "${url}"`);
|
|
216
|
+
else if (p === "linux") execSync(`xdg-open "${url}"`);
|
|
217
|
+
else if (p === "win32") execSync(`start "" "${url}"`);
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function approvalPage(title, body) {
|
|
222
|
+
return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>oma \u2014 ${title}</title><style>
|
|
223
|
+
body{font-family:-apple-system,system-ui,sans-serif;max-width:480px;margin:80px auto;padding:0 24px;color:#222}
|
|
224
|
+
h1{font-size:20px;margin-bottom:12px} p{color:#555;line-height:1.5}
|
|
225
|
+
.card{border:1px solid #e5e5e5;border-radius:12px;padding:32px;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,0.04)}
|
|
226
|
+
</style></head><body><div class="card"><h1>${title}</h1><p>${body}</p></div></body></html>`;
|
|
227
|
+
}
|
|
65
228
|
var commands = [
|
|
229
|
+
// Auth
|
|
230
|
+
{
|
|
231
|
+
group: "Auth",
|
|
232
|
+
match: ["auth", "login"],
|
|
233
|
+
usage: "oma auth login [--base-url <url>]",
|
|
234
|
+
desc: "Open browser to authenticate; stores ~/.config/oma/credentials.json",
|
|
235
|
+
http: "POST /v1/me/cli-tokens (browser handoff via /cli/login)",
|
|
236
|
+
async run(_config, args) {
|
|
237
|
+
const baseUrl = (flag(args, "--base-url") ?? process.env.OMA_BASE_URL ?? "https://openma.dev").replace(/\/+$/, "");
|
|
238
|
+
await authLogin(baseUrl);
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
group: "Auth",
|
|
243
|
+
match: ["auth", "logout"],
|
|
244
|
+
usage: "oma auth logout",
|
|
245
|
+
desc: "Delete stored credentials (does not revoke the token; use `oma keys revoke <id>` for that)",
|
|
246
|
+
http: "(local file delete)",
|
|
247
|
+
async run() {
|
|
248
|
+
const removed = clearCredentials();
|
|
249
|
+
console.log(removed ? `Cleared ${credentialsPath()}` : "No stored credentials.");
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
group: "Auth",
|
|
254
|
+
match: ["whoami"],
|
|
255
|
+
usage: "oma whoami",
|
|
256
|
+
desc: "Show current user, tenant, and base URL",
|
|
257
|
+
http: "GET /v1/me",
|
|
258
|
+
async run(config) {
|
|
259
|
+
try {
|
|
260
|
+
const me = await apiFetch(config, "/v1/me");
|
|
261
|
+
if (config.json) {
|
|
262
|
+
console.log(JSON.stringify({ ...me, base_url: config.baseUrl, source: config.source }, null, 2));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
console.log(`Base URL : ${config.baseUrl}`);
|
|
266
|
+
console.log(`Source : ${config.source === "env" ? "OMA_API_KEY env var" : "stored credentials"}`);
|
|
267
|
+
console.log(`User : ${me.user?.email ?? me.user?.id ?? "(unknown \u2014 legacy key without user_id)"}`);
|
|
268
|
+
console.log(`Tenant : ${me.tenant.name || me.tenant.id} (${me.tenant.id})`);
|
|
269
|
+
if (me.tenants.length > 1) {
|
|
270
|
+
console.log(`Available: ${me.tenants.map((t) => t.id).join(", ")}`);
|
|
271
|
+
}
|
|
272
|
+
} catch (err) {
|
|
273
|
+
console.error(`whoami failed: ${err.message}`);
|
|
274
|
+
if (config.source === "stored") {
|
|
275
|
+
console.error("Stored token may have been revoked. Try: oma auth login");
|
|
276
|
+
}
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
group: "Auth",
|
|
283
|
+
match: ["auth", "tenant", "ls"],
|
|
284
|
+
usage: "oma auth tenant ls",
|
|
285
|
+
desc: "List tenants the current user belongs to",
|
|
286
|
+
http: "GET /v1/me/tenants",
|
|
287
|
+
async run(config) {
|
|
288
|
+
const { data } = await apiFetch(config, "/v1/me/tenants");
|
|
289
|
+
if (!data.length) {
|
|
290
|
+
console.log("No tenants on this account.");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const stored = readCredentials();
|
|
294
|
+
const current = stored?.tenant.id;
|
|
295
|
+
table([["", "ID", "NAME", "ROLE"], ...data.map((t) => [t.id === current ? "*" : " ", t.id, t.name || "\u2014", t.role])]);
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
group: "Auth",
|
|
300
|
+
match: ["auth", "tenant", "use"],
|
|
301
|
+
needsArg: true,
|
|
302
|
+
usage: "oma auth tenant use <tenant-id>",
|
|
303
|
+
desc: "Switch active tenant (mints a new CLI token for that tenant)",
|
|
304
|
+
http: "POST /v1/me/cli-tokens {tenant_id}",
|
|
305
|
+
async run(config, args) {
|
|
306
|
+
const tenantId = args[0];
|
|
307
|
+
if (!tenantId) {
|
|
308
|
+
console.error("Usage: oma auth tenant use <tenant-id>");
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
const stored = readCredentials();
|
|
312
|
+
if (!stored) {
|
|
313
|
+
console.error("No stored credentials. Run: oma auth login");
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
console.log(`Switching to tenant ${tenantId} \u2014 opening browser to confirm\u2026`);
|
|
317
|
+
await authLogin(stored.base_url, tenantId);
|
|
318
|
+
}
|
|
319
|
+
},
|
|
66
320
|
// Agents
|
|
67
321
|
{
|
|
68
322
|
group: "Agents",
|
|
@@ -1413,8 +1667,12 @@ Usage:`);
|
|
|
1413
1667
|
oma api <resource> Show endpoints for a resource
|
|
1414
1668
|
|
|
1415
1669
|
Environment:
|
|
1416
|
-
OMA_BASE_URL API base (default:
|
|
1417
|
-
OMA_API_KEY API key
|
|
1670
|
+
OMA_BASE_URL API base (default: https://openma.dev)
|
|
1671
|
+
OMA_API_KEY API key \u2014 overrides stored credentials when set
|
|
1672
|
+
XDG_CONFIG_HOME Base dir for credentials (default: ~/.config)
|
|
1673
|
+
|
|
1674
|
+
Stored credentials live at ~/.config/oma/credentials.json (created by
|
|
1675
|
+
'oma auth login', mode 0600). Delete with 'oma auth logout'.
|
|
1418
1676
|
`);
|
|
1419
1677
|
}
|
|
1420
1678
|
async function main() {
|
|
@@ -1429,7 +1687,8 @@ async function main() {
|
|
|
1429
1687
|
}
|
|
1430
1688
|
const wantJson = args.includes("--json");
|
|
1431
1689
|
args = args.filter((a) => a !== "--json");
|
|
1432
|
-
const
|
|
1690
|
+
const isPreAuth = args[0] === "auth" && (args[1] === "login" || args[1] === "logout");
|
|
1691
|
+
const config = isPreAuth ? loadConfigOptional() : loadConfig();
|
|
1433
1692
|
config.json = wantJson;
|
|
1434
1693
|
let needsArgMatch = null;
|
|
1435
1694
|
for (const c of commands) {
|