@appuo/orbit 1.0.6 → 1.0.10

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command8 } from "commander";
4
+ import { Command as Command9 } from "commander";
5
5
  import { createRequire } from "module";
6
6
 
7
7
  // src/providers/provider.interface.ts
@@ -21,45 +21,106 @@ var getProvider = (name) => {
21
21
  };
22
22
 
23
23
  // src/providers/vercel.ts
24
- import fs2 from "fs";
25
- import path2 from "path";
24
+ import fs3 from "fs";
25
+ import path3 from "path";
26
26
  import os2 from "os";
27
27
 
28
28
  // src/utils/paths.ts
29
29
  import os from "os";
30
- import path from "path";
30
+ import path2 from "path";
31
+ import fs2 from "fs";
32
+
33
+ // src/utils/fileOps.ts
34
+ import crypto from "crypto";
31
35
  import fs from "fs";
36
+ import path from "path";
37
+ var ensureFileMode = (filePath, mode) => {
38
+ if (!fs.existsSync(filePath)) {
39
+ return;
40
+ }
41
+ const currentMode = fs.statSync(filePath).mode & 511;
42
+ if (currentMode !== mode) {
43
+ fs.chmodSync(filePath, mode);
44
+ }
45
+ };
46
+ var atomicWriteFile = (filePath, content, mode) => {
47
+ const directory = path.dirname(filePath);
48
+ if (!fs.existsSync(directory)) {
49
+ fs.mkdirSync(directory, { recursive: true, mode: 448 });
50
+ }
51
+ const tempName = `.${path.basename(filePath)}.${process.pid}.${crypto.randomUUID()}.tmp`;
52
+ const tempPath = path.join(directory, tempName);
53
+ try {
54
+ if (mode === void 0) {
55
+ fs.writeFileSync(tempPath, content, "utf-8");
56
+ } else {
57
+ fs.writeFileSync(tempPath, content, { encoding: "utf-8", mode });
58
+ }
59
+ fs.renameSync(tempPath, filePath);
60
+ if (mode !== void 0) {
61
+ fs.chmodSync(filePath, mode);
62
+ }
63
+ } finally {
64
+ if (fs.existsSync(tempPath)) {
65
+ fs.rmSync(tempPath, { force: true });
66
+ }
67
+ }
68
+ };
69
+ var quarantineCorruptFile = (filePath, suffix) => {
70
+ if (!fs.existsSync(filePath)) {
71
+ return filePath;
72
+ }
73
+ const quarantinePath = `${filePath}.corrupt-${suffix}`;
74
+ try {
75
+ fs.renameSync(filePath, quarantinePath);
76
+ return quarantinePath;
77
+ } catch {
78
+ return filePath;
79
+ }
80
+ };
81
+
82
+ // src/utils/paths.ts
32
83
  var getConfigDir = () => {
33
- return path.join(os.homedir(), ".orbit");
84
+ const overrideDir = process.env["ORBIT_CONFIG_DIR"];
85
+ if (overrideDir && overrideDir.trim().length > 0) {
86
+ return overrideDir;
87
+ }
88
+ return path2.join(os.homedir(), ".orbit");
34
89
  };
35
90
  var getConfigPath = () => {
36
- return path.join(getConfigDir(), "config.json");
91
+ return path2.join(getConfigDir(), "config.json");
37
92
  };
