timeback 0.1.6 → 0.1.7
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 +70 -1
- package/dist/cli.js +1908 -1526
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -31971,14 +31971,20 @@ var TimebackConfig = exports_external2.object({
|
|
|
31971
31971
|
message: "Duplicate courseCode found; each must be unique",
|
|
31972
31972
|
path: ["courses"]
|
|
31973
31973
|
}).refine((config22) => {
|
|
31974
|
-
return config22.courses.every((c) =>
|
|
31975
|
-
|
|
31976
|
-
|
|
31977
|
-
|
|
31978
|
-
|
|
31979
|
-
|
|
31974
|
+
return config22.courses.every((c) => {
|
|
31975
|
+
if (c.sensor !== undefined || config22.sensor !== undefined) {
|
|
31976
|
+
return true;
|
|
31977
|
+
}
|
|
31978
|
+
const launchUrls = [
|
|
31979
|
+
c.launchUrl,
|
|
31980
|
+
config22.launchUrl,
|
|
31981
|
+
c.overrides?.staging?.launchUrl,
|
|
31982
|
+
c.overrides?.production?.launchUrl
|
|
31983
|
+
].filter(Boolean);
|
|
31984
|
+
return launchUrls.length > 0;
|
|
31985
|
+
});
|
|
31980
31986
|
}, {
|
|
31981
|
-
message: "Each course must have an effective
|
|
31987
|
+
message: "Each course must have an effective sensor. Either set `sensor` explicitly (top-level or per-course), or provide a `launchUrl` so sensor can be derived from its origin.",
|
|
31982
31988
|
path: ["courses"]
|
|
31983
31989
|
});
|
|
31984
31990
|
var EdubridgeDateString = exports_external2.union([IsoDateString, IsoDateTimeString]);
|
|
@@ -48374,14 +48380,20 @@ var TimebackConfig2 = exports_external22.object({
|
|
|
48374
48380
|
message: "Duplicate courseCode found; each must be unique",
|
|
48375
48381
|
path: ["courses"]
|
|
48376
48382
|
}).refine((config222) => {
|
|
48377
|
-
return config222.courses.every((c) =>
|
|
48378
|
-
|
|
48379
|
-
|
|
48380
|
-
|
|
48381
|
-
|
|
48382
|
-
|
|
48383
|
+
return config222.courses.every((c) => {
|
|
48384
|
+
if (c.sensor !== undefined || config222.sensor !== undefined) {
|
|
48385
|
+
return true;
|
|
48386
|
+
}
|
|
48387
|
+
const launchUrls = [
|
|
48388
|
+
c.launchUrl,
|
|
48389
|
+
config222.launchUrl,
|
|
48390
|
+
c.overrides?.staging?.launchUrl,
|
|
48391
|
+
c.overrides?.production?.launchUrl
|
|
48392
|
+
].filter(Boolean);
|
|
48393
|
+
return launchUrls.length > 0;
|
|
48394
|
+
});
|
|
48383
48395
|
}, {
|
|
48384
|
-
message: "Each course must have an effective
|
|
48396
|
+
message: "Each course must have an effective sensor. Either set `sensor` explicitly (top-level or per-course), or provide a `launchUrl` so sensor can be derived from its origin.",
|
|
48385
48397
|
path: ["courses"]
|
|
48386
48398
|
});
|
|
48387
48399
|
var EdubridgeDateString2 = exports_external22.union([IsoDateString2, IsoDateTimeString2]);
|
|
@@ -65812,14 +65824,20 @@ var TimebackConfig3 = exports_external3.object({
|
|
|
65812
65824
|
message: "Duplicate courseCode found; each must be unique",
|
|
65813
65825
|
path: ["courses"]
|
|
65814
65826
|
}).refine((config222) => {
|
|
65815
|
-
return config222.courses.every((c) =>
|
|
65816
|
-
|
|
65817
|
-
|
|
65818
|
-
|
|
65819
|
-
|
|
65820
|
-
|
|
65827
|
+
return config222.courses.every((c) => {
|
|
65828
|
+
if (c.sensor !== undefined || config222.sensor !== undefined) {
|
|
65829
|
+
return true;
|
|
65830
|
+
}
|
|
65831
|
+
const launchUrls = [
|
|
65832
|
+
c.launchUrl,
|
|
65833
|
+
config222.launchUrl,
|
|
65834
|
+
c.overrides?.staging?.launchUrl,
|
|
65835
|
+
c.overrides?.production?.launchUrl
|
|
65836
|
+
].filter(Boolean);
|
|
65837
|
+
return launchUrls.length > 0;
|
|
65838
|
+
});
|
|
65821
65839
|
}, {
|
|
65822
|
-
message: "Each course must have an effective
|
|
65840
|
+
message: "Each course must have an effective sensor. Either set `sensor` explicitly (top-level or per-course), or provide a `launchUrl` so sensor can be derived from its origin.",
|
|
65823
65841
|
path: ["courses"]
|
|
65824
65842
|
});
|
|
65825
65843
|
var EdubridgeDateString3 = exports_external3.union([IsoDateString3, IsoDateTimeString3]);
|
|
@@ -83444,14 +83462,20 @@ var TimebackConfig4 = exports_external4.object({
|
|
|
83444
83462
|
message: "Duplicate courseCode found; each must be unique",
|
|
83445
83463
|
path: ["courses"]
|
|
83446
83464
|
}).refine((config222) => {
|
|
83447
|
-
return config222.courses.every((c) =>
|
|
83448
|
-
|
|
83449
|
-
|
|
83450
|
-
|
|
83451
|
-
|
|
83452
|
-
|
|
83465
|
+
return config222.courses.every((c) => {
|
|
83466
|
+
if (c.sensor !== undefined || config222.sensor !== undefined) {
|
|
83467
|
+
return true;
|
|
83468
|
+
}
|
|
83469
|
+
const launchUrls = [
|
|
83470
|
+
c.launchUrl,
|
|
83471
|
+
config222.launchUrl,
|
|
83472
|
+
c.overrides?.staging?.launchUrl,
|
|
83473
|
+
c.overrides?.production?.launchUrl
|
|
83474
|
+
].filter(Boolean);
|
|
83475
|
+
return launchUrls.length > 0;
|
|
83476
|
+
});
|
|
83453
83477
|
}, {
|
|
83454
|
-
message: "Each course must have an effective
|
|
83478
|
+
message: "Each course must have an effective sensor. Either set `sensor` explicitly (top-level or per-course), or provide a `launchUrl` so sensor can be derived from its origin.",
|
|
83455
83479
|
path: ["courses"]
|
|
83456
83480
|
});
|
|
83457
83481
|
var EdubridgeDateString4 = exports_external4.union([IsoDateString4, IsoDateTimeString4]);
|
|
@@ -100106,14 +100130,20 @@ var TimebackConfig5 = exports_external5.object({
|
|
|
100106
100130
|
message: "Duplicate courseCode found; each must be unique",
|
|
100107
100131
|
path: ["courses"]
|
|
100108
100132
|
}).refine((config222) => {
|
|
100109
|
-
return config222.courses.every((c) =>
|
|
100110
|
-
|
|
100111
|
-
|
|
100112
|
-
|
|
100113
|
-
|
|
100114
|
-
|
|
100133
|
+
return config222.courses.every((c) => {
|
|
100134
|
+
if (c.sensor !== undefined || config222.sensor !== undefined) {
|
|
100135
|
+
return true;
|
|
100136
|
+
}
|
|
100137
|
+
const launchUrls = [
|
|
100138
|
+
c.launchUrl,
|
|
100139
|
+
config222.launchUrl,
|
|
100140
|
+
c.overrides?.staging?.launchUrl,
|
|
100141
|
+
c.overrides?.production?.launchUrl
|
|
100142
|
+
].filter(Boolean);
|
|
100143
|
+
return launchUrls.length > 0;
|
|
100144
|
+
});
|
|
100115
100145
|
}, {
|
|
100116
|
-
message: "Each course must have an effective
|
|
100146
|
+
message: "Each course must have an effective sensor. Either set `sensor` explicitly (top-level or per-course), or provide a `launchUrl` so sensor can be derived from its origin.",
|
|
100117
100147
|
path: ["courses"]
|
|
100118
100148
|
});
|
|
100119
100149
|
var EdubridgeDateString5 = exports_external5.union([IsoDateString5, IsoDateTimeString5]);
|
|
@@ -102332,14 +102362,20 @@ var TimebackConfig6 = exports_external.object({
|
|
|
102332
102362
|
message: "Duplicate courseCode found; each must be unique",
|
|
102333
102363
|
path: ["courses"]
|
|
102334
102364
|
}).refine((config6) => {
|
|
102335
|
-
return config6.courses.every((c) =>
|
|
102336
|
-
|
|
102337
|
-
|
|
102338
|
-
|
|
102339
|
-
|
|
102340
|
-
|
|
102365
|
+
return config6.courses.every((c) => {
|
|
102366
|
+
if (c.sensor !== undefined || config6.sensor !== undefined) {
|
|
102367
|
+
return true;
|
|
102368
|
+
}
|
|
102369
|
+
const launchUrls = [
|
|
102370
|
+
c.launchUrl,
|
|
102371
|
+
config6.launchUrl,
|
|
102372
|
+
c.overrides?.staging?.launchUrl,
|
|
102373
|
+
c.overrides?.production?.launchUrl
|
|
102374
|
+
].filter(Boolean);
|
|
102375
|
+
return launchUrls.length > 0;
|
|
102376
|
+
});
|
|
102341
102377
|
}, {
|
|
102342
|
-
message: "Each course must have an effective
|
|
102378
|
+
message: "Each course must have an effective sensor. Either set `sensor` explicitly (top-level or per-course), or provide a `launchUrl` so sensor can be derived from its origin.",
|
|
102343
102379
|
path: ["courses"]
|
|
102344
102380
|
});
|
|
102345
102381
|
// ../types/src/zod/edubridge.ts
|
|
@@ -103453,6 +103489,46 @@ function toCourseConfig(course, env2) {
|
|
|
103453
103489
|
}
|
|
103454
103490
|
return config7;
|
|
103455
103491
|
}
|
|
103492
|
+
async function inferLaunchUrl(client, courseId) {
|
|
103493
|
+
try {
|
|
103494
|
+
const scopedCourse = client.oneroster.courses(courseId);
|
|
103495
|
+
const components = await scopedCourse.components({ where: { status: "active" } });
|
|
103496
|
+
const componentResourceIds = [];
|
|
103497
|
+
for (const component of components) {
|
|
103498
|
+
if (!component.sourcedId)
|
|
103499
|
+
continue;
|
|
103500
|
+
const crs = await client.oneroster.courses.componentResources({
|
|
103501
|
+
where: { "courseComponent.sourcedId": component.sourcedId, status: "active" }
|
|
103502
|
+
});
|
|
103503
|
+
for (const cr of crs) {
|
|
103504
|
+
if (cr.resource?.sourcedId) {
|
|
103505
|
+
componentResourceIds.push(cr.resource.sourcedId);
|
|
103506
|
+
}
|
|
103507
|
+
}
|
|
103508
|
+
}
|
|
103509
|
+
if (componentResourceIds.length === 0) {
|
|
103510
|
+
return;
|
|
103511
|
+
}
|
|
103512
|
+
const resourceIds = [...new Set(componentResourceIds)];
|
|
103513
|
+
const resources = await client.oneroster.resources.listAll({
|
|
103514
|
+
where: { sourcedId: { in: resourceIds } }
|
|
103515
|
+
});
|
|
103516
|
+
const launchUrls = new Set;
|
|
103517
|
+
for (const resource of resources) {
|
|
103518
|
+
const metadata = resource.metadata;
|
|
103519
|
+
const launchUrl = metadata?.launchUrl;
|
|
103520
|
+
if (typeof launchUrl === "string" && launchUrl.length > 0) {
|
|
103521
|
+
launchUrls.add(launchUrl);
|
|
103522
|
+
}
|
|
103523
|
+
}
|
|
103524
|
+
if (launchUrls.size === 1) {
|
|
103525
|
+
return [...launchUrls][0];
|
|
103526
|
+
}
|
|
103527
|
+
return;
|
|
103528
|
+
} catch {
|
|
103529
|
+
return;
|
|
103530
|
+
}
|
|
103531
|
+
}
|
|
103456
103532
|
// ../internal/cli-infra/src/search/search.ts
|
|
103457
103533
|
function searchCourses(client, query, max = 50) {
|
|
103458
103534
|
return client.oneroster.courses.listAll({
|
|
@@ -103475,11 +103551,11 @@ async function promptSelectEnv(configuredEnvs) {
|
|
|
103475
103551
|
}
|
|
103476
103552
|
async function promptAppName() {
|
|
103477
103553
|
const name = await he({
|
|
103478
|
-
message: "
|
|
103554
|
+
message: "Search for your app",
|
|
103479
103555
|
placeholder: "My Timeback App",
|
|
103480
103556
|
validate: (value) => {
|
|
103481
103557
|
if (!value.trim())
|
|
103482
|
-
return "
|
|
103558
|
+
return "Please enter a search term";
|
|
103483
103559
|
}
|
|
103484
103560
|
});
|
|
103485
103561
|
if (pD(name))
|
|
@@ -103584,6 +103660,40 @@ async function selectCourses(client, initialResults, filterFn) {
|
|
|
103584
103660
|
results = filter(moreResults);
|
|
103585
103661
|
}
|
|
103586
103662
|
}
|
|
103663
|
+
async function enrichCoursesWithLaunchUrls(client, courses) {
|
|
103664
|
+
if (courses.length === 0)
|
|
103665
|
+
return courses;
|
|
103666
|
+
const s = Y2();
|
|
103667
|
+
let didStop = false;
|
|
103668
|
+
const stopOnce = (message) => {
|
|
103669
|
+
if (didStop)
|
|
103670
|
+
return;
|
|
103671
|
+
s.stop(message);
|
|
103672
|
+
didStop = true;
|
|
103673
|
+
};
|
|
103674
|
+
s.start("Fetching course details...");
|
|
103675
|
+
const enriched = [];
|
|
103676
|
+
try {
|
|
103677
|
+
for (const course of courses) {
|
|
103678
|
+
const courseId = course.ids?.staging ?? course.ids?.production;
|
|
103679
|
+
if (!courseId) {
|
|
103680
|
+
enriched.push(course);
|
|
103681
|
+
continue;
|
|
103682
|
+
}
|
|
103683
|
+
const launchUrl = await inferLaunchUrl(client, courseId);
|
|
103684
|
+
if (launchUrl && !course.launchUrl) {
|
|
103685
|
+
enriched.push({ ...course, launchUrl });
|
|
103686
|
+
} else {
|
|
103687
|
+
enriched.push(course);
|
|
103688
|
+
}
|
|
103689
|
+
}
|
|
103690
|
+
stopOnce("Course details loaded");
|
|
103691
|
+
return enriched;
|
|
103692
|
+
} catch (error57) {
|
|
103693
|
+
stopOnce("Failed to load course details");
|
|
103694
|
+
throw error57;
|
|
103695
|
+
}
|
|
103696
|
+
}
|
|
103587
103697
|
async function searchAndSelectCourses(options) {
|
|
103588
103698
|
const { client, environment, appName: existingAppName, excludeCourseIds = [] } = options;
|
|
103589
103699
|
const filterExcluded = (courses) => {
|
|
@@ -103606,11 +103716,11 @@ async function searchAndSelectCourses(options) {
|
|
|
103606
103716
|
return { success: false, cancelled: true };
|
|
103607
103717
|
}
|
|
103608
103718
|
const courseConfigs = courses.map((c) => toCourseConfig(c, environment));
|
|
103609
|
-
|
|
103719
|
+
const enrichedConfigs = await enrichCoursesWithLaunchUrls(client, courseConfigs);
|
|
103610
103720
|
return {
|
|
103611
103721
|
success: true,
|
|
103612
103722
|
appName,
|
|
103613
|
-
courses:
|
|
103723
|
+
courses: enrichedConfigs,
|
|
103614
103724
|
environment
|
|
103615
103725
|
};
|
|
103616
103726
|
} catch (error57) {
|
|
@@ -103626,35 +103736,6 @@ function isValidUrl(url6) {
|
|
|
103626
103736
|
return false;
|
|
103627
103737
|
}
|
|
103628
103738
|
}
|
|
103629
|
-
function deriveSensorFromLaunchUrl(launchUrl) {
|
|
103630
|
-
try {
|
|
103631
|
-
const url6 = new URL(launchUrl);
|
|
103632
|
-
return url6.origin;
|
|
103633
|
-
} catch {
|
|
103634
|
-
return;
|
|
103635
|
-
}
|
|
103636
|
-
}
|
|
103637
|
-
async function promptSensor(options) {
|
|
103638
|
-
const { defaultValue } = options ?? {};
|
|
103639
|
-
const input = await he({
|
|
103640
|
-
message: "Sensor URL (your app origin, used for activity tracking)",
|
|
103641
|
-
placeholder: "https://myapp.example.com",
|
|
103642
|
-
defaultValue,
|
|
103643
|
-
validate: (value) => {
|
|
103644
|
-
if (!value.trim()) {
|
|
103645
|
-
return "Sensor URL is required for activity tracking";
|
|
103646
|
-
}
|
|
103647
|
-
if (!isValidUrl(value.trim())) {
|
|
103648
|
-
return "Invalid URL format";
|
|
103649
|
-
}
|
|
103650
|
-
return;
|
|
103651
|
-
}
|
|
103652
|
-
});
|
|
103653
|
-
if (isCancelled(input)) {
|
|
103654
|
-
return;
|
|
103655
|
-
}
|
|
103656
|
-
return input.trim();
|
|
103657
|
-
}
|
|
103658
103739
|
async function promptLaunchUrl() {
|
|
103659
103740
|
const input = await he({
|
|
103660
103741
|
message: "Launch URL (where students go when clicking your app in dashboards)",
|
|
@@ -104645,30 +104726,43 @@ function run(cmd, args, cwd, silent = false) {
|
|
|
104645
104726
|
child.on("error", () => resolvePromise(1));
|
|
104646
104727
|
});
|
|
104647
104728
|
}
|
|
104729
|
+
function getPrettierBin(cwd) {
|
|
104730
|
+
return resolve3(cwd, "node_modules", ".bin", process.platform === "win32" ? "prettier.cmd" : "prettier");
|
|
104731
|
+
}
|
|
104732
|
+
function runPrettierUsingRunner(cwd, args) {
|
|
104733
|
+
const { packageManager } = detectPackageManager(cwd);
|
|
104734
|
+
const dlx = getDlxCommand(packageManager, "prettier");
|
|
104735
|
+
return run(dlx.cmd, [...dlx.args, ...args], cwd, true);
|
|
104736
|
+
}
|
|
104648
104737
|
async function formatWithPrettier(options) {
|
|
104649
|
-
const { cwd, filePath,
|
|
104650
|
-
const prettierBin =
|
|
104738
|
+
const { cwd, filePath, silent = false } = options;
|
|
104739
|
+
const prettierBin = getPrettierBin(cwd);
|
|
104651
104740
|
const args = ["--write", filePath];
|
|
104652
|
-
|
|
104653
|
-
|
|
104654
|
-
|
|
104655
|
-
|
|
104656
|
-
if (
|
|
104741
|
+
const s = silent ? null : Y2();
|
|
104742
|
+
s?.start("Formatting config file...");
|
|
104743
|
+
try {
|
|
104744
|
+
let code;
|
|
104745
|
+
if (existsSync3(prettierBin)) {
|
|
104746
|
+
code = await run(prettierBin, args, cwd, true);
|
|
104747
|
+
} else {
|
|
104748
|
+
code = await runPrettierUsingRunner(cwd, args);
|
|
104749
|
+
}
|
|
104750
|
+
if (code === 0) {
|
|
104751
|
+
s?.stop("Formatted config file");
|
|
104657
104752
|
return true;
|
|
104658
|
-
|
|
104659
|
-
|
|
104753
|
+
}
|
|
104754
|
+
s?.stop();
|
|
104755
|
+
if (!silent) {
|
|
104756
|
+
M2.warn("Failed to format config file");
|
|
104757
|
+
}
|
|
104758
|
+
return false;
|
|
104759
|
+
} catch {
|
|
104760
|
+
s?.stop();
|
|
104761
|
+
if (!silent) {
|
|
104762
|
+
M2.warn("Failed to format config file");
|
|
104763
|
+
}
|
|
104660
104764
|
return false;
|
|
104661
104765
|
}
|
|
104662
|
-
const { packageManager } = detectPackageManager(cwd);
|
|
104663
|
-
const dlx = getDlxCommand(packageManager, "prettier");
|
|
104664
|
-
if (!silent)
|
|
104665
|
-
logger.info(`Formatting ${filePath} with ${dlx.cmd}...`);
|
|
104666
|
-
const code = await run(dlx.cmd, [...dlx.args, ...args], cwd, silent);
|
|
104667
|
-
if (code === 0)
|
|
104668
|
-
return true;
|
|
104669
|
-
if (!silent)
|
|
104670
|
-
logger.warn(`Prettier failed (exit ${code}).`);
|
|
104671
|
-
return false;
|
|
104672
104766
|
}
|
|
104673
104767
|
// src/lib/config/generate.ts
|
|
104674
104768
|
var SCHEMA_URL = "https://timeback.dev/schema.json";
|
|
@@ -104826,6 +104920,32 @@ function formatCourse(course) {
|
|
|
104826
104920
|
}
|
|
104827
104921
|
return orderKeys(result, COURSE_KEY_ORDER);
|
|
104828
104922
|
}
|
|
104923
|
+
function normalizeLaunchUrls(config7) {
|
|
104924
|
+
const allCoursesHaveLaunchUrl = config7.courses.every((c) => c.launchUrl !== undefined);
|
|
104925
|
+
if (!allCoursesHaveLaunchUrl) {
|
|
104926
|
+
return config7;
|
|
104927
|
+
}
|
|
104928
|
+
const courseLaunchUrls = config7.courses.map((c) => c.launchUrl);
|
|
104929
|
+
const uniqueUrls = new Set(courseLaunchUrls);
|
|
104930
|
+
if (uniqueUrls.size !== 1) {
|
|
104931
|
+
return config7;
|
|
104932
|
+
}
|
|
104933
|
+
const sharedUrl = courseLaunchUrls[0];
|
|
104934
|
+
if (config7.launchUrl && config7.launchUrl !== sharedUrl) {
|
|
104935
|
+
return config7;
|
|
104936
|
+
}
|
|
104937
|
+
return {
|
|
104938
|
+
...config7,
|
|
104939
|
+
launchUrl: sharedUrl,
|
|
104940
|
+
courses: config7.courses.map((course) => {
|
|
104941
|
+
if (course.launchUrl === sharedUrl) {
|
|
104942
|
+
const { launchUrl: _3, ...rest } = course;
|
|
104943
|
+
return rest;
|
|
104944
|
+
}
|
|
104945
|
+
return course;
|
|
104946
|
+
})
|
|
104947
|
+
};
|
|
104948
|
+
}
|
|
104829
104949
|
function generateConfigContent(config7) {
|
|
104830
104950
|
const output2 = {
|
|
104831
104951
|
$schema: SCHEMA_URL,
|
|
@@ -104848,9 +104968,13 @@ function generateConfigContent(config7) {
|
|
|
104848
104968
|
}
|
|
104849
104969
|
// src/lib/config/loader.ts
|
|
104850
104970
|
import { writeFile as writeFile2 } from "node:fs/promises";
|
|
104851
|
-
async function saveConfig(configPath, config7) {
|
|
104971
|
+
async function saveConfig(configPath, config7, options = {}) {
|
|
104972
|
+
const { format = false, cwd = process.cwd(), silent = true } = options;
|
|
104852
104973
|
const content = generateConfigContent(config7);
|
|
104853
104974
|
await writeFile2(configPath, content, "utf-8");
|
|
104975
|
+
if (format) {
|
|
104976
|
+
await formatWithPrettier({ cwd, filePath: configPath, silent });
|
|
104977
|
+
}
|
|
104854
104978
|
}
|
|
104855
104979
|
// src/lib/compare.ts
|
|
104856
104980
|
function compareValues(local, remote, path, changes, options = {}) {
|
|
@@ -104921,32 +105045,84 @@ function logErrors(errors3) {
|
|
|
104921
105045
|
}
|
|
104922
105046
|
}
|
|
104923
105047
|
|
|
104924
|
-
// src/
|
|
104925
|
-
|
|
104926
|
-
|
|
104927
|
-
|
|
104928
|
-
|
|
104929
|
-
|
|
104930
|
-
s.start("Fetching course data...");
|
|
104931
|
-
const authOk = await initAuth({ env: first.env });
|
|
104932
|
-
if (!authOk) {
|
|
104933
|
-
s.stop("Could not authenticate");
|
|
105048
|
+
// src/version.ts
|
|
105049
|
+
var cliVersion = "0.1.7";
|
|
105050
|
+
|
|
105051
|
+
// src/lib/metadata.ts
|
|
105052
|
+
function deepMerge(base, override) {
|
|
105053
|
+
if (!base && !override)
|
|
104934
105054
|
return;
|
|
105055
|
+
if (!base)
|
|
105056
|
+
return override;
|
|
105057
|
+
if (!override)
|
|
105058
|
+
return base;
|
|
105059
|
+
const result = { ...base };
|
|
105060
|
+
for (const key of Object.keys(override)) {
|
|
105061
|
+
const baseVal = base[key];
|
|
105062
|
+
const overrideVal = override[key];
|
|
105063
|
+
if (overrideVal !== undefined && typeof overrideVal === "object" && overrideVal !== null && !Array.isArray(overrideVal) && typeof baseVal === "object" && baseVal !== null && !Array.isArray(baseVal)) {
|
|
105064
|
+
result[key] = deepMerge(baseVal, overrideVal);
|
|
105065
|
+
} else if (overrideVal !== undefined) {
|
|
105066
|
+
result[key] = overrideVal;
|
|
105067
|
+
}
|
|
104935
105068
|
}
|
|
104936
|
-
|
|
104937
|
-
|
|
104938
|
-
|
|
104939
|
-
|
|
104940
|
-
return {
|
|
104941
|
-
courseCode: remote.courseCode ?? undefined,
|
|
104942
|
-
level: remote.level ?? undefined,
|
|
104943
|
-
metadata: remote.metadata ?? undefined
|
|
104944
|
-
};
|
|
104945
|
-
} catch {
|
|
104946
|
-
s.stop("Could not fetch course data");
|
|
105069
|
+
return result;
|
|
105070
|
+
}
|
|
105071
|
+
function buildConfigMetadata(course) {
|
|
105072
|
+
if (!course.metadata)
|
|
104947
105073
|
return;
|
|
104948
|
-
|
|
105074
|
+
const m2 = course.metadata;
|
|
105075
|
+
const metadata = {};
|
|
105076
|
+
if (m2.courseType)
|
|
105077
|
+
metadata.courseType = m2.courseType;
|
|
105078
|
+
if (m2.isSupplemental !== undefined)
|
|
105079
|
+
metadata.isSupplemental = m2.isSupplemental;
|
|
105080
|
+
if (m2.isCustom !== undefined)
|
|
105081
|
+
metadata.isCustom = m2.isCustom;
|
|
105082
|
+
if (m2.publishStatus)
|
|
105083
|
+
metadata.publishStatus = m2.publishStatus;
|
|
105084
|
+
if (m2.contactEmail)
|
|
105085
|
+
metadata.contactEmail = m2.contactEmail;
|
|
105086
|
+
if (m2.primaryApp)
|
|
105087
|
+
metadata.primaryApp = m2.primaryApp;
|
|
105088
|
+
if (m2.goals)
|
|
105089
|
+
metadata.goals = m2.goals;
|
|
105090
|
+
if (m2.metrics)
|
|
105091
|
+
metadata.metrics = m2.metrics;
|
|
105092
|
+
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
104949
105093
|
}
|
|
105094
|
+
function buildUpsertMetadata(existing, course) {
|
|
105095
|
+
const base = existing ?? {};
|
|
105096
|
+
const configMetadata = buildConfigMetadata(course);
|
|
105097
|
+
const result = { ...base };
|
|
105098
|
+
if (configMetadata) {
|
|
105099
|
+
if (configMetadata.courseType !== undefined)
|
|
105100
|
+
result.courseType = configMetadata.courseType;
|
|
105101
|
+
if (configMetadata.isSupplemental !== undefined)
|
|
105102
|
+
result.isSupplemental = configMetadata.isSupplemental;
|
|
105103
|
+
if (configMetadata.isCustom !== undefined)
|
|
105104
|
+
result.isCustom = configMetadata.isCustom;
|
|
105105
|
+
if (configMetadata.publishStatus !== undefined)
|
|
105106
|
+
result.publishStatus = configMetadata.publishStatus;
|
|
105107
|
+
if (configMetadata.contactEmail !== undefined)
|
|
105108
|
+
result.contactEmail = configMetadata.contactEmail;
|
|
105109
|
+
if (configMetadata.primaryApp !== undefined)
|
|
105110
|
+
result.primaryApp = configMetadata.primaryApp;
|
|
105111
|
+
if (configMetadata.goals || base.goals) {
|
|
105112
|
+
result.goals = deepMerge(base.goals, configMetadata.goals);
|
|
105113
|
+
}
|
|
105114
|
+
if (configMetadata.metrics || base.metrics) {
|
|
105115
|
+
result.metrics = deepMerge(base.metrics, configMetadata.metrics);
|
|
105116
|
+
}
|
|
105117
|
+
}
|
|
105118
|
+
result.managedBy = `timeback@${cliVersion}`;
|
|
105119
|
+
return result;
|
|
105120
|
+
}
|
|
105121
|
+
function buildCreateMetadata(course) {
|
|
105122
|
+
return buildUpsertMetadata(undefined, course);
|
|
105123
|
+
}
|
|
105124
|
+
|
|
105125
|
+
// src/lib/courses.ts
|
|
104950
105126
|
function mergeMetadata(base, override) {
|
|
104951
105127
|
if (!base && !override)
|
|
104952
105128
|
return;
|
|
@@ -104983,29 +105159,6 @@ function resolveCourseForEnv(course, defaults, env2) {
|
|
|
104983
105159
|
metadata: mergeMetadata(merged.metadata, envOverrides.metadata)
|
|
104984
105160
|
};
|
|
104985
105161
|
}
|
|
104986
|
-
function buildMetadata(course) {
|
|
104987
|
-
if (!course.metadata)
|
|
104988
|
-
return;
|
|
104989
|
-
const m2 = course.metadata;
|
|
104990
|
-
const metadata = {};
|
|
104991
|
-
if (m2.courseType)
|
|
104992
|
-
metadata.courseType = m2.courseType;
|
|
104993
|
-
if (m2.isSupplemental !== undefined)
|
|
104994
|
-
metadata.isSupplemental = m2.isSupplemental;
|
|
104995
|
-
if (m2.isCustom !== undefined)
|
|
104996
|
-
metadata.isCustom = m2.isCustom;
|
|
104997
|
-
if (m2.publishStatus)
|
|
104998
|
-
metadata.publishStatus = m2.publishStatus;
|
|
104999
|
-
if (m2.contactEmail)
|
|
105000
|
-
metadata.contactEmail = m2.contactEmail;
|
|
105001
|
-
if (m2.primaryApp)
|
|
105002
|
-
metadata.primaryApp = m2.primaryApp;
|
|
105003
|
-
if (m2.goals)
|
|
105004
|
-
metadata.goals = m2.goals;
|
|
105005
|
-
if (m2.metrics)
|
|
105006
|
-
metadata.metrics = m2.metrics;
|
|
105007
|
-
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
105008
|
-
}
|
|
105009
105162
|
async function updateCourse(options) {
|
|
105010
105163
|
const { env: env2, courseId, title, course, defaults } = options;
|
|
105011
105164
|
const authOk = await initAuth({ env: env2 });
|
|
@@ -105020,10 +105173,10 @@ async function updateCourse(options) {
|
|
|
105020
105173
|
};
|
|
105021
105174
|
}
|
|
105022
105175
|
const resolved = resolveCourseForEnv(course, defaults, env2);
|
|
105023
|
-
const metadata = buildMetadata(resolved);
|
|
105024
105176
|
try {
|
|
105025
105177
|
const client = getAuthContext(env2).timeback;
|
|
105026
105178
|
const existing = await client.oneroster.courses.get(courseId);
|
|
105179
|
+
const metadata = buildUpsertMetadata(existing.metadata, resolved);
|
|
105027
105180
|
await client.oneroster.courses.update(courseId, {
|
|
105028
105181
|
title,
|
|
105029
105182
|
subjects: [resolved.subject],
|
|
@@ -105032,7 +105185,7 @@ async function updateCourse(options) {
|
|
|
105032
105185
|
org: existing.org,
|
|
105033
105186
|
courseCode: resolved.courseCode,
|
|
105034
105187
|
level: resolved.level,
|
|
105035
|
-
metadata
|
|
105188
|
+
metadata
|
|
105036
105189
|
});
|
|
105037
105190
|
return { success: true };
|
|
105038
105191
|
} catch (error57) {
|
|
@@ -105052,37 +105205,41 @@ function deriveCourseStructureIds(courseId) {
|
|
|
105052
105205
|
componentResource: `${courseId}-cr`
|
|
105053
105206
|
};
|
|
105054
105207
|
}
|
|
105055
|
-
async function
|
|
105208
|
+
async function getIfExists(getter) {
|
|
105056
105209
|
try {
|
|
105057
|
-
await getter();
|
|
105058
|
-
return true;
|
|
105210
|
+
return await getter();
|
|
105059
105211
|
} catch (error57) {
|
|
105060
105212
|
if (isApiError2(error57) && error57.statusCode === 404) {
|
|
105061
|
-
return
|
|
105213
|
+
return null;
|
|
105062
105214
|
}
|
|
105063
105215
|
throw error57;
|
|
105064
105216
|
}
|
|
105065
105217
|
}
|
|
105066
105218
|
async function ensureComponent(client, ids, title) {
|
|
105067
105219
|
const componentTitle = `${title} - Component`;
|
|
105068
|
-
const
|
|
105069
|
-
if (
|
|
105070
|
-
await client.oneroster.courses.updateComponent(ids.component, {
|
|
105071
|
-
title: componentTitle,
|
|
105072
|
-
status: "active"
|
|
105073
|
-
});
|
|
105074
|
-
} else {
|
|
105220
|
+
const existing = await getIfExists(() => client.oneroster.courses.getComponent(ids.component));
|
|
105221
|
+
if (!existing) {
|
|
105075
105222
|
await client.oneroster.courses.createComponent({
|
|
105076
105223
|
sourcedId: ids.component,
|
|
105077
105224
|
title: componentTitle,
|
|
105078
105225
|
course: { sourcedId: ids.course },
|
|
105079
105226
|
status: "active"
|
|
105080
105227
|
});
|
|
105228
|
+
return;
|
|
105081
105229
|
}
|
|
105230
|
+
const needsUpdate = existing.title !== componentTitle || existing.status !== "active" || existing.course?.sourcedId !== ids.course;
|
|
105231
|
+
if (!needsUpdate)
|
|
105232
|
+
return;
|
|
105233
|
+
await client.oneroster.courses.updateComponent(ids.component, {
|
|
105234
|
+
...existing,
|
|
105235
|
+
title: componentTitle,
|
|
105236
|
+
course: { sourcedId: ids.course },
|
|
105237
|
+
status: "active"
|
|
105238
|
+
});
|
|
105082
105239
|
}
|
|
105083
105240
|
async function ensureResource(client, ids, title, appName, launchUrl) {
|
|
105084
105241
|
const resourceTitle = `${title} - Resource`;
|
|
105085
|
-
const
|
|
105242
|
+
const existing = await getIfExists(() => client.oneroster.resources.get(ids.resource));
|
|
105086
105243
|
const resourceData = {
|
|
105087
105244
|
title: resourceTitle,
|
|
105088
105245
|
vendorResourceId: ids.resource,
|
|
@@ -105097,24 +105254,23 @@ async function ensureResource(client, ids, title, appName, launchUrl) {
|
|
|
105097
105254
|
toolProvider: appName
|
|
105098
105255
|
}
|
|
105099
105256
|
};
|
|
105100
|
-
if (
|
|
105101
|
-
await client.oneroster.resources.update(ids.resource, resourceData);
|
|
105102
|
-
} else {
|
|
105257
|
+
if (!existing) {
|
|
105103
105258
|
await client.oneroster.resources.create({
|
|
105104
105259
|
sourcedId: ids.resource,
|
|
105105
105260
|
...resourceData
|
|
105106
105261
|
});
|
|
105262
|
+
return;
|
|
105107
105263
|
}
|
|
105264
|
+
const existingLaunchUrl = existing.metadata?.launchUrl;
|
|
105265
|
+
const needsUpdate = existing.title !== resourceTitle || String(existingLaunchUrl ?? "") !== launchUrl;
|
|
105266
|
+
if (!needsUpdate)
|
|
105267
|
+
return;
|
|
105268
|
+
await client.oneroster.resources.update(ids.resource, resourceData);
|
|
105108
105269
|
}
|
|
105109
105270
|
async function ensureComponentResource(client, ids, title) {
|
|
105110
105271
|
const crTitle = `${title} - Link`;
|
|
105111
|
-
const
|
|
105112
|
-
if (
|
|
105113
|
-
await client.oneroster.courses.updateComponentResource(ids.componentResource, {
|
|
105114
|
-
title: crTitle,
|
|
105115
|
-
status: "active"
|
|
105116
|
-
});
|
|
105117
|
-
} else {
|
|
105272
|
+
const existing = await getIfExists(() => client.oneroster.courses.getComponentResource(ids.componentResource));
|
|
105273
|
+
if (!existing) {
|
|
105118
105274
|
await client.oneroster.courses.createComponentResource({
|
|
105119
105275
|
sourcedId: ids.componentResource,
|
|
105120
105276
|
title: crTitle,
|
|
@@ -105122,7 +105278,19 @@ async function ensureComponentResource(client, ids, title) {
|
|
|
105122
105278
|
resource: { sourcedId: ids.resource },
|
|
105123
105279
|
status: "active"
|
|
105124
105280
|
});
|
|
105281
|
+
return;
|
|
105125
105282
|
}
|
|
105283
|
+
const needsUpdate = existing.title !== crTitle || existing.status !== "active" || existing.courseComponent?.sourcedId !== ids.component || existing.resource?.sourcedId !== ids.resource;
|
|
105284
|
+
if (!needsUpdate)
|
|
105285
|
+
return;
|
|
105286
|
+
await client.oneroster.courses.updateComponentResource(ids.componentResource, {
|
|
105287
|
+
...existing,
|
|
105288
|
+
sourcedId: ids.componentResource,
|
|
105289
|
+
title: crTitle,
|
|
105290
|
+
courseComponent: { sourcedId: ids.component },
|
|
105291
|
+
resource: { sourcedId: ids.resource },
|
|
105292
|
+
status: "active"
|
|
105293
|
+
});
|
|
105126
105294
|
}
|
|
105127
105295
|
async function ensureCourseStructure(options) {
|
|
105128
105296
|
const { env: env2, courseId, courseTitle, appName, launchUrl, onProgress } = options;
|
|
@@ -105155,43 +105323,6 @@ async function ensureCourseStructure(options) {
|
|
|
105155
105323
|
};
|
|
105156
105324
|
}
|
|
105157
105325
|
}
|
|
105158
|
-
// src/lib/diff.ts
|
|
105159
|
-
function formatCourse2(course) {
|
|
105160
|
-
return `${course.subject} - Grade ${course.grade}`;
|
|
105161
|
-
}
|
|
105162
|
-
function logAdditions(courses) {
|
|
105163
|
-
M2.message("");
|
|
105164
|
-
M2.message(bold("Changes:"));
|
|
105165
|
-
const lines = courses.map((c) => ` ${green("+")} ${formatCourse2(c)}`);
|
|
105166
|
-
M2.message(lines.join(`
|
|
105167
|
-
`));
|
|
105168
|
-
M2.message("");
|
|
105169
|
-
}
|
|
105170
|
-
function logRemovals(courses) {
|
|
105171
|
-
M2.message("");
|
|
105172
|
-
M2.message(bold("Changes:"));
|
|
105173
|
-
const lines = courses.map((c) => ` ${red("-")} ${formatCourse2(c)}`);
|
|
105174
|
-
M2.message(lines.join(`
|
|
105175
|
-
`));
|
|
105176
|
-
M2.message("");
|
|
105177
|
-
}
|
|
105178
|
-
function logEdit(original, edited) {
|
|
105179
|
-
const changes = compareCourseFields(edited, original);
|
|
105180
|
-
if (changes.length === 0) {
|
|
105181
|
-
return;
|
|
105182
|
-
}
|
|
105183
|
-
M2.message(bold("Changes:"));
|
|
105184
|
-
const maxFieldLen = Math.max(...changes.map((c) => c.field.length));
|
|
105185
|
-
const maxOldLen = Math.max(...changes.map((c) => c.remote.length || 7));
|
|
105186
|
-
const lines = changes.map((change) => {
|
|
105187
|
-
const paddedField = change.field.padStart(maxFieldLen);
|
|
105188
|
-
const paddedOld = (change.remote || "(empty)").padStart(maxOldLen);
|
|
105189
|
-
const newVal = change.local || dim("(empty)");
|
|
105190
|
-
return ` ${dim(`${paddedField}:`)} ${red(paddedOld)} ${dim("→")} ${green(newVal)}`;
|
|
105191
|
-
});
|
|
105192
|
-
M2.message(lines.join(`
|
|
105193
|
-
`));
|
|
105194
|
-
}
|
|
105195
105326
|
// src/lib/environment/prompts.ts
|
|
105196
105327
|
async function promptEnvironment() {
|
|
105197
105328
|
const env2 = await ve({
|
|
@@ -105222,8 +105353,12 @@ async function resolveEnvironment(options = {}) {
|
|
|
105222
105353
|
return null;
|
|
105223
105354
|
return { env: selected, promptedUser: true };
|
|
105224
105355
|
}
|
|
105356
|
+
// ../internal/utils/src/shared/format.ts
|
|
105357
|
+
function pluralize(count, singular, plural) {
|
|
105358
|
+
return count === 1 ? singular : plural ?? `${singular}s`;
|
|
105359
|
+
}
|
|
105225
105360
|
// src/lib/org/select.ts
|
|
105226
|
-
async function selectOrganization(client, email4) {
|
|
105361
|
+
async function selectOrganization(client, email4, courseCount) {
|
|
105227
105362
|
const s = Y2();
|
|
105228
105363
|
s.start("Fetching your organizations...");
|
|
105229
105364
|
const user = await client.oneroster.users.first({ where: { email: email4 } });
|
|
@@ -105255,7 +105390,7 @@ async function selectOrganization(client, email4) {
|
|
|
105255
105390
|
}
|
|
105256
105391
|
s.stop("Organizations loaded");
|
|
105257
105392
|
const orgChoice = await ve({
|
|
105258
|
-
message:
|
|
105393
|
+
message: `Select an organization for the new ${pluralize(courseCount ?? 2, "course")}:`,
|
|
105259
105394
|
options: Array.from(orgMap.entries()).map(([id, name]) => ({
|
|
105260
105395
|
value: id,
|
|
105261
105396
|
label: name
|
|
@@ -105297,8 +105432,6 @@ async function searchAndSelectCourses2(options) {
|
|
|
105297
105432
|
}
|
|
105298
105433
|
}
|
|
105299
105434
|
// src/lib/setup/setup.ts
|
|
105300
|
-
import { writeFile as writeFile3 } from "node:fs/promises";
|
|
105301
|
-
import { resolve as resolve4 } from "node:path";
|
|
105302
105435
|
var CONFIG_FILENAME2 = "timeback.config.json";
|
|
105303
105436
|
async function promptEnvironmentToSetup() {
|
|
105304
105437
|
const env2 = await ve({
|
|
@@ -105327,67 +105460,6 @@ You need to configure your Timeback API credentials first.`, "Credentials Setup"
|
|
|
105327
105460
|
Me(`Saved to ${dim(getCredentialsPath())}`, "Persisted credentials to disk");
|
|
105328
105461
|
return true;
|
|
105329
105462
|
}
|
|
105330
|
-
async function ensureConfig(opts = {}) {
|
|
105331
|
-
const { configPath: customPath, skipIntro = false, format = true } = opts;
|
|
105332
|
-
if (!skipIntro) {
|
|
105333
|
-
intro("Timeback");
|
|
105334
|
-
}
|
|
105335
|
-
const cwd = process.cwd();
|
|
105336
|
-
const configPath = customPath ? resolve4(cwd, customPath) : resolve4(cwd, CONFIG_FILENAME2);
|
|
105337
|
-
const configResult = await loadConfig({ configPath: customPath });
|
|
105338
|
-
if (configResult.success) {
|
|
105339
|
-
M2.info(`Found ${dim(CONFIG_FILENAME2)}`);
|
|
105340
|
-
return {
|
|
105341
|
-
success: true,
|
|
105342
|
-
config: configResult.config,
|
|
105343
|
-
configPath: configResult.configPath
|
|
105344
|
-
};
|
|
105345
|
-
}
|
|
105346
|
-
Me(`No ${greenBright(CONFIG_FILENAME2)} found in this directory.
|
|
105347
|
-
Let's set up your Timeback project.`, "First-time setup");
|
|
105348
|
-
let configuredEnvs = await getConfiguredEnvironments();
|
|
105349
|
-
if (configuredEnvs.length === 0) {
|
|
105350
|
-
const ok = await setupCredentials();
|
|
105351
|
-
if (!ok) {
|
|
105352
|
-
return { success: false, cancelled: true };
|
|
105353
|
-
}
|
|
105354
|
-
configuredEnvs = await getConfiguredEnvironments();
|
|
105355
|
-
if (configuredEnvs.length === 0) {
|
|
105356
|
-
return { success: false, error: "Failed to configure credentials" };
|
|
105357
|
-
}
|
|
105358
|
-
}
|
|
105359
|
-
const searchResult = await searchAndSelectCourses2({ configuredEnvs });
|
|
105360
|
-
if (!searchResult.success) {
|
|
105361
|
-
return {
|
|
105362
|
-
success: false,
|
|
105363
|
-
cancelled: searchResult.cancelled,
|
|
105364
|
-
error: searchResult.error
|
|
105365
|
-
};
|
|
105366
|
-
}
|
|
105367
|
-
const sensor = await promptSensor();
|
|
105368
|
-
if (sensor === undefined) {
|
|
105369
|
-
return { success: false, cancelled: true };
|
|
105370
|
-
}
|
|
105371
|
-
const config7 = {
|
|
105372
|
-
name: searchResult.appName,
|
|
105373
|
-
sensor,
|
|
105374
|
-
courses: searchResult.courses
|
|
105375
|
-
};
|
|
105376
|
-
const content = generateConfigContent(config7);
|
|
105377
|
-
await writeFile3(configPath, content, "utf-8");
|
|
105378
|
-
M2.success(`Created ${dim(CONFIG_FILENAME2)}`);
|
|
105379
|
-
if (format) {
|
|
105380
|
-
await formatWithPrettier({ cwd, filePath: configPath, logger: M2 });
|
|
105381
|
-
}
|
|
105382
|
-
if (!skipIntro) {
|
|
105383
|
-
outro.success();
|
|
105384
|
-
}
|
|
105385
|
-
return {
|
|
105386
|
-
success: true,
|
|
105387
|
-
config: config7,
|
|
105388
|
-
configPath
|
|
105389
|
-
};
|
|
105390
|
-
}
|
|
105391
105463
|
async function runImportFlow(opts = {}) {
|
|
105392
105464
|
const { format = true } = opts;
|
|
105393
105465
|
let configuredEnvs = await getConfiguredEnvironments();
|
|
@@ -105434,15 +105506,14 @@ async function runImportFlow(opts = {}) {
|
|
|
105434
105506
|
config7.courses.push(course);
|
|
105435
105507
|
}
|
|
105436
105508
|
}
|
|
105437
|
-
const
|
|
105438
|
-
|
|
105439
|
-
|
|
105440
|
-
|
|
105441
|
-
|
|
105442
|
-
}
|
|
105509
|
+
const s = Y2();
|
|
105510
|
+
s.start(`Updating ${dim(CONFIG_FILENAME2)}...`);
|
|
105511
|
+
const normalizedConfig = normalizeLaunchUrls(config7);
|
|
105512
|
+
await saveConfig(configPath, normalizedConfig, { format });
|
|
105513
|
+
s.stop(`Updated ${dim(CONFIG_FILENAME2)}`);
|
|
105443
105514
|
return {
|
|
105444
105515
|
success: true,
|
|
105445
|
-
config:
|
|
105516
|
+
config: normalizedConfig,
|
|
105446
105517
|
configPath
|
|
105447
105518
|
};
|
|
105448
105519
|
}
|
|
@@ -108681,115 +108752,11 @@ function registerCredentialsCommand(program2) {
|
|
|
108681
108752
|
creds.action(() => showCredentialsMenu());
|
|
108682
108753
|
}
|
|
108683
108754
|
|
|
108684
|
-
// src/commands/
|
|
108685
|
-
|
|
108686
|
-
|
|
108687
|
-
const result = {
|
|
108688
|
-
created: 0,
|
|
108689
|
-
updated: 0,
|
|
108690
|
-
skipped: diff.unchanged.length,
|
|
108691
|
-
errors: [...diff.errors]
|
|
108692
|
-
};
|
|
108693
|
-
const updatedConfig = { ...config7, courses: [...config7.courses] };
|
|
108694
|
-
for (const create of diff.creates) {
|
|
108695
|
-
if (!createEnv || !createOrgId) {
|
|
108696
|
-
result.errors.push({
|
|
108697
|
-
title: create.title,
|
|
108698
|
-
header: "Environment and organization required for new courses",
|
|
108699
|
-
details: []
|
|
108700
|
-
});
|
|
108701
|
-
continue;
|
|
108702
|
-
}
|
|
108703
|
-
const authOk = await initAuth({ env: createEnv });
|
|
108704
|
-
if (!authOk) {
|
|
108705
|
-
result.errors.push({
|
|
108706
|
-
title: create.title,
|
|
108707
|
-
header: `Failed to authenticate for ${createEnv}`,
|
|
108708
|
-
details: []
|
|
108709
|
-
});
|
|
108710
|
-
continue;
|
|
108711
|
-
}
|
|
108712
|
-
onProgress?.(`Creating ${create.title} on ${createEnv}...`);
|
|
108713
|
-
const resolved = resolveCourseForEnv(create.local, config7.defaults, createEnv);
|
|
108714
|
-
const metadata = buildMetadata(resolved);
|
|
108715
|
-
try {
|
|
108716
|
-
const client = getAuthContext(createEnv).timeback;
|
|
108717
|
-
const response = await client.oneroster.courses.create({
|
|
108718
|
-
title: create.title,
|
|
108719
|
-
subjects: [resolved.subject],
|
|
108720
|
-
...resolved.grade === undefined ? {} : { grades: [resolved.grade] },
|
|
108721
|
-
org: { sourcedId: createOrgId },
|
|
108722
|
-
status: "active",
|
|
108723
|
-
courseCode: resolved.courseCode,
|
|
108724
|
-
level: resolved.level,
|
|
108725
|
-
metadata
|
|
108726
|
-
});
|
|
108727
|
-
const allocatedCourseId = response.sourcedIdPairs.allocatedSourcedId;
|
|
108728
|
-
updatedConfig.courses[create.index] = {
|
|
108729
|
-
...create.local,
|
|
108730
|
-
ids: {
|
|
108731
|
-
...create.local.ids,
|
|
108732
|
-
[createEnv]: allocatedCourseId
|
|
108733
|
-
}
|
|
108734
|
-
};
|
|
108735
|
-
result.created++;
|
|
108736
|
-
const effectiveLaunchUrl = resolved.launchUrl ?? config7.launchUrl;
|
|
108737
|
-
if (effectiveLaunchUrl) {
|
|
108738
|
-
onProgress?.(`Ensuring course structure for ${create.title}...`);
|
|
108739
|
-
const structureResult = await ensureCourseStructure({
|
|
108740
|
-
env: createEnv,
|
|
108741
|
-
courseId: allocatedCourseId,
|
|
108742
|
-
courseTitle: create.title,
|
|
108743
|
-
appName: config7.name,
|
|
108744
|
-
launchUrl: effectiveLaunchUrl,
|
|
108745
|
-
onProgress
|
|
108746
|
-
});
|
|
108747
|
-
if (!structureResult.success && structureResult.error) {
|
|
108748
|
-
result.errors.push(structureResult.error);
|
|
108749
|
-
}
|
|
108750
|
-
}
|
|
108751
|
-
} catch (error57) {
|
|
108752
|
-
const formatted = formatApiError(error57);
|
|
108753
|
-
result.errors.push({ title: create.title, ...formatted });
|
|
108754
|
-
}
|
|
108755
|
-
}
|
|
108756
|
-
for (const update of diff.updates) {
|
|
108757
|
-
for (const { env: env2, courseId } of update.environments) {
|
|
108758
|
-
onProgress?.(`Updating ${update.localTitle} on ${env2}...`);
|
|
108759
|
-
const updateResult = await updateCourse({
|
|
108760
|
-
env: env2,
|
|
108761
|
-
courseId,
|
|
108762
|
-
title: update.localTitle,
|
|
108763
|
-
course: update.local,
|
|
108764
|
-
defaults: config7.defaults
|
|
108765
|
-
});
|
|
108766
|
-
if (!updateResult.success && updateResult.error) {
|
|
108767
|
-
result.errors.push(updateResult.error);
|
|
108768
|
-
continue;
|
|
108769
|
-
}
|
|
108770
|
-
const resolved = resolveCourseForEnv(update.local, config7.defaults, env2);
|
|
108771
|
-
const effectiveLaunchUrl = resolved.launchUrl ?? config7.launchUrl;
|
|
108772
|
-
if (effectiveLaunchUrl) {
|
|
108773
|
-
onProgress?.(`Ensuring course structure for ${update.localTitle}...`);
|
|
108774
|
-
const structureResult = await ensureCourseStructure({
|
|
108775
|
-
env: env2,
|
|
108776
|
-
courseId,
|
|
108777
|
-
courseTitle: update.localTitle,
|
|
108778
|
-
appName: config7.name,
|
|
108779
|
-
launchUrl: effectiveLaunchUrl,
|
|
108780
|
-
onProgress
|
|
108781
|
-
});
|
|
108782
|
-
if (!structureResult.success && structureResult.error) {
|
|
108783
|
-
result.errors.push(structureResult.error);
|
|
108784
|
-
}
|
|
108785
|
-
}
|
|
108786
|
-
}
|
|
108787
|
-
result.updated++;
|
|
108788
|
-
}
|
|
108789
|
-
return { result, updatedConfig };
|
|
108790
|
-
}
|
|
108755
|
+
// src/commands/init/init.ts
|
|
108756
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
108757
|
+
import { basename as basename2, resolve as resolve4 } from "node:path";
|
|
108791
108758
|
|
|
108792
|
-
// src/commands/
|
|
108759
|
+
// src/commands/resources/push/lib/utils.ts
|
|
108793
108760
|
function getGradeLabel(grade) {
|
|
108794
108761
|
if (grade === -1)
|
|
108795
108762
|
return "Pre-K";
|
|
@@ -108799,996 +108766,20 @@ function getGradeLabel(grade) {
|
|
|
108799
108766
|
return "AP";
|
|
108800
108767
|
return `Grade ${grade}`;
|
|
108801
108768
|
}
|
|
108802
|
-
function getCourseTitle(appName, course) {
|
|
108769
|
+
function getCourseTitle(appName, course, options = {}) {
|
|
108803
108770
|
if (course.grade !== undefined) {
|
|
108804
108771
|
return `${appName} - ${getGradeLabel(course.grade)}`;
|
|
108805
108772
|
}
|
|
108806
|
-
if (
|
|
108807
|
-
|
|
108808
|
-
|
|
108809
|
-
|
|
108810
|
-
}
|
|
108811
|
-
|
|
108812
|
-
// src/commands/init/lib/constants.ts
|
|
108813
|
-
var SUBJECTS = [
|
|
108814
|
-
"Math",
|
|
108815
|
-
"FastMath",
|
|
108816
|
-
"Reading",
|
|
108817
|
-
"Language",
|
|
108818
|
-
"Vocabulary",
|
|
108819
|
-
"Writing",
|
|
108820
|
-
"Science",
|
|
108821
|
-
"Social Studies"
|
|
108822
|
-
];
|
|
108823
|
-
var GRADES = [
|
|
108824
|
-
{ value: -1, label: "Pre-K" },
|
|
108825
|
-
{ value: 0, label: "Kindergarten" },
|
|
108826
|
-
{ value: 1, label: "Grade 1" },
|
|
108827
|
-
{ value: 2, label: "Grade 2" },
|
|
108828
|
-
{ value: 3, label: "Grade 3" },
|
|
108829
|
-
{ value: 4, label: "Grade 4" },
|
|
108830
|
-
{ value: 5, label: "Grade 5" },
|
|
108831
|
-
{ value: 6, label: "Grade 6" },
|
|
108832
|
-
{ value: 7, label: "Grade 7" },
|
|
108833
|
-
{ value: 8, label: "Grade 8" },
|
|
108834
|
-
{ value: 9, label: "Grade 9" },
|
|
108835
|
-
{ value: 10, label: "Grade 10" },
|
|
108836
|
-
{ value: 11, label: "Grade 11" },
|
|
108837
|
-
{ value: 12, label: "Grade 12" },
|
|
108838
|
-
{ value: 13, label: "AP" }
|
|
108839
|
-
];
|
|
108840
|
-
|
|
108841
|
-
// src/commands/edit/lib/properties.ts
|
|
108842
|
-
var EDITABLE_PROPERTIES = [
|
|
108843
|
-
{ key: "subject", label: "Subject", type: "select-subject", path: ["subject"] },
|
|
108844
|
-
{ key: "grade", label: "Grade", type: "select-grade", path: ["grade"] },
|
|
108845
|
-
{ key: "courseCode", label: "Course Code", type: "text", path: ["courseCode"] },
|
|
108846
|
-
{ key: "level", label: "Level", type: "text", path: ["level"] },
|
|
108847
|
-
{
|
|
108848
|
-
key: "courseType",
|
|
108849
|
-
label: "Course Type",
|
|
108850
|
-
type: "select-enum",
|
|
108851
|
-
path: ["metadata", "courseType"],
|
|
108852
|
-
options: CourseType6.options.map((v2) => ({ value: v2, label: v2 }))
|
|
108853
|
-
},
|
|
108854
|
-
{
|
|
108855
|
-
key: "publishStatus",
|
|
108856
|
-
label: "Publish Status",
|
|
108857
|
-
type: "select-enum",
|
|
108858
|
-
path: ["metadata", "publishStatus"],
|
|
108859
|
-
options: PublishStatus6.options.map((v2) => ({ value: v2, label: v2 }))
|
|
108860
|
-
},
|
|
108861
|
-
{
|
|
108862
|
-
key: "isSupplemental",
|
|
108863
|
-
label: "Supplemental?",
|
|
108864
|
-
type: "boolean",
|
|
108865
|
-
path: ["metadata", "isSupplemental"]
|
|
108866
|
-
},
|
|
108867
|
-
{ key: "isCustom", label: "Custom?", type: "boolean", path: ["metadata", "isCustom"] },
|
|
108868
|
-
{
|
|
108869
|
-
key: "contactEmail",
|
|
108870
|
-
label: "Contact Email",
|
|
108871
|
-
type: "text",
|
|
108872
|
-
path: ["metadata", "contactEmail"],
|
|
108873
|
-
validate: (v2) => {
|
|
108874
|
-
if (v2 === "")
|
|
108875
|
-
return;
|
|
108876
|
-
const result = CourseMetadata6.shape.contactEmail.safeParse(v2);
|
|
108877
|
-
if (!result.success)
|
|
108878
|
-
return "Must be a valid email address";
|
|
108879
|
-
}
|
|
108880
|
-
},
|
|
108881
|
-
{ key: "primaryApp", label: "Primary App", type: "text", path: ["metadata", "primaryApp"] },
|
|
108882
|
-
{
|
|
108883
|
-
key: "dailyXp",
|
|
108884
|
-
label: "Daily XP Goal",
|
|
108885
|
-
type: "number",
|
|
108886
|
-
path: ["metadata", "goals", "dailyXp"],
|
|
108887
|
-
validate: (v2) => {
|
|
108888
|
-
const result = CourseGoals6.shape.dailyXp.safeParse(Number(v2));
|
|
108889
|
-
if (!result.success)
|
|
108890
|
-
return result.error.issues[0]?.message;
|
|
108891
|
-
}
|
|
108892
|
-
},
|
|
108893
|
-
{
|
|
108894
|
-
key: "dailyLessons",
|
|
108895
|
-
label: "Daily Lessons Goal",
|
|
108896
|
-
type: "number",
|
|
108897
|
-
path: ["metadata", "goals", "dailyLessons"],
|
|
108898
|
-
validate: (v2) => {
|
|
108899
|
-
const result = CourseGoals6.shape.dailyLessons.safeParse(Number(v2));
|
|
108900
|
-
if (!result.success)
|
|
108901
|
-
return result.error.issues[0]?.message;
|
|
108902
|
-
}
|
|
108903
|
-
},
|
|
108904
|
-
{
|
|
108905
|
-
key: "dailyActiveMinutes",
|
|
108906
|
-
label: "Daily Active Minutes Goal",
|
|
108907
|
-
type: "number",
|
|
108908
|
-
path: ["metadata", "goals", "dailyActiveMinutes"],
|
|
108909
|
-
validate: (v2) => {
|
|
108910
|
-
const result = CourseGoals6.shape.dailyActiveMinutes.safeParse(Number(v2));
|
|
108911
|
-
if (!result.success)
|
|
108912
|
-
return result.error.issues[0]?.message;
|
|
108913
|
-
}
|
|
108914
|
-
},
|
|
108915
|
-
{
|
|
108916
|
-
key: "dailyAccuracy",
|
|
108917
|
-
label: "Daily Accuracy Goal (%)",
|
|
108918
|
-
type: "number",
|
|
108919
|
-
path: ["metadata", "goals", "dailyAccuracy"],
|
|
108920
|
-
validate: (v2) => {
|
|
108921
|
-
const result = CourseGoals6.shape.dailyAccuracy.safeParse(Number(v2));
|
|
108922
|
-
if (!result.success)
|
|
108923
|
-
return result.error.issues[0]?.message;
|
|
108924
|
-
}
|
|
108925
|
-
},
|
|
108926
|
-
{
|
|
108927
|
-
key: "dailyMasteredUnits",
|
|
108928
|
-
label: "Daily Mastered Units Goal",
|
|
108929
|
-
type: "number",
|
|
108930
|
-
path: ["metadata", "goals", "dailyMasteredUnits"],
|
|
108931
|
-
validate: (v2) => {
|
|
108932
|
-
const result = CourseGoals6.shape.dailyMasteredUnits.safeParse(Number(v2));
|
|
108933
|
-
if (!result.success)
|
|
108934
|
-
return result.error.issues[0]?.message;
|
|
108935
|
-
}
|
|
108936
|
-
},
|
|
108937
|
-
{
|
|
108938
|
-
key: "totalXp",
|
|
108939
|
-
label: "Total XP",
|
|
108940
|
-
type: "number",
|
|
108941
|
-
path: ["metadata", "metrics", "totalXp"],
|
|
108942
|
-
validate: (v2) => {
|
|
108943
|
-
const result = CourseMetrics6.shape.totalXp.safeParse(Number(v2));
|
|
108944
|
-
if (!result.success)
|
|
108945
|
-
return result.error.issues[0]?.message;
|
|
108946
|
-
}
|
|
108947
|
-
},
|
|
108948
|
-
{
|
|
108949
|
-
key: "totalLessons",
|
|
108950
|
-
label: "Total Lessons",
|
|
108951
|
-
type: "number",
|
|
108952
|
-
path: ["metadata", "metrics", "totalLessons"],
|
|
108953
|
-
validate: (v2) => {
|
|
108954
|
-
const result = CourseMetrics6.shape.totalLessons.safeParse(Number(v2));
|
|
108955
|
-
if (!result.success)
|
|
108956
|
-
return result.error.issues[0]?.message;
|
|
108957
|
-
}
|
|
108958
|
-
},
|
|
108959
|
-
{
|
|
108960
|
-
key: "totalGrades",
|
|
108961
|
-
label: "Total Grades",
|
|
108962
|
-
type: "number",
|
|
108963
|
-
path: ["metadata", "metrics", "totalGrades"],
|
|
108964
|
-
validate: (v2) => {
|
|
108965
|
-
const result = CourseMetrics6.shape.totalGrades.safeParse(Number(v2));
|
|
108966
|
-
if (!result.success)
|
|
108967
|
-
return result.error.issues[0]?.message;
|
|
108968
|
-
}
|
|
108969
|
-
}
|
|
108970
|
-
];
|
|
108971
|
-
|
|
108972
|
-
// src/commands/edit/lib/prompts.ts
|
|
108973
|
-
function getNestedValue(obj, path) {
|
|
108974
|
-
let current = obj;
|
|
108975
|
-
for (const key of path) {
|
|
108976
|
-
if (current === null || current === undefined || typeof current !== "object") {
|
|
108977
|
-
return;
|
|
108978
|
-
}
|
|
108979
|
-
current = current[key];
|
|
108980
|
-
}
|
|
108981
|
-
return current;
|
|
108982
|
-
}
|
|
108983
|
-
function setNestedValue(obj, path, value) {
|
|
108984
|
-
let current = obj;
|
|
108985
|
-
for (let i = 0;i < path.length - 1; i++) {
|
|
108986
|
-
const key = path[i];
|
|
108987
|
-
const val = current[key];
|
|
108988
|
-
if (val === null || val === undefined || typeof val !== "object") {
|
|
108989
|
-
current[key] = {};
|
|
108990
|
-
}
|
|
108991
|
-
current = current[key];
|
|
108992
|
-
}
|
|
108993
|
-
const lastKey = path.at(-1);
|
|
108994
|
-
if (lastKey)
|
|
108995
|
-
current[lastKey] = value;
|
|
108996
|
-
}
|
|
108997
|
-
function formatCurrentValue(value) {
|
|
108998
|
-
if (value === null || value === undefined)
|
|
108999
|
-
return dim("(not set)");
|
|
109000
|
-
if (typeof value === "boolean")
|
|
109001
|
-
return value ? "yes" : "no";
|
|
109002
|
-
return String(value);
|
|
109003
|
-
}
|
|
109004
|
-
async function promptAction(courseCount) {
|
|
109005
|
-
const hasCourses = courseCount > 0;
|
|
109006
|
-
const action = await ve({
|
|
109007
|
-
message: "What would you like to do?",
|
|
109008
|
-
options: [
|
|
109009
|
-
{ value: "add", label: "Add course(s)" },
|
|
109010
|
-
...hasCourses ? [
|
|
109011
|
-
{ value: "edit", label: "Edit course(s)" },
|
|
109012
|
-
{ value: "remove", label: "Remove course(s)" }
|
|
109013
|
-
] : [],
|
|
109014
|
-
{ value: "done", label: "Done" }
|
|
109015
|
-
]
|
|
109016
|
-
});
|
|
109017
|
-
if (isCancelled(action))
|
|
109018
|
-
return null;
|
|
109019
|
-
return action;
|
|
109020
|
-
}
|
|
109021
|
-
async function promptSelectCourse(courses, message) {
|
|
109022
|
-
const choice = await ve({
|
|
109023
|
-
message,
|
|
109024
|
-
options: courses.map((c, i) => ({
|
|
109025
|
-
value: i,
|
|
109026
|
-
label: `${c.subject} - Grade ${c.grade}`
|
|
109027
|
-
}))
|
|
109028
|
-
});
|
|
109029
|
-
if (isCancelled(choice))
|
|
109030
|
-
return null;
|
|
109031
|
-
return choice;
|
|
109032
|
-
}
|
|
109033
|
-
async function promptAddCourses(existingCourses) {
|
|
109034
|
-
const subjects = await fe({
|
|
109035
|
-
message: "Select subjects",
|
|
109036
|
-
options: SUBJECTS.map((s) => ({ value: s, label: s }))
|
|
109037
|
-
});
|
|
109038
|
-
if (isCancelled(subjects))
|
|
109039
|
-
return null;
|
|
109040
|
-
if (subjects.length === 0) {
|
|
109041
|
-
M2.error("At least one subject is required");
|
|
109042
|
-
return null;
|
|
109043
|
-
}
|
|
109044
|
-
const grades = await fe({
|
|
109045
|
-
message: "Select grade levels",
|
|
109046
|
-
options: GRADES
|
|
109047
|
-
});
|
|
109048
|
-
if (isCancelled(grades))
|
|
109049
|
-
return null;
|
|
109050
|
-
if (grades.length === 0) {
|
|
109051
|
-
M2.error("At least one grade is required");
|
|
109052
|
-
return null;
|
|
109053
|
-
}
|
|
109054
|
-
const newCourses = [];
|
|
109055
|
-
let skipped = 0;
|
|
109056
|
-
for (const subject of subjects) {
|
|
109057
|
-
for (const grade of grades) {
|
|
109058
|
-
const exists = existingCourses.some((c) => c.subject === subject && c.grade === grade);
|
|
109059
|
-
if (exists) {
|
|
109060
|
-
skipped++;
|
|
109061
|
-
continue;
|
|
109062
|
-
}
|
|
109063
|
-
newCourses.push({
|
|
109064
|
-
subject,
|
|
109065
|
-
grade,
|
|
109066
|
-
ids: null
|
|
109067
|
-
});
|
|
109068
|
-
}
|
|
109069
|
-
}
|
|
109070
|
-
if (skipped > 0) {
|
|
109071
|
-
M2.warn(`Skipped ${skipped} duplicate course${skipped === 1 ? "" : "s"}`);
|
|
109072
|
-
}
|
|
109073
|
-
return newCourses;
|
|
109074
|
-
}
|
|
109075
|
-
var ADD_NEW_PROPERTIES_KEY = "__add_new__";
|
|
109076
|
-
async function promptEditCourse(course, allCourses, editIndex, remoteData) {
|
|
109077
|
-
const configuredProps = EDITABLE_PROPERTIES.filter((prop) => {
|
|
109078
|
-
const value = getNestedValue(course, prop.path);
|
|
109079
|
-
return value !== null && value !== undefined;
|
|
109080
|
-
});
|
|
109081
|
-
const unconfiguredProps = EDITABLE_PROPERTIES.filter((prop) => {
|
|
109082
|
-
const value = getNestedValue(course, prop.path);
|
|
109083
|
-
return value === null || value === undefined;
|
|
109084
|
-
});
|
|
109085
|
-
const options = [
|
|
109086
|
-
...configuredProps.map((prop) => {
|
|
109087
|
-
const currentValue = getNestedValue(course, prop.path);
|
|
109088
|
-
return {
|
|
109089
|
-
value: prop.key,
|
|
109090
|
-
label: `${prop.label} ${dim(`(${formatCurrentValue(currentValue)})`)}`
|
|
109091
|
-
};
|
|
109092
|
-
}),
|
|
109093
|
-
...unconfiguredProps.length > 0 ? [{ value: ADD_NEW_PROPERTIES_KEY, label: dim("+ Add new properties") }] : []
|
|
109094
|
-
];
|
|
109095
|
-
if (options.length === 0) {
|
|
109096
|
-
M2.warn("No editable properties available");
|
|
109097
|
-
return null;
|
|
109098
|
-
}
|
|
109099
|
-
const selectedKeys = await fe({
|
|
109100
|
-
message: "Select properties to edit",
|
|
109101
|
-
options
|
|
109102
|
-
});
|
|
109103
|
-
if (isCancelled(selectedKeys))
|
|
109104
|
-
return null;
|
|
109105
|
-
const keys = selectedKeys;
|
|
109106
|
-
if (keys.length === 0) {
|
|
109107
|
-
M2.warn("No properties selected");
|
|
109108
|
-
return null;
|
|
109109
|
-
}
|
|
109110
|
-
const updated = structuredClone(course);
|
|
109111
|
-
let hasChanges = false;
|
|
109112
|
-
const wantsNewProps = keys.includes(ADD_NEW_PROPERTIES_KEY);
|
|
109113
|
-
const propsToEdit = keys.filter((k3) => k3 !== ADD_NEW_PROPERTIES_KEY);
|
|
109114
|
-
for (const key of propsToEdit) {
|
|
109115
|
-
const prop = configuredProps.find((p2) => p2.key === key);
|
|
109116
|
-
const currentValue = getNestedValue(course, prop.path);
|
|
109117
|
-
const newValue = await promptForProperty(prop, currentValue);
|
|
109118
|
-
if (newValue === null)
|
|
109119
|
-
return null;
|
|
109120
|
-
if (newValue !== currentValue) {
|
|
109121
|
-
setNestedValue(updated, prop.path, newValue);
|
|
109122
|
-
hasChanges = true;
|
|
109123
|
-
}
|
|
109124
|
-
}
|
|
109125
|
-
if (wantsNewProps) {
|
|
109126
|
-
const newPropKeys = await fe({
|
|
109127
|
-
message: "Select properties to add",
|
|
109128
|
-
options: unconfiguredProps.map((prop) => {
|
|
109129
|
-
const remoteValue = remoteData ? getNestedValue(remoteData, prop.path) : undefined;
|
|
109130
|
-
const hint = remoteValue !== null && remoteValue !== undefined ? dim(` (current: ${formatCurrentValue(remoteValue)})`) : "";
|
|
109131
|
-
return {
|
|
109132
|
-
value: prop.key,
|
|
109133
|
-
label: `${prop.label}${hint}`
|
|
109134
|
-
};
|
|
109135
|
-
})
|
|
109136
|
-
});
|
|
109137
|
-
if (isCancelled(newPropKeys))
|
|
109138
|
-
return null;
|
|
109139
|
-
for (const key of newPropKeys) {
|
|
109140
|
-
const prop = unconfiguredProps.find((p2) => p2.key === key);
|
|
109141
|
-
const remoteValue = remoteData ? getNestedValue(remoteData, prop.path) : undefined;
|
|
109142
|
-
const newValue = await promptForProperty(prop, remoteValue);
|
|
109143
|
-
if (newValue === null)
|
|
109144
|
-
return null;
|
|
109145
|
-
if (newValue !== undefined) {
|
|
109146
|
-
setNestedValue(updated, prop.path, newValue);
|
|
109147
|
-
hasChanges = true;
|
|
109148
|
-
}
|
|
109149
|
-
}
|
|
109150
|
-
}
|
|
109151
|
-
const updatedCourse = updated;
|
|
109152
|
-
const isDuplicate = allCourses.some((c, i) => i !== editIndex && c.subject === updatedCourse.subject && c.grade === updatedCourse.grade);
|
|
109153
|
-
if (isDuplicate) {
|
|
109154
|
-
M2.error(`A course for ${updatedCourse.subject} - Grade ${updatedCourse.grade} already exists`);
|
|
109155
|
-
return null;
|
|
109156
|
-
}
|
|
109157
|
-
if (!hasChanges) {
|
|
109158
|
-
M2.warn("No changes made");
|
|
109159
|
-
return null;
|
|
109160
|
-
}
|
|
109161
|
-
return updatedCourse;
|
|
109162
|
-
}
|
|
109163
|
-
async function promptForProperty(prop, currentValue) {
|
|
109164
|
-
switch (prop.type) {
|
|
109165
|
-
case "select-subject": {
|
|
109166
|
-
const value = await ve({
|
|
109167
|
-
message: prop.label,
|
|
109168
|
-
options: SUBJECTS.map((s) => ({ value: s, label: s })),
|
|
109169
|
-
initialValue: currentValue
|
|
109170
|
-
});
|
|
109171
|
-
if (isCancelled(value))
|
|
109172
|
-
return null;
|
|
109173
|
-
return value;
|
|
108773
|
+
if (options.disambiguate) {
|
|
108774
|
+
const suffix = course.courseCode ?? course.subject;
|
|
108775
|
+
if (suffix) {
|
|
108776
|
+
return `${appName} - ${suffix}`;
|
|
109174
108777
|
}
|
|
109175
|
-
case "select-grade": {
|
|
109176
|
-
const value = await ve({
|
|
109177
|
-
message: prop.label,
|
|
109178
|
-
options: GRADES,
|
|
109179
|
-
initialValue: currentValue
|
|
109180
|
-
});
|
|
109181
|
-
if (isCancelled(value))
|
|
109182
|
-
return null;
|
|
109183
|
-
return value;
|
|
109184
|
-
}
|
|
109185
|
-
case "select-enum": {
|
|
109186
|
-
const options = [{ value: "", label: dim("(clear)") }, ...prop.options];
|
|
109187
|
-
const value = await ve({
|
|
109188
|
-
message: prop.label,
|
|
109189
|
-
options,
|
|
109190
|
-
initialValue: currentValue ?? ""
|
|
109191
|
-
});
|
|
109192
|
-
if (isCancelled(value))
|
|
109193
|
-
return null;
|
|
109194
|
-
return value === "" ? undefined : value;
|
|
109195
|
-
}
|
|
109196
|
-
case "text": {
|
|
109197
|
-
const initial = currentValue ?? "";
|
|
109198
|
-
const value = await he({
|
|
109199
|
-
message: prop.label,
|
|
109200
|
-
initialValue: initial,
|
|
109201
|
-
placeholder: initial ? undefined : "(leave empty to clear)",
|
|
109202
|
-
validate: prop.validate
|
|
109203
|
-
});
|
|
109204
|
-
if (isCancelled(value))
|
|
109205
|
-
return null;
|
|
109206
|
-
return value === "" ? undefined : value;
|
|
109207
|
-
}
|
|
109208
|
-
case "number": {
|
|
109209
|
-
const initial = currentValue !== null && currentValue !== undefined ? String(currentValue) : "";
|
|
109210
|
-
const value = await he({
|
|
109211
|
-
message: prop.label,
|
|
109212
|
-
initialValue: initial,
|
|
109213
|
-
placeholder: initial ? undefined : "(leave empty to clear)",
|
|
109214
|
-
validate: (v2) => {
|
|
109215
|
-
if (v2 === "")
|
|
109216
|
-
return;
|
|
109217
|
-
if (!/^\d+$/.test(v2))
|
|
109218
|
-
return "Must be a positive integer";
|
|
109219
|
-
return prop.validate?.(v2);
|
|
109220
|
-
}
|
|
109221
|
-
});
|
|
109222
|
-
if (isCancelled(value))
|
|
109223
|
-
return null;
|
|
109224
|
-
return value === "" ? undefined : Number(value);
|
|
109225
|
-
}
|
|
109226
|
-
case "boolean": {
|
|
109227
|
-
const value = await ye({
|
|
109228
|
-
message: prop.label,
|
|
109229
|
-
initialValue: currentValue ?? false
|
|
109230
|
-
});
|
|
109231
|
-
if (isCancelled(value))
|
|
109232
|
-
return null;
|
|
109233
|
-
return value;
|
|
109234
|
-
}
|
|
109235
|
-
default:
|
|
109236
|
-
return currentValue;
|
|
109237
108778
|
}
|
|
109238
|
-
|
|
109239
|
-
async function promptRemoveCourses(courses) {
|
|
109240
|
-
const indices = await fe({
|
|
109241
|
-
message: "Select courses to remove",
|
|
109242
|
-
options: courses.map((c, i) => ({
|
|
109243
|
-
value: i,
|
|
109244
|
-
label: `${c.subject} - Grade ${c.grade}`
|
|
109245
|
-
}))
|
|
109246
|
-
});
|
|
109247
|
-
if (isCancelled(indices))
|
|
109248
|
-
return null;
|
|
109249
|
-
return indices;
|
|
108779
|
+
return appName;
|
|
109250
108780
|
}
|
|
109251
108781
|
|
|
109252
|
-
// src/commands/
|
|
109253
|
-
async function handleAdd(ctx) {
|
|
109254
|
-
const newCourses = await promptAddCourses(ctx.config.courses);
|
|
109255
|
-
if (newCourses === null)
|
|
109256
|
-
return "cancelled";
|
|
109257
|
-
if (newCourses.length === 0)
|
|
109258
|
-
return "discarded";
|
|
109259
|
-
logAdditions(newCourses);
|
|
109260
|
-
const confirmed = await ye({ message: "Sync with Timeback?" });
|
|
109261
|
-
if (isCancelled(confirmed))
|
|
109262
|
-
return "cancelled";
|
|
109263
|
-
if (!confirmed) {
|
|
109264
|
-
M2.info("Changes discarded");
|
|
109265
|
-
return "discarded";
|
|
109266
|
-
}
|
|
109267
|
-
const envResult = await resolveEnvironment({ skipIntro: true });
|
|
109268
|
-
if (!envResult)
|
|
109269
|
-
return "cancelled";
|
|
109270
|
-
const authSuccess = await initAuth({ env: envResult.env });
|
|
109271
|
-
if (!authSuccess) {
|
|
109272
|
-
M2.error("Failed to authenticate");
|
|
109273
|
-
return "discarded";
|
|
109274
|
-
}
|
|
109275
|
-
const authCtx = getAuthContext();
|
|
109276
|
-
if (!authCtx.credentials.email) {
|
|
109277
|
-
M2.error("Email not configured.");
|
|
109278
|
-
M2.info("Run `timeback credentials email` to configure your email.");
|
|
109279
|
-
return "discarded";
|
|
109280
|
-
}
|
|
109281
|
-
const orgResult = await selectOrganization(authCtx.timeback, authCtx.credentials.email);
|
|
109282
|
-
if (!orgResult.success) {
|
|
109283
|
-
if (orgResult.cancelled)
|
|
109284
|
-
return "cancelled";
|
|
109285
|
-
M2.error(orgResult.error ?? "Failed to select organization");
|
|
109286
|
-
return "discarded";
|
|
109287
|
-
}
|
|
109288
|
-
const baseIndex = ctx.config.courses.length;
|
|
109289
|
-
ctx.config.courses.push(...newCourses);
|
|
109290
|
-
const diff = {
|
|
109291
|
-
creates: newCourses.map((course, i) => ({
|
|
109292
|
-
type: "create",
|
|
109293
|
-
index: baseIndex + i,
|
|
109294
|
-
local: course,
|
|
109295
|
-
title: getCourseTitle(ctx.config.name, course)
|
|
109296
|
-
})),
|
|
109297
|
-
updates: [],
|
|
109298
|
-
unchanged: [],
|
|
109299
|
-
errors: []
|
|
109300
|
-
};
|
|
109301
|
-
const s = Y2();
|
|
109302
|
-
s.start("Syncing with Timeback...");
|
|
109303
|
-
const { result, updatedConfig } = await applyDiff({
|
|
109304
|
-
config: ctx.config,
|
|
109305
|
-
diff,
|
|
109306
|
-
createEnv: envResult.env,
|
|
109307
|
-
createOrgId: orgResult.orgId,
|
|
109308
|
-
onProgress: (msg) => s.message(msg)
|
|
109309
|
-
});
|
|
109310
|
-
s.stop("Sync complete");
|
|
109311
|
-
ctx.config.courses = updatedConfig.courses;
|
|
109312
|
-
await saveConfig(ctx.configPath, ctx.config);
|
|
109313
|
-
M2.success(`Saved ${dim(ctx.relativeConfigPath)}`);
|
|
109314
|
-
if (result.created > 0) {
|
|
109315
|
-
M2.success(`Created ${bold(result.created)} course${result.created === 1 ? "" : "s"} on Timeback`);
|
|
109316
|
-
}
|
|
109317
|
-
logErrors(result.errors);
|
|
109318
|
-
return "synced";
|
|
109319
|
-
}
|
|
109320
|
-
async function handleEdit(ctx) {
|
|
109321
|
-
const index = await promptSelectCourse(ctx.config.courses, "Select a course to edit");
|
|
109322
|
-
if (index === null)
|
|
109323
|
-
return "cancelled";
|
|
109324
|
-
const original = ctx.config.courses[index];
|
|
109325
|
-
const synced = getSyncedCourses(original);
|
|
109326
|
-
const remoteData = await fetchRemoteCourseData(synced);
|
|
109327
|
-
const edited = await promptEditCourse(original, ctx.config.courses, index, remoteData);
|
|
109328
|
-
if (edited === null)
|
|
109329
|
-
return "discarded";
|
|
109330
|
-
logEdit(original, edited);
|
|
109331
|
-
const confirmed = await ye({ message: "Sync with Timeback?" });
|
|
109332
|
-
if (isCancelled(confirmed))
|
|
109333
|
-
return "cancelled";
|
|
109334
|
-
if (!confirmed) {
|
|
109335
|
-
M2.info("Changes discarded");
|
|
109336
|
-
return "discarded";
|
|
109337
|
-
}
|
|
109338
|
-
const s = Y2();
|
|
109339
|
-
if (synced.length > 0) {
|
|
109340
|
-
edited.ids = { ...original.ids };
|
|
109341
|
-
const title = getCourseTitle(ctx.config.name, edited);
|
|
109342
|
-
for (const { env: env2, courseId } of synced) {
|
|
109343
|
-
s.start(`Updating on ${env2}...`);
|
|
109344
|
-
const result = await updateCourse({
|
|
109345
|
-
env: env2,
|
|
109346
|
-
courseId,
|
|
109347
|
-
title,
|
|
109348
|
-
course: edited,
|
|
109349
|
-
defaults: ctx.config.defaults
|
|
109350
|
-
});
|
|
109351
|
-
if (!result.success) {
|
|
109352
|
-
s.stop(`Update failed on ${env2}`);
|
|
109353
|
-
if (result.error) {
|
|
109354
|
-
logErrors([result.error]);
|
|
109355
|
-
}
|
|
109356
|
-
return "discarded";
|
|
109357
|
-
}
|
|
109358
|
-
}
|
|
109359
|
-
s.stop(`Updated on ${synced.map((s2) => s2.env).join(" and ")}`);
|
|
109360
|
-
} else {
|
|
109361
|
-
const envResult = await resolveEnvironment({ skipIntro: true });
|
|
109362
|
-
if (!envResult)
|
|
109363
|
-
return "cancelled";
|
|
109364
|
-
const authSuccess = await initAuth({ env: envResult.env });
|
|
109365
|
-
if (!authSuccess) {
|
|
109366
|
-
M2.error("Failed to authenticate");
|
|
109367
|
-
return "discarded";
|
|
109368
|
-
}
|
|
109369
|
-
const authCtx = getAuthContext();
|
|
109370
|
-
if (!authCtx.credentials.email) {
|
|
109371
|
-
M2.error("Email not configured.");
|
|
109372
|
-
M2.info("Run `timeback credentials email` to configure your email.");
|
|
109373
|
-
return "discarded";
|
|
109374
|
-
}
|
|
109375
|
-
const orgResult = await selectOrganization(authCtx.timeback, authCtx.credentials.email);
|
|
109376
|
-
if (!orgResult.success) {
|
|
109377
|
-
if (orgResult.cancelled)
|
|
109378
|
-
return "cancelled";
|
|
109379
|
-
M2.error(orgResult.error ?? "Failed to select organization");
|
|
109380
|
-
return "discarded";
|
|
109381
|
-
}
|
|
109382
|
-
ctx.config.courses[index] = edited;
|
|
109383
|
-
const diff = {
|
|
109384
|
-
creates: [
|
|
109385
|
-
{
|
|
109386
|
-
type: "create",
|
|
109387
|
-
index,
|
|
109388
|
-
local: edited,
|
|
109389
|
-
title: getCourseTitle(ctx.config.name, edited)
|
|
109390
|
-
}
|
|
109391
|
-
],
|
|
109392
|
-
updates: [],
|
|
109393
|
-
unchanged: [],
|
|
109394
|
-
errors: []
|
|
109395
|
-
};
|
|
109396
|
-
s.start("Creating course on Timeback...");
|
|
109397
|
-
const { result, updatedConfig } = await applyDiff({
|
|
109398
|
-
config: ctx.config,
|
|
109399
|
-
diff,
|
|
109400
|
-
createEnv: envResult.env,
|
|
109401
|
-
createOrgId: orgResult.orgId,
|
|
109402
|
-
onProgress: (msg) => s.message(msg)
|
|
109403
|
-
});
|
|
109404
|
-
s.stop("Sync complete");
|
|
109405
|
-
ctx.config.courses = updatedConfig.courses;
|
|
109406
|
-
if (result.created > 0) {
|
|
109407
|
-
M2.success("Created course on Timeback");
|
|
109408
|
-
}
|
|
109409
|
-
logErrors(result.errors);
|
|
109410
|
-
await saveConfig(ctx.configPath, ctx.config);
|
|
109411
|
-
M2.success(`Saved ${dim(ctx.relativeConfigPath)}`);
|
|
109412
|
-
return "synced";
|
|
109413
|
-
}
|
|
109414
|
-
ctx.config.courses[index] = edited;
|
|
109415
|
-
await saveConfig(ctx.configPath, ctx.config);
|
|
109416
|
-
M2.success(`Saved ${dim(ctx.relativeConfigPath)}`);
|
|
109417
|
-
return "synced";
|
|
109418
|
-
}
|
|
109419
|
-
async function handleRemove(ctx) {
|
|
109420
|
-
const indices = await promptRemoveCourses(ctx.config.courses);
|
|
109421
|
-
if (indices === null)
|
|
109422
|
-
return "cancelled";
|
|
109423
|
-
if (indices.length === 0)
|
|
109424
|
-
return "discarded";
|
|
109425
|
-
const coursesToRemove = indices.map((i) => ctx.config.courses[i]);
|
|
109426
|
-
logRemovals(coursesToRemove);
|
|
109427
|
-
const confirmed = await ye({
|
|
109428
|
-
message: "Remove from config? (Timeback courses are unaffected)"
|
|
109429
|
-
});
|
|
109430
|
-
if (isCancelled(confirmed))
|
|
109431
|
-
return "cancelled";
|
|
109432
|
-
if (!confirmed) {
|
|
109433
|
-
M2.info("Changes discarded");
|
|
109434
|
-
return "discarded";
|
|
109435
|
-
}
|
|
109436
|
-
const sortedIndices = indices.toSorted((a, b3) => b3 - a);
|
|
109437
|
-
for (const i of sortedIndices) {
|
|
109438
|
-
ctx.config.courses.splice(i, 1);
|
|
109439
|
-
}
|
|
109440
|
-
await saveConfig(ctx.configPath, ctx.config);
|
|
109441
|
-
M2.success(`Saved ${dim(ctx.relativeConfigPath)}`);
|
|
109442
|
-
return "saved";
|
|
109443
|
-
}
|
|
109444
|
-
// src/commands/edit/edit.ts
|
|
109445
|
-
async function editConfig(options = {}) {
|
|
109446
|
-
const { configPath: customConfigPath, exitOnComplete = true } = options;
|
|
109447
|
-
intro("Timeback Edit");
|
|
109448
|
-
const configResult = await loadConfig({ configPath: customConfigPath });
|
|
109449
|
-
if (!configResult.success) {
|
|
109450
|
-
M2.error(configResult.error);
|
|
109451
|
-
outro.error("Edit failed");
|
|
109452
|
-
if (exitOnComplete)
|
|
109453
|
-
process.exit(1);
|
|
109454
|
-
return;
|
|
109455
|
-
}
|
|
109456
|
-
const { configPath } = configResult;
|
|
109457
|
-
const relativeConfigPath = getRelativeConfigPath(configPath);
|
|
109458
|
-
const config7 = {
|
|
109459
|
-
...configResult.config,
|
|
109460
|
-
courses: [...configResult.config.courses]
|
|
109461
|
-
};
|
|
109462
|
-
M2.info(`Editing ${dim(relativeConfigPath)}`);
|
|
109463
|
-
M2.info(`${bold(config7.courses.length)} course${config7.courses.length === 1 ? "" : "s"} defined`);
|
|
109464
|
-
const ctx = { config: config7, configPath, relativeConfigPath };
|
|
109465
|
-
let hasChanges = false;
|
|
109466
|
-
while (true) {
|
|
109467
|
-
const action = await promptAction(config7.courses.length);
|
|
109468
|
-
if (action === null) {
|
|
109469
|
-
outro.cancelled();
|
|
109470
|
-
if (exitOnComplete)
|
|
109471
|
-
process.exit(0);
|
|
109472
|
-
return;
|
|
109473
|
-
}
|
|
109474
|
-
if (action === "done")
|
|
109475
|
-
break;
|
|
109476
|
-
let result;
|
|
109477
|
-
if (action === "add")
|
|
109478
|
-
result = await handleAdd(ctx);
|
|
109479
|
-
else if (action === "edit")
|
|
109480
|
-
result = await handleEdit(ctx);
|
|
109481
|
-
else
|
|
109482
|
-
result = await handleRemove(ctx);
|
|
109483
|
-
if (result === "cancelled") {
|
|
109484
|
-
outro.cancelled();
|
|
109485
|
-
if (exitOnComplete)
|
|
109486
|
-
process.exit(0);
|
|
109487
|
-
return;
|
|
109488
|
-
}
|
|
109489
|
-
if (result === "synced" || result === "saved")
|
|
109490
|
-
hasChanges = true;
|
|
109491
|
-
}
|
|
109492
|
-
if (!hasChanges) {
|
|
109493
|
-
M2.info("No changes made");
|
|
109494
|
-
}
|
|
109495
|
-
outro.success();
|
|
109496
|
-
if (exitOnComplete)
|
|
109497
|
-
process.exit(0);
|
|
109498
|
-
}
|
|
109499
|
-
|
|
109500
|
-
// src/commands/edit/index.ts
|
|
109501
|
-
function registerEditCommand(program2) {
|
|
109502
|
-
program2.command("edit").description("Add, edit, or remove courses in local config").option("-c, --config <path>", "Path to the config file").action((opts) => editConfig({ configPath: opts.config }));
|
|
109503
|
-
}
|
|
109504
|
-
|
|
109505
|
-
// src/commands/import/lib/fetch.ts
|
|
109506
|
-
async function fetchRemoteCourse(options) {
|
|
109507
|
-
const { courseId, env: env2 } = options;
|
|
109508
|
-
try {
|
|
109509
|
-
const client = getAuthContext(env2).timeback;
|
|
109510
|
-
const remoteCourse = await client.oneroster.courses.get(courseId);
|
|
109511
|
-
const course = {
|
|
109512
|
-
subject: remoteCourse.subjects?.[0] ?? "None",
|
|
109513
|
-
grade: remoteCourse.grades?.[0] ?? 0,
|
|
109514
|
-
ids: {
|
|
109515
|
-
[env2]: courseId
|
|
109516
|
-
}
|
|
109517
|
-
};
|
|
109518
|
-
if (remoteCourse.courseCode) {
|
|
109519
|
-
course.courseCode = remoteCourse.courseCode;
|
|
109520
|
-
}
|
|
109521
|
-
if (remoteCourse.level) {
|
|
109522
|
-
course.level = remoteCourse.level;
|
|
109523
|
-
}
|
|
109524
|
-
if (remoteCourse.metadata) {
|
|
109525
|
-
course.metadata = {
|
|
109526
|
-
courseType: remoteCourse.metadata.courseType,
|
|
109527
|
-
isSupplemental: remoteCourse.metadata.isSupplemental,
|
|
109528
|
-
isCustom: remoteCourse.metadata.isCustom,
|
|
109529
|
-
publishStatus: remoteCourse.metadata.publishStatus,
|
|
109530
|
-
contactEmail: remoteCourse.metadata.contactEmail,
|
|
109531
|
-
primaryApp: remoteCourse.metadata.primaryApp,
|
|
109532
|
-
goals: remoteCourse.metadata.goals,
|
|
109533
|
-
metrics: remoteCourse.metadata.metrics
|
|
109534
|
-
};
|
|
109535
|
-
course.metadata = Object.fromEntries(Object.entries(course.metadata).filter(([, v2]) => v2 !== undefined));
|
|
109536
|
-
if (Object.keys(course.metadata).length === 0) {
|
|
109537
|
-
delete course.metadata;
|
|
109538
|
-
}
|
|
109539
|
-
}
|
|
109540
|
-
return { success: true, course };
|
|
109541
|
-
} catch (error57) {
|
|
109542
|
-
const formatted = formatApiError(error57);
|
|
109543
|
-
return {
|
|
109544
|
-
success: false,
|
|
109545
|
-
error: `${formatted.header}${formatted.details.length > 0 ? `: ${formatted.details.join(", ")}` : ""}`
|
|
109546
|
-
};
|
|
109547
|
-
}
|
|
109548
|
-
}
|
|
109549
|
-
function findExistingCourse(config7, courseId, env2) {
|
|
109550
|
-
const index = config7.courses.findIndex((c) => c.ids?.[env2] === courseId);
|
|
109551
|
-
if (index === -1)
|
|
109552
|
-
return;
|
|
109553
|
-
if (!config7.courses[index])
|
|
109554
|
-
return;
|
|
109555
|
-
return {
|
|
109556
|
-
index,
|
|
109557
|
-
course: config7.courses[index]
|
|
109558
|
-
};
|
|
109559
|
-
}
|
|
109560
|
-
// src/commands/import/import.ts
|
|
109561
|
-
function getExistingCourseIds(config7) {
|
|
109562
|
-
const byEnv = {};
|
|
109563
|
-
for (const course of config7.courses) {
|
|
109564
|
-
for (const [env2, id] of Object.entries(course.ids ?? {})) {
|
|
109565
|
-
if (!byEnv[env2])
|
|
109566
|
-
byEnv[env2] = [];
|
|
109567
|
-
if (id)
|
|
109568
|
-
byEnv[env2].push(id);
|
|
109569
|
-
}
|
|
109570
|
-
}
|
|
109571
|
-
return byEnv;
|
|
109572
|
-
}
|
|
109573
|
-
function formatCourseLabel2(course) {
|
|
109574
|
-
if (course.grade !== undefined) {
|
|
109575
|
-
return `${course.subject} Grade ${course.grade}`;
|
|
109576
|
-
}
|
|
109577
|
-
if (course.courseCode) {
|
|
109578
|
-
return `${course.subject} (${course.courseCode})`;
|
|
109579
|
-
}
|
|
109580
|
-
return course.subject;
|
|
109581
|
-
}
|
|
109582
|
-
function formatExistingCourses(config7) {
|
|
109583
|
-
if (config7.courses.length === 0) {
|
|
109584
|
-
return dim("No courses configured yet.");
|
|
109585
|
-
}
|
|
109586
|
-
const byEnv = {};
|
|
109587
|
-
for (const course of config7.courses) {
|
|
109588
|
-
const label = formatCourseLabel2(course);
|
|
109589
|
-
const envs = Object.keys(course.ids ?? {});
|
|
109590
|
-
for (const env2 of envs) {
|
|
109591
|
-
if (!byEnv[env2])
|
|
109592
|
-
byEnv[env2] = [];
|
|
109593
|
-
byEnv[env2].push(label);
|
|
109594
|
-
}
|
|
109595
|
-
}
|
|
109596
|
-
const envOrder = ["staging", "production"];
|
|
109597
|
-
const sortedEnvs = Object.keys(byEnv).sort((a, b3) => envOrder.indexOf(a) - envOrder.indexOf(b3));
|
|
109598
|
-
return sortedEnvs.map((env2) => {
|
|
109599
|
-
const courses = byEnv[env2] ?? [];
|
|
109600
|
-
const header = bold(env2);
|
|
109601
|
-
const list = courses.map((c, i) => ` ${dim(`${i + 1}.`)} ${c}`).join(`
|
|
109602
|
-
`);
|
|
109603
|
-
return `${header}
|
|
109604
|
-
${list}`;
|
|
109605
|
-
}).join(`
|
|
109606
|
-
|
|
109607
|
-
`);
|
|
109608
|
-
}
|
|
109609
|
-
async function importByCourseId(options) {
|
|
109610
|
-
const {
|
|
109611
|
-
courseId,
|
|
109612
|
-
configPath: customConfigPath,
|
|
109613
|
-
env: envOption,
|
|
109614
|
-
exitOnComplete = true
|
|
109615
|
-
} = options;
|
|
109616
|
-
intro("Timeback Import");
|
|
109617
|
-
const configResult = await loadConfig({ configPath: customConfigPath });
|
|
109618
|
-
if (!configResult.success) {
|
|
109619
|
-
M2.error(configResult.error);
|
|
109620
|
-
outro.error("Import failed");
|
|
109621
|
-
if (exitOnComplete)
|
|
109622
|
-
process.exit(1);
|
|
109623
|
-
return;
|
|
109624
|
-
}
|
|
109625
|
-
const { config: config7, configPath } = configResult;
|
|
109626
|
-
const relativeConfigPath = getRelativeConfigPath(configPath);
|
|
109627
|
-
M2.info(`Loaded ${dim(relativeConfigPath)}`);
|
|
109628
|
-
const envResult = await resolveEnvironment({
|
|
109629
|
-
env: envOption,
|
|
109630
|
-
skipIntro: true
|
|
109631
|
-
});
|
|
109632
|
-
if (!envResult) {
|
|
109633
|
-
outro.cancelled();
|
|
109634
|
-
if (exitOnComplete)
|
|
109635
|
-
process.exit(0);
|
|
109636
|
-
return;
|
|
109637
|
-
}
|
|
109638
|
-
const { env: env2 } = envResult;
|
|
109639
|
-
const authOk = await initAuth({ env: env2 });
|
|
109640
|
-
if (!authOk) {
|
|
109641
|
-
outro.error("Import failed");
|
|
109642
|
-
if (exitOnComplete)
|
|
109643
|
-
process.exit(1);
|
|
109644
|
-
return;
|
|
109645
|
-
}
|
|
109646
|
-
const s = Y2();
|
|
109647
|
-
s.start(`Fetching course ${dim(courseId)} from ${bold(env2)}...`);
|
|
109648
|
-
const fetchResult = await fetchRemoteCourse({ courseId, env: env2 });
|
|
109649
|
-
if (!fetchResult.success || !fetchResult.course) {
|
|
109650
|
-
s.stop("Fetch failed");
|
|
109651
|
-
M2.error(fetchResult.error ?? "Unknown error fetching course");
|
|
109652
|
-
outro.error("Import failed");
|
|
109653
|
-
if (exitOnComplete) {
|
|
109654
|
-
process.exit(1);
|
|
109655
|
-
}
|
|
109656
|
-
return;
|
|
109657
|
-
}
|
|
109658
|
-
s.stop("Course fetched");
|
|
109659
|
-
const remoteCourse = fetchResult.course;
|
|
109660
|
-
const existing = findExistingCourse(config7, courseId, env2);
|
|
109661
|
-
if (existing) {
|
|
109662
|
-
M2.warn(`Course ${blueBright(courseId)} already exists in config at index ${existing.index}`);
|
|
109663
|
-
const shouldSync = await ye({
|
|
109664
|
-
message: "Sync (update) the existing entry with remote data?",
|
|
109665
|
-
initialValue: false
|
|
109666
|
-
});
|
|
109667
|
-
if (isCancelled(shouldSync) || !shouldSync) {
|
|
109668
|
-
outro.cancelled();
|
|
109669
|
-
if (exitOnComplete) {
|
|
109670
|
-
process.exit(0);
|
|
109671
|
-
}
|
|
109672
|
-
return;
|
|
109673
|
-
}
|
|
109674
|
-
config7.courses[existing.index] = {
|
|
109675
|
-
...existing.course,
|
|
109676
|
-
...remoteCourse,
|
|
109677
|
-
ids: {
|
|
109678
|
-
...existing.course.ids,
|
|
109679
|
-
...remoteCourse.ids
|
|
109680
|
-
}
|
|
109681
|
-
};
|
|
109682
|
-
await saveConfig(configPath, config7);
|
|
109683
|
-
M2.success(`Updated course at index ${existing.index}`);
|
|
109684
|
-
} else {
|
|
109685
|
-
config7.courses.push(remoteCourse);
|
|
109686
|
-
await saveConfig(configPath, config7);
|
|
109687
|
-
M2.success(`Added ${bold(formatCourseLabel2(remoteCourse))} to config`);
|
|
109688
|
-
}
|
|
109689
|
-
M2.success(`Saved ${dim(relativeConfigPath)}`);
|
|
109690
|
-
outro.success();
|
|
109691
|
-
if (exitOnComplete) {
|
|
109692
|
-
process.exit(0);
|
|
109693
|
-
}
|
|
109694
|
-
}
|
|
109695
|
-
async function importInteractive(options) {
|
|
109696
|
-
const { configPath, exitOnComplete = true, format = true } = options;
|
|
109697
|
-
intro("Timeback Import");
|
|
109698
|
-
const existingConfig = await loadConfig({ configPath });
|
|
109699
|
-
if (!existingConfig.success && existingConfig.error?.includes("No timeback config found")) {
|
|
109700
|
-
const createResult = await ensureConfig({
|
|
109701
|
-
configPath,
|
|
109702
|
-
skipIntro: true,
|
|
109703
|
-
format
|
|
109704
|
-
});
|
|
109705
|
-
if (!createResult.success) {
|
|
109706
|
-
if (createResult.cancelled) {
|
|
109707
|
-
outro.cancelled();
|
|
109708
|
-
} else if (createResult.error) {
|
|
109709
|
-
M2.error(createResult.error);
|
|
109710
|
-
outro.error("Import failed");
|
|
109711
|
-
}
|
|
109712
|
-
if (exitOnComplete)
|
|
109713
|
-
process.exit(createResult.cancelled ? 0 : 1);
|
|
109714
|
-
return;
|
|
109715
|
-
}
|
|
109716
|
-
outro.success();
|
|
109717
|
-
if (exitOnComplete)
|
|
109718
|
-
process.exit(0);
|
|
109719
|
-
return;
|
|
109720
|
-
}
|
|
109721
|
-
if (!existingConfig.success) {
|
|
109722
|
-
M2.error(existingConfig.error);
|
|
109723
|
-
outro.error("Import failed");
|
|
109724
|
-
if (exitOnComplete)
|
|
109725
|
-
process.exit(1);
|
|
109726
|
-
return;
|
|
109727
|
-
}
|
|
109728
|
-
const { config: config7 } = existingConfig;
|
|
109729
|
-
Me(`${blueBright(bold(config7.name))}
|
|
109730
|
-
|
|
109731
|
-
${formatExistingCourses(config7)}`, `${config7.courses.length} course${config7.courses.length === 1 ? "" : "s"} configured`);
|
|
109732
|
-
const shouldImport = await ye({
|
|
109733
|
-
message: "Would you like to import more courses?",
|
|
109734
|
-
initialValue: true
|
|
109735
|
-
});
|
|
109736
|
-
if (isCancelled(shouldImport) || !shouldImport) {
|
|
109737
|
-
outro.cancelled();
|
|
109738
|
-
if (exitOnComplete) {
|
|
109739
|
-
process.exit(0);
|
|
109740
|
-
}
|
|
109741
|
-
return;
|
|
109742
|
-
}
|
|
109743
|
-
const existingCourseIds = getExistingCourseIds(config7);
|
|
109744
|
-
const result = await runImportFlow({
|
|
109745
|
-
configPath,
|
|
109746
|
-
appName: config7.name,
|
|
109747
|
-
existingCourseIds,
|
|
109748
|
-
format
|
|
109749
|
-
});
|
|
109750
|
-
if (!result.success) {
|
|
109751
|
-
if (result.cancelled) {
|
|
109752
|
-
outro.cancelled();
|
|
109753
|
-
} else if (result.error) {
|
|
109754
|
-
M2.error(result.error);
|
|
109755
|
-
outro.error("Import failed");
|
|
109756
|
-
}
|
|
109757
|
-
if (exitOnComplete)
|
|
109758
|
-
process.exit(result.cancelled ? 0 : 1);
|
|
109759
|
-
return;
|
|
109760
|
-
}
|
|
109761
|
-
outro.success();
|
|
109762
|
-
if (exitOnComplete)
|
|
109763
|
-
process.exit(0);
|
|
109764
|
-
}
|
|
109765
|
-
async function importCommand(options) {
|
|
109766
|
-
const { courseId, configPath, env: env2, exitOnComplete = true, format = true } = options;
|
|
109767
|
-
if (courseId) {
|
|
109768
|
-
await importByCourseId({ courseId, configPath, env: env2, exitOnComplete });
|
|
109769
|
-
} else {
|
|
109770
|
-
await importInteractive({ configPath, exitOnComplete, format });
|
|
109771
|
-
}
|
|
109772
|
-
}
|
|
109773
|
-
|
|
109774
|
-
// src/commands/import/index.ts
|
|
109775
|
-
function registerImportCommand(program2) {
|
|
109776
|
-
program2.command("import [course-id]").description("Import courses from Timeback (exclude course-id to enter interactive mode)").option("-c, --config <path>", "Path to the config file").option("--env <environment>", "Target environment (staging or production)").option("--no-format", "Do not run prettier on the config file").action(async (courseId, opts) => {
|
|
109777
|
-
await importCommand({
|
|
109778
|
-
courseId,
|
|
109779
|
-
configPath: opts.config,
|
|
109780
|
-
env: opts.env,
|
|
109781
|
-
format: opts.format
|
|
109782
|
-
});
|
|
109783
|
-
});
|
|
109784
|
-
}
|
|
109785
|
-
|
|
109786
|
-
// src/commands/init/create.ts
|
|
109787
|
-
import { existsSync as existsSync4 } from "node:fs";
|
|
109788
|
-
import { writeFile as writeFile4 } from "node:fs/promises";
|
|
109789
|
-
import { basename as basename2, resolve as resolve5 } from "node:path";
|
|
109790
|
-
|
|
109791
|
-
// src/commands/sync/lib/compare.ts
|
|
108782
|
+
// src/commands/resources/push/lib/compare.ts
|
|
109792
108783
|
function compareWithTitle(localTitle, remoteTitle, localConfig, remoteData) {
|
|
109793
108784
|
const changes = [];
|
|
109794
108785
|
if (remoteTitle !== localTitle) {
|
|
@@ -109806,11 +108797,15 @@ async function compareCourses(options) {
|
|
|
109806
108797
|
unchanged: [],
|
|
109807
108798
|
errors: []
|
|
109808
108799
|
};
|
|
108800
|
+
const gradelessCourseCount = config7.courses.filter((c) => c?.grade === undefined).length;
|
|
108801
|
+
const disambiguateGradeless = gradelessCourseCount > 1;
|
|
109809
108802
|
for (let i = 0;i < config7.courses.length; i++) {
|
|
109810
108803
|
const course = config7.courses[i];
|
|
109811
|
-
if (!course)
|
|
108804
|
+
if (!course) {
|
|
109812
108805
|
continue;
|
|
109813
|
-
|
|
108806
|
+
}
|
|
108807
|
+
const disambiguate = disambiguateGradeless && course.grade === undefined;
|
|
108808
|
+
const expectedTitle = getCourseTitle(config7.name, course, { disambiguate });
|
|
109814
108809
|
const synced = getSyncedCourses(course);
|
|
109815
108810
|
if (synced.length === 0) {
|
|
109816
108811
|
diff.creates.push({
|
|
@@ -109876,7 +108871,128 @@ async function compareCourses(options) {
|
|
|
109876
108871
|
}
|
|
109877
108872
|
return diff;
|
|
109878
108873
|
}
|
|
109879
|
-
// src/commands/
|
|
108874
|
+
// src/commands/resources/push/lib/courses.ts
|
|
108875
|
+
async function applyDiff(options) {
|
|
108876
|
+
const { config: config7, diff, createEnv, createOrgId, onProgress } = options;
|
|
108877
|
+
const result = {
|
|
108878
|
+
created: 0,
|
|
108879
|
+
updated: 0,
|
|
108880
|
+
skipped: diff.unchanged.length,
|
|
108881
|
+
errors: [...diff.errors],
|
|
108882
|
+
skippedStructure: []
|
|
108883
|
+
};
|
|
108884
|
+
const structureAttempted = new Set;
|
|
108885
|
+
const updatedConfig = { ...config7, courses: [...config7.courses] };
|
|
108886
|
+
for (const create of diff.creates) {
|
|
108887
|
+
if (!createEnv || !createOrgId) {
|
|
108888
|
+
result.errors.push({
|
|
108889
|
+
title: create.title,
|
|
108890
|
+
header: "Environment and organization required for new courses",
|
|
108891
|
+
details: []
|
|
108892
|
+
});
|
|
108893
|
+
continue;
|
|
108894
|
+
}
|
|
108895
|
+
const authOk = await initAuth({ env: createEnv });
|
|
108896
|
+
if (!authOk) {
|
|
108897
|
+
result.errors.push({
|
|
108898
|
+
title: create.title,
|
|
108899
|
+
header: `Failed to authenticate for ${createEnv}`,
|
|
108900
|
+
details: []
|
|
108901
|
+
});
|
|
108902
|
+
continue;
|
|
108903
|
+
}
|
|
108904
|
+
onProgress?.(`Creating ${create.title} on ${createEnv}...`);
|
|
108905
|
+
const resolved = resolveCourseForEnv(create.local, config7.defaults, createEnv);
|
|
108906
|
+
const metadata = buildCreateMetadata(resolved);
|
|
108907
|
+
try {
|
|
108908
|
+
const client = getAuthContext(createEnv).timeback;
|
|
108909
|
+
const response = await client.oneroster.courses.create({
|
|
108910
|
+
title: create.title,
|
|
108911
|
+
subjects: [resolved.subject],
|
|
108912
|
+
...resolved.grade === undefined ? {} : { grades: [resolved.grade] },
|
|
108913
|
+
org: { sourcedId: createOrgId },
|
|
108914
|
+
status: "active",
|
|
108915
|
+
courseCode: resolved.courseCode,
|
|
108916
|
+
level: resolved.level,
|
|
108917
|
+
metadata
|
|
108918
|
+
});
|
|
108919
|
+
const allocatedCourseId = response.sourcedIdPairs.allocatedSourcedId;
|
|
108920
|
+
const createdCourse = await client.oneroster.courses.get(allocatedCourseId);
|
|
108921
|
+
const serverMetadata = createdCourse?.metadata;
|
|
108922
|
+
updatedConfig.courses[create.index] = {
|
|
108923
|
+
...create.local,
|
|
108924
|
+
ids: {
|
|
108925
|
+
...create.local.ids,
|
|
108926
|
+
[createEnv]: allocatedCourseId
|
|
108927
|
+
},
|
|
108928
|
+
...serverMetadata?.goals ? { metadata: { ...create.local.metadata, goals: serverMetadata.goals } } : {}
|
|
108929
|
+
};
|
|
108930
|
+
result.created++;
|
|
108931
|
+
const effectiveLaunchUrl = resolved.launchUrl ?? config7.launchUrl;
|
|
108932
|
+
if (effectiveLaunchUrl) {
|
|
108933
|
+
structureAttempted.add(create.title);
|
|
108934
|
+
onProgress?.(`Ensuring course structure for ${create.title}...`);
|
|
108935
|
+
const structureResult = await ensureCourseStructure({
|
|
108936
|
+
env: createEnv,
|
|
108937
|
+
courseId: allocatedCourseId,
|
|
108938
|
+
courseTitle: create.title,
|
|
108939
|
+
appName: config7.name,
|
|
108940
|
+
launchUrl: effectiveLaunchUrl,
|
|
108941
|
+
onProgress
|
|
108942
|
+
});
|
|
108943
|
+
if (!structureResult.success && structureResult.error) {
|
|
108944
|
+
result.errors.push(structureResult.error);
|
|
108945
|
+
}
|
|
108946
|
+
} else if (!result.skippedStructure.includes(create.title)) {
|
|
108947
|
+
result.skippedStructure.push(create.title);
|
|
108948
|
+
}
|
|
108949
|
+
} catch (error57) {
|
|
108950
|
+
const formatted = formatApiError(error57);
|
|
108951
|
+
result.errors.push({ title: create.title, ...formatted });
|
|
108952
|
+
}
|
|
108953
|
+
}
|
|
108954
|
+
for (const update of diff.updates) {
|
|
108955
|
+
for (const { env: env2, courseId } of update.environments) {
|
|
108956
|
+
onProgress?.(`Updating ${update.localTitle} on ${env2}...`);
|
|
108957
|
+
const updateResult = await updateCourse({
|
|
108958
|
+
env: env2,
|
|
108959
|
+
courseId,
|
|
108960
|
+
title: update.localTitle,
|
|
108961
|
+
course: update.local,
|
|
108962
|
+
defaults: config7.defaults
|
|
108963
|
+
});
|
|
108964
|
+
if (!updateResult.success && updateResult.error) {
|
|
108965
|
+
result.errors.push(updateResult.error);
|
|
108966
|
+
continue;
|
|
108967
|
+
}
|
|
108968
|
+
const resolved = resolveCourseForEnv(update.local, config7.defaults, env2);
|
|
108969
|
+
const effectiveLaunchUrl = resolved.launchUrl ?? config7.launchUrl;
|
|
108970
|
+
if (effectiveLaunchUrl) {
|
|
108971
|
+
structureAttempted.add(update.localTitle);
|
|
108972
|
+
onProgress?.(`Ensuring course structure for ${update.localTitle}...`);
|
|
108973
|
+
const structureResult = await ensureCourseStructure({
|
|
108974
|
+
env: env2,
|
|
108975
|
+
courseId,
|
|
108976
|
+
courseTitle: update.localTitle,
|
|
108977
|
+
appName: config7.name,
|
|
108978
|
+
launchUrl: effectiveLaunchUrl,
|
|
108979
|
+
onProgress
|
|
108980
|
+
});
|
|
108981
|
+
if (!structureResult.success && structureResult.error) {
|
|
108982
|
+
result.errors.push(structureResult.error);
|
|
108983
|
+
}
|
|
108984
|
+
}
|
|
108985
|
+
}
|
|
108986
|
+
if (!structureAttempted.has(update.localTitle)) {
|
|
108987
|
+
if (!result.skippedStructure.includes(update.localTitle)) {
|
|
108988
|
+
result.skippedStructure.push(update.localTitle);
|
|
108989
|
+
}
|
|
108990
|
+
}
|
|
108991
|
+
result.updated++;
|
|
108992
|
+
}
|
|
108993
|
+
return { result, updatedConfig };
|
|
108994
|
+
}
|
|
108995
|
+
// src/commands/resources/push/lib/diff.ts
|
|
109880
108996
|
function formatChange(update) {
|
|
109881
108997
|
const { changes } = update;
|
|
109882
108998
|
const maxFieldLen = Math.max(...changes.map((c) => c.field.length));
|
|
@@ -109885,30 +109001,59 @@ function formatChange(update) {
|
|
|
109885
109001
|
const paddedField = change.field.padStart(maxFieldLen);
|
|
109886
109002
|
const localVal = change.local || dim("(empty)");
|
|
109887
109003
|
const paddedRemote = (change.remote || "(empty)").padStart(maxRemoteLen);
|
|
109888
|
-
return ` ${dim(
|
|
109004
|
+
return ` ${dim(paddedField)} ${red(paddedRemote)} ${dim("→")} ${green(localVal)}`;
|
|
109889
109005
|
});
|
|
109890
109006
|
}
|
|
109891
|
-
function
|
|
109007
|
+
function displayDiff(diff, options) {
|
|
109008
|
+
const { createEnv } = options ?? {};
|
|
109892
109009
|
const hasChanges = diff.creates.length > 0 || diff.updates.length > 0;
|
|
109893
109010
|
if (!hasChanges) {
|
|
109894
109011
|
return;
|
|
109895
109012
|
}
|
|
109896
|
-
M2.message(bold("Changes:"));
|
|
109897
|
-
const lines = [];
|
|
109898
109013
|
for (const create of diff.creates) {
|
|
109899
|
-
lines
|
|
109014
|
+
const lines = [];
|
|
109015
|
+
const { local } = create;
|
|
109016
|
+
const courseCode = local.courseCode ? ` (${local.courseCode})` : "";
|
|
109017
|
+
lines.push(`${bold(create.title)}${dim(courseCode)}`);
|
|
109018
|
+
lines.push("");
|
|
109019
|
+
const targetInfo = createEnv ? dim((createEnv === "production" ? magenta : cyan)(createEnv)) : dim("(new)");
|
|
109020
|
+
lines.push(` ${green("+ create")} ${targetInfo}`);
|
|
109021
|
+
lines.push("");
|
|
109022
|
+
const fields = [
|
|
109023
|
+
{ label: "title", value: create.title },
|
|
109024
|
+
{ label: "subject", value: local.subject },
|
|
109025
|
+
{ label: "grade", value: local.grade },
|
|
109026
|
+
{ label: "courseCode", value: local.courseCode }
|
|
109027
|
+
].filter((f2) => f2.value !== undefined && f2.value !== null);
|
|
109028
|
+
const maxFieldLen = Math.max(...fields.map((f2) => f2.label.length));
|
|
109029
|
+
for (const field of fields) {
|
|
109030
|
+
lines.push(` ${dim(field.label.padStart(maxFieldLen))} ${green(field.value)}`);
|
|
109031
|
+
}
|
|
109032
|
+
M2.message(lines.join(`
|
|
109033
|
+
`), { symbol: green("●") });
|
|
109900
109034
|
}
|
|
109901
109035
|
for (const update of diff.updates) {
|
|
109036
|
+
const lines = [];
|
|
109037
|
+
const { local } = update;
|
|
109038
|
+
const courseCode = local.courseCode ? ` (${local.courseCode})` : "";
|
|
109039
|
+
lines.push(`${bold(update.localTitle)}${dim(courseCode)}`);
|
|
109040
|
+
lines.push("");
|
|
109041
|
+
const targetInfo = update.environments.map((env2) => {
|
|
109042
|
+
const envColor = env2.env === "production" ? magenta : cyan;
|
|
109043
|
+
const shortId = env2.courseId.slice(-4);
|
|
109044
|
+
return `${dim(envColor(`${env2.env}:`))}${dim(`..${shortId}`)}`;
|
|
109045
|
+
}).join(" ");
|
|
109046
|
+
lines.push(` ${yellow("~ update")} ${targetInfo}`);
|
|
109047
|
+
lines.push("");
|
|
109902
109048
|
lines.push(...formatChange(update));
|
|
109049
|
+
M2.message(lines.join(`
|
|
109050
|
+
`), { symbol: yellow("●") });
|
|
109903
109051
|
}
|
|
109904
109052
|
if (diff.unchanged.length > 0) {
|
|
109905
|
-
|
|
109906
|
-
lines.push(dim(` ${diff.unchanged.length} course${diff.unchanged.length === 1 ? "" : "s"} unchanged`));
|
|
109053
|
+
M2.message(dim(`${diff.unchanged.length} course${diff.unchanged.length === 1 ? "" : "s"} unchanged`));
|
|
109907
109054
|
}
|
|
109908
|
-
M2.message(lines.join(`
|
|
109909
|
-
`));
|
|
109910
109055
|
}
|
|
109911
|
-
function
|
|
109056
|
+
function getDiffSummary(diff) {
|
|
109912
109057
|
const parts = [];
|
|
109913
109058
|
if (diff.creates.length > 0) {
|
|
109914
109059
|
parts.push(`${bold(diff.creates.length)} to create`);
|
|
@@ -109921,11 +109066,125 @@ function getSyncSummary(diff) {
|
|
|
109921
109066
|
}
|
|
109922
109067
|
return parts.join(", ");
|
|
109923
109068
|
}
|
|
109924
|
-
// src/commands/
|
|
109069
|
+
// src/commands/resources/push/lib/metrics.ts
|
|
109070
|
+
function findMissingCourseMetrics(config7) {
|
|
109071
|
+
const issues = [];
|
|
109072
|
+
const gradelessCourseCount = config7.courses.filter((c) => c?.grade === undefined).length;
|
|
109073
|
+
const disambiguateGradeless = gradelessCourseCount > 1;
|
|
109074
|
+
for (let i = 0;i < config7.courses.length; i++) {
|
|
109075
|
+
const course = config7.courses[i];
|
|
109076
|
+
if (!course) {
|
|
109077
|
+
continue;
|
|
109078
|
+
}
|
|
109079
|
+
const disambiguate = disambiguateGradeless && course.grade === undefined;
|
|
109080
|
+
const title = getCourseTitle(config7.name, course, { disambiguate });
|
|
109081
|
+
const metrics = course.metadata?.metrics;
|
|
109082
|
+
if (metrics?.totalXp === undefined) {
|
|
109083
|
+
issues.push({
|
|
109084
|
+
courseIndex: i,
|
|
109085
|
+
courseTitle: title,
|
|
109086
|
+
metric: "totalXp",
|
|
109087
|
+
jsonPath: `courses[${i}].metadata.metrics.totalXp`
|
|
109088
|
+
});
|
|
109089
|
+
}
|
|
109090
|
+
if (metrics?.totalLessons === undefined) {
|
|
109091
|
+
issues.push({
|
|
109092
|
+
courseIndex: i,
|
|
109093
|
+
courseTitle: title,
|
|
109094
|
+
metric: "totalLessons",
|
|
109095
|
+
jsonPath: `courses[${i}].metadata.metrics.totalLessons`
|
|
109096
|
+
});
|
|
109097
|
+
}
|
|
109098
|
+
}
|
|
109099
|
+
return issues;
|
|
109100
|
+
}
|
|
109101
|
+
function formatMissingMetricsError(issues) {
|
|
109102
|
+
if (issues.length === 0) {
|
|
109103
|
+
return [];
|
|
109104
|
+
}
|
|
109105
|
+
const lines = ["Missing required course metrics:", ""];
|
|
109106
|
+
const byCourse = new Map;
|
|
109107
|
+
for (const issue4 of issues) {
|
|
109108
|
+
const existing = byCourse.get(issue4.courseIndex) ?? [];
|
|
109109
|
+
existing.push(issue4);
|
|
109110
|
+
byCourse.set(issue4.courseIndex, existing);
|
|
109111
|
+
}
|
|
109112
|
+
for (const [, courseIssues] of byCourse) {
|
|
109113
|
+
const first = courseIssues[0];
|
|
109114
|
+
if (!first) {
|
|
109115
|
+
continue;
|
|
109116
|
+
}
|
|
109117
|
+
lines.push(` ${bold(first.courseTitle)}:`);
|
|
109118
|
+
for (const issue4 of courseIssues) {
|
|
109119
|
+
lines.push(` - ${issue4.metric} (${dim(issue4.jsonPath)})`);
|
|
109120
|
+
}
|
|
109121
|
+
}
|
|
109122
|
+
lines.push("");
|
|
109123
|
+
lines.push("Add these fields to your timeback.config.json and try again.");
|
|
109124
|
+
return lines;
|
|
109125
|
+
}
|
|
109126
|
+
async function promptForMissingMetrics(issues, config7) {
|
|
109127
|
+
if (issues.length === 0) {
|
|
109128
|
+
return config7;
|
|
109129
|
+
}
|
|
109130
|
+
M2.warn(yellow("Some courses are missing required metrics."));
|
|
109131
|
+
const byCourse = new Map;
|
|
109132
|
+
for (const issue4 of issues) {
|
|
109133
|
+
const existing = byCourse.get(issue4.courseIndex) ?? [];
|
|
109134
|
+
existing.push(issue4);
|
|
109135
|
+
byCourse.set(issue4.courseIndex, existing);
|
|
109136
|
+
}
|
|
109137
|
+
for (const [courseIndex, courseIssues] of byCourse) {
|
|
109138
|
+
const course = config7.courses[courseIndex];
|
|
109139
|
+
const first = courseIssues[0];
|
|
109140
|
+
if (!course || !first) {
|
|
109141
|
+
continue;
|
|
109142
|
+
}
|
|
109143
|
+
M2.info(cyan(bold(first.courseTitle)));
|
|
109144
|
+
for (const issue4 of courseIssues) {
|
|
109145
|
+
const value = await promptForMetric(issue4.metric);
|
|
109146
|
+
if (value === null) {
|
|
109147
|
+
return null;
|
|
109148
|
+
}
|
|
109149
|
+
course.metadata = course.metadata ?? {};
|
|
109150
|
+
course.metadata.metrics = course.metadata.metrics ?? {};
|
|
109151
|
+
course.metadata.metrics[issue4.metric] = value;
|
|
109152
|
+
}
|
|
109153
|
+
}
|
|
109154
|
+
return config7;
|
|
109155
|
+
}
|
|
109156
|
+
async function promptForMetric(metric) {
|
|
109157
|
+
const labels = {
|
|
109158
|
+
totalXp: "Total course XP",
|
|
109159
|
+
totalLessons: "Total course lessons/units"
|
|
109160
|
+
};
|
|
109161
|
+
const result = await he({
|
|
109162
|
+
message: labels[metric],
|
|
109163
|
+
placeholder: "Enter a positive integer",
|
|
109164
|
+
validate: (value) => {
|
|
109165
|
+
const trimmed = value.trim();
|
|
109166
|
+
if (!trimmed) {
|
|
109167
|
+
return "Value is required";
|
|
109168
|
+
}
|
|
109169
|
+
const num = Number(trimmed);
|
|
109170
|
+
if (!Number.isInteger(num) || num <= 0) {
|
|
109171
|
+
return "Must be a positive integer";
|
|
109172
|
+
}
|
|
109173
|
+
}
|
|
109174
|
+
});
|
|
109175
|
+
if (isCancelled(result)) {
|
|
109176
|
+
return null;
|
|
109177
|
+
}
|
|
109178
|
+
return Number(result);
|
|
109179
|
+
}
|
|
109180
|
+
function isInteractive() {
|
|
109181
|
+
return process.stdout.isTTY === true && process.stdin.isTTY === true;
|
|
109182
|
+
}
|
|
109183
|
+
// src/commands/resources/push/lib/setup.ts
|
|
109925
109184
|
async function setupCreateContext(options = {}) {
|
|
109926
109185
|
if (options.env && !isTargetEnvironment(options.env)) {
|
|
109927
109186
|
M2.error(`Invalid environment: ${options.env}. Must be 'staging' or 'production'.`);
|
|
109928
|
-
outro.error("
|
|
109187
|
+
outro.error("Push failed");
|
|
109929
109188
|
return { success: false, exitCode: 1 };
|
|
109930
109189
|
}
|
|
109931
109190
|
const envResult = await resolveEnvironment({ env: options.env, skipIntro: true });
|
|
@@ -109935,24 +109194,24 @@ async function setupCreateContext(options = {}) {
|
|
|
109935
109194
|
}
|
|
109936
109195
|
const authOk = await initAuth({ env: envResult.env });
|
|
109937
109196
|
if (!authOk) {
|
|
109938
|
-
outro.error("
|
|
109197
|
+
outro.error("Push failed");
|
|
109939
109198
|
return { success: false, exitCode: 1 };
|
|
109940
109199
|
}
|
|
109941
109200
|
const ctx = getAuthContext(envResult.env);
|
|
109942
109201
|
if (!ctx.credentials.email) {
|
|
109943
109202
|
M2.error("Email not configured.");
|
|
109944
109203
|
M2.info("Run `timeback credentials email` to configure your email.");
|
|
109945
|
-
outro.error("
|
|
109204
|
+
outro.error("Push failed");
|
|
109946
109205
|
return { success: false, exitCode: 1 };
|
|
109947
109206
|
}
|
|
109948
|
-
const orgResult = await selectOrganization(ctx.timeback, ctx.credentials.email);
|
|
109207
|
+
const orgResult = await selectOrganization(ctx.timeback, ctx.credentials.email, options.courseCount);
|
|
109949
109208
|
if (!orgResult.success) {
|
|
109950
109209
|
if (orgResult.cancelled) {
|
|
109951
109210
|
outro.cancelled();
|
|
109952
109211
|
return { success: false, exitCode: 0 };
|
|
109953
109212
|
}
|
|
109954
109213
|
M2.error(orgResult.error);
|
|
109955
|
-
outro.error("
|
|
109214
|
+
outro.error("Push failed");
|
|
109956
109215
|
return { success: false, exitCode: 1 };
|
|
109957
109216
|
}
|
|
109958
109217
|
M2.info(`Creating on ${bold(envResult.env)} in ${bold(orgResult.orgName)}`);
|
|
@@ -109961,8 +109220,77 @@ async function setupCreateContext(options = {}) {
|
|
|
109961
109220
|
context: { env: envResult.env, orgId: orgResult.orgId }
|
|
109962
109221
|
};
|
|
109963
109222
|
}
|
|
109964
|
-
// src/commands/
|
|
109965
|
-
|
|
109223
|
+
// src/commands/resources/push/lib/warnings.ts
|
|
109224
|
+
function formatSkippedStructureWarning(titles) {
|
|
109225
|
+
const uniqueTitles = [...new Set(titles)].filter(Boolean);
|
|
109226
|
+
if (uniqueTitles.length === 0)
|
|
109227
|
+
return null;
|
|
109228
|
+
return {
|
|
109229
|
+
warning: `${uniqueTitles.length} ${pluralize(uniqueTitles.length, "course")} synced but not launchable (missing launchUrl)`,
|
|
109230
|
+
info: [
|
|
109231
|
+
...uniqueTitles.map((title) => ` • ${title}`),
|
|
109232
|
+
"Students won't be able to launch these courses until you add a launchUrl.",
|
|
109233
|
+
"Add launchUrl to your config and run sync again to fix this."
|
|
109234
|
+
]
|
|
109235
|
+
};
|
|
109236
|
+
}
|
|
109237
|
+
|
|
109238
|
+
// src/commands/resources/push/push.ts
|
|
109239
|
+
function logPushResults(result) {
|
|
109240
|
+
if (result.created > 0) {
|
|
109241
|
+
M2.success(`Created ${bold(result.created)} course${result.created === 1 ? "" : "s"}`);
|
|
109242
|
+
}
|
|
109243
|
+
if (result.updated > 0) {
|
|
109244
|
+
M2.success(`Updated ${bold(result.updated)} course${result.updated === 1 ? "" : "s"}`);
|
|
109245
|
+
}
|
|
109246
|
+
logErrors(result.errors);
|
|
109247
|
+
if (result.skippedStructure.length > 0) {
|
|
109248
|
+
const warning = formatSkippedStructureWarning(result.skippedStructure);
|
|
109249
|
+
if (warning) {
|
|
109250
|
+
M2.warn(warning.warning);
|
|
109251
|
+
for (const line of warning.info) {
|
|
109252
|
+
M2.info(line);
|
|
109253
|
+
}
|
|
109254
|
+
}
|
|
109255
|
+
}
|
|
109256
|
+
}
|
|
109257
|
+
async function validateMetricsGate(config7, configPath, relativeConfigPath) {
|
|
109258
|
+
const missingMetrics = findMissingCourseMetrics(config7);
|
|
109259
|
+
if (missingMetrics.length === 0) {
|
|
109260
|
+
return { success: true, config: config7 };
|
|
109261
|
+
}
|
|
109262
|
+
if (isInteractive()) {
|
|
109263
|
+
const updatedConfig = await promptForMissingMetrics(missingMetrics, config7);
|
|
109264
|
+
if (updatedConfig === null) {
|
|
109265
|
+
outro.cancelled();
|
|
109266
|
+
return { success: false, exitCode: 0 };
|
|
109267
|
+
}
|
|
109268
|
+
await saveConfig(configPath, updatedConfig);
|
|
109269
|
+
M2.success(`Updated ${dim(relativeConfigPath)} with metrics`);
|
|
109270
|
+
return { success: true, config: updatedConfig };
|
|
109271
|
+
}
|
|
109272
|
+
const errorLines = formatMissingMetricsError(missingMetrics);
|
|
109273
|
+
for (const line of errorLines) {
|
|
109274
|
+
M2.error(line);
|
|
109275
|
+
}
|
|
109276
|
+
outro.error("Push failed");
|
|
109277
|
+
return { success: false, exitCode: 1 };
|
|
109278
|
+
}
|
|
109279
|
+
async function computeDiff(config7, initMode) {
|
|
109280
|
+
const hasLinkedCourses = config7.courses.some((course) => getSyncedCourses(course).length > 0);
|
|
109281
|
+
if (initMode && !hasLinkedCourses) {
|
|
109282
|
+
return compareCourses({ config: config7 });
|
|
109283
|
+
}
|
|
109284
|
+
const s = Y2();
|
|
109285
|
+
s.start(hasLinkedCourses ? "Comparing with Timeback..." : "Computing changes...");
|
|
109286
|
+
const diff = await compareCourses({
|
|
109287
|
+
config: config7,
|
|
109288
|
+
onProgress: (msg) => s.message(msg)
|
|
109289
|
+
});
|
|
109290
|
+
s.stop(hasLinkedCourses ? "Remote check complete" : "Changes computed");
|
|
109291
|
+
return diff;
|
|
109292
|
+
}
|
|
109293
|
+
async function pushCommand(options) {
|
|
109966
109294
|
const {
|
|
109967
109295
|
configPath: customConfigPath,
|
|
109968
109296
|
exitOnComplete = true,
|
|
@@ -109971,10 +109299,11 @@ async function syncCommand(options) {
|
|
|
109971
109299
|
env: env2,
|
|
109972
109300
|
createEnv: preResolvedEnv,
|
|
109973
109301
|
preloadedConfig,
|
|
109974
|
-
yes = false
|
|
109302
|
+
yes = false,
|
|
109303
|
+
initMode = false
|
|
109975
109304
|
} = options;
|
|
109976
109305
|
if (!skipIntro) {
|
|
109977
|
-
intro("Timeback
|
|
109306
|
+
intro("Timeback Push");
|
|
109978
109307
|
}
|
|
109979
109308
|
let config7, configPath;
|
|
109980
109309
|
if (preloadedConfig) {
|
|
@@ -109984,7 +109313,7 @@ async function syncCommand(options) {
|
|
|
109984
109313
|
const configResult = await loadConfig({ configPath: customConfigPath });
|
|
109985
109314
|
if (!configResult.success) {
|
|
109986
109315
|
M2.error(configResult.error);
|
|
109987
|
-
outro.error("
|
|
109316
|
+
outro.error("Push failed");
|
|
109988
109317
|
if (exitOnComplete)
|
|
109989
109318
|
process.exit(1);
|
|
109990
109319
|
return false;
|
|
@@ -109993,14 +109322,17 @@ async function syncCommand(options) {
|
|
|
109993
109322
|
configPath = configResult.configPath;
|
|
109994
109323
|
}
|
|
109995
109324
|
const relativeConfigPath = getRelativeConfigPath(configPath);
|
|
109996
|
-
|
|
109997
|
-
|
|
109998
|
-
|
|
109999
|
-
const
|
|
110000
|
-
|
|
110001
|
-
|
|
110002
|
-
|
|
110003
|
-
|
|
109325
|
+
if (!initMode) {
|
|
109326
|
+
M2.info(`Loaded ${dim(relativeConfigPath)}`);
|
|
109327
|
+
}
|
|
109328
|
+
const metricsResult = await validateMetricsGate(config7, configPath, relativeConfigPath);
|
|
109329
|
+
if (!metricsResult.success) {
|
|
109330
|
+
if (exitOnComplete)
|
|
109331
|
+
process.exit(metricsResult.exitCode);
|
|
109332
|
+
return metricsResult.exitCode === 0;
|
|
109333
|
+
}
|
|
109334
|
+
config7 = metricsResult.config;
|
|
109335
|
+
const diff = await computeDiff(config7, initMode);
|
|
110004
109336
|
if (diff.errors.length > 0) {
|
|
110005
109337
|
logErrors(diff.errors);
|
|
110006
109338
|
}
|
|
@@ -110012,25 +109344,24 @@ async function syncCommand(options) {
|
|
|
110012
109344
|
process.exit(0);
|
|
110013
109345
|
return true;
|
|
110014
109346
|
}
|
|
110015
|
-
|
|
110016
|
-
|
|
109347
|
+
if (!initMode) {
|
|
109348
|
+
M2.info(`Found changes: ${getDiffSummary(diff)}`);
|
|
109349
|
+
}
|
|
110017
109350
|
if (dryRun) {
|
|
109351
|
+
displayDiff(diff);
|
|
110018
109352
|
outro.warn("Dry run (no changes made)");
|
|
110019
|
-
if (exitOnComplete)
|
|
110020
|
-
process.exit(0);
|
|
110021
|
-
return true;
|
|
110022
|
-
}
|
|
110023
|
-
const shouldSync = yes || await ye({ message: "Apply these changes?" });
|
|
110024
|
-
if (isCancelled(shouldSync) || !shouldSync) {
|
|
110025
|
-
outro.cancelled();
|
|
110026
|
-
if (exitOnComplete)
|
|
109353
|
+
if (exitOnComplete) {
|
|
110027
109354
|
process.exit(0);
|
|
109355
|
+
}
|
|
110028
109356
|
return true;
|
|
110029
109357
|
}
|
|
110030
109358
|
let createEnv;
|
|
110031
109359
|
let createOrgId;
|
|
110032
109360
|
if (diff.creates.length > 0) {
|
|
110033
|
-
const setupResult = await setupCreateContext({
|
|
109361
|
+
const setupResult = await setupCreateContext({
|
|
109362
|
+
env: env2 ?? preResolvedEnv,
|
|
109363
|
+
courseCount: diff.creates.length
|
|
109364
|
+
});
|
|
110034
109365
|
if (!setupResult.success) {
|
|
110035
109366
|
if (exitOnComplete)
|
|
110036
109367
|
process.exit(setupResult.exitCode);
|
|
@@ -110039,26 +109370,31 @@ async function syncCommand(options) {
|
|
|
110039
109370
|
createEnv = setupResult.context.env;
|
|
110040
109371
|
createOrgId = setupResult.context.orgId;
|
|
110041
109372
|
}
|
|
110042
|
-
|
|
109373
|
+
displayDiff(diff, { createEnv, initMode });
|
|
109374
|
+
const count = diff.creates.length + diff.updates.length;
|
|
109375
|
+
const msg = initMode ? `Create ${count === 1 ? "this" : "these"} course${count === 1 ? "" : "s"}?` : "Apply these changes?";
|
|
109376
|
+
const shouldSync = yes || await ye({ message: msg });
|
|
109377
|
+
if (isCancelled(shouldSync) || !shouldSync) {
|
|
109378
|
+
outro.cancelled();
|
|
109379
|
+
if (exitOnComplete)
|
|
109380
|
+
process.exit(0);
|
|
109381
|
+
return true;
|
|
109382
|
+
}
|
|
109383
|
+
const applySpinner = Y2();
|
|
109384
|
+
applySpinner.start(initMode ? "Creating courses..." : "Applying changes...");
|
|
110043
109385
|
const { result, updatedConfig } = await applyDiff({
|
|
110044
109386
|
config: config7,
|
|
110045
109387
|
diff,
|
|
110046
109388
|
createEnv,
|
|
110047
109389
|
createOrgId,
|
|
110048
|
-
onProgress: (
|
|
109390
|
+
onProgress: (msg2) => applySpinner.message(msg2)
|
|
110049
109391
|
});
|
|
110050
|
-
|
|
109392
|
+
applySpinner.stop(initMode ? "Courses created" : "Changes applied");
|
|
110051
109393
|
if (result.created > 0 || result.updated > 0) {
|
|
110052
109394
|
await saveConfig(configPath, updatedConfig);
|
|
110053
|
-
M2.success(`
|
|
109395
|
+
M2.success(`Saved ${dim(relativeConfigPath)}`);
|
|
110054
109396
|
}
|
|
110055
|
-
|
|
110056
|
-
M2.success(`Created ${bold(result.created)} course${result.created === 1 ? "" : "s"}`);
|
|
110057
|
-
}
|
|
110058
|
-
if (result.updated > 0) {
|
|
110059
|
-
M2.success(`Updated ${bold(result.updated)} course${result.updated === 1 ? "" : "s"}`);
|
|
110060
|
-
}
|
|
110061
|
-
logErrors(result.errors);
|
|
109397
|
+
logPushResults(result);
|
|
110062
109398
|
if (result.errors.length > 0) {
|
|
110063
109399
|
outro.warn("Completed with errors");
|
|
110064
109400
|
if (exitOnComplete)
|
|
@@ -110066,8 +109402,9 @@ async function syncCommand(options) {
|
|
|
110066
109402
|
return false;
|
|
110067
109403
|
}
|
|
110068
109404
|
outro.success();
|
|
110069
|
-
if (exitOnComplete)
|
|
109405
|
+
if (exitOnComplete) {
|
|
110070
109406
|
process.exit(0);
|
|
109407
|
+
}
|
|
110071
109408
|
return true;
|
|
110072
109409
|
}
|
|
110073
109410
|
|
|
@@ -110126,22 +109463,6 @@ async function ensureConfiguredEnvironments(targetEnv) {
|
|
|
110126
109463
|
return { status: "ok", envs: configuredEnvs };
|
|
110127
109464
|
}
|
|
110128
109465
|
// src/commands/init/lib/postInit.ts
|
|
110129
|
-
async function decideSyncAfterInit(opts) {
|
|
110130
|
-
if (opts.noSync) {
|
|
110131
|
-
return { shouldSync: false, cancelled: false };
|
|
110132
|
-
}
|
|
110133
|
-
if (opts.yes) {
|
|
110134
|
-
return { shouldSync: true, cancelled: false };
|
|
110135
|
-
}
|
|
110136
|
-
const syncConfirm = await ye({
|
|
110137
|
-
message: "Sync with Timeback now?",
|
|
110138
|
-
initialValue: true
|
|
110139
|
-
});
|
|
110140
|
-
if (isCancelled(syncConfirm)) {
|
|
110141
|
-
return { shouldSync: false, cancelled: true };
|
|
110142
|
-
}
|
|
110143
|
-
return { shouldSync: syncConfirm, cancelled: false };
|
|
110144
|
-
}
|
|
110145
109466
|
async function ensureSyncEmail(targetEnv) {
|
|
110146
109467
|
const creds = await getSavedCredentials(targetEnv);
|
|
110147
109468
|
if (creds && !creds.email) {
|
|
@@ -110152,7 +109473,7 @@ async function ensureSyncEmail(targetEnv) {
|
|
|
110152
109473
|
function finishWithoutSync(options) {
|
|
110153
109474
|
const { withHint, cancelled, exitOnComplete } = options;
|
|
110154
109475
|
if (withHint) {
|
|
110155
|
-
M2.info(`Run ${greenBright("timeback
|
|
109476
|
+
M2.info(`Run ${greenBright("timeback resources push")} once ready`);
|
|
110156
109477
|
}
|
|
110157
109478
|
if (cancelled) {
|
|
110158
109479
|
outro.cancelled();
|
|
@@ -110205,6 +109526,35 @@ async function resolveInitSyncCreateEnv(options) {
|
|
|
110205
109526
|
}
|
|
110206
109527
|
return { success: true, createEnv };
|
|
110207
109528
|
}
|
|
109529
|
+
// src/commands/init/lib/constants.ts
|
|
109530
|
+
var SUBJECTS = [
|
|
109531
|
+
"Math",
|
|
109532
|
+
"FastMath",
|
|
109533
|
+
"Reading",
|
|
109534
|
+
"Language",
|
|
109535
|
+
"Vocabulary",
|
|
109536
|
+
"Writing",
|
|
109537
|
+
"Science",
|
|
109538
|
+
"Social Studies"
|
|
109539
|
+
];
|
|
109540
|
+
var GRADES = [
|
|
109541
|
+
{ value: -1, label: "Pre-K" },
|
|
109542
|
+
{ value: 0, label: "Kindergarten" },
|
|
109543
|
+
{ value: 1, label: "Grade 1" },
|
|
109544
|
+
{ value: 2, label: "Grade 2" },
|
|
109545
|
+
{ value: 3, label: "Grade 3" },
|
|
109546
|
+
{ value: 4, label: "Grade 4" },
|
|
109547
|
+
{ value: 5, label: "Grade 5" },
|
|
109548
|
+
{ value: 6, label: "Grade 6" },
|
|
109549
|
+
{ value: 7, label: "Grade 7" },
|
|
109550
|
+
{ value: 8, label: "Grade 8" },
|
|
109551
|
+
{ value: 9, label: "Grade 9" },
|
|
109552
|
+
{ value: 10, label: "Grade 10" },
|
|
109553
|
+
{ value: 11, label: "Grade 11" },
|
|
109554
|
+
{ value: 12, label: "Grade 12" },
|
|
109555
|
+
{ value: 13, label: "AP" }
|
|
109556
|
+
];
|
|
109557
|
+
|
|
110208
109558
|
// src/commands/init/lib/prompts.ts
|
|
110209
109559
|
var GRADE_NONE = "none";
|
|
110210
109560
|
var GRADE_OPTIONS = [
|
|
@@ -110289,10 +109639,9 @@ async function promptCourses(appName) {
|
|
|
110289
109639
|
}
|
|
110290
109640
|
}
|
|
110291
109641
|
}
|
|
110292
|
-
M2.info(`Creating ${courses.length} course${courses.length === 1 ? "" : "s"}`);
|
|
110293
109642
|
return courses;
|
|
110294
109643
|
}
|
|
110295
|
-
// src/commands/init/
|
|
109644
|
+
// src/commands/init/init.ts
|
|
110296
109645
|
async function promptInitMode() {
|
|
110297
109646
|
const mode = await ve({
|
|
110298
109647
|
message: "How would you like to set up your config?",
|
|
@@ -110300,12 +109649,12 @@ async function promptInitMode() {
|
|
|
110300
109649
|
{
|
|
110301
109650
|
value: "create",
|
|
110302
109651
|
label: "Initialize a new app",
|
|
110303
|
-
hint: "
|
|
109652
|
+
hint: "First time setup"
|
|
110304
109653
|
},
|
|
110305
109654
|
{
|
|
110306
109655
|
value: "import",
|
|
110307
109656
|
label: "Import an existing app",
|
|
110308
|
-
hint: "
|
|
109657
|
+
hint: "Import existing resources"
|
|
110309
109658
|
}
|
|
110310
109659
|
]
|
|
110311
109660
|
});
|
|
@@ -110329,21 +109678,19 @@ async function createFromScratch() {
|
|
|
110329
109678
|
if (launchUrl === undefined) {
|
|
110330
109679
|
return null;
|
|
110331
109680
|
}
|
|
110332
|
-
const suggestedSensor = deriveSensorFromLaunchUrl(launchUrl);
|
|
110333
|
-
const sensor = await promptSensor({ defaultValue: suggestedSensor });
|
|
110334
|
-
if (sensor === undefined) {
|
|
110335
|
-
return null;
|
|
110336
|
-
}
|
|
110337
109681
|
return {
|
|
110338
109682
|
name,
|
|
110339
109683
|
launchUrl,
|
|
110340
|
-
sensor,
|
|
110341
109684
|
courses: courses.map((c) => ({
|
|
110342
109685
|
...c,
|
|
110343
109686
|
ids: null
|
|
110344
109687
|
}))
|
|
110345
109688
|
};
|
|
110346
109689
|
}
|
|
109690
|
+
function extractCommonLaunchUrl(courses) {
|
|
109691
|
+
const urls = new Set(courses.map((c) => c.launchUrl).filter(Boolean));
|
|
109692
|
+
return urls.size === 1 ? [...urls][0] : undefined;
|
|
109693
|
+
}
|
|
110347
109694
|
async function createFromImport() {
|
|
110348
109695
|
const setupResult = await ensureConfiguredEnvironments();
|
|
110349
109696
|
if (setupResult.status === "cancelled") {
|
|
@@ -110360,18 +109707,16 @@ async function createFromImport() {
|
|
|
110360
109707
|
}
|
|
110361
109708
|
return null;
|
|
110362
109709
|
}
|
|
110363
|
-
|
|
110364
|
-
if (launchUrl
|
|
110365
|
-
|
|
110366
|
-
|
|
110367
|
-
|
|
110368
|
-
|
|
110369
|
-
return null;
|
|
109710
|
+
let launchUrl = extractCommonLaunchUrl(result.courses);
|
|
109711
|
+
if (!launchUrl) {
|
|
109712
|
+
launchUrl = await promptLaunchUrl();
|
|
109713
|
+
if (launchUrl === undefined) {
|
|
109714
|
+
return null;
|
|
109715
|
+
}
|
|
110370
109716
|
}
|
|
110371
109717
|
return {
|
|
110372
109718
|
name: result.appName,
|
|
110373
109719
|
launchUrl,
|
|
110374
|
-
sensor,
|
|
110375
109720
|
courses: result.courses
|
|
110376
109721
|
};
|
|
110377
109722
|
}
|
|
@@ -110380,15 +109725,16 @@ async function createConfig(options = {}) {
|
|
|
110380
109725
|
configPath: customPath,
|
|
110381
109726
|
exitOnComplete = true,
|
|
110382
109727
|
format = true,
|
|
110383
|
-
|
|
109728
|
+
syncEnv,
|
|
110384
109729
|
yes = false,
|
|
110385
|
-
|
|
109730
|
+
sync = false
|
|
110386
109731
|
} = options;
|
|
110387
109732
|
const cwd = process.cwd();
|
|
110388
|
-
const configPath = customPath ?
|
|
109733
|
+
const configPath = customPath ? resolve4(cwd, customPath) : resolve4(cwd, DEFAULT_CONFIG_FILENAME);
|
|
110389
109734
|
const configFilename = basename2(configPath);
|
|
110390
|
-
intro("Timeback");
|
|
110391
|
-
|
|
109735
|
+
intro("Timeback Init");
|
|
109736
|
+
const configExisted = existsSync4(configPath);
|
|
109737
|
+
if (configExisted) {
|
|
110392
109738
|
M2.warn(`${blueBright(configFilename)} already exists`);
|
|
110393
109739
|
const overwrite = yes ? true : await ye({
|
|
110394
109740
|
message: "Overwrite existing config?",
|
|
@@ -110420,32 +109766,18 @@ async function createConfig(options = {}) {
|
|
|
110420
109766
|
process.exit(0);
|
|
110421
109767
|
return;
|
|
110422
109768
|
}
|
|
109769
|
+
const normalizedConfig = normalizeLaunchUrls(config7);
|
|
110423
109770
|
const s = Y2();
|
|
110424
109771
|
s.start(`Creating ${configFilename}...`);
|
|
110425
|
-
|
|
110426
|
-
await writeFile4(configPath, content, "utf-8");
|
|
110427
|
-
if (format) {
|
|
110428
|
-
await formatWithPrettier({ cwd, filePath: configPath, logger: M2, silent: true });
|
|
110429
|
-
}
|
|
109772
|
+
await saveConfig(configPath, normalizedConfig, { format });
|
|
110430
109773
|
s.stop(`Created ${dim(configFilename)}`);
|
|
110431
|
-
if (
|
|
110432
|
-
outro.success();
|
|
110433
|
-
if (exitOnComplete)
|
|
110434
|
-
process.exit(0);
|
|
110435
|
-
return;
|
|
110436
|
-
}
|
|
110437
|
-
const syncDecision = await decideSyncAfterInit({ noSync, yes });
|
|
110438
|
-
if (syncDecision.cancelled) {
|
|
110439
|
-
finishWithoutSync({ withHint: false, cancelled: true, exitOnComplete });
|
|
110440
|
-
return;
|
|
110441
|
-
}
|
|
110442
|
-
if (!syncDecision.shouldSync) {
|
|
109774
|
+
if (!sync) {
|
|
110443
109775
|
finishWithoutSync({ withHint: true, cancelled: false, exitOnComplete });
|
|
110444
109776
|
return;
|
|
110445
109777
|
}
|
|
110446
109778
|
const createEnvResult = await resolveInitSyncCreateEnv({
|
|
110447
|
-
config:
|
|
110448
|
-
env:
|
|
109779
|
+
config: normalizedConfig,
|
|
109780
|
+
env: syncEnv,
|
|
110449
109781
|
exitOnComplete
|
|
110450
109782
|
});
|
|
110451
109783
|
if (!createEnvResult.success) {
|
|
@@ -110471,35 +109803,32 @@ async function createConfig(options = {}) {
|
|
|
110471
109803
|
return;
|
|
110472
109804
|
}
|
|
110473
109805
|
}
|
|
110474
|
-
const
|
|
109806
|
+
const pushSuccess = await pushCommand({
|
|
110475
109807
|
exitOnComplete: false,
|
|
110476
109808
|
skipIntro: true,
|
|
110477
|
-
preloadedConfig: { config:
|
|
109809
|
+
preloadedConfig: { config: normalizedConfig, configPath },
|
|
110478
109810
|
createEnv: createEnvResult.createEnv,
|
|
110479
|
-
yes
|
|
109811
|
+
yes,
|
|
109812
|
+
initMode: true
|
|
110480
109813
|
});
|
|
110481
|
-
if (format) {
|
|
110482
|
-
await formatWithPrettier({ cwd, filePath: configPath,
|
|
109814
|
+
if (format && existsSync4(configPath)) {
|
|
109815
|
+
await formatWithPrettier({ cwd, filePath: configPath, silent: true });
|
|
110483
109816
|
}
|
|
110484
109817
|
if (exitOnComplete)
|
|
110485
|
-
process.exit(
|
|
109818
|
+
process.exit(pushSuccess ? 0 : 1);
|
|
110486
109819
|
}
|
|
110487
109820
|
|
|
110488
109821
|
// src/commands/init/index.ts
|
|
110489
109822
|
function registerInitCommand(program2) {
|
|
110490
|
-
program2.command("init").description("Create a new timeback.config.json file").option("-c, --config <path>", "Path to the config file (default: timeback.config.json)").option("--
|
|
109823
|
+
program2.command("init").description("Create a new timeback.config.json file").option("-c, --config <path>", "Path to the config file (default: timeback.config.json)").option("--syncEnv <environment>", "Target environment for sync (staging, production)").option("-y, --yes", "Skip confirmations and use defaults where possible").option("--no-format", "Do not run prettier on the generated config file").option("--sync", "Sync with Timeback after creating the config").action((opts) => createConfig({
|
|
110491
109824
|
configPath: opts.config,
|
|
110492
|
-
|
|
109825
|
+
syncEnv: opts.syncEnv,
|
|
110493
109826
|
yes: opts.yes,
|
|
110494
109827
|
format: opts.format,
|
|
110495
|
-
|
|
109828
|
+
sync: opts.sync === true
|
|
110496
109829
|
}));
|
|
110497
109830
|
}
|
|
110498
109831
|
|
|
110499
|
-
// ../internal/utils/src/shared/format.ts
|
|
110500
|
-
function pluralize(count, singular, plural) {
|
|
110501
|
-
return count === 1 ? singular : plural ?? `${singular}s`;
|
|
110502
|
-
}
|
|
110503
109832
|
// src/commands/inspect/lib/format.ts
|
|
110504
109833
|
function formatStat(label, value, labelWidth = 24) {
|
|
110505
109834
|
const formattedValue = typeof value === "number" ? value.toLocaleString() : value;
|
|
@@ -110894,6 +110223,594 @@ function registerInspectCommand(program2) {
|
|
|
110894
110223
|
});
|
|
110895
110224
|
}
|
|
110896
110225
|
|
|
110226
|
+
// src/commands/resources/pull/lib/apply.ts
|
|
110227
|
+
function applyComparison(config7, comparison) {
|
|
110228
|
+
const course = config7.courses[comparison.index];
|
|
110229
|
+
if (!course)
|
|
110230
|
+
return;
|
|
110231
|
+
for (const change of comparison.changes) {
|
|
110232
|
+
switch (change.field) {
|
|
110233
|
+
case "subject":
|
|
110234
|
+
course.subject = change.remote;
|
|
110235
|
+
break;
|
|
110236
|
+
case "grade":
|
|
110237
|
+
course.grade = change.remote;
|
|
110238
|
+
break;
|
|
110239
|
+
case "courseCode":
|
|
110240
|
+
course.courseCode = change.remote;
|
|
110241
|
+
break;
|
|
110242
|
+
}
|
|
110243
|
+
}
|
|
110244
|
+
}
|
|
110245
|
+
function applyChanges(config7, comparisons) {
|
|
110246
|
+
const updated = {
|
|
110247
|
+
...config7,
|
|
110248
|
+
courses: config7.courses.map((c) => ({ ...c }))
|
|
110249
|
+
};
|
|
110250
|
+
for (const comparison of comparisons) {
|
|
110251
|
+
applyComparison(updated, comparison);
|
|
110252
|
+
}
|
|
110253
|
+
return updated;
|
|
110254
|
+
}
|
|
110255
|
+
// src/commands/resources/pull/lib/compare.ts
|
|
110256
|
+
function compareCourse(local, remote) {
|
|
110257
|
+
const changes = [];
|
|
110258
|
+
const remoteSubject = remote.subjects?.[0];
|
|
110259
|
+
if (local.subject !== remoteSubject) {
|
|
110260
|
+
changes.push({ field: "subject", local: local.subject, remote: remoteSubject });
|
|
110261
|
+
}
|
|
110262
|
+
const remoteGrade = remote.grades?.[0];
|
|
110263
|
+
if (local.grade !== remoteGrade) {
|
|
110264
|
+
changes.push({ field: "grade", local: local.grade, remote: remoteGrade });
|
|
110265
|
+
}
|
|
110266
|
+
if (local.courseCode !== remote.courseCode) {
|
|
110267
|
+
changes.push({ field: "courseCode", local: local.courseCode, remote: remote.courseCode });
|
|
110268
|
+
}
|
|
110269
|
+
return changes;
|
|
110270
|
+
}
|
|
110271
|
+
function findLinkedCourses(courses) {
|
|
110272
|
+
const linked = [];
|
|
110273
|
+
for (let i = 0;i < courses.length; i++) {
|
|
110274
|
+
const course = courses[i];
|
|
110275
|
+
const ids = course.ids;
|
|
110276
|
+
if (ids?.staging) {
|
|
110277
|
+
linked.push({ index: i, course, env: "staging", courseId: ids.staging });
|
|
110278
|
+
}
|
|
110279
|
+
if (ids?.production) {
|
|
110280
|
+
linked.push({ index: i, course, env: "production", courseId: ids.production });
|
|
110281
|
+
}
|
|
110282
|
+
}
|
|
110283
|
+
return linked;
|
|
110284
|
+
}
|
|
110285
|
+
function filterChanges(comparisons) {
|
|
110286
|
+
return comparisons.filter((c) => c.changes.length > 0);
|
|
110287
|
+
}
|
|
110288
|
+
async function fetchAndCompare(client, linkedCourses, callbacks) {
|
|
110289
|
+
const comparisons = [];
|
|
110290
|
+
for (const linked of linkedCourses) {
|
|
110291
|
+
callbacks.onProgress(linked.course);
|
|
110292
|
+
try {
|
|
110293
|
+
const remote = await client.oneroster.courses.get(linked.courseId);
|
|
110294
|
+
const changes = compareCourse(linked.course, remote);
|
|
110295
|
+
comparisons.push({
|
|
110296
|
+
index: linked.index,
|
|
110297
|
+
course: linked.course,
|
|
110298
|
+
env: linked.env,
|
|
110299
|
+
courseId: linked.courseId,
|
|
110300
|
+
changes
|
|
110301
|
+
});
|
|
110302
|
+
} catch (error57) {
|
|
110303
|
+
const msg = error57 instanceof Error ? error57.message : String(error57);
|
|
110304
|
+
callbacks.onError(linked.course, msg);
|
|
110305
|
+
}
|
|
110306
|
+
}
|
|
110307
|
+
return comparisons;
|
|
110308
|
+
}
|
|
110309
|
+
// src/commands/resources/pull/lib/display.ts
|
|
110310
|
+
function formatCourse2(course) {
|
|
110311
|
+
if (course.grade !== undefined) {
|
|
110312
|
+
return `${course.subject} Grade ${course.grade}`;
|
|
110313
|
+
}
|
|
110314
|
+
if (course.courseCode) {
|
|
110315
|
+
return `${course.subject} (${course.courseCode})`;
|
|
110316
|
+
}
|
|
110317
|
+
return course.subject;
|
|
110318
|
+
}
|
|
110319
|
+
function formatValue6(value) {
|
|
110320
|
+
if (value === undefined)
|
|
110321
|
+
return dim("(not set)");
|
|
110322
|
+
if (value === null)
|
|
110323
|
+
return dim("(null)");
|
|
110324
|
+
return String(value);
|
|
110325
|
+
}
|
|
110326
|
+
function displayChange(change) {
|
|
110327
|
+
M2.info(` ${bold(change.field)}:`);
|
|
110328
|
+
M2.info(` ${red("-")} ${formatValue6(change.local)}`);
|
|
110329
|
+
M2.info(` ${green("+")} ${formatValue6(change.remote)}`);
|
|
110330
|
+
}
|
|
110331
|
+
function displayChanges(comparisons) {
|
|
110332
|
+
M2.info(`
|
|
110333
|
+
${yellow("Changes to pull:")}
|
|
110334
|
+
`);
|
|
110335
|
+
for (const comparison of comparisons) {
|
|
110336
|
+
const title = formatCourse2(comparison.course);
|
|
110337
|
+
M2.info(` ${bold(title)} ${dim(`(${comparison.env})`)}`);
|
|
110338
|
+
for (const change of comparison.changes) {
|
|
110339
|
+
displayChange(change);
|
|
110340
|
+
}
|
|
110341
|
+
M2.info("");
|
|
110342
|
+
}
|
|
110343
|
+
}
|
|
110344
|
+
// src/commands/resources/pull/pull.ts
|
|
110345
|
+
function getExistingCourseIds(config7) {
|
|
110346
|
+
const byEnv = {};
|
|
110347
|
+
for (const course of config7.courses) {
|
|
110348
|
+
for (const [env2, id] of Object.entries(course.ids ?? {})) {
|
|
110349
|
+
if (!byEnv[env2])
|
|
110350
|
+
byEnv[env2] = [];
|
|
110351
|
+
if (id)
|
|
110352
|
+
byEnv[env2].push(id);
|
|
110353
|
+
}
|
|
110354
|
+
}
|
|
110355
|
+
return byEnv;
|
|
110356
|
+
}
|
|
110357
|
+
async function importMoreCourses(options) {
|
|
110358
|
+
const result = await runImportFlow({
|
|
110359
|
+
configPath: options.configPath,
|
|
110360
|
+
appName: options.config.name,
|
|
110361
|
+
existingCourseIds: getExistingCourseIds(options.config)
|
|
110362
|
+
});
|
|
110363
|
+
if (!result.success) {
|
|
110364
|
+
return {
|
|
110365
|
+
success: false,
|
|
110366
|
+
cancelled: result.cancelled,
|
|
110367
|
+
error: result.error ?? "Import failed"
|
|
110368
|
+
};
|
|
110369
|
+
}
|
|
110370
|
+
return { success: true };
|
|
110371
|
+
}
|
|
110372
|
+
async function handleNoLinkedCourses(options) {
|
|
110373
|
+
const { env: env2, importAfterPull, config: config7, customConfigPath, exitOnComplete } = options;
|
|
110374
|
+
if (!importAfterPull) {
|
|
110375
|
+
const message = [
|
|
110376
|
+
`No courses linked to ${env2}.`,
|
|
110377
|
+
`Run ${bold(greenBright("timeback resources push"))} first.`
|
|
110378
|
+
].join(`
|
|
110379
|
+
`);
|
|
110380
|
+
Me(message, "No Linked Courses");
|
|
110381
|
+
outro.cancelled();
|
|
110382
|
+
if (exitOnComplete) {
|
|
110383
|
+
process.exit(0);
|
|
110384
|
+
}
|
|
110385
|
+
return true;
|
|
110386
|
+
}
|
|
110387
|
+
const importResult = await importMoreCourses({ config: config7, configPath: customConfigPath });
|
|
110388
|
+
if (!importResult.success) {
|
|
110389
|
+
if (importResult.cancelled) {
|
|
110390
|
+
outro.cancelled();
|
|
110391
|
+
if (exitOnComplete) {
|
|
110392
|
+
process.exit(0);
|
|
110393
|
+
}
|
|
110394
|
+
return true;
|
|
110395
|
+
}
|
|
110396
|
+
M2.error(importResult.error);
|
|
110397
|
+
outro.error("Import failed");
|
|
110398
|
+
if (exitOnComplete) {
|
|
110399
|
+
process.exit(1);
|
|
110400
|
+
}
|
|
110401
|
+
return true;
|
|
110402
|
+
}
|
|
110403
|
+
outro.success();
|
|
110404
|
+
if (exitOnComplete) {
|
|
110405
|
+
process.exit(0);
|
|
110406
|
+
}
|
|
110407
|
+
return true;
|
|
110408
|
+
}
|
|
110409
|
+
async function pullCommand(options) {
|
|
110410
|
+
const {
|
|
110411
|
+
configPath: customConfigPath,
|
|
110412
|
+
env: envOption,
|
|
110413
|
+
apply = false,
|
|
110414
|
+
import: importAfterPull = false,
|
|
110415
|
+
exitOnComplete = true,
|
|
110416
|
+
skipIntro = false
|
|
110417
|
+
} = options;
|
|
110418
|
+
if (!skipIntro) {
|
|
110419
|
+
intro("Timeback Pull");
|
|
110420
|
+
}
|
|
110421
|
+
const configResult = await loadConfig({ configPath: customConfigPath });
|
|
110422
|
+
if (!configResult.success) {
|
|
110423
|
+
M2.error(configResult.error);
|
|
110424
|
+
outro.error("Pull failed");
|
|
110425
|
+
if (exitOnComplete) {
|
|
110426
|
+
process.exit(1);
|
|
110427
|
+
}
|
|
110428
|
+
return;
|
|
110429
|
+
}
|
|
110430
|
+
const { config: config7, configPath } = configResult;
|
|
110431
|
+
const relativeConfigPath = getRelativeConfigPath(configPath);
|
|
110432
|
+
M2.info(`Loaded ${dim(relativeConfigPath)}`);
|
|
110433
|
+
const envResult = await resolveEnvironment({ env: envOption, skipIntro: true });
|
|
110434
|
+
if (!envResult) {
|
|
110435
|
+
outro.cancelled();
|
|
110436
|
+
if (exitOnComplete) {
|
|
110437
|
+
process.exit(0);
|
|
110438
|
+
}
|
|
110439
|
+
return;
|
|
110440
|
+
}
|
|
110441
|
+
const env2 = envResult.env;
|
|
110442
|
+
const linkedCourses = findLinkedCourses(config7.courses).filter((c) => c.env === env2);
|
|
110443
|
+
if (linkedCourses.length === 0) {
|
|
110444
|
+
await handleNoLinkedCourses({
|
|
110445
|
+
env: env2,
|
|
110446
|
+
importAfterPull,
|
|
110447
|
+
config: config7,
|
|
110448
|
+
customConfigPath,
|
|
110449
|
+
exitOnComplete
|
|
110450
|
+
});
|
|
110451
|
+
return;
|
|
110452
|
+
}
|
|
110453
|
+
const authOk = await initAuth({ env: env2 });
|
|
110454
|
+
if (!authOk) {
|
|
110455
|
+
M2.error(`Failed to authenticate for ${env2}`);
|
|
110456
|
+
outro.error("Pull failed");
|
|
110457
|
+
if (exitOnComplete) {
|
|
110458
|
+
process.exit(1);
|
|
110459
|
+
}
|
|
110460
|
+
return;
|
|
110461
|
+
}
|
|
110462
|
+
const client = getAuthContext(env2).timeback;
|
|
110463
|
+
const s = Y2();
|
|
110464
|
+
s.start("Fetching remote course data...");
|
|
110465
|
+
const comparisons = await fetchAndCompare(client, linkedCourses, {
|
|
110466
|
+
onProgress: (course) => s.message(`Checking ${formatCourse2(course)}...`),
|
|
110467
|
+
onError: (course, error57) => M2.warn(`Failed to fetch ${formatCourse2(course)}: ${error57}`)
|
|
110468
|
+
});
|
|
110469
|
+
const withChanges = filterChanges(comparisons);
|
|
110470
|
+
if (withChanges.length === 0) {
|
|
110471
|
+
s.stop("Local config is up to date with remote");
|
|
110472
|
+
if (!importAfterPull) {
|
|
110473
|
+
outro.success();
|
|
110474
|
+
if (exitOnComplete) {
|
|
110475
|
+
process.exit(0);
|
|
110476
|
+
}
|
|
110477
|
+
return;
|
|
110478
|
+
}
|
|
110479
|
+
} else {
|
|
110480
|
+
s.stop("Remote check complete");
|
|
110481
|
+
}
|
|
110482
|
+
if (withChanges.length > 0) {
|
|
110483
|
+
displayChanges(withChanges);
|
|
110484
|
+
const shouldApply = apply || await ye({ message: "Apply these changes to local config?" });
|
|
110485
|
+
if (isCancelled(shouldApply) || !shouldApply) {
|
|
110486
|
+
outro.cancelled();
|
|
110487
|
+
if (exitOnComplete)
|
|
110488
|
+
process.exit(0);
|
|
110489
|
+
return;
|
|
110490
|
+
}
|
|
110491
|
+
const updatedConfig = applyChanges(config7, withChanges);
|
|
110492
|
+
await saveConfig(configPath, updatedConfig);
|
|
110493
|
+
M2.success(`Updated ${dim(relativeConfigPath)}`);
|
|
110494
|
+
M2.success(`Pulled ${bold(withChanges.length)} course${withChanges.length === 1 ? "" : "s"}`);
|
|
110495
|
+
}
|
|
110496
|
+
if (importAfterPull) {
|
|
110497
|
+
const importResult = await importMoreCourses({ config: config7, configPath: customConfigPath });
|
|
110498
|
+
if (!importResult.success) {
|
|
110499
|
+
if (importResult.cancelled) {
|
|
110500
|
+
outro.cancelled();
|
|
110501
|
+
if (exitOnComplete)
|
|
110502
|
+
process.exit(0);
|
|
110503
|
+
return;
|
|
110504
|
+
}
|
|
110505
|
+
M2.error(importResult.error);
|
|
110506
|
+
outro.error("Import failed");
|
|
110507
|
+
if (exitOnComplete)
|
|
110508
|
+
process.exit(1);
|
|
110509
|
+
return;
|
|
110510
|
+
}
|
|
110511
|
+
}
|
|
110512
|
+
outro.success();
|
|
110513
|
+
if (exitOnComplete) {
|
|
110514
|
+
process.exit(0);
|
|
110515
|
+
}
|
|
110516
|
+
}
|
|
110517
|
+
|
|
110518
|
+
// src/commands/resources/pull/index.ts
|
|
110519
|
+
function registerPullSubcommand(resources) {
|
|
110520
|
+
resources.command("pull").description("Pull remote data from Timeback (remote → local)").option("-c, --config <path>", "Path to the config file").option("--env <environment>", "Target environment (staging or production)").option("--apply", "Apply changes without prompting").option("--import", "Also import additional courses (interactive)").action(async (opts) => {
|
|
110521
|
+
await pullCommand({
|
|
110522
|
+
configPath: opts.config,
|
|
110523
|
+
env: opts.env,
|
|
110524
|
+
apply: opts.apply,
|
|
110525
|
+
import: opts.import,
|
|
110526
|
+
skipIntro: true
|
|
110527
|
+
});
|
|
110528
|
+
});
|
|
110529
|
+
}
|
|
110530
|
+
|
|
110531
|
+
// src/commands/resources/push/index.ts
|
|
110532
|
+
function registerPushSubcommand(resources) {
|
|
110533
|
+
resources.command("push").description("Push local data to Timeback (local → remote)").option("-c, --config <path>", "Path to the config file").option("--env <environment>", "Target environment for new courses (staging, production)").option("--dry-run", "Show what would be pushed without making changes").option("-y, --yes", "Skip confirmation prompt").action(async (opts) => {
|
|
110534
|
+
await pushCommand({
|
|
110535
|
+
configPath: opts.config,
|
|
110536
|
+
env: opts.env,
|
|
110537
|
+
dryRun: opts.dryRun,
|
|
110538
|
+
yes: opts.yes,
|
|
110539
|
+
skipIntro: true
|
|
110540
|
+
});
|
|
110541
|
+
});
|
|
110542
|
+
}
|
|
110543
|
+
|
|
110544
|
+
// src/commands/resources/unlink/lib/display.ts
|
|
110545
|
+
function formatCourse3(course) {
|
|
110546
|
+
if (course.grade !== undefined) {
|
|
110547
|
+
return `${course.subject} Grade ${course.grade}`;
|
|
110548
|
+
}
|
|
110549
|
+
if (course.courseCode) {
|
|
110550
|
+
return `${course.subject} (${course.courseCode})`;
|
|
110551
|
+
}
|
|
110552
|
+
return course.subject;
|
|
110553
|
+
}
|
|
110554
|
+
function displayUnlinkSummary(coursesToUnlink) {
|
|
110555
|
+
for (const course of coursesToUnlink) {
|
|
110556
|
+
const lines = [];
|
|
110557
|
+
const courseCode = course.course.courseCode ? ` (${course.course.courseCode})` : "";
|
|
110558
|
+
lines.push(`${bold(course.title)}${dim(courseCode)}`);
|
|
110559
|
+
lines.push("");
|
|
110560
|
+
lines.push(` ${red("- unlink")} ${dim(course.courseId)}`);
|
|
110561
|
+
M2.message(lines.join(`
|
|
110562
|
+
`), { symbol: red("●") });
|
|
110563
|
+
}
|
|
110564
|
+
}
|
|
110565
|
+
|
|
110566
|
+
// src/commands/resources/unlink/lib/config.ts
|
|
110567
|
+
function updateConfigAfterUnlink(config7, unlinkedIndices, env2) {
|
|
110568
|
+
const updatedConfig = { ...config7, courses: [...config7.courses] };
|
|
110569
|
+
for (const index of unlinkedIndices) {
|
|
110570
|
+
const course = { ...updatedConfig.courses[index] };
|
|
110571
|
+
const ids = { ...course.ids };
|
|
110572
|
+
delete ids[env2];
|
|
110573
|
+
if (Object.keys(ids).length === 0) {
|
|
110574
|
+
course.ids = null;
|
|
110575
|
+
} else {
|
|
110576
|
+
course.ids = ids;
|
|
110577
|
+
}
|
|
110578
|
+
updatedConfig.courses[index] = course;
|
|
110579
|
+
}
|
|
110580
|
+
return updatedConfig;
|
|
110581
|
+
}
|
|
110582
|
+
function findLinkedCourses2(config7, env2) {
|
|
110583
|
+
const courses = [];
|
|
110584
|
+
for (let i = 0;i < config7.courses.length; i++) {
|
|
110585
|
+
const course = config7.courses[i];
|
|
110586
|
+
const courseId = course.ids?.[env2];
|
|
110587
|
+
if (courseId) {
|
|
110588
|
+
courses.push({
|
|
110589
|
+
index: i,
|
|
110590
|
+
course,
|
|
110591
|
+
courseId,
|
|
110592
|
+
title: formatCourse3(course)
|
|
110593
|
+
});
|
|
110594
|
+
}
|
|
110595
|
+
}
|
|
110596
|
+
return courses;
|
|
110597
|
+
}
|
|
110598
|
+
// src/commands/resources/unlink/lib/confirm.ts
|
|
110599
|
+
async function confirmUnlink(env2, appName, courseCount) {
|
|
110600
|
+
if (env2 === "production") {
|
|
110601
|
+
const confirmation = await he({
|
|
110602
|
+
message: `Type "${appName}" to confirm unlinking from production:`,
|
|
110603
|
+
placeholder: appName,
|
|
110604
|
+
validate: (value) => {
|
|
110605
|
+
if (value !== appName) {
|
|
110606
|
+
return `Please type "${appName}" exactly to confirm`;
|
|
110607
|
+
}
|
|
110608
|
+
}
|
|
110609
|
+
});
|
|
110610
|
+
if (isCancelled(confirmation) || confirmation !== appName) {
|
|
110611
|
+
return false;
|
|
110612
|
+
}
|
|
110613
|
+
return true;
|
|
110614
|
+
}
|
|
110615
|
+
const shouldUnlink = await ye({
|
|
110616
|
+
message: `Unlink ${courseCount} course${courseCount === 1 ? "" : "s"} from ${env2}?`,
|
|
110617
|
+
initialValue: false
|
|
110618
|
+
});
|
|
110619
|
+
return !isCancelled(shouldUnlink) && shouldUnlink;
|
|
110620
|
+
}
|
|
110621
|
+
async function selectCoursesToUnlink(linkedCourses, all, env2) {
|
|
110622
|
+
if (all)
|
|
110623
|
+
return linkedCourses;
|
|
110624
|
+
const selected = await fe({
|
|
110625
|
+
message: `Select courses to unlink from ${bold(env2)}:`,
|
|
110626
|
+
options: linkedCourses.map((c) => ({
|
|
110627
|
+
value: c.courseId,
|
|
110628
|
+
label: c.title,
|
|
110629
|
+
hint: dim(c.courseId)
|
|
110630
|
+
})),
|
|
110631
|
+
required: true
|
|
110632
|
+
});
|
|
110633
|
+
if (isCancelled(selected))
|
|
110634
|
+
return;
|
|
110635
|
+
return linkedCourses.filter((c) => selected.includes(c.courseId));
|
|
110636
|
+
}
|
|
110637
|
+
// src/commands/resources/unlink/lib/delete.ts
|
|
110638
|
+
async function safeDelete(deleteFn) {
|
|
110639
|
+
try {
|
|
110640
|
+
await deleteFn();
|
|
110641
|
+
return { success: true, notFound: false };
|
|
110642
|
+
} catch (error57) {
|
|
110643
|
+
if (isApiError2(error57) && error57.statusCode === 404) {
|
|
110644
|
+
return { success: true, notFound: true };
|
|
110645
|
+
}
|
|
110646
|
+
throw error57;
|
|
110647
|
+
}
|
|
110648
|
+
}
|
|
110649
|
+
async function deleteCourseStructure(client, courseId, onProgress) {
|
|
110650
|
+
const ids = deriveCourseStructureIds(courseId);
|
|
110651
|
+
let resourcesDeleted = 0;
|
|
110652
|
+
onProgress?.(`Unlinking componentResource ${dim(ids.componentResource)}...`);
|
|
110653
|
+
const crResult = await safeDelete(() => client.oneroster.courses.deleteComponentResource(ids.componentResource));
|
|
110654
|
+
if (crResult.success && !crResult.notFound)
|
|
110655
|
+
resourcesDeleted++;
|
|
110656
|
+
onProgress?.(`Unlinking resource ${dim(ids.resource)}...`);
|
|
110657
|
+
const resourceResult = await safeDelete(() => client.oneroster.resources.delete(ids.resource));
|
|
110658
|
+
if (resourceResult.success && !resourceResult.notFound)
|
|
110659
|
+
resourcesDeleted++;
|
|
110660
|
+
onProgress?.(`Unlinking component ${dim(ids.component)}...`);
|
|
110661
|
+
const componentResult = await safeDelete(() => client.oneroster.courses.deleteComponent(ids.component));
|
|
110662
|
+
if (componentResult.success && !componentResult.notFound)
|
|
110663
|
+
resourcesDeleted++;
|
|
110664
|
+
onProgress?.(`Unlinking course ${dim(ids.course)}...`);
|
|
110665
|
+
const courseResult = await safeDelete(() => client.oneroster.courses.delete(ids.course));
|
|
110666
|
+
if (courseResult.success && !courseResult.notFound)
|
|
110667
|
+
resourcesDeleted++;
|
|
110668
|
+
return resourcesDeleted;
|
|
110669
|
+
}
|
|
110670
|
+
async function executeUnlink(client, courses) {
|
|
110671
|
+
const s = Y2();
|
|
110672
|
+
s.start("Unlinking resources...");
|
|
110673
|
+
let unlinked = 0;
|
|
110674
|
+
let resourcesDeleted = 0;
|
|
110675
|
+
const errors3 = [];
|
|
110676
|
+
const unlinkedIndices = [];
|
|
110677
|
+
for (const course of courses) {
|
|
110678
|
+
try {
|
|
110679
|
+
const count = await deleteCourseStructure(client, course.courseId, (msg) => s.message(msg));
|
|
110680
|
+
resourcesDeleted += count;
|
|
110681
|
+
unlinked++;
|
|
110682
|
+
unlinkedIndices.push(course.index);
|
|
110683
|
+
} catch (error57) {
|
|
110684
|
+
const msg = error57 instanceof Error ? error57.message : String(error57);
|
|
110685
|
+
errors3.push(`Failed to unlink ${course.title}: ${msg}`);
|
|
110686
|
+
}
|
|
110687
|
+
}
|
|
110688
|
+
s.stop("Unlink complete");
|
|
110689
|
+
return { unlinked, resourcesDeleted, errors: errors3, unlinkedIndices };
|
|
110690
|
+
}
|
|
110691
|
+
// src/commands/resources/unlink/unlink.ts
|
|
110692
|
+
async function unlinkCommand(options) {
|
|
110693
|
+
const {
|
|
110694
|
+
configPath: customConfigPath,
|
|
110695
|
+
env: envOption,
|
|
110696
|
+
all = false,
|
|
110697
|
+
dryRun = false,
|
|
110698
|
+
yes = false,
|
|
110699
|
+
exitOnComplete = true,
|
|
110700
|
+
skipIntro = false
|
|
110701
|
+
} = options;
|
|
110702
|
+
if (!skipIntro) {
|
|
110703
|
+
intro("Timeback Unlink");
|
|
110704
|
+
}
|
|
110705
|
+
const configResult = await loadConfig({ configPath: customConfigPath });
|
|
110706
|
+
if (!configResult.success) {
|
|
110707
|
+
M2.error(configResult.error);
|
|
110708
|
+
outro.error("Unlink failed");
|
|
110709
|
+
if (exitOnComplete)
|
|
110710
|
+
process.exit(1);
|
|
110711
|
+
return;
|
|
110712
|
+
}
|
|
110713
|
+
const { config: config7, configPath } = configResult;
|
|
110714
|
+
M2.info(`Loaded ${dim(getRelativeConfigPath(configPath))}`);
|
|
110715
|
+
if (!envOption && yes) {
|
|
110716
|
+
M2.error("--yes requires explicit --env flag");
|
|
110717
|
+
outro.error("Unlink failed");
|
|
110718
|
+
if (exitOnComplete)
|
|
110719
|
+
process.exit(1);
|
|
110720
|
+
return;
|
|
110721
|
+
}
|
|
110722
|
+
const envResult = await resolveEnvironment({ env: envOption, skipIntro: true });
|
|
110723
|
+
if (!envResult) {
|
|
110724
|
+
outro.cancelled();
|
|
110725
|
+
if (exitOnComplete)
|
|
110726
|
+
process.exit(0);
|
|
110727
|
+
return;
|
|
110728
|
+
}
|
|
110729
|
+
const env2 = envResult.env;
|
|
110730
|
+
const linkedCourses = findLinkedCourses2(config7, env2);
|
|
110731
|
+
if (linkedCourses.length === 0) {
|
|
110732
|
+
M2.warn(`No courses linked to ${env2}. Nothing to unlink.`);
|
|
110733
|
+
outro.cancelled();
|
|
110734
|
+
if (exitOnComplete)
|
|
110735
|
+
process.exit(0);
|
|
110736
|
+
return;
|
|
110737
|
+
}
|
|
110738
|
+
const coursesToUnlink = await selectCoursesToUnlink(linkedCourses, all, env2);
|
|
110739
|
+
if (!coursesToUnlink || coursesToUnlink.length === 0) {
|
|
110740
|
+
outro.cancelled();
|
|
110741
|
+
if (exitOnComplete)
|
|
110742
|
+
process.exit(0);
|
|
110743
|
+
return;
|
|
110744
|
+
}
|
|
110745
|
+
displayUnlinkSummary(coursesToUnlink);
|
|
110746
|
+
if (dryRun) {
|
|
110747
|
+
outro.warn("Dry run (no changes made)");
|
|
110748
|
+
if (exitOnComplete)
|
|
110749
|
+
process.exit(0);
|
|
110750
|
+
return;
|
|
110751
|
+
}
|
|
110752
|
+
if (!yes && !await confirmUnlink(env2, config7.name, coursesToUnlink.length)) {
|
|
110753
|
+
outro.cancelled();
|
|
110754
|
+
if (exitOnComplete)
|
|
110755
|
+
process.exit(0);
|
|
110756
|
+
return;
|
|
110757
|
+
}
|
|
110758
|
+
const authOk = await initAuth({ env: env2 });
|
|
110759
|
+
if (!authOk) {
|
|
110760
|
+
M2.error(`Failed to authenticate for ${env2}`);
|
|
110761
|
+
outro.error("Unlink failed");
|
|
110762
|
+
if (exitOnComplete)
|
|
110763
|
+
process.exit(1);
|
|
110764
|
+
return;
|
|
110765
|
+
}
|
|
110766
|
+
const result = await executeUnlink(getAuthContext(env2).timeback, coursesToUnlink);
|
|
110767
|
+
if (result.unlinkedIndices.length > 0) {
|
|
110768
|
+
const updatedConfig = updateConfigAfterUnlink(config7, result.unlinkedIndices, env2);
|
|
110769
|
+
await saveConfig(configPath, updatedConfig);
|
|
110770
|
+
M2.success(`Updated ${dim(getRelativeConfigPath(configPath))}`);
|
|
110771
|
+
}
|
|
110772
|
+
if (result.unlinked > 0) {
|
|
110773
|
+
M2.success(`Unlinked ${bold(result.unlinked)} course${result.unlinked === 1 ? "" : "s"} (${result.resourcesDeleted} resources)`);
|
|
110774
|
+
}
|
|
110775
|
+
for (const error57 of result.errors) {
|
|
110776
|
+
M2.error(error57);
|
|
110777
|
+
}
|
|
110778
|
+
if (result.errors.length > 0) {
|
|
110779
|
+
outro.warn("Completed with errors");
|
|
110780
|
+
if (exitOnComplete)
|
|
110781
|
+
process.exit(1);
|
|
110782
|
+
return;
|
|
110783
|
+
}
|
|
110784
|
+
outro.success();
|
|
110785
|
+
if (exitOnComplete)
|
|
110786
|
+
process.exit(0);
|
|
110787
|
+
}
|
|
110788
|
+
|
|
110789
|
+
// src/commands/resources/unlink/index.ts
|
|
110790
|
+
function registerUnlinkSubcommand(resources) {
|
|
110791
|
+
resources.command("unlink").description("Unlink courses from Timeback (deletes remote, keeps local definition)").option("-c, --config <path>", "Path to the config file").option("--env <environment>", "Target environment (staging or production)").option("--all", "Unlink all linked courses for the environment").option("--dry-run", "Show what would be unlinked without making changes").option("-y, --yes", "Skip confirmation (requires --env)").action(async (opts) => {
|
|
110792
|
+
await unlinkCommand({
|
|
110793
|
+
configPath: opts.config,
|
|
110794
|
+
env: opts.env,
|
|
110795
|
+
all: opts.all,
|
|
110796
|
+
dryRun: opts.dryRun,
|
|
110797
|
+
yes: opts.yes,
|
|
110798
|
+
skipIntro: true
|
|
110799
|
+
});
|
|
110800
|
+
});
|
|
110801
|
+
}
|
|
110802
|
+
|
|
110803
|
+
// src/commands/resources/index.ts
|
|
110804
|
+
function registerResourcesCommand(program2) {
|
|
110805
|
+
const resources = program2.command("resources").description("Manage course resources (push/pull/unlink)");
|
|
110806
|
+
resources.hook("preAction", () => {
|
|
110807
|
+
intro("Timeback Resources");
|
|
110808
|
+
});
|
|
110809
|
+
registerPushSubcommand(resources);
|
|
110810
|
+
registerPullSubcommand(resources);
|
|
110811
|
+
registerUnlinkSubcommand(resources);
|
|
110812
|
+
}
|
|
110813
|
+
|
|
110897
110814
|
// ../studio/dist/index.js
|
|
110898
110815
|
import { createRequire as createRequire2 } from "node:module";
|
|
110899
110816
|
import { stripVTControlCharacters as S22 } from "node:util";
|
|
@@ -110903,11 +110820,11 @@ import O2 from "node:readline";
|
|
|
110903
110820
|
import { Writable as X2 } from "node:stream";
|
|
110904
110821
|
import y22 from "node:process";
|
|
110905
110822
|
import * as tty2 from "tty";
|
|
110906
|
-
import { mkdir as mkdir2, readFile as readFile3, stat as stat2, writeFile as
|
|
110823
|
+
import { mkdir as mkdir2, readFile as readFile3, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
|
|
110907
110824
|
import { homedir as homedir2, platform as platform22 } from "node:os";
|
|
110908
110825
|
import { join as join2 } from "node:path";
|
|
110909
110826
|
import { readFile as readFile22 } from "node:fs/promises";
|
|
110910
|
-
import { basename as basename3, extname as extname2, relative as relative2, resolve as
|
|
110827
|
+
import { basename as basename3, extname as extname2, relative as relative2, resolve as resolve5 } from "node:path";
|
|
110911
110828
|
import { loadConfig as c12LoadConfig2 } from "c12";
|
|
110912
110829
|
import { basename as basename22 } from "node:path";
|
|
110913
110830
|
import { createServer as createServerHTTP } from "http";
|
|
@@ -125497,7 +125414,7 @@ async function readCredentialsStore2() {
|
|
|
125497
125414
|
async function writeCredentialsStore2(store) {
|
|
125498
125415
|
if (!await ensureCredentialsDir2())
|
|
125499
125416
|
return false;
|
|
125500
|
-
await
|
|
125417
|
+
await writeFile3(CREDENTIALS_FILE2, JSON.stringify(store, null, 2), { mode: 384 });
|
|
125501
125418
|
return true;
|
|
125502
125419
|
}
|
|
125503
125420
|
async function getSavedCredentials2(environment) {
|
|
@@ -125969,14 +125886,20 @@ var TimebackConfig8 = exports_external6.object({
|
|
|
125969
125886
|
message: "Duplicate courseCode found; each must be unique",
|
|
125970
125887
|
path: ["courses"]
|
|
125971
125888
|
}).refine((config23) => {
|
|
125972
|
-
return config23.courses.every((c) =>
|
|
125973
|
-
|
|
125974
|
-
|
|
125975
|
-
|
|
125976
|
-
|
|
125977
|
-
|
|
125889
|
+
return config23.courses.every((c) => {
|
|
125890
|
+
if (c.sensor !== undefined || config23.sensor !== undefined) {
|
|
125891
|
+
return true;
|
|
125892
|
+
}
|
|
125893
|
+
const launchUrls = [
|
|
125894
|
+
c.launchUrl,
|
|
125895
|
+
config23.launchUrl,
|
|
125896
|
+
c.overrides?.staging?.launchUrl,
|
|
125897
|
+
c.overrides?.production?.launchUrl
|
|
125898
|
+
].filter(Boolean);
|
|
125899
|
+
return launchUrls.length > 0;
|
|
125900
|
+
});
|
|
125978
125901
|
}, {
|
|
125979
|
-
message: "Each course must have an effective
|
|
125902
|
+
message: "Each course must have an effective sensor. Either set `sensor` explicitly (top-level or per-course), or provide a `launchUrl` so sensor can be derived from its origin.",
|
|
125980
125903
|
path: ["courses"]
|
|
125981
125904
|
});
|
|
125982
125905
|
var EdubridgeDateString8 = exports_external6.union([IsoDateString8, IsoDateTimeString8]);
|
|
@@ -126972,7 +126895,7 @@ function deriveCourseIds(config32) {
|
|
|
126972
126895
|
}
|
|
126973
126896
|
async function readPackageVersion(cwd) {
|
|
126974
126897
|
try {
|
|
126975
|
-
const pkgPath =
|
|
126898
|
+
const pkgPath = resolve5(cwd, "package.json");
|
|
126976
126899
|
const pkg = JSON.parse(await readFile22(pkgPath, "utf-8"));
|
|
126977
126900
|
return pkg.version ?? "0.0.0";
|
|
126978
126901
|
} catch {
|
|
@@ -127006,7 +126929,7 @@ async function loadWithC122(cwd, configPath) {
|
|
|
127006
126929
|
const { $schema: _schema, ...configWithoutSchema } = rawConfig;
|
|
127007
126930
|
return {
|
|
127008
126931
|
config: configWithoutSchema,
|
|
127009
|
-
configFile: result.configFile ??
|
|
126932
|
+
configFile: result.configFile ?? resolve5(cwd, configPath ?? CONFIG_FILENAME3)
|
|
127010
126933
|
};
|
|
127011
126934
|
}
|
|
127012
126935
|
async function parse62() {
|
|
@@ -127059,7 +126982,6 @@ function printError2(error483) {
|
|
|
127059
126982
|
console.log(` ${yellow2(' "$schema": "https://timeback.dev/schema.json",')}`);
|
|
127060
126983
|
console.log(` ${yellow2(' "name": "My Timeback App",')}`);
|
|
127061
126984
|
console.log(` ${yellow2(' "launchUrl": "https://example.com/play",')}`);
|
|
127062
|
-
console.log(` ${yellow2(' "sensor": "https://example.com/sensor",')}`);
|
|
127063
126985
|
console.log(` ${yellow2(' "courses": [')}`);
|
|
127064
126986
|
console.log(` ${yellow2(' { "subject": "Math", "grade": 3 }')}`);
|
|
127065
126987
|
console.log(` ${yellow2(" ]")}`);
|
|
@@ -127110,6 +127032,46 @@ function toCourseConfig2(course, env22) {
|
|
|
127110
127032
|
}
|
|
127111
127033
|
return config32;
|
|
127112
127034
|
}
|
|
127035
|
+
async function inferLaunchUrl2(client, courseId) {
|
|
127036
|
+
try {
|
|
127037
|
+
const scopedCourse = client.oneroster.courses(courseId);
|
|
127038
|
+
const components = await scopedCourse.components({ where: { status: "active" } });
|
|
127039
|
+
const componentResourceIds = [];
|
|
127040
|
+
for (const component of components) {
|
|
127041
|
+
if (!component.sourcedId)
|
|
127042
|
+
continue;
|
|
127043
|
+
const crs = await client.oneroster.courses.componentResources({
|
|
127044
|
+
where: { "courseComponent.sourcedId": component.sourcedId, status: "active" }
|
|
127045
|
+
});
|
|
127046
|
+
for (const cr of crs) {
|
|
127047
|
+
if (cr.resource?.sourcedId) {
|
|
127048
|
+
componentResourceIds.push(cr.resource.sourcedId);
|
|
127049
|
+
}
|
|
127050
|
+
}
|
|
127051
|
+
}
|
|
127052
|
+
if (componentResourceIds.length === 0) {
|
|
127053
|
+
return;
|
|
127054
|
+
}
|
|
127055
|
+
const resourceIds = [...new Set(componentResourceIds)];
|
|
127056
|
+
const resources = await client.oneroster.resources.listAll({
|
|
127057
|
+
where: { sourcedId: { in: resourceIds } }
|
|
127058
|
+
});
|
|
127059
|
+
const launchUrls = new Set;
|
|
127060
|
+
for (const resource of resources) {
|
|
127061
|
+
const metadata = resource.metadata;
|
|
127062
|
+
const launchUrl = metadata?.launchUrl;
|
|
127063
|
+
if (typeof launchUrl === "string" && launchUrl.length > 0) {
|
|
127064
|
+
launchUrls.add(launchUrl);
|
|
127065
|
+
}
|
|
127066
|
+
}
|
|
127067
|
+
if (launchUrls.size === 1) {
|
|
127068
|
+
return [...launchUrls][0];
|
|
127069
|
+
}
|
|
127070
|
+
return;
|
|
127071
|
+
} catch {
|
|
127072
|
+
return;
|
|
127073
|
+
}
|
|
127074
|
+
}
|
|
127113
127075
|
function getParser2(opts = {}) {
|
|
127114
127076
|
return opts.playcademy ? playcademyParser2 : timebackParser2;
|
|
127115
127077
|
}
|
|
@@ -127134,11 +127096,11 @@ async function promptSelectEnv2(configuredEnvs) {
|
|
|
127134
127096
|
}
|
|
127135
127097
|
async function promptAppName3() {
|
|
127136
127098
|
const name = await he2({
|
|
127137
|
-
message: "
|
|
127099
|
+
message: "Search for your app",
|
|
127138
127100
|
placeholder: "My Timeback App",
|
|
127139
127101
|
validate: (value) => {
|
|
127140
127102
|
if (!value.trim())
|
|
127141
|
-
return "
|
|
127103
|
+
return "Please enter a search term";
|
|
127142
127104
|
}
|
|
127143
127105
|
});
|
|
127144
127106
|
if (pD2(name))
|
|
@@ -127158,7 +127120,7 @@ async function promptSearchQuery2() {
|
|
|
127158
127120
|
return;
|
|
127159
127121
|
return query;
|
|
127160
127122
|
}
|
|
127161
|
-
function
|
|
127123
|
+
function formatCourseLabel2(course) {
|
|
127162
127124
|
return course.title ?? "Untitled";
|
|
127163
127125
|
}
|
|
127164
127126
|
function sortCoursesByGrade2(courses) {
|
|
@@ -127174,7 +127136,7 @@ async function promptSelectCourses2(courses) {
|
|
|
127174
127136
|
message: `Which courses would you like to import?`,
|
|
127175
127137
|
options: sorted.map((c) => ({
|
|
127176
127138
|
value: c,
|
|
127177
|
-
label:
|
|
127139
|
+
label: formatCourseLabel2(c)
|
|
127178
127140
|
})),
|
|
127179
127141
|
required: true
|
|
127180
127142
|
});
|
|
@@ -127243,6 +127205,40 @@ async function selectCourses2(client, initialResults, filterFn) {
|
|
|
127243
127205
|
results = filter(moreResults);
|
|
127244
127206
|
}
|
|
127245
127207
|
}
|
|
127208
|
+
async function enrichCoursesWithLaunchUrls2(client, courses) {
|
|
127209
|
+
if (courses.length === 0)
|
|
127210
|
+
return courses;
|
|
127211
|
+
const s = Y22();
|
|
127212
|
+
let didStop = false;
|
|
127213
|
+
const stopOnce = (message) => {
|
|
127214
|
+
if (didStop)
|
|
127215
|
+
return;
|
|
127216
|
+
s.stop(message);
|
|
127217
|
+
didStop = true;
|
|
127218
|
+
};
|
|
127219
|
+
s.start("Fetching course details...");
|
|
127220
|
+
const enriched = [];
|
|
127221
|
+
try {
|
|
127222
|
+
for (const course of courses) {
|
|
127223
|
+
const courseId = course.ids?.staging ?? course.ids?.production;
|
|
127224
|
+
if (!courseId) {
|
|
127225
|
+
enriched.push(course);
|
|
127226
|
+
continue;
|
|
127227
|
+
}
|
|
127228
|
+
const launchUrl = await inferLaunchUrl2(client, courseId);
|
|
127229
|
+
if (launchUrl && !course.launchUrl) {
|
|
127230
|
+
enriched.push({ ...course, launchUrl });
|
|
127231
|
+
} else {
|
|
127232
|
+
enriched.push(course);
|
|
127233
|
+
}
|
|
127234
|
+
}
|
|
127235
|
+
stopOnce("Course details loaded");
|
|
127236
|
+
return enriched;
|
|
127237
|
+
} catch (error483) {
|
|
127238
|
+
stopOnce("Failed to load course details");
|
|
127239
|
+
throw error483;
|
|
127240
|
+
}
|
|
127241
|
+
}
|
|
127246
127242
|
async function searchAndSelectCourses3(options) {
|
|
127247
127243
|
const { client, environment, appName: existingAppName, excludeCourseIds = [] } = options;
|
|
127248
127244
|
const filterExcluded = (courses) => {
|
|
@@ -127265,11 +127261,11 @@ async function searchAndSelectCourses3(options) {
|
|
|
127265
127261
|
return { success: false, cancelled: true };
|
|
127266
127262
|
}
|
|
127267
127263
|
const courseConfigs = courses.map((c) => toCourseConfig2(c, environment));
|
|
127268
|
-
|
|
127264
|
+
const enrichedConfigs = await enrichCoursesWithLaunchUrls2(client, courseConfigs);
|
|
127269
127265
|
return {
|
|
127270
127266
|
success: true,
|
|
127271
127267
|
appName,
|
|
127272
|
-
courses:
|
|
127268
|
+
courses: enrichedConfigs,
|
|
127273
127269
|
environment
|
|
127274
127270
|
};
|
|
127275
127271
|
} catch (error483) {
|
|
@@ -130262,9 +130258,9 @@ var LEVEL_PREFIX9 = {
|
|
|
130262
130258
|
error: "[ERROR]"
|
|
130263
130259
|
};
|
|
130264
130260
|
function formatContext26(context) {
|
|
130265
|
-
return Object.entries(context).map(([key, value]) => `${key}=${
|
|
130261
|
+
return Object.entries(context).map(([key, value]) => `${key}=${formatValue10(value)}`).join(" ");
|
|
130266
130262
|
}
|
|
130267
|
-
function
|
|
130263
|
+
function formatValue10(value) {
|
|
130268
130264
|
if (typeof value === "string")
|
|
130269
130265
|
return value;
|
|
130270
130266
|
if (typeof value === "number")
|
|
@@ -131598,26 +131594,412 @@ function registerStudioCommand(program2) {
|
|
|
131598
131594
|
});
|
|
131599
131595
|
}
|
|
131600
131596
|
|
|
131601
|
-
// src/commands/
|
|
131602
|
-
|
|
131603
|
-
|
|
131604
|
-
|
|
131605
|
-
|
|
131606
|
-
|
|
131607
|
-
|
|
131608
|
-
|
|
131597
|
+
// src/commands/upgrade/upgrade.ts
|
|
131598
|
+
import { execSync as execSync2 } from "node:child_process";
|
|
131599
|
+
|
|
131600
|
+
// ../internal/utils/src/server/spinner.ts
|
|
131601
|
+
import { stdout as stdout2 } from "node:process";
|
|
131602
|
+
|
|
131603
|
+
// ../internal/utils/src/server/terminal.ts
|
|
131604
|
+
import { stdout } from "node:process";
|
|
131605
|
+
var isInteractive2 = stdout.isTTY ?? false;
|
|
131606
|
+
var cursor = {
|
|
131607
|
+
hide: "\x1B[?25l",
|
|
131608
|
+
show: "\x1B[?25h",
|
|
131609
|
+
up: (n) => `\x1B[${n}A`,
|
|
131610
|
+
down: (n) => `\x1B[${n}B`,
|
|
131611
|
+
forward: (n) => `\x1B[${n}C`,
|
|
131612
|
+
back: (n) => `\x1B[${n}D`,
|
|
131613
|
+
clearLine: "\x1B[K",
|
|
131614
|
+
clearScreen: "\x1B[2J",
|
|
131615
|
+
home: "\x1B[H"
|
|
131616
|
+
};
|
|
131617
|
+
function stripAnsi(str) {
|
|
131618
|
+
return str.replaceAll(/\u001B\[[0-9;]*[a-zA-Z]/g, "");
|
|
131619
|
+
}
|
|
131620
|
+
|
|
131621
|
+
// ../internal/utils/src/server/spinner.ts
|
|
131622
|
+
var SPINNER_FRAMES = [
|
|
131623
|
+
10251,
|
|
131624
|
+
10265,
|
|
131625
|
+
10297,
|
|
131626
|
+
10296,
|
|
131627
|
+
10300,
|
|
131628
|
+
10292,
|
|
131629
|
+
10278,
|
|
131630
|
+
10279,
|
|
131631
|
+
10247,
|
|
131632
|
+
10255
|
|
131633
|
+
].map((code) => String.fromCodePoint(code));
|
|
131634
|
+
var SPINNER_INTERVAL = 80;
|
|
131635
|
+
var CHECK_MARK = "✔";
|
|
131636
|
+
var CROSS_MARK = "✖";
|
|
131637
|
+
var STATUS_LABELS = {
|
|
131638
|
+
pending: "[PENDING]",
|
|
131639
|
+
running: "[RUNNING]",
|
|
131640
|
+
success: "[SUCCESS]",
|
|
131641
|
+
error: "[ERROR]"
|
|
131642
|
+
};
|
|
131643
|
+
|
|
131644
|
+
class Spinner {
|
|
131645
|
+
tasks = new Map;
|
|
131646
|
+
frameIndex = 0;
|
|
131647
|
+
intervalId = null;
|
|
131648
|
+
previousLineCount = 0;
|
|
131649
|
+
printedTasks = new Set;
|
|
131650
|
+
constructor(taskIds, texts) {
|
|
131651
|
+
for (const [index, id] of taskIds.entries()) {
|
|
131652
|
+
this.tasks.set(id, {
|
|
131653
|
+
text: texts[index] ?? "",
|
|
131654
|
+
status: "pending"
|
|
131655
|
+
});
|
|
131656
|
+
}
|
|
131657
|
+
}
|
|
131658
|
+
start() {
|
|
131659
|
+
if (!isInteractive2)
|
|
131660
|
+
return;
|
|
131661
|
+
stdout2.write(cursor.hide);
|
|
131662
|
+
this.render();
|
|
131663
|
+
this.intervalId = setInterval(() => {
|
|
131664
|
+
this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
|
|
131665
|
+
this.render();
|
|
131666
|
+
}, SPINNER_INTERVAL);
|
|
131667
|
+
}
|
|
131668
|
+
updateTask(taskId, status, finalText) {
|
|
131669
|
+
const task = this.tasks.get(taskId);
|
|
131670
|
+
if (!task)
|
|
131671
|
+
return;
|
|
131672
|
+
task.status = status;
|
|
131673
|
+
if (finalText)
|
|
131674
|
+
task.finalText = finalText;
|
|
131675
|
+
if (!isInteractive2) {
|
|
131676
|
+
this.renderNonInteractive(taskId, task);
|
|
131677
|
+
}
|
|
131678
|
+
}
|
|
131679
|
+
stop() {
|
|
131680
|
+
if (this.intervalId) {
|
|
131681
|
+
clearInterval(this.intervalId);
|
|
131682
|
+
this.intervalId = null;
|
|
131683
|
+
}
|
|
131684
|
+
if (isInteractive2) {
|
|
131685
|
+
this.render();
|
|
131686
|
+
stdout2.write(cursor.show);
|
|
131687
|
+
}
|
|
131688
|
+
}
|
|
131689
|
+
clear() {
|
|
131690
|
+
if (this.intervalId) {
|
|
131691
|
+
clearInterval(this.intervalId);
|
|
131692
|
+
this.intervalId = null;
|
|
131693
|
+
}
|
|
131694
|
+
if (isInteractive2 && this.previousLineCount > 0) {
|
|
131695
|
+
stdout2.write(cursor.up(this.previousLineCount));
|
|
131696
|
+
for (let i = 0;i < this.previousLineCount; i++) {
|
|
131697
|
+
stdout2.write(`\r${cursor.clearLine}
|
|
131698
|
+
`);
|
|
131699
|
+
}
|
|
131700
|
+
stdout2.write(cursor.up(this.previousLineCount));
|
|
131701
|
+
stdout2.write(cursor.show);
|
|
131702
|
+
}
|
|
131703
|
+
this.previousLineCount = 0;
|
|
131704
|
+
}
|
|
131705
|
+
render() {
|
|
131706
|
+
if (this.previousLineCount > 0) {
|
|
131707
|
+
stdout2.write(cursor.up(this.previousLineCount));
|
|
131708
|
+
}
|
|
131709
|
+
const spinner = SPINNER_FRAMES[this.frameIndex];
|
|
131710
|
+
const visibleTasks = [...this.tasks.values()].filter((t) => t.status !== "pending");
|
|
131711
|
+
for (const task of visibleTasks) {
|
|
131712
|
+
stdout2.write(`\r${cursor.clearLine}`);
|
|
131713
|
+
console.log(this.formatLine(task, spinner));
|
|
131714
|
+
}
|
|
131715
|
+
this.previousLineCount = visibleTasks.length;
|
|
131716
|
+
}
|
|
131717
|
+
formatLine(task, spinner) {
|
|
131718
|
+
switch (task.status) {
|
|
131719
|
+
case "running":
|
|
131720
|
+
return `${blue(spinner ?? "○")} ${task.text}`;
|
|
131721
|
+
case "success":
|
|
131722
|
+
return `${green(CHECK_MARK)} ${task.finalText ?? task.text}`;
|
|
131723
|
+
case "error":
|
|
131724
|
+
return `${red(CROSS_MARK)} Failed: ${task.text}`;
|
|
131725
|
+
default:
|
|
131726
|
+
return task.text;
|
|
131727
|
+
}
|
|
131728
|
+
}
|
|
131729
|
+
renderNonInteractive(taskId, task) {
|
|
131730
|
+
const key = `${taskId}-${task.status}`;
|
|
131731
|
+
if (this.printedTasks.has(key))
|
|
131732
|
+
return;
|
|
131733
|
+
this.printedTasks.add(key);
|
|
131734
|
+
const text = task.status === "success" ? task.finalText ?? task.text : task.text;
|
|
131735
|
+
console.log(`${STATUS_LABELS[task.status]} ${stripAnsi(text)}`);
|
|
131736
|
+
}
|
|
131737
|
+
}
|
|
131738
|
+
// ../internal/utils/src/server/runtime.ts
|
|
131739
|
+
import { execSync } from "node:child_process";
|
|
131740
|
+
import { join as join3 } from "node:path";
|
|
131741
|
+
var PACKAGE_MANAGERS = ["bun", "pnpm", "yarn", "npm"];
|
|
131742
|
+
var PACKAGE_NAME = "timeback";
|
|
131743
|
+
function getPlatformInfo() {
|
|
131744
|
+
return {
|
|
131745
|
+
platform: process.platform,
|
|
131746
|
+
arch: process.arch
|
|
131747
|
+
};
|
|
131748
|
+
}
|
|
131749
|
+
var STANDALONE_PATHS = [
|
|
131750
|
+
join3(".timeback", "bin"),
|
|
131751
|
+
join3(".local", "bin"),
|
|
131752
|
+
"/usr/local/bin/timeback",
|
|
131753
|
+
"/opt/homebrew/bin/timeback"
|
|
131754
|
+
];
|
|
131755
|
+
function isStandaloneBinary() {
|
|
131756
|
+
const execPath = process.execPath.toLowerCase();
|
|
131757
|
+
return STANDALONE_PATHS.some((p3) => execPath.includes(p3.toLowerCase()));
|
|
131758
|
+
}
|
|
131759
|
+
function runCommand(cmd) {
|
|
131760
|
+
try {
|
|
131761
|
+
return execSync(cmd, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
131762
|
+
} catch {
|
|
131763
|
+
return "";
|
|
131764
|
+
}
|
|
131765
|
+
}
|
|
131766
|
+
var GLOBAL_LIST_COMMANDS = {
|
|
131767
|
+
npm: "npm list -g --depth=0 2>/dev/null",
|
|
131768
|
+
bun: "bun pm ls -g 2>/dev/null",
|
|
131769
|
+
pnpm: "pnpm list -g --depth=0 2>/dev/null",
|
|
131770
|
+
yarn: "yarn global list 2>/dev/null"
|
|
131771
|
+
};
|
|
131772
|
+
function getInstallMethod() {
|
|
131773
|
+
if (isStandaloneBinary()) {
|
|
131774
|
+
return "standalone";
|
|
131775
|
+
}
|
|
131776
|
+
const execPath = process.execPath.toLowerCase();
|
|
131777
|
+
const sortedManagers = [...PACKAGE_MANAGERS].sort((a, b3) => {
|
|
131778
|
+
const aMatches = execPath.includes(a);
|
|
131779
|
+
const bMatches = execPath.includes(b3);
|
|
131780
|
+
if (aMatches && !bMatches)
|
|
131781
|
+
return -1;
|
|
131782
|
+
if (!aMatches && bMatches)
|
|
131783
|
+
return 1;
|
|
131784
|
+
return 0;
|
|
131785
|
+
});
|
|
131786
|
+
for (const pm of sortedManagers) {
|
|
131787
|
+
const output2 = runCommand(GLOBAL_LIST_COMMANDS[pm]);
|
|
131788
|
+
if (output2.includes(PACKAGE_NAME)) {
|
|
131789
|
+
return pm;
|
|
131790
|
+
}
|
|
131791
|
+
}
|
|
131792
|
+
return "unknown";
|
|
131793
|
+
}
|
|
131794
|
+
// src/commands/upgrade/lib/constants.ts
|
|
131795
|
+
var LATEST_VERSION_URL = "https://dl.timeback.dev/cli/latest/version.txt";
|
|
131796
|
+
var DOWNLOAD_BASE_URL = "https://dl.timeback.dev/cli/latest";
|
|
131797
|
+
var BINARY_NAME = "timeback";
|
|
131798
|
+
var DEFAULT_INSTALL_PATH = "/usr/local/bin/timeback";
|
|
131799
|
+
var BINARY_NAMES = {
|
|
131800
|
+
"darwin-arm64": "timeback-darwin-arm64",
|
|
131801
|
+
"darwin-x64": "timeback-darwin-x64",
|
|
131802
|
+
"linux-arm64": "timeback-linux-arm64",
|
|
131803
|
+
"linux-x64": "timeback-linux-x64",
|
|
131804
|
+
"win32-x64": "timeback-windows-x64.exe"
|
|
131805
|
+
};
|
|
131806
|
+
var PACKAGE_MANAGER_COMMANDS = {
|
|
131807
|
+
bun: "bun update -g timeback",
|
|
131808
|
+
npm: "npm update -g timeback",
|
|
131809
|
+
yarn: "yarn global upgrade timeback",
|
|
131810
|
+
pnpm: "pnpm update -g timeback"
|
|
131811
|
+
};
|
|
131812
|
+
// src/commands/upgrade/lib/platform.ts
|
|
131813
|
+
function getBinaryName() {
|
|
131814
|
+
const { platform: platform4, arch } = getPlatformInfo();
|
|
131815
|
+
const key = `${platform4}-${arch}`;
|
|
131816
|
+
const binaryName = BINARY_NAMES[key];
|
|
131817
|
+
if (!binaryName) {
|
|
131818
|
+
throw new Error(`Unsupported platform: ${platform4} ${arch}`);
|
|
131819
|
+
}
|
|
131820
|
+
return binaryName;
|
|
131821
|
+
}
|
|
131822
|
+
// src/commands/upgrade/lib/download.ts
|
|
131823
|
+
import { createWriteStream, existsSync as existsSync5, renameSync, unlinkSync } from "node:fs";
|
|
131824
|
+
import { chmod } from "node:fs/promises";
|
|
131825
|
+
import { tmpdir } from "node:os";
|
|
131826
|
+
import { dirname, join as join4 } from "node:path";
|
|
131827
|
+
import { Readable as Readable2 } from "node:stream";
|
|
131828
|
+
import { finished } from "node:stream/promises";
|
|
131829
|
+
async function fetchLatestVersion() {
|
|
131830
|
+
const response = await fetch(LATEST_VERSION_URL);
|
|
131831
|
+
if (!response.ok) {
|
|
131832
|
+
throw new Error(`Failed to fetch latest version: ${response.status} ${response.statusText}`);
|
|
131833
|
+
}
|
|
131834
|
+
return (await response.text()).trim();
|
|
131835
|
+
}
|
|
131836
|
+
async function downloadBinary(binaryName, targetPath) {
|
|
131837
|
+
const url7 = `${DOWNLOAD_BASE_URL}/${binaryName}`;
|
|
131838
|
+
const response = await fetch(url7);
|
|
131839
|
+
if (!response.ok) {
|
|
131840
|
+
throw new Error(`Failed to download binary: ${response.status} ${response.statusText}`);
|
|
131841
|
+
}
|
|
131842
|
+
if (!response.body) {
|
|
131843
|
+
throw new Error("No response body");
|
|
131844
|
+
}
|
|
131845
|
+
const fileStream = createWriteStream(targetPath);
|
|
131846
|
+
const readable = Readable2.fromWeb(response.body);
|
|
131847
|
+
await finished(readable.pipe(fileStream));
|
|
131848
|
+
}
|
|
131849
|
+
async function installBinary() {
|
|
131850
|
+
const binaryName = getBinaryName();
|
|
131851
|
+
const tempPath = join4(tmpdir(), `timeback-upgrade-${Date.now()}`);
|
|
131852
|
+
await downloadBinary(binaryName, tempPath);
|
|
131853
|
+
await chmod(tempPath, 493);
|
|
131854
|
+
const currentBinary = process.argv[0] ?? DEFAULT_INSTALL_PATH;
|
|
131855
|
+
const targetDir = dirname(currentBinary);
|
|
131856
|
+
const targetPath = join4(targetDir, BINARY_NAME);
|
|
131857
|
+
const backupPath = `${targetPath}.backup`;
|
|
131858
|
+
if (existsSync5(targetPath)) {
|
|
131859
|
+
renameSync(targetPath, backupPath);
|
|
131860
|
+
}
|
|
131861
|
+
try {
|
|
131862
|
+
renameSync(tempPath, targetPath);
|
|
131863
|
+
if (existsSync5(backupPath)) {
|
|
131864
|
+
unlinkSync(backupPath);
|
|
131865
|
+
}
|
|
131866
|
+
return targetPath;
|
|
131867
|
+
} catch (installError) {
|
|
131868
|
+
if (existsSync5(backupPath)) {
|
|
131869
|
+
renameSync(backupPath, targetPath);
|
|
131870
|
+
}
|
|
131871
|
+
throw installError;
|
|
131872
|
+
}
|
|
131873
|
+
}
|
|
131874
|
+
// src/commands/upgrade/upgrade.ts
|
|
131875
|
+
async function upgradeStandalone(latestVersion) {
|
|
131876
|
+
const s = Y2();
|
|
131877
|
+
s.start("Downloading upgrade...");
|
|
131878
|
+
s.message("Installing upgrade...");
|
|
131879
|
+
await installBinary();
|
|
131880
|
+
s.stop("Upgrade installed");
|
|
131881
|
+
M2.success(`Upgraded to ${bold(latestVersion)}`);
|
|
131882
|
+
}
|
|
131883
|
+
async function upgradeViaPackageManager(method, latestVersion) {
|
|
131884
|
+
const command = PACKAGE_MANAGER_COMMANDS[method];
|
|
131885
|
+
const shouldRun = await ye({ message: "Run this command now?", initialValue: true });
|
|
131886
|
+
if (isCancelled(shouldRun) || !shouldRun) {
|
|
131887
|
+
const message = [`To upgrade via ${method}, run:`, "", ` ${bold(bold(greenBright(command)))}`];
|
|
131888
|
+
Me(message.join(`
|
|
131889
|
+
`), "Manual Upgrade Instructions");
|
|
131890
|
+
return false;
|
|
131891
|
+
}
|
|
131892
|
+
try {
|
|
131893
|
+
execSync2(command, { stdio: "inherit" });
|
|
131894
|
+
M2.success(`Upgraded to ${bold(latestVersion)}`);
|
|
131895
|
+
return true;
|
|
131896
|
+
} catch {
|
|
131897
|
+
M2.error("Upgrade command failed. Please run manually.");
|
|
131898
|
+
return false;
|
|
131899
|
+
}
|
|
131900
|
+
}
|
|
131901
|
+
function showManualUpgradeInstructions() {
|
|
131902
|
+
M2.message(dim("Upgrade with your package manager, e.g. ") + bold("bun update -g timeback"));
|
|
131903
|
+
}
|
|
131904
|
+
async function upgradeCommand(options = {}) {
|
|
131905
|
+
const { exitOnComplete = true } = options;
|
|
131906
|
+
intro("Timeback Upgrade");
|
|
131907
|
+
const s = Y2();
|
|
131908
|
+
s.start("Detecting installation method...");
|
|
131909
|
+
const installMethod = getInstallMethod();
|
|
131910
|
+
const methodLabel = installMethod === "unknown" ? "" : ` ${dim(`(${installMethod})`)}`;
|
|
131911
|
+
s.stop(`Current: ${bold(cliVersion)}${methodLabel}`);
|
|
131912
|
+
s.start("Checking for updates...");
|
|
131913
|
+
let latestVersion;
|
|
131914
|
+
try {
|
|
131915
|
+
latestVersion = await fetchLatestVersion();
|
|
131916
|
+
} catch (error59) {
|
|
131917
|
+
s.stop("Failed to check for updates");
|
|
131918
|
+
M2.error(error59 instanceof Error ? error59.message : String(error59));
|
|
131919
|
+
outro.error("Upgrade failed");
|
|
131920
|
+
if (exitOnComplete) {
|
|
131921
|
+
process.exit(1);
|
|
131922
|
+
}
|
|
131923
|
+
return;
|
|
131924
|
+
}
|
|
131925
|
+
s.stop(`Latest version: ${bold(latestVersion)}`);
|
|
131926
|
+
if (cliVersion === latestVersion || cliVersion === `${latestVersion}-dev`) {
|
|
131927
|
+
M2.success("Already up to date!");
|
|
131928
|
+
outro.success();
|
|
131929
|
+
if (exitOnComplete) {
|
|
131930
|
+
process.exit(0);
|
|
131931
|
+
}
|
|
131932
|
+
return;
|
|
131933
|
+
}
|
|
131934
|
+
M2.info(`${yellow("Upgrade available:")} ${bold(red(cliVersion))} → ${bold(greenBright(latestVersion))}`);
|
|
131935
|
+
try {
|
|
131936
|
+
switch (installMethod) {
|
|
131937
|
+
case "standalone": {
|
|
131938
|
+
const shouldUpgrade = await ye({
|
|
131939
|
+
message: "Download and install the upgrade?",
|
|
131940
|
+
initialValue: true
|
|
131941
|
+
});
|
|
131942
|
+
if (isCancelled(shouldUpgrade) || !shouldUpgrade) {
|
|
131943
|
+
outro.cancelled();
|
|
131944
|
+
if (exitOnComplete) {
|
|
131945
|
+
process.exit(0);
|
|
131946
|
+
}
|
|
131947
|
+
return;
|
|
131948
|
+
}
|
|
131949
|
+
await upgradeStandalone(latestVersion);
|
|
131950
|
+
break;
|
|
131951
|
+
}
|
|
131952
|
+
case "bun":
|
|
131953
|
+
case "npm":
|
|
131954
|
+
case "yarn":
|
|
131955
|
+
case "pnpm": {
|
|
131956
|
+
const success7 = await upgradeViaPackageManager(installMethod, latestVersion);
|
|
131957
|
+
if (!success7) {
|
|
131958
|
+
outro.error("Upgrade incomplete");
|
|
131959
|
+
if (exitOnComplete) {
|
|
131960
|
+
process.exit(1);
|
|
131961
|
+
}
|
|
131962
|
+
return;
|
|
131963
|
+
}
|
|
131964
|
+
break;
|
|
131965
|
+
}
|
|
131966
|
+
default: {
|
|
131967
|
+
showManualUpgradeInstructions();
|
|
131968
|
+
outro.cancelled();
|
|
131969
|
+
if (exitOnComplete) {
|
|
131970
|
+
process.exit(0);
|
|
131971
|
+
}
|
|
131972
|
+
return;
|
|
131973
|
+
}
|
|
131974
|
+
}
|
|
131975
|
+
} catch (error59) {
|
|
131976
|
+
M2.error(error59 instanceof Error ? error59.message : String(error59));
|
|
131977
|
+
outro.error("Upgrade failed");
|
|
131978
|
+
if (exitOnComplete) {
|
|
131979
|
+
process.exit(1);
|
|
131980
|
+
}
|
|
131981
|
+
return;
|
|
131982
|
+
}
|
|
131983
|
+
outro.success();
|
|
131984
|
+
if (exitOnComplete) {
|
|
131985
|
+
process.exit(0);
|
|
131986
|
+
}
|
|
131987
|
+
}
|
|
131988
|
+
|
|
131989
|
+
// src/commands/upgrade/index.ts
|
|
131990
|
+
function registerUpgradeCommand(program2) {
|
|
131991
|
+
program2.command("upgrade").description("Upgrade the Timeback CLI to the latest version").action(async () => {
|
|
131992
|
+
await upgradeCommand({});
|
|
131609
131993
|
});
|
|
131610
131994
|
}
|
|
131611
131995
|
|
|
131612
131996
|
// src/cli.ts
|
|
131613
|
-
|
|
131614
|
-
program.name("timeback").description("CLI for Timeback tools and integrations").version(version8);
|
|
131997
|
+
program.name("timeback").description("CLI for Timeback tools and integrations").version(cliVersion);
|
|
131615
131998
|
registerApiCommand(program);
|
|
131616
|
-
registerCredentialsCommand(program);
|
|
131617
|
-
registerEditCommand(program);
|
|
131618
|
-
registerImportCommand(program);
|
|
131619
131999
|
registerInitCommand(program);
|
|
132000
|
+
registerCredentialsCommand(program);
|
|
131620
132001
|
registerInspectCommand(program);
|
|
132002
|
+
registerResourcesCommand(program);
|
|
131621
132003
|
registerStudioCommand(program);
|
|
131622
|
-
|
|
132004
|
+
registerUpgradeCommand(program);
|
|
131623
132005
|
program.parse();
|