@chalksurf/cli 0.1.0 → 0.2.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 +19 -85
- package/dist/bin/chalksurf.js +77 -10
- package/dist/commands/auth.js +31 -19
- package/dist/commands/exercise.js +428 -0
- package/dist/commands/job.js +18 -8
- package/dist/commands/org.js +13 -8
- package/dist/commands/sheet.js +286 -129
- package/dist/lib/api-client.js +9 -3
- package/dist/lib/cli-error.js +37 -1
- package/dist/lib/import-files.js +120 -0
- package/dist/lib/import-output.js +67 -0
- package/dist/lib/manifest.js +183 -19
- package/dist/lib/output.js +35 -2
- package/dist/lib/prompt-secret.js +32 -0
- package/dist/lib/session.js +1 -1
- package/dist/lib/source-resolver.js +72 -10
- package/dist/lib/translation-languages.js +1 -0
- package/dist/lib/user-jobs.js +16 -1
- package/docs/agents.md +195 -0
- package/docs/examples/exercise-import-manifest.json +13 -0
- package/docs/examples/exercise-solution-import-manifest.json +13 -0
- package/docs/examples/sheet-import-manifest.json +33 -0
- package/docs/exit-codes.md +57 -0
- package/docs/manifest.md +129 -0
- package/docs/manual.md +152 -0
- package/package.json +8 -4
- package/schemas/exercise-import-manifest.schema.json +104 -0
- package/schemas/exercise-solution-import-manifest.schema.json +104 -0
- package/schemas/sheet-import-manifest.schema.json +142 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createWriteStream } from 'node:fs';
|
|
2
|
-
import { mkdtemp, readdir, rm, stat } from 'node:fs/promises';
|
|
2
|
+
import { mkdtemp, open, readdir, rm, stat } from 'node:fs/promises';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { basename, isAbsolute, relative, resolve } from 'node:path';
|
|
5
5
|
import { Readable } from 'node:stream';
|
|
@@ -8,6 +8,8 @@ import pLimit from 'p-limit';
|
|
|
8
8
|
import { CliCommandError } from './cli-error.js';
|
|
9
9
|
const sourceResolutionExitCode = 4;
|
|
10
10
|
const defaultMaxConcurrentUrlDownloads = 4;
|
|
11
|
+
const htmlSniffBytes = 1024;
|
|
12
|
+
const genericMimeTypes = new Set(['application/octet-stream', 'binary/octet-stream']);
|
|
11
13
|
const createSourceResolutionError = (message) => {
|
|
12
14
|
return new CliCommandError(message, sourceResolutionExitCode);
|
|
13
15
|
};
|
|
@@ -85,7 +87,44 @@ const deriveRelativePathFromUrl = (url) => {
|
|
|
85
87
|
}
|
|
86
88
|
return fileName;
|
|
87
89
|
};
|
|
88
|
-
const
|
|
90
|
+
const normalizeMimeType = (contentTypeHeaderValue) => {
|
|
91
|
+
const mimeType = contentTypeHeaderValue?.split(';', 1)[0]?.trim().toLowerCase();
|
|
92
|
+
return mimeType || undefined;
|
|
93
|
+
};
|
|
94
|
+
const stripLeadingHtmlComments = (value) => {
|
|
95
|
+
let normalizedValue = value.trimStart();
|
|
96
|
+
while (normalizedValue.startsWith('<!--')) {
|
|
97
|
+
const commentEndIndex = normalizedValue.indexOf('-->');
|
|
98
|
+
if (commentEndIndex === -1) {
|
|
99
|
+
return normalizedValue;
|
|
100
|
+
}
|
|
101
|
+
normalizedValue = normalizedValue.slice(commentEndIndex + 3).trimStart();
|
|
102
|
+
}
|
|
103
|
+
return normalizedValue;
|
|
104
|
+
};
|
|
105
|
+
const looksLikeHtmlDocument = (value) => {
|
|
106
|
+
const normalizedValue = stripLeadingHtmlComments(value.replace(/^\uFEFF/, ''));
|
|
107
|
+
return /^<(?:!doctype html|html|head|body)\b/i.test(normalizedValue);
|
|
108
|
+
};
|
|
109
|
+
const sniffMimeType = async ({ filePath, responseMimeType }) => {
|
|
110
|
+
if (responseMimeType && !genericMimeTypes.has(responseMimeType)) {
|
|
111
|
+
return responseMimeType;
|
|
112
|
+
}
|
|
113
|
+
const fileHandle = await open(filePath, 'r');
|
|
114
|
+
try {
|
|
115
|
+
const buffer = Buffer.alloc(htmlSniffBytes);
|
|
116
|
+
const { bytesRead } = await fileHandle.read(buffer, 0, buffer.length, 0);
|
|
117
|
+
const filePrefix = buffer.subarray(0, bytesRead).toString('utf8');
|
|
118
|
+
if (looksLikeHtmlDocument(filePrefix)) {
|
|
119
|
+
return 'text/html';
|
|
120
|
+
}
|
|
121
|
+
return responseMimeType;
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
await fileHandle.close();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const resolveLocalFileSource = async ({ source, cwd, sourceInputIndex, }) => {
|
|
89
128
|
const resolvedPath = resolve(cwd, source.path);
|
|
90
129
|
await ensureExistingFile(resolvedPath, 'Local source');
|
|
91
130
|
const relativePath = normalizeRelativePath(source.relativePath ?? basename(resolvedPath));
|
|
@@ -96,10 +135,12 @@ const resolveLocalFileSource = async ({ source, cwd, }) => {
|
|
|
96
135
|
filePath: resolvedPath,
|
|
97
136
|
fileName: basename(relativePath),
|
|
98
137
|
relativePath,
|
|
138
|
+
sourceInputIndex,
|
|
139
|
+
sourceInput: source,
|
|
99
140
|
},
|
|
100
141
|
];
|
|
101
142
|
};
|
|
102
|
-
const resolveDirectorySource = async ({ source, cwd, }) => {
|
|
143
|
+
const resolveDirectorySource = async ({ source, cwd, sourceInputIndex, }) => {
|
|
103
144
|
const resolvedDirectoryPath = resolve(cwd, source.path);
|
|
104
145
|
const resolvedRelativeRoot = resolve(cwd, source.relativeRoot ?? source.path);
|
|
105
146
|
await ensureExistingDirectory(resolvedDirectoryPath, 'Directory source');
|
|
@@ -119,10 +160,12 @@ const resolveDirectorySource = async ({ source, cwd, }) => {
|
|
|
119
160
|
filePath,
|
|
120
161
|
fileName: basename(relativePath),
|
|
121
162
|
relativePath,
|
|
163
|
+
sourceInputIndex,
|
|
164
|
+
sourceInput: source,
|
|
122
165
|
};
|
|
123
166
|
});
|
|
124
167
|
};
|
|
125
|
-
const resolveUrlSource = async ({ source,
|
|
168
|
+
const resolveUrlSource = async ({ source, sourceInputIndex, }) => {
|
|
126
169
|
let parsedUrl;
|
|
127
170
|
try {
|
|
128
171
|
parsedUrl = new URL(source.url);
|
|
@@ -137,22 +180,30 @@ const resolveUrlSource = async ({ source, fetchImpl, }) => {
|
|
|
137
180
|
const downloadDirectoryPath = await mkdtemp(resolve(tmpdir(), 'chalksurf-cli-source-'));
|
|
138
181
|
const downloadedFilePath = resolve(downloadDirectoryPath, basename(relativePath));
|
|
139
182
|
try {
|
|
140
|
-
const response = await
|
|
183
|
+
const response = await fetch(source.url);
|
|
141
184
|
if (!response.ok) {
|
|
142
185
|
throw createSourceResolutionError(`Failed to download "${source.url}": HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ''}.`);
|
|
143
186
|
}
|
|
144
187
|
if (!response.body) {
|
|
145
188
|
throw createSourceResolutionError(`Failed to download "${source.url}": response body was empty.`);
|
|
146
189
|
}
|
|
190
|
+
const responseMimeType = normalizeMimeType(response.headers.get('content-type'));
|
|
147
191
|
await pipeline(Readable.fromWeb(response.body), createWriteStream(downloadedFilePath));
|
|
192
|
+
const mimeType = await sniffMimeType({
|
|
193
|
+
filePath: downloadedFilePath,
|
|
194
|
+
responseMimeType,
|
|
195
|
+
});
|
|
148
196
|
return [
|
|
149
197
|
{
|
|
150
198
|
kind: 'url',
|
|
151
199
|
input: source.url,
|
|
152
200
|
filePath: downloadedFilePath,
|
|
153
201
|
fileName: basename(relativePath),
|
|
202
|
+
mimeType,
|
|
154
203
|
relativePath,
|
|
204
|
+
sourceInputIndex,
|
|
155
205
|
cleanupPath: downloadDirectoryPath,
|
|
206
|
+
sourceInput: source,
|
|
156
207
|
},
|
|
157
208
|
];
|
|
158
209
|
}
|
|
@@ -182,16 +233,27 @@ export const cleanupResolvedSources = async (resolvedSources) => {
|
|
|
182
233
|
await rm(cleanupPath, { force: true, recursive: true });
|
|
183
234
|
}));
|
|
184
235
|
};
|
|
185
|
-
export const resolveSources = async ({ sources, cwd = process.cwd(),
|
|
236
|
+
export const resolveSources = async ({ sources, cwd = process.cwd(), maxConcurrentUrlDownloads = defaultMaxConcurrentUrlDownloads, }) => {
|
|
186
237
|
const limitUrlDownloads = pLimit(Math.max(1, maxConcurrentUrlDownloads));
|
|
187
|
-
const settledSourceGroups = await Promise.allSettled(sources.map(async (source) => {
|
|
238
|
+
const settledSourceGroups = await Promise.allSettled(sources.map(async (source, sourceInputIndex) => {
|
|
188
239
|
if (source.kind === 'local') {
|
|
189
|
-
return await resolveLocalFileSource({
|
|
240
|
+
return await resolveLocalFileSource({
|
|
241
|
+
source: source,
|
|
242
|
+
cwd,
|
|
243
|
+
sourceInputIndex,
|
|
244
|
+
});
|
|
190
245
|
}
|
|
191
246
|
if (source.kind === 'directory') {
|
|
192
|
-
return await resolveDirectorySource({
|
|
247
|
+
return await resolveDirectorySource({
|
|
248
|
+
source: source,
|
|
249
|
+
cwd,
|
|
250
|
+
sourceInputIndex,
|
|
251
|
+
});
|
|
193
252
|
}
|
|
194
|
-
return await limitUrlDownloads(() => resolveUrlSource({
|
|
253
|
+
return await limitUrlDownloads(() => resolveUrlSource({
|
|
254
|
+
source: source,
|
|
255
|
+
sourceInputIndex,
|
|
256
|
+
}));
|
|
195
257
|
}));
|
|
196
258
|
const resolvedSources = settledSourceGroups.flatMap((result) => (result.status === 'fulfilled' ? result.value : []));
|
|
197
259
|
const firstRejectedResult = settledSourceGroups.find((result) => result.status === 'rejected');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const translationLanguages = ['english', 'hungarian', 'german', 'french', 'spanish', 'italian'];
|
package/dist/lib/user-jobs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CliCommandError } from './cli-error.js';
|
|
1
|
+
import { CliCommandError, createSerializableCliError } from './cli-error.js';
|
|
2
2
|
import { mapApiErrorToCliError } from './session.js';
|
|
3
3
|
const isTerminalJob = (job) => {
|
|
4
4
|
return ['completed', 'failed'].includes(job.status);
|
|
@@ -41,6 +41,21 @@ export const getWaitExitCode = ({ jobs, timedOut }) => {
|
|
|
41
41
|
}
|
|
42
42
|
return 0;
|
|
43
43
|
};
|
|
44
|
+
export const getWaitError = ({ jobs, timedOut }) => {
|
|
45
|
+
if (timedOut) {
|
|
46
|
+
return createSerializableCliError({
|
|
47
|
+
exitCode: 6,
|
|
48
|
+
message: 'Timed out before all jobs completed.',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (jobs.some((job) => job.status === 'failed')) {
|
|
52
|
+
return createSerializableCliError({
|
|
53
|
+
exitCode: 7,
|
|
54
|
+
message: 'One or more jobs failed.',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
};
|
|
44
59
|
export const waitForCliJobs = async ({ getUserJob, jobIds, maxPollIntervalMs = 5000, now, sleep, timeoutMs = 300000, }) => {
|
|
45
60
|
const startedAt = now();
|
|
46
61
|
let nextPollDelayMs = 1000;
|
package/docs/agents.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Agent And Codex Guide
|
|
2
|
+
|
|
3
|
+
Use this flow when ChalkSurf is driven by Codex, CI, or another orchestration layer.
|
|
4
|
+
|
|
5
|
+
## Authentication Strategy
|
|
6
|
+
|
|
7
|
+
Prefer environment variables over interactive config:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
export CHALKSURF_BASE_URL=https://api.chalksurf.com
|
|
11
|
+
export CHALKSURF_TOKEN=cs_cli_...
|
|
12
|
+
export CHALKSURF_ORGANIZATION_ID=org_123
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Check the session in machine-readable mode:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
chalksurf auth status --json
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Avoid `auth login` for short-lived runs unless you explicitly want local persistence.
|
|
22
|
+
|
|
23
|
+
## Command Contract
|
|
24
|
+
|
|
25
|
+
For agent-driven imports, always use:
|
|
26
|
+
|
|
27
|
+
- `--manifest -` or a generated manifest file
|
|
28
|
+
- `--wait` when the next step depends on completed imports
|
|
29
|
+
- `--json` so the response stays machine-readable
|
|
30
|
+
|
|
31
|
+
Recommended invocation shape:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
chalksurf sheet import --manifest - --wait --json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
In `--json` mode:
|
|
38
|
+
|
|
39
|
+
- stdout contains a stable JSON envelope
|
|
40
|
+
- known failures still produce JSON on stdout
|
|
41
|
+
- stderr is reserved for unexpected runtime failures
|
|
42
|
+
|
|
43
|
+
Wait-style failures still include a populated `result` payload, so agents can inspect partial outcomes on exit code `6` or `7`.
|
|
44
|
+
|
|
45
|
+
## Manifest Design
|
|
46
|
+
|
|
47
|
+
Prefer manifests over large positional argument lists.
|
|
48
|
+
|
|
49
|
+
Why:
|
|
50
|
+
|
|
51
|
+
- multi-source imports stay explicit
|
|
52
|
+
- each logical source can carry a stable `sourceId`
|
|
53
|
+
- sheet imports can group multiple source files into one resulting sheet
|
|
54
|
+
- sheet imports carry `targetFolderPath`, `title`, and `translateTo` at the sheet level
|
|
55
|
+
- agents can correlate import results back to the discovered source set
|
|
56
|
+
|
|
57
|
+
Use `sourceId` whenever a browsing step or upstream scraper already has a stable identifier:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"organizationId": "org_123",
|
|
62
|
+
"wait": true,
|
|
63
|
+
"sheets": [
|
|
64
|
+
{
|
|
65
|
+
"targetFolderPath": "OKTV/2014",
|
|
66
|
+
"title": "OKTV 2014 Round 1",
|
|
67
|
+
"translateTo": ["english"],
|
|
68
|
+
"sources": [
|
|
69
|
+
{
|
|
70
|
+
"sourceId": "oktv-2014-round-1-part-a",
|
|
71
|
+
"kind": "url",
|
|
72
|
+
"url": "https://example.com/oktv-2014-round-1-part-a.pdf"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"sourceId": "oktv-2014-round-1-part-b",
|
|
76
|
+
"kind": "url",
|
|
77
|
+
"url": "https://example.com/oktv-2014-round-1-part-b.pdf"
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
For sheet imports, `targetFolderPath` is the destination of the resulting sheet. Use `null` for the root folder. Source-level `relativePath` is only a filename or source-label override and should not be used as a destination path.
|
|
86
|
+
|
|
87
|
+
Further reference:
|
|
88
|
+
|
|
89
|
+
- [Manifest reference](./manifest.md)
|
|
90
|
+
- [Sheet import schema](../schemas/sheet-import-manifest.schema.json)
|
|
91
|
+
- [Exercise import schema](../schemas/exercise-import-manifest.schema.json)
|
|
92
|
+
- [Exercise solution import schema](../schemas/exercise-solution-import-manifest.schema.json)
|
|
93
|
+
|
|
94
|
+
## JSON Result Shape
|
|
95
|
+
|
|
96
|
+
Import commands normalize results around:
|
|
97
|
+
|
|
98
|
+
- `request`
|
|
99
|
+
- `sources`
|
|
100
|
+
- `jobs`
|
|
101
|
+
- `summary`
|
|
102
|
+
|
|
103
|
+
Each resolved source includes stable correlation fields:
|
|
104
|
+
|
|
105
|
+
- `sourceIndex`
|
|
106
|
+
- `sourceInputIndex`
|
|
107
|
+
- `sourceId`
|
|
108
|
+
|
|
109
|
+
Each job includes:
|
|
110
|
+
|
|
111
|
+
- `jobId`
|
|
112
|
+
- `sourceIndexes`
|
|
113
|
+
- `sourceIds`
|
|
114
|
+
- `status`
|
|
115
|
+
|
|
116
|
+
`summary` includes:
|
|
117
|
+
|
|
118
|
+
- source and job counts
|
|
119
|
+
- per-status counts
|
|
120
|
+
- `timedOut`
|
|
121
|
+
|
|
122
|
+
## Recommended Control Flow
|
|
123
|
+
|
|
124
|
+
1. Discover candidate URLs or local files.
|
|
125
|
+
2. Filter them to the target scope.
|
|
126
|
+
3. Build a manifest with stable `sourceId` values.
|
|
127
|
+
4. Pipe the manifest into the CLI with `--wait --json`.
|
|
128
|
+
5. Inspect `ok`, `result.summary`, and `result.jobs`.
|
|
129
|
+
6. Retry only the failed or timed-out source set.
|
|
130
|
+
|
|
131
|
+
For example:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
cat import.json | chalksurf sheet import --manifest - --wait --json
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
On success, the envelope looks like:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"schemaVersion": "v1",
|
|
142
|
+
"command": "sheet import",
|
|
143
|
+
"ok": true,
|
|
144
|
+
"result": {
|
|
145
|
+
"request": {},
|
|
146
|
+
"sources": [],
|
|
147
|
+
"jobs": [],
|
|
148
|
+
"summary": {
|
|
149
|
+
"timedOut": false
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
"warnings": []
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
On timeout or job failure, `ok` becomes `false`, `error.code` is stable, and `result` still contains the normalized import data needed for retries.
|
|
157
|
+
|
|
158
|
+
## Codex Workflow Example
|
|
159
|
+
|
|
160
|
+
Target task:
|
|
161
|
+
|
|
162
|
+
Import all OKTV exercise sheets between 2010 and 2020 from an archive page, and translate the non-English sheets to English.
|
|
163
|
+
|
|
164
|
+
Recommended workflow:
|
|
165
|
+
|
|
166
|
+
1. Codex browses the archive page and collects the relevant links.
|
|
167
|
+
2. Codex filters the links to years `2010` through `2020`.
|
|
168
|
+
3. Codex builds a sheet-import manifest with one `sheets[]` entry per resulting sheet.
|
|
169
|
+
4. Codex groups multiple source files inside one `sources[]` array when they belong to the same resulting sheet.
|
|
170
|
+
5. Codex sets `translateTo: ["english"]` on the sheet entries that should produce an English translation.
|
|
171
|
+
6. Codex runs `chalksurf sheet import --manifest - --wait --json`.
|
|
172
|
+
7. Codex inspects `result.summary.failed`, `result.summary.timedOut`, and `jobs` to decide whether to retry or report failures.
|
|
173
|
+
|
|
174
|
+
Minimal shell shape:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
cat import.json | chalksurf sheet import --manifest - --wait --json
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The discovery step belongs outside the ChalkSurf CLI. The CLI contract starts at a concrete manifest and ends at structured import results.
|
|
181
|
+
|
|
182
|
+
## Failure Handling
|
|
183
|
+
|
|
184
|
+
Use the exit code and JSON `error.code` together:
|
|
185
|
+
|
|
186
|
+
- `2` / `usage_error`: the manifest or command input is invalid
|
|
187
|
+
- `3` / `not_authenticated`: missing or invalid token
|
|
188
|
+
- `4` / `source_resolution_failed`: local file, directory, or URL preparation failed
|
|
189
|
+
- `5` / `api_error`: the ChalkSurf API rejected the request
|
|
190
|
+
- `6` / `wait_timed_out`: jobs did not finish in time
|
|
191
|
+
- `7` / `job_failed`: one or more jobs finished with failure
|
|
192
|
+
|
|
193
|
+
Reference:
|
|
194
|
+
|
|
195
|
+
- [Exit codes and JSON errors](./exit-codes.md)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"organizationId": "org_123",
|
|
3
|
+
"exerciseSheetId": "00000000-0000-4000-8000-000000000111",
|
|
4
|
+
"wait": true,
|
|
5
|
+
"sources": [
|
|
6
|
+
{
|
|
7
|
+
"sourceId": "problem-1",
|
|
8
|
+
"kind": "local",
|
|
9
|
+
"path": "./imports/problem-1.pdf",
|
|
10
|
+
"relativePath": "Problem Set/problem-1.pdf"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"organizationId": "org_123",
|
|
3
|
+
"exerciseId": "00000000-0000-4000-8000-000000000222",
|
|
4
|
+
"wait": true,
|
|
5
|
+
"sources": [
|
|
6
|
+
{
|
|
7
|
+
"sourceId": "problem-1-solution",
|
|
8
|
+
"kind": "local",
|
|
9
|
+
"path": "./imports/problem-1-solution.pdf",
|
|
10
|
+
"relativePath": "Solutions/problem-1.pdf"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"organizationId": "org_123",
|
|
3
|
+
"wait": true,
|
|
4
|
+
"sheets": [
|
|
5
|
+
{
|
|
6
|
+
"targetFolderPath": "OKTV/2014",
|
|
7
|
+
"title": "OKTV 2014 Round 1",
|
|
8
|
+
"translateTo": ["english"],
|
|
9
|
+
"sources": [
|
|
10
|
+
{
|
|
11
|
+
"sourceId": "oktv-2014-round-1-part-a",
|
|
12
|
+
"kind": "url",
|
|
13
|
+
"url": "https://example.com/oktv-2014-round-1-part-a.pdf"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"sourceId": "oktv-2014-round-1-part-b",
|
|
17
|
+
"kind": "url",
|
|
18
|
+
"url": "https://example.com/oktv-2014-round-1-part-b.pdf"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"targetFolderPath": null,
|
|
24
|
+
"sources": [
|
|
25
|
+
{
|
|
26
|
+
"sourceId": "practice-sheet",
|
|
27
|
+
"kind": "local",
|
|
28
|
+
"path": "./practice-sheet.pdf"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Exit Codes And JSON Errors
|
|
2
|
+
|
|
3
|
+
The CLI uses stable exit codes so automation can branch without parsing human text.
|
|
4
|
+
|
|
5
|
+
## Exit Codes
|
|
6
|
+
|
|
7
|
+
| Exit code | Error code | Meaning |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `0` | n/a | Success |
|
|
10
|
+
| `1` | `unexpected_error` | Unhandled runtime failure |
|
|
11
|
+
| `2` | `usage_error` | Invalid arguments, invalid manifest, or missing required config such as `--base-url` |
|
|
12
|
+
| `3` | `not_authenticated` | Missing token or rejected CLI token |
|
|
13
|
+
| `4` | `source_resolution_failed` | Local file, directory, URL, or relative path resolution failed |
|
|
14
|
+
| `5` | `api_error` | The ChalkSurf API returned a non-auth failure |
|
|
15
|
+
| `6` | `wait_timed_out` | Waiting ended before all jobs reached a terminal state |
|
|
16
|
+
| `7` | `job_failed` | One or more jobs completed with failure |
|
|
17
|
+
|
|
18
|
+
These same codes are mirrored in `error.exitCode` and `error.code` inside the JSON envelope.
|
|
19
|
+
|
|
20
|
+
## JSON Envelope
|
|
21
|
+
|
|
22
|
+
In `--json` mode, stdout contains a stable envelope:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"schemaVersion": "v1",
|
|
27
|
+
"command": "sheet import",
|
|
28
|
+
"ok": true,
|
|
29
|
+
"result": {},
|
|
30
|
+
"warnings": []
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Known failures also return JSON on stdout:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"schemaVersion": "v1",
|
|
39
|
+
"command": "sheet import",
|
|
40
|
+
"ok": false,
|
|
41
|
+
"result": {},
|
|
42
|
+
"error": {
|
|
43
|
+
"code": "source_resolution_failed",
|
|
44
|
+
"message": "Local source \"/tmp/imports/missing.pdf\" does not exist.",
|
|
45
|
+
"exitCode": 4,
|
|
46
|
+
"retryable": false
|
|
47
|
+
},
|
|
48
|
+
"warnings": []
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Contract Notes
|
|
53
|
+
|
|
54
|
+
- `warnings` contains informational messages that would otherwise be printed for humans.
|
|
55
|
+
- In `--json` mode, stderr is reserved for unexpected runtime failures only.
|
|
56
|
+
- Wait-style commands and imports keep their `result` payload even when `ok` is `false`.
|
|
57
|
+
- Import commands normalize their `result` payloads around `request`, `sources`, `jobs`, and `summary`.
|
package/docs/manifest.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Manifest Reference
|
|
2
|
+
|
|
3
|
+
ChalkSurf uses JSON manifests for batch and agent-driven imports.
|
|
4
|
+
|
|
5
|
+
One manifest file is passed to exactly one command:
|
|
6
|
+
|
|
7
|
+
- `chalksurf sheet import --manifest <path|->`
|
|
8
|
+
- `chalksurf exercise import --manifest <path|->`
|
|
9
|
+
- `chalksurf exercise import-solution --manifest <path|->`
|
|
10
|
+
|
|
11
|
+
Use `--manifest -` to pipe JSON on stdin.
|
|
12
|
+
|
|
13
|
+
## Common Fields
|
|
14
|
+
|
|
15
|
+
Every manifest is a JSON object with these shared metadata fields:
|
|
16
|
+
|
|
17
|
+
| Field | Type | Required | Notes |
|
|
18
|
+
| --- | --- | --- | --- |
|
|
19
|
+
| `organizationId` | string | no | Overrides the default organization for this invocation. |
|
|
20
|
+
| `wait` | boolean | no | Behaves like `--wait`. The CLI also accepts `--wait`, which wins if set. |
|
|
21
|
+
|
|
22
|
+
Every source object supports these common fields. For `sheet import`, sources appear inside `sheets[].sources[]`. For the exercise import commands, they appear in top-level `sources[]`.
|
|
23
|
+
|
|
24
|
+
| Field | Type | Required | Notes |
|
|
25
|
+
| --- | --- | --- | --- |
|
|
26
|
+
| `sourceId` | string | no | Optional stable identifier for correlating source inputs to results. Must be unique within the manifest. |
|
|
27
|
+
| `kind` | `local` \| `directory` \| `url` | yes | Selects the source shape. |
|
|
28
|
+
|
|
29
|
+
### Source Kinds
|
|
30
|
+
|
|
31
|
+
`local`
|
|
32
|
+
|
|
33
|
+
| Field | Type | Required | Notes |
|
|
34
|
+
| --- | --- | --- | --- |
|
|
35
|
+
| `path` | string | yes | Local file path relative to `cwd` unless already absolute. |
|
|
36
|
+
| `relativePath` | string | no | Stored path shown to ChalkSurf. Defaults to the basename of `path`. |
|
|
37
|
+
|
|
38
|
+
`directory`
|
|
39
|
+
|
|
40
|
+
| Field | Type | Required | Notes |
|
|
41
|
+
| --- | --- | --- | --- |
|
|
42
|
+
| `path` | string | yes | Directory to expand recursively. |
|
|
43
|
+
| `relativeRoot` | string | no | Base directory used to compute stored relative paths. Defaults to `path`. |
|
|
44
|
+
|
|
45
|
+
`url`
|
|
46
|
+
|
|
47
|
+
| Field | Type | Required | Notes |
|
|
48
|
+
| --- | --- | --- | --- |
|
|
49
|
+
| `url` | string | yes | HTTP(S) URL to download before import. |
|
|
50
|
+
| `relativePath` | string | no | Stored path shown to ChalkSurf. Defaults to the filename derived from the URL. |
|
|
51
|
+
|
|
52
|
+
## Command-Specific Fields
|
|
53
|
+
|
|
54
|
+
### Sheet Import
|
|
55
|
+
|
|
56
|
+
Top-level fields:
|
|
57
|
+
|
|
58
|
+
| Field | Type | Required | Notes |
|
|
59
|
+
| --- | --- | --- | --- |
|
|
60
|
+
| `sheets` | array | yes | One or more sheets to import. Each sheet may contain one or more source files. |
|
|
61
|
+
|
|
62
|
+
Sheet fields:
|
|
63
|
+
|
|
64
|
+
| Field | Type | Required | Notes |
|
|
65
|
+
| --- | --- | --- | --- |
|
|
66
|
+
| `targetFolderPath` | string \| null | yes | Destination folder for the resulting sheet. Use `null` for the root folder. |
|
|
67
|
+
| `sources` | array | yes | One or more source files that belong to this sheet, in import order. |
|
|
68
|
+
| `title` | string | no | Overrides the imported sheet title. |
|
|
69
|
+
| `translateTo` | string[] | no | Target translation languages. Must be unique and non-empty when present. |
|
|
70
|
+
|
|
71
|
+
Sheet import metadata is sheet-level, not source-level. Do not put `title` or `translateTo` on individual sources inside `sheets[].sources[]`.
|
|
72
|
+
|
|
73
|
+
Source-level `relativePath` is only a source label or filename override. It does not choose the destination folder for grouped sheet imports; `targetFolderPath` is the only destination field.
|
|
74
|
+
|
|
75
|
+
Canonical example:
|
|
76
|
+
|
|
77
|
+
- [docs/examples/sheet-import-manifest.json](./examples/sheet-import-manifest.json)
|
|
78
|
+
- [schemas/sheet-import-manifest.schema.json](../schemas/sheet-import-manifest.schema.json)
|
|
79
|
+
|
|
80
|
+
### Exercise Import
|
|
81
|
+
|
|
82
|
+
Top-level fields:
|
|
83
|
+
|
|
84
|
+
| Field | Type | Required | Notes |
|
|
85
|
+
| --- | --- | --- | --- |
|
|
86
|
+
| `sources` | array | yes | One or more import sources. |
|
|
87
|
+
| `exerciseSheetId` | string | no | Default target sheet for the imported exercises. Can still be overridden by `--sheet-id`. |
|
|
88
|
+
|
|
89
|
+
Per-source fields:
|
|
90
|
+
|
|
91
|
+
- Only the common source fields are allowed.
|
|
92
|
+
- `title` and `translateTo` are rejected for this command.
|
|
93
|
+
|
|
94
|
+
Canonical example:
|
|
95
|
+
|
|
96
|
+
- [docs/examples/exercise-import-manifest.json](./examples/exercise-import-manifest.json)
|
|
97
|
+
- [schemas/exercise-import-manifest.schema.json](../schemas/exercise-import-manifest.schema.json)
|
|
98
|
+
|
|
99
|
+
### Exercise Solution Import
|
|
100
|
+
|
|
101
|
+
Top-level fields:
|
|
102
|
+
|
|
103
|
+
| Field | Type | Required | Notes |
|
|
104
|
+
| --- | --- | --- | --- |
|
|
105
|
+
| `sources` | array | yes | One or more import sources. |
|
|
106
|
+
| `exerciseId` | string | no | Default target exercise for the imported solution files. The command still requires an exercise id overall, either here or positionally. |
|
|
107
|
+
|
|
108
|
+
Per-source fields:
|
|
109
|
+
|
|
110
|
+
- Only the common source fields are allowed.
|
|
111
|
+
- `title` and `translateTo` are rejected for this command.
|
|
112
|
+
|
|
113
|
+
Canonical example:
|
|
114
|
+
|
|
115
|
+
- [docs/examples/exercise-solution-import-manifest.json](./examples/exercise-solution-import-manifest.json)
|
|
116
|
+
- [schemas/exercise-solution-import-manifest.schema.json](../schemas/exercise-solution-import-manifest.schema.json)
|
|
117
|
+
|
|
118
|
+
## Validation Notes
|
|
119
|
+
|
|
120
|
+
The CLI validates more than the JSON schema can express on its own:
|
|
121
|
+
|
|
122
|
+
- `sourceId` values must be unique within one manifest.
|
|
123
|
+
- Sheet import `translateTo` values must be unique within one sheet.
|
|
124
|
+
- `relativePath` cannot be empty or contain `..`.
|
|
125
|
+
- Sheet import `targetFolderPath` must be `null` or a normalized folder path without `.` or `..` segments.
|
|
126
|
+
- `directory` imports must resolve to at least one file.
|
|
127
|
+
- URL imports must use `http` or `https`.
|
|
128
|
+
|
|
129
|
+
Treat the schema files as a reference format and the CLI as the final validator.
|