@playcademy/vite-plugin 0.1.37 → 0.1.40

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.3",
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,20 +199445,44 @@ 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 });
199448
+ function resolveStudentId(studentId) {
199449
+ if (!studentId)
199450
+ return null;
199451
+ if (studentId === "mock")
199452
+ return `mock-student-${crypto.randomUUID().slice(0, 8)}`;
199453
+ return studentId;
199454
+ }
199455
+ function getAdminTimebackId() {
199456
+ return resolveStudentId(config.timeback.studentId);
199457
+ }
199458
+ async function seedTimebackIntegrations(db, gameId, courses) {
199459
+ const now2 = new Date;
199460
+ let seededCount = 0;
199461
+ for (const course of courses) {
199462
+ const courseId = course.courseId || `mock-${course.subject.toLowerCase()}-g${course.grade}`;
199463
+ try {
199464
+ const existing = await db.query.gameTimebackIntegrations.findFirst({
199465
+ where: (table14, { and: and3, eq: eq3 }) => and3(eq3(table14.gameId, gameId), eq3(table14.grade, course.grade), eq3(table14.subject, course.subject))
199466
+ });
199467
+ if (existing) {
199468
+ continue;
199469
+ }
199470
+ await db.insert(gameTimebackIntegrations).values({
199471
+ gameId,
199472
+ courseId,
199473
+ grade: course.grade,
199474
+ subject: course.subject,
199475
+ totalXp: course.totalXp ?? 1000,
199476
+ lastVerifiedAt: now2
199477
+ });
199478
+ seededCount++;
199479
+ } catch (error2) {
199480
+ console.error(`❌ Error seeding TimeBack integration for ${course.subject}:${course.grade}:`, error2);
199535
199481
  }
199536
199482
  }
199537
- return courses;
199483
+ if (seededCount > 0) {
199484
+ logger3.info(`\uD83D\uDCDA Seeded ${seededCount} TimeBack integration(s)`);
199485
+ }
199538
199486
  }
199539
199487
  async function seedCoreGames(db) {
199540
199488
  const now2 = new Date;
@@ -199560,28 +199508,6 @@ async function seedCoreGames(db) {
199560
199508
  console.error(`Error seeding core game '${gameData.slug}':`, error2);
199561
199509
  }
199562
199510
  }
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
199511
  }
199586
199512
  async function seedCurrentProjectGame(db, project) {
199587
199513
  const now2 = new Date;
@@ -199591,6 +199517,9 @@ async function seedCurrentProjectGame(db, project) {
199591
199517
  });
199592
199518
  if (existingGame) {
199593
199519
  logger3.info(`\uD83C\uDFAE Game "${project.displayName}" (${project.slug}) already exists`);
199520
+ if (project.timebackCourses && project.timebackCourses.length > 0) {
199521
+ await seedTimebackIntegrations(db, existingGame.id, project.timebackCourses);
199522
+ }
199594
199523
  return existingGame;
199595
199524
  }
199596
199525
  const gameRecord = {
@@ -199610,23 +199539,11 @@ async function seedCurrentProjectGame(db, project) {
199610
199539
  updatedAt: now2
199611
199540
  };
199612
199541
  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
- }
199542
+ if (!newGame) {
199543
+ throw new Error("Failed to create game record");
199544
+ }
199545
+ if (project.timebackCourses && project.timebackCourses.length > 0) {
199546
+ await seedTimebackIntegrations(db, newGame.id, project.timebackCourses);
199630
199547
  }
199631
199548
  return newGame;
