@pitcher/cli-scripts 2.11.0 → 2.13.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.
Files changed (34) hide show
  1. package/README.md +40 -2
  2. package/dist/index.js +2 -0
  3. package/dist/src/commands/analytics.d.ts +16 -0
  4. package/dist/src/commands/analytics.js +44 -1
  5. package/dist/src/commands/ld/assessment.d.ts +61 -0
  6. package/dist/src/commands/ld/assessment.js +235 -0
  7. package/dist/src/commands/ld/canvasCourse.d.ts +20 -0
  8. package/dist/src/commands/ld/canvasCourse.js +69 -0
  9. package/dist/src/commands/ld/cert.d.ts +93 -0
  10. package/dist/src/commands/ld/cert.js +355 -0
  11. package/dist/src/commands/ld/course.d.ts +62 -0
  12. package/dist/src/commands/ld/course.js +308 -0
  13. package/dist/src/commands/ld/module.d.ts +43 -0
  14. package/dist/src/commands/ld/module.js +199 -0
  15. package/dist/src/commands/ld.d.ts +2 -0
  16. package/dist/src/commands/ld.js +15 -0
  17. package/dist/src/core/http.d.ts +10 -0
  18. package/dist/src/core/http.js +20 -0
  19. package/dist/src/core/ld.d.ts +38 -0
  20. package/dist/src/core/ld.js +102 -0
  21. package/dist/src/core/readInput.d.ts +1 -0
  22. package/dist/src/core/readInput.js +14 -0
  23. package/dist/src/mcp/bundled.js +1 -1
  24. package/dist/src/mcp/prompts/ldCanvasCourse.d.ts +2 -0
  25. package/dist/src/mcp/prompts/ldCanvasCourse.js +50 -0
  26. package/dist/src/mcp/prompts/registry.js +8 -1
  27. package/dist/src/mcp/registry.js +2 -0
  28. package/dist/src/mcp/resources/ldCanvasDesign.d.ts +3 -0
  29. package/dist/src/mcp/resources/ldCanvasDesign.js +51 -0
  30. package/dist/src/mcp/resources/registry.js +2 -1
  31. package/dist/src/mcp/tools/analytics.js +18 -1
  32. package/dist/src/mcp/tools/ld.d.ts +2 -0
  33. package/dist/src/mcp/tools/ld.js +680 -0
  34. package/package.json +1 -1
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 114 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 (111)** — 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
 
@@ -360,6 +360,14 @@ p analytics org --api-key myapikey
360
360
 
361
361
  # Instance rollups (instance-admin or org-admin key required)
362
362
  p analytics instance --instance-id 01... --api-key myapikey
363
+
364
+ # KPI metrics from the clickhouse analytics service (PIT-7647).
365
+ # Access follows the key user's real role on the instance; --org/--url pick the
366
+ # env exactly as for other commands (they route via x-original-domain).
367
+ # Activity metrics need a date window; inventory counts (total-members, ...) do not.
368
+ p analytics kpi --instance-id 01... --type total-members --api-key myapikey
369
+ p analytics kpi --instance-id 01... --type total-active-users \
370
+ --start-date 2026-01-01 --end-date 2026-01-31 --api-key myapikey
363
371
  ```
364
372
 
365
373
  ## To run the `users` command:
@@ -491,3 +499,33 @@ p dsr scaffold --out share.json
491
499
  ```
492
500
 
