playcademy 0.16.4 → 0.16.6
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 +3 -3
- package/dist/constants/src/timeback.ts +1 -0
- package/dist/constants/src/typescript.ts +4 -1
- package/dist/constants.js +1 -1
- package/dist/db.js +1 -1
- package/dist/edge-play/src/constants.ts +1 -0
- package/dist/edge-play/src/lib/errors.ts +45 -0
- package/dist/edge-play/src/lib/index.ts +2 -0
- package/dist/edge-play/src/lib/validation.test.ts +189 -0
- package/dist/edge-play/src/lib/validation.ts +61 -0
- package/dist/edge-play/src/register-routes.ts +10 -3
- package/dist/edge-play/src/routes/integrations/timeback/end-activity.ts +13 -58
- package/dist/edge-play/src/routes/integrations/timeback/get-xp.ts +147 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +29 -21
- package/dist/utils.js +6 -4
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1299,7 +1299,7 @@ var ACHIEVEMENT_DEFINITIONS = [
|
|
|
1299
1299
|
];
|
|
1300
1300
|
|
|
1301
1301
|
// ../constants/src/typescript.ts
|
|
1302
|
-
var TSC_PACKAGE = "
|
|
1302
|
+
var TSC_PACKAGE = "typescript";
|
|
1303
1303
|
var USE_NATIVE_TSC = TSC_PACKAGE.includes("native-preview");
|
|
1304
1304
|
|
|
1305
1305
|
// ../constants/src/overworld.ts
|
|
@@ -1818,7 +1818,7 @@ var init_achievements = __esm2(() => {
|
|
|
1818
1818
|
});
|
|
1819
1819
|
var init_auth = () => {
|
|
1820
1820
|
};
|
|
1821
|
-
var TSC_PACKAGE2 = "
|
|
1821
|
+
var TSC_PACKAGE2 = "typescript";
|
|
1822
1822
|
var USE_NATIVE_TSC2;
|
|
1823
1823
|
var init_typescript = __esm2(() => {
|
|
1824
1824
|
USE_NATIVE_TSC2 = TSC_PACKAGE2.includes("native-preview");
|
|
@@ -2965,7 +2965,7 @@ import { join as join13 } from "path";
|
|
|
2965
2965
|
// package.json with { type: 'json' }
|
|
2966
2966
|
var package_default2 = {
|
|
2967
2967
|
name: "playcademy",
|
|
2968
|
-
version: "0.16.
|
|
2968
|
+
version: "0.16.5",
|
|
2969
2969
|
type: "module",
|
|
2970
2970
|
exports: {
|
|
2971
2971
|
".": {
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
* - '@typescript/native-preview' - Native TypeScript compiler (faster)
|
|
12
12
|
* - 'typescript' - Standard TypeScript compiler
|
|
13
13
|
*/
|
|
14
|
-
|
|
14
|
+
// Temporarily using regular TypeScript because @typescript/native-preview daily
|
|
15
|
+
// dev builds can break unexpectedly (e.g., 7.0.0-e3a5e657689e43fb from Dec 18 hung).
|
|
16
|
+
// TODO: Switch back to native-preview once stable releases are available.
|
|
17
|
+
export const TSC_PACKAGE = 'typescript'
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* Whether using the native TypeScript compiler.
|
package/dist/constants.js
CHANGED
|
@@ -387,7 +387,7 @@ var ACHIEVEMENT_DEFINITIONS = [
|
|
|
387
387
|
];
|
|
388
388
|
|
|
389
389
|
// ../constants/src/typescript.ts
|
|
390
|
-
var TSC_PACKAGE = "
|
|
390
|
+
var TSC_PACKAGE = "typescript";
|
|
391
391
|
var USE_NATIVE_TSC = TSC_PACKAGE.includes("native-preview");
|
|
392
392
|
|
|
393
393
|
// ../constants/src/domains.ts
|
package/dist/db.js
CHANGED
|
@@ -311,7 +311,7 @@ var ACHIEVEMENT_DEFINITIONS = [
|
|
|
311
311
|
];
|
|
312
312
|
|
|
313
313
|
// ../constants/src/typescript.ts
|
|
314
|
-
var TSC_PACKAGE = "
|
|
314
|
+
var TSC_PACKAGE = "typescript";
|
|
315
315
|
var USE_NATIVE_TSC = TSC_PACKAGE.includes("native-preview");
|
|
316
316
|
|
|
317
317
|
// ../constants/src/overworld.ts
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
/**
|
|
3
|
+
* Shared error handling utilities for edge-play routes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Context } from 'hono'
|
|
7
|
+
import type { HonoEnv } from '../types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if running in development mode.
|
|
11
|
+
* Dev mode is detected by localhost or .dev. in the base URL.
|
|
12
|
+
*/
|
|
13
|
+
export function isDev(c: Context<HonoEnv>): boolean {
|
|
14
|
+
const baseUrl = c.env.PLAYCADEMY_BASE_URL || ''
|
|
15
|
+
return baseUrl.includes('localhost') || baseUrl.includes('.dev.')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build an error response object with optional stack trace (dev only).
|
|
20
|
+
*/
|
|
21
|
+
export function buildErrorResponse(
|
|
22
|
+
c: Context<HonoEnv>,
|
|
23
|
+
error: unknown,
|
|
24
|
+
fallbackMessage: string,
|
|
25
|
+
): { error: string; message: string; stack?: string } {
|
|
26
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
27
|
+
const stack = error instanceof Error ? error.stack : undefined
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
error: fallbackMessage,
|
|
31
|
+
message,
|
|
32
|
+
...(isDev(c) && stack && { stack }),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Log an error with a prefix tag for consistent logging.
|
|
38
|
+
*/
|
|
39
|
+
export function logError(tag: string, error: unknown): void {
|
|
40
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
41
|
+
const stack = error instanceof Error ? error.stack : undefined
|
|
42
|
+
|
|
43
|
+
if (message) console.error(`[${tag}] Error:`, message)
|
|
44
|
+
if (stack) console.error(`[${tag}] Stack:`, stack)
|
|
45
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isValidGrade,
|
|
5
|
+
isValidSubject,
|
|
6
|
+
validateCourseConfig,
|
|
7
|
+
VALID_GRADES,
|
|
8
|
+
VALID_SUBJECTS,
|
|
9
|
+
} from './validation'
|
|
10
|
+
|
|
11
|
+
import type { PlaycademyConfig } from '@playcademy/sdk/server'
|
|
12
|
+
|
|
13
|
+
describe('isValidGrade', () => {
|
|
14
|
+
it('accepts valid grade levels', () => {
|
|
15
|
+
for (const grade of VALID_GRADES) {
|
|
16
|
+
expect(isValidGrade(grade)).toBe(true)
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('accepts Pre-K (-1), Kindergarten (0), and AP (13)', () => {
|
|
21
|
+
expect(isValidGrade(-1)).toBe(true) // Pre-K
|
|
22
|
+
expect(isValidGrade(0)).toBe(true) // Kindergarten
|
|
23
|
+
expect(isValidGrade(13)).toBe(true) // AP
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('rejects invalid grade levels', () => {
|
|
27
|
+
expect(isValidGrade(-2)).toBe(false)
|
|
28
|
+
expect(isValidGrade(14)).toBe(false)
|
|
29
|
+
expect(isValidGrade(100)).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('rejects non-integer values', () => {
|
|
33
|
+
expect(isValidGrade(3.5)).toBe(false)
|
|
34
|
+
expect(isValidGrade(NaN)).toBe(false)
|
|
35
|
+
expect(isValidGrade(Infinity)).toBe(false)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('rejects non-number values', () => {
|
|
39
|
+
expect(isValidGrade('3')).toBe(false)
|
|
40
|
+
expect(isValidGrade(null)).toBe(false)
|
|
41
|
+
expect(isValidGrade(undefined)).toBe(false)
|
|
42
|
+
expect(isValidGrade({})).toBe(false)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('isValidSubject', () => {
|
|
47
|
+
it('accepts valid subjects', () => {
|
|
48
|
+
for (const subject of VALID_SUBJECTS) {
|
|
49
|
+
expect(isValidSubject(subject)).toBe(true)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('accepts common subjects', () => {
|
|
54
|
+
expect(isValidSubject('Math')).toBe(true)
|
|
55
|
+
expect(isValidSubject('FastMath')).toBe(true)
|
|
56
|
+
expect(isValidSubject('Reading')).toBe(true)
|
|
57
|
+
expect(isValidSubject('Science')).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('rejects invalid subjects', () => {
|
|
61
|
+
expect(isValidSubject('History')).toBe(false)
|
|
62
|
+
expect(isValidSubject('Art')).toBe(false)
|
|
63
|
+
expect(isValidSubject('math')).toBe(false) // case-sensitive
|
|
64
|
+
expect(isValidSubject('MATH')).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('rejects non-string values', () => {
|
|
68
|
+
expect(isValidSubject(123)).toBe(false)
|
|
69
|
+
expect(isValidSubject(null)).toBe(false)
|
|
70
|
+
expect(isValidSubject(undefined)).toBe(false)
|
|
71
|
+
expect(isValidSubject({})).toBe(false)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('validateCourseConfig', () => {
|
|
76
|
+
/**
|
|
77
|
+
* Simulates a typical playcademy.config.js for a math game
|
|
78
|
+
* covering grades 3-5.
|
|
79
|
+
*/
|
|
80
|
+
const mathGameConfig: PlaycademyConfig = {
|
|
81
|
+
name: 'Math Adventure',
|
|
82
|
+
integrations: {
|
|
83
|
+
timeback: {
|
|
84
|
+
courses: [
|
|
85
|
+
{ subject: 'Math', grade: 3, totalXp: 1000, masterableUnits: 10 },
|
|
86
|
+
{ subject: 'Math', grade: 4, totalXp: 1200, masterableUnits: 12 },
|
|
87
|
+
{ subject: 'Math', grade: 5, totalXp: 1500, masterableUnits: 15 },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Simulates a multi-subject game config.
|
|
95
|
+
*/
|
|
96
|
+
const multiSubjectConfig: PlaycademyConfig = {
|
|
97
|
+
name: 'Learning Hub',
|
|
98
|
+
integrations: {
|
|
99
|
+
timeback: {
|
|
100
|
+
courses: [
|
|
101
|
+
{ subject: 'Math', grade: 3, totalXp: 500, masterableUnits: 5 },
|
|
102
|
+
{ subject: 'Reading', grade: 3, totalXp: 500, masterableUnits: 5 },
|
|
103
|
+
{ subject: 'Science', grade: 4, totalXp: 600, masterableUnits: 6 },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
it('returns null for configured grade/subject combinations', () => {
|
|
110
|
+
expect(validateCourseConfig({ grade: 3, subject: 'Math', config: mathGameConfig })).toBeNull()
|
|
111
|
+
expect(validateCourseConfig({ grade: 4, subject: 'Math', config: mathGameConfig })).toBeNull()
|
|
112
|
+
expect(validateCourseConfig({ grade: 5, subject: 'Math', config: mathGameConfig })).toBeNull()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('returns error for unconfigured grade', () => {
|
|
116
|
+
const result = validateCourseConfig({ grade: 6, subject: 'Math', config: mathGameConfig })
|
|
117
|
+
|
|
118
|
+
expect(result).not.toBeNull()
|
|
119
|
+
expect(result?.error).toContain('Invalid grade/subject combination')
|
|
120
|
+
expect(result?.error).toContain('Math (Grade 6)')
|
|
121
|
+
expect(result?.error).toContain('Configured courses:')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('returns error for unconfigured subject', () => {
|
|
125
|
+
const result = validateCourseConfig({ grade: 3, subject: 'Reading', config: mathGameConfig })
|
|
126
|
+
|
|
127
|
+
expect(result).not.toBeNull()
|
|
128
|
+
expect(result?.error).toContain('Invalid grade/subject combination')
|
|
129
|
+
expect(result?.error).toContain('Reading (Grade 3)')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('lists all configured courses in error message', () => {
|
|
133
|
+
const result = validateCourseConfig({ grade: 1, subject: 'Math', config: mathGameConfig })
|
|
134
|
+
|
|
135
|
+
expect(result?.error).toContain('Math (Grade 3)')
|
|
136
|
+
expect(result?.error).toContain('Math (Grade 4)')
|
|
137
|
+
expect(result?.error).toContain('Math (Grade 5)')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('validates multi-subject configs correctly', () => {
|
|
141
|
+
// Configured combinations
|
|
142
|
+
expect(
|
|
143
|
+
validateCourseConfig({ grade: 3, subject: 'Math', config: multiSubjectConfig }),
|
|
144
|
+
).toBeNull()
|
|
145
|
+
expect(
|
|
146
|
+
validateCourseConfig({ grade: 3, subject: 'Reading', config: multiSubjectConfig }),
|
|
147
|
+
).toBeNull()
|
|
148
|
+
expect(
|
|
149
|
+
validateCourseConfig({ grade: 4, subject: 'Science', config: multiSubjectConfig }),
|
|
150
|
+
).toBeNull()
|
|
151
|
+
|
|
152
|
+
// Not configured: Math grade 4 (only Science is grade 4)
|
|
153
|
+
const result = validateCourseConfig({
|
|
154
|
+
grade: 4,
|
|
155
|
+
subject: 'Math',
|
|
156
|
+
config: multiSubjectConfig,
|
|
157
|
+
})
|
|
158
|
+
expect(result).not.toBeNull()
|
|
159
|
+
expect(result?.error).toContain('Math (Grade 4)')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('handles config with no courses', () => {
|
|
163
|
+
const emptyConfig: PlaycademyConfig = {
|
|
164
|
+
name: 'Empty Game',
|
|
165
|
+
integrations: {
|
|
166
|
+
timeback: {
|
|
167
|
+
courses: [],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const result = validateCourseConfig({ grade: 3, subject: 'Math', config: emptyConfig })
|
|
173
|
+
|
|
174
|
+
expect(result).not.toBeNull()
|
|
175
|
+
expect(result?.error).toContain('Configured courses: none')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('handles config with no timeback integration', () => {
|
|
179
|
+
const noTimebackConfig: PlaycademyConfig = {
|
|
180
|
+
name: 'No Timeback',
|
|
181
|
+
integrations: {},
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const result = validateCourseConfig({ grade: 3, subject: 'Math', config: noTimebackConfig })
|
|
185
|
+
|
|
186
|
+
expect(result).not.toBeNull()
|
|
187
|
+
expect(result?.error).toContain('Configured courses: none')
|
|
188
|
+
})
|
|
189
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared validation utilities for edge-play routes.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { PlaycademyConfig } from '@playcademy/sdk/server'
|
|
6
|
+
|
|
7
|
+
/** Valid grade levels: -1 (Pre-K), 0 (Kindergarten), 1-12 (Grades), 13 (AP) */
|
|
8
|
+
export const VALID_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] as const
|
|
9
|
+
|
|
10
|
+
/** Valid TimeBack subject values */
|
|
11
|
+
export const VALID_SUBJECTS = [
|
|
12
|
+
'Reading',
|
|
13
|
+
'Language',
|
|
14
|
+
'Vocabulary',
|
|
15
|
+
'Social Studies',
|
|
16
|
+
'Writing',
|
|
17
|
+
'Science',
|
|
18
|
+
'FastMath',
|
|
19
|
+
'Math',
|
|
20
|
+
'None',
|
|
21
|
+
] as const
|
|
22
|
+
|
|
23
|
+
export type ValidGrade = (typeof VALID_GRADES)[number]
|
|
24
|
+
export type ValidSubject = (typeof VALID_SUBJECTS)[number]
|
|
25
|
+
|
|
26
|
+
export function isValidGrade(value: unknown): value is ValidGrade {
|
|
27
|
+
return (
|
|
28
|
+
typeof value === 'number' &&
|
|
29
|
+
Number.isInteger(value) &&
|
|
30
|
+
VALID_GRADES.includes(value as ValidGrade)
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isValidSubject(value: unknown): value is ValidSubject {
|
|
35
|
+
return typeof value === 'string' && VALID_SUBJECTS.includes(value as ValidSubject)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate that a grade/subject combination is configured for this game.
|
|
40
|
+
* Returns an error object if invalid, null if valid.
|
|
41
|
+
*/
|
|
42
|
+
export function validateCourseConfig(params: {
|
|
43
|
+
grade: number
|
|
44
|
+
subject: string
|
|
45
|
+
config: PlaycademyConfig
|
|
46
|
+
}): { error: string } | null {
|
|
47
|
+
const { grade, subject, config } = params
|
|
48
|
+
const timebackConfig = config.integrations?.timeback
|
|
49
|
+
const configuredCourse = timebackConfig?.courses?.find(
|
|
50
|
+
course => course.grade === grade && course.subject === subject,
|
|
51
|
+
)
|
|
52
|
+
if (!configuredCourse) {
|
|
53
|
+
const configured = timebackConfig?.courses
|
|
54
|
+
?.map(c => `${c.subject} (Grade ${c.grade})`)
|
|
55
|
+
.join(', ')
|
|
56
|
+
return {
|
|
57
|
+
error: `Invalid grade/subject combination: ${subject} (Grade ${grade}). Configured courses: ${configured || 'none'}`,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
@@ -27,12 +27,12 @@ export async function registerBuiltinRoutes(app: Hono<HonoEnv>, integrations?: I
|
|
|
27
27
|
|
|
28
28
|
// TimeBack integration
|
|
29
29
|
if (integrations?.timeback) {
|
|
30
|
-
const [endActivity] = await Promise.all([
|
|
30
|
+
const [endActivity, getXp] = await Promise.all([
|
|
31
31
|
import('./routes/integrations/timeback/end-activity'),
|
|
32
|
-
|
|
32
|
+
import('./routes/integrations/timeback/get-xp'),
|
|
33
33
|
])
|
|
34
34
|
app.post(ROUTES.TIMEBACK.END_ACTIVITY, endActivity.POST)
|
|
35
|
-
|
|
35
|
+
app.get(ROUTES.TIMEBACK.GET_XP, getXp.GET)
|
|
36
36
|
} else if (integrations?.timeback === null) {
|
|
37
37
|
app.post('/api/integrations/timeback/end-activity', async c => {
|
|
38
38
|
return c.json({
|
|
@@ -40,6 +40,13 @@ export async function registerBuiltinRoutes(app: Hono<HonoEnv>, integrations?: I
|
|
|
40
40
|
__playcademyDevWarning: 'timeback-not-configured',
|
|
41
41
|
})
|
|
42
42
|
})
|
|
43
|
+
app.get('/api/integrations/timeback/xp', async c => {
|
|
44
|
+
return c.json({
|
|
45
|
+
timebackId: '',
|
|
46
|
+
totalXp: 0,
|
|
47
|
+
__playcademyDevWarning: 'timeback-not-configured',
|
|
48
|
+
})
|
|
49
|
+
})
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
// TODO: Auth integration
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildErrorResponse,
|
|
3
|
+
isValidGrade,
|
|
4
|
+
isValidSubject,
|
|
5
|
+
logError,
|
|
6
|
+
VALID_GRADES,
|
|
7
|
+
VALID_SUBJECTS,
|
|
8
|
+
validateCourseConfig,
|
|
9
|
+
} from '../../../lib'
|
|
10
|
+
|
|
1
11
|
import type { Context } from 'hono'
|
|
2
12
|
import type { PlaycademyConfig } from '@playcademy/sdk/server'
|
|
3
13
|
import type { ActivityData } from '@playcademy/types/timeback'
|
|
@@ -17,37 +27,6 @@ import type { HonoEnv } from '../../../types'
|
|
|
17
27
|
* their own backend infrastructure. The SDK handles config enrichment and metadata.
|
|
18
28
|
*/
|
|
19
29
|
|
|
20
|
-
/** Valid grade levels: -1 (Pre-K), 0 (Kindergarten), 1-12 (Grades), 13 (AP) */
|
|
21
|
-
const VALID_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] as const
|
|
22
|
-
|
|
23
|
-
/** Valid TimeBack subject values */
|
|
24
|
-
const VALID_SUBJECTS = [
|
|
25
|
-
'Reading',
|
|
26
|
-
'Language',
|
|
27
|
-
'Vocabulary',
|
|
28
|
-
'Social Studies',
|
|
29
|
-
'Writing',
|
|
30
|
-
'Science',
|
|
31
|
-
'FastMath',
|
|
32
|
-
'Math',
|
|
33
|
-
'None',
|
|
34
|
-
] as const
|
|
35
|
-
|
|
36
|
-
function isValidGrade(value: unknown): value is (typeof VALID_GRADES)[number] {
|
|
37
|
-
return (
|
|
38
|
-
typeof value === 'number' &&
|
|
39
|
-
Number.isInteger(value) &&
|
|
40
|
-
VALID_GRADES.includes(value as (typeof VALID_GRADES)[number])
|
|
41
|
-
)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function isValidSubject(value: unknown): value is (typeof VALID_SUBJECTS)[number] {
|
|
45
|
-
return (
|
|
46
|
-
typeof value === 'string' &&
|
|
47
|
-
VALID_SUBJECTS.includes(value as (typeof VALID_SUBJECTS)[number])
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
30
|
function getConfig(c: Context<HonoEnv>): PlaycademyConfig {
|
|
52
31
|
const config = c.get('config')
|
|
53
32
|
const timebackConfig = config?.integrations?.timeback
|
|
@@ -88,27 +67,6 @@ function validateRequestBody(body: {
|
|
|
88
67
|
return null
|
|
89
68
|
}
|
|
90
69
|
|
|
91
|
-
function validateCourse(params: {
|
|
92
|
-
grade: number
|
|
93
|
-
subject: string
|
|
94
|
-
config: PlaycademyConfig
|
|
95
|
-
}): { error: string } | null {
|
|
96
|
-
const { grade, subject, config } = params
|
|
97
|
-
const timebackConfig = config.integrations?.timeback
|
|
98
|
-
const configuredCourse = timebackConfig?.courses?.find(
|
|
99
|
-
course => course.grade === grade && course.subject === subject,
|
|
100
|
-
)
|
|
101
|
-
if (!configuredCourse) {
|
|
102
|
-
const configured = timebackConfig?.courses
|
|
103
|
-
?.map(c => `${c.subject} (Grade ${c.grade})`)
|
|
104
|
-
.join(', ')
|
|
105
|
-
return {
|
|
106
|
-
error: `Invalid grade/subject combination: ${subject} (Grade ${grade}). Configured courses: ${configured || 'none'}`,
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return null
|
|
110
|
-
}
|
|
111
|
-
|
|
112
70
|
function enrichActivityData(params: {
|
|
113
71
|
activityData: ActivityData
|
|
114
72
|
config: PlaycademyConfig
|
|
@@ -163,7 +121,7 @@ export async function POST(c: Context<HonoEnv>): Promise<Response> {
|
|
|
163
121
|
|
|
164
122
|
// 6. Validate grade/subject against configured courses
|
|
165
123
|
const { grade, subject } = activityData
|
|
166
|
-
const courseValidationError =
|
|
124
|
+
const courseValidationError = validateCourseConfig({ grade, subject, config })
|
|
167
125
|
|
|
168
126
|
if (courseValidationError) {
|
|
169
127
|
const message = courseValidationError.error
|
|
@@ -193,10 +151,7 @@ export async function POST(c: Context<HonoEnv>): Promise<Response> {
|
|
|
193
151
|
|
|
194
152
|
return c.json(result)
|
|
195
153
|
} catch (error) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (message) console.error('[TimeBack End Activity] Error:', message)
|
|
199
|
-
if (stack) console.error('[TimeBack End Activity] Stack:', stack)
|
|
200
|
-
return c.json({ error: 'Failed to end activity', message, stack }, 500)
|
|
154
|
+
logError('TimeBack End Activity', error)
|
|
155
|
+
return c.json(buildErrorResponse(c, error, 'Failed to end activity'), 500)
|
|
201
156
|
}
|
|
202
157
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildErrorResponse,
|
|
3
|
+
isValidGrade,
|
|
4
|
+
isValidSubject,
|
|
5
|
+
logError,
|
|
6
|
+
VALID_GRADES,
|
|
7
|
+
VALID_SUBJECTS,
|
|
8
|
+
validateCourseConfig,
|
|
9
|
+
} from '../../../lib'
|
|
10
|
+
|
|
11
|
+
import type { Context } from 'hono'
|
|
12
|
+
import type { PlaycademyConfig } from '@playcademy/sdk/server'
|
|
13
|
+
import type { HonoEnv } from '../../../types'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* TimeBack integration - Get student XP
|
|
17
|
+
* Route: GET /api/integrations/timeback/xp
|
|
18
|
+
* Auto-generated when integrations.timeback is configured
|
|
19
|
+
*
|
|
20
|
+
* Flow:
|
|
21
|
+
* 1. Game frontend calls this route on deployed backend ({slug}.playcademy.gg/api/integrations/timeback/xp)
|
|
22
|
+
* 2. This route calls Playcademy platform API via SDK (hub.playcademy.net/api/timeback/student-xp/:timebackId)
|
|
23
|
+
* 3. Platform API fetches XP data from TimeBack EduBridge analytics API
|
|
24
|
+
*
|
|
25
|
+
* By default, returns XP for all courses configured for this game/app.
|
|
26
|
+
* Use `grade` and `subject` to query a specific course instead.
|
|
27
|
+
*
|
|
28
|
+
* Query Parameters:
|
|
29
|
+
*
|
|
30
|
+
* @param {number} [grade] - Grade level to filter (-1 to 13).
|
|
31
|
+
* Must be used together with `subject`.
|
|
32
|
+
* Example: `?grade=3&subject=Math`
|
|
33
|
+
*
|
|
34
|
+
* @param {string} [subject] - Subject to filter.
|
|
35
|
+
* Must be used together with `grade`.
|
|
36
|
+
* Valid values: Reading, Language, Vocabulary, Social Studies, Writing, Science, FastMath, Math, None
|
|
37
|
+
* Example: `?grade=3&subject=Math`
|
|
38
|
+
*
|
|
39
|
+
* @param {string} [include] - Comma-separated list of additional data to include.
|
|
40
|
+
* Available options:
|
|
41
|
+
* - `perCourse` - Include per-course XP breakdown with grade, subject, and xp
|
|
42
|
+
* - `today` - Include today's XP (both total and per-course if perCourse is set)
|
|
43
|
+
* Example: `?include=perCourse,today`
|
|
44
|
+
*
|
|
45
|
+
* Response:
|
|
46
|
+
* ```json
|
|
47
|
+
* {
|
|
48
|
+
* "totalXp": 1500,
|
|
49
|
+
* "todayXp": 50, // Only if include contains 'today'
|
|
50
|
+
* "courses": [ // Only if include contains 'perCourse'
|
|
51
|
+
* {
|
|
52
|
+
* "grade": 3,
|
|
53
|
+
* "subject": "Math",
|
|
54
|
+
* "title": "Math Grade 3",
|
|
55
|
+
* "totalXp": 1000,
|
|
56
|
+
* "todayXp": 30 // Only if include contains 'today'
|
|
57
|
+
* }
|
|
58
|
+
* ]
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get config and validate that the game has TimeBack configured.
|
|
65
|
+
*/
|
|
66
|
+
function getConfig(c: Context<HonoEnv>): PlaycademyConfig {
|
|
67
|
+
const config = c.get('config')
|
|
68
|
+
const timebackConfig = config?.integrations?.timeback
|
|
69
|
+
if (!timebackConfig) throw new Error('TimeBack integration not found')
|
|
70
|
+
return config
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function GET(c: Context<HonoEnv>): Promise<Response> {
|
|
74
|
+
try {
|
|
75
|
+
// 1. Get authenticated user from middleware
|
|
76
|
+
const user = c.get('playcademyUser')
|
|
77
|
+
if (!user) return c.json({ error: 'Unauthorized' }, 401)
|
|
78
|
+
|
|
79
|
+
// 2. Ensure user has TimeBack integration
|
|
80
|
+
if (!user.timeback_id) {
|
|
81
|
+
const message = 'User does not have TimeBack integration'
|
|
82
|
+
console.error('[TimeBack Get XP] Error:', message)
|
|
83
|
+
return c.json({ error: message }, 400)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 3. Validate TimeBack is configured for this game
|
|
87
|
+
const config = getConfig(c)
|
|
88
|
+
|
|
89
|
+
// 4. Parse query parameters
|
|
90
|
+
const url = new URL(c.req.url)
|
|
91
|
+
const gradeParam = url.searchParams.get('grade')
|
|
92
|
+
const subjectParam = url.searchParams.get('subject')
|
|
93
|
+
const includeParam = url.searchParams.get('include')
|
|
94
|
+
const include = includeParam
|
|
95
|
+
? (includeParam.split(',').map(s => s.trim()) as ('perCourse' | 'today')[])
|
|
96
|
+
: undefined
|
|
97
|
+
|
|
98
|
+
let grade: number | undefined
|
|
99
|
+
let subject: string | undefined
|
|
100
|
+
|
|
101
|
+
if (gradeParam !== null || subjectParam !== null) {
|
|
102
|
+
if (gradeParam === null || subjectParam === null) {
|
|
103
|
+
return c.json({ error: 'Both grade and subject must be provided together' }, 400)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
grade = parseInt(gradeParam, 10)
|
|
107
|
+
subject = subjectParam
|
|
108
|
+
|
|
109
|
+
if (!isValidGrade(grade)) {
|
|
110
|
+
return c.json(
|
|
111
|
+
{ error: `grade must be a valid grade level (${VALID_GRADES.join(', ')})` },
|
|
112
|
+
400,
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!isValidSubject(subject)) {
|
|
117
|
+
return c.json(
|
|
118
|
+
{ error: `subject must be a valid subject (${VALID_SUBJECTS.join(', ')})` },
|
|
119
|
+
400,
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 5. Validate grade/subject against configured courses
|
|
124
|
+
const courseValidationError = validateCourseConfig({ grade, subject, config })
|
|
125
|
+
|
|
126
|
+
if (courseValidationError) {
|
|
127
|
+
return c.json({ error: courseValidationError.error }, 400)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 6. Get SDK client from context
|
|
132
|
+
const sdk = c.get('sdk')
|
|
133
|
+
|
|
134
|
+
// 7. Fetch student XP from platform API
|
|
135
|
+
// The SDK automatically passes gameId, so XP is filtered to this game's courses
|
|
136
|
+
const result = await sdk.timeback.getStudentXp(user.timeback_id, {
|
|
137
|
+
grade,
|
|
138
|
+
subject,
|
|
139
|
+
include,
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
return c.json(result)
|
|
143
|
+
} catch (error) {
|
|
144
|
+
logError('TimeBack Get XP', error)
|
|
145
|
+
return c.json(buildErrorResponse(c, error, 'Failed to get XP'), 500)
|
|
146
|
+
}
|
|
147
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -731,7 +731,7 @@ declare const games: drizzle_orm_pg_core.PgTableWithColumns<{
|
|
|
731
731
|
tableName: "games";
|
|
732
732
|
dataType: "string";
|
|
733
733
|
columnType: "PgEnumColumn";
|
|
734
|
-
data: "
|
|
734
|
+
data: "hosted" | "external";
|
|
735
735
|
driverParam: string;
|
|
736
736
|
notNull: true;
|
|
737
737
|
hasDefault: true;
|
|
@@ -782,7 +782,7 @@ declare const games: drizzle_orm_pg_core.PgTableWithColumns<{
|
|
|
782
782
|
tableName: "games";
|
|
783
783
|
dataType: "string";
|
|
784
784
|
columnType: "PgEnumColumn";
|
|
785
|
-
data: "
|
|
785
|
+
data: "web" | "godot" | "unity";
|
|
786
786
|
driverParam: string;
|
|
787
787
|
notNull: true;
|
|
788
788
|
hasDefault: true;
|
package/dist/index.js
CHANGED
|
@@ -700,7 +700,7 @@ var ACHIEVEMENT_DEFINITIONS = [
|
|
|
700
700
|
];
|
|
701
701
|
|
|
702
702
|
// ../constants/src/typescript.ts
|
|
703
|
-
var TSC_PACKAGE = "
|
|
703
|
+
var TSC_PACKAGE = "typescript";
|
|
704
704
|
var USE_NATIVE_TSC = TSC_PACKAGE.includes("native-preview");
|
|
705
705
|
|
|
706
706
|
// ../constants/src/domains.ts
|
|
@@ -747,7 +747,8 @@ var BADGES = {
|
|
|
747
747
|
|
|
748
748
|
// ../constants/src/timeback.ts
|
|
749
749
|
var TIMEBACK_ROUTES = {
|
|
750
|
-
END_ACTIVITY: "/integrations/timeback/end-activity"
|
|
750
|
+
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
751
|
+
GET_XP: "/integrations/timeback/xp"
|
|
751
752
|
};
|
|
752
753
|
|
|
753
754
|
// ../constants/src/workers.ts
|
|
@@ -2397,7 +2398,7 @@ var init_achievements = __esm2(() => {
|
|
|
2397
2398
|
});
|
|
2398
2399
|
var init_auth = () => {
|
|
2399
2400
|
};
|
|
2400
|
-
var TSC_PACKAGE2 = "
|
|
2401
|
+
var TSC_PACKAGE2 = "typescript";
|
|
2401
2402
|
var USE_NATIVE_TSC2;
|
|
2402
2403
|
var init_typescript = __esm2(() => {
|
|
2403
2404
|
USE_NATIVE_TSC2 = TSC_PACKAGE2.includes("native-preview");
|
|
@@ -2801,7 +2802,7 @@ var init_achievements2 = __esm3(() => {
|
|
|
2801
2802
|
});
|
|
2802
2803
|
var init_auth2 = () => {
|
|
2803
2804
|
};
|
|
2804
|
-
var TSC_PACKAGE3 = "
|
|
2805
|
+
var TSC_PACKAGE3 = "typescript";
|
|
2805
2806
|
var USE_NATIVE_TSC3;
|
|
2806
2807
|
var init_typescript2 = __esm3(() => {
|
|
2807
2808
|
USE_NATIVE_TSC3 = TSC_PACKAGE3.includes("native-preview");
|
|
@@ -3995,7 +3996,7 @@ import { join as join12 } from "path";
|
|
|
3995
3996
|
// package.json with { type: 'json' }
|
|
3996
3997
|
var package_default2 = {
|
|
3997
3998
|
name: "playcademy",
|
|
3998
|
-
version: "0.16.
|
|
3999
|
+
version: "0.16.5",
|
|
3999
4000
|
type: "module",
|
|
4000
4001
|
exports: {
|
|
4001
4002
|
".": {
|
|
@@ -6332,7 +6333,8 @@ var ROUTES = {
|
|
|
6332
6333
|
HEALTH: "/api/health",
|
|
6333
6334
|
/** TimeBack integration routes */
|
|
6334
6335
|
TIMEBACK: {
|
|
6335
|
-
END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}
|
|
6336
|
+
END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
|
|
6337
|
+
GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}`
|
|
6336
6338
|
}
|
|
6337
6339
|
};
|
|
6338
6340
|
|
|
@@ -8421,9 +8423,11 @@ async function analyzeChanges(context2) {
|
|
|
8421
8423
|
deployedGameInfo
|
|
8422
8424
|
} = context2;
|
|
8423
8425
|
let secretsDiff = void 0;
|
|
8424
|
-
|
|
8426
|
+
const hasLocalSecrets = Object.keys(localSecrets ?? {}).length > 0;
|
|
8427
|
+
const hasRemoteSecrets = remoteSecretKeys && remoteSecretKeys.length > 0;
|
|
8428
|
+
if (hasLocalSecrets || hasRemoteSecrets) {
|
|
8425
8429
|
secretsDiff = computeSecretsDiff({
|
|
8426
|
-
localSecrets,
|
|
8430
|
+
localSecrets: localSecrets ?? {},
|
|
8427
8431
|
remoteKeys: remoteSecretKeys ?? [],
|
|
8428
8432
|
cachedHashes: deployedGameInfo?.secretsHashes
|
|
8429
8433
|
});
|
|
@@ -8514,8 +8518,8 @@ async function analyzeChanges(context2) {
|
|
|
8514
8518
|
async function calculateDeploymentPlan(context2, changes) {
|
|
8515
8519
|
const { config, isGameDeployed: isGameDeployed2, projectPath, deployBackend, forceBackend } = context2;
|
|
8516
8520
|
if (!isGameDeployed2) {
|
|
8517
|
-
const
|
|
8518
|
-
const shouldDeployBackend2 = deployBackend &&
|
|
8521
|
+
const needsBackend3 = hasLocalCustomRoutes(projectPath, context2.fullConfig) || !!context2.fullConfig?.integrations;
|
|
8522
|
+
const shouldDeployBackend2 = deployBackend && needsBackend3;
|
|
8519
8523
|
return {
|
|
8520
8524
|
action: "deploy-new",
|
|
8521
8525
|
gameType: config.gameType ?? "hosted",
|
|
@@ -8541,7 +8545,8 @@ async function calculateDeploymentPlan(context2, changes) {
|
|
|
8541
8545
|
}
|
|
8542
8546
|
const shouldUpdateGame = buildHasChanges || configHasChanges;
|
|
8543
8547
|
const shouldUploadBuild = buildHasChanges;
|
|
8544
|
-
const
|
|
8548
|
+
const needsBackend2 = hasLocalCustomRoutes(projectPath, context2.fullConfig) || !!context2.fullConfig?.integrations;
|
|
8549
|
+
const shouldDeployBackend = deployBackend && (backendHasChanges || forceBackend || shouldUploadBuild && needsBackend2);
|
|
8545
8550
|
return {
|
|
8546
8551
|
action: "update-existing",
|
|
8547
8552
|
gameType: config.gameType ?? "hosted",
|
|
@@ -10675,8 +10680,11 @@ async function executeDeployment(plan, changes, context2, environment) {
|
|
|
10675
10680
|
return { game, backendMetadata };
|
|
10676
10681
|
}
|
|
10677
10682
|
async function runDeployment(options) {
|
|
10678
|
-
|
|
10679
|
-
|
|
10683
|
+
if (options.env) {
|
|
10684
|
+
ensureEnvironment(options.env);
|
|
10685
|
+
}
|
|
10686
|
+
await selectEnvironment({ env: options.env, dryRun: options.dryRun });
|
|
10687
|
+
const environment = ensureEnvironment(void 0);
|
|
10680
10688
|
const context2 = await prepareDeploymentContext(options);
|
|
10681
10689
|
displayCurrentConfiguration(context2);
|
|
10682
10690
|
const didPrompt = await promptForMissingConfig(context2);
|
|
@@ -10991,7 +10999,7 @@ var getStatusCommand = new Command13("status").description("Check your developer
|
|
|
10991
10999
|
});
|
|
10992
11000
|
|
|
10993
11001
|
// package.json
|
|
10994
|
-
var version2 = "0.16.
|
|
11002
|
+
var version2 = "0.16.5";
|
|
10995
11003
|
|
|
10996
11004
|
// src/commands/dev/server.ts
|
|
10997
11005
|
function setupCleanupHandlers(workspace, getServer) {
|
|
@@ -14503,18 +14511,18 @@ async function runSecretsPush(options = {}) {
|
|
|
14503
14511
|
}
|
|
14504
14512
|
const localSecrets = await readEnvFile(workspace);
|
|
14505
14513
|
const localKeys = Object.keys(localSecrets);
|
|
14506
|
-
if (localKeys.length === 0) {
|
|
14507
|
-
logger.admonition("info", "No Secrets", [
|
|
14508
|
-
`Your <.env> file is empty or contains no valid secrets`
|
|
14509
|
-
]);
|
|
14510
|
-
logger.newLine();
|
|
14511
|
-
return false;
|
|
14512
|
-
}
|
|
14513
14514
|
const remoteKeys = await runStep(
|
|
14514
14515
|
`Fetching secrets from ${environment}`,
|
|
14515
14516
|
() => client.dev.games.secrets.list(game.slug),
|
|
14516
14517
|
(keys) => `Found ${keys.length} existing ${pluralize(keys.length, "secret")} on ${environment}`
|
|
14517
14518
|
);
|
|
14519
|
+
const noSyncNecessary = localKeys.length === 0 && remoteKeys.length === 0;
|
|
14520
|
+
if (noSyncNecessary) {
|
|
14521
|
+
logger.newLine();
|
|
14522
|
+
logger.success("No secrets to sync");
|
|
14523
|
+
logger.newLine();
|
|
14524
|
+
return true;
|
|
14525
|
+
}
|
|
14518
14526
|
const cachedHashes = deployedGame.secretsHashes;
|
|
14519
14527
|
const diff = computeSecretsDiff({
|
|
14520
14528
|
localSecrets,
|
package/dist/utils.js
CHANGED
|
@@ -616,7 +616,7 @@ var ACHIEVEMENT_DEFINITIONS = [
|
|
|
616
616
|
];
|
|
617
617
|
|
|
618
618
|
// ../constants/src/typescript.ts
|
|
619
|
-
var TSC_PACKAGE = "
|
|
619
|
+
var TSC_PACKAGE = "typescript";
|
|
620
620
|
var USE_NATIVE_TSC = TSC_PACKAGE.includes("native-preview");
|
|
621
621
|
|
|
622
622
|
// ../constants/src/overworld.ts
|
|
@@ -652,7 +652,8 @@ var BADGES = {
|
|
|
652
652
|
|
|
653
653
|
// ../constants/src/timeback.ts
|
|
654
654
|
var TIMEBACK_ROUTES = {
|
|
655
|
-
END_ACTIVITY: "/integrations/timeback/end-activity"
|
|
655
|
+
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
656
|
+
GET_XP: "/integrations/timeback/xp"
|
|
656
657
|
};
|
|
657
658
|
|
|
658
659
|
// ../constants/src/workers.ts
|
|
@@ -1327,7 +1328,8 @@ var ROUTES = {
|
|
|
1327
1328
|
HEALTH: "/api/health",
|
|
1328
1329
|
/** TimeBack integration routes */
|
|
1329
1330
|
TIMEBACK: {
|
|
1330
|
-
END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}
|
|
1331
|
+
END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
|
|
1332
|
+
GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}`
|
|
1331
1333
|
}
|
|
1332
1334
|
};
|
|
1333
1335
|
|
|
@@ -2356,7 +2358,7 @@ import { join as join12 } from "path";
|
|
|
2356
2358
|
// package.json with { type: 'json' }
|
|
2357
2359
|
var package_default2 = {
|
|
2358
2360
|
name: "playcademy",
|
|
2359
|
-
version: "0.16.
|
|
2361
|
+
version: "0.16.5",
|
|
2360
2362
|
type: "module",
|
|
2361
2363
|
exports: {
|
|
2362
2364
|
".": {
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playcademy",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@inquirer/prompts": "^7.8.6",
|
|
53
|
-
"@playcademy/sdk": "0.2.
|
|
53
|
+
"@playcademy/sdk": "0.2.12",
|
|
54
54
|
"chokidar": "^4.0.3",
|
|
55
55
|
"colorette": "^2.0.20",
|
|
56
56
|
"commander": "^14.0.1",
|