@playcademy/vite-plugin 0.1.38 → 0.2.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
@@ -40990,105 +40990,6 @@ function closeBundleHook(context) {
40990
40990
  // src/hooks/config.ts
40991
40991
  import { DEFAULT_PORTS } from "playcademy/constants";
40992
40992
 
40993
- // ../utils/src/port.ts
40994
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
40995
- import { createServer } from "node:net";
40996
- import { homedir } from "node:os";
40997
- import { join } from "node:path";
40998
- async function isPortAvailableOnHost(port, host) {
40999
- return new Promise((resolve) => {
41000
- const server = createServer();
41001
- let resolved = false;
41002
- const cleanup = (result) => {
41003
- if (resolved)
41004
- return;
41005
- resolved = true;
41006
- try {
41007
- server.close();
41008
- } catch {}
41009
- resolve(result);
41010
- };
41011
- const timeout = setTimeout(() => cleanup(true), 100);
41012
- server.once("error", (err2) => {
41013
- clearTimeout(timeout);
41014
- if (err2.code === "EAFNOSUPPORT" || err2.code === "EADDRNOTAVAIL") {
41015
- cleanup(true);
41016
- } else {
41017
- cleanup(false);
41018
- }
41019
- });
41020
- server.once("listening", () => {
41021
- clearTimeout(timeout);
41022
- cleanup(true);
41023
- });
41024
- server.listen(port, host).unref();
41025
- });
41026
- }
41027
- async function findAvailablePort(startPort = 4321) {
41028
- if (await isPortAvailableOnHost(startPort, "0.0.0.0")) {
41029
- return startPort;
41030
- } else {
41031
- return findAvailablePort(startPort + 1);
41032
- }
41033
- }
41034
- function getRegistryPath() {
41035
- const home = homedir();
41036
- const dir = join(home, ".playcademy");
41037
- if (!existsSync(dir)) {
41038
- mkdirSync(dir, { recursive: true });
41039
- }
41040
- return join(dir, ".proc");
41041
- }
41042
- function readRegistry() {
41043
- const registryPath = getRegistryPath();
41044
- if (!existsSync(registryPath)) {
41045
- return {};
41046
- }
41047
- try {
41048
- const content = readFileSync(registryPath, "utf-8");
41049
- return JSON.parse(content);
41050
- } catch {
41051
- return {};
41052
- }
41053
- }
41054
- function writeRegistry(registry) {
41055
- const registryPath = getRegistryPath();
41056
- writeFileSync(registryPath, JSON.stringify(registry, null, 2), "utf-8");
41057
- }
41058
- function getServerKey(type, port) {
41059
- return `${type}-${port}`;
41060
- }
41061
- function writeServerInfo(type, info2) {
41062
- const registry = readRegistry();
41063
- const key = getServerKey(type, info2.port);
41064
- registry[key] = info2;
41065
- writeRegistry(registry);
41066
- }
41067
- function cleanupServerInfo(type, projectRoot, pid) {
41068
- const registry = readRegistry();
41069
- const keysToRemove = [];
41070
- for (const [key, info2] of Object.entries(registry)) {
41071
- if (key.startsWith(`${type}-`)) {
41072
- let matches = true;
41073
- if (projectRoot && info2.projectRoot !== projectRoot) {
41074
- matches = false;
41075
- }
41076
- if (pid !== undefined && info2.pid !== pid) {
41077
- matches = false;
41078
- }
41079
- if (matches) {
41080
- keysToRemove.push(key);
41081
- }
41082
- }
41083
- }
41084
- for (const key of keysToRemove) {
41085
- delete registry[key];
41086
- }
41087
- if (keysToRemove.length > 0) {
41088
- writeRegistry(registry);
41089
- }
41090
- }
41091
-
41092
40993
  // src/config/proxy.ts
41093
40994
  function createProxyConfig(existingProxy, backendPort) {
41094
40995
  if (existingProxy["/api"]) {
@@ -41122,7 +41023,8 @@ function createViteConfig(userConfig, backendPort) {
41122
41023
  // src/hooks/config.ts
41123
41024
  async function configHook(userConfig, context) {
41124
41025
  process.noDeprecation = true;
41125
- context.backendPort = await findAvailablePort(DEFAULT_PORTS.BACKEND);
41026
+ context.backendPort = DEFAULT_PORTS.BACKEND;
41027
+ context.sandboxPort = DEFAULT_PORTS.SANDBOX;
41126
41028
  return createViteConfig(userConfig, context.backendPort);
41127
41029
  }
41128
41030
 
@@ -41141,8 +41043,12 @@ var serverState = {
41141
41043
  backend: null,
41142
41044
  viteServer: null,
41143
41045
  currentMode: "platform",
41144
- timebackRoleOverride: null
41046
+ timebackRoleOverride: null,
41047
+ platformRoleOverride: null
41145
41048
  };
41049
+ function getSandboxRef() {
41050
+ return serverState.sandbox;
41051
+ }
41146
41052
  function hasActiveServers() {
41147
41053
  return !!(serverState.backend || serverState.sandbox);
41148
41054
  }
@@ -41164,6 +41070,12 @@ function getTimebackRoleOverride() {
41164
41070
  function setTimebackRoleOverride(role) {
41165
41071
  serverState.timebackRoleOverride = role;
41166
41072
  }
41073
+ function getPlatformRoleOverride() {
41074
+ return serverState.platformRoleOverride;
41075
+ }
41076
+ function setPlatformRoleOverride(role) {
41077
+ serverState.platformRoleOverride = role;
41078
+ }
41167
41079
 
41168
41080
  // src/server/cleanup.ts
41169
41081
  async function cleanupServers() {
@@ -41172,23 +41084,29 @@ async function cleanupServers() {
41172
41084
  await serverState.backend.server.dispose();
41173
41085
  serverState.backend.stopHotReload();
41174
41086
  serverState.backend.cleanup();
41087
+ await new Promise((resolve) => setTimeout(resolve, 100));
41175
41088
  } catch {}
41176
41089
  serverState.backend = null;
41177
41090
  }
41178
41091
  if (serverState.sandbox) {
41179
41092
  try {
41180
- serverState.sandbox.cleanup();
41093
+ await serverState.sandbox.cleanup();
41181
41094
  } catch {}
41182
41095
  serverState.sandbox = null;
41183
41096
  }
41184
41097
  }
41185
41098
 
41186
- // src/server/hotkeys/cycle-timeback-role.ts
41187
- var import_picocolors2 = __toESM(require_picocolors(), 1);
41099
+ // src/server/config-watcher.ts
41100
+ import fs8 from "node:fs";
41101
+ import path6 from "node:path";
41102
+ import { CONFIG_FILE_NAMES } from "playcademy/constants";
41188
41103
 
41189
- // src/types/internal.ts
41190
- var TIMEBACK_ROLES = ["student", "parent", "teacher", "administrator"];
41191
- // src/server/hotkeys/cycle-timeback-role.ts
41104
+ // src/server/recreate-sandbox.ts
41105
+ var import_picocolors6 = __toESM(require_picocolors(), 1);
41106
+
41107
+ // ../utils/src/vite-logger.ts
41108
+ var import_picocolors2 = __toESM(require_picocolors(), 1);
41109
+ var { bold, cyan, dim } = import_picocolors2.default;
41192
41110
  function formatTimestamp() {
41193
41111
  const now = new Date;
41194
41112
  const hours = now.getHours();
@@ -41196,32 +41114,19 @@ function formatTimestamp() {
41196
41114
  const seconds = now.getSeconds().toString().padStart(2, "0");
41197
41115
  const ampm = hours >= 12 ? "PM" : "AM";
41198
41116
  const displayHours = hours % 12 || 12;
41199
- return import_picocolors2.default.dim(`${displayHours}:${minutes}:${seconds} ${ampm}`);
41117
+ return dim(`${displayHours}:${minutes}:${seconds} ${ampm}`);
41200
41118
  }
41201
- function cycleTimebackRole(logger) {
41202
- const currentRole = getTimebackRoleOverride() ?? "student";
41203
- const currentIndex = TIMEBACK_ROLES.indexOf(currentRole);
41204
- const nextIndex = (currentIndex + 1) % TIMEBACK_ROLES.length;
41205
- const nextRole = TIMEBACK_ROLES[nextIndex];
41206
- setTimebackRoleOverride(nextRole);
41207
- logger.info(`${formatTimestamp()} ${import_picocolors2.default.cyan(import_picocolors2.default.bold("[playcademy]"))} ${import_picocolors2.default.dim("(timeback)")} ${import_picocolors2.default.red(currentRole)} → ${import_picocolors2.default.green(nextRole)}`);
41208
- if (getViteServerRef()) {
41209
- getViteServerRef()?.ws.send({ type: "full-reload", path: "*" });
41210
- } else {
41211
- logger.warn(`${formatTimestamp()} ${import_picocolors2.default.red(import_picocolors2.bold("[playcademy]"))} ${import_picocolors2.dim("(timeback)")} ${import_picocolors2.yellow("Cannot cycle TimeBack role: no Vite server reference")}`);
41119
+ function createLogPrefix(entity, domain) {
41120
+ const timestamp = formatTimestamp();
41121
+ const label = bold(cyan(`[${entity}]`));
41122
+ if (domain) {
41123
+ return `${timestamp} ${label} ${dim(`(${domain})`)}`;
41212
41124
  }
41125
+ return `${timestamp} ${label}`;
41213
41126
  }
41214
- var cycleTimebackRoleHotkey = (options) => ({
41215
- key: "t",
41216
- description: "cycle TimeBack role",
41217
- action: () => cycleTimebackRole(options.viteConfig.logger)
41218
- });
41219
-
41220
- // src/server/hotkeys/recreate-database.ts
41221
- var import_picocolors7 = __toESM(require_picocolors(), 1);
41222
41127
 
41223
41128
  // src/lib/sandbox/server.ts
41224
- var import_picocolors6 = __toESM(require_picocolors(), 1);
41129
+ var import_picocolors5 = __toESM(require_picocolors(), 1);
41225
41130
  import { DEFAULT_PORTS as DEFAULT_PORTS2 } from "playcademy/constants";
41226
41131
 
41227
41132
  // ../sandbox/dist/server.js
@@ -41258,20 +41163,24 @@ import { Http2ServerRequest as Http2ServerRequest2 } from "http2";
41258
41163
  import { Http2ServerRequest } from "http2";
41259
41164
  import { Readable } from "stream";
41260
41165
  import crypto2 from "crypto";
41166
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "node:fs";
41167
+ import { createServer } from "node:net";
41168
+ import { homedir } from "node:os";
41169
+ import { join as join2 } from "node:path";
41261
41170
  import fs5 from "node:fs";
41262
41171
  import fs3 from "node:fs";
41263
41172
  import fs22 from "node:fs";
41264
- import { dirname, isAbsolute, join as join22 } from "node:path";
41173
+ import { dirname, isAbsolute, join as join3 } from "node:path";
41265
41174
  import path2 from "path";
41266
41175
  import { stdout } from "process";
41267
41176
  import { createPublicKey as createPublicKey2, createVerify, verify as verify3 } from "crypto";
41268
41177
  import { request as request2 } from "https";
41269
41178
  import { pipeline } from "stream";
41270
41179
  import { readdir as readdir2, readFile as readFile2, stat } from "node:fs/promises";
41271
- import { join as join4, relative } from "node:path";
41180
+ import { join as join5, relative } from "node:path";
41272
41181
  import { createHash } from "node:crypto";
41273
41182
  import { readdir, readFile } from "node:fs/promises";
41274
- import { join as join3 } from "node:path";
41183
+ import { join as join4 } from "node:path";
41275
41184
  import crypto6 from "node:crypto";
41276
41185
  import * as crypto7 from "node:crypto";
41277
41186
  var __create2 = Object.create;
@@ -45601,7 +45510,7 @@ var require_node22 = __commonJS2((exports) => {
45601
45510
  return path3;
45602
45511
  }
45603
45512
  exports2.normalize = normalize;
45604
- function join222(aRoot, aPath) {
45513
+ function join22(aRoot, aPath) {
45605
45514
  if (aRoot === "") {
45606
45515
  aRoot = ".";
45607
45516
  }
@@ -45633,7 +45542,7 @@ var require_node22 = __commonJS2((exports) => {
45633
45542
  }
45634
45543
  return joined;
45635
45544
  }
45636
- exports2.join = join222;
45545
+ exports2.join = join22;
45637
45546
  exports2.isAbsolute = function(aPath) {
45638
45547
  return aPath.charAt(0) === "/" || urlRegexp.test(aPath);
45639
45548
  };
@@ -45806,7 +45715,7 @@ var require_node22 = __commonJS2((exports) => {
45806
45715
  parsed.path = parsed.path.substring(0, index2 + 1);
45807
45716
  }
45808
45717
  }
45809
- sourceURL = join222(urlGenerate(parsed), sourceURL);
45718
+ sourceURL = join22(urlGenerate(parsed), sourceURL);
45810
45719
  }
45811
45720
  return normalize(sourceURL);
45812
45721
  }
@@ -49532,16 +49441,16 @@ If you have no idea what this means or what Pirates is, let me explain: Pirates
49532
49441
  return walkForTsConfig(parentDirectory, readdirSync);
49533
49442
  }
49534
49443
  exports2.walkForTsConfig = walkForTsConfig;
49535
- function loadTsconfig(configFilePath, existsSync22, readFileSync2) {
49536
- if (existsSync22 === undefined) {
49537
- existsSync22 = fs32.existsSync;
49444
+ function loadTsconfig(configFilePath, existsSync3, readFileSync2) {
49445
+ if (existsSync3 === undefined) {
49446
+ existsSync3 = fs32.existsSync;
49538
49447
  }
49539
49448
  if (readFileSync2 === undefined) {
49540
49449
  readFileSync2 = function(filename) {
49541
49450
  return fs32.readFileSync(filename, "utf8");
49542
49451
  };
49543
49452
  }
49544
- if (!existsSync22(configFilePath)) {
49453
+ if (!existsSync3(configFilePath)) {
49545
49454
  return;
49546
49455
  }
49547
49456
  var configString = readFileSync2(configFilePath);
@@ -49557,27 +49466,27 @@ If you have no idea what this means or what Pirates is, let me explain: Pirates
49557
49466
  var base = undefined;
49558
49467
  if (Array.isArray(extendedConfig)) {
49559
49468
  base = extendedConfig.reduce(function(currBase, extendedConfigElement) {
49560
- return mergeTsconfigs(currBase, loadTsconfigFromExtends(configFilePath, extendedConfigElement, existsSync22, readFileSync2));
49469
+ return mergeTsconfigs(currBase, loadTsconfigFromExtends(configFilePath, extendedConfigElement, existsSync3, readFileSync2));
49561
49470
  }, {});
49562
49471
  } else {
49563
- base = loadTsconfigFromExtends(configFilePath, extendedConfig, existsSync22, readFileSync2);
49472
+ base = loadTsconfigFromExtends(configFilePath, extendedConfig, existsSync3, readFileSync2);
49564
49473
  }
49565
49474
  return mergeTsconfigs(base, config2);
49566
49475
  }
49567
49476
  return config2;
49568
49477
  }
49569
49478
  exports2.loadTsconfig = loadTsconfig;
49570
- function loadTsconfigFromExtends(configFilePath, extendedConfigValue, existsSync22, readFileSync2) {
49479
+ function loadTsconfigFromExtends(configFilePath, extendedConfigValue, existsSync3, readFileSync2) {
49571
49480
  var _a;
49572
49481
  if (typeof extendedConfigValue === "string" && extendedConfigValue.indexOf(".json") === -1) {
49573
49482
  extendedConfigValue += ".json";
49574
49483
  }
49575
49484
  var currentDir = path3.dirname(configFilePath);
49576
49485
  var extendedConfigPath = path3.join(currentDir, extendedConfigValue);
49577
- if (extendedConfigValue.indexOf("/") !== -1 && extendedConfigValue.indexOf(".") !== -1 && !existsSync22(extendedConfigPath)) {
49486
+ if (extendedConfigValue.indexOf("/") !== -1 && extendedConfigValue.indexOf(".") !== -1 && !existsSync3(extendedConfigPath)) {
49578
49487
  extendedConfigPath = path3.join(currentDir, "node_modules", extendedConfigValue);
49579
49488
  }
49580
- var config2 = loadTsconfig(extendedConfigPath, existsSync22, readFileSync2) || {};
49489
+ var config2 = loadTsconfig(extendedConfigPath, existsSync3, readFileSync2) || {};
49581
49490
  if ((_a = config2.compilerOptions) === null || _a === undefined ? undefined : _a.baseUrl) {
49582
49491
  var extendsDir = path3.dirname(extendedConfigValue);
49583
49492
  config2.compilerOptions.baseUrl = path3.join(extendsDir, config2.compilerOptions.baseUrl);
@@ -76048,7 +75957,7 @@ globstar while`, file, fr, pattern, pr2, swallowee);
76048
75957
  return new SQL2([new StringChunk2(str)]);
76049
75958
  }
76050
75959
  sql22.raw = raw2;
76051
- function join32(chunks, separator) {
75960
+ function join42(chunks, separator) {
76052
75961
  const result = [];
76053
75962
  for (const [i3, chunk] of chunks.entries()) {
76054
75963
  if (i3 > 0 && separator !== undefined) {
@@ -76058,7 +75967,7 @@ globstar while`, file, fr, pattern, pr2, swallowee);
76058
75967
  }
76059
75968
  return new SQL2(result);
76060
75969
  }
76061
- sql22.join = join32;
75970
+ sql22.join = join42;
76062
75971
  function identifier(value) {
76063
75972
  return new Name2(value);
76064
75973
  }
@@ -78888,7 +78797,7 @@ params: ${params}`);
78888
78797
  const tableName = getTableLikeName2(table62);
78889
78798
  for (const item of extractUsedTable(table62))
78890
78799
  this.usedTables.add(item);
78891
- if (typeof tableName === "string" && this.config.joins?.some((join32) => join32.alias === tableName)) {
78800
+ if (typeof tableName === "string" && this.config.joins?.some((join42) => join42.alias === tableName)) {
78892
78801
  throw new Error(`Alias "${tableName}" is already used in this query`);
78893
78802
  }
78894
78803
  if (!this.isPartialSelect) {
@@ -79746,7 +79655,7 @@ params: ${params}`);
79746
79655
  createJoin(joinType) {
79747
79656
  return (table62, on2) => {
79748
79657
  const tableName = getTableLikeName2(table62);
79749
- if (typeof tableName === "string" && this.config.joins.some((join32) => join32.alias === tableName)) {
79658
+ if (typeof tableName === "string" && this.config.joins.some((join42) => join42.alias === tableName)) {
79750
79659
  throw new Error(`Alias "${tableName}" is already used in this query`);
79751
79660
  }
79752
79661
  if (typeof on2 === "function") {
@@ -79792,10 +79701,10 @@ params: ${params}`);
79792
79701
  const fromFields = this.getTableLikeFields(this.config.from);
79793
79702
  fields[tableName] = fromFields;
79794
79703
  }
79795
- for (const join32 of this.config.joins) {
79796
- const tableName2 = getTableLikeName2(join32.table);
79797
- if (typeof tableName2 === "string" && !is2(join32.table, SQL2)) {
79798
- const fromFields = this.getTableLikeFields(join32.table);
79704
+ for (const join42 of this.config.joins) {
79705
+ const tableName2 = getTableLikeName2(join42.table);
79706
+ if (typeof tableName2 === "string" && !is2(join42.table, SQL2)) {
79707
+ const fromFields = this.getTableLikeFields(join42.table);
79799
79708
  fields[tableName2] = fromFields;
79800
79709
  }
79801
79710
  }
@@ -83370,7 +83279,7 @@ ORDER BY
83370
83279
  const tableName = getTableLikeName2(table62);
83371
83280
  for (const item of extractUsedTable2(table62))
83372
83281
  this.usedTables.add(item);
83373
- if (typeof tableName === "string" && this.config.joins?.some((join32) => join32.alias === tableName)) {
83282
+ if (typeof tableName === "string" && this.config.joins?.some((join42) => join42.alias === tableName)) {
83374
83283
  throw new Error(`Alias "${tableName}" is already used in this query`);
83375
83284
  }
83376
83285
  if (!this.isPartialSelect) {
@@ -83811,7 +83720,7 @@ ORDER BY
83811
83720
  createJoin(joinType) {
83812
83721
  return (table62, on2) => {
83813
83722
  const tableName = getTableLikeName2(table62);
83814
- if (typeof tableName === "string" && this.config.joins.some((join32) => join32.alias === tableName)) {
83723
+ if (typeof tableName === "string" && this.config.joins.some((join42) => join42.alias === tableName)) {
83815
83724
  throw new Error(`Alias "${tableName}" is already used in this query`);
83816
83725
  }
83817
83726
  if (typeof on2 === "function") {
@@ -87418,7 +87327,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
87418
87327
  const tableName = getTableLikeName2(table62);
87419
87328
  for (const item of extractUsedTable3(table62))
87420
87329
  this.usedTables.add(item);
87421
- if (typeof tableName === "string" && this.config.joins?.some((join32) => join32.alias === tableName)) {
87330
+ if (typeof tableName === "string" && this.config.joins?.some((join42) => join42.alias === tableName)) {
87422
87331
  throw new Error(`Alias "${tableName}" is already used in this query`);
87423
87332
  }
87424
87333
  if (!this.isPartialSelect) {
@@ -91460,7 +91369,7 @@ AND
91460
91369
  const tableName = getTableLikeName2(table62);
91461
91370
  for (const item of extractUsedTable4(table62))
91462
91371
  this.usedTables.add(item);
91463
- if (typeof tableName === "string" && this.config.joins?.some((join32) => join32.alias === tableName)) {
91372
+ if (typeof tableName === "string" && this.config.joins?.some((join42) => join42.alias === tableName)) {
91464
91373
  throw new Error(`Alias "${tableName}" is already used in this query`);
91465
91374
  }
91466
91375
  if (!this.isPartialSelect) {
@@ -162051,10 +161960,10 @@ var require_colorette = __commonJS2((exports) => {
162051
161960
  black,
162052
161961
  red,
162053
161962
  green,
162054
- yellow: yellow2,
161963
+ yellow,
162055
161964
  blue,
162056
161965
  magenta,
162057
- cyan,
161966
+ cyan: cyan2,
162058
161967
  white,
162059
161968
  gray,
162060
161969
  bgBlack,
@@ -162104,7 +162013,7 @@ var require_colorette = __commonJS2((exports) => {
162104
162013
  exports.blueBright = blueBright;
162105
162014
  exports.bold = bold2;
162106
162015
  exports.createColors = createColors;
162107
- exports.cyan = cyan;
162016
+ exports.cyan = cyan2;
162108
162017
  exports.cyanBright = cyanBright;
162109
162018
  exports.dim = dim2;
162110
162019
  exports.gray = gray;
@@ -162123,7 +162032,7 @@ var require_colorette = __commonJS2((exports) => {
162123
162032
  exports.underline = underline;
162124
162033
  exports.white = white;
162125
162034
  exports.whiteBright = whiteBright;
162126
- exports.yellow = yellow2;
162035
+ exports.yellow = yellow;
162127
162036
  exports.yellowBright = yellowBright;
162128
162037
  });
162129
162038
  var require_dist22 = __commonJS2((exports) => {
@@ -175066,7 +174975,7 @@ function hasTimebackCredentials() {
175066
174975
  return false;
175067
174976
  }
175068
174977
  function hasTimebackFullConfig() {
175069
- return hasTimebackCredentials() && !!(config.timeback.courseId && config.timeback.studentId);
174978
+ return hasTimebackCredentials() && !!(config.timeback.courseId && config.timeback.timebackId);
175070
174979
  }
175071
174980
  function requireTimebackCredentials() {
175072
174981
  if (hasTimebackCredentials())
@@ -175120,11 +175029,36 @@ function configureTimeback(options) {
175120
175029
  config.timeback.courseId = options.courseId;
175121
175030
  process.env.SANDBOX_TIMEBACK_COURSE_ID = options.courseId;
175122
175031
  }
175123
- if (options.studentId) {
175124
- config.timeback.studentId = options.studentId;
175125
- process.env.SANDBOX_TIMEBACK_STUDENT_ID = options.studentId;
175032
+ if (options.timebackId) {
175033
+ config.timeback.timebackId = options.timebackId;
175034
+ process.env.SANDBOX_TIMEBACK_STUDENT_ID = options.timebackId;
175035
+ const isMockMode = options.timebackId === "mock";
175036
+ process.env.SANDBOX_TIMEBACK_MOCK_MODE = isMockMode ? "true" : "false";
175037
+ }
175038
+ if (options.organization) {
175039
+ config.timeback.organization = options.organization;
175040
+ process.env.SANDBOX_TIMEBACK_ORG_ID = options.organization.id;
175041
+ if (options.organization.name) {
175042
+ process.env.SANDBOX_TIMEBACK_ORG_NAME = options.organization.name;
175043
+ }
175044
+ if (options.organization.type) {
175045
+ process.env.SANDBOX_TIMEBACK_ORG_TYPE = options.organization.type;
175046
+ }
175047
+ }
175048
+ if (options.role) {
175049
+ config.timeback.role = options.role;
175050
+ process.env.SANDBOX_TIMEBACK_ROLE = options.role;
175126
175051
  }
175127
175052
  }
175053
+ function getTimebackDisplayMode() {
175054
+ const { timebackId, mode } = config.timeback;
175055
+ if (!timebackId)
175056
+ return null;
175057
+ if (timebackId === "mock" || !hasTimebackCredentials()) {
175058
+ return "mock";
175059
+ }
175060
+ return mode;
175061
+ }
175128
175062
  function setEmbeddedMode(embedded) {
175129
175063
  config.embedded = embedded;
175130
175064
  process.env.PLAYCADEMY_EMBEDDED = embedded ? "1" : undefined;
@@ -175143,12 +175077,199 @@ var config = {
175143
175077
  clientSecret: process.env.TIMEBACK_API_CLIENT_SECRET,
175144
175078
  authUrl: process.env.TIMEBACK_API_AUTH_URL,
175145
175079
  courseId: process.env.SANDBOX_TIMEBACK_COURSE_ID,
175146
- studentId: process.env.SANDBOX_TIMEBACK_STUDENT_ID
175080
+ timebackId: process.env.SANDBOX_TIMEBACK_STUDENT_ID
175147
175081
  }
175148
175082
  };
175149
175083
  process.env.BETTER_AUTH_SECRET = config.auth.betterAuthSecret;
175150
175084
  process.env.GAME_JWT_SECRET = config.auth.gameJwtSecret;
175151
175085
  process.env.PUBLIC_IS_LOCAL = "true";
175086
+ var now = new Date;
175087
+ var DEMO_USER_IDS = {
175088
+ player: "00000000-0000-0000-0000-000000000001",
175089
+ developer: "00000000-0000-0000-0000-000000000002",
175090
+ admin: "00000000-0000-0000-0000-000000000003",
175091
+ pendingDeveloper: "00000000-0000-0000-0000-000000000004",
175092
+ unverifiedPlayer: "00000000-0000-0000-0000-000000000005"
175093
+ };
175094
+ var DEMO_USERS = {
175095
+ admin: {
175096
+ id: DEMO_USER_IDS.admin,
175097
+ name: "Admin User",
175098
+ username: "admin_user",
175099
+ email: "admin@playcademy.com",
175100
+ emailVerified: true,
175101
+ image: null,
175102
+ role: "admin",
175103
+ developerStatus: "approved",
175104
+ createdAt: now,
175105
+ updatedAt: now
175106
+ },
175107
+ player: {
175108
+ id: DEMO_USER_IDS.player,
175109
+ name: "Player User",
175110
+ username: "player_user",
175111
+ email: "player@playcademy.com",
175112
+ emailVerified: true,
175113
+ image: null,
175114
+ role: "player",
175115
+ developerStatus: "none",
175116
+ createdAt: now,
175117
+ updatedAt: now
175118
+ },
175119
+ developer: {
175120
+ id: DEMO_USER_IDS.developer,
175121
+ name: "Developer User",
175122
+ username: "developer_user",
175123
+ email: "developer@playcademy.com",
175124
+ emailVerified: true,
175125
+ image: null,
175126
+ role: "developer",
175127
+ developerStatus: "approved",
175128
+ createdAt: now,
175129
+ updatedAt: now
175130
+ },
175131
+ pendingDeveloper: {
175132
+ id: DEMO_USER_IDS.pendingDeveloper,
175133
+ name: "Pending Developer",
175134
+ username: "pending_dev",
175135
+ email: "pending@playcademy.com",
175136
+ emailVerified: true,
175137
+ image: null,
175138
+ role: "developer",
175139
+ developerStatus: "pending",
175140
+ createdAt: now,
175141
+ updatedAt: now
175142
+ },
175143
+ unverifiedPlayer: {
175144
+ id: DEMO_USER_IDS.unverifiedPlayer,
175145
+ name: "Unverified Player",
175146
+ username: "unverified_player",
175147
+ email: "unverified@playcademy.com",
175148
+ emailVerified: false,
175149
+ image: null,
175150
+ role: "player",
175151
+ developerStatus: "none",
175152
+ createdAt: now,
175153
+ updatedAt: now
175154
+ }
175155
+ };
175156
+ var DEMO_USER = DEMO_USERS.player;
175157
+ var DEMO_TOKENS = {
175158
+ "sandbox-demo-token": DEMO_USERS.player,
175159
+ "sandbox-admin-token": DEMO_USERS.admin,
175160
+ "sandbox-player-token": DEMO_USERS.player,
175161
+ "sandbox-developer-token": DEMO_USERS.developer,
175162
+ "sandbox-pending-dev-token": DEMO_USERS.pendingDeveloper,
175163
+ "sandbox-unverified-token": DEMO_USERS.unverifiedPlayer,
175164
+ "mock-game-token-for-local-dev": DEMO_USERS.player
175165
+ };
175166
+ var DEMO_TOKEN = "sandbox-demo-token";
175167
+ var MOCK_GAME_ID = "mock-game-id-from-template";
175168
+ var DEMO_ITEM_IDS = {
175169
+ playcademyCredits: "10000000-0000-0000-0000-000000000001",
175170
+ foundingMemberBadge: "10000000-0000-0000-0000-000000000002",
175171
+ earlyAdopterBadge: "10000000-0000-0000-0000-000000000003",
175172
+ firstGameBadge: "10000000-0000-0000-0000-000000000004",
175173
+ commonSword: "10000000-0000-0000-0000-000000000005",
175174
+ smallHealthPotion: "10000000-0000-0000-0000-000000000006",
175175
+ smallBackpack: "10000000-0000-0000-0000-000000000007"
175176
+ };
175177
+ var PLAYCADEMY_CREDITS_ID = DEMO_ITEM_IDS.playcademyCredits;
175178
+ var SAMPLE_ITEMS = [
175179
+ {
175180
+ id: PLAYCADEMY_CREDITS_ID,
175181
+ slug: "PLAYCADEMY_CREDITS",
175182
+ gameId: null,
175183
+ displayName: "PLAYCADEMY credits",
175184
+ description: "The main currency used across PLAYCADEMY.",
175185
+ type: "currency",
175186
+ isPlaceable: false,
175187
+ imageUrl: "http://playcademy-sandbox.local/playcademy-credit.png",
175188
+ metadata: {
175189
+ rarity: "common"
175190
+ }
175191
+ },
175192
+ {
175193
+ id: DEMO_ITEM_IDS.foundingMemberBadge,
175194
+ slug: "FOUNDING_MEMBER_BADGE",
175195
+ gameId: null,
175196
+ displayName: "Founding Member Badge",
175197
+ description: "Reserved for founding core team of the PLAYCADEMY platform.",
175198
+ type: "badge",
175199
+ isPlaceable: false,
175200
+ imageUrl: null,
175201
+ metadata: {
175202
+ rarity: "legendary"
175203
+ }
175204
+ },
175205
+ {
175206
+ id: DEMO_ITEM_IDS.earlyAdopterBadge,
175207
+ slug: "EARLY_ADOPTER_BADGE",
175208
+ gameId: null,
175209
+ displayName: "Early Adopter Badge",
175210
+ description: "Awarded to users who joined during the beta phase.",
175211
+ type: "badge",
175212
+ isPlaceable: false,
175213
+ imageUrl: null,
175214
+ metadata: {
175215
+ rarity: "epic"
175216
+ }
175217
+ },
175218
+ {
175219
+ id: DEMO_ITEM_IDS.firstGameBadge,
175220
+ slug: "FIRST_GAME_BADGE",
175221
+ gameId: null,
175222
+ displayName: "First Game Played",
175223
+ description: "Awarded for playing your first game in the Playcademy platform.",
175224
+ type: "badge",
175225
+ isPlaceable: false,
175226
+ imageUrl: "http://playcademy-sandbox.local/first-game-badge.png",
175227
+ metadata: {
175228
+ rarity: "uncommon"
175229
+ }
175230
+ },
175231
+ {
175232
+ id: DEMO_ITEM_IDS.commonSword,
175233
+ slug: "COMMON_SWORD",
175234
+ gameId: null,
175235
+ displayName: "Common Sword",
175236
+ description: "A basic sword, good for beginners.",
175237
+ type: "unlock",
175238
+ isPlaceable: false,
175239
+ imageUrl: "http://playcademy-sandbox.local/common-sword.png",
175240
+ metadata: undefined
175241
+ },
175242
+ {
175243
+ id: DEMO_ITEM_IDS.smallHealthPotion,
175244
+ slug: "SMALL_HEALTH_POTION",
175245
+ gameId: null,
175246
+ displayName: "Small Health Potion",
175247
+ description: "Restores a small amount of health.",
175248
+ type: "other",
175249
+ isPlaceable: false,
175250
+ imageUrl: "http://playcademy-sandbox.local/small-health-potion.png",
175251
+ metadata: undefined
175252
+ },
175253
+ {
175254
+ id: DEMO_ITEM_IDS.smallBackpack,
175255
+ slug: "SMALL_BACKPACK",
175256
+ gameId: null,
175257
+ displayName: "Small Backpack",
175258
+ description: "Increases your inventory capacity by 5 slots.",
175259
+ type: "upgrade",
175260
+ isPlaceable: false,
175261
+ imageUrl: "http://playcademy-sandbox.local/small-backpack.png",
175262
+ metadata: undefined
175263
+ }
175264
+ ];
175265
+ var SAMPLE_INVENTORY = [
175266
+ {
175267
+ id: "20000000-0000-0000-0000-000000000001",
175268
+ userId: DEMO_USER.id,
175269
+ itemId: PLAYCADEMY_CREDITS_ID,
175270
+ quantity: 1000
175271
+ }
175272
+ ];
175152
175273
  var RequestError = class extends Error {
175153
175274
  constructor(message3, options) {
175154
175275
  super(message3, options);
@@ -175684,9 +175805,99 @@ var serve = (options, listeningListener) => {
175684
175805
  });
175685
175806
  return server;
175686
175807
  };
175808
+ function getRegistryPath() {
175809
+ const home = homedir();
175810
+ const dir = join2(home, ".playcademy");
175811
+ if (!existsSync2(dir)) {
175812
+ mkdirSync2(dir, { recursive: true });
175813
+ }
175814
+ return join2(dir, ".proc");
175815
+ }
175816
+ function readRegistry() {
175817
+ const registryPath = getRegistryPath();
175818
+ if (!existsSync2(registryPath)) {
175819
+ return {};
175820
+ }
175821
+ try {
175822
+ const content = readFileSync(registryPath, "utf-8");
175823
+ return JSON.parse(content);
175824
+ } catch {
175825
+ return {};
175826
+ }
175827
+ }
175828
+ function writeRegistry(registry) {
175829
+ const registryPath = getRegistryPath();
175830
+ writeFileSync(registryPath, JSON.stringify(registry, null, 2), "utf-8");
175831
+ }
175832
+ function getServerKey(type, port) {
175833
+ return `${type}-${port}`;
175834
+ }
175835
+ function writeServerInfo(type, info2) {
175836
+ const registry = readRegistry();
175837
+ const key = getServerKey(type, info2.port);
175838
+ registry[key] = info2;
175839
+ writeRegistry(registry);
175840
+ }
175841
+ function cleanupServerInfo(type, projectRoot, pid) {
175842
+ const registry = readRegistry();
175843
+ const keysToRemove = [];
175844
+ for (const [key, info2] of Object.entries(registry)) {
175845
+ if (key.startsWith(`${type}-`)) {
175846
+ let matches = true;
175847
+ if (projectRoot && info2.projectRoot !== projectRoot) {
175848
+ matches = false;
175849
+ }
175850
+ if (pid !== undefined && info2.pid !== pid) {
175851
+ matches = false;
175852
+ }
175853
+ if (matches) {
175854
+ keysToRemove.push(key);
175855
+ }
175856
+ }
175857
+ }
175858
+ for (const key of keysToRemove) {
175859
+ delete registry[key];
175860
+ }
175861
+ if (keysToRemove.length > 0) {
175862
+ writeRegistry(registry);
175863
+ }
175864
+ }
175865
+ async function isPortInUse(port) {
175866
+ return new Promise((resolve2) => {
175867
+ const server = createServer();
175868
+ server.once("error", () => {
175869
+ resolve2(true);
175870
+ });
175871
+ server.once("listening", () => {
175872
+ server.close();
175873
+ resolve2(false);
175874
+ });
175875
+ server.listen(port);
175876
+ });
175877
+ }
175878
+ async function waitForPort(port, timeoutMs = 5000) {
175879
+ const start2 = Date.now();
175880
+ while (await isPortInUse(port)) {
175881
+ if (Date.now() - start2 > timeoutMs) {
175882
+ throw new Error(`Port ${port} is already in use.
175883
+ Stop the other server or specify a different port with --port <number>.`);
175884
+ }
175885
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
175886
+ }
175887
+ }
175888
+ async function requirePortAvailable(port, timeoutMs = 100) {
175889
+ const start2 = Date.now();
175890
+ while (await isPortInUse(port)) {
175891
+ if (Date.now() - start2 > timeoutMs) {
175892
+ throw new Error(`Port ${port} is already in use.
175893
+ Stop the other server or specify a different port with --port <number>.`);
175894
+ }
175895
+ await new Promise((resolve2) => setTimeout(resolve2, 50));
175896
+ }
175897
+ }
175687
175898
  var package_default = {
175688
175899
  name: "@playcademy/sandbox",
175689
- version: "0.2.0",
175900
+ version: "0.3.0",
175690
175901
  description: "Local development server for Playcademy game development",
175691
175902
  type: "module",
175692
175903
  exports: {
@@ -175701,6 +175912,10 @@ var package_default = {
175701
175912
  "./config": {
175702
175913
  import: "./dist/config.js",
175703
175914
  types: "./dist/config.d.ts"
175915
+ },
175916
+ "./constants": {
175917
+ import: "./dist/constants.js",
175918
+ types: "./dist/constants.d.ts"
175704
175919
  }
175705
175920
  },
175706
175921
  bin: {
@@ -178130,7 +178345,7 @@ function sql(strings, ...params) {
178130
178345
  return new SQL([new StringChunk(str)]);
178131
178346
  }
178132
178347
  sql22.raw = raw2;
178133
- function join5(chunks, separator) {
178348
+ function join22(chunks, separator) {
178134
178349
  const result = [];
178135
178350
  for (const [i22, chunk] of chunks.entries()) {
178136
178351
  if (i22 > 0 && separator !== undefined) {
@@ -178140,7 +178355,7 @@ function sql(strings, ...params) {
178140
178355
  }
178141
178356
  return new SQL(result);
178142
178357
  }
178143
- sql22.join = join5;
178358
+ sql22.join = join22;
178144
178359
  function identifier(value) {
178145
178360
  return new Name(value);
178146
178361
  }
@@ -181053,7 +181268,7 @@ class PgSelectQueryBuilderBase extends TypedQueryBuilder {
181053
181268
  return (table, on) => {
181054
181269
  const baseTableName = this.tableName;
181055
181270
  const tableName = getTableLikeName(table);
181056
- if (typeof tableName === "string" && this.config.joins?.some((join5) => join5.alias === tableName)) {
181271
+ if (typeof tableName === "string" && this.config.joins?.some((join22) => join22.alias === tableName)) {
181057
181272
  throw new Error(`Alias "${tableName}" is already used in this query`);
181058
181273
  }
181059
181274
  if (!this.isPartialSelect) {
@@ -181558,7 +181773,7 @@ class PgUpdateBase extends QueryPromise {
181558
181773
  createJoin(joinType) {
181559
181774
  return (table, on) => {
181560
181775
  const tableName = getTableLikeName(table);
181561
- if (typeof tableName === "string" && this.config.joins.some((join5) => join5.alias === tableName)) {
181776
+ if (typeof tableName === "string" && this.config.joins.some((join22) => join22.alias === tableName)) {
181562
181777
  throw new Error(`Alias "${tableName}" is already used in this query`);
181563
181778
  }
181564
181779
  if (typeof on === "function") {
@@ -181608,10 +181823,10 @@ class PgUpdateBase extends QueryPromise {
181608
181823
  const fromFields = this.getTableLikeFields(this.config.from);
181609
181824
  fields[tableName] = fromFields;
181610
181825
  }
181611
- for (const join5 of this.config.joins) {
181612
- const tableName2 = getTableLikeName(join5.table);
181613
- if (typeof tableName2 === "string" && !is(join5.table, SQL)) {
181614
- const fromFields = this.getTableLikeFields(join5.table);
181826
+ for (const join22 of this.config.joins) {
181827
+ const tableName2 = getTableLikeName(join22.table);
181828
+ if (typeof tableName2 === "string" && !is(join22.table, SQL)) {
181829
+ const fromFields = this.getTableLikeFields(join22.table);
181615
181830
  fields[tableName2] = fromFields;
181616
181831
  }
181617
181832
  }
@@ -182732,217 +182947,59 @@ var notificationsRelations = relations(notifications, ({ one }) => ({
182732
182947
  references: [users.id]
182733
182948
  })
182734
182949
  }));
182735
- var now = new Date;
182736
- var DEMO_USER_IDS = {
182737
- admin: "00000000-0000-0000-0000-000000000001",
182738
- player: "00000000-0000-0000-0000-000000000002",
182739
- developer: "00000000-0000-0000-0000-000000000003",
182740
- pendingDeveloper: "00000000-0000-0000-0000-000000000004",
182741
- unverifiedPlayer: "00000000-0000-0000-0000-000000000005"
182742
- };
182743
- var DEMO_USERS = {
182744
- admin: {
182745
- id: DEMO_USER_IDS.admin,
182746
- name: "Admin User",
182747
- username: "admin_user",
182748
- email: "admin@playcademy.com",
182749
- emailVerified: true,
182750
- image: null,
182751
- role: "admin",
182752
- developerStatus: "approved",
182753
- createdAt: now,
182754
- updatedAt: now
182755
- },
182756
- player: {
182757
- id: DEMO_USER_IDS.player,
182758
- name: "Player User",
182759
- username: "player_user",
182760
- email: "player@playcademy.com",
182761
- emailVerified: true,
182762
- image: null,
182763
- role: "player",
182764
- developerStatus: "none",
182765
- createdAt: now,
182766
- updatedAt: now
182767
- },
182768
- developer: {
182769
- id: DEMO_USER_IDS.developer,
182770
- name: "Developer User",
182771
- username: "developer_user",
182772
- email: "developer@playcademy.com",
182773
- emailVerified: true,
182774
- image: null,
182775
- role: "developer",
182776
- developerStatus: "approved",
182777
- createdAt: now,
182778
- updatedAt: now
182779
- },
182780
- pendingDeveloper: {
182781
- id: DEMO_USER_IDS.pendingDeveloper,
182782
- name: "Pending Developer",
182783
- username: "pending_dev",
182784
- email: "pending@playcademy.com",
182785
- emailVerified: true,
182786
- image: null,
182787
- role: "developer",
182788
- developerStatus: "pending",
182789
- createdAt: now,
182790
- updatedAt: now
182791
- },
182792
- unverifiedPlayer: {
182793
- id: DEMO_USER_IDS.unverifiedPlayer,
182794
- name: "Unverified Player",
182795
- username: "unverified_player",
182796
- email: "unverified@playcademy.com",
182797
- emailVerified: false,
182798
- image: null,
182799
- role: "player",
182800
- developerStatus: "none",
182801
- createdAt: now,
182802
- updatedAt: now
182950
+ function extractBearerToken(authHeader) {
182951
+ if (!authHeader?.startsWith("Bearer ")) {
182952
+ return null;
182803
182953
  }
182804
- };
182805
- var DEMO_USER = DEMO_USERS.admin;
182806
- var DEMO_TOKENS = {
182807
- "sandbox-demo-token": DEMO_USERS.admin,
182808
- "sandbox-admin-token": DEMO_USERS.admin,
182809
- "sandbox-player-token": DEMO_USERS.player,
182810
- "sandbox-developer-token": DEMO_USERS.developer,
182811
- "sandbox-pending-dev-token": DEMO_USERS.pendingDeveloper,
182812
- "sandbox-unverified-token": DEMO_USERS.unverifiedPlayer,
182813
- "mock-game-token-for-local-dev": DEMO_USERS.admin
182814
- };
182815
- var MOCK_GAME_ID = "mock-game-id-from-template";
182816
- var DEMO_ITEM_IDS = {
182817
- playcademyCredits: "10000000-0000-0000-0000-000000000001",
182818
- foundingMemberBadge: "10000000-0000-0000-0000-000000000002",
182819
- earlyAdopterBadge: "10000000-0000-0000-0000-000000000003",
182820
- firstGameBadge: "10000000-0000-0000-0000-000000000004",
182821
- commonSword: "10000000-0000-0000-0000-000000000005",
182822
- smallHealthPotion: "10000000-0000-0000-0000-000000000006",
182823
- smallBackpack: "10000000-0000-0000-0000-000000000007"
182824
- };
182825
- var PLAYCADEMY_CREDITS_ID = DEMO_ITEM_IDS.playcademyCredits;
182826
- var SAMPLE_ITEMS = [
182827
- {
182828
- id: PLAYCADEMY_CREDITS_ID,
182829
- slug: "PLAYCADEMY_CREDITS",
182830
- gameId: null,
182831
- displayName: "PLAYCADEMY credits",
182832
- description: "The main currency used across PLAYCADEMY.",
182833
- type: "currency",
182834
- isPlaceable: false,
182835
- imageUrl: "http://playcademy-sandbox.local/playcademy-credit.png",
182836
- metadata: {
182837
- rarity: "common"
182838
- }
182839
- },
182840
- {
182841
- id: DEMO_ITEM_IDS.foundingMemberBadge,
182842
- slug: "FOUNDING_MEMBER_BADGE",
182843
- gameId: null,
182844
- displayName: "Founding Member Badge",
182845
- description: "Reserved for founding core team of the PLAYCADEMY platform.",
182846
- type: "badge",
182847
- isPlaceable: false,
182848
- imageUrl: null,
182849
- metadata: {
182850
- rarity: "legendary"
182954
+ return authHeader.substring(7);
182955
+ }
182956
+ function parseSandboxToken(token) {
182957
+ try {
182958
+ const parts2 = token.split(".");
182959
+ if (parts2.length !== 3 || parts2[2] !== "sandbox") {
182960
+ return null;
182851
182961
  }
182852
- },
182853
- {
182854
- id: DEMO_ITEM_IDS.earlyAdopterBadge,
182855
- slug: "EARLY_ADOPTER_BADGE",
182856
- gameId: null,
182857
- displayName: "Early Adopter Badge",
182858
- description: "Awarded to users who joined during the beta phase.",
182859
- type: "badge",
182860
- isPlaceable: false,
182861
- imageUrl: null,
182862
- metadata: {
182863
- rarity: "epic"
182962
+ const header = JSON.parse(atob(parts2[0]));
182963
+ if (header.typ !== "sandbox") {
182964
+ return null;
182864
182965
  }
182865
- },
182866
- {
182867
- id: DEMO_ITEM_IDS.firstGameBadge,
182868
- slug: "FIRST_GAME_BADGE",
182869
- gameId: null,
182870
- displayName: "First Game Played",
182871
- description: "Awarded for playing your first game in the Playcademy platform.",
182872
- type: "badge",
182873
- isPlaceable: false,
182874
- imageUrl: "http://playcademy-sandbox.local/first-game-badge.png",
182875
- metadata: {
182876
- rarity: "uncommon"
182966
+ const payload = JSON.parse(atob(parts2[1]));
182967
+ if (!payload.uid || !payload.sub) {
182968
+ return null;
182877
182969
  }
182878
- },
182879
- {
182880
- id: DEMO_ITEM_IDS.commonSword,
182881
- slug: "COMMON_SWORD",
182882
- gameId: null,
182883
- displayName: "Common Sword",
182884
- description: "A basic sword, good for beginners.",
182885
- type: "unlock",
182886
- isPlaceable: false,
182887
- imageUrl: "http://playcademy-sandbox.local/common-sword.png",
182888
- metadata: undefined
182889
- },
182890
- {
182891
- id: DEMO_ITEM_IDS.smallHealthPotion,
182892
- slug: "SMALL_HEALTH_POTION",
182893
- gameId: null,
182894
- displayName: "Small Health Potion",
182895
- description: "Restores a small amount of health.",
182896
- type: "other",
182897
- isPlaceable: false,
182898
- imageUrl: "http://playcademy-sandbox.local/small-health-potion.png",
182899
- metadata: undefined
182900
- },
182901
- {
182902
- id: DEMO_ITEM_IDS.smallBackpack,
182903
- slug: "SMALL_BACKPACK",
182904
- gameId: null,
182905
- displayName: "Small Backpack",
182906
- description: "Increases your inventory capacity by 5 slots.",
182907
- type: "upgrade",
182908
- isPlaceable: false,
182909
- imageUrl: "http://playcademy-sandbox.local/small-backpack.png",
182910
- metadata: undefined
182911
- }
182912
- ];
182913
- var SAMPLE_INVENTORY = [
182914
- {
182915
- id: "20000000-0000-0000-0000-000000000001",
182916
- userId: DEMO_USER.id,
182917
- itemId: PLAYCADEMY_CREDITS_ID,
182918
- quantity: 1000
182919
- }
182920
- ];
182921
- function extractBearerToken(authHeader) {
182922
- if (!authHeader?.startsWith("Bearer ")) {
182970
+ return {
182971
+ userId: payload.uid,
182972
+ gameSlug: payload.sub
182973
+ };
182974
+ } catch {
182923
182975
  return null;
182924
182976
  }
182925
- return authHeader.substring(7);
182926
182977
  }
182927
- function parseJwtUserId(token) {
182978
+ function parseJwtClaims(token) {
182928
182979
  try {
182929
182980
  const parts2 = token.split(".");
182930
182981
  if (parts2.length === 3 && parts2[1]) {
182931
182982
  const payload = JSON.parse(atob(parts2[1]));
182932
- return payload.uid || null;
182983
+ if (payload.uid) {
182984
+ return {
182985
+ userId: payload.uid,
182986
+ gameId: payload.sub
182987
+ };
182988
+ }
182933
182989
  }
182934
- } catch (error2) {
182935
- console.warn("[Auth] Failed to decode JWT token:", error2);
182936
- }
182990
+ } catch {}
182937
182991
  return null;
182938
182992
  }
182939
- function resolveUserId(token) {
182993
+ function resolveAuth(token) {
182940
182994
  const demoUser = DEMO_TOKENS[token];
182941
- if (demoUser) {
182942
- return demoUser.id;
182943
- }
182995
+ if (demoUser)
182996
+ return { userId: demoUser.id };
182944
182997
  if (token.includes(".")) {
182945
- return parseJwtUserId(token);
182998
+ const sandboxClaims = parseSandboxToken(token);
182999
+ if (sandboxClaims) {
183000
+ return sandboxClaims;
183001
+ }
183002
+ return parseJwtClaims(token);
182946
183003
  }
182947
183004
  return null;
182948
183005
  }
@@ -182957,6 +183014,18 @@ async function fetchUserFromDatabase(db, userId) {
182957
183014
  throw error2;
182958
183015
  }
182959
183016
  }
183017
+ async function resolveGameIdFromSlug(db, slug) {
183018
+ try {
183019
+ const game = await db.query.games.findFirst({
183020
+ where: eq(games.slug, slug),
183021
+ columns: { id: true }
183022
+ });
183023
+ return game?.id || null;
183024
+ } catch (error2) {
183025
+ console.error("[Auth] Error looking up game by slug:", error2);
183026
+ return null;
183027
+ }
183028
+ }
182960
183029
  function isPublicRoute(path4, exceptions) {
182961
183030
  return exceptions.some((exception) => {
182962
183031
  if (path4 === exception)
@@ -182978,11 +183047,11 @@ async function authenticateRequest(c3) {
182978
183047
  shouldReturn404: true
182979
183048
  };
182980
183049
  }
182981
- let targetUserId;
183050
+ let claims;
182982
183051
  if (apiKey && !bearerToken) {
182983
- targetUserId = DEMO_USERS.admin.id;
183052
+ claims = { userId: DEMO_USERS.admin.id };
182984
183053
  } else {
182985
- const resolved = resolveUserId(token);
183054
+ const resolved = resolveAuth(token);
182986
183055
  if (!resolved) {
182987
183056
  return {
182988
183057
  success: false,
@@ -182990,26 +183059,22 @@ async function authenticateRequest(c3) {
182990
183059
  shouldReturn404: true
182991
183060
  };
182992
183061
  }
182993
- targetUserId = resolved;
183062
+ claims = resolved;
182994
183063
  }
182995
183064
  const db = c3.get("db");
182996
183065
  if (!db) {
182997
183066
  console.error("[Auth] Database not available in context");
182998
- return {
182999
- success: false,
183000
- error: "Internal server error",
183001
- shouldReturn404: false
183002
- };
183067
+ return { success: false, error: "Internal server error", shouldReturn404: false };
183003
183068
  }
183004
- const user = await fetchUserFromDatabase(db, targetUserId);
183069
+ const user = await fetchUserFromDatabase(db, claims.userId);
183005
183070
  if (!user) {
183006
- return {
183007
- success: false,
183008
- error: "User not found or token invalid",
183009
- shouldReturn404: true
183010
- };
183071
+ return { success: false, error: "User not found or token invalid", shouldReturn404: true };
183011
183072
  }
183012
- return { success: true, user };
183073
+ let gameId = claims.gameId;
183074
+ if (!gameId && claims.gameSlug) {
183075
+ gameId = await resolveGameIdFromSlug(db, claims.gameSlug) ?? undefined;
183076
+ }
183077
+ return { success: true, user, gameId };
183013
183078
  }
183014
183079
  function setupAuth(options = {}) {
183015
183080
  const { exceptions = [] } = options;
@@ -183021,6 +183086,8 @@ function setupAuth(options = {}) {
183021
183086
  const result = await authenticateRequest(c3);
183022
183087
  if (result.success) {
183023
183088
  c3.set("user", result.user);
183089
+ if (result.gameId)
183090
+ c3.set("gameId", result.gameId);
183024
183091
  await next();
183025
183092
  return;
183026
183093
  }
@@ -188861,31 +188928,31 @@ function getDatabase() {
188861
188928
  }
188862
188929
 
188863
188930
  class DatabasePathManager {
188864
- static DEFAULT_DB_SUBPATH = join22("@playcademy", "vite-plugin", "node_modules", ".playcademy", "sandbox.db");
188931
+ static DEFAULT_DB_SUBPATH = join3("@playcademy", "vite-plugin", "node_modules", ".playcademy", "sandbox.db");
188865
188932
  static findNodeModulesPath() {
188866
188933
  let currentDir = process.cwd();
188867
188934
  while (currentDir !== dirname(currentDir)) {
188868
- const nodeModulesPath = join22(currentDir, "node_modules");
188935
+ const nodeModulesPath = join3(currentDir, "node_modules");
188869
188936
  if (fs22.existsSync(nodeModulesPath)) {
188870
188937
  return nodeModulesPath;
188871
188938
  }
188872
188939
  currentDir = dirname(currentDir);
188873
188940
  }
188874
- return join22(process.cwd(), "node_modules");
188941
+ return join3(process.cwd(), "node_modules");
188875
188942
  }
188876
188943
  static resolveDatabasePath(customPath) {
188877
188944
  if (customPath) {
188878
188945
  if (customPath === ":memory:")
188879
188946
  return ":memory:";
188880
- return isAbsolute(customPath) ? customPath : join22(process.cwd(), customPath);
188947
+ return isAbsolute(customPath) ? customPath : join3(process.cwd(), customPath);
188881
188948
  }
188882
- return join22(this.findNodeModulesPath(), this.DEFAULT_DB_SUBPATH);
188949
+ return join3(this.findNodeModulesPath(), this.DEFAULT_DB_SUBPATH);
188883
188950
  }
188884
188951
  static ensureDatabaseDirectory(dbPath) {
188885
188952
  if (dbPath === ":memory:")
188886
188953
  return;
188887
188954
  const dirPath = dirname(dbPath);
188888
- const absolutePath = isAbsolute(dirPath) ? dirPath : join22(process.cwd(), dirPath);
188955
+ const absolutePath = isAbsolute(dirPath) ? dirPath : join3(process.cwd(), dirPath);
188889
188956
  try {
188890
188957
  if (!fs22.existsSync(absolutePath)) {
188891
188958
  fs22.mkdirSync(absolutePath, { recursive: true });
@@ -189196,7 +189263,51 @@ var init_overworld = __esm3(() => {
189196
189263
  FIRST_GAME: ITEM_SLUGS2.FIRST_GAME_BADGE
189197
189264
  };
189198
189265
  });
189199
- var init_timeback = () => {};
189266
+ var TIMEBACK_ORG_SOURCED_ID = "PLAYCADEMY";
189267
+ var TIMEBACK_ORG_NAME = "Playcademy Studios";
189268
+ var TIMEBACK_ORG_TYPE = "department";
189269
+ var TIMEBACK_COURSE_DEFAULTS;
189270
+ var TIMEBACK_RESOURCE_DEFAULTS;
189271
+ var TIMEBACK_COMPONENT_DEFAULTS;
189272
+ var TIMEBACK_COMPONENT_RESOURCE_DEFAULTS;
189273
+ var init_timeback = __esm3(() => {
189274
+ TIMEBACK_COURSE_DEFAULTS = {
189275
+ gradingScheme: "STANDARD",
189276
+ level: {
189277
+ elementary: "Elementary",
189278
+ middle: "Middle",
189279
+ high: "High",
189280
+ ap: "AP"
189281
+ },
189282
+ goals: {
189283
+ dailyXp: 50,
189284
+ dailyLessons: 3
189285
+ },
189286
+ metrics: {
189287
+ totalXp: 1000,
189288
+ totalLessons: 50
189289
+ }
189290
+ };
189291
+ TIMEBACK_RESOURCE_DEFAULTS = {
189292
+ vendorId: "playcademy",
189293
+ roles: ["primary"],
189294
+ importance: "primary",
189295
+ metadata: {
189296
+ type: "interactive",
189297
+ toolProvider: "Playcademy",
189298
+ instructionalMethod: "exploratory",
189299
+ language: "en-US"
189300
+ }
189301
+ };
189302
+ TIMEBACK_COMPONENT_DEFAULTS = {
189303
+ sortOrder: 1,
189304
+ prerequisiteCriteria: "ALL"
189305
+ };
189306
+ TIMEBACK_COMPONENT_RESOURCE_DEFAULTS = {
189307
+ sortOrder: 1,
189308
+ lessonType: "quiz"
189309
+ };
189310
+ });
189200
189311
  var init_workers = () => {};
189201
189312
  var init_src2 = __esm3(() => {
189202
189313
  init_auth();
@@ -189237,7 +189348,6 @@ var HTTP_DEFAULTS;
189237
189348
  var AUTH_DEFAULTS;
189238
189349
  var CACHE_DEFAULTS;
189239
189350
  var CONFIG_DEFAULTS;
189240
- var DEFAULT_PLAYCADEMY_ORGANIZATION_ID;
189241
189351
  var PLAYCADEMY_DEFAULTS;
189242
189352
  var RESOURCE_DEFAULTS;
189243
189353
  var HTTP_STATUS;
@@ -189384,54 +189494,26 @@ var init_constants = __esm3(() => {
189384
189494
  CONFIG_DEFAULTS = {
189385
189495
  fileNames: ["timeback.config.js", "timeback.config.json"]
189386
189496
  };
189387
- DEFAULT_PLAYCADEMY_ORGANIZATION_ID = process.env.TIMEBACK_ORG_SOURCE_ID || "PLAYCADEMY";
189388
189497
  PLAYCADEMY_DEFAULTS = {
189389
- organization: DEFAULT_PLAYCADEMY_ORGANIZATION_ID,
189498
+ organization: TIMEBACK_ORG_SOURCED_ID,
189390
189499
  launchBaseUrls: PLAYCADEMY_BASE_URLS
189391
189500
  };
189392
189501
  RESOURCE_DEFAULTS = {
189393
189502
  organization: {
189394
- name: "Playcademy Studios",
189395
- type: "department"
189503
+ name: TIMEBACK_ORG_NAME,
189504
+ type: TIMEBACK_ORG_TYPE
189396
189505
  },
189397
189506
  course: {
189398
- gradingScheme: "STANDARD",
189399
- level: {
189400
- elementary: "Elementary",
189401
- middle: "Middle",
189402
- high: "High",
189403
- ap: "AP"
189404
- },
189405
- metadata: {
189406
- goals: {
189407
- dailyXp: 50,
189408
- dailyLessons: 3
189409
- },
189410
- metrics: {
189411
- totalXp: 1000,
189412
- totalLessons: 50
189413
- }
189414
- }
189415
- },
189416
- component: {
189417
- sortOrder: 1,
189418
- prerequisiteCriteria: "ALL"
189419
- },
189420
- resource: {
189421
- vendorId: "playcademy",
189422
- roles: ["primary"],
189423
- importance: "primary",
189507
+ gradingScheme: TIMEBACK_COURSE_DEFAULTS.gradingScheme,
189508
+ level: TIMEBACK_COURSE_DEFAULTS.level,
189424
189509
  metadata: {
189425
- type: "interactive",
189426
- toolProvider: "Playcademy",
189427
- instructionalMethod: "exploratory",
189428
- language: "en-US"
189510
+ goals: TIMEBACK_COURSE_DEFAULTS.goals,
189511
+ metrics: TIMEBACK_COURSE_DEFAULTS.metrics
189429
189512
  }
189430
189513
  },
189431
- componentResource: {
189432
- sortOrder: 1,
189433
- lessonType: "quiz"
189434
- }
189514
+ component: TIMEBACK_COMPONENT_DEFAULTS,
189515
+ resource: TIMEBACK_RESOURCE_DEFAULTS,
189516
+ componentResource: TIMEBACK_COMPONENT_RESOURCE_DEFAULTS
189435
189517
  };
189436
189518
  HTTP_STATUS = {
189437
189519
  CLIENT_ERROR_MIN: 400,
@@ -195440,7 +195522,8 @@ class TimebackClient {
195440
195522
  courseId: enrollment.course.id,
195441
195523
  status: "active",
195442
195524
  grades,
195443
- subjects
195525
+ subjects,
195526
+ school: enrollment.school
195444
195527
  };
195445
195528
  });
195446
195529
  this.cacheManager.setEnrollments(studentId, enrollments);
@@ -195511,7 +195594,51 @@ var init_overworld2 = __esm4(() => {
195511
195594
  FIRST_GAME: ITEM_SLUGS3.FIRST_GAME_BADGE
195512
195595
  };
195513
195596
  });
195514
- var init_timeback2 = () => {};
195597
+ var TIMEBACK_ORG_SOURCED_ID2 = "PLAYCADEMY";
195598
+ var TIMEBACK_ORG_NAME2 = "Playcademy Studios";
195599
+ var TIMEBACK_ORG_TYPE2 = "department";
195600
+ var TIMEBACK_COURSE_DEFAULTS2;
195601
+ var TIMEBACK_RESOURCE_DEFAULTS2;
195602
+ var TIMEBACK_COMPONENT_DEFAULTS2;
195603
+ var TIMEBACK_COMPONENT_RESOURCE_DEFAULTS2;
195604
+ var init_timeback2 = __esm4(() => {
195605
+ TIMEBACK_COURSE_DEFAULTS2 = {
195606
+ gradingScheme: "STANDARD",
195607
+ level: {
195608
+ elementary: "Elementary",
195609
+ middle: "Middle",
195610
+ high: "High",
195611
+ ap: "AP"
195612
+ },
195613
+ goals: {
195614
+ dailyXp: 50,
195615
+ dailyLessons: 3
195616
+ },
195617
+ metrics: {
195618
+ totalXp: 1000,
195619
+ totalLessons: 50
195620
+ }
195621
+ };
195622
+ TIMEBACK_RESOURCE_DEFAULTS2 = {
195623
+ vendorId: "playcademy",
195624
+ roles: ["primary"],
195625
+ importance: "primary",
195626
+ metadata: {
195627
+ type: "interactive",
195628
+ toolProvider: "Playcademy",
195629
+ instructionalMethod: "exploratory",
195630
+ language: "en-US"
195631
+ }
195632
+ };
195633
+ TIMEBACK_COMPONENT_DEFAULTS2 = {
195634
+ sortOrder: 1,
195635
+ prerequisiteCriteria: "ALL"
195636
+ };
195637
+ TIMEBACK_COMPONENT_RESOURCE_DEFAULTS2 = {
195638
+ sortOrder: 1,
195639
+ lessonType: "quiz"
195640
+ };
195641
+ });
195515
195642
  var init_workers2 = () => {};
195516
195643
  var init_src3 = __esm4(() => {
195517
195644
  init_auth2();
@@ -195543,7 +195670,6 @@ var HTTP_DEFAULTS2;
195543
195670
  var AUTH_DEFAULTS2;
195544
195671
  var CACHE_DEFAULTS2;
195545
195672
  var CONFIG_DEFAULTS2;
195546
- var DEFAULT_PLAYCADEMY_ORGANIZATION_ID2;
195547
195673
  var PLAYCADEMY_DEFAULTS2;
195548
195674
  var RESOURCE_DEFAULTS2;
195549
195675
  var HTTP_STATUS2;
@@ -195690,54 +195816,26 @@ var init_constants2 = __esm4(() => {
195690
195816
  CONFIG_DEFAULTS2 = {
195691
195817
  fileNames: ["timeback.config.js", "timeback.config.json"]
195692
195818
  };
195693
- DEFAULT_PLAYCADEMY_ORGANIZATION_ID2 = process.env.TIMEBACK_ORG_SOURCE_ID || "PLAYCADEMY";
195694
195819
  PLAYCADEMY_DEFAULTS2 = {
195695
- organization: DEFAULT_PLAYCADEMY_ORGANIZATION_ID2,
195820
+ organization: TIMEBACK_ORG_SOURCED_ID2,
195696
195821
  launchBaseUrls: PLAYCADEMY_BASE_URLS2
195697
195822
  };
195698
195823
  RESOURCE_DEFAULTS2 = {
195699
195824
  organization: {
195700
- name: "Playcademy Studios",
195701
- type: "department"
195825
+ name: TIMEBACK_ORG_NAME2,
195826
+ type: TIMEBACK_ORG_TYPE2
195702
195827
  },
195703
195828
  course: {
195704
- gradingScheme: "STANDARD",
195705
- level: {
195706
- elementary: "Elementary",
195707
- middle: "Middle",
195708
- high: "High",
195709
- ap: "AP"
195710
- },
195711
- metadata: {
195712
- goals: {
195713
- dailyXp: 50,
195714
- dailyLessons: 3
195715
- },
195716
- metrics: {
195717
- totalXp: 1000,
195718
- totalLessons: 50
195719
- }
195720
- }
195721
- },
195722
- component: {
195723
- sortOrder: 1,
195724
- prerequisiteCriteria: "ALL"
195725
- },
195726
- resource: {
195727
- vendorId: "playcademy",
195728
- roles: ["primary"],
195729
- importance: "primary",
195829
+ gradingScheme: TIMEBACK_COURSE_DEFAULTS2.gradingScheme,
195830
+ level: TIMEBACK_COURSE_DEFAULTS2.level,
195730
195831
  metadata: {
195731
- type: "interactive",
195732
- toolProvider: "Playcademy",
195733
- instructionalMethod: "exploratory",
195734
- language: "en-US"
195832
+ goals: TIMEBACK_COURSE_DEFAULTS2.goals,
195833
+ metrics: TIMEBACK_COURSE_DEFAULTS2.metrics
195735
195834
  }
195736
195835
  },
195737
- componentResource: {
195738
- sortOrder: 1,
195739
- lessonType: "quiz"
195740
- }
195836
+ component: TIMEBACK_COMPONENT_DEFAULTS2,
195837
+ resource: TIMEBACK_RESOURCE_DEFAULTS2,
195838
+ componentResource: TIMEBACK_COMPONENT_RESOURCE_DEFAULTS2
195741
195839
  };
195742
195840
  HTTP_STATUS2 = {
195743
195841
  CLIENT_ERROR_MIN: 400,
@@ -196079,66 +196177,87 @@ function buildResourceMetadata({
196079
196177
  return metadata2;
196080
196178
  }
196081
196179
  init_src();
196082
- async function fetchEnrollmentsForUser(timebackId) {
196083
- const db = getDatabase();
196084
- const isLocal = process.env.PUBLIC_IS_LOCAL === "true";
196085
- if (isLocal) {
196086
- const allIntegrations = await db.query.gameTimebackIntegrations.findMany();
196087
- return allIntegrations.map((integration) => ({
196088
- gameId: integration.gameId,
196089
- grade: integration.grade,
196090
- subject: integration.subject,
196091
- courseId: integration.courseId
196092
- }));
196180
+ async function fetchStudentFromOneRoster(timebackId) {
196181
+ log2.debug("[OneRoster] Fetching student profile", { timebackId });
196182
+ try {
196183
+ const client2 = await getTimebackClient();
196184
+ const user = await client2.oneroster.users.get(timebackId);
196185
+ const primaryRoleEntry = user.roles.find((r22) => r22.roleType === "primary");
196186
+ const role = primaryRoleEntry?.role ?? user.roles[0]?.role ?? "student";
196187
+ const orgMap = new Map;
196188
+ if (user.primaryOrg) {
196189
+ orgMap.set(user.primaryOrg.sourcedId, {
196190
+ id: user.primaryOrg.sourcedId,
196191
+ name: user.primaryOrg.name ?? null,
196192
+ type: user.primaryOrg.type || "school",
196193
+ isPrimary: true
196194
+ });
196195
+ }
196196
+ for (const r22 of user.roles) {
196197
+ if (r22.org && !orgMap.has(r22.org.sourcedId)) {
196198
+ orgMap.set(r22.org.sourcedId, {
196199
+ id: r22.org.sourcedId,
196200
+ name: null,
196201
+ type: "school",
196202
+ isPrimary: false
196203
+ });
196204
+ }
196205
+ }
196206
+ const organizations = Array.from(orgMap.values());
196207
+ return { role, organizations };
196208
+ } catch (error2) {
196209
+ log2.warn("[OneRoster] Failed to fetch student, using defaults", { error: error2, timebackId });
196210
+ return { role: "student", organizations: [] };
196093
196211
  }
196094
- log2.debug("[timeback-enrollments] Fetching student enrollments from TimeBack", { timebackId });
196212
+ }
196213
+ async function fetchEnrollmentsFromEduBridge(timebackId) {
196214
+ const db = getDatabase();
196215
+ log2.debug("[EduBridge] Fetching student enrollments", { timebackId });
196095
196216
  try {
196096
196217
  const client2 = await getTimebackClient();
196097
- const classes = await client2.getEnrollments(timebackId);
196098
- const courseIds = classes.map((cls) => cls.courseId).filter((id) => Boolean(id));
196099
- if (courseIds.length === 0) {
196218
+ const enrollments = await client2.getEnrollments(timebackId);
196219
+ const courseIds = enrollments.map((e2) => e2.courseId).filter((id) => Boolean(id));
196220
+ if (courseIds.length === 0)
196100
196221
  return [];
196101
- }
196222
+ const courseToSchool = new Map(enrollments.filter((e2) => e2.school?.id).map((e2) => [e2.courseId, e2.school.id]));
196102
196223
  const integrations = await db.query.gameTimebackIntegrations.findMany({
196103
196224
  where: inArray(gameTimebackIntegrations.courseId, courseIds)
196104
196225
  });
196105
- return integrations.map((integration) => ({
196106
- gameId: integration.gameId,
196107
- grade: integration.grade,
196108
- subject: integration.subject,
196109
- courseId: integration.courseId
196226
+ return integrations.map((i32) => ({
196227
+ gameId: i32.gameId,
196228
+ grade: i32.grade,
196229
+ subject: i32.subject,
196230
+ courseId: i32.courseId,
196231
+ orgId: courseToSchool.get(i32.courseId)
196110
196232
  }));
196111
196233
  } catch (error2) {
196112
- log2.warn("[timeback-enrollments] Failed to fetch TimeBack enrollments:", {
196113
- error: error2,
196114
- timebackId
196115
- });
196234
+ log2.warn("[EduBridge] Failed to fetch enrollments", { error: error2, timebackId });
196116
196235
  return [];
196117
196236
  }
196118
196237
  }
196119
- async function fetchUserRole(timebackId) {
196120
- log2.debug("[timeback] Fetching user role from TimeBack", { timebackId });
196121
- try {
196122
- const client2 = await getTimebackClient();
196123
- const user = await client2.oneroster.users.get(timebackId);
196124
- const primaryRole = user.roles.find((r22) => r22.roleType === "primary");
196125
- const role = primaryRole?.role ?? user.roles[0]?.role ?? "student";
196126
- log2.debug("[timeback] Resolved user role", { timebackId, role });
196127
- return role;
196128
- } catch (error2) {
196129
- log2.warn("[timeback] Failed to fetch user role, defaulting to student:", {
196130
- error: error2,
196131
- timebackId
196132
- });
196133
- return "student";
196134
- }
196238
+ function filterEnrollmentsByGame(enrollments, gameId) {
196239
+ return enrollments.filter((e2) => e2.gameId === gameId).map(({ gameId: _42, ...rest }) => rest);
196135
196240
  }
196136
- async function fetchUserTimebackData(timebackId) {
196137
- const [role, enrollments] = await Promise.all([
196138
- fetchUserRole(timebackId),
196139
- fetchEnrollmentsForUser(timebackId)
196241
+ function filterOrganizationsByEnrollments(organizations, enrollments) {
196242
+ const enrollmentOrgIds = new Set(enrollments.map((e2) => e2.orgId).filter(Boolean));
196243
+ if (enrollmentOrgIds.size === 0)
196244
+ return [];
196245
+ return organizations.filter((o42) => enrollmentOrgIds.has(o42.id));
196246
+ }
196247
+ async function fetchUserTimebackData(timebackId, gameId) {
196248
+ const [{ role, organizations: allOrganizations }, allEnrollments] = await Promise.all([
196249
+ fetchStudentFromOneRoster(timebackId),
196250
+ fetchEnrollmentsFromEduBridge(timebackId)
196140
196251
  ]);
196141
- return { role, enrollments };
196252
+ const enrollments = gameId ? filterEnrollmentsByGame(allEnrollments, gameId) : allEnrollments;
196253
+ const organizations = gameId ? filterOrganizationsByEnrollments(allOrganizations, enrollments) : allOrganizations;
196254
+ log2.debug("[Timeback] Fetched student data", {
196255
+ timebackId,
196256
+ role,
196257
+ enrollments: enrollments.map((e2) => `${e2.subject}:${e2.grade}`),
196258
+ organizations: organizations.map((o42) => `${o42.name ?? o42.id} (${o42.type})`)
196259
+ });
196260
+ return { id: timebackId, role, enrollments, organizations };
196142
196261
  }
196143
196262
  var AchievementCompletionType;
196144
196263
  ((AchievementCompletionType2) => {
@@ -198546,7 +198665,7 @@ async function scanAssetDirectory(distPath) {
198546
198665
  async function scanFiles(dir, baseDir = dir) {
198547
198666
  const entries = await readdir(dir, { withFileTypes: true });
198548
198667
  for (const entry of entries) {
198549
- const fullPath = join3(dir, entry.name);
198668
+ const fullPath = join4(dir, entry.name);
198550
198669
  if (entry.isDirectory()) {
198551
198670
  await scanFiles(fullPath, baseDir);
198552
198671
  } else {
@@ -198912,7 +199031,7 @@ class CloudflareProvider {
198912
199031
  async function scanDirectory(dir) {
198913
199032
  const entries = await readdir2(dir, { withFileTypes: true });
198914
199033
  for (const entry of entries) {
198915
- const fullPath = join4(dir, entry.name);
199034
+ const fullPath = join5(dir, entry.name);
198916
199035
  if (entry.isDirectory()) {
198917
199036
  if (await scanDirectory(fullPath))
198918
199037
  return true;
@@ -198934,7 +199053,7 @@ class CloudflareProvider {
198934
199053
  async resolveAssetBasePath(dirPath) {
198935
199054
  const entries = await readdir2(dirPath, { withFileTypes: true });
198936
199055
  if (entries.length === 1 && entries[0]?.isDirectory()) {
198937
- const unwrappedPath = join4(dirPath, entries[0].name);
199056
+ const unwrappedPath = join5(dirPath, entries[0].name);
198938
199057
  log2.debug("[CloudflareProvider] Unwrapping wrapper directory", {
198939
199058
  wrapper: entries[0].name
198940
199059
  });
@@ -198945,7 +199064,7 @@ class CloudflareProvider {
198945
199064
  async uploadFilesToR2(dir, baseDir, bucketName) {
198946
199065
  const entries = await readdir2(dir, { withFileTypes: true });
198947
199066
  for (const entry of entries) {
198948
- const fullPath = join4(dir, entry.name);
199067
+ const fullPath = join5(dir, entry.name);
198949
199068
  if (entry.isDirectory()) {
198950
199069
  await this.uploadFilesToR2(fullPath, baseDir, bucketName);
198951
199070
  } else {
@@ -199422,29 +199541,46 @@ async function seedCurrencies(db) {
199422
199541
  });
199423
199542
  }
199424
199543
  }
199425
- var customLogger;
199426
- function setLogger(logger3) {
199427
- customLogger = logger3;
199544
+ function generateMockStudentId(userId) {
199545
+ return `mock-student-${userId.slice(-8)}`;
199428
199546
  }
199429
- function getLogger() {
199430
- if (customLogger) {
199431
- return customLogger;
199547
+ function generateTimebackId(userId, isPrimaryUser = false) {
199548
+ const timebackId = config.timeback.timebackId;
199549
+ if (!timebackId) {
199550
+ return null;
199432
199551
  }
199433
- return {
199434
- info: (msg) => console.log(msg),
199435
- warn: (msg) => console.warn(msg),
199436
- error: (msg) => console.error(msg)
199437
- };
199552
+ if (timebackId === "mock") {
199553
+ return generateMockStudentId(userId);
199554
+ }
199555
+ return isPrimaryUser ? timebackId : generateMockStudentId(userId);
199438
199556
  }
199439
- var logger3 = {
199440
- info: (msg) => {
199441
- if (customLogger || !config.embedded) {
199442
- getLogger().info(msg);
199557
+ async function seedTimebackIntegrations(db, gameId, courses) {
199558
+ const now2 = new Date;
199559
+ let seededCount = 0;
199560
+ for (const course of courses) {
199561
+ const courseId = course.courseId || `mock-${course.subject.toLowerCase()}-g${course.grade}`;
199562
+ try {
199563
+ const existing = await db.query.gameTimebackIntegrations.findFirst({
199564
+ where: (table14, { and: and3, eq: eq3 }) => and3(eq3(table14.gameId, gameId), eq3(table14.grade, course.grade), eq3(table14.subject, course.subject))
199565
+ });
199566
+ if (existing) {
199567
+ continue;
199568
+ }
199569
+ await db.insert(gameTimebackIntegrations).values({
199570
+ gameId,
199571
+ courseId,
199572
+ grade: course.grade,
199573
+ subject: course.subject,
199574
+ totalXp: course.totalXp ?? 1000,
199575
+ lastVerifiedAt: now2
199576
+ });
199577
+ seededCount++;
199578
+ } catch (error2) {
199579
+ console.error(`❌ Error seeding Timeback integration for ${course.subject}:${course.grade}:`, error2);
199443
199580
  }
199444
- },
199445
- warn: (msg) => getLogger().warn(msg),
199446
- error: (msg) => getLogger().error(msg)
199447
- };
199581
+ }
199582
+ return seededCount;
199583
+ }
199448
199584
  async function seedCoreGames(db) {
199449
199585
  const now2 = new Date;
199450
199586
  const coreGames = [
@@ -199477,7 +199613,9 @@ async function seedCurrentProjectGame(db, project) {
199477
199613
  where: (games3, { eq: eq3 }) => eq3(games3.slug, project.slug)
199478
199614
  });
199479
199615
  if (existingGame) {
199480
- logger3.info(`\uD83C\uDFAE Game "${project.displayName}" (${project.slug}) already exists`);
199616
+ if (project.timebackCourses && project.timebackCourses.length > 0) {
199617
+ await seedTimebackIntegrations(db, existingGame.id, project.timebackCourses);
199618
+ }
199481
199619
  return existingGame;
199482
199620
  }
199483
199621
  const gameRecord = {
@@ -199497,6 +199635,12 @@ async function seedCurrentProjectGame(db, project) {
199497
199635
  updatedAt: now2
199498
199636
  };
199499
199637
  const [newGame] = await db.insert(games).values(gameRecord).returning();
199638
+ if (!newGame) {
199639
+ throw new Error("Failed to create game record");
199640
+ }
199641
+ if (project.timebackCourses && project.timebackCourses.length > 0) {
199642
+ await seedTimebackIntegrations(db, newGame.id, project.timebackCourses);
199643
+ }
199500
199644
  return newGame;
199501
199645
  } catch (error2) {
199502
199646
  console.error("❌ Error seeding project game:", error2);
@@ -199553,23 +199697,14 @@ async function seedSpriteTemplates(db) {
199553
199697
  }
199554
199698
  }
199555
199699
  }
199556
- function resolveStudentId(studentId) {
199557
- if (!studentId)
199558
- return null;
199559
- if (studentId === "mock")
199560
- return `mock-student-${crypto.randomUUID().slice(0, 8)}`;
199561
- return studentId;
199562
- }
199563
- function getAdminTimebackId() {
199564
- return resolveStudentId(config.timeback.studentId);
199565
- }
199566
199700
  async function seedDemoData(db) {
199567
199701
  try {
199568
- const adminTimebackId = getAdminTimebackId();
199569
- for (const [role, user] of Object.entries(DEMO_USERS)) {
199702
+ const primaryUserId = DEMO_USERS.player.id;
199703
+ for (const user of Object.values(DEMO_USERS)) {
199704
+ const isPrimaryUser = user.id === primaryUserId;
199570
199705
  const userValues = {
199571
199706
  ...user,
199572
- timebackId: role === "admin" ? adminTimebackId : null
199707
+ timebackId: generateTimebackId(user.id, isPrimaryUser)
199573
199708
  };
199574
199709
  await db.insert(users).values(userValues).onConflictDoNothing();
199575
199710
  }
@@ -199590,7 +199725,7 @@ async function seedDemoData(db) {
199590
199725
  console.error("❌ Error seeding demo data:", error2);
199591
199726
  throw error2;
199592
199727
  }
199593
- return DEMO_USERS.admin;
199728
+ return DEMO_USERS.player;
199594
199729
  }
199595
199730
  async function checkIfNeedsSeeding(db) {
199596
199731
  try {
@@ -199626,6 +199761,10 @@ async function setupServerDatabase(processedOptions, project) {
199626
199761
  }
199627
199762
  init_src();
199628
199763
  var import_json_colorizer = __toESM2(require_dist22(), 1);
199764
+ var customLogger;
199765
+ function setLogger(logger3) {
199766
+ customLogger = logger3;
199767
+ }
199629
199768
  function processServerOptions(port, options) {
199630
199769
  const {
199631
199770
  verbose = false,
@@ -199674,6 +199813,10 @@ async function startRealtimeServer(realtimeOptions, betterAuthSecret) {
199674
199813
  if (!realtimeOptions.enabled) {
199675
199814
  return null;
199676
199815
  }
199816
+ if (!realtimeOptions.port) {
199817
+ return null;
199818
+ }
199819
+ await waitForPort(realtimeOptions.port);
199677
199820
  if (typeof Bun === "undefined") {
199678
199821
  try {
199679
199822
  return Promise.resolve().then(() => (init_sandbox(), exports_sandbox)).then(({ createSandboxRealtimeServer: createSandboxRealtimeServer2 }) => createSandboxRealtimeServer2({
@@ -204929,12 +205072,32 @@ async function getUserMe(ctx) {
204929
205072
  log2.error(`[API /users/me] User not found in DB for valid token ID: ${user.id}`);
204930
205073
  throw ApiError.notFound("User not found");
204931
205074
  }
205075
+ const timeback3 = userData.timebackId ? await fetchUserTimebackData(userData.timebackId, ctx.gameId) : undefined;
205076
+ if (ctx.gameId) {
205077
+ return {
205078
+ id: userData.id,
205079
+ name: userData.name,
205080
+ role: userData.role,
205081
+ username: userData.username,
205082
+ email: userData.email,
205083
+ timeback: timeback3
205084
+ };
205085
+ }
204932
205086
  const timebackAccount = await db.query.accounts.findFirst({
204933
205087
  where: and(eq(accounts.userId, user.id), eq(accounts.providerId, "timeback"))
204934
205088
  });
204935
- const timeback3 = userData.timebackId ? await fetchUserTimebackData(userData.timebackId) : undefined;
204936
205089
  return {
204937
- ...userData,
205090
+ id: userData.id,
205091
+ name: userData.name,
205092
+ username: userData.username,
205093
+ email: userData.email,
205094
+ emailVerified: userData.emailVerified,
205095
+ image: userData.image,
205096
+ role: userData.role,
205097
+ developerStatus: userData.developerStatus,
205098
+ characterCreated: userData.characterCreated,
205099
+ createdAt: userData.createdAt,
205100
+ updatedAt: userData.updatedAt,
204938
205101
  hasTimebackAccount: !!timebackAccount,
204939
205102
  timeback: timeback3
204940
205103
  };
@@ -204945,15 +205108,104 @@ async function getUserMe(ctx) {
204945
205108
  throw ApiError.internal("Internal server error", error2);
204946
205109
  }
204947
205110
  }
205111
+ init_src();
205112
+ function shouldMockTimeback() {
205113
+ return config.timeback.timebackId === "mock";
205114
+ }
205115
+ function getMockStudentProfile() {
205116
+ const { organization: org, role } = config.timeback;
205117
+ return {
205118
+ role: role ?? "student",
205119
+ organizations: [
205120
+ {
205121
+ id: org?.id ?? "PLAYCADEMY",
205122
+ name: org?.name ?? "Playcademy Studios",
205123
+ type: org?.type ?? "department",
205124
+ isPrimary: true
205125
+ }
205126
+ ]
205127
+ };
205128
+ }
205129
+ async function getMockEnrollments(db) {
205130
+ const allIntegrations = await db.query.gameTimebackIntegrations.findMany();
205131
+ return allIntegrations.map((i4) => ({
205132
+ gameId: i4.gameId,
205133
+ grade: i4.grade,
205134
+ subject: i4.subject,
205135
+ courseId: i4.courseId
205136
+ }));
205137
+ }
205138
+ async function getMockTimebackData(db, timebackId, gameId) {
205139
+ const { role, organizations } = getMockStudentProfile();
205140
+ const allEnrollments = await getMockEnrollments(db);
205141
+ const enrollments = gameId ? allEnrollments.filter((e2) => e2.gameId === gameId).map(({ gameId: _5, ...rest }) => rest) : allEnrollments;
205142
+ log2.debug("[Timeback] Sandbox is using mock data", {
205143
+ timebackId,
205144
+ role,
205145
+ enrollments: enrollments.map((e2) => `${e2.subject}:${e2.grade}`),
205146
+ organizations: organizations.map((o5) => `${o5.name ?? o5.id}`)
205147
+ });
205148
+ return { id: timebackId, role, enrollments, organizations };
205149
+ }
205150
+ async function buildMockUserResponse(db, user, gameId) {
205151
+ const timeback3 = user.timebackId ? await getMockTimebackData(db, user.timebackId, gameId) : undefined;
205152
+ if (gameId) {
205153
+ return {
205154
+ id: user.id,
205155
+ name: user.name,
205156
+ role: user.role,
205157
+ username: user.username,
205158
+ email: user.email,
205159
+ timeback: timeback3
205160
+ };
205161
+ }
205162
+ const timebackAccount = await db.query.accounts.findFirst({
205163
+ where: and(eq(accounts.userId, user.id), eq(accounts.providerId, "timeback"))
205164
+ });
205165
+ return {
205166
+ id: user.id,
205167
+ name: user.name,
205168
+ username: user.username,
205169
+ email: user.email,
205170
+ emailVerified: user.emailVerified,
205171
+ image: user.image,
205172
+ role: user.role,
205173
+ developerStatus: user.developerStatus,
205174
+ characterCreated: user.characterCreated,
205175
+ createdAt: user.createdAt,
205176
+ updatedAt: user.updatedAt,
205177
+ hasTimebackAccount: !!timebackAccount,
205178
+ timeback: timeback3
205179
+ };
205180
+ }
204948
205181
  var usersRouter = new Hono2;
204949
205182
  usersRouter.get("/me", async (c3) => {
204950
- const ctx = {
204951
- user: c3.get("user"),
204952
- params: {},
204953
- url: new URL(c3.req.url),
204954
- request: c3.req.raw
204955
- };
205183
+ const user = c3.get("user");
205184
+ const gameId = c3.get("gameId");
205185
+ if (!user) {
205186
+ const error2 = ApiError.unauthorized("Valid session or bearer token required");
205187
+ return c3.json(createErrorResponse(error2), error2.statusCode);
205188
+ }
204956
205189
  try {
205190
+ if (shouldMockTimeback()) {
205191
+ const db = c3.get("db");
205192
+ const userData2 = await db.query.users.findFirst({
205193
+ where: eq(users.id, user.id)
205194
+ });
205195
+ if (!userData2) {
205196
+ const error2 = ApiError.notFound("User not found");
205197
+ return c3.json(createErrorResponse(error2), error2.statusCode);
205198
+ }
205199
+ const response = await buildMockUserResponse(db, userData2, gameId);
205200
+ return c3.json(response);
205201
+ }
205202
+ const ctx = {
205203
+ user,
205204
+ params: {},
205205
+ url: new URL(c3.req.url),
205206
+ request: c3.req.raw,
205207
+ gameId
205208
+ };
204957
205209
  const userData = await getUserMe(ctx);
204958
205210
  return c3.json(userData);
204959
205211
  } catch (error2) {
@@ -210203,7 +210455,7 @@ async function getTodayTimeBackXp(ctx) {
210203
210455
  throw error2;
210204
210456
  if (error2 instanceof InvalidTimezoneError)
210205
210457
  throw ApiError.badRequest(error2.message);
210206
- log2.error("[timeback] getTodayTimeBackXp failed", { error: error2 });
210458
+ log2.error("[Timeback] getTodayTimeBackXp failed", { error: error2 });
210207
210459
  throw ApiError.internal("Failed to get today's TimeBack XP", error2);
210208
210460
  }
210209
210461
  }
@@ -210219,7 +210471,7 @@ async function getTotalTimeBackXp(ctx) {
210219
210471
  totalXp: Number(result[0]?.totalXp) || 0
210220
210472
  };
210221
210473
  } catch (error2) {
210222
- log2.error("[timeback] getTotalTimeBackXp failed", { error: error2 });
210474
+ log2.error("[Timeback] getTotalTimeBackXp failed", { error: error2 });
210223
210475
  throw ApiError.internal("Failed to get total TimeBack XP", error2);
210224
210476
  }
210225
210477
  }
@@ -210269,7 +210521,7 @@ async function updateTodayTimeBackXp(ctx) {
210269
210521
  } catch (error2) {
210270
210522
  if (error2 instanceof ApiError)
210271
210523
  throw error2;
210272
- log2.error("[timeback] updateTodayTimeBackXp failed", { error: error2 });
210524
+ log2.error("[Timeback] updateTodayTimeBackXp failed", { error: error2 });
210273
210525
  throw ApiError.internal("Failed to update today's TimeBack XP", error2);
210274
210526
  }
210275
210527
  }
@@ -210304,7 +210556,7 @@ async function getTimeBackXpHistory(ctx) {
210304
210556
  }))
210305
210557
  };
210306
210558
  } catch (error2) {
210307
- log2.error("[timeback] getTimeBackXpHistory failed", { error: error2 });
210559
+ log2.error("[Timeback] getTimeBackXpHistory failed", { error: error2 });
210308
210560
  throw ApiError.internal("Failed to get TimeBack XP history", error2);
210309
210561
  }
210310
210562
  }
@@ -210319,7 +210571,7 @@ async function getStudentEnrollments(ctx) {
210319
210571
  throw ApiError.badRequest("Missing timebackId parameter");
210320
210572
  }
210321
210573
  log2.debug("[API] Getting student enrollments", { userId: user.id, timebackId });
210322
- const enrollments = await fetchEnrollmentsForUser(timebackId);
210574
+ const enrollments = await fetchEnrollmentsFromEduBridge(timebackId);
210323
210575
  log2.info("[API] Retrieved student enrollments", {
210324
210576
  userId: user.id,
210325
210577
  timebackId,
@@ -210519,13 +210771,23 @@ timebackRouter.post("/end-activity", async (c3) => {
210519
210771
  });
210520
210772
  timebackRouter.get("/enrollments/:timebackId", async (c3) => {
210521
210773
  const timebackId = c3.req.param("timebackId");
210522
- const ctx = {
210523
- user: c3.get("user"),
210524
- params: { timebackId },
210525
- url: new URL(c3.req.url),
210526
- request: c3.req.raw
210527
- };
210774
+ const user = c3.get("user");
210775
+ if (!user) {
210776
+ const error2 = ApiError.unauthorized("Must be logged in to get enrollments");
210777
+ return c3.json(createErrorResponse(error2), error2.statusCode);
210778
+ }
210528
210779
  try {
210780
+ if (shouldMockTimeback()) {
210781
+ const db = c3.get("db");
210782
+ const enrollments2 = await getMockEnrollments(db);
210783
+ return c3.json({ enrollments: enrollments2 });
210784
+ }
210785
+ const ctx = {
210786
+ user,
210787
+ params: { timebackId },
210788
+ url: new URL(c3.req.url),
210789
+ request: c3.req.raw
210790
+ };
210529
210791
  const result = await getStudentEnrollments(ctx);
210530
210792
  return c3.json(result);
210531
210793
  } catch (error2) {
@@ -210833,6 +211095,7 @@ function registerRoutes(app) {
210833
211095
  }
210834
211096
  var version3 = package_default.version;
210835
211097
  async function startServer(port, project, options = {}) {
211098
+ await waitForPort(port);
210836
211099
  const processedOptions = processServerOptions(port, options);
210837
211100
  const db = await setupServerDatabase(processedOptions, project);
210838
211101
  const app = createApp(db, {
@@ -210845,50 +211108,75 @@ async function startServer(port, project, options = {}) {
210845
211108
  return {
210846
211109
  main: mainServer,
210847
211110
  realtime: realtimeServer,
211111
+ timebackMode: getTimebackDisplayMode(),
211112
+ setRole: (role) => {
211113
+ config.timeback.role = role;
211114
+ },
210848
211115
  stop: () => {
210849
- if (mainServer.close)
210850
- mainServer.close();
210851
- if (realtimeServer?.stop)
210852
- realtimeServer.stop();
211116
+ return new Promise((resolve2) => {
211117
+ if (realtimeServer?.stop)
211118
+ realtimeServer.stop();
211119
+ if (mainServer.close) {
211120
+ mainServer.close(() => resolve2());
211121
+ } else {
211122
+ resolve2();
211123
+ }
211124
+ });
210853
211125
  }
210854
211126
  };
210855
211127
  }
210856
211128
 
210857
211129
  // src/lib/logging/adapter.ts
210858
- var import_picocolors3 = __toESM(require_picocolors(), 1);
210859
- function formatTimestamp2() {
210860
- const now2 = new Date;
210861
- const hours = now2.getHours();
210862
- const minutes = now2.getMinutes().toString().padStart(2, "0");
210863
- const seconds = now2.getSeconds().toString().padStart(2, "0");
210864
- const ampm = hours >= 12 ? "PM" : "AM";
210865
- const displayHours = hours % 12 || 12;
210866
- return import_picocolors3.dim(`${displayHours}:${minutes}:${seconds} ${ampm}`);
210867
- }
210868
- function createLoggerAdapter(prefix2) {
210869
- const formattedPrefix = import_picocolors3.dim(`(${prefix2})`);
210870
- const label = import_picocolors3.cyan(import_picocolors3.bold("[playcademy]"));
211130
+ function createLoggerAdapter(domain) {
210871
211131
  return {
210872
- info: (msg) => console.log(`${formatTimestamp2()} ${label} ${formattedPrefix} ${msg}`),
210873
- warn: (msg) => console.warn(`${formatTimestamp2()} ${label} ${formattedPrefix} ${msg}`),
210874
- error: (msg) => console.error(`${formatTimestamp2()} ${label} ${formattedPrefix} ${msg}`)
211132
+ info: (msg) => console.log(`${createLogPrefix("playcademy", domain)} ${msg}`),
211133
+ warn: (msg) => console.warn(`${createLogPrefix("playcademy", domain)} ${msg}`),
211134
+ error: (msg) => console.error(`${createLogPrefix("playcademy", domain)} ${msg}`)
210875
211135
  };
210876
211136
  }
210877
211137
  // src/lib/logging/utils.ts
210878
- var import_picocolors4 = __toESM(require_picocolors(), 1);
210879
- function printBanner(viteConfig, servers, projectInfo, pluginVersion) {
211138
+ var import_picocolors3 = __toESM(require_picocolors(), 1);
211139
+
211140
+ // ../utils/src/string.ts
211141
+ function pluralize(count2, singular, plural) {
211142
+ return count2 === 1 ? singular : plural || `${singular}s`;
211143
+ }
211144
+
211145
+ // src/lib/logging/utils.ts
211146
+ function createBackendBannerOptions(backendPort, vitePort) {
211147
+ if (!backendPort)
211148
+ return;
211149
+ return { port: backendPort, vitePort };
211150
+ }
211151
+ function createTimebackBannerOptions(timebackMode, courseCount) {
211152
+ if (!timebackMode || !courseCount)
211153
+ return;
211154
+ return { courseCount, mode: timebackMode };
211155
+ }
211156
+ function printBanner(viteConfig, options) {
210880
211157
  const INDENT = " ".repeat(2);
211158
+ const { version: version4, gameName, sandbox, backend, realtimePort, timeback } = options;
210881
211159
  viteConfig.logger.info("");
210882
- viteConfig.logger.info(`${INDENT}${import_picocolors4.green(import_picocolors4.bold("PLAYCADEMY"))} ${import_picocolors4.green(`v${pluginVersion}`)}`);
211160
+ viteConfig.logger.info(`${INDENT}${import_picocolors3.green(import_picocolors3.bold("PLAYCADEMY"))} ${import_picocolors3.green(`v${version4}`)}`);
210883
211161
  viteConfig.logger.info("");
210884
- viteConfig.logger.info(`${INDENT}${import_picocolors4.green("➜")} ${import_picocolors4.bold("Game:")} ${import_picocolors4.cyan(projectInfo.slug)}`);
210885
- viteConfig.logger.info(`${INDENT}${import_picocolors4.green("➜")} ${import_picocolors4.bold("Sandbox:")} ${import_picocolors4.cyan(`http://localhost:${import_picocolors4.bold(servers.sandbox.toString())}/api`)}`);
210886
- if (servers.backend) {
210887
- const backendUrl = servers.vite ? `http://localhost:${import_picocolors4.bold(servers.vite.toString())}/api ${import_picocolors4.dim(`(via ${servers.backend})`)}` : `http://localhost:${import_picocolors4.bold(servers.backend.toString())}/api`;
210888
- viteConfig.logger.info(`${INDENT}${import_picocolors4.green("➜")} ${import_picocolors4.bold("Backend:")} ${import_picocolors4.cyan(backendUrl)}`);
211162
+ if (gameName) {
211163
+ viteConfig.logger.info(`${INDENT}${import_picocolors3.green("➜")} ${import_picocolors3.bold("Game:")} ${import_picocolors3.cyan(gameName)}`);
211164
+ }
211165
+ if (sandbox?.enabled) {
211166
+ viteConfig.logger.info(`${INDENT}${import_picocolors3.green("➜")} ${import_picocolors3.bold("Sandbox:")} ${import_picocolors3.cyan(`http://localhost:${import_picocolors3.bold(sandbox.port.toString())}/api`)}`);
211167
+ } else if (sandbox) {
211168
+ viteConfig.logger.info(`${INDENT}${import_picocolors3.green("➜")} ${import_picocolors3.bold("Sandbox:")} ${import_picocolors3.cyan("Disabled")}`);
211169
+ }
211170
+ if (backend) {
211171
+ const backendUrl = backend.vitePort ? `http://localhost:${import_picocolors3.bold(backend.vitePort.toString())}/api ${import_picocolors3.dim(`(via ${backend.port})`)}` : `http://localhost:${import_picocolors3.bold(backend.port.toString())}/api`;
211172
+ viteConfig.logger.info(`${INDENT}${import_picocolors3.green("➜")} ${import_picocolors3.bold("Backend:")} ${import_picocolors3.cyan(backendUrl)}`);
210889
211173
  }
210890
- if (servers.realtime) {
210891
- viteConfig.logger.info(`${INDENT}${import_picocolors4.green("➜")} ${import_picocolors4.bold("Realtime:")} ${import_picocolors4.cyan(`ws://localhost:${import_picocolors4.bold(servers.realtime.toString())}`)}`);
211174
+ if (realtimePort) {
211175
+ viteConfig.logger.info(`${INDENT}${import_picocolors3.green("➜")} ${import_picocolors3.bold("Realtime:")} ${import_picocolors3.cyan(`ws://localhost:${import_picocolors3.bold(realtimePort.toString())}`)}`);
211176
+ }
211177
+ if (timeback && timeback.courseCount > 0) {
211178
+ const label = `${timeback.courseCount} ${pluralize(timeback.courseCount, "course")} ${import_picocolors3.dim(`(${timeback.mode})`)}`;
211179
+ viteConfig.logger.info(`${INDENT}${import_picocolors3.green("➜")} ${import_picocolors3.bold("Timeback:")} ${import_picocolors3.cyan(label)}`);
210892
211180
  }
210893
211181
  viteConfig.logger.info("");
210894
211182
  }
@@ -210896,13 +211184,29 @@ function printBanner(viteConfig, servers, projectInfo, pluginVersion) {
210896
211184
  import fs6 from "node:fs";
210897
211185
  import path5 from "node:path";
210898
211186
  import { loadPlaycademyConfig } from "playcademy/utils";
210899
- async function extractProjectInfo(viteConfig) {
211187
+ function extractTimebackCourses(config2, timebackOptions) {
211188
+ const courses = config2?.integrations?.timeback?.courses;
211189
+ if (!courses || courses.length === 0)
211190
+ return;
211191
+ return courses.map((course) => {
211192
+ const key = `${course.subject}:${course.grade}`;
211193
+ const overrideId = timebackOptions?.courses?.[key];
211194
+ const courseId = overrideId && overrideId !== "mock" ? overrideId : undefined;
211195
+ return {
211196
+ subject: course.subject,
211197
+ grade: course.grade,
211198
+ courseId,
211199
+ totalXp: course.totalXp,
211200
+ masterableUnits: course.masterableUnits
211201
+ };
211202
+ });
211203
+ }
211204
+ async function extractProjectInfo(viteConfig, timebackOptions) {
210900
211205
  const projectRoot = viteConfig.root;
210901
211206
  const directoryName = path5.basename(projectRoot);
210902
- let configName;
211207
+ let config2 = null;
210903
211208
  try {
210904
- const config2 = await loadPlaycademyConfig();
210905
- configName = config2?.name;
211209
+ config2 = await loadPlaycademyConfig();
210906
211210
  } catch {}
210907
211211
  let packageJson = {};
210908
211212
  try {
@@ -210912,7 +211216,7 @@ async function extractProjectInfo(viteConfig) {
210912
211216
  packageJson = JSON.parse(packageJsonContent);
210913
211217
  }
210914
211218
  } catch {}
210915
- const name3 = configName || (typeof packageJson.name === "string" ? packageJson.name : "");
211219
+ const name3 = config2?.name || packageJson.name || "";
210916
211220
  let slug = name3;
210917
211221
  if (slug.includes("/")) {
210918
211222
  slug = slug.split("/")[1] || slug;
@@ -210924,20 +211228,42 @@ async function extractProjectInfo(viteConfig) {
210924
211228
  return {
210925
211229
  slug,
210926
211230
  displayName,
210927
- version: (typeof packageJson.version === "string" ? packageJson.version : null) || "dev",
210928
- description: typeof packageJson.description === "string" ? packageJson.description : undefined
211231
+ version: packageJson.version || "dev",
211232
+ description: packageJson.description,
211233
+ timebackCourses: extractTimebackCourses(config2, timebackOptions)
210929
211234
  };
210930
211235
  }
210931
211236
 
210932
211237
  // src/lib/sandbox/timeback.ts
210933
- var import_picocolors5 = __toESM(require_picocolors(), 1);
210934
- function getEffectiveTimebackId(timeback) {
210935
- if (timeback?.timebackId)
210936
- return timeback.timebackId;
210937
- if (timeback?.courses && Object.keys(timeback.courses).length > 0)
210938
- return "mock";
210939
- return;
210940
- }
211238
+ var import_picocolors4 = __toESM(require_picocolors(), 1);
211239
+ // ../constants/src/overworld.ts
211240
+ var ITEM_SLUGS4 = {
211241
+ PLAYCADEMY_CREDITS: "PLAYCADEMY_CREDITS",
211242
+ PLAYCADEMY_XP: "PLAYCADEMY_XP",
211243
+ FOUNDING_MEMBER_BADGE: "FOUNDING_MEMBER_BADGE",
211244
+ EARLY_ADOPTER_BADGE: "EARLY_ADOPTER_BADGE",
211245
+ FIRST_GAME_BADGE: "FIRST_GAME_BADGE",
211246
+ COMMON_SWORD: "COMMON_SWORD",
211247
+ SMALL_HEALTH_POTION: "SMALL_HEALTH_POTION",
211248
+ SMALL_BACKPACK: "SMALL_BACKPACK",
211249
+ LAVA_LAMP: "LAVA_LAMP",
211250
+ BOOMBOX: "BOOMBOX",
211251
+ CABIN_BED: "CABIN_BED"
211252
+ };
211253
+ var CURRENCIES4 = {
211254
+ PRIMARY: ITEM_SLUGS4.PLAYCADEMY_CREDITS,
211255
+ XP: ITEM_SLUGS4.PLAYCADEMY_XP
211256
+ };
211257
+ var BADGES4 = {
211258
+ FOUNDING_MEMBER: ITEM_SLUGS4.FOUNDING_MEMBER_BADGE,
211259
+ EARLY_ADOPTER: ITEM_SLUGS4.EARLY_ADOPTER_BADGE,
211260
+ FIRST_GAME: ITEM_SLUGS4.FIRST_GAME_BADGE
211261
+ };
211262
+ // ../constants/src/timeback.ts
211263
+ var TIMEBACK_ORG_SOURCED_ID3 = "PLAYCADEMY";
211264
+ var TIMEBACK_ORG_NAME3 = "Playcademy Studios";
211265
+ var TIMEBACK_ORG_TYPE3 = "department";
211266
+ // src/lib/sandbox/timeback.ts
210941
211267
  function detectTimebackOptions() {
210942
211268
  if (process.env.TIMEBACK_LOCAL === "true") {
210943
211269
  return {
@@ -210945,7 +211271,7 @@ function detectTimebackOptions() {
210945
211271
  onerosterApiUrl: process.env.TIMEBACK_ONEROSTER_API_URL,
210946
211272
  caliperApiUrl: process.env.TIMEBACK_CALIPER_API_URL,
210947
211273
  courseId: process.env.SANDBOX_TIMEBACK_COURSE_ID,
210948
- studentId: process.env.SANDBOX_TIMEBACK_STUDENT_ID
211274
+ timebackId: process.env.SANDBOX_TIMEBACK_STUDENT_ID
210949
211275
  };
210950
211276
  }
210951
211277
  if (process.env.TIMEBACK_API_CLIENT_ID) {
@@ -210957,7 +211283,7 @@ function detectTimebackOptions() {
210957
211283
  clientSecret: process.env.TIMEBACK_API_CLIENT_SECRET,
210958
211284
  authUrl: process.env.TIMEBACK_API_AUTH_URL,
210959
211285
  courseId: process.env.SANDBOX_TIMEBACK_COURSE_ID,
210960
- studentId: process.env.SANDBOX_TIMEBACK_STUDENT_ID
211286
+ timebackId: process.env.SANDBOX_TIMEBACK_STUDENT_ID
210961
211287
  };
210962
211288
  }
210963
211289
  return;
@@ -210971,59 +211297,77 @@ function hasTimebackCredentials2() {
210971
211297
  }
210972
211298
  return !!(config2.onerosterApiUrl && config2.clientId && config2.clientSecret && config2.authUrl);
210973
211299
  }
210974
- function validateTimebackConfig(viteConfig, timeback) {
210975
- if (!timeback?.courses)
211300
+ function validateTimebackConfig(viteConfig, timeback2) {
211301
+ if (!timeback2?.courses)
210976
211302
  return;
210977
- const realCourses = Object.entries(timeback.courses).filter(([, value]) => value !== "mock");
211303
+ const realCourses = Object.entries(timeback2.courses).filter(([, value]) => value !== "mock");
210978
211304
  if (realCourses.length > 0 && !hasTimebackCredentials2()) {
210979
211305
  const courseList = realCourses.map(([key]) => key).join(", ");
210980
211306
  viteConfig.logger.warn("");
210981
- viteConfig.logger.warn(import_picocolors5.default.yellow(`⚠️ TimeBack: Real course IDs for ${import_picocolors5.default.bold(courseList)} but credentials missing.`));
210982
- viteConfig.logger.warn(import_picocolors5.default.dim(` Required: TIMEBACK_API_CLIENT_ID, TIMEBACK_API_CLIENT_SECRET, TIMEBACK_API_AUTH_URL, TIMEBACK_ONEROSTER_API_URL`));
210983
- viteConfig.logger.warn(import_picocolors5.default.dim(` Or use 'mock' for local testing.`));
211307
+ viteConfig.logger.warn(import_picocolors4.default.yellow(`⚠️ TimeBack: Real course IDs for ${import_picocolors4.default.bold(courseList)} but credentials missing.`));
211308
+ viteConfig.logger.warn(import_picocolors4.default.dim(` Required: TIMEBACK_API_CLIENT_ID, TIMEBACK_API_CLIENT_SECRET, TIMEBACK_API_AUTH_URL, TIMEBACK_ONEROSTER_API_URL`));
211309
+ viteConfig.logger.warn(import_picocolors4.default.dim(` Or use 'mock' for local testing.`));
210984
211310
  viteConfig.logger.warn("");
210985
211311
  }
210986
211312
  }
210987
- function generateTimebackJson(timeback) {
210988
- if (!timeback?.courses || Object.keys(timeback.courses).length === 0) {
210989
- return;
211313
+ function resolveOrganization(orgConfig) {
211314
+ if (!orgConfig || orgConfig === "mock") {
211315
+ return {
211316
+ id: TIMEBACK_ORG_SOURCED_ID3,
211317
+ name: TIMEBACK_ORG_NAME3,
211318
+ type: TIMEBACK_ORG_TYPE3
211319
+ };
211320
+ }
211321
+ return {
211322
+ id: orgConfig.id ?? TIMEBACK_ORG_SOURCED_ID3,
211323
+ name: orgConfig.name ?? TIMEBACK_ORG_NAME3,
211324
+ type: orgConfig.type ?? TIMEBACK_ORG_TYPE3
211325
+ };
211326
+ }
211327
+ function resolveOrganizations(orgConfig) {
211328
+ const org = resolveOrganization(orgConfig);
211329
+ return [
211330
+ {
211331
+ id: org.id,
211332
+ name: org.name ?? null,
211333
+ type: org.type ?? TIMEBACK_ORG_TYPE3,
211334
+ isPrimary: true
211335
+ }
211336
+ ];
211337
+ }
211338
+ function generateTimebackJson(context) {
211339
+ if (!context?.baseCourses || context.baseCourses.length === 0) {
211340
+ return "null";
210990
211341
  }
211342
+ const { baseCourses, overrides } = context;
210991
211343
  const enrollments = [];
210992
- for (const [key, value] of Object.entries(timeback.courses)) {
210993
- const parts2 = key.split(":");
210994
- const subject = parts2[0];
210995
- const gradeStr = parts2[1];
210996
- const grade = gradeStr ? parseInt(gradeStr, 10) : NaN;
210997
- if (!subject || isNaN(grade))
211344
+ for (const course of baseCourses) {
211345
+ const key = `${course.subject}:${course.grade}`;
211346
+ const override = overrides?.courses?.[key];
211347
+ if (override === null)
210998
211348
  continue;
210999
- const courseId = value === "mock" ? `mock-${subject.toLowerCase()}-g${grade}` : value;
211000
- enrollments.push({ subject, grade, courseId });
211349
+ const courseId = override && override !== "mock" ? override : `mock-${course.subject.toLowerCase()}-g${course.grade}`;
211350
+ enrollments.push({
211351
+ subject: course.subject,
211352
+ grade: course.grade,
211353
+ courseId
211354
+ });
211001
211355
  }
211002
211356
  if (enrollments.length === 0) {
211003
- return;
211357
+ return "null";
211004
211358
  }
211005
211359
  const roleOverride = getTimebackRoleOverride();
211006
- const role = roleOverride ?? timeback.role ?? "student";
211007
- return JSON.stringify({ role, enrollments });
211360
+ const role = roleOverride ?? overrides?.role ?? "student";
211361
+ const organizations = resolveOrganizations(overrides?.organization);
211362
+ return JSON.stringify({ role, enrollments, organizations });
211008
211363
  }
211009
211364
 
211010
211365
  // src/lib/sandbox/server.ts
211011
- function printSandboxInfo(viteConfig, apiPort, realtimePort, projectInfo, realtimeEnabled) {
211012
- viteConfig.logger.info("");
211013
- viteConfig.logger.info(` ${import_picocolors6.default.green(import_picocolors6.default.bold("PLAYCADEMY"))} ${import_picocolors6.default.green(`v${version3}`)}`);
211014
- viteConfig.logger.info("");
211015
- viteConfig.logger.info(` ${import_picocolors6.default.green("➜")} ${import_picocolors6.default.bold("Game:")} ${import_picocolors6.default.cyan(projectInfo.slug)}`);
211016
- viteConfig.logger.info(` ${import_picocolors6.default.green("➜")} ${import_picocolors6.default.bold("Sandbox:")} ${import_picocolors6.default.cyan(`http://localhost:${import_picocolors6.default.bold(apiPort.toString())}/api`)}`);
211017
- if (realtimeEnabled) {
211018
- viteConfig.logger.info(` ${import_picocolors6.default.green("➜")} ${import_picocolors6.default.bold("Realtime:")} ${import_picocolors6.default.cyan(`ws://localhost:${import_picocolors6.default.bold(realtimePort.toString())}`)}`);
211019
- }
211020
- viteConfig.logger.info("");
211021
- }
211022
211366
  async function startSandbox(viteConfig, autoStart = true, options = {}) {
211023
211367
  const {
211368
+ port = DEFAULT_PORTS2.SANDBOX,
211024
211369
  verbose = false,
211025
211370
  customUrl,
211026
- quiet = false,
211027
211371
  recreateDb = false,
211028
211372
  seed = true,
211029
211373
  memoryOnly = false,
@@ -211031,16 +211375,17 @@ async function startSandbox(viteConfig, autoStart = true, options = {}) {
211031
211375
  realtimeEnabled = false,
211032
211376
  realtimePort,
211033
211377
  logLevel = "info",
211034
- timebackId
211378
+ timebackOptions
211035
211379
  } = options;
211380
+ const timebackDisabled = timebackOptions === false;
211036
211381
  if (!autoStart || viteConfig.command !== "serve") {
211037
211382
  const baseUrl = customUrl ?? `http://localhost:${DEFAULT_PORTS2.SANDBOX}`;
211038
211383
  const deriveRealtimeUrl = (url) => {
211039
211384
  try {
211040
211385
  const u4 = new URL(url);
211041
- const port = Number(u4.port || (u4.protocol === "https:" ? 443 : 80));
211386
+ const port2 = Number(u4.port || (u4.protocol === "https:" ? 443 : 80));
211042
211387
  const wsProto = u4.protocol === "https:" ? "wss:" : "ws:";
211043
- return `${wsProto}//${u4.hostname}:${port + 1}`;
211388
+ return `${wsProto}//${u4.hostname}:${port2 + 1}`;
211044
211389
  } catch {
211045
211390
  return "ws://localhost:4322";
211046
211391
  }
@@ -211055,57 +211400,49 @@ async function startSandbox(viteConfig, autoStart = true, options = {}) {
211055
211400
  };
211056
211401
  }
211057
211402
  try {
211058
- const sandboxPort = await findAvailablePort(DEFAULT_PORTS2.SANDBOX);
211059
- const baseUrl = `http://localhost:${sandboxPort}`;
211060
- const projectInfo = await extractProjectInfo(viteConfig);
211061
- let timebackOptions = detectTimebackOptions();
211062
- if (timebackId) {
211063
- timebackOptions = {
211064
- ...timebackOptions,
211065
- studentId: timebackId
211066
- };
211067
- }
211068
- const finalRealtimePort = realtimePort ?? await findAvailablePort(sandboxPort + 1);
211403
+ const baseUrl = `http://localhost:${port}`;
211404
+ const effectiveTimebackOptions = timebackDisabled ? undefined : timebackOptions || undefined;
211405
+ const projectInfo = await extractProjectInfo(viteConfig, effectiveTimebackOptions);
211406
+ let sandboxTimebackOptions = detectTimebackOptions();
211407
+ const effectiveTimebackId = timebackDisabled ? undefined : effectiveTimebackOptions?.id ?? sandboxTimebackOptions?.timebackId ?? (projectInfo.timebackCourses?.length ? "mock" : undefined);
211408
+ if (effectiveTimebackId) {
211409
+ sandboxTimebackOptions = {
211410
+ ...sandboxTimebackOptions,
211411
+ timebackId: effectiveTimebackId,
211412
+ organization: resolveOrganization(effectiveTimebackOptions?.organization),
211413
+ role: effectiveTimebackOptions?.role
211414
+ };
211415
+ }
211416
+ const finalRealtimePort = realtimePort ?? port + 1;
211069
211417
  const realtimeUrl = `ws://localhost:${finalRealtimePort}`;
211070
- const server = await startServer(sandboxPort, projectInfo, {
211418
+ const server = await startServer(port, projectInfo, {
211071
211419
  verbose,
211072
- quiet,
211420
+ quiet: true,
211073
211421
  seed,
211074
211422
  memoryOnly,
211075
211423
  databasePath,
211076
211424
  recreateDb,
211077
211425
  logLevel,
211078
211426
  realtime: { enabled: realtimeEnabled, port: finalRealtimePort },
211079
- timeback: timebackOptions,
211427
+ timeback: sandboxTimebackOptions,
211080
211428
  logger: createLoggerAdapter("sandbox")
211081
211429
  });
211082
- writeServerInfo("sandbox", {
211083
- pid: process.pid,
211084
- port: sandboxPort,
211085
- url: baseUrl,
211086
- startedAt: Date.now(),
211087
- projectRoot: viteConfig.root
211088
- });
211089
- if (!quiet) {
211090
- setTimeout(() => {
211091
- printSandboxInfo(viteConfig, sandboxPort, finalRealtimePort, projectInfo, realtimeEnabled);
211092
- }, 100);
211093
- }
211094
211430
  return {
211095
211431
  baseUrl,
211096
211432
  realtimeUrl,
211097
- port: sandboxPort,
211433
+ port,
211098
211434
  realtimePort: realtimeEnabled ? finalRealtimePort : undefined,
211099
211435
  project: projectInfo,
211100
- cleanup: () => {
211101
- cleanupServerInfo("sandbox", viteConfig.root, process.pid);
211436
+ timebackMode: server.timebackMode,
211437
+ setRole: server.setRole,
211438
+ cleanup: async () => {
211102
211439
  if (server && typeof server.stop === "function") {
211103
- server.stop();
211440
+ await server.stop();
211104
211441
  }
211105
211442
  }
211106
211443
  };
211107
211444
  } catch (error2) {
211108
- viteConfig.logger.error(import_picocolors6.default.red(`[Playcademy] Failed to start sandbox: ${error2}`));
211445
+ viteConfig.logger.error(import_picocolors5.default.red(`[Playcademy] Failed to start sandbox: ${error2}`));
211109
211446
  return {
211110
211447
  baseUrl: `http://localhost:${DEFAULT_PORTS2.SANDBOX}`,
211111
211448
  realtimeUrl: "ws://localhost:4322",
@@ -211116,216 +211453,212 @@ async function startSandbox(viteConfig, autoStart = true, options = {}) {
211116
211453
  };
211117
211454
  }
211118
211455
  }
211119
- // src/shells/shell-no-badge.html
211120
- var shell_no_badge_default = `<!doctype html>
211121
- <html lang="en">
211122
- <head>
211123
- <meta charset="UTF-8" />
211124
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
211125
- <title>Playcademy - Development</title>
211126
- <script type="importmap">
211127
- {
211128
- "imports": {
211129
- "@playcademy/sdk": "https://esm.sh/@playcademy/sdk@latest"
211130
- }
211131
- }
211132
- </script>
211133
- <style>
211134
- * {
211135
- margin: 0;
211136
- padding: 0;
211137
- box-sizing: border-box;
211138
- }
211139
-
211140
- body {
211141
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
211142
- height: 100vh;
211143
- overflow: hidden;
211144
- }
211145
-
211146
- .game-container {
211147
- width: 100vw;
211148
- height: 100vh;
211149
- position: relative;
211150
- overflow: hidden;
211151
- }
211152
-
211153
- .game-frame {
211154
- position: absolute;
211155
- inset: 0;
211156
- border: none;
211157
- width: 100%;
211158
- height: 100%;
211159
- }
211160
-
211161
- .loading {
211162
- position: absolute;
211163
- inset: 0;
211164
- display: flex;
211165
- align-items: center;
211166
- justify-content: center;
211167
- color: #666;
211168
- font-size: 1rem;
211169
- background-color: #fafafa;
211170
- }
211171
-
211172
- .error {
211173
- position: absolute;
211174
- inset: 0;
211175
- display: flex;
211176
- flex-direction: column;
211177
- align-items: center;
211178
- justify-content: center;
211179
- color: #d32f2f;
211180
- background-color: #ffebee;
211181
- }
211182
-
211183
- .hidden {
211184
- display: none;
211185
- }
211186
- </style>
211187
- </head>
211188
- <body>
211189
- <div class="game-container">
211190
- <div class="loading" id="loading">Loading your game...</div>
211191
-
211192
- <div class="error hidden" id="error">
211193
- <div>❌ Failed to load game</div>
211194
- <div
211195
- style="margin-top: 0.5rem; font-size: 0.9rem; opacity: 0.8"
211196
- id="errorMessage"
211197
- ></div>
211198
- </div>
211199
-
211200
- <iframe class="game-frame hidden" id="gameFrame" src="/"></iframe>
211201
- </div>
211202
-
211203
- <script type="module">
211204
- import { MessageEvents, messaging } from '@playcademy/sdk'
211205
-
211206
- const loading = document.getElementById('loading')
211207
- const error = document.getElementById('error')
211208
- const errorMessage = document.getElementById('errorMessage')
211209
- const gameFrame = document.getElementById('gameFrame')
211210
-
211211
- const logIfDebug = (...args) => {
211212
- if (window.PLAYCADEMY_DEBUG) {
211213
- console.log('[PlaycademyDevShell]', ...args)
211214
- }
211215
- }
211216
-
211217
- logIfDebug('[PlaycademyDevShell] Initialized')
211218
-
211219
- async function checkSandboxConnection() {
211220
- try {
211221
- const response = await fetch('{{SANDBOX_URL}}/api/users/me', {
211222
- headers: {
211223
- Authorization: 'Bearer sandbox-demo-token',
211224
- },
211225
- })
211226
-
211227
- if (response.ok) {
211228
- logIfDebug('[PlaycademyDevShell] Sandbox API connection successful')
211229
- } else {
211230
- throw new Error('Sandbox API not available')
211231
- }
211232
- } catch (err) {
211233
- logIfDebug('[PlaycademyDevShell] Sandbox connection failed:', err)
211234
- }
211235
- }
211236
-
211237
- // Load the game iframe
211238
- function loadGame() {
211239
- let handshakeInterval = null
211240
- let handshakeTimeout = null
211241
-
211242
- const initPayload = {
211243
- baseUrl: '{{SANDBOX_URL}}',
211244
- gameUrl: '{{GAME_URL}}' || undefined,
211245
- token: 'sandbox-demo-token',
211246
- gameId: '{{GAME_ID}}',
211247
- realtimeUrl: '{{REALTIME_URL}}',
211248
- }
211249
-
211250
- const sendInit = () => {
211251
- if (!gameFrame.contentWindow) return
211252
-
211253
- messaging.send(MessageEvents.INIT, initPayload, {
211254
- target: gameFrame.contentWindow,
211255
- origin: '*',
211256
- })
211257
- }
211258
-
211259
- const stopHandshake = () => {
211260
- if (handshakeInterval) {
211261
- clearInterval(handshakeInterval)
211262
- handshakeInterval = null
211263
- }
211264
- if (handshakeTimeout) {
211265
- clearTimeout(handshakeTimeout)
211266
- handshakeTimeout = null
211267
- }
211268
- }
211269
-
211270
- gameFrame.onload = () => {
211271
- loading.classList.add('hidden')
211272
- gameFrame.classList.remove('hidden')
211273
-
211274
- logIfDebug('[PlaycademyDevShell] Game iframe loaded, beginning init handshake')
211275
-
211276
- // Begin handshake: send immediately, then every 300ms
211277
- sendInit()
211278
- handshakeInterval = setInterval(sendInit, 300)
211279
-
211280
- // Stop handshake after 10 seconds
211281
- handshakeTimeout = setTimeout(() => {
211282
- stopHandshake()
211283
- logIfDebug('[PlaycademyDevShell] Init handshake timeout reached')
211284
- }, 10000)
211285
- }
211286
-
211287
- gameFrame.onerror = () => {
211288
- loading.classList.add('hidden')
211289
- error.classList.remove('hidden')
211290
- errorMessage.textContent = 'Game iframe failed to load'
211291
- logIfDebug('[PlaycademyDevShell] Game iframe failed to load')
211292
- stopHandshake()
211293
- }
211294
-
211295
- // Listen for READY message to stop handshake
211296
- messaging.listen(MessageEvents.READY, () => {
211297
- logIfDebug('[PlaycademyDevShell] Received READY from game, stopping handshake')
211298
- stopHandshake()
211299
- })
211300
-
211301
- gameFrame.src = '/'
211302
- }
211303
-
211304
- window.addEventListener('message', event => {
211305
- if (event.source === gameFrame.contentWindow) {
211306
- const { type, ...payload } = event.data || {}
211307
- if (type && type.startsWith('PLAYCADEMY_')) {
211308
- logIfDebug(
211309
- '[PlaycademyDevShell] Received message from game:',
211310
- type,
211311
- payload,
211312
- )
211313
- // Bridge the message to the local context using CustomEvent
211314
- messaging.send(type, payload)
211315
- }
211316
- }
211317
- })
211456
+ // ../sandbox/dist/constants.js
211457
+ var now2 = new Date;
211458
+ var DEMO_USER_IDS2 = {
211459
+ player: "00000000-0000-0000-0000-000000000001",
211460
+ developer: "00000000-0000-0000-0000-000000000002",
211461
+ admin: "00000000-0000-0000-0000-000000000003",
211462
+ pendingDeveloper: "00000000-0000-0000-0000-000000000004",
211463
+ unverifiedPlayer: "00000000-0000-0000-0000-000000000005"
211464
+ };
211465
+ var DEMO_USERS2 = {
211466
+ admin: {
211467
+ id: DEMO_USER_IDS2.admin,
211468
+ name: "Admin User",
211469
+ username: "admin_user",
211470
+ email: "admin@playcademy.com",
211471
+ emailVerified: true,
211472
+ image: null,
211473
+ role: "admin",
211474
+ developerStatus: "approved",
211475
+ createdAt: now2,
211476
+ updatedAt: now2
211477
+ },
211478
+ player: {
211479
+ id: DEMO_USER_IDS2.player,
211480
+ name: "Player User",
211481
+ username: "player_user",
211482
+ email: "player@playcademy.com",
211483
+ emailVerified: true,
211484
+ image: null,
211485
+ role: "player",
211486
+ developerStatus: "none",
211487
+ createdAt: now2,
211488
+ updatedAt: now2
211489
+ },
211490
+ developer: {
211491
+ id: DEMO_USER_IDS2.developer,
211492
+ name: "Developer User",
211493
+ username: "developer_user",
211494
+ email: "developer@playcademy.com",
211495
+ emailVerified: true,
211496
+ image: null,
211497
+ role: "developer",
211498
+ developerStatus: "approved",
211499
+ createdAt: now2,
211500
+ updatedAt: now2
211501
+ },
211502
+ pendingDeveloper: {
211503
+ id: DEMO_USER_IDS2.pendingDeveloper,
211504
+ name: "Pending Developer",
211505
+ username: "pending_dev",
211506
+ email: "pending@playcademy.com",
211507
+ emailVerified: true,
211508
+ image: null,
211509
+ role: "developer",
211510
+ developerStatus: "pending",
211511
+ createdAt: now2,
211512
+ updatedAt: now2
211513
+ },
211514
+ unverifiedPlayer: {
211515
+ id: DEMO_USER_IDS2.unverifiedPlayer,
211516
+ name: "Unverified Player",
211517
+ username: "unverified_player",
211518
+ email: "unverified@playcademy.com",
211519
+ emailVerified: false,
211520
+ image: null,
211521
+ role: "player",
211522
+ developerStatus: "none",
211523
+ createdAt: now2,
211524
+ updatedAt: now2
211525
+ }
211526
+ };
211527
+ var DEMO_USER2 = DEMO_USERS2.player;
211528
+ var DEMO_TOKENS2 = {
211529
+ "sandbox-demo-token": DEMO_USERS2.player,
211530
+ "sandbox-admin-token": DEMO_USERS2.admin,
211531
+ "sandbox-player-token": DEMO_USERS2.player,
211532
+ "sandbox-developer-token": DEMO_USERS2.developer,
211533
+ "sandbox-pending-dev-token": DEMO_USERS2.pendingDeveloper,
211534
+ "sandbox-unverified-token": DEMO_USERS2.unverifiedPlayer,
211535
+ "mock-game-token-for-local-dev": DEMO_USERS2.player
211536
+ };
211537
+ var DEMO_ITEM_IDS2 = {
211538
+ playcademyCredits: "10000000-0000-0000-0000-000000000001",
211539
+ foundingMemberBadge: "10000000-0000-0000-0000-000000000002",
211540
+ earlyAdopterBadge: "10000000-0000-0000-0000-000000000003",
211541
+ firstGameBadge: "10000000-0000-0000-0000-000000000004",
211542
+ commonSword: "10000000-0000-0000-0000-000000000005",
211543
+ smallHealthPotion: "10000000-0000-0000-0000-000000000006",
211544
+ smallBackpack: "10000000-0000-0000-0000-000000000007"
211545
+ };
211546
+ var PLAYCADEMY_CREDITS_ID2 = DEMO_ITEM_IDS2.playcademyCredits;
211547
+ var SAMPLE_ITEMS2 = [
211548
+ {
211549
+ id: PLAYCADEMY_CREDITS_ID2,
211550
+ slug: "PLAYCADEMY_CREDITS",
211551
+ gameId: null,
211552
+ displayName: "PLAYCADEMY credits",
211553
+ description: "The main currency used across PLAYCADEMY.",
211554
+ type: "currency",
211555
+ isPlaceable: false,
211556
+ imageUrl: "http://playcademy-sandbox.local/playcademy-credit.png",
211557
+ metadata: {
211558
+ rarity: "common"
211559
+ }
211560
+ },
211561
+ {
211562
+ id: DEMO_ITEM_IDS2.foundingMemberBadge,
211563
+ slug: "FOUNDING_MEMBER_BADGE",
211564
+ gameId: null,
211565
+ displayName: "Founding Member Badge",
211566
+ description: "Reserved for founding core team of the PLAYCADEMY platform.",
211567
+ type: "badge",
211568
+ isPlaceable: false,
211569
+ imageUrl: null,
211570
+ metadata: {
211571
+ rarity: "legendary"
211572
+ }
211573
+ },
211574
+ {
211575
+ id: DEMO_ITEM_IDS2.earlyAdopterBadge,
211576
+ slug: "EARLY_ADOPTER_BADGE",
211577
+ gameId: null,
211578
+ displayName: "Early Adopter Badge",
211579
+ description: "Awarded to users who joined during the beta phase.",
211580
+ type: "badge",
211581
+ isPlaceable: false,
211582
+ imageUrl: null,
211583
+ metadata: {
211584
+ rarity: "epic"
211585
+ }
211586
+ },
211587
+ {
211588
+ id: DEMO_ITEM_IDS2.firstGameBadge,
211589
+ slug: "FIRST_GAME_BADGE",
211590
+ gameId: null,
211591
+ displayName: "First Game Played",
211592
+ description: "Awarded for playing your first game in the Playcademy platform.",
211593
+ type: "badge",
211594
+ isPlaceable: false,
211595
+ imageUrl: "http://playcademy-sandbox.local/first-game-badge.png",
211596
+ metadata: {
211597
+ rarity: "uncommon"
211598
+ }
211599
+ },
211600
+ {
211601
+ id: DEMO_ITEM_IDS2.commonSword,
211602
+ slug: "COMMON_SWORD",
211603
+ gameId: null,
211604
+ displayName: "Common Sword",
211605
+ description: "A basic sword, good for beginners.",
211606
+ type: "unlock",
211607
+ isPlaceable: false,
211608
+ imageUrl: "http://playcademy-sandbox.local/common-sword.png",
211609
+ metadata: undefined
211610
+ },
211611
+ {
211612
+ id: DEMO_ITEM_IDS2.smallHealthPotion,
211613
+ slug: "SMALL_HEALTH_POTION",
211614
+ gameId: null,
211615
+ displayName: "Small Health Potion",
211616
+ description: "Restores a small amount of health.",
211617
+ type: "other",
211618
+ isPlaceable: false,
211619
+ imageUrl: "http://playcademy-sandbox.local/small-health-potion.png",
211620
+ metadata: undefined
211621
+ },
211622
+ {
211623
+ id: DEMO_ITEM_IDS2.smallBackpack,
211624
+ slug: "SMALL_BACKPACK",
211625
+ gameId: null,
211626
+ displayName: "Small Backpack",
211627
+ description: "Increases your inventory capacity by 5 slots.",
211628
+ type: "upgrade",
211629
+ isPlaceable: false,
211630
+ imageUrl: "http://playcademy-sandbox.local/small-backpack.png",
211631
+ metadata: undefined
211632
+ }
211633
+ ];
211634
+ var SAMPLE_INVENTORY2 = [
211635
+ {
211636
+ id: "20000000-0000-0000-0000-000000000001",
211637
+ userId: DEMO_USER2.id,
211638
+ itemId: PLAYCADEMY_CREDITS_ID2,
211639
+ quantity: 1000
211640
+ }
211641
+ ];
211318
211642
 
211319
- // Initialize
211320
- checkSandboxConnection()
211321
- loadGame()
211322
- </script>
211323
- </body>
211324
- </html>
211325
- `;
211643
+ // src/lib/sandbox/token.ts
211644
+ var ROLE_TO_USER_ID = {
211645
+ player: DEMO_USER_IDS2.player,
211646
+ developer: DEMO_USER_IDS2.developer,
211647
+ admin: DEMO_USER_IDS2.admin
211648
+ };
211649
+ function createSandboxGameToken(gameSlug, role = "player") {
211650
+ const header = { alg: "none", typ: "sandbox" };
211651
+ const payload = {
211652
+ sub: gameSlug,
211653
+ uid: ROLE_TO_USER_ID[role],
211654
+ iat: Math.floor(Date.now() / 1000)
211655
+ };
211656
+ const encode3 = (obj) => btoa(JSON.stringify(obj));
211657
+ return `${encode3(header)}.${encode3(payload)}.sandbox`;
211658
+ }
211326
211659
 
211327
- // src/shells/shell-with-corner-badge.html
211328
- var shell_with_corner_badge_default = `<!doctype html>
211660
+ // src/shells/shell.html
211661
+ var shell_default = `<!doctype html>
211329
211662
  <html lang="en">
211330
211663
  <head>
211331
211664
  <meta charset="UTF-8" />
@@ -211385,16 +211718,16 @@ var shell_with_corner_badge_default = `<!doctype html>
211385
211718
  <script type="module">
211386
211719
  import { MessageEvents, messaging } from '@playcademy/sdk'
211387
211720
 
211388
- // Config (injected by vite plugin)
211389
211721
  const CONFIG = {
211390
211722
  sandboxUrl: '{{SANDBOX_URL}}',
211391
211723
  gameId: '{{GAME_ID}}',
211724
+ gameToken: '{{GAME_TOKEN}}',
211392
211725
  gameUrl: '{{GAME_URL}}' || undefined,
211393
211726
  realtimeUrl: '{{REALTIME_URL}}',
211394
211727
  timebackJson: '{{TIMEBACK_DATA}}',
211728
+ hideBadge: '{{HIDE_BADGE}}' === 'true',
211395
211729
  }
211396
211730
 
211397
- // Parse timeback data (role + enrollments)
211398
211731
  const timeback = (() => {
211399
211732
  try {
211400
211733
  return JSON.parse(CONFIG.timebackJson)
@@ -211403,18 +211736,19 @@ var shell_with_corner_badge_default = `<!doctype html>
211403
211736
  }
211404
211737
  })()
211405
211738
 
211406
- // Elements
211407
211739
  const badge = document.getElementById('badge')
211408
211740
  const frame = document.getElementById('frame')
211409
211741
 
211410
- // Debug logging
211742
+ if (CONFIG.hideBadge) {
211743
+ badge.classList.add('hidden')
211744
+ }
211745
+
211411
211746
  const log = (...args) => window.PLAYCADEMY_DEBUG && console.log('[DevShell]', ...args)
211412
211747
 
211413
- // Check sandbox connection
211414
211748
  async function checkSandbox() {
211415
211749
  try {
211416
211750
  const res = await fetch(\`\${CONFIG.sandboxUrl}/api/users/me\`, {
211417
- headers: { Authorization: 'Bearer sandbox-demo-token' },
211751
+ headers: { Authorization: \`Bearer \${CONFIG.gameToken}\` },
211418
211752
  })
211419
211753
  if (!res.ok) throw new Error('Sandbox unavailable')
211420
211754
  badge.classList.remove('offline')
@@ -211428,14 +211762,13 @@ var shell_with_corner_badge_default = `<!doctype html>
211428
211762
  }
211429
211763
  }
211430
211764
 
211431
- // Init handshake with game iframe
211432
211765
  function initHandshake(timebackData) {
211433
211766
  const payload = {
211434
211767
  baseUrl: CONFIG.sandboxUrl,
211435
211768
  gameUrl: CONFIG.gameUrl,
211436
211769
  gameId: CONFIG.gameId,
211437
211770
  realtimeUrl: CONFIG.realtimeUrl,
211438
- token: 'sandbox-demo-token',
211771
+ token: CONFIG.gameToken,
211439
211772
  timeback: timebackData,
211440
211773
  }
211441
211774
 
@@ -211474,7 +211807,6 @@ var shell_with_corner_badge_default = `<!doctype html>
211474
211807
  frame.src = '/'
211475
211808
  }
211476
211809
 
211477
- // Bridge messages from game to shell context
211478
211810
  window.addEventListener('message', e => {
211479
211811
  if (e.source !== frame.contentWindow) return
211480
211812
  const { type, ...payload } = e.data || {}
@@ -211484,7 +211816,6 @@ var shell_with_corner_badge_default = `<!doctype html>
211484
211816
  }
211485
211817
  })
211486
211818
 
211487
- // Start
211488
211819
  checkSandbox().then(() => initHandshake(timeback))
211489
211820
  </script>
211490
211821
  </body>
@@ -211492,10 +211823,11 @@ var shell_with_corner_badge_default = `<!doctype html>
211492
211823
  `;
211493
211824
 
211494
211825
  // src/server/middleware.ts
211495
- function generateLoaderHTML(sandboxUrl, gameId, realtimeUrl, options, gameUrl) {
211496
- const shell = options.showBadge ? shell_with_corner_badge_default : shell_no_badge_default;
211497
- const timebackJson = generateTimebackJson(options.timeback) ?? "null";
211498
- return shell.replace(/{{SANDBOX_URL}}/g, sandboxUrl).replace(/{{GAME_ID}}/g, gameId).replace(/{{REALTIME_URL}}/g, realtimeUrl).replace(/{{GAME_URL}}/g, gameUrl || "").replace(/{{TIMEBACK_DATA}}/g, timebackJson);
211826
+ function generateLoaderHTML(sandboxUrl, gameSlug, realtimeUrl, options, gameUrl) {
211827
+ const timebackJson = generateTimebackJson(options.timeback);
211828
+ const platformRole = getPlatformRoleOverride() ?? "player";
211829
+ const gameToken = createSandboxGameToken(gameSlug, platformRole);
211830
+ return shell_default.replace(/{{SANDBOX_URL}}/g, sandboxUrl).replace(/{{GAME_ID}}/g, gameSlug).replace(/{{GAME_TOKEN}}/g, gameToken).replace(/{{REALTIME_URL}}/g, realtimeUrl).replace(/{{GAME_URL}}/g, gameUrl || "").replace(/{{TIMEBACK_DATA}}/g, timebackJson).replace(/{{HIDE_BADGE}}/g, String(options.hideBadge));
211499
211831
  }
211500
211832
  function devServerMiddleware(server, sandbox, gameUrl, options) {
211501
211833
  server.middlewares.use("/", (req, res, next) => {
@@ -211517,69 +211849,166 @@ function devServerMiddleware(server, sandbox, gameUrl, options) {
211517
211849
  });
211518
211850
  }
211519
211851
 
211520
- // src/server/hotkeys/recreate-database.ts
211521
- var { bold: bold4, cyan: cyan3, dim: dim4, green: green2, red, yellow: yellow2 } = import_picocolors7.default;
211522
- function formatTimestamp3() {
211523
- const now2 = new Date;
211524
- const hours = now2.getHours();
211525
- const minutes = now2.getMinutes().toString().padStart(2, "0");
211526
- const seconds = now2.getSeconds().toString().padStart(2, "0");
211527
- const ampm = hours >= 12 ? "PM" : "AM";
211528
- const displayHours = hours % 12 || 12;
211529
- return dim4(`${displayHours}:${minutes}:${seconds} ${ampm}`);
211530
- }
211531
- async function recreateSandboxDatabase(options) {
211852
+ // src/server/recreate-sandbox.ts
211853
+ async function recreateSandbox(options) {
211854
+ const { viteConfig, platformModeOptions } = options;
211532
211855
  const currentMode = getCurrentMode();
211533
211856
  const viteServer = getViteServerRef();
211857
+ const prefix2 = createLogPrefix("playcademy", "sandbox");
211534
211858
  if (!viteServer) {
211535
- options.viteConfig.logger.error(`${formatTimestamp3()} ${red(bold4("[playcademy]"))} ${dim4("(sandbox)")} ${red("Cannot recreate sandbox database: no Vite server reference")}`);
211536
- return;
211859
+ viteConfig.logger.error(`${prefix2} ${import_picocolors6.red("Cannot recreate sandbox database: no Vite server reference")}`);
211860
+ return { success: false, error: "No Vite server reference" };
211537
211861
  }
211538
211862
  if (currentMode !== "platform") {
211539
- options.viteConfig.logger.warn(`${formatTimestamp3()} ${yellow2(bold4("[playcademy]"))} ${dim4("(sandbox)")} ${yellow2("can only recreate sandbox database in platform mode (m + enter)")}`);
211540
- return;
211863
+ viteConfig.logger.warn(`${prefix2} ${import_picocolors6.yellow("can only recreate sandbox database in platform mode (m + enter)")}`);
211864
+ return { success: false, error: "Not in platform mode" };
211541
211865
  }
211542
- options.viteConfig.logger.info(`${formatTimestamp3()} ${cyan3(bold4("[playcademy]"))} ${dim4("(sandbox)")} recreating database...`);
211866
+ viteConfig.logger.info(`${prefix2} recreating database...`);
211543
211867
  if (serverState.sandbox) {
211544
211868
  serverState.sandbox.cleanup();
211545
211869
  serverState.sandbox = null;
211546
211870
  }
211547
211871
  await new Promise((resolve2) => setTimeout(resolve2, 100));
211548
- const sandbox = await startSandbox(options.viteConfig, options.platformModeOptions.startSandbox, {
211549
- verbose: options.platformModeOptions.verbose,
211550
- logLevel: options.platformModeOptions.logLevel,
211551
- customUrl: options.platformModeOptions.sandboxUrl,
211552
- quiet: true,
211872
+ const timebackDisabled = platformModeOptions.timeback === false;
211873
+ const timebackOptions = platformModeOptions.timeback || undefined;
211874
+ const sandbox = await startSandbox(viteConfig, platformModeOptions.startSandbox, {
211875
+ port: platformModeOptions.sandboxPort,
211876
+ verbose: platformModeOptions.verbose,
211877
+ logLevel: platformModeOptions.logLevel,
211878
+ customUrl: platformModeOptions.sandboxUrl,
211553
211879
  recreateDb: true,
211554
- seed: options.platformModeOptions.seed,
211555
- memoryOnly: options.platformModeOptions.memoryOnly,
211556
- databasePath: options.platformModeOptions.databasePath,
211557
- realtimeEnabled: options.platformModeOptions.realtimeEnabled,
211558
- realtimePort: options.platformModeOptions.realtimePort,
211559
- timebackId: getEffectiveTimebackId(options.platformModeOptions.timeback)
211880
+ seed: platformModeOptions.seed,
211881
+ memoryOnly: platformModeOptions.memoryOnly,
211882
+ databasePath: platformModeOptions.databasePath,
211883
+ realtimeEnabled: platformModeOptions.realtimeEnabled,
211884
+ realtimePort: platformModeOptions.realtimePort,
211885
+ timebackOptions: timebackDisabled ? false : platformModeOptions.timeback
211560
211886
  });
211561
211887
  serverState.sandbox = sandbox;
211562
211888
  if (sandbox.project && serverState.backend) {
211563
211889
  const gameUrl = `http://localhost:${serverState.backend.port}`;
211564
211890
  devServerMiddleware(viteServer, sandbox, gameUrl, {
211565
- showBadge: options.platformModeOptions.showBadge,
211566
- timeback: options.platformModeOptions.timeback
211891
+ hideBadge: platformModeOptions.hideBadge,
211892
+ timeback: timebackDisabled ? undefined : {
211893
+ baseCourses: sandbox.project.timebackCourses ?? [],
211894
+ overrides: timebackOptions
211895
+ }
211567
211896
  });
211568
211897
  }
211569
- options.viteConfig.logger.info(`${formatTimestamp3()} ${cyan3(bold4("[playcademy]"))} ${dim4("(sandbox)")} ${green2("database recreated")}`);
211898
+ viteConfig.logger.info(`${prefix2} ${import_picocolors6.green("database recreated")}`);
211899
+ return { success: true };
211900
+ }
211901
+
211902
+ // src/server/config-watcher.ts
211903
+ var DEBOUNCE_MS = 500;
211904
+ var VITE_CONFIG_NAMES = ["vite.config.ts", "vite.config.js", "vite.config.mjs"];
211905
+ var debounceTimer = null;
211906
+ function findExistingFiles(projectRoot, fileNames) {
211907
+ return fileNames.map((name3) => path6.join(projectRoot, name3)).filter((file) => fs8.existsSync(file));
211908
+ }
211909
+ function createChangeHandler(server, viteConfig, platformModeOptions, watchedFiles) {
211910
+ return async (changedPath) => {
211911
+ const isWatchedFile = watchedFiles.some((file) => path6.resolve(changedPath) === path6.resolve(file));
211912
+ if (!isWatchedFile)
211913
+ return;
211914
+ if (getCurrentMode() !== "platform")
211915
+ return;
211916
+ if (debounceTimer)
211917
+ clearTimeout(debounceTimer);
211918
+ debounceTimer = setTimeout(async () => {
211919
+ await recreateSandbox({
211920
+ viteConfig,
211921
+ platformModeOptions
211922
+ });
211923
+ server.ws.send({ type: "full-reload" });
211924
+ }, DEBOUNCE_MS);
211925
+ };
211926
+ }
211927
+ function setupConfigWatcher(server, viteConfig, platformModeOptions) {
211928
+ const projectRoot = viteConfig.root;
211929
+ const playcademyConfigs = findExistingFiles(projectRoot, CONFIG_FILE_NAMES);
211930
+ const viteConfigs = findExistingFiles(projectRoot, VITE_CONFIG_NAMES);
211931
+ const allConfigFiles = [...playcademyConfigs, ...viteConfigs];
211932
+ if (allConfigFiles.length === 0)
211933
+ return;
211934
+ for (const configFile of allConfigFiles) {
211935
+ server.watcher.add(configFile);
211936
+ }
211937
+ server.watcher.on("change", createChangeHandler(server, viteConfig, platformModeOptions, allConfigFiles));
211938
+ }
211939
+
211940
+ // src/server/hotkeys/cycle-platform-role.ts
211941
+ var import_picocolors7 = __toESM(require_picocolors(), 1);
211942
+
211943
+ // src/types/internal.ts
211944
+ var TIMEBACK_ROLES = ["student", "parent", "teacher", "administrator"];
211945
+ var PLATFORM_ROLES = ["player", "developer", "admin"];
211946
+ // src/server/hotkeys/cycle-platform-role.ts
211947
+ function cyclePlatformRole(logger) {
211948
+ const currentRole = getPlatformRoleOverride() ?? "player";
211949
+ const currentIndex = PLATFORM_ROLES.indexOf(currentRole);
211950
+ const nextIndex = (currentIndex + 1) % PLATFORM_ROLES.length;
211951
+ const nextRole = PLATFORM_ROLES[nextIndex];
211952
+ const prefix2 = createLogPrefix("playcademy", "user");
211953
+ setPlatformRoleOverride(nextRole);
211954
+ logger.info(`${prefix2} ${import_picocolors7.red(currentRole)} → ${import_picocolors7.green(nextRole)}`);
211955
+ if (getViteServerRef()) {
211956
+ getViteServerRef()?.ws.send({ type: "full-reload", path: "*" });
211957
+ } else {
211958
+ logger.warn(`${prefix2} ${import_picocolors7.yellow("Cannot cycle platform role: no Vite server reference")}`);
211959
+ }
211960
+ }
211961
+ var cyclePlatformRoleHotkey = (options) => ({
211962
+ key: "p",
211963
+ description: `${import_picocolors7.cyan(import_picocolors7.bold("[playcademy]"))} cycle platform role`,
211964
+ action: () => cyclePlatformRole(options.viteConfig.logger)
211965
+ });
211966
+
211967
+ // src/server/hotkeys/cycle-timeback-role.ts
211968
+ var import_picocolors8 = __toESM(require_picocolors(), 1);
211969
+ var { bold: bold4, cyan: cyan4, green: green4, red: red3, yellow: yellow3 } = import_picocolors8.default;
211970
+ function cycleTimebackRole(logger) {
211971
+ const currentRole = getTimebackRoleOverride() ?? "student";
211972
+ const currentIndex = TIMEBACK_ROLES.indexOf(currentRole);
211973
+ const nextIndex = (currentIndex + 1) % TIMEBACK_ROLES.length;
211974
+ const nextRole = TIMEBACK_ROLES[nextIndex];
211975
+ setTimebackRoleOverride(nextRole);
211976
+ getSandboxRef()?.setRole?.(nextRole);
211977
+ const prefix2 = createLogPrefix("playcademy", "timeback");
211978
+ logger.info(`${prefix2} ${red3(currentRole)} → ${green4(nextRole)}`);
211979
+ if (getViteServerRef()) {
211980
+ getViteServerRef()?.ws.send({ type: "full-reload", path: "*" });
211981
+ } else {
211982
+ logger.warn(`${prefix2} ${yellow3("Cannot cycle TimeBack role: no Vite server reference")}`);
211983
+ }
211984
+ }
211985
+ var cycleTimebackRoleHotkey = (options) => ({
211986
+ key: "t",
211987
+ description: `${cyan4(bold4("[playcademy]"))} cycle Timeback role`,
211988
+ action: () => cycleTimebackRole(options.viteConfig.logger)
211989
+ });
211990
+
211991
+ // src/server/hotkeys/recreate-database.ts
211992
+ var import_picocolors9 = __toESM(require_picocolors(), 1);
211993
+ var { bold: bold5, cyan: cyan5 } = import_picocolors9.default;
211994
+ async function recreateSandboxDatabase(options) {
211995
+ await recreateSandbox({
211996
+ viteConfig: options.viteConfig,
211997
+ platformModeOptions: options.platformModeOptions
211998
+ });
211570
211999
  }
211571
212000
  var recreateDatabaseHotkey = (options) => ({
211572
212001
  key: "d",
211573
- description: "recreate sandbox database",
212002
+ description: `${cyan5(bold5("[playcademy]"))} recreate sandbox database`,
211574
212003
  action: () => recreateSandboxDatabase(options)
211575
212004
  });
211576
212005
 
211577
212006
  // src/server/hotkeys/toggle-mode.ts
211578
- var import_picocolors10 = __toESM(require_picocolors(), 1);
212007
+ var import_picocolors11 = __toESM(require_picocolors(), 1);
211579
212008
  // package.json
211580
212009
  var package_default2 = {
211581
212010
  name: "@playcademy/vite-plugin",
211582
- version: "0.1.37",
212011
+ version: "0.2.0",
211583
212012
  type: "module",
211584
212013
  exports: {
211585
212014
  ".": {
@@ -211598,6 +212027,7 @@ var package_default2 = {
211598
212027
  pub: "bun publish.ts"
211599
212028
  },
211600
212029
  dependencies: {
212030
+ "@playcademy/utils": "workspace:*",
211601
212031
  archiver: "^7.0.1",
211602
212032
  picocolors: "^1.1.1",
211603
212033
  playcademy: "workspace:*"
@@ -211622,27 +212052,29 @@ import {
211622
212052
  } from "playcademy/utils";
211623
212053
 
211624
212054
  // src/lib/backend/hot-reload.ts
211625
- var import_picocolors8 = __toESM(require_picocolors(), 1);
211626
- function formatChangedPath(changedPath) {
211627
- if (!changedPath)
211628
- return;
211629
- if (changedPath.includes("/api/")) {
211630
- return changedPath.substring(changedPath.indexOf("/api/"));
211631
- }
211632
- return changedPath;
211633
- }
212055
+ var import_picocolors10 = __toESM(require_picocolors(), 1);
212056
+ import path7 from "node:path";
211634
212057
  function createHotReloadCallbacks(viteConfig) {
212058
+ const formatPath = (changedPath) => {
212059
+ if (!changedPath)
212060
+ return;
212061
+ if (changedPath.startsWith(viteConfig.root)) {
212062
+ return path7.relative(viteConfig.root, changedPath);
212063
+ }
212064
+ return changedPath;
212065
+ };
212066
+ const prefix2 = createLogPrefix("playcademy", "backend");
211635
212067
  return {
211636
212068
  onSuccess: (changedPath) => {
211637
- const relativePath = formatChangedPath(changedPath);
212069
+ const relativePath = formatPath(changedPath);
211638
212070
  if (relativePath) {
211639
- viteConfig.logger.info(`${import_picocolors8.dim("(backend)")} ${import_picocolors8.green("hmr update")} ${import_picocolors8.dim(relativePath)}`, { timestamp: true });
212071
+ viteConfig.logger.info(`${prefix2} ${import_picocolors10.green("hmr update")} ${import_picocolors10.dim(relativePath)}`);
211640
212072
  } else {
211641
- viteConfig.logger.info("backend reloaded", { timestamp: true });
212073
+ viteConfig.logger.info(`${prefix2} reloaded`);
211642
212074
  }
211643
212075
  },
211644
212076
  onError: (error2) => {
211645
- viteConfig.logger.error(`backend reload failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
212077
+ viteConfig.logger.error(`${prefix2} reload failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
211646
212078
  }
211647
212079
  };
211648
212080
  }
@@ -211679,78 +212111,101 @@ function setupHotReload(serverRef, options) {
211679
212111
  return () => watcher.close();
211680
212112
  }
211681
212113
  async function setupCliDevServer(options) {
211682
- const { preferredPort, viteConfig, platformUrl, configPath } = options;
212114
+ const { port, viteConfig, platformUrl, configPath } = options;
211683
212115
  const config2 = await tryLoadConfig(viteConfig, configPath);
211684
212116
  if (!config2)
211685
212117
  return null;
211686
212118
  if (!needsCliDevServer(config2))
211687
212119
  return null;
211688
212120
  try {
211689
- const port = await findAvailablePort(preferredPort);
211690
- const serverOptions = { port, config: config2, platformUrl, viteConfig };
212121
+ const serverOptions = {
212122
+ port,
212123
+ config: config2,
212124
+ platformUrl,
212125
+ viteConfig
212126
+ };
211691
212127
  const serverRef = { current: await startServer2(serverOptions) };
211692
212128
  const stopHotReload = setupHotReload(serverRef, serverOptions);
211693
212129
  return {
211694
212130
  server: serverRef.current.server,
211695
212131
  port: serverRef.current.port,
211696
212132
  stopHotReload,
211697
- cleanup: () => cleanupServerInfo("backend", viteConfig.root, process.pid)
212133
+ cleanup: () => {}
211698
212134
  };
211699
212135
  } catch (error2) {
211700
- viteConfig.logger.error(`Failed to start game backend: ${error2 instanceof Error ? error2.message : String(error2)}`);
212136
+ const message3 = error2 instanceof Error ? error2.message : String(error2);
212137
+ viteConfig.logger.error(`Failed to start game backend: ${message3}`);
211701
212138
  return null;
211702
212139
  }
211703
212140
  }
211704
212141
  // src/server/platform-mode.ts
212142
+ var BANNER_PRINTED = false;
212143
+ function isCycleableRole(role) {
212144
+ return TIMEBACK_ROLES.includes(role);
212145
+ }
211705
212146
  async function configurePlatformMode(server, viteConfig, options) {
212147
+ const timebackDisabled = options.timeback === false;
212148
+ const timebackOptions = options.timeback || undefined;
212149
+ if (timebackOptions?.role && isCycleableRole(timebackOptions.role)) {
212150
+ setTimebackRoleOverride(timebackOptions.role);
212151
+ }
211706
212152
  const sandbox = await startSandbox(viteConfig, options.startSandbox, {
212153
+ port: options.sandboxPort,
211707
212154
  verbose: options.verbose,
211708
212155
  logLevel: options.logLevel,
211709
212156
  customUrl: options.sandboxUrl,
211710
- quiet: true,
211711
212157
  recreateDb: options.recreateDb,
211712
212158
  seed: options.seed,
211713
212159
  memoryOnly: options.memoryOnly,
211714
212160
  databasePath: options.databasePath,
211715
212161
  realtimeEnabled: options.realtimeEnabled,
211716
212162
  realtimePort: options.realtimePort,
211717
- timebackId: getEffectiveTimebackId(options.timeback)
212163
+ timebackOptions: timebackDisabled ? false : options.timeback
211718
212164
  });
211719
212165
  serverState.sandbox = sandbox;
211720
212166
  const backend = await setupCliDevServer({
211721
- preferredPort: options.preferredBackendPort,
212167
+ port: options.backendPort,
211722
212168
  viteConfig,
211723
212169
  platformUrl: sandbox.baseUrl,
211724
212170
  configPath: options.configPath
211725
212171
  });
211726
212172
  serverState.backend = backend;
211727
212173
  if (sandbox.project) {
211728
- validateTimebackConfig(viteConfig, options.timeback);
212174
+ if (!timebackDisabled) {
212175
+ validateTimebackConfig(viteConfig, timebackOptions);
212176
+ }
211729
212177
  const gameUrl = backend ? `http://localhost:${backend.port}` : undefined;
212178
+ const timebackContext = timebackDisabled ? undefined : {
212179
+ baseCourses: sandbox.project.timebackCourses ?? [],
212180
+ overrides: timebackOptions
212181
+ };
211730
212182
  devServerMiddleware(server, sandbox, gameUrl, {
211731
- showBadge: options.showBadge,
211732
- timeback: options.timeback
212183
+ hideBadge: options.hideBadge,
212184
+ timeback: timebackContext
212185
+ });
212186
+ }
212187
+ if (!BANNER_PRINTED) {
212188
+ server.httpServer?.once("listening", () => {
212189
+ setTimeout(async () => {
212190
+ const projectInfo = await extractProjectInfo(viteConfig, timebackOptions);
212191
+ printBanner(viteConfig, {
212192
+ version: package_default2.version,
212193
+ gameName: projectInfo.slug,
212194
+ sandbox: { port: sandbox.port, enabled: true },
212195
+ backend: createBackendBannerOptions(backend?.port, server.config.server.port),
212196
+ realtimePort: sandbox.realtimePort,
212197
+ timeback: createTimebackBannerOptions(sandbox.timebackMode, projectInfo.timebackCourses?.length)
212198
+ });
212199
+ BANNER_PRINTED = true;
212200
+ }, 100);
211733
212201
  });
211734
212202
  }
211735
- server.httpServer?.once("listening", () => {
211736
- setTimeout(async () => {
211737
- const projectInfo = await extractProjectInfo(viteConfig);
211738
- const vitePort = server.config.server.port;
211739
- printBanner(viteConfig, {
211740
- sandbox: sandbox.port,
211741
- backend: backend?.port,
211742
- realtime: sandbox.realtimePort,
211743
- vite: vitePort
211744
- }, projectInfo, package_default2.version);
211745
- }, 100);
211746
- });
211747
212203
  }
211748
212204
 
211749
212205
  // src/server/standalone-mode.ts
211750
- var import_picocolors9 = __toESM(require_picocolors(), 1);
211751
212206
  async function configureStandaloneMode(server, viteConfig, options) {
211752
212207
  const backend = await setupCliDevServer({
211753
- preferredPort: options.preferredPort,
212208
+ port: options.backendPort,
211754
212209
  viteConfig,
211755
212210
  platformUrl: undefined,
211756
212211
  configPath: options.configPath
@@ -211762,33 +212217,23 @@ async function configureStandaloneMode(server, viteConfig, options) {
211762
212217
  serverState.backend = backend;
211763
212218
  server.httpServer?.once("listening", () => {
211764
212219
  setTimeout(() => {
211765
- viteConfig.logger.info("");
211766
- viteConfig.logger.info(` ${import_picocolors9.default.green(import_picocolors9.default.bold("PLAYCADEMY"))} ${import_picocolors9.default.green(`v${package_default2.version}`)}`);
211767
- viteConfig.logger.info("");
211768
- viteConfig.logger.info(` ${import_picocolors9.default.green("➜")} ${import_picocolors9.default.bold("Backend:")} ${import_picocolors9.default.cyan(`http://localhost:${backend.port}`)}`);
211769
- viteConfig.logger.info(` ${import_picocolors9.default.green("➜")} ${import_picocolors9.default.bold("Sandbox:")} ${import_picocolors9.default.cyan("Disabled")}`);
211770
- viteConfig.logger.info("");
212220
+ printBanner(viteConfig, {
212221
+ version: package_default2.version,
212222
+ sandbox: { enabled: false },
212223
+ backend: { port: backend.port }
212224
+ });
211771
212225
  }, 100);
211772
212226
  });
211773
212227
  }
211774
212228
 
211775
212229
  // src/server/hotkeys/toggle-mode.ts
211776
- var { bold: bold5, cyan: cyan4, dim: dim6, green: green4, red: red2 } = import_picocolors10.default;
211777
- function formatTimestamp4() {
211778
- const now2 = new Date;
211779
- const hours = now2.getHours();
211780
- const minutes = now2.getMinutes().toString().padStart(2, "0");
211781
- const seconds = now2.getSeconds().toString().padStart(2, "0");
211782
- const ampm = hours >= 12 ? "PM" : "AM";
211783
- const displayHours = hours % 12 || 12;
211784
- return dim6(`${displayHours}:${minutes}:${seconds} ${ampm}`);
211785
- }
211786
212230
  async function toggleMode(options) {
211787
212231
  const currentMode = getCurrentMode();
211788
212232
  const newMode = currentMode === "platform" ? "standalone" : "platform";
211789
212233
  const viteServer = getViteServerRef();
212234
+ const prefix2 = createLogPrefix("playcademy");
211790
212235
  if (!viteServer) {
211791
- options.viteConfig.logger.error(`${formatTimestamp4()} ${red2(bold5("[playcademy]"))} ${red2("Cannot toggle mode: no Vite server reference")}`);
212236
+ options.viteConfig.logger.error(`${prefix2} ${import_picocolors11.red("Cannot toggle mode: no Vite server reference")}`);
211792
212237
  return;
211793
212238
  }
211794
212239
  await cleanupServers();
@@ -211796,17 +212241,17 @@ async function toggleMode(options) {
211796
212241
  setCurrentMode(newMode);
211797
212242
  if (newMode === "standalone") {
211798
212243
  await configureStandaloneMode(viteServer, options.viteConfig, {
211799
- preferredPort: options.platformModeOptions.preferredBackendPort,
212244
+ backendPort: options.platformModeOptions.backendPort,
211800
212245
  configPath: options.platformModeOptions.configPath
211801
212246
  });
211802
212247
  } else {
211803
212248
  await configurePlatformMode(viteServer, options.viteConfig, options.platformModeOptions);
211804
212249
  }
211805
- options.viteConfig.logger.info(`${formatTimestamp4()} ${cyan4(bold5("[playcademy]"))} ${green4("switched to")} ${green4(bold5(newMode))} ${green4("mode")}`);
212250
+ options.viteConfig.logger.info(`${prefix2} ${import_picocolors11.green("switched to")} ${import_picocolors11.green(import_picocolors11.bold(newMode))} ${import_picocolors11.green("mode")}`);
211806
212251
  }
211807
212252
  var toggleModeHotkey = (options) => ({
211808
212253
  key: "m",
211809
- description: "toggle platform/standalone mode",
212254
+ description: `${import_picocolors11.cyan(import_picocolors11.bold("[playcademy]"))} toggle platform/standalone mode`,
211810
212255
  action: () => toggleMode(options)
211811
212256
  });
211812
212257
 
@@ -211815,6 +212260,7 @@ function getHotkeys(options) {
211815
212260
  return [
211816
212261
  toggleModeHotkey(options),
211817
212262
  recreateDatabaseHotkey(options),
212263
+ cyclePlatformRoleHotkey(options),
211818
212264
  cycleTimebackRoleHotkey(options)
211819
212265
  ];
211820
212266
  }
@@ -211847,11 +212293,12 @@ async function configureServerHook(server, context) {
211847
212293
  setupProcessShutdownHandlers();
211848
212294
  if (hasActiveServers()) {
211849
212295
  await cleanupServers();
211850
- await new Promise((resolve2) => setTimeout(resolve2, 100));
211851
212296
  }
211852
- const preferredPort = context.backendPort ?? DEFAULT_PORTS3.BACKEND;
212297
+ const backendPort = context.backendPort ?? DEFAULT_PORTS3.BACKEND;
212298
+ const sandboxPort = context.sandboxPort ?? DEFAULT_PORTS3.SANDBOX;
211853
212299
  const platformModeOptions = {
211854
212300
  startSandbox: context.options.startSandbox,
212301
+ sandboxPort,
211855
212302
  verbose: context.options.verbose,
211856
212303
  logLevel: context.options.logLevel,
211857
212304
  sandboxUrl: context.options.sandboxUrl,
@@ -211861,18 +212308,19 @@ async function configureServerHook(server, context) {
211861
212308
  databasePath: context.options.databasePath,
211862
212309
  realtimeEnabled: context.options.realtimeEnabled,
211863
212310
  realtimePort: context.options.realtimePort,
211864
- showBadge: context.options.showBadge,
211865
- preferredBackendPort: preferredPort,
212311
+ hideBadge: context.options.hideBadge,
212312
+ backendPort,
211866
212313
  configPath: context.options.configPath,
211867
212314
  timeback: context.options.timeback
211868
212315
  };
211869
212316
  if (context.options.mode === "standalone") {
211870
212317
  await configureStandaloneMode(server, context.viteConfig, {
211871
- preferredPort,
212318
+ backendPort,
211872
212319
  configPath: context.options.configPath
211873
212320
  });
211874
212321
  } else {
211875
212322
  await configurePlatformMode(server, context.viteConfig, platformModeOptions);
212323
+ setupConfigWatcher(server, context.viteConfig, platformModeOptions);
211876
212324
  }
211877
212325
  const originalBindCLIShortcuts = server.bindCLIShortcuts?.bind(server);
211878
212326
  if (originalBindCLIShortcuts) {
@@ -211892,12 +212340,12 @@ async function configureServerHook(server, context) {
211892
212340
  }
211893
212341
 
211894
212342
  // src/hooks/write-bundle.ts
211895
- import path6 from "node:path";
212343
+ import path8 from "node:path";
211896
212344
  async function writeBundleHook(context) {
211897
212345
  if (!context.viteConfig) {
211898
212346
  throw new Error("[Playcademy] Vite config not resolved before writeBundle");
211899
212347
  }
211900
- const outDir = context.viteConfig.build.outDir || path6.join(process.cwd(), "dist");
212348
+ const outDir = context.viteConfig.build.outDir || path8.join(process.cwd(), "dist");
211901
212349
  try {
211902
212350
  await generatePlaycademyManifest(context.viteConfig, outDir, context.buildOutputs);
211903
212351
  if (context.options.autoZip) {
@@ -211916,7 +212364,7 @@ async function writeBundleHook(context) {
211916
212364
  function resolveOptions(options) {
211917
212365
  const exportOptions = options.export ?? {};
211918
212366
  const sandboxOptions = options.sandbox ?? {};
211919
- const shellOptions = options.shell ?? {};
212367
+ const displayOptions = options.display ?? {};
211920
212368
  const realtimeOptions = sandboxOptions.realtime ?? {};
211921
212369
  return {
211922
212370
  configPath: options.configPath,
@@ -211932,7 +212380,7 @@ function resolveOptions(options) {
211932
212380
  databasePath: sandboxOptions.databasePath,
211933
212381
  realtimeEnabled: realtimeOptions.enabled ?? false,
211934
212382
  realtimePort: realtimeOptions.port,
211935
- showBadge: shellOptions.showBadge ?? true,
212383
+ hideBadge: displayOptions.hideBadge ?? false,
211936
212384
  timeback: options.timeback
211937
212385
  };
211938
212386
  }
@@ -211941,6 +212389,7 @@ function createPluginContext(options) {
211941
212389
  options: resolveOptions(options),
211942
212390
  viteConfig: null,
211943
212391
  backendPort: null,
212392
+ sandboxPort: null,
211944
212393
  buildOutputs: {}
211945
212394
  };
211946
212395
  }