playcademy 0.7.0 → 0.9.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/dist/index.js CHANGED
@@ -2513,6 +2513,16 @@ var CONFIG_FILE_NAMES = [
2513
2513
  "playcademy.config.mjs"
2514
2514
  ];
2515
2515
 
2516
+ // src/constants/urls.ts
2517
+ var PLAYCADEMY_BASE_URLS = {
2518
+ staging: "https://hub.dev.playcademy.net",
2519
+ production: "https://hub.playcademy.com"
2520
+ };
2521
+ var GAME_BACKEND_DOMAINS = {
2522
+ staging: "staging.playcademy.gg",
2523
+ production: "playcademy.gg"
2524
+ };
2525
+
2516
2526
  // src/lib/auth/storage.ts
2517
2527
  import { access, mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
2518
2528
  import { homedir } from "node:os";
@@ -2521,7 +2531,7 @@ import { join } from "node:path";
2521
2531
  // src/lib/core/context.ts
2522
2532
  var context = {};
2523
2533
  function setCliContext(ctx) {
2524
- context = ctx;
2534
+ context = { ...context, ...ctx };
2525
2535
  }
2526
2536
  function getCliContext() {
2527
2537
  return context;
@@ -2531,26 +2541,47 @@ function getWorkspace() {
2531
2541
  }
2532
2542
 
2533
2543
  // src/lib/core/config.ts
2544
+ function normalizeEnvironment(env) {
2545
+ if (!env) return null;
2546
+ const normalized = env.toLowerCase().trim();
2547
+ if (normalized === "staging" || normalized === "stage") {
2548
+ return "staging";
2549
+ }
2550
+ if (normalized === "production" || normalized === "prod") {
2551
+ return "production";
2552
+ }
2553
+ return null;
2554
+ }
2555
+ function ensureEnvironment(env, logger2) {
2556
+ if (env) {
2557
+ const normalized = normalizeEnvironment(env);
2558
+ if (!normalized) {
2559
+ logger2.newLine();
2560
+ logger2.error(`Invalid environment: "${env}"`);
2561
+ logger2.newLine();
2562
+ logger2.info("Valid environments: staging, production");
2563
+ logger2.newLine();
2564
+ process.exit(1);
2565
+ }
2566
+ setCliContext({ environment: normalized });
2567
+ }
2568
+ return getEnvironment();
2569
+ }
2534
2570
  function getEnvironment() {
2535
2571
  const context2 = getCliContext();
2536
2572
  if (context2.environment) {
2537
2573
  return context2.environment;
2538
2574
  }
2539
2575
  const env = process.env.PLAYCADEMY_ENV?.toLowerCase();
2540
- if (env === "staging") {
2541
- return "staging";
2576
+ if (env === "staging" || env === "production") {
2577
+ return env;
2542
2578
  }
2543
- return "production";
2579
+ return "staging";
2544
2580
  }
2545
2581
  function getBaseUrl() {
2546
2582
  const explicitUrl = process.env.PLAYCADEMY_BASE_URL;
2547
- if (explicitUrl) {
2548
- return explicitUrl;
2549
- }
2550
- if (getEnvironment() === "staging") {
2551
- return "https://hub.dev.playcademy.net";
2552
- }
2553
- return "https://hub.playcademy.com";
2583
+ if (explicitUrl) return explicitUrl;
2584
+ return PLAYCADEMY_BASE_URLS[getEnvironment()];
2554
2585
  }
2555
2586
  function getApiUrl(path) {
2556
2587
  const baseUrl = getBaseUrl();
@@ -2573,6 +2604,12 @@ async function ensureAuthDir() {
2573
2604
  const authDir = join(homedir(), ".playcademy");
2574
2605
  await mkdir(authDir, { recursive: true });
2575
2606
  }
2607
+ function createEmptyEnvironmentProfiles() {
2608
+ return {
2609
+ default: null,
2610
+ profiles: {}
2611
+ };
2612
+ }
2576
2613
  async function loadAuthStore() {
2577
2614
  try {
2578
2615
  await ensureAuthDir();
@@ -2583,8 +2620,8 @@ async function loadAuthStore() {
2583
2620
  } catch {
2584
2621
  }
2585
2622
  return {
2586
- default: null,
2587
- profiles: {}
2623
+ staging: createEmptyEnvironmentProfiles(),
2624
+ production: createEmptyEnvironmentProfiles()
2588
2625
  };
2589
2626
  }
2590
2627
  async function saveAuthStore(store) {
@@ -2601,45 +2638,79 @@ async function getCurrentProfile() {
2601
2638
  };
2602
2639
  }
2603
2640
  const store = await loadAuthStore();
2641
+ const environment = getEnvironment();
2604
2642
  const profileName = getProfileName();
2643
+ const envProfiles = store[environment];
2644
+ if (!envProfiles) {
2645
+ return null;
2646
+ }
2605
2647
  if (profileName === "default") {
2606
- return store.default;
2648
+ return envProfiles.default;
2607
2649
  }
2608
- return store.profiles[profileName] || null;
2650
+ return envProfiles.profiles[profileName] || null;
2609
2651
  }
2610
- async function saveProfile(profileName, profile) {
2652
+ async function saveProfile(environment, profileName, profile) {
2611
2653
  const store = await loadAuthStore();
2654
+ if (!store[environment]) {
2655
+ store[environment] = createEmptyEnvironmentProfiles();
2656
+ }
2657
+ const envProfiles = store[environment];
2612
2658
  if (profileName === "default") {
2613
- store.default = profile;
2659
+ envProfiles.default = profile;
2614
2660
  } else {
2615
- store.profiles[profileName] = profile;
2661
+ envProfiles.profiles[profileName] = profile;
2616
2662
  }
2617
2663
  await saveAuthStore(store);
2618
2664
  }
2619
- async function removeProfile(profileName) {
2665
+ async function removeProfile(environment, profileName) {
2620
2666
  const store = await loadAuthStore();
2667
+ if (!store[environment]) {
2668
+ return;
2669
+ }
2670
+ const envProfiles = store[environment];
2621
2671
  if (profileName === "default") {
2622
- store.default = null;
2672
+ envProfiles.default = null;
2623
2673
  } else {
2624
- delete store.profiles[profileName];
2674
+ delete envProfiles.profiles[profileName];
2625
2675
  }
2626
2676
  await saveAuthStore(store);
2627
2677
  }
2628
- async function getProfile(profileName) {
2678
+ async function getProfile(environment, profileName) {
2629
2679
  const store = await loadAuthStore();
2680
+ const envProfiles = store[environment];
2681
+ if (!envProfiles) {
2682
+ return null;
2683
+ }
2630
2684
  if (profileName === "default") {
2631
- return store.default;
2685
+ return envProfiles.default;
2632
2686
  }
2633
- return store.profiles[profileName] || null;
2687
+ return envProfiles.profiles[profileName] || null;
2634
2688
  }
2635
2689
  async function listProfiles() {
2636
2690
  const store = await loadAuthStore();
2637
- const profiles = [];
2638
- if (store.default) {
2639
- profiles.push("default");
2640
- }
2641
- profiles.push(...Object.keys(store.profiles));
2642
- return profiles;
2691
+ const profileMap = /* @__PURE__ */ new Map();
2692
+ const stagingProfiles = [];
2693
+ if (store.staging.default) {
2694
+ stagingProfiles.push("default");
2695
+ }
2696
+ stagingProfiles.push(...Object.keys(store.staging.profiles));
2697
+ profileMap.set("staging", stagingProfiles);
2698
+ const productionProfiles = [];
2699
+ if (store.production.default) {
2700
+ productionProfiles.push("default");
2701
+ }
2702
+ productionProfiles.push(...Object.keys(store.production.profiles));
2703
+ profileMap.set("production", productionProfiles);
2704
+ return profileMap;
2705
+ }
2706
+ async function getAuthenticatedEnvironments() {
2707
+ const store = await loadAuthStore();
2708
+ const environments = [];
2709
+ const hasStaging = store.staging.default !== null || Object.keys(store.staging.profiles).length > 0;
2710
+ const hasProduction = store.production.default !== null || Object.keys(store.production.profiles).length > 0;
2711
+ if (hasStaging) environments.push("staging");
2712
+ if (hasProduction) environments.push("production");
2713
+ return environments;
2643
2714
  }
2644
2715
 
2645
2716
  // src/lib/auth/http-server/server.ts
@@ -7260,15 +7331,7 @@ function createDevNamespace(client) {
7260
7331
  throw new Error(`File upload failed: ${uploadResponse.status} ${uploadResponse.statusText}`);
7261
7332
  }
7262
7333
  }
7263
- const baseUrl = (() => {
7264
- const anyClient = client;
7265
- try {
7266
- return typeof anyClient.getBaseUrl === "function" ? anyClient.getBaseUrl() : "/api";
7267
- } catch {
7268
- return "/api";
7269
- }
7270
- })();
7271
- const finalizeUrl = baseUrl.replace(/\/$/, "") + "/games/uploads/finalize/";
7334
+ const finalizeUrl = `${client.baseUrl}/games/uploads/finalize/`;
7272
7335
  const authToken = client.getToken();
7273
7336
  const tokenType = client.getTokenType();
7274
7337
  const headers = {
@@ -8746,13 +8809,13 @@ var init_achievements = () => {
8746
8809
  function createTimebackNamespace(client) {
8747
8810
  return {
8748
8811
  recordProgress: (progressData) => {
8749
- return client["request"]("/api/integrations/timeback/progress", "POST", { progressData }, void 0, true);
8812
+ return client["requestGameBackend"]("/api/integrations/timeback/progress", "POST", { progressData });
8750
8813
  },
8751
8814
  recordSessionEnd: (sessionData) => {
8752
- return client["request"]("/api/integrations/timeback/session-end", "POST", { sessionData }, void 0, true);
8815
+ return client["requestGameBackend"]("/api/integrations/timeback/session-end", "POST", { sessionData });
8753
8816
  },
8754
8817
  awardXP: (xpAmount, metadata) => {
8755
- return client["request"]("/api/integrations/timeback/award-xp", "POST", { xpAmount, metadata }, void 0, true);
8818
+ return client["requestGameBackend"]("/api/integrations/timeback/award-xp", "POST", { xpAmount, metadata });
8756
8819
  },
8757
8820
  management: {
8758
8821
  setup: (request2) => {
@@ -9061,7 +9124,7 @@ var init_client = __esm2(() => {
9061
9124
  authContext;
9062
9125
  initPayload;
9063
9126
  constructor(config) {
9064
- this.baseUrl = config?.baseUrl || "";
9127
+ this.baseUrl = config?.baseUrl?.endsWith("/api") ? config.baseUrl : `${config?.baseUrl}/api`;
9065
9128
  this.gameUrl = config?.gameUrl;
9066
9129
  this.gameId = config?.gameId;
9067
9130
  this.config = config || {};
@@ -9077,8 +9140,9 @@ var init_client = __esm2(() => {
9077
9140
  return `${effectiveBaseUrl}/api`;
9078
9141
  }
9079
9142
  getGameBackendUrl() {
9080
- if (!this.gameUrl)
9081
- return this.getBaseUrl();
9143
+ if (!this.gameUrl) {
9144
+ throw new PlaycademyError("Game backend URL not configured. gameUrl must be set to use game backend features.");
9145
+ }
9082
9146
  const isRelative = this.gameUrl.startsWith("/");
9083
9147
  const isBrowser2 = typeof window !== "undefined";
9084
9148
  const effectiveGameUrl = isRelative && isBrowser2 ? `${window.location.origin}${this.gameUrl}` : this.gameUrl;
@@ -9115,17 +9179,29 @@ var init_client = __esm2(() => {
9115
9179
  listener(payload);
9116
9180
  });
9117
9181
  }
9118
- async request(path, method, body, headers, useGameBackend = false) {
9182
+ async request(path, method, body, headers) {
9183
+ const effectiveHeaders = {
9184
+ ...headers,
9185
+ ...this.authStrategy.getHeaders()
9186
+ };
9187
+ return request({
9188
+ path,
9189
+ method,
9190
+ body,
9191
+ baseUrl: this.baseUrl,
9192
+ extraHeaders: effectiveHeaders
9193
+ });
9194
+ }
9195
+ async requestGameBackend(path, method, body, headers) {
9119
9196
  const effectiveHeaders = {
9120
9197
  ...headers,
9121
9198
  ...this.authStrategy.getHeaders()
9122
9199
  };
9123
- const targetBaseUrl = useGameBackend ? this.getGameBackendUrl() : this.baseUrl;
9124
9200
  return request({
9125
9201
  path,
9126
9202
  method,
9127
9203
  body,
9128
- baseUrl: targetBaseUrl,
9204
+ baseUrl: this.getGameBackendUrl(),
9129
9205
  extraHeaders: effectiveHeaders
9130
9206
  });
9131
9207
  }
@@ -9204,9 +9280,40 @@ function customTransform(text2) {
9204
9280
  const highlightCode = (text3) => text3.replace(/`([^`]+)`/g, (_, code) => greenBright(code));
9205
9281
  return highlightCode(text2);
9206
9282
  }
9283
+ function formatTable(data, title) {
9284
+ if (data.length === 0) return;
9285
+ const keys = Object.keys(data[0]);
9286
+ const rows = data.map((item) => keys.map((key) => String(item[key] ?? "")));
9287
+ const widths = keys.map((key, i) => {
9288
+ const headerWidth = key.length;
9289
+ const dataWidth = Math.max(...rows.map((row) => row[i].length));
9290
+ return Math.max(headerWidth, dataWidth);
9291
+ });
9292
+ const totalWidth = widths.reduce((sum, w) => sum + w + 3, -1);
9293
+ const separator = "\u251C" + widths.map((w) => "\u2500".repeat(w + 2)).join("\u253C") + "\u2524";
9294
+ const topBorder = "\u250C" + "\u2500".repeat(totalWidth) + "\u2510";
9295
+ const titleSeparator = "\u251C" + widths.map((w) => "\u2500".repeat(w + 2)).join("\u252C") + "\u2524";
9296
+ const bottomBorder = "\u2514" + widths.map((w) => "\u2500".repeat(w + 2)).join("\u2534") + "\u2518";
9297
+ console.log(topBorder);
9298
+ if (title) {
9299
+ const titleText = bold(title);
9300
+ const titlePadding = totalWidth - title.length - 1;
9301
+ const titleRow = "\u2502 " + titleText + " ".repeat(titlePadding) + "\u2502";
9302
+ console.log(titleRow);
9303
+ console.log(titleSeparator);
9304
+ }
9305
+ const header = "\u2502 " + keys.map((key, i) => key.padEnd(widths[i])).join(" \u2502 ") + " \u2502";
9306
+ console.log(header);
9307
+ console.log(separator);
9308
+ rows.forEach((row) => {
9309
+ const dataRow = "\u2502 " + row.map((cell, i) => cell.padEnd(widths[i])).join(" \u2502 ") + " \u2502";
9310
+ console.log(dataRow);
9311
+ });
9312
+ console.log(bottomBorder);
9313
+ }
9207
9314
  var logger = {
9208
- table: (data) => {
9209
- console.table(data);
9315
+ table: (data, title) => {
9316
+ formatTable(data, title);
9210
9317
  },
9211
9318
  /**
9212
9319
  * Info message - general information
@@ -9362,9 +9469,8 @@ var logger = {
9362
9469
  async function createClient() {
9363
9470
  const profile = await getCurrentProfile();
9364
9471
  const baseUrl = getBaseUrl();
9365
- const apiBaseUrl = `${baseUrl}/api`;
9366
9472
  const client = new PlaycademyClient({
9367
- baseUrl: apiBaseUrl,
9473
+ baseUrl,
9368
9474
  token: profile?.token
9369
9475
  });
9370
9476
  if (profile?.token && profile?.tokenType) {
@@ -9374,11 +9480,12 @@ async function createClient() {
9374
9480
  }
9375
9481
  async function requireAuthenticatedClient() {
9376
9482
  const profile = await getCurrentProfile();
9483
+ const environment = getEnvironment();
9377
9484
  if (!profile) {
9378
9485
  logger.newLine();
9379
9486
  logger.admonition("warning", "Login Required", [
9380
- "You need to be logged in to run this command.",
9381
- "Run `playcademy login` to authenticate."
9487
+ `You are not logged into ${environment}.`,
9488
+ environment === "production" ? `Run \`playcademy login --env ${environment}\` to authenticate.` : "Run `playcademy login` to authenticate."
9382
9489
  ]);
9383
9490
  logger.newLine();
9384
9491
  process.exit(1);
@@ -10238,16 +10345,25 @@ async function validateDeployConfig(config) {
10238
10345
  // src/lib/deploy/interaction.ts
10239
10346
  async function selectEnvironment(options) {
10240
10347
  let environment = options.env;
10241
- const shouldPrompt = !environment && !options.dryRun && !process.env.PLAYCADEMY_ENV && !process.env.PLAYCADEMY_BASE_URL;
10242
- if (shouldPrompt) {
10348
+ if (environment || process.env.PLAYCADEMY_ENV || process.env.PLAYCADEMY_BASE_URL) {
10349
+ if (environment) {
10350
+ setCliContext({ environment });
10351
+ }
10352
+ return;
10353
+ }
10354
+ const authenticatedEnvs = await getAuthenticatedEnvironments();
10355
+ if (authenticatedEnvs.length === 1) {
10356
+ environment = authenticatedEnvs[0];
10357
+ logger.info(`Deploying to ${environment}`, 1);
10358
+ } else if (authenticatedEnvs.length === 2 && !options.dryRun) {
10243
10359
  logger.newLine();
10244
10360
  environment = await select({
10245
10361
  message: "Select deployment environment:",
10246
10362
  choices: [
10247
- { value: "production", name: "Production (hub.playcademy.com)" },
10248
- { value: "staging", name: "Staging (hub.dev.playcademy.net)" }
10363
+ { value: "staging", name: "Staging (hub.dev.playcademy.net)" },
10364
+ { value: "production", name: "Production (hub.playcademy.com)" }
10249
10365
  ],
10250
- default: "production"
10366
+ default: "staging"
10251
10367
  });
10252
10368
  }
10253
10369
  if (environment) {
@@ -10395,10 +10511,12 @@ function displayCurrentConfiguration(context2) {
10395
10511
  } else if (config.externalUrl) {
10396
10512
  logger.data("URL", config.externalUrl, 1);
10397
10513
  }
10398
- if (existingGame) {
10514
+ if (context2.isGameDeployed) {
10399
10515
  logger.data("Status", "Deployed", 1);
10400
- } else {
10516
+ } else if (existingGame) {
10401
10517
  logger.data("Status", "Not deployed", 1);
10518
+ } else {
10519
+ logger.data("Status", "New deployment", 1);
10402
10520
  }
10403
10521
  logger.newLine();
10404
10522
  }
@@ -10427,14 +10545,15 @@ async function reportNoChanges(context2) {
10427
10545
  function reportDeploymentSuccess(result, context2) {
10428
10546
  const { game, backendDeployment } = result;
10429
10547
  const { client, previousBackendHash } = context2;
10430
- const action = context2.isNewDeployment ? "deployed" : "updated";
10548
+ const action = context2.isGameDeployed ? "updated" : "deployed";
10431
10549
  logger.newLine();
10432
10550
  logger.success(`${game.displayName} ${action} successfully!`);
10433
10551
  logger.newLine();
10434
10552
  const baseUrl = getWebBaseUrl(client.getBaseUrl());
10435
10553
  logger.data("Game URL", underline(blueBright(`${baseUrl}/g/${game.slug}`)), 1);
10436
10554
  if (backendDeployment || previousBackendHash) {
10437
- const backendUrl = `https://${game.slug}.playcademy.gg/api`;
10555
+ const gameDomain = GAME_BACKEND_DOMAINS[getEnvironment()];
10556
+ const backendUrl = `https://${game.slug}.${gameDomain}/api`;
10438
10557
  logger.data("Backend API", underline(blueBright(backendUrl)), 1);
10439
10558
  }
10440
10559
  if (game.version !== "external") {
@@ -10458,10 +10577,10 @@ import { stat as stat2 } from "node:fs/promises";
10458
10577
 
10459
10578
  // ../data/src/domains/game/helpers.ts
10460
10579
  var isHostedGame = (game) => {
10461
- return game.gameType === "hosted" && game.assetBundleBase !== null;
10580
+ return game.gameType === "hosted" && !!game.assetBundleBase;
10462
10581
  };
10463
10582
  var isExternalGame = (game) => {
10464
- return game.gameType === "external" && game.externalUrl !== null;
10583
+ return game.gameType === "external" && !!game.externalUrl;
10465
10584
  };
10466
10585
  function isGameDeployed(game) {
10467
10586
  return isHostedGame(game) || isExternalGame(game);
@@ -10527,9 +10646,9 @@ async function prepareDeploymentContext(options) {
10527
10646
  configFileName,
10528
10647
  client,
10529
10648
  projectPath,
10530
- existingGame: gameIsDeployed ? existingGame : void 0,
10649
+ existingGame,
10531
10650
  deployedGameInfo: deployedGameInfo ?? void 0,
10532
- isNewDeployment: !gameIsDeployed,
10651
+ isGameDeployed: gameIsDeployed,
10533
10652
  buildHash,
10534
10653
  buildSize,
10535
10654
  previousBuildHash: deployedGameInfo?.buildHash,
@@ -10579,8 +10698,8 @@ async function analyzeChanges(context2) {
10579
10698
  };
10580
10699
  }
10581
10700
  async function calculateDeploymentPlan(context2, changes) {
10582
- const { config, isNewDeployment, projectPath, deployBackend } = context2;
10583
- if (isNewDeployment) {
10701
+ const { config, isGameDeployed: isGameDeployed2, projectPath, deployBackend } = context2;
10702
+ if (!isGameDeployed2) {
10584
10703
  const needsBackend3 = hasLocalBackend(projectPath) || !!context2.fullConfig?.integrations;
10585
10704
  const shouldDeployBackend2 = deployBackend && needsBackend3;
10586
10705
  return {
@@ -11435,19 +11554,28 @@ initCommand.addCommand(configCommand);
11435
11554
 
11436
11555
  // src/commands/login.ts
11437
11556
  import { input as input4, password, select as select3 } from "@inquirer/prompts";
11557
+ import { bold as bold4, dim as dim6, whiteBright } from "colorette";
11438
11558
  import { Command as Command4 } from "commander";
11439
11559
  import open from "open";
11440
11560
  init_file_loader();
11441
- var loginCommand = new Command4("login").description("Authenticate with Playcademy").option("-e, --email <email>", "Email address (for email/password auth)").option("-p, --password <password>", "Password (for email/password auth)").option("--sso", "Use Timeback SSO authentication").action(async (options) => {
11442
- const { email, password: password2, sso } = options;
11443
- logger.newLine();
11561
+ var loginCommand = new Command4("login").description("Authenticate with Playcademy").option("-e, --email <email>", "Email address (for email/password auth)").option("-p, --password <password>", "Password (for email/password auth)").option("--sso", "Use Timeback SSO authentication").option("--env <environment>", "Environment to login to (staging or production)").action(async (options) => {
11562
+ const { email, password: password2, sso, env } = options;
11563
+ const environment = ensureEnvironment(env, logger);
11444
11564
  const profileName = getProfileName();
11445
- const existingProfile = await getProfile(profileName);
11565
+ logger.newLine();
11566
+ const existingProfile = await getProfile(environment, profileName);
11446
11567
  if (existingProfile) {
11447
- const email2 = existingProfile.email;
11448
- logger.info(
11449
- `Already logged in to profile "${profileName}"` + (email2 ? ` as ${email2}` : "")
11450
- );
11568
+ const email2 = existingProfile.email || "unknown user";
11569
+ const otherEnv = environment === "staging" ? "production" : "staging";
11570
+ const otherProfile = await getProfile(otherEnv, profileName);
11571
+ logger.admonition("note", "Hello again", [
11572
+ bold4("You're already logged in"),
11573
+ "",
11574
+ dim6("Email: ") + bold4(email2),
11575
+ dim6("Environment: ") + bold4(environment),
11576
+ dim6("Profile: ") + bold4(profileName),
11577
+ ...otherProfile ? [dim6("Other Profile: ") + bold4(otherEnv)] : []
11578
+ ]);
11451
11579
  logger.newLine();
11452
11580
  return;
11453
11581
  }
@@ -11462,7 +11590,7 @@ var loginCommand = new Command4("login").description("Authenticate with Playcade
11462
11590
  });
11463
11591
  logger.newLine();
11464
11592
  if (authMethod === "sso") {
11465
- await handleSsoLogin(profileName);
11593
+ await handleSsoLogin(environment, profileName);
11466
11594
  return;
11467
11595
  }
11468
11596
  const finalEmail = email || await promptForEmail();
@@ -11515,14 +11643,17 @@ var loginCommand = new Command4("login").description("Authenticate with Playcade
11515
11643
  logger.newLine();
11516
11644
  await runStep(
11517
11645
  `Saving credentials to profile "${profileName}"`,
11518
- () => saveProfile(profileName, authProfile),
11646
+ () => saveProfile(environment, profileName, authProfile),
11519
11647
  "Credentials saved successfully"
11520
11648
  );
11521
- logger.success(`Logged in as ${authProfile.email}`);
11522
- if (profileName !== "default") {
11523
- logger.newLine();
11524
- logger.info(`Profile saved as "${profileName}"`);
11525
- }
11649
+ logger.newLine();
11650
+ logger.admonition("tip", "Logged In Successfully!", [
11651
+ `Email: ${authProfile.email}`,
11652
+ `Environment: ${environment}`,
11653
+ ...profileName !== "default" ? [`Profile: ${profileName}`] : [],
11654
+ "",
11655
+ ...profileName !== "default" ? [`Use \`--profile ${profileName}\` with commands to use this profile`] : []
11656
+ ]);
11526
11657
  logger.newLine();
11527
11658
  } catch (error) {
11528
11659
  logger.newLine();
@@ -11533,7 +11664,7 @@ var loginCommand = new Command4("login").description("Authenticate with Playcade
11533
11664
  process.exit(1);
11534
11665
  }
11535
11666
  });
11536
- async function handleSsoLogin(profileName) {
11667
+ async function handleSsoLogin(environment, profileName) {
11537
11668
  try {
11538
11669
  const serverPromise = startCallbackServer(SSO_AUTH_TIMEOUT_MS);
11539
11670
  const baseUrl = getBaseUrl();
@@ -11578,21 +11709,20 @@ async function handleSsoLogin(profileName) {
11578
11709
  email: result.data.email
11579
11710
  };
11580
11711
  logger.newLine();
11581
- logger.admonition("warning", "Your API Key", [apiKeyResult.apiKey]);
11582
- logger.warn("Save this key securely - it will not be shown again!", 1);
11712
+ logger.admonition("warning", "Your API Key", [
11713
+ "",
11714
+ whiteBright(bold4(apiKeyResult.apiKey)),
11715
+ "",
11716
+ "Save this key securely - it will not be shown again!"
11717
+ ]);
11583
11718
  logger.newLine();
11584
11719
  await runStep(
11585
11720
  `Saving credentials to profile "${profileName}"`,
11586
- () => saveProfile(profileName, authProfile),
11587
- "Credentials saved successfully"
11721
+ () => saveProfile(environment, profileName, authProfile),
11722
+ "Credentials saved successfully",
11723
+ { silent: true }
11588
11724
  );
11589
- logger.success(`Logged in as ${authProfile.email}`);
11590
- if (profileName !== "default") {
11591
- logger.newLine();
11592
- logger.info(`Profile saved as "${profileName}"`);
11593
- logger.newLine();
11594
- logger.aside(`Use \`--profile ${profileName}\` with commands to use this profile`, 1);
11595
- }
11725
+ logger.success(`Logged in to ${environment} as ${authProfile.email}`);
11596
11726
  logger.newLine();
11597
11727
  } catch (error) {
11598
11728
  logger.newLine();
@@ -11631,27 +11761,23 @@ async function promptForPassword() {
11631
11761
 
11632
11762
  // src/commands/logout.ts
11633
11763
  import { Command as Command5 } from "commander";
11634
- var logoutCommand = new Command5("logout").description("Log out from Playcademy").option("--profile <name>", "Profile to log out from", "default").action(async (options) => {
11635
- const { profile: profileName } = options;
11764
+ var logoutCommand = new Command5("logout").description("Log out from Playcademy").argument("[profile]", "Profile name to logout from", "default").option("--env <environment>", "Environment to logout from (staging or production)").action(async (profileName, options) => {
11765
+ const { env } = options;
11766
+ const environment = ensureEnvironment(env, logger);
11636
11767
  try {
11637
11768
  logger.newLine();
11638
- const profile = await getProfile(profileName);
11769
+ const profile = await getProfile(environment, profileName);
11639
11770
  if (!profile) {
11640
- logger.error(`Profile "${profileName}" not found`);
11771
+ logger.error(`Profile "${profileName}" not found in ${environment}`);
11772
+ logger.newLine();
11641
11773
  logger.info("Use `playcademy profiles list` to see available profiles");
11642
11774
  logger.newLine();
11643
11775
  process.exit(1);
11644
11776
  }
11645
- await removeProfile(profileName);
11646
- logger.success(`Logged out from profile "${profileName}"`);
11647
- if (profile.email) {
11648
- logger.info(`Previously logged in as: ${profile.email}`);
11649
- }
11650
- if (profileName === "default") {
11651
- logger.info("To log in again: playcademy login");
11652
- } else {
11653
- logger.info(`To log in again: playcademy login --profile ${profileName}`);
11654
- }
11777
+ await removeProfile(environment, profileName);
11778
+ logger.success(
11779
+ `Logged out from ${environment} profile "${profileName}" (${profile.email})`
11780
+ );
11655
11781
  logger.newLine();
11656
11782
  } catch (error) {
11657
11783
  logger.newLine();
@@ -11665,18 +11791,24 @@ var logoutCommand = new Command5("logout").description("Log out from Playcademy"
11665
11791
 
11666
11792
  // src/commands/me.ts
11667
11793
  import { Command as Command6 } from "commander";
11668
- var meCommand = new Command6("me").description("Display current user information").action(async () => {
11794
+ var meCommand = new Command6("me").description("Display current user information").option(
11795
+ "--env <environment>",
11796
+ "Environment to check user information from (staging or production)"
11797
+ ).action(async (options) => {
11798
+ const { env } = options;
11669
11799
  try {
11800
+ const environment = ensureEnvironment(env, logger);
11670
11801
  const client = await requireAuthenticatedClient();
11671
11802
  logger.newLine();
11672
11803
  const user = await runStep(
11673
- "Fetching user information",
11804
+ `Fetching user information from ${environment}`,
11674
11805
  client.users.me,
11675
11806
  "User information retrieved"
11676
11807
  );
11677
11808
  logger.newLine();
11678
11809
  logger.data("ID", user.id, 1);
11679
11810
  logger.data("Email", user.email, 1);
11811
+ logger.data("Environment", environment, 1);
11680
11812
  if (user.username) {
11681
11813
  logger.data("Username", user.username, 1);
11682
11814
  }
@@ -11724,7 +11856,8 @@ async function executeDeployment(plan, context2) {
11724
11856
  return { game, backendDeployment };
11725
11857
  }
11726
11858
  async function runDeployment(options) {
11727
- await selectEnvironment(options);
11859
+ const environment = ensureEnvironment(options.env, logger);
11860
+ await selectEnvironment({ env: environment, dryRun: options.dryRun });
11728
11861
  const context2 = await prepareDeploymentContext(options);
11729
11862
  displayCurrentConfiguration(context2);
11730
11863
  const didPrompt = await promptForMissingConfig(context2);
@@ -11789,17 +11922,19 @@ import { Command as Command10 } from "commander";
11789
11922
  // src/commands/games/delete.ts
11790
11923
  import { confirm as confirm5, input as input5, select as select4 } from "@inquirer/prompts";
11791
11924
  import { Command as Command8 } from "commander";
11792
- var deleteCommand = new Command8("delete").alias("rm").alias("remove").description("Delete a game").argument("[slug]", "Game slug to delete").option("-f, --force", "Skip confirmation prompt").action(async (slug, options) => {
11925
+ var deleteCommand = new Command8("delete").alias("rm").alias("remove").description("Delete a game").argument("[slug]", "Game slug to delete").option("-f, --force", "Skip confirmation prompt").option("--env <environment>", "Environment to delete game from (staging or production)").action(async (slug, options) => {
11926
+ const { env } = options;
11793
11927
  try {
11794
- logger.newLine();
11928
+ const environment = ensureEnvironment(env, logger);
11795
11929
  const client = await requireAuthenticatedClient();
11930
+ logger.newLine();
11796
11931
  const games2 = await runStep(
11797
- "Fetching games",
11932
+ `Fetching games from ${environment}`,
11798
11933
  client.games.list,
11799
- (games3) => `Found ${games3.length} game${games3.length === 1 ? "" : "s"}`
11934
+ (games3) => `Found ${games3.length} game${games3.length === 1 ? "" : "s"} in ${environment}`
11800
11935
  );
11801
11936
  if (games2.length === 0) {
11802
- logger.info("No games found");
11937
+ logger.info(`No games found in ${environment}`);
11803
11938
  return;
11804
11939
  }
11805
11940
  logger.newLine();
@@ -11847,11 +11982,11 @@ var deleteCommand = new Command8("delete").alias("rm").alias("remove").descripti
11847
11982
  logger.newLine();
11848
11983
  }
11849
11984
  await runStep(
11850
- "Deleting game",
11985
+ `Deleting game from ${environment}`,
11851
11986
  async () => {
11852
11987
  await client.dev.games.delete(game.id);
11853
11988
  },
11854
- "Game deleted successfully"
11989
+ `Game deleted successfully from ${environment}`
11855
11990
  );
11856
11991
  const deployedGame = await getDeployedGame(getWorkspace());
11857
11992
  if (deployedGame && deployedGame.gameId === game.id) {
@@ -11870,14 +12005,16 @@ var deleteCommand = new Command8("delete").alias("rm").alias("remove").descripti
11870
12005
 
11871
12006
  // src/commands/games/list.ts
11872
12007
  import { Command as Command9 } from "commander";
11873
- var listCommand = new Command9("list").alias("ls").description("List all games").action(async () => {
12008
+ var listCommand = new Command9("list").alias("ls").description("List all games").option("--env <environment>", "Environment to list games from (staging or production)").action(async (options) => {
12009
+ const { env } = options;
11874
12010
  try {
12011
+ const environment = ensureEnvironment(env, logger);
11875
12012
  const client = await requireAuthenticatedClient();
11876
12013
  logger.newLine();
11877
12014
  const games2 = await runStep(
11878
- "Fetching games",
12015
+ `Fetching games from ${environment}`,
11879
12016
  client.games.list,
11880
- (games3) => `Found ${games3.length} game${games3.length === 1 ? "" : "s"}`
12017
+ (games3) => `Found ${games3.length} game${games3.length === 1 ? "" : "s"} in ${environment}`
11881
12018
  );
11882
12019
  if (games2.length === 0) {
11883
12020
  logger.info("No games found");
@@ -11916,22 +12053,30 @@ import { Command as Command13 } from "commander";
11916
12053
 
11917
12054
  // src/commands/dev/apply.ts
11918
12055
  import { Command as Command11 } from "commander";
11919
- var applyCommand = new Command11("apply").description("Apply for developer status").action(async () => {
12056
+ var applyCommand = new Command11("apply").description("Apply for developer status").option(
12057
+ "--env <environment>",
12058
+ "Environment to apply for developer status in (staging or production)"
12059
+ ).action(async (options) => {
12060
+ const { env } = options;
11920
12061
  try {
11921
- logger.newLine();
12062
+ const environment = ensureEnvironment(env, logger);
11922
12063
  const client = await requireAuthenticatedClient();
12064
+ logger.newLine();
11923
12065
  const currentStatus = await runStep(
11924
- "Checking current developer status",
12066
+ `Checking current developer status in ${environment}`,
11925
12067
  client.dev.status.get,
11926
- "Current status retrieved"
12068
+ `Current status retrieved from ${environment}`
11927
12069
  );
12070
+ logger.newLine();
11928
12071
  if (currentStatus === "approved") {
11929
- logger.success("You are already an approved developer!");
12072
+ logger.admonition("note", "Approved!", ["You are already an approved developer!"]);
11930
12073
  logger.newLine();
11931
12074
  return;
11932
12075
  }
11933
12076
  if (currentStatus === "pending") {
11934
- logger.info("Your developer application is pending review");
12077
+ logger.admonition("warning", "Developer Status", [
12078
+ "Your developer application is pending review"
12079
+ ]);
11935
12080
  logger.newLine();
11936
12081
  return;
11937
12082
  }
@@ -11941,14 +12086,15 @@ var applyCommand = new Command11("apply").description("Apply for developer statu
11941
12086
  "Developer application submitted successfully"
11942
12087
  );
11943
12088
  logger.newLine();
11944
- logger.success("Your application has been submitted!");
11945
- logger.info("What happens next:", 1);
11946
- logger.data("1.", "We will review your application shortly", 1);
11947
- logger.data("2.", "Once approved, you can create and deploy games", 1);
11948
- logger.data("3.", "Use `playcademy games create` to create your first game", 1);
12089
+ logger.admonition("tip", "Application Submitted!", [
12090
+ "Your developer application has been submitted.",
12091
+ "",
12092
+ "What happens next:",
12093
+ " \u2022 We will review your application shortly",
12094
+ " \u2022 Once approved, you can create and deploy games"
12095
+ ]);
11949
12096
  logger.newLine();
11950
12097
  } catch (error) {
11951
- logger.newLine();
11952
12098
  logger.error(
11953
12099
  `Failed to apply for developer status: ${error instanceof Error ? error.message : "Unknown error"}`
11954
12100
  );
@@ -11959,31 +12105,37 @@ var applyCommand = new Command11("apply").description("Apply for developer statu
11959
12105
 
11960
12106
  // src/commands/dev/get-status.ts
11961
12107
  import { Command as Command12 } from "commander";
11962
- var getStatusCommand = new Command12("status").description("Check your developer status").action(async () => {
12108
+ var getStatusCommand = new Command12("status").description("Check your developer status").option(
12109
+ "--env <environment>",
12110
+ "Environment to check developer status from (staging or production)"
12111
+ ).action(async (options) => {
12112
+ const { env } = options;
11963
12113
  try {
11964
- logger.newLine();
12114
+ const environment = ensureEnvironment(env, logger);
11965
12115
  const client = await requireAuthenticatedClient();
12116
+ logger.newLine();
11966
12117
  const status = await runStep(
11967
- "Fetching developer status",
12118
+ `Fetching developer status from ${environment}`,
11968
12119
  () => client.dev.status.get(),
11969
- "Developer status retrieved"
12120
+ `Developer status retrieved from ${environment}`
11970
12121
  );
11971
- logger.newLine();
11972
- logger.highlight("Developer Status");
11973
- logger.data("Status", status, 1);
11974
12122
  switch (status) {
11975
12123
  case "none":
11976
12124
  logger.newLine();
11977
- logger.info("You have not applied for developer status yet.");
11978
- logger.info("Run `playcademy dev apply` to apply.");
12125
+ logger.admonition("warning", "Become a Developer", [
12126
+ "Want to create and deploy games?",
12127
+ "Run `playcademy dev apply` to apply for developer status"
12128
+ ]);
11979
12129
  break;
11980
12130
  case "pending":
11981
12131
  logger.newLine();
11982
- logger.info("Your application is pending review.");
12132
+ logger.admonition("warning", "Developer Status", [
12133
+ "Your application is pending review."
12134
+ ]);
11983
12135
  break;
11984
12136
  case "approved":
11985
12137
  logger.newLine();
11986
- logger.success("You are an approved developer!");
12138
+ logger.admonition("note", "Approved!", ["You are an approved developer!"]);
11987
12139
  break;
11988
12140
  default:
11989
12141
  logger.newLine();
@@ -12069,26 +12221,39 @@ import { Command as Command17 } from "commander";
12069
12221
  import { Command as Command14 } from "commander";
12070
12222
  async function listProfilesAction() {
12071
12223
  try {
12072
- const profiles = await listProfiles();
12224
+ const profilesMap = await listProfiles();
12073
12225
  logger.newLine();
12074
- if (profiles.length === 0) {
12226
+ let hasAnyProfiles = false;
12227
+ for (const [, profiles] of profilesMap.entries()) {
12228
+ if (profiles.length > 0) {
12229
+ hasAnyProfiles = true;
12230
+ break;
12231
+ }
12232
+ }
12233
+ if (!hasAnyProfiles) {
12075
12234
  logger.warn("No authentication profiles found");
12076
12235
  logger.newLine();
12077
12236
  logger.admonition("tip", "Getting Started", [
12078
12237
  "Run `playcademy login` to create your first profile"
12079
12238
  ]);
12239
+ logger.newLine();
12080
12240
  return;
12081
12241
  }
12082
- const tableData = [];
12083
- for (const profileName of profiles.values()) {
12084
- const profile = await getProfile(profileName);
12085
- tableData.push({
12086
- Profile: profileName,
12087
- Email: profile?.email ?? ""
12088
- });
12089
- }
12090
- if (tableData.length > 0) {
12091
- logger.table(tableData);
12242
+ for (const [environment, profiles] of profilesMap.entries()) {
12243
+ if (profiles.length === 0) continue;
12244
+ const tableData = [];
12245
+ for (const profileName of profiles) {
12246
+ const profile = await getProfile(environment, profileName);
12247
+ tableData.push({
12248
+ Profile: profileName,
12249
+ Email: profile?.email ?? ""
12250
+ });
12251
+ }
12252
+ if (tableData.length > 0) {
12253
+ const envTitle = environment.charAt(0).toUpperCase() + environment.slice(1);
12254
+ logger.table(tableData, envTitle);
12255
+ logger.newLine();
12256
+ }
12092
12257
  }
12093
12258
  } catch (error) {
12094
12259
  logger.error(
@@ -12096,28 +12261,29 @@ async function listProfilesAction() {
12096
12261
  );
12097
12262
  process.exit(1);
12098
12263
  }
12099
- logger.newLine();
12100
12264
  }
12101
12265
  var listCommand2 = new Command14("list").alias("ls").description("List all stored authentication profiles").action(listProfilesAction);
12102
12266
 
12103
12267
  // src/commands/profiles/remove.ts
12268
+ import { bold as bold5 } from "colorette";
12104
12269
  import { Command as Command15 } from "commander";
12105
- var removeCommand = new Command15("remove <name>").alias("rm").description("Remove an authentication profile").action(async (name) => {
12270
+ var removeCommand = new Command15("remove").alias("rm").description('Remove an authentication profile (defaults to "default")').argument("[name]", "Profile name to remove", "default").option("--env <environment>", "Environment to remove profile from (staging or production)").action(async (name, options) => {
12271
+ const { env } = options;
12272
+ const environment = ensureEnvironment(env, logger);
12106
12273
  try {
12107
12274
  logger.newLine();
12108
- const profiles = await listProfiles();
12109
- if (!profiles.includes(name)) {
12110
- logger.error(`Profile "${name}" not found`);
12275
+ const profilesMap = await listProfiles();
12276
+ const envProfiles = profilesMap.get(environment) || [];
12277
+ if (!envProfiles.includes(name)) {
12278
+ logger.error(`Profile "${name}" not found in ${environment}`);
12111
12279
  logger.newLine();
12112
12280
  process.exit(1);
12113
12281
  }
12114
- await removeProfile(name);
12115
- logger.success(`Profile "${name}" removed successfully`);
12116
- if (name !== "default") {
12117
- logger.info(`To re-authenticate: playcademy login --profile ${name}`);
12118
- } else {
12119
- logger.info("To re-authenticate: playcademy login");
12120
- }
12282
+ await removeProfile(environment, name);
12283
+ logger.admonition("note", "Removed!", [
12284
+ `Profile ${bold5(name)} removed from ${environment}`,
12285
+ environment === "production" ? `To re-authenticate run \`playcademy login --env ${environment}\`` : "To re-authenticate run `playcademy login`"
12286
+ ]);
12121
12287
  logger.newLine();
12122
12288
  } catch (error) {
12123
12289
  logger.newLine();
@@ -12132,10 +12298,16 @@ var removeCommand = new Command15("remove <name>").alias("rm").description("Remo
12132
12298
  // src/commands/profiles/reset.ts
12133
12299
  import { confirm as confirm6 } from "@inquirer/prompts";
12134
12300
  import { Command as Command16 } from "commander";
12135
- var resetCommand = new Command16("reset").description("Remove all authentication profiles (requires confirmation)").alias("clear").action(async () => {
12301
+ var resetCommand = new Command16("reset").description(
12302
+ "Remove all authentication profiles across all environments (requires confirmation)"
12303
+ ).alias("clear").action(async () => {
12136
12304
  try {
12137
- const profiles = await listProfiles();
12138
- if (profiles.length === 0) {
12305
+ const profilesMap = await listProfiles();
12306
+ let totalProfiles = 0;
12307
+ for (const profiles of profilesMap.values()) {
12308
+ totalProfiles += profiles.length;
12309
+ }
12310
+ if (totalProfiles === 0) {
12139
12311
  logger.newLine();
12140
12312
  logger.warn("No authentication profiles found");
12141
12313
  logger.newLine();
@@ -12143,14 +12315,25 @@ var resetCommand = new Command16("reset").description("Remove all authentication
12143
12315
  }
12144
12316
  logger.newLine();
12145
12317
  logger.warn(
12146
- `This will remove ${profiles.length} authentication profile${profiles.length > 1 ? "s" : ""}:`
12318
+ `This will remove ${totalProfiles} authentication profile${totalProfiles > 1 ? "s" : ""} across all environments:`
12147
12319
  );
12148
12320
  logger.newLine();
12149
- for (const profileName of profiles) {
12150
- const profile = await getProfile(profileName);
12151
- logger.data(profileName, profile?.email || "", 1);
12321
+ for (const [environment, profiles] of profilesMap.entries()) {
12322
+ if (profiles.length === 0) continue;
12323
+ const tableData = [];
12324
+ for (const profileName of profiles) {
12325
+ const profile = await getProfile(environment, profileName);
12326
+ tableData.push({
12327
+ Profile: profileName,
12328
+ Email: profile?.email ?? ""
12329
+ });
12330
+ }
12331
+ if (tableData.length > 0) {
12332
+ const envTitle = environment.charAt(0).toUpperCase() + environment.slice(1);
12333
+ logger.table(tableData, envTitle);
12334
+ logger.newLine();
12335
+ }
12152
12336
  }
12153
- logger.newLine();
12154
12337
  const confirmed = await confirm6({
12155
12338
  message: "Are you sure you want to remove all profiles?",
12156
12339
  default: false
@@ -12160,23 +12343,27 @@ var resetCommand = new Command16("reset").description("Remove all authentication
12160
12343
  return;
12161
12344
  }
12162
12345
  let removedCount = 0;
12163
- for (const profileName of profiles) {
12164
- try {
12165
- await removeProfile(profileName);
12166
- removedCount++;
12167
- } catch (error) {
12168
- logger.newLine();
12169
- logger.error(`Failed to remove profile "${profileName}"`);
12170
- logger.error(error instanceof Error ? error.message : "Unknown error");
12171
- logger.newLine();
12346
+ for (const [environment, profiles] of profilesMap.entries()) {
12347
+ for (const profileName of profiles) {
12348
+ try {
12349
+ await removeProfile(environment, profileName);
12350
+ removedCount++;
12351
+ } catch (error) {
12352
+ logger.newLine();
12353
+ logger.error(
12354
+ `Failed to remove profile "${profileName}" from ${environment}`
12355
+ );
12356
+ logger.error(error instanceof Error ? error.message : "Unknown error");
12357
+ logger.newLine();
12358
+ }
12172
12359
  }
12173
12360
  }
12174
- if (removedCount === profiles.length) {
12361
+ if (removedCount === totalProfiles) {
12175
12362
  logger.success(
12176
12363
  `${removedCount > 1 ? "All " : ""}${removedCount} profile${removedCount > 1 ? "s" : ""} removed successfully`
12177
12364
  );
12178
12365
  } else {
12179
- logger.warn(`Removed ${removedCount} of ${profiles.length} profiles`);
12366
+ logger.warn(`Removed ${removedCount} of ${totalProfiles} profiles`);
12180
12367
  }
12181
12368
  logger.newLine();
12182
12369
  } catch (error) {
@@ -12201,13 +12388,18 @@ import { Command as Command22 } from "commander";
12201
12388
  // src/commands/timeback/cleanup.ts
12202
12389
  import { confirm as confirm7 } from "@inquirer/prompts";
12203
12390
  import { Command as Command18 } from "commander";
12204
- var cleanupCommand = new Command18("cleanup").description("Remove TimeBack integration for your game").action(async () => {
12391
+ var cleanupCommand = new Command18("cleanup").description("Remove TimeBack integration for your game").option(
12392
+ "--env <environment>",
12393
+ "Environment to remove TimeBack integration from (staging or production)"
12394
+ ).action(async (options) => {
12395
+ const { env } = options;
12205
12396
  try {
12206
- logger.newLine();
12397
+ const environment = ensureEnvironment(env, logger);
12207
12398
  const client = await requireAuthenticatedClient();
12399
+ logger.newLine();
12208
12400
  const { game } = await getGameFromConfig(client);
12209
12401
  const integration = await runStep(
12210
- "Checking for TimeBack integration",
12402
+ `Checking for TimeBack integration in ${environment}`,
12211
12403
  () => client.timeback.management.get(game.id),
12212
12404
  "Integration status checked"
12213
12405
  );
@@ -12230,9 +12422,9 @@ var cleanupCommand = new Command18("cleanup").description("Remove TimeBack integ
12230
12422
  }
12231
12423
  logger.newLine();
12232
12424
  await runStep(
12233
- "Removing TimeBack integration",
12425
+ `Removing TimeBack integration from ${environment}`,
12234
12426
  () => client.timeback.management.cleanup(game.id),
12235
- "TimeBack integration removed"
12427
+ `TimeBack integration removed from ${environment}`
12236
12428
  );
12237
12429
  logger.newLine();
12238
12430
  logger.admonition("note", "Note", [
@@ -12251,8 +12443,13 @@ var cleanupCommand = new Command18("cleanup").description("Remove TimeBack integ
12251
12443
  // src/commands/timeback/setup.ts
12252
12444
  import { Command as Command19 } from "commander";
12253
12445
  import dedent from "dedent";
12254
- var setupCommand = new Command19("setup").description("Set up TimeBack integration for your game").option("--dry-run", "Validate configuration without creating resources").option("--verbose, -v", "Output detailed information").action(async (options) => {
12446
+ var setupCommand = new Command19("setup").description("Set up TimeBack integration for your game").option("--dry-run", "Validate configuration without creating resources").option("--verbose, -v", "Output detailed information").option(
12447
+ "--env <environment>",
12448
+ "Environment to set up TimeBack integration in (staging or production)"
12449
+ ).action(async (options) => {
12450
+ const { env } = options;
12255
12451
  try {
12452
+ ensureEnvironment(env, logger);
12256
12453
  logger.newLine();
12257
12454
  const config = await runStep(
12258
12455
  "Loading configuration",
@@ -12354,8 +12551,13 @@ var setupCommand = new Command19("setup").description("Set up TimeBack integrati
12354
12551
  import { confirm as confirm8 } from "@inquirer/prompts";
12355
12552
  import { green as green3, red as red3 } from "colorette";
12356
12553
  import { Command as Command20 } from "commander";
12357
- var updateCommand = new Command20("update").description("Update TimeBack integration configuration for your game").option("--verbose, -v", "Output detailed information").action(async (options) => {
12554
+ var updateCommand = new Command20("update").description("Update TimeBack integration configuration for your game").option("--verbose, -v", "Output detailed information").option(
12555
+ "--env <environment>",
12556
+ "Environment to update TimeBack integration in (staging or production)"
12557
+ ).action(async (options) => {
12558
+ const { env } = options;
12358
12559
  try {
12560
+ ensureEnvironment(env, logger);
12359
12561
  logger.newLine();
12360
12562
  const config = await runStep(
12361
12563
  "Loading configuration",
@@ -12461,13 +12663,18 @@ var updateCommand = new Command20("update").description("Update TimeBack integra
12461
12663
 
12462
12664
  // src/commands/timeback/verify.ts
12463
12665
  import { Command as Command21 } from "commander";
12464
- var verifyCommand = new Command21("verify").description("Verify TimeBack integration for your game").option("--verbose, -v", "Output detailed resource information").action(async (options) => {
12666
+ var verifyCommand = new Command21("verify").description("Verify TimeBack integration for your game").option("--verbose, -v", "Output detailed resource information").option(
12667
+ "--env <environment>",
12668
+ "Environment to verify TimeBack integration in (staging or production)"
12669
+ ).action(async (options) => {
12670
+ const { env } = options;
12465
12671
  try {
12466
- logger.newLine();
12672
+ const environment = ensureEnvironment(env, logger);
12467
12673
  const client = await requireAuthenticatedClient();
12674
+ logger.newLine();
12468
12675
  const { game } = await getGameFromConfig(client);
12469
12676
  const result = await runStep(
12470
- "Verifying TimeBack integration",
12677
+ `Verifying TimeBack integration in ${environment}`,
12471
12678
  () => client.timeback.management.verify(game.id),
12472
12679
  "Verification complete"
12473
12680
  );
@@ -12508,11 +12715,12 @@ timebackCommand.addCommand(cleanupCommand);
12508
12715
  var __dirname = dirname4(fileURLToPath2(import.meta.url));
12509
12716
  var packageJson = await loadPackageJson({ cwd: __dirname, searchUp: true, required: true });
12510
12717
  program.name("playcademy").description("CLI for deploying and managing Playcademy games").version(packageJson?.version || "0.0.0").option("--profile <name>", "Use a specific authentication profile", "default").hook("preAction", (thisCommand) => {
12511
- const options = thisCommand.opts();
12718
+ const options = thisCommand.optsWithGlobals();
12512
12719
  setCliContext({ profile: options.profile, workspace: process.cwd() });
12513
12720
  });
12514
12721
  program.addCommand(initCommand);
12515
12722
  program.addCommand(loginCommand);
12723
+ program.addCommand(logoutCommand);
12516
12724
  program.addCommand(profilesCommand);
12517
12725
  program.addCommand(meCommand);
12518
12726
  program.addCommand(devCommand);
@@ -12550,6 +12758,7 @@ export {
12550
12758
  displayRegisteredRoutes,
12551
12759
  displayResourcesStatus,
12552
12760
  displaySuccessMessage,
12761
+ ensureEnvironment,
12553
12762
  ensureGameExists,
12554
12763
  findConfigPath,
12555
12764
  generateEntryCode,
@@ -12557,6 +12766,7 @@ export {
12557
12766
  generateJsonConfig,
12558
12767
  getApiUrl,
12559
12768
  getAuthPath,
12769
+ getAuthenticatedEnvironments,
12560
12770
  getBackendHash,
12561
12771
  getBackendSize,
12562
12772
  getBaseUrl,
@@ -12588,6 +12798,7 @@ export {
12588
12798
  loadGameStore,
12589
12799
  logger,
12590
12800
  needsBackend,
12801
+ normalizeEnvironment,
12591
12802
  prepareDeploymentContext,
12592
12803
  processConfigVariables,
12593
12804
  promptForApiRoutes,
package/dist/types.d.ts CHANGED
@@ -23,15 +23,28 @@ interface AuthProfile {
23
23
  /** ISO 8601 date string - when the token expires (if applicable) */
24
24
  expiresAt?: string;
25
25
  }
26
+ /**
27
+ * Environment-specific auth profiles
28
+ * Each environment (staging, production) has its own set of profiles
29
+ */
30
+ interface EnvironmentAuthProfiles {
31
+ /** Default authentication profile for this environment */
32
+ default: AuthProfile | null;
33
+ /** Named authentication profiles for this environment */
34
+ profiles: Record<string, AuthProfile>;
35
+ }
26
36
  /**
27
37
  * The complete auth store structure
28
38
  * CLI-specific type for ~/.playcademy/auth.json
39
+ *
40
+ * Profiles are scoped per environment to ensure staging and production
41
+ * credentials are kept separate for security.
29
42
  */
30
43
  interface AuthStore {
31
- /** Default authentication profile used when no profile is specified */
32
- default: AuthProfile | null;
33
- /** Named authentication profiles for different environments/contexts */
34
- profiles: Record<string, AuthProfile>;
44
+ /** Staging environment profiles */
45
+ staging: EnvironmentAuthProfiles;
46
+ /** Production environment profiles */
47
+ production: EnvironmentAuthProfiles;
35
48
  }
36
49
  /**
37
50
  * Login credentials for email/password authentication
@@ -486,7 +499,7 @@ interface DeploymentContext {
486
499
  projectPath: string;
487
500
  existingGame?: Game;
488
501
  deployedGameInfo?: DeployedGameInfo;
489
- isNewDeployment: boolean;
502
+ isGameDeployed: boolean;
490
503
  buildHash?: string;
491
504
  buildSize?: number;
492
505
  previousBuildHash?: string;
@@ -625,4 +638,4 @@ interface PreviewResponse {
625
638
  qrCode?: string;
626
639
  }
627
640
 
628
- export type { ApiConfig, ApiErrorResponse, ApiKeyListItem, ApiKeyWithSecret, ApiRequestOptions, AuthProfile, AuthStore, BackendDeploymentWithHash, CallbackServerResult, CreateApiKeyResponse, DeployConfig, DeployNewGameOptions, DeployedGameInfo, DeploymentChanges, DeploymentContext, DeploymentPlan, DeploymentResult, GameStore, IntegrationsConfig, LoginCredentials, LoginResponse, PlaycademyConfig, PreviewOptions, PreviewResponse, SignInResponse, SsoCallbackData, TimebackIntegrationConfig, TokenType, UpdateExistingGameOptions };
641
+ export type { ApiConfig, ApiErrorResponse, ApiKeyListItem, ApiKeyWithSecret, ApiRequestOptions, AuthProfile, AuthStore, BackendDeploymentWithHash, CallbackServerResult, CreateApiKeyResponse, DeployConfig, DeployNewGameOptions, DeployedGameInfo, DeploymentChanges, DeploymentContext, DeploymentPlan, DeploymentResult, EnvironmentAuthProfiles, GameStore, IntegrationsConfig, LoginCredentials, LoginResponse, PlaycademyConfig, PreviewOptions, PreviewResponse, SignInResponse, SsoCallbackData, TimebackIntegrationConfig, TokenType, UpdateExistingGameOptions };
package/dist/utils.js CHANGED
@@ -4548,15 +4548,7 @@ function createDevNamespace(client) {
4548
4548
  throw new Error(`File upload failed: ${uploadResponse.status} ${uploadResponse.statusText}`);
4549
4549
  }
4550
4550
  }
4551
- const baseUrl = (() => {
4552
- const anyClient = client;
4553
- try {
4554
- return typeof anyClient.getBaseUrl === "function" ? anyClient.getBaseUrl() : "/api";
4555
- } catch {
4556
- return "/api";
4557
- }
4558
- })();
4559
- const finalizeUrl = baseUrl.replace(/\/$/, "") + "/games/uploads/finalize/";
4551
+ const finalizeUrl = `${client.baseUrl}/games/uploads/finalize/`;
4560
4552
  const authToken = client.getToken();
4561
4553
  const tokenType = client.getTokenType();
4562
4554
  const headers = {
@@ -6034,13 +6026,13 @@ var init_achievements = () => {
6034
6026
  function createTimebackNamespace(client) {
6035
6027
  return {
6036
6028
  recordProgress: (progressData) => {
6037
- return client["request"]("/api/integrations/timeback/progress", "POST", { progressData }, void 0, true);
6029
+ return client["requestGameBackend"]("/api/integrations/timeback/progress", "POST", { progressData });
6038
6030
  },
6039
6031
  recordSessionEnd: (sessionData) => {
6040
- return client["request"]("/api/integrations/timeback/session-end", "POST", { sessionData }, void 0, true);
6032
+ return client["requestGameBackend"]("/api/integrations/timeback/session-end", "POST", { sessionData });
6041
6033
  },
6042
6034
  awardXP: (xpAmount, metadata) => {
6043
- return client["request"]("/api/integrations/timeback/award-xp", "POST", { xpAmount, metadata }, void 0, true);
6035
+ return client["requestGameBackend"]("/api/integrations/timeback/award-xp", "POST", { xpAmount, metadata });
6044
6036
  },
6045
6037
  management: {
6046
6038
  setup: (request2) => {
@@ -6349,7 +6341,7 @@ var init_client = __esm2(() => {
6349
6341
  authContext;
6350
6342
  initPayload;
6351
6343
  constructor(config) {
6352
- this.baseUrl = config?.baseUrl || "";
6344
+ this.baseUrl = config?.baseUrl?.endsWith("/api") ? config.baseUrl : `${config?.baseUrl}/api`;
6353
6345
  this.gameUrl = config?.gameUrl;
6354
6346
  this.gameId = config?.gameId;
6355
6347
  this.config = config || {};
@@ -6365,8 +6357,9 @@ var init_client = __esm2(() => {
6365
6357
  return `${effectiveBaseUrl}/api`;
6366
6358
  }
6367
6359
  getGameBackendUrl() {
6368
- if (!this.gameUrl)
6369
- return this.getBaseUrl();
6360
+ if (!this.gameUrl) {
6361
+ throw new PlaycademyError("Game backend URL not configured. gameUrl must be set to use game backend features.");
6362
+ }
6370
6363
  const isRelative = this.gameUrl.startsWith("/");
6371
6364
  const isBrowser2 = typeof window !== "undefined";
6372
6365
  const effectiveGameUrl = isRelative && isBrowser2 ? `${window.location.origin}${this.gameUrl}` : this.gameUrl;
@@ -6403,17 +6396,29 @@ var init_client = __esm2(() => {
6403
6396
  listener(payload);
6404
6397
  });
6405
6398
  }
6406
- async request(path, method, body, headers, useGameBackend = false) {
6399
+ async request(path, method, body, headers) {
6400
+ const effectiveHeaders = {
6401
+ ...headers,
6402
+ ...this.authStrategy.getHeaders()
6403
+ };
6404
+ return request({
6405
+ path,
6406
+ method,
6407
+ body,
6408
+ baseUrl: this.baseUrl,
6409
+ extraHeaders: effectiveHeaders
6410
+ });
6411
+ }
6412
+ async requestGameBackend(path, method, body, headers) {
6407
6413
  const effectiveHeaders = {
6408
6414
  ...headers,
6409
6415
  ...this.authStrategy.getHeaders()
6410
6416
  };
6411
- const targetBaseUrl = useGameBackend ? this.getGameBackendUrl() : this.baseUrl;
6412
6417
  return request({
6413
6418
  path,
6414
6419
  method,
6415
6420
  body,
6416
- baseUrl: targetBaseUrl,
6421
+ baseUrl: this.getGameBackendUrl(),
6417
6422
  extraHeaders: effectiveHeaders
6418
6423
  });
6419
6424
  }
@@ -6498,9 +6503,40 @@ function customTransform(text2) {
6498
6503
  const highlightCode = (text3) => text3.replace(/`([^`]+)`/g, (_, code) => greenBright(code));
6499
6504
  return highlightCode(text2);
6500
6505
  }
6506
+ function formatTable(data, title) {
6507
+ if (data.length === 0) return;
6508
+ const keys = Object.keys(data[0]);
6509
+ const rows = data.map((item) => keys.map((key) => String(item[key] ?? "")));
6510
+ const widths = keys.map((key, i) => {
6511
+ const headerWidth = key.length;
6512
+ const dataWidth = Math.max(...rows.map((row) => row[i].length));
6513
+ return Math.max(headerWidth, dataWidth);
6514
+ });
6515
+ const totalWidth = widths.reduce((sum, w) => sum + w + 3, -1);
6516
+ const separator = "\u251C" + widths.map((w) => "\u2500".repeat(w + 2)).join("\u253C") + "\u2524";
6517
+ const topBorder = "\u250C" + "\u2500".repeat(totalWidth) + "\u2510";
6518
+ const titleSeparator = "\u251C" + widths.map((w) => "\u2500".repeat(w + 2)).join("\u252C") + "\u2524";
6519
+ const bottomBorder = "\u2514" + widths.map((w) => "\u2500".repeat(w + 2)).join("\u2534") + "\u2518";
6520
+ console.log(topBorder);
6521
+ if (title) {
6522
+ const titleText = bold(title);
6523
+ const titlePadding = totalWidth - title.length - 1;
6524
+ const titleRow = "\u2502 " + titleText + " ".repeat(titlePadding) + "\u2502";
6525
+ console.log(titleRow);
6526
+ console.log(titleSeparator);
6527
+ }
6528
+ const header = "\u2502 " + keys.map((key, i) => key.padEnd(widths[i])).join(" \u2502 ") + " \u2502";
6529
+ console.log(header);
6530
+ console.log(separator);
6531
+ rows.forEach((row) => {
6532
+ const dataRow = "\u2502 " + row.map((cell, i) => cell.padEnd(widths[i])).join(" \u2502 ") + " \u2502";
6533
+ console.log(dataRow);
6534
+ });
6535
+ console.log(bottomBorder);
6536
+ }
6501
6537
  var logger = {
6502
- table: (data) => {
6503
- console.table(data);
6538
+ table: (data, title) => {
6539
+ formatTable(data, title);
6504
6540
  },
6505
6541
  /**
6506
6542
  * Info message - general information
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playcademy",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "module": "./dist/index.js",
6
6
  "exports": {