@bagdock/cli 0.4.0 → 0.6.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/README.md CHANGED
@@ -1,3 +1,17 @@
1
+ ```
2
+ ----++ ----++ ---+++
3
+ ---+++ ---++ ---++
4
+ ----+--- ----- --------- --------++ ------ ----- ----++-----
5
+ ---------+ --------++----------++--------+++--------+ --------++---++---++++
6
+ ---+++---++ ++++---++---+++---++---+++---++---+++---++---++---++------++++
7
+ ----++ ---++--------++---++----++---++ ---++---++ ---+---++ -------++
8
+ ----++----+---+++---++---++----++---++----++---++---+++--++ --------+---++
9
+ ---------++--------+++--------+++--------++ -------+++ -------++---++----++
10
+ +++++++++ +++++++++- +++---++ ++++++++ ++++++ ++++++ ++++ ++++
11
+ --------+++
12
+ +++++++
13
+ ```
14
+
1
15
  # @bagdock/cli
2
16
 
3
17
  The official CLI for Bagdock. Built for humans, AI agents, and CI/CD pipelines.
@@ -160,6 +174,42 @@ The CLI resolves your API key using the following priority chain:
160
174
 
161
175
  If no key is found from any source, the CLI errors with code `auth_error`.
162
176
 
177
+ ## Environment context
178
+
179
+ The CLI supports Stripe-style live/test mode switching. Login is universal — you authenticate once, then select which operator and environment to target.
180
+
181
+ ### Operator + environment resolution
182
+
183
+ | Priority | Source | How to set |
184
+ |----------|--------|-----------|
185
+ | 1 (highest) | `--env` global flag | `bagdock --env test deploy` |
186
+ | 2 | `.bagdock/link.json` | `bagdock link --env test` |
187
+ | 3 | Profile stored value | `bagdock switch` |
188
+ | 4 (lowest) | Default | `live` |
189
+
190
+ For operator slug:
191
+
192
+ | Priority | Source | How to set |
193
+ |----------|--------|-----------|
194
+ | 1 (highest) | `BAGDOCK_OPERATOR` env var | `export BAGDOCK_OPERATOR=wisestorage` |
195
+ | 2 (lowest) | Profile stored value | `bagdock switch` or `bagdock login` |
196
+
197
+ ### Typical workflow
198
+
199
+ ```bash
200
+ # Login (universal identity)
201
+ bagdock login
202
+
203
+ # Select operator and environment
204
+ bagdock switch
205
+
206
+ # Or override per-command
207
+ bagdock --env test deploy --target staging
208
+ bagdock --env live apps list
209
+ ```
210
+
211
+ All API requests include `X-Environment` and `X-Operator-Slug` headers, ensuring the backend resolves the correct tenant database.
212
+
163
213
  ## Commands
164
214
 
165
215
  ### `bagdock login`
@@ -246,6 +296,7 @@ bagdock doctor
246
296
  |-------|------|------|------|
247
297
  | CLI Version | Running latest | Update available or registry unreachable | — |
248
298
  | API Key | Key found (shows masked key + source) | — | No key found |
299
+ | Operator Context | Operator + environment set | No operator selected | — |
249
300
  | Project Config | Valid `bagdock.json` found | No config or incomplete | — |
250
301
  | AI Agents | Lists detected agents (or none) | — | — |
251
302
 
@@ -300,6 +351,46 @@ Exits `0` when all checks pass or warn. Exits `1` if any check fails.
300
351
 
301
352
  ---
302
353
 
354
+ ### `bagdock switch`
355
+
356
+ Switch operator and environment context. After login, use this to select which operator (live or sandbox) to target.
357
+
358
+ ```bash
359
+ bagdock switch
360
+ ```
361
+
362
+ #### Interactive mode
363
+
364
+ ```
365
+ ? Select operator:
366
+ 1. WiseStorage (wisestorage)
367
+ 2. Ardran REIT (ardran-reit)
368
+ > 1
369
+
370
+ ? Select environment:
371
+ 1. Live
372
+ 2. Sandbox: crm-integration (default)
373
+ 3. Sandbox: access-testing
374
+ > 2
375
+
376
+ Switched to wisestorage [test] (sandbox: crm-integration)
377
+ ```
378
+
379
+ #### Non-interactive mode (CI/CD)
380
+
381
+ ```bash
382
+ bagdock switch --operator wisestorage --env test
383
+ ```
384
+
385
+ #### JSON output
386
+
387
+ ```bash
388
+ bagdock switch --operator wisestorage --env live --json
389
+ # => {"operator":{"id":"opreg_xxx","slug":"wisestorage","name":"WiseStorage"},"environment":"live"}
390
+ ```
391
+
392
+ ---
393
+
303
394
  ### Switch between profiles
304
395
 
305
396
  If you work across multiple Bagdock operators, the CLI supports named profiles.
@@ -700,6 +791,7 @@ bagdock [global options] <command> [command options]
700
791
  |------|-------------|
701
792
  | `--api-key <key>` | Override API key for this invocation (takes highest priority) |
702
793
  | `-p, --profile <name>` | Profile to use (overrides `BAGDOCK_PROFILE` env var) |
794
+ | `--env <live\|test>` | Override environment for this invocation |
703
795
  | `--json` | Force JSON output even in interactive terminals |
704
796
  | `-q, --quiet` | Suppress spinners and status output (implies `--json`) |
705
797
  | `--version` | Print version and exit |
package/dist/bagdock.js CHANGED
@@ -2081,110 +2081,6 @@ var require_commander = __commonJS((exports) => {
2081
2081
  exports.InvalidOptionArgumentError = InvalidArgumentError;
2082
2082
  });
2083
2083
 
2084
- // src/config.ts
2085
- import { homedir } from "os";
2086
- import { join } from "path";
2087
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2088
- function setProfileOverride(name) {
2089
- profileOverride = name;
2090
- }
2091
- function ensureConfigDir() {
2092
- if (!existsSync(CONFIG_DIR)) {
2093
- mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
2094
- }
2095
- }
2096
- function loadStore() {
2097
- try {
2098
- if (!existsSync(CREDENTIALS_FILE)) {
2099
- return { activeProfile: "default", profiles: {} };
2100
- }
2101
- const raw = JSON.parse(readFileSync(CREDENTIALS_FILE, "utf-8"));
2102
- if (raw.accessToken && !raw.profiles) {
2103
- const migrated = {
2104
- activeProfile: "default",
2105
- profiles: { default: raw }
2106
- };
2107
- saveStore(migrated);
2108
- return migrated;
2109
- }
2110
- return raw;
2111
- } catch {
2112
- return { activeProfile: "default", profiles: {} };
2113
- }
2114
- }
2115
- function saveStore(store) {
2116
- ensureConfigDir();
2117
- writeFileSync(CREDENTIALS_FILE, JSON.stringify(store, null, 2), { mode: 384 });
2118
- }
2119
- function resolveProfile() {
2120
- if (profileOverride)
2121
- return profileOverride;
2122
- if (process.env.BAGDOCK_PROFILE)
2123
- return process.env.BAGDOCK_PROFILE;
2124
- return loadStore().activeProfile;
2125
- }
2126
- function loadCredentials() {
2127
- const store = loadStore();
2128
- const name = resolveProfile();
2129
- return store.profiles[name] ?? null;
2130
- }
2131
- function saveCredentials(creds, profileName) {
2132
- const store = loadStore();
2133
- const name = profileName ?? resolveProfile();
2134
- store.profiles[name] = creds;
2135
- if (!store.activeProfile || Object.keys(store.profiles).length === 1) {
2136
- store.activeProfile = name;
2137
- }
2138
- saveStore(store);
2139
- }
2140
- function clearCredentials() {
2141
- const store = loadStore();
2142
- const name = resolveProfile();
2143
- delete store.profiles[name];
2144
- if (store.activeProfile === name) {
2145
- const remaining = Object.keys(store.profiles);
2146
- store.activeProfile = remaining[0] ?? "default";
2147
- }
2148
- saveStore(store);
2149
- }
2150
- function listProfiles() {
2151
- const store = loadStore();
2152
- return Object.entries(store.profiles).map(([name, creds]) => ({
2153
- name,
2154
- email: creds.email,
2155
- operatorId: creds.operatorId,
2156
- active: name === store.activeProfile
2157
- }));
2158
- }
2159
- function switchProfile(name) {
2160
- const store = loadStore();
2161
- if (!store.profiles[name])
2162
- return false;
2163
- store.activeProfile = name;
2164
- saveStore(store);
2165
- return true;
2166
- }
2167
- function getActiveProfileName() {
2168
- return resolveProfile();
2169
- }
2170
- function loadBagdockJson(dir) {
2171
- const file = join(dir, "bagdock.json");
2172
- if (!existsSync(file))
2173
- return null;
2174
- try {
2175
- return JSON.parse(readFileSync(file, "utf-8"));
2176
- } catch {
2177
- return null;
2178
- }
2179
- }
2180
- var CONFIG_DIR, CREDENTIALS_FILE, API_BASE, DASHBOARD_BASE, profileOverride;
2181
- var init_config = __esm(() => {
2182
- CONFIG_DIR = join(homedir(), ".bagdock");
2183
- CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
2184
- API_BASE = process.env.BAGDOCK_API_URL ?? "https://api.bagdock.com";
2185
- DASHBOARD_BASE = process.env.BAGDOCK_DASHBOARD_URL ?? "https://dashboard.bagdock.com";
2186
- });
2187
-
2188
2084
  // node_modules/chalk/source/vendor/ansi-styles/index.js
