timeback-studio 0.1.3 → 0.1.4

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/bin.js CHANGED
@@ -16926,10 +16926,9 @@ var playcademyParser = {
16926
16926
  };
16927
16927
 
16928
16928
  // ../internal/cli-infra/src/config/timeback.ts
16929
- import { existsSync } from "node:fs";
16930
16929
  import { readFile as readFile2 } from "node:fs/promises";
16931
- import { basename, relative, resolve } from "node:path";
16932
- import { pathToFileURL } from "node:url";
16930
+ import { basename, extname, relative, resolve } from "node:path";
16931
+ import { loadConfig as c12LoadConfig } from "c12";
16933
16932
  // ../types/src/zod/primitives.ts
16934
16933
  var IsoDateTimeRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
16935
16934
  var IsoDateTimeString = exports_external.string().min(1).regex(IsoDateTimeRegex, "must be a valid ISO 8601 datetime");
@@ -16945,7 +16944,7 @@ var TimebackSubject = exports_external.enum([
16945
16944
  "Math",
16946
16945
  "None",
16947
16946
  "Other"
16948
- ]);
16947
+ ]).meta({ id: "TimebackSubject", description: "Subject area" });
16949
16948
  var TimebackGrade = exports_external.union([
16950
16949
  exports_external.literal(-1),
16951
16950
  exports_external.literal(0),
@@ -16962,7 +16961,10 @@ var TimebackGrade = exports_external.union([
16962
16961
  exports_external.literal(11),
16963
16962
  exports_external.literal(12),
16964
16963
  exports_external.literal(13)
16965
- ]);
16964
+ ]).meta({
16965
+ id: "TimebackGrade",
16966
+ description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
16967
+ });
16966
16968
  var ScoreStatus = exports_external.enum([
16967
16969
  "exempt",
16968
16970
  "fully graded",
@@ -17194,62 +17196,84 @@ var CaliperListEventsParams = exports_external.object({
17194
17196
  }).strict();
17195
17197
  // ../types/src/zod/config.ts
17196
17198
  var CourseIds = exports_external.object({
17197
- staging: exports_external.string().optional(),
17198
- production: exports_external.string().optional()
17199
- });
17200
- var CourseType = exports_external.enum(["base", "hole-filling", "optional"]);
17201
- var PublishStatus = exports_external.enum(["draft", "testing", "published", "deactivated"]);
17199
+ staging: exports_external.string().meta({ description: "Course ID in staging environment" }).optional(),
17200
+ production: exports_external.string().meta({ description: "Course ID in production environment" }).optional()
17201
+ }).meta({ id: "CourseIds", description: "Environment-specific course IDs (populated by sync)" });
17202
+ var CourseType = exports_external.enum(["base", "hole-filling", "optional"]).meta({ id: "CourseType", description: "Course classification type" });
17203
+ var PublishStatus = exports_external.enum(["draft", "testing", "published", "deactivated"]).meta({ id: "PublishStatus", description: "Course publication status" });
17202
17204
  var CourseGoals = exports_external.object({
17203
- dailyXp: exports_external.number().int().positive().optional(),
17204
- dailyLessons: exports_external.number().int().positive().optional(),
17205
- dailyActiveMinutes: exports_external.number().int().positive().optional(),
17206
- dailyAccuracy: exports_external.number().int().min(0).max(100).optional(),
17207
- dailyMasteredUnits: exports_external.number().int().positive().optional()
17208
- });
17205
+ dailyXp: exports_external.number().int().positive().meta({ description: "Target XP to earn per day" }).optional(),
17206
+ dailyLessons: exports_external.number().int().positive().meta({ description: "Target lessons to complete per day" }).optional(),
17207
+ dailyActiveMinutes: exports_external.number().int().positive().meta({ description: "Target active learning minutes per day" }).optional(),
17208
+ dailyAccuracy: exports_external.number().int().min(0).max(100).meta({ description: "Target accuracy percentage (0-100)" }).optional(),
17209
+ dailyMasteredUnits: exports_external.number().int().positive().meta({ description: "Target units to master per day" }).optional()
17210
+ }).meta({ id: "CourseGoals", description: "Daily learning goals for a course" });
17209
17211
  var CourseMetrics = exports_external.object({
17210
- totalXp: exports_external.number().int().positive().optional(),
17211
- totalLessons: exports_external.number().int().positive().optional(),
17212
- totalGrades: exports_external.number().int().positive().optional()
17213
- });
17212
+ totalXp: exports_external.number().int().positive().meta({ description: "Total XP available in the course" }).optional(),
17213
+ totalLessons: exports_external.number().int().positive().meta({ description: "Total number of lessons/activities" }).optional(),
17214
+ totalGrades: exports_external.number().int().positive().meta({ description: "Total grade levels covered" }).optional()
17215
+ }).meta({ id: "CourseMetrics", description: "Aggregate metrics for a course" });
17214
17216
  var CourseMetadata = exports_external.object({
17215
17217
  courseType: CourseType.optional(),
17216
- isSupplemental: exports_external.boolean().optional(),
17217
- isCustom: exports_external.boolean().optional(),
17218
+ isSupplemental: exports_external.boolean().meta({ description: "Whether this is supplemental to a base course" }).optional(),
17219
+ isCustom: exports_external.boolean().meta({ description: "Whether this is a custom course for an individual student" }).optional(),
17218
17220
  publishStatus: PublishStatus.optional(),
17219
- contactEmail: exports_external.email().optional(),
17220
- primaryApp: exports_external.string().optional(),
17221
+ contactEmail: exports_external.email().meta({ description: "Contact email for course issues" }).optional(),
17222
+ primaryApp: exports_external.string().meta({ description: "Primary application identifier" }).optional(),
17221
17223
  goals: CourseGoals.optional(),
17222
17224
  metrics: CourseMetrics.optional()
17223
- });
17225
+ }).meta({ id: "CourseMetadata", description: "Course metadata (matches API metadata object)" });
17224
17226
  var CourseDefaults = exports_external.object({
17225
- courseCode: exports_external.string().optional(),
17226
- level: exports_external.string().optional(),
17227
+ courseCode: exports_external.string().meta({ description: "Course code (e.g., 'MATH101')" }).optional(),
17228
+ level: exports_external.string().meta({ description: "Course level (e.g., 'AP', 'Honors')" }).optional(),
17227
17229
  metadata: CourseMetadata.optional()
17230
+ }).meta({
17231
+ id: "CourseDefaults",
17232
+ description: "Default properties that apply to all courses unless overridden"
17228
17233
  });
