contextspin 0.1.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/.contextspin.example.json +72 -0
- package/LICENSE +21 -0
- package/README.md +318 -0
- package/package.json +40 -0
- package/src/cli.js +492 -0
- package/src/config.js +232 -0
- package/src/daemon-entry.js +8 -0
- package/src/daemon.js +294 -0
- package/src/formatter.js +166 -0
- package/src/inject/patcher.js +757 -0
- package/src/inject/statusline.js +310 -0
- package/src/runner.js +69 -0
- package/src/sources/cli.js +148 -0
- package/src/sources/http.js +294 -0
- package/src/sources/mcp.js +586 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/cli.js — Commander-based command-line interface for ContextSpin.
|
|
3
|
+
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import fsp from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import process from 'node:process';
|
|
8
|
+
import readline from 'node:readline/promises';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
CONFIG_PATH,
|
|
15
|
+
configExists,
|
|
16
|
+
loadConfig,
|
|
17
|
+
saveConfig,
|
|
18
|
+
normalizeConfig,
|
|
19
|
+
} from './config.js';
|
|
20
|
+
import {
|
|
21
|
+
startDaemonDetached,
|
|
22
|
+
stopDaemon,
|
|
23
|
+
isDaemonRunning,
|
|
24
|
+
readCache,
|
|
25
|
+
} from './daemon.js';
|
|
26
|
+
import { installStatusline, uninstallStatusline } from './inject/statusline.js';
|
|
27
|
+
import { installPatcher, restorePatcher } from './inject/patcher.js';
|
|
28
|
+
|
|
29
|
+
/** Absolute path to this module's directory. */
|
|
30
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
/** Absolute path to the package root (one level up from src/). */
|
|
32
|
+
const ROOT = path.resolve(HERE, '..');
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read the package version from package.json, resolved relative to this module
|
|
36
|
+
* (never hard-coded). Falls back to "0.1.0" if it cannot be read.
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
function readVersion() {
|
|
40
|
+
try {
|
|
41
|
+
const pkgPath = path.join(ROOT, 'package.json');
|
|
42
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
43
|
+
return pkg.version || '0.1.0';
|
|
44
|
+
} catch {
|
|
45
|
+
return '0.1.0';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Wrap an async command action so any thrown error prints a single clean line
|
|
51
|
+
* and exits with code 1.
|
|
52
|
+
* @param {(...args:any[])=>Promise<void>} fn
|
|
53
|
+
* @returns {(...args:any[])=>Promise<void>}
|
|
54
|
+
*/
|
|
55
|
+
function action(fn) {
|
|
56
|
+
return async (...args) => {
|
|
57
|
+
try {
|
|
58
|
+
await fn(...args);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
const message = err && err.message ? err.message : String(err);
|
|
61
|
+
console.error(`contextspin: ${message}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Format a millisecond age into a short human string (e.g. "12s", "3m", "2h").
|
|
69
|
+
* @param {number} ms
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
function formatAge(ms) {
|
|
73
|
+
if (!Number.isFinite(ms) || ms < 0) return '?';
|
|
74
|
+
const s = Math.floor(ms / 1000);
|
|
75
|
+
if (s < 60) return `${s}s`;
|
|
76
|
+
const m = Math.floor(s / 60);
|
|
77
|
+
if (m < 60) return `${m}m`;
|
|
78
|
+
const h = Math.floor(m / 60);
|
|
79
|
+
if (h < 24) return `${h}h`;
|
|
80
|
+
const d = Math.floor(h / 24);
|
|
81
|
+
return `${d}d`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Print the "next steps" hint shown when no config is present.
|
|
86
|
+
* @returns {void}
|
|
87
|
+
*/
|
|
88
|
+
function printSetupHint() {
|
|
89
|
+
console.error('No ContextSpin config found.');
|
|
90
|
+
console.error('Run: contextspin setup');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Write the bundled example config to the destination path. Confirms before
|
|
95
|
+
* overwriting an existing file unless `force` is true.
|
|
96
|
+
* @param {string} dest
|
|
97
|
+
* @param {boolean} force
|
|
98
|
+
* @returns {Promise<boolean>} true if written, false if skipped.
|
|
99
|
+
*/
|
|
100
|
+
async function writeExampleConfig(dest, force) {
|
|
101
|
+
const examplePath = path.join(ROOT, '.contextspin.example.json');
|
|
102
|
+
const raw = await fsp.readFile(examplePath, 'utf8');
|
|
103
|
+
if (fs.existsSync(dest) && !force) {
|
|
104
|
+
console.log(`Config already exists at ${dest} (left unchanged).`);
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
await fsp.writeFile(dest, raw);
|
|
108
|
+
console.log(`Wrote example config to ${dest}`);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Run the setup command: create a config either non-interactively (example
|
|
114
|
+
* config) or via an interactive prompt that builds a minimal config.
|
|
115
|
+
* @param {{ yes?: boolean }} opts
|
|
116
|
+
* @returns {Promise<void>}
|
|
117
|
+
*/
|
|
118
|
+
async function runSetup(opts = {}) {
|
|
119
|
+
const interactive = process.stdin.isTTY && !opts.yes;
|
|
120
|
+
|
|
121
|
+
if (!interactive) {
|
|
122
|
+
// Non-TTY or --yes: drop the example config unless one already exists.
|
|
123
|
+
if (configExists()) {
|
|
124
|
+
console.log(`Config already exists at ${CONFIG_PATH} (left unchanged).`);
|
|
125
|
+
} else {
|
|
126
|
+
await writeExampleConfig(CONFIG_PATH, false);
|
|
127
|
+
}
|
|
128
|
+
console.log('');
|
|
129
|
+
console.log('Next steps:');
|
|
130
|
+
console.log(' contextspin start # start the background daemon');
|
|
131
|
+
console.log(' contextspin inject # wire up your Claude Code status bar');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const rl = readline.createInterface({
|
|
136
|
+
input: process.stdin,
|
|
137
|
+
output: process.stdout,
|
|
138
|
+
});
|
|
139
|
+
try {
|
|
140
|
+
if (configExists()) {
|
|
141
|
+
const ans = (
|
|
142
|
+
await rl.question(
|
|
143
|
+
`A config already exists at ${CONFIG_PATH}. Overwrite? (y/N) `,
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
.trim()
|
|
147
|
+
.toLowerCase();
|
|
148
|
+
if (ans !== 'y' && ans !== 'yes') {
|
|
149
|
+
console.log('Keeping the existing config. Nothing changed.');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const modeRaw = (
|
|
155
|
+
await rl.question('Injection mode? statusline / patcher / both [statusline]: ')
|
|
156
|
+
)
|
|
157
|
+
.trim()
|
|
158
|
+
.toLowerCase();
|
|
159
|
+
const mode = ['statusline', 'patcher', 'both'].includes(modeRaw)
|
|
160
|
+
? modeRaw
|
|
161
|
+
: 'statusline';
|
|
162
|
+
|
|
163
|
+
const refreshRaw = (
|
|
164
|
+
await rl.question('Refresh interval in seconds [30]: ')
|
|
165
|
+
).trim();
|
|
166
|
+
const refreshParsed = Number.parseInt(refreshRaw, 10);
|
|
167
|
+
const refresh = Number.isFinite(refreshParsed) && refreshParsed > 0
|
|
168
|
+
? refreshParsed
|
|
169
|
+
: 30;
|
|
170
|
+
|
|
171
|
+
/** @type {Array<object>} */
|
|
172
|
+
const sources = [];
|
|
173
|
+
const seedAns = (
|
|
174
|
+
await rl.question('Seed a couple of safe starter sources? (Y/n) ')
|
|
175
|
+
)
|
|
176
|
+
.trim()
|
|
177
|
+
.toLowerCase();
|
|
178
|
+
if (seedAns !== 'n' && seedAns !== 'no') {
|
|
179
|
+
// Safe starters: read-only `gh` queries that do nothing harmful.
|
|
180
|
+
sources.push({
|
|
181
|
+
type: 'cli',
|
|
182
|
+
command: 'gh pr list --review-requested @me --json title,number --limit 3',
|
|
183
|
+
format: 'PR #{{ number }} needs your review: {{ title }}',
|
|
184
|
+
label: 'GitHub',
|
|
185
|
+
cooldown: 120,
|
|
186
|
+
maxSnippets: 3,
|
|
187
|
+
});
|
|
188
|
+
sources.push({
|
|
189
|
+
type: 'cli',
|
|
190
|
+
command: 'gh run list --json status,name,headBranch --limit 5',
|
|
191
|
+
filter: '{{ status }} == failure',
|
|
192
|
+
format: 'CI failing: {{ name }} on {{ headBranch }}',
|
|
193
|
+
label: 'CI',
|
|
194
|
+
cooldown: 60,
|
|
195
|
+
maxSnippets: 2,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const config = normalizeConfig({
|
|
200
|
+
sources,
|
|
201
|
+
injection: { mode, refresh },
|
|
202
|
+
});
|
|
203
|
+
await saveConfig(config, CONFIG_PATH);
|
|
204
|
+
console.log(`Saved config to ${CONFIG_PATH}`);
|
|
205
|
+
console.log('');
|
|
206
|
+
console.log('Next steps:');
|
|
207
|
+
console.log(' contextspin start # start the background daemon');
|
|
208
|
+
console.log(' contextspin inject # wire up your Claude Code status bar');
|
|
209
|
+
} finally {
|
|
210
|
+
rl.close();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Start the background daemon. Requires a valid config.
|
|
216
|
+
* @returns {Promise<void>}
|
|
217
|
+
*/
|
|
218
|
+
async function runStart() {
|
|
219
|
+
if (!configExists()) {
|
|
220
|
+
printSetupHint();
|
|
221
|
+
process.exit(1);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
// loadConfig validates; surfaces a clean error if the config is broken.
|
|
225
|
+
await loadConfig();
|
|
226
|
+
const res = await startDaemonDetached();
|
|
227
|
+
if (res.already) {
|
|
228
|
+
console.log(`ContextSpin daemon already running (pid ${res.pid}).`);
|
|
229
|
+
} else {
|
|
230
|
+
console.log(`ContextSpin daemon started (pid ${res.pid}).`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Stop the background daemon.
|
|
236
|
+
* @returns {Promise<void>}
|
|
237
|
+
*/
|
|
238
|
+
async function runStop() {
|
|
239
|
+
const res = await stopDaemon();
|
|
240
|
+
if (res.stopped) {
|
|
241
|
+
console.log(`ContextSpin daemon stopped (pid ${res.pid}).`);
|
|
242
|
+
} else {
|
|
243
|
+
console.log('ContextSpin daemon was not running.');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Restart the background daemon: stop then start.
|
|
249
|
+
* @returns {Promise<void>}
|
|
250
|
+
*/
|
|
251
|
+
async function runRestart() {
|
|
252
|
+
await runStop();
|
|
253
|
+
await runStart();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Print the daemon running state plus the current cache contents.
|
|
258
|
+
* @returns {Promise<void>}
|
|
259
|
+
*/
|
|
260
|
+
async function runStatus() {
|
|
261
|
+
const { running, pid } = isDaemonRunning();
|
|
262
|
+
if (running) {
|
|
263
|
+
console.log(`Daemon: running (pid ${pid})`);
|
|
264
|
+
} else {
|
|
265
|
+
console.log('Daemon: stopped');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const cache = await readCache();
|
|
269
|
+
const snippets = Array.isArray(cache.snippets) ? cache.snippets : [];
|
|
270
|
+
if (cache.updatedAt) {
|
|
271
|
+
console.log(`Cache updated: ${cache.updatedAt}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (snippets.length === 0) {
|
|
275
|
+
console.log('No snippets cached yet.');
|
|
276
|
+
if (!running) {
|
|
277
|
+
console.log('Hint: run `contextspin start` to begin collecting context.');
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const now = Date.now();
|
|
283
|
+
console.log('');
|
|
284
|
+
console.log('Snippets:');
|
|
285
|
+
for (const snip of snippets) {
|
|
286
|
+
const fetched = Date.parse(snip.fetchedAt);
|
|
287
|
+
const age = Number.isFinite(fetched) ? formatAge(now - fetched) : '?';
|
|
288
|
+
const src = snip.source || `#${snip.sourceId}`;
|
|
289
|
+
const shown = Number.isFinite(snip.shownCount) ? snip.shownCount : 0;
|
|
290
|
+
console.log(` [${src}] ${snip.text} (age ${age}, shown ${shown})`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Resolve the injection mode from a CLI option or the config default.
|
|
296
|
+
* @param {string|undefined} optionMode
|
|
297
|
+
* @param {object} config
|
|
298
|
+
* @returns {string}
|
|
299
|
+
*/
|
|
300
|
+
function resolveMode(optionMode, config) {
|
|
301
|
+
if (optionMode) return optionMode;
|
|
302
|
+
return config && config.injection && config.injection.mode
|
|
303
|
+
? config.injection.mode
|
|
304
|
+
: 'statusline';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Run the inject command for the chosen mode (statusline / patcher / both).
|
|
309
|
+
* @param {{ mode?: string }} opts
|
|
310
|
+
* @returns {Promise<void>}
|
|
311
|
+
*/
|
|
312
|
+
async function runInject(opts = {}) {
|
|
313
|
+
const config = await loadConfig();
|
|
314
|
+
const mode = resolveMode(opts.mode, config);
|
|
315
|
+
if (!['statusline', 'patcher', 'both'].includes(mode)) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
`unknown injection mode "${mode}" (expected statusline, patcher, or both)`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (mode === 'statusline' || mode === 'both') {
|
|
322
|
+
const res = await installStatusline(config);
|
|
323
|
+
console.log('Statusline installed:');
|
|
324
|
+
console.log(` script: ${res.statuslineSh}`);
|
|
325
|
+
console.log(` renderer: ${res.statuslineJs}`);
|
|
326
|
+
console.log(` settings: ${res.settingsPath}`);
|
|
327
|
+
if (res.backedUp) {
|
|
328
|
+
console.log(' (backed up your previous statusLine setting)');
|
|
329
|
+
}
|
|
330
|
+
if (res.warning) {
|
|
331
|
+
console.log(` warning: ${res.warning}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (mode === 'patcher' || mode === 'both') {
|
|
336
|
+
const res = await installPatcher(config);
|
|
337
|
+
if (res.warning) {
|
|
338
|
+
console.log(`Patcher: ${res.warning}`);
|
|
339
|
+
}
|
|
340
|
+
const patched = Array.isArray(res.patched) ? res.patched : [];
|
|
341
|
+
if (patched.length > 0) {
|
|
342
|
+
console.log('Patcher applied to:');
|
|
343
|
+
for (const p of patched) {
|
|
344
|
+
const status = p.patched ? 'patched' : 'skipped';
|
|
345
|
+
const note = p.note ? ` — ${p.note}` : '';
|
|
346
|
+
console.log(` [${status}] ${p.path}${note}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (res.wrapper) {
|
|
350
|
+
console.log(`Wrapper script: ${res.wrapper}`);
|
|
351
|
+
}
|
|
352
|
+
if (res.note) {
|
|
353
|
+
console.log(res.note);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Run the uninject command, reversing whichever injection mode is selected.
|
|
360
|
+
* @param {{ mode?: string }} opts
|
|
361
|
+
* @returns {Promise<void>}
|
|
362
|
+
*/
|
|
363
|
+
async function runUninject(opts = {}) {
|
|
364
|
+
const config = await loadConfig();
|
|
365
|
+
const mode = resolveMode(opts.mode, config);
|
|
366
|
+
if (!['statusline', 'patcher', 'both'].includes(mode)) {
|
|
367
|
+
throw new Error(
|
|
368
|
+
`unknown injection mode "${mode}" (expected statusline, patcher, or both)`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (mode === 'statusline' || mode === 'both') {
|
|
373
|
+
const res = await uninstallStatusline();
|
|
374
|
+
if (res.removed) {
|
|
375
|
+
console.log(
|
|
376
|
+
res.restored
|
|
377
|
+
? 'Statusline removed (restored your previous settings).'
|
|
378
|
+
: 'Statusline removed.',
|
|
379
|
+
);
|
|
380
|
+
} else {
|
|
381
|
+
console.log('Statusline: nothing to remove.');
|
|
382
|
+
}
|
|
383
|
+
if (res.note) console.log(` ${res.note}`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (mode === 'patcher' || mode === 'both') {
|
|
387
|
+
const res = await restorePatcher();
|
|
388
|
+
const restored = Array.isArray(res.restored) ? res.restored : [];
|
|
389
|
+
if (restored.length > 0) {
|
|
390
|
+
console.log('Patcher restore:');
|
|
391
|
+
for (const r of restored) {
|
|
392
|
+
const status = r.restored ? 'restored' : 'failed';
|
|
393
|
+
const note = r.note ? ` — ${r.note}` : '';
|
|
394
|
+
console.log(` [${status}] ${r.path}${note}`);
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
console.log('Patcher: no patched installs with backups found.');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* The default action when no subcommand is given: set up if there is no config,
|
|
404
|
+
* otherwise start the daemon and inject per the configured mode.
|
|
405
|
+
* @returns {Promise<void>}
|
|
406
|
+
*/
|
|
407
|
+
async function runDefault() {
|
|
408
|
+
if (!configExists()) {
|
|
409
|
+
await runSetup({});
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
await runStart();
|
|
413
|
+
const config = await loadConfig();
|
|
414
|
+
await runInject({ mode: config.injection.mode });
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Build and configure the Commander program.
|
|
419
|
+
* @returns {Command}
|
|
420
|
+
*/
|
|
421
|
+
function buildProgram() {
|
|
422
|
+
const program = new Command();
|
|
423
|
+
|
|
424
|
+
program
|
|
425
|
+
.name('contextspin')
|
|
426
|
+
.description(
|
|
427
|
+
'Replace your Claude Code spinner/statusline with live org context.',
|
|
428
|
+
)
|
|
429
|
+
.version(readVersion())
|
|
430
|
+
.showHelpAfterError();
|
|
431
|
+
|
|
432
|
+
program
|
|
433
|
+
.command('setup')
|
|
434
|
+
.description('Create a ContextSpin config (interactive, or --yes for example)')
|
|
435
|
+
.option('--yes', 'skip prompts and write the example config')
|
|
436
|
+
.action(action(async (opts) => runSetup(opts)));
|
|
437
|
+
|
|
438
|
+
program
|
|
439
|
+
.command('start')
|
|
440
|
+
.description('Start the background daemon')
|
|
441
|
+
.action(action(async () => runStart()));
|
|
442
|
+
|
|
443
|
+
program
|
|
444
|
+
.command('stop')
|
|
445
|
+
.description('Stop the background daemon')
|
|
446
|
+
.action(action(async () => runStop()));
|
|
447
|
+
|
|
448
|
+
program
|
|
449
|
+
.command('restart')
|
|
450
|
+
.description('Restart the background daemon')
|
|
451
|
+
.action(action(async () => runRestart()));
|
|
452
|
+
|
|
453
|
+
program
|
|
454
|
+
.command('status')
|
|
455
|
+
.description('Show daemon state and cached snippets')
|
|
456
|
+
.action(action(async () => runStatus()));
|
|
457
|
+
|
|
458
|
+
program
|
|
459
|
+
.command('inject')
|
|
460
|
+
.description('Wire ContextSpin into Claude Code (statusline/patcher/both)')
|
|
461
|
+
.option('--mode <m>', 'injection mode: statusline, patcher, or both')
|
|
462
|
+
.action(action(async (opts) => runInject(opts)));
|
|
463
|
+
|
|
464
|
+
program
|
|
465
|
+
.command('uninject')
|
|
466
|
+
.description('Remove ContextSpin from Claude Code')
|
|
467
|
+
.option('--mode <m>', 'injection mode: statusline, patcher, or both')
|
|
468
|
+
.action(action(async (opts) => runUninject(opts)));
|
|
469
|
+
|
|
470
|
+
// Default action: run when no subcommand is provided. Any leftover operand
|
|
471
|
+
// means the user typed an unrecognized command (e.g. a typo) — error on it
|
|
472
|
+
// rather than silently running the (potentially destructive) default.
|
|
473
|
+
program.action(
|
|
474
|
+
action(async (_opts, command) => {
|
|
475
|
+
const operands = (command && command.args) || [];
|
|
476
|
+
if (operands.length > 0) {
|
|
477
|
+
command.error(`unknown command '${operands[0]}'`);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
await runDefault();
|
|
481
|
+
}),
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
return program;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const program = buildProgram();
|
|
488
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
489
|
+
const message = err && err.message ? err.message : String(err);
|
|
490
|
+
console.error(`contextspin: ${message}`);
|
|
491
|
+
process.exit(1);
|
|
492
|
+
});
|