recess-cli 2.5.0 → 2.6.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.
@@ -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
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]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recess-cli",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "Safe Recess administration and family AI tools from the command line.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {