recess-cli 3.3.1 → 3.4.0

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 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
@@ -6,6 +6,7 @@ import { CliError } from "./errors.js";
6
6
  */
7
7
  export const REPEATABLE_FLAGS = new Set(["kid", "unassign"]);
8
8
  const BOOLEAN_FLAGS = new Set([
9
+ "clear",
9
10
  "zero-balance",
10
11
  "allow-assigned-edits",
11
12
  "all-references",
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" ? undefined : requiredRequestReason(parsed);
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
- return template;
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 () => unwrap(await api.client.POST("/ai/goal-templates/", {
3804
- body: document,
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"));
@@ -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",
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]
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recess-cli",
3
- "version": "3.3.1",
3
+ "version": "3.4.0",
4
4
  "description": "Safe Recess administration and family AI tools from the command line.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -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