493
501
  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`.
502
+
503
+ ## To run the `ld` command:
504
+
505
+ 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.
506
+
507
+ 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.
508
+
509
+ ```bash
510
+ # headline: author a designed canvas course in one step (create → set-canvas → publish)
511
+ p ld create-canvas --instance-id 01... --title "Onboarding 101" --canvas '{"content":{"data":[]}}' --publish
512
+
513
+ # courses
514
+ p ld course list --instance-id 01... --status draft
515
+ p ld course create --instance-id 01... --title "Intro" --description "A short intro course." --level Beginner --duration 10 --sections-count 1
516
+ p ld course set-canvas --instance-id 01... --id 01... --canvas-file ./canvas.json
517
+ p ld course publish --instance-id 01... --id 01...
518
+ p ld course add-to-modules --instance-id 01... --id 01... --module-ids m1,m2
519
+
520
+ # modules / certificates / assessments
521
+ p ld module create --instance-id 01... --title "Basics" --subtitle "Getting started" --description "Foundational module."
522
+ p ld module add-courses --instance-id 01... --id m1 --course-ids c1,c2
523
+ p ld cert create --instance-id 01... --title "Certified Rep"
524
+ p ld assessment create --instance-id 01... --title "Quiz 1"
525
+ p ld assessment add-question --instance-id 01... --id a1 --question '{"type":"true_false","questionText":"...","config":{},"points":1}'
526
+
527
+ # override the L&D base URL (else resolved from ld-app config)
528
+ NG_LD_API_URL=https://<lambda-host>/dev/api p ld course list --instance-id 01...
529
+ ```
530
+
531
+ 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());
@@ -9,4 +9,20 @@ export declare function runAnalyticsOrg(opts: CommonOpts, deps?: AnalyticsDeps):
9
9
  export declare function runAnalyticsInstance(opts: CommonOpts & {
10
10
  instanceId?: string;
11
11
  }, deps?: AnalyticsDeps): Promise<Envelope<unknown>>;
12
+ export interface AnalyticsKpiOpts extends CommonOpts {
13
+ instanceId?: string;
14
+ type?: string;
15
+ startDate?: string;
16
+ endDate?: string;
17
+ groupIds?: string;
18
+ userEmails?: string;
19
+ }
20
+ /**
21
+ * KPI metrics from the clickhouse-api analytics service (PIT-7647). Unlike
22
+ * org/instance rollups (Django), this hits `clickhouse.pitcher.com` with the
23
+ * org api-key; the caller inherits the key user's real role. Dates are required
24
+ * by the server for activity-metric types (inventory counts like
25
+ * `total-members` need none) — that validation stays server-side.
26
+ */
27
+ export declare function runAnalyticsKpi(opts: AnalyticsKpiOpts, deps?: AnalyticsDeps): Promise<Envelope<unknown>>;
12
28
  export declare function createAnalyticsCommand(): Command;
@@ -1,5 +1,5 @@
1
1
  import { Command } from 'commander';
2
- import { clientFor, toCliError } from '../core/http.js';
2
+ import { clickhouseClientFor, clientFor, toCliError } from '../core/http.js';
3
3
  import { ok, fail } from '../core/envelope.js';
4
4
  import { createCliError } from '../core/errors.js';
5
5
  import { emitEnvelope } from '../core/output.js';
@@ -26,6 +26,37 @@ export async function runAnalyticsInstance(opts, deps = {}) {
26
26
  return fail(toCliError(e));
27
27
  }
28
28
  }
29
+ /**
30
+ * KPI metrics from the clickhouse-api analytics service (PIT-7647). Unlike
31
+ * org/instance rollups (Django), this hits `clickhouse.pitcher.com` with the
32
+ * org api-key; the caller inherits the key user's real role. Dates are required
33
+ * by the server for activity-metric types (inventory counts like
34
+ * `total-members` need none) — that validation stays server-side.
35
+ */
36
+ export async function runAnalyticsKpi(opts, deps = {}) {
37
+ try {
38
+ if (!opts.instanceId)
39
+ throw createCliError('VALIDATION', '--instance-id is required for analytics kpi');
40
+ const client = clickhouseClientFor(opts, deps);
41
+ const params = {
42
+ instance_id: opts.instanceId,
43
+ type: opts.type || 'all',
44
+ };
45
+ if (opts.startDate)
46
+ params.start_date = opts.startDate;
47
+ if (opts.endDate)
48
+ params.end_date = opts.endDate;
49
+ if (opts.groupIds)
50
+ params.group_ids = opts.groupIds;
51
+ if (opts.userEmails)
52
+ params.user_emails = opts.userEmails;
53
+ const res = await client.get('/analytics/kpi', { params });
54
+ return ok(res.data);
55
+ }
56
+ catch (e) {
57
+ return fail(toCliError(e));
58
+ }
59
+ }
29
60
  export function createAnalyticsCommand() {
30
61
  const cmd = new Command('analytics').description('read rollup analytics totals (requires an org-admin API key)');
31
62
  withAuthFlags(cmd.command('org'))
@@ -41,5 +72,17 @@ export function createAnalyticsCommand() {
41
72
  const globals = command.optsWithGlobals();
42
73
  emitEnvelope(await runAnalyticsInstance({ ...options, quiet: globals.quiet }), globals);
43
74
  });
75
+ withAuthFlags(cmd.command('kpi'))
76
+ .option('--instance-id <id>', 'Instance ID (required)')
77
+ .option('--type <type>', "KPI type, e.g. total-members, total-active-users (default 'all')")
78
+ .option('--start-date <iso>', 'Window start (ISO date; required by the server for activity metrics)')
79
+ .option('--end-date <iso>', 'Window end (ISO date; required by the server for activity metrics)')
80
+ .option('--group-ids <csv>', 'Comma-separated distribution-group IDs to scope by')
81
+ .option('--user-emails <csv>', 'Comma-separated user emails to scope by')
82
+ .description('KPI metrics from the clickhouse analytics service (role follows the key user)')
83
+ .action(async (options, command) => {
84
+ const globals = command.optsWithGlobals();
85
+ emitEnvelope(await runAnalyticsKpi({ ...options, quiet: globals.quiet }), globals);
86
+ });
44
87
  return cmd;
45
88
  }
@@ -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;