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.
- package/dist/commands/apps.js +131 -1
- package/dist/commands/onboarding.js +25 -0
- package/dist/help.js +4 -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
|
|
@@ -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>
|