@chalksurf/cli 0.2.0 → 0.2.2
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 +26 -22
- package/dist/bin/chalksurf.js +11 -4
- package/dist/commands/auth.js +19 -12
- package/dist/commands/exercise.js +182 -10
- package/dist/commands/job.js +108 -40
- package/dist/commands/org.js +10 -6
- package/dist/commands/profile.js +97 -0
- package/dist/commands/sheet.js +325 -31
- package/dist/lib/api-client.js +11 -2
- package/dist/lib/command-options.js +61 -0
- package/dist/lib/config-store.js +188 -10
- package/dist/lib/import-output.js +14 -0
- package/dist/lib/manifest.js +24 -0
- package/dist/lib/session.js +27 -0
- package/dist/lib/translation-languages.js +15 -0
- package/dist/lib/user-jobs.js +6 -0
- package/docs/agents.md +76 -14
- package/docs/examples/exercise-import-manifest.json +1 -0
- package/docs/examples/exercise-sheet-solution-import-manifest.json +13 -0
- package/docs/manifest.md +25 -3
- package/docs/manual.md +72 -11
- package/package.json +1 -1
- package/schemas/exercise-import-manifest.schema.json +12 -0
- package/schemas/exercise-sheet-solution-import-manifest.schema.json +104 -0
package/dist/lib/config-store.js
CHANGED
|
@@ -5,7 +5,11 @@ import { CliCommandError } from './cli-error.js';
|
|
|
5
5
|
export const chalksurfBaseUrlEnvVar = 'CHALKSURF_BASE_URL';
|
|
6
6
|
export const chalksurfTokenEnvVar = 'CHALKSURF_TOKEN';
|
|
7
7
|
export const chalksurfOrganizationIdEnvVar = 'CHALKSURF_ORGANIZATION_ID';
|
|
8
|
+
export const chalksurfProfileEnvVar = 'CHALKSURF_PROFILE';
|
|
8
9
|
export const chalksurfConfigPathEnvVar = 'CHALKSURF_CONFIG_PATH';
|
|
10
|
+
export const cliConfigSchemaVersion = 2;
|
|
11
|
+
export const implicitDefaultProfileName = 'default';
|
|
12
|
+
const profileNamePattern = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
9
13
|
const normalizeOptionalString = (value) => {
|
|
10
14
|
const trimmedValue = value?.trim();
|
|
11
15
|
return trimmedValue ? trimmedValue : undefined;
|
|
@@ -14,13 +18,91 @@ const normalizeStoredBaseUrl = (value) => {
|
|
|
14
18
|
const normalizedValue = normalizeOptionalString(value);
|
|
15
19
|
return normalizedValue?.replace(/\/+$/, '') || undefined;
|
|
16
20
|
};
|
|
17
|
-
const
|
|
21
|
+
const isRecord = (value) => {
|
|
22
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
23
|
+
};
|
|
24
|
+
export const assertValidProfileName = (profileName) => {
|
|
25
|
+
if (!profileNamePattern.test(profileName)) {
|
|
26
|
+
throw new CliCommandError(`Invalid profile "${profileName}". Profile names must start with a letter or number and contain only letters, numbers, ".", "_", or "-".`, 2);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const normalizeProfileName = (value) => {
|
|
30
|
+
const profileName = normalizeOptionalString(value);
|
|
31
|
+
if (!profileName) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
assertValidProfileName(profileName);
|
|
35
|
+
return profileName;
|
|
36
|
+
};
|
|
37
|
+
const sanitizeProfileConfig = (config) => {
|
|
18
38
|
return Object.fromEntries(Object.entries({
|
|
19
39
|
token: normalizeOptionalString(config.token),
|
|
20
40
|
baseUrl: normalizeStoredBaseUrl(config.baseUrl),
|
|
21
41
|
organizationId: normalizeOptionalString(config.organizationId),
|
|
22
42
|
}).filter(([, value]) => value !== undefined));
|
|
23
43
|
};
|
|
44
|
+
const hasProfileConfigValues = (config) => {
|
|
45
|
+
return Boolean(config.baseUrl || config.token || config.organizationId);
|
|
46
|
+
};
|
|
47
|
+
const readProfileConfig = (value) => {
|
|
48
|
+
if (!isRecord(value)) {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
return sanitizeProfileConfig({
|
|
52
|
+
baseUrl: typeof value.baseUrl === 'string' ? value.baseUrl : undefined,
|
|
53
|
+
organizationId: typeof value.organizationId === 'string' ? value.organizationId : undefined,
|
|
54
|
+
token: typeof value.token === 'string' ? value.token : undefined,
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
const sanitizeRootConfig = (config) => {
|
|
58
|
+
const profiles = Object.fromEntries(Object.entries(config.profiles)
|
|
59
|
+
.map(([profileName, profileConfig]) => {
|
|
60
|
+
const normalizedProfileName = normalizeProfileName(profileName);
|
|
61
|
+
const sanitizedProfileConfig = sanitizeProfileConfig(profileConfig);
|
|
62
|
+
return normalizedProfileName && hasProfileConfigValues(sanitizedProfileConfig)
|
|
63
|
+
? [normalizedProfileName, sanitizedProfileConfig]
|
|
64
|
+
: null;
|
|
65
|
+
})
|
|
66
|
+
.filter((entry) => entry !== null));
|
|
67
|
+
const defaultProfile = normalizeProfileName(config.defaultProfile);
|
|
68
|
+
return {
|
|
69
|
+
schemaVersion: cliConfigSchemaVersion,
|
|
70
|
+
...(defaultProfile && profiles[defaultProfile] ? { defaultProfile } : {}),
|
|
71
|
+
profiles,
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
const readRootConfig = (value) => {
|
|
75
|
+
if (!isRecord(value)) {
|
|
76
|
+
return { schemaVersion: cliConfigSchemaVersion, profiles: {} };
|
|
77
|
+
}
|
|
78
|
+
if (isRecord(value.profiles)) {
|
|
79
|
+
const profiles = Object.fromEntries(Object.entries(value.profiles)
|
|
80
|
+
.map(([profileName, profileConfig]) => {
|
|
81
|
+
const sanitizedProfileConfig = readProfileConfig(profileConfig);
|
|
82
|
+
return hasProfileConfigValues(sanitizedProfileConfig)
|
|
83
|
+
? [profileName, sanitizedProfileConfig]
|
|
84
|
+
: null;
|
|
85
|
+
})
|
|
86
|
+
.filter((entry) => entry !== null));
|
|
87
|
+
return sanitizeRootConfig({
|
|
88
|
+
schemaVersion: cliConfigSchemaVersion,
|
|
89
|
+
defaultProfile: typeof value.defaultProfile === 'string' ? value.defaultProfile : undefined,
|
|
90
|
+
profiles,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const legacyProfile = readProfileConfig(value);
|
|
94
|
+
if (!hasProfileConfigValues(legacyProfile)) {
|
|
95
|
+
return { schemaVersion: cliConfigSchemaVersion, profiles: {} };
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
schemaVersion: cliConfigSchemaVersion,
|
|
99
|
+
defaultProfile: implicitDefaultProfileName,
|
|
100
|
+
profiles: {
|
|
101
|
+
[implicitDefaultProfileName]: legacyProfile,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
const rootConfigHasProfiles = (config) => Object.keys(config.profiles).length > 0;
|
|
24
106
|
const getDefaultConfigPath = ({ env, platform, homeDirectory, }) => {
|
|
25
107
|
const explicitConfigPath = normalizeOptionalString(env[chalksurfConfigPathEnvVar]);
|
|
26
108
|
if (explicitConfigPath) {
|
|
@@ -73,6 +155,24 @@ export const resolveToken = ({ env, config }) => {
|
|
|
73
155
|
}
|
|
74
156
|
return { source: 'none' };
|
|
75
157
|
};
|
|
158
|
+
export const resolveProfile = ({ flagValue, env, config, }) => {
|
|
159
|
+
const flagProfileName = normalizeProfileName(flagValue);
|
|
160
|
+
if (flagProfileName) {
|
|
161
|
+
return { profileName: flagProfileName, profileSource: 'flag' };
|
|
162
|
+
}
|
|
163
|
+
const envProfileName = normalizeProfileName(env[chalksurfProfileEnvVar]);
|
|
164
|
+
if (envProfileName) {
|
|
165
|
+
return { profileName: envProfileName, profileSource: 'env' };
|
|
166
|
+
}
|
|
167
|
+
const defaultProfile = normalizeProfileName(config.defaultProfile);
|
|
168
|
+
if (defaultProfile) {
|
|
169
|
+
return { profileName: defaultProfile, profileSource: 'config' };
|
|
170
|
+
}
|
|
171
|
+
if (!rootConfigHasProfiles(config)) {
|
|
172
|
+
return { profileName: implicitDefaultProfileName, profileSource: 'implicit-default' };
|
|
173
|
+
}
|
|
174
|
+
throw new CliCommandError(`No active ChalkSurf profile selected. Pass --profile, set ${chalksurfProfileEnvVar}, or run "chalksurf profile use <profile>".`, 2);
|
|
175
|
+
};
|
|
76
176
|
export const resolveOrganization = ({ flagValue, env, config, profile, }) => {
|
|
77
177
|
const flagOrganizationId = normalizeOptionalString(flagValue);
|
|
78
178
|
if (flagOrganizationId) {
|
|
@@ -134,14 +234,23 @@ export const resolveOrganization = ({ flagValue, env, config, profile, }) => {
|
|
|
134
234
|
export const createConfigStore = ({ configPath, env = process.env, platform = process.platform, homeDirectory = homedir(), fs = { chmod, mkdir, readFile, rm, writeFile }, } = {}) => {
|
|
135
235
|
const resolvedConfigPath = configPath || getDefaultConfigPath({ env, platform, homeDirectory });
|
|
136
236
|
const load = async () => {
|
|
237
|
+
const rootConfig = await loadRoot();
|
|
238
|
+
const resolvedProfile = resolveProfile({ env, config: rootConfig });
|
|
239
|
+
const profileConfig = rootConfig.profiles[resolvedProfile.profileName] ?? {};
|
|
240
|
+
return {
|
|
241
|
+
...profileConfig,
|
|
242
|
+
...resolvedProfile,
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
const loadRoot = async () => {
|
|
137
246
|
try {
|
|
138
247
|
const configContents = await fs.readFile(resolvedConfigPath, 'utf8');
|
|
139
248
|
const parsedConfig = JSON.parse(configContents);
|
|
140
|
-
return
|
|
249
|
+
return readRootConfig(parsedConfig);
|
|
141
250
|
}
|
|
142
251
|
catch (error) {
|
|
143
252
|
if (error.code === 'ENOENT') {
|
|
144
|
-
return {};
|
|
253
|
+
return { schemaVersion: cliConfigSchemaVersion, profiles: {} };
|
|
145
254
|
}
|
|
146
255
|
if (error instanceof SyntaxError) {
|
|
147
256
|
throw new CliCommandError(`Failed to parse CLI config at ${resolvedConfigPath}.`, 2);
|
|
@@ -149,9 +258,18 @@ export const createConfigStore = ({ configPath, env = process.env, platform = pr
|
|
|
149
258
|
throw error;
|
|
150
259
|
}
|
|
151
260
|
};
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
|
|
261
|
+
const loadProfile = async ({ profileName } = {}) => {
|
|
262
|
+
const rootConfig = await loadRoot();
|
|
263
|
+
const resolvedProfile = resolveProfile({ flagValue: profileName, env, config: rootConfig });
|
|
264
|
+
const profileConfig = rootConfig.profiles[resolvedProfile.profileName] ?? {};
|
|
265
|
+
return {
|
|
266
|
+
...profileConfig,
|
|
267
|
+
...resolvedProfile,
|
|
268
|
+
};
|
|
269
|
+
};
|
|
270
|
+
const saveRoot = async (config) => {
|
|
271
|
+
const sanitizedConfig = sanitizeRootConfig(config);
|
|
272
|
+
if (!rootConfigHasProfiles(sanitizedConfig)) {
|
|
155
273
|
await fs.rm(resolvedConfigPath, { force: true });
|
|
156
274
|
return sanitizedConfig;
|
|
157
275
|
}
|
|
@@ -162,15 +280,75 @@ export const createConfigStore = ({ configPath, env = process.env, platform = pr
|
|
|
162
280
|
await fs.chmod(resolvedConfigPath, 0o600);
|
|
163
281
|
return sanitizedConfig;
|
|
164
282
|
};
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
|
|
283
|
+
const save = async (config) => {
|
|
284
|
+
const rootConfig = await loadRoot();
|
|
285
|
+
const nextProfileConfig = sanitizeProfileConfig(config);
|
|
286
|
+
const nextRootConfig = {
|
|
287
|
+
...rootConfig,
|
|
288
|
+
profiles: {
|
|
289
|
+
...rootConfig.profiles,
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
if (hasProfileConfigValues(nextProfileConfig)) {
|
|
293
|
+
nextRootConfig.profiles[config.profileName] = nextProfileConfig;
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
delete nextRootConfig.profiles[config.profileName];
|
|
297
|
+
}
|
|
298
|
+
if (config.profileSource === 'implicit-default' && hasProfileConfigValues(nextProfileConfig)) {
|
|
299
|
+
nextRootConfig.defaultProfile = config.profileName;
|
|
300
|
+
}
|
|
301
|
+
return await saveRoot(nextRootConfig);
|
|
302
|
+
};
|
|
303
|
+
const update = async ({ profileName }, updater) => {
|
|
304
|
+
const currentConfig = await loadProfile({ profileName });
|
|
305
|
+
const nextProfileConfig = await updater(currentConfig);
|
|
306
|
+
return await save({
|
|
307
|
+
...nextProfileConfig,
|
|
308
|
+
profileName: currentConfig.profileName,
|
|
309
|
+
profileSource: currentConfig.profileSource,
|
|
310
|
+
});
|
|
311
|
+
};
|
|
312
|
+
const setDefaultProfile = async (profileName) => {
|
|
313
|
+
const normalizedProfileName = normalizeProfileName(profileName);
|
|
314
|
+
if (!normalizedProfileName) {
|
|
315
|
+
throw new CliCommandError('Profile name is required.', 2);
|
|
316
|
+
}
|
|
317
|
+
const rootConfig = await loadRoot();
|
|
318
|
+
if (!rootConfig.profiles[normalizedProfileName]) {
|
|
319
|
+
throw new CliCommandError(`Profile "${normalizedProfileName}" does not exist. Run "chalksurf auth login --profile ${normalizedProfileName} --base-url <url>" first.`, 2);
|
|
320
|
+
}
|
|
321
|
+
return await saveRoot({
|
|
322
|
+
...rootConfig,
|
|
323
|
+
defaultProfile: normalizedProfileName,
|
|
324
|
+
});
|
|
325
|
+
};
|
|
326
|
+
const deleteProfile = async (profileName) => {
|
|
327
|
+
const normalizedProfileName = normalizeProfileName(profileName);
|
|
328
|
+
if (!normalizedProfileName) {
|
|
329
|
+
throw new CliCommandError('Profile name is required.', 2);
|
|
330
|
+
}
|
|
331
|
+
const rootConfig = await loadRoot();
|
|
332
|
+
if (!rootConfig.profiles[normalizedProfileName]) {
|
|
333
|
+
throw new CliCommandError(`Profile "${normalizedProfileName}" does not exist.`, 2);
|
|
334
|
+
}
|
|
335
|
+
const profiles = { ...rootConfig.profiles };
|
|
336
|
+
delete profiles[normalizedProfileName];
|
|
337
|
+
return await saveRoot({
|
|
338
|
+
schemaVersion: cliConfigSchemaVersion,
|
|
339
|
+
...(rootConfig.defaultProfile === normalizedProfileName ? {} : { defaultProfile: rootConfig.defaultProfile }),
|
|
340
|
+
profiles,
|
|
341
|
+
});
|
|
169
342
|
};
|
|
170
343
|
return {
|
|
171
344
|
path: resolvedConfigPath,
|
|
172
345
|
load,
|
|
346
|
+
loadProfile,
|
|
347
|
+
loadRoot,
|
|
173
348
|
save,
|
|
349
|
+
saveRoot,
|
|
174
350
|
update,
|
|
351
|
+
setDefaultProfile,
|
|
352
|
+
deleteProfile,
|
|
175
353
|
};
|
|
176
354
|
};
|
|
@@ -65,3 +65,17 @@ export const formatCliImportSummary = (summary) => {
|
|
|
65
65
|
}
|
|
66
66
|
return `Summary: ${parts.join(', ') || '0 jobs'}.`;
|
|
67
67
|
};
|
|
68
|
+
export const formatCliImportTranslationJobs = (translationJobs) => {
|
|
69
|
+
if (!translationJobs || translationJobs.length === 0) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
return [
|
|
73
|
+
`Queued ${translationJobs.length} translation job${translationJobs.length === 1 ? '' : 's'}:`,
|
|
74
|
+
...translationJobs.map((job) => {
|
|
75
|
+
if (job.type === 'exercise_translation_generation') {
|
|
76
|
+
return `${job.jobId} exercise ${job.exerciseId} ${job.languages.join(', ')}`;
|
|
77
|
+
}
|
|
78
|
+
return `${job.jobId} sheet ${job.exerciseSheetId} ${job.language}`;
|
|
79
|
+
}),
|
|
80
|
+
];
|
|
81
|
+
};
|
package/dist/lib/manifest.js
CHANGED
|
@@ -161,6 +161,10 @@ const parseExerciseImportManifest = (manifestText) => {
|
|
|
161
161
|
parsedManifest,
|
|
162
162
|
topLevelFields: ['exerciseSheetId'],
|
|
163
163
|
}),
|
|
164
|
+
translateTo: parseTranslateTo({
|
|
165
|
+
value: parsedManifest.translateTo,
|
|
166
|
+
label: 'Manifest',
|
|
167
|
+
}),
|
|
164
168
|
sources,
|
|
165
169
|
};
|
|
166
170
|
};
|
|
@@ -176,6 +180,18 @@ const parseExerciseSolutionImportManifest = (manifestText) => {
|
|
|
176
180
|
sources,
|
|
177
181
|
};
|
|
178
182
|
};
|
|
183
|
+
const parseExerciseSheetSolutionImportManifest = (manifestText) => {
|
|
184
|
+
const parsedManifest = parseSourceManifestObject(manifestText);
|
|
185
|
+
const sources = parsedManifest.sources.map((source, index) => parseBaseManifestSource(source, index));
|
|
186
|
+
assertUniqueSourceIds(sources);
|
|
187
|
+
return {
|
|
188
|
+
...parseManifestMetadata({
|
|
189
|
+
parsedManifest,
|
|
190
|
+
topLevelFields: ['exerciseSheetId'],
|
|
191
|
+
}),
|
|
192
|
+
sources,
|
|
193
|
+
};
|
|
194
|
+
};
|
|
179
195
|
const parseSheetImportManifest = (manifestText) => {
|
|
180
196
|
const parsedManifest = parseManifestObject(manifestText);
|
|
181
197
|
if ('sources' in parsedManifest) {
|
|
@@ -261,6 +277,14 @@ export const loadExerciseSolutionImportManifest = async ({ cwd, manifestPath, st
|
|
|
261
277
|
stdin,
|
|
262
278
|
});
|
|
263
279
|
};
|
|
280
|
+
export const loadExerciseSheetSolutionImportManifest = async ({ cwd, manifestPath, stdin, }) => {
|
|
281
|
+
return await loadManifest({
|
|
282
|
+
cwd,
|
|
283
|
+
manifestPath,
|
|
284
|
+
parseManifest: parseExerciseSheetSolutionImportManifest,
|
|
285
|
+
stdin,
|
|
286
|
+
});
|
|
287
|
+
};
|
|
264
288
|
export const loadSheetImportManifest = async ({ cwd, manifestPath, stdin, }) => {
|
|
265
289
|
return await loadManifest({
|
|
266
290
|
cwd,
|
package/dist/lib/session.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createApiClient } from './api-client.js';
|
|
1
2
|
import { CliCommandError } from './cli-error.js';
|
|
2
3
|
import { chalksurfBaseUrlEnvVar, chalksurfOrganizationIdEnvVar, chalksurfTokenEnvVar, resolveBaseUrl, resolveToken, } from './config-store.js';
|
|
3
4
|
const normalizeOptionalString = (value) => {
|
|
@@ -31,6 +32,32 @@ export const resolveRequestedOrganizationId = ({ config, env, fallbackValue, fla
|
|
|
31
32
|
normalizeOptionalString(env[chalksurfOrganizationIdEnvVar]) ??
|
|
32
33
|
normalizeOptionalString(config.organizationId));
|
|
33
34
|
};
|
|
35
|
+
export const createResolvedApiClient = async ({ context, baseUrlFlagValue, organizationFallbackValue, organizationFlagValue, profileName, }) => {
|
|
36
|
+
const config = await context.configStore.loadProfile({ profileName });
|
|
37
|
+
const resolvedBaseUrl = requireResolvedBaseUrl({
|
|
38
|
+
flagValue: baseUrlFlagValue,
|
|
39
|
+
env: context.env,
|
|
40
|
+
config,
|
|
41
|
+
});
|
|
42
|
+
const resolvedToken = requireResolvedToken({
|
|
43
|
+
env: context.env,
|
|
44
|
+
config,
|
|
45
|
+
});
|
|
46
|
+
const organizationId = resolveRequestedOrganizationId({
|
|
47
|
+
flagValue: organizationFlagValue,
|
|
48
|
+
fallbackValue: organizationFallbackValue,
|
|
49
|
+
env: context.env,
|
|
50
|
+
config,
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
apiClient: createApiClient({
|
|
54
|
+
baseUrl: resolvedBaseUrl.value,
|
|
55
|
+
token: resolvedToken.value,
|
|
56
|
+
organizationId,
|
|
57
|
+
}),
|
|
58
|
+
organizationId,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
34
61
|
export const formatUserIdentity = (profile) => {
|
|
35
62
|
if (profile.name && profile.email) {
|
|
36
63
|
return `${profile.name} <${profile.email}>`;
|
|
@@ -1 +1,16 @@
|
|
|
1
|
+
import { CliCommandError } from './cli-error.js';
|
|
1
2
|
export const translationLanguages = ['english', 'hungarian', 'german', 'french', 'spanish', 'italian'];
|
|
3
|
+
export const resolveRequestedTranslateToLanguages = (rawValues) => {
|
|
4
|
+
if (!rawValues || rawValues.length === 0) {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
const invalidLanguage = rawValues.find((value) => !translationLanguages.includes(value));
|
|
8
|
+
if (invalidLanguage) {
|
|
9
|
+
throw new CliCommandError(`--translate-to must be one of: ${translationLanguages.join(', ')}`, 2);
|
|
10
|
+
}
|
|
11
|
+
const languages = rawValues;
|
|
12
|
+
if (new Set(languages).size !== languages.length) {
|
|
13
|
+
throw new CliCommandError('--translate-to languages must be unique.', 2);
|
|
14
|
+
}
|
|
15
|
+
return languages;
|
|
16
|
+
};
|
package/dist/lib/user-jobs.js
CHANGED
|
@@ -14,7 +14,13 @@ export const serializeCliJob = (job) => {
|
|
|
14
14
|
exerciseId: job.result?.exerciseId,
|
|
15
15
|
exerciseIds: job.result?.exerciseIds,
|
|
16
16
|
exerciseSheetId: job.result?.exerciseSheetId,
|
|
17
|
+
eligibleExerciseCount: job.result?.eligibleExerciseCount,
|
|
18
|
+
nonUpdatableExerciseCount: job.result?.nonUpdatableExerciseCount,
|
|
17
19
|
resultCode: job.result?.resultCode,
|
|
20
|
+
skippedExerciseCount: job.result?.skippedExerciseCount,
|
|
21
|
+
translationJobs: job.result?.translationJobs,
|
|
22
|
+
unmatchedImportedSolutionCount: job.result?.unmatchedImportedSolutionCount,
|
|
23
|
+
updatedExerciseCount: job.result?.updatedExerciseCount,
|
|
18
24
|
};
|
|
19
25
|
};
|
|
20
26
|
export const formatCliJobSummary = (job) => {
|
package/docs/agents.md
CHANGED
|
@@ -4,34 +4,42 @@ Use this flow when ChalkSurf is driven by Codex, CI, or another orchestration la
|
|
|
4
4
|
|
|
5
5
|
## Authentication Strategy
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Agents must always use an explicit profile. The profile name must follow the
|
|
8
|
+
`ENV-AGENT_TYPE` convention, such as `prod-codex`, `staging-codex`, `dev-claude`,
|
|
9
|
+
or `prod-claude`.
|
|
8
10
|
|
|
9
11
|
```bash
|
|
10
|
-
|
|
11
|
-
export CHALKSURF_TOKEN=cs_cli_...
|
|
12
|
-
export CHALKSURF_ORGANIZATION_ID=org_123
|
|
12
|
+
chalksurf --profile prod-codex auth status --json
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Do not rely on the default profile in agent-driven runs. The default profile is
|
|
16
|
+
for the human operator's terminal workflow and may point at a different
|
|
17
|
+
environment, user token, or selected organization.
|
|
18
|
+
|
|
19
|
+
Create or refresh an agent profile explicitly:
|
|
16
20
|
|
|
17
21
|
```bash
|
|
18
|
-
chalksurf auth
|
|
22
|
+
printf '%s' "$CHALKSURF_TOKEN" | chalksurf auth login \
|
|
23
|
+
--profile prod-codex \
|
|
24
|
+
--base-url https://chalksurf-api.fly.dev \
|
|
25
|
+
--with-token
|
|
19
26
|
```
|
|
20
27
|
|
|
21
|
-
|
|
28
|
+
Use `CHALKSURF_TOKEN` only as the source for login or as a short-lived override.
|
|
29
|
+
Prefer storing the token in the explicit agent profile for repeated Codex/CI runs.
|
|
22
30
|
|
|
23
31
|
## Command Contract
|
|
24
32
|
|
|
25
33
|
For agent-driven imports, always use:
|
|
26
34
|
|
|
27
35
|
- `--manifest -` or a generated manifest file
|
|
28
|
-
- `--wait` when the next step depends on
|
|
36
|
+
- `--wait` when the next step depends on parsed content being saved
|
|
29
37
|
- `--json` so the response stays machine-readable
|
|
30
38
|
|
|
31
39
|
Recommended invocation shape:
|
|
32
40
|
|
|
33
41
|
```bash
|
|
34
|
-
chalksurf sheet import --manifest - --wait --json
|
|
42
|
+
chalksurf --profile prod-codex sheet import --manifest - --wait --json
|
|
35
43
|
```
|
|
36
44
|
|
|
37
45
|
In `--json` mode:
|
|
@@ -41,6 +49,7 @@ In `--json` mode:
|
|
|
41
49
|
- stderr is reserved for unexpected runtime failures
|
|
42
50
|
|
|
43
51
|
Wait-style failures still include a populated `result` payload, so agents can inspect partial outcomes on exit code `6` or `7`.
|
|
52
|
+
When an import requests translations, `--wait` waits for the import job only. Follow-up translation jobs are listed under each completed import job's `translationJobs`.
|
|
44
53
|
|
|
45
54
|
## Manifest Design
|
|
46
55
|
|
|
@@ -52,6 +61,8 @@ Why:
|
|
|
52
61
|
- each logical source can carry a stable `sourceId`
|
|
53
62
|
- sheet imports can group multiple source files into one resulting sheet
|
|
54
63
|
- sheet imports carry `targetFolderPath`, `title`, and `translateTo` at the sheet level
|
|
64
|
+
- exercise imports can carry top-level `translateTo` for every imported exercise
|
|
65
|
+
- sheet solution imports can reconcile one or more solution files against all updatable exercises in one existing sheet
|
|
55
66
|
- agents can correlate import results back to the discovered source set
|
|
56
67
|
|
|
57
68
|
Use `sourceId` whenever a browsing step or upstream scraper already has a stable identifier:
|
|
@@ -90,6 +101,7 @@ Further reference:
|
|
|
90
101
|
- [Sheet import schema](../schemas/sheet-import-manifest.schema.json)
|
|
91
102
|
- [Exercise import schema](../schemas/exercise-import-manifest.schema.json)
|
|
92
103
|
- [Exercise solution import schema](../schemas/exercise-solution-import-manifest.schema.json)
|
|
104
|
+
- [Exercise sheet solution import schema](../schemas/exercise-sheet-solution-import-manifest.schema.json)
|
|
93
105
|
|
|
94
106
|
## JSON Result Shape
|
|
95
107
|
|
|
@@ -112,6 +124,7 @@ Each job includes:
|
|
|
112
124
|
- `sourceIndexes`
|
|
113
125
|
- `sourceIds`
|
|
114
126
|
- `status`
|
|
127
|
+
- `translationJobs` when requested translations were queued after import
|
|
115
128
|
|
|
116
129
|
`summary` includes:
|
|
117
130
|
|
|
@@ -125,13 +138,14 @@ Each job includes:
|
|
|
125
138
|
2. Filter them to the target scope.
|
|
126
139
|
3. Build a manifest with stable `sourceId` values.
|
|
127
140
|
4. Pipe the manifest into the CLI with `--wait --json`.
|
|
128
|
-
5. Inspect `ok`, `result.summary`,
|
|
129
|
-
6.
|
|
141
|
+
5. Inspect `ok`, `result.summary`, `result.jobs`, and any nested `translationJobs`.
|
|
142
|
+
6. Use `job list`, `job wait`, `sheet search`, and `exercise search` to verify the created resources or translation jobs when needed.
|
|
143
|
+
7. Retry only the failed or timed-out source set.
|
|
130
144
|
|
|
131
145
|
For example:
|
|
132
146
|
|
|
133
147
|
```bash
|
|
134
|
-
cat import.json | chalksurf sheet import --manifest - --wait --json
|
|
148
|
+
cat import.json | chalksurf --profile prod-codex sheet import --manifest - --wait --json
|
|
135
149
|
```
|
|
136
150
|
|
|
137
151
|
On success, the envelope looks like:
|
|
@@ -155,6 +169,54 @@ On success, the envelope looks like:
|
|
|
155
169
|
|
|
156
170
|
On timeout or job failure, `ok` becomes `false`, `error.code` is stable, and `result` still contains the normalized import data needed for retries.
|
|
157
171
|
|
|
172
|
+
## Verification Commands
|
|
173
|
+
|
|
174
|
+
Agents can inspect their own work without opening the web UI. Search commands default to `--ownership own`, which means the selected organization from `--organization`, `CHALKSURF_ORGANIZATION_ID`, or the active profile.
|
|
175
|
+
|
|
176
|
+
List recent jobs, including jobs the UI tray has already viewed or dismissed:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
chalksurf --profile prod-codex job list --include-viewed --include-dismissed --json
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Find failed import jobs for retry:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
chalksurf --profile prod-codex job list \
|
|
186
|
+
--status failed \
|
|
187
|
+
--type exercise_sheet_import \
|
|
188
|
+
--include-viewed \
|
|
189
|
+
--include-dismissed \
|
|
190
|
+
--json
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Verify an imported private sheet by title:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
chalksurf --profile prod-codex sheet search --text "OKTV 2014" --json
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Verify imported exercises by text. Text search requires an explicit language:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
chalksurf --profile prod-codex exercise search \
|
|
203
|
+
--text "binomial theorem" \
|
|
204
|
+
--language english \
|
|
205
|
+
--json
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Import solutions for an already imported sheet when the solution key is separate:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
chalksurf --profile prod-codex sheet import-solutions \
|
|
212
|
+
00000000-0000-4000-8000-000000000001 \
|
|
213
|
+
./solutions.pdf \
|
|
214
|
+
--wait \
|
|
215
|
+
--json
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Use `--ownership public` for public-library checks and `--ownership all` when the agent intentionally wants public results plus selected-organization results.
|
|
219
|
+
|
|
158
220
|
## Codex Workflow Example
|
|
159
221
|
|
|
160
222
|
Target task:
|
|
@@ -168,13 +230,13 @@ Recommended workflow:
|
|
|
168
230
|
3. Codex builds a sheet-import manifest with one `sheets[]` entry per resulting sheet.
|
|
169
231
|
4. Codex groups multiple source files inside one `sources[]` array when they belong to the same resulting sheet.
|
|
170
232
|
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`.
|
|
233
|
+
6. Codex runs `chalksurf --profile prod-codex sheet import --manifest - --wait --json`.
|
|
172
234
|
7. Codex inspects `result.summary.failed`, `result.summary.timedOut`, and `jobs` to decide whether to retry or report failures.
|
|
173
235
|
|
|
174
236
|
Minimal shell shape:
|
|
175
237
|
|
|
176
238
|
```bash
|
|
177
|
-
cat import.json | chalksurf sheet import --manifest - --wait --json
|
|
239
|
+
cat import.json | chalksurf --profile prod-codex sheet import --manifest - --wait --json
|
|
178
240
|
```
|
|
179
241
|
|
|
180
242
|
The discovery step belongs outside the ChalkSurf CLI. The CLI contract starts at a concrete manifest and ends at structured import results.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"organizationId": "org_123",
|
|
3
|
+
"exerciseSheetId": "00000000-0000-4000-8000-000000000333",
|
|
4
|
+
"wait": true,
|
|
5
|
+
"sources": [
|
|
6
|
+
{
|
|
7
|
+
"sourceId": "problem-set-solutions",
|
|
8
|
+
"kind": "local",
|
|
9
|
+
"path": "./imports/problem-set-solutions.pdf",
|
|
10
|
+
"relativePath": "Solutions/problem-set-solutions.pdf"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
package/docs/manifest.md
CHANGED
|
@@ -7,6 +7,7 @@ One manifest file is passed to exactly one command:
|
|
|
7
7
|
- `chalksurf sheet import --manifest <path|->`
|
|
8
8
|
- `chalksurf exercise import --manifest <path|->`
|
|
9
9
|
- `chalksurf exercise import-solution --manifest <path|->`
|
|
10
|
+
- `chalksurf sheet import-solutions --manifest <path|->`
|
|
10
11
|
|
|
11
12
|
Use `--manifest -` to pipe JSON on stdin.
|
|
12
13
|
|
|
@@ -17,9 +18,9 @@ Every manifest is a JSON object with these shared metadata fields:
|
|
|
17
18
|
| Field | Type | Required | Notes |
|
|
18
19
|
| --- | --- | --- | --- |
|
|
19
20
|
| `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
|
+
| `wait` | boolean | no | Behaves like `--wait`. The CLI also accepts `--wait`, which wins if set. Requested translations run as follow-up jobs listed in the import result. |
|
|
21
22
|
|
|
22
|
-
Every source object supports these common fields. For `sheet import`, sources appear inside `sheets[].sources[]`. For the exercise import commands
|
|
23
|
+
Every source object supports these common fields. For `sheet import`, sources appear inside `sheets[].sources[]`. For the exercise import commands and `sheet import-solutions`, they appear in top-level `sources[]`.
|
|
23
24
|
|
|
24
25
|
| Field | Type | Required | Notes |
|
|
25
26
|
| --- | --- | --- | --- |
|
|
@@ -85,11 +86,12 @@ Top-level fields:
|
|
|
85
86
|
| --- | --- | --- | --- |
|
|
86
87
|
| `sources` | array | yes | One or more import sources. |
|
|
87
88
|
| `exerciseSheetId` | string | no | Default target sheet for the imported exercises. Can still be overridden by `--sheet-id`. |
|
|
89
|
+
| `translateTo` | string[] | no | Target translation languages for every imported exercise. Must be unique and non-empty when present. |
|
|
88
90
|
|
|
89
91
|
Per-source fields:
|
|
90
92
|
|
|
91
93
|
- Only the common source fields are allowed.
|
|
92
|
-
- `title` and `translateTo` are rejected for this command.
|
|
94
|
+
- `title` and source-level `translateTo` are rejected for this command.
|
|
93
95
|
|
|
94
96
|
Canonical example:
|
|
95
97
|
|
|
@@ -115,12 +117,32 @@ Canonical example:
|
|
|
115
117
|
- [docs/examples/exercise-solution-import-manifest.json](./examples/exercise-solution-import-manifest.json)
|
|
116
118
|
- [schemas/exercise-solution-import-manifest.schema.json](../schemas/exercise-solution-import-manifest.schema.json)
|
|
117
119
|
|
|
120
|
+
### Exercise Sheet Solution Import
|
|
121
|
+
|
|
122
|
+
Top-level fields:
|
|
123
|
+
|
|
124
|
+
| Field | Type | Required | Notes |
|
|
125
|
+
| --- | --- | --- | --- |
|
|
126
|
+
| `sources` | array | yes | One or more source files containing solutions for some or all exercises in the target sheet. |
|
|
127
|
+
| `exerciseSheetId` | string | no | Default target sheet for the imported solution files. The command still requires a sheet id overall, either here or positionally. |
|
|
128
|
+
|
|
129
|
+
Per-source fields:
|
|
130
|
+
|
|
131
|
+
- Only the common source fields are allowed.
|
|
132
|
+
- `title` and `translateTo` are rejected for this command.
|
|
133
|
+
|
|
134
|
+
Canonical example:
|
|
135
|
+
|
|
136
|
+
- [docs/examples/exercise-sheet-solution-import-manifest.json](./examples/exercise-sheet-solution-import-manifest.json)
|
|
137
|
+
- [schemas/exercise-sheet-solution-import-manifest.schema.json](../schemas/exercise-sheet-solution-import-manifest.schema.json)
|
|
138
|
+
|
|
118
139
|
## Validation Notes
|
|
119
140
|
|
|
120
141
|
The CLI validates more than the JSON schema can express on its own:
|
|
121
142
|
|
|
122
143
|
- `sourceId` values must be unique within one manifest.
|
|
123
144
|
- Sheet import `translateTo` values must be unique within one sheet.
|
|
145
|
+
- Exercise import top-level `translateTo` values must be unique.
|
|
124
146
|
- `relativePath` cannot be empty or contain `..`.
|
|
125
147
|
- Sheet import `targetFolderPath` must be `null` or a normalized folder path without `.` or `..` segments.
|
|
126
148
|
- `directory` imports must resolve to at least one file.
|