2189
2085
  function assembleStyles() {
2190
2086
  const codes = new Map;
@@ -2674,6 +2570,196 @@ var init_source = __esm(() => {
2674
2570
  source_default = chalk;
2675
2571
  });
2676
2572
 
2573
+ // src/config.ts
2574
+ import { homedir } from "os";
2575
+ import { join } from "path";
2576
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2577
+ function getApiBase() {
2578
+ return _apiBase;
2579
+ }
2580
+ function getDashboardBase() {
2581
+ return _dashboardBase;
2582
+ }
2583
+ function loadLocalEnv() {
2584
+ const envPath = join(process.cwd(), ".env.local");
2585
+ if (!existsSync(envPath)) {
2586
+ console.error(source_default.red("No .env.local found in"), source_default.bold(process.cwd()));
2587
+ console.error(source_default.dim("Create one with BAGDOCK_API_URL=https://your-ngrok-url"));
2588
+ process.exit(1);
2589
+ }
2590
+ const vars = {};
2591
+ const lines = readFileSync(envPath, "utf-8").split(`
2592
+ `);
2593
+ for (const line of lines) {
2594
+ const trimmed = line.trim();
2595
+ if (!trimmed || trimmed.startsWith("#"))
2596
+ continue;
2597
+ const eq = trimmed.indexOf("=");
2598
+ if (eq === -1)
2599
+ continue;
2600
+ vars[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim();
2601
+ }
2602
+ const apiUrl = vars["BAGDOCK_API_URL"];
2603
+ if (!apiUrl) {
2604
+ console.error(source_default.red("BAGDOCK_API_URL not found in .env.local"));
2605
+ console.error(source_default.dim("Add BAGDOCK_API_URL=https://your-ngrok-url to .env.local"));
2606
+ process.exit(1);
2607
+ }
2608
+ _apiBase = apiUrl;
2609
+ if (vars["BAGDOCK_DASHBOARD_URL"]) {
2610
+ _dashboardBase = vars["BAGDOCK_DASHBOARD_URL"];
2611
+ }
2612
+ console.log(source_default.dim(`Using local env → ${_apiBase}`));
2613
+ }
2614
+ function setProfileOverride(name) {
2615
+ profileOverride = name;
2616
+ }
2617
+ function setEnvironmentOverride(env2) {
2618
+ envOverride = env2;
2619
+ }
2620
+ function getEnvironmentOverride() {
2621
+ return envOverride;
2622
+ }
2623
+ function ensureConfigDir() {
2624
+ if (!existsSync(CONFIG_DIR)) {
2625
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
2626
+ }
2627
+ }
2628
+ function loadStore() {
2629
+ try {
2630
+ if (!existsSync(CREDENTIALS_FILE)) {
2631
+ return { activeProfile: "default", profiles: {} };
2632
+ }
2633
+ const raw = JSON.parse(readFileSync(CREDENTIALS_FILE, "utf-8"));
2634
+ if (raw.accessToken && !raw.profiles) {
2635
+ const migrated = {
2636
+ activeProfile: "default",
2637
+ profiles: { default: raw }
2638
+ };
2639
+ saveStore(migrated);
2640
+ return migrated;
2641
+ }
2642
+ return raw;
2643
+ } catch {
2644
+ return { activeProfile: "default", profiles: {} };
2645
+ }
2646
+ }
2647
+ function saveStore(store) {
2648
+ ensureConfigDir();
2649
+ writeFileSync(CREDENTIALS_FILE, JSON.stringify(store, null, 2), { mode: 384 });
2650
+ }
2651
+ function resolveProfile() {
2652
+ if (profileOverride)
2653
+ return profileOverride;
2654
+ if (process.env.BAGDOCK_PROFILE)
2655
+ return process.env.BAGDOCK_PROFILE;
2656
+ return loadStore().activeProfile;
2657
+ }
2658
+ function loadCredentials() {
2659
+ const store = loadStore();
2660
+ const name = resolveProfile();
2661
+ return store.profiles[name] ?? null;
2662
+ }
2663
+ function saveCredentials(creds, profileName) {
2664
+ const store = loadStore();
2665
+ const name = profileName ?? resolveProfile();
2666
+ store.profiles[name] = creds;
2667
+ if (!store.activeProfile || Object.keys(store.profiles).length === 1) {
2668
+ store.activeProfile = name;
2669
+ }
2670
+ saveStore(store);
2671
+ }
2672
+ function clearCredentials() {
2673
+ const store = loadStore();
2674
+ const name = resolveProfile();
2675
+ delete store.profiles[name];
2676
+ if (store.activeProfile === name) {
2677
+ const remaining = Object.keys(store.profiles);
2678
+ store.activeProfile = remaining[0] ?? "default";
2679
+ }
2680
+ saveStore(store);
2681
+ }
2682
+ function listProfiles() {
2683
+ const store = loadStore();
2684
+ return Object.entries(store.profiles).map(([name, creds]) => ({
2685
+ name,
2686
+ email: creds.email,
2687
+ operatorId: creds.operatorId,
2688
+ operatorSlug: creds.operatorSlug,
2689
+ environment: creds.environment,
2690
+ active: name === store.activeProfile
2691
+ }));
2692
+ }
2693
+ function switchProfile(name) {
2694
+ const store = loadStore();
2695
+ if (!store.profiles[name])
2696
+ return false;
2697
+ store.activeProfile = name;
2698
+ saveStore(store);
2699
+ return true;
2700
+ }
2701
+ function getActiveProfileName() {
2702
+ return resolveProfile();
2703
+ }
2704
+ function resolveLinkEnvironment() {
2705
+ try {
2706
+ const p = join(process.cwd(), ".bagdock", "link.json");
2707
+ if (!existsSync(p))
2708
+ return;
2709
+ const data = JSON.parse(readFileSync(p, "utf-8"));
2710
+ if (data.environment === "live" || data.environment === "test")
2711
+ return data.environment;
2712
+ return;
2713
+ } catch {
2714
+ return;
2715
+ }
2716
+ }
2717
+ function resolveEnvironment() {
2718
+ if (envOverride)
2719
+ return envOverride;
2720
+ const envVar = process.env.BAGDOCK_ENV;
2721
+ if (envVar === "test" || envVar === "live")
2722
+ return envVar;
2723
+ const creds = loadCredentials();
2724
+ return creds?.environment ?? "live";
2725
+ }
2726
+ function resolveOperatorSlug() {
2727
+ const envVar = process.env.BAGDOCK_OPERATOR;
2728
+ if (envVar)
2729
+ return envVar;
2730
+ const creds = loadCredentials();
2731
+ return creds?.operatorSlug;
2732
+ }
2733
+ function updateProfileContext(operatorId, operatorSlug, environment) {
2734
+ const creds = loadCredentials();
2735
+ if (!creds)
2736
+ return;
2737
+ saveCredentials({
2738
+ ...creds,
2739
+ operatorId,
2740
+ operatorSlug,
2741
+ environment
2742
+ });
2743
+ }
2744
+ function loadBagdockJson(dir) {
2745
+ const file = join(dir, "bagdock.json");
2746
+ if (!existsSync(file))
2747
+ return null;
2748
+ try {
2749
+ return JSON.parse(readFileSync(file, "utf-8"));
2750
+ } catch {
2751
+ return null;
2752
+ }
2753
+ }
2754
+ var CONFIG_DIR, CREDENTIALS_FILE, _apiBase, _dashboardBase, profileOverride, envOverride;
2755
+ var init_config = __esm(() => {
2756
+ init_source();
2757
+ CONFIG_DIR = join(homedir(), ".bagdock");
2758
+ CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
2759
+ _apiBase = process.env.BAGDOCK_API_URL ?? "https://api.bagdock.com";
2760
+ _dashboardBase = process.env.BAGDOCK_DASHBOARD_URL ?? "https://dashboard.bagdock.com";
2761
+ });
2762
+
2677
2763
  // src/output.ts
2678
2764
  function setOutputMode(opts) {
2679
2765
  quiet = !!opts.quiet;
@@ -3303,7 +3389,7 @@ async function login() {
3303
3389
  console.log(source_default.cyan(`
3304
3390
  Requesting device authorization...
3305
3391
  `));
3306
- const deviceRes = await fetch(`${API_BASE}/oauth2/device/authorize`, {
3392
+ const deviceRes = await fetch(`${getApiBase()}/oauth2/device/authorize`, {
3307
3393
  method: "POST",
3308
3394
  headers: { "Content-Type": "application/json" },
3309
3395
  body: JSON.stringify({ client_id: CLIENT_ID, scope: "developer:read developer:write" })
@@ -3325,7 +3411,7 @@ Requesting device authorization...
3325
3411
  const startedAt = Date.now();
3326
3412
  while (Date.now() - startedAt < MAX_POLL_DURATION_MS) {
3327
3413
  await sleep(pollInterval);
3328
- const tokenRes = await fetch(`${API_BASE}/oauth2/token`, {
3414
+ const tokenRes = await fetch(`${getApiBase()}/oauth2/token`, {
3329
3415
  method: "POST",
3330
3416
  headers: { "Content-Type": "application/json" },
3331
3417
  body: JSON.stringify({
@@ -3341,15 +3427,27 @@ Requesting device authorization...
3341
3427
  refreshToken: tokens.refresh_token,
3342
3428
  expiresAt: tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : undefined,
3343
3429
  email: tokens.email,
3344
- operatorId: tokens.operator_id
3430
+ operatorId: tokens.operator_id,
3431
+ operatorSlug: tokens.operator_slug,
3432
+ environment: "live"
3345
3433
  });
3346
3434
  console.log(source_default.green(`
3347
3435
  Logged in successfully!`));
3348
3436
  if (tokens.email)
3349
3437
  console.log(" Email:", source_default.bold(tokens.email));
3350
- if (tokens.operator_id)
3351
- console.log(" Operator:", source_default.bold(tokens.operator_id));
3438
+ if (tokens.operator_slug) {
3439
+ console.log(" Operator:", source_default.bold(tokens.operator_slug));
3440
+ } else if (tokens.operator_id) {
3441
+ console.log(" Operator ID:", source_default.bold(tokens.operator_id));
3442
+ }
3443
+ console.log(" Environment:", source_default.bold("live"));
3352
3444
  console.log(" Profile:", source_default.bold(getActiveProfileName()));
3445
+ if (!tokens.operator_slug) {
3446
+ await resolveOperatorAfterLogin(tokens.access_token);
3447
+ } else {
3448
+ console.log();
3449
+ console.log(source_default.dim(" Tip: run"), source_default.cyan("bagdock switch"), source_default.dim("to change operator or environment."));
3450
+ }
3353
3451
  return;
3354
3452
  }
3355
3453
  const error = await tokenRes.json().catch(() => ({ error: "unknown" }));
@@ -3388,7 +3486,7 @@ async function whoami() {
3388
3486
  process.exit(1);
3389
3487
  }
3390
3488
  try {
3391
- const res = await fetch(`${API_BASE}/v1/auth/me`, {
3489
+ const res = await fetch(`${getApiBase()}/v1/auth/me`, {
3392
3490
  headers: { Authorization: `Bearer ${token}` }
3393
3491
  });
3394
3492
  if (!res.ok) {
@@ -3396,12 +3494,22 @@ async function whoami() {
3396
3494
  process.exit(1);
3397
3495
  }
3398
3496
  const user = await res.json();
3497
+ const creds = loadCredentials();
3399
3498
  if (isJsonMode()) {
3400
- outputSuccess({ ...user, profile: getActiveProfileName() });
3499
+ outputSuccess({
3500
+ ...user,
3501
+ profile: getActiveProfileName(),
3502
+ operator_slug: creds?.operatorSlug,
3503
+ environment: creds?.environment ?? "live"
3504
+ });
3401
3505
  } else {
3402
3506
  console.log(source_default.green("Logged in as"), source_default.bold(user.email));
3403
- if (user.operator_id)
3404
- console.log("Operator:", source_default.bold(user.operator_id));
3507
+ if (creds?.operatorSlug) {
3508
+ console.log("Operator:", source_default.bold(creds.operatorSlug));
3509
+ } else if (user.operator_id) {
3510
+ console.log("Operator ID:", source_default.bold(user.operator_id));
3511
+ }
3512
+ console.log("Environment:", source_default.bold(creds?.environment ?? "live"));
3405
3513
  if (user.name)
3406
3514
  console.log("Name:", user.name);
3407
3515
  console.log("Profile:", source_default.bold(getActiveProfileName()));
@@ -3428,8 +3536,9 @@ async function authList() {
3428
3536
  const marker = p.active ? source_default.green("* ") : " ";
3429
3537
  const label = p.active ? source_default.bold(p.name) : p.name;
3430
3538
  const email = p.email ? source_default.dim(` (${p.email})`) : "";
3431
- const op = p.operatorId ? source_default.dim(` [${p.operatorId}]`) : "";
3432
- console.log(` ${marker}${label}${email}${op}`);
3539
+ const operator = p.operatorSlug ? source_default.dim(` ${p.operatorSlug}`) : p.operatorId ? source_default.dim(` ${p.operatorId}`) : "";
3540
+ const env2 = p.environment ? source_default.dim(` [${p.environment}]`) : "";
3541
+ console.log(` ${marker}${label}${email}${operator}${env2}`);
3433
3542
  }
3434
3543
  console.log();
3435
3544
  }
@@ -3482,6 +3591,28 @@ Available profiles:
3482
3591
  process.exit(1);
3483
3592
  }
3484
3593
  }
3594
+ async function resolveOperatorAfterLogin(accessToken) {
3595
+ try {
3596
+ const res = await fetch(`${getApiBase()}/api/v1/me/operators`, {
3597
+ headers: { Authorization: `Bearer ${accessToken}` }
3598
+ });
3599
+ if (!res.ok)
3600
+ return;
3601
+ const body = await res.json();
3602
+ const operators = body.data ?? [];
3603
+ if (operators.length === 1) {
3604
+ const op = operators[0];
3605
+ updateProfileContext(op.id, op.slug, "live");
3606
+ console.log(" Operator:", source_default.bold(`${op.name} (${op.slug})`));
3607
+ console.log();
3608
+ console.log(source_default.dim(" Tip: run"), source_default.cyan("bagdock switch"), source_default.dim("to change environment to test/sandbox."));
3609
+ } else if (operators.length > 1) {
3610
+ console.log();
3611
+ console.log(source_default.dim(` You have access to ${operators.length} operators.`));
3612
+ console.log(source_default.dim(" Run"), source_default.cyan("bagdock switch"), source_default.dim("to select one."));
3613
+ }
3614
+ } catch {}
3615
+ }
3485
3616
  function sleep(ms) {
3486
3617
  return new Promise((resolve) => setTimeout(resolve, ms));
3487
3618
  }
@@ -3492,6 +3623,175 @@ var init_auth = __esm(() => {
3492
3623
  init_output();
3493
3624
  });
3494
3625
 
3626
+ // src/api.ts
3627
+ function resolveFullEnvironment() {
3628
+ const flagOverride = getEnvironmentOverride();
3629
+ if (flagOverride)
3630
+ return flagOverride;
3631
+ const linkEnv = resolveLinkEnvironment();
3632
+ if (linkEnv)
3633
+ return linkEnv;
3634
+ return resolveEnvironment();
3635
+ }
3636
+ async function apiFetch(path2, init2) {
3637
+ const token = getAuthToken();
3638
+ if (!token) {
3639
+ outputError("auth_error", "Not authenticated. Run bagdock login or set BAGDOCK_API_KEY.");
3640
+ process.exit(1);
3641
+ }
3642
+ const env2 = resolveFullEnvironment();
3643
+ const opSlug = resolveOperatorSlug();
3644
+ const headers = new Headers(init2?.headers);
3645
+ headers.set("Authorization", `Bearer ${token}`);
3646
+ headers.set("X-Environment", env2);
3647
+ if (opSlug)
3648
+ headers.set("X-Operator-Slug", opSlug);
3649
+ return fetch(`${getApiBase()}${path2}`, { ...init2, headers });
3650
+ }
3651
+ async function apiFetchJson(path2, method, body) {
3652
+ return apiFetch(path2, {
3653
+ method,
3654
+ headers: { "Content-Type": "application/json" },
3655
+ body: JSON.stringify(body)
3656
+ });
3657
+ }
3658
+ var init_api = __esm(() => {
3659
+ init_config();
3660
+ init_auth();
3661
+ init_output();
3662
+ });
3663
+
3664
+ // src/switch.ts
3665
+ var exports_switch = {};
3666
+ __export(exports_switch, {
3667
+ switchContext: () => switchContext
3668
+ });
3669
+ async function fetchOperators() {
3670
+ const res = await apiFetch("/api/v1/me/operators");
3671
+ if (!res.ok) {
3672
+ const err = await res.json().catch(() => ({}));
3673
+ throw new Error(err?.error?.message || `API returned ${res.status}`);
3674
+ }
3675
+ const body = await res.json();
3676
+ return body.data ?? [];
3677
+ }
3678
+ async function fetchSandboxes() {
3679
+ const res = await apiFetch("/api/v1/me/sandboxes");
3680
+ if (!res.ok)
3681
+ return [];
3682
+ const body = await res.json();
3683
+ return body.data ?? [];
3684
+ }
3685
+ function prompt(question) {
3686
+ const readline = __require("readline");
3687
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3688
+ return new Promise((resolve) => rl.question(question, (answer) => {
3689
+ rl.close();
3690
+ resolve(answer.trim());
3691
+ }));
3692
+ }
3693
+ async function switchContext(opts) {
3694
+ status("Fetching your operators...");
3695
+ let operators;
3696
+ try {
3697
+ operators = await fetchOperators();
3698
+ } catch (err) {
3699
+ outputError("api_error", `Failed to list operators: ${err.message}`);
3700
+ return;
3701
+ }
3702
+ if (!operators.length) {
3703
+ outputError("no_operators", "No operators found for your account.");
3704
+ return;
3705
+ }
3706
+ let selected;
3707
+ if (opts.operator) {
3708
+ const match = operators.find((o) => o.slug === opts.operator || o.id === opts.operator);
3709
+ if (!match) {
3710
+ outputError("not_found", `Operator "${opts.operator}" not found. Available: ${operators.map((o) => o.slug).join(", ")}`);
3711
+ return;
3712
+ }
3713
+ selected = match;
3714
+ } else if (operators.length === 1) {
3715
+ selected = operators[0];
3716
+ if (!isJsonMode()) {
3717
+ console.log(source_default.dim(` Auto-selected: ${selected.name} (${selected.slug})`));
3718
+ }
3719
+ } else if (!process.stdout.isTTY || isJsonMode()) {
3720
+ outputError("operator_required", `Multiple operators available. Pass --operator <slug>. Available: ${operators.map((o) => o.slug).join(", ")}`);
3721
+ return;
3722
+ } else {
3723
+ console.log(source_default.bold(`
3724
+ Select operator:
3725
+ `));
3726
+ operators.forEach((op, i) => {
3727
+ console.log(` ${source_default.cyan(String(i + 1))} ${op.name} ${source_default.dim(`(${op.slug})`)}`);
3728
+ });
3729
+ console.log();
3730
+ const answer = await prompt(" > ");
3731
+ const idx = parseInt(answer, 10) - 1;
3732
+ if (isNaN(idx) || idx < 0 || idx >= operators.length) {
3733
+ outputError("invalid_selection", "Invalid selection.");
3734
+ return;
3735
+ }
3736
+ selected = operators[idx];
3737
+ }
3738
+ let environment = "live";
3739
+ if (opts.env) {
3740
+ if (opts.env !== "live" && opts.env !== "test") {
3741
+ outputError("invalid_env", 'Environment must be "live" or "test".');
3742
+ return;
3743
+ }
3744
+ environment = opts.env;
3745
+ } else if (process.stdout.isTTY && !isJsonMode()) {
3746
+ status("Fetching sandboxes...");
3747
+ updateProfileContext(selected.id, selected.slug, "live");
3748
+ const sandboxes = await fetchSandboxes();
3749
+ console.log(source_default.bold(`
3750
+ Select environment:
3751
+ `));
3752
+ console.log(` ${source_default.cyan("1")} Live`);
3753
+ if (sandboxes.length > 0) {
3754
+ sandboxes.forEach((sb, i) => {
3755
+ const tag = sb.is_default ? source_default.dim(" (default)") : "";
3756
+ console.log(` ${source_default.cyan(String(i + 2))} Sandbox: ${sb.name}${tag}`);
3757
+ });
3758
+ } else {
3759
+ console.log(` ${source_default.cyan("2")} Test ${source_default.dim("(default sandbox)")}`);
3760
+ }
3761
+ console.log();
3762
+ const envAnswer = await prompt(" > ");
3763
+ const envIdx = parseInt(envAnswer, 10);
3764
+ if (envIdx === 1) {
3765
+ environment = "live";
3766
+ } else if (sandboxes.length > 0 && envIdx >= 2 && envIdx <= sandboxes.length + 1) {
3767
+ environment = "test";
3768
+ } else if (envIdx === 2 && sandboxes.length === 0) {
3769
+ environment = "test";
3770
+ } else {
3771
+ outputError("invalid_selection", "Invalid selection.");
3772
+ return;
3773
+ }
3774
+ }
3775
+ updateProfileContext(selected.id, selected.slug, environment);
3776
+ if (isJsonMode()) {
3777
+ outputSuccess({
3778
+ operator: { id: selected.id, slug: selected.slug, name: selected.name },
3779
+ environment
3780
+ });
3781
+ } else {
3782
+ const envLabel = environment === "test" ? source_default.yellow("test") : source_default.green("live");
3783
+ console.log();
3784
+ console.log(source_default.green(" Switched to"), source_default.bold(selected.name), source_default.dim(`(${selected.slug})`), `[${envLabel}]`);
3785
+ console.log();
3786
+ }
3787
+ }
3788
+ var init_switch = __esm(() => {
3789
+ init_source();
3790
+ init_config();
3791
+ init_api();
3792
+ init_output();
3793
+ });
3794
+
3495
3795
  // src/doctor.ts
3496
3796
  var exports_doctor = {};
3497
3797
  __export(exports_doctor, {
@@ -3579,11 +3879,30 @@ function checkAgents() {
3579
3879
  }
3580
3880
  return { name: "AI Agents", status: "pass", message: `Detected: ${detected.join(", ")}` };
3581
3881
  }
3882
+ function checkOperatorContext() {
3883
+ const creds = loadCredentials();
3884
+ const slug = resolveOperatorSlug();
3885
+ const env2 = resolveEnvironment();
3886
+ const profile = getActiveProfileName();
3887
+ if (!slug) {
3888
+ return {
3889
+ name: "Operator Context",
3890
+ status: "warn",
3891
+ message: `No operator selected (profile: ${profile}). Run \`bagdock switch\` to set one.`
3892
+ };
3893
+ }
3894
+ return {
3895
+ name: "Operator Context",
3896
+ status: "pass",
3897
+ message: `${slug} [${env2}] (profile: ${profile})`
3898
+ };
3899
+ }
3582
3900
  async function doctor() {
3583
3901
  const checks = [];
3584
3902
  if (isJsonMode()) {
3585
3903
  checks.push(await checkVersion());
3586
3904
  checks.push(checkAuth());
3905
+ checks.push(checkOperatorContext());
3587
3906
  checks.push(checkProjectConfig());
3588
3907
  checks.push(checkAgents());
3589
3908
  const ok = checks.every((c) => c.status !== "fail");
@@ -3601,6 +3920,9 @@ async function doctor() {
3601
3920
  const authCheck = checkAuth();
3602
3921
  checks.push(authCheck);
3603
3922
  console.log(` ${ICON[authCheck.status]} ${authCheck.name}: ${authCheck.message}`);
3923
+ const contextCheck = checkOperatorContext();
3924
+ checks.push(contextCheck);
3925
+ console.log(` ${ICON[contextCheck.status]} ${contextCheck.name}: ${contextCheck.message}`);
3604
3926
  const configCheck = checkProjectConfig();
3605
3927
  checks.push(configCheck);
3606
3928
  console.log(` ${ICON[configCheck.status]} ${configCheck.name}: ${configCheck.message}`);
@@ -3778,7 +4100,7 @@ Deploying ${config.slug}@${config.version} → ${envLabel}
3778
4100
  compatibilityDate: config.compatibilityDate ?? "2024-09-23"
3779
4101
  }));
3780
4102
  try {
3781
- const res = await fetch(`${API_BASE}/api/v1/developer/apps/${config.slug}/deploy`, {
4103
+ const res = await fetch(`${getApiBase()}/api/v1/developer/apps/${config.slug}/deploy`, {
3782
4104
  method: "POST",
3783
4105
  headers: {
3784
4106
  Authorization: `Bearer ${token}`
@@ -3825,10 +4147,10 @@ Deploying ${config.slug}@${config.version} → ${envLabel}
3825
4147
  process.exit(1);
3826
4148
  }
3827
4149
  }
3828
- function confirm(prompt) {
4150
+ function confirm(prompt2) {
3829
4151
  return new Promise((resolve) => {
3830
4152
  const rl = createInterface({ input: process.stdin, output: process.stdout });
3831
- rl.question(prompt, (answer) => {
4153
+ rl.question(prompt2, (answer) => {
3832
4154
  rl.close();
3833
4155
  resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
3834
4156
  });
@@ -3860,7 +4182,7 @@ async function submit() {
3860
4182
  Submitting ${source_default.bold(config.slug)} for marketplace review...
3861
4183
  `));
3862
4184
  try {
3863
- const res = await fetch(`${API_BASE}/api/v1/developer/apps/${config.slug}/submit`, {
4185
+ const res = await fetch(`${getApiBase()}/api/v1/developer/apps/${config.slug}/submit`, {
3864
4186
  method: "POST",
3865
4187
  headers: {
3866
4188
  Authorization: `Bearer ${token}`,
@@ -3931,6 +4253,7 @@ function requireSlug(slugArg) {
3931
4253
  }
3932
4254
  async function link(opts) {
3933
4255
  let slug = opts.slug;
4256
+ const linkEnv = opts.env === "test" || opts.env === "live" ? opts.env : undefined;
3934
4257
  if (!slug) {
3935
4258
  const config = loadBagdockJson(process.cwd());
3936
4259
  if (config?.slug) {
@@ -3946,9 +4269,7 @@ async function link(opts) {
3946
4269
  }
3947
4270
  status("Fetching your apps...");
3948
4271
  try {
3949
- const res = await fetch(`${API_BASE}/v1/developer/apps`, {
3950
- headers: { Authorization: `Bearer ${token}` }
3951
- });
4272
+ const res = await apiFetch("/api/v1/developer/apps");
3952
4273
  if (!res.ok)
3953
4274
  throw new Error(`API returned ${res.status}`);
3954
4275
  const { data } = await res.json();
@@ -3983,12 +4304,13 @@ Your apps:
3983
4304
  const dir = join6(process.cwd(), LINK_DIR);
3984
4305
  if (!existsSync6(dir))
3985
4306
  mkdirSync3(dir, { recursive: true });
3986
- const linkData = { slug, linkedAt: new Date().toISOString() };
4307
+ const linkData = { slug, environment: linkEnv, linkedAt: new Date().toISOString() };
3987
4308
  writeFileSync4(join6(dir, LINK_FILE), JSON.stringify(linkData, null, 2));
3988
4309
  if (isJsonMode()) {
3989
- outputSuccess({ slug, path: join6(dir, LINK_FILE) });
4310
+ outputSuccess({ slug, environment: linkEnv, path: join6(dir, LINK_FILE) });
3990
4311
  } else {
3991
- console.log(source_default.green(`Linked to ${source_default.bold(slug)}`));
4312
+ const envLabel = linkEnv ? ` [${linkEnv}]` : "";
4313
+ console.log(source_default.green(`Linked to ${source_default.bold(slug)}${envLabel}`));
3992
4314
  console.log(source_default.dim(` Stored in ${LINK_DIR}/${LINK_FILE}`));
3993
4315
  }
3994
4316
  }
@@ -3997,6 +4319,7 @@ var init_link = __esm(() => {
3997
4319
  init_source();
3998
4320
  init_config();
3999
4321
  init_auth();
4322
+ init_api();
4000
4323
  init_output();
4001
4324
  });
4002
4325
 
@@ -4098,22 +4421,11 @@ __export(exports_submission, {
4098
4421
  submissionStatus: () => submissionStatus,
4099
4422
  submissionList: () => submissionList
4100
4423
  });
4101
- function requireAuth() {
4102
- const token = getAuthToken();
4103
- if (!token) {
4104
- outputError("auth_error", "Not authenticated. Run bagdock login or set BAGDOCK_API_KEY.");
4105
- process.exit(1);
4106
- }
4107
- return token;
4108
- }
4109
4424
  async function submissionList(opts) {
4110
- const token = requireAuth();
4111
4425
  const slug = requireSlug(opts.app);
4112
4426
  status(`Fetching submissions for ${slug}...`);
4113
4427
  try {
4114
- const res = await fetch(`${API_BASE}/v1/developer/apps/${slug}/submissions`, {
4115
- headers: { Authorization: `Bearer ${token}` }
4116
- });
4428
+ const res = await apiFetch(`/api/v1/developer/apps/${slug}/submissions`);
4117
4429
  if (res.status === 404) {
4118
4430
  outputError("not_found", `App "${slug}" not found or no submissions exist.`);
4119
4431
  }
@@ -4144,13 +4456,10 @@ Submissions for ${slug}:
4144
4456
  }
4145
4457
  }
4146
4458
  async function submissionStatus(id, opts) {
4147
- const token = requireAuth();
4148
4459
  const slug = requireSlug(opts.app);
4149
4460
  status(`Fetching submission ${id}...`);
4150
4461
  try {
4151
- const res = await fetch(`${API_BASE}/v1/developer/apps/${slug}/submissions/${id}`, {
4152
- headers: { Authorization: `Bearer ${token}` }
4153
- });
4462
+ const res = await apiFetch(`/api/v1/developer/apps/${slug}/submissions/${id}`);
4154
4463
  if (res.status === 404) {
4155
4464
  outputError("not_found", `Submission "${id}" not found.`);
4156
4465
  }
@@ -4184,13 +4493,11 @@ async function submissionStatus(id, opts) {
4184
4493
  }
4185
4494
  }
4186
4495
  async function submissionWithdraw(id, opts) {
4187
- const token = requireAuth();
4188
4496
  const slug = requireSlug(opts.app);
4189
4497
  status(`Withdrawing submission ${id}...`);
4190
4498
  try {
4191
- const res = await fetch(`${API_BASE}/v1/developer/apps/${slug}/submissions/${id}/withdraw`, {
4192
- method: "POST",
4193
- headers: { Authorization: `Bearer ${token}` }
4499
+ const res = await apiFetch(`/api/v1/developer/apps/${slug}/submissions/${id}/withdraw`, {
4500
+ method: "POST"
4194
4501
  });
4195
4502
  if (res.status === 404) {
4196
4503
  outputError("not_found", `App "${slug}" not found.`);
@@ -4215,8 +4522,7 @@ async function submissionWithdraw(id, opts) {
4215
4522
  }
4216
4523
  var init_submission = __esm(() => {
4217
4524
  init_source();
4218
- init_config();
4219
- init_auth();
4525
+ init_api();
4220
4526
  init_output();
4221
4527
  init_link();
4222
4528
  });
@@ -4228,9 +4534,16 @@ __export(exports_open2, {
4228
4534
  });
4229
4535
  async function open2(slugArg) {
4230
4536
  const slug = requireSlug(slugArg);
4231
- const url = `${DASHBOARD_BASE}/developer/apps/${slug}`;
4537
+ const operatorSlug = resolveOperatorSlug();
4538
+ const env2 = resolveEnvironment();
4539
+ if (!operatorSlug) {
4540
+ outputError("no_operator", "No operator context. Run bagdock switch or bagdock login to set one.");
4541
+ return;
4542
+ }
4543
+ const envSegment = env2 === "test" ? "/test" : "";
4544
+ const url = `${getDashboardBase()}/${operatorSlug}${envSegment}/developer/apps/${slug}`;
4232
4545
  if (isJsonMode()) {
4233
- outputSuccess({ url, slug });
4546
+ outputSuccess({ url, slug, operator: operatorSlug, environment: env2 });
4234
4547
  return;
4235
4548
  }
4236
4549
  status(`Opening ${url}`);
@@ -4251,17 +4564,10 @@ __export(exports_inspect, {
4251
4564
  inspect: () => inspect
4252
4565
  });
4253
4566
  async function inspect(slugArg) {
4254
- const token = getAuthToken();
4255
- if (!token) {
4256
- outputError("auth_error", "Not authenticated. Run bagdock login or set BAGDOCK_API_KEY.");
4257
- process.exit(1);
4258
- }
4259
4567
  const slug = requireSlug(slugArg);
4260
4568
  status(`Inspecting ${slug}...`);
4261
4569
  try {
4262
- const res = await fetch(`${API_BASE}/v1/developer/apps/${slug}`, {
4263
- headers: { Authorization: `Bearer ${token}` }
4264
- });
4570
+ const res = await apiFetch(`/api/v1/developer/apps/${slug}`);
4265
4571
  if (res.status === 404)
4266
4572
  outputError("not_found", `App "${slug}" not found.`);
4267
4573
  if (!res.ok)
@@ -4301,8 +4607,7 @@ async function inspect(slugArg) {
4301
4607
  }
4302
4608
  var init_inspect = __esm(() => {
4303
4609
  init_source();
4304
- init_config();
4305
- init_auth();
4610
+ init_api();
4306
4611
  init_output();
4307
4612
  init_link();
4308
4613
  });
@@ -4317,14 +4622,6 @@ __export(exports_env_cmd, {
4317
4622
  });
4318
4623
  import { writeFileSync as writeFileSync5 } from "fs";
4319
4624
  import { resolve } from "path";
4320
- function requireAuth2() {
4321
- const token = getAuthToken();
4322
- if (!token) {
4323
- console.error(source_default.red("Not authenticated. Run"), source_default.cyan("bagdock login"), source_default.red("or set BAGDOCK_API_KEY."));
4324
- process.exit(1);
4325
- }
4326
- return token;
4327
- }
4328
4625
  function requireConfig() {
4329
4626
  const config = loadBagdockJson(process.cwd());
4330
4627
  if (!config) {
@@ -4334,12 +4631,9 @@ function requireConfig() {
4334
4631
  return config;
4335
4632
  }
4336
4633
  async function envList() {
4337
- const token = requireAuth2();
4338
4634
  const config = requireConfig();
4339
4635
  try {
4340
- const res = await fetch(`${API_BASE}/v1/developer/apps/${config.slug}/env`, {
4341
- headers: { Authorization: `Bearer ${token}` }
4342
- });
4636
+ const res = await apiFetch(`/api/v1/developer/apps/${config.slug}/env`);
4343
4637
  if (!res.ok) {
4344
4638
  console.error(source_default.red(`Failed to list env vars (${res.status})`));
4345
4639
  process.exit(1);
@@ -4363,17 +4657,9 @@ Environment variables for ${config.slug}:
4363
4657
  }
4364
4658
  }
4365
4659
  async function envSet(key, value) {
4366
- const token = requireAuth2();
4367
4660
  const config = requireConfig();
4368
4661
  try {
4369
- const res = await fetch(`${API_BASE}/v1/developer/apps/${config.slug}/env`, {
4370
- method: "PUT",
4371
- headers: {
4372
- "Content-Type": "application/json",
4373
- Authorization: `Bearer ${token}`
4374
- },
4375
- body: JSON.stringify({ key, value })
4376
- });
4662
+ const res = await apiFetchJson(`/api/v1/developer/apps/${config.slug}/env`, "PUT", { key, value });
4377
4663
  if (!res.ok) {
4378
4664
  const body = await res.text();
4379
4665
  console.error(source_default.red(`Failed to set ${key} (${res.status}):`), body.slice(0, 200));
@@ -4386,13 +4672,9 @@ async function envSet(key, value) {
4386
4672
  }
4387
4673
  }
4388
4674
  async function envRemove(key) {
4389
- const token = requireAuth2();
4390
4675
  const config = requireConfig();
4391
4676
  try {
4392
- const res = await fetch(`${API_BASE}/v1/developer/apps/${config.slug}/env/${key}`, {
4393
- method: "DELETE",
4394
- headers: { Authorization: `Bearer ${token}` }
4395
- });
4677
+ const res = await apiFetch(`/api/v1/developer/apps/${config.slug}/env/${key}`, { method: "DELETE" });
4396
4678
  if (!res.ok) {
4397
4679
  console.error(source_default.red(`Failed to remove ${key} (${res.status})`));
4398
4680
  process.exit(1);
@@ -4404,14 +4686,11 @@ async function envRemove(key) {
4404
4686
  }
4405
4687
  }
4406
4688
  async function envPull(file) {
4407
- const token = requireAuth2();
4408
4689
  const slug = requireSlug();
4409
4690
  const target = resolve(file ?? ".env.local");
4410
4691
  status(`Pulling env vars for ${slug}...`);
4411
4692
  try {
4412
- const res = await fetch(`${API_BASE}/v1/developer/apps/${slug}/env`, {
4413
- headers: { Authorization: `Bearer ${token}` }
4414
- });
4693
+ const res = await apiFetch(`/api/v1/developer/apps/${slug}/env`);
4415
4694
  if (!res.ok) {
4416
4695
  outputError("api_error", `Failed to pull env vars (${res.status})`);
4417
4696
  process.exit(1);
@@ -4446,7 +4725,7 @@ async function envPull(file) {
4446
4725
  var init_env_cmd = __esm(() => {
4447
4726
  init_source();
4448
4727
  init_config();
4449
- init_auth();
4728
+ init_api();
4450
4729
  init_output();
4451
4730
  init_link();
4452
4731
  });
@@ -4458,23 +4737,9 @@ __export(exports_keys, {
4458
4737
  keysDelete: () => keysDelete,
4459
4738
  keysCreate: () => keysCreate
4460
4739
  });
4461
- async function apiRequest(method, path2, body) {
4462
- const token = getAuthToken();
4463
- if (!token) {
4464
- outputError("UNAUTHENTICATED", "Not logged in. Run `bagdock login` or set BAGDOCK_API_KEY.");
4465
- }
4466
- const headers = { Authorization: `Bearer ${token}` };
4467
- const init2 = { method, headers };
4468
- if (body) {
4469
- headers["Content-Type"] = "application/json";
4470
- init2.body = JSON.stringify(body);
4471
- }
4472
- const res = await fetch(`${API_BASE}${path2}`, init2);
4473
- return res;
4474
- }
4475
4740
  async function keysCreate(opts) {
4476
4741
  status("Creating API key...");
4477
- const res = await apiRequest("POST", "/api/v1/operator/api-keys", {
4742
+ const res = await apiFetchJson("/api/v1/operator/api-keys", "POST", {
4478
4743
  name: opts.name,
4479
4744
  key_type: opts.type || "secret",
4480
4745
  key_category: opts.category || "standard",
@@ -4511,7 +4776,7 @@ async function keysList(opts) {
4511
4776
  let path2 = "/api/v1/operator/api-keys";
4512
4777
  if (opts.environment)
4513
4778
  path2 += `?environment=${opts.environment}`;
4514
- const res = await apiRequest("GET", path2);
4779
+ const res = await apiFetch(path2);
4515
4780
  if (!res.ok) {
4516
4781
  const err = await res.json().catch(() => ({ error: { message: res.statusText } }));
4517
4782
  outputError(err.error?.code || "API_ERROR", err.error?.message || `HTTP ${res.status}`);
@@ -4541,7 +4806,7 @@ async function keysDelete(id, opts) {
4541
4806
  outputError("CONFIRMATION_REQUIRED", "Pass --yes to confirm deletion in non-interactive mode.");
4542
4807
  }
4543
4808
  status(`Revoking API key ${id}...`);
4544
- const res = await apiRequest("DELETE", `/api/v1/operator/api-keys/${id}`, {
4809
+ const res = await apiFetchJson(`/api/v1/operator/api-keys/${id}`, "DELETE", {
4545
4810
  reason: opts.reason
4546
4811
  });
4547
4812
  if (!res.ok) {
@@ -4558,8 +4823,7 @@ async function keysDelete(id, opts) {
4558
4823
  }
4559
4824
  var init_keys = __esm(() => {
4560
4825
  init_source();
4561
- init_config();
4562
- init_auth();
4826
+ init_api();
4563
4827
  init_output();
4564
4828
  });
4565
4829
 
@@ -4569,19 +4833,9 @@ __export(exports_apps, {
4569
4833
  appsList: () => appsList,
4570
4834
  appsGet: () => appsGet
4571
4835
  });
4572
- async function apiRequest2(method, path2) {
4573
- const token = getAuthToken();
4574
- if (!token) {
4575
- outputError("UNAUTHENTICATED", "Not logged in. Run `bagdock login` or set BAGDOCK_API_KEY.");
4576
- }
4577
- return fetch(`${API_BASE}${path2}`, {
4578
- method,
4579
- headers: { Authorization: `Bearer ${token}` }
4580
- });
4581
- }
4582
4836
  async function appsList() {
4583
4837
  status("Fetching apps...");
4584
- const res = await apiRequest2("GET", "/api/v1/developer/apps");
4838
+ const res = await apiFetch("/api/v1/developer/apps");
4585
4839
  if (!res.ok) {
4586
4840
  const err = await res.json().catch(() => ({ error: { message: res.statusText } }));
4587
4841
  outputError(err.error?.code || "API_ERROR", err.error?.message || `HTTP ${res.status}`);
@@ -4611,7 +4865,7 @@ async function appsList() {
4611
4865
  }
4612
4866
  async function appsGet(slug) {
4613
4867
  status(`Fetching app ${slug}...`);
4614
- const res = await apiRequest2("GET", `/api/v1/developer/apps/${slug}`);
4868
+ const res = await apiFetch(`/api/v1/developer/apps/${slug}`);
4615
4869
  if (!res.ok) {
4616
4870
  if (res.status === 404) {
4617
4871
  outputError("NOT_FOUND", `App "${slug}" not found`);
@@ -4645,8 +4899,7 @@ async function appsGet(slug) {
4645
4899
  }
4646
4900
  var init_apps = __esm(() => {
4647
4901
  init_source();
4648
- init_config();
4649
- init_auth();
4902
+ init_api();
4650
4903
  init_output();
4651
4904
  });
4652
4905
 
@@ -4657,16 +4910,6 @@ __export(exports_logs, {
4657
4910
  logsList: () => logsList,
4658
4911
  logsGet: () => logsGet
4659
4912
  });
4660
- async function apiRequest3(method, path2) {
4661
- const token = getAuthToken();
4662
- if (!token) {
4663
- outputError("UNAUTHENTICATED", "Not logged in. Run `bagdock login` or set BAGDOCK_API_KEY.");
4664
- }
4665
- return fetch(`${API_BASE}${path2}`, {
4666
- method,
4667
- headers: { Authorization: `Bearer ${token}` }
4668
- });
4669
- }
4670
4913
  function resolveSlug2(slug) {
4671
4914
  if (slug)
4672
4915
  return slug;
@@ -4680,7 +4923,7 @@ async function logsList(opts) {
4680
4923
  const slug = resolveSlug2(opts.app);
4681
4924
  const limit = opts.limit || "50";
4682
4925
  status(`Fetching logs for ${slug}...`);
4683
- const res = await apiRequest3("GET", `/api/v1/developer/apps/${slug}/logs?limit=${limit}`);
4926
+ const res = await apiFetch(`/api/v1/developer/apps/${slug}/logs?limit=${limit}`);
4684
4927
  if (!res.ok) {
4685
4928
  if (res.status === 404) {
4686
4929
  outputError("NOT_FOUND", `App "${slug}" not found or no logs available`);
@@ -4710,7 +4953,7 @@ async function logsList(opts) {
4710
4953
  async function logsGet(id, opts) {
4711
4954
  const slug = resolveSlug2(opts.app);
4712
4955
  status(`Fetching log entry ${id}...`);
4713
- const res = await apiRequest3("GET", `/api/v1/developer/apps/${slug}/logs/${id}`);
4956
+ const res = await apiFetch(`/api/v1/developer/apps/${slug}/logs/${id}`);
4714
4957
  if (!res.ok) {
4715
4958
  const err = await res.json().catch(() => ({ error: { message: res.statusText } }));
4716
4959
  outputError(err.error?.code || "API_ERROR", err.error?.message || `HTTP ${res.status}`);
@@ -4733,7 +4976,7 @@ async function logsTail(opts) {
4733
4976
  let lastTimestamp = new Date().toISOString();
4734
4977
  const poll = async () => {
4735
4978
  try {
4736
- const res = await apiRequest3("GET", `/api/v1/developer/apps/${slug}/logs?since=${encodeURIComponent(lastTimestamp)}&limit=100`);
4979
+ const res = await apiFetch(`/api/v1/developer/apps/${slug}/logs?since=${encodeURIComponent(lastTimestamp)}&limit=100`);
4737
4980
  if (res.ok) {
4738
4981
  const result = await res.json();
4739
4982
  for (const entry of result.data || []) {
@@ -4759,7 +5002,7 @@ async function logsTail(opts) {
4759
5002
  var init_logs = __esm(() => {
4760
5003
  init_source();
4761
5004
  init_config();
4762
- init_auth();
5005
+ init_api();
4763
5006
  init_output();
4764
5007
  });
4765
5008
 
@@ -5033,13 +5276,22 @@ function toPascalCase(s) {
5033
5276
  init_output();
5034
5277
  init_config();
5035
5278
  var program2 = new Command;
5036
- program2.name("bagdock").description("Bagdock developer CLI — built for humans, AI agents, and CI/CD pipelines").version("0.4.0").option("--json", "Force JSON output (auto-enabled in non-TTY)").option("-q, --quiet", "Suppress status messages (implies --json)").option("--api-key <key>", "API key to use for this invocation").option("-p, --profile <name>", "Profile to use (overrides BAGDOCK_PROFILE)").hook("preAction", (_thisCommand, actionCommand) => {
5279
+ program2.name("bagdock").description("Bagdock developer CLI — built for humans, AI agents, and CI/CD pipelines").version("0.6.0").option("--json", "Force JSON output (auto-enabled in non-TTY)").option("-q, --quiet", "Suppress status messages (implies --json)").option("--api-key <key>", "API key to use for this invocation").option("-p, --profile <name>", "Profile to use (overrides BAGDOCK_PROFILE)").option("--env <environment>", "Override environment for this invocation (live, test)").option("--ngrok", "Use API URLs from .env.local (for ngrok tunnels)").hook("preAction", (_thisCommand, actionCommand) => {
5037
5280
  const opts = program2.opts();
5281
+ if (opts.ngrok)
5282
+ loadLocalEnv();
5038
5283
  setOutputMode({ json: opts.json, quiet: opts.quiet });
5039
5284
  if (opts.apiKey)
5040
5285
  setApiKeyOverride(opts.apiKey);
5041
5286
  if (opts.profile)
5042
5287
  setProfileOverride(opts.profile);
5288
+ if (opts.env) {
5289
+ if (opts.env !== "live" && opts.env !== "test") {
5290
+ console.error('Error: --env must be "live" or "test"');
5291
+ process.exit(1);
5292
+ }
5293
+ setEnvironmentOverride(opts.env);
5294
+ }
5043
5295
  });
5044
5296
  program2.command("login").description("Authenticate with Bagdock (opens browser)").action(login);
5045
5297
  program2.command("logout").description("Clear stored credentials").action(logout);
@@ -5047,6 +5299,10 @@ program2.command("whoami").description("Show current authenticated user").action
5047
5299
  var authCmd = program2.command("auth").description("Manage authentication profiles");
5048
5300
  authCmd.command("list").description("List all stored profiles").action(authList);
5049
5301
  authCmd.command("switch [name]").description("Switch active profile").action(async (name) => authSwitch(name));
5302
+ program2.command("switch").description("Switch operator and environment context (live/test)").option("--operator <slug>", "Operator slug (required in non-interactive mode)").option("--env <environment>", "Environment: live or test").action(async (opts) => {
5303
+ const { switchContext: switchContext2 } = await Promise.resolve().then(() => (init_switch(), exports_switch));
5304
+ await switchContext2(opts);
5305
+ });
5050
5306
  program2.command("doctor").description("Run environment diagnostics").action(async () => {
5051
5307
  const { doctor: doctor2 } = await Promise.resolve().then(() => (init_doctor(), exports_doctor));
5052
5308
  await doctor2();
@@ -5056,7 +5312,7 @@ program2.command("dev").description("Start local dev server").option("-p, --port
5056
5312
  const { dev: dev2 } = await Promise.resolve().then(() => (init_dev(), exports_dev));
5057
5313
  await dev2(opts);
5058
5314
  });
5059
- program2.command("deploy").description("Build locally and deploy via Bagdock API").option("--env <environment>", "Target environment (preview, staging, production)", "staging").option("--preview", "Deploy an ephemeral preview ({slug}-{hash}.pre.bdok.dev)").option("--production", "Deploy to production ({slug}.bdok.dev)").option("-y, --yes", "Skip confirmation prompts").action(async (opts) => {
5315
+ program2.command("deploy").description("Build locally and deploy via Bagdock API").option("--target <target>", "Deploy target (preview, staging, production)", "staging").option("--preview", "Deploy an ephemeral preview ({slug}-{hash}.pre.bdok.dev)").option("--production", "Deploy to production ({slug}.bdok.dev)").option("-y, --yes", "Skip confirmation prompts").action(async (opts) => {
5060
5316
  const { deploy: deploy2 } = await Promise.resolve().then(() => (init_deploy(), exports_deploy));
5061
5317
  await deploy2(opts);
5062
5318
  });
@@ -5089,7 +5345,7 @@ program2.command("inspect [slug]").description("Show deployment details and stat
5089
5345
  const { inspect: inspect2 } = await Promise.resolve().then(() => (init_inspect(), exports_inspect));
5090
5346
  await inspect2(slug);
5091
5347
  });
5092
- program2.command("link").description("Link current directory to a Bagdock app or edge").option("--slug <slug>", "Project slug (required in non-interactive mode)").action(async (opts) => {
5348
+ program2.command("link").description("Link current directory to a Bagdock app or edge").option("--slug <slug>", "App slug (required in non-interactive mode)").option("--env <environment>", "Default environment for this link (live, test)").action(async (opts) => {
5093
5349
  const { link: link2 } = await Promise.resolve().then(() => (init_link(), exports_link));
5094
5350
  await link2(opts);
5095
5351
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bagdock/cli",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Bagdock developer CLI — build, test, and deploy apps and edges on the Bagdock platform",
5
5
  "keywords": [
6
6
  "bagdock",
@@ -53,7 +53,7 @@
53
53
  "open": "^10.1.0"
54
54
  },
55
55
  "devDependencies": {
56
- "@types/bun": "latest",
56
+ "@types/bun": "^1.3.10",
57
57
  "typescript": "^5.3.3",
58
58
  "vitest": "^3.2.4"
59
59
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "skill": "bagdock-cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "evals": [
5
5
  {
6
6
  "id": "login-flow",
@@ -203,6 +203,48 @@
203
203
  "input": "Link this directory to my smart-entry adapter",
204
204
  "expected_commands": ["bagdock link --slug smart-entry"],
205
205
  "expected_behavior": "Creates .bagdock/link.json with slug, other commands use it as fallback"
206
+ },
207
+ {
208
+ "id": "switch-operator-sandbox",
209
+ "description": "Agent should switch to sandbox environment for testing",
210
+ "input": "Switch to test mode for the WiseStorage operator",
211
+ "expected_commands": ["bagdock switch --operator wisestorage --env test"],
212
+ "expected_behavior": "Updates active profile with operatorSlug=wisestorage and environment=test"
213
+ },
214
+ {
215
+ "id": "switch-interactive",
216
+ "description": "Agent should use interactive switch when operator unknown",
217
+ "input": "I want to switch to a different operator",
218
+ "expected_commands": ["bagdock switch"],
219
+ "expected_behavior": "Lists available operators, prompts selection, then prompts environment choice"
220
+ },
221
+ {
222
+ "id": "deploy-to-test",
223
+ "description": "Agent should deploy to sandbox environment",
224
+ "input": "Deploy my adapter to the sandbox for testing",
225
+ "expected_commands": ["bagdock --env test deploy --target staging --yes"],
226
+ "expected_behavior": "Deploys to staging target within the test/sandbox environment"
227
+ },
228
+ {
229
+ "id": "inspect-test-env",
230
+ "description": "Agent should inspect an app in test environment",
231
+ "input": "Show me the deployment details of my app in the sandbox",
232
+ "expected_commands": ["bagdock --env test inspect --json"],
233
+ "expected_behavior": "Returns app details from the sandbox tenant database"
234
+ },
235
+ {
236
+ "id": "link-with-env",
237
+ "description": "Agent should link directory with specific environment",
238
+ "input": "Link this directory to smart-entry in test mode",
239
+ "expected_commands": ["bagdock link --slug smart-entry --env test"],
240
+ "expected_behavior": "Creates .bagdock/link.json with slug and environment=test"
241
+ },
242
+ {
243
+ "id": "env-override-per-command",
244
+ "description": "Agent should use --env flag to override environment for a single command",
245
+ "input": "List apps on the live environment even though I'm currently in test mode",
246
+ "expected_commands": ["bagdock --env live apps list --json"],
247
+ "expected_behavior": "Lists apps from the live tenant database, does not change profile"
206
248
  }
207
249
  ]
208
250
  }
@@ -45,9 +45,19 @@ For CI/CD, set `BAGDOCK_API_KEY` in your environment. For interactive use, run `
45
45
  | `-q, --quiet` | Suppress status messages (implies `--json`) |
46
46
  | `--api-key <key>` | Override auth for this invocation |
47
47
  | `-p, --profile <name>` | Use a named profile (overrides `BAGDOCK_PROFILE`) |
48
+ | `--env <live\|test>` | Override environment for this invocation |
48
49
  | `-V, --version` | Print version |
49
50
  | `-h, --help` | Print help |
50
51
 
52
+ ## Environment Context
53
+
54
+ All API calls include `X-Environment` and `X-Operator-Slug` headers. Resolution:
55
+
56
+ - **Environment**: `--env` flag > `.bagdock/link.json` > profile > `BAGDOCK_ENV` > `live`
57
+ - **Operator**: `BAGDOCK_OPERATOR` env var > profile stored value
58
+
59
+ Use `bagdock switch` to select operator and environment interactively, or pass `--env test` to any command.
60
+
51
61
  ## Available Commands
52
62
 
53
63
  | Command | Description |
@@ -73,8 +83,9 @@ For CI/CD, set `BAGDOCK_API_KEY` in your environment. For interactive use, run `
73
83
  | `open [slug]` | Open project in Bagdock dashboard |
74
84
  | `inspect [slug]` | Show deployment details and status |
75
85
  | `link` | Link directory to a Bagdock app or edge |
86
+ | `switch` | Switch operator and environment context (live/test) |
76
87
  | `doctor` | Run environment diagnostics (version, auth, config, agents) |
77
- | `auth list` | List stored profiles |
88
+ | `auth list` | List stored profiles (with operator + env) |
78
89
  | `auth switch [name]` | Switch active profile |
79
90
  | `apps list` | List deployed applications |
80
91
  | `apps get <slug>` | Show details for an application |