199632
199549
  } catch (error2) {
@@ -199686,10 +199603,11 @@ async function seedSpriteTemplates(db) {
199686
199603
  }
199687
199604
  async function seedDemoData(db) {
199688
199605
  try {
199606
+ const adminTimebackId = getAdminTimebackId();
199689
199607
  for (const [role, user] of Object.entries(DEMO_USERS)) {
199690
199608
  const userValues = {
199691
199609
  ...user,
199692
- timebackId: role === "admin" && config.timeback.studentId ? config.timeback.studentId : null
199610
+ timebackId: role === "admin" ? adminTimebackId : null
199693
199611
  };
199694
199612
  await db.insert(users).values(userValues).onConflictDoNothing();
199695
199613
  }
@@ -205052,9 +204970,11 @@ async function getUserMe(ctx) {
205052
204970
  const timebackAccount = await db.query.accounts.findFirst({
205053
204971
  where: and(eq(accounts.userId, user.id), eq(accounts.providerId, "timeback"))
205054
204972
  });
204973
+ const timeback3 = userData.timebackId ? await fetchUserTimebackData(userData.timebackId) : undefined;
205055
204974
  return {
205056
204975
  ...userData,
205057
- hasTimebackAccount: !!timebackAccount
204976
+ hasTimebackAccount: !!timebackAccount,
204977
+ timeback: timeback3
205058
204978
  };
205059
204979
  } catch (error2) {
205060
204980
  if (error2 instanceof ApiError)
@@ -209165,7 +209085,7 @@ async function getPlayerCharacter(ctx) {
209165
209085
  throw ApiError.unauthorized("Login required");
209166
209086
  try {
209167
209087
  const db = getDatabase();
209168
- const pc2 = await db.query.playerCharacters.findFirst({
209088
+ const pc3 = await db.query.playerCharacters.findFirst({
209169
209089
  where: eq(playerCharacters.userId, user.id),
209170
209090
  with: {
209171
209091
  accessories: {
@@ -209175,7 +209095,7 @@ async function getPlayerCharacter(ctx) {
209175
209095
  }
209176
209096
  }
209177
209097
  });
209178
- return pc2 ?? null;
209098
+ return pc3 ?? null;
209179
209099
  } catch (error2) {
209180
209100
  log2.error("Error fetching player character", { error: error2 });
209181
209101
  throw ApiError.internal("Failed to get player character", error2);
@@ -209191,7 +209111,7 @@ async function getPlayerCharacterById(ctx) {
209191
209111
  }
209192
209112
  try {
209193
209113
  const db = getDatabase();
209194
- const pc2 = await db.query.playerCharacters.findFirst({
209114
+ const pc3 = await db.query.playerCharacters.findFirst({
209195
209115
  where: eq(playerCharacters.userId, userId),
209196
209116
  with: {
209197
209117
  accessories: {
@@ -209201,7 +209121,7 @@ async function getPlayerCharacterById(ctx) {
209201
209121
  }
209202
209122
  }
209203
209123
  });
209204
- return pc2 ?? null;
209124
+ return pc3 ?? null;
209205
209125
  } catch (error2) {
209206
209126
  log2.error("Error fetching player character by ID", { userId, error: error2 });
209207
209127
  throw ApiError.internal("Failed to get player character", error2);
@@ -210436,48 +210356,12 @@ async function getStudentEnrollments(ctx) {
210436
210356
  if (!timebackId) {
210437
210357
  throw ApiError.badRequest("Missing timebackId parameter");
210438
210358
  }
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
210359
  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
- }));
210360
+ const enrollments = await fetchEnrollmentsForUser(timebackId);
210476
210361
  log2.info("[API] Retrieved student enrollments", {
210477
210362
  userId: user.id,
210478
210363
  timebackId,
210479
- totalClasses: classes.length,
210480
- mappedEnrollments: enrollments.length
210364
+ enrollmentCount: enrollments.length
210481
210365
  });
210482
210366
  return { enrollments };
210483
210367
  }
@@ -210985,8 +210869,8 @@ function registerRoutes(app) {
210985
210869
  return c3.json({ error: "Not Found", message: `Route ${c3.req.path} not found` }, 404);
210986
210870
  });
210987
210871
  }
210988
- var version3 = package_default2.version;
210989
- async function startServer2(port, project, options = {}) {
210872
+ var version3 = package_default.version;
210873
+ async function startServer(port, project, options = {}) {
210990
210874
  const processedOptions = processServerOptions(port, options);
210991
210875
  const db = await setupServerDatabase(processedOptions, project);
210992
210876
  const app = createApp(db, {
@@ -211008,17 +210892,71 @@ async function startServer2(port, project, options = {}) {
211008
210892
  };
211009
210893
  }
211010
210894
 
210895
+ // src/lib/logging/adapter.ts
210896
+ var import_picocolors3 = __toESM(require_picocolors(), 1);
210897
+ function formatTimestamp2() {
210898
+ const now2 = new Date;
210899
+ const hours = now2.getHours();
210900
+ const minutes = now2.getMinutes().toString().padStart(2, "0");
210901
+ const seconds = now2.getSeconds().toString().padStart(2, "0");
210902
+ const ampm = hours >= 12 ? "PM" : "AM";
210903
+ const displayHours = hours % 12 || 12;
210904
+ return import_picocolors3.dim(`${displayHours}:${minutes}:${seconds} ${ampm}`);
210905
+ }
210906
+ function createLoggerAdapter(prefix2) {
210907
+ const formattedPrefix = import_picocolors3.dim(`(${prefix2})`);
210908
+ const label = import_picocolors3.cyan(import_picocolors3.bold("[playcademy]"));
210909
+ return {
210910
+ info: (msg) => console.log(`${formatTimestamp2()} ${label} ${formattedPrefix} ${msg}`),
210911
+ warn: (msg) => console.warn(`${formatTimestamp2()} ${label} ${formattedPrefix} ${msg}`),
210912
+ error: (msg) => console.error(`${formatTimestamp2()} ${label} ${formattedPrefix} ${msg}`)
210913
+ };
210914
+ }
210915
+ // src/lib/logging/utils.ts
210916
+ var import_picocolors4 = __toESM(require_picocolors(), 1);
210917
+ function printBanner(viteConfig, servers, projectInfo, pluginVersion) {
210918
+ const INDENT = " ".repeat(2);
210919
+ viteConfig.logger.info("");
210920
+ viteConfig.logger.info(`${INDENT}${import_picocolors4.green(import_picocolors4.bold("PLAYCADEMY"))} ${import_picocolors4.green(`v${pluginVersion}`)}`);
210921
+ viteConfig.logger.info("");
210922
+ viteConfig.logger.info(`${INDENT}${import_picocolors4.green("➜")} ${import_picocolors4.bold("Game:")} ${import_picocolors4.cyan(projectInfo.slug)}`);
210923
+ viteConfig.logger.info(`${INDENT}${import_picocolors4.green("➜")} ${import_picocolors4.bold("Sandbox:")} ${import_picocolors4.cyan(`http://localhost:${import_picocolors4.bold(servers.sandbox.toString())}/api`)}`);
210924
+ if (servers.backend) {
210925
+ 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`;
210926
+ viteConfig.logger.info(`${INDENT}${import_picocolors4.green("➜")} ${import_picocolors4.bold("Backend:")} ${import_picocolors4.cyan(backendUrl)}`);
210927
+ }
210928
+ if (servers.realtime) {
210929
+ viteConfig.logger.info(`${INDENT}${import_picocolors4.green("➜")} ${import_picocolors4.bold("Realtime:")} ${import_picocolors4.cyan(`ws://localhost:${import_picocolors4.bold(servers.realtime.toString())}`)}`);
210930
+ }
210931
+ viteConfig.logger.info("");
210932
+ }
211011
210933
  // src/lib/sandbox/project-info.ts
211012
210934
  import fs6 from "node:fs";
211013
210935
  import path5 from "node:path";
211014
210936
  import { loadPlaycademyConfig } from "playcademy/utils";
211015
- async function extractProjectInfo(viteConfig) {
210937
+ function extractTimebackCourses(config2, timebackOptions) {
210938
+ const courses = config2?.integrations?.timeback?.courses;
210939
+ if (!courses || courses.length === 0)
210940
+ return;
210941
+ return courses.map((course) => {
210942
+ const key = `${course.subject}:${course.grade}`;
210943
+ const overrideId = timebackOptions?.courses?.[key];
210944
+ const courseId = overrideId && overrideId !== "mock" ? overrideId : undefined;
210945
+ return {
210946
+ subject: course.subject,
210947
+ grade: course.grade,
210948
+ courseId,
210949
+ totalXp: course.totalXp,
210950
+ masterableUnits: course.masterableUnits
210951
+ };
210952
+ });
210953
+ }
210954
+ async function extractProjectInfo(viteConfig, timebackOptions) {
211016
210955
  const projectRoot = viteConfig.root;
211017
210956
  const directoryName = path5.basename(projectRoot);
211018
- let configName;
210957
+ let config2 = null;
211019
210958
  try {
211020
- const config2 = await loadPlaycademyConfig();
211021
- configName = config2?.name;
210959
+ config2 = await loadPlaycademyConfig();
211022
210960
  } catch {}
211023
210961
  let packageJson = {};
211024
210962
  try {
@@ -211028,7 +210966,7 @@ async function extractProjectInfo(viteConfig) {
211028
210966
  packageJson = JSON.parse(packageJsonContent);
211029
210967
  }
211030
210968
  } catch {}
211031
- const name3 = configName || (typeof packageJson.name === "string" ? packageJson.name : "");
210969
+ const name3 = config2?.name || packageJson.name || "";
211032
210970
  let slug = name3;
211033
210971
  if (slug.includes("/")) {
211034
210972
  slug = slug.split("/")[1] || slug;
@@ -211040,12 +210978,21 @@ async function extractProjectInfo(viteConfig) {
211040
210978
  return {
211041
210979
  slug,
211042
210980
  displayName,
211043
- version: (typeof packageJson.version === "string" ? packageJson.version : null) || "dev",
211044
- description: typeof packageJson.description === "string" ? packageJson.description : undefined
210981
+ version: packageJson.version || "dev",
210982
+ description: packageJson.description,
210983
+ timebackCourses: extractTimebackCourses(config2, timebackOptions)
211045
210984
  };
211046
210985
  }
211047
210986
 
211048
210987
  // src/lib/sandbox/timeback.ts
210988
+ var import_picocolors5 = __toESM(require_picocolors(), 1);
210989
+ function getEffectiveTimebackId(timeback) {
210990
+ if (timeback?.timebackId)
210991
+ return timeback.timebackId;
210992
+ if (timeback?.courses && Object.keys(timeback.courses).length > 0)
210993
+ return "mock";
210994
+ return;
210995
+ }
211049
210996
  function detectTimebackOptions() {
211050
210997
  if (process.env.TIMEBACK_LOCAL === "true") {
211051
210998
  return {
@@ -211070,16 +211017,60 @@ function detectTimebackOptions() {
211070
211017
  }
211071
211018
  return;
211072
211019
  }
211020
+ function hasTimebackCredentials2() {
211021
+ const config2 = detectTimebackOptions();
211022
+ if (!config2)
211023
+ return false;
211024
+ if (config2.mode === "local") {
211025
+ return !!(config2.onerosterApiUrl && config2.caliperApiUrl);
211026
+ }
211027
+ return !!(config2.onerosterApiUrl && config2.clientId && config2.clientSecret && config2.authUrl);
211028
+ }
211029
+ function validateTimebackConfig(viteConfig, timeback) {
211030
+ if (!timeback?.courses)
211031
+ return;
211032
+ const realCourses = Object.entries(timeback.courses).filter(([, value]) => value !== "mock");
211033
+ if (realCourses.length > 0 && !hasTimebackCredentials2()) {
211034
+ const courseList = realCourses.map(([key]) => key).join(", ");
211035
+ viteConfig.logger.warn("");
211036
+ viteConfig.logger.warn(import_picocolors5.default.yellow(`⚠️ TimeBack: Real course IDs for ${import_picocolors5.default.bold(courseList)} but credentials missing.`));
211037
+ viteConfig.logger.warn(import_picocolors5.default.dim(` Required: TIMEBACK_API_CLIENT_ID, TIMEBACK_API_CLIENT_SECRET, TIMEBACK_API_AUTH_URL, TIMEBACK_ONEROSTER_API_URL`));
211038
+ viteConfig.logger.warn(import_picocolors5.default.dim(` Or use 'mock' for local testing.`));
211039
+ viteConfig.logger.warn("");
211040
+ }
211041
+ }
211042
+ function generateTimebackJson(timeback) {
211043
+ if (!timeback?.courses || Object.keys(timeback.courses).length === 0) {
211044
+ return;
211045
+ }
211046
+ const enrollments = [];
211047
+ for (const [key, value] of Object.entries(timeback.courses)) {
211048
+ const parts2 = key.split(":");
211049
+ const subject = parts2[0];
211050
+ const gradeStr = parts2[1];
211051
+ const grade = gradeStr ? parseInt(gradeStr, 10) : NaN;
211052
+ if (!subject || isNaN(grade))
211053
+ continue;
211054
+ const courseId = value === "mock" ? `mock-${subject.toLowerCase()}-g${grade}` : value;
211055
+ enrollments.push({ subject, grade, courseId });
211056
+ }
211057
+ if (enrollments.length === 0) {
211058
+ return;
211059
+ }
211060
+ const roleOverride = getTimebackRoleOverride();
211061
+ const role = roleOverride ?? timeback.role ?? "student";
211062
+ return JSON.stringify({ role, enrollments });
211063
+ }
211073
211064
 
211074
211065
  // src/lib/sandbox/server.ts
211075
211066
  function printSandboxInfo(viteConfig, apiPort, realtimePort, projectInfo, realtimeEnabled) {
211076
211067
  viteConfig.logger.info("");
211077
- viteConfig.logger.info(` ${import_picocolors5.default.green(import_picocolors5.default.bold("PLAYCADEMY"))} ${import_picocolors5.default.green(`v${version3}`)}`);
211068
+ viteConfig.logger.info(` ${import_picocolors6.default.green(import_picocolors6.default.bold("PLAYCADEMY"))} ${import_picocolors6.default.green(`v${version3}`)}`);
211078
211069
  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`)}`);
211070
+ viteConfig.logger.info(` ${import_picocolors6.default.green("➜")} ${import_picocolors6.default.bold("Game:")} ${import_picocolors6.default.cyan(projectInfo.slug)}`);
211071
+ 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
211072
  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())}`)}`);
211073
+ 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
211074
  }
211084
211075
  viteConfig.logger.info("");
211085
211076
  }
@@ -211094,7 +211085,9 @@ async function startSandbox(viteConfig, autoStart = true, options = {}) {
211094
211085
  databasePath,
211095
211086
  realtimeEnabled = false,
211096
211087
  realtimePort,
211097
- logLevel = "info"
211088
+ logLevel = "info",
211089
+ timebackId,
211090
+ timebackOptions
211098
211091
  } = options;
211099
211092
  if (!autoStart || viteConfig.command !== "serve") {
211100
211093
  const baseUrl = customUrl ?? `http://localhost:${DEFAULT_PORTS2.SANDBOX}`;
@@ -211120,11 +211113,17 @@ async function startSandbox(viteConfig, autoStart = true, options = {}) {
211120
211113
  try {
211121
211114
  const sandboxPort = await findAvailablePort(DEFAULT_PORTS2.SANDBOX);
211122
211115
  const baseUrl = `http://localhost:${sandboxPort}`;
211123
- const projectInfo = await extractProjectInfo(viteConfig);
211124
- const timebackOptions = detectTimebackOptions();
211116
+ const projectInfo = await extractProjectInfo(viteConfig, timebackOptions);
211117
+ let sandboxTimebackOptions = detectTimebackOptions();
211118
+ if (timebackId) {
211119
+ sandboxTimebackOptions = {
211120
+ ...sandboxTimebackOptions,
211121
+ studentId: timebackId
211122
+ };
211123
+ }
211125
211124
  const finalRealtimePort = realtimePort ?? await findAvailablePort(sandboxPort + 1);
211126
211125
  const realtimeUrl = `ws://localhost:${finalRealtimePort}`;
211127
- const server = await startServer2(sandboxPort, projectInfo, {
211126
+ const server = await startServer(sandboxPort, projectInfo, {
211128
211127
  verbose,
211129
211128
  quiet,
211130
211129
  seed,
@@ -211133,7 +211132,7 @@ async function startSandbox(viteConfig, autoStart = true, options = {}) {
211133
211132
  recreateDb,
211134
211133
  logLevel,
211135
211134
  realtime: { enabled: realtimeEnabled, port: finalRealtimePort },
211136
- timeback: timebackOptions,
211135
+ timeback: sandboxTimebackOptions,
211137
211136
  logger: createLoggerAdapter("sandbox")
211138
211137
  });
211139
211138
  writeServerInfo("sandbox", {
@@ -211162,7 +211161,7 @@ async function startSandbox(viteConfig, autoStart = true, options = {}) {
211162
211161
  }
211163
211162
  };
211164
211163
  } catch (error2) {
211165
- viteConfig.logger.error(import_picocolors5.default.red(`[Playcademy] Failed to start sandbox: ${error2}`));
211164
+ viteConfig.logger.error(import_picocolors6.default.red(`[Playcademy] Failed to start sandbox: ${error2}`));
211166
211165
  return {
211167
211166
  baseUrl: `http://localhost:${DEFAULT_PORTS2.SANDBOX}`,
211168
211167
  realtimeUrl: "ws://localhost:4322",
@@ -211387,16 +211386,13 @@ var shell_with_corner_badge_default = `<!doctype html>
211387
211386
  <head>
211388
211387
  <meta charset="UTF-8" />
211389
211388
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
211390
- <title>Playcademy - Development</title>
211389
+ <title>Playcademy Dev</title>
211391
211390
  <link rel="preconnect" href="https://fonts.googleapis.com" />
211392
211391
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
211393
211392
  <link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet" />
211393
+ <script type="module" src="/@vite/client"></script>
211394
211394
  <script type="importmap">
211395
- {
211396
- "imports": {
211397
- "@playcademy/sdk": "https://esm.sh/@playcademy/sdk@latest"
211398
- }
211399
- }
211395
+ { "imports": { "@playcademy/sdk": "https://esm.sh/@playcademy/sdk@latest" } }
211400
211396
  </script>
211401
211397
  <style>
211402
211398
  * {
@@ -211404,14 +211400,11 @@ var shell_with_corner_badge_default = `<!doctype html>
211404
211400
  padding: 0;
211405
211401
  box-sizing: border-box;
211406
211402
  }
211407
-
211408
211403
  body {
211409
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
211410
211404
  height: 100vh;
211411
211405
  overflow: hidden;
211412
211406
  }
211413
-
211414
- .dev-badge {
211407
+ #badge {
211415
211408
  position: fixed;
211416
211409
  top: 0.5rem;
211417
211410
  left: 0.5rem;
@@ -211426,202 +211419,144 @@ var shell_with_corner_badge_default = `<!doctype html>
211426
211419
  letter-spacing: 0.05em;
211427
211420
  transition: background 0.2s ease;
211428
211421
  }
211429
-
211430
- .dev-badge.offline {
211422
+ #badge.offline {
211431
211423
  background: rgba(211, 47, 47, 0.9);
211432
211424
  }
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 {
211425
+ #frame {
211450
211426
  position: absolute;
211451
211427
  inset: 0;
211452
211428
  border: none;
211453
211429
  width: 100%;
211454
211430
  height: 100%;
211455
211431
  }
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
211432
  .hidden {
211469
211433
  display: none;
211470
211434
  }
211471
211435
  </style>
211472
211436
  </head>
211473
211437
  <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>
211438
+ <div id="badge">PLAYCADEMY</div>
211439
+ <iframe id="frame" class="hidden"></iframe>
211489
211440
 
211490
211441
  <script type="module">
211491
211442
  import { MessageEvents, messaging } from '@playcademy/sdk'
211492
211443
 
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')
211444
+ // Config (injected by vite plugin)
211445
+ const CONFIG = {
211446
+ sandboxUrl: '{{SANDBOX_URL}}',
211447
+ gameId: '{{GAME_ID}}',
211448
+ gameUrl: '{{GAME_URL}}' || undefined,
211449
+ realtimeUrl: '{{REALTIME_URL}}',
211450
+ timebackJson: '{{TIMEBACK_DATA}}',
211451
+ }
211499
211452
 
211500
- const logIfDebug = (...args) => {
211501
- if (window.PLAYCADEMY_DEBUG) {
211502
- console.log('[PlaycademyDevShell]', ...args)
211453
+ // Parse timeback data (role + enrollments)
211454
+ const timeback = (() => {
211455
+ try {
211456
+ return JSON.parse(CONFIG.timebackJson)
211457
+ } catch {
211458
+ return null
211503
211459
  }
211504
- }
211460
+ })()
211505
211461
 
211506
- logIfDebug('[PlaycademyDevShell] Initialized')
211462
+ // Elements
211463
+ const badge = document.getElementById('badge')
211464
+ const frame = document.getElementById('frame')
211507
211465
 
211508
- async function checkSandboxConnection() {
211466
+ // Debug logging
211467
+ const log = (...args) => window.PLAYCADEMY_DEBUG && console.log('[DevShell]', ...args)
211468
+
211469
+ // Check sandbox connection
211470
+ async function checkSandbox() {
211509
211471
  try {
211510
- const response = await fetch('{{SANDBOX_URL}}/api/users/me', {
211511
- headers: {
211512
- Authorization: 'Bearer sandbox-demo-token',
211513
- },
211472
+ const res = await fetch(\`\${CONFIG.sandboxUrl}/api/users/me\`, {
211473
+ headers: { Authorization: 'Bearer sandbox-demo-token' },
211514
211474
  })
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')
211475
+ if (!res.ok) throw new Error('Sandbox unavailable')
211476
+ badge.classList.remove('offline')
211477
+ log('Sandbox connected')
211478
+ return true
211479
+ } catch (e) {
211480
+ badge.classList.add('offline')
211481
+ badge.textContent = 'PLAYCADEMY ⚠️'
211482
+ log('Sandbox failed:', e)
211483
+ return false
211527
211484
  }
211528
211485
  }
211529
211486
 
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,
211487
+ // Init handshake with game iframe
211488
+ function initHandshake(timebackData) {
211489
+ const payload = {
211490
+ baseUrl: CONFIG.sandboxUrl,
211491
+ gameUrl: CONFIG.gameUrl,
211492
+ gameId: CONFIG.gameId,
211493
+ realtimeUrl: CONFIG.realtimeUrl,
211538
211494
  token: 'sandbox-demo-token',
211539
- gameId: '{{GAME_ID}}',
211540
- realtimeUrl: '{{REALTIME_URL}}',
211495
+ timeback: timebackData,
211541
211496
  }
211542
211497
 
211543
- const sendInit = () => {
211544
- if (!gameFrame.contentWindow) return
211545
-
211546
- messaging.send(MessageEvents.INIT, initPayload, {
211547
- target: gameFrame.contentWindow,
211548
- origin: '*',
211549
- })
211550
- }
211498
+ let interval, timeout
211551
211499
 
211552
- const stopHandshake = () => {
211553
- if (handshakeInterval) {
211554
- clearInterval(handshakeInterval)
211555
- handshakeInterval = null
211556
- }
211557
- if (handshakeTimeout) {
211558
- clearTimeout(handshakeTimeout)
211559
- handshakeTimeout = null
211500
+ const send = () => {
211501
+ if (frame.contentWindow) {
211502
+ messaging.send(MessageEvents.INIT, payload, {
211503
+ target: frame.contentWindow,
211504
+ origin: '*',
211505
+ })
211560
211506
  }
211561
211507
  }
211562
211508
 
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)
211509
+ const stop = () => {
211510
+ clearInterval(interval)
211511
+ clearTimeout(timeout)
211577
211512
  }
211578
211513
 
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()
211514
+ frame.onload = () => {
211515
+ frame.classList.remove('hidden')
211516
+ log('Frame loaded, starting handshake')
211517
+ send()
211518
+ interval = setInterval(send, 300)
211519
+ timeout = setTimeout(() => {
211520
+ stop()
211521
+ log('Handshake timeout')
211522
+ }, 10000)
211585
211523
  }
211586
211524
 
211587
- // Listen for READY message to stop handshake
211588
211525
  messaging.listen(MessageEvents.READY, () => {
211589
- logIfDebug('[PlaycademyDevShell] Received READY from game, stopping handshake')
211590
- stopHandshake()
211526
+ stop()
211527
+ log('Game ready')
211591
211528
  })
211592
211529
 
211593
- gameFrame.src = '/'
211530
+ frame.src = '/'
211594
211531
  }
211595
211532
 
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
- }
211533
+ // Bridge messages from game to shell context
211534
+ window.addEventListener('message', e => {
211535
+ if (e.source !== frame.contentWindow) return
211536
+ const { type, ...payload } = e.data || {}
211537
+ if (type?.startsWith('PLAYCADEMY_')) {
211538
+ log('Message:', type, payload)
211539
+ messaging.send(type, payload)
211608
211540
  }
211609
211541
  })
211610
211542
 
211611
- // Initialize
211612
- checkSandboxConnection()
211613
- loadGame()
211543
+ // Start
211544
+ checkSandbox().then(() => initHandshake(timeback))
211614
211545
  </script>
211615
211546
  </body>
211616
211547
  </html>
211548
+
211549
+
211550
+
211617
211551
  `;
211618
211552
 
211619
211553
  // 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 || "");
211554
+ function generateLoaderHTML(sandboxUrl, gameId, realtimeUrl, options, gameUrl) {
211555
+ const shell = options.showBadge ? shell_with_corner_badge_default : shell_no_badge_default;
211556
+ const timebackJson = generateTimebackJson(options.timeback) ?? "null";
211557
+ 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
211558
  }
211624
- function devServerMiddleware(server, sandbox, gameUrl, showBadge) {
211559
+ function devServerMiddleware(server, sandbox, gameUrl, options) {
211625
211560
  server.middlewares.use("/", (req, res, next) => {
211626
211561
  if (getCurrentMode() !== "platform") {
211627
211562
  next();
@@ -211633,8 +211568,7 @@ function devServerMiddleware(server, sandbox, gameUrl, showBadge) {
211633
211568
  next();
211634
211569
  } else {
211635
211570
  res.setHeader("Content-Type", "text/html");
211636
- const gameId = sandbox.project?.slug;
211637
- res.end(generateLoaderHTML(sandbox.baseUrl, gameId ?? "", sandbox.realtimeUrl, showBadge, gameUrl));
211571
+ res.end(generateLoaderHTML(sandbox.baseUrl, sandbox.project?.slug ?? "", sandbox.realtimeUrl, options, gameUrl));
211638
211572
  }
211639
211573
  return;
211640
211574
  }
@@ -211642,6 +211576,190 @@ function devServerMiddleware(server, sandbox, gameUrl, showBadge) {
211642
211576
  });
211643
211577
  }
211644
211578
 
211579
+ // src/server/hotkeys/recreate-database.ts
211580
+ var { bold: bold4, cyan: cyan3, dim: dim4, green: green2, red, yellow: yellow2 } = import_picocolors7.default;
211581
+ function formatTimestamp3() {
211582
+ const now2 = new Date;
211583
+ const hours = now2.getHours();
211584
+ const minutes = now2.getMinutes().toString().padStart(2, "0");
211585
+ const seconds = now2.getSeconds().toString().padStart(2, "0");
211586
+ const ampm = hours >= 12 ? "PM" : "AM";
211587
+ const displayHours = hours % 12 || 12;
211588
+ return dim4(`${displayHours}:${minutes}:${seconds} ${ampm}`);
211589
+ }
211590
+ async function recreateSandboxDatabase(options) {
211591
+ const currentMode = getCurrentMode();
211592
+ const viteServer = getViteServerRef();
211593
+ if (!viteServer) {
211594
+ options.viteConfig.logger.error(`${formatTimestamp3()} ${red(bold4("[playcademy]"))} ${dim4("(sandbox)")} ${red("Cannot recreate sandbox database: no Vite server reference")}`);
211595
+ return;
211596
+ }
211597
+ if (currentMode !== "platform") {
211598
+ options.viteConfig.logger.warn(`${formatTimestamp3()} ${yellow2(bold4("[playcademy]"))} ${dim4("(sandbox)")} ${yellow2("can only recreate sandbox database in platform mode (m + enter)")}`);
211599
+ return;
211600
+ }
211601
+ options.viteConfig.logger.info(`${formatTimestamp3()} ${cyan3(bold4("[playcademy]"))} ${dim4("(sandbox)")} recreating database...`);
211602
+ if (serverState.sandbox) {
211603
+ serverState.sandbox.cleanup();
211604
+ serverState.sandbox = null;
211605
+ }
211606
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
211607
+ const sandbox = await startSandbox(options.viteConfig, options.platformModeOptions.startSandbox, {
211608
+ verbose: options.platformModeOptions.verbose,
211609
+ logLevel: options.platformModeOptions.logLevel,
211610
+ customUrl: options.platformModeOptions.sandboxUrl,
211611
+ quiet: true,
211612
+ recreateDb: true,
211613
+ seed: options.platformModeOptions.seed,
211614
+ memoryOnly: options.platformModeOptions.memoryOnly,
211615
+ databasePath: options.platformModeOptions.databasePath,
211616
+ realtimeEnabled: options.platformModeOptions.realtimeEnabled,
211617
+ realtimePort: options.platformModeOptions.realtimePort,
211618
+ timebackId: getEffectiveTimebackId(options.platformModeOptions.timeback)
211619
+ });
211620
+ serverState.sandbox = sandbox;
211621
+ if (sandbox.project && serverState.backend) {
211622
+ const gameUrl = `http://localhost:${serverState.backend.port}`;
211623
+ devServerMiddleware(viteServer, sandbox, gameUrl, {
211624
+ showBadge: options.platformModeOptions.showBadge,
211625
+ timeback: options.platformModeOptions.timeback
211626
+ });
211627
+ }
211628
+ options.viteConfig.logger.info(`${formatTimestamp3()} ${cyan3(bold4("[playcademy]"))} ${dim4("(sandbox)")} ${green2("database recreated")}`);
211629
+ }
211630
+ var recreateDatabaseHotkey = (options) => ({
211631
+ key: "d",
211632
+ description: "recreate sandbox database",
211633
+ action: () => recreateSandboxDatabase(options)
211634
+ });
211635
+
211636
+ // src/server/hotkeys/toggle-mode.ts
211637
+ var import_picocolors10 = __toESM(require_picocolors(), 1);
211638
+ // package.json
211639
+ var package_default2 = {
211640
+ name: "@playcademy/vite-plugin",
211641
+ version: "0.1.40",
211642
+ type: "module",
211643
+ exports: {
211644
+ ".": {
211645
+ import: "./dist/index.js",
211646
+ types: "./dist/index.d.ts"
211647
+ }
211648
+ },
211649
+ main: "dist/index.js",
211650
+ module: "dist/index.js",
211651
+ files: [
211652
+ "dist"
211653
+ ],
211654
+ scripts: {
211655
+ build: "rm -rf dist && bun build.ts",
211656
+ docs: "typedoc --skipErrorChecking",
211657
+ pub: "bun publish.ts"
211658
+ },
211659
+ dependencies: {
211660
+ archiver: "^7.0.1",
211661
+ picocolors: "^1.1.1",
211662
+ playcademy: "workspace:*"
211663
+ },
211664
+ devDependencies: {
211665
+ "@inquirer/prompts": "^7.8.6",
211666
+ "@playcademy/sandbox": "workspace:*",
211667
+ "@types/archiver": "^6.0.3",
211668
+ "@types/bun": "latest"
211669
+ },
211670
+ peerDependencies: {
211671
+ typescript: "^5",
211672
+ vite: "^5 || ^6"
211673
+ }
211674
+ };
211675
+
211676
+ // src/lib/backend/server.ts
211677
+ import {
211678
+ loadPlaycademyConfigAndSetWorkspace as loadConfigAndSetWorkspace,
211679
+ startPlaycademyDevServer,
211680
+ startPlaycademyHotReload
211681
+ } from "playcademy/utils";
211682
+
211683
+ // src/lib/backend/hot-reload.ts
211684
+ var import_picocolors8 = __toESM(require_picocolors(), 1);
211685
+ function formatChangedPath(changedPath) {
211686
+ if (!changedPath)
211687
+ return;
211688
+ if (changedPath.includes("/api/")) {
211689
+ return changedPath.substring(changedPath.indexOf("/api/"));
211690
+ }
211691
+ return changedPath;
211692
+ }
211693
+ function createHotReloadCallbacks(viteConfig) {
211694
+ return {
211695
+ onSuccess: (changedPath) => {
211696
+ const relativePath = formatChangedPath(changedPath);
211697
+ if (relativePath) {
211698
+ viteConfig.logger.info(`${import_picocolors8.dim("(backend)")} ${import_picocolors8.green("hmr update")} ${import_picocolors8.dim(relativePath)}`, { timestamp: true });
211699
+ } else {
211700
+ viteConfig.logger.info("backend reloaded", { timestamp: true });
211701
+ }
211702
+ },
211703
+ onError: (error2) => {
211704
+ viteConfig.logger.error(`backend reload failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
211705
+ }
211706
+ };
211707
+ }
211708
+
211709
+ // src/lib/backend/server.ts
211710
+ async function tryLoadConfig(viteConfig, configPath) {
211711
+ try {
211712
+ return await loadConfigAndSetWorkspace(configPath);
211713
+ } catch (error2) {
211714
+ if (error2 instanceof Error && !error2.message.includes("Could not find")) {
211715
+ viteConfig.logger.warn(`Could not load playcademy.config.js: ${error2.message}`);
211716
+ }
211717
+ return null;
211718
+ }
211719
+ }
211720
+ function needsCliDevServer(config2) {
211721
+ return !!config2.integrations;
211722
+ }
211723
+ async function startServer2(options) {
211724
+ const { port, config: config2, platformUrl } = options;
211725
+ return startPlaycademyDevServer({
211726
+ port,
211727
+ config: config2,
211728
+ quiet: true,
211729
+ platformUrl,
211730
+ customLogger: createLoggerAdapter("backend")
211731
+ });
211732
+ }
211733
+ function setupHotReload(serverRef, options) {
211734
+ const watcher = startPlaycademyHotReload(async () => {
211735
+ await serverRef.current.server.dispose();
211736
+ serverRef.current = await startServer2(options);
211737
+ }, createHotReloadCallbacks(options.viteConfig));
211738
+ return () => watcher.close();
211739
+ }
211740
+ async function setupCliDevServer(options) {
211741
+ const { preferredPort, viteConfig, platformUrl, configPath } = options;
211742
+ const config2 = await tryLoadConfig(viteConfig, configPath);
211743
+ if (!config2)
211744
+ return null;
211745
+ if (!needsCliDevServer(config2))
211746
+ return null;
211747
+ try {
211748
+ const port = await findAvailablePort(preferredPort);
211749
+ const serverOptions = { port, config: config2, platformUrl, viteConfig };
211750
+ const serverRef = { current: await startServer2(serverOptions) };
211751
+ const stopHotReload = setupHotReload(serverRef, serverOptions);
211752
+ return {
211753
+ server: serverRef.current.server,
211754
+ port: serverRef.current.port,
211755
+ stopHotReload,
211756
+ cleanup: () => cleanupServerInfo("backend", viteConfig.root, process.pid)
211757
+ };
211758
+ } catch (error2) {
211759
+ viteConfig.logger.error(`Failed to start game backend: ${error2 instanceof Error ? error2.message : String(error2)}`);
211760
+ return null;
211761
+ }
211762
+ }
211645
211763
  // src/server/platform-mode.ts
211646
211764
  async function configurePlatformMode(server, viteConfig, options) {
211647
211765
  const sandbox = await startSandbox(viteConfig, options.startSandbox, {
@@ -211654,7 +211772,9 @@ async function configurePlatformMode(server, viteConfig, options) {
211654
211772
  memoryOnly: options.memoryOnly,
211655
211773
  databasePath: options.databasePath,
211656
211774
  realtimeEnabled: options.realtimeEnabled,
211657
- realtimePort: options.realtimePort
211775
+ realtimePort: options.realtimePort,
211776
+ timebackId: getEffectiveTimebackId(options.timeback),
211777
+ timebackOptions: options.timeback
211658
211778
  });
211659
211779
  serverState.sandbox = sandbox;
211660
211780
  const backend = await setupCliDevServer({
@@ -211665,25 +211785,29 @@ async function configurePlatformMode(server, viteConfig, options) {
211665
211785
  });
211666
211786
  serverState.backend = backend;
211667
211787
  if (sandbox.project) {
211788
+ validateTimebackConfig(viteConfig, options.timeback);
211668
211789
  const gameUrl = backend ? `http://localhost:${backend.port}` : undefined;
211669
- devServerMiddleware(server, sandbox, gameUrl, options.showBadge);
211790
+ devServerMiddleware(server, sandbox, gameUrl, {
211791
+ showBadge: options.showBadge,
211792
+ timeback: options.timeback
211793
+ });
211670
211794
  }
211671
211795
  server.httpServer?.once("listening", () => {
211672
211796
  setTimeout(async () => {
211673
- const projectInfo = await extractProjectInfo(viteConfig);
211797
+ const projectInfo = await extractProjectInfo(viteConfig, options.timeback);
211674
211798
  const vitePort = server.config.server.port;
211675
211799
  printBanner(viteConfig, {
211676
211800
  sandbox: sandbox.port,
211677
211801
  backend: backend?.port,
211678
211802
  realtime: sandbox.realtimePort,
211679
211803
  vite: vitePort
211680
- }, projectInfo, package_default.version);
211804
+ }, projectInfo, package_default2.version);
211681
211805
  }, 100);
211682
211806
  });
211683
211807
  }
211684
211808
 
211685
211809
  // src/server/standalone-mode.ts
211686
- var import_picocolors6 = __toESM(require_picocolors(), 1);
211810
+ var import_picocolors9 = __toESM(require_picocolors(), 1);
211687
211811
  async function configureStandaloneMode(server, viteConfig, options) {
211688
211812
  const backend = await setupCliDevServer({
211689
211813
  preferredPort: options.preferredPort,
@@ -211699,32 +211823,32 @@ async function configureStandaloneMode(server, viteConfig, options) {
211699
211823
  server.httpServer?.once("listening", () => {
211700
211824
  setTimeout(() => {
211701
211825
  viteConfig.logger.info("");
211702
- viteConfig.logger.info(` ${import_picocolors6.default.green(import_picocolors6.default.bold("PLAYCADEMY"))} ${import_picocolors6.default.green(`v${package_default.version}`)}`);
211826
+ viteConfig.logger.info(` ${import_picocolors9.default.green(import_picocolors9.default.bold("PLAYCADEMY"))} ${import_picocolors9.default.green(`v${package_default2.version}`)}`);
211703
211827
  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")}`);
211828
+ viteConfig.logger.info(` ${import_picocolors9.default.green("➜")} ${import_picocolors9.default.bold("Backend:")} ${import_picocolors9.default.cyan(`http://localhost:${backend.port}`)}`);
211829
+ viteConfig.logger.info(` ${import_picocolors9.default.green("➜")} ${import_picocolors9.default.bold("Sandbox:")} ${import_picocolors9.default.cyan("Disabled")}`);
211706
211830
  viteConfig.logger.info("");
211707
211831
  }, 100);
211708
211832
  });
211709
211833
  }
211710
211834
 
211711
- // src/server/mode-switcher.ts
211712
- var { bold: bold3, cyan: cyan3, dim: dim4, green: green3, red } = import_picocolors7.default;
211713
- function formatTimestamp2() {
211835
+ // src/server/hotkeys/toggle-mode.ts
211836
+ var { bold: bold5, cyan: cyan4, dim: dim6, green: green4, red: red2 } = import_picocolors10.default;
211837
+ function formatTimestamp4() {
211714
211838
  const now2 = new Date;
211715
211839
  const hours = now2.getHours();
211716
211840
  const minutes = now2.getMinutes().toString().padStart(2, "0");
211717
211841
  const seconds = now2.getSeconds().toString().padStart(2, "0");
211718
211842
  const ampm = hours >= 12 ? "PM" : "AM";
211719
211843
  const displayHours = hours % 12 || 12;
211720
- return dim4(`${displayHours}:${minutes}:${seconds} ${ampm}`);
211844
+ return dim6(`${displayHours}:${minutes}:${seconds} ${ampm}`);
211721
211845
  }
211722
211846
  async function toggleMode(options) {
211723
211847
  const currentMode = getCurrentMode();
211724
211848
  const newMode = currentMode === "platform" ? "standalone" : "platform";
211725
211849
  const viteServer = getViteServerRef();
211726
211850
  if (!viteServer) {
211727
- options.viteConfig.logger.error(`${formatTimestamp2()} ${red(bold3("[playcademy]"))} ${red("Cannot toggle mode: no Vite server reference")}`);
211851
+ options.viteConfig.logger.error(`${formatTimestamp4()} ${red2(bold5("[playcademy]"))} ${red2("Cannot toggle mode: no Vite server reference")}`);
211728
211852
  return;
211729
211853
  }
211730
211854
  await cleanupServers();
@@ -211738,56 +211862,39 @@ async function toggleMode(options) {
211738
211862
  } else {
211739
211863
  await configurePlatformMode(viteServer, options.viteConfig, options.platformModeOptions);
211740
211864
  }
211741
- options.viteConfig.logger.info(`${formatTimestamp2()} ${cyan3(bold3("[playcademy]"))} ${green3("switched to")} ${green3(bold3(newMode))} ${green3("mode")}`);
211865
+ options.viteConfig.logger.info(`${formatTimestamp4()} ${cyan4(bold5("[playcademy]"))} ${green4("switched to")} ${green4(bold5(newMode))} ${green4("mode")}`);
211742
211866
  }
211867
+ var toggleModeHotkey = (options) => ({
211868
+ key: "m",
211869
+ description: "toggle platform/standalone mode",
211870
+ action: () => toggleMode(options)
211871
+ });
211743
211872
 
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}`);
211873
+ // src/server/hotkeys/index.ts
211874
+ function getHotkeys(options) {
211875
+ return [
211876
+ toggleModeHotkey(options),
211877
+ recreateDatabaseHotkey(options),
211878
+ cycleTimebackRoleHotkey(options)
211879
+ ];
211755
211880
  }
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)")}`);
211881
+
211882
+ // src/server/lifecycle.ts
211883
+ var shutdownHandlersRegistered = false;
211884
+ function setupProcessShutdownHandlers() {
211885
+ if (shutdownHandlersRegistered)
211765
211886
  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")}`);
211887
+ shutdownHandlersRegistered = true;
211888
+ let isShuttingDown = false;
211889
+ const shutdown = async () => {
211890
+ if (isShuttingDown)
211891
+ return;
211892
+ isShuttingDown = true;
211893
+ await cleanupServers();
211894
+ process.exit(0);
211895
+ };
211896
+ process.on("SIGINT", shutdown);
211897
+ process.on("SIGTERM", shutdown);
211791
211898
  }
211792
211899
 
211793
211900
  // src/hooks/configure-server.ts
@@ -211816,7 +211923,8 @@ async function configureServerHook(server, context) {
211816
211923
  realtimePort: context.options.realtimePort,
211817
211924
  showBadge: context.options.showBadge,
211818
211925
  preferredBackendPort: preferredPort,
211819
- configPath: context.options.configPath
211926
+ configPath: context.options.configPath,
211927
+ timeback: context.options.timeback
211820
211928
  };
211821
211929
  if (context.options.mode === "standalone") {
211822
211930
  await configureStandaloneMode(server, context.viteConfig, {
@@ -211833,26 +211941,10 @@ async function configureServerHook(server, context) {
211833
211941
  ...options,
211834
211942
  customShortcuts: [
211835
211943
  ...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
- }
211944
+ ...getHotkeys({
211945
+ viteConfig: context.viteConfig,
211946
+ platformModeOptions
211947
+ })
211856
211948
  ]
211857
211949
  });
211858
211950
  };
@@ -211900,7 +211992,8 @@ function resolveOptions(options) {
211900
211992
  databasePath: sandboxOptions.databasePath,
211901
211993
  realtimeEnabled: realtimeOptions.enabled ?? false,
211902
211994
  realtimePort: realtimeOptions.port,
211903
- showBadge: shellOptions.showBadge ?? true
211995
+ showBadge: shellOptions.showBadge ?? true,
211996
+ timeback: options.timeback
211904
211997
  };
211905
211998
  }
211906
211999
  function createPluginContext(options) {