@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 CHANGED
@@ -12,7 +12,7 @@ p command --help
12
12
 
13
13
  ## MCP server
14
14
 
15
- Run `p mcp` to start a stdio [MCP](https://modelcontextprotocol.io) server that exposes this CLI's commands as tools to an MCP-capable host (Claude Desktop, Claude Code, etc). It wraps 50 tools, one per CLI `run*` function: every command above (including `dsr create` + `dsr bind-landing`) except the five local `scaffold` subcommands (which just print/write a starter file and make no request). `dev-sync` is exposed as a one-shot sync only; the continuous `watch` mode is not available as a tool.
15
+ Run `p mcp` to start a stdio [MCP](https://modelcontextprotocol.io) server that exposes this CLI's commands as tools to an MCP-capable host (Claude Desktop, Claude Code, etc). It wraps 113 tools, one per CLI `run*` function: every command above (including `dsr create` + `dsr bind-landing` and the full `ld` surface) except the five local `scaffold` subcommands (which just print/write a starter file and make no request). `dev-sync` is exposed as a one-shot sync only; the continuous `watch` mode is not available as a tool.
16
16
 
17
17
  ```bash
18
18
  p mcp
@@ -55,7 +55,7 @@ yarn mcpb:pack # → pitcher.mcpb (validated + packed)
55
55
 
56
56
  Install by double-clicking `pitcher.mcpb` (or Settings → Extensions → Install Extension). Claude Desktop prompts for the API key (stored in the OS keychain) and organization; both are editable afterward, so switching orgs needs no reinstall.
57
57
 
58
- The bundle reuses the same server as `p mcp` but registers a **content-tool subset (47)** — it omits the developer-only `publish-app` and `dev-sync`/`dev-sync-revert`. The `organization` field accepts a subdomain (`acme`), a bare hostname (`acme.my.pitcher.com`), or a full `https://` URL. Build details (esbuild single-file, version injection) live in `mcpb/` and `CLAUDE.md`.
58
+ The bundle reuses the same server as `p mcp` but registers a **content-tool subset (110)** — it omits the developer-only `publish-app` and `dev-sync`/`dev-sync-revert`. The `organization` field accepts a subdomain (`acme`), a bare hostname (`acme.my.pitcher.com`), or a full `https://` URL. Build details (esbuild single-file, version injection) live in `mcpb/` and `CLAUDE.md`.
59
59
 
60
60
  This section is a reference only. For app-development conventions (modules, settings, mount points, SFDC), see the `pitcher-apps` skill, not this README.
61
61
 
@@ -491,3 +491,33 @@ p dsr scaffold --out share.json
491
491
  ```
492
492
 
493
493
  Creating needs a share-capable key; **binding a landing page currently needs an editor key** (PIT-7525 will open it to reps for their own DSRs) — a denied bind returns the link with a warning. Discover landing template IDs via `p dsr-landing list`.
494
+
495
+ ## To run the `ld` command:
496
+
497
+ Author L&D (MicroLearn) content — courses, modules, certificates, and assessments — against the standalone L&D backend. The general form is `p ld <resource> <verb>`, plus the top-level `p ld create-canvas` recipe (create → set-canvas → publish in one step). `--instance-id` is required on every subcommand.
498
+
499
+ Unlike the other commands, L&D targets a **separate backend** (not `<org>.my.pitcher.com`). The base URL is resolved automatically from the instance's `ld-app` config; override with `--ld-url` or the `NG_LD_API_URL` env var (e.g. for a region whose `ld-app` config is unavailable). Auth is the same org `x-api-key`. The surface is authoring-only — enrollment, attempts, progress, groups, and analytics are not exposed (the org key cannot reach them). `set-canvas` likewise has no `--require-recompletion`: the backend rejects that flag for API-key callers (the bulk rep-completion reset is human-admin-only), so it could only ever 403.
500
+
501
+ ```bash
502
+ # headline: author a designed canvas course in one step (create → set-canvas → publish)
503
+ p ld create-canvas --instance-id 01... --title "Onboarding 101" --canvas '{"content":{"data":[]}}' --publish
504
+
505
+ # courses
506
+ p ld course list --instance-id 01... --status draft
507
+ p ld course create --instance-id 01... --title "Intro" --description "A short intro course." --level Beginner --duration 10 --sections-count 1
508
+ p ld course set-canvas --instance-id 01... --id 01... --canvas-file ./canvas.json
509
+ p ld course publish --instance-id 01... --id 01...
510
+ p ld course add-to-modules --instance-id 01... --id 01... --module-ids m1,m2
511
+
512
+ # modules / certificates / assessments
513
+ p ld module create --instance-id 01... --title "Basics" --subtitle "Getting started" --description "Foundational module."
514
+ p ld module add-courses --instance-id 01... --id m1 --course-ids c1,c2
515
+ p ld cert create --instance-id 01... --title "Certified Rep"
516
+ p ld assessment create --instance-id 01... --title "Quiz 1"
517
+ p ld assessment add-question --instance-id 01... --id a1 --question '{"type":"true_false","questionText":"...","config":{},"points":1}'
518
+
519
+ # override the L&D base URL (else resolved from ld-app config)
520
+ NG_LD_API_URL=https://<lambda-host>/dev/api p ld course list --instance-id 01...
521
+ ```
522
+
523
+ MCP: the same surface is exposed as `pitcher_ld_*` tools, plus a `create_ld_canvas_course` recipe prompt and a `pitcher://design/ld-canvas-course` resource. `NG_LD_API_URL` is the only way to override the L&D base URL under MCP (no per-call override, by design).
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ import { createGroupsCommand } from './src/commands/groups.js';
25
25
  import { createRolesCommand } from './src/commands/roles.js';
26
26
  import { createMetadataTemplateCommand } from './src/commands/metadataTemplate.js';
27
27
  import { createDsrCommand } from './src/commands/dsr.js';
28
+ import { createLdCommand } from './src/commands/ld.js';
28
29
  import { createMcpCommand } from './src/commands/mcp.js';
29
30
  const __filename = fileURLToPath(import.meta.url);
30
31
  const __dirname = dirname(__filename);
@@ -56,6 +57,7 @@ program.addCommand(createGroupsCommand());
56
57
  program.addCommand(createRolesCommand());
57
58
  program.addCommand(createMetadataTemplateCommand());
58
59
  program.addCommand(createDsrCommand());
60
+ program.addCommand(createLdCommand());
59
61
  program.addCommand(createOrgCommand());
60
62
  program.addCommand(createAnalyticsCommand());
61
63
  program.addCommand(createMcpCommand());
@@ -0,0 +1,61 @@
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 runLdAssessmentTags(opts: LdCommonOpts, deps?: LdDeps): Promise<Envelope<unknown>>;
5
+ export declare function runLdAssessmentList(opts: LdCommonOpts & {
6
+ status?: string;
7
+ title?: string;
8
+ limit?: number;
9
+ offset?: number;
10
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
11
+ export declare function runLdAssessmentGet(opts: LdCommonOpts & {
12
+ id?: string;
13
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
14
+ export declare function runLdAssessmentCreate(opts: LdCommonOpts & {
15
+ title?: string;
16
+ description?: string;
17
+ passingScore?: number;
18
+ timeLimit?: number;
19
+ maxAttempts?: number;
20
+ tags?: string[];
21
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
22
+ export declare function runLdAssessmentUpdate(opts: LdCommonOpts & {
23
+ id?: string;
24
+ } & Record<string, unknown>, deps?: LdDeps): Promise<Envelope<unknown>>;
25
+ export declare function runLdAssessmentDelete(opts: LdCommonOpts & {
26
+ id?: string;
27
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
28
+ export declare function runLdAssessmentListQuestions(opts: LdCommonOpts & {
29
+ id?: string;
30
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
31
+ export declare function runLdAssessmentAddQuestion(opts: LdCommonOpts & {
32
+ id?: string;
33
+ question?: string;
34
+ questionFile?: string;
35
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
36
+ export declare function runLdAssessmentUpdateQuestion(opts: LdCommonOpts & {
37
+ id?: string;
38
+ questionId?: string;
39
+ question?: string;
40
+ questionFile?: string;
41
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
42
+ export declare function runLdAssessmentReorderQuestions(opts: LdCommonOpts & {
43
+ id?: string;
44
+ questionIds?: string[];
45
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
46
+ export declare function runLdAssessmentDeleteQuestion(opts: LdCommonOpts & {
47
+ id?: string;
48
+ questionId?: string;
49
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
50
+ export declare function runLdAssessmentPreviewStart(opts: LdCommonOpts & {
51
+ id?: string;
52
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
53
+ export declare function runLdAssessmentPreviewGrade(opts: LdCommonOpts & {
54
+ id?: string;
55
+ answers?: string;
56
+ answersFile?: string;
57
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
58
+ export declare function runLdAssessmentListCourses(opts: LdCommonOpts & {
59
+ id?: string;
60
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
61
+ export declare function createAssessmentSubcommand(): Command;
@@ -0,0 +1,235 @@
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 } from '../../core/readInput.js';
6
+ import { listLimitOffsetParams } from '../../core/pagination.js';
7
+ import { assertIntFlags } from '../../schema/content.js';
8
+ import { csv, ldFlags, render } from './course.js';
9
+ const ADMIN = '/admin/assessments';
10
+ // ---- run* functions ----
11
+ export async function runLdAssessmentTags(opts, deps = {}) {
12
+ try {
13
+ const instanceId = requireInstance(opts);
14
+ const client = await ldClientFor(opts, deps);
15
+ const res = await client.get(`${ADMIN}/tags`, { params: { instanceId } });
16
+ return ok(ldData(res));
17
+ }
18
+ catch (e) {
19
+ return fail(toLdCliError(e));
20
+ }
21
+ }
22
+ export async function runLdAssessmentList(opts, deps = {}) {
23
+ try {
24
+ const instanceId = requireInstance(opts);
25
+ assertIntFlags(opts);
26
+ const params = { instanceId, ...listLimitOffsetParams(opts), ...pickTruthy(opts, ['status', 'title']) };
27
+ const client = await ldClientFor(opts, deps);
28
+ const res = await client.get(ADMIN, { params });
29
+ return ok(ldData(res));
30
+ }
31
+ catch (e) {
32
+ return fail(toLdCliError(e));
33
+ }
34
+ }
35
+ export async function runLdAssessmentGet(opts, deps = {}) {
36
+ try {
37
+ const instanceId = requireInstance(opts);
38
+ if (!opts.id)
39
+ throw createCliError('VALIDATION', '--id is required');
40
+ const client = await ldClientFor(opts, deps);
41
+ const res = await client.get(`${ADMIN}/${opts.id}`, { params: { instanceId } });
42
+ return ok(ldData(res));
43
+ }
44
+ catch (e) {
45
+ return fail(toLdCliError(e));
46
+ }
47
+ }
48
+ // instanceId is REQUIRED in the body (validation parses req.body alone; the controller also
49
+ // reads instanceId from the query but validation runs first). title required.
50
+ export async function runLdAssessmentCreate(opts, deps = {}) {
51
+ try {
52
+ const instanceId = requireInstance(opts);
53
+ if (!opts.title)
54
+ throw createCliError('VALIDATION', '--title is required');
55
+ const body = { instanceId, title: opts.title };
56
+ if (opts.description !== undefined)
57
+ body.description = opts.description;
58
+ if (opts.passingScore !== undefined)
59
+ body.passingScore = opts.passingScore;
60
+ if (opts.timeLimit !== undefined)
61
+ body.timeLimit = opts.timeLimit;
62
+ if (opts.maxAttempts !== undefined)
63
+ body.maxAttempts = opts.maxAttempts;
64
+ if (opts.tags !== undefined)
65
+ body.tags = opts.tags;
66
+ const client = await ldClientFor(opts, deps);
67
+ const res = await client.post(ADMIN, body, { params: { instanceId } });
68
+ return ok(ldData(res));
69
+ }
70
+ catch (e) {
71
+ return fail(toLdCliError(e));
72
+ }
73
+ }
74
+ // Partial update — send only what changed. Publish via --status published.
75
+ export async function runLdAssessmentUpdate(opts, deps = {}) {
76
+ try {
77
+ const instanceId = requireInstance(opts);
78
+ if (!opts.id)
79
+ throw createCliError('VALIDATION', '--id is required');
80
+ const body = {};
81
+ for (const k of ['title', 'description', 'passingScore', 'timeLimit', 'maxAttempts', 'status', 'tags']) {
82
+ if (opts[k] !== undefined)
83
+ body[k] = opts[k];
84
+ }
85
+ const client = await ldClientFor(opts, deps);
86
+ const res = await client.put(`${ADMIN}/${opts.id}`, body, { params: { instanceId } });
87
+ return ok(ldData(res));
88
+ }
89
+ catch (e) {
90
+ return fail(toLdCliError(e));
91
+ }
92
+ }
93
+ export async function runLdAssessmentDelete(opts, deps = {}) {
94
+ try {
95
+ const instanceId = requireInstance(opts);
96
+ if (!opts.id)
97
+ throw createCliError('VALIDATION', '--id is required');
98
+ const client = await ldClientFor(opts, deps);
99
+ const res = await client.delete(`${ADMIN}/${opts.id}`, { params: { instanceId } });
100
+ return ok(ldData(res) ?? { id: opts.id, deleted: true });
101
+ }
102
+ catch (e) {
103
+ return fail(toLdCliError(e));
104
+ }
105
+ }
106
+ export async function runLdAssessmentListQuestions(opts, deps = {}) {
107
+ try {
108
+ const instanceId = requireInstance(opts);
109
+ if (!opts.id)
110
+ throw createCliError('VALIDATION', '--id is required');
111
+ const client = await ldClientFor(opts, deps);
112
+ const res = await client.get(`${ADMIN}/${opts.id}/questions`, { params: { instanceId } });
113
+ return ok(ldData(res));
114
+ }
115
+ catch (e) {
116
+ return fail(toLdCliError(e));
117
+ }
118
+ }
119
+ export async function runLdAssessmentAddQuestion(opts, deps = {}) {
120
+ try {
121
+ const instanceId = requireInstance(opts);
122
+ if (!opts.id)
123
+ throw createCliError('VALIDATION', '--id is required');
124
+ const question = parseJsonInput(opts.question, opts.questionFile, 'question');
125
+ const client = await ldClientFor(opts, deps);
126
+ const res = await client.post(`${ADMIN}/${opts.id}/questions`, question, { params: { instanceId } });
127
+ return ok(ldData(res));
128
+ }
129
+ catch (e) {
130
+ return fail(toLdCliError(e));
131
+ }
132
+ }
133
+ export async function runLdAssessmentUpdateQuestion(opts, deps = {}) {
134
+ try {
135
+ const instanceId = requireInstance(opts);
136
+ if (!opts.id || !opts.questionId)
137
+ throw createCliError('VALIDATION', '--id and --question-id are required');
138
+ const question = parseJsonInput(opts.question, opts.questionFile, 'question');
139
+ const client = await ldClientFor(opts, deps);
140
+ const res = await client.put(`${ADMIN}/${opts.id}/questions/${opts.questionId}`, question, {
141
+ params: { instanceId },
142
+ });
143
+ return ok(ldData(res));
144
+ }
145
+ catch (e) {
146
+ return fail(toLdCliError(e));
147
+ }
148
+ }
149
+ export async function runLdAssessmentReorderQuestions(opts, deps = {}) {
150
+ try {
151
+ const instanceId = requireInstance(opts);
152
+ if (!opts.id)
153
+ throw createCliError('VALIDATION', '--id is required');
154
+ if (!opts.questionIds?.length)
155
+ throw createCliError('VALIDATION', '--question-ids is required (comma-separated)');
156
+ const client = await ldClientFor(opts, deps);
157
+ const res = await client.put(`${ADMIN}/${opts.id}/questions/reorder`, { questionIds: opts.questionIds }, { params: { instanceId } });
158
+ return ok(ldData(res));
159
+ }
160
+ catch (e) {
161
+ return fail(toLdCliError(e));
162
+ }
163
+ }
164
+ export async function runLdAssessmentDeleteQuestion(opts, deps = {}) {
165
+ try {
166
+ const instanceId = requireInstance(opts);
167
+ if (!opts.id || !opts.questionId)
168
+ throw createCliError('VALIDATION', '--id and --question-id are required');
169
+ const client = await ldClientFor(opts, deps);
170
+ const res = await client.delete(`${ADMIN}/${opts.id}/questions/${opts.questionId}`, { params: { instanceId } });
171
+ return ok(ldData(res) ?? { removed: true });
172
+ }
173
+ catch (e) {
174
+ return fail(toLdCliError(e));
175
+ }
176
+ }
177
+ export async function runLdAssessmentPreviewStart(opts, deps = {}) {
178
+ try {
179
+ const instanceId = requireInstance(opts);
180
+ if (!opts.id)
181
+ throw createCliError('VALIDATION', '--id is required');
182
+ const client = await ldClientFor(opts, deps);
183
+ const res = await client.post(`${ADMIN}/${opts.id}/preview/start`, {}, { params: { instanceId } });
184
+ return ok(ldData(res));
185
+ }
186
+ catch (e) {
187
+ return fail(toLdCliError(e));
188
+ }
189
+ }
190
+ export async function runLdAssessmentPreviewGrade(opts, deps = {}) {
191
+ try {
192
+ const instanceId = requireInstance(opts);
193
+ if (!opts.id)
194
+ throw createCliError('VALIDATION', '--id is required');
195
+ const answers = parseJsonInput(opts.answers, opts.answersFile, 'answers');
196
+ const client = await ldClientFor(opts, deps);
197
+ const res = await client.post(`${ADMIN}/${opts.id}/preview/grade`, answers, { params: { instanceId } });
198
+ return ok(ldData(res));
199
+ }
200
+ catch (e) {
201
+ return fail(toLdCliError(e));
202
+ }
203
+ }
204
+ export async function runLdAssessmentListCourses(opts, deps = {}) {
205
+ try {
206
+ const instanceId = requireInstance(opts);
207
+ if (!opts.id)
208
+ throw createCliError('VALIDATION', '--id is required');
209
+ const client = await ldClientFor(opts, deps);
210
+ const res = await client.get(`${ADMIN}/${opts.id}/courses`, { params: { instanceId } });
211
+ return ok(ldData(res));
212
+ }
213
+ catch (e) {
214
+ return fail(toLdCliError(e));
215
+ }
216
+ }
217
+ // ---- commander subcommand ----
218
+ export function createAssessmentSubcommand() {
219
+ const cmd = new Command('assessment').description('Author L&D assessments');
220
+ ldFlags(cmd.command('tags')).action(render(runLdAssessmentTags));
221
+ ldFlags(cmd.command('list')).option('--status <s>').option('--title <t>').option('--limit <n>', '', Number).option('--offset <n>', '', Number).action(render(runLdAssessmentList));
222
+ ldFlags(cmd.command('get')).option('--id <id>', 'Assessment ID (required)').action(render(runLdAssessmentGet));
223
+ ldFlags(cmd.command('create')).option('--title <t>', 'required').option('--description <d>').option('--passing-score <n>', 'integer', Number).option('--time-limit <n>', 'minutes, integer', Number).option('--max-attempts <n>', 'integer', Number).option('--tags <t>', 'Comma-separated', csv).action(render(runLdAssessmentCreate));
224
+ ldFlags(cmd.command('update')).option('--id <id>', 'required').option('--title <t>').option('--description <d>').option('--passing-score <n>', '', Number).option('--time-limit <n>', '', Number).option('--max-attempts <n>', '', Number).option('--status <s>', "published to publish").option('--tags <t>', 'Comma-separated', csv).action(render(runLdAssessmentUpdate));
225
+ ldFlags(cmd.command('delete')).option('--id <id>', 'required').action(render(runLdAssessmentDelete));
226
+ ldFlags(cmd.command('list-questions')).option('--id <id>', 'required').action(render(runLdAssessmentListQuestions));
227
+ ldFlags(cmd.command('add-question')).option('--id <id>', 'required').option('--question <json>', 'Inline question JSON').option('--question-file <path>').action(render(runLdAssessmentAddQuestion));
228
+ ldFlags(cmd.command('update-question')).option('--id <id>', 'required').option('--question-id <id>', 'required').option('--question <json>', 'Inline question JSON').option('--question-file <path>').action(render(runLdAssessmentUpdateQuestion));
229
+ ldFlags(cmd.command('reorder-questions')).option('--id <id>', 'required').option('--question-ids <ids>', 'Comma-separated (required)', csv).action(render(runLdAssessmentReorderQuestions));
230
+ ldFlags(cmd.command('delete-question')).option('--id <id>', 'required').option('--question-id <id>', 'required').action(render(runLdAssessmentDeleteQuestion));
231
+ ldFlags(cmd.command('preview-start')).option('--id <id>', 'required').action(render(runLdAssessmentPreviewStart));
232
+ ldFlags(cmd.command('preview-grade')).option('--id <id>', 'required').option('--answers <json>', 'Inline answers JSON').option('--answers-file <path>').action(render(runLdAssessmentPreviewGrade));
233
+ ldFlags(cmd.command('list-courses')).option('--id <id>', 'required').action(render(runLdAssessmentListCourses));
234
+ return cmd;
235
+ }
@@ -0,0 +1,20 @@
1
+ import { Command } from 'commander';
2
+ import { type Envelope } from '../../core/envelope.js';
3
+ import { type LdCommonOpts, type LdDeps } from '../../core/ld.js';
4
+ /**
5
+ * One-call canvas-course flow: create a draft (contentSource: canvas) → set the canvas
6
+ * content → optionally publish. The L&D backend has no transaction across these calls, so
7
+ * on a later-step failure we surface the created courseId in the error for recovery.
8
+ * NOTE: not idempotent — re-running creates a NEW draft each time; on a set-canvas/publish
9
+ * failure the created draft is left in place (recoverable via the returned courseId).
10
+ * createCourseSchema requires description/level/duration/sectionsCount, so we default them
11
+ * (a canvas course carries its content in the LearningCanvas, not sections/html).
12
+ */
13
+ export declare function runLdCourseCreateCanvas(opts: LdCommonOpts & {
14
+ title?: string;
15
+ description?: string;
16
+ canvas?: string;
17
+ canvasFile?: string;
18
+ publish?: boolean;
19
+ } & Record<string, unknown>, deps?: LdDeps): Promise<Envelope<unknown>>;
20
+ export declare function createCanvasCourseSubcommand(): Command;
@@ -0,0 +1,69 @@
1
+ import { Command } from 'commander';
2
+ import { ok, fail } from '../../core/envelope.js';
3
+ import { createCliError } from '../../core/errors.js';
4
+ import { parseJsonInput } from '../../core/readInput.js';
5
+ import { emitEnvelope } from '../../core/output.js';
6
+ import { withAuthFlags } from '../withAuth.js';
7
+ import { withInstanceFlag } from '../withInstance.js';
8
+ import { toLdCliError } from '../../core/ld.js';
9
+ import { runLdCourseCreate, runLdCourseSetCanvas, runLdCoursePublish } from './course.js';
10
+ /**
11
+ * One-call canvas-course flow: create a draft (contentSource: canvas) → set the canvas
12
+ * content → optionally publish. The L&D backend has no transaction across these calls, so
13
+ * on a later-step failure we surface the created courseId in the error for recovery.
14
+ * NOTE: not idempotent — re-running creates a NEW draft each time; on a set-canvas/publish
15
+ * failure the created draft is left in place (recoverable via the returned courseId).
16
+ * createCourseSchema requires description/level/duration/sectionsCount, so we default them
17
+ * (a canvas course carries its content in the LearningCanvas, not sections/html).
18
+ */
19
+ export async function runLdCourseCreateCanvas(opts, deps = {}) {
20
+ // Fail fast: validate the canvas JSON BEFORE creating the draft, so a missing/malformed
21
+ // --canvas doesn't mint an orphan draft that then fails at the set-canvas step.
22
+ try {
23
+ parseJsonInput(opts.canvas, opts.canvasFile, 'canvas');
24
+ }
25
+ catch (e) {
26
+ return fail(toLdCliError(e));
27
+ }
28
+ // Step 1: create the draft as a canvas course, defaulting the schema-required fields.
29
+ const created = await runLdCourseCreate({
30
+ ...opts,
31
+ contentSource: 'canvas',
32
+ description: opts.description || `Canvas course: ${opts.title ?? ''}`.trim(),
33
+ level: opts.level || 'Beginner',
34
+ duration: opts.duration ?? 5,
35
+ sectionsCount: opts.sectionsCount ?? 1,
36
+ }, deps);
37
+ if (!created.ok)
38
+ return created;
39
+ const courseId = created.data?._id ?? created.data?.id;
40
+ if (!courseId)
41
+ return fail(createCliError('UNKNOWN', 'course create returned no id'));
42
+ // Step 2: set the canvas content.
43
+ const setRes = await runLdCourseSetCanvas({ ...opts, id: courseId }, deps);
44
+ if (!setRes.ok) {
45
+ return fail(createCliError(setRes.error.code, `set-canvas failed for course ${courseId}: ${setRes.error.message}`));
46
+ }
47
+ // Step 3: optional publish.
48
+ if (opts.publish) {
49
+ const pub = await runLdCoursePublish({ ...opts, id: courseId }, deps);
50
+ if (!pub.ok) {
51
+ return fail(createCliError(pub.error.code, `publish failed for course ${courseId}: ${pub.error.message}`));
52
+ }
53
+ }
54
+ return ok({ courseId, published: Boolean(opts.publish), course: created.data });
55
+ }
56
+ export function createCanvasCourseSubcommand() {
57
+ // Leaf command: auth + instance flags go directly on it. NEVER .requiredOption (house rule);
58
+ // the run* enforces --title / --instance-id via VALIDATION guards.
59
+ const cmd = withInstanceFlag(withAuthFlags(new Command('create-canvas')))
60
+ .description('Create a canvas-based course in one step (create → set-canvas → publish)')
61
+ .option('--ld-url <url>', 'Override L&D API base URL (CLI only)')
62
+ .option('--title <t>', 'Course title (required)')
63
+ .option('--description <d>')
64
+ .option('--canvas <json>', 'Inline canvas JSON: object { content: { data: [...] }, theme? } (a bare content.data array is auto-wrapped)')
65
+ .option('--canvas-file <path>')
66
+ .option('--publish', 'Publish after authoring');
67
+ cmd.action(async (o, c) => emitEnvelope(await runLdCourseCreateCanvas({ ...c.optsWithGlobals(), ...o }), c.optsWithGlobals()));
68
+ return cmd;
69
+ }
@@ -0,0 +1,93 @@
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 runLdCertTags(opts: LdCommonOpts, deps?: LdDeps): Promise<Envelope<unknown>>;
5
+ export declare function runLdCertList(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 runLdCertGet(opts: LdCommonOpts & {
13
+ id?: string;
14
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
15
+ export declare function runLdCertCreate(opts: LdCommonOpts & {
16
+ title?: string;
17
+ description?: string;
18
+ tags?: string[];
19
+ validityPeriod?: number;
20
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
21
+ export declare function runLdCertUpdate(opts: LdCommonOpts & {
22
+ id?: string;
23
+ title?: string;
24
+ description?: string;
25
+ tags?: string[];
26
+ validityPeriod?: number;
27
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
28
+ export declare function runLdCertDelete(opts: LdCommonOpts & {
29
+ id?: string;
30
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
31
+ export declare function runLdCertTrainingMaterials(opts: LdCommonOpts & {
32
+ id?: string;
33
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
34
+ export declare function runLdCertListModules(opts: LdCommonOpts & {
35
+ id?: string;
36
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
37
+ export declare function runLdCertAddModules(opts: LdCommonOpts & {
38
+ id?: string;
39
+ moduleIds?: string[];
40
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
41
+ export declare function runLdCertReorderModules(opts: LdCommonOpts & {
42
+ id?: string;
43
+ itemId?: string;
44
+ newOrder?: number;
45
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
46
+ export declare function runLdCertUpdateModule(opts: LdCommonOpts & {
47
+ id?: string;
48
+ moduleId?: string;
49
+ isRequired?: boolean;
50
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
51
+ export declare function runLdCertRemoveModule(opts: LdCommonOpts & {
52
+ id?: string;
53
+ moduleId?: string;
54
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
55
+ export declare function runLdCertListCourses(opts: LdCommonOpts & {
56
+ id?: string;
57
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
58
+ export declare function runLdCertAddCourses(opts: LdCommonOpts & {
59
+ id?: string;
60
+ courseIds?: string[];
61
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
62
+ export declare function runLdCertReorderCourses(opts: LdCommonOpts & {
63
+ id?: string;
64
+ itemId?: string;
65
+ newOrder?: number;
66
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
67
+ export declare function runLdCertUpdateCourse(opts: LdCommonOpts & {
68
+ id?: string;
69
+ courseId?: string;
70
+ isRequired?: boolean;
71
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
72
+ export declare function runLdCertRemoveCourse(opts: LdCommonOpts & {
73
+ id?: string;
74
+ courseId?: string;
75
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
76
+ export declare function runLdCertListAssessments(opts: LdCommonOpts & {
77
+ id?: string;
78
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
79
+ export declare function runLdCertAddAssessments(opts: LdCommonOpts & {
80
+ id?: string;
81
+ assessments?: string;
82
+ assessmentsFile?: string;
83
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
84
+ export declare function runLdCertUpdateAssessment(opts: LdCommonOpts & {
85
+ id?: string;
86
+ assessmentId?: string;
87
+ isRequired?: boolean;
88
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
89
+ export declare function runLdCertRemoveAssessment(opts: LdCommonOpts & {
90
+ id?: string;
91
+ assessmentId?: string;
92
+ }, deps?: LdDeps): Promise<Envelope<unknown>>;
93
+ export declare function createCertSubcommand(): Command;