playcademy 0.14.19 → 0.14.21

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.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Sample API route with database
3
3
  *
4
- * This route will be available at: https://<your-game-slug>.playcademy.gg/api/hello
4
+ * This route will be available at: https://<your-project-slug>.playcademy.gg/api/hello
5
5
  */
6
6
 
7
7
  import { desc } from 'drizzle-orm'
@@ -3,7 +3,7 @@
3
3
  * Calling your backend from your frontend:
4
4
  * ─────────────────────────────────────────────────────────────────
5
5
  *
6
- * In your game's frontend, use the Playcademy SDK client to call
6
+ * In your project's frontend, use the Playcademy SDK client to call
7
7
  * your custom backend routes:
8
8
  *
9
9
  * ```typescript
@@ -32,7 +32,7 @@
32
32
  /**
33
33
  * Sample API route
34
34
  *
35
- * This route will be available at: https://<your-game-slug>.playcademy.gg/api/hello
35
+ * This route will be available at: https://<your-project-slug>.playcademy.gg/api/hello
36
36
  */
37
37
 
38
38
  /**
@@ -40,7 +40,7 @@
40
40
  */
41
41
  export async function GET(c: Context): Promise<Response> {
42
42
  return c.json({
43
- message: 'Hello from your game backend!',
43
+ message: 'Hello from your project backend!',
44
44
  timestamp: new Date().toISOString(),
45
45
  })
46
46
  }
@@ -59,8 +59,8 @@ export async function POST(c: Context): Promise<Response> {
59
59
 
60
60
  /**
61
61
  * Environment variables available via c.env:
62
- * - c.env.PLAYCADEMY_API_KEY - Game-scoped API key for calling Playcademy APIs
63
- * - c.env.GAME_ID - Your game's unique ID
62
+ * - c.env.PLAYCADEMY_API_KEY - Project-scoped API key for calling Playcademy APIs
63
+ * - c.env.GAME_ID - Your project's unique ID
64
64
  * - c.env.PLAYCADEMY_BASE_URL - Playcademy platform URL
65
65
  *
66
66
  * Access the SDK client:
@@ -15,7 +15,7 @@ export const user = sqliteTable('user', {
15
15
  updatedAt: integer('updatedAt', { mode: 'timestamp' }).notNull(),
16
16
 
17
17
  // Platform linkage: Links this user to a Playcademy platform identity
18
- // When users launch the game from Playcademy, their platform identity
18
+ // When users launch your project from Playcademy, their platform identity
19
19
  // is verified and linked to a Better Auth session via this field
20
20
  playcademyUserId: text('playcademy_user_id').unique(),
21
21
  })
@@ -27,8 +27,8 @@ export function getAuth(c: Context) {
27
27
  const secret = getAuthSecret(c)
28
28
 
29
29
  // CUSTOMIZABLE: Configure trusted origins for CORS
30
- // These origins are allowed to make cross-origin requests to your game's auth endpoints.
31
- // Add your deployed game URLs here when you deploy to staging or production.
30
+ // These origins are allowed to make cross-origin requests to your project's auth endpoints.
31
+ // Add your deployed project URLs here when you deploy to staging or production.
32
32
  const trustedOrigins = [
33
33
  'http://localhost:5173', // Local development
34
34
  // 'https://{{GAME_SLUG}}-staging.playcademy.gg', // Staging deployment
@@ -2,7 +2,7 @@
2
2
  * Example Schema
3
3
  *
4
4
  * Define your database tables here using Drizzle ORM.
5
- * This is a starter example - customize for your game's needs.
5
+ * This is a starter example - customize for your project's needs.
6
6
  */
7
7
 
8
8
  import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
package/dist/utils.d.ts CHANGED
@@ -1,8 +1,23 @@
1
- import { OrganizationConfig, CourseConfig, ComponentConfig, ResourceConfig, ComponentResourceConfig } from '@playcademy/timeback/types';
1
+ import { CourseConfig, OrganizationConfig, ComponentConfig, ResourceConfig, ComponentResourceConfig } from '@playcademy/timeback/types';
2
2
  import { Miniflare } from 'miniflare';
3
3
  import * as chokidar from 'chokidar';
4
+ import { PackageManager } from '@playcademy/utils/package-manager';
4
5
  import * as esbuild from 'esbuild';
5
6
 
7
+ /**
8
+ * Minimal course configuration for TimeBack integration (used in user-facing config).
9
+ *
10
+ * NOTE: Per-course overrides (title, courseCode, level, metadata) are defined
11
+ * in @playcademy/sdk/server as TimebackCourseConfigWithOverrides.
12
+ * This base type only includes the minimal required fields.
13
+ *
14
+ * For totalXp, use metadata.metrics.totalXp (aligns with upstream TimeBack structure).
15
+ */
16
+ type TimebackCourseConfig = {
17
+ subject: string;
18
+ grade: number;
19
+ };
20
+
6
21
  /**
7
22
  * @fileoverview Server SDK Type Definitions
8
23
  *
@@ -11,20 +26,50 @@ import * as esbuild from 'esbuild';
11
26
  */
12
27
 
13
28
  /**
14
- * TimeBack integration configuration for Playcademy config file
29
+ * Base configuration for TimeBack integration (shared across all courses).
30
+ * References upstream TimeBack types from @playcademy/timeback.
31
+ *
32
+ * All fields are optional and support template variables: {grade}, {subject}, {gameSlug}
15
33
  */
16
- interface TimebackIntegrationConfig {
17
- /** Organization overrides */
34
+ interface TimebackBaseConfig {
35
+ /** Organization configuration (shared across all courses) */
18
36
  organization?: Partial<OrganizationConfig>;
19
- /** Course configuration (subjects and grades REQUIRED) */
20
- course: CourseConfig;
21
- /** Component overrides */
37
+ /** Course defaults (can be overridden per-course) */
38
+ course?: Partial<CourseConfig>;
39
+ /** Component defaults */
22
40
  component?: Partial<ComponentConfig>;
23
- /** Resource overrides */
41
+ /** Resource defaults */
24
42
  resource?: Partial<ResourceConfig>;
25
- /** Component-Resource link overrides */
43
+ /** ComponentResource defaults */
26
44
  componentResource?: Partial<ComponentResourceConfig>;
27
45
  }
46
+ /**
47
+ * Extended course configuration that merges TimebackCourseConfig with per-course overrides.
48
+ * Used in playcademy.config.* to allow per-course customization.
49
+ */
50
+ interface TimebackCourseConfigWithOverrides extends TimebackCourseConfig {
51
+ title?: string;
52
+ courseCode?: string;
53
+ level?: string;
54
+ metadata?: CourseConfig['metadata'];
55
+ totalXp?: number | null;
56
+ masterableUnits?: number | null;
57
+ }
58
+ /**
59
+ * TimeBack integration configuration for Playcademy config file.
60
+ *
61
+ * Supports two levels of customization:
62
+ * 1. `base`: Shared defaults for all courses (organization, course, component, resource, componentResource)
63
+ * 2. Per-course overrides in the `courses` array (title, courseCode, level, gradingScheme, metadata)
64
+ *
65
+ * Template variables ({grade}, {subject}, {gameSlug}) can be used in string fields.
66
+ */
67
+ interface TimebackIntegrationConfig {
68
+ /** Multi-grade course configuration (array of grade/subject/totalXp with optional per-course overrides) */
69
+ courses: TimebackCourseConfigWithOverrides[];
70
+ /** Optional base configuration (shared across all courses, can be overridden per-course) */
71
+ base?: TimebackBaseConfig;
72
+ }
28
73
  /**
29
74
  * Custom API routes integration
30
75
  */
@@ -45,7 +90,7 @@ interface DatabaseIntegration {
45
90
  */
46
91
  interface IntegrationsConfig {
47
92
  /** TimeBack integration (optional) */
48
- timeback?: TimebackIntegrationConfig;
93
+ timeback?: TimebackIntegrationConfig | null;
49
94
  /** Custom API routes (optional) */
50
95
  customRoutes?: CustomRoutesIntegration | boolean;
51
96
  /** Database (optional) */
@@ -116,6 +161,14 @@ declare function findConfigPath(configPath?: string): Promise<string>;
116
161
  * Load Playcademy configuration from file
117
162
  */
118
163
  declare function loadConfig(configPath?: string): Promise<PlaycademyConfig>;
164
+ /**
165
+ * Load Playcademy configuration and set the CLI workspace to the config's directory
166
+ * This ensures that all relative paths (like server/) are resolved from the config location.
167
+ *
168
+ * Use this in CLI commands that need workspace-relative paths to work correctly
169
+ * when the user runs commands from a subdirectory (e.g., client/).
170
+ */
171
+ declare function loadConfigAndSetWorkspace(configPath?: string): Promise<PlaycademyConfig>;
119
172
  /**
120
173
  * Validate configuration structure
121
174
  */
@@ -147,6 +200,26 @@ interface HotReloadOptions {
147
200
  */
148
201
  declare function startHotReload(onReload: () => Promise<void>, options?: HotReloadOptions): chokidar.FSWatcher;
149
202
 
203
+ /**
204
+ * Configuration utilities for environment variables and API endpoints
205
+ */
206
+ type Environment = 'staging' | 'production';
207
+
208
+ /**
209
+ * Global CLI context for storing options that apply to all commands
210
+ */
211
+
212
+ interface CliContext {
213
+ profile?: string;
214
+ workspace?: string;
215
+ environment?: Environment;
216
+ packageManager?: PackageManager;
217
+ }
218
+ /**
219
+ * Set the CLI context (called from the main program after parsing global options)
220
+ */
221
+ declare function setCliContext(ctx: CliContext): void;
222
+
150
223
  /**
151
224
  * Import utilities for Node-safe TypeScript file loading
152
225
  */
@@ -187,4 +260,4 @@ declare function importTypescriptFile(filePath: string, bundleOptions?: Partial<
187
260
  */
188
261
  declare function importTypescriptDefault(filePath: string, bundleOptions?: Partial<esbuild.BuildOptions>): Promise<unknown>;
189
262
 
190
- export { findConfigPath as findPlaycademyConfigPath, importTypescriptDefault, importTypescriptFile, loadConfig as loadPlaycademyConfig, startDevServer as startPlaycademyDevServer, startHotReload as startPlaycademyHotReload, validateConfig as validatePlaycademyConfig };
263
+ export { findConfigPath as findPlaycademyConfigPath, importTypescriptDefault, importTypescriptFile, loadConfig as loadPlaycademyConfig, loadConfigAndSetWorkspace as loadPlaycademyConfigAndSetWorkspace, setCliContext as setPlaycademyCliContext, startDevServer as startPlaycademyDevServer, startHotReload as startPlaycademyHotReload, validateConfig as validatePlaycademyConfig };
package/dist/utils.js CHANGED
@@ -1733,7 +1733,6 @@ var package_default = {
1733
1733
  sharp: "^0.34.2",
1734
1734
  typedoc: "^0.28.5",
1735
1735
  "typedoc-plugin-markdown": "^4.7.0",
1736
- "typedoc-vitepress-theme": "^1.1.2",
1737
1736
  "typescript-eslint": "^8.30.1",
1738
1737
  "yocto-spinner": "^0.2.2"
1739
1738
  },
@@ -1899,7 +1898,7 @@ var CLI_DEFAULT_OUTPUTS = {
1899
1898
  var DEFAULT_PORTS = {
1900
1899
  /** Sandbox server (mock platform API) */
1901
1900
  SANDBOX: 4321,
1902
- /** Backend dev server (game backend with HMR) */
1901
+ /** Backend dev server (project backend with HMR) */
1903
1902
  BACKEND: 8788
1904
1903
  };
1905
1904
 
@@ -1945,6 +1944,68 @@ var CORE_GAME_UUIDS = {
1945
1944
  PLAYGROUND: "00000000-0000-0000-0000-000000000001"
1946
1945
  };
1947
1946
 
1947
+ // ../utils/src/package-manager.ts
1948
+ import { execSync } from "child_process";
1949
+ import { existsSync as existsSync2 } from "fs";
1950
+ import { join as join5 } from "path";
1951
+ function isCommandAvailable(command) {
1952
+ try {
1953
+ execSync(`command -v ${command}`, { stdio: "ignore" });
1954
+ return true;
1955
+ } catch {
1956
+ return false;
1957
+ }
1958
+ }
1959
+ function detectPackageManager(cwd = process.cwd()) {
1960
+ if (existsSync2(join5(cwd, "bun.lock")) || existsSync2(join5(cwd, "bun.lockb"))) {
1961
+ return "bun";
1962
+ }
1963
+ if (existsSync2(join5(cwd, "pnpm-lock.yaml"))) {
1964
+ return "pnpm";
1965
+ }
1966
+ if (existsSync2(join5(cwd, "yarn.lock"))) {
1967
+ return "yarn";
1968
+ }
1969
+ if (existsSync2(join5(cwd, "package-lock.json"))) {
1970
+ return "npm";
1971
+ }
1972
+ return detectByCommandAvailability();
1973
+ }
1974
+ function detectByCommandAvailability() {
1975
+ if (isCommandAvailable("bun")) {
1976
+ return "bun";
1977
+ }
1978
+ if (isCommandAvailable("pnpm")) {
1979
+ return "pnpm";
1980
+ }
1981
+ if (isCommandAvailable("yarn")) {
1982
+ return "yarn";
1983
+ }
1984
+ return "npm";
1985
+ }
1986
+ function getInstallCommand(pm) {
1987
+ switch (pm) {
1988
+ case "bun":
1989
+ return "bun install";
1990
+ case "pnpm":
1991
+ return "pnpm install";
1992
+ case "yarn":
1993
+ return "yarn install";
1994
+ case "npm":
1995
+ default:
1996
+ return "npm install";
1997
+ }
1998
+ }
1999
+
2000
+ // src/lib/core/context.ts
2001
+ var context = {};
2002
+ function setCliContext(ctx) {
2003
+ context = { ...context, ...ctx };
2004
+ }
2005
+ function getWorkspace() {
2006
+ return context.workspace || process.cwd();
2007
+ }
2008
+
1948
2009
  // src/lib/core/logger.ts
1949
2010
  import {
1950
2011
  blue,
@@ -2549,7 +2610,7 @@ import colors2 from "yoctocolors-cjs";
2549
2610
 
2550
2611
  // src/lib/core/error.ts
2551
2612
  import { bold, dim, redBright } from "colorette";
2552
- import { ApiError, extractApiErrorInfo } from "@playcademy/sdk";
2613
+ import { ApiError, extractApiErrorInfo } from "@playcademy/sdk/internal";
2553
2614
  function isConfigError(error) {
2554
2615
  return error !== null && typeof error === "object" && "name" in error && error.name === "ConfigError" && "message" in error;
2555
2616
  }
@@ -2661,12 +2722,20 @@ function customTransform(text) {
2661
2722
  return result;
2662
2723
  }
2663
2724
  function formatTable(data, title) {
2725
+ const ANSI_REGEX = /\u001B\[[0-9;]*m/g;
2726
+ const stripAnsi2 = (value) => value.replace(ANSI_REGEX, "");
2727
+ const visibleLength = (value) => stripAnsi2(value).length;
2728
+ const padCell = (value, width) => {
2729
+ const length = visibleLength(value);
2730
+ if (length >= width) return value;
2731
+ return value + " ".repeat(width - length);
2732
+ };
2664
2733
  if (data.length === 0) return;
2665
2734
  const keys = Object.keys(data[0]);
2666
2735
  const rows = data.map((item) => keys.map((key) => String(item[key] ?? "")));
2667
2736
  const widths = keys.map((key, i) => {
2668
- const headerWidth = key.length;
2669
- const dataWidth = Math.max(...rows.map((row) => row[i].length));
2737
+ const headerWidth = visibleLength(key);
2738
+ const dataWidth = Math.max(...rows.map((row) => visibleLength(row[i])));
2670
2739
  return Math.max(headerWidth, dataWidth);
2671
2740
  });
2672
2741
  const totalWidth = widths.reduce((sum, w) => sum + w + 3, -1);
@@ -2682,11 +2751,11 @@ function formatTable(data, title) {
2682
2751
  console.log(titleRow);
2683
2752
  console.log(titleSeparator);
2684
2753
  }
2685
- const header = "\u2502 " + keys.map((key, i) => key.padEnd(widths[i])).join(" \u2502 ") + " \u2502";
2754
+ const header = "\u2502 " + keys.map((key, i) => padCell(key, widths[i])).join(" \u2502 ") + " \u2502";
2686
2755
  console.log(header);
2687
2756
  console.log(separator);
2688
2757
  rows.forEach((row) => {
2689
- const dataRow = "\u2502 " + row.map((cell, i) => cell.padEnd(widths[i])).join(" \u2502 ") + " \u2502";
2758
+ const dataRow = "\u2502 " + row.map((cell, i) => padCell(cell, widths[i])).join(" \u2502 ") + " \u2502";
2690
2759
  console.log(dataRow);
2691
2760
  });
2692
2761
  console.log(bottomBorder);
@@ -2902,7 +2971,7 @@ async function findConfigPath(configPath) {
2902
2971
  }
2903
2972
  return result2.path;
2904
2973
  }
2905
- const result = await findFile2(CONFIG_FILE_NAMES);
2974
+ const result = await findFile2(CONFIG_FILE_NAMES, { searchUp: true, maxLevels: 3 });
2906
2975
  if (!result) {
2907
2976
  throw new ConfigError(
2908
2977
  "No Playcademy config file found in this directory or any parent directory",
@@ -2920,6 +2989,10 @@ async function loadConfigFile(path2) {
2920
2989
  return module.default || module;
2921
2990
  }
2922
2991
  async function loadConfig(configPath) {
2992
+ const result = await loadConfigWithPath(configPath);
2993
+ return result.config;
2994
+ }
2995
+ async function loadConfigWithPath(configPath) {
2923
2996
  try {
2924
2997
  const actualPath = configPath ? resolve2(configPath) : await findConfigPath();
2925
2998
  const config = await loadConfigFile(actualPath);
@@ -2931,7 +3004,11 @@ async function loadConfig(configPath) {
2931
3004
  );
2932
3005
  }
2933
3006
  validateConfig(config);
2934
- return processConfigVariables(config);
3007
+ return {
3008
+ config: processConfigVariables(config),
3009
+ configPath: actualPath,
3010
+ configDir: dirname2(actualPath)
3011
+ };
2935
3012
  } catch (error) {
2936
3013
  if (error instanceof ConfigError) {
2937
3014
  throw error;
@@ -2944,6 +3021,11 @@ async function loadConfig(configPath) {
2944
3021
  );
2945
3022
  }
2946
3023
  }
3024
+ async function loadConfigAndSetWorkspace(configPath) {
3025
+ const result = await loadConfigWithPath(configPath);
3026
+ setCliContext({ workspace: result.configDir });
3027
+ return result.config;
3028
+ }
2947
3029
  function validateConfig(config) {
2948
3030
  if (!config || typeof config !== "object") {
2949
3031
  throw new ConfigError("Configuration must be an object");
@@ -2974,61 +3056,114 @@ function validateConfig(config) {
2974
3056
  );
2975
3057
  }
2976
3058
  const tb = integrations.timeback;
2977
- if (!tb.course) {
2978
- throw new ConfigError(
2979
- 'TimeBack integration requires a "course" configuration',
2980
- "integrations.timeback.course",
2981
- 'Add timeback.course with subjects and grades: timeback: { course: { subjects: ["Math"], grades: [3, 4, 5] } }'
2982
- );
2983
- }
2984
- if (typeof tb.course !== "object") {
3059
+ if (!tb.courses) {
2985
3060
  throw new ConfigError(
2986
- 'The "integrations.timeback.course" field must be an object',
2987
- "integrations.timeback.course",
2988
- 'Change to an object: course: { subjects: ["Math"], grades: [3, 4, 5] }'
3061
+ 'TimeBack integration requires a "courses" array',
3062
+ "integrations.timeback.courses",
3063
+ 'Add timeback.courses array: timeback: { courses: [{ subject: "Math", grade: 3 }] }'
2989
3064
  );
2990
3065
  }
2991
- const course = tb.course;
2992
- if (!course.subjects || !Array.isArray(course.subjects) || course.subjects.length === 0) {
3066
+ if (!Array.isArray(tb.courses)) {
2993
3067
  throw new ConfigError(
2994
- "TimeBack course requires at least one subject",
2995
- "integrations.timeback.course.subjects",
2996
- 'Add subjects array: subjects: ["Math"], or subjects: ["Reading", "Writing"]'
3068
+ 'The "integrations.timeback.courses" field must be an array',
3069
+ "integrations.timeback.courses",
3070
+ 'Change to an array: courses: [{ subject: "Math", grade: 3 }]'
2997
3071
  );
2998
3072
  }
2999
- if (!course.grades || !Array.isArray(course.grades) || course.grades.length === 0) {
3073
+ if (tb.courses.length === 0) {
3000
3074
  throw new ConfigError(
3001
- "TimeBack course requires at least one grade level",
3002
- "integrations.timeback.course.grades",
3003
- "Add grades array: grades: [3, 4, 5], or grades: [9, 10, 11, 12]"
3075
+ "TimeBack courses array cannot be empty",
3076
+ "integrations.timeback.courses",
3077
+ 'Add at least one course: courses: [{ subject: "Math", grade: 3 }]'
3004
3078
  );
3005
3079
  }
3006
- if (tb.organization) {
3007
- if (typeof tb.organization !== "object") {
3080
+ tb.courses.forEach((course, idx) => {
3081
+ if (typeof course !== "object" || course === null) {
3008
3082
  throw new ConfigError(
3009
- 'The "integrations.timeback.organization" field must be an object',
3010
- "integrations.timeback.organization",
3011
- 'Change to an object: organization: { name: "My School", type: "school" }'
3083
+ `Course at index ${idx} must be an object`,
3084
+ `integrations.timeback.courses[${idx}]`,
3085
+ 'Each course must have subject and grade: { subject: "Math", grade: 3 }'
3012
3086
  );
3013
3087
  }
3014
- const org = tb.organization;
3015
- if (org.type) {
3016
- const validOrgTypes = [
3017
- "department",
3018
- "school",
3019
- "district",
3020
- "local",
3021
- "state",
3022
- "national"
3023
- ];
3024
- if (!validOrgTypes.includes(org.type)) {
3088
+ const courseObj = course;
3089
+ if (typeof courseObj.subject !== "string") {
3090
+ throw new ConfigError(
3091
+ `Course at index ${idx} is missing required "subject" field`,
3092
+ `integrations.timeback.courses[${idx}].subject`,
3093
+ 'Add subject string: { subject: "Math", grade: 3 }'
3094
+ );
3095
+ }
3096
+ if (typeof courseObj.grade !== "number") {
3097
+ throw new ConfigError(
3098
+ `Course at index ${idx} is missing required "grade" field`,
3099
+ `integrations.timeback.courses[${idx}].grade`,
3100
+ 'Add grade number: { subject: "Math", grade: 3 }'
3101
+ );
3102
+ }
3103
+ const stringOverrides = ["title", "courseCode", "level", "gradingScheme"];
3104
+ stringOverrides.forEach((field) => {
3105
+ if (courseObj[field] !== void 0 && typeof courseObj[field] !== "string") {
3025
3106
  throw new ConfigError(
3026
- `Invalid organization type: "${org.type}"`,
3027
- "integrations.timeback.organization.type",
3028
- `Use one of: ${validOrgTypes.join(", ")}. Example: type: "school"`
3107
+ `Course at index ${idx} has invalid "${field}" field (must be a string)`,
3108
+ `integrations.timeback.courses[${idx}].${field}`,
3109
+ `${field} must be a string if provided`
3029
3110
  );
3030
3111
  }
3112
+ });
3113
+ if (courseObj.metadata !== void 0 && typeof courseObj.metadata !== "object") {
3114
+ throw new ConfigError(
3115
+ `Course at index ${idx} has invalid "metadata" field (must be an object)`,
3116
+ `integrations.timeback.courses[${idx}].metadata`,
3117
+ "metadata must be an object if provided"
3118
+ );
3119
+ }
3120
+ });
3121
+ if (tb.base) {
3122
+ if (typeof tb.base !== "object") {
3123
+ throw new ConfigError(
3124
+ 'The "integrations.timeback.base" field must be an object',
3125
+ "integrations.timeback.base",
3126
+ "Change to an object: base: { organization: { ... }, course: { ... } }"
3127
+ );
3031
3128
  }
3129
+ const base = tb.base;
3130
+ if (base.organization) {
3131
+ if (typeof base.organization !== "object") {
3132
+ throw new ConfigError(
3133
+ 'The "integrations.timeback.base.organization" field must be an object',
3134
+ "integrations.timeback.base.organization",
3135
+ 'Change to an object: organization: { name: "My School", type: "school" }'
3136
+ );
3137
+ }
3138
+ const org = base.organization;
3139
+ if (org.type) {
3140
+ const validOrgTypes = [
3141
+ "department",
3142
+ "school",
3143
+ "district",
3144
+ "local",
3145
+ "state",
3146
+ "national"
3147
+ ];
3148
+ if (!validOrgTypes.includes(org.type)) {
3149
+ throw new ConfigError(
3150
+ `Invalid organization type: "${org.type}"`,
3151
+ "integrations.timeback.base.organization.type",
3152
+ `Use one of: ${validOrgTypes.join(", ")}. Example: type: "school"`
3153
+ );
3154
+ }
3155
+ }
3156
+ }
3157
+ const baseSections = ["course", "component", "resource", "componentResource"];
3158
+ baseSections.forEach((section) => {
3159
+ if (base[section] !== void 0 && typeof base[section] !== "object") {
3160
+ throw new ConfigError(
3161
+ `The "integrations.timeback.base.${section}" field must be an object`,
3162
+ `integrations.timeback.base.${section}`,
3163
+ `Change to an object: ${section}: { ... }`
3164
+ );
3165
+ }
3166
+ });
3032
3167
  }
3033
3168
  }
3034
3169
  }
@@ -3061,10 +3196,10 @@ import { join as join15 } from "path";
3061
3196
  import { Log, LogLevel, Miniflare } from "miniflare";
3062
3197
 
3063
3198
  // ../utils/src/port.ts
3064
- import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3199
+ import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3065
3200
  import { createServer } from "node:net";
3066
3201
  import { homedir } from "node:os";
3067
- import { join as join5 } from "node:path";
3202
+ import { join as join6 } from "node:path";
3068
3203
  async function isPortAvailableOnHost(port, host) {
3069
3204
  return new Promise((resolve4) => {
3070
3205
  const server = createServer();
@@ -3103,15 +3238,15 @@ async function findAvailablePort(startPort = 4321) {
3103
3238
  }
3104
3239
  function getRegistryPath() {
3105
3240
  const home = homedir();
3106
- const dir = join5(home, ".playcademy");
3107
- if (!existsSync2(dir)) {
3241
+ const dir = join6(home, ".playcademy");
3242
+ if (!existsSync3(dir)) {
3108
3243
  mkdirSync(dir, { recursive: true });
3109
3244
  }
3110
- return join5(dir, ".proc");
3245
+ return join6(dir, ".proc");
3111
3246
  }
3112
3247
  function readRegistry() {
3113
3248
  const registryPath = getRegistryPath();
3114
- if (!existsSync2(registryPath)) {
3249
+ if (!existsSync3(registryPath)) {
3115
3250
  return {};
3116
3251
  }
3117
3252
  try {
@@ -3148,66 +3283,7 @@ function readServerInfo(type, projectRoot) {
3148
3283
  }
3149
3284
 
3150
3285
  // src/lib/core/client.ts
3151
- import { PlaycademyClient } from "@playcademy/sdk";
3152
-
3153
- // ../utils/src/package-manager.ts
3154
- import { execSync } from "child_process";
3155
- import { existsSync as existsSync3 } from "fs";
3156
- import { join as join6 } from "path";
3157
- function isCommandAvailable(command) {
3158
- try {
3159
- execSync(`command -v ${command}`, { stdio: "ignore" });
3160
- return true;
3161
- } catch {
3162
- return false;
3163
- }
3164
- }
3165
- function detectPackageManager(cwd = process.cwd()) {
3166
- if (existsSync3(join6(cwd, "bun.lock")) || existsSync3(join6(cwd, "bun.lockb"))) {
3167
- return "bun";
3168
- }
3169
- if (existsSync3(join6(cwd, "pnpm-lock.yaml"))) {
3170
- return "pnpm";
3171
- }
3172
- if (existsSync3(join6(cwd, "yarn.lock"))) {
3173
- return "yarn";
3174
- }
3175
- if (existsSync3(join6(cwd, "package-lock.json"))) {
3176
- return "npm";
3177
- }
3178
- return detectByCommandAvailability();
3179
- }
3180
- function detectByCommandAvailability() {
3181
- if (isCommandAvailable("bun")) {
3182
- return "bun";
3183
- }
3184
- if (isCommandAvailable("pnpm")) {
3185
- return "pnpm";
3186
- }
3187
- if (isCommandAvailable("yarn")) {
3188
- return "yarn";
3189
- }
3190
- return "npm";
3191
- }
3192
- function getInstallCommand(pm) {
3193
- switch (pm) {
3194
- case "bun":
3195
- return "bun install";
3196
- case "pnpm":
3197
- return "pnpm install";
3198
- case "yarn":
3199
- return "yarn install";
3200
- case "npm":
3201
- default:
3202
- return "npm install";
3203
- }
3204
- }
3205
-
3206
- // src/lib/core/context.ts
3207
- var context = {};
3208
- function getWorkspace() {
3209
- return context.workspace || process.cwd();
3210
- }
3286
+ import { PlaycademyClient } from "@playcademy/sdk/internal";
3211
3287
 
3212
3288
  // src/lib/core/errors.ts
3213
3289
  function getErrorMessage(error) {
@@ -3913,7 +3989,7 @@ import { join as join11 } from "path";
3913
3989
  // package.json
3914
3990
  var package_default2 = {
3915
3991
  name: "playcademy",
3916
- version: "0.14.18",
3992
+ version: "0.14.20",
3917
3993
  type: "module",
3918
3994
  exports: {
3919
3995
  ".": {
@@ -4295,7 +4371,7 @@ async function startDevServer(options) {
4295
4371
  const config = providedConfig ?? await loadConfig();
4296
4372
  await ensurePlaycademyTypes();
4297
4373
  const hasSandboxTimebackCreds = !!process.env.TIMEBACK_API_CLIENT_ID;
4298
- const devConfig = config.integrations?.timeback && !hasSandboxTimebackCreds ? { ...config, integrations: { ...config.integrations, timeback: void 0 } } : config;
4374
+ const devConfig = config.integrations?.timeback && !hasSandboxTimebackCreds ? { ...config, integrations: { ...config.integrations, timeback: null } } : config;
4299
4375
  const bundle = await bundleBackend(devConfig, {
4300
4376
  sourcemap: false,
4301
4377
  minify: false
@@ -4459,6 +4535,8 @@ export {
4459
4535
  importTypescriptDefault,
4460
4536
  importTypescriptFile,
4461
4537
  loadConfig as loadPlaycademyConfig,
4538
+ loadConfigAndSetWorkspace as loadPlaycademyConfigAndSetWorkspace,
4539
+ setCliContext as setPlaycademyCliContext,
4462
4540
  startDevServer as startPlaycademyDevServer,
4463
4541
  startHotReload as startPlaycademyHotReload,
4464
4542
  validateConfig as validatePlaycademyConfig