17229
17234
  var CourseEnvOverrides = exports_external.object({
17230
- level: exports_external.string().optional(),
17231
- sensor: exports_external.string().url().optional(),
17232
- launchUrl: exports_external.string().url().optional(),
17235
+ level: exports_external.string().meta({ description: "Course level for this environment" }).optional(),
17236
+ sensor: exports_external.url().meta({ description: "Caliper sensor endpoint URL for this environment" }).optional(),
17237
+ launchUrl: exports_external.url().meta({ description: "LTI launch URL for this environment" }).optional(),
17233
17238
  metadata: CourseMetadata.optional()
17239
+ }).meta({
17240
+ id: "CourseEnvOverrides",
17241
+ description: "Environment-specific course overrides (non-identity fields)"
17234
17242
  });
17235
17243
  var CourseOverrides = exports_external.object({
17236
- staging: CourseEnvOverrides.optional(),
17237
- production: CourseEnvOverrides.optional()
17238
- });
17244
+ staging: CourseEnvOverrides.meta({
17245
+ description: "Overrides for staging environment"
17246
+ }).optional(),
17247
+ production: CourseEnvOverrides.meta({
17248
+ description: "Overrides for production environment"
17249
+ }).optional()
17250
+ }).meta({ id: "CourseOverrides", description: "Per-environment course overrides" });
17239
17251
  var CourseConfig = CourseDefaults.extend({
17240
- subject: TimebackSubject,
17241
- grade: TimebackGrade.optional(),
17252
+ subject: TimebackSubject.meta({ description: "Subject area for this course" }),
17253
+ grade: TimebackGrade.meta({
17254
+ description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
17255
+ }).optional(),
17242
17256
  ids: CourseIds.nullable().optional(),
17243
- sensor: exports_external.string().url().optional(),
17244
- launchUrl: exports_external.string().url().optional(),
17257
+ sensor: exports_external.url().meta({ description: "Caliper sensor endpoint URL for this course" }).optional(),
17258
+ launchUrl: exports_external.url().meta({ description: "LTI launch URL for this course" }).optional(),
17245
17259
  overrides: CourseOverrides.optional()
17260
+ }).meta({
17261
+ id: "CourseConfig",
17262
+ description: "Configuration for a single course. Must have either grade or courseCode (or both)."
17246
17263
  });
17247
17264
  var TimebackConfig = exports_external.object({
17248
- name: exports_external.string().min(1, "App name is required"),
17249
- defaults: CourseDefaults.optional(),
17250
- courses: exports_external.array(CourseConfig).min(1, "At least one course is required"),
17251
- sensor: exports_external.string().url().optional(),
17252
- launchUrl: exports_external.string().url().optional()
17265
+ $schema: exports_external.string().meta({ description: "JSON Schema reference for editor support" }).optional(),
17266
+ name: exports_external.string().min(1, "App name is required").meta({ description: "Display name for your app" }),
17267
+ defaults: CourseDefaults.meta({
17268
+ description: "Default properties applied to all courses"
17269
+ }).optional(),
17270
+ courses: exports_external.array(CourseConfig).min(1, "At least one course is required").meta({ description: "Courses available in this app" }),
17271
+ sensor: exports_external.url().meta({ description: "Default Caliper sensor endpoint URL for all courses" }).optional(),
17272
+ launchUrl: exports_external.url().meta({ description: "Default LTI launch URL for all courses" }).optional()
17273
+ }).meta({
17274
+ id: "TimebackConfig",
17275
+ title: "Timeback Config",
17276
+ description: "Configuration schema for timeback.config.json files"
17253
17277
  }).refine((config2) => {
17254
17278
  return config2.courses.every((c) => c.grade !== undefined || c.courseCode !== undefined);
17255
17279
  }, {
@@ -18264,26 +18288,10 @@ var QtiLessonFeedbackInput = exports_external.object({
18264
18288
  humanApproved: exports_external.boolean().optional()
18265
18289
  }).strict();
18266
18290
  // ../internal/cli-infra/src/config/timeback.ts
18267
- var FILE_PATTERNS2 = ["timeback.config.ts", "timeback.config.js", "timeback.config.mjs"];
18268
- var isBun = typeof globalThis.Bun !== "undefined";
18269
- var jitiInstance = null;
18270
- async function getJiti() {
18271
- if (jitiInstance)
18272
- return jitiInstance;
18273
- const { createJiti } = await import("jiti");
18274
- jitiInstance = createJiti(import.meta.url, { fsCache: false });
18275
- return jitiInstance;
18276
- }
18277
- async function importModule(fullPath) {
18278
- let module;
18279
- if (isBun) {
18280
- const fileUrl = pathToFileURL(fullPath).href;
18281
- module = await import(fileUrl);
18282
- } else {
18283
- const jiti = await getJiti();
18284
- module = await jiti.import(fullPath);
18285
- }
18286
- return module.default ?? module;
18291
+ var CONFIG_FILENAME = "timeback.config.json";
18292
+ var FILE_PATTERNS2 = [CONFIG_FILENAME];
18293
+ function isJsonConfigPath(configPath) {
18294
+ return extname(configPath).toLowerCase() === ".json";
18287
18295
  }
18288
18296
  function deriveCourseIds(config3) {
18289
18297
  const result = { staging: [], production: [] };
@@ -18301,58 +18309,73 @@ async function readPackageVersion(cwd) {
18301
18309
  return "0.0.0";
18302
18310
  }
18303
18311
  }
18312
+ async function loadWithC12(cwd, configPath) {
18313
+ if (configPath && !isJsonConfigPath(configPath)) {
18314
+ throw new Error(`Config file must be JSON (.json): ${configPath}`);
18315
+ }
18316
+ const result = await c12LoadConfig({
18317
+ cwd,
18318
+ name: "timeback",
18319
+ configFile: configPath ?? CONFIG_FILENAME,
18320
+ rcFile: false,
18321
+ packageJson: false,
18322
+ dotenv: false,
18323
+ envName: false,
18324
+ extend: false,
18325
+ omit$Keys: true,
18326
+ defaults: {},
18327
+ overrides: {}
18328
+ });
18329
+ if (!result.config || Object.keys(result.config).length === 0) {
18330
+ return null;
18331
+ }
18332
+ const rawConfig = result.config;
18333
+ if ("extends" in rawConfig) {
18334
+ throw new Error("The 'extends' feature is not supported in timeback.config.json. " + "Please inline all configuration.");
18335
+ }
18336
+ const { $schema: _schema, ...configWithoutSchema } = rawConfig;
18337
+ return {
18338
+ config: configWithoutSchema,
18339
+ configFile: result.configFile ?? resolve(cwd, configPath ?? CONFIG_FILENAME)
18340
+ };
18341
+ }
18304
18342
  async function parse6() {
18305
18343
  const cwd = process.cwd();
18306
- let rawConfig = null;
18307
- let foundPath = null;
18308
- let loadError = null;
18309
- for (const configPath of FILE_PATTERNS2) {
18310
- const fullPath = resolve(cwd, configPath);
18311
- if (!existsSync(fullPath))
18312
- continue;
18313
- try {
18314
- rawConfig = await importModule(fullPath);
18315
- foundPath = configPath;
18316
- break;
18317
- } catch (err) {
18318
- loadError = err instanceof Error ? err : new Error(String(err));
18319
- foundPath = configPath;
18320
- break;
18344
+ try {
18345
+ const loaded = await loadWithC12(cwd);
18346
+ if (!loaded) {
18347
+ return {
18348
+ success: false,
18349
+ error: `No timeback config found. Create ${CONFIG_FILENAME}`
18350
+ };
18321
18351
  }
18322
- }
18323
- if (loadError && foundPath) {
18324
- return {
18325
- success: false,
18326
- error: `Failed to load ${foundPath}:
18327
- ${loadError.message}`
18328
- };
18329
- }
18330
- if (!rawConfig || !foundPath) {
18352
+ const result = TimebackConfig.safeParse(loaded.config);
18353
+ if (!result.success) {
18354
+ const issues = result.error.issues.map((issue2) => ` - ${issue2.path.join(".")}: ${issue2.message}`).join(`
18355
+ `);
18356
+ return {
18357
+ success: false,
18358
+ error: `Invalid config in ${CONFIG_FILENAME}:
18359
+ ${issues}`
18360
+ };
18361
+ }
18362
+ const version2 = await readPackageVersion(cwd);
18363
+ const baseConfig = { ...result.data, version: version2 };
18331
18364
  return {
18332
- success: false,
18333
- error: `No timeback config found. Create one of: ${FILE_PATTERNS2.join(", ")}`
18365
+ success: true,
18366
+ config: {
18367
+ ...baseConfig,
18368
+ path: loaded.configFile,
18369
+ courseIds: deriveCourseIds(baseConfig)
18370
+ }
18334
18371
  };
18335
- }
18336
- const result = TimebackConfig.safeParse(rawConfig);
18337
- if (!result.success) {
18338
- const issues = result.error.issues.map((issue2) => ` - ${issue2.path.join(".")}: ${issue2.message}`).join(`
18339
- `);
18372
+ } catch (err) {
18340
18373
  return {
18341
18374
  success: false,
18342
- error: `Invalid config in ${foundPath}:
18343
- ${issues}`
18375
+ error: `Failed to load ${CONFIG_FILENAME}:
18376
+ ${err instanceof Error ? err.message : String(err)}`
18344
18377
  };
18345
18378
  }
18346
- const version2 = await readPackageVersion(cwd);
18347
- const baseConfig = { ...result.data, version: version2 };
18348
- return {
18349
- success: true,
18350
- config: {
18351
- ...baseConfig,
18352
- path: `${cwd}/${foundPath}`,
18353
- courseIds: deriveCourseIds(baseConfig)
18354
- }
18355
- };
18356
18379
  }
18357
18380
  function printError2(error48) {
18358
18381
  console.log();
@@ -18360,13 +18383,16 @@ function printError2(error48) {
18360
18383
  console.log();
18361
18384
  console.log(` ${error48}`);
18362
18385
  console.log();
18363
- console.log(` ${dim("Example timeback.config.ts:")}`);
18386
+ console.log(` ${dim("Example timeback.config.json:")}`);
18364
18387
  console.log();
18365
- console.log(` ${yellow("export default {")}`);
18366
- console.log(` ${yellow(" name: 'My Timeback App',")}`);
18367
- console.log(` ${yellow(" courses: [")}`);
18368
- console.log(` ${yellow(" { subject: 'Math', grade: 3 },")}`);
18369
- console.log(` ${yellow(" ],")}`);
18388
+ console.log(` ${yellow("{")}`);
18389
+ console.log(` ${yellow(' "$schema": "https://timeback.dev/schema.json",')}`);
18390
+ console.log(` ${yellow(' "name": "My Timeback App",')}`);
18391
+ console.log(` ${yellow(' "launchUrl": "https://example.com/play",')}`);
18392
+ console.log(` ${yellow(' "sensor": "https://example.com/sensor",')}`);
18393
+ console.log(` ${yellow(' "courses": [')}`);
18394
+ console.log(` ${yellow(' { "subject": "Math", "grade": 3 }')}`);
18395
+ console.log(` ${yellow(" ]")}`);
18370
18396
  console.log(` ${yellow("}")}`);
18371
18397
  console.log();
18372
18398
  }
@@ -49,7 +49,7 @@ export declare function getEffectiveSensors(config: LoadedUserConfig, opts: Serv
49
49
  *
50
50
  * Resolution priority:
51
51
  * 1. CLI course IDs (if provided)
52
- * 2. Config file (timeback.config.ts / playcademy.config.ts)
52
+ * 2. Config file (timeback.config.json / playcademy.config.ts)
53
53
  * 3. Interactive import flow
54
54
  *
55
55
  * @param courseIds - Course IDs from CLI positional args
@@ -10,7 +10,7 @@ export interface ServeOptions extends ParserOptions {
10
10
  export interface ResolvedConfig {
11
11
  userConfig: LoadedUserConfig;
12
12
  environment: Environment;
13
- /** Config filename for logging (e.g., 'timeback.config.ts') */
13
+ /** Config filename for logging (e.g., 'timeback.config.json') */
14
14
  configFile?: string;
15
15
  }
16
16
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/serve/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEnF,MAAM,WAAW,YAAa,SAAQ,aAAa;IAClD,GAAG,CAAC,EAAE,WAAW,CAAA;IACjB,sDAAsD;IACtD,OAAO,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,cAAc;IAC9B,UAAU,EAAE,gBAAgB,CAAA;IAC5B,WAAW,EAAE,WAAW,CAAA;IACxB,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAA;CACnB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/serve/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEnF,MAAM,WAAW,YAAa,SAAQ,aAAa;IAClD,GAAG,CAAC,EAAE,WAAW,CAAA;IACjB,sDAAsD;IACtD,OAAO,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,cAAc;IAC9B,UAAU,EAAE,gBAAgB,CAAA;IAC5B,WAAW,EAAE,WAAW,CAAA;IACxB,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAA;CACnB"}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Onboarding Utilities
3
3
  *
4
- * Handles first-time setup when timeback.config.ts is missing.
4
+ * Handles first-time setup when timeback.config.json is missing.
5
5
  * Runs import flow and returns data in-memory (no file writes).
6
6
  */
7
7
  import type { Environment, EnvironmentCredentials } from '../../../config';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Timeback Config Loading
3
3
  *
4
- * Utilities for loading and validating the user's timeback.config.ts file.
4
+ * Utilities for loading and validating the user's timeback.config.json file.
5
5
  */
6
6
  import type { ConfigParseResult, ParserOptions } from '@timeback/internal-cli-infra';
7
7
  /**
package/dist/index.js CHANGED
@@ -14816,10 +14816,9 @@ var playcademyParser = {
14816
14816
  };
14817
14817
 
14818
14818
  // ../internal/cli-infra/src/config/timeback.ts
14819
- import { existsSync } from "node:fs";
14820
14819
  import { readFile as readFile2 } from "node:fs/promises";
14821
- import { basename, relative, resolve } from "node:path";
14822
- import { pathToFileURL } from "node:url";
14820
+ import { basename, extname, relative, resolve } from "node:path";
14821
+ import { loadConfig as c12LoadConfig } from "c12";
14823
14822
  // ../types/src/zod/primitives.ts
14824
14823
  var IsoDateTimeRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
14825
14824
  var IsoDateTimeString = exports_external.string().min(1).regex(IsoDateTimeRegex, "must be a valid ISO 8601 datetime");
@@ -14835,7 +14834,7 @@ var TimebackSubject = exports_external.enum([
14835
14834
  "Math",
14836
14835
  "None",
14837
14836
  "Other"
14838
- ]);
14837
+ ]).meta({ id: "TimebackSubject", description: "Subject area" });
14839
14838
  var TimebackGrade = exports_external.union([
14840
14839
  exports_external.literal(-1),
14841
14840
  exports_external.literal(0),
@@ -14852,7 +14851,10 @@ var TimebackGrade = exports_external.union([
14852
14851
  exports_external.literal(11),
14853
14852
  exports_external.literal(12),
14854
14853
  exports_external.literal(13)
14855
- ]);
14854
+ ]).meta({
14855
+ id: "TimebackGrade",
14856
+ description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
14857
+ });
14856
14858
  var ScoreStatus = exports_external.enum([
14857
14859
  "exempt",
14858
14860
  "fully graded",
@@ -15084,62 +15086,84 @@ var CaliperListEventsParams = exports_external.object({
15084
15086
  }).strict();
15085
15087
  // ../types/src/zod/config.ts
15086
15088
  var CourseIds = exports_external.object({
15087
- staging: exports_external.string().optional(),
15088
- production: exports_external.string().optional()
15089
- });
15090
- var CourseType = exports_external.enum(["base", "hole-filling", "optional"]);
15091
- var PublishStatus = exports_external.enum(["draft", "testing", "published", "deactivated"]);
15089
+ staging: exports_external.string().meta({ description: "Course ID in staging environment" }).optional(),
15090
+ production: exports_external.string().meta({ description: "Course ID in production environment" }).optional()
15091
+ }).meta({ id: "CourseIds", description: "Environment-specific course IDs (populated by sync)" });
15092
+ var CourseType = exports_external.enum(["base", "hole-filling", "optional"]).meta({ id: "CourseType", description: "Course classification type" });
15093
+ var PublishStatus = exports_external.enum(["draft", "testing", "published", "deactivated"]).meta({ id: "PublishStatus", description: "Course publication status" });
15092
15094
  var CourseGoals = exports_external.object({
15093
- dailyXp: exports_external.number().int().positive().optional(),
15094
- dailyLessons: exports_external.number().int().positive().optional(),
15095
- dailyActiveMinutes: exports_external.number().int().positive().optional(),
15096
- dailyAccuracy: exports_external.number().int().min(0).max(100).optional(),
15097
- dailyMasteredUnits: exports_external.number().int().positive().optional()
15098
- });
15095
+ dailyXp: exports_external.number().int().positive().meta({ description: "Target XP to earn per day" }).optional(),
15096
+ dailyLessons: exports_external.number().int().positive().meta({ description: "Target lessons to complete per day" }).optional(),
15097
+ dailyActiveMinutes: exports_external.number().int().positive().meta({ description: "Target active learning minutes per day" }).optional(),
15098
+ dailyAccuracy: exports_external.number().int().min(0).max(100).meta({ description: "Target accuracy percentage (0-100)" }).optional(),
15099
+ dailyMasteredUnits: exports_external.number().int().positive().meta({ description: "Target units to master per day" }).optional()
15100
+ }).meta({ id: "CourseGoals", description: "Daily learning goals for a course" });
15099
15101
  var CourseMetrics = exports_external.object({
15100
- totalXp: exports_external.number().int().positive().optional(),
15101
- totalLessons: exports_external.number().int().positive().optional(),
15102
- totalGrades: exports_external.number().int().positive().optional()
15103
- });
15102
+ totalXp: exports_external.number().int().positive().meta({ description: "Total XP available in the course" }).optional(),
15103
+ totalLessons: exports_external.number().int().positive().meta({ description: "Total number of lessons/activities" }).optional(),
15104
+ totalGrades: exports_external.number().int().positive().meta({ description: "Total grade levels covered" }).optional()
15105
+ }).meta({ id: "CourseMetrics", description: "Aggregate metrics for a course" });
15104
15106
  var CourseMetadata = exports_external.object({
15105
15107
  courseType: CourseType.optional(),
15106
- isSupplemental: exports_external.boolean().optional(),
15107
- isCustom: exports_external.boolean().optional(),
15108
+ isSupplemental: exports_external.boolean().meta({ description: "Whether this is supplemental to a base course" }).optional(),
15109
+ isCustom: exports_external.boolean().meta({ description: "Whether this is a custom course for an individual student" }).optional(),
15108
15110
  publishStatus: PublishStatus.optional(),
15109
- contactEmail: exports_external.email().optional(),
15110
- primaryApp: exports_external.string().optional(),
15111
+ contactEmail: exports_external.email().meta({ description: "Contact email for course issues" }).optional(),
15112
+ primaryApp: exports_external.string().meta({ description: "Primary application identifier" }).optional(),
15111
15113
  goals: CourseGoals.optional(),
15112
15114
  metrics: CourseMetrics.optional()
15113
- });
15115
+ }).meta({ id: "CourseMetadata", description: "Course metadata (matches API metadata object)" });
15114
15116
  var CourseDefaults = exports_external.object({
15115
- courseCode: exports_external.string().optional(),
15116
- level: exports_external.string().optional(),
15117
+ courseCode: exports_external.string().meta({ description: "Course code (e.g., 'MATH101')" }).optional(),
15118
+ level: exports_external.string().meta({ description: "Course level (e.g., 'AP', 'Honors')" }).optional(),
15117
15119
  metadata: CourseMetadata.optional()
15120
+ }).meta({
15121
+ id: "CourseDefaults",
15122
+ description: "Default properties that apply to all courses unless overridden"
15118
15123
  });
15119
15124
  var CourseEnvOverrides = exports_external.object({
15120
- level: exports_external.string().optional(),
15121
- sensor: exports_external.string().url().optional(),
15122
- launchUrl: exports_external.string().url().optional(),
15125
+ level: exports_external.string().meta({ description: "Course level for this environment" }).optional(),
15126
+ sensor: exports_external.url().meta({ description: "Caliper sensor endpoint URL for this environment" }).optional(),
15127
+ launchUrl: exports_external.url().meta({ description: "LTI launch URL for this environment" }).optional(),
15123
15128
  metadata: CourseMetadata.optional()
15129
+ }).meta({
15130
+ id: "CourseEnvOverrides",
15131
+ description: "Environment-specific course overrides (non-identity fields)"
15124
15132
  });
15125
15133
  var CourseOverrides = exports_external.object({
15126
- staging: CourseEnvOverrides.optional(),
15127
- production: CourseEnvOverrides.optional()
15128
- });
15134
+ staging: CourseEnvOverrides.meta({
15135
+ description: "Overrides for staging environment"
15136
+ }).optional(),
15137
+ production: CourseEnvOverrides.meta({
15138
+ description: "Overrides for production environment"
15139
+ }).optional()
15140
+ }).meta({ id: "CourseOverrides", description: "Per-environment course overrides" });
15129
15141
  var CourseConfig = CourseDefaults.extend({
15130
- subject: TimebackSubject,
15131
- grade: TimebackGrade.optional(),
15142
+ subject: TimebackSubject.meta({ description: "Subject area for this course" }),
15143
+ grade: TimebackGrade.meta({
15144
+ description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
15145
+ }).optional(),
15132
15146
  ids: CourseIds.nullable().optional(),
15133
- sensor: exports_external.string().url().optional(),
15134
- launchUrl: exports_external.string().url().optional(),
15147
+ sensor: exports_external.url().meta({ description: "Caliper sensor endpoint URL for this course" }).optional(),
15148
+ launchUrl: exports_external.url().meta({ description: "LTI launch URL for this course" }).optional(),
15135
15149
  overrides: CourseOverrides.optional()
15150
+ }).meta({
15151
+ id: "CourseConfig",
15152
+ description: "Configuration for a single course. Must have either grade or courseCode (or both)."
15136
15153
  });
15137
15154
  var TimebackConfig = exports_external.object({
15138
- name: exports_external.string().min(1, "App name is required"),
15139
- defaults: CourseDefaults.optional(),
15140
- courses: exports_external.array(CourseConfig).min(1, "At least one course is required"),
15141
- sensor: exports_external.string().url().optional(),
15142
- launchUrl: exports_external.string().url().optional()
15155
+ $schema: exports_external.string().meta({ description: "JSON Schema reference for editor support" }).optional(),
15156
+ name: exports_external.string().min(1, "App name is required").meta({ description: "Display name for your app" }),
15157
+ defaults: CourseDefaults.meta({
15158
+ description: "Default properties applied to all courses"
15159
+ }).optional(),
15160
+ courses: exports_external.array(CourseConfig).min(1, "At least one course is required").meta({ description: "Courses available in this app" }),
15161
+ sensor: exports_external.url().meta({ description: "Default Caliper sensor endpoint URL for all courses" }).optional(),
15162
+ launchUrl: exports_external.url().meta({ description: "Default LTI launch URL for all courses" }).optional()
15163
+ }).meta({
15164
+ id: "TimebackConfig",
15165
+ title: "Timeback Config",
15166
+ description: "Configuration schema for timeback.config.json files"
15143
15167
  }).refine((config2) => {
15144
15168
  return config2.courses.every((c) => c.grade !== undefined || c.courseCode !== undefined);
15145
15169
  }, {
@@ -16154,26 +16178,10 @@ var QtiLessonFeedbackInput = exports_external.object({
16154
16178
  humanApproved: exports_external.boolean().optional()
16155
16179
  }).strict();
16156
16180
  // ../internal/cli-infra/src/config/timeback.ts
16157
- var FILE_PATTERNS2 = ["timeback.config.ts", "timeback.config.js", "timeback.config.mjs"];
16158
- var isBun = typeof globalThis.Bun !== "undefined";
16159
- var jitiInstance = null;
16160
- async function getJiti() {
16161
- if (jitiInstance)
16162
- return jitiInstance;
16163
- const { createJiti } = await import("jiti");
16164
- jitiInstance = createJiti(import.meta.url, { fsCache: false });
16165
- return jitiInstance;
16166
- }
16167
- async function importModule(fullPath) {
16168
- let module;
16169
- if (isBun) {
16170
- const fileUrl = pathToFileURL(fullPath).href;
16171
- module = await import(fileUrl);
16172
- } else {
16173
- const jiti = await getJiti();
16174
- module = await jiti.import(fullPath);
16175
- }
16176
- return module.default ?? module;
16181
+ var CONFIG_FILENAME = "timeback.config.json";
16182
+ var FILE_PATTERNS2 = [CONFIG_FILENAME];
16183
+ function isJsonConfigPath(configPath) {
16184
+ return extname(configPath).toLowerCase() === ".json";
16177
16185
  }
16178
16186
  function deriveCourseIds(config3) {
16179
16187
  const result = { staging: [], production: [] };
@@ -16191,58 +16199,73 @@ async function readPackageVersion(cwd) {
16191
16199
  return "0.0.0";
16192
16200
  }
16193
16201
  }
16202
+ async function loadWithC12(cwd, configPath) {
16203
+ if (configPath && !isJsonConfigPath(configPath)) {
16204
+ throw new Error(`Config file must be JSON (.json): ${configPath}`);
16205
+ }
16206
+ const result = await c12LoadConfig({
16207
+ cwd,
16208
+ name: "timeback",
16209
+ configFile: configPath ?? CONFIG_FILENAME,
16210
+ rcFile: false,
16211
+ packageJson: false,
16212
+ dotenv: false,
16213
+ envName: false,
16214
+ extend: false,
16215
+ omit$Keys: true,
16216
+ defaults: {},
16217
+ overrides: {}
16218
+ });
16219
+ if (!result.config || Object.keys(result.config).length === 0) {
16220
+ return null;
16221
+ }
16222
+ const rawConfig = result.config;
16223
+ if ("extends" in rawConfig) {
16224
+ throw new Error("The 'extends' feature is not supported in timeback.config.json. " + "Please inline all configuration.");
16225
+ }
16226
+ const { $schema: _schema, ...configWithoutSchema } = rawConfig;
16227
+ return {
16228
+ config: configWithoutSchema,
16229
+ configFile: result.configFile ?? resolve(cwd, configPath ?? CONFIG_FILENAME)
16230
+ };
16231
+ }
16194
16232
  async function parse6() {
16195
16233
  const cwd = process.cwd();
16196
- let rawConfig = null;
16197
- let foundPath = null;
16198
- let loadError = null;
16199
- for (const configPath of FILE_PATTERNS2) {
16200
- const fullPath = resolve(cwd, configPath);
16201
- if (!existsSync(fullPath))
16202
- continue;
16203
- try {
16204
- rawConfig = await importModule(fullPath);
16205
- foundPath = configPath;
16206
- break;
16207
- } catch (err) {
16208
- loadError = err instanceof Error ? err : new Error(String(err));
16209
- foundPath = configPath;
16210
- break;
16234
+ try {
16235
+ const loaded = await loadWithC12(cwd);
16236
+ if (!loaded) {
16237
+ return {
16238
+ success: false,
16239
+ error: `No timeback config found. Create ${CONFIG_FILENAME}`
16240
+ };
16211
16241
  }
16212
- }
16213
- if (loadError && foundPath) {
16214
- return {
16215
- success: false,
16216
- error: `Failed to load ${foundPath}:
16217
- ${loadError.message}`
16218
- };
16219
- }
16220
- if (!rawConfig || !foundPath) {
16242
+ const result = TimebackConfig.safeParse(loaded.config);
16243
+ if (!result.success) {
16244
+ const issues = result.error.issues.map((issue2) => ` - ${issue2.path.join(".")}: ${issue2.message}`).join(`
16245
+ `);
16246
+ return {
16247
+ success: false,
16248
+ error: `Invalid config in ${CONFIG_FILENAME}:
16249
+ ${issues}`
16250
+ };
16251
+ }
16252
+ const version2 = await readPackageVersion(cwd);
16253
+ const baseConfig = { ...result.data, version: version2 };
16221
16254
  return {
16222
- success: false,
16223
- error: `No timeback config found. Create one of: ${FILE_PATTERNS2.join(", ")}`
16255
+ success: true,
16256
+ config: {
16257
+ ...baseConfig,
16258
+ path: loaded.configFile,
16259
+ courseIds: deriveCourseIds(baseConfig)
16260
+ }
16224
16261
  };
16225
- }
16226
- const result = TimebackConfig.safeParse(rawConfig);
16227
- if (!result.success) {
16228
- const issues = result.error.issues.map((issue2) => ` - ${issue2.path.join(".")}: ${issue2.message}`).join(`
16229
- `);
16262
+ } catch (err) {
16230
16263
  return {
16231
16264
  success: false,
16232
- error: `Invalid config in ${foundPath}:
16233
- ${issues}`
16265
+ error: `Failed to load ${CONFIG_FILENAME}:
16266
+ ${err instanceof Error ? err.message : String(err)}`
16234
16267
  };
16235
16268
  }
16236
- const version2 = await readPackageVersion(cwd);
16237
- const baseConfig = { ...result.data, version: version2 };
16238
- return {
16239
- success: true,
16240
- config: {
16241
- ...baseConfig,
16242
- path: `${cwd}/${foundPath}`,
16243
- courseIds: deriveCourseIds(baseConfig)
16244
- }
16245
- };
16246
16269
  }
16247
16270
  function printError2(error48) {
16248
16271
  console.log();
@@ -16250,13 +16273,16 @@ function printError2(error48) {
16250
16273
  console.log();
16251
16274
  console.log(` ${error48}`);
16252
16275
  console.log();
16253
- console.log(` ${dim("Example timeback.config.ts:")}`);
16276
+ console.log(` ${dim("Example timeback.config.json:")}`);
16254
16277
  console.log();
16255
- console.log(` ${yellow("export default {")}`);
16256
- console.log(` ${yellow(" name: 'My Timeback App',")}`);
16257
- console.log(` ${yellow(" courses: [")}`);
16258
- console.log(` ${yellow(" { subject: 'Math', grade: 3 },")}`);
16259
- console.log(` ${yellow(" ],")}`);
16278
+ console.log(` ${yellow("{")}`);
16279
+ console.log(` ${yellow(' "$schema": "https://timeback.dev/schema.json",')}`);
16280
+ console.log(` ${yellow(' "name": "My Timeback App",')}`);
16281
+ console.log(` ${yellow(' "launchUrl": "https://example.com/play",')}`);
16282
+ console.log(` ${yellow(' "sensor": "https://example.com/sensor",')}`);
16283
+ console.log(` ${yellow(' "courses": [')}`);
16284
+ console.log(` ${yellow(' { "subject": "Math", "grade": 3 }')}`);
16285
+ console.log(` ${yellow(" ]")}`);
16260
16286
  console.log(` ${yellow("}")}`);
16261
16287
  console.log();
16262
16288
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "timeback-studio",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -28,10 +28,10 @@
28
28
  "@clack/prompts": "^0.11.0",
29
29
  "@hono/node-server": "^1.19.7",
30
30
  "@timeback/core": "0.1.3",
31
+ "c12": "^3.3.3",
31
32
  "colorette": "^2.0.20",
32
33
  "commander": "^14.0.2",
33
34
  "hono": "^4.11.1",
34
- "jiti": "^2.6.1",
35
35
  "zod": "^4.2.1"
36
36
  },
37
37
  "devDependencies": {