playcademy 0.14.19 → 0.14.20

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/README.md CHANGED
@@ -213,6 +213,26 @@ Credentials are stored in `~/.playcademy/auth.json`:
213
213
 
214
214
  Set up TimeBack LTI integration for your game.
215
215
 
216
+ **Note:** Before running this command, ensure that `totalXp` is configured for each course in your `playcademy.config.js`:
217
+
218
+ ```js
219
+ integrations: {
220
+ timeback: {
221
+ courses: [
222
+ {
223
+ subject: 'Math',
224
+ grade: 3,
225
+ metadata: {
226
+ metrics: {
227
+ totalXp: 1000, // Required
228
+ },
229
+ },
230
+ },
231
+ ]
232
+ }
233
+ }
234
+ ```
235
+
216
236
  ```bash
217
237
  playcademy timeback setup
218
238
  playcademy timeback setup --dry-run
@@ -278,7 +278,7 @@ declare const CLI_FILES: {
278
278
  declare const DEFAULT_PORTS: {
279
279
  /** Sandbox server (mock platform API) */
280
280
  readonly SANDBOX: 4321;
281
- /** Backend dev server (game backend with HMR) */
281
+ /** Backend dev server (project backend with HMR) */
282
282
  readonly BACKEND: 8788;
283
283
  };
284
284
 
package/dist/constants.js CHANGED
@@ -31,7 +31,6 @@ var package_default = {
31
31
  sharp: "^0.34.2",
32
32
  typedoc: "^0.28.5",
33
33
  "typedoc-plugin-markdown": "^4.7.0",
34
- "typedoc-vitepress-theme": "^1.1.2",
35
34
  "typescript-eslint": "^8.30.1",
36
35
  "yocto-spinner": "^0.2.2"
37
36
  },
@@ -255,7 +254,7 @@ var CLI_FILES = {
255
254
  var DEFAULT_PORTS = {
256
255
  /** Sandbox server (mock platform API) */
257
256
  SANDBOX: 4321,
258
- /** Backend dev server (game backend with HMR) */
257
+ /** Backend dev server (project backend with HMR) */
259
258
  BACKEND: 8788
260
259
  };
261
260
 
package/dist/db.js CHANGED
@@ -1458,7 +1458,6 @@ var package_default = {
1458
1458
  sharp: "^0.34.2",
1459
1459
  typedoc: "^0.28.5",
1460
1460
  "typedoc-plugin-markdown": "^4.7.0",
1461
- "typedoc-vitepress-theme": "^1.1.2",
1462
1461
  "typescript-eslint": "^8.30.1",
1463
1462
  "yocto-spinner": "^0.2.2"
1464
1463
  },
@@ -2017,7 +2016,7 @@ function getRunCommand(pm, script) {
2017
2016
  }
2018
2017
 
2019
2018
  // src/lib/core/client.ts
2020
- import { PlaycademyClient } from "@playcademy/sdk";
2019
+ import { PlaycademyClient } from "@playcademy/sdk/internal";
2021
2020
 
2022
2021
  // src/lib/core/context.ts
2023
2022
  var context = {};
@@ -2637,7 +2636,7 @@ import colors3 from "yoctocolors-cjs";
2637
2636
 
2638
2637
  // src/lib/core/error.ts
2639
2638
  import { bold as bold2, dim as dim2, redBright } from "colorette";
2640
- import { ApiError, extractApiErrorInfo } from "@playcademy/sdk";
2639
+ import { ApiError, extractApiErrorInfo } from "@playcademy/sdk/internal";
2641
2640
  function isConfigError(error) {
2642
2641
  return error !== null && typeof error === "object" && "name" in error && error.name === "ConfigError" && "message" in error;
2643
2642
  }
@@ -2749,12 +2748,20 @@ function customTransform(text) {
2749
2748
  return result;
2750
2749
  }
2751
2750
  function formatTable(data, title) {
2751
+ const ANSI_REGEX = /\u001B\[[0-9;]*m/g;
2752
+ const stripAnsi2 = (value) => value.replace(ANSI_REGEX, "");
2753
+ const visibleLength = (value) => stripAnsi2(value).length;
2754
+ const padCell = (value, width) => {
2755
+ const length = visibleLength(value);
2756
+ if (length >= width) return value;
2757
+ return value + " ".repeat(width - length);
2758
+ };
2752
2759
  if (data.length === 0) return;
2753
2760
  const keys = Object.keys(data[0]);
2754
2761
  const rows = data.map((item) => keys.map((key) => String(item[key] ?? "")));
2755
2762
  const widths = keys.map((key, i) => {
2756
- const headerWidth = key.length;
2757
- const dataWidth = Math.max(...rows.map((row) => row[i].length));
2763
+ const headerWidth = visibleLength(key);
2764
+ const dataWidth = Math.max(...rows.map((row) => visibleLength(row[i])));
2758
2765
  return Math.max(headerWidth, dataWidth);
2759
2766
  });
2760
2767
  const totalWidth = widths.reduce((sum, w) => sum + w + 3, -1);
@@ -2770,11 +2777,11 @@ function formatTable(data, title) {
2770
2777
  console.log(titleRow);
2771
2778
  console.log(titleSeparator);
2772
2779
  }
2773
- const header = "\u2502 " + keys.map((key, i) => key.padEnd(widths[i])).join(" \u2502 ") + " \u2502";
2780
+ const header = "\u2502 " + keys.map((key, i) => padCell(key, widths[i])).join(" \u2502 ") + " \u2502";
2774
2781
  console.log(header);
2775
2782
  console.log(separator);
2776
2783
  rows.forEach((row) => {
2777
- const dataRow = "\u2502 " + row.map((cell, i) => cell.padEnd(widths[i])).join(" \u2502 ") + " \u2502";
2784
+ const dataRow = "\u2502 " + row.map((cell, i) => padCell(cell, widths[i])).join(" \u2502 ") + " \u2502";
2778
2785
  console.log(dataRow);
2779
2786
  });
2780
2787
  console.log(bottomBorder);
@@ -33,6 +33,13 @@ export async function registerBuiltinRoutes(app: Hono<HonoEnv>, integrations?: I
33
33
  ])
34
34
  app.post(ROUTES.TIMEBACK.END_ACTIVITY, endActivity.POST)
35
35
  // ... other routes
36
+ } else if (integrations?.timeback === null) {
37
+ app.post('/api/integrations/timeback/end-activity', async c => {
38
+ return c.json({
39
+ status: 'ok',
40
+ __playcademyDevWarning: 'timeback-not-configured',
41
+ })
42
+ })
36
43
  }
37
44
 
38
45
  // TODO: Auth integration
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { Context } from 'hono'
8
- import type { PlaycademyClient, PlaycademyConfig } from '@playcademy/sdk/server'
8
+ import type { PlaycademyConfig } from '@playcademy/sdk/server'
9
9
  import type { RouteMetadata } from '../entry/types'
10
10
  import type { HonoEnv, ServerEnv } from '../types'
11
11
 
@@ -49,16 +49,27 @@ function formatRoutes(routes: RouteMetadata[]): Array<{ path: string; methods: s
49
49
  /**
50
50
  * Get TimeBack debug info if applicable
51
51
  */
52
- function getTimebackDebugInfo(config?: PlaycademyConfig, sdk?: PlaycademyClient) {
53
- if (config?.integrations?.timeback && sdk?.timeback?.courseId) {
54
- return { timeback: { courseId: sdk.timeback.courseId } }
52
+ function getTimebackDebugInfo(config?: PlaycademyConfig) {
53
+ const timeback = config?.integrations?.timeback
54
+
55
+ if (!timeback || !timeback.courses || timeback.courses.length === 0) {
56
+ return {}
57
+ }
58
+
59
+ const grades = Array.from(new Set(timeback.courses.map(c => c.grade))).sort()
60
+ const subjects = Array.from(new Set(timeback.courses.map(c => c.subject))).sort()
61
+
62
+ return {
63
+ timeback: {
64
+ courseCount: timeback.courses.length,
65
+ grades,
66
+ subjects,
67
+ },
55
68
  }
56
- return {}
57
69
  }
58
70
 
59
71
  export async function GET(c: Context<HonoEnv>): Promise<Response> {
60
72
  const config = c.get('config')
61
- const sdk = c.get('sdk')
62
73
  const routeMetadata = c.get('routeMetadata')
63
74
 
64
75
  return c.json({
@@ -68,6 +79,6 @@ export async function GET(c: Context<HonoEnv>): Promise<Response> {
68
79
  secrets: getSecretsCount(c.env),
69
80
  integrations: getEnabledIntegrations(config),
70
81
  routes: formatRoutes(routeMetadata),
71
- ...getTimebackDebugInfo(config, sdk),
82
+ ...getTimebackDebugInfo(config),
72
83
  })
73
84
  }
@@ -1,5 +1,3 @@
1
- import { verifyGameToken } from '@playcademy/sdk/server'
2
-
3
1
  import type { Context } from 'hono'
4
2
  import type { PlaycademyConfig } from '@playcademy/sdk/server'
5
3
  import type { ActivityData } from '@playcademy/timeback/types'
@@ -26,82 +24,148 @@ function getConfig(c: Context<HonoEnv>): PlaycademyConfig {
26
24
  return config
27
25
  }
28
26
 
29
- function enrichActivityData(
30
- activityData: ActivityData,
31
- config: PlaycademyConfig,
32
- c: Context<HonoEnv>,
33
- ): ActivityData {
27
+ function validateRequestBody(body: {
28
+ activityData?: ActivityData
29
+ scoreData?: { correctQuestions?: number; totalQuestions?: number }
30
+ timingData?: { durationSeconds?: number }
31
+ masteredUnits?: number
32
+ }): { error: string } | null {
33
+ if (!body.activityData?.activityId) {
34
+ return { error: 'activityId is required' }
35
+ }
36
+ if (!body.activityData?.grade) {
37
+ return { error: 'grade is required' }
38
+ }
39
+ if (!body.activityData?.subject) {
40
+ return { error: 'subject is required' }
41
+ }
42
+ if (
43
+ typeof body.scoreData?.correctQuestions !== 'number' ||
44
+ typeof body.scoreData?.totalQuestions !== 'number'
45
+ ) {
46
+ return { error: 'correctQuestions and totalQuestions are required' }
47
+ }
48
+ if (typeof body.timingData?.durationSeconds !== 'number') {
49
+ return { error: 'durationSeconds is required' }
50
+ }
51
+ if (
52
+ body.masteredUnits !== undefined &&
53
+ (typeof body.masteredUnits !== 'number' || body.masteredUnits < 0)
54
+ ) {
55
+ return { error: 'masteredUnits must be a non-negative number when provided' }
56
+ }
57
+ return null
58
+ }
59
+
60
+ function validateCourse(params: {
61
+ grade: number
62
+ subject: string
63
+ config: PlaycademyConfig
64
+ }): { error: string } | null {
65
+ const { grade, subject, config } = params
66
+ const timebackConfig = config.integrations?.timeback
67
+ const configuredCourse = timebackConfig?.courses?.find(
68
+ course => course.grade === grade && course.subject === subject,
69
+ )
70
+ if (!configuredCourse) {
71
+ const configured = timebackConfig?.courses
72
+ ?.map(c => `${c.subject} (Grade ${c.grade})`)
73
+ .join(', ')
74
+ return {
75
+ error: `Invalid grade/subject combination: ${subject} (Grade ${grade}). Configured courses: ${configured || 'none'}`,
76
+ }
77
+ }
78
+ return null
79
+ }
80
+
81
+ function enrichActivityData(params: {
82
+ activityData: ActivityData
83
+ config: PlaycademyConfig
84
+ c: Context<HonoEnv>
85
+ }): { data?: ActivityData; error?: string } {
86
+ const { activityData, config, c } = params
34
87
  const appName = activityData.appName || config?.name
35
- const subject =
36
- activityData.subject ||
37
- config?.integrations?.timeback?.course?.defaultSubject ||
38
- config?.integrations?.timeback?.course?.subjects?.[0]
39
88
  const sensorUrl = activityData.sensorUrl || new URL(c.req.url).origin
40
89
 
41
- if (!appName) throw new Error('App name is required')
42
- if (!subject) throw new Error('Subject is required')
43
- if (!sensorUrl) throw new Error('Sensor URL is required')
90
+ if (!appName) {
91
+ return { error: 'App name is required (missing from activityData and config)' }
92
+ }
93
+ if (!sensorUrl) {
94
+ return { error: 'Sensor URL is required' }
95
+ }
44
96
 
45
- return { ...activityData, appName, subject, sensorUrl }
97
+ return { data: { ...activityData, appName, sensorUrl } }
46
98
  }
47
99
 
48
100
  export async function POST(c: Context<HonoEnv>): Promise<Response> {
49
101
  try {
50
- // 1. Verify game token from frontend (calls platform API for verification)
51
- const token = c.req.header('Authorization')?.replace('Bearer ', '')
52
- if (!token) {
53
- return c.json({ error: 'Unauthorized' }, 401)
54
- }
55
-
56
- const { user } = await verifyGameToken(token, { baseUrl: c.env.PLAYCADEMY_BASE_URL })
102
+ // 1. Get authenticated user from middleware
103
+ const user = c.get('playcademyUser')
104
+ if (!user) return c.json({ error: 'Unauthorized' }, 401)
57
105
 
58
106
  // 2. Ensure user has TimeBack integration
59
107
  if (!user.timeback_id) {
60
- return c.json({ error: 'User does not have TimeBack integration' }, 400)
108
+ const message = 'User does not have TimeBack integration'
109
+ console.error('[TimeBack End Activity] Error:', message)
110
+ return c.json({ error: message }, 400)
61
111
  }
62
112
 
63
113
  // 3. Parse request body
64
- const { activityData, scoreData, timingData, xpEarned } = await c.req.json()
114
+ const { activityData, scoreData, timingData, xpEarned, masteredUnits } = await c.req.json()
65
115
 
66
116
  // 4. Validate required fields
67
- if (!activityData?.activityId) {
68
- return c.json({ error: 'activityId is required' }, 400)
69
- }
70
- if (
71
- typeof scoreData?.correctQuestions !== 'number' ||
72
- typeof scoreData?.totalQuestions !== 'number'
73
- ) {
74
- return c.json({ error: 'correctQuestions and totalQuestions are required' }, 400)
75
- }
76
- if (typeof timingData?.durationSeconds !== 'number') {
77
- return c.json({ error: 'durationSeconds is required' }, 400)
117
+ const bodyValidationError = validateRequestBody({
118
+ activityData,
119
+ scoreData,
120
+ timingData,
121
+ masteredUnits,
122
+ })
123
+
124
+ if (bodyValidationError) {
125
+ const message = bodyValidationError.error
126
+ console.error('[TimeBack End Activity] Error:', message)
127
+ return c.json({ error: message }, 400)
78
128
  }
79
129
 
80
- // 5. Get config and enrich activity data with required Caliper fields
130
+ // 5. Get config
81
131
  const config = getConfig(c)
82
- const enrichedActivityData = enrichActivityData(activityData, config, c)
83
132
 
84
- // 6. Get SDK client from context (initialized once per Worker, reused across requests)
133
+ // 6. Validate grade/subject against configured courses
134
+ const { grade, subject } = activityData
135
+ const courseValidationError = validateCourse({ grade, subject, config })
136
+
137
+ if (courseValidationError) {
138
+ const message = courseValidationError.error
139
+ console.error('[TimeBack End Activity] Error:', message)
140
+ return c.json({ error: message }, 400)
141
+ }
142
+
143
+ // 7. Enrich activity data with required Caliper fields
144
+ const enrichResult = enrichActivityData({ activityData, config, c })
145
+
146
+ if (!enrichResult.data) {
147
+ console.error('[TimeBack End Activity] Error:', enrichResult.error)
148
+ return c.json({ error: enrichResult.error }, 500)
149
+ }
150
+
151
+ // 8. Get SDK client from context (initialized once per Worker, reused across requests)
85
152
  const sdk = c.get('sdk')
86
153
 
87
- // 7. End activity (SDK calculates XP server-side with attempt tracking)
154
+ // 9. End activity (SDK calculates XP server-side with attempt tracking)
88
155
  const result = await sdk.timeback.endActivity(user.timeback_id, {
89
- activityData: enrichedActivityData,
156
+ activityData: enrichResult.data,
90
157
  scoreData,
91
158
  timingData,
92
159
  xpEarned,
160
+ masteredUnits,
93
161
  })
94
162
 
95
163
  return c.json(result)
96
164
  } catch (error) {
97
- console.error('[TimeBack End Activity] Error:', error)
98
- return c.json(
99
- {
100
- error: 'Failed to end activity',
101
- message: error instanceof Error ? error.message : String(error),
102
- stack: error instanceof Error ? error.stack : undefined,
103
- },
104
- 500,
105
- )
165
+ const message = error instanceof Error ? error.message : String(error)
166
+ const stack = error instanceof Error ? error.stack : undefined
167
+ if (message) console.error('[TimeBack End Activity] Error:', message)
168
+ if (stack) console.error('[TimeBack End Activity] Stack:', stack)
169
+ return c.json({ error: 'Failed to end activity', message, stack }, 500)
106
170
  }
107
171
  }
@@ -7,6 +7,7 @@
7
7
 
8
8
  /// <reference types="@cloudflare/workers-types" />
9
9
 
10
+ import type { UserInfo } from '@playcademy/data/types'
10
11
  import type { PlaycademyClient, PlaycademyConfig } from '@playcademy/sdk/server'
11
12
  import type { RouteMetadata } from './entry/types'
12
13
 
@@ -70,6 +71,7 @@ export interface HonoVariables {
70
71
  sdk: PlaycademyClient
71
72
  config: PlaycademyConfig
72
73
  routeMetadata: Array<RouteMetadata>
74
+ playcademyUser?: UserInfo
73
75
  [key: string]: unknown
74
76
  }
75
77
 
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { UserInfo, ApiKey } from '@playcademy/data/types';
2
+ export { TimebackCourseConfig } from '@playcademy/data/types';
2
3
  import { SchemaInfo } from '@playcademy/cloudflare';
3
- import { OrganizationConfig, CourseConfig, ComponentConfig, ResourceConfig, ComponentResourceConfig } from '@playcademy/timeback/types';
4
- export { ComponentConfig, ComponentResourceConfig, CourseConfig, DerivedComponentConfig, DerivedComponentResourceConfig, DerivedCourseConfig, DerivedOrganizationConfig, DerivedResourceConfig, DerivedTimebackConfig, OrganizationConfig, ResourceConfig, TimebackGrade, TimebackSourcedIds, TimebackSubject } from '@playcademy/timeback/types';
5
- import { PlaycademyClient } from '@playcademy/sdk';
4
+ import { CourseConfig, OrganizationConfig, ComponentConfig, ResourceConfig, ComponentResourceConfig } from '@playcademy/timeback/types';
5
+ export { ComponentConfig, ComponentResourceConfig, OrganizationConfig, ResourceConfig, TimebackGrade, TimebackSourcedIds, TimebackSubject } from '@playcademy/timeback/types';
6
+ import { PlaycademyClient } from '@playcademy/sdk/internal';
6
7
  import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
7
8
 
8
9
  /**
@@ -243,6 +244,20 @@ interface BulkCollectionResult {
243
244
  totalSize: number;
244
245
  }
245
246
 
247
+ /**
248
+ * Minimal course configuration for TimeBack integration (used in user-facing config).
249
+ *
250
+ * NOTE: Per-course overrides (title, courseCode, level, metadata) are defined
251
+ * in @playcademy/sdk/server as TimebackCourseConfigWithOverrides.
252
+ * This base type only includes the minimal required fields.
253
+ *
254
+ * For totalXp, use metadata.metrics.totalXp (aligns with upstream TimeBack structure).
255
+ */
256
+ type TimebackCourseConfig = {
257
+ subject: string;
258
+ grade: number;
259
+ };
260
+
246
261
  /**
247
262
  * @fileoverview Server SDK Type Definitions
248
263
  *
@@ -251,20 +266,50 @@ interface BulkCollectionResult {
251
266
  */
252
267
 
253
268
  /**
254
- * TimeBack integration configuration for Playcademy config file
269
+ * Base configuration for TimeBack integration (shared across all courses).
270
+ * References upstream TimeBack types from @playcademy/timeback.
271
+ *
272
+ * All fields are optional and support template variables: {grade}, {subject}, {gameSlug}
255
273
  */
256
- interface TimebackIntegrationConfig {
257
- /** Organization overrides */
274
+ interface TimebackBaseConfig {
275
+ /** Organization configuration (shared across all courses) */
258
276
  organization?: Partial<OrganizationConfig>;
259
- /** Course configuration (subjects and grades REQUIRED) */
260
- course: CourseConfig;
261
- /** Component overrides */
277
+ /** Course defaults (can be overridden per-course) */
278
+ course?: Partial<CourseConfig>;
279
+ /** Component defaults */
262
280
  component?: Partial<ComponentConfig>;
263
- /** Resource overrides */
281
+ /** Resource defaults */
264
282
  resource?: Partial<ResourceConfig>;
265
- /** Component-Resource link overrides */
283
+ /** ComponentResource defaults */
266
284
  componentResource?: Partial<ComponentResourceConfig>;
267
285
  }
286
+ /**
287
+ * Extended course configuration that merges TimebackCourseConfig with per-course overrides.
288
+ * Used in playcademy.config.* to allow per-course customization.
289
+ */
290
+ interface TimebackCourseConfigWithOverrides extends TimebackCourseConfig {
291
+ title?: string;
292
+ courseCode?: string;
293
+ level?: string;
294
+ metadata?: CourseConfig['metadata'];
295
+ totalXp?: number | null;
296
+ masterableUnits?: number | null;
297
+ }
298
+ /**
299
+ * TimeBack integration configuration for Playcademy config file.
300
+ *
301
+ * Supports two levels of customization:
302
+ * 1. `base`: Shared defaults for all courses (organization, course, component, resource, componentResource)
303
+ * 2. Per-course overrides in the `courses` array (title, courseCode, level, gradingScheme, metadata)
304
+ *
305
+ * Template variables ({grade}, {subject}, {gameSlug}) can be used in string fields.
306
+ */
307
+ interface TimebackIntegrationConfig {
308
+ /** Multi-grade course configuration (array of grade/subject/totalXp with optional per-course overrides) */
309
+ courses: TimebackCourseConfigWithOverrides[];
310
+ /** Optional base configuration (shared across all courses, can be overridden per-course) */
311
+ base?: TimebackBaseConfig;
312
+ }
268
313
  /**
269
314
  * Custom API routes integration
270
315
  */
@@ -285,7 +330,7 @@ interface DatabaseIntegration {
285
330
  */
286
331
  interface IntegrationsConfig {
287
332
  /** TimeBack integration (optional) */
288
- timeback?: TimebackIntegrationConfig;
333
+ timeback?: TimebackIntegrationConfig | null;
289
334
  /** Custom API routes (optional) */
290
335
  customRoutes?: CustomRoutesIntegration | boolean;
291
336
  /** Database (optional) */
@@ -653,7 +698,7 @@ interface IntegrationChangeDetector<TConfig = unknown> {
653
698
  * Called automatically during deployment when metadata has changed
654
699
  * Implementation should: fetch current config, detect changes, update if needed
655
700
  *
656
- * @param gameId - The game ID
701
+ * @param gameId - The project ID
657
702
  * @param client - Playcademy API client
658
703
  * @param localConfig - Full local PlaycademyConfig
659
704
  * @param verbose - Verbose logging flag
@@ -801,17 +846,17 @@ interface DeploymentResult {
801
846
  /**
802
847
  * Deployment configuration
803
848
  *
804
- * Configuration for deploying a game to Playcademy.
849
+ * Configuration for deploying a project to Playcademy.
805
850
  * Can be provided via CLI options, playcademy.json file, or package.json.
806
851
  */
807
852
  interface DeployConfig {
808
- /** Game slug (URL-friendly identifier) */
853
+ /** Project slug (URL-friendly identifier) */
809
854
  slug?: string;
810
- /** Display name for the game */
855
+ /** Display name for the project */
811
856
  displayName?: string;
812
- /** Game description */
857
+ /** Project description */
813
858
  description?: string;
814
- /** Emoji icon for the game */
859
+ /** Emoji icon for the project */
815
860
  emoji?: string;
816
861
  /** Path to the build directory or zip file */
817
862
  buildPath?: string;
@@ -935,6 +980,21 @@ interface CallbackServerResult {
935
980
  error?: string;
936
981
  }
937
982
 
983
+ /**
984
+ * @fileoverview Config Loader Types
985
+ *
986
+ * Types for the config loader functionality.
987
+ */
988
+
989
+ /**
990
+ * Result of loading config with path information
991
+ */
992
+ interface LoadConfigResult {
993
+ config: PlaycademyConfig;
994
+ configPath: string;
995
+ configDir: string;
996
+ }
997
+
938
998
  /**
939
999
  * Preview options
940
1000
  */
@@ -1083,4 +1143,4 @@ interface KeyMetadata {
1083
1143
  valueType?: 'json' | 'string';
1084
1144
  }
1085
1145
 
1086
- export type { ApiConfig, ApiErrorResponse, ApiKeyListItem, ApiKeyWithSecret, ApiRequestOptions, AuthProfile, AuthStore, AuthStrategy, BackendBundle, BackendDeploymentMetadata, BackendDiff, BackendFeatures, BucketBulkOptions, BucketDeleteOptions, BucketGetOptions, BucketListOptions, BucketPutOptions, BuildDiff, BulkCollectionResult, BundleOptions, CallbackServerResult, CollectedFile, ConfigDiff, CreateApiKeyResponse, CustomRoutesIntegrationOptions, DatabaseIntegrationOptions, DeployConfig, DeployNewGameOptions, DeployedGameInfo, DeploymentChanges, DeploymentContext, DeploymentDiffOptions, DeploymentPlan, DeploymentResult, DevServerOptions, EmbeddedSourcePaths, EnvironmentAuthProfiles, GameStore, IntegrationChangeDetector, IntegrationChangeDetectors, IntegrationConfigChange, IntegrationsConfig, IntegrationsDiff, KeyMetadata, KeyStats, LoginCredentials, LoginResponse, PlaycademyConfig, PluginLogger, PreviewOptions, PreviewResponse, SecretsDiff, SignInResponse, SsoCallbackData, TimebackIntegrationConfig, TokenType, UpdateExistingGameOptions };
1146
+ export type { ApiConfig, ApiErrorResponse, ApiKeyListItem, ApiKeyWithSecret, ApiRequestOptions, AuthProfile, AuthStore, AuthStrategy, BackendBundle, BackendDeploymentMetadata, BackendDiff, BackendFeatures, BucketBulkOptions, BucketDeleteOptions, BucketGetOptions, BucketListOptions, BucketPutOptions, BuildDiff, BulkCollectionResult, BundleOptions, CallbackServerResult, CollectedFile, ConfigDiff, CreateApiKeyResponse, CustomRoutesIntegrationOptions, DatabaseIntegrationOptions, DeployConfig, DeployNewGameOptions, DeployedGameInfo, DeploymentChanges, DeploymentContext, DeploymentDiffOptions, DeploymentPlan, DeploymentResult, DevServerOptions, EmbeddedSourcePaths, EnvironmentAuthProfiles, GameStore, IntegrationChangeDetector, IntegrationChangeDetectors, IntegrationConfigChange, IntegrationsConfig, IntegrationsDiff, KeyMetadata, KeyStats, LoadConfigResult, LoginCredentials, LoginResponse, PlaycademyConfig, PluginLogger, PreviewOptions, PreviewResponse, SecretsDiff, SignInResponse, SsoCallbackData, TimebackBaseConfig, TimebackCourseConfigWithOverrides, TimebackIntegrationConfig, TokenType, UpdateExistingGameOptions };