recess-cli 2.5.0 → 2.6.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.
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs/promises";
2
+ import http from "node:http";
2
3
  import path from "node:path";
3
4
  import { unwrap } from "../api.js";
4
5
  import { flagString, hasFlag } from "../args.js";
@@ -240,6 +241,7 @@ async function submit(ctx, dir, dryRun) {
240
241
  files,
241
242
  contract,
242
243
  ...(manifest.template ? { template: manifest.template } : {}),
244
+ ...(manifest.primitives?.length ? { primitives: manifest.primitives } : {}),
243
245
  dryRun,
244
246
  };
245
247
  const assignTo = flagString(ctx.parsed, "assign");
@@ -314,6 +316,9 @@ export async function runAppsCommand(ctx) {
314
316
  const scaffold = unwrap(await api.client.GET("/studio/scaffold"));
315
317
  const written = await writeFiles(dir, scaffold.files);
316
318
  await fs.writeFile(path.join(dir, "AGENTS.md"), agentsGuide(scaffold.docs));
319
+ if (scaffold.docs.forks) {
320
+ await fs.writeFile(path.join(dir, "FORKS.md"), scaffold.docs.forks);
321
+ }
317
322
  if (analysis) {
318
323
  await fs.writeFile(path.join(dir, BRIEF_FILE), briefFor(analysis));
319
324
  await writeAppLink(dir, {
@@ -337,6 +342,8 @@ export async function runAppsCommand(ctx) {
337
342
  analysis
338
343
  ? "Open the folder with your agent: brief.md is the assignment, AGENTS.md the contract."
339
344
  : "Open the folder with your agent and describe what the app should teach (AGENTS.md has the contract).",
345
+ "Customizing the wrong-answer detour? FORKS.md explains the fork and how to shape it per misconception.",
346
+ 'recess apps preview --reason "See it in my browser"',
340
347
  'recess apps validate --reason "Check my app"',
341
348
  analysis
342
349
  ? `recess apps publish --assign ${analysis.todo.studentId} --reason "Ship it to the kid"`
@@ -344,6 +351,129 @@ export async function runAppsCommand(ctx) {
344
351
  ],
345
352
  };
346
353
  }
354
+ if (verb === "preview") {
355
+ const dir = resolveDir(ctx, 2);
356
+ const indexHtml = await fs
357
+ .readFile(path.join(dir, "index.html"), "utf8")
358
+ .catch(() => null);
359
+ if (!indexHtml) {
360
+ throw new CliError("invalid_arguments", `${dir} has no index.html. Run \`recess apps init\` first.`);
361
+ }
362
+ const scaffold = unwrap(await api.client.GET("/studio/scaffold"));
363
+ const importMap = scaffold.importMap;
364
+ const serveOrigin = scaffold.serveOrigin;
365
+ if (!importMap || !serveOrigin) {
366
+ throw new CliError("api_error", "This Recess server does not expose the preview import map yet.");
367
+ }
368
+ const port = Number(flagString(parsed, "port") ?? "4820");
369
+ const types = {
370
+ ".html": "text/html; charset=utf-8",
371
+ ".js": "text/javascript; charset=utf-8",
372
+ ".mjs": "text/javascript; charset=utf-8",
373
+ ".css": "text/css; charset=utf-8",
374
+ ".json": "application/json; charset=utf-8",
375
+ ".svg": "image/svg+xml",
376
+ ".png": "image/png",
377
+ ".jpg": "image/jpeg",
378
+ ".webp": "image/webp",
379
+ };
380
+ const mapTag = `<script type="importmap">${JSON.stringify({ imports: importMap })}</script>`;
381
+ const server = http.createServer((req, res) => {
382
+ void (async () => {
383
+ const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0]);
384
+ if (urlPath.startsWith("/vendor/")) {
385
+ const upstream = await fetch(`${serveOrigin}${urlPath}`).catch(() => null);
386
+ if (!upstream || !upstream.ok) {
387
+ res.writeHead(upstream?.status ?? 502).end();
388
+ return;
389
+ }
390
+ res.writeHead(200, {
391
+ "content-type": upstream.headers.get("content-type") ??
392
+ "application/octet-stream",
393
+ "cache-control": "no-store",
394
+ });
395
+ res.end(Buffer.from(await upstream.arrayBuffer()));
396
+ return;
397
+ }
398
+ const rel = urlPath === "/" ? "index.html" : urlPath.slice(1);
399
+ const resolved = path.resolve(dir, rel);
400
+ if (resolved !== dir && !resolved.startsWith(dir + path.sep)) {
401
+ res.writeHead(404).end();
402
+ return;
403
+ }
404
+ const raw = await fs.readFile(resolved).catch(() => null);
405
+ if (raw === null) {
406
+ res.writeHead(404).end("not found");
407
+ return;
408
+ }
409
+ const type = types[path.extname(resolved).toLowerCase()] ??
410
+ "application/octet-stream";
411
+ if (rel === "index.html") {
412
+ let html = raw
413
+ .toString("utf8")
414
+ .replace(/<script\s+type="importmap">[\s\S]*?<\/script>/gi, "");
415
+ html = html.includes("<head>")
416
+ ? html.replace("<head>", `<head>\n${mapTag}`)
417
+ : `${mapTag}\n${html}`;
418
+ res.writeHead(200, {
419
+ "content-type": type,
420
+ "cache-control": "no-store",
421
+ });
422
+ res.end(html);
423
+ return;
424
+ }
425
+ res.writeHead(200, {
426
+ "content-type": type,
427
+ "cache-control": "no-store",
428
+ });
429
+ res.end(raw);
430
+ })().catch(() => {
431
+ try {
432
+ res.writeHead(500).end();
433
+ }
434
+ catch {
435
+ /* response already gone */
436
+ }
437
+ });
438
+ });
439
+ await new Promise((ready) => server.listen(port, ready));
440
+ console.log(JSON.stringify({
441
+ ok: true,
442
+ data: {
443
+ url: `http://localhost:${port}/`,
444
+ dir,
445
+ note: "Serving until Ctrl-C. Vendor libraries proxy from the Studio origin; params.json is served from this folder.",
446
+ },
447
+ }));
448
+ await new Promise(() => {
449
+ /* serve until the human stops it */
450
+ });
451
+ }
452
+ if (verb === "primitives") {
453
+ const sub = parsed.positionals[2];
454
+ if (sub === "list") {
455
+ return unwrap(await api.client.GET("/studio/primitives"));
456
+ }
457
+ if (sub === "publish") {
458
+ const file = path.resolve(positional(parsed, 3, "primitive module file"));
459
+ const name = flagString(parsed, "name");
460
+ if (!name) {
461
+ throw new CliError("invalid_arguments", "--name <kebab-name> is required.");
462
+ }
463
+ const code = await fs.readFile(file, "utf8").catch(() => null);
464
+ if (code === null) {
465
+ throw new CliError("invalid_arguments", `${file} is not readable.`);
466
+ }
467
+ return unwrap(await api.client.POST("/studio/primitives", {
468
+ body: {
469
+ name,
470
+ description: flagString(parsed, "description") ?? "",
471
+ code,
472
+ },
473
+ }));
474
+ }
475
+ throw new CliError("invalid_arguments", "Use: apps primitives list | apps primitives publish <file.js> --name <kebab-name> [--description TEXT].");
476
+ }
347
477
  if (verb === "standards") {
348
478
  const q = parsed.positionals.slice(2).join(" ").trim();
349
479
  if (!q) {
@@ -408,6 +538,6 @@ export async function runAppsCommand(ctx) {
408
538
  params: { path: { buildId } },
409
539
  }));
410
540
  }
