@playcademy/sandbox 0.2.1 → 0.2.2

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/cli.js CHANGED
@@ -135703,7 +135703,7 @@ var serve = (options, listeningListener) => {
135703
135703
  // package.json
135704
135704
  var package_default = {
135705
135705
  name: "@playcademy/sandbox",
135706
- version: "0.2.1",
135706
+ version: "0.2.2",
135707
135707
  description: "Local development server for Playcademy game development",
135708
135708
  type: "module",
135709
135709
  exports: {
@@ -159749,6 +159749,47 @@ var logger3 = {
159749
159749
  warn: (msg) => getLogger().warn(msg),
159750
159750
  error: (msg) => getLogger().error(msg)
159751
159751
  };
159752
+ // src/database/seed/timeback.ts
159753
+ function resolveStudentId(studentId) {
159754
+ if (!studentId)
159755
+ return null;
159756
+ if (studentId === "mock")
159757
+ return `mock-student-${crypto.randomUUID().slice(0, 8)}`;
159758
+ return studentId;
159759
+ }
159760
+ function getAdminTimebackId() {
159761
+ return resolveStudentId(config.timeback.studentId);
159762
+ }
159763
+ async function seedTimebackIntegrations(db, gameId, courses) {
159764
+ const now2 = new Date;
159765
+ let seededCount = 0;
159766
+ for (const course of courses) {
159767
+ const courseId = `mock-${course.subject.toLowerCase()}-g${course.grade}`;
159768
+ try {
159769
+ const existing = await db.query.gameTimebackIntegrations.findFirst({
159770
+ where: (table14, { and: and3, eq: eq3 }) => and3(eq3(table14.gameId, gameId), eq3(table14.grade, course.grade), eq3(table14.subject, course.subject))
159771
+ });
159772
+ if (existing) {
159773
+ continue;
159774
+ }
159775
+ await db.insert(gameTimebackIntegrations).values({
159776
+ gameId,
159777
+ courseId,
159778
+ grade: course.grade,
159779
+ subject: course.subject,
159780
+ totalXp: course.totalXp ?? 1000,
159781
+ lastVerifiedAt: now2
159782
+ });
159783
+ seededCount++;
159784
+ } catch (error2) {
159785
+ console.error(`❌ Error seeding TimeBack integration for ${course.subject}:${course.grade}:`, error2);
159786
+ }
159787
+ }
159788
+ if (seededCount > 0) {
159789
+ logger3.info(`\uD83D\uDCDA Seeded ${seededCount} TimeBack integration(s)`);
159790
+ }
159791
+ }
159792
+
159752
159793
  // src/database/seed/games.ts
159753
159794
  async function seedCoreGames(db) {
159754
159795
  const now2 = new Date;
@@ -159783,6 +159824,9 @@ async function seedCurrentProjectGame(db, project) {
159783
159824
  });
159784
159825
  if (existingGame) {
159785
159826
  logger3.info(`\uD83C\uDFAE Game "${project.displayName}" (${project.slug}) already exists`);
159827
+ if (project.timebackCourses && project.timebackCourses.length > 0) {
159828
+ await seedTimebackIntegrations(db, existingGame.id, project.timebackCourses);
159829
+ }
159786
159830
  return existingGame;
159787
159831
  }
159788
159832
  const gameRecord = {
@@ -159802,6 +159846,12 @@ async function seedCurrentProjectGame(db, project) {
159802
159846
  updatedAt: now2
159803
159847
  };
159804
159848
  const [newGame] = await db.insert(games).values(gameRecord).returning();
159849
+ if (!newGame) {
159850
+ throw new Error("Failed to create game record");
159851
+ }
159852
+ if (project.timebackCourses && project.timebackCourses.length > 0) {
159853
+ await seedTimebackIntegrations(db, newGame.id, project.timebackCourses);
159854
+ }
159805
159855
  return newGame;
159806
159856
  } catch (error2) {
159807
159857
  console.error("❌ Error seeding project game:", error2);
@@ -159863,18 +159913,6 @@ async function seedSpriteTemplates(db) {
159863
159913
  }
159864
159914
  }
159865
159915
 
159866
- // src/database/seed/timeback.ts
159867
- function resolveStudentId(studentId) {
159868
- if (!studentId)
159869
- return null;
159870
- if (studentId === "mock")
159871
- return `mock-student-${crypto.randomUUID().slice(0, 8)}`;
159872
- return studentId;
159873
- }
159874
- function getAdminTimebackId() {
159875
- return resolveStudentId(config.timeback.studentId);
159876
- }
159877
-
159878
159916
  // src/database/seed/index.ts
159879
159917
  async function seedDemoData(db) {
159880
159918
  try {
@@ -171450,14 +171488,196 @@ function printBanner(options) {
171450
171488
  }
171451
171489
 
171452
171490
  // src/cli/options.ts
171453
- function parseProjectInfo(options) {
171491
+ import { resolve as resolve3 } from "node:path";
171492
+
171493
+ // ../utils/src/file-loader.ts
171494
+ import { existsSync as existsSync3, readdirSync, statSync as statSync2 } from "fs";
171495
+ import { readFile as readFile3 } from "fs/promises";
171496
+ import { dirname as dirname2, parse as parse2, resolve as resolve2 } from "path";
171497
+ function findFilePath(filename, startDir, maxLevels = 3) {
171498
+ const filenames = Array.isArray(filename) ? filename : [filename];
171499
+ let currentDir = resolve2(startDir);
171500
+ let levelsSearched = 0;
171501
+ while (levelsSearched <= maxLevels) {
171502
+ for (const fname of filenames) {
171503
+ const filePath = resolve2(currentDir, fname);
171504
+ if (existsSync3(filePath)) {
171505
+ return {
171506
+ path: filePath,
171507
+ dir: currentDir,
171508
+ filename: fname
171509
+ };
171510
+ }
171511
+ }
171512
+ if (levelsSearched >= maxLevels) {
171513
+ break;
171514
+ }
171515
+ const parentDir = dirname2(currentDir);
171516
+ if (parentDir === currentDir) {
171517
+ break;
171518
+ }
171519
+ const parsed = parse2(currentDir);
171520
+ if (parsed.root === currentDir) {
171521
+ break;
171522
+ }
171523
+ currentDir = parentDir;
171524
+ levelsSearched++;
171525
+ }
171526
+ return null;
171527
+ }
171528
+ async function loadFile(filename, options = {}) {
171529
+ const {
171530
+ cwd = process.cwd(),
171531
+ required = false,
171532
+ searchUp = false,
171533
+ maxLevels = 3,
171534
+ parseJson = false,
171535
+ stripComments = false
171536
+ } = options;
171537
+ let fileResult;
171538
+ if (searchUp) {
171539
+ fileResult = findFilePath(filename, cwd, maxLevels);
171540
+ } else {
171541
+ const filenames = Array.isArray(filename) ? filename : [filename];
171542
+ fileResult = null;
171543
+ for (const fname of filenames) {
171544
+ const filePath = resolve2(cwd, fname);
171545
+ if (existsSync3(filePath)) {
171546
+ fileResult = {
171547
+ path: filePath,
171548
+ dir: cwd,
171549
+ filename: fname
171550
+ };
171551
+ break;
171552
+ }
171553
+ }
171554
+ }
171555
+ if (!fileResult) {
171556
+ if (required) {
171557
+ const fileList = Array.isArray(filename) ? filename.join(" or ") : filename;
171558
+ const message3 = searchUp ? `${fileList} not found in ${cwd} or up to ${maxLevels} parent directories` : `${fileList} not found at ${cwd}`;
171559
+ throw new Error(message3);
171560
+ }
171561
+ return null;
171562
+ }
171563
+ try {
171564
+ let content = await readFile3(fileResult.path, "utf-8");
171565
+ if (parseJson) {
171566
+ if (stripComments) {
171567
+ content = stripJsonComments(content);
171568
+ }
171569
+ return JSON.parse(content);
171570
+ }
171571
+ return content;
171572
+ } catch (error2) {
171573
+ throw new Error(`Failed to load ${fileResult.filename} from ${fileResult.path}: ${error2 instanceof Error ? error2.message : String(error2)}`);
171574
+ }
171575
+ }
171576
+ function getFileExtension(path3) {
171577
+ return path3.split(".").pop()?.toLowerCase();
171578
+ }
171579
+ function stripJsonComments(jsonc) {
171580
+ let result = jsonc.replace(/\/\*[\s\S]*?\*\//g, "");
171581
+ result = result.replace(/\/\/.*/g, "");
171582
+ return result;
171583
+ }
171584
+ async function loadModule2(filename, options = {}) {
171585
+ const { cwd = process.cwd(), required = false, searchUp = false, maxLevels = 3 } = options;
171586
+ let fileResult;
171587
+ if (searchUp) {
171588
+ fileResult = findFilePath(filename, cwd, maxLevels);
171589
+ } else {
171590
+ const filenames = Array.isArray(filename) ? filename : [filename];
171591
+ fileResult = null;
171592
+ for (const fname of filenames) {
171593
+ const filePath = resolve2(cwd, fname);
171594
+ if (existsSync3(filePath)) {
171595
+ fileResult = {
171596
+ path: filePath,
171597
+ dir: cwd,
171598
+ filename: fname
171599
+ };
171600
+ break;
171601
+ }
171602
+ }
171603
+ }
171604
+ if (!fileResult) {
171605
+ if (required) {
171606
+ const fileList = Array.isArray(filename) ? filename.join(" or ") : filename;
171607
+ const message3 = searchUp ? `${fileList} not found in ${cwd} or up to ${maxLevels} parent directories` : `${fileList} not found at ${cwd}`;
171608
+ throw new Error(message3);
171609
+ }
171610
+ return null;
171611
+ }
171612
+ try {
171613
+ const ext2 = getFileExtension(fileResult.filename);
171614
+ if (ext2 === "js" || ext2 === "mjs" || ext2 === "cjs") {
171615
+ const module2 = await import(fileResult.path);
171616
+ return module2.default || module2;
171617
+ } else {
171618
+ throw new Error(`Unsupported module type: ${ext2} (only .js, .mjs, .cjs supported)`);
171619
+ }
171620
+ } catch (error2) {
171621
+ throw new Error(`Failed to load module ${fileResult.filename} from ${fileResult.path}: ${error2 instanceof Error ? error2.message : String(error2)}`);
171622
+ }
171623
+ }
171624
+
171625
+ // src/cli/options.ts
171626
+ async function loadPlaycademyConfig(configPath) {
171627
+ try {
171628
+ if (configPath) {
171629
+ const resolved = resolve3(configPath);
171630
+ if (resolved.endsWith(".json")) {
171631
+ return await loadFile(resolved, { parseJson: true });
171632
+ } else {
171633
+ return await loadModule2(resolved);
171634
+ }
171635
+ }
171636
+ const jsonConfig = await loadFile("playcademy.config.json", {
171637
+ parseJson: true
171638
+ });
171639
+ if (jsonConfig)
171640
+ return jsonConfig;
171641
+ const jsConfig = await loadModule2("playcademy.config.js");
171642
+ if (jsConfig)
171643
+ return jsConfig;
171644
+ return null;
171645
+ } catch (error2) {
171646
+ console.warn(`[Sandbox] Failed to load config:`, error2);
171647
+ return null;
171648
+ }
171649
+ }
171650
+ function extractTimebackCourses(config2) {
171651
+ const integrations = config2.integrations;
171652
+ if (!integrations)
171653
+ return;
171654
+ const timeback3 = integrations.timeback;
171655
+ if (!timeback3)
171656
+ return;
171657
+ const courses = timeback3.courses;
171658
+ if (!Array.isArray(courses) || courses.length === 0)
171659
+ return;
171660
+ return courses.map((course) => ({
171661
+ subject: course.subject,
171662
+ grade: course.grade,
171663
+ totalXp: course.totalXp,
171664
+ masterableUnits: course.masterableUnits
171665
+ }));
171666
+ }
171667
+ async function parseProjectInfo(options) {
171454
171668
  if (!options.projectName || !options.projectSlug) {
171455
171669
  return;
171456
171670
  }
171671
+ const config2 = await loadPlaycademyConfig(options.configPath);
171672
+ const timebackCourses = config2 ? extractTimebackCourses(config2) : undefined;
171673
+ if (timebackCourses && timebackCourses.length > 0) {
171674
+ console.log(`[Sandbox] Found ${timebackCourses.length} TimeBack course(s) in config`);
171675
+ }
171457
171676
  return {
171458
171677
  slug: options.projectSlug,
171459
171678
  displayName: options.projectName,
171460
- version: "1.0.0"
171679
+ version: "1.0.0",
171680
+ timebackCourses
171461
171681
  };
171462
171682
  }
171463
171683
  function parseTimebackOptions(options) {
@@ -171484,12 +171704,12 @@ function parseTimebackOptions(options) {
171484
171704
 
171485
171705
  // src/cli/index.ts
171486
171706
  var program2 = new Command;
171487
- program2.name("playcademy-sandbox").description("Local development server for Playcademy game development").version(version3).option("-p, --port <number>", "Port to run the server on", "4321").option("-v, --verbose", "Enable verbose logging", false).option("--project-name <name>", "Name of the current project").option("--project-slug <slug>", "Slug of the current project").option("--realtime", "Enable the realtime server", false).option("--realtime-port <number>", "Port for the realtime server (defaults to main port + 1)").option("--no-seed", "Do not seed the database with demo data").option("--recreate-db", "Recreate the on-disk database on start", false).option("--memory", "Use in-memory database (no persistence)", false).option("--db-path <path>", "Custom path for the database file (relative to cwd or absolute)").option("--timeback-local", "Use local TimeBack instance").option("--timeback-oneroster-url <url>", "TimeBack OneRoster API URL").option("--timeback-caliper-url <url>", "TimeBack Caliper API URL").option("--timeback-course-id <id>", "TimeBack course ID for seeding").option("--timeback-student-id <id>", "TimeBack student ID for demo user").action(async (options) => {
171707
+ program2.name("playcademy-sandbox").description("Local development server for Playcademy game development").version(version3).option("-p, --port <number>", "Port to run the server on", "4321").option("-v, --verbose", "Enable verbose logging", false).option("--project-name <name>", "Name of the current project").option("--project-slug <slug>", "Slug of the current project").option("--realtime", "Enable the realtime server", false).option("--realtime-port <number>", "Port for the realtime server (defaults to main port + 1)").option("--no-seed", "Do not seed the database with demo data").option("--recreate-db", "Recreate the on-disk database on start", false).option("--memory", "Use in-memory database (no persistence)", false).option("--db-path <path>", "Custom path for the database file (relative to cwd or absolute)").option("--config-path <path>", "Path to playcademy.config.json (defaults to cwd)").option("--timeback-local", "Use local TimeBack instance").option("--timeback-oneroster-url <url>", "TimeBack OneRoster API URL").option("--timeback-caliper-url <url>", "TimeBack Caliper API URL").option("--timeback-course-id <id>", "TimeBack course ID for seeding").option("--timeback-student-id <id>", "TimeBack student ID for demo user").action(async (options) => {
171488
171708
  try {
171489
171709
  const requestedPort = parseInt(options.port);
171490
171710
  const availablePort = await findAvailablePort(requestedPort);
171491
171711
  const realtimePort = options.realtimePort ? parseInt(options.realtimePort) : availablePort + 1;
171492
- const project = parseProjectInfo(options);
171712
+ const project = await parseProjectInfo(options);
171493
171713
  const timebackOptions = parseTimebackOptions(options);
171494
171714
  const servers = await startServer(availablePort, project, {
171495
171715
  seed: options.seed,
package/dist/server.d.ts CHANGED
@@ -20,6 +20,15 @@ interface TimebackConfig {
20
20
  /**
21
21
  * Project information types
22
22
  */
23
+ /**
24
+ * TimeBack course configuration from playcademy.config.json
25
+ */
26
+ interface TimebackCourseConfig {
27
+ subject: string;
28
+ grade: number;
29
+ totalXp?: number | null;
30
+ masterableUnits?: number | null;
31
+ }
23
32
  /**
24
33
  * Information about the current game project
25
34
  * Used for seeding and display
@@ -29,6 +38,8 @@ interface ProjectInfo {
29
38
  displayName: string;
30
39
  version: string;
31
40
  description?: string;
41
+ /** TimeBack courses from playcademy.config.json integrations.timeback.courses */
42
+ timebackCourses?: TimebackCourseConfig[];
32
43
  }
33
44
 
34
45
  /**
package/dist/server.js CHANGED
@@ -133793,7 +133793,7 @@ var serve = (options, listeningListener) => {
133793
133793
  // package.json
133794
133794
  var package_default = {
133795
133795
  name: "@playcademy/sandbox",
133796
- version: "0.2.1",
133796
+ version: "0.2.2",
133797
133797
  description: "Local development server for Playcademy game development",
133798
133798
  type: "module",
133799
133799
  exports: {
@@ -157839,6 +157839,47 @@ var logger3 = {
157839
157839
  warn: (msg) => getLogger().warn(msg),
157840
157840
  error: (msg) => getLogger().error(msg)
157841
157841
  };
157842
+ // src/database/seed/timeback.ts
157843
+ function resolveStudentId(studentId) {
157844
+ if (!studentId)
157845
+ return null;
157846
+ if (studentId === "mock")
157847
+ return `mock-student-${crypto.randomUUID().slice(0, 8)}`;
157848
+ return studentId;
157849
+ }
157850
+ function getAdminTimebackId() {
157851
+ return resolveStudentId(config.timeback.studentId);
157852
+ }
157853
+ async function seedTimebackIntegrations(db, gameId, courses) {
157854
+ const now2 = new Date;
157855
+ let seededCount = 0;
157856
+ for (const course of courses) {
157857
+ const courseId = `mock-${course.subject.toLowerCase()}-g${course.grade}`;
157858
+ try {
157859
+ const existing = await db.query.gameTimebackIntegrations.findFirst({
157860
+ where: (table14, { and: and3, eq: eq3 }) => and3(eq3(table14.gameId, gameId), eq3(table14.grade, course.grade), eq3(table14.subject, course.subject))
157861
+ });
157862
+ if (existing) {
157863
+ continue;
157864
+ }
157865
+ await db.insert(gameTimebackIntegrations).values({
157866
+ gameId,
157867
+ courseId,
157868
+ grade: course.grade,
157869
+ subject: course.subject,
157870
+ totalXp: course.totalXp ?? 1000,
157871
+ lastVerifiedAt: now2
157872
+ });
157873
+ seededCount++;
157874
+ } catch (error2) {
157875
+ console.error(`❌ Error seeding TimeBack integration for ${course.subject}:${course.grade}:`, error2);
157876
+ }
157877
+ }
157878
+ if (seededCount > 0) {
157879
+ logger3.info(`\uD83D\uDCDA Seeded ${seededCount} TimeBack integration(s)`);
157880
+ }
157881
+ }
157882
+
157842
157883
  // src/database/seed/games.ts
157843
157884
  async function seedCoreGames(db) {
157844
157885
  const now2 = new Date;
@@ -157873,6 +157914,9 @@ async function seedCurrentProjectGame(db, project) {
157873
157914
  });
157874
157915
  if (existingGame) {
157875
157916
  logger3.info(`\uD83C\uDFAE Game "${project.displayName}" (${project.slug}) already exists`);
157917
+ if (project.timebackCourses && project.timebackCourses.length > 0) {
157918
+ await seedTimebackIntegrations(db, existingGame.id, project.timebackCourses);
157919
+ }
157876
157920
  return existingGame;
157877
157921
  }
157878
157922
  const gameRecord = {
@@ -157892,6 +157936,12 @@ async function seedCurrentProjectGame(db, project) {
157892
157936
  updatedAt: now2
157893
157937
  };
157894
157938
  const [newGame] = await db.insert(games).values(gameRecord).returning();
157939
+ if (!newGame) {
157940
+ throw new Error("Failed to create game record");
157941
+ }
157942
+ if (project.timebackCourses && project.timebackCourses.length > 0) {
157943
+ await seedTimebackIntegrations(db, newGame.id, project.timebackCourses);
157944
+ }
157895
157945
  return newGame;
157896
157946
  } catch (error2) {
157897
157947
  console.error("❌ Error seeding project game:", error2);
@@ -157953,18 +158003,6 @@ async function seedSpriteTemplates(db) {
157953
158003
  }
157954
158004
  }
157955
158005
 
157956
- // src/database/seed/timeback.ts
157957
- function resolveStudentId(studentId) {
157958
- if (!studentId)
157959
- return null;
157960
- if (studentId === "mock")
157961
- return `mock-student-${crypto.randomUUID().slice(0, 8)}`;
157962
- return studentId;
157963
- }
157964
- function getAdminTimebackId() {
157965
- return resolveStudentId(config.timeback.studentId);
157966
- }
157967
-
157968
158006
  // src/database/seed/index.ts
157969
158007
  async function seedDemoData(db) {
157970
158008
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/sandbox",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Local development server for Playcademy game development",
5
5
  "type": "module",
6
6
  "exports": {