@emit-vision/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.
Files changed (2) hide show
  1. package/dist/index.js +391 -0
  2. package/package.json +21 -0
package/dist/index.js ADDED
@@ -0,0 +1,391 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/login.ts
4
+ import { createInterface } from "readline";
5
+ import { exec } from "child_process";
6
+
7
+ // src/config.ts
8
+ import { homedir } from "os";
9
+ import { join } from "path";
10
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
11
+ var DEFAULT_API_URL = "https://api.emitvision.com";
12
+ var CONFIG_DIR = join(homedir(), ".emit-vision");
13
+ var CONFIG_PATH = join(CONFIG_DIR, "config.json");
14
+ function loadConfig() {
15
+ if (!existsSync(CONFIG_PATH)) return {};
16
+ try {
17
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
18
+ } catch {
19
+ return {};
20
+ }
21
+ }
22
+ function saveConfig(patch) {
23
+ mkdirSync(CONFIG_DIR, { recursive: true });
24
+ const current = loadConfig();
25
+ writeFileSync(
26
+ CONFIG_PATH,
27
+ JSON.stringify({ ...current, ...patch }, null, 2),
28
+ "utf-8"
29
+ );
30
+ }
31
+ function clearConfig() {
32
+ mkdirSync(CONFIG_DIR, { recursive: true });
33
+ writeFileSync(CONFIG_PATH, "{}", "utf-8");
34
+ }
35
+
36
+ // src/api.ts
37
+ var ApiError = class extends Error {
38
+ constructor(status, message) {
39
+ super(message);
40
+ this.status = status;
41
+ this.name = "ApiError";
42
+ }
43
+ status;
44
+ };
45
+ async function apiFetch(path, options = {}) {
46
+ const config = loadConfig();
47
+ const apiUrl = options.apiUrl ?? config.apiUrl ?? DEFAULT_API_URL;
48
+ const pat = options.pat ?? config.pat;
49
+ const headers = {
50
+ "Content-Type": "application/json"
51
+ };
52
+ if (pat) headers["Authorization"] = `Bearer ${pat}`;
53
+ let res;
54
+ try {
55
+ res = await fetch(`${apiUrl}${path}`, {
56
+ method: options.method ?? "GET",
57
+ headers,
58
+ body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
59
+ });
60
+ } catch (err) {
61
+ throw new Error(
62
+ `Network error: ${err instanceof Error ? err.message : String(err)}`
63
+ );
64
+ }
65
+ if (res.status === 401) {
66
+ throw new ApiError(401, "Run `emit-vision login` first.");
67
+ }
68
+ if (!res.ok) {
69
+ let msg = `HTTP ${res.status}`;
70
+ try {
71
+ const body = await res.json();
72
+ if (body.error) msg = body.error;
73
+ } catch {
74
+ }
75
+ throw new ApiError(res.status, msg);
76
+ }
77
+ if (res.status === 204) return void 0;
78
+ return res.json();
79
+ }
80
+
81
+ // src/commands/login.ts
82
+ async function runLogin(args) {
83
+ let apiUrl;
84
+ const apiUrlIdx = args.indexOf("--api-url");
85
+ if (apiUrlIdx !== -1 && args[apiUrlIdx + 1]) {
86
+ apiUrl = args[apiUrlIdx + 1];
87
+ }
88
+ if (args.includes("--browser")) {
89
+ await runBrowserLogin(apiUrl);
90
+ return;
91
+ }
92
+ await runPasteLogin(apiUrl);
93
+ }
94
+ async function runPasteLogin(apiUrl) {
95
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
96
+ const pat = await new Promise((resolve) => {
97
+ rl.question("Paste your Personal Access Token: ", (answer) => {
98
+ rl.close();
99
+ resolve(answer.trim());
100
+ });
101
+ });
102
+ if (!pat) {
103
+ console.error("No token provided.");
104
+ process.exit(1);
105
+ }
106
+ try {
107
+ await apiFetch("/v1/management/projects", { pat, apiUrl });
108
+ const patch = { pat };
109
+ if (apiUrl) patch.apiUrl = apiUrl;
110
+ saveConfig(patch);
111
+ console.log("Logged in successfully.");
112
+ } catch (err) {
113
+ console.error(
114
+ `Login failed: ${err instanceof Error ? err.message : String(err)}`
115
+ );
116
+ process.exit(1);
117
+ }
118
+ }
119
+ async function runBrowserLogin(apiUrl) {
120
+ const config = loadConfig();
121
+ if (config.pat) {
122
+ const rl = createInterface({
123
+ input: process.stdin,
124
+ output: process.stdout
125
+ });
126
+ const answer = await new Promise((resolve) => {
127
+ rl.question("Already logged in. Overwrite? [y/N] ", (a) => {
128
+ rl.close();
129
+ resolve(a.trim().toLowerCase());
130
+ });
131
+ });
132
+ if (answer !== "y" && answer !== "yes") {
133
+ console.log("Aborted.");
134
+ return;
135
+ }
136
+ }
137
+ let initResult;
138
+ try {
139
+ initResult = await apiFetch(
140
+ "/v1/cli-auth/init",
141
+ { method: "POST", apiUrl }
142
+ );
143
+ } catch (err) {
144
+ console.error(
145
+ `Failed to start browser login: ${err instanceof Error ? err.message : String(err)}`
146
+ );
147
+ process.exit(1);
148
+ }
149
+ const { code, authUrl } = initResult;
150
+ console.log(
151
+ `Open this URL in your browser to authorize the CLI:
152
+ ${authUrl}`
153
+ );
154
+ openBrowser(authUrl);
155
+ const controller = new AbortController();
156
+ const { signal } = controller;
157
+ process.on("SIGINT", () => {
158
+ controller.abort();
159
+ console.log("\nLogin cancelled.");
160
+ process.exit(1);
161
+ });
162
+ const POLL_INTERVAL_MS = 2e3;
163
+ const POLL_TIMEOUT_MS = 5 * 60 * 1e3;
164
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
165
+ console.log("Waiting for browser authorization\u2026");
166
+ while (Date.now() < deadline) {
167
+ if (signal.aborted) break;
168
+ await sleep(POLL_INTERVAL_MS);
169
+ if (signal.aborted) break;
170
+ let pollResult;
171
+ try {
172
+ pollResult = await apiFetch(
173
+ `/v1/cli-auth/poll/${encodeURIComponent(code)}`,
174
+ { apiUrl }
175
+ );
176
+ } catch {
177
+ continue;
178
+ }
179
+ if (pollResult.status === "authorized") {
180
+ if (!pollResult.rawToken) {
181
+ console.error("Authorization succeeded but no token was returned.");
182
+ process.exit(1);
183
+ }
184
+ const patch = {
185
+ pat: pollResult.rawToken
186
+ };
187
+ if (apiUrl) patch.apiUrl = apiUrl;
188
+ saveConfig(patch);
189
+ console.log("Logged in successfully.");
190
+ return;
191
+ }
192
+ if (pollResult.status === "expired") {
193
+ console.error("Login timed out. Try again.");
194
+ process.exit(1);
195
+ }
196
+ }
197
+ console.error("Login timed out. Try again.");
198
+ process.exit(1);
199
+ }
200
+ function openBrowser(url) {
201
+ const platform = process.platform;
202
+ const cmd2 = platform === "darwin" ? `open "${url}"` : platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
203
+ exec(cmd2, (err) => {
204
+ if (err) {
205
+ }
206
+ });
207
+ }
208
+ function sleep(ms) {
209
+ return new Promise((resolve) => setTimeout(resolve, ms));
210
+ }
211
+
212
+ // src/commands/logout.ts
213
+ function runLogout() {
214
+ clearConfig();
215
+ console.log("Logged out.");
216
+ }
217
+
218
+ // src/commands/project.ts
219
+ async function runProjectList() {
220
+ const projects = await apiFetch("/v1/management/projects");
221
+ if (projects.length === 0) {
222
+ console.log(
223
+ "No projects found. Create one with: emit-vision project create <name>"
224
+ );
225
+ return;
226
+ }
227
+ const rows = projects.map((p) => [
228
+ p.name,
229
+ p.id,
230
+ p.environment ?? "\u2014",
231
+ p.createdAt ? new Date(p.createdAt).toLocaleDateString() : "\u2014"
232
+ ]);
233
+ printTable(["Name", "ID", "Environment", "Created"], rows);
234
+ }
235
+ async function runProjectCreate(args) {
236
+ const name = args[0];
237
+ if (!name) {
238
+ console.error("Usage: emit-vision project create <name>");
239
+ process.exit(1);
240
+ }
241
+ const result = await apiFetch("/v1/management/projects", { method: "POST", body: { name } });
242
+ console.log(`Project created: ${result.name} (${result.id})`);
243
+ if (result.rawIngestKey) {
244
+ console.log(
245
+ "\nWarning: Copy this ingest key now \u2014 it will not be shown again."
246
+ );
247
+ console.log(` ${result.rawIngestKey}`);
248
+ }
249
+ }
250
+ async function runProjectEnvCreate(args) {
251
+ const [projectId, envName, ...rest2] = args;
252
+ const typeIdx = rest2.indexOf("--type");
253
+ const envType = typeIdx !== -1 ? rest2[typeIdx + 1] : void 0;
254
+ if (!projectId || !envName) {
255
+ console.error(
256
+ "Usage: emit-vision project env create <projectId> <envName> [--type development|staging|custom]"
257
+ );
258
+ process.exit(1);
259
+ }
260
+ const result = await apiFetch(`/v1/management/projects/${encodeURIComponent(projectId)}/environments`, {
261
+ method: "POST",
262
+ body: { name: envName, ...envType ? { type: envType } : {} }
263
+ });
264
+ console.log(`Environment created: ${result.name} (${result.id})`);
265
+ if (result.rawIngestKey) {
266
+ console.log(
267
+ "\nWarning: Copy this ingest key now \u2014 it will not be shown again."
268
+ );
269
+ console.log(` ${result.rawIngestKey}`);
270
+ }
271
+ }
272
+ function printTable(headers, rows) {
273
+ const widths = headers.map(
274
+ (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
275
+ );
276
+ const line = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ");
277
+ console.log(line(headers));
278
+ console.log(widths.map((w) => "-".repeat(w)).join(" "));
279
+ rows.forEach((r) => console.log(line(r)));
280
+ }
281
+
282
+ // src/commands/key.ts
283
+ import { createInterface as createInterface2 } from "readline";
284
+ async function runKeyGet(args) {
285
+ const projectId = args[0];
286
+ if (!projectId) {
287
+ console.error("Usage: emit-vision key get <projectId>");
288
+ process.exit(1);
289
+ }
290
+ const key = await apiFetch(
291
+ `/v1/management/projects/${encodeURIComponent(projectId)}/keys`
292
+ );
293
+ console.log(`Prefix: ${key.keyPrefix}`);
294
+ console.log(`Scope: ${key.scope}`);
295
+ console.log(
296
+ `Last used: ${key.lastUsedAt ? new Date(key.lastUsedAt).toLocaleDateString() : "Never"}`
297
+ );
298
+ }
299
+ async function runKeyRotate(args) {
300
+ const projectId = args[0];
301
+ if (!projectId) {
302
+ console.error("Usage: emit-vision key rotate <projectId>");
303
+ process.exit(1);
304
+ }
305
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
306
+ const answer = await new Promise((resolve) => {
307
+ rl.question(
308
+ "Rotate the ingest key? This will invalidate the current key. [y/N] ",
309
+ (a) => {
310
+ rl.close();
311
+ resolve(a.trim().toLowerCase());
312
+ }
313
+ );
314
+ });
315
+ if (answer !== "y" && answer !== "yes") {
316
+ console.log("Aborted.");
317
+ return;
318
+ }
319
+ const result = await apiFetch(
320
+ `/v1/management/projects/${encodeURIComponent(projectId)}/keys/rotate`,
321
+ { method: "POST" }
322
+ );
323
+ console.log(
324
+ "\nWarning: Copy this new ingest key now \u2014 it will not be shown again."
325
+ );
326
+ console.log(` ${result.rawIngestKey}`);
327
+ }
328
+
329
+ // src/index.ts
330
+ var [cmd, subcmd, ...rest] = process.argv.slice(2);
331
+ async function main() {
332
+ try {
333
+ if (cmd === "login") {
334
+ await runLogin(subcmd ? [subcmd, ...rest] : rest);
335
+ } else if (cmd === "logout") {
336
+ runLogout();
337
+ } else if (cmd === "project") {
338
+ if (subcmd === "list") {
339
+ await runProjectList();
340
+ } else if (subcmd === "create") {
341
+ await runProjectCreate(rest);
342
+ } else if (subcmd === "env" && rest[0] === "create") {
343
+ await runProjectEnvCreate(rest.slice(1));
344
+ } else {
345
+ printHelp();
346
+ process.exit(1);
347
+ }
348
+ } else if (cmd === "key") {
349
+ if (subcmd === "get") {
350
+ await runKeyGet(rest);
351
+ } else if (subcmd === "rotate") {
352
+ await runKeyRotate(rest);
353
+ } else {
354
+ printHelp();
355
+ process.exit(1);
356
+ }
357
+ } else {
358
+ printHelp();
359
+ if (cmd !== void 0) process.exit(1);
360
+ }
361
+ } catch (err) {
362
+ if (err instanceof ApiError) {
363
+ if (err.status === 401) {
364
+ console.error("Not authenticated. Run `emit-vision login` first.");
365
+ } else if (err.status === 402) {
366
+ console.error(err.message);
367
+ } else {
368
+ console.error(`Error: ${err.message}`);
369
+ }
370
+ } else {
371
+ console.error(
372
+ `Error: ${err instanceof Error ? err.message : String(err)}`
373
+ );
374
+ }
375
+ process.exit(1);
376
+ }
377
+ }
378
+ function printHelp() {
379
+ console.log(`emit-vision CLI
380
+
381
+ Commands:
382
+ login [--api-url <url>] Authenticate with a Personal Access Token
383
+ logout Clear stored credentials
384
+ project list List your projects
385
+ project create <name> Create a new project
386
+ project env create <projectId> <name> [--type ...] Create a new environment
387
+ key get <projectId> Get ingest key info for a project
388
+ key rotate <projectId> Rotate the ingest key for a project
389
+ `);
390
+ }
391
+ main();
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@emit-vision/cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI for emit-vision project management",
5
+ "private": false,
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "bin": {
9
+ "emit-vision": "./dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "scripts": {
18
+ "build": "tsup --config tsup.config.ts",
19
+ "typecheck": "tsc -p tsconfig.json --noEmit"
20
+ }
21
+ }