@empirical-gauzza/cli 1.0.9

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.
@@ -0,0 +1,622 @@
1
+ 'use strict';
2
+
3
+ // US-010: CLI commands for Kit management (create/add-source/remove-source/list/autosync/
4
+ // devices/deactivate) and Skill authoring (push/history/rollback).
5
+ //
6
+ // `<kit>` arguments throughout this file accept either a kitId or a Kit's name — the server
7
+ // resolves both (names are unique per owner), so the CLI just passes the raw argument through.
8
+ const fs = require('fs');
9
+ const os = require('os');
10
+ const path = require('path');
11
+ const matter = require('gray-matter');
12
+ const { log, warn } = require('./ui');
13
+ const { httpJson, printApiError } = require('./http');
14
+ const { parseOption, parseFlag } = require('./env');
15
+ const { requireAccessToken, resolveApiBaseUrl, buildApiAuthHeaders } = require('./auth');
16
+ const { withCliAnalyticsContext } = require('./memoryCommands');
17
+ const { readDeviceConfig, writeDeviceConfig, ensureDeviceId, readPendingReview } = require('./deviceConfig');
18
+ const { performSync, printSyncSummary } = require('./skillSync');
19
+ const { registerScheduledSync, deregisterScheduledSync } = require('./scheduledSync');
20
+
21
+ // US-016: nag banner surfaced at the START of every interactive (non-`--quiet`) `kit`
22
+ // subcommand — informational only, never blocks the subcommand's own work. `kit sync` is the
23
+ // only place pending-review entries actually get resolved (see skillSync.js's
24
+ // reviewPendingEntries), so this banner just points the user there.
25
+ function surfacePendingReviewBanner(quiet) {
26
+ if (quiet) return;
27
+ const pendingReview = readPendingReview();
28
+ const names = Object.keys(pendingReview);
29
+ if (names.length === 0) return;
30
+ warn(`${names.length} skill(s) updated since you last reviewed them: ${names.join(', ')}. Run 'empirical kit sync' to review.`);
31
+ }
32
+
33
+ // US-012: reconciles the local OS scheduled task to match a Kit's `autoSyncEnabled` flag.
34
+ // Best-effort/non-fatal: a broken scheduler (e.g. `schtasks`/`launchctl`/`crontab` missing or
35
+ // erroring) is logged to stderr and never blocks the calling `kit use`/`kit autosync` command,
36
+ // consistent with this being a background drift-catch-up floor, not a hard requirement.
37
+ // Tracks which kitId currently "owns" the local schedule via deviceConfig's `scheduledSyncKitId`
38
+ // so `kit autosync <kit> off` can tell whether `<kit>` is the one actually registered locally.
39
+ function reconcileScheduledSyncForKit(kitId, autoSyncEnabled, quiet) {
40
+ const config = readDeviceConfig();
41
+ try {
42
+ if (autoSyncEnabled) {
43
+ registerScheduledSync();
44
+ writeDeviceConfig({ ...config, scheduledSyncKitId: kitId });
45
+ } else {
46
+ deregisterScheduledSync();
47
+ writeDeviceConfig({ ...config, scheduledSyncKitId: null });
48
+ }
49
+ } catch (error) {
50
+ if (!quiet) {
51
+ warn(`Warning: failed to reconcile local scheduled sync task: ${error.message}`);
52
+ }
53
+ }
54
+ }
55
+
56
+ // ─── Kit subcommands ──────────────────────────────────────────────────────────
57
+
58
+ async function runKitCreate(args) {
59
+ const name = args[0] && !args[0].startsWith('--') ? args[0] : '';
60
+ if (!name) {
61
+ warn('Usage: empirical kit create <name> [--workspace <workspaceId>]');
62
+ process.exit(1);
63
+ }
64
+
65
+ const workspaceId = parseOption(args, '--workspace', '');
66
+
67
+ const accessToken = requireAccessToken();
68
+ const baseUrl = resolveApiBaseUrl();
69
+ const url = new URL('/api/v1/kits', baseUrl).toString();
70
+
71
+ const body = { name };
72
+ if (workspaceId) body.workspaceId = workspaceId;
73
+
74
+ let response;
75
+ try {
76
+ response = await httpJson(url, 'POST', body, buildApiAuthHeaders(accessToken));
77
+ } catch (error) {
78
+ printApiError(error);
79
+ process.exit(1);
80
+ }
81
+
82
+ const created = (response && response.data) ? response.data : response;
83
+ log(JSON.stringify(created));
84
+ }
85
+
86
+ async function runKitAddSource(args) {
87
+ const kitId = args[0] && !args[0].startsWith('--') ? args[0] : '';
88
+ const gitUrl = args[1] && !args[1].startsWith('--') ? args[1] : '';
89
+
90
+ if (!kitId || !gitUrl) {
91
+ warn('Usage: empirical kit add-source <kit> <git-url> [--ref <ref>] [--path <subpath>]');
92
+ process.exit(1);
93
+ }
94
+
95
+ const ref = parseOption(args, '--ref', '');
96
+ const subpath = parseOption(args, '--path', '');
97
+
98
+ const accessToken = requireAccessToken();
99
+ const baseUrl = resolveApiBaseUrl();
100
+ const url = new URL(`/api/v1/kits/${encodeURIComponent(kitId)}/sources`, baseUrl).toString();
101
+
102
+ const body = { sourceType: 'git_custom', repoUrl: gitUrl };
103
+ if (ref) body.ref = ref;
104
+ if (subpath) body.subpath = subpath;
105
+
106
+ let response;
107
+ try {
108
+ response = await httpJson(url, 'POST', body, buildApiAuthHeaders(accessToken));
109
+ } catch (error) {
110
+ printApiError(error);
111
+ process.exit(1);
112
+ }
113
+
114
+ const created = (response && response.data) ? response.data : response;
115
+ log(JSON.stringify(created));
116
+ }
117
+
118
+ async function runKitRemoveSource(args) {
119
+ const kitId = args[0] && !args[0].startsWith('--') ? args[0] : '';
120
+ const sourceId = args[1] && !args[1].startsWith('--') ? args[1] : '';
121
+
122
+ if (!kitId || !sourceId) {
123
+ warn('Usage: empirical kit remove-source <kit> <source-id>');
124
+ process.exit(1);
125
+ }
126
+
127
+ const accessToken = requireAccessToken();
128
+ const baseUrl = resolveApiBaseUrl();
129
+ const url = new URL(
130
+ `/api/v1/kits/${encodeURIComponent(kitId)}/sources/${encodeURIComponent(sourceId)}`,
131
+ baseUrl
132
+ ).toString();
133
+
134
+ let response;
135
+ try {
136
+ response = await httpJson(url, 'DELETE', null, buildApiAuthHeaders(accessToken));
137
+ } catch (error) {
138
+ printApiError(error);
139
+ process.exit(1);
140
+ }
141
+
142
+ const result = (response && response.data) ? response.data : response;
143
+ log(JSON.stringify(result));
144
+ }
145
+
146
+ async function runKitList(args) {
147
+ const accessToken = requireAccessToken();
148
+ const baseUrl = resolveApiBaseUrl();
149
+ const url = new URL('/api/v1/kits', baseUrl).toString();
150
+
151
+ let response;
152
+ try {
153
+ response = await httpJson(url, 'GET', undefined, buildApiAuthHeaders(accessToken));
154
+ } catch (error) {
155
+ printApiError(error);
156
+ process.exit(1);
157
+ }
158
+
159
+ const result = (response && response.data) ? response.data : response;
160
+ log(JSON.stringify(result));
161
+ }
162
+
163
+ async function fetchInventoryJson(url, accessToken) {
164
+ try {
165
+ return await httpJson(url, 'GET', undefined, buildApiAuthHeaders(accessToken));
166
+ } catch (error) {
167
+ printApiError(error);
168
+ process.exit(1);
169
+ }
170
+ }
171
+
172
+ async function listOwnedKits(baseUrl, accessToken) {
173
+ const response = await fetchInventoryJson(new URL('/api/v1/kits', baseUrl).toString(), accessToken);
174
+ return response?.data?.kits || response?.kits || [];
175
+ }
176
+
177
+ async function runSkillList(args) {
178
+ const accessToken = requireAccessToken();
179
+ const baseUrl = resolveApiBaseUrl();
180
+ const kitFilter = parseOption(args, '--kit', '');
181
+ const kits = await listOwnedKits(baseUrl, accessToken);
182
+ const selectedKits = kitFilter
183
+ ? kits.filter((kit) => kit.kitId === kitFilter || kit.name === kitFilter)
184
+ : kits;
185
+ const skills = [];
186
+
187
+ for (const kit of selectedKits) {
188
+ const response = await fetchInventoryJson(
189
+ new URL(`/api/v1/kits/${encodeURIComponent(kit.kitId)}/resolved`, baseUrl).toString(),
190
+ accessToken
191
+ );
192
+ const kitSkills = response?.data?.skills || response?.skills || [];
193
+ for (const skill of kitSkills) {
194
+ skills.push({
195
+ ...skill,
196
+ kitId: kit.kitId,
197
+ kitName: kit.name,
198
+ isDefault: Boolean(kit.isDefault)
199
+ });
200
+ }
201
+ }
202
+
203
+ log(JSON.stringify({ skills }));
204
+ }
205
+
206
+ async function runDeviceList() {
207
+ const accessToken = requireAccessToken();
208
+ const baseUrl = resolveApiBaseUrl();
209
+ const kits = await listOwnedKits(baseUrl, accessToken);
210
+ const devicesById = new Map();
211
+
212
+ for (const kit of kits.filter((candidate) => !candidate.isDefault)) {
213
+ const response = await fetchInventoryJson(
214
+ new URL(`/api/v1/kits/${encodeURIComponent(kit.kitId)}/devices`, baseUrl).toString(),
215
+ accessToken
216
+ );
217
+ const devices = response?.data?.devices || response?.devices || [];
218
+ for (const device of devices) {
219
+ const existing = devicesById.get(device.deviceId) || {
220
+ ...device,
221
+ kits: []
222
+ };
223
+ existing.kits.push({
224
+ kitId: kit.kitId,
225
+ kitName: kit.name,
226
+ active: Boolean(device.active)
227
+ });
228
+ devicesById.set(device.deviceId, existing);
229
+ }
230
+ }
231
+
232
+ log(JSON.stringify({ devices: [...devicesById.values()] }));
233
+ }
234
+
235
+ async function runDeviceShow(args) {
236
+ const deviceId = args[0] && !args[0].startsWith('--') ? args[0] : '';
237
+ if (!deviceId) {
238
+ warn('Usage: empirical device show <deviceId>');
239
+ process.exit(1);
240
+ }
241
+
242
+ const accessToken = requireAccessToken();
243
+ const baseUrl = resolveApiBaseUrl();
244
+ const response = await fetchInventoryJson(
245
+ new URL(`/api/v1/devices/${encodeURIComponent(deviceId)}/resolved`, baseUrl).toString(),
246
+ accessToken
247
+ );
248
+ const data = response?.data || response || {};
249
+ log(JSON.stringify({ deviceId, ...data }));
250
+ }
251
+
252
+ async function runDeviceCommand(subArgs) {
253
+ const sub = subArgs[0];
254
+ const rest = subArgs.slice(1);
255
+ return withCliAnalyticsContext(
256
+ { command: `device.${sub || 'unknown'}`, subcommand: sub || 'unknown' },
257
+ async () => {
258
+ if (sub === 'list') return runDeviceList(rest);
259
+ if (sub === 'show') return runDeviceShow(rest);
260
+ warn('Unknown device subcommand: ' + (sub || '(none)'));
261
+ warn('Usage: empirical device <list|show> [options]');
262
+ process.exit(1);
263
+ }
264
+ );
265
+ }
266
+
267
+ async function runKitAutosync(args) {
268
+ const kitId = args[0] && !args[0].startsWith('--') ? args[0] : '';
269
+ const mode = args[1] && !args[1].startsWith('--') ? args[1] : '';
270
+
271
+ if (!kitId || (mode !== 'on' && mode !== 'off')) {
272
+ warn('Usage: empirical kit autosync <kit> on|off');
273
+ process.exit(1);
274
+ }
275
+
276
+ const accessToken = requireAccessToken();
277
+ const baseUrl = resolveApiBaseUrl();
278
+ const url = new URL(`/api/v1/kits/${encodeURIComponent(kitId)}/autosync`, baseUrl).toString();
279
+
280
+ let response;
281
+ try {
282
+ response = await httpJson(url, 'POST', { enabled: mode === 'on' }, buildApiAuthHeaders(accessToken));
283
+ } catch (error) {
284
+ printApiError(error);
285
+ process.exit(1);
286
+ }
287
+
288
+ const result = (response && response.data) ? response.data : response;
289
+ log(JSON.stringify(result));
290
+
291
+ // Only reconcile the local scheduled task when toggling autosync for the kit that's actually
292
+ // active on THIS device — toggling a non-active kit's server-side flag doesn't affect this
293
+ // machine's schedule (see US-012 AC).
294
+ const config = readDeviceConfig();
295
+ if (config.activeKit === kitId) {
296
+ reconcileScheduledSyncForKit(kitId, mode === 'on', false);
297
+ }
298
+ }
299
+
300
+ async function runKitDevices(args) {
301
+ const kitId = args[0] && !args[0].startsWith('--') ? args[0] : '';
302
+
303
+ if (!kitId) {
304
+ warn('Usage: empirical kit devices <kit>');
305
+ process.exit(1);
306
+ }
307
+
308
+ const accessToken = requireAccessToken();
309
+ const baseUrl = resolveApiBaseUrl();
310
+ const url = new URL(`/api/v1/kits/${encodeURIComponent(kitId)}/devices`, baseUrl).toString();
311
+
312
+ let response;
313
+ try {
314
+ response = await httpJson(url, 'GET', undefined, buildApiAuthHeaders(accessToken));
315
+ } catch (error) {
316
+ printApiError(error);
317
+ process.exit(1);
318
+ }
319
+
320
+ const result = (response && response.data) ? response.data : response;
321
+ log(JSON.stringify(result));
322
+ }
323
+
324
+ async function runKitDeactivate(args) {
325
+ const kitId = args[0] && !args[0].startsWith('--') ? args[0] : '';
326
+ const deviceId = args[1] && !args[1].startsWith('--') ? args[1] : '';
327
+
328
+ if (!kitId || !deviceId) {
329
+ warn('Usage: empirical kit deactivate <kit> <deviceId>');
330
+ process.exit(1);
331
+ }
332
+
333
+ const accessToken = requireAccessToken();
334
+ const baseUrl = resolveApiBaseUrl();
335
+ const url = new URL(
336
+ `/api/v1/kits/${encodeURIComponent(kitId)}/devices/${encodeURIComponent(deviceId)}/deactivate`,
337
+ baseUrl
338
+ ).toString();
339
+
340
+ let response;
341
+ try {
342
+ response = await httpJson(url, 'POST', null, buildApiAuthHeaders(accessToken));
343
+ } catch (error) {
344
+ printApiError(error);
345
+ process.exit(1);
346
+ }
347
+
348
+ const result = (response && response.data) ? response.data : response;
349
+ log(JSON.stringify(result));
350
+ }
351
+
352
+ // `empirical kit use <kit>` — <kit> is a kitId or Kit name (server resolves both). Sets
353
+ // activeKit locally to whatever identifier the user passed, registers this device against
354
+ // the named Kit server-side, then triggers a sync via the shared skillSync module (never by
355
+ // shelling back out to `empirical kit sync`).
356
+ async function runKitUse(args) {
357
+ const kitId = args[0] && !args[0].startsWith('--') ? args[0] : '';
358
+ if (!kitId) {
359
+ warn('Usage: empirical kit use <kit> [--dry-run] [--quiet] [--force]');
360
+ process.exit(1);
361
+ }
362
+
363
+ const dryRun = parseFlag(args, '--dry-run');
364
+ const quiet = parseFlag(args, '--quiet');
365
+ const force = parseFlag(args, '--force');
366
+
367
+ const accessToken = requireAccessToken();
368
+ const baseUrl = resolveApiBaseUrl();
369
+ const deviceId = ensureDeviceId();
370
+
371
+ const url = new URL(`/api/v1/kits/${encodeURIComponent(kitId)}/devices`, baseUrl).toString();
372
+ let response;
373
+ try {
374
+ response = await httpJson(
375
+ url,
376
+ 'POST',
377
+ { deviceId, hostname: os.hostname() },
378
+ buildApiAuthHeaders(accessToken)
379
+ );
380
+ } catch (error) {
381
+ printApiError(error);
382
+ process.exit(1);
383
+ }
384
+
385
+ const config = readDeviceConfig();
386
+ writeDeviceConfig({ ...config, activeKit: kitId });
387
+
388
+ const result = (response && response.data) ? response.data : response;
389
+ if (!quiet) {
390
+ log(JSON.stringify(result));
391
+ }
392
+
393
+ let syncResult;
394
+ try {
395
+ syncResult = await performSync({ accessToken, baseUrl, dryRun, force, quiet });
396
+ } catch (error) {
397
+ printApiError(error);
398
+ process.exit(1);
399
+ }
400
+ printSyncSummary(syncResult, quiet);
401
+
402
+ // US-012: fetch this Kit's own autoSyncEnabled flag and reconcile the local scheduled task to
403
+ // match it. A failure here is fetched best-effort — it must never turn a successful `kit use`
404
+ // (device registered, sync applied) into a failing command.
405
+ const kitUrl = new URL(`/api/v1/kits/${encodeURIComponent(kitId)}`, baseUrl).toString();
406
+ try {
407
+ const kitResponse = await httpJson(kitUrl, 'GET', undefined, buildApiAuthHeaders(accessToken));
408
+ const kit = (kitResponse && kitResponse.data && kitResponse.data.kit) ? kitResponse.data.kit : null;
409
+ reconcileScheduledSyncForKit(kitId, !!(kit && kit.autoSyncEnabled), quiet);
410
+ } catch (error) {
411
+ if (!quiet) {
412
+ warn(`Warning: failed to fetch Kit autosync setting: ${error.message}`);
413
+ }
414
+ }
415
+ }
416
+
417
+ // `empirical kit sync [--dry-run] [--quiet] [--force]` — see skillSync.js for the actual
418
+ // version-check/resolved-fetch/per-target-write/manifest logic; this just wires CLI flags,
419
+ // auth, and stdout/stderr reporting around it.
420
+ async function runKitSync(args) {
421
+ const dryRun = parseFlag(args, '--dry-run');
422
+ const quiet = parseFlag(args, '--quiet');
423
+ const force = parseFlag(args, '--force');
424
+
425
+ const accessToken = requireAccessToken();
426
+ const baseUrl = resolveApiBaseUrl();
427
+
428
+ let result;
429
+ try {
430
+ result = await performSync({ accessToken, baseUrl, dryRun, force, quiet });
431
+ } catch (error) {
432
+ printApiError(error);
433
+ process.exit(1);
434
+ }
435
+
436
+ printSyncSummary(result, quiet);
437
+ }
438
+
439
+ async function runKitCommand(subArgs) {
440
+ const sub = subArgs[0];
441
+ const rest = subArgs.slice(1);
442
+
443
+ return withCliAnalyticsContext(
444
+ { command: `kit.${sub || 'unknown'}`, subcommand: sub || 'unknown' },
445
+ async () => {
446
+ // US-016: single choke point — every interactive kit subcommand (sync included; it's
447
+ // where entries actually get cleared) surfaces the pending-review banner first, before
448
+ // doing its own work. `--quiet` may appear anywhere in `rest` (not every subcommand's own
449
+ // parser reads it), so check for it directly here rather than relying on each handler.
450
+ surfacePendingReviewBanner(parseFlag(rest, '--quiet'));
451
+
452
+ if (sub === 'create') return runKitCreate(rest);
453
+ if (sub === 'add-source') return runKitAddSource(rest);
454
+ if (sub === 'remove-source') return runKitRemoveSource(rest);
455
+ if (sub === 'list') return runKitList(rest);
456
+ if (sub === 'autosync') return runKitAutosync(rest);
457
+ if (sub === 'devices') return runKitDevices(rest);
458
+ if (sub === 'deactivate') return runKitDeactivate(rest);
459
+ if (sub === 'use') return runKitUse(rest);
460
+ if (sub === 'sync') return runKitSync(rest);
461
+
462
+ warn('Unknown kit subcommand: ' + (sub || '(none)'));
463
+ warn('Usage: empirical kit <create|add-source|remove-source|list|autosync|devices|deactivate|use|sync> [options]');
464
+ process.exit(1);
465
+ }
466
+ );
467
+ }
468
+
469
+ // ─── Skill subcommands ────────────────────────────────────────────────────────
470
+
471
+ async function runSkillPush(args) {
472
+ const filePath = args[0] && !args[0].startsWith('--') ? args[0] : '';
473
+ if (!filePath) {
474
+ warn('Usage: empirical skill push <path/to/SKILL.md> [--kit <kitId>]');
475
+ warn('Wrote or edited a SKILL.md? Push it so it follows you across machines/projects. See `empirical --help`.');
476
+ process.exit(1);
477
+ }
478
+
479
+ const kitId = parseOption(args, '--kit', '');
480
+
481
+ let raw;
482
+ try {
483
+ raw = fs.readFileSync(path.resolve(filePath), 'utf8');
484
+ } catch (error) {
485
+ warn(JSON.stringify({ error: true, code: 'file_read_error', message: error.message }));
486
+ process.exit(1);
487
+ }
488
+
489
+ let parsed;
490
+ try {
491
+ parsed = matter(raw);
492
+ } catch (error) {
493
+ warn(JSON.stringify({ error: true, code: 'frontmatter_parse_error', message: error.message }));
494
+ process.exit(1);
495
+ }
496
+
497
+ const name = typeof parsed.data.name === 'string' ? parsed.data.name.trim() : '';
498
+ const description = typeof parsed.data.description === 'string' ? parsed.data.description.trim() : '';
499
+ const bodyMarkdown = parsed.content;
500
+
501
+ if (!name || !description) {
502
+ warn(JSON.stringify({
503
+ error: true,
504
+ code: 'invalid_frontmatter',
505
+ message: 'SKILL.md frontmatter must include both "name" and "description".'
506
+ }));
507
+ process.exit(1);
508
+ }
509
+
510
+ const accessToken = requireAccessToken();
511
+ const baseUrl = resolveApiBaseUrl();
512
+ const url = new URL('/api/v1/skills/push', baseUrl).toString();
513
+
514
+ const body = { name, description, bodyMarkdown };
515
+ if (kitId) body.kitId = kitId;
516
+
517
+ let response;
518
+ try {
519
+ response = await httpJson(url, 'POST', body, buildApiAuthHeaders(accessToken));
520
+ } catch (error) {
521
+ printApiError(error);
522
+ process.exit(1);
523
+ }
524
+
525
+ const result = (response && response.data) ? response.data : response;
526
+ log(JSON.stringify(result));
527
+ }
528
+
529
+ async function runSkillHistory(args) {
530
+ const authoredSkillId = args[0] && !args[0].startsWith('--') ? args[0] : '';
531
+ if (!authoredSkillId) {
532
+ warn('Usage: empirical skill history <authoredSkillId>');
533
+ process.exit(1);
534
+ }
535
+
536
+ const accessToken = requireAccessToken();
537
+ const baseUrl = resolveApiBaseUrl();
538
+ const url = new URL(
539
+ `/api/v1/skills/${encodeURIComponent(authoredSkillId)}/history`,
540
+ baseUrl
541
+ ).toString();
542
+
543
+ let response;
544
+ try {
545
+ response = await httpJson(url, 'GET', undefined, buildApiAuthHeaders(accessToken));
546
+ } catch (error) {
547
+ printApiError(error);
548
+ process.exit(1);
549
+ }
550
+
551
+ const result = (response && response.data) ? response.data : response;
552
+ log(JSON.stringify(result));
553
+ }
554
+
555
+ async function runSkillRollback(args) {
556
+ const authoredSkillId = args[0] && !args[0].startsWith('--') ? args[0] : '';
557
+ const toVersionRaw = parseOption(args, '--to-version', '');
558
+ const toVersion = Number(toVersionRaw);
559
+
560
+ if (!authoredSkillId || !toVersionRaw || !Number.isInteger(toVersion) || toVersion < 1) {
561
+ warn('Usage: empirical skill rollback <authoredSkillId> --to-version <n>');
562
+ process.exit(1);
563
+ }
564
+
565
+ const accessToken = requireAccessToken();
566
+ const baseUrl = resolveApiBaseUrl();
567
+ const url = new URL(
568
+ `/api/v1/skills/${encodeURIComponent(authoredSkillId)}/rollback`,
569
+ baseUrl
570
+ ).toString();
571
+
572
+ let response;
573
+ try {
574
+ response = await httpJson(url, 'POST', { toVersion }, buildApiAuthHeaders(accessToken));
575
+ } catch (error) {
576
+ printApiError(error);
577
+ process.exit(1);
578
+ }
579
+
580
+ const result = (response && response.data) ? response.data : response;
581
+ log(JSON.stringify(result));
582
+ }
583
+
584
+ async function runSkillCommand(subArgs) {
585
+ const sub = subArgs[0];
586
+ const rest = subArgs.slice(1);
587
+
588
+ return withCliAnalyticsContext(
589
+ { command: `skill.${sub || 'unknown'}`, subcommand: sub || 'unknown' },
590
+ async () => {
591
+ if (sub === 'push') return runSkillPush(rest);
592
+ if (sub === 'list') return runSkillList(rest);
593
+ if (sub === 'history') return runSkillHistory(rest);
594
+ if (sub === 'rollback') return runSkillRollback(rest);
595
+
596
+ warn('Unknown skill subcommand: ' + (sub || '(none)'));
597
+ warn('Usage: empirical skill <list|push|history|rollback> [options]');
598
+ process.exit(1);
599
+ }
600
+ );
601
+ }
602
+
603
+ module.exports = {
604
+ runKitCreate,
605
+ runKitAddSource,
606
+ runKitRemoveSource,
607
+ runKitList,
608
+ runKitAutosync,
609
+ runKitDevices,
610
+ runKitDeactivate,
611
+ runKitUse,
612
+ runKitSync,
613
+ runSkillList,
614
+ runDeviceList,
615
+ runDeviceShow,
616
+ runDeviceCommand,
617
+ runKitCommand,
618
+ runSkillPush,
619
+ runSkillHistory,
620
+ runSkillRollback,
621
+ runSkillCommand
622
+ };