38
93
  var ensureConfigDir = () => {
39
94
  const dir = getConfigDir();
40
- if (!fs.existsSync(dir)) {
41
- fs.mkdirSync(dir, { recursive: true });
95
+ if (!fs2.existsSync(dir)) {
96
+ fs2.mkdirSync(dir, { recursive: true, mode: 448 });
42
97
  }
98
+ ensureFileMode(dir, 448);
43
99
  };
44
100
  var getAuthDir = (provider) => {
45
- return path.join(getConfigDir(), "auth", provider);
101
+ return path2.join(getConfigDir(), "auth", provider);
46
102
  };
47
103
  var getAuthFilePath = (provider, profile) => {
48
- return path.join(getAuthDir(provider), `${profile}.json`);
104
+ return path2.join(getAuthDir(provider), `${profile}.json`);
49
105
  };
50
106
  var ensureAuthDir = (provider) => {
51
107
  const dir = getAuthDir(provider);
52
- if (!fs.existsSync(dir)) {
53
- fs.mkdirSync(dir, { recursive: true });
108
+ if (!fs2.existsSync(dir)) {
109
+ fs2.mkdirSync(dir, { recursive: true, mode: 448 });
54
110
  }
111
+ ensureFileMode(dir, 448);
55
112
  };
56
113
 
57
114
  // src/providers/vercel.ts
58
115
  var getVercelAuthPath = () => {
59
116
  const platform = os2.platform();
60
117
  const home = os2.homedir();
118
+ const xdgDataHome = process.env["XDG_DATA_HOME"];
119
+ if (xdgDataHome && xdgDataHome.trim().length > 0) {
120
+ return path3.join(xdgDataHome, "com.vercel.cli", "auth.json");
121
+ }
61
122
  if (platform === "darwin") {
62
- return path2.join(
123
+ return path3.join(
63
124
  home,
64
125
  "Library",
65
126
  "Application Support",
@@ -67,13 +128,30 @@ var getVercelAuthPath = () => {
67
128
  "auth.json"
68
129
  );
69
130
  } else if (platform === "win32") {
70
- const appData = process.env["APPDATA"] || path2.join(home, "AppData", "Roaming");
71
- return path2.join(appData, "com.vercel.cli", "auth.json");
131
+ const appData = process.env["APPDATA"] || path3.join(home, "AppData", "Roaming");
132
+ return path3.join(appData, "com.vercel.cli", "auth.json");
72
133
  } else {
73
- const xdgDataHome = process.env["XDG_DATA_HOME"] || path2.join(home, ".local", "share");
74
- return path2.join(xdgDataHome, "com.vercel.cli", "auth.json");
134
+ const linuxDataHome = path3.join(home, ".local", "share");
135
+ return path3.join(linuxDataHome, "com.vercel.cli", "auth.json");
75
136
  }
76
137
  };
138
+ var ensureVercelAuthDir = (authPath) => {
139
+ const authDir = path3.dirname(authPath);
140
+ if (!fs3.existsSync(authDir)) {
141
+ fs3.mkdirSync(authDir, { recursive: true, mode: 448 });
142
+ }
143
+ };
144
+ var writeVercelAuthToken = (token) => {
145
+ const authPath = getVercelAuthPath();
146
+ if (!authPath) return false;
147
+ ensureVercelAuthDir(authPath);
148
+ fs3.writeFileSync(authPath, JSON.stringify({ token }, null, 2), {
149
+ encoding: "utf-8",
150
+ mode: 384
151
+ });
152
+ ensureFileMode(authPath, 384);
153
+ return true;
154
+ };
77
155
  var vercelProvider = {
78
156
  name: "vercel",
79
157
  getEnvVar() {
@@ -88,8 +166,8 @@ var vercelProvider = {
88
166
  async getStoredToken() {
89
167
  try {
90
168
  const configPath = getVercelAuthPath();
91
- if (configPath && fs2.existsSync(configPath)) {
92
- const content = fs2.readFileSync(configPath, "utf-8");
169
+ if (configPath && fs3.existsSync(configPath)) {
170
+ const content = fs3.readFileSync(configPath, "utf-8");
93
171
  const config = JSON.parse(content);
94
172
  if (config.token && typeof config.token === "string") {
95
173
  return config.token;
@@ -127,57 +205,169 @@ var vercelProvider = {
127
205
  },
128
206
  async captureAuth(profile) {
129
207
  const authPath = getVercelAuthPath();
130
- if (!authPath || !fs2.existsSync(authPath)) return;
208
+ if (!authPath || !fs3.existsSync(authPath)) return;
131
209
  ensureAuthDir("vercel");
132
210
  const destPath = getAuthFilePath("vercel", profile);
133
- fs2.copyFileSync(authPath, destPath);
211
+ fs3.copyFileSync(authPath, destPath);
212
+ ensureFileMode(destPath, 384);
134
213
  },
135
214
  async restoreAuth(profile) {
136
215
  const sourcePath = getAuthFilePath("vercel", profile);
137
- if (!fs2.existsSync(sourcePath)) return false;
216
+ if (!fs3.existsSync(sourcePath)) return false;
138
217
  const authPath = getVercelAuthPath();
139
218
  if (!authPath) return false;
140
- const authDir = path2.dirname(authPath);
141
- if (!fs2.existsSync(authDir)) {
142
- fs2.mkdirSync(authDir, { recursive: true });
143
- }
144
- fs2.copyFileSync(sourcePath, authPath);
219
+ ensureVercelAuthDir(authPath);
220
+ fs3.copyFileSync(sourcePath, authPath);
221
+ ensureFileMode(authPath, 384);
145
222
  return true;
223
+ },
224
+ async seedAuthFromToken(token) {
225
+ return writeVercelAuthToken(token);
226
+ },
227
+ async validateActiveAuth() {
228
+ const token = await this.getStoredToken?.();
229
+ if (!token) return false;
230
+ try {
231
+ const { execa: execa3 } = await import("execa");
232
+ const result = await execa3("vercel", ["whoami", "--token", token], {
233
+ stdio: "pipe",
234
+ reject: false
235
+ });
236
+ if (result.exitCode === 0) {
237
+ return true;
238
+ }
239
+ const output = `${result.stdout}
240
+ ${result.stderr}`.toLowerCase();
241
+ if (output.includes("token is not valid") || output.includes("not valid")) {
242
+ return false;
243
+ }
244
+ return true;
245
+ } catch {
246
+ return true;
247
+ }
248
+ },
249
+ async removeAuthSnapshot(profile) {
250
+ const snapshotPath = getAuthFilePath("vercel", profile);
251
+ if (fs3.existsSync(snapshotPath)) {
252
+ fs3.rmSync(snapshotPath, { force: true });
253
+ }
146
254
  }
147
255
  };
148
256
 
149
257
  // src/commands/add.ts
150
258
  import { Command } from "commander";
151
- import ora from "ora";
152
259
  import chalk2 from "chalk";
153
260
  import readline from "readline";
154
261
 
155
262
  // src/storage/keychain.ts
156
- import fs3 from "fs";
157
- import path3 from "path";
158
- import crypto from "crypto";
263
+ import fs4 from "fs";
264
+ import path4 from "path";
265
+ import crypto2 from "crypto";
266
+
267
+ // src/utils/logger.ts
268
+ import chalk from "chalk";
269
+ var jsonMode = false;
270
+ var write = (stream, message) => {
271
+ stream.write(`${message}
272
+ `);
273
+ };
274
+ var writeJson = (stream, level, message) => {
275
+ write(
276
+ stream,
277
+ JSON.stringify({
278
+ level,
279
+ message
280
+ })
281
+ );
282
+ };
283
+ var setJsonMode = (enabled) => {
284
+ jsonMode = enabled;
285
+ };
286
+ var logger = {
287
+ isJsonMode: () => {
288
+ return jsonMode;
289
+ },
290
+ json: (payload) => {
291
+ write(process.stdout, JSON.stringify(payload));
292
+ },
293
+ info: (message) => {
294
+ if (jsonMode) {
295
+ writeJson(process.stdout, "info", message);
296
+ return;
297
+ }
298
+ write(process.stdout, chalk.blue("\u2139") + ` ${message}`);
299
+ },
300
+ success: (message) => {
301
+ if (jsonMode) {
302
+ writeJson(process.stdout, "success", message);
303
+ return;
304
+ }
305
+ write(process.stdout, chalk.green("\u2714") + ` ${message}`);
306
+ },
307
+ warn: (message) => {
308
+ if (jsonMode) {
309
+ writeJson(process.stderr, "warn", message);
310
+ return;
311
+ }
312
+ write(process.stderr, chalk.yellow("\u26A0") + ` ${message}`);
313
+ },
314
+ error: (message) => {
315
+ if (jsonMode) {
316
+ writeJson(process.stderr, "error", message);
317
+ return;
318
+ }
319
+ write(process.stderr, chalk.red("\u2716") + ` ${message}`);
320
+ },
321
+ plain: (message) => {
322
+ if (jsonMode) {
323
+ write(process.stdout, message);
324
+ return;
325
+ }
326
+ write(process.stdout, message);
327
+ },
328
+ newline: () => {
329
+ if (jsonMode) {
330
+ return;
331
+ }
332
+ write(process.stdout, "");
333
+ }
334
+ };
335
+
336
+ // src/storage/keychain.ts
159
337
  var CREDENTIALS_FILE = "credentials.json";
160
338
  var KEY_FILE = ".key";
161
339
  var ALGORITHM = "aes-256-gcm";
340
+ var ENCRYPTED_FILE_MODE = 384;
162
341
  var getCredentialsPath = () => {
163
- return path3.join(getConfigDir(), CREDENTIALS_FILE);
342
+ return path4.join(getConfigDir(), CREDENTIALS_FILE);
164
343
  };
165
344
  var getKeyPath = () => {
166
- return path3.join(getConfigDir(), KEY_FILE);
345
+ return path4.join(getConfigDir(), KEY_FILE);
346
+ };
347
+ var writeKey = (keyPath, key) => {
348
+ atomicWriteFile(keyPath, key.toString("hex"), ENCRYPTED_FILE_MODE);
349
+ };
350
+ var readKey = (keyPath) => {
351
+ ensureFileMode(keyPath, ENCRYPTED_FILE_MODE);
352
+ const raw = fs4.readFileSync(keyPath, "utf-8").trim();
353
+ if (!/^[0-9a-fA-F]{64}$/.test(raw)) {
354
+ throw new Error("Invalid encryption key format.");
355
+ }
356
+ return Buffer.from(raw, "hex");
167
357
  };
168
358
  var getOrCreateKey = () => {
169
359
  const keyPath = getKeyPath();
170
360
  ensureConfigDir();
171
- if (fs3.existsSync(keyPath)) {
172
- return Buffer.from(fs3.readFileSync(keyPath, "utf-8"), "hex");
361
+ if (fs4.existsSync(keyPath)) {
362
+ return readKey(keyPath);
173
363
  }
174
- const key = crypto.randomBytes(32);
175
- fs3.writeFileSync(keyPath, key.toString("hex"), { mode: 384 });
364
+ const key = crypto2.randomBytes(32);
365
+ writeKey(keyPath, key);
176
366
  return key;
177
367
  };
178
368
  var encrypt = (text, key) => {
179
- const iv = crypto.randomBytes(16);
180
- const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
369
+ const iv = crypto2.randomBytes(16);
370
+ const cipher = crypto2.createCipheriv(ALGORITHM, key, iv);
181
371
  let encrypted = cipher.update(text, "utf-8", "hex");
182
372
  encrypted += cipher.final("hex");
183
373
  const tag = cipher.getAuthTag();
@@ -188,7 +378,7 @@ var encrypt = (text, key) => {
188
378
  };
189
379
  };
190
380
  var decrypt = (entry, key) => {
191
- const decipher = crypto.createDecipheriv(
381
+ const decipher = crypto2.createDecipheriv(
192
382
  ALGORITHM,
193
383
  key,
194
384
  Buffer.from(entry.iv, "hex")
@@ -200,17 +390,22 @@ var decrypt = (entry, key) => {
200
390
  };
201
391
  var loadStore = () => {
202
392
  const filePath = getCredentialsPath();
203
- if (!fs3.existsSync(filePath)) return {};
393
+ if (!fs4.existsSync(filePath)) return {};
394
+ ensureFileMode(filePath, ENCRYPTED_FILE_MODE);
204
395
  try {
205
- return JSON.parse(fs3.readFileSync(filePath, "utf-8"));
396
+ return JSON.parse(fs4.readFileSync(filePath, "utf-8"));
206
397
  } catch {
398
+ const quarantinePath = quarantineCorruptFile(filePath, `${Date.now()}`);
399
+ logger.warn(
400
+ `Detected invalid credentials store. Backed it up to "${quarantinePath}" and re-initialized credentials.`
401
+ );
207
402
  return {};
208
403
  }
209
404
  };
210
405
  var saveStore = (store) => {
211
406
  ensureConfigDir();
212
407
  const filePath = getCredentialsPath();
213
- fs3.writeFileSync(filePath, JSON.stringify(store, null, 2), { mode: 384 });
408
+ atomicWriteFile(filePath, JSON.stringify(store, null, 2), ENCRYPTED_FILE_MODE);
214
409
  };
215
410
  var getKey = (provider, profile) => {
216
411
  return `${provider}:${profile}`;
@@ -240,9 +435,41 @@ var deleteToken = async (provider, profile) => {
240
435
  saveStore(store);
241
436
  return true;
242
437
  };
438
+ var rotateEncryptionKey = async () => {
439
+ const keyPath = getKeyPath();
440
+ const oldKey = getOrCreateKey();
441
+ const store = loadStore();
442
+ const decryptedEntries = /* @__PURE__ */ new Map();
443
+ for (const [entryKey, entryValue] of Object.entries(store)) {
444
+ decryptedEntries.set(entryKey, decrypt(entryValue, oldKey));
445
+ }
446
+ const newKey = crypto2.randomBytes(32);
447
+ const rotatedStore = {};
448
+ for (const [entryKey, token] of decryptedEntries.entries()) {
449
+ rotatedStore[entryKey] = encrypt(token, newKey);
450
+ }
451
+ const backupPath = `${keyPath}.bak-${Date.now()}`;
452
+ if (fs4.existsSync(keyPath)) {
453
+ fs4.copyFileSync(keyPath, backupPath);
454
+ }
455
+ try {
456
+ writeKey(keyPath, newKey);
457
+ saveStore(rotatedStore);
458
+ } catch (error) {
459
+ if (fs4.existsSync(backupPath)) {
460
+ fs4.copyFileSync(backupPath, keyPath);
461
+ ensureFileMode(keyPath, ENCRYPTED_FILE_MODE);
462
+ }
463
+ throw error;
464
+ } finally {
465
+ if (fs4.existsSync(backupPath)) {
466
+ fs4.rmSync(backupPath, { force: true });
467
+ }
468
+ }
469
+ };
243
470
 
244
471
  // src/storage/configStore.ts
245
- import fs4 from "fs";
472
+ import fs5 from "fs";
246
473
 
247
474
  // src/types/config.ts
248
475
  import { z } from "zod";
@@ -259,21 +486,37 @@ var OrbitConfigSchema = z.object({
259
486
  var defaultConfig = {
260
487
  providers: {}
261
488
  };
489
+ var writeDefaultConfig = (configPath) => {
490
+ ensureConfigDir();
491
+ atomicWriteFile(
492
+ configPath,
493
+ JSON.stringify(defaultConfig, null, 2),
494
+ 384
495
+ );
496
+ return { providers: {} };
497
+ };
262
498
  var loadConfig = () => {
263
499
  const configPath = getConfigPath();
264
- if (!fs4.existsSync(configPath)) {
265
- ensureConfigDir();
266
- fs4.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), "utf-8");
267
- return { ...defaultConfig };
500
+ if (!fs5.existsSync(configPath)) {
501
+ return writeDefaultConfig(configPath);
502
+ }
503
+ ensureFileMode(configPath, 384);
504
+ try {
505
+ const raw = fs5.readFileSync(configPath, "utf-8");
506
+ const parsed = JSON.parse(raw);
507
+ return OrbitConfigSchema.parse(parsed);
508
+ } catch (error) {
509
+ const quarantinePath = quarantineCorruptFile(configPath, `${Date.now()}`);
510
+ logger.warn(
511
+ `Detected invalid config. Backed it up to "${quarantinePath}" and re-initialized Orbit config.`
512
+ );
513
+ return writeDefaultConfig(configPath);
268
514
  }
269
- const raw = fs4.readFileSync(configPath, "utf-8");
270
- const parsed = JSON.parse(raw);
271
- return OrbitConfigSchema.parse(parsed);
272
515
  };
273
516
  var saveConfig = (config) => {
274
517
  ensureConfigDir();
275
518
  const configPath = getConfigPath();
276
- fs4.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
519
+ atomicWriteFile(configPath, JSON.stringify(config, null, 2), 384);
277
520
  };
278
521
  var addProfile = (provider, profile, metadata) => {
279
522
  const config = loadConfig();
@@ -300,6 +543,12 @@ var removeProfile = (provider, profile) => {
300
543
  if (providerConfig.current === profile) {
301
544
  providerConfig.current = void 0;
302
545
  }
546
+ if (providerConfig.metadata?.[profile]) {
547
+ delete providerConfig.metadata[profile];
548
+ if (Object.keys(providerConfig.metadata).length === 0) {
549
+ delete providerConfig.metadata;
550
+ }
551
+ }
303
552
  if (providerConfig.profiles.length === 0) {
304
553
  delete config.providers[provider];
305
554
  }
@@ -336,50 +585,135 @@ var validateProfileName = (name) => {
336
585
  return nameSchema.parse(name);
337
586
  };
338
587
 
339
- // src/utils/logger.ts
340
- import chalk from "chalk";
341
- var write = (stream, message) => {
342
- stream.write(`${message}
343
- `);
588
+ // src/utils/errorHandler.ts
589
+ import { ZodError } from "zod";
590
+
591
+ // src/utils/cliError.ts
592
+ var CliError = class extends Error {
593
+ exitCode;
594
+ constructor(message, exitCode = 1) {
595
+ super(message);
596
+ this.name = "CliError";
597
+ this.exitCode = exitCode;
598
+ }
344
599
  };
345
- var logger = {
346
- info: (message) => {
347
- write(process.stdout, chalk.blue("\u2139") + ` ${message}`);
348
- },
349
- success: (message) => {
350
- write(process.stdout, chalk.green("\u2714") + ` ${message}`);
351
- },
352
- warn: (message) => {
353
- write(process.stderr, chalk.yellow("\u26A0") + ` ${message}`);
354
- },
355
- error: (message) => {
356
- write(process.stderr, chalk.red("\u2716") + ` ${message}`);
357
- },
358
- plain: (message) => {
359
- write(process.stdout, message);
360
- },
361
- newline: () => {
362
- write(process.stdout, "");
600
+ var fail = (message, exitCode = 1) => {
601
+ throw new CliError(message, exitCode);
602
+ };
603
+
604
+ // src/utils/redact.ts
605
+ var PATTERNS = [
606
+ [/(Bearer\s+)[A-Za-z0-9._\-~=+/]+/g, "$1[REDACTED]"],
607
+ [/([A-Z_]*TOKEN=)[^\s"']+/g, "$1[REDACTED]"],
608
+ [/"token"\s*:\s*"[^"]+"/g, '"token":"[REDACTED]"']
609
+ ];
610
+ var redactSecrets = (value) => {
611
+ let output = value;
612
+ for (const [pattern, replacement] of PATTERNS) {
613
+ output = output.replace(pattern, replacement);
363
614
  }
615
+ return output;
364
616
  };
365
617
 
366
618
  // src/utils/errorHandler.ts
367
- import { ZodError } from "zod";
619
+ var printDebugStack = (error) => {
620
+ if (process.env["ORBIT_DEBUG"] !== "1") {
621
+ return;
622
+ }
623
+ logger.plain(redactSecrets(error.stack ?? ""));
624
+ };
368
625
  var handleError = (error) => {
369
626
  if (error instanceof ZodError) {
370
627
  const messages = error.issues.map((e) => e.message).join(", ");
371
- logger.error(`Validation error: ${messages}`);
372
- process.exit(1);
628
+ const message = `Validation error: ${messages}`;
629
+ if (logger.isJsonMode()) {
630
+ logger.json({
631
+ ok: false,
632
+ error: {
633
+ type: "validation_error",
634
+ message
635
+ }
636
+ });
637
+ } else {
638
+ logger.error(message);
639
+ }
640
+ process.exitCode = 1;
641
+ return;
642
+ }
643
+ if (error instanceof CliError) {
644
+ if (logger.isJsonMode()) {
645
+ logger.json({
646
+ ok: false,
647
+ error: {
648
+ type: "cli_error",
649
+ message: error.message
650
+ }
651
+ });
652
+ } else {
653
+ logger.error(error.message);
654
+ }
655
+ process.exitCode = error.exitCode;
656
+ printDebugStack(error);
657
+ return;
373
658
  }
374
659
  if (error instanceof Error) {
375
- logger.error(error.message);
376
- if (process.env["ORBIT_DEBUG"] === "1") {
377
- logger.plain(error.stack ?? "");
660
+ const message = redactSecrets(error.message);
661
+ if (logger.isJsonMode()) {
662
+ logger.json({
663
+ ok: false,
664
+ error: {
665
+ type: "runtime_error",
666
+ message
667
+ }
668
+ });
669
+ } else {
670
+ logger.error(message);
378
671
  }
379
- process.exit(1);
672
+ process.exitCode = 1;
673
+ printDebugStack(error);
674
+ return;
675
+ }
676
+ if (logger.isJsonMode()) {
677
+ logger.json({
678
+ ok: false,
679
+ error: {
680
+ type: "unknown_error",
681
+ message: "An unexpected error occurred."
682
+ }
683
+ });
684
+ } else {
685
+ logger.error("An unexpected error occurred.");
686
+ }
687
+ process.exitCode = 1;
688
+ };
689
+
690
+ // src/utils/spinner.ts
691
+ import ora from "ora";
692
+ var NoopSpinner = class {
693
+ text;
694
+ constructor(text) {
695
+ this.text = text;
696
+ }
697
+ start() {
698
+ return this;
699
+ }
700
+ stop() {
701
+ return this;
702
+ }
703
+ succeed(text) {
704
+ logger.success(text ?? this.text);
705
+ return this;
706
+ }
707
+ fail(text) {
708
+ logger.error(text ?? this.text);
709
+ return this;
380
710
  }
381
- logger.error("An unexpected error occurred.");
382
- process.exit(1);
711
+ };
712
+ var createSpinner = (text) => {
713
+ if (logger.isJsonMode()) {
714
+ return new NoopSpinner(text);
715
+ }
716
+ return ora(text).start();
383
717
  };
384
718
 
385
719
  // src/commands/add.ts
@@ -449,7 +783,7 @@ var addCommand = new Command("add").description("Add a new cloud provider profil
449
783
  let token = "";
450
784
  let storedTokenFound = false;
451
785
  if (provider.getStoredToken) {
452
- const spinner2 = ora(`Checking for existing ${provider.name} credentials...`).start();
786
+ const spinner2 = createSpinner(`Checking for existing ${provider.name} credentials...`);
453
787
  try {
454
788
  const storedToken = await provider.getStoredToken();
455
789
  spinner2.stop();
@@ -481,7 +815,7 @@ var addCommand = new Command("add").description("Add a new cloud provider profil
481
815
  const loginSuccess = await provider.login();
482
816
  if (loginSuccess) {
483
817
  if (provider.getStoredToken) {
484
- const spinner2 = ora("Checking for new credentials...").start();
818
+ const spinner2 = createSpinner("Checking for new credentials...");
485
819
  try {
486
820
  const storedToken = await provider.getStoredToken();
487
821
  spinner2.stop();
@@ -502,14 +836,12 @@ var addCommand = new Command("add").description("Add a new cloud provider profil
502
836
  token = await promptHiddenInput(`\u{1F511} Enter token for ${provider.name}/${validProfile}: `);
503
837
  }
504
838
  if (!token.trim()) {
505
- logger.error("Token cannot be empty.");
506
- process.exit(1);
839
+ fail("Token cannot be empty.");
507
840
  }
508
- const spinner = ora("Validating token...").start();
841
+ const spinner = createSpinner("Validating token...");
509
842
  if (!provider.validateToken(token.trim())) {
510
843
  spinner.fail("Token validation failed.");
511
- logger.error(`Invalid token format for ${provider.name}. Please check your token and try again.`);
512
- process.exit(1);
844
+ fail(`Invalid token format for ${provider.name}. Please check your token and try again.`);
513
845
  }
514
846
  spinner.text = "Storing token securely...";
515
847
  await storeToken(validProvider, validProfile, token.trim());
@@ -548,6 +880,25 @@ var listCommand = new Command2("list").description("List all profiles across pro
548
880
  try {
549
881
  const config = loadConfig();
550
882
  const providers2 = Object.keys(config.providers);
883
+ if (logger.isJsonMode()) {
884
+ const payload = providers2.map((providerName) => {
885
+ const providerConfig = config.providers[providerName];
886
+ return {
887
+ provider: providerName,
888
+ current: providerConfig?.current ?? null,
889
+ profiles: (providerConfig?.profiles ?? []).map((profile) => ({
890
+ name: profile,
891
+ email: providerConfig?.metadata?.[profile]?.email ?? null,
892
+ isCurrent: providerConfig?.current === profile
893
+ }))
894
+ };
895
+ });
896
+ logger.json({
897
+ ok: true,
898
+ providers: payload
899
+ });
900
+ return;
901
+ }
551
902
  if (providers2.length === 0) {
552
903
  logger.info('No profiles configured yet. Use "orbit add <provider> <profile>" to get started.');
553
904
  return;
@@ -578,19 +929,21 @@ var listCommand = new Command2("list").description("List all profiles across pro
578
929
 
579
930
  // src/commands/remove.ts
580
931
  import { Command as Command3 } from "commander";
581
- import ora2 from "ora";
582
932
  var removeCommand = new Command3("remove").description("Remove a cloud provider profile").argument("<provider>", "Cloud provider name").argument("<profile>", "Profile name to remove").action(async (providerName, profileName) => {
583
933
  try {
584
934
  const validProvider = validateProviderName(providerName);
585
935
  const validProfile = validateProfileName(profileName);
586
- getProvider(validProvider);
936
+ const provider = getProvider(validProvider);
587
937
  if (!profileExists(validProvider, validProfile)) {
588
- logger.error(`Profile "${validProfile}" does not exist for provider "${validProvider}".`);
589
- process.exit(1);
938
+ fail(`Profile "${validProfile}" does not exist for provider "${validProvider}".`);
590
939
  }
591
- const spinner = ora2("Removing profile...").start();
940
+ const spinner = createSpinner("Removing profile...");
592
941
  spinner.text = "Deleting token from secure storage...";
593
942
  await deleteToken(validProvider, validProfile);
943
+ if (provider.removeAuthSnapshot) {
944
+ spinner.text = "Removing local auth snapshot...";
945
+ await provider.removeAuthSnapshot(validProfile);
946
+ }
594
947
  spinner.text = "Updating configuration...";
595
948
  removeProfile(validProvider, validProfile);
596
949
  spinner.succeed(`Profile "${validProfile}" removed from ${validProvider}.`);
@@ -601,32 +954,69 @@ var removeCommand = new Command3("remove").description("Remove a cloud provider
601
954
 
602
955
  // src/commands/use.ts
603
956
  import { Command as Command4 } from "commander";
604
- import ora3 from "ora";
605
957
  var useCommand = new Command4("use").description("Set the current active profile for a provider").argument("<provider>", "Cloud provider name").argument("<profile>", "Profile name to activate").action(async (providerName, profileName) => {
606
958
  try {
607
959
  const validProvider = validateProviderName(providerName);
608
960
  const validProfile = validateProfileName(profileName);
609
961
  const provider = getProvider(validProvider);
610
962
  if (!profileExists(validProvider, validProfile)) {
611
- logger.error(`Profile "${validProfile}" does not exist for provider "${provider.name}".`);
612
- process.exit(1);
963
+ fail(`Profile "${validProfile}" does not exist for provider "${provider.name}".`);
613
964
  }
614
- const spinner = ora3("Switching profile...").start();
615
- const currentProfile = getCurrentProfile(validProvider);
616
- if (currentProfile && currentProfile !== validProfile && provider.captureAuth) {
617
- try {
618
- await provider.captureAuth(currentProfile);
619
- } catch {
965
+ const spinner = createSpinner("Switching profile...");
966
+ try {
967
+ const currentProfile = getCurrentProfile(validProvider);
968
+ if (currentProfile && currentProfile !== validProfile && provider.captureAuth) {
969
+ try {
970
+ await provider.captureAuth(currentProfile);
971
+ } catch {
972
+ }
620
973
  }
621
- }
622
- if (provider.restoreAuth) {
623
- const restored = await provider.restoreAuth(validProfile);
624
- if (restored) {
974
+ if (provider.restoreAuth && currentProfile !== validProfile) {
975
+ const restoredSnapshot = await provider.restoreAuth(validProfile);
976
+ if (!restoredSnapshot) {
977
+ fail(
978
+ `No saved auth snapshot found for ${provider.name}/${validProfile}. Re-add the profile with "orbit add ${validProvider} ${validProfile}" to capture credentials.`
979
+ );
980
+ }
625
981
  spinner.text = "Auth credentials restored...";
626
982
  }
983
+ const authValid = provider.validateActiveAuth ? await provider.validateActiveAuth() : true;
984
+ if (!authValid) {
985
+ let recovered = false;
986
+ if (provider.seedAuthFromToken) {
987
+ const savedToken = await getToken(validProvider, validProfile);
988
+ if (savedToken && provider.validateToken(savedToken)) {
989
+ spinner.text = "Refreshing auth from saved token...";
990
+ const seeded = await provider.seedAuthFromToken(savedToken);
991
+ if (seeded) {
992
+ recovered = provider.validateActiveAuth ? await provider.validateActiveAuth() : true;
993
+ }
994
+ }
995
+ }
996
+ if (!recovered) {
997
+ if (currentProfile && currentProfile !== validProfile && provider.restoreAuth) {
998
+ try {
999
+ await provider.restoreAuth(currentProfile);
1000
+ } catch {
1001
+ }
1002
+ }
1003
+ fail(
1004
+ `Credentials for ${provider.name}/${validProfile} are invalid or expired. Re-authenticate with "${provider.getCliName()} login", then refresh Orbit with "orbit add ${validProvider} ${validProfile}".`
1005
+ );
1006
+ }
1007
+ }
1008
+ if (provider.captureAuth) {
1009
+ try {
1010
+ await provider.captureAuth(validProfile);
1011
+ } catch {
1012
+ }
1013
+ }
1014
+ setCurrentProfile(validProvider, validProfile);
1015
+ spinner.succeed(`Now using profile "${validProfile}" for ${provider.name}.`);
1016
+ } catch (error) {
1017
+ spinner.fail("Failed to switch profile.");
1018
+ throw error;
627
1019
  }
628
- setCurrentProfile(validProvider, validProfile);
629
- spinner.succeed(`Now using profile "${validProfile}" for ${provider.name}.`);
630
1020
  } catch (error) {
631
1021
  handleError(error);
632
1022
  }
@@ -641,8 +1031,7 @@ var runCommand = new Command5("run").description("Run a command with a specific
641
1031
  const validProfile = validateProfileName(profileName);
642
1032
  const provider = getProvider(validProvider);
643
1033
  if (!profileExists(validProvider, validProfile)) {
644
- logger.error(`Profile "${validProfile}" does not exist for provider "${provider.name}".`);
645
- process.exit(1);
1034
+ fail(`Profile "${validProfile}" does not exist for provider "${provider.name}".`);
646
1035
  }
647
1036
  logger.info(`Running as ${provider.name}/${validProfile}...`);
648
1037
  const currentProfile = getCurrentProfile(validProvider);
@@ -655,6 +1044,11 @@ var runCommand = new Command5("run").description("Run a command with a specific
655
1044
  }
656
1045
  }
657
1046
  swapped = await provider.restoreAuth(validProfile);
1047
+ if (!swapped) {
1048
+ logger.warn(
1049
+ `No local auth snapshot found for ${provider.name}/${validProfile}. Using token fallback.`
1050
+ );
1051
+ }
658
1052
  }
659
1053
  try {
660
1054
  if (swapped) {
@@ -667,15 +1061,18 @@ var runCommand = new Command5("run").description("Run a command with a specific
667
1061
  } else {
668
1062
  const token = await getToken(validProvider, validProfile);
669
1063
  if (!token) {
670
- logger.error(`No token found for ${provider.name}/${validProfile}. Try adding it again with "orbit add".`);
671
- process.exit(1);
1064
+ fail(
1065
+ `No token found for ${provider.name}/${validProfile}. Try adding it again with "orbit add".`
1066
+ );
1067
+ return;
672
1068
  }
1069
+ const envToken = token;
673
1070
  const cliName = provider.getCliName();
674
1071
  const envVar = provider.getEnvVar();
675
1072
  const result = await execa(cliName, args, {
676
1073
  env: {
677
1074
  ...process.env,
678
- [envVar]: token
1075
+ [envVar]: envToken
679
1076
  },
680
1077
  stdio: "inherit",
681
1078
  reject: false
@@ -690,7 +1087,6 @@ var runCommand = new Command5("run").description("Run a command with a specific
690
1087
  }
691
1088
  }
692
1089
  }
693
- process.exit(process.exitCode ?? 0);
694
1090
  } catch (error) {
695
1091
  handleError(error);
696
1092
  }
@@ -705,40 +1101,47 @@ var execCommand = new Command6("exec").description("Execute a command using the
705
1101
  const provider = getProvider(validProvider);
706
1102
  const currentProfile = getCurrentProfile(validProvider);
707
1103
  if (!currentProfile) {
708
- logger.error(
709
- `No active profile set for ${provider.name}. Use "orbit use ${validProvider} <profile>" first.`
710
- );
711
- process.exit(1);
1104
+ fail(`No active profile set for ${provider.name}. Use "orbit use ${validProvider} <profile>" first.`);
1105
+ return;
712
1106
  }
1107
+ const activeProfile = currentProfile;
1108
+ const cliName = provider.getCliName();
1109
+ logger.info(`Running as ${provider.name}/${activeProfile}...`);
1110
+ let swapped = false;
713
1111
  if (provider.restoreAuth) {
714
- await provider.restoreAuth(currentProfile);
1112
+ swapped = await provider.restoreAuth(activeProfile);
715
1113
  }
716
- const cliName = provider.getCliName();
717
- logger.info(`Running as ${provider.name}/${currentProfile}...`);
718
- if (provider.getAuthConfigPath?.()) {
1114
+ if (swapped) {
719
1115
  const result2 = await execa2(cliName, args, {
720
1116
  stdio: "inherit",
721
1117
  reject: false
722
1118
  });
723
- process.exit(result2.exitCode ?? 0);
1119
+ process.exitCode = result2.exitCode ?? 0;
1120
+ return;
1121
+ }
1122
+ if (provider.restoreAuth) {
1123
+ logger.warn(
1124
+ `Unable to restore local auth snapshot for ${provider.name}/${activeProfile}. Falling back to token-based execution.`
1125
+ );
724
1126
  }
725
- const token = await getToken(validProvider, currentProfile);
1127
+ const token = await getToken(validProvider, activeProfile);
726
1128
  if (!token) {
727
- logger.error(
728
- `No token found for ${provider.name}/${currentProfile}. Try adding it again with "orbit add".`
1129
+ fail(
1130
+ `No token found for ${provider.name}/${activeProfile}. Cannot verify identity; try "orbit add ${validProvider} ${activeProfile}" first.`
729
1131
  );
730
- process.exit(1);
1132
+ return;
731
1133
  }
1134
+ const envToken = token;
732
1135
  const envVar = provider.getEnvVar();
733
1136
  const result = await execa2(cliName, args, {
734
1137
  env: {
735
1138
  ...process.env,
736
- [envVar]: token
1139
+ [envVar]: envToken
737
1140
  },
738
1141
  stdio: "inherit",
739
1142
  reject: false
740
1143
  });
741
- process.exit(result.exitCode ?? 0);
1144
+ process.exitCode = result.exitCode ?? 0;
742
1145
  } catch (error) {
743
1146
  handleError(error);
744
1147
  }
@@ -751,6 +1154,23 @@ var currentCommand = new Command7("current").description("Show current active pr
751
1154
  try {
752
1155
  const config = loadConfig();
753
1156
  const providers2 = Object.keys(config.providers);
1157
+ if (logger.isJsonMode()) {
1158
+ const current = providers2.map((providerName) => {
1159
+ const providerConfig = config.providers[providerName];
1160
+ if (!providerConfig?.current) {
1161
+ return null;
1162
+ }
1163
+ return {
1164
+ provider: providerName,
1165
+ profile: providerConfig.current
1166
+ };
1167
+ }).filter((entry) => entry !== null);
1168
+ logger.json({
1169
+ ok: true,
1170
+ current
1171
+ });
1172
+ return;
1173
+ }
754
1174
  if (providers2.length === 0) {
755
1175
  logger.info("No profiles configured yet.");
756
1176
  return;
@@ -773,12 +1193,31 @@ var currentCommand = new Command7("current").description("Show current active pr
773
1193
  }
774
1194
  });
775
1195
 
1196
+ // src/commands/rotate-key.ts
1197
+ import { Command as Command8 } from "commander";
1198
+ var rotateKeyCommand = new Command8("rotate-key").description("Rotate local encryption key and re-encrypt stored tokens").action(async () => {
1199
+ const spinner = createSpinner("Rotating encryption key...");
1200
+ try {
1201
+ await rotateEncryptionKey();
1202
+ spinner.succeed("Encryption key rotated successfully.");
1203
+ } catch (error) {
1204
+ spinner.fail("Failed to rotate encryption key.");
1205
+ handleError(error);
1206
+ }
1207
+ });
1208
+
776
1209
  // src/index.ts
777
1210
  var require2 = createRequire(import.meta.url);
778
1211
  var pkg = require2("../package.json");
1212
+ if (process.argv.includes("--json")) {
1213
+ setJsonMode(true);
1214
+ }
1215
+ if (process.argv.includes("--debug")) {
1216
+ process.env["ORBIT_DEBUG"] = "1";
1217
+ }
779
1218
  registerProvider(vercelProvider);
780
- var program = new Command8();
781
- program.name("orbit").description("Switch Vercel identities instantly. No logout required.").version(pkg.version).enablePositionalOptions();
1219
+ var program = new Command9();
1220
+ program.name("orbit").description("Switch Vercel identities instantly. No logout required.").version(pkg.version).option("--json", "Output machine-readable JSON logs").option("--debug", "Enable debug diagnostics (with secret redaction)").enablePositionalOptions();
782
1221
  program.addCommand(addCommand);
783
1222
  program.addCommand(listCommand);
784
1223
  program.addCommand(removeCommand);
@@ -786,6 +1225,7 @@ program.addCommand(useCommand);
786
1225
  program.addCommand(runCommand);
787
1226
  program.addCommand(execCommand);
788
1227
  program.addCommand(currentCommand);
1228
+ program.addCommand(rotateKeyCommand);
789
1229
  try {
790
1230
  await program.parseAsync(process.argv);
791
1231
  } catch (error) {