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.
- package/dist/commands/apps.js +131 -1
- package/dist/help.js +3 -0
- package/package.json +1 -1
package/dist/commands/apps.js
CHANGED
|
@@ -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]
|