@playcademy/vite-plugin 0.1.37 → 0.1.38

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
@@ -41140,7 +41140,8 @@ var serverState = {
41140
41140
  sandbox: null,
41141
41141
  backend: null,
41142
41142
  viteServer: null,
41143
- currentMode: "platform"
41143
+ currentMode: "platform",
41144
+ timebackRoleOverride: null
41144
41145
  };
41145
41146
  function hasActiveServers() {
41146
41147
  return !!(serverState.backend || serverState.sandbox);
@@ -41157,6 +41158,12 @@ function getViteServerRef() {
41157
41158
  function setViteServerRef(server) {
41158
41159
  serverState.viteServer = server;
41159
41160
  }
41161
+ function getTimebackRoleOverride() {
41162
+ return serverState.timebackRoleOverride;
41163
+ }
41164
+ function setTimebackRoleOverride(role) {
41165
+ serverState.timebackRoleOverride = role;
41166
+ }
41160
41167
 
41161
41168
  // src/server/cleanup.ts
41162
41169
  async function cleanupServers() {
@@ -41176,73 +41183,12 @@ async function cleanupServers() {
41176
41183
  }
41177
41184
  }
41178
41185
 
41179
- // src/server/lifecycle.ts
41180
- var shutdownHandlersRegistered = false;
41181
- function setupProcessShutdownHandlers() {
41182
- if (shutdownHandlersRegistered)
41183
- return;
41184
- shutdownHandlersRegistered = true;
41185
- let isShuttingDown = false;
41186
- const shutdown = async () => {
41187
- if (isShuttingDown)
41188
- return;
41189
- isShuttingDown = true;
41190
- await cleanupServers();
41191
- process.exit(0);
41192
- };
41193
- process.on("SIGINT", shutdown);
41194
- process.on("SIGTERM", shutdown);
41195
- }
41196
-
41197
- // src/server/mode-switcher.ts
41198
- var import_picocolors7 = __toESM(require_picocolors(), 1);
41199
- // package.json
41200
- var package_default = {
41201
- name: "@playcademy/vite-plugin",
41202
- version: "0.1.36",
41203
- type: "module",
41204
- exports: {
41205
- ".": {
41206
- import: "./dist/index.js",
41207
- types: "./dist/index.d.ts"
41208
- }
41209
- },
41210
- main: "dist/index.js",
41211
- module: "dist/index.js",
41212
- files: [
41213
- "dist"
41214
- ],
41215
- scripts: {
41216
- build: "rm -rf dist && bun build.ts",
41217
- docs: "typedoc --skipErrorChecking",
41218
- pub: "bun publish.ts"
41219
- },
41220
- dependencies: {
41221
- archiver: "^7.0.1",
41222
- picocolors: "^1.1.1",
41223
- playcademy: "workspace:*"
41224
- },
41225
- devDependencies: {
41226
- "@inquirer/prompts": "^7.8.6",
41227
- "@playcademy/sandbox": "workspace:*",
41228
- "@types/archiver": "^6.0.3",
41229
- "@types/bun": "latest"
41230
- },
41231
- peerDependencies: {
41232
- typescript: "^5",
41233
- vite: "^5 || ^6"
41234
- }
41235
- };
41236
-
41237
- // src/lib/backend/server.ts
41238
- import {
41239
- loadPlaycademyConfigAndSetWorkspace as loadConfigAndSetWorkspace,
41240
- startPlaycademyDevServer,
41241
- startPlaycademyHotReload
41242
- } from "playcademy/utils";
41243
-
41244
- // src/lib/logging/adapter.ts
41186
+ // src/server/hotkeys/cycle-timeback-role.ts
41245
41187
  var import_picocolors2 = __toESM(require_picocolors(), 1);
41188
+
41189
+ // src/types/internal.ts
41190
+ var TIMEBACK_ROLES = ["student", "parent", "teacher", "administrator"];
41191
+ // src/server/hotkeys/cycle-timeback-role.ts
41246
41192
  function formatTimestamp() {
41247
41193
  const now = new Date;
41248
41194
  const hours = now.getHours();
@@ -41250,117 +41196,32 @@ function formatTimestamp() {
41250
41196
  const seconds = now.getSeconds().toString().padStart(2, "0");
41251
41197
  const ampm = hours >= 12 ? "PM" : "AM";
41252
41198
  const displayHours = hours % 12 || 12;
41253
- return import_picocolors2.dim(`${displayHours}:${minutes}:${seconds} ${ampm}`);
41254
- }
41255
- function createLoggerAdapter(prefix) {
41256
- const formattedPrefix = import_picocolors2.dim(`(${prefix})`);
41257
- const label = import_picocolors2.cyan(import_picocolors2.bold("[playcademy]"));
41258
- return {
41259
- info: (msg) => console.log(`${formatTimestamp()} ${label} ${formattedPrefix} ${msg}`),
41260
- warn: (msg) => console.warn(`${formatTimestamp()} ${label} ${formattedPrefix} ${msg}`),
41261
- error: (msg) => console.error(`${formatTimestamp()} ${label} ${formattedPrefix} ${msg}`)
41262
- };
41263
- }
41264
- // src/lib/logging/utils.ts
41265
- var import_picocolors3 = __toESM(require_picocolors(), 1);
41266
- function printBanner(viteConfig, servers, projectInfo, pluginVersion) {
41267
- const INDENT = " ".repeat(2);
41268
- viteConfig.logger.info("");
41269
- viteConfig.logger.info(`${INDENT}${import_picocolors3.green(import_picocolors3.bold("PLAYCADEMY"))} ${import_picocolors3.green(`v${pluginVersion}`)}`);
41270
- viteConfig.logger.info("");
41271
- viteConfig.logger.info(`${INDENT}${import_picocolors3.green("➜")} ${import_picocolors3.bold("Game:")} ${import_picocolors3.cyan(projectInfo.slug)}`);
41272
- viteConfig.logger.info(`${INDENT}${import_picocolors3.green("➜")} ${import_picocolors3.bold("Sandbox:")} ${import_picocolors3.cyan(`http://localhost:${import_picocolors3.bold(servers.sandbox.toString())}/api`)}`);
41273
- if (servers.backend) {
41274
- const backendUrl = servers.vite ? `http://localhost:${import_picocolors3.bold(servers.vite.toString())}/api ${import_picocolors3.dim(`(via ${servers.backend})`)}` : `http://localhost:${import_picocolors3.bold(servers.backend.toString())}/api`;
41275
- viteConfig.logger.info(`${INDENT}${import_picocolors3.green("➜")} ${import_picocolors3.bold("Backend:")} ${import_picocolors3.cyan(backendUrl)}`);
41276
- }
41277
- if (servers.realtime) {
41278
- viteConfig.logger.info(`${INDENT}${import_picocolors3.green("➜")} ${import_picocolors3.bold("Realtime:")} ${import_picocolors3.cyan(`ws://localhost:${import_picocolors3.bold(servers.realtime.toString())}`)}`);
41279
- }
41280
- viteConfig.logger.info("");
41281
- }
41282
- // src/lib/backend/hot-reload.ts
41283
- var import_picocolors4 = __toESM(require_picocolors(), 1);
41284
- function formatChangedPath(changedPath) {
41285
- if (!changedPath)
41286
- return;
41287
- if (changedPath.includes("/api/")) {
41288
- return changedPath.substring(changedPath.indexOf("/api/"));
41199
+ return import_picocolors2.default.dim(`${displayHours}:${minutes}:${seconds} ${ampm}`);
41200
+ }
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")}`);
41289
41212
  }
41290
- return changedPath;
41291
- }
41292
- function createHotReloadCallbacks(viteConfig) {
41293
- return {
41294
- onSuccess: (changedPath) => {
41295
- const relativePath = formatChangedPath(changedPath);
41296
- if (relativePath) {
41297
- viteConfig.logger.info(`${import_picocolors4.dim("(backend)")} ${import_picocolors4.green("hmr update")} ${import_picocolors4.dim(relativePath)}`, { timestamp: true });
41298
- } else {
41299
- viteConfig.logger.info("backend reloaded", { timestamp: true });
41300
- }
41301
- },
41302
- onError: (error) => {
41303
- viteConfig.logger.error(`backend reload failed: ${error instanceof Error ? error.message : String(error)}`);
41304
- }
41305
- };
41306
41213
  }
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);
41307
41222
 
41308
- // src/lib/backend/server.ts
41309
- async function tryLoadConfig(viteConfig, configPath) {
41310
- try {
41311
- return await loadConfigAndSetWorkspace(configPath);
41312
- } catch (error) {
41313
- if (error instanceof Error && !error.message.includes("Could not find")) {
41314
- viteConfig.logger.warn(`Could not load playcademy.config.js: ${error.message}`);
41315
- }
41316
- return null;
41317
- }
41318
- }
41319
- function needsCliDevServer(config) {
41320
- return !!config.integrations;
41321
- }
41322
- async function startServer(options) {
41323
- const { port, config, platformUrl } = options;
41324
- return startPlaycademyDevServer({
41325
- port,
41326
- config,
41327
- quiet: true,
41328
- platformUrl,
41329
- customLogger: createLoggerAdapter("backend")
41330
- });
41331
- }
41332
- function setupHotReload(serverRef, options) {
41333
- const watcher = startPlaycademyHotReload(async () => {
41334
- await serverRef.current.server.dispose();
41335
- serverRef.current = await startServer(options);
41336
- }, createHotReloadCallbacks(options.viteConfig));
41337
- return () => watcher.close();
41338
- }
41339
- async function setupCliDevServer(options) {
41340
- const { preferredPort, viteConfig, platformUrl, configPath } = options;
41341
- const config = await tryLoadConfig(viteConfig, configPath);
41342
- if (!config)
41343
- return null;
41344
- if (!needsCliDevServer(config))
41345
- return null;
41346
- try {
41347
- const port = await findAvailablePort(preferredPort);
41348
- const serverOptions = { port, config, platformUrl, viteConfig };
41349
- const serverRef = { current: await startServer(serverOptions) };
41350
- const stopHotReload = setupHotReload(serverRef, serverOptions);
41351
- return {
41352
- server: serverRef.current.server,
41353
- port: serverRef.current.port,
41354
- stopHotReload,
41355
- cleanup: () => cleanupServerInfo("backend", viteConfig.root, process.pid)
41356
- };
41357
- } catch (error) {
41358
- viteConfig.logger.error(`Failed to start game backend: ${error instanceof Error ? error.message : String(error)}`);
41359
- return null;
41360
- }
41361
- }
41362
41223
  // src/lib/sandbox/server.ts
41363
- var import_picocolors5 = __toESM(require_picocolors(), 1);
41224
+ var import_picocolors6 = __toESM(require_picocolors(), 1);
41364
41225
  import { DEFAULT_PORTS as DEFAULT_PORTS2 } from "playcademy/constants";
41365
41226
 
41366
41227
  // ../sandbox/dist/server.js
@@ -50427,8 +50288,8 @@ function assembleStyles() {
50427
50288
  styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET);
50428
50289
  Object.defineProperties(styles, {
50429
50290
  rgbToAnsi256: {
50430
- value(red, green3, blue2) {
50431
- if (red === green3 && green3 === blue2) {
50291
+ value(red, green, blue) {
50292
+ if (red === green && green === blue) {
50432
50293
  if (red < 8) {
50433
50294
  return 16;
50434
50295
  }
@@ -50437,7 +50298,7 @@ function assembleStyles() {
50437
50298
  }
50438
50299
  return Math.round((red - 8) / 247 * 24) + 232;
50439
50300
  }
50440
- return 16 + 36 * Math.round(red / 255 * 5) + 6 * Math.round(green3 / 255 * 5) + Math.round(blue2 / 255 * 5);
50301
+ return 16 + 36 * Math.round(red / 255 * 5) + 6 * Math.round(green / 255 * 5) + Math.round(blue / 255 * 5);
50441
50302
  },
50442
50303
  enumerable: false
50443
50304
  },
@@ -50473,24 +50334,24 @@ function assembleStyles() {
50473
50334
  return 90 + (code - 8);
50474
50335
  }
50475
50336
  let red;
50476
- let green3;
50477
- let blue2;
50337
+ let green;
50338
+ let blue;
50478
50339
  if (code >= 232) {
50479
50340
  red = ((code - 232) * 10 + 8) / 255;
50480
- green3 = red;
50481
- blue2 = red;
50341
+ green = red;
50342
+ blue = red;
50482
50343
  } else {
50483
50344
  code -= 16;
50484
50345
  const remainder = code % 36;
50485
50346
  red = Math.floor(code / 36) / 5;
50486
- green3 = Math.floor(remainder / 6) / 5;
50487
- blue2 = remainder % 6 / 5;
50347
+ green = Math.floor(remainder / 6) / 5;
50348
+ blue = remainder % 6 / 5;
50488
50349
  }
50489
- const value = Math.max(red, green3, blue2) * 2;
50350
+ const value = Math.max(red, green, blue) * 2;
50490
50351
  if (value === 0) {
50491
50352
  return 30;
50492
50353
  }
50493
- let result = 30 + (Math.round(blue2) << 2 | Math.round(green3) << 1 | Math.round(red));
50354
+ let result = 30 + (Math.round(blue) << 2 | Math.round(green) << 1 | Math.round(red));
50494
50355
  if (value === 2) {
50495
50356
  result += 60;
50496
50357
  }
@@ -50499,7 +50360,7 @@ function assembleStyles() {
50499
50360
  enumerable: false
50500
50361
  },
50501
50362
  rgbToAnsi: {
50502
- value: (red, green3, blue2) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green3, blue2)),
50363
+ value: (red, green, blue) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green, blue)),
50503
50364
  enumerable: false
50504
50365
  },
50505
50366
  hexToAnsi: {
@@ -55966,7 +55827,7 @@ var init_api = __esm(() => {
55966
55827
  ANSI_BACKGROUND_OFFSET = 10;
55967
55828
  wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
55968
55829
  wrapAnsi256 = (offset = 0) => (code) => `\x1B[${38 + offset};5;${code}m`;
55969
- wrapAnsi16m = (offset = 0) => (red, green3, blue2) => `\x1B[${38 + offset};2;${red};${green3};${blue2}m`;
55830
+ wrapAnsi16m = (offset = 0) => (red, green, blue) => `\x1B[${38 + offset};2;${red};${green};${blue}m`;
55970
55831
  styles = {
55971
55832
  modifier: {
55972
55833
  reset: [0, 0],
@@ -162180,8 +162041,8 @@ var require_colorette = __commonJS2((exports) => {
162180
162041
  var createColors = ({ useColor = isColorSupported } = {}) => useColor ? colors4 : Object.keys(colors4).reduce((colors5, key) => ({ ...colors5, [key]: String }), {});
162181
162042
  var {
162182
162043
  reset,
162183
- bold: bold22,
162184
- dim: dim22,
162044
+ bold: bold2,
162045
+ dim: dim2,
162185
162046
  italic,
162186
162047
  underline,
162187
162048
  inverse,
@@ -162189,11 +162050,11 @@ var require_colorette = __commonJS2((exports) => {
162189
162050
  strikethrough,
162190
162051
  black,
162191
162052
  red,
162192
- green: green3,
162193
- yellow,
162194
- blue: blue2,
162053
+ green,
162054
+ yellow: yellow2,
162055
+ blue,
162195
162056
  magenta,
162196
- cyan: cyan3,
162057
+ cyan,
162197
162058
  white,
162198
162059
  gray,
162199
162060
  bgBlack,
@@ -162239,15 +162100,15 @@ var require_colorette = __commonJS2((exports) => {
162239
162100
  exports.bgYellowBright = bgYellowBright;
162240
162101
  exports.black = black;
162241
162102
  exports.blackBright = blackBright;
162242
- exports.blue = blue2;
162103
+ exports.blue = blue;
162243
162104
  exports.blueBright = blueBright;
162244
- exports.bold = bold22;
162105
+ exports.bold = bold2;
162245
162106
  exports.createColors = createColors;
162246
- exports.cyan = cyan3;
162107
+ exports.cyan = cyan;
162247
162108
  exports.cyanBright = cyanBright;
162248
- exports.dim = dim22;
162109
+ exports.dim = dim2;
162249
162110
  exports.gray = gray;
162250
- exports.green = green3;
162111
+ exports.green = green;
162251
162112
  exports.greenBright = greenBright;
162252
162113
  exports.hidden = hidden;
162253
162114
  exports.inverse = inverse;
@@ -162262,7 +162123,7 @@ var require_colorette = __commonJS2((exports) => {
162262
162123
  exports.underline = underline;
162263
162124
  exports.white = white;
162264
162125
  exports.whiteBright = whiteBright;
162265
- exports.yellow = yellow;
162126
+ exports.yellow = yellow2;
162266
162127
  exports.yellowBright = yellowBright;
162267
162128
  });
162268
162129
  var require_dist22 = __commonJS2((exports) => {
@@ -175287,6 +175148,7 @@ var config = {
175287
175148
  };
175288
175149
  process.env.BETTER_AUTH_SECRET = config.auth.betterAuthSecret;
175289
175150
  process.env.GAME_JWT_SECRET = config.auth.gameJwtSecret;
175151
+ process.env.PUBLIC_IS_LOCAL = "true";
175290
175152
  var RequestError = class extends Error {
175291
175153
  constructor(message3, options) {
175292
175154
  super(message3, options);
@@ -175822,9 +175684,9 @@ var serve = (options, listeningListener) => {
175822
175684
  });
175823
175685
  return server;
175824
175686
  };
175825
- var package_default2 = {
175687
+ var package_default = {
175826
175688
  name: "@playcademy/sandbox",
175827
- version: "0.1.11",
175689
+ version: "0.2.0",
175828
175690
  description: "Local development server for Playcademy game development",
175829
175691
  type: "module",
175830
175692
  exports: {
@@ -177412,7 +177274,7 @@ var colorStatus = async (status) => {
177412
177274
  }
177413
177275
  return `${status}`;
177414
177276
  };
177415
- async function log3(fn, prefix2, method, path4, status = 0, elapsed) {
177277
+ async function log(fn, prefix2, method, path4, status = 0, elapsed) {
177416
177278
  const out2 = prefix2 === "<--" ? `${prefix2} ${method} ${path4}` : `${prefix2} ${method} ${path4} ${await colorStatus(status)} ${elapsed}`;
177417
177279
  fn(out2);
177418
177280
  }
@@ -177420,10 +177282,10 @@ var logger2 = (fn = console.log) => {
177420
177282
  return async function logger2(c3, next) {
177421
177283
  const { method, url } = c3.req;
177422
177284
  const path4 = url.slice(url.indexOf("/", 8));
177423
- await log3(fn, "<--", method, path4);
177285
+ await log(fn, "<--", method, path4);
177424
177286
  const start2 = Date.now();
177425
177287
  await next();
177426
- await log3(fn, "-->", method, path4, c3.res.status, time(start2));
177288
+ await log(fn, "-->", method, path4, c3.res.status, time(start2));
177427
177289
  };
177428
177290
  };
177429
177291
  var entityKind = Symbol.for("drizzle:entityKind");
@@ -189928,7 +189790,7 @@ var createLogger2 = () => {
189928
189790
  log: performLog2
189929
189791
  };
189930
189792
  };
189931
- var log32 = createLogger2();
189793
+ var log3 = createLogger2();
189932
189794
  async function deleteTimebackResources(client2, courseId) {
189933
189795
  const sourcedIds = deriveSourcedIds(courseId);
189934
189796
  const { fetchTimebackConfig: fetchTimebackConfig2 } = await Promise.resolve().then(() => (init_verify5(), exports_verify));
@@ -189976,7 +189838,7 @@ async function deleteTimebackResources(client2, courseId) {
189976
189838
  lessonType: config2.componentResource.lessonType
189977
189839
  })
189978
189840
  ]);
189979
- log32.info("[TimeBack] Resources soft-deleted", { courseId });
189841
+ log3.info("[TimeBack] Resources soft-deleted", { courseId });
189980
189842
  }
189981
189843
  init_verify5();
189982
189844
  async function createCourse(client2, config2) {
@@ -189996,12 +189858,12 @@ async function createCourse(client2, config2) {
189996
189858
  if (!response.sourcedIdPairs?.allocatedSourcedId) {
189997
189859
  throw new Error("Course created but TimeBack did not return allocatedSourcedId");
189998
189860
  }
189999
- log32.info("[TimeBack] Course created", {
189861
+ log3.info("[TimeBack] Course created", {
190000
189862
  courseId: response.sourcedIdPairs.allocatedSourcedId
190001
189863
  });
190002
189864
  return response.sourcedIdPairs.allocatedSourcedId;
190003
189865
  } catch (error2) {
190004
- log32.error("[TimeBack] Failed to create course", {
189866
+ log3.error("[TimeBack] Failed to create course", {
190005
189867
  title: config2.course.title,
190006
189868
  error: error2
190007
189869
  });
@@ -190020,9 +189882,9 @@ async function createComponent(client2, config2, sourcedIds) {
190020
189882
  prerequisiteCriteria: config2.component.prerequisiteCriteria
190021
189883
  };
190022
189884
  await client2.oneroster.courseComponents.create({ courseComponent: componentData });
190023
- log32.info("[TimeBack] Component created", { componentId: sourcedIds.component });
189885
+ log3.info("[TimeBack] Component created", { componentId: sourcedIds.component });
190024
189886
  } catch (error2) {
190025
- log32.error("[TimeBack] Failed to create component", {
189887
+ log3.error("[TimeBack] Failed to create component", {
190026
189888
  componentId: sourcedIds.component,
190027
189889
  error: error2
190028
189890
  });
@@ -190042,11 +189904,11 @@ async function createResource(client2, config2, sourcedIds) {
190042
189904
  importance: config2.resource.importance,
190043
189905
  metadata: config2.resource.metadata
190044
189906
  };
190045
- log32.debug("[TimeBack] Creating resource", { resourceId: sourcedIds.resource });
189907
+ log3.debug("[TimeBack] Creating resource", { resourceId: sourcedIds.resource });
190046
189908
  await client2.oneroster.resources.create({ resource: resourceData });
190047
- log32.info("[TimeBack] Resource created", { resourceId: sourcedIds.resource });
189909
+ log3.info("[TimeBack] Resource created", { resourceId: sourcedIds.resource });
190048
189910
  } catch (error2) {
190049
- log32.error("[TimeBack] Failed to create resource", {
189911
+ log3.error("[TimeBack] Failed to create resource", {
190050
189912
  resourceId: sourcedIds.resource,
190051
189913
  error: error2
190052
189914
  });
@@ -190064,17 +189926,17 @@ async function createComponentResourceLink(client2, config2, sourcedIds) {
190064
189926
  sortOrder: config2.componentResource.sortOrder,
190065
189927
  lessonType: config2.componentResource.lessonType
190066
189928
  };
190067
- log32.debug("[TimeBack] Creating component-resource link", {
189929
+ log3.debug("[TimeBack] Creating component-resource link", {
190068
189930
  componentResourceId: sourcedIds.componentResource
190069
189931
  });
190070
189932
  await client2.oneroster.componentResources.create({
190071
189933
  componentResource: componentResourceData
190072
189934
  });
190073
- log32.info("[TimeBack] Component-resource link created", {
189935
+ log3.info("[TimeBack] Component-resource link created", {
190074
189936
  componentResourceId: sourcedIds.componentResource
190075
189937
  });
190076
189938
  } catch (error2) {
190077
- log32.error("[TimeBack] Failed to create component-resource link", {
189939
+ log3.error("[TimeBack] Failed to create component-resource link", {
190078
189940
  componentResourceId: sourcedIds.componentResource,
190079
189941
  error: error2
190080
189942
  });
@@ -190098,7 +189960,7 @@ async function setupTimebackResources(client2, config2, options) {
190098
189960
  ...verboseData && { verboseData }
190099
189961
  };
190100
189962
  } catch (error2) {
190101
- log32.error("[TimeBack] Setup failed", { error: error2 });
189963
+ log3.error("[TimeBack] Setup failed", { error: error2 });
190102
189964
  throw error2;
190103
189965
  }
190104
189966
  }
@@ -190117,9 +189979,9 @@ async function updateCourse(client2, config2, courseId) {
190117
189979
  metadata: config2.course.metadata
190118
189980
  };
190119
189981
  await client2.oneroster.courses.update(courseId, courseData);
190120
- log32.info("[TimeBack] Course updated", { courseId });
189982
+ log3.info("[TimeBack] Course updated", { courseId });
190121
189983
  } catch (error2) {
190122
- log32.error("[TimeBack] Failed to update course", {
189984
+ log3.error("[TimeBack] Failed to update course", {
190123
189985
  courseId,
190124
189986
  error: error2
190125
189987
  });
@@ -190138,9 +190000,9 @@ async function updateComponent(client2, config2, sourcedIds) {
190138
190000
  prerequisiteCriteria: config2.component.prerequisiteCriteria
190139
190001
  };
190140
190002
  await client2.oneroster.courseComponents.update(sourcedIds.component, componentData);
190141
- log32.info("[TimeBack] Component updated", { componentId: sourcedIds.component });
190003
+ log3.info("[TimeBack] Component updated", { componentId: sourcedIds.component });
190142
190004
  } catch (error2) {
190143
- log32.error("[TimeBack] Failed to update component", {
190005
+ log3.error("[TimeBack] Failed to update component", {
190144
190006
  componentId: sourcedIds.component,
190145
190007
  error: error2
190146
190008
  });
@@ -190161,9 +190023,9 @@ async function updateResource(client2, config2, resourceId) {
190161
190023
  metadata: config2.resource.metadata
190162
190024
  };
190163
190025
  await client2.oneroster.resources.update(resourceId, resourceData);
190164
- log32.info("[TimeBack] Resource updated", { resourceId });
190026
+ log3.info("[TimeBack] Resource updated", { resourceId });
190165
190027
  } catch (error2) {
190166
- log32.error("[TimeBack] Failed to update resource", {
190028
+ log3.error("[TimeBack] Failed to update resource", {
190167
190029
  resourceId,
190168
190030
  error: error2
190169
190031
  });
@@ -190182,11 +190044,11 @@ async function updateComponentResourceLink(client2, config2, sourcedIds) {
190182
190044
  lessonType: config2.componentResource.lessonType
190183
190045
  };
190184
190046
  await client2.oneroster.componentResources.update(sourcedIds.componentResource, componentResourceData);
190185
- log32.info("[TimeBack] Component-resource link updated", {
190047
+ log3.info("[TimeBack] Component-resource link updated", {
190186
190048
  componentResourceId: sourcedIds.componentResource
190187
190049
  });
190188
190050
  } catch (error2) {
190189
- log32.error("[TimeBack] Failed to update component-resource link", {
190051
+ log3.error("[TimeBack] Failed to update component-resource link", {
190190
190052
  componentResourceId: sourcedIds.componentResource,
190191
190053
  error: error2
190192
190054
  });
@@ -190355,7 +190217,7 @@ async function request({
190355
190217
  lastError = error2;
190356
190218
  if (attempt < retries) {
190357
190219
  const delay = Math.pow(HTTP_DEFAULTS.retryBackoffBase, attempt) * 1000;
190358
- log32.warn(`[TimebackClient] Request failed, retrying in ${delay}ms`, {
190220
+ log3.warn(`[TimebackClient] Request failed, retrying in ${delay}ms`, {
190359
190221
  attempt: attempt + 1,
190360
190222
  maxRetries: retries,
190361
190223
  status: res.status,
@@ -190387,7 +190249,7 @@ async function request({
190387
190249
  lastError = error2 instanceof Error ? error2 : new Error(String(error2));
190388
190250
  if (attempt < retries) {
190389
190251
  const delay = Math.pow(HTTP_DEFAULTS.retryBackoffBase, attempt) * 1000;
190390
- log32.warn(`[TimebackClient] Network error, retrying in ${delay}ms`, {
190252
+ log3.warn(`[TimebackClient] Network error, retrying in ${delay}ms`, {
190391
190253
  attempt: attempt + 1,
190392
190254
  maxRetries: retries,
190393
190255
  error: lastError.message,
@@ -190410,11 +190272,158 @@ async function requestCaliper(options) {
190410
190272
  });
190411
190273
  }
190412
190274
  init_constants();
190275
+ function createCaliperNamespace(client2) {
190276
+ const urls = createOneRosterUrls(client2.getBaseUrl());
190277
+ const caliper = {
190278
+ emit: async (event, sensorUrl) => {
190279
+ const envelope = {
190280
+ sensor: sensorUrl,
190281
+ sendTime: new Date().toISOString(),
190282
+ dataVersion: CALIPER_CONSTANTS.dataVersion,
190283
+ data: [event]
190284
+ };
190285
+ return client2["requestCaliper"](CALIPER_ENDPOINTS.events, "POST", envelope);
190286
+ },
190287
+ emitBatch: async (events, sensorUrl) => {
190288
+ if (events.length === 0)
190289
+ return;
190290
+ const envelope = {
190291
+ sensor: sensorUrl,
190292
+ sendTime: new Date().toISOString(),
190293
+ dataVersion: CALIPER_CONSTANTS.dataVersion,
190294
+ data: events
190295
+ };
190296
+ return client2["requestCaliper"](CALIPER_ENDPOINTS.events, "POST", envelope);
190297
+ },
190298
+ emitActivityEvent: async (data) => {
190299
+ const event = {
190300
+ "@context": CALIPER_CONSTANTS.context,
190301
+ id: `urn:uuid:${crypto.randomUUID()}`,
190302
+ type: TIMEBACK_EVENT_TYPES.activityEvent,
190303
+ eventTime: new Date().toISOString(),
190304
+ profile: CALIPER_CONSTANTS.profile,
190305
+ actor: {
190306
+ id: urls.user(data.studentId),
190307
+ type: TIMEBACK_TYPES.user,
190308
+ email: data.studentEmail
190309
+ },
190310
+ action: TIMEBACK_ACTIONS.completed,
190311
+ object: {
190312
+ id: caliper.buildActivityUrl(data),
190313
+ type: TIMEBACK_TYPES.activityContext,
190314
+ subject: data.subject,
190315
+ app: {
190316
+ name: data.appName
190317
+ },
190318
+ activity: {
190319
+ name: data.activityName
190320
+ },
190321
+ course: { id: urls.course(data.courseId), name: data.activityName },
190322
+ process: false
190323
+ },
190324
+ generated: {
190325
+ id: `urn:timeback:metrics:activity-completion-${crypto.randomUUID()}`,
190326
+ type: TIMEBACK_TYPES.activityMetricsCollection,
190327
+ attempt: data.attemptNumber || 1,
190328
+ items: [
190329
+ ...data.totalQuestions !== undefined ? [
190330
+ {
190331
+ type: ACTIVITY_METRIC_TYPES.totalQuestions,
190332
+ value: data.totalQuestions
190333
+ }
190334
+ ] : [],
190335
+ ...data.correctQuestions !== undefined ? [
190336
+ {
190337
+ type: ACTIVITY_METRIC_TYPES.correctQuestions,
190338
+ value: data.correctQuestions
190339
+ }
190340
+ ] : [],
190341
+ ...data.xpEarned !== undefined ? [{ type: ACTIVITY_METRIC_TYPES.xpEarned, value: data.xpEarned }] : [],
190342
+ ...data.masteredUnits !== undefined ? [
190343
+ {
190344
+ type: ACTIVITY_METRIC_TYPES.masteredUnits,
190345
+ value: data.masteredUnits
190346
+ }
190347
+ ] : []
190348
+ ],
190349
+ ...data.extensions ? { extensions: data.extensions } : {}
190350
+ }
190351
+ };
190352
+ return caliper.emit(event, data.sensorUrl);
190353
+ },
190354
+ emitTimeSpentEvent: async (data) => {
190355
+ const event = {
190356
+ "@context": CALIPER_CONSTANTS.context,
190357
+ id: `urn:uuid:${crypto.randomUUID()}`,
190358
+ type: TIMEBACK_EVENT_TYPES.timeSpentEvent,
190359
+ eventTime: new Date().toISOString(),
190360
+ profile: CALIPER_CONSTANTS.profile,
190361
+ actor: {
190362
+ id: urls.user(data.studentId),
190363
+ type: TIMEBACK_TYPES.user,
190364
+ email: data.studentEmail
190365
+ },
190366
+ action: TIMEBACK_ACTIONS.spentTime,
190367
+ object: {
190368
+ id: caliper.buildActivityUrl(data),
190369
+ type: TIMEBACK_TYPES.activityContext,
190370
+ subject: data.subject,
190371
+ app: {
190372
+ name: data.appName
190373
+ },
190374
+ activity: {
190375
+ name: data.activityName
190376
+ },
190377
+ course: { id: urls.course(data.courseId), name: data.activityName },
190378
+ process: false
190379
+ },
190380
+ generated: {
190381
+ id: `urn:timeback:metrics:time-spent-${crypto.randomUUID()}`,
190382
+ type: TIMEBACK_TYPES.timeSpentMetricsCollection,
190383
+ items: [
190384
+ { type: TIME_METRIC_TYPES.active, value: data.activeTimeSeconds },
190385
+ ...data.inactiveTimeSeconds !== undefined ? [
190386
+ {
190387
+ type: TIME_METRIC_TYPES.inactive,
190388
+ value: data.inactiveTimeSeconds
190389
+ }
190390
+ ] : [],
190391
+ ...data.wasteTimeSeconds !== undefined ? [{ type: TIME_METRIC_TYPES.waste, value: data.wasteTimeSeconds }] : []
190392
+ ]
190393
+ }
190394
+ };
190395
+ return caliper.emit(event, data.sensorUrl);
190396
+ },
190397
+ buildActivityUrl: (data) => {
190398
+ const base = data.sensorUrl.replace(/\/$/, "");
190399
+ return `${base}/activities/${data.courseId}/${data.activityId}/${crypto.randomUUID()}`;
190400
+ }
190401
+ };
190402
+ return caliper;
190403
+ }
190404
+ function createEduBridgeNamespace(client2) {
190405
+ const enrollments = {
190406
+ listByUser: async (userId) => {
190407
+ const response = await client2["request"](`/edubridge/enrollments/user/${userId}`, "GET");
190408
+ return response.data;
190409
+ }
190410
+ };
190411
+ const analytics = {
190412
+ getEnrollmentFacts: async (enrollmentId) => {
190413
+ return client2["request"](`/edubridge/analytics/enrollment/${enrollmentId}`, "GET");
190414
+ }
190415
+ };
190416
+ return {
190417
+ enrollments,
190418
+ analytics
190419
+ };
190420
+ }
190421
+ init_constants();
190413
190422
  function logTimebackError(operation, error2, context) {
190414
190423
  const errorMessage = error2 instanceof Error ? error2.message : String(error2);
190415
190424
  if (error2 instanceof TimebackApiError) {
190416
190425
  const imsError = error2.details;
190417
- log32.error(`[TimeBack ${operation}] ${errorMessage}`, {
190426
+ log3.error(`[TimeBack ${operation}] ${errorMessage}`, {
190418
190427
  ...context,
190419
190428
  status: error2.status,
190420
190429
  imsx_codeMajor: imsError?.imsx_codeMajor,
@@ -190425,7 +190434,7 @@ function logTimebackError(operation, error2, context) {
190425
190434
  console.error(`[TimeBack Error during ${operation}] Full details:`, JSON.stringify(error2.details, null, 2));
190426
190435
  }
190427
190436
  } else {
190428
- log32.error(`[TimeBack ${operation}] ${errorMessage}`, context);
190437
+ log3.error(`[TimeBack ${operation}] ${errorMessage}`, context);
190429
190438
  }
190430
190439
  }
190431
190440
  function createOneRosterNamespace(client2) {
@@ -190624,153 +190633,6 @@ function createOneRosterNamespace(client2) {
190624
190633
  };
190625
190634
  }
190626
190635
  init_constants();
190627
- function createCaliperNamespace(client2) {
190628
- const urls = createOneRosterUrls(client2.getBaseUrl());
190629
- const caliper = {
190630
- emit: async (event, sensorUrl) => {
190631
- const envelope = {
190632
- sensor: sensorUrl,
190633
- sendTime: new Date().toISOString(),
190634
- dataVersion: CALIPER_CONSTANTS.dataVersion,
190635
- data: [event]
190636
- };
190637
- return client2["requestCaliper"](CALIPER_ENDPOINTS.events, "POST", envelope);
190638
- },
190639
- emitBatch: async (events, sensorUrl) => {
190640
- if (events.length === 0)
190641
- return;
190642
- const envelope = {
190643
- sensor: sensorUrl,
190644
- sendTime: new Date().toISOString(),
190645
- dataVersion: CALIPER_CONSTANTS.dataVersion,
190646
- data: events
190647
- };
190648
- return client2["requestCaliper"](CALIPER_ENDPOINTS.events, "POST", envelope);
190649
- },
190650
- emitActivityEvent: async (data) => {
190651
- const event = {
190652
- "@context": CALIPER_CONSTANTS.context,
190653
- id: `urn:uuid:${crypto.randomUUID()}`,
190654
- type: TIMEBACK_EVENT_TYPES.activityEvent,
190655
- eventTime: new Date().toISOString(),
190656
- profile: CALIPER_CONSTANTS.profile,
190657
- actor: {
190658
- id: urls.user(data.studentId),
190659
- type: TIMEBACK_TYPES.user,
190660
- email: data.studentEmail
190661
- },
190662
- action: TIMEBACK_ACTIONS.completed,
190663
- object: {
190664
- id: caliper.buildActivityUrl(data),
190665
- type: TIMEBACK_TYPES.activityContext,
190666
- subject: data.subject,
190667
- app: {
190668
- name: data.appName
190669
- },
190670
- activity: {
190671
- name: data.activityName
190672
- },
190673
- course: { id: urls.course(data.courseId), name: data.activityName },
190674
- process: false
190675
- },
190676
- generated: {
190677
- id: `urn:timeback:metrics:activity-completion-${crypto.randomUUID()}`,
190678
- type: TIMEBACK_TYPES.activityMetricsCollection,
190679
- attempt: data.attemptNumber || 1,
190680
- items: [
190681
- ...data.totalQuestions !== undefined ? [
190682
- {
190683
- type: ACTIVITY_METRIC_TYPES.totalQuestions,
190684
- value: data.totalQuestions
190685
- }
190686
- ] : [],
190687
- ...data.correctQuestions !== undefined ? [
190688
- {
190689
- type: ACTIVITY_METRIC_TYPES.correctQuestions,
190690
- value: data.correctQuestions
190691
- }
190692
- ] : [],
190693
- ...data.xpEarned !== undefined ? [{ type: ACTIVITY_METRIC_TYPES.xpEarned, value: data.xpEarned }] : [],
190694
- ...data.masteredUnits !== undefined ? [
190695
- {
190696
- type: ACTIVITY_METRIC_TYPES.masteredUnits,
190697
- value: data.masteredUnits
190698
- }
190699
- ] : []
190700
- ],
190701
- ...data.extensions ? { extensions: data.extensions } : {}
190702
- }
190703
- };
190704
- return caliper.emit(event, data.sensorUrl);
190705
- },
190706
- emitTimeSpentEvent: async (data) => {
190707
- const event = {
190708
- "@context": CALIPER_CONSTANTS.context,
190709
- id: `urn:uuid:${crypto.randomUUID()}`,
190710
- type: TIMEBACK_EVENT_TYPES.timeSpentEvent,
190711
- eventTime: new Date().toISOString(),
190712
- profile: CALIPER_CONSTANTS.profile,
190713
- actor: {
190714
- id: urls.user(data.studentId),
190715
- type: TIMEBACK_TYPES.user,
190716
- email: data.studentEmail
190717
- },
190718
- action: TIMEBACK_ACTIONS.spentTime,
190719
- object: {
190720
- id: caliper.buildActivityUrl(data),
190721
- type: TIMEBACK_TYPES.activityContext,
190722
- subject: data.subject,
190723
- app: {
190724
- name: data.appName
190725
- },
190726
- activity: {
190727
- name: data.activityName
190728
- },
190729
- course: { id: urls.course(data.courseId), name: data.activityName },
190730
- process: false
190731
- },
190732
- generated: {
190733
- id: `urn:timeback:metrics:time-spent-${crypto.randomUUID()}`,
190734
- type: TIMEBACK_TYPES.timeSpentMetricsCollection,
190735
- items: [
190736
- { type: TIME_METRIC_TYPES.active, value: data.activeTimeSeconds },
190737
- ...data.inactiveTimeSeconds !== undefined ? [
190738
- {
190739
- type: TIME_METRIC_TYPES.inactive,
190740
- value: data.inactiveTimeSeconds
190741
- }
190742
- ] : [],
190743
- ...data.wasteTimeSeconds !== undefined ? [{ type: TIME_METRIC_TYPES.waste, value: data.wasteTimeSeconds }] : []
190744
- ]
190745
- }
190746
- };
190747
- return caliper.emit(event, data.sensorUrl);
190748
- },
190749
- buildActivityUrl: (data) => {
190750
- const base = data.sensorUrl.replace(/\/$/, "");
190751
- return `${base}/activities/${data.courseId}/${data.activityId}/${crypto.randomUUID()}`;
190752
- }
190753
- };
190754
- return caliper;
190755
- }
190756
- function createEduBridgeNamespace(client2) {
190757
- const enrollments = {
190758
- listByUser: async (userId) => {
190759
- const response = await client2["request"](`/edubridge/enrollments/user/${userId}`, "GET");
190760
- return response.data;
190761
- }
190762
- };
190763
- const analytics = {
190764
- getEnrollmentFacts: async (enrollmentId) => {
190765
- return client2["request"](`/edubridge/analytics/enrollment/${enrollmentId}`, "GET");
190766
- }
190767
- };
190768
- return {
190769
- enrollments,
190770
- analytics
190771
- };
190772
- }
190773
- init_constants();
190774
190636
  init_constants();
190775
190637
 
190776
190638
  class TimebackCache {
@@ -190792,7 +190654,7 @@ class TimebackCache {
190792
190654
  }
190793
190655
  if (Date.now() > entry.expiresAt) {
190794
190656
  this.delete(key);
190795
- log32.debug(`[${this.name}] Cache entry expired`, { key });
190657
+ log3.debug(`[${this.name}] Cache entry expired`, { key });
190796
190658
  this.misses++;
190797
190659
  return;
190798
190660
  }
@@ -190807,7 +190669,7 @@ class TimebackCache {
190807
190669
  }
190808
190670
  this.cache.set(key, { value, expiresAt });
190809
190671
  this._updateAccessOrder(key);
190810
- log32.debug(`[${this.name}] Cache entry set`, {
190672
+ log3.debug(`[${this.name}] Cache entry set`, {
190811
190673
  key,
190812
190674
  expiresAt: new Date(expiresAt).toISOString()
190813
190675
  });
@@ -190822,7 +190684,7 @@ class TimebackCache {
190822
190684
  clear() {
190823
190685
  this.cache.clear();
190824
190686
  this.accessOrder = [];
190825
- log32.debug(`[${this.name}] Cache cleared`);
190687
+ log3.debug(`[${this.name}] Cache cleared`);
190826
190688
  }
190827
190689
  size() {
190828
190690
  return this.cache.size;
@@ -190845,7 +190707,7 @@ class TimebackCache {
190845
190707
  this.delete(key);
190846
190708
  }
190847
190709
  if (expiredKeys.length > 0) {
190848
- log32.debug(`[${this.name}] Cleaned up expired entries`, {
190710
+ log3.debug(`[${this.name}] Cleaned up expired entries`, {
190849
190711
  count: expiredKeys.length
190850
190712
  });
190851
190713
  }
@@ -190870,7 +190732,7 @@ class TimebackCache {
190870
190732
  return;
190871
190733
  const oldestKey = this.accessOrder[0];
190872
190734
  this.delete(oldestKey);
190873
- log32.debug(`[${this.name}] Evicted LRU entry`, { key: oldestKey });
190735
+ log3.debug(`[${this.name}] Evicted LRU entry`, { key: oldestKey });
190874
190736
  }
190875
190737
  }
190876
190738
 
@@ -190930,7 +190792,7 @@ class TimebackCacheManager {
190930
190792
  this.assessmentLineItemCache.clear();
190931
190793
  this.resourceMasteryCache.clear();
190932
190794
  this.enrollmentCache.clear();
190933
- log32.info("[TimebackCacheManager] All caches cleared");
190795
+ log3.info("[TimebackCacheManager] All caches cleared");
190934
190796
  }
190935
190797
  getStats() {
190936
190798
  return {
@@ -190945,7 +190807,7 @@ class TimebackCacheManager {
190945
190807
  this.assessmentLineItemCache.cleanup();
190946
190808
  this.resourceMasteryCache.cleanup();
190947
190809
  this.enrollmentCache.cleanup();
190948
- log32.debug("[TimebackCacheManager] Cache cleanup completed");
190810
+ log3.debug("[TimebackCacheManager] Cache cleanup completed");
190949
190811
  }
190950
190812
  }
190951
190813
  init_constants();
@@ -190966,7 +190828,7 @@ class MasteryTracker {
190966
190828
  }
190967
190829
  const masterableUnits = await this.resolveMasterableUnits(resourceId);
190968
190830
  if (!masterableUnits || masterableUnits <= 0) {
190969
- log32.warn("[MasteryTracker] No masterableUnits configured for course", {
190831
+ log3.warn("[MasteryTracker] No masterableUnits configured for course", {
190970
190832
  courseId,
190971
190833
  resourceId
190972
190834
  });
@@ -190974,7 +190836,7 @@ class MasteryTracker {
190974
190836
  }
190975
190837
  const facts = await this.fetchEnrollmentAnalyticsFacts(studentId, courseId);
190976
190838
  if (!facts) {
190977
- log32.warn("[MasteryTracker] Unable to retrieve analytics for mastery-based completion", {
190839
+ log3.warn("[MasteryTracker] Unable to retrieve analytics for mastery-based completion", {
190978
190840
  studentId,
190979
190841
  courseId
190980
190842
  });
@@ -191014,13 +190876,13 @@ class MasteryTracker {
191014
190876
  appName
191015
190877
  }
191016
190878
  });
191017
- log32.info("[MasteryTracker] Created mastery completion entry", {
190879
+ log3.info("[MasteryTracker] Created mastery completion entry", {
191018
190880
  studentId,
191019
190881
  lineItemId,
191020
190882
  resultId
191021
190883
  });
191022
190884
  } catch (error2) {
191023
- log32.error("[MasteryTracker] Failed to create mastery completion entry", {
190885
+ log3.error("[MasteryTracker] Failed to create mastery completion entry", {
191024
190886
  studentId,
191025
190887
  lineItemId,
191026
190888
  error: error2
@@ -191045,7 +190907,7 @@ class MasteryTracker {
191045
190907
  this.cacheManager.setResourceMasterableUnits(resourceId, masterableUnits ?? null);
191046
190908
  return masterableUnits;
191047
190909
  } catch (error2) {
191048
- log32.error("[MasteryTracker] Failed to fetch resource metadata for mastery config", {
190910
+ log3.error("[MasteryTracker] Failed to fetch resource metadata for mastery config", {
191049
190911
  resourceId,
191050
190912
  error: error2
191051
190913
  });
@@ -191058,7 +190920,7 @@ class MasteryTracker {
191058
190920
  const enrollments = await this.edubridgeNamespace.enrollments.listByUser(studentId);
191059
190921
  const enrollment = enrollments.find((e2) => e2.course.id === courseId);
191060
190922
  if (!enrollment) {
191061
- log32.warn("[MasteryTracker] Enrollment not found for student/course", {
190923
+ log3.warn("[MasteryTracker] Enrollment not found for student/course", {
191062
190924
  studentId,
191063
190925
  courseId
191064
190926
  });
@@ -191067,7 +190929,7 @@ class MasteryTracker {
191067
190929
  const analytics = await this.edubridgeNamespace.analytics.getEnrollmentFacts(enrollment.id);
191068
190930
  return analytics.facts;
191069
190931
  } catch (error2) {
191070
- log32.error("[MasteryTracker] Failed to load enrollment analytics facts", {
190932
+ log3.error("[MasteryTracker] Failed to load enrollment analytics facts", {
191071
190933
  studentId,
191072
190934
  courseId,
191073
190935
  error: error2
@@ -191190,7 +191052,7 @@ class ProgressRecorder {
191190
191052
  if (score !== undefined) {
191191
191053
  await this.createGradebookEntry(actualLineItemId, studentId, currentAttemptNumber, score, totalQuestions, correctQuestions, calculatedXp, masteredUnits, scoreStatus, inProgress, progressData.appName);
191192
191054
  } else {
191193
- log32.warn("[ProgressRecorder] Score not provided, skipping gradebook entry", {
191055
+ log3.warn("[ProgressRecorder] Score not provided, skipping gradebook entry", {
191194
191056
  studentId,
191195
191057
  activityId,
191196
191058
  attemptNumber: currentAttemptNumber
@@ -191237,13 +191099,13 @@ class ProgressRecorder {
191237
191099
  }
191238
191100
  calculateXpForProgress(progressData, totalQuestions, correctQuestions, xpEarned, isFirstAttempt) {
191239
191101
  if (xpEarned !== undefined) {
191240
- log32.debug("[ProgressRecorder] Using provided XP", { xpEarned });
191102
+ log3.debug("[ProgressRecorder] Using provided XP", { xpEarned });
191241
191103
  return xpEarned;
191242
191104
  }
191243
191105
  if (progressData.sessionDurationSeconds && totalQuestions && correctQuestions) {
191244
191106
  const accuracy = correctQuestions / totalQuestions;
191245
191107
  const calculatedXp = calculateXp(progressData.sessionDurationSeconds, accuracy, isFirstAttempt);
191246
- log32.debug("[ProgressRecorder] Calculated XP", {
191108
+ log3.debug("[ProgressRecorder] Calculated XP", {
191247
191109
  durationSeconds: progressData.sessionDurationSeconds,
191248
191110
  accuracy,
191249
191111
  isFirstAttempt,
@@ -191325,7 +191187,7 @@ class ProgressRecorder {
191325
191187
  sensorUrl: progressData.sensorUrl,
191326
191188
  extensions: extensions || progressData.extensions
191327
191189
  }).catch((error2) => {
191328
- log32.error("[ProgressRecorder] Failed to emit activity event", { error: error2 });
191190
+ log3.error("[ProgressRecorder] Failed to emit activity event", { error: error2 });
191329
191191
  });
191330
191192
  }
191331
191193
  }
@@ -195371,7 +195233,7 @@ class StudentResolver {
195371
195233
  if (error2 instanceof TimebackAuthenticationError || error2 instanceof StudentNotFoundError || error2 instanceof TimebackError) {
195372
195234
  throw error2;
195373
195235
  }
195374
- log32.error("[StudentResolver] Failed to find student by email", {
195236
+ log3.error("[StudentResolver] Failed to find student by email", {
195375
195237
  email,
195376
195238
  error: error2
195377
195239
  });
@@ -195395,7 +195257,7 @@ class StudentResolver {
195395
195257
  this.cacheStudent(email, studentId, studentData);
195396
195258
  return studentData;
195397
195259
  } catch (error2) {
195398
- log32.error("[StudentResolver] Failed to fetch student by ID, using fallback", {
195260
+ log3.error("[StudentResolver] Failed to fetch student by ID, using fallback", {
195399
195261
  studentId,
195400
195262
  error: error2,
195401
195263
  errorMessage: error2 instanceof Error ? error2.message : String(error2),
@@ -195446,7 +195308,7 @@ class TimebackClient {
195446
195308
  this.sessionRecorder = new SessionRecorder(this.studentResolver, this.caliper);
195447
195309
  if (this.credentials) {
195448
195310
  this._ensureAuthenticated().catch((error2) => {
195449
- log32.error("[TimebackClient] Auto-authentication failed", { error: error2 });
195311
+ log3.error("[TimebackClient] Auto-authentication failed", { error: error2 });
195450
195312
  });
195451
195313
  }
195452
195314
  }
@@ -195506,11 +195368,11 @@ class TimebackClient {
195506
195368
  };
195507
195369
  const tokenData = await getTimebackTokenResponse(authConfig);
195508
195370
  this.setToken(tokenData.access_token, tokenData.expires_in);
195509
- log32.debug("[TimebackClient] Authentication successful", {
195371
+ log3.debug("[TimebackClient] Authentication successful", {
195510
195372
  expiresIn: tokenData.expires_in
195511
195373
  });
195512
195374
  } catch (error2) {
195513
- log32.error("[TimebackClient] Authentication failed", { error: error2 });
195375
+ log3.error("[TimebackClient] Authentication failed", { error: error2 });
195514
195376
  const errorMessage = error2 instanceof Error ? error2.message : String(error2);
195515
195377
  throw new TimebackAuthenticationError(`TimeBack authentication failed: ${errorMessage}. Verify that your client credentials are correct and not expired. Environment: ${this.baseUrl}.`);
195516
195378
  }
@@ -196216,6 +196078,68 @@ function buildResourceMetadata({
196216
196078
  }
196217
196079
  return metadata2;
196218
196080
  }
196081
+ 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
+ }));
196093
+ }
196094
+ log2.debug("[timeback-enrollments] Fetching student enrollments from TimeBack", { timebackId });
196095
+ try {
196096
+ 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) {
196100
+ return [];
196101
+ }
196102
+ const integrations = await db.query.gameTimebackIntegrations.findMany({
196103
+ where: inArray(gameTimebackIntegrations.courseId, courseIds)
196104
+ });
196105
+ return integrations.map((integration) => ({
196106
+ gameId: integration.gameId,
196107
+ grade: integration.grade,
196108
+ subject: integration.subject,
196109
+ courseId: integration.courseId
196110
+ }));
196111
+ } catch (error2) {
196112
+ log2.warn("[timeback-enrollments] Failed to fetch TimeBack enrollments:", {
196113
+ error: error2,
196114
+ timebackId
196115
+ });
196116
+ return [];
196117
+ }
196118
+ }
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
+ }
196135
+ }
196136
+ async function fetchUserTimebackData(timebackId) {
196137
+ const [role, enrollments] = await Promise.all([
196138
+ fetchUserRole(timebackId),
196139
+ fetchEnrollmentsForUser(timebackId)
196140
+ ]);
196141
+ return { role, enrollments };
196142
+ }
196219
196143
  var AchievementCompletionType;
196220
196144
  ((AchievementCompletionType2) => {
196221
196145
  AchievementCompletionType2["TIME_PLAYED_SESSION"] = "time_played_session";
@@ -199521,21 +199445,6 @@ var logger3 = {
199521
199445
  warn: (msg) => getLogger().warn(msg),
199522
199446
  error: (msg) => getLogger().error(msg)
199523
199447
  };
199524
- function detectTimebackCourses() {
199525
- const coursePattern = /^SANDBOX_TIMEBACK_COURSE_(\d+)_([A-Z_]+)$/i;
199526
- const courses = [];
199527
- for (const [key, value] of Object.entries(process.env)) {
199528
- const match2 = key.match(coursePattern);
199529
- if (match2 && value) {
199530
- const gradeStr = match2[1];
199531
- const subjectStr = match2[2];
199532
- const grade = parseInt(gradeStr, 10);
199533
- const subject = subjectStr.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
199534
- courses.push({ grade, subject, courseId: value });
199535
- }
199536
- }
199537
- return courses;
199538
- }
199539
199448
  async function seedCoreGames(db) {
199540
199449
  const now2 = new Date;
199541
199450
  const coreGames = [
@@ -199560,28 +199469,6 @@ async function seedCoreGames(db) {
199560
199469
  console.error(`Error seeding core game '${gameData.slug}':`, error2);
199561
199470
  }
199562
199471
  }
199563
- if (hasTimebackCredentials()) {
199564
- const courses = detectTimebackCourses();
199565
- if (courses.length > 0) {
199566
- for (const course of courses) {
199567
- try {
199568
- await db.insert(gameTimebackIntegrations).values({
199569
- id: crypto.randomUUID(),
199570
- gameId: CORE_GAME_UUIDS.PLAYGROUND,
199571
- courseId: course.courseId,
199572
- grade: course.grade,
199573
- subject: course.subject,
199574
- totalXp: null,
199575
- lastVerifiedAt: null,
199576
- createdAt: now2,
199577
- updatedAt: now2
199578
- }).onConflictDoNothing();
199579
- } catch (error2) {
199580
- console.error(`Error seeding TimeBack integration for playground (${course.subject} grade ${course.grade}):`, error2);
199581
- }
199582
- }
199583
- }
199584
- }
199585
199472
  }
199586
199473
  async function seedCurrentProjectGame(db, project) {
199587
199474
  const now2 = new Date;
@@ -199610,24 +199497,6 @@ async function seedCurrentProjectGame(db, project) {
199610
199497
  updatedAt: now2
199611
199498
  };
199612
199499
  const [newGame] = await db.insert(games).values(gameRecord).returning();
199613
- if (hasTimebackCredentials()) {
199614
- const courses = detectTimebackCourses();
199615
- if (courses.length > 0) {
199616
- for (const course of courses) {
199617
- await db.insert(gameTimebackIntegrations).values({
199618
- id: crypto.randomUUID(),
199619
- gameId: newGame.id,
199620
- courseId: course.courseId,
199621
- grade: course.grade,
199622
- subject: course.subject,
199623
- totalXp: null,
199624
- lastVerifiedAt: null,
199625
- createdAt: now2,
199626
- updatedAt: now2
199627
- }).onConflictDoNothing();
199628
- }
199629
- }
199630
- }
199631
199500
  return newGame;
199632
199501
  } catch (error2) {
199633
199502
  console.error("❌ Error seeding project game:", error2);
@@ -199684,12 +199553,23 @@ async function seedSpriteTemplates(db) {
199684
199553
  }
199685
199554
  }
199686
199555
  }
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
+ }
199687
199566
  async function seedDemoData(db) {
199688
199567
  try {
199568
+ const adminTimebackId = getAdminTimebackId();
199689
199569
  for (const [role, user] of Object.entries(DEMO_USERS)) {
199690
199570
  const userValues = {
199691
199571
  ...user,
199692
- timebackId: role === "admin" && config.timeback.studentId ? config.timeback.studentId : null
199572
+ timebackId: role === "admin" ? adminTimebackId : null
199693
199573
  };
199694
199574
  await db.insert(users).values(userValues).onConflictDoNothing();
199695
199575
  }
@@ -205052,9 +204932,11 @@ async function getUserMe(ctx) {
205052
204932
  const timebackAccount = await db.query.accounts.findFirst({
205053
204933
  where: and(eq(accounts.userId, user.id), eq(accounts.providerId, "timeback"))
205054
204934
  });
204935
+ const timeback3 = userData.timebackId ? await fetchUserTimebackData(userData.timebackId) : undefined;
205055
204936
  return {
205056
204937
  ...userData,
205057
- hasTimebackAccount: !!timebackAccount
204938
+ hasTimebackAccount: !!timebackAccount,
204939
+ timeback: timeback3
205058
204940
  };
205059
204941
  } catch (error2) {
205060
204942
  if (error2 instanceof ApiError)
@@ -209165,7 +209047,7 @@ async function getPlayerCharacter(ctx) {
209165
209047
  throw ApiError.unauthorized("Login required");
209166
209048
  try {
209167
209049
  const db = getDatabase();
209168
- const pc2 = await db.query.playerCharacters.findFirst({
209050
+ const pc3 = await db.query.playerCharacters.findFirst({
209169
209051
  where: eq(playerCharacters.userId, user.id),
209170
209052
  with: {
209171
209053
  accessories: {
@@ -209175,7 +209057,7 @@ async function getPlayerCharacter(ctx) {
209175
209057
  }
209176
209058
  }
209177
209059
  });
209178
- return pc2 ?? null;
209060
+ return pc3 ?? null;
209179
209061
  } catch (error2) {
209180
209062
  log2.error("Error fetching player character", { error: error2 });
209181
209063
  throw ApiError.internal("Failed to get player character", error2);
@@ -209191,7 +209073,7 @@ async function getPlayerCharacterById(ctx) {
209191
209073
  }
209192
209074
  try {
209193
209075
  const db = getDatabase();
209194
- const pc2 = await db.query.playerCharacters.findFirst({
209076
+ const pc3 = await db.query.playerCharacters.findFirst({
209195
209077
  where: eq(playerCharacters.userId, userId),
209196
209078
  with: {
209197
209079
  accessories: {
@@ -209201,7 +209083,7 @@ async function getPlayerCharacterById(ctx) {
209201
209083
  }
209202
209084
  }
209203
209085
  });
209204
- return pc2 ?? null;
209086
+ return pc3 ?? null;
209205
209087
  } catch (error2) {
209206
209088
  log2.error("Error fetching player character by ID", { userId, error: error2 });
209207
209089
  throw ApiError.internal("Failed to get player character", error2);
@@ -210436,48 +210318,12 @@ async function getStudentEnrollments(ctx) {
210436
210318
  if (!timebackId) {
210437
210319
  throw ApiError.badRequest("Missing timebackId parameter");
210438
210320
  }
210439
- const db = getDatabase();
210440
- const isLocal = process.env.PUBLIC_IS_LOCAL === "true";
210441
- if (isLocal) {
210442
- log2.debug("[API] Local mode: returning all integrations as mock enrollments", {
210443
- userId: user.id,
210444
- timebackId
210445
- });
210446
- const allIntegrations = await db.query.gameTimebackIntegrations.findMany();
210447
- const enrollments2 = allIntegrations.map((integration) => ({
210448
- gameId: integration.gameId,
210449
- grade: integration.grade,
210450
- subject: integration.subject,
210451
- courseId: integration.courseId
210452
- }));
210453
- log2.info("[API] Retrieved mock enrollments (local mode)", {
210454
- userId: user.id,
210455
- timebackId,
210456
- enrollmentCount: enrollments2.length
210457
- });
210458
- return { enrollments: enrollments2 };
210459
- }
210460
210321
  log2.debug("[API] Getting student enrollments", { userId: user.id, timebackId });
210461
- const client2 = await getTimebackClient();
210462
- const classes = await client2.getEnrollments(timebackId);
210463
- const courseIds = classes.map((cls) => cls.courseId).filter((id) => Boolean(id));
210464
- if (courseIds.length === 0) {
210465
- return { enrollments: [] };
210466
- }
210467
- const integrations = await db.query.gameTimebackIntegrations.findMany({
210468
- where: inArray(gameTimebackIntegrations.courseId, courseIds)
210469
- });
210470
- const enrollments = integrations.map((integration) => ({
210471
- gameId: integration.gameId,
210472
- grade: integration.grade,
210473
- subject: integration.subject,
210474
- courseId: integration.courseId
210475
- }));
210322
+ const enrollments = await fetchEnrollmentsForUser(timebackId);
210476
210323
  log2.info("[API] Retrieved student enrollments", {
210477
210324
  userId: user.id,
210478
210325
  timebackId,
210479
- totalClasses: classes.length,
210480
- mappedEnrollments: enrollments.length
210326
+ enrollmentCount: enrollments.length
210481
210327
  });
210482
210328
  return { enrollments };
210483
210329
  }
@@ -210985,8 +210831,8 @@ function registerRoutes(app) {
210985
210831
  return c3.json({ error: "Not Found", message: `Route ${c3.req.path} not found` }, 404);
210986
210832
  });
210987
210833
  }
210988
- var version3 = package_default2.version;
210989
- async function startServer2(port, project, options = {}) {
210834
+ var version3 = package_default.version;
210835
+ async function startServer(port, project, options = {}) {
210990
210836
  const processedOptions = processServerOptions(port, options);
210991
210837
  const db = await setupServerDatabase(processedOptions, project);
210992
210838
  const app = createApp(db, {
@@ -211008,6 +210854,44 @@ async function startServer2(port, project, options = {}) {
211008
210854
  };
211009
210855
  }
211010
210856
 
210857
+ // 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]"));
210871
+ 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}`)
210875
+ };
210876
+ }
210877
+ // src/lib/logging/utils.ts
210878
+ var import_picocolors4 = __toESM(require_picocolors(), 1);
210879
+ function printBanner(viteConfig, servers, projectInfo, pluginVersion) {
210880
+ const INDENT = " ".repeat(2);
210881
+ viteConfig.logger.info("");
210882
+ viteConfig.logger.info(`${INDENT}${import_picocolors4.green(import_picocolors4.bold("PLAYCADEMY"))} ${import_picocolors4.green(`v${pluginVersion}`)}`);
210883
+ 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)}`);
210889
+ }
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())}`)}`);
210892
+ }
210893
+ viteConfig.logger.info("");
210894
+ }
211011
210895
  // src/lib/sandbox/project-info.ts
211012
210896
  import fs6 from "node:fs";
211013
210897
  import path5 from "node:path";
@@ -211046,6 +210930,14 @@ async function extractProjectInfo(viteConfig) {
211046
210930
  }
211047
210931
 
211048
210932
  // 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
+ }
211049
210941
  function detectTimebackOptions() {
211050
210942
  if (process.env.TIMEBACK_LOCAL === "true") {
211051
210943
  return {
@@ -211070,16 +210962,60 @@ function detectTimebackOptions() {
211070
210962
  }
211071
210963
  return;
211072
210964
  }
210965
+ function hasTimebackCredentials2() {
210966
+ const config2 = detectTimebackOptions();
210967
+ if (!config2)
210968
+ return false;
210969
+ if (config2.mode === "local") {
210970
+ return !!(config2.onerosterApiUrl && config2.caliperApiUrl);
210971
+ }
210972
+ return !!(config2.onerosterApiUrl && config2.clientId && config2.clientSecret && config2.authUrl);
210973
+ }
210974
+ function validateTimebackConfig(viteConfig, timeback) {
210975
+ if (!timeback?.courses)
210976
+ return;
210977
+ const realCourses = Object.entries(timeback.courses).filter(([, value]) => value !== "mock");
210978
+ if (realCourses.length > 0 && !hasTimebackCredentials2()) {
210979
+ const courseList = realCourses.map(([key]) => key).join(", ");
210980
+ 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.`));
210984
+ viteConfig.logger.warn("");
210985
+ }
210986
+ }
210987
+ function generateTimebackJson(timeback) {
210988
+ if (!timeback?.courses || Object.keys(timeback.courses).length === 0) {
210989
+ return;
210990
+ }
210991
+ 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))
210998
+ continue;
210999
+ const courseId = value === "mock" ? `mock-${subject.toLowerCase()}-g${grade}` : value;
211000
+ enrollments.push({ subject, grade, courseId });
211001
+ }
211002
+ if (enrollments.length === 0) {
211003
+ return;
211004
+ }
211005
+ const roleOverride = getTimebackRoleOverride();
211006
+ const role = roleOverride ?? timeback.role ?? "student";
211007
+ return JSON.stringify({ role, enrollments });
211008
+ }
211073
211009
 
211074
211010
  // src/lib/sandbox/server.ts
211075
211011
  function printSandboxInfo(viteConfig, apiPort, realtimePort, projectInfo, realtimeEnabled) {
211076
211012
  viteConfig.logger.info("");
211077
- viteConfig.logger.info(` ${import_picocolors5.default.green(import_picocolors5.default.bold("PLAYCADEMY"))} ${import_picocolors5.default.green(`v${version3}`)}`);
211013
+ viteConfig.logger.info(` ${import_picocolors6.default.green(import_picocolors6.default.bold("PLAYCADEMY"))} ${import_picocolors6.default.green(`v${version3}`)}`);
211078
211014
  viteConfig.logger.info("");
211079
- viteConfig.logger.info(` ${import_picocolors5.default.green("➜")} ${import_picocolors5.default.bold("Game:")} ${import_picocolors5.default.cyan(projectInfo.slug)}`);
211080
- viteConfig.logger.info(` ${import_picocolors5.default.green("➜")} ${import_picocolors5.default.bold("Sandbox:")} ${import_picocolors5.default.cyan(`http://localhost:${import_picocolors5.default.bold(apiPort.toString())}/api`)}`);
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`)}`);
211081
211017
  if (realtimeEnabled) {
211082
- viteConfig.logger.info(` ${import_picocolors5.default.green("➜")} ${import_picocolors5.default.bold("Realtime:")} ${import_picocolors5.default.cyan(`ws://localhost:${import_picocolors5.default.bold(realtimePort.toString())}`)}`);
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())}`)}`);
211083
211019
  }
211084
211020
  viteConfig.logger.info("");
211085
211021
  }
@@ -211094,7 +211030,8 @@ async function startSandbox(viteConfig, autoStart = true, options = {}) {
211094
211030
  databasePath,
211095
211031
  realtimeEnabled = false,
211096
211032
  realtimePort,
211097
- logLevel = "info"
211033
+ logLevel = "info",
211034
+ timebackId
211098
211035
  } = options;
211099
211036
  if (!autoStart || viteConfig.command !== "serve") {
211100
211037
  const baseUrl = customUrl ?? `http://localhost:${DEFAULT_PORTS2.SANDBOX}`;
@@ -211121,10 +211058,16 @@ async function startSandbox(viteConfig, autoStart = true, options = {}) {
211121
211058
  const sandboxPort = await findAvailablePort(DEFAULT_PORTS2.SANDBOX);
211122
211059
  const baseUrl = `http://localhost:${sandboxPort}`;
211123
211060
  const projectInfo = await extractProjectInfo(viteConfig);
211124
- const timebackOptions = detectTimebackOptions();
211061
+ let timebackOptions = detectTimebackOptions();
211062
+ if (timebackId) {
211063
+ timebackOptions = {
211064
+ ...timebackOptions,
211065
+ studentId: timebackId
211066
+ };
211067
+ }
211125
211068
  const finalRealtimePort = realtimePort ?? await findAvailablePort(sandboxPort + 1);
211126
211069
  const realtimeUrl = `ws://localhost:${finalRealtimePort}`;
211127
- const server = await startServer2(sandboxPort, projectInfo, {
211070
+ const server = await startServer(sandboxPort, projectInfo, {
211128
211071
  verbose,
211129
211072
  quiet,
211130
211073
  seed,
@@ -211162,7 +211105,7 @@ async function startSandbox(viteConfig, autoStart = true, options = {}) {
211162
211105
  }
211163
211106
  };
211164
211107
  } catch (error2) {
211165
- viteConfig.logger.error(import_picocolors5.default.red(`[Playcademy] Failed to start sandbox: ${error2}`));
211108
+ viteConfig.logger.error(import_picocolors6.default.red(`[Playcademy] Failed to start sandbox: ${error2}`));
211166
211109
  return {
211167
211110
  baseUrl: `http://localhost:${DEFAULT_PORTS2.SANDBOX}`,
211168
211111
  realtimeUrl: "ws://localhost:4322",
@@ -211387,16 +211330,13 @@ var shell_with_corner_badge_default = `<!doctype html>
211387
211330
  <head>
211388
211331
  <meta charset="UTF-8" />
211389
211332
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
211390
- <title>Playcademy - Development</title>
211333
+ <title>Playcademy Dev</title>
211391
211334
  <link rel="preconnect" href="https://fonts.googleapis.com" />
211392
211335
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
211393
211336
  <link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet" />
211337
+ <script type="module" src="/@vite/client"></script>
211394
211338
  <script type="importmap">
211395
- {
211396
- "imports": {
211397
- "@playcademy/sdk": "https://esm.sh/@playcademy/sdk@latest"
211398
- }
211399
- }
211339
+ { "imports": { "@playcademy/sdk": "https://esm.sh/@playcademy/sdk@latest" } }
211400
211340
  </script>
211401
211341
  <style>
211402
211342
  * {
@@ -211404,14 +211344,11 @@ var shell_with_corner_badge_default = `<!doctype html>
211404
211344
  padding: 0;
211405
211345
  box-sizing: border-box;
211406
211346
  }
211407
-
211408
211347
  body {
211409
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
211410
211348
  height: 100vh;
211411
211349
  overflow: hidden;
211412
211350
  }
211413
-
211414
- .dev-badge {
211351
+ #badge {
211415
211352
  position: fixed;
211416
211353
  top: 0.5rem;
211417
211354
  left: 0.5rem;
@@ -211426,202 +211363,141 @@ var shell_with_corner_badge_default = `<!doctype html>
211426
211363
  letter-spacing: 0.05em;
211427
211364
  transition: background 0.2s ease;
211428
211365
  }
211429
-
211430
- .dev-badge.offline {
211366
+ #badge.offline {
211431
211367
  background: rgba(211, 47, 47, 0.9);
211432
211368
  }
211433
-
211434
- .dev-badge-status {
211435
- font-size: 0.7rem;
211436
- }
211437
-
211438
- .dev-badge-status:not(:empty) {
211439
- margin-left: 0.5rem;
211440
- }
211441
-
211442
- .game-container {
211443
- width: 100vw;
211444
- height: 100vh;
211445
- position: relative;
211446
- overflow: hidden;
211447
- }
211448
-
211449
- .game-frame {
211369
+ #frame {
211450
211370
  position: absolute;
211451
211371
  inset: 0;
211452
211372
  border: none;
211453
211373
  width: 100%;
211454
211374
  height: 100%;
211455
211375
  }
211456
-
211457
- .error {
211458
- position: absolute;
211459
- inset: 0;
211460
- display: flex;
211461
- flex-direction: column;
211462
- align-items: center;
211463
- justify-content: center;
211464
- color: #d32f2f;
211465
- background-color: #ffebee;
211466
- }
211467
-
211468
211376
  .hidden {
211469
211377
  display: none;
211470
211378
  }
211471
211379
  </style>
211472
211380
  </head>
211473
211381
  <body>
211474
- <div class="dev-badge" id="devBadge">
211475
- PLAYCADEMY<span class="dev-badge-status" id="badgeStatus"></span>
211476
- </div>
211477
-
211478
- <div class="game-container">
211479
- <div class="error hidden" id="error">
211480
- <div>❌ Failed to load game</div>
211481
- <div
211482
- style="margin-top: 0.5rem; font-size: 0.9rem; opacity: 0.8"
211483
- id="errorMessage"
211484
- ></div>
211485
- </div>
211486
-
211487
- <iframe class="game-frame hidden" id="gameFrame" src="/"></iframe>
211488
- </div>
211382
+ <div id="badge">PLAYCADEMY</div>
211383
+ <iframe id="frame" class="hidden"></iframe>
211489
211384
 
211490
211385
  <script type="module">
211491
211386
  import { MessageEvents, messaging } from '@playcademy/sdk'
211492
211387
 
211493
- const devBadge = document.getElementById('devBadge')
211494
- const badgeStatus = document.getElementById('badgeStatus')
211495
- const loading = document.getElementById('loading')
211496
- const error = document.getElementById('error')
211497
- const errorMessage = document.getElementById('errorMessage')
211498
- const gameFrame = document.getElementById('gameFrame')
211388
+ // Config (injected by vite plugin)
211389
+ const CONFIG = {
211390
+ sandboxUrl: '{{SANDBOX_URL}}',
211391
+ gameId: '{{GAME_ID}}',
211392
+ gameUrl: '{{GAME_URL}}' || undefined,
211393
+ realtimeUrl: '{{REALTIME_URL}}',
211394
+ timebackJson: '{{TIMEBACK_DATA}}',
211395
+ }
211499
211396
 
211500
- const logIfDebug = (...args) => {
211501
- if (window.PLAYCADEMY_DEBUG) {
211502
- console.log('[PlaycademyDevShell]', ...args)
211397
+ // Parse timeback data (role + enrollments)
211398
+ const timeback = (() => {
211399
+ try {
211400
+ return JSON.parse(CONFIG.timebackJson)
211401
+ } catch {
211402
+ return null
211503
211403
  }
211504
- }
211404
+ })()
211505
211405
 
211506
- logIfDebug('[PlaycademyDevShell] Initialized')
211406
+ // Elements
211407
+ const badge = document.getElementById('badge')
211408
+ const frame = document.getElementById('frame')
211507
211409
 
211508
- async function checkSandboxConnection() {
211410
+ // Debug logging
211411
+ const log = (...args) => window.PLAYCADEMY_DEBUG && console.log('[DevShell]', ...args)
211412
+
211413
+ // Check sandbox connection
211414
+ async function checkSandbox() {
211509
211415
  try {
211510
- const response = await fetch('{{SANDBOX_URL}}/api/users/me', {
211511
- headers: {
211512
- Authorization: 'Bearer sandbox-demo-token',
211513
- },
211416
+ const res = await fetch(\`\${CONFIG.sandboxUrl}/api/users/me\`, {
211417
+ headers: { Authorization: 'Bearer sandbox-demo-token' },
211514
211418
  })
211515
-
211516
- if (response.ok) {
211517
- badgeStatus.textContent = ''
211518
- devBadge.classList.remove('offline')
211519
- logIfDebug('[PlaycademyDevShell] Sandbox API connection successful')
211520
- } else {
211521
- throw new Error('Sandbox API not available')
211522
- }
211523
- } catch (err) {
211524
- logIfDebug('[PlaycademyDevShell] Sandbox connection failed:', err)
211525
- badgeStatus.textContent = ' ⚠️'
211526
- devBadge.classList.add('offline')
211419
+ if (!res.ok) throw new Error('Sandbox unavailable')
211420
+ badge.classList.remove('offline')
211421
+ log('Sandbox connected')
211422
+ return true
211423
+ } catch (e) {
211424
+ badge.classList.add('offline')
211425
+ badge.textContent = 'PLAYCADEMY ⚠️'
211426
+ log('Sandbox failed:', e)
211427
+ return false
211527
211428
  }
211528
211429
  }
211529
211430
 
211530
- // Load the game iframe
211531
- function loadGame() {
211532
- let handshakeInterval = null
211533
- let handshakeTimeout = null
211534
-
211535
- const initPayload = {
211536
- baseUrl: '{{SANDBOX_URL}}',
211537
- gameUrl: '{{GAME_URL}}' || undefined,
211431
+ // Init handshake with game iframe
211432
+ function initHandshake(timebackData) {
211433
+ const payload = {
211434
+ baseUrl: CONFIG.sandboxUrl,
211435
+ gameUrl: CONFIG.gameUrl,
211436
+ gameId: CONFIG.gameId,
211437
+ realtimeUrl: CONFIG.realtimeUrl,
211538
211438
  token: 'sandbox-demo-token',
211539
- gameId: '{{GAME_ID}}',
211540
- realtimeUrl: '{{REALTIME_URL}}',
211439
+ timeback: timebackData,
211541
211440
  }
211542
211441
 
211543
- const sendInit = () => {
211544
- if (!gameFrame.contentWindow) return
211545
-
211546
- messaging.send(MessageEvents.INIT, initPayload, {
211547
- target: gameFrame.contentWindow,
211548
- origin: '*',
211549
- })
211550
- }
211442
+ let interval, timeout
211551
211443
 
211552
- const stopHandshake = () => {
211553
- if (handshakeInterval) {
211554
- clearInterval(handshakeInterval)
211555
- handshakeInterval = null
211556
- }
211557
- if (handshakeTimeout) {
211558
- clearTimeout(handshakeTimeout)
211559
- handshakeTimeout = null
211444
+ const send = () => {
211445
+ if (frame.contentWindow) {
211446
+ messaging.send(MessageEvents.INIT, payload, {
211447
+ target: frame.contentWindow,
211448
+ origin: '*',
211449
+ })
211560
211450
  }
211561
211451
  }
211562
211452
 
211563
- gameFrame.onload = () => {
211564
- gameFrame.classList.remove('hidden')
211565
-
211566
- logIfDebug('[PlaycademyDevShell] Game iframe loaded, beginning init handshake')
211567
-
211568
- // Begin handshake: send immediately, then every 300ms
211569
- sendInit()
211570
- handshakeInterval = setInterval(sendInit, 300)
211571
-
211572
- // Stop handshake after 10 seconds
211573
- handshakeTimeout = setTimeout(() => {
211574
- stopHandshake()
211575
- logIfDebug('[PlaycademyDevShell] Init handshake timeout reached')
211576
- }, 10000)
211453
+ const stop = () => {
211454
+ clearInterval(interval)
211455
+ clearTimeout(timeout)
211577
211456
  }
211578
211457
 
211579
- gameFrame.onerror = () => {
211580
- loading.classList.add('hidden')
211581
- error.classList.remove('hidden')
211582
- errorMessage.textContent = 'Game iframe failed to load'
211583
- logIfDebug('[PlaycademyDevShell] Game iframe failed to load')
211584
- stopHandshake()
211458
+ frame.onload = () => {
211459
+ frame.classList.remove('hidden')
211460
+ log('Frame loaded, starting handshake')
211461
+ send()
211462
+ interval = setInterval(send, 300)
211463
+ timeout = setTimeout(() => {
211464
+ stop()
211465
+ log('Handshake timeout')
211466
+ }, 10000)
211585
211467
  }
211586
211468
 
211587
- // Listen for READY message to stop handshake
211588
211469
  messaging.listen(MessageEvents.READY, () => {
211589
- logIfDebug('[PlaycademyDevShell] Received READY from game, stopping handshake')
211590
- stopHandshake()
211470
+ stop()
211471
+ log('Game ready')
211591
211472
  })
211592
211473
 
211593
- gameFrame.src = '/'
211474
+ frame.src = '/'
211594
211475
  }
211595
211476
 
211596
- window.addEventListener('message', event => {
211597
- if (event.source === gameFrame.contentWindow) {
211598
- const { type, ...payload } = event.data || {}
211599
- if (type && type.startsWith('PLAYCADEMY_')) {
211600
- logIfDebug(
211601
- '[PlaycademyDevShell] Received message from game:',
211602
- type,
211603
- payload,
211604
- )
211605
- // Bridge the message to the local context using CustomEvent
211606
- messaging.send(type, payload)
211607
- }
211477
+ // Bridge messages from game to shell context
211478
+ window.addEventListener('message', e => {
211479
+ if (e.source !== frame.contentWindow) return
211480
+ const { type, ...payload } = e.data || {}
211481
+ if (type?.startsWith('PLAYCADEMY_')) {
211482
+ log('Message:', type, payload)
211483
+ messaging.send(type, payload)
211608
211484
  }
211609
211485
  })
211610
211486
 
211611
- // Initialize
211612
- checkSandboxConnection()
211613
- loadGame()
211487
+ // Start
211488
+ checkSandbox().then(() => initHandshake(timeback))
211614
211489
  </script>
211615
211490
  </body>
211616
211491
  </html>
211617
211492
  `;
211618
211493
 
211619
211494
  // src/server/middleware.ts
211620
- function generateLoaderHTML(sandboxUrl, gameId, realtimeUrl, showBadge, gameUrl) {
211621
- const shell = showBadge ? shell_with_corner_badge_default : shell_no_badge_default;
211622
- return shell.replace(/{{SANDBOX_URL}}/g, sandboxUrl).replace(/{{GAME_ID}}/g, gameId).replace(/{{REALTIME_URL}}/g, realtimeUrl).replace(/{{GAME_URL}}/g, gameUrl || "");
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);
211623
211499
  }
211624
- function devServerMiddleware(server, sandbox, gameUrl, showBadge) {
211500
+ function devServerMiddleware(server, sandbox, gameUrl, options) {
211625
211501
  server.middlewares.use("/", (req, res, next) => {
211626
211502
  if (getCurrentMode() !== "platform") {
211627
211503
  next();
@@ -211633,8 +211509,7 @@ function devServerMiddleware(server, sandbox, gameUrl, showBadge) {
211633
211509
  next();
211634
211510
  } else {
211635
211511
  res.setHeader("Content-Type", "text/html");
211636
- const gameId = sandbox.project?.slug;
211637
- res.end(generateLoaderHTML(sandbox.baseUrl, gameId ?? "", sandbox.realtimeUrl, showBadge, gameUrl));
211512
+ res.end(generateLoaderHTML(sandbox.baseUrl, sandbox.project?.slug ?? "", sandbox.realtimeUrl, options, gameUrl));
211638
211513
  }
211639
211514
  return;
211640
211515
  }
@@ -211642,6 +211517,190 @@ function devServerMiddleware(server, sandbox, gameUrl, showBadge) {
211642
211517
  });
211643
211518
  }
211644
211519
 
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) {
211532
+ const currentMode = getCurrentMode();
211533
+ const viteServer = getViteServerRef();
211534
+ if (!viteServer) {
211535
+ options.viteConfig.logger.error(`${formatTimestamp3()} ${red(bold4("[playcademy]"))} ${dim4("(sandbox)")} ${red("Cannot recreate sandbox database: no Vite server reference")}`);
211536
+ return;
211537
+ }
211538
+ 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;
211541
+ }
211542
+ options.viteConfig.logger.info(`${formatTimestamp3()} ${cyan3(bold4("[playcademy]"))} ${dim4("(sandbox)")} recreating database...`);
211543
+ if (serverState.sandbox) {
211544
+ serverState.sandbox.cleanup();
211545
+ serverState.sandbox = null;
211546
+ }
211547
+ 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,
211553
+ 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)
211560
+ });
211561
+ serverState.sandbox = sandbox;
211562
+ if (sandbox.project && serverState.backend) {
211563
+ const gameUrl = `http://localhost:${serverState.backend.port}`;
211564
+ devServerMiddleware(viteServer, sandbox, gameUrl, {
211565
+ showBadge: options.platformModeOptions.showBadge,
211566
+ timeback: options.platformModeOptions.timeback
211567
+ });
211568
+ }
211569
+ options.viteConfig.logger.info(`${formatTimestamp3()} ${cyan3(bold4("[playcademy]"))} ${dim4("(sandbox)")} ${green2("database recreated")}`);
211570
+ }
211571
+ var recreateDatabaseHotkey = (options) => ({
211572
+ key: "d",
211573
+ description: "recreate sandbox database",
211574
+ action: () => recreateSandboxDatabase(options)
211575
+ });
211576
+
211577
+ // src/server/hotkeys/toggle-mode.ts
211578
+ var import_picocolors10 = __toESM(require_picocolors(), 1);
211579
+ // package.json
211580
+ var package_default2 = {
211581
+ name: "@playcademy/vite-plugin",
211582
+ version: "0.1.37",
211583
+ type: "module",
211584
+ exports: {
211585
+ ".": {
211586
+ import: "./dist/index.js",
211587
+ types: "./dist/index.d.ts"
211588
+ }
211589
+ },
211590
+ main: "dist/index.js",
211591
+ module: "dist/index.js",
211592
+ files: [
211593
+ "dist"
211594
+ ],
211595
+ scripts: {
211596
+ build: "rm -rf dist && bun build.ts",
211597
+ docs: "typedoc --skipErrorChecking",
211598
+ pub: "bun publish.ts"
211599
+ },
211600
+ dependencies: {
211601
+ archiver: "^7.0.1",
211602
+ picocolors: "^1.1.1",
211603
+ playcademy: "workspace:*"
211604
+ },
211605
+ devDependencies: {
211606
+ "@inquirer/prompts": "^7.8.6",
211607
+ "@playcademy/sandbox": "workspace:*",
211608
+ "@types/archiver": "^6.0.3",
211609
+ "@types/bun": "latest"
211610
+ },
211611
+ peerDependencies: {
211612
+ typescript: "^5",
211613
+ vite: "^5 || ^6"
211614
+ }
211615
+ };
211616
+
211617
+ // src/lib/backend/server.ts
211618
+ import {
211619
+ loadPlaycademyConfigAndSetWorkspace as loadConfigAndSetWorkspace,
211620
+ startPlaycademyDevServer,
211621
+ startPlaycademyHotReload
211622
+ } from "playcademy/utils";
211623
+
211624
+ // 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
+ }
211634
+ function createHotReloadCallbacks(viteConfig) {
211635
+ return {
211636
+ onSuccess: (changedPath) => {
211637
+ const relativePath = formatChangedPath(changedPath);
211638
+ if (relativePath) {
211639
+ viteConfig.logger.info(`${import_picocolors8.dim("(backend)")} ${import_picocolors8.green("hmr update")} ${import_picocolors8.dim(relativePath)}`, { timestamp: true });
211640
+ } else {
211641
+ viteConfig.logger.info("backend reloaded", { timestamp: true });
211642
+ }
211643
+ },
211644
+ onError: (error2) => {
211645
+ viteConfig.logger.error(`backend reload failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
211646
+ }
211647
+ };
211648
+ }
211649
+
211650
+ // src/lib/backend/server.ts
211651
+ async function tryLoadConfig(viteConfig, configPath) {
211652
+ try {
211653
+ return await loadConfigAndSetWorkspace(configPath);
211654
+ } catch (error2) {
211655
+ if (error2 instanceof Error && !error2.message.includes("Could not find")) {
211656
+ viteConfig.logger.warn(`Could not load playcademy.config.js: ${error2.message}`);
211657
+ }
211658
+ return null;
211659
+ }
211660
+ }
211661
+ function needsCliDevServer(config2) {
211662
+ return !!config2.integrations;
211663
+ }
211664
+ async function startServer2(options) {
211665
+ const { port, config: config2, platformUrl } = options;
211666
+ return startPlaycademyDevServer({
211667
+ port,
211668
+ config: config2,
211669
+ quiet: true,
211670
+ platformUrl,
211671
+ customLogger: createLoggerAdapter("backend")
211672
+ });
211673
+ }
211674
+ function setupHotReload(serverRef, options) {
211675
+ const watcher = startPlaycademyHotReload(async () => {
211676
+ await serverRef.current.server.dispose();
211677
+ serverRef.current = await startServer2(options);
211678
+ }, createHotReloadCallbacks(options.viteConfig));
211679
+ return () => watcher.close();
211680
+ }
211681
+ async function setupCliDevServer(options) {
211682
+ const { preferredPort, viteConfig, platformUrl, configPath } = options;
211683
+ const config2 = await tryLoadConfig(viteConfig, configPath);
211684
+ if (!config2)
211685
+ return null;
211686
+ if (!needsCliDevServer(config2))
211687
+ return null;
211688
+ try {
211689
+ const port = await findAvailablePort(preferredPort);
211690
+ const serverOptions = { port, config: config2, platformUrl, viteConfig };
211691
+ const serverRef = { current: await startServer2(serverOptions) };
211692
+ const stopHotReload = setupHotReload(serverRef, serverOptions);
211693
+ return {
211694
+ server: serverRef.current.server,
211695
+ port: serverRef.current.port,
211696
+ stopHotReload,
211697
+ cleanup: () => cleanupServerInfo("backend", viteConfig.root, process.pid)
211698
+ };
211699
+ } catch (error2) {
211700
+ viteConfig.logger.error(`Failed to start game backend: ${error2 instanceof Error ? error2.message : String(error2)}`);
211701
+ return null;
211702
+ }
211703
+ }
211645
211704
  // src/server/platform-mode.ts
211646
211705
  async function configurePlatformMode(server, viteConfig, options) {
211647
211706
  const sandbox = await startSandbox(viteConfig, options.startSandbox, {
@@ -211654,7 +211713,8 @@ async function configurePlatformMode(server, viteConfig, options) {
211654
211713
  memoryOnly: options.memoryOnly,
211655
211714
  databasePath: options.databasePath,
211656
211715
  realtimeEnabled: options.realtimeEnabled,
211657
- realtimePort: options.realtimePort
211716
+ realtimePort: options.realtimePort,
211717
+ timebackId: getEffectiveTimebackId(options.timeback)
211658
211718
  });
211659
211719
  serverState.sandbox = sandbox;
211660
211720
  const backend = await setupCliDevServer({
@@ -211665,8 +211725,12 @@ async function configurePlatformMode(server, viteConfig, options) {
211665
211725
  });
211666
211726
  serverState.backend = backend;
211667
211727
  if (sandbox.project) {
211728
+ validateTimebackConfig(viteConfig, options.timeback);
211668
211729
  const gameUrl = backend ? `http://localhost:${backend.port}` : undefined;
211669
- devServerMiddleware(server, sandbox, gameUrl, options.showBadge);
211730
+ devServerMiddleware(server, sandbox, gameUrl, {
211731
+ showBadge: options.showBadge,
211732
+ timeback: options.timeback
211733
+ });
211670
211734
  }
211671
211735
  server.httpServer?.once("listening", () => {
211672
211736
  setTimeout(async () => {
@@ -211677,13 +211741,13 @@ async function configurePlatformMode(server, viteConfig, options) {
211677
211741
  backend: backend?.port,
211678
211742
  realtime: sandbox.realtimePort,
211679
211743
  vite: vitePort
211680
- }, projectInfo, package_default.version);
211744
+ }, projectInfo, package_default2.version);
211681
211745
  }, 100);
211682
211746
  });
211683
211747
  }
211684
211748
 
211685
211749
  // src/server/standalone-mode.ts
211686
- var import_picocolors6 = __toESM(require_picocolors(), 1);
211750
+ var import_picocolors9 = __toESM(require_picocolors(), 1);
211687
211751
  async function configureStandaloneMode(server, viteConfig, options) {
211688
211752
  const backend = await setupCliDevServer({
211689
211753
  preferredPort: options.preferredPort,
@@ -211699,32 +211763,32 @@ async function configureStandaloneMode(server, viteConfig, options) {
211699
211763
  server.httpServer?.once("listening", () => {
211700
211764
  setTimeout(() => {
211701
211765
  viteConfig.logger.info("");
211702
- viteConfig.logger.info(` ${import_picocolors6.default.green(import_picocolors6.default.bold("PLAYCADEMY"))} ${import_picocolors6.default.green(`v${package_default.version}`)}`);
211766
+ viteConfig.logger.info(` ${import_picocolors9.default.green(import_picocolors9.default.bold("PLAYCADEMY"))} ${import_picocolors9.default.green(`v${package_default2.version}`)}`);
211703
211767
  viteConfig.logger.info("");
211704
- viteConfig.logger.info(` ${import_picocolors6.default.green("➜")} ${import_picocolors6.default.bold("Backend:")} ${import_picocolors6.default.cyan(`http://localhost:${backend.port}`)}`);
211705
- viteConfig.logger.info(` ${import_picocolors6.default.green("➜")} ${import_picocolors6.default.bold("Sandbox:")} ${import_picocolors6.default.cyan("Disabled")}`);
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")}`);
211706
211770
  viteConfig.logger.info("");
211707
211771
  }, 100);
211708
211772
  });
211709
211773
  }
211710
211774
 
211711
- // src/server/mode-switcher.ts
211712
- var { bold: bold3, cyan: cyan3, dim: dim4, green: green3, red } = import_picocolors7.default;
211713
- function formatTimestamp2() {
211775
+ // src/server/hotkeys/toggle-mode.ts
211776
+ var { bold: bold5, cyan: cyan4, dim: dim6, green: green4, red: red2 } = import_picocolors10.default;
211777
+ function formatTimestamp4() {
211714
211778
  const now2 = new Date;
211715
211779
  const hours = now2.getHours();
211716
211780
  const minutes = now2.getMinutes().toString().padStart(2, "0");
211717
211781
  const seconds = now2.getSeconds().toString().padStart(2, "0");
211718
211782
  const ampm = hours >= 12 ? "PM" : "AM";
211719
211783
  const displayHours = hours % 12 || 12;
211720
- return dim4(`${displayHours}:${minutes}:${seconds} ${ampm}`);
211784
+ return dim6(`${displayHours}:${minutes}:${seconds} ${ampm}`);
211721
211785
  }
211722
211786
  async function toggleMode(options) {
211723
211787
  const currentMode = getCurrentMode();
211724
211788
  const newMode = currentMode === "platform" ? "standalone" : "platform";
211725
211789
  const viteServer = getViteServerRef();
211726
211790
  if (!viteServer) {
211727
- options.viteConfig.logger.error(`${formatTimestamp2()} ${red(bold3("[playcademy]"))} ${red("Cannot toggle mode: no Vite server reference")}`);
211791
+ options.viteConfig.logger.error(`${formatTimestamp4()} ${red2(bold5("[playcademy]"))} ${red2("Cannot toggle mode: no Vite server reference")}`);
211728
211792
  return;
211729
211793
  }
211730
211794
  await cleanupServers();
@@ -211738,56 +211802,39 @@ async function toggleMode(options) {
211738
211802
  } else {
211739
211803
  await configurePlatformMode(viteServer, options.viteConfig, options.platformModeOptions);
211740
211804
  }
211741
- options.viteConfig.logger.info(`${formatTimestamp2()} ${cyan3(bold3("[playcademy]"))} ${green3("switched to")} ${green3(bold3(newMode))} ${green3("mode")}`);
211805
+ options.viteConfig.logger.info(`${formatTimestamp4()} ${cyan4(bold5("[playcademy]"))} ${green4("switched to")} ${green4(bold5(newMode))} ${green4("mode")}`);
211742
211806
  }
211807
+ var toggleModeHotkey = (options) => ({
211808
+ key: "m",
211809
+ description: "toggle platform/standalone mode",
211810
+ action: () => toggleMode(options)
211811
+ });
211743
211812
 
211744
- // src/server/recreate-sandbox-database.ts
211745
- var import_picocolors8 = __toESM(require_picocolors(), 1);
211746
- var { bold: bold4, cyan: cyan4, dim: dim5, green: green4, red: red2, yellow } = import_picocolors8.default;
211747
- function formatTimestamp3() {
211748
- const now2 = new Date;
211749
- const hours = now2.getHours();
211750
- const minutes = now2.getMinutes().toString().padStart(2, "0");
211751
- const seconds = now2.getSeconds().toString().padStart(2, "0");
211752
- const ampm = hours >= 12 ? "PM" : "AM";
211753
- const displayHours = hours % 12 || 12;
211754
- return dim5(`${displayHours}:${minutes}:${seconds} ${ampm}`);
211813
+ // src/server/hotkeys/index.ts
211814
+ function getHotkeys(options) {
211815
+ return [
211816
+ toggleModeHotkey(options),
211817
+ recreateDatabaseHotkey(options),
211818
+ cycleTimebackRoleHotkey(options)
211819
+ ];
211755
211820
  }
211756
- async function recreateSandboxDatabase(options) {
211757
- const currentMode = getCurrentMode();
211758
- const viteServer = getViteServerRef();
211759
- if (!viteServer) {
211760
- options.viteConfig.logger.error(`${formatTimestamp3()} ${red2(bold4("[playcademy]"))} ${dim5("(sandbox)")} ${red2("Cannot recreate sandbox database: no Vite server reference")}`);
211761
- return;
211762
- }
211763
- if (currentMode !== "platform") {
211764
- options.viteConfig.logger.warn(`${formatTimestamp3()} ${yellow(bold4("[playcademy]"))} ${dim5("(sandbox)")} ${yellow("can only recreate sandbox database in platform mode (m + enter)")}`);
211821
+
211822
+ // src/server/lifecycle.ts
211823
+ var shutdownHandlersRegistered = false;
211824
+ function setupProcessShutdownHandlers() {
211825
+ if (shutdownHandlersRegistered)
211765
211826
  return;
211766
- }
211767
- options.viteConfig.logger.info(`${formatTimestamp3()} ${cyan4(bold4("[playcademy]"))} ${dim5("(sandbox)")} recreating database...`);
211768
- if (serverState.sandbox) {
211769
- serverState.sandbox.cleanup();
211770
- serverState.sandbox = null;
211771
- }
211772
- await new Promise((resolve2) => setTimeout(resolve2, 100));
211773
- const sandbox = await startSandbox(options.viteConfig, options.platformModeOptions.startSandbox, {
211774
- verbose: options.platformModeOptions.verbose,
211775
- logLevel: options.platformModeOptions.logLevel,
211776
- customUrl: options.platformModeOptions.sandboxUrl,
211777
- quiet: true,
211778
- recreateDb: true,
211779
- seed: options.platformModeOptions.seed,
211780
- memoryOnly: options.platformModeOptions.memoryOnly,
211781
- databasePath: options.platformModeOptions.databasePath,
211782
- realtimeEnabled: options.platformModeOptions.realtimeEnabled,
211783
- realtimePort: options.platformModeOptions.realtimePort
211784
- });
211785
- serverState.sandbox = sandbox;
211786
- if (sandbox.project && serverState.backend) {
211787
- const gameUrl = `http://localhost:${serverState.backend.port}`;
211788
- devServerMiddleware(viteServer, sandbox, gameUrl, options.platformModeOptions.showBadge);
211789
- }
211790
- options.viteConfig.logger.info(`${formatTimestamp3()} ${cyan4(bold4("[playcademy]"))} ${dim5("(sandbox)")} ${green4("database recreated")}`);
211827
+ shutdownHandlersRegistered = true;
211828
+ let isShuttingDown = false;
211829
+ const shutdown = async () => {
211830
+ if (isShuttingDown)
211831
+ return;
211832
+ isShuttingDown = true;
211833
+ await cleanupServers();
211834
+ process.exit(0);
211835
+ };
211836
+ process.on("SIGINT", shutdown);
211837
+ process.on("SIGTERM", shutdown);
211791
211838
  }
211792
211839
 
211793
211840
  // src/hooks/configure-server.ts
@@ -211816,7 +211863,8 @@ async function configureServerHook(server, context) {
211816
211863
  realtimePort: context.options.realtimePort,
211817
211864
  showBadge: context.options.showBadge,
211818
211865
  preferredBackendPort: preferredPort,
211819
- configPath: context.options.configPath
211866
+ configPath: context.options.configPath,
211867
+ timeback: context.options.timeback
211820
211868
  };
211821
211869
  if (context.options.mode === "standalone") {
211822
211870
  await configureStandaloneMode(server, context.viteConfig, {
@@ -211833,26 +211881,10 @@ async function configureServerHook(server, context) {
211833
211881
  ...options,
211834
211882
  customShortcuts: [
211835
211883
  ...options?.customShortcuts || [],
211836
- {
211837
- key: "m",
211838
- description: "toggle platform/standalone mode",
211839
- action: async () => {
211840
- await toggleMode({
211841
- viteConfig: context.viteConfig,
211842
- platformModeOptions
211843
- });
211844
- }
211845
- },
211846
- {
211847
- key: "d",
211848
- description: "recreate sandbox database",
211849
- action: async () => {
211850
- await recreateSandboxDatabase({
211851
- viteConfig: context.viteConfig,
211852
- platformModeOptions
211853
- });
211854
- }
211855
- }
211884
+ ...getHotkeys({
211885
+ viteConfig: context.viteConfig,
211886
+ platformModeOptions
211887
+ })
211856
211888
  ]
211857
211889
  });
211858
211890
  };
@@ -211900,7 +211932,8 @@ function resolveOptions(options) {
211900
211932
  databasePath: sandboxOptions.databasePath,
211901
211933
  realtimeEnabled: realtimeOptions.enabled ?? false,
211902
211934
  realtimePort: realtimeOptions.port,
211903
- showBadge: shellOptions.showBadge ?? true
211935
+ showBadge: shellOptions.showBadge ?? true,
211936
+ timeback: options.timeback
211904
211937
  };
211905
211938
  }
211906
211939
  function createPluginContext(options) {