@pitcher/cli-scripts 2.11.0 → 2.12.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 +32 -2
- package/dist/index.js +2 -0
- package/dist/src/commands/ld/assessment.d.ts +61 -0
- package/dist/src/commands/ld/assessment.js +235 -0
- package/dist/src/commands/ld/canvasCourse.d.ts +20 -0
- package/dist/src/commands/ld/canvasCourse.js +69 -0
- package/dist/src/commands/ld/cert.d.ts +93 -0
- package/dist/src/commands/ld/cert.js +355 -0
- package/dist/src/commands/ld/course.d.ts +62 -0
- package/dist/src/commands/ld/course.js +308 -0
- package/dist/src/commands/ld/module.d.ts +43 -0
- package/dist/src/commands/ld/module.js +199 -0
- package/dist/src/commands/ld.d.ts +2 -0
- package/dist/src/commands/ld.js +15 -0
- package/dist/src/core/ld.d.ts +38 -0
- package/dist/src/core/ld.js +102 -0
- package/dist/src/core/readInput.d.ts +1 -0
- package/dist/src/core/readInput.js +14 -0
- package/dist/src/mcp/bundled.js +1 -1
- package/dist/src/mcp/prompts/ldCanvasCourse.d.ts +2 -0
- package/dist/src/mcp/prompts/ldCanvasCourse.js +50 -0
- package/dist/src/mcp/prompts/registry.js +8 -1
- package/dist/src/mcp/registry.js +2 -0
- package/dist/src/mcp/resources/ldCanvasDesign.d.ts +3 -0
- package/dist/src/mcp/resources/ldCanvasDesign.js +51 -0
- package/dist/src/mcp/resources/registry.js +2 -1
- package/dist/src/mcp/tools/ld.d.ts +2 -0
- package/dist/src/mcp/tools/ld.js +680 -0
- package/package.json +1 -1
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { ldClientFor, ldData, toLdCliError, requireInstance, pickTruthy } from '../../core/ld.js';
|
|
3
|
+
import { ok, fail } from '../../core/envelope.js';
|
|
4
|
+
import { createCliError } from '../../core/errors.js';
|
|
5
|
+
import { parseJsonInput, resolveTextInput } from '../../core/readInput.js';
|
|
6
|
+
import { emitEnvelope } from '../../core/output.js';
|
|
7
|
+
import { listLimitOffsetParams } from '../../core/pagination.js';
|
|
8
|
+
import { assertIntFlags } from '../../schema/content.js';
|
|
9
|
+
import { withAuthFlags } from '../withAuth.js';
|
|
10
|
+
import { withInstanceFlag } from '../withInstance.js';
|
|
11
|
+
const ADMIN = '/admin/courses';
|
|
12
|
+
// ---- run* functions ----
|
|
13
|
+
export async function runLdCourseList(opts, deps = {}) {
|
|
14
|
+
try {
|
|
15
|
+
const instanceId = requireInstance(opts);
|
|
16
|
+
assertIntFlags(opts); // rejects --limit/--offset that parsed to NaN (matches sibling commands)
|
|
17
|
+
const params = { instanceId, ...listLimitOffsetParams(opts), ...pickTruthy(opts, ['status', 'title', 'tags']) };
|
|
18
|
+
const client = await ldClientFor(opts, deps);
|
|
19
|
+
const res = await client.get(ADMIN, { params });
|
|
20
|
+
return ok(ldData(res));
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
return fail(toLdCliError(e));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function runLdCourseGet(opts, deps = {}) {
|
|
27
|
+
try {
|
|
28
|
+
const instanceId = requireInstance(opts);
|
|
29
|
+
if (!opts.id)
|
|
30
|
+
throw createCliError('VALIDATION', '--id is required');
|
|
31
|
+
const client = await ldClientFor(opts, deps);
|
|
32
|
+
const res = await client.get(`${ADMIN}/${opts.id}`, { params: { instanceId } });
|
|
33
|
+
return ok(ldData(res));
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
return fail(toLdCliError(e));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function runLdCourseTags(opts, deps = {}) {
|
|
40
|
+
try {
|
|
41
|
+
const instanceId = requireInstance(opts);
|
|
42
|
+
const client = await ldClientFor(opts, deps);
|
|
43
|
+
const res = await client.get(`${ADMIN}/tags`, { params: { instanceId } });
|
|
44
|
+
return ok(ldData(res));
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
return fail(toLdCliError(e));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Optional course fields that pass straight through when set. `tags` is a string[] (csv on the CLI);
|
|
51
|
+
// `contentHtml` is handled separately (inline-or-file). An html course REQUIRES contentHtml server-side.
|
|
52
|
+
const COURSE_OPTIONAL_FIELDS = [
|
|
53
|
+
'status',
|
|
54
|
+
'contentSource',
|
|
55
|
+
'completionCriteria',
|
|
56
|
+
'thumbnailUrl',
|
|
57
|
+
'scormPassRequired',
|
|
58
|
+
'tags',
|
|
59
|
+
];
|
|
60
|
+
function addOptional(body, opts) {
|
|
61
|
+
for (const k of COURSE_OPTIONAL_FIELDS)
|
|
62
|
+
if (opts[k] !== undefined)
|
|
63
|
+
body[k] = opts[k];
|
|
64
|
+
// contentHtml can be large HTML — resolve inline (--content-html) or file (--content-html-file).
|
|
65
|
+
const html = resolveTextInput(opts.contentHtml, opts.contentHtmlFile, 'content-html');
|
|
66
|
+
if (html !== undefined)
|
|
67
|
+
body.contentHtml = html;
|
|
68
|
+
}
|
|
69
|
+
// createCourseSchema requires ALL of these IN THE BODY (validateBody parses req.body alone; the
|
|
70
|
+
// controller reads instanceId from the query AND strips a body copy, but validation runs first,
|
|
71
|
+
// so instanceId must be in the body too). description ≥10, level enum, duration/sectionsCount ints.
|
|
72
|
+
export async function runLdCourseCreate(opts, deps = {}) {
|
|
73
|
+
try {
|
|
74
|
+
const instanceId = requireInstance(opts);
|
|
75
|
+
if (!opts.title)
|
|
76
|
+
throw createCliError('VALIDATION', '--title is required');
|
|
77
|
+
if (!opts.description)
|
|
78
|
+
throw createCliError('VALIDATION', '--description is required (min 10 chars)');
|
|
79
|
+
if (!opts.level)
|
|
80
|
+
throw createCliError('VALIDATION', '--level is required (Beginner|Intermediate|Advanced)');
|
|
81
|
+
if (opts.duration === undefined)
|
|
82
|
+
throw createCliError('VALIDATION', '--duration is required (minutes, integer)');
|
|
83
|
+
if (opts.sectionsCount === undefined)
|
|
84
|
+
throw createCliError('VALIDATION', '--sections-count is required (integer)');
|
|
85
|
+
const body = {
|
|
86
|
+
instanceId,
|
|
87
|
+
title: opts.title,
|
|
88
|
+
description: opts.description,
|
|
89
|
+
level: opts.level,
|
|
90
|
+
duration: opts.duration,
|
|
91
|
+
sectionsCount: opts.sectionsCount,
|
|
92
|
+
};
|
|
93
|
+
addOptional(body, opts);
|
|
94
|
+
// createCourseSchema (backend) requires contentHtml for an html course (the default source).
|
|
95
|
+
// Fail fast with a clear message rather than let the backend 400 with an opaque one.
|
|
96
|
+
const source = body.contentSource ?? 'html';
|
|
97
|
+
if (source === 'html' && body.contentHtml === undefined) {
|
|
98
|
+
throw createCliError('VALIDATION', '--content-html (or --content-html-file) is required for an html course');
|
|
99
|
+
}
|
|
100
|
+
const client = await ldClientFor(opts, deps);
|
|
101
|
+
const res = await client.post(ADMIN, body, { params: { instanceId } });
|
|
102
|
+
return ok(ldData(res));
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
return fail(toLdCliError(e));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// updateCourseSchema is createCourseSchema.partial() — every field optional. Send only what changed.
|
|
109
|
+
export async function runLdCourseUpdate(opts, deps = {}) {
|
|
110
|
+
try {
|
|
111
|
+
const instanceId = requireInstance(opts);
|
|
112
|
+
if (!opts.id)
|
|
113
|
+
throw createCliError('VALIDATION', '--id is required');
|
|
114
|
+
const body = {};
|
|
115
|
+
for (const k of ['title', 'description', 'level', 'duration', 'sectionsCount']) {
|
|
116
|
+
if (opts[k] !== undefined)
|
|
117
|
+
body[k] = opts[k];
|
|
118
|
+
}
|
|
119
|
+
addOptional(body, opts);
|
|
120
|
+
const client = await ldClientFor(opts, deps);
|
|
121
|
+
const res = await client.put(`${ADMIN}/${opts.id}`, body, { params: { instanceId } });
|
|
122
|
+
return ok(ldData(res));
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
return fail(toLdCliError(e));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export async function runLdCourseDelete(opts, deps = {}) {
|
|
129
|
+
try {
|
|
130
|
+
const instanceId = requireInstance(opts);
|
|
131
|
+
if (!opts.id)
|
|
132
|
+
throw createCliError('VALIDATION', '--id is required');
|
|
133
|
+
const client = await ldClientFor(opts, deps);
|
|
134
|
+
const res = await client.delete(`${ADMIN}/${opts.id}`, { params: { instanceId } });
|
|
135
|
+
return ok(ldData(res) ?? { id: opts.id, deleted: true });
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
return fail(toLdCliError(e));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function patchCourseState(path, opts, deps) {
|
|
142
|
+
try {
|
|
143
|
+
const instanceId = requireInstance(opts);
|
|
144
|
+
if (!opts.id)
|
|
145
|
+
throw createCliError('VALIDATION', '--id is required');
|
|
146
|
+
const client = await ldClientFor(opts, deps);
|
|
147
|
+
const res = await client.patch(`${ADMIN}/${opts.id}/${path}`, {}, { params: { instanceId } });
|
|
148
|
+
return ok(ldData(res));
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
return fail(toLdCliError(e));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export const runLdCoursePublish = (o, d = {}) => patchCourseState('publish', o, d);
|
|
155
|
+
export const runLdCourseUnpublish = (o, d = {}) => patchCourseState('unpublish', o, d);
|
|
156
|
+
export async function runLdCourseGetCanvas(opts, deps = {}) {
|
|
157
|
+
try {
|
|
158
|
+
const instanceId = requireInstance(opts);
|
|
159
|
+
if (!opts.id)
|
|
160
|
+
throw createCliError('VALIDATION', '--id is required');
|
|
161
|
+
const client = await ldClientFor(opts, deps);
|
|
162
|
+
const res = await client.get(`/app/courses/${opts.id}/canvas`, { params: { instanceId } });
|
|
163
|
+
return ok(ldData(res));
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
return fail(toLdCliError(e));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export async function runLdCourseSetCanvas(opts, deps = {}) {
|
|
170
|
+
try {
|
|
171
|
+
const instanceId = requireInstance(opts);
|
|
172
|
+
if (!opts.id)
|
|
173
|
+
throw createCliError('VALIDATION', '--id is required');
|
|
174
|
+
const parsed = parseJsonInput(opts.canvas, opts.canvasFile, 'canvas');
|
|
175
|
+
// The L&D backend requires `canvas` to be a JSON OBJECT — a full canvas whose
|
|
176
|
+
// `content.data` holds the flat component array (verified live against the PIT-7609
|
|
177
|
+
// Lambda: a bare array 400s "Validation failed"). The flat content.data array is the
|
|
178
|
+
// natural authoring unit, so accept it and wrap it; an already-shaped object (with its
|
|
179
|
+
// own `theme` etc.) passes through untouched.
|
|
180
|
+
const canvas = Array.isArray(parsed) ? { content: { data: parsed } } : parsed;
|
|
181
|
+
// NB: no requireRecompletion here — the L&D backend (PIT-7609) rejects it for the org
|
|
182
|
+
// x-api-key (the only auth this CLI has), since a bulk rep-completion wipe is a
|
|
183
|
+
// human-admin-only action. Exposing it would only ever 403.
|
|
184
|
+
const client = await ldClientFor(opts, deps);
|
|
185
|
+
const res = await client.patch(`/app/courses/${opts.id}/canvas`, { canvas }, { params: { instanceId } });
|
|
186
|
+
return ok(ldData(res));
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
return fail(toLdCliError(e));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
export async function runLdCourseListModules(opts, deps = {}) {
|
|
193
|
+
try {
|
|
194
|
+
const instanceId = requireInstance(opts);
|
|
195
|
+
if (!opts.id)
|
|
196
|
+
throw createCliError('VALIDATION', '--id is required');
|
|
197
|
+
const client = await ldClientFor(opts, deps);
|
|
198
|
+
const res = await client.get(`${ADMIN}/${opts.id}/modules`, { params: { instanceId } });
|
|
199
|
+
return ok(ldData(res));
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
return fail(toLdCliError(e));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
export async function runLdCourseAddToModules(opts, deps = {}) {
|
|
206
|
+
try {
|
|
207
|
+
const instanceId = requireInstance(opts);
|
|
208
|
+
if (!opts.id)
|
|
209
|
+
throw createCliError('VALIDATION', '--id is required');
|
|
210
|
+
if (!opts.moduleIds?.length)
|
|
211
|
+
throw createCliError('VALIDATION', '--module-ids is required (comma-separated)');
|
|
212
|
+
const client = await ldClientFor(opts, deps);
|
|
213
|
+
const res = await client.post(`${ADMIN}/${opts.id}/modules`, { moduleIds: opts.moduleIds }, { params: { instanceId } });
|
|
214
|
+
return ok(ldData(res));
|
|
215
|
+
}
|
|
216
|
+
catch (e) {
|
|
217
|
+
return fail(toLdCliError(e));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
export async function runLdCourseRemoveFromModule(opts, deps = {}) {
|
|
221
|
+
try {
|
|
222
|
+
const instanceId = requireInstance(opts);
|
|
223
|
+
if (!opts.id || !opts.moduleId)
|
|
224
|
+
throw createCliError('VALIDATION', '--id and --module-id are required');
|
|
225
|
+
const client = await ldClientFor(opts, deps);
|
|
226
|
+
const res = await client.delete(`${ADMIN}/${opts.id}/modules/${opts.moduleId}`, { params: { instanceId } });
|
|
227
|
+
return ok(ldData(res) ?? { removed: true });
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
return fail(toLdCliError(e));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
export async function runLdCourseListAssessments(opts, deps = {}) {
|
|
234
|
+
try {
|
|
235
|
+
const instanceId = requireInstance(opts);
|
|
236
|
+
if (!opts.id)
|
|
237
|
+
throw createCliError('VALIDATION', '--id is required');
|
|
238
|
+
const client = await ldClientFor(opts, deps);
|
|
239
|
+
const res = await client.get(`${ADMIN}/${opts.id}/assessments`, { params: { instanceId } });
|
|
240
|
+
return ok(ldData(res));
|
|
241
|
+
}
|
|
242
|
+
catch (e) {
|
|
243
|
+
return fail(toLdCliError(e));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
export async function runLdCourseLinkAssessments(opts, deps = {}) {
|
|
247
|
+
try {
|
|
248
|
+
const instanceId = requireInstance(opts);
|
|
249
|
+
if (!opts.id)
|
|
250
|
+
throw createCliError('VALIDATION', '--id is required');
|
|
251
|
+
if (!opts.assessmentIds?.length)
|
|
252
|
+
throw createCliError('VALIDATION', '--assessment-ids is required (comma-separated)');
|
|
253
|
+
const client = await ldClientFor(opts, deps);
|
|
254
|
+
const res = await client.post(`${ADMIN}/${opts.id}/assessments`, { assessmentIds: opts.assessmentIds }, { params: { instanceId } });
|
|
255
|
+
return ok(ldData(res));
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
return fail(toLdCliError(e));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
export async function runLdCourseUnlinkAssessment(opts, deps = {}) {
|
|
262
|
+
try {
|
|
263
|
+
const instanceId = requireInstance(opts);
|
|
264
|
+
if (!opts.id || !opts.assessmentId)
|
|
265
|
+
throw createCliError('VALIDATION', '--id and --assessment-id are required');
|
|
266
|
+
const client = await ldClientFor(opts, deps);
|
|
267
|
+
const res = await client.delete(`${ADMIN}/${opts.id}/assessments/${opts.assessmentId}`, { params: { instanceId } });
|
|
268
|
+
return ok(ldData(res) ?? { removed: true });
|
|
269
|
+
}
|
|
270
|
+
catch (e) {
|
|
271
|
+
return fail(toLdCliError(e));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// ---- commander subcommand ----
|
|
275
|
+
// A comma-separated → string[] parser for repeatable id / tag flags. Exported so the
|
|
276
|
+
// other resource subcommands (module/cert/assessment) reuse the exact same parser.
|
|
277
|
+
export const csv = (v) => v.split(',').map((s) => s.trim()).filter(Boolean);
|
|
278
|
+
// Boolean flag parser — `--is-required true|false`. Lenient (only 'true' is true) so a bad value
|
|
279
|
+
// can't throw an InvalidArgumentError before the action (which would bypass the JSON envelope).
|
|
280
|
+
export const boolFlag = (v) => v === 'true';
|
|
281
|
+
// Per-subcommand auth + instance + ld-url, matching the shipped pattern (flags AFTER the
|
|
282
|
+
// verb). NEVER use .requiredOption — commander aborts before the action and prints plain
|
|
283
|
+
// text / exit 1, breaking the --output json envelope contract (house rule; PIT-7412). The
|
|
284
|
+
// run* functions carry the real required-field guards (createCliError VALIDATION → exit 3).
|
|
285
|
+
export const ldFlags = (c) => withInstanceFlag(withAuthFlags(c)).option('--ld-url <url>', 'Override L&D API base URL (CLI only)');
|
|
286
|
+
// Shared commander action wrapper: merge global auth flags into the run* opts and render the
|
|
287
|
+
// envelope. Exported so the module/cert/assessment subcommands reuse it (like csv/ldFlags).
|
|
288
|
+
export const render = (fn) => async (o, c) => emitEnvelope(await fn({ ...c.optsWithGlobals(), ...o }), c.optsWithGlobals());
|
|
289
|
+
export function createCourseSubcommand() {
|
|
290
|
+
const cmd = new Command('course').description('Author L&D courses');
|
|
291
|
+
ldFlags(cmd.command('list')).option('--status <s>').option('--title <t>').option('--tags <t>', 'Comma-separated', csv).option('--limit <n>', '', Number).option('--offset <n>', '', Number).action(render(runLdCourseList));
|
|
292
|
+
ldFlags(cmd.command('get')).option('--id <id>', 'Course ID (required)').action(render(runLdCourseGet));
|
|
293
|
+
ldFlags(cmd.command('tags')).action(render(runLdCourseTags));
|
|
294
|
+
ldFlags(cmd.command('create')).option('--title <t>', 'required').option('--description <d>', 'required, min 10 chars').option('--level <l>', 'Beginner|Intermediate|Advanced (required)').option('--duration <n>', 'minutes, integer (required)', Number).option('--sections-count <n>', 'integer (required)', Number).option('--content-source <s>', 'html|scorm|canvas (default html)').option('--content-html <html>', 'Inline HTML body (required for an html course)').option('--content-html-file <path>').option('--completion-criteria <c>').option('--thumbnail-url <url>').option('--scorm-pass-required <bool>', 'true|false', boolFlag).option('--status <s>').option('--tags <t>', 'Comma-separated', csv).action(render(runLdCourseCreate));
|
|
295
|
+
ldFlags(cmd.command('update')).option('--id <id>', 'required').option('--title <t>').option('--description <d>').option('--level <l>').option('--duration <n>', '', Number).option('--sections-count <n>', '', Number).option('--content-source <s>').option('--content-html <html>').option('--content-html-file <path>').option('--completion-criteria <c>').option('--thumbnail-url <url>').option('--scorm-pass-required <bool>', 'true|false', boolFlag).option('--status <s>').option('--tags <t>', 'Comma-separated', csv).action(render(runLdCourseUpdate));
|
|
296
|
+
ldFlags(cmd.command('delete')).option('--id <id>', 'required').action(render(runLdCourseDelete));
|
|
297
|
+
ldFlags(cmd.command('publish')).option('--id <id>', 'required').action(render(runLdCoursePublish));
|
|
298
|
+
ldFlags(cmd.command('unpublish')).option('--id <id>', 'required').action(render(runLdCourseUnpublish));
|
|
299
|
+
ldFlags(cmd.command('get-canvas')).option('--id <id>', 'required').action(render(runLdCourseGetCanvas));
|
|
300
|
+
ldFlags(cmd.command('set-canvas')).option('--id <id>', 'required').option('--canvas <json>', 'Inline canvas JSON: object { content: { data: [...] }, theme? } (a bare content.data array is auto-wrapped)').option('--canvas-file <path>').action(render(runLdCourseSetCanvas));
|
|
301
|
+
ldFlags(cmd.command('list-modules')).option('--id <id>', 'required').action(render(runLdCourseListModules));
|
|
302
|
+
ldFlags(cmd.command('add-to-modules')).option('--id <id>', 'required').option('--module-ids <ids>', 'Comma-separated (required)', csv).action(render(runLdCourseAddToModules));
|
|
303
|
+
ldFlags(cmd.command('remove-from-module')).option('--id <id>', 'required').option('--module-id <id>', 'required').action(render(runLdCourseRemoveFromModule));
|
|
304
|
+
ldFlags(cmd.command('list-assessments')).option('--id <id>', 'required').action(render(runLdCourseListAssessments));
|
|
305
|
+
ldFlags(cmd.command('link-assessments')).option('--id <id>', 'required').option('--assessment-ids <ids>', 'Comma-separated (required)', csv).action(render(runLdCourseLinkAssessments));
|
|
306
|
+
ldFlags(cmd.command('unlink-assessment')).option('--id <id>', 'required').option('--assessment-id <id>', 'required').action(render(runLdCourseUnlinkAssessment));
|
|
307
|
+
return cmd;
|
|
308
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { type LdCommonOpts, type LdDeps } from '../../core/ld.js';
|
|
3
|
+
import { type Envelope } from '../../core/envelope.js';
|
|
4
|
+
export declare function runLdModuleTags(opts: LdCommonOpts, deps?: LdDeps): Promise<Envelope<unknown>>;
|
|
5
|
+
export declare function runLdModuleList(opts: LdCommonOpts & {
|
|
6
|
+
status?: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
tags?: string[];
|
|
9
|
+
limit?: number;
|
|
10
|
+
offset?: number;
|
|
11
|
+
}, deps?: LdDeps): Promise<Envelope<unknown>>;
|
|
12
|
+
export declare function runLdModuleGet(opts: LdCommonOpts & {
|
|
13
|
+
id?: string;
|
|
14
|
+
}, deps?: LdDeps): Promise<Envelope<unknown>>;
|
|
15
|
+
export declare function runLdModuleCreate(opts: LdCommonOpts & Record<string, unknown>, deps?: LdDeps): Promise<Envelope<unknown>>;
|
|
16
|
+
export declare function runLdModuleUpdate(opts: LdCommonOpts & {
|
|
17
|
+
id?: string;
|
|
18
|
+
} & Record<string, unknown>, deps?: LdDeps): Promise<Envelope<unknown>>;
|
|
19
|
+
export declare function runLdModuleDelete(opts: LdCommonOpts & {
|
|
20
|
+
id?: string;
|
|
21
|
+
}, deps?: LdDeps): Promise<Envelope<unknown>>;
|
|
22
|
+
export declare function runLdModuleListCourses(opts: LdCommonOpts & {
|
|
23
|
+
id?: string;
|
|
24
|
+
}, deps?: LdDeps): Promise<Envelope<unknown>>;
|
|
25
|
+
export declare function runLdModuleAddCourses(opts: LdCommonOpts & {
|
|
26
|
+
id?: string;
|
|
27
|
+
courseIds?: string[];
|
|
28
|
+
}, deps?: LdDeps): Promise<Envelope<unknown>>;
|
|
29
|
+
export declare function runLdModuleReorderCourses(opts: LdCommonOpts & {
|
|
30
|
+
id?: string;
|
|
31
|
+
courseId?: string;
|
|
32
|
+
newOrder?: number;
|
|
33
|
+
}, deps?: LdDeps): Promise<Envelope<unknown>>;
|
|
34
|
+
export declare function runLdModuleUpdateCourse(opts: LdCommonOpts & {
|
|
35
|
+
id?: string;
|
|
36
|
+
courseId?: string;
|
|
37
|
+
isRequired?: boolean;
|
|
38
|
+
}, deps?: LdDeps): Promise<Envelope<unknown>>;
|
|
39
|
+
export declare function runLdModuleRemoveCourse(opts: LdCommonOpts & {
|
|
40
|
+
id?: string;
|
|
41
|
+
courseId?: string;
|
|
42
|
+
}, deps?: LdDeps): Promise<Envelope<unknown>>;
|
|
43
|
+
export declare function createModuleSubcommand(): Command;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { ldClientFor, ldData, toLdCliError, requireInstance, pickTruthy } from '../../core/ld.js';
|
|
3
|
+
import { ok, fail } from '../../core/envelope.js';
|
|
4
|
+
import { createCliError } from '../../core/errors.js';
|
|
5
|
+
import { listLimitOffsetParams } from '../../core/pagination.js';
|
|
6
|
+
import { assertIntFlags } from '../../schema/content.js';
|
|
7
|
+
import { csv, ldFlags, render, boolFlag } from './course.js';
|
|
8
|
+
const ADMIN = '/admin/modules';
|
|
9
|
+
// ---- run* functions ----
|
|
10
|
+
export async function runLdModuleTags(opts, deps = {}) {
|
|
11
|
+
try {
|
|
12
|
+
const instanceId = requireInstance(opts);
|
|
13
|
+
const client = await ldClientFor(opts, deps);
|
|
14
|
+
const res = await client.get(`${ADMIN}/tags`, { params: { instanceId } });
|
|
15
|
+
return ok(ldData(res));
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
return fail(toLdCliError(e));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function runLdModuleList(opts, deps = {}) {
|
|
22
|
+
try {
|
|
23
|
+
const instanceId = requireInstance(opts);
|
|
24
|
+
assertIntFlags(opts); // rejects --limit/--offset that parsed to NaN (matches sibling commands)
|
|
25
|
+
const params = { instanceId, ...listLimitOffsetParams(opts), ...pickTruthy(opts, ['status', 'title', 'tags']) };
|
|
26
|
+
const client = await ldClientFor(opts, deps);
|
|
27
|
+
const res = await client.get(ADMIN, { params });
|
|
28
|
+
return ok(ldData(res));
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
return fail(toLdCliError(e));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function runLdModuleGet(opts, deps = {}) {
|
|
35
|
+
try {
|
|
36
|
+
const instanceId = requireInstance(opts);
|
|
37
|
+
if (!opts.id)
|
|
38
|
+
throw createCliError('VALIDATION', '--id is required');
|
|
39
|
+
const client = await ldClientFor(opts, deps);
|
|
40
|
+
const res = await client.get(`${ADMIN}/${opts.id}`, { params: { instanceId } });
|
|
41
|
+
return ok(ldData(res));
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
return fail(toLdCliError(e));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// createModuleSchema requires title, subtitle AND description in the body (validateBody parses
|
|
48
|
+
// req.body alone; the controller reads instanceId from the query but validation runs first, so
|
|
49
|
+
// instanceId must be in the body too — same shape as createCourseSchema).
|
|
50
|
+
export async function runLdModuleCreate(opts, deps = {}) {
|
|
51
|
+
try {
|
|
52
|
+
const instanceId = requireInstance(opts);
|
|
53
|
+
if (!opts.title)
|
|
54
|
+
throw createCliError('VALIDATION', '--title is required');
|
|
55
|
+
if (!opts.subtitle)
|
|
56
|
+
throw createCliError('VALIDATION', '--subtitle is required');
|
|
57
|
+
if (!opts.description)
|
|
58
|
+
throw createCliError('VALIDATION', '--description is required');
|
|
59
|
+
const body = {
|
|
60
|
+
instanceId,
|
|
61
|
+
title: opts.title,
|
|
62
|
+
subtitle: opts.subtitle,
|
|
63
|
+
description: opts.description,
|
|
64
|
+
};
|
|
65
|
+
if (opts.tags !== undefined)
|
|
66
|
+
body.tags = opts.tags;
|
|
67
|
+
const client = await ldClientFor(opts, deps);
|
|
68
|
+
const res = await client.post(ADMIN, body, { params: { instanceId } });
|
|
69
|
+
return ok(ldData(res));
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
return fail(toLdCliError(e));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// updateModuleSchema is createModuleSchema.partial() — every field optional. Send only what changed.
|
|
76
|
+
export async function runLdModuleUpdate(opts, deps = {}) {
|
|
77
|
+
try {
|
|
78
|
+
const instanceId = requireInstance(opts);
|
|
79
|
+
if (!opts.id)
|
|
80
|
+
throw createCliError('VALIDATION', '--id is required');
|
|
81
|
+
const body = {};
|
|
82
|
+
for (const k of ['title', 'subtitle', 'description', 'tags']) {
|
|
83
|
+
if (opts[k] !== undefined)
|
|
84
|
+
body[k] = opts[k];
|
|
85
|
+
}
|
|
86
|
+
const client = await ldClientFor(opts, deps);
|
|
87
|
+
const res = await client.put(`${ADMIN}/${opts.id}`, body, { params: { instanceId } });
|
|
88
|
+
return ok(ldData(res));
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
return fail(toLdCliError(e));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export async function runLdModuleDelete(opts, deps = {}) {
|
|
95
|
+
try {
|
|
96
|
+
const instanceId = requireInstance(opts);
|
|
97
|
+
if (!opts.id)
|
|
98
|
+
throw createCliError('VALIDATION', '--id is required');
|
|
99
|
+
const client = await ldClientFor(opts, deps);
|
|
100
|
+
const res = await client.delete(`${ADMIN}/${opts.id}`, { params: { instanceId } });
|
|
101
|
+
return ok(ldData(res) ?? { id: opts.id, deleted: true });
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
return fail(toLdCliError(e));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export async function runLdModuleListCourses(opts, deps = {}) {
|
|
108
|
+
try {
|
|
109
|
+
const instanceId = requireInstance(opts);
|
|
110
|
+
if (!opts.id)
|
|
111
|
+
throw createCliError('VALIDATION', '--id is required');
|
|
112
|
+
const client = await ldClientFor(opts, deps);
|
|
113
|
+
const res = await client.get(`${ADMIN}/${opts.id}/courses`, { params: { instanceId } });
|
|
114
|
+
return ok(ldData(res));
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
return fail(toLdCliError(e));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// The API expects a `courses` array of `{ id }` objects — NOT a flat `{ courseIds }` array.
|
|
121
|
+
export async function runLdModuleAddCourses(opts, deps = {}) {
|
|
122
|
+
try {
|
|
123
|
+
const instanceId = requireInstance(opts);
|
|
124
|
+
if (!opts.id)
|
|
125
|
+
throw createCliError('VALIDATION', '--id is required');
|
|
126
|
+
if (!opts.courseIds?.length)
|
|
127
|
+
throw createCliError('VALIDATION', '--course-ids is required (comma-separated)');
|
|
128
|
+
const body = { courses: opts.courseIds.map((id) => ({ id })) };
|
|
129
|
+
const client = await ldClientFor(opts, deps);
|
|
130
|
+
const res = await client.post(`${ADMIN}/${opts.id}/courses`, body, { params: { instanceId } });
|
|
131
|
+
return ok(ldData(res));
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
return fail(toLdCliError(e));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export async function runLdModuleReorderCourses(opts, deps = {}) {
|
|
138
|
+
try {
|
|
139
|
+
const instanceId = requireInstance(opts);
|
|
140
|
+
if (!opts.id)
|
|
141
|
+
throw createCliError('VALIDATION', '--id is required');
|
|
142
|
+
if (!opts.courseId)
|
|
143
|
+
throw createCliError('VALIDATION', '--course-id is required');
|
|
144
|
+
if (opts.newOrder === undefined || !Number.isInteger(opts.newOrder))
|
|
145
|
+
throw createCliError('VALIDATION', '--new-order is required (integer)');
|
|
146
|
+
const body = { courseId: opts.courseId, newOrder: opts.newOrder };
|
|
147
|
+
const client = await ldClientFor(opts, deps);
|
|
148
|
+
const res = await client.put(`${ADMIN}/${opts.id}/courses/order`, body, { params: { instanceId } });
|
|
149
|
+
return ok(ldData(res));
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
return fail(toLdCliError(e));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export async function runLdModuleUpdateCourse(opts, deps = {}) {
|
|
156
|
+
try {
|
|
157
|
+
const instanceId = requireInstance(opts);
|
|
158
|
+
if (!opts.id || !opts.courseId)
|
|
159
|
+
throw createCliError('VALIDATION', '--id and --course-id are required');
|
|
160
|
+
const body = {};
|
|
161
|
+
if (opts.isRequired !== undefined)
|
|
162
|
+
body.isRequired = opts.isRequired;
|
|
163
|
+
const client = await ldClientFor(opts, deps);
|
|
164
|
+
const res = await client.put(`${ADMIN}/${opts.id}/courses/${opts.courseId}`, body, { params: { instanceId } });
|
|
165
|
+
return ok(ldData(res));
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
return fail(toLdCliError(e));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
export async function runLdModuleRemoveCourse(opts, deps = {}) {
|
|
172
|
+
try {
|
|
173
|
+
const instanceId = requireInstance(opts);
|
|
174
|
+
if (!opts.id || !opts.courseId)
|
|
175
|
+
throw createCliError('VALIDATION', '--id and --course-id are required');
|
|
176
|
+
const client = await ldClientFor(opts, deps);
|
|
177
|
+
const res = await client.delete(`${ADMIN}/${opts.id}/courses/${opts.courseId}`, { params: { instanceId } });
|
|
178
|
+
return ok(ldData(res) ?? { removed: true });
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
return fail(toLdCliError(e));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// ---- commander subcommand ----
|
|
185
|
+
export function createModuleSubcommand() {
|
|
186
|
+
const cmd = new Command('module').description('Author L&D modules');
|
|
187
|
+
ldFlags(cmd.command('tags')).action(render(runLdModuleTags));
|
|
188
|
+
ldFlags(cmd.command('list')).option('--status <s>').option('--title <t>').option('--tags <t>', 'Comma-separated', csv).option('--limit <n>', '', Number).option('--offset <n>', '', Number).action(render(runLdModuleList));
|
|
189
|
+
ldFlags(cmd.command('get')).option('--id <id>', 'Module ID (required)').action(render(runLdModuleGet));
|
|
190
|
+
ldFlags(cmd.command('create')).option('--title <t>', 'required').option('--subtitle <s>', 'required').option('--description <d>', 'required').option('--tags <t>', 'Comma-separated', csv).action(render(runLdModuleCreate));
|
|
191
|
+
ldFlags(cmd.command('update')).option('--id <id>', 'required').option('--title <t>').option('--subtitle <s>').option('--description <d>').option('--tags <t>', 'Comma-separated', csv).action(render(runLdModuleUpdate));
|
|
192
|
+
ldFlags(cmd.command('delete')).option('--id <id>', 'required').action(render(runLdModuleDelete));
|
|
193
|
+
ldFlags(cmd.command('list-courses')).option('--id <id>', 'required').action(render(runLdModuleListCourses));
|
|
194
|
+
ldFlags(cmd.command('add-courses')).option('--id <id>', 'required').option('--course-ids <ids>', 'Comma-separated (required)', csv).action(render(runLdModuleAddCourses));
|
|
195
|
+
ldFlags(cmd.command('reorder-courses')).option('--id <id>', 'required').option('--course-id <id>', 'required').option('--new-order <n>', 'integer (required)', Number).action(render(runLdModuleReorderCourses));
|
|
196
|
+
ldFlags(cmd.command('update-course')).option('--id <id>', 'required').option('--course-id <id>', 'required').option('--is-required <bool>', 'true|false', boolFlag).action(render(runLdModuleUpdateCourse));
|
|
197
|
+
ldFlags(cmd.command('remove-course')).option('--id <id>', 'required').option('--course-id <id>', 'required').action(render(runLdModuleRemoveCourse));
|
|
198
|
+
return cmd;
|
|
199
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { createCourseSubcommand } from './ld/course.js';
|
|
3
|
+
import { createCanvasCourseSubcommand } from './ld/canvasCourse.js';
|
|
4
|
+
import { createModuleSubcommand } from './ld/module.js';
|
|
5
|
+
import { createCertSubcommand } from './ld/cert.js';
|
|
6
|
+
import { createAssessmentSubcommand } from './ld/assessment.js';
|
|
7
|
+
export function createLdCommand() {
|
|
8
|
+
const cmd = new Command('ld').description('Author L&D (MicroLearn) content via the L&D backend');
|
|
9
|
+
cmd.addCommand(createCourseSubcommand());
|
|
10
|
+
cmd.addCommand(createCanvasCourseSubcommand());
|
|
11
|
+
cmd.addCommand(createModuleSubcommand());
|
|
12
|
+
cmd.addCommand(createCertSubcommand());
|
|
13
|
+
cmd.addCommand(createAssessmentSubcommand());
|
|
14
|
+
return cmd;
|
|
15
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AxiosInstance } from 'axios';
|
|
2
|
+
import { type CommonOpts } from './http.js';
|
|
3
|
+
import { type CliError } from './errors.js';
|
|
4
|
+
export interface LdCommonOpts extends CommonOpts {
|
|
5
|
+
instanceId?: string;
|
|
6
|
+
ldUrl?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface LdDeps {
|
|
9
|
+
ldClient?: AxiosInstance;
|
|
10
|
+
platformClient?: AxiosInstance;
|
|
11
|
+
}
|
|
12
|
+
/** Test-only: reset the resolved-base-URL cache between cases. */
|
|
13
|
+
export declare function __clearLdBaseUrlCache(): void;
|
|
14
|
+
/** Every L&D run* requires an instance; returns it narrowed to string, else VALIDATION. */
|
|
15
|
+
export declare function requireInstance(opts: LdCommonOpts): string;
|
|
16
|
+
/** Copy the truthy values of `keys` from `opts` into a params object (shared list-filter pick). */
|
|
17
|
+
export declare function pickTruthy<T extends object>(opts: T, keys: readonly (keyof T)[]): Partial<Record<keyof T, unknown>>;
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the standalone L&D Lambda base URL. Order: --ld-url flag → NG_LD_API_URL env →
|
|
20
|
+
* ld-app app config (api_base_url) via the platform client. Cached per (org, instanceId).
|
|
21
|
+
* The org key can't be region-inferred, so app config (region-correct) is the default.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveLdBaseUrl(opts: LdCommonOpts, deps?: LdDeps, env?: NodeJS.ProcessEnv): Promise<string>;
|
|
24
|
+
/** Build (or inject, in tests) an axios client pointed at the resolved L&D Lambda base. */
|
|
25
|
+
export declare function ldClientFor(opts: LdCommonOpts, deps?: LdDeps, env?: NodeJS.ProcessEnv): Promise<AxiosInstance>;
|
|
26
|
+
/**
|
|
27
|
+
* Unwrap the L&D success envelope `{ success, data, message? }` to its resource payload.
|
|
28
|
+
* Falls back to the raw body if it is not enveloped (defensive).
|
|
29
|
+
*/
|
|
30
|
+
export declare function ldData(res: {
|
|
31
|
+
data?: unknown;
|
|
32
|
+
}): unknown;
|
|
33
|
+
/**
|
|
34
|
+
* Map an L&D axios error to a clean CliError. The L&D error body is `{ success:false,
|
|
35
|
+
* message, statusCode }`; the generic `extractApiMessage` would concatenate every key, so
|
|
36
|
+
* read `.message` explicitly and derive the code from the HTTP status. Falls back to `toCliError`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function toLdCliError(e: unknown): CliError;
|