@karmaniverous/jeeves 0.4.0 → 0.4.1
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/dist/cli/jeeves/index.js +85 -5
- package/dist/cli/plugin/index.js +1003 -0
- package/dist/cli/service/index.js +916 -0
- package/dist/index.d.ts +465 -171
- package/dist/index.js +3152 -1937
- package/package.json +1 -1
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync, spawn } from 'node:child_process';
|
|
3
|
+
import { existsSync, unlinkSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import * as commander from 'commander';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
|
|
9
|
+
function getDefaultExportFromCjs (x) {
|
|
10
|
+
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getAugmentedNamespace(n) {
|
|
14
|
+
if (Object.prototype.hasOwnProperty.call(n, '__esModule')) return n;
|
|
15
|
+
var f = n.default;
|
|
16
|
+
if (typeof f == "function") {
|
|
17
|
+
var a = function a () {
|
|
18
|
+
var isInstance = false;
|
|
19
|
+
try {
|
|
20
|
+
isInstance = this instanceof a;
|
|
21
|
+
} catch {}
|
|
22
|
+
if (isInstance) {
|
|
23
|
+
return Reflect.construct(f, arguments, this.constructor);
|
|
24
|
+
}
|
|
25
|
+
return f.apply(this, arguments);
|
|
26
|
+
};
|
|
27
|
+
a.prototype = f.prototype;
|
|
28
|
+
} else a = {};
|
|
29
|
+
Object.defineProperty(a, '__esModule', {value: true});
|
|
30
|
+
Object.keys(n).forEach(function (k) {
|
|
31
|
+
var d = Object.getOwnPropertyDescriptor(n, k);
|
|
32
|
+
Object.defineProperty(a, k, d.get ? d : {
|
|
33
|
+
enumerable: true,
|
|
34
|
+
get: function () {
|
|
35
|
+
return n[k];
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
return a;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
var extraTypings = {exports: {}};
|
|
43
|
+
|
|
44
|
+
var require$$0 = /*@__PURE__*/getAugmentedNamespace(commander);
|
|
45
|
+
|
|
46
|
+
var hasRequiredExtraTypings;
|
|
47
|
+
|
|
48
|
+
function requireExtraTypings () {
|
|
49
|
+
if (hasRequiredExtraTypings) return extraTypings.exports;
|
|
50
|
+
hasRequiredExtraTypings = 1;
|
|
51
|
+
(function (module, exports$1) {
|
|
52
|
+
const commander = require$$0;
|
|
53
|
+
|
|
54
|
+
exports$1 = module.exports = {};
|
|
55
|
+
|
|
56
|
+
// Return a different global program than commander,
|
|
57
|
+
// and don't also return it as default export.
|
|
58
|
+
exports$1.program = new commander.Command();
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Expose classes. The FooT versions are just types, so return Commander original implementations!
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
exports$1.Argument = commander.Argument;
|
|
65
|
+
exports$1.Command = commander.Command;
|
|
66
|
+
exports$1.CommanderError = commander.CommanderError;
|
|
67
|
+
exports$1.Help = commander.Help;
|
|
68
|
+
exports$1.InvalidArgumentError = commander.InvalidArgumentError;
|
|
69
|
+
exports$1.InvalidOptionArgumentError = commander.InvalidArgumentError; // Deprecated
|
|
70
|
+
exports$1.Option = commander.Option;
|
|
71
|
+
|
|
72
|
+
exports$1.createCommand = (name) => new commander.Command(name);
|
|
73
|
+
exports$1.createOption = (flags, description) =>
|
|
74
|
+
new commander.Option(flags, description);
|
|
75
|
+
exports$1.createArgument = (name, description) =>
|
|
76
|
+
new commander.Argument(name, description);
|
|
77
|
+
} (extraTypings, extraTypings.exports));
|
|
78
|
+
return extraTypings.exports;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
var extraTypingsExports = requireExtraTypings();
|
|
82
|
+
var extraTypingsCommander = /*@__PURE__*/getDefaultExportFromCjs(extraTypingsExports);
|
|
83
|
+
|
|
84
|
+
// wrapper to provide named exports for ESM.
|
|
85
|
+
const {
|
|
86
|
+
program,
|
|
87
|
+
createCommand,
|
|
88
|
+
createArgument,
|
|
89
|
+
createOption,
|
|
90
|
+
CommanderError,
|
|
91
|
+
InvalidArgumentError,
|
|
92
|
+
InvalidOptionArgumentError, // deprecated old name
|
|
93
|
+
Command,
|
|
94
|
+
Argument,
|
|
95
|
+
Option,
|
|
96
|
+
Help,
|
|
97
|
+
} = extraTypingsCommander;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Zod schema for the Jeeves component descriptor.
|
|
101
|
+
*
|
|
102
|
+
* @remarks
|
|
103
|
+
* The descriptor replaces the v0.4.0 `JeevesComponent` interface with a
|
|
104
|
+
* Zod-first approach. The TypeScript type is inferred via `z.infer<>`.
|
|
105
|
+
* Validates at parse time: prime interval, callable functions.
|
|
106
|
+
*/
|
|
107
|
+
/**
|
|
108
|
+
* Check whether a number is prime.
|
|
109
|
+
*
|
|
110
|
+
* @param n - Number to check.
|
|
111
|
+
* @returns `true` if n is prime.
|
|
112
|
+
*/
|
|
113
|
+
function isPrime(n) {
|
|
114
|
+
if (n < 2)
|
|
115
|
+
return false;
|
|
116
|
+
if (n === 2)
|
|
117
|
+
return true;
|
|
118
|
+
if (n % 2 === 0)
|
|
119
|
+
return false;
|
|
120
|
+
for (let i = 3; i * i <= n; i += 2) {
|
|
121
|
+
if (n % i === 0)
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Zod schema for the Jeeves component descriptor.
|
|
128
|
+
*
|
|
129
|
+
* @remarks
|
|
130
|
+
* Single source of truth for what a component must provide.
|
|
131
|
+
* Factories consume this descriptor to produce CLI commands,
|
|
132
|
+
* plugin tools, and HTTP handlers.
|
|
133
|
+
*/
|
|
134
|
+
z.object({
|
|
135
|
+
/** Component name (e.g., 'watcher', 'runner', 'server', 'meta'). */
|
|
136
|
+
name: z.string().min(1, 'name must be a non-empty string'),
|
|
137
|
+
/** Component version (from package.json). */
|
|
138
|
+
version: z.string().min(1, 'version must be a non-empty string'),
|
|
139
|
+
/** npm package name for the service. */
|
|
140
|
+
servicePackage: z.string().min(1),
|
|
141
|
+
/** npm package name for the plugin. */
|
|
142
|
+
pluginPackage: z.string().min(1),
|
|
143
|
+
/** System service name. Defaults to `jeeves-${name}` when not provided. */
|
|
144
|
+
serviceName: z.string().min(1).optional(),
|
|
145
|
+
/** Default port for the service's HTTP API. */
|
|
146
|
+
defaultPort: z.number().int().positive(),
|
|
147
|
+
/** Zod schema for validating config files. */
|
|
148
|
+
configSchema: z.custom((val) => val !== null &&
|
|
149
|
+
typeof val === 'object' &&
|
|
150
|
+
typeof val.parse === 'function', { message: 'configSchema must be a Zod schema' }),
|
|
151
|
+
/** Config file name (e.g., 'jeeves-watcher.config.json'). */
|
|
152
|
+
configFileName: z.string().min(1),
|
|
153
|
+
/** Returns a default config object for `init`. */
|
|
154
|
+
initTemplate: z.function().returns(z.record(z.unknown())),
|
|
155
|
+
/**
|
|
156
|
+
* Service-side callback after config apply. Receives the merged,
|
|
157
|
+
* validated config (not the raw patch). Optional — if omitted,
|
|
158
|
+
* write-only (service picks up changes on restart).
|
|
159
|
+
*/
|
|
160
|
+
onConfigApply: z
|
|
161
|
+
.function()
|
|
162
|
+
.args(z.record(z.unknown()))
|
|
163
|
+
.returns(z.promise(z.void()))
|
|
164
|
+
.optional(),
|
|
165
|
+
/**
|
|
166
|
+
* Returns command + args for launching the service process.
|
|
167
|
+
* Consumed by `start` CLI command and `service install`.
|
|
168
|
+
*/
|
|
169
|
+
startCommand: z.function().args(z.string()).returns(z.array(z.string())),
|
|
170
|
+
/** TOOLS.md section name (e.g., 'Watcher'). */
|
|
171
|
+
sectionId: z.string().min(1, 'sectionId must be a non-empty string'),
|
|
172
|
+
/** Refresh interval in seconds (must be a prime number). */
|
|
173
|
+
refreshIntervalSeconds: z.number().int().positive().refine(isPrime, {
|
|
174
|
+
message: 'refreshIntervalSeconds must be a prime number',
|
|
175
|
+
}),
|
|
176
|
+
/** Produce the component's TOOLS.md section content. */
|
|
177
|
+
generateToolsContent: z.function().returns(z.string()),
|
|
178
|
+
/** Component dependencies for HEARTBEAT alert suppression. */
|
|
179
|
+
dependencies: z
|
|
180
|
+
.object({
|
|
181
|
+
hard: z.array(z.string()),
|
|
182
|
+
soft: z.array(z.string()),
|
|
183
|
+
})
|
|
184
|
+
.optional(),
|
|
185
|
+
/** Extension point: add custom CLI commands to the service CLI. */
|
|
186
|
+
customCliCommands: z
|
|
187
|
+
.function()
|
|
188
|
+
.args(z.custom())
|
|
189
|
+
.returns(z.void())
|
|
190
|
+
.optional(),
|
|
191
|
+
/** Extension point: return additional plugin tool descriptors. */
|
|
192
|
+
customPluginTools: z
|
|
193
|
+
.function()
|
|
194
|
+
.args(z.custom())
|
|
195
|
+
.returns(z.array(z.unknown()))
|
|
196
|
+
.optional(),
|
|
197
|
+
});
|
|
198
|
+
/**
|
|
199
|
+
* Derive the effective service name from a descriptor.
|
|
200
|
+
*
|
|
201
|
+
* @param descriptor - The component descriptor.
|
|
202
|
+
* @returns The service name (explicit or derived from `jeeves-{name}`).
|
|
203
|
+
*/
|
|
204
|
+
function getEffectiveServiceName(descriptor) {
|
|
205
|
+
return descriptor.serviceName ?? `jeeves-${descriptor.name}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Workspace and config root initialization.
|
|
210
|
+
*
|
|
211
|
+
* @remarks
|
|
212
|
+
* `init()` must be called once before any other core library functions.
|
|
213
|
+
* It caches `workspacePath` and `configRoot` at module level.
|
|
214
|
+
* Core derives all namespaced paths from these values:
|
|
215
|
+
* - `{configRoot}/jeeves-core/` for core config
|
|
216
|
+
* - `{configRoot}/jeeves-{name}/` for each component
|
|
217
|
+
*/
|
|
218
|
+
/**
|
|
219
|
+
* Derive the component config directory from the component name.
|
|
220
|
+
*
|
|
221
|
+
* @param componentName - The component name (e.g., 'watcher', 'runner').
|
|
222
|
+
* @returns Absolute path to the component's config directory.
|
|
223
|
+
* @throws Error if `init()` has not been called.
|
|
224
|
+
*/
|
|
225
|
+
function getComponentConfigDir(componentName) {
|
|
226
|
+
throw new Error('jeeves-core: init() must be called first');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* HTTP helpers for the OpenClaw plugin SDK.
|
|
231
|
+
*
|
|
232
|
+
* @remarks
|
|
233
|
+
* Thin wrappers around `fetch` that throw on non-OK responses
|
|
234
|
+
* and handle JSON serialisation/deserialisation.
|
|
235
|
+
*/
|
|
236
|
+
/**
|
|
237
|
+
* Fetch a URL with an automatic abort timeout.
|
|
238
|
+
*
|
|
239
|
+
* @param url - URL to fetch.
|
|
240
|
+
* @param timeoutMs - Timeout in milliseconds before aborting.
|
|
241
|
+
* @param init - Optional `fetch` init options.
|
|
242
|
+
* @returns The fetch Response object.
|
|
243
|
+
*/
|
|
244
|
+
/**
|
|
245
|
+
* Fetch JSON from a URL, throwing on non-OK responses.
|
|
246
|
+
*
|
|
247
|
+
* @param url - URL to fetch.
|
|
248
|
+
* @param init - Optional `fetch` init options.
|
|
249
|
+
* @returns Parsed JSON response body.
|
|
250
|
+
* @throws Error with `HTTP {status}: {body}` message on non-OK responses.
|
|
251
|
+
*/
|
|
252
|
+
async function fetchJson(url, init) {
|
|
253
|
+
const res = await fetch(url, init);
|
|
254
|
+
if (!res.ok) {
|
|
255
|
+
throw new Error('HTTP ' + String(res.status) + ': ' + (await res.text()));
|
|
256
|
+
}
|
|
257
|
+
return res.json();
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* POST JSON to a URL and return parsed response.
|
|
261
|
+
*
|
|
262
|
+
* @param url - URL to POST to.
|
|
263
|
+
* @param body - Request body (will be JSON-stringified).
|
|
264
|
+
* @returns Parsed JSON response body.
|
|
265
|
+
*/
|
|
266
|
+
async function postJson(url, body) {
|
|
267
|
+
return fetchJson(url, {
|
|
268
|
+
method: 'POST',
|
|
269
|
+
headers: { 'Content-Type': 'application/json' },
|
|
270
|
+
body: JSON.stringify(body),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Platform-aware service state detection.
|
|
276
|
+
*
|
|
277
|
+
* @remarks
|
|
278
|
+
* Detects whether a system service is installed and running.
|
|
279
|
+
* Delegates to NSSM (Windows), systemd (Linux), or launchd (macOS).
|
|
280
|
+
*/
|
|
281
|
+
/**
|
|
282
|
+
* Detect the state of a system service by name.
|
|
283
|
+
*
|
|
284
|
+
* @param serviceName - The service name (e.g., 'jeeves-runner').
|
|
285
|
+
* @returns The detected service state.
|
|
286
|
+
*/
|
|
287
|
+
function getServiceState(serviceName) {
|
|
288
|
+
switch (process.platform) {
|
|
289
|
+
case 'win32':
|
|
290
|
+
return getServiceStateWindows(serviceName);
|
|
291
|
+
case 'darwin':
|
|
292
|
+
return getServiceStateMacOS(serviceName);
|
|
293
|
+
default:
|
|
294
|
+
return getServiceStateLinux(serviceName);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Windows: detect via NSSM.
|
|
299
|
+
* - Exit code 3 = service does not exist
|
|
300
|
+
* - "SERVICE_RUNNING" in output = running
|
|
301
|
+
* - Other output = stopped/paused
|
|
302
|
+
*/
|
|
303
|
+
function getServiceStateWindows(serviceName) {
|
|
304
|
+
try {
|
|
305
|
+
const output = execSync(`nssm status ${serviceName}`, {
|
|
306
|
+
encoding: 'utf-8',
|
|
307
|
+
timeout: 5000,
|
|
308
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
309
|
+
}).trim();
|
|
310
|
+
if (output.includes('SERVICE_RUNNING'))
|
|
311
|
+
return 'running';
|
|
312
|
+
return 'stopped';
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
// NSSM exits with code 3 when the service doesn't exist
|
|
316
|
+
if (isExecError(err) && err.status === 3)
|
|
317
|
+
return 'not_installed';
|
|
318
|
+
// Any other error (nssm not found, timeout, etc.) — treat as not installed
|
|
319
|
+
return 'not_installed';
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Linux: detect via systemd user services.
|
|
324
|
+
* - `systemctl --user is-enabled {name}.service` exits non-zero = not installed
|
|
325
|
+
* - `systemctl --user is-active {name}.service` returns "active" = running
|
|
326
|
+
*/
|
|
327
|
+
function getServiceStateLinux(serviceName) {
|
|
328
|
+
try {
|
|
329
|
+
execSync(`systemctl --user is-enabled ${serviceName}.service`, {
|
|
330
|
+
encoding: 'utf-8',
|
|
331
|
+
timeout: 5000,
|
|
332
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return 'not_installed';
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
const output = execSync(`systemctl --user is-active ${serviceName}.service`, {
|
|
340
|
+
encoding: 'utf-8',
|
|
341
|
+
timeout: 5000,
|
|
342
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
343
|
+
}).trim();
|
|
344
|
+
if (output === 'active')
|
|
345
|
+
return 'running';
|
|
346
|
+
return 'stopped';
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return 'stopped';
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* macOS: detect via launchctl.
|
|
354
|
+
* - `launchctl list {name}` exits non-zero = not installed
|
|
355
|
+
* - PID column is `-` or `0` = stopped
|
|
356
|
+
*/
|
|
357
|
+
function getServiceStateMacOS(serviceName) {
|
|
358
|
+
try {
|
|
359
|
+
const output = execSync(`launchctl list ${serviceName}`, {
|
|
360
|
+
encoding: 'utf-8',
|
|
361
|
+
timeout: 5000,
|
|
362
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
363
|
+
}).trim();
|
|
364
|
+
// launchctl list output formats:
|
|
365
|
+
// 1. Table row: "PID\tStatus\tLabel" (e.g., "1234\t0\tcom.jeeves.runner")
|
|
366
|
+
// 2. Plist-style: '"PID" = 1234;'
|
|
367
|
+
// 3. Single service: first token is the PID or "-"
|
|
368
|
+
// Try table format: first token is PID
|
|
369
|
+
const tableMatch = /^(\d+|-)\s/m.exec(output);
|
|
370
|
+
if (tableMatch) {
|
|
371
|
+
const pid = tableMatch[1];
|
|
372
|
+
return pid !== '-' && Number(pid) > 0 ? 'running' : 'stopped';
|
|
373
|
+
}
|
|
374
|
+
// Try plist-style: "PID" = <number>;
|
|
375
|
+
const plistMatch = /"PID"\s*=\s*(\d+)/m.exec(output);
|
|
376
|
+
if (plistMatch) {
|
|
377
|
+
return Number(plistMatch[1]) > 0 ? 'running' : 'stopped';
|
|
378
|
+
}
|
|
379
|
+
// If we got output but can't parse it, assume stopped (service exists but state unclear)
|
|
380
|
+
return 'stopped';
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return 'not_installed';
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/** Type guard for execSync errors with a status code. */
|
|
387
|
+
function isExecError(err) {
|
|
388
|
+
return (typeof err === 'object' &&
|
|
389
|
+
err !== null &&
|
|
390
|
+
'status' in err &&
|
|
391
|
+
typeof err.status === 'number');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Factory for platform-aware service lifecycle management.
|
|
396
|
+
*
|
|
397
|
+
* @remarks
|
|
398
|
+
* Produces a `ServiceManager` that handles install, uninstall, start,
|
|
399
|
+
* stop, restart, and status for system services. Delegates to NSSM
|
|
400
|
+
* (Windows), systemd (Linux), or launchd (macOS) based on platform.
|
|
401
|
+
*/
|
|
402
|
+
/** Exec helper that returns stdout. */
|
|
403
|
+
function run(cmd) {
|
|
404
|
+
return execSync(cmd, {
|
|
405
|
+
encoding: 'utf-8',
|
|
406
|
+
timeout: 30_000,
|
|
407
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
408
|
+
}).trim();
|
|
409
|
+
}
|
|
410
|
+
/** Exec helper that suppresses errors and returns success boolean. */
|
|
411
|
+
function runQuiet(cmd) {
|
|
412
|
+
try {
|
|
413
|
+
execSync(cmd, {
|
|
414
|
+
encoding: 'utf-8',
|
|
415
|
+
timeout: 30_000,
|
|
416
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
417
|
+
});
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Resolve the effective service name from options and descriptor.
|
|
426
|
+
*
|
|
427
|
+
* @param descriptor - Component descriptor.
|
|
428
|
+
* @param options - Optional overrides.
|
|
429
|
+
* @returns The service name to use.
|
|
430
|
+
*/
|
|
431
|
+
function resolveServiceName(descriptor, options) {
|
|
432
|
+
return options?.name ?? getEffectiveServiceName(descriptor);
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Resolve the config path for install.
|
|
436
|
+
*
|
|
437
|
+
* @param descriptor - Component descriptor.
|
|
438
|
+
* @param options - Optional overrides.
|
|
439
|
+
* @returns Absolute config file path.
|
|
440
|
+
*/
|
|
441
|
+
function resolveConfigFilePath(descriptor, options) {
|
|
442
|
+
if (options?.configPath)
|
|
443
|
+
return options.configPath;
|
|
444
|
+
const configDir = getComponentConfigDir(descriptor.name);
|
|
445
|
+
return join(configDir, descriptor.configFileName);
|
|
446
|
+
}
|
|
447
|
+
/** Build a Windows NSSM service manager. */
|
|
448
|
+
function createWindowsManager(descriptor) {
|
|
449
|
+
return {
|
|
450
|
+
install(options) {
|
|
451
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
452
|
+
const cfgPath = resolveConfigFilePath(descriptor, options);
|
|
453
|
+
const cmdArgs = descriptor.startCommand(cfgPath);
|
|
454
|
+
const appPath = cmdArgs[0];
|
|
455
|
+
const appArgs = cmdArgs.slice(1).join(' ');
|
|
456
|
+
run(`nssm install ${svcName} ${appPath}`);
|
|
457
|
+
if (appArgs) {
|
|
458
|
+
run(`nssm set ${svcName} AppParameters ${appArgs}`);
|
|
459
|
+
}
|
|
460
|
+
run(`nssm set ${svcName} AppStdout ${join(homedir(), `${svcName}.log`)}`);
|
|
461
|
+
run(`nssm set ${svcName} AppStderr ${join(homedir(), `${svcName}.log`)}`);
|
|
462
|
+
run(`nssm set ${svcName} AppRotateFiles 1`);
|
|
463
|
+
run(`nssm set ${svcName} AppRotateBytes 1048576`);
|
|
464
|
+
},
|
|
465
|
+
uninstall(options) {
|
|
466
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
467
|
+
runQuiet(`nssm stop ${svcName}`);
|
|
468
|
+
run(`nssm remove ${svcName} confirm`);
|
|
469
|
+
},
|
|
470
|
+
start(options) {
|
|
471
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
472
|
+
run(`nssm start ${svcName}`);
|
|
473
|
+
},
|
|
474
|
+
stop(options) {
|
|
475
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
476
|
+
run(`nssm stop ${svcName}`);
|
|
477
|
+
},
|
|
478
|
+
restart(options) {
|
|
479
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
480
|
+
run(`nssm restart ${svcName}`);
|
|
481
|
+
},
|
|
482
|
+
status(options) {
|
|
483
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
484
|
+
return getServiceState(svcName);
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Generate a systemd user unit file.
|
|
490
|
+
*
|
|
491
|
+
* @param svcName - Service name.
|
|
492
|
+
* @param cmdArgs - Command + args array.
|
|
493
|
+
* @returns Unit file content.
|
|
494
|
+
*/
|
|
495
|
+
function buildSystemdUnit(svcName, cmdArgs) {
|
|
496
|
+
const execStart = cmdArgs.join(' ');
|
|
497
|
+
return [
|
|
498
|
+
'[Unit]',
|
|
499
|
+
`Description=${svcName}`,
|
|
500
|
+
'After=network.target',
|
|
501
|
+
'',
|
|
502
|
+
'[Service]',
|
|
503
|
+
'Type=simple',
|
|
504
|
+
`ExecStart=${execStart}`,
|
|
505
|
+
'Restart=on-failure',
|
|
506
|
+
'RestartSec=5',
|
|
507
|
+
'',
|
|
508
|
+
'[Install]',
|
|
509
|
+
'WantedBy=default.target',
|
|
510
|
+
].join('\n');
|
|
511
|
+
}
|
|
512
|
+
/** Build a Linux systemd service manager. */
|
|
513
|
+
function createLinuxManager(descriptor) {
|
|
514
|
+
const unitDir = join(homedir(), '.config', 'systemd', 'user');
|
|
515
|
+
function unitPath(svcName) {
|
|
516
|
+
return join(unitDir, `${svcName}.service`);
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
install(options) {
|
|
520
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
521
|
+
const cfgPath = resolveConfigFilePath(descriptor, options);
|
|
522
|
+
const cmdArgs = descriptor.startCommand(cfgPath);
|
|
523
|
+
mkdirSync(unitDir, { recursive: true });
|
|
524
|
+
writeFileSync(unitPath(svcName), buildSystemdUnit(svcName, cmdArgs));
|
|
525
|
+
run('systemctl --user daemon-reload');
|
|
526
|
+
run(`systemctl --user enable ${svcName}.service`);
|
|
527
|
+
},
|
|
528
|
+
uninstall(options) {
|
|
529
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
530
|
+
runQuiet(`systemctl --user stop ${svcName}.service`);
|
|
531
|
+
runQuiet(`systemctl --user disable ${svcName}.service`);
|
|
532
|
+
const path = unitPath(svcName);
|
|
533
|
+
if (existsSync(path))
|
|
534
|
+
unlinkSync(path);
|
|
535
|
+
runQuiet('systemctl --user daemon-reload');
|
|
536
|
+
},
|
|
537
|
+
start(options) {
|
|
538
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
539
|
+
run(`systemctl --user start ${svcName}.service`);
|
|
540
|
+
},
|
|
541
|
+
stop(options) {
|
|
542
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
543
|
+
run(`systemctl --user stop ${svcName}.service`);
|
|
544
|
+
},
|
|
545
|
+
restart(options) {
|
|
546
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
547
|
+
run(`systemctl --user restart ${svcName}.service`);
|
|
548
|
+
},
|
|
549
|
+
status(options) {
|
|
550
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
551
|
+
return getServiceState(svcName);
|
|
552
|
+
},
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Generate a macOS launchd plist.
|
|
557
|
+
*
|
|
558
|
+
* @param svcName - Service label.
|
|
559
|
+
* @param cmdArgs - Command + args array.
|
|
560
|
+
* @returns Plist XML content.
|
|
561
|
+
*/
|
|
562
|
+
function buildLaunchdPlist(svcName, cmdArgs) {
|
|
563
|
+
const argsXml = cmdArgs.map((a) => ` <string>${a}</string>`).join('\n');
|
|
564
|
+
return [
|
|
565
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
566
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"',
|
|
567
|
+
' "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
568
|
+
'<plist version="1.0">',
|
|
569
|
+
'<dict>',
|
|
570
|
+
' <key>Label</key>',
|
|
571
|
+
` <string>${svcName}</string>`,
|
|
572
|
+
' <key>ProgramArguments</key>',
|
|
573
|
+
' <array>',
|
|
574
|
+
argsXml,
|
|
575
|
+
' </array>',
|
|
576
|
+
' <key>RunAtLoad</key>',
|
|
577
|
+
' <true/>',
|
|
578
|
+
' <key>KeepAlive</key>',
|
|
579
|
+
' <true/>',
|
|
580
|
+
' <key>StandardOutPath</key>',
|
|
581
|
+
` <string>${join(homedir(), 'Library', 'Logs', `${svcName}.log`)}</string>`,
|
|
582
|
+
' <key>StandardErrorPath</key>',
|
|
583
|
+
` <string>${join(homedir(), 'Library', 'Logs', `${svcName}.log`)}</string>`,
|
|
584
|
+
'</dict>',
|
|
585
|
+
'</plist>',
|
|
586
|
+
].join('\n');
|
|
587
|
+
}
|
|
588
|
+
/** Build a macOS launchd service manager. */
|
|
589
|
+
function createMacOSManager(descriptor) {
|
|
590
|
+
const agentsDir = join(homedir(), 'Library', 'LaunchAgents');
|
|
591
|
+
function plistPath(svcName) {
|
|
592
|
+
return join(agentsDir, `${svcName}.plist`);
|
|
593
|
+
}
|
|
594
|
+
return {
|
|
595
|
+
install(options) {
|
|
596
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
597
|
+
const cfgPath = resolveConfigFilePath(descriptor, options);
|
|
598
|
+
const cmdArgs = descriptor.startCommand(cfgPath);
|
|
599
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
600
|
+
writeFileSync(plistPath(svcName), buildLaunchdPlist(svcName, cmdArgs));
|
|
601
|
+
},
|
|
602
|
+
uninstall(options) {
|
|
603
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
604
|
+
runQuiet(`launchctl unload ${plistPath(svcName)}`);
|
|
605
|
+
const path = plistPath(svcName);
|
|
606
|
+
if (existsSync(path))
|
|
607
|
+
unlinkSync(path);
|
|
608
|
+
},
|
|
609
|
+
start(options) {
|
|
610
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
611
|
+
run(`launchctl load ${plistPath(svcName)}`);
|
|
612
|
+
},
|
|
613
|
+
stop(options) {
|
|
614
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
615
|
+
run(`launchctl unload ${plistPath(svcName)}`);
|
|
616
|
+
},
|
|
617
|
+
restart(options) {
|
|
618
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
619
|
+
runQuiet(`launchctl unload ${plistPath(svcName)}`);
|
|
620
|
+
run(`launchctl load ${plistPath(svcName)}`);
|
|
621
|
+
},
|
|
622
|
+
status(options) {
|
|
623
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
624
|
+
return getServiceState(svcName);
|
|
625
|
+
},
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Create a platform-aware service manager from a component descriptor.
|
|
630
|
+
*
|
|
631
|
+
* @remarks
|
|
632
|
+
* Detects the current platform and returns a `ServiceManager` that
|
|
633
|
+
* delegates to NSSM (Windows), systemd (Linux), or launchd (macOS).
|
|
634
|
+
*
|
|
635
|
+
* @param descriptor - The component descriptor.
|
|
636
|
+
* @returns A `ServiceManager` for the current platform.
|
|
637
|
+
*/
|
|
638
|
+
function createServiceManager(descriptor) {
|
|
639
|
+
switch (process.platform) {
|
|
640
|
+
case 'win32':
|
|
641
|
+
return createWindowsManager(descriptor);
|
|
642
|
+
case 'darwin':
|
|
643
|
+
return createMacOSManager(descriptor);
|
|
644
|
+
default:
|
|
645
|
+
return createLinuxManager(descriptor);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Factory for the standard Jeeves service CLI.
|
|
651
|
+
*
|
|
652
|
+
* @remarks
|
|
653
|
+
* Produces a Commander program with all standard commands from
|
|
654
|
+
* a component descriptor. Components add domain-specific commands
|
|
655
|
+
* via `descriptor.customCliCommands`.
|
|
656
|
+
*/
|
|
657
|
+
/**
|
|
658
|
+
* Create a standard service CLI program from a component descriptor.
|
|
659
|
+
*
|
|
660
|
+
* @remarks
|
|
661
|
+
* Standard commands:
|
|
662
|
+
* - `start -c <path>` - Launch the service process (foreground)
|
|
663
|
+
* - `status [-p port]` - Probe service health
|
|
664
|
+
* - `config [jsonpath] [-p port]` - Query running config
|
|
665
|
+
* - `config validate -c <path>` - Validate a config file
|
|
666
|
+
* - `config apply [-p port] [--file path] [--replace]` - Apply config patch
|
|
667
|
+
* - `init [-o path]` - Generate default config
|
|
668
|
+
* - `service install` - Install system service
|
|
669
|
+
* - `service uninstall` - Uninstall system service
|
|
670
|
+
* - `service start` - Start system service
|
|
671
|
+
* - `service stop` - Stop system service
|
|
672
|
+
* - `service restart` - Restart system service
|
|
673
|
+
* - `service status` - Query system service state
|
|
674
|
+
*
|
|
675
|
+
* @param descriptor - The component descriptor.
|
|
676
|
+
* @returns A Commander program ready for custom commands and `.parse()`.
|
|
677
|
+
*/
|
|
678
|
+
function createServiceCli(descriptor) {
|
|
679
|
+
const defaultServiceName = getEffectiveServiceName(descriptor);
|
|
680
|
+
const program = new Command()
|
|
681
|
+
.name(`jeeves-${descriptor.name}`)
|
|
682
|
+
.description(`Jeeves ${descriptor.name} service CLI`)
|
|
683
|
+
.version(descriptor.version)
|
|
684
|
+
.enablePositionalOptions()
|
|
685
|
+
.passThroughOptions();
|
|
686
|
+
// --- start ---
|
|
687
|
+
program
|
|
688
|
+
.command('start')
|
|
689
|
+
.description('Launch the service process (foreground)')
|
|
690
|
+
.requiredOption('-c, --config <path>', 'Config file path')
|
|
691
|
+
.action((opts) => {
|
|
692
|
+
const cmdArgs = descriptor.startCommand(opts.config);
|
|
693
|
+
const proc = spawn(cmdArgs[0], cmdArgs.slice(1), {
|
|
694
|
+
stdio: 'inherit',
|
|
695
|
+
});
|
|
696
|
+
proc.on('exit', (code) => {
|
|
697
|
+
process.exit(code ?? 1);
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
// --- status ---
|
|
701
|
+
program
|
|
702
|
+
.command('status')
|
|
703
|
+
.description('Probe service health and version')
|
|
704
|
+
.option('-p, --port <port>', 'Service port', String(descriptor.defaultPort))
|
|
705
|
+
.action(async (opts) => {
|
|
706
|
+
const url = `http://127.0.0.1:${opts.port}`;
|
|
707
|
+
try {
|
|
708
|
+
const result = await fetchJson(`${url}/status`);
|
|
709
|
+
console.log(JSON.stringify(result, null, 2));
|
|
710
|
+
}
|
|
711
|
+
catch (err) {
|
|
712
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
713
|
+
console.error(`Service unreachable: ${msg}`);
|
|
714
|
+
process.exitCode = 1;
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
// --- config ---
|
|
718
|
+
const configCmd = program
|
|
719
|
+
.command('config')
|
|
720
|
+
.description('Query or manage service configuration');
|
|
721
|
+
configCmd
|
|
722
|
+
.command('query')
|
|
723
|
+
.description('Query running service config via JSONPath')
|
|
724
|
+
.argument('[jsonpath]', 'JSONPath expression')
|
|
725
|
+
.option('-p, --port <port>', 'Service port', String(descriptor.defaultPort))
|
|
726
|
+
.action(async (jsonpath, opts) => {
|
|
727
|
+
const url = `http://127.0.0.1:${opts.port}`;
|
|
728
|
+
const qs = jsonpath ? `?path=${encodeURIComponent(jsonpath)}` : '';
|
|
729
|
+
try {
|
|
730
|
+
const result = await fetchJson(`${url}/config${qs}`);
|
|
731
|
+
console.log(JSON.stringify(result, null, 2));
|
|
732
|
+
}
|
|
733
|
+
catch (err) {
|
|
734
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
735
|
+
console.error(`Config query failed: ${msg}`);
|
|
736
|
+
process.exitCode = 1;
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
configCmd
|
|
740
|
+
.command('validate')
|
|
741
|
+
.description('Validate a config file against the schema')
|
|
742
|
+
.requiredOption('-c, --config <path>', 'Config file path')
|
|
743
|
+
.action((opts) => {
|
|
744
|
+
try {
|
|
745
|
+
const raw = readFileSync(opts.config, 'utf-8');
|
|
746
|
+
const parsed = JSON.parse(raw);
|
|
747
|
+
descriptor.configSchema.parse(parsed);
|
|
748
|
+
console.log('Config is valid.');
|
|
749
|
+
}
|
|
750
|
+
catch (err) {
|
|
751
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
752
|
+
console.error(`Validation failed: ${msg}`);
|
|
753
|
+
process.exitCode = 1;
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
configCmd
|
|
757
|
+
.command('apply')
|
|
758
|
+
.description('Apply a config patch to the running service')
|
|
759
|
+
.option('-p, --port <port>', 'Service port', String(descriptor.defaultPort))
|
|
760
|
+
.option('-f, --file <path>', 'Config patch file (JSON)')
|
|
761
|
+
.option('--replace', 'Replace entire config instead of merging')
|
|
762
|
+
.action(async (opts) => {
|
|
763
|
+
const url = `http://127.0.0.1:${opts.port}`;
|
|
764
|
+
let patch = {};
|
|
765
|
+
if (opts.file) {
|
|
766
|
+
const raw = readFileSync(opts.file, 'utf-8');
|
|
767
|
+
patch = JSON.parse(raw);
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
// Read from stdin
|
|
771
|
+
const chunks = [];
|
|
772
|
+
for await (const chunk of process.stdin) {
|
|
773
|
+
chunks.push(chunk);
|
|
774
|
+
}
|
|
775
|
+
const input = Buffer.concat(chunks).toString('utf-8').trim();
|
|
776
|
+
if (input) {
|
|
777
|
+
patch = JSON.parse(input);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const qs = opts.replace ? '?replace=true' : '';
|
|
781
|
+
try {
|
|
782
|
+
const result = await postJson(`${url}/config/apply${qs}`, patch);
|
|
783
|
+
console.log(JSON.stringify(result, null, 2));
|
|
784
|
+
}
|
|
785
|
+
catch (err) {
|
|
786
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
787
|
+
console.error(`Config apply failed: ${msg}`);
|
|
788
|
+
process.exitCode = 1;
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
// --- init ---
|
|
792
|
+
program
|
|
793
|
+
.command('init')
|
|
794
|
+
.description('Generate default config file')
|
|
795
|
+
.option('-o, --output <path>', 'Output directory')
|
|
796
|
+
.action((opts) => {
|
|
797
|
+
const outputDir = opts.output ?? getComponentConfigDir(descriptor.name);
|
|
798
|
+
mkdirSync(outputDir, { recursive: true });
|
|
799
|
+
const configPath = join(outputDir, descriptor.configFileName);
|
|
800
|
+
if (existsSync(configPath)) {
|
|
801
|
+
console.log(`Config already exists at ${configPath}`);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const template = descriptor.initTemplate();
|
|
805
|
+
writeFileSync(configPath, JSON.stringify(template, null, 2) + '\n');
|
|
806
|
+
console.log(`Config written to ${configPath}`);
|
|
807
|
+
});
|
|
808
|
+
// --- service ---
|
|
809
|
+
const serviceCmd = program
|
|
810
|
+
.command('service')
|
|
811
|
+
.description('System service management');
|
|
812
|
+
const svcManager = createServiceManager(descriptor);
|
|
813
|
+
serviceCmd
|
|
814
|
+
.command('install')
|
|
815
|
+
.description('Install as a system service')
|
|
816
|
+
.option('-c, --config <path>', 'Config file path')
|
|
817
|
+
.option('-n, --name <name>', 'Service name', defaultServiceName)
|
|
818
|
+
.action((opts) => {
|
|
819
|
+
try {
|
|
820
|
+
svcManager.install({ name: opts.name, configPath: opts.config });
|
|
821
|
+
console.log(`Service "${opts.name}" installed.`);
|
|
822
|
+
}
|
|
823
|
+
catch (err) {
|
|
824
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
825
|
+
console.error(`Install failed: ${msg}`);
|
|
826
|
+
process.exitCode = 1;
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
serviceCmd
|
|
830
|
+
.command('uninstall')
|
|
831
|
+
.description('Uninstall the system service')
|
|
832
|
+
.option('-n, --name <name>', 'Service name', defaultServiceName)
|
|
833
|
+
.action((opts) => {
|
|
834
|
+
try {
|
|
835
|
+
svcManager.uninstall({ name: opts.name });
|
|
836
|
+
console.log(`Service "${opts.name}" uninstalled.`);
|
|
837
|
+
}
|
|
838
|
+
catch (err) {
|
|
839
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
840
|
+
console.error(`Uninstall failed: ${msg}`);
|
|
841
|
+
process.exitCode = 1;
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
serviceCmd
|
|
845
|
+
.command('start')
|
|
846
|
+
.description('Start the system service')
|
|
847
|
+
.option('-n, --name <name>', 'Service name', defaultServiceName)
|
|
848
|
+
.action((opts) => {
|
|
849
|
+
try {
|
|
850
|
+
svcManager.start({ name: opts.name });
|
|
851
|
+
console.log(`Service "${opts.name}" started.`);
|
|
852
|
+
}
|
|
853
|
+
catch (err) {
|
|
854
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
855
|
+
console.error(`Start failed: ${msg}`);
|
|
856
|
+
process.exitCode = 1;
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
serviceCmd
|
|
860
|
+
.command('stop')
|
|
861
|
+
.description('Stop the system service')
|
|
862
|
+
.option('-n, --name <name>', 'Service name', defaultServiceName)
|
|
863
|
+
.action((opts) => {
|
|
864
|
+
try {
|
|
865
|
+
svcManager.stop({ name: opts.name });
|
|
866
|
+
console.log(`Service "${opts.name}" stopped.`);
|
|
867
|
+
}
|
|
868
|
+
catch (err) {
|
|
869
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
870
|
+
console.error(`Stop failed: ${msg}`);
|
|
871
|
+
process.exitCode = 1;
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
serviceCmd
|
|
875
|
+
.command('restart')
|
|
876
|
+
.description('Restart the system service')
|
|
877
|
+
.option('-n, --name <name>', 'Service name', defaultServiceName)
|
|
878
|
+
.action((opts) => {
|
|
879
|
+
try {
|
|
880
|
+
svcManager.restart({ name: opts.name });
|
|
881
|
+
console.log(`Service "${opts.name}" restarted.`);
|
|
882
|
+
}
|
|
883
|
+
catch (err) {
|
|
884
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
885
|
+
console.error(`Restart failed: ${msg}`);
|
|
886
|
+
process.exitCode = 1;
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
serviceCmd
|
|
890
|
+
.command('status')
|
|
891
|
+
.description('Query system service state')
|
|
892
|
+
.option('-n, --name <name>', 'Service name', defaultServiceName)
|
|
893
|
+
.action((opts) => {
|
|
894
|
+
try {
|
|
895
|
+
const state = svcManager.status({ name: opts.name });
|
|
896
|
+
console.log(`Service "${opts.name}": ${state}`);
|
|
897
|
+
}
|
|
898
|
+
catch (err) {
|
|
899
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
900
|
+
console.error(`Status failed: ${msg}`);
|
|
901
|
+
process.exitCode = 1;
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
// Apply custom CLI commands if provided
|
|
905
|
+
if (descriptor.customCliCommands) {
|
|
906
|
+
// Cast required: @commander-js/extra-typings Command has generic type
|
|
907
|
+
// parameters that don't align with the descriptor's base Command type.
|
|
908
|
+
// The descriptor can't know the parent Command's exact generic parameters
|
|
909
|
+
// at definition time. The cast is safe — customCliCommands only adds
|
|
910
|
+
// subcommands and doesn't depend on the parent's generic state.
|
|
911
|
+
descriptor.customCliCommands(program);
|
|
912
|
+
}
|
|
913
|
+
return program;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
export { createServiceCli };
|