411
- throw new CliError("invalid_arguments", "Unknown apps command. Use: apps init <dir> [--for-todo <todo-id>] | apps standards <words> | apps list | apps pull <project-id> [dir] | apps validate [dir] | apps publish [dir] [--assign <student-id>] | apps assign [dir] --student <id> | apps status <build-id>.");
541
+ throw new CliError("invalid_arguments", "Unknown apps command. Use: apps init <dir> [--for-todo <todo-id>] | apps preview [dir] [--port N] | apps standards <words> | apps list | apps pull <project-id> [dir] | apps validate [dir] | apps publish [dir] [--assign <student-id>] | apps assign [dir] --student <id> | apps status <build-id> | apps primitives list | apps primitives publish <file.js> --name <kebab-name>.");
412
542
  }
413
543
  //# sourceMappingURL=apps.js.map
@@ -152,6 +152,31 @@ export async function runOnboardingCommand({ parsed, api, writeCommand, }) {
152
152
  },
153
153
  }));
154
154
  }
155
+ if (verb === "rearm-kid-first-run") {
156
+ const kidUserId = positional(parsed, 2, "kid ID");
157
+ const preview = unwrap(await api.client.GET("/admin/onboarding/kids/{kidUserId}/first-run/rearm", { params: { path: { kidUserId } } }));
158
+ const body = {
159
+ expectedCompletedAt: preview.kidFirstRunCompletedAt,
160
+ };
161
+ return writeCommand(parsed, {
162
+ action: preview.alreadyEligible
163
+ ? "leave the kid's native first-run onboarding rearmed (it is already eligible)"
164
+ : "rearm the kid's Rocky-guided native first-run onboarding",
165
+ target: {
166
+ kidUserId: preview.kid.id,
167
+ familyId: preview.kid.familyId,
168
+ name: personName(preview.kid.firstName, preview.kid.lastName, preview.kid.id),
169
+ email: preview.kid.email,
170
+ },
171
+ request: body,
172
+ details: {
173
+ currentKidFirstRunCompletedAt: preview.kidFirstRunCompletedAt,
174
+ changes: ["User.kidFirstRunCompletedAt → null"],
175
+ retained: "Profile, todos, goals, XP, family/enrollment state, and Village progress are unchanged. A partial prior first run resumes from its existing durable progress.",
176
+ prerequisite: "The kid-onboarding feature flag must evaluate true for this kid separately.",
177
+ },
178
+ }, async () => unwrap(await api.client.POST("/admin/onboarding/kids/{kidUserId}/first-run/rearm", { params: { path: { kidUserId } }, body })));
179
+ }
155
180
  if (verb === "timeline") {
156
181
  const familyId = positional(parsed, 2, "family ID");
157
182
  return unwrap(await api.client.GET("/admin/onboarding/families/{familyId}/timeline", {
package/dist/help.js CHANGED
@@ -55,6 +55,9 @@ Usage:
55
55
  --method refund|credit|tokens [--full | --amount-cents N]
56
56
  [--who-pays guide|recess] [--reason TEXT] [--confirm]
57
57
  recess [--json] apps init <dir> [--for-todo <todo-id>] [--force]
58
+ recess [--json] apps preview [dir] [--port N]
59
+ recess [--json] apps primitives list
60
+ recess [--json] apps primitives publish <file.js> --name <kebab-name> [--description TEXT]
58
61
  recess [--json] apps standards <query>
59
62
  recess [--json] apps list
60
63
  recess [--json] apps pull <project-id> [dir]
@@ -145,6 +148,7 @@ Usage:
145
148
  recess [--json] onboarding queue [--school <institution-slug>]
146
149
  recess [--json] onboarding kids [--time-period-days N] [--cohort <id>]
147
150
  [--limit N] [--stage-filter all|scheduled|oriented|course|converted|lost]
151
+ recess [--json] onboarding rearm-kid-first-run <kid-id> [--confirm]
148
152
  recess [--json] onboarding timeline <family-id>
149
153
  recess [--json] onboarding readiness <family-id>
150
154
  recess [--json] onboarding family <family-id>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recess-cli",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
4
4
  "description": "Safe Recess administration and family AI tools from the command line.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {