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.
- package/README.md +20 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -2
- package/dist/db.js +14 -7
- package/dist/edge-play/src/register-routes.ts +7 -0
- package/dist/edge-play/src/routes/health.ts +18 -7
- package/dist/edge-play/src/routes/integrations/timeback/end-activity.ts +113 -49
- package/dist/edge-play/src/types.ts +2 -0
- package/dist/index.d.ts +79 -19
- package/dist/index.js +7553 -7573
- package/dist/templates/api/sample-route-with-db.ts.template +1 -1
- package/dist/templates/api/sample-route.ts.template +5 -5
- package/dist/templates/auth/auth-schema.ts.template +1 -1
- package/dist/templates/auth/auth.ts.template +2 -2
- package/dist/templates/database/db-schema-example.ts.template +1 -1
- package/dist/utils.d.ts +84 -11
- package/dist/utils.js +194 -116
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Calling your backend from your frontend:
|
|
4
4
|
* ─────────────────────────────────────────────────────────────────
|
|
5
5
|
*
|
|
6
|
-
* In your
|
|
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-
|
|
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
|
|
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 -
|
|
63
|
-
* - c.env.GAME_ID - Your
|
|
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
|
|
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
|
|
31
|
-
// Add your deployed
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
17
|
-
/** Organization
|
|
34
|
+
interface TimebackBaseConfig {
|
|
35
|
+
/** Organization configuration (shared across all courses) */
|
|
18
36
|
organization?: Partial<OrganizationConfig>;
|
|
19
|
-
/** Course
|
|
20
|
-
course
|
|
21
|
-
/** Component
|
|
37
|
+
/** Course defaults (can be overridden per-course) */
|
|
38
|
+
course?: Partial<CourseConfig>;
|
|
39
|
+
/** Component defaults */
|
|
22
40
|
component?: Partial<ComponentConfig>;
|
|
23
|
-
/** Resource
|
|
41
|
+
/** Resource defaults */
|
|
24
42
|
resource?: Partial<ResourceConfig>;
|
|
25
|
-
/**
|
|
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 (
|
|
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
|
|
2669
|
-
const dataWidth = Math.max(...rows.map((row) => row[i]
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
'
|
|
2987
|
-
"integrations.timeback.
|
|
2988
|
-
'
|
|
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
|
-
|
|
2992
|
-
if (!course.subjects || !Array.isArray(course.subjects) || course.subjects.length === 0) {
|
|
3066
|
+
if (!Array.isArray(tb.courses)) {
|
|
2993
3067
|
throw new ConfigError(
|
|
2994
|
-
"
|
|
2995
|
-
"integrations.timeback.
|
|
2996
|
-
'
|
|
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 (
|
|
3073
|
+
if (tb.courses.length === 0) {
|
|
3000
3074
|
throw new ConfigError(
|
|
3001
|
-
"TimeBack
|
|
3002
|
-
"integrations.timeback.
|
|
3003
|
-
|
|
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
|
-
|
|
3007
|
-
if (typeof
|
|
3080
|
+
tb.courses.forEach((course, idx) => {
|
|
3081
|
+
if (typeof course !== "object" || course === null) {
|
|
3008
3082
|
throw new ConfigError(
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
'
|
|
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
|
|
3015
|
-
if (
|
|
3016
|
-
|
|
3017
|
-
"
|
|
3018
|
-
|
|
3019
|
-
"
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
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
|
-
`
|
|
3027
|
-
|
|
3028
|
-
|
|
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
|
|
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
|
|
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 =
|
|
3107
|
-
if (!
|
|
3241
|
+
const dir = join6(home, ".playcademy");
|
|
3242
|
+
if (!existsSync3(dir)) {
|
|
3108
3243
|
mkdirSync(dir, { recursive: true });
|
|
3109
3244
|
}
|
|
3110
|
-
return
|
|
3245
|
+
return join6(dir, ".proc");
|
|
3111
3246
|
}
|
|
3112
3247
|
function readRegistry() {
|
|
3113
3248
|
const registryPath = getRegistryPath();
|
|
3114
|
-
if (!
|
|
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.
|
|
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:
|
|
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
|