@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.
Files changed (2) hide show
  1. package/dist/index.js +270 -11
  2. 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 baseUrl = process.env.OMA_BASE_URL || "http://localhost:8787";
8
- const apiKey = process.env.OMA_API_KEY || "";
9
- if (!apiKey) {
10
- console.error("Error: OMA_API_KEY environment variable is required");
11
- console.error(" export OMA_API_KEY=your-api-key");
12
- console.error("\n Generate one at: " + baseUrl + " \u2192 API Keys");
13
- process.exit(1);
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
- return { baseUrl, apiKey, json: false };
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: http://localhost:8787)
1417
- OMA_API_KEY API key (required)
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 config = loadConfig();
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openma/cli",
3
- "version": "0.1.0-beta.0",
3
+ "version": "0.1.0-beta.2",
4
4
  "description": "CLI for the openma managed agents platform",
5
5
  "type": "module",
6
6
  "bin": {