recess-cli 1.9.1 → 2.0.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 +82 -55
- package/dist/api.js +42 -10
- package/dist/args.js +12 -1
- package/dist/auth.js +17 -12
- package/dist/cli.js +1312 -241
- package/dist/command-schema.js +257 -0
- package/dist/config.js +116 -19
- package/dist/delivery.js +26 -0
- package/dist/feedback.js +64 -0
- package/dist/http.js +25 -0
- package/dist/index.js +15 -3
- package/dist/jobs.js +80 -0
- package/dist/skill-update.js +2 -1
- package/dist/ui/index.js +2 -1
- package/package.json +1 -1
- package/skill/recess-cli/SKILL.md +18 -8
- package/skill/recess-cli/agents/version.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Recess CLI
|
|
2
2
|
|
|
3
|
-
`recess-cli` is the typed, agent-friendly command layer for Recess operations. ADMIN accounts receive the full staff surface; GUARDIAN accounts with `access:ai` receive family-scoped class schedules, progress, goals, todos, memories, Rocky configuration, learning research, GoalTemplate, and goal-content commands; GUIDE accounts receive that same student surface for the students they hold an ACTIVE tutor assignment to — not their wider class roster. It uses the web-server OpenAPI document, authenticates through Recess SSO, emits stable JSON, and refuses live writes until the exact command is rerun with `--confirm` after human approval.
|
|
3
|
+
`recess-cli` is the typed, agent-friendly command layer for Recess operations. ADMIN accounts receive the full staff surface; GUARDIAN accounts with `access:ai` receive family-scoped class schedules, progress, goals, todos, memories, Rocky configuration, learning research, GoalTemplate, and goal-content commands; GUIDE accounts receive that same student surface for the students they hold an ACTIVE tutor assignment to — not their wider class roster. It uses the web-server OpenAPI document, authenticates through Recess SSO, emits stable JSON, and refuses live writes until the exact command is rerun with `--confirm` plus the preview's operation key after human approval.
|
|
4
4
|
|
|
5
5
|
## Install (no checkout needed)
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ Published to npm as [`recess-cli`](https://www.npmjs.com/package/recess-cli). On
|
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npm install -g recess-cli
|
|
11
|
-
recess setup
|
|
11
|
+
recess setup --reason "Install and update the Recess agent skill"
|
|
12
12
|
```
|
|
13
13
|
|
|
14
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.
|
|
@@ -44,7 +44,7 @@ The browser SSO assertion is exchanged once and discarded. The CLI stores a sepa
|
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
46
|
recess --json auth login
|
|
47
|
-
recess --json doctor
|
|
47
|
+
recess --json doctor --reason "Verify CLI connectivity, identity, and scope"
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
## Testing against a local server
|
|
@@ -54,7 +54,7 @@ recess --json doctor
|
|
|
54
54
|
```bash
|
|
55
55
|
export RECESS_CLI_API_ORIGIN=http://localhost:5068
|
|
56
56
|
export RECESS_CLI_COOKIE='recess.auth-token=<signed-value>'
|
|
57
|
-
recess --json doctor
|
|
57
|
+
recess --json doctor --reason "Verify the local Recess API connection"
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
To mint `<signed-value>`: sign `{sub, role, cliScope}` with the server's `JWT_SECRET` (audience = `CLIENT_ORIGIN`), then sign THAT string with `cookie.signerFactory(COOKIE_SECRET)` from `@fastify/cookie`. Two traps, both silent:
|
|
@@ -97,32 +97,42 @@ Error or write preview:
|
|
|
97
97
|
"error": {
|
|
98
98
|
"code": "confirmation_required",
|
|
99
99
|
"message": "...",
|
|
100
|
-
"details": { "preview": {
|
|
100
|
+
"details": { "preview": { "details": { "operationKey": "..." } } }
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
Exit code `0` means success, `1` means an input/auth/API failure, and `2` means a write is awaiting explicit human confirmation.
|
|
106
106
|
|
|
107
|
+
`recess --json agent-context` returns the canonical command/flag/positional schema. `recess --json help payout recipients` returns scoped help. Unknown flags, duplicate non-repeatable flags, missing values, and extra positionals are errors instead of being silently ignored.
|
|
108
|
+
|
|
109
|
+
Every command-driven request to the Recess API except `auth` requires `--reason "..."`: a
|
|
110
|
+
non-empty, human-readable purpose of at most 1024 characters. The CLI sends it as
|
|
111
|
+
`x-recess-reason`; the server rejects missing reasons before route execution and stores one audit
|
|
112
|
+
row per request. The human-only `recess ui` console is explicitly exempt and identifies itself as
|
|
113
|
+
`x-recess-client: cli-ui` instead.
|
|
114
|
+
|
|
107
115
|
## Common flow
|
|
108
116
|
|
|
109
117
|
```bash
|
|
110
|
-
recess --json users search "Morgan Rivera"
|
|
111
|
-
recess --json users tier get <kid-id>
|
|
112
|
-
recess --json users tier preview <kid-id> --tier lite --slots 1
|
|
113
|
-
recess --json students upload-map-scores --student <kid-id> --file /path/to/map-report.pdf
|
|
114
|
-
recess --json enrollments list --user <kid-id>
|
|
115
|
-
recess --json subscriptions list --family <family-id> --kid <kid-id>
|
|
116
|
-
recess --json invoices list --subscription <subscription-id>
|
|
118
|
+
recess --json users search "Morgan Rivera" --reason "Find the exact student record"
|
|
119
|
+
recess --json users tier get <kid-id> --reason "Inspect the student's current tier"
|
|
120
|
+
recess --json users tier preview <kid-id> --tier lite --slots 1 --reason "Preview a tier change"
|
|
121
|
+
recess --json students upload-map-scores --student <kid-id> --file /path/to/map-report.pdf --reason "Import this student's MAP scores"
|
|
122
|
+
recess --json enrollments list --user <kid-id> --reason "Inspect the student's enrollments"
|
|
123
|
+
recess --json subscriptions list --family <family-id> --kid <kid-id> --reason "Inspect family subscriptions"
|
|
124
|
+
recess --json invoices list --subscription <subscription-id> --reason "Inspect subscription invoices"
|
|
117
125
|
```
|
|
118
126
|
|
|
119
127
|
Preview a write by omitting `--confirm`:
|
|
120
128
|
|
|
121
129
|
```bash
|
|
122
|
-
recess --json billing pause --subscription <subscription-id> --until 2026-09-01
|
|
130
|
+
recess --json billing pause --subscription <subscription-id> --until 2026-09-01 --reason "Pause billing through September 1"
|
|
123
131
|
```
|
|
124
132
|
|
|
125
|
-
After a human approves that exact preview, rerun the unchanged command with `--confirm
|
|
133
|
+
After a human approves that exact preview, rerun the unchanged command with `--confirm --operation-key <operationKey>`. File-backed family edits, deterministic template apply, and goal-content writes also return `details.approvalToken`; echo it with `--approval-token TOKEN`. The token covers the local bytes plus the server preflight/CAS state, so a changed file or goal produces a new preview instead of consuming stale approval. Confirmed requests are fenced server-side per actor, operation, route/body, and preview fingerprint; retry an interrupted command with the same operation key to replay a completed response rather than duplicate the write. `jobs list|get` reads the protected local recovery ledger.
|
|
134
|
+
|
|
135
|
+
Named `profile save|use|list` configurations keep environment selection explicit, with global `--profile` as a one-command override. `--deliver file:<path>` atomically writes the JSON envelope with mode 0600; webhook delivery is intentionally unavailable for authenticated Recess data. `feedback submit` stores deduplicated CLI friction locally and can forward it to `RECESS_CLI_FEEDBACK_ENDPOINT`.
|
|
126
136
|
|
|
127
137
|
School tier writes require the `updatedAt` token from `users tier get`. Their read-only preflight
|
|
128
138
|
shows `capabilitiesLockedNow`/`capabilitiesLockedAfter`, the resolved class allowance, current
|
|
@@ -137,34 +147,45 @@ Village store goods are main-Recess `StoreItem` rows, not Village-island databas
|
|
|
137
147
|
and preview an availability change with:
|
|
138
148
|
|
|
139
149
|
```bash
|
|
140
|
-
recess --json store-items list --item-type VILLAGE_ITEM
|
|
141
|
-
recess --json store-items set-status <store-item-id> --status INACTIVE
|
|
150
|
+
recess --json store-items list --item-type VILLAGE_ITEM --reason "Inspect the Village item catalog"
|
|
151
|
+
recess --json store-items set-status <store-item-id> --status INACTIVE --reason "Deactivate this Village item"
|
|
142
152
|
```
|
|
143
153
|
|
|
144
154
|
The status command resolves the exact `VILLAGE_ITEM` first and includes its name, catalog metadata,
|
|
145
155
|
current status, purchase count, and lack of feed-publication side effects in the approval preview;
|
|
146
|
-
it refuses non-Village item IDs. After approval, rerun the unchanged command with `--confirm
|
|
156
|
+
it refuses non-Village item IDs. After approval, rerun the unchanged command with `--confirm` and
|
|
157
|
+
the preview's `--operation-key`. The
|
|
147
158
|
separate `village models` commands edit the Village island's reusable models and placements.
|
|
148
159
|
|
|
149
160
|
## Authoring learning content
|
|
150
161
|
|
|
151
162
|
```bash
|
|
152
|
-
recess --json skills guardian get recess-goal-authoring --all-references
|
|
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
|
|
156
|
-
recess --json goal-templates validate-spec --file ./template.json
|
|
157
|
-
recess --json goal-templates create --file ./template.json
|
|
158
|
-
recess --json goal-templates create --file ./template.json --confirm
|
|
159
|
-
|
|
160
|
-
recess --json
|
|
161
|
-
recess --json goals
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
recess --json goals
|
|
166
|
-
|
|
167
|
-
recess --json
|
|
163
|
+
recess --json skills guardian get recess-goal-authoring --all-references --reason "Load goal authoring guidance"
|
|
164
|
+
recess --json content-library search "fractions through visual puzzles" --limit 8 --reason "Find visual fraction resources"
|
|
165
|
+
recess --json content-library status <gem-id-or-url> --reason "Inspect this gem's pipeline status"
|
|
166
|
+
recess --json content-library set-stage <gem-id-or-url...> --stage archived --reason "Archive these gems"
|
|
167
|
+
recess --json goal-templates validate-spec --file ./template.json --reason "Validate this template draft"
|
|
168
|
+
recess --json goal-templates create --file ./template.json --reason "Create this reusable goal template"
|
|
169
|
+
recess --json goal-templates create --file ./template.json --confirm \
|
|
170
|
+
--operation-key <preview-operation-key> --reason "Create this reusable goal template"
|
|
171
|
+
recess --json goal-templates patch-spec <id-or-slug> --expected-version 7 --patches-file ./patches.json --reason "Update this template specification"
|
|
172
|
+
recess --json goals files init --student <kid-id> --draft <draft-slug> \
|
|
173
|
+
--output-dir ./goal-content --reason "Start this goal draft"
|
|
174
|
+
git -C ./goal-content add -A
|
|
175
|
+
git -C ./goal-content commit -m "Author the learning path"
|
|
176
|
+
recess --json goals files push --source-dir ./goal-content --reason "Publish this goal draft"
|
|
177
|
+
recess --json goals pdf upload --student <kid-id> --draft <draft-slug> --source-file ./textbook.pdf --reason "Attach this textbook to the goal draft"
|
|
178
|
+
recess --json goal-templates capture-snapshot <id-or-slug> --source-dir ./goal-content --dry-run --reason "Preview a template snapshot"
|
|
179
|
+
recess --json goal-templates apply <id-or-slug> --answers-file ./answers.json --dry-run --reason "Preview applying this template"
|
|
180
|
+
recess --json goals create --student <kid-id> --title "..." --description-file ./goal.md --reason "Create this student goal"
|
|
181
|
+
recess --json goals create --source-dir ./goal-content --title "..." \
|
|
182
|
+
--description-file ./goal.md --enable-applet-follow-ups --reason "Create this student goal draft"
|
|
183
|
+
recess --json goals files list --student <kid-id> --goal <goal-id> --reason "Inspect the goal workspace files"
|
|
184
|
+
recess --json goals files checkout --student <kid-id> --goal <goal-id> \
|
|
185
|
+
--output-dir ./goal-content --reason "Check out this goal for editing"
|
|
186
|
+
git -C ./goal-content add -A
|
|
187
|
+
git -C ./goal-content commit -m "Revise module 3"
|
|
188
|
+
recess --json goals files push --source-dir ./goal-content --reason "Publish the module revision"
|
|
168
189
|
```
|
|
169
190
|
|
|
170
191
|
ADMIN discovery batches use the same Content Library admission door as the dashboard. Omit
|
|
@@ -173,9 +194,10 @@ Review preselected; JSON/non-interactive runs safely default to Review. Use `--s
|
|
|
173
194
|
start automatic decoration immediately:
|
|
174
195
|
|
|
175
196
|
```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
|
|
197
|
+
recess --json content-library submit https://example.org/activity --stage review --reason "Submit this activity for review"
|
|
198
|
+
recess --json content-library submit --file ./gems.json --reason "Submit this resource batch for review"
|
|
199
|
+
recess --json content-library submit --file ./urls.txt --stage polish --confirm \
|
|
200
|
+
--operation-key <preview-operation-key> --reason "Submit this batch for polishing"
|
|
179
201
|
```
|
|
180
202
|
|
|
181
203
|
JSON files are arrays of URL strings or `{ "url", "title"?, "summary"?, "lane"? }` objects;
|
|
@@ -186,21 +208,24 @@ reports its Review/Polishing/Live/Archived stage plus metadata, cover, and searc
|
|
|
186
208
|
`content-library set-stage` accepts one or many IDs/URLs (or a newline/JSON-string-array `--file`),
|
|
187
209
|
previews every resolved current stage, and requires `--confirm`. It uses the same lifecycle as
|
|
188
210
|
Manage: direct-to-Live routes unfinished gems through Polishing, and moving out of Polishing
|
|
189
|
-
cancels that exact run first.
|
|
211
|
+
cancels that exact run first. Add `--wait --timeout 900` when promoting to Live (or submitting to
|
|
212
|
+
polish) to poll the durable island status with bounded concurrency and exponential backoff.
|
|
190
213
|
|
|
191
214
|
## Family AI operations
|
|
192
215
|
|
|
193
216
|
```bash
|
|
194
|
-
recess --json students list
|
|
195
|
-
recess --json students today --student <kid-id>
|
|
196
|
-
recess --json students schedule --student <kid-id> --days 30
|
|
197
|
-
recess --json students xp-history --student <kid-id> --range month
|
|
198
|
-
recess --json goals list --student <kid-id>
|
|
199
|
-
recess --json todos create --student <kid-id> --title "Read chapter 4"
|
|
200
|
-
recess --json
|
|
201
|
-
recess --json
|
|
202
|
-
recess --json
|
|
203
|
-
recess --json
|
|
217
|
+
recess --json students list --reason "List the students I can support"
|
|
218
|
+
recess --json students today --student <kid-id> --reason "Review today's learning plan"
|
|
219
|
+
recess --json students schedule --student <kid-id> --days 30 --reason "Review the student's upcoming schedule"
|
|
220
|
+
recess --json students xp-history --student <kid-id> --range month --reason "Review recent XP history"
|
|
221
|
+
recess --json goals list --student <kid-id> --reason "Review the student's goals"
|
|
222
|
+
recess --json todos create --student <kid-id> --title "Read chapter 4" --reason "Add the assigned reading"
|
|
223
|
+
recess --json goals delete <goal-id> --student <kid-id> --reason "Remove this obsolete goal"
|
|
224
|
+
recess --json todos delete <todo-id> --reason "Remove this disposable todo"
|
|
225
|
+
recess --json todos generate-applet <todo-id> --student <kid-id> --reason "Generate this todo's applet"
|
|
226
|
+
recess --json memories context --student <kid-id> --reason "Review durable tutor context"
|
|
227
|
+
recess --json memories log --student <kid-id> --date 2026-08-12 --reason "Review the learning log for this date"
|
|
228
|
+
recess --json rocky get --student <kid-id> --reason "Inspect the student's Rocky configuration"
|
|
204
229
|
```
|
|
205
230
|
|
|
206
231
|
All family writes still preview first. Goal/todo/Rocky edits also carry the current server version
|
|
@@ -216,8 +241,8 @@ not expose Postgres `UserMemory`.
|
|
|
216
241
|
|
|
217
242
|
```bash
|
|
218
243
|
recess --json --version # {cliVersion, skillVersion}
|
|
219
|
-
recess --json doctor
|
|
220
|
-
recess --json setup --skill-only
|
|
244
|
+
recess --json doctor --reason "Check CLI health" # .skill reports whether a newer bundle exists
|
|
245
|
+
recess --json setup --skill-only --reason "Update the installed Recess skill"
|
|
221
246
|
```
|
|
222
247
|
|
|
223
248
|
The CLI's small shared router skill is **both** bundled in this package and served from
|
|
@@ -243,11 +268,13 @@ previews the per-student outcome. `set-metadata` and `delete` require `--expecte
|
|
|
243
268
|
it always runs the backend's guarded preview first and requires the preview's exact loss token in
|
|
244
269
|
addition to `--confirm` when protected template data would be removed.
|
|
245
270
|
|
|
246
|
-
Module-backed content
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
271
|
+
Module-backed content starts with `goals files init --draft`, which creates a normal local Git
|
|
272
|
+
repository pinned to the student's Mesa revision. Edit, rename, delete, inspect, and commit with
|
|
273
|
+
ordinary Git, then use `goals files push`. The same checkout can become a personal goal through
|
|
274
|
+
`goals create --source-dir` or a reusable BLUEPRINT snapshot through
|
|
275
|
+
`goal-templates capture-snapshot --source-dir`; successful goal creation retargets its metadata to
|
|
276
|
+
the new live goal for continued pushes. Existing live goals use `checkout --goal`. Every push
|
|
277
|
+
previews the committed range as one Mesa change and refuses a stale remote tip. The direct
|
|
278
|
+
`goals files write` upsert remains available for small or automated writes.
|
|
252
279
|
|
|
253
280
|
See `recess --help` for the complete command surface. The raw escape hatch is intentionally read-only: `recess --json request get /path`.
|
package/dist/api.js
CHANGED
|
@@ -1,16 +1,42 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import createClient from "openapi-fetch";
|
|
2
3
|
import { apiError, CliError } from "./errors.js";
|
|
4
|
+
import { cliRequestHeaders, markCliRequest, RECESS_CLIENT_CLI, } from "./http.js";
|
|
5
|
+
const idempotencyContext = new AsyncLocalStorage();
|
|
6
|
+
export function withIdempotencyContext(context, execute) {
|
|
7
|
+
return idempotencyContext.run(context, execute);
|
|
8
|
+
}
|
|
9
|
+
function applyIdempotencyHeaders(headers) {
|
|
10
|
+
const context = idempotencyContext.getStore();
|
|
11
|
+
if (!context)
|
|
12
|
+
return;
|
|
13
|
+
headers.set("x-recess-operation-key", context.operationKey);
|
|
14
|
+
headers.set("x-recess-operation-fingerprint", context.fingerprint);
|
|
15
|
+
}
|
|
3
16
|
export class RecessAdminApi {
|
|
4
17
|
config;
|
|
18
|
+
reason;
|
|
19
|
+
clientTag;
|
|
5
20
|
client;
|
|
6
|
-
constructor(config) {
|
|
21
|
+
constructor(config, reason, clientTag = RECESS_CLIENT_CLI) {
|
|
7
22
|
this.config = config;
|
|
23
|
+
this.reason = reason;
|
|
24
|
+
this.clientTag = clientTag;
|
|
8
25
|
this.client = createClient({
|
|
9
26
|
baseUrl: config.apiOrigin,
|
|
10
27
|
headers: config.sessionCookie
|
|
11
28
|
? { cookie: config.sessionCookie }
|
|
12
29
|
: undefined,
|
|
13
30
|
});
|
|
31
|
+
const requestReason = this.reason;
|
|
32
|
+
const requestClientTag = this.clientTag;
|
|
33
|
+
this.client.use({
|
|
34
|
+
onRequest({ request }) {
|
|
35
|
+
markCliRequest(request.headers, requestReason, requestClientTag);
|
|
36
|
+
applyIdempotencyHeaders(request.headers);
|
|
37
|
+
return request;
|
|
38
|
+
},
|
|
39
|
+
});
|
|
14
40
|
}
|
|
15
41
|
requireAuth() {
|
|
16
42
|
if (!this.config.sessionCookie) {
|
|
@@ -23,7 +49,7 @@ export class RecessAdminApi {
|
|
|
23
49
|
throw new CliError("invalid_arguments", "Raw request paths must start with one '/'.");
|
|
24
50
|
}
|
|
25
51
|
const response = await fetch(new URL(path, this.config.apiOrigin), {
|
|
26
|
-
headers: { cookie: this.config.sessionCookie },
|
|
52
|
+
headers: cliRequestHeaders({ cookie: this.config.sessionCookie }, this.reason, this.clientTag),
|
|
27
53
|
});
|
|
28
54
|
const text = await response.text();
|
|
29
55
|
let body = text;
|
|
@@ -39,14 +65,16 @@ export class RecessAdminApi {
|
|
|
39
65
|
}
|
|
40
66
|
async villageRequest(path, options = {}) {
|
|
41
67
|
this.requireAuth();
|
|
68
|
+
const headers = cliRequestHeaders({
|
|
69
|
+
cookie: this.config.sessionCookie,
|
|
70
|
+
...(options.body === undefined
|
|
71
|
+
? {}
|
|
72
|
+
: { "content-type": "application/json" }),
|
|
73
|
+
}, this.reason, this.clientTag);
|
|
74
|
+
applyIdempotencyHeaders(headers);
|
|
42
75
|
const response = await fetch(new URL(path, this.config.apiOrigin), {
|
|
43
76
|
method: options.method ?? "GET",
|
|
44
|
-
headers
|
|
45
|
-
cookie: this.config.sessionCookie,
|
|
46
|
-
...(options.body === undefined
|
|
47
|
-
? {}
|
|
48
|
-
: { "content-type": "application/json" }),
|
|
49
|
-
},
|
|
77
|
+
headers,
|
|
50
78
|
...(options.body === undefined
|
|
51
79
|
? {}
|
|
52
80
|
: { body: JSON.stringify(options.body) }),
|
|
@@ -72,9 +100,11 @@ export class RecessAdminApi {
|
|
|
72
100
|
}
|
|
73
101
|
const arrayBuffer = file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
|
|
74
102
|
form.set("file", new Blob([arrayBuffer], { type: "model/gltf-binary" }), fileName);
|
|
103
|
+
const headers = cliRequestHeaders({ cookie: this.config.sessionCookie }, this.reason, this.clientTag);
|
|
104
|
+
applyIdempotencyHeaders(headers);
|
|
75
105
|
const response = await fetch(new URL(`/admin/village/models/upload?worldId=${encodeURIComponent(worldId)}`, this.config.apiOrigin), {
|
|
76
106
|
method: "POST",
|
|
77
|
-
headers
|
|
107
|
+
headers,
|
|
78
108
|
body: form,
|
|
79
109
|
});
|
|
80
110
|
const text = await response.text();
|
|
@@ -93,9 +123,11 @@ export class RecessAdminApi {
|
|
|
93
123
|
this.requireAuth();
|
|
94
124
|
const formData = new FormData();
|
|
95
125
|
formData.append("file", new Blob([pdf], { type: "application/pdf" }), fileName);
|
|
126
|
+
const headers = cliRequestHeaders({ cookie: this.config.sessionCookie }, this.reason, this.clientTag);
|
|
127
|
+
applyIdempotencyHeaders(headers);
|
|
96
128
|
const response = await fetch(new URL(`/tutor/students/${encodeURIComponent(studentId)}/map-test-scores/upload`, this.config.apiOrigin), {
|
|
97
129
|
method: "POST",
|
|
98
|
-
headers
|
|
130
|
+
headers,
|
|
99
131
|
body: formData,
|
|
100
132
|
});
|
|
101
133
|
const text = await response.text();
|
package/dist/args.js
CHANGED
|
@@ -10,6 +10,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
10
10
|
"allow-strand",
|
|
11
11
|
"cancel-subscriptions",
|
|
12
12
|
"archived",
|
|
13
|
+
"confirm-destructive-changes",
|
|
13
14
|
"confirm",
|
|
14
15
|
"dry-run",
|
|
15
16
|
"disable-applet-follow-ups",
|
|
@@ -19,20 +20,28 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
19
20
|
"immediate",
|
|
20
21
|
"include-deleted",
|
|
21
22
|
"mirrored",
|
|
23
|
+
"no-invite",
|
|
22
24
|
"no-collision",
|
|
23
25
|
"json",
|
|
24
26
|
"refresh",
|
|
25
27
|
"restore",
|
|
26
28
|
"revoke",
|
|
27
29
|
"send-email",
|
|
30
|
+
"skill-only",
|
|
28
31
|
"spec-only",
|
|
29
32
|
"starter-only",
|
|
30
33
|
"visual-only",
|
|
34
|
+
"version",
|
|
35
|
+
"wait",
|
|
31
36
|
]);
|
|
32
37
|
export function parseArgs(args) {
|
|
33
38
|
const positionals = [];
|
|
34
39
|
const flags = new Map();
|
|
35
40
|
const repeated = new Map();
|
|
41
|
+
const occurrences = new Map();
|
|
42
|
+
const markSeen = (name) => {
|
|
43
|
+
occurrences.set(name, (occurrences.get(name) ?? 0) + 1);
|
|
44
|
+
};
|
|
36
45
|
const record = (name, value) => {
|
|
37
46
|
if (!REPEATABLE_FLAGS.has(name))
|
|
38
47
|
return;
|
|
@@ -52,11 +61,13 @@ export function parseArgs(args) {
|
|
|
52
61
|
if (equalsAt > 2) {
|
|
53
62
|
const name = value.slice(2, equalsAt);
|
|
54
63
|
const flagValue = value.slice(equalsAt + 1);
|
|
64
|
+
markSeen(name);
|
|
55
65
|
flags.set(name, flagValue);
|
|
56
66
|
record(name, flagValue);
|
|
57
67
|
continue;
|
|
58
68
|
}
|
|
59
69
|
const name = value.slice(2);
|
|
70
|
+
markSeen(name);
|
|
60
71
|
if (BOOLEAN_FLAGS.has(name)) {
|
|
61
72
|
flags.set(name, true);
|
|
62
73
|
continue;
|
|
@@ -71,7 +82,7 @@ export function parseArgs(args) {
|
|
|
71
82
|
flags.set(name, true);
|
|
72
83
|
}
|
|
73
84
|
}
|
|
74
|
-
return { positionals, flags, repeated };
|
|
85
|
+
return { positionals, flags, repeated, occurrences };
|
|
75
86
|
}
|
|
76
87
|
/**
|
|
77
88
|
* Every value given for a repeatable flag, in order.
|
package/dist/auth.js
CHANGED
|
@@ -3,13 +3,18 @@ import { createServer } from "node:http";
|
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { clearPendingDeviceAuth, updateStoredConfig } from "./config.js";
|
|
5
5
|
import { apiError, CliError } from "./errors.js";
|
|
6
|
+
import { cliRequestHeaders } from "./http.js";
|
|
6
7
|
function openBrowser(url) {
|
|
8
|
+
// Windows goes through rundll32, not `cmd /c start`: cmd treats `&` (and the
|
|
9
|
+
// `%`-encoded redirect_uri) as metacharacters and chops the URL at the first
|
|
10
|
+
// `&`, so the browser only ever received `?client_id=…` and the OAuth page
|
|
11
|
+
// rejected it. rundll32 takes the URL as a single, unparsed argument.
|
|
7
12
|
const command = process.platform === "darwin"
|
|
8
13
|
? "open"
|
|
9
14
|
: process.platform === "win32"
|
|
10
|
-
? "
|
|
15
|
+
? "rundll32"
|
|
11
16
|
: "xdg-open";
|
|
12
|
-
const args = process.platform === "win32" ? ["
|
|
17
|
+
const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
13
18
|
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
14
19
|
child.unref();
|
|
15
20
|
}
|
|
@@ -72,7 +77,7 @@ export async function login(config, options) {
|
|
|
72
77
|
const assertion = await callback;
|
|
73
78
|
const response = await fetch(new URL("/auth/admin-cli/exchange/", config.apiOrigin), {
|
|
74
79
|
method: "POST",
|
|
75
|
-
headers: { authorization: `Bearer ${assertion}` },
|
|
80
|
+
headers: cliRequestHeaders({ authorization: `Bearer ${assertion}` }),
|
|
76
81
|
});
|
|
77
82
|
const body = (await response.json());
|
|
78
83
|
if (!response.ok)
|
|
@@ -89,7 +94,7 @@ export async function login(config, options) {
|
|
|
89
94
|
sessionCookie: pair,
|
|
90
95
|
sessionExpiresAt: exchange.expiresAt,
|
|
91
96
|
user: exchange.user,
|
|
92
|
-
});
|
|
97
|
+
}, config.profileName);
|
|
93
98
|
return exchange;
|
|
94
99
|
}
|
|
95
100
|
// Start the headless (device-authorization) sign-in. Non-blocking: it stores the secret
|
|
@@ -98,7 +103,7 @@ export async function login(config, options) {
|
|
|
98
103
|
export async function requestDeviceAuth(config, options) {
|
|
99
104
|
const response = await fetch(new URL("/auth/admin-cli/device/authorize/", config.apiOrigin), {
|
|
100
105
|
method: "POST",
|
|
101
|
-
headers: { "content-type": "application/json" },
|
|
106
|
+
headers: cliRequestHeaders({ "content-type": "application/json" }),
|
|
102
107
|
body: JSON.stringify(options.label ? { label: options.label } : {}),
|
|
103
108
|
});
|
|
104
109
|
const body = (await response.json());
|
|
@@ -112,7 +117,7 @@ export async function requestDeviceAuth(config, options) {
|
|
|
112
117
|
pendingUserCode: authorize.userCode,
|
|
113
118
|
pendingApprovalUrl: authorize.approvalUrl,
|
|
114
119
|
pendingExpiresAt: authorize.expiresAt,
|
|
115
|
-
});
|
|
120
|
+
}, config.profileName);
|
|
116
121
|
return {
|
|
117
122
|
approvalUrl: authorize.approvalUrl,
|
|
118
123
|
userCode: authorize.userCode,
|
|
@@ -138,12 +143,12 @@ export async function pollDeviceAuth(config, options) {
|
|
|
138
143
|
for (;;) {
|
|
139
144
|
const response = await fetch(new URL("/auth/admin-cli/device/token/", config.apiOrigin), {
|
|
140
145
|
method: "POST",
|
|
141
|
-
headers: { "content-type": "application/json" },
|
|
146
|
+
headers: cliRequestHeaders({ "content-type": "application/json" }),
|
|
142
147
|
body: JSON.stringify({ deviceCode }),
|
|
143
148
|
});
|
|
144
149
|
const body = (await response.json());
|
|
145
150
|
if (!response.ok) {
|
|
146
|
-
await clearPendingDeviceAuth();
|
|
151
|
+
await clearPendingDeviceAuth(config.profileName);
|
|
147
152
|
throw apiError(response.status, body);
|
|
148
153
|
}
|
|
149
154
|
const token = body;
|
|
@@ -163,16 +168,16 @@ export async function pollDeviceAuth(config, options) {
|
|
|
163
168
|
sessionCookie: pair,
|
|
164
169
|
sessionExpiresAt: exchange.expiresAt,
|
|
165
170
|
user: exchange.user,
|
|
166
|
-
});
|
|
167
|
-
await clearPendingDeviceAuth();
|
|
171
|
+
}, config.profileName);
|
|
172
|
+
await clearPendingDeviceAuth(config.profileName);
|
|
168
173
|
return exchange;
|
|
169
174
|
}
|
|
170
175
|
if (token.status === "denied") {
|
|
171
|
-
await clearPendingDeviceAuth();
|
|
176
|
+
await clearPendingDeviceAuth(config.profileName);
|
|
172
177
|
throw new CliError("auth_failed", "The sign-in request was denied.");
|
|
173
178
|
}
|
|
174
179
|
if (token.status === "expired" || token.status === "consumed") {
|
|
175
|
-
await clearPendingDeviceAuth();
|
|
180
|
+
await clearPendingDeviceAuth(config.profileName);
|
|
176
181
|
throw new CliError("auth_failed", token.status === "expired"
|
|
177
182
|
? "The sign-in request expired. Run `recess auth request` again."
|
|
178
183
|
: "The sign-in request was already used. Run `recess auth request` again.");
|