recess-cli 1.8.0 → 1.9.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/README.md +24 -1
- package/dist/cli.js +291 -3
- package/dist/index.js +37 -1
- package/dist/skills-cache.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ npm install -g recess-cli
|
|
|
11
11
|
recess setup
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
`setup` installs the bundled skill
|
|
14
|
+
`setup` installs the bundled skill in the Codex and Claude Code user directories, which Cursor also discovers for compatibility, and then opens Recess SSO in your browser (skip the browser step with `--skill-only`; it is also skipped when a live session already exists). Restart your agent afterwards so it discovers the skill. `npx -y recess-cli setup` works too, but leaves no `recess` on your PATH — which is the command the installed skill tells the agent to run — so `setup` warns when it detects it is running from an npx cache.
|
|
15
15
|
|
|
16
16
|
Publishing rides the production deploy (`.github/workflows/admin-cli-publish.yml`): bump `version` in `apps/admin-cli/package.json` in a normal PR to `staging`, and it publishes when `staging` promotes to `production`. A production deploy that did not bump the version is a no-op — a `gate` job checks the version against npm first. The same workflow is still dispatchable by hand for out-of-band releases. pnpm packs the CLI so the workspace `catalog:` dependency becomes a real range; npm then publishes that tarball through the workflow's OIDC trusted-publishing path. The registry-side publisher must be configured as described in `docs/codebase/admin-cli.md`.
|
|
17
17
|
|
|
@@ -151,6 +151,8 @@ separate `village models` commands edit the Village island's reusable models and
|
|
|
151
151
|
```bash
|
|
152
152
|
recess --json skills guardian get recess-goal-authoring --all-references
|
|
153
153
|
recess --json content-library search "fractions through visual puzzles" --limit 8
|
|
154
|
+
recess --json content-library status <gem-id-or-url>
|
|
155
|
+
recess --json content-library set-stage <gem-id-or-url...> --stage archived
|
|
154
156
|
recess --json goal-templates validate-spec --file ./template.json # iterate; writes nothing
|
|
155
157
|
recess --json goal-templates create --file ./template.json # preview, exit 2
|
|
156
158
|
recess --json goal-templates create --file ./template.json --confirm
|
|
@@ -165,6 +167,27 @@ recess --json goals create --student <kid-id> --draft <draft-slug> --title "..."
|
|
|
165
167
|
recess --json goals files list --student <kid-id> --goal <goal-id>
|
|
166
168
|
```
|
|
167
169
|
|
|
170
|
+
ADMIN discovery batches use the same Content Library admission door as the dashboard. Omit
|
|
171
|
+
`--confirm` to preview the exact payload first. An interactive run asks for Review or polish with
|
|
172
|
+
Review preselected; JSON/non-interactive runs safely default to Review. Use `--stage polish` to
|
|
173
|
+
start automatic decoration immediately:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
recess --json content-library submit https://example.org/activity --stage review
|
|
177
|
+
recess --json content-library submit --file ./gems.json
|
|
178
|
+
recess --json content-library submit --file ./urls.txt --stage polish --confirm
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
JSON files are arrays of URL strings or `{ "url", "title"?, "summary"?, "lane"? }` objects;
|
|
182
|
+
plain-text files contain one URL per line. The server checks that the deployed island understands
|
|
183
|
+
the review/polish lifecycle before sending any item, and then writes with concurrency three.
|
|
184
|
+
`content-library status` accepts an exact gem ID (including one returned by search) or URL and
|
|
185
|
+
reports its Review/Polishing/Live/Archived stage plus metadata, cover, and search-index progress.
|
|
186
|
+
`content-library set-stage` accepts one or many IDs/URLs (or a newline/JSON-string-array `--file`),
|
|
187
|
+
previews every resolved current stage, and requires `--confirm`. It uses the same lifecycle as
|
|
188
|
+
Manage: direct-to-Live routes unfinished gems through Polishing, and moving out of Polishing
|
|
189
|
+
cancels that exact run first.
|
|
190
|
+
|
|
168
191
|
## Family AI operations
|
|
169
192
|
|
|
170
193
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -130,6 +130,13 @@ Usage:
|
|
|
130
130
|
recess [--json] store-items set-status <village-store-item-id>
|
|
131
131
|
--status ACTIVE|INACTIVE|COMING_SOON [--confirm]
|
|
132
132
|
recess [--json] content-library search <query> [--limit 8]
|
|
133
|
+
recess [--json] content-library status <gem-id-or-url>
|
|
134
|
+
recess [--json] content-library set-stage <gem-id-or-url...> [--file <path>]
|
|
135
|
+
--stage review|polishing|live|archived [--confirm]
|
|
136
|
+
recess [--json] content-library submit <url...> [--file <path>]
|
|
137
|
+
[--stage review|polish] [--title TEXT] [--summary TEXT]
|
|
138
|
+
[--lane web-toys|mechanics|explorables|data-stories|sims|maps-scale|sound-art|puzzles|wonder|idea-games]
|
|
139
|
+
[--confirm]
|
|
133
140
|
recess [--json] skills guardian list [--query TEXT] [--category TEXT]
|
|
134
141
|
recess [--json] skills guardian get <skill-name>
|
|
135
142
|
[--reference NAME | --all-references] [--refresh]
|
|
@@ -189,8 +196,8 @@ Usage:
|
|
|
189
196
|
[--path uploads/name.pdf] [--message TEXT]
|
|
190
197
|
[--confirm --approval-token TOKEN]
|
|
191
198
|
|
|
192
|
-
Authoring notes: "skills" serves the in-product tutor skills (the
|
|
193
|
-
packages/skills
|
|
199
|
+
Authoring notes: "skills" serves the in-product tutor skills (the private
|
|
200
|
+
packages/skills workspace package) read-only over your admin session — they are never
|
|
194
201
|
bundled into this npm package. Load os-v2-goal-template-builder and its
|
|
195
202
|
references/deterministic-workflow-setup.md BEFORE authoring a template; that is
|
|
196
203
|
the same guidance the recess.gg/ai agent follows, so there is exactly one
|
|
@@ -629,6 +636,25 @@ const STORE_ITEM_SORT_FIELDS = [
|
|
|
629
636
|
"updatedAt",
|
|
630
637
|
"order",
|
|
631
638
|
];
|
|
639
|
+
const CONTENT_LIBRARY_STAGES = ["review", "polish"];
|
|
640
|
+
const CONTENT_LIBRARY_RESOURCE_STAGES = [
|
|
641
|
+
"review",
|
|
642
|
+
"polishing",
|
|
643
|
+
"live",
|
|
644
|
+
"archived",
|
|
645
|
+
];
|
|
646
|
+
const CONTENT_LIBRARY_DISCOVERY_LANES = [
|
|
647
|
+
"web-toys",
|
|
648
|
+
"mechanics",
|
|
649
|
+
"explorables",
|
|
650
|
+
"data-stories",
|
|
651
|
+
"sims",
|
|
652
|
+
"maps-scale",
|
|
653
|
+
"sound-art",
|
|
654
|
+
"puzzles",
|
|
655
|
+
"wonder",
|
|
656
|
+
"idea-games",
|
|
657
|
+
];
|
|
632
658
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
633
659
|
/**
|
|
634
660
|
* Read and parse a JSON file supplied by an authoring agent. Large payloads —
|
|
@@ -665,6 +691,215 @@ async function readJsonFile(filePath, label) {
|
|
|
665
691
|
}
|
|
666
692
|
return parsed;
|
|
667
693
|
}
|
|
694
|
+
function contentLibrarySubmitItem(value, label) {
|
|
695
|
+
const record = typeof value === "string"
|
|
696
|
+
? { url: value }
|
|
697
|
+
: value && typeof value === "object" && !Array.isArray(value)
|
|
698
|
+
? value
|
|
699
|
+
: null;
|
|
700
|
+
if (!record) {
|
|
701
|
+
throw new CliError("invalid_arguments", `${label} must be a URL string or an object with url and optional title, summary, and lane.`);
|
|
702
|
+
}
|
|
703
|
+
const allowed = new Set(["url", "title", "summary", "lane"]);
|
|
704
|
+
const unknown = Object.keys(record).filter((key) => !allowed.has(key));
|
|
705
|
+
if (unknown.length) {
|
|
706
|
+
throw new CliError("invalid_arguments", `${label} has unsupported fields: ${unknown.join(", ")}.`);
|
|
707
|
+
}
|
|
708
|
+
const text = (key) => {
|
|
709
|
+
const raw = record[key];
|
|
710
|
+
if (raw === undefined)
|
|
711
|
+
return undefined;
|
|
712
|
+
if (typeof raw !== "string" || !raw.trim()) {
|
|
713
|
+
throw new CliError("invalid_arguments", `${label}.${key} must be a non-empty string.`);
|
|
714
|
+
}
|
|
715
|
+
return raw.trim();
|
|
716
|
+
};
|
|
717
|
+
const rawUrl = text("url");
|
|
718
|
+
if (!rawUrl) {
|
|
719
|
+
throw new CliError("invalid_arguments", `${label}.url is required.`);
|
|
720
|
+
}
|
|
721
|
+
let url;
|
|
722
|
+
try {
|
|
723
|
+
url = new URL(rawUrl);
|
|
724
|
+
}
|
|
725
|
+
catch {
|
|
726
|
+
throw new CliError("invalid_arguments", `${label}.url must be a valid http(s) URL.`);
|
|
727
|
+
}
|
|
728
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
729
|
+
throw new CliError("invalid_arguments", `${label}.url must be a valid http(s) URL.`);
|
|
730
|
+
}
|
|
731
|
+
if (url.toString().length > 2_000) {
|
|
732
|
+
throw new CliError("invalid_arguments", `${label}.url must be at most 2000 characters.`);
|
|
733
|
+
}
|
|
734
|
+
const title = text("title");
|
|
735
|
+
const summary = text("summary");
|
|
736
|
+
const laneRaw = text("lane");
|
|
737
|
+
const lane = laneRaw
|
|
738
|
+
? assertChoice(laneRaw, CONTENT_LIBRARY_DISCOVERY_LANES, `${label}.lane`)
|
|
739
|
+
: undefined;
|
|
740
|
+
if (title && title.length > 500) {
|
|
741
|
+
throw new CliError("invalid_arguments", `${label}.title must be at most 500 characters.`);
|
|
742
|
+
}
|
|
743
|
+
if (summary && summary.length > 4_000) {
|
|
744
|
+
throw new CliError("invalid_arguments", `${label}.summary must be at most 4000 characters.`);
|
|
745
|
+
}
|
|
746
|
+
return {
|
|
747
|
+
url: url.toString(),
|
|
748
|
+
...(title ? { title } : {}),
|
|
749
|
+
...(summary ? { summary } : {}),
|
|
750
|
+
...(lane ? { lane } : {}),
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
async function readContentLibrarySubmitItems(parsed) {
|
|
754
|
+
const items = parsed.positionals
|
|
755
|
+
.slice(2)
|
|
756
|
+
.map((url, index) => contentLibrarySubmitItem(url, `URL ${index + 1}`));
|
|
757
|
+
const filePath = flagString(parsed, "file");
|
|
758
|
+
let file;
|
|
759
|
+
if (filePath) {
|
|
760
|
+
const absolutePath = path.resolve(filePath);
|
|
761
|
+
let raw;
|
|
762
|
+
try {
|
|
763
|
+
raw = await fs.readFile(absolutePath, "utf8");
|
|
764
|
+
}
|
|
765
|
+
catch (error) {
|
|
766
|
+
if (error.code === "ENOENT") {
|
|
767
|
+
throw new CliError("invalid_arguments", `Content Library input file does not exist: ${absolutePath}`);
|
|
768
|
+
}
|
|
769
|
+
throw error;
|
|
770
|
+
}
|
|
771
|
+
const trimmed = raw.trim();
|
|
772
|
+
if (!trimmed) {
|
|
773
|
+
throw new CliError("invalid_arguments", `Content Library input file is empty: ${absolutePath}`);
|
|
774
|
+
}
|
|
775
|
+
let entries;
|
|
776
|
+
if (trimmed.startsWith("[")) {
|
|
777
|
+
let parsedFile;
|
|
778
|
+
try {
|
|
779
|
+
parsedFile = JSON.parse(trimmed);
|
|
780
|
+
}
|
|
781
|
+
catch (error) {
|
|
782
|
+
throw new CliError("invalid_arguments", `Content Library input file is not valid JSON (${absolutePath}): ${error instanceof Error ? error.message : String(error)}`);
|
|
783
|
+
}
|
|
784
|
+
if (!Array.isArray(parsedFile)) {
|
|
785
|
+
throw new CliError("invalid_arguments", `Content Library JSON input must be an array (${absolutePath}).`);
|
|
786
|
+
}
|
|
787
|
+
entries = parsedFile;
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
entries = raw
|
|
791
|
+
.split(/\r?\n/)
|
|
792
|
+
.map((line) => line.trim())
|
|
793
|
+
.filter((line) => line && !line.startsWith("#"));
|
|
794
|
+
}
|
|
795
|
+
items.push(...entries.map((entry, index) => contentLibrarySubmitItem(entry, `File item ${index + 1}`)));
|
|
796
|
+
file = {
|
|
797
|
+
absolutePath,
|
|
798
|
+
sizeBytes: Buffer.byteLength(raw),
|
|
799
|
+
sha256: createHash("sha256").update(raw).digest("hex"),
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
if (items.length === 0) {
|
|
803
|
+
throw new CliError("invalid_arguments", "Pass at least one URL as an argument or through --file.");
|
|
804
|
+
}
|
|
805
|
+
if (items.length > 500) {
|
|
806
|
+
throw new CliError("invalid_arguments", "Content Library submit accepts at most 500 items per confirmed batch.");
|
|
807
|
+
}
|
|
808
|
+
const title = flagString(parsed, "title");
|
|
809
|
+
const summary = flagString(parsed, "summary");
|
|
810
|
+
const laneRaw = flagString(parsed, "lane");
|
|
811
|
+
if ((title || summary || laneRaw) && items.length !== 1) {
|
|
812
|
+
throw new CliError("invalid_arguments", "--title, --summary, and --lane apply to one URL only; use JSON objects in --file for per-item metadata.");
|
|
813
|
+
}
|
|
814
|
+
if (items.length === 1 && (title || summary || laneRaw)) {
|
|
815
|
+
items[0] = contentLibrarySubmitItem({
|
|
816
|
+
...items[0],
|
|
817
|
+
...(title ? { title } : {}),
|
|
818
|
+
...(summary ? { summary } : {}),
|
|
819
|
+
...(laneRaw ? { lane: laneRaw } : {}),
|
|
820
|
+
}, "Submitted item");
|
|
821
|
+
}
|
|
822
|
+
return { items, ...(file ? { file } : {}) };
|
|
823
|
+
}
|
|
824
|
+
function contentLibraryGemTarget(value, label) {
|
|
825
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
826
|
+
throw new CliError("invalid_arguments", `${label} must be an exact resource ID or http(s) URL.`);
|
|
827
|
+
}
|
|
828
|
+
const target = value.trim();
|
|
829
|
+
if (UUID_RE.test(target))
|
|
830
|
+
return target;
|
|
831
|
+
let url;
|
|
832
|
+
try {
|
|
833
|
+
url = new URL(target);
|
|
834
|
+
}
|
|
835
|
+
catch {
|
|
836
|
+
throw new CliError("invalid_arguments", `${label} must be an exact resource ID or http(s) URL. Use content-library search to find a live gem's ID.`);
|
|
837
|
+
}
|
|
838
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
839
|
+
throw new CliError("invalid_arguments", `${label} must be an exact resource ID or http(s) URL.`);
|
|
840
|
+
}
|
|
841
|
+
if (url.toString().length > 2_000) {
|
|
842
|
+
throw new CliError("invalid_arguments", `${label} must be at most 2000 characters.`);
|
|
843
|
+
}
|
|
844
|
+
return url.toString();
|
|
845
|
+
}
|
|
846
|
+
async function readContentLibraryGemTargets(parsed) {
|
|
847
|
+
const gems = parsed.positionals
|
|
848
|
+
.slice(2)
|
|
849
|
+
.map((value, index) => contentLibraryGemTarget(value, `Gem ${index + 1}`));
|
|
850
|
+
const filePath = flagString(parsed, "file");
|
|
851
|
+
let file;
|
|
852
|
+
if (filePath) {
|
|
853
|
+
const absolutePath = path.resolve(filePath);
|
|
854
|
+
let raw;
|
|
855
|
+
try {
|
|
856
|
+
raw = await fs.readFile(absolutePath, "utf8");
|
|
857
|
+
}
|
|
858
|
+
catch (error) {
|
|
859
|
+
if (error.code === "ENOENT") {
|
|
860
|
+
throw new CliError("invalid_arguments", `Content Library gem file does not exist: ${absolutePath}`);
|
|
861
|
+
}
|
|
862
|
+
throw error;
|
|
863
|
+
}
|
|
864
|
+
const trimmed = raw.trim();
|
|
865
|
+
if (!trimmed) {
|
|
866
|
+
throw new CliError("invalid_arguments", `Content Library gem file is empty: ${absolutePath}`);
|
|
867
|
+
}
|
|
868
|
+
let entries;
|
|
869
|
+
if (trimmed.startsWith("[")) {
|
|
870
|
+
let parsedFile;
|
|
871
|
+
try {
|
|
872
|
+
parsedFile = JSON.parse(trimmed);
|
|
873
|
+
}
|
|
874
|
+
catch (error) {
|
|
875
|
+
throw new CliError("invalid_arguments", `Content Library gem file is not valid JSON (${absolutePath}): ${error instanceof Error ? error.message : String(error)}`);
|
|
876
|
+
}
|
|
877
|
+
if (!Array.isArray(parsedFile)) {
|
|
878
|
+
throw new CliError("invalid_arguments", `Content Library gem JSON input must be an array (${absolutePath}).`);
|
|
879
|
+
}
|
|
880
|
+
entries = parsedFile;
|
|
881
|
+
}
|
|
882
|
+
else {
|
|
883
|
+
entries = raw
|
|
884
|
+
.split(/\r?\n/)
|
|
885
|
+
.map((line) => line.trim())
|
|
886
|
+
.filter((line) => line && !line.startsWith("#"));
|
|
887
|
+
}
|
|
888
|
+
gems.push(...entries.map((entry, index) => contentLibraryGemTarget(entry, `File gem ${index + 1}`)));
|
|
889
|
+
file = {
|
|
890
|
+
absolutePath,
|
|
891
|
+
sizeBytes: Buffer.byteLength(raw),
|
|
892
|
+
sha256: createHash("sha256").update(raw).digest("hex"),
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
if (gems.length === 0) {
|
|
896
|
+
throw new CliError("invalid_arguments", "Pass at least one gem ID or URL as an argument or through --file.");
|
|
897
|
+
}
|
|
898
|
+
if (gems.length > 500) {
|
|
899
|
+
throw new CliError("invalid_arguments", "Content Library set-stage accepts at most 500 gems per confirmed batch.");
|
|
900
|
+
}
|
|
901
|
+
return { gems, ...(file ? { file } : {}) };
|
|
902
|
+
}
|
|
668
903
|
async function readGoalTemplateSpecPatches(filePath) {
|
|
669
904
|
const { absolutePath, raw, parsed } = await readJsonValue(filePath, "Goal-template patch file");
|
|
670
905
|
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
@@ -2446,7 +2681,60 @@ export async function runCommand(argv) {
|
|
|
2446
2681
|
params: { query: { q: query, limit } },
|
|
2447
2682
|
}));
|
|
2448
2683
|
}
|
|
2449
|
-
|
|
2684
|
+
if (verb === "status") {
|
|
2685
|
+
const gem = contentLibraryGemTarget(positional(parsed, 2, "gem id or URL"), "Gem");
|
|
2686
|
+
return unwrap(await api.client.GET("/admin/content-library/status", {
|
|
2687
|
+
params: { query: { gem } },
|
|
2688
|
+
}));
|
|
2689
|
+
}
|
|
2690
|
+
if (verb === "set-stage") {
|
|
2691
|
+
const stage = assertChoice(flagString(parsed, "stage", { required: true }), CONTENT_LIBRARY_RESOURCE_STAGES, "--stage");
|
|
2692
|
+
const input = await readContentLibraryGemTargets(parsed);
|
|
2693
|
+
const preflightBody = {
|
|
2694
|
+
gems: input.gems,
|
|
2695
|
+
stage,
|
|
2696
|
+
dryRun: true,
|
|
2697
|
+
};
|
|
2698
|
+
const preflight = unwrap(await api.client.POST("/admin/content-library/transition", {
|
|
2699
|
+
body: preflightBody,
|
|
2700
|
+
}));
|
|
2701
|
+
const preview = {
|
|
2702
|
+
action: "content-library.set-stage",
|
|
2703
|
+
target: { stage, resourceCount: preflight.results.length },
|
|
2704
|
+
request: { gems: input.gems, stage },
|
|
2705
|
+
details: {
|
|
2706
|
+
resources: preflight.results,
|
|
2707
|
+
lifecycle: "Uses the same lifecycle as Manage. Direct-to-Live never bypasses unfinished polishing; leaving Polishing cancels its bound run before moving.",
|
|
2708
|
+
concurrency: 3,
|
|
2709
|
+
...(input.file ? { inputFile: input.file } : {}),
|
|
2710
|
+
},
|
|
2711
|
+
};
|
|
2712
|
+
return writeCommand(parsed, preview, async () => unwrap(await api.client.POST("/admin/content-library/transition", {
|
|
2713
|
+
body: { gems: input.gems, stage, dryRun: false },
|
|
2714
|
+
})));
|
|
2715
|
+
}
|
|
2716
|
+
if (verb === "submit") {
|
|
2717
|
+
const stage = assertChoice(flagString(parsed, "stage") ?? "review", CONTENT_LIBRARY_STAGES, "--stage");
|
|
2718
|
+
const input = await readContentLibrarySubmitItems(parsed);
|
|
2719
|
+
const body = { stage, items: input.items };
|
|
2720
|
+
const preview = {
|
|
2721
|
+
action: "content-library.submit",
|
|
2722
|
+
target: { stage, resourceCount: input.items.length },
|
|
2723
|
+
request: body,
|
|
2724
|
+
details: {
|
|
2725
|
+
admission: stage === "polish"
|
|
2726
|
+
? "Starts automatic decoration; the island promotes each successful resource to LIVE after the polish and deterministic tail finish."
|
|
2727
|
+
: "Holds each new resource in REVIEW until an admin approves it.",
|
|
2728
|
+
duplicateBehavior: "Existing URLs are returned as duplicates and are not overwritten.",
|
|
2729
|
+
deployOrderFence: "The server verifies the island's review/polish lifecycle capability before its first write.",
|
|
2730
|
+
...(input.file ? { inputFile: input.file } : {}),
|
|
2731
|
+
},
|
|
2732
|
+
};
|
|
2733
|
+
return writeCommand(parsed, preview, async () => unwrap(await api.client.POST("/admin/content-library/submit", {
|
|
2734
|
+
body,
|
|
2735
|
+
})));
|
|
2736
|
+
}
|
|
2737
|
+
throw new CliError("invalid_arguments", "Use content-library search, status, set-stage, or submit.");
|
|
2450
2738
|
}
|
|
2451
2739
|
if (noun === "goal-templates") {
|
|
2452
2740
|
if (verb === "list") {
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { parseArgs } from "./args.js";
|
|
2
4
|
import { runCommand } from "./cli.js";
|
|
3
5
|
import { CliError } from "./errors.js";
|
|
4
|
-
|
|
6
|
+
async function withInteractiveAdmissionStage(args) {
|
|
7
|
+
const parsed = parseArgs(args);
|
|
8
|
+
if (parsed.flags.has("json") ||
|
|
9
|
+
parsed.flags.has("help") ||
|
|
10
|
+
parsed.flags.has("stage") ||
|
|
11
|
+
parsed.positionals[0] !== "content-library" ||
|
|
12
|
+
parsed.positionals[1] !== "submit" ||
|
|
13
|
+
!process.stdin.isTTY ||
|
|
14
|
+
!process.stderr.isTTY) {
|
|
15
|
+
return args;
|
|
16
|
+
}
|
|
17
|
+
const terminal = createInterface({
|
|
18
|
+
input: process.stdin,
|
|
19
|
+
output: process.stderr,
|
|
20
|
+
terminal: true,
|
|
21
|
+
});
|
|
22
|
+
try {
|
|
23
|
+
while (true) {
|
|
24
|
+
const answer = (await terminal.question("Admission stage [Review/polish] (Review): "))
|
|
25
|
+
.trim()
|
|
26
|
+
.toLowerCase();
|
|
27
|
+
if (!answer || answer === "r" || answer === "review") {
|
|
28
|
+
return [...args, "--stage", "review"];
|
|
29
|
+
}
|
|
30
|
+
if (answer === "p" || answer === "polish") {
|
|
31
|
+
return [...args, "--stage", "polish"];
|
|
32
|
+
}
|
|
33
|
+
process.stderr.write("Choose Review or polish.\n");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
terminal.close();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const argv = await withInteractiveAdmissionStage(process.argv.slice(2));
|
|
5
41
|
const json = argv.includes("--json");
|
|
6
42
|
// The interactive console owns the terminal, so it runs before the JSON
|
|
7
43
|
// envelope machinery rather than through it.
|
package/dist/skills-cache.js
CHANGED
|
@@ -6,12 +6,12 @@ import { defaultConfigPath } from "./config.js";
|
|
|
6
6
|
* On-disk cache for tutor skills fetched from `GET /admin/skills/*`.
|
|
7
7
|
*
|
|
8
8
|
* The skills are proprietary and live in the private `packages/skills`
|
|
9
|
-
*
|
|
9
|
+
* workspace package; `recess-cli` is published PUBLIC on npm, so they are fetched at
|
|
10
10
|
* runtime and never bundled. The cache exists so an authoring loop that reads
|
|
11
11
|
* `os-v2-goal-template-builder` on every step does not re-fetch a ~40 KB
|
|
12
12
|
* document each time — it is a latency cache, not a distribution channel:
|
|
13
13
|
* entries live under the operator's 0700 config directory, and `--refresh`
|
|
14
|
-
* bypasses them so a skills
|
|
14
|
+
* bypasses them so a newly deployed skills update is always one flag away.
|
|
15
15
|
*
|
|
16
16
|
* Deliberately short-lived. A stale authoring standard is the failure this whole
|
|
17
17
|
* design exists to prevent, so an entry older than the TTL is simply re-fetched.
|