recess-cli 3.3.1 → 3.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.js +6 -1
- package/dist/args.js +1 -0
- package/dist/cli.js +231 -7
- package/dist/command-schema.js +3 -0
- package/dist/commands/apps.js +5 -1
- package/dist/help.js +17 -1
- package/dist/http.js +21 -0
- package/dist/index.js +8 -0
- package/dist/update-notice.js +85 -0
- package/package.json +1 -1
- package/skill/recess-cli/SKILL.md +3 -0
package/dist/api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import createClient from "openapi-fetch";
|
|
3
3
|
import { apiError, CliError } from "./errors.js";
|
|
4
|
-
import { cliRequestHeaders, markCliRequest, RECESS_CLIENT_CLI, } from "./http.js";
|
|
4
|
+
import { cliRequestHeaders, markCliRequest, noteServerCliVersions, RECESS_CLIENT_CLI, } from "./http.js";
|
|
5
5
|
const idempotencyContext = new AsyncLocalStorage();
|
|
6
6
|
export function withIdempotencyContext(context, execute) {
|
|
7
7
|
return idempotencyContext.run(context, execute);
|
|
@@ -40,6 +40,10 @@ export class RecessAdminApi {
|
|
|
40
40
|
applyIdempotencyHeaders(request.headers);
|
|
41
41
|
return request;
|
|
42
42
|
},
|
|
43
|
+
onResponse({ response }) {
|
|
44
|
+
noteServerCliVersions(response.headers);
|
|
45
|
+
return response;
|
|
46
|
+
},
|
|
43
47
|
});
|
|
44
48
|
}
|
|
45
49
|
requireAuth() {
|
|
@@ -55,6 +59,7 @@ export class RecessAdminApi {
|
|
|
55
59
|
const response = await fetch(new URL(path, this.config.apiOrigin), {
|
|
56
60
|
headers: cliRequestHeaders({ cookie: this.config.sessionCookie }, this.reason, this.clientTag),
|
|
57
61
|
});
|
|
62
|
+
noteServerCliVersions(response.headers);
|
|
58
63
|
const text = await response.text();
|
|
59
64
|
let body = text;
|
|
60
65
|
try {
|
package/dist/args.js
CHANGED
package/dist/cli.js
CHANGED
|
@@ -21,7 +21,8 @@ import { CliError } from "./errors.js";
|
|
|
21
21
|
import { listFeedback, submitFeedback } from "./feedback.js";
|
|
22
22
|
import { HELP } from "./help.js";
|
|
23
23
|
export { HELP };
|
|
24
|
-
import { cliRequestHeaders, requireCliRequestReason } from "./http.js";
|
|
24
|
+
import { cliRequestHeaders, noteServerCliVersions, requireCliRequestReason, } from "./http.js";
|
|
25
|
+
import { runSelfUpdate, UPGRADE_COMMAND, updateNoticeText, updateStatus, } from "./update-notice.js";
|
|
25
26
|
import { requireConfirmation } from "./safety.js";
|
|
26
27
|
import { installSkill, isEphemeralInstall, readBundledSkillVersion, readCliVersion, } from "./setup.js";
|
|
27
28
|
import { compareVersions, updateSkillFromServer } from "./skill-update.js";
|
|
@@ -174,6 +175,7 @@ async function doctor(config, reason) {
|
|
|
174
175
|
headers: cliRequestHeaders(undefined, reason),
|
|
175
176
|
signal: AbortSignal.timeout(5000),
|
|
176
177
|
});
|
|
178
|
+
noteServerCliVersions(response.headers);
|
|
177
179
|
checks.api = { reachable: response.ok, status: response.status };
|
|
178
180
|
}
|
|
179
181
|
catch (error) {
|
|
@@ -183,6 +185,9 @@ async function doctor(config, reason) {
|
|
|
183
185
|
};
|
|
184
186
|
}
|
|
185
187
|
checks.skill = await skillStatus(config, reason);
|
|
188
|
+
const update = updateStatus(await readCliVersion());
|
|
189
|
+
const updateAction = updateNoticeText(update);
|
|
190
|
+
checks.update = updateAction ? { ...update, action: updateAction } : update;
|
|
186
191
|
return checks;
|
|
187
192
|
}
|
|
188
193
|
/**
|
|
@@ -1438,6 +1443,19 @@ function parseGoalTemplateDocument(doc) {
|
|
|
1438
1443
|
* Resolve a template reference that may be a UUID or a slug. Slugs are what an
|
|
1439
1444
|
* author types and what the skill's prose uses; the routes take a UUID only.
|
|
1440
1445
|
*/
|
|
1446
|
+
/**
|
|
1447
|
+
* `/challenges-v2/{id}/preview` on the web app: the template as the card a
|
|
1448
|
+
* kid would see, with no chat around it. Returned by `goal-templates get`
|
|
1449
|
+
* and `create` so an agent can open it, or hand it to the human, right after
|
|
1450
|
+
* authoring.
|
|
1451
|
+
*/
|
|
1452
|
+
function goalTemplatePreviewUrl(webOrigin, templateId) {
|
|
1453
|
+
return new URL(`/challenges-v2/${encodeURIComponent(templateId)}/preview`, webOrigin).toString();
|
|
1454
|
+
}
|
|
1455
|
+
/** Runs the template with Rocky as a kid would (staff only; ephemeral, never touches real todos). */
|
|
1456
|
+
function goalTemplatePlayTestUrl(webOrigin, templateId) {
|
|
1457
|
+
return new URL(`/challenges-v2/${encodeURIComponent(templateId)}?play=1`, webOrigin).toString();
|
|
1458
|
+
}
|
|
1441
1459
|
async function resolveGoalTemplateId(api, reference) {
|
|
1442
1460
|
if (UUID_RE.test(reference))
|
|
1443
1461
|
return reference;
|
|
@@ -1910,7 +1928,11 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
1910
1928
|
}
|
|
1911
1929
|
}
|
|
1912
1930
|
const config = options.config ?? (await resolveConfig(flagString(parsed, "profile")));
|
|
1913
|
-
const requestReason = noun === "auth"
|
|
1931
|
+
const requestReason = noun === "auth"
|
|
1932
|
+
? undefined
|
|
1933
|
+
: noun === "update"
|
|
1934
|
+
? requireCliRequestReason(flagString(parsed, "reason") ?? "check for a recess CLI update")
|
|
1935
|
+
: requiredRequestReason(parsed);
|
|
1914
1936
|
if (noun === "doctor" && isRemote) {
|
|
1915
1937
|
return {
|
|
1916
1938
|
transport: "streamable-http-mcp",
|
|
@@ -1922,6 +1944,31 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
1922
1944
|
}
|
|
1923
1945
|
if (noun === "doctor")
|
|
1924
1946
|
return doctor(config, requestReason);
|
|
1947
|
+
if (noun === "update") {
|
|
1948
|
+
if (isRemote) {
|
|
1949
|
+
throw new CliError("remote_command_unavailable", "The update command is not exposed by the hosted Recess CLI.");
|
|
1950
|
+
}
|
|
1951
|
+
const cliVersion = await readCliVersion();
|
|
1952
|
+
const probe = await fetch(new URL("/health", config.apiOrigin), {
|
|
1953
|
+
headers: cliRequestHeaders(undefined, requestReason),
|
|
1954
|
+
signal: AbortSignal.timeout(5000),
|
|
1955
|
+
}).catch(() => null);
|
|
1956
|
+
if (probe)
|
|
1957
|
+
noteServerCliVersions(probe.headers);
|
|
1958
|
+
const status = updateStatus(cliVersion);
|
|
1959
|
+
if (status.state === "current")
|
|
1960
|
+
return { ...status, updated: false };
|
|
1961
|
+
if (isEphemeralInstall()) {
|
|
1962
|
+
throw new CliError("invalid_arguments", "This recess is running from a temporary npx cache. Run `npx recess-cli@latest` or install it with `npm install -g recess-cli@latest`.");
|
|
1963
|
+
}
|
|
1964
|
+
requireConfirmation(hasFlag(parsed, "confirm"), {
|
|
1965
|
+
action: "install the latest recess CLI globally with npm",
|
|
1966
|
+
target: { ...status },
|
|
1967
|
+
request: { command: UPGRADE_COMMAND },
|
|
1968
|
+
});
|
|
1969
|
+
const result = await runSelfUpdate();
|
|
1970
|
+
return { ...status, updated: true, ...result };
|
|
1971
|
+
}
|
|
1925
1972
|
if (noun === "setup") {
|
|
1926
1973
|
// Always lay down the bundled copy first: it is the floor, and it is the
|
|
1927
1974
|
// only copy guaranteed to match this binary. The served upgrade below is
|
|
@@ -2484,6 +2531,145 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
2484
2531
|
body,
|
|
2485
2532
|
})));
|
|
2486
2533
|
}
|
|
2534
|
+
if (noun === "users" && verb === "school") {
|
|
2535
|
+
const action = parsed.positionals[2];
|
|
2536
|
+
const userId = positional(parsed, 3, "user ID");
|
|
2537
|
+
if (action === "status")
|
|
2538
|
+
return unwrap(await api.client.GET("/admin/users/{userId}/school-status", {
|
|
2539
|
+
params: { path: { userId } },
|
|
2540
|
+
}));
|
|
2541
|
+
if (action === "history")
|
|
2542
|
+
return unwrap(await api.client.GET("/admin/users/{userId}/school-history", {
|
|
2543
|
+
params: {
|
|
2544
|
+
path: { userId },
|
|
2545
|
+
query: {
|
|
2546
|
+
limit: flagNumber(parsed, "limit"),
|
|
2547
|
+
cursor: flagNumber(parsed, "cursor"),
|
|
2548
|
+
},
|
|
2549
|
+
},
|
|
2550
|
+
}));
|
|
2551
|
+
if (action === "complete-action") {
|
|
2552
|
+
const actionId = positional(parsed, 4, "action ID");
|
|
2553
|
+
const body = { reason: flagString(parsed, "note", { required: true }) };
|
|
2554
|
+
return writeCommand(parsed, {
|
|
2555
|
+
action: "record verified provider cancellation or deactivation; does not contact the provider",
|
|
2556
|
+
target: { userId, actionId },
|
|
2557
|
+
request: body,
|
|
2558
|
+
}, async () => unwrap(await api.client.POST("/admin/users/{userId}/school-actions/{actionId}/complete", { params: { path: { userId, actionId } }, body })));
|
|
2559
|
+
}
|
|
2560
|
+
const status = action === "withdraw" ? "WITHDRAWN" : "ACTIVE";
|
|
2561
|
+
const tierId = action === "restore"
|
|
2562
|
+
? assertChoice(flagString(parsed, "tier", { required: true }), SCHOOL_TIER_IDS, "--tier")
|
|
2563
|
+
: undefined;
|
|
2564
|
+
const body = {
|
|
2565
|
+
status,
|
|
2566
|
+
reason: flagString(parsed, "note", { required: true }),
|
|
2567
|
+
expectedUpdatedAt: flagString(parsed, "expected-updated-at", {
|
|
2568
|
+
required: true,
|
|
2569
|
+
}),
|
|
2570
|
+
...(tierId ? { tierId } : {}),
|
|
2571
|
+
};
|
|
2572
|
+
return writeCommand(parsed, {
|
|
2573
|
+
action: status === "WITHDRAWN"
|
|
2574
|
+
? "withdraw SCHOOL student, cancel subscriptions immediately without refund, and revoke external learning access"
|
|
2575
|
+
: "restore Recess access; billing, classes, and external accounts are not restarted",
|
|
2576
|
+
target: { userId },
|
|
2577
|
+
request: body,
|
|
2578
|
+
details: status === "WITHDRAWN"
|
|
2579
|
+
? {
|
|
2580
|
+
cancelSubscriptionsImmediately: true,
|
|
2581
|
+
refunds: false,
|
|
2582
|
+
revokeExternalAccess: true,
|
|
2583
|
+
ixlRemoval: "nightly roster import",
|
|
2584
|
+
manualProviderFollowUpMayBeRequired: true,
|
|
2585
|
+
}
|
|
2586
|
+
: { restartsBilling: false, restoresExternalAccounts: false },
|
|
2587
|
+
}, async () => unwrap(await api.client.PATCH("/admin/users/{userId}/school-status", {
|
|
2588
|
+
params: { path: { userId } },
|
|
2589
|
+
body,
|
|
2590
|
+
})));
|
|
2591
|
+
}
|
|
2592
|
+
if (noun === "users" && verb === "enrollment") {
|
|
2593
|
+
if (parsed.positionals[2] === "set-partner-school-cc") {
|
|
2594
|
+
const school = positional(parsed, 3, "partner school ID or slug");
|
|
2595
|
+
const email = flagString(parsed, "email");
|
|
2596
|
+
const clear = hasFlag(parsed, "clear");
|
|
2597
|
+
if (!!email === clear) {
|
|
2598
|
+
throw new CliError("invalid_arguments", "Specify either --email <address> or --clear.");
|
|
2599
|
+
}
|
|
2600
|
+
const { schools } = unwrap(await api.client.GET("/admin/users/enrollment-partner-schools"));
|
|
2601
|
+
const partnerSchool = schools.find((entry) => entry.id === school || entry.slug === school);
|
|
2602
|
+
if (!partnerSchool)
|
|
2603
|
+
throw new CliError("invalid_arguments", "Unknown partner school. Use users enrollment list-partner-schools.");
|
|
2604
|
+
const body = {
|
|
2605
|
+
communicationsCcEmail: email?.trim().toLowerCase() ?? null,
|
|
2606
|
+
};
|
|
2607
|
+
return writeCommand(parsed, {
|
|
2608
|
+
action: "set partner school communications CC",
|
|
2609
|
+
target: {
|
|
2610
|
+
id: partnerSchool.id,
|
|
2611
|
+
slug: partnerSchool.slug,
|
|
2612
|
+
name: partnerSchool.name,
|
|
2613
|
+
},
|
|
2614
|
+
request: body,
|
|
2615
|
+
}, async () => unwrap(await api.client.PATCH("/admin/users/enrollment-partner-schools/{schoolId}/communications", {
|
|
2616
|
+
params: { path: { schoolId: partnerSchool.id } },
|
|
2617
|
+
body,
|
|
2618
|
+
})));
|
|
2619
|
+
}
|
|
2620
|
+
if (parsed.positionals[2] === "history") {
|
|
2621
|
+
const userId = positional(parsed, 3, "user ID");
|
|
2622
|
+
return unwrap(await api.client.GET("/admin/users/{userId}/enrollment-history", {
|
|
2623
|
+
params: {
|
|
2624
|
+
path: { userId },
|
|
2625
|
+
query: {
|
|
2626
|
+
limit: flagNumber(parsed, "limit"),
|
|
2627
|
+
cursor: flagNumber(parsed, "cursor"),
|
|
2628
|
+
},
|
|
2629
|
+
},
|
|
2630
|
+
}));
|
|
2631
|
+
}
|
|
2632
|
+
if (parsed.positionals[2] === "list-partner-schools") {
|
|
2633
|
+
return unwrap(await api.client.GET("/admin/users/enrollment-partner-schools"));
|
|
2634
|
+
}
|
|
2635
|
+
const userId = positional(parsed, 3, "user ID");
|
|
2636
|
+
const entities = {
|
|
2637
|
+
"online-learning-program": "ONLINE_LEARNING_PROGRAM",
|
|
2638
|
+
"recess-academy-school": "RECESS_ACADEMY_SCHOOL",
|
|
2639
|
+
"partner-school": "PARTNER_SCHOOL",
|
|
2640
|
+
unset: null,
|
|
2641
|
+
};
|
|
2642
|
+
const entity = assertChoice(flagString(parsed, "entity", { required: true }), [
|
|
2643
|
+
"online-learning-program",
|
|
2644
|
+
"recess-academy-school",
|
|
2645
|
+
"partner-school",
|
|
2646
|
+
"unset",
|
|
2647
|
+
], "--entity");
|
|
2648
|
+
const partner = flagString(parsed, "partner-school");
|
|
2649
|
+
if ((entity === "partner-school") !== !!partner) {
|
|
2650
|
+
throw new CliError("invalid_arguments", "--partner-school is required only with --entity partner-school.");
|
|
2651
|
+
}
|
|
2652
|
+
const schools = partner
|
|
2653
|
+
? unwrap(await api.client.GET("/admin/users/enrollment-partner-schools"))
|
|
2654
|
+
.schools
|
|
2655
|
+
: [];
|
|
2656
|
+
const partnerSchool = schools.find((school) => school.id === partner || school.slug === partner);
|
|
2657
|
+
if (partner && !partnerSchool) {
|
|
2658
|
+
throw new CliError("invalid_arguments", "Unknown partner school. Use users enrollment list-partner-schools to find its ID or slug.");
|
|
2659
|
+
}
|
|
2660
|
+
const body = {
|
|
2661
|
+
enrollmentEntity: entities[entity],
|
|
2662
|
+
enrollmentPartnerSchoolId: partnerSchool?.id ?? null,
|
|
2663
|
+
};
|
|
2664
|
+
return writeCommand(parsed, {
|
|
2665
|
+
action: "set user enrollment entity",
|
|
2666
|
+
target: { userId, partnerSchool: partnerSchool ?? null },
|
|
2667
|
+
request: body,
|
|
2668
|
+
}, async () => unwrap(await api.client.PATCH("/admin/users/{userId}/enrollment-entity", {
|
|
2669
|
+
params: { path: { userId } },
|
|
2670
|
+
body,
|
|
2671
|
+
})));
|
|
2672
|
+
}
|
|
2487
2673
|
if (noun === "users" && verb === "get") {
|
|
2488
2674
|
const userId = positional(parsed, 2, "user ID");
|
|
2489
2675
|
return unwrap(await api.client.GET("/admin/users/{userId}", {
|
|
@@ -3708,7 +3894,13 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
3708
3894
|
setupWorkflowSpec: template.setupWorkflowSpec,
|
|
3709
3895
|
};
|
|
3710
3896
|
}
|
|
3711
|
-
|
|
3897
|
+
// The card a kid would see, with no chat around it: the page to open
|
|
3898
|
+
// (or hand to the human) after authoring.
|
|
3899
|
+
return {
|
|
3900
|
+
...template,
|
|
3901
|
+
previewUrl: goalTemplatePreviewUrl(config.webOrigin, template.id),
|
|
3902
|
+
playTestUrl: goalTemplatePlayTestUrl(config.webOrigin, template.id),
|
|
3903
|
+
};
|
|
3712
3904
|
}
|
|
3713
3905
|
if (verb === "versions") {
|
|
3714
3906
|
const id = await resolveGoalTemplateId(api, positional(parsed, 2, "template ID or slug"));
|
|
@@ -3800,9 +3992,16 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
3800
3992
|
specInventory: validation.totals,
|
|
3801
3993
|
note: "Setup mode is DETERMINISTIC_WORKFLOW and is one-way: this template can never be converted back to a chat-driven setup.",
|
|
3802
3994
|
},
|
|
3803
|
-
}, async () =>
|
|
3804
|
-
|
|
3805
|
-
|
|
3995
|
+
}, async () => {
|
|
3996
|
+
const created = unwrap(await api.client.POST("/ai/goal-templates/", {
|
|
3997
|
+
body: document,
|
|
3998
|
+
}));
|
|
3999
|
+
return {
|
|
4000
|
+
...created,
|
|
4001
|
+
previewUrl: goalTemplatePreviewUrl(config.webOrigin, created.id),
|
|
4002
|
+
playTestUrl: goalTemplatePlayTestUrl(config.webOrigin, created.id),
|
|
4003
|
+
};
|
|
4004
|
+
});
|
|
3806
4005
|
}
|
|
3807
4006
|
if (verb === "policy") {
|
|
3808
4007
|
const id = await resolveGoalTemplateId(api, positional(parsed, 2, "template ID or slug"));
|
|
@@ -4060,6 +4259,7 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
4060
4259
|
(!Number.isInteger(coinAmount) || coinAmount < 1)) {
|
|
4061
4260
|
throw new CliError("invalid_arguments", "--coin-amount must be a positive integer.");
|
|
4062
4261
|
}
|
|
4262
|
+
const visibility = flagString(parsed, "visibility");
|
|
4063
4263
|
const isStarterRaw = flagString(parsed, "is-starter");
|
|
4064
4264
|
const setupAudienceRaw = flagString(parsed, "setup-audience");
|
|
4065
4265
|
const isStarter = isStarterRaw === undefined
|
|
@@ -4101,6 +4301,11 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
4101
4301
|
...(kind
|
|
4102
4302
|
? { kind: assertChoice(kind, GOAL_TEMPLATE_KINDS, "--kind") }
|
|
4103
4303
|
: {}),
|
|
4304
|
+
...(visibility
|
|
4305
|
+
? {
|
|
4306
|
+
visibility: assertChoice(visibility, ["PUBLIC", "PRIVATE", "STAFF_ONLY"], "--visibility"),
|
|
4307
|
+
}
|
|
4308
|
+
: {}),
|
|
4104
4309
|
...(agentInstructionsFile
|
|
4105
4310
|
? {
|
|
4106
4311
|
agentInstructions: await fs.readFile(path.resolve(agentInstructionsFile), "utf8"),
|
|
@@ -4117,7 +4322,7 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
4117
4322
|
// shape that caused the template incident; editing an existing spec goes
|
|
4118
4323
|
// through the guarded /ai patch path with its destructive-change token.
|
|
4119
4324
|
if (Object.keys(body).length === 1) {
|
|
4120
|
-
throw new CliError("invalid_arguments", "Pass at least one field to change (--title, --description, --emoji, --image-url, --coin-amount, --category, --tags, --sort-order, --is-starter, --setup-audience, --kind, --agent-instructions-file, --output-template-file).");
|
|
4325
|
+
throw new CliError("invalid_arguments", "Pass at least one field to change (--title, --description, --emoji, --image-url, --coin-amount, --category, --tags, --sort-order, --is-starter, --setup-audience, --kind, --visibility, --agent-instructions-file, --output-template-file).");
|
|
4121
4326
|
}
|
|
4122
4327
|
return writeCommand(parsed, {
|
|
4123
4328
|
action: "update goal template metadata (never its setupWorkflowSpec)",
|
|
@@ -4358,6 +4563,25 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
4358
4563
|
}
|
|
4359
4564
|
throw new CliError("invalid_arguments", "Use goal-templates list|get|versions|validate-spec|create|patch-spec|set-metadata|generate-image|delete|snapshot-files|capture-snapshot|apply|apply-starter.");
|
|
4360
4565
|
}
|
|
4566
|
+
if (noun === "goals" && verb === "adaptations") {
|
|
4567
|
+
const action = positional(parsed, 2, "goals adaptations action");
|
|
4568
|
+
const studentId = flagString(parsed, "student", { required: true });
|
|
4569
|
+
const goalId = flagString(parsed, "goal", { required: true });
|
|
4570
|
+
if (action === "list") {
|
|
4571
|
+
return unwrap(await api.client.GET("/tutor/students/{studentId}/goals/{goalId}/adaptations", { params: { path: { studentId, goalId } } }));
|
|
4572
|
+
}
|
|
4573
|
+
if (action === "diff") {
|
|
4574
|
+
const runId = positional(parsed, 3, "adaptation run ID");
|
|
4575
|
+
const path = flagString(parsed, "path");
|
|
4576
|
+
return unwrap(await api.client.GET("/tutor/students/{studentId}/goals/{goalId}/adaptations/{runId}/diff", {
|
|
4577
|
+
params: {
|
|
4578
|
+
path: { studentId, goalId, runId },
|
|
4579
|
+
query: path ? { path } : {},
|
|
4580
|
+
},
|
|
4581
|
+
}));
|
|
4582
|
+
}
|
|
4583
|
+
throw new CliError("invalid_arguments", "Use goals adaptations list|diff.");
|
|
4584
|
+
}
|
|
4361
4585
|
if (noun === "goals" && verb !== "files" && verb !== "pdf") {
|
|
4362
4586
|
if (verb === "complete" || verb === "undo-completion") {
|
|
4363
4587
|
const goalId = positional(parsed, 2, "goal ID");
|
package/dist/command-schema.js
CHANGED
|
@@ -135,6 +135,7 @@ const LOCAL_COMMAND_NOUNS = new Set([
|
|
|
135
135
|
"jobs",
|
|
136
136
|
"profile",
|
|
137
137
|
"setup",
|
|
138
|
+
"update",
|
|
138
139
|
]);
|
|
139
140
|
const AUTHENTICATED_COMMAND_PREFIXES = [
|
|
140
141
|
"village build",
|
|
@@ -164,6 +165,8 @@ const FAMILY_AI_COMMANDS = new Set([
|
|
|
164
165
|
"goal-templates list",
|
|
165
166
|
"goal-templates snapshot-files",
|
|
166
167
|
"goal-templates validate-spec",
|
|
168
|
+
"goals adaptations diff",
|
|
169
|
+
"goals adaptations list",
|
|
167
170
|
"goals archive",
|
|
168
171
|
"goals create",
|
|
169
172
|
"goals edit",
|
package/dist/commands/apps.js
CHANGED
|
@@ -212,7 +212,7 @@ function agentsGuide(docs) {
|
|
|
212
212
|
"widgets.js, engine.js, key.js — a modified locked file fails validation.",
|
|
213
213
|
"",
|
|
214
214
|
"Before publishing, write two files at the root:",
|
|
215
|
-
"- contract.json — the BuildContract
|
|
215
|
+
"- contract.json — the BuildContract: copy the example in the contract.json section below.",
|
|
216
216
|
'- manifest.json — { "title": "...", "description": "...", "template": { "ccss": "5.NF.A.1", "modes": ["prepare","remediate"], "misconceptions": ["..."] } }',
|
|
217
217
|
"",
|
|
218
218
|
'Then: `recess apps validate --reason "..."` (reports without publishing) and',
|
|
@@ -228,6 +228,10 @@ function agentsGuide(docs) {
|
|
|
228
228
|
"",
|
|
229
229
|
"---",
|
|
230
230
|
"",
|
|
231
|
+
docs.contract ?? "",
|
|
232
|
+
"",
|
|
233
|
+
"---",
|
|
234
|
+
"",
|
|
231
235
|
docs.pedagogy ? `# Pedagogy\n\n${docs.pedagogy}` : "",
|
|
232
236
|
"",
|
|
233
237
|
"---",
|
package/dist/help.js
CHANGED
|
@@ -17,6 +17,7 @@ Usage:
|
|
|
17
17
|
recess [--json] agent-context
|
|
18
18
|
recess [--json] setup [--skill-only]
|
|
19
19
|
recess [--json] doctor
|
|
20
|
+
recess [--json] update [--confirm]
|
|
20
21
|
recess [--json] profile list
|
|
21
22
|
recess [--json] profile show <name>
|
|
22
23
|
recess [--json] profile save <name> [--api-origin URL] [--web-origin URL]
|
|
@@ -34,6 +35,18 @@ Usage:
|
|
|
34
35
|
recess [--json] auth status|logout
|
|
35
36
|
recess [--json] users search <name-or-id> [--limit 10]
|
|
36
37
|
recess [--json] users get <user-id>
|
|
38
|
+
recess [--json] users school complete-action <user-id> <action-id> --note <text> [--confirm]
|
|
39
|
+
recess [--json] users school status <user-id>
|
|
40
|
+
recess [--json] users school history <user-id> [--limit 25] [--cursor <id>]
|
|
41
|
+
recess [--json] users school withdraw <user-id> --note <text> --expected-updated-at <iso> [--confirm]
|
|
42
|
+
recess [--json] users school restore <user-id> --tier <tier-id> --note <text> --expected-updated-at <iso> [--confirm]
|
|
43
|
+
recess [--json] users enrollment history <user-id> [--limit 25] [--cursor <id>]
|
|
44
|
+
recess [--json] users enrollment set-partner-school-cc <school-id-or-slug>
|
|
45
|
+
[--email <address> | --clear] [--confirm]
|
|
46
|
+
recess [--json] users enrollment list-partner-schools
|
|
47
|
+
recess [--json] users enrollment set <user-id>
|
|
48
|
+
--entity online-learning-program|recess-academy-school|partner-school|unset
|
|
49
|
+
[--partner-school <school-id-or-slug>] [--confirm]
|
|
37
50
|
recess [--json] users lock <user-id> [--confirm]
|
|
38
51
|
recess [--json] users unlock <user-id> [--confirm]
|
|
39
52
|
recess [--json] users disable <user-id> [--whole-family] [--allow-billing] [--confirm]
|
|
@@ -378,7 +391,7 @@ Usage:
|
|
|
378
391
|
[--title TEXT] [--description TEXT] [--emoji X] [--category TEXT] [--tags A,B]
|
|
379
392
|
[--image-url URL] [--coin-amount N] [--sort-order N] [--is-starter true|false]
|
|
380
393
|
[--setup-audience KID_FRIENDLY|PARENT_SETUP] [--kind SIMPLE|BLUEPRINT]
|
|
381
|
-
[--agent-instructions-file <path>]
|
|
394
|
+
[--visibility PUBLIC|PRIVATE|STAFF_ONLY] [--agent-instructions-file <path>]
|
|
382
395
|
[--output-template-file <path>] [--confirm]
|
|
383
396
|
recess [--json] goal-templates delete <template-id> --expected-version N [--confirm]
|
|
384
397
|
recess [--json] goal-templates generate-image <template-id|slug>
|
|
@@ -455,6 +468,9 @@ Usage:
|
|
|
455
468
|
[--lessons-dir <dir>] [--emoji TEXT] [--grade-min N] [--grade-max N]
|
|
456
469
|
[--visibility PUBLIC|STAFF] [--confirm]
|
|
457
470
|
recess [--json] tutor-templates delete <slug> [--confirm]
|
|
471
|
+
recess [--json] goals adaptations list --student <goal-owner-id> --goal <goal-id>
|
|
472
|
+
recess [--json] goals adaptations diff <run-id> --student <goal-owner-id>
|
|
473
|
+
--goal <goal-id> [--path P]
|
|
458
474
|
recess [--json] goals files list --student <goal-owner-id> --goal <goal-id>
|
|
459
475
|
recess [--json] goals files read --student <goal-owner-id> --goal <goal-id> --path P
|
|
460
476
|
recess [--json] goals files init --student <goal-owner-id> --draft <draft-slug>
|
package/dist/http.js
CHANGED
|
@@ -10,6 +10,25 @@ export const RECESS_REASON_HEADER = "x-recess-reason";
|
|
|
10
10
|
*/
|
|
11
11
|
export const RECESS_REASON_ENCODING_HEADER = "x-recess-reason-encoding";
|
|
12
12
|
export const RECESS_REASON_ENCODING_UTF8 = "utf-8-percent";
|
|
13
|
+
export const RECESS_CLI_VERSION_HEADER = "x-recess-cli-version";
|
|
14
|
+
export const RECESS_CLI_LATEST_HEADER = "x-recess-cli-latest";
|
|
15
|
+
export const RECESS_CLI_MINIMUM_HEADER = "x-recess-cli-minimum";
|
|
16
|
+
let requestCliVersion;
|
|
17
|
+
const observedCliVersions = {};
|
|
18
|
+
export function setRequestCliVersion(version) {
|
|
19
|
+
requestCliVersion = version;
|
|
20
|
+
}
|
|
21
|
+
export function noteServerCliVersions(headers) {
|
|
22
|
+
const latest = headers.get(RECESS_CLI_LATEST_HEADER)?.trim();
|
|
23
|
+
const minimum = headers.get(RECESS_CLI_MINIMUM_HEADER)?.trim();
|
|
24
|
+
if (latest)
|
|
25
|
+
observedCliVersions.latest = latest;
|
|
26
|
+
if (minimum)
|
|
27
|
+
observedCliVersions.minimum = minimum;
|
|
28
|
+
}
|
|
29
|
+
export function serverCliVersions() {
|
|
30
|
+
return { ...observedCliVersions };
|
|
31
|
+
}
|
|
13
32
|
const ASCII_PRINTABLE = /^[\x20-\x7E]*$/;
|
|
14
33
|
/**
|
|
15
34
|
* Make a human-written reason safe to put in an HTTP header.
|
|
@@ -53,6 +72,8 @@ export function requireCliRequestReason(value) {
|
|
|
53
72
|
}
|
|
54
73
|
export function markCliRequest(headers, reason, client = RECESS_CLIENT_CLI) {
|
|
55
74
|
headers.set(RECESS_CLIENT_HEADER, client);
|
|
75
|
+
if (requestCliVersion)
|
|
76
|
+
headers.set(RECESS_CLI_VERSION_HEADER, requestCliVersion);
|
|
56
77
|
if (reason) {
|
|
57
78
|
const encoded = encodeReasonHeader(reason);
|
|
58
79
|
headers.set(RECESS_REASON_HEADER, encoded.value);
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,9 @@ import { parseArgs } from "./args.js";
|
|
|
4
4
|
import { runCommand } from "./cli.js";
|
|
5
5
|
import { deliverJson } from "./delivery.js";
|
|
6
6
|
import { CliError } from "./errors.js";
|
|
7
|
+
import { setRequestCliVersion } from "./http.js";
|
|
8
|
+
import { readCliVersion } from "./setup.js";
|
|
9
|
+
import { printUpdateNotice } from "./update-notice.js";
|
|
7
10
|
async function withInteractiveAdmissionStage(args) {
|
|
8
11
|
const parsed = parseArgs(args);
|
|
9
12
|
if (parsed.flags.has("json") ||
|
|
@@ -41,6 +44,8 @@ async function withInteractiveAdmissionStage(args) {
|
|
|
41
44
|
const argv = await withInteractiveAdmissionStage(process.argv.slice(2));
|
|
42
45
|
const parsedArgv = parseArgs(argv);
|
|
43
46
|
const json = parsedArgv.flags.has("json");
|
|
47
|
+
const cliVersion = await readCliVersion();
|
|
48
|
+
setRequestCliVersion(cliVersion);
|
|
44
49
|
// The interactive console owns the terminal, so it runs before the JSON
|
|
45
50
|
// envelope machinery rather than through it.
|
|
46
51
|
if (argv[0] === "ui") {
|
|
@@ -103,4 +108,7 @@ catch (error) {
|
|
|
103
108
|
}
|
|
104
109
|
process.exitCode = cliError.exitCode;
|
|
105
110
|
}
|
|
111
|
+
if (parsedArgv.positionals[0] !== "update") {
|
|
112
|
+
await printUpdateNotice(cliVersion);
|
|
113
|
+
}
|
|
106
114
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { defaultConfigPath } from "./config.js";
|
|
5
|
+
import { CliError } from "./errors.js";
|
|
6
|
+
import { serverCliVersions } from "./http.js";
|
|
7
|
+
import { compareVersions } from "./skill-update.js";
|
|
8
|
+
export const UPGRADE_COMMAND = "npm install -g recess-cli@latest";
|
|
9
|
+
const NOTICE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
10
|
+
export function updateStatus(cliVersion, versions = serverCliVersions()) {
|
|
11
|
+
const { latest, minimum } = versions;
|
|
12
|
+
if (minimum && compareVersions(cliVersion, minimum) < 0) {
|
|
13
|
+
return { state: "required", cliVersion, latest, minimum };
|
|
14
|
+
}
|
|
15
|
+
if (!latest)
|
|
16
|
+
return { state: "unknown", cliVersion };
|
|
17
|
+
if (compareVersions(cliVersion, latest) < 0) {
|
|
18
|
+
return { state: "available", cliVersion, latest, minimum };
|
|
19
|
+
}
|
|
20
|
+
return { state: "current", cliVersion, latest };
|
|
21
|
+
}
|
|
22
|
+
export function updateNoticeText(status) {
|
|
23
|
+
if (status.state === "required") {
|
|
24
|
+
return `Warning: recess ${status.cliVersion} is older than the minimum supported version ${status.minimum}, so some commands may fail. Update with \`recess update --confirm\` or \`${UPGRADE_COMMAND}\`.`;
|
|
25
|
+
}
|
|
26
|
+
if (status.state === "available") {
|
|
27
|
+
return `recess ${status.latest} is available (you have ${status.cliVersion}). Update with \`recess update --confirm\` or \`${UPGRADE_COMMAND}\`.`;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
function noticeStampPath() {
|
|
32
|
+
return path.join(path.dirname(defaultConfigPath()), "update-notice.json");
|
|
33
|
+
}
|
|
34
|
+
async function recentlyNotified(latest) {
|
|
35
|
+
try {
|
|
36
|
+
const stamp = JSON.parse(await fs.readFile(noticeStampPath(), "utf8"));
|
|
37
|
+
if (stamp.latest !== latest || !stamp.notifiedAt)
|
|
38
|
+
return false;
|
|
39
|
+
return Date.now() - Date.parse(stamp.notifiedAt) < NOTICE_INTERVAL_MS;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function recordNotice(latest) {
|
|
46
|
+
try {
|
|
47
|
+
await fs.mkdir(path.dirname(noticeStampPath()), { recursive: true });
|
|
48
|
+
await fs.writeFile(noticeStampPath(), JSON.stringify({ latest, notifiedAt: new Date().toISOString() }));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export async function printUpdateNotice(cliVersion, write = (text) => process.stderr.write(text)) {
|
|
55
|
+
if (process.env.RECESS_CLI_NO_UPDATE_NOTICE === "1")
|
|
56
|
+
return;
|
|
57
|
+
const status = updateStatus(cliVersion);
|
|
58
|
+
const text = updateNoticeText(status);
|
|
59
|
+
if (!text)
|
|
60
|
+
return;
|
|
61
|
+
if (status.state === "available") {
|
|
62
|
+
if (await recentlyNotified(status.latest))
|
|
63
|
+
return;
|
|
64
|
+
await recordNotice(status.latest);
|
|
65
|
+
}
|
|
66
|
+
write(`\n${text}\n`);
|
|
67
|
+
}
|
|
68
|
+
export async function runSelfUpdate() {
|
|
69
|
+
const [bin, ...args] = UPGRADE_COMMAND.split(" ");
|
|
70
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
71
|
+
const child = spawn(bin, args, {
|
|
72
|
+
stdio: ["ignore", process.stderr, process.stderr],
|
|
73
|
+
shell: process.platform === "win32",
|
|
74
|
+
});
|
|
75
|
+
child.on("error", reject);
|
|
76
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
77
|
+
}).catch((error) => {
|
|
78
|
+
throw new CliError("update_failed", `Could not run \`${UPGRADE_COMMAND}\`: ${error instanceof Error ? error.message : String(error)}`);
|
|
79
|
+
});
|
|
80
|
+
if (exitCode !== 0) {
|
|
81
|
+
throw new CliError("update_failed", `\`${UPGRADE_COMMAND}\` exited with code ${exitCode}. Run it yourself to see the full npm output; a permissions error usually means the global npm directory needs sudo or a Node version manager.`);
|
|
82
|
+
}
|
|
83
|
+
return { command: UPGRADE_COMMAND, exitCode };
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=update-notice.js.map
|
package/package.json
CHANGED
|
@@ -107,6 +107,9 @@ prompts a model for them.
|
|
|
107
107
|
findings — fix them and publish again. Both commands wait for the result (`--no-wait` to return the
|
|
108
108
|
build id and poll with `apps status`).
|
|
109
109
|
- `recess --json apps pull <project-id> [dir]` downloads a project you own to keep editing it.
|
|
110
|
+
- A published `appUrl` is also a valid goal queue entry (`goals queue set`, or a template's
|
|
111
|
+
external practice queue): each app becomes one module. Build interactive content this way rather
|
|
112
|
+
than linking a chat artifact the kid cannot open inside Recess.
|
|
110
113
|
- `recess --json apps standards "<words or notation>"` finds the CCSS standard for `manifest.json`
|
|
111
114
|
(`"grade 4 adding fractions"` → `4.NF.B.3a`…); guides rarely know the codes, look them up.
|
|
112
115
|
|