@celilo/core 0.6.0 → 0.8.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/package.json +2 -3
- package/src/command-registry.ts +100 -4
- package/src/protocol.ts +14 -0
- package/src/remote-client.ts +26 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celilo/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Lightweight shared core for Celilo CLI tools — command registry, NDJSON API protocol, and remote SSH client. No Ink/React/drizzle/aws-sdk transitive deps, so an MCP server can reach the transport without installing the full CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -24,8 +24,7 @@
|
|
|
24
24
|
"directory": "packages/core"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@celilo/cli-display": "^0.
|
|
28
|
-
"@clack/prompts": "^1.1.0",
|
|
27
|
+
"@celilo/cli-display": "^0.2.0",
|
|
29
28
|
"zod": "^3.24.1"
|
|
30
29
|
}
|
|
31
30
|
}
|
package/src/command-registry.ts
CHANGED
|
@@ -581,6 +581,62 @@ export const COMMANDS: CommandDef[] = [
|
|
|
581
581
|
},
|
|
582
582
|
],
|
|
583
583
|
},
|
|
584
|
+
{
|
|
585
|
+
name: 'pause',
|
|
586
|
+
description:
|
|
587
|
+
"Take a module out of celilo's control plane without uninstalling it (keeps running unless --stop-infra)",
|
|
588
|
+
args: [{ name: 'id', description: 'Module ID', completion: 'module_ids' }],
|
|
589
|
+
flags: [
|
|
590
|
+
{
|
|
591
|
+
name: 'cascade',
|
|
592
|
+
description: 'Also pause every module that depends on it, consumers first',
|
|
593
|
+
takesValue: false,
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
name: 'stop-infra',
|
|
597
|
+
description: "Also stop the module's container or service",
|
|
598
|
+
takesValue: false,
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
name: 'reason',
|
|
602
|
+
description: 'Why the pause was taken, recorded on the module',
|
|
603
|
+
takesValue: true,
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
name: 'dry-run',
|
|
607
|
+
description: 'Print the ordered plan and change nothing',
|
|
608
|
+
takesValue: false,
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
name: 'yes',
|
|
612
|
+
description: 'Skip the cascade confirmation',
|
|
613
|
+
takesValue: false,
|
|
614
|
+
},
|
|
615
|
+
],
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
name: 'unpause',
|
|
619
|
+
description:
|
|
620
|
+
'Return a paused module to service by redeploying it, rebinding it to the providers present now',
|
|
621
|
+
args: [{ name: 'id', description: 'Module ID', completion: 'module_ids' }],
|
|
622
|
+
flags: [
|
|
623
|
+
{
|
|
624
|
+
name: 'cascade',
|
|
625
|
+
description: 'Also unpause everything that depends on it, providers first',
|
|
626
|
+
takesValue: false,
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
name: 'dry-run',
|
|
630
|
+
description: 'Print the ordered plan and change nothing',
|
|
631
|
+
takesValue: false,
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
name: 'yes',
|
|
635
|
+
description: 'Skip the cascade confirmation',
|
|
636
|
+
takesValue: false,
|
|
637
|
+
},
|
|
638
|
+
],
|
|
639
|
+
},
|
|
584
640
|
{
|
|
585
641
|
name: 'update',
|
|
586
642
|
description:
|
|
@@ -659,6 +715,14 @@ export const COMMANDS: CommandDef[] = [
|
|
|
659
715
|
{ name: 'key', description: 'Config key', completion: 'config_keys' },
|
|
660
716
|
],
|
|
661
717
|
},
|
|
718
|
+
{
|
|
719
|
+
name: 'unset',
|
|
720
|
+
description: "Remove an override so the key follows the module's manifest again",
|
|
721
|
+
args: [
|
|
722
|
+
{ name: 'id', description: 'Module ID', completion: 'module_ids' },
|
|
723
|
+
{ name: 'key', description: 'Config key', completion: 'config_keys' },
|
|
724
|
+
],
|
|
725
|
+
},
|
|
662
726
|
],
|
|
663
727
|
},
|
|
664
728
|
{
|
|
@@ -1030,6 +1094,29 @@ export const COMMANDS: CommandDef[] = [
|
|
|
1030
1094
|
},
|
|
1031
1095
|
],
|
|
1032
1096
|
},
|
|
1097
|
+
{
|
|
1098
|
+
name: 'firewall',
|
|
1099
|
+
description: "Inspect a firewall's interfaces",
|
|
1100
|
+
subcommands: [
|
|
1101
|
+
{
|
|
1102
|
+
name: 'interface',
|
|
1103
|
+
description: 'Interface classification for a firewall',
|
|
1104
|
+
subcommands: [
|
|
1105
|
+
{
|
|
1106
|
+
name: 'list',
|
|
1107
|
+
description:
|
|
1108
|
+
'Show how celilo classifies each interface (zone, external, or unaccounted for)',
|
|
1109
|
+
args: [
|
|
1110
|
+
{
|
|
1111
|
+
name: 'hostname',
|
|
1112
|
+
description: 'Firewall hostname (default: every machine celilo calls a router)',
|
|
1113
|
+
},
|
|
1114
|
+
],
|
|
1115
|
+
},
|
|
1116
|
+
],
|
|
1117
|
+
},
|
|
1118
|
+
],
|
|
1119
|
+
},
|
|
1033
1120
|
{
|
|
1034
1121
|
name: 'machine',
|
|
1035
1122
|
description: 'Manage existing machines (Raspberry Pi, VPS, bare metal)',
|
|
@@ -1616,15 +1703,24 @@ export const COMMANDS: CommandDef[] = [
|
|
|
1616
1703
|
description: 'Run a monitor now',
|
|
1617
1704
|
args: [{ name: 'target', description: 'Module id or built-in check name' }],
|
|
1618
1705
|
},
|
|
1706
|
+
{
|
|
1707
|
+
name: 'set-interval',
|
|
1708
|
+
description:
|
|
1709
|
+
"Change a built-in check's cadence (a module's comes from `module config set <m> health_check_interval`)",
|
|
1710
|
+
args: [
|
|
1711
|
+
{ name: 'check', description: 'Built-in check name' },
|
|
1712
|
+
{ name: 'cadence', description: 'Cadence, e.g. 15m, 1h, daily' },
|
|
1713
|
+
],
|
|
1714
|
+
},
|
|
1619
1715
|
{
|
|
1620
1716
|
name: 'enable',
|
|
1621
|
-
description: 'Enable a monitor',
|
|
1622
|
-
args: [{ name: 'target', description: '
|
|
1717
|
+
description: 'Enable a built-in check monitor',
|
|
1718
|
+
args: [{ name: 'target', description: 'Built-in check name' }],
|
|
1623
1719
|
},
|
|
1624
1720
|
{
|
|
1625
1721
|
name: 'disable',
|
|
1626
|
-
description: 'Disable a monitor and resolve its alerts',
|
|
1627
|
-
args: [{ name: 'target', description: '
|
|
1722
|
+
description: 'Disable a built-in check monitor and resolve its alerts',
|
|
1723
|
+
args: [{ name: 'target', description: 'Built-in check name' }],
|
|
1628
1724
|
},
|
|
1629
1725
|
],
|
|
1630
1726
|
},
|
package/src/protocol.ts
CHANGED
|
@@ -108,6 +108,20 @@ export const ResultMessageSchema = z.object({
|
|
|
108
108
|
type: z.literal('result'),
|
|
109
109
|
success: z.boolean(),
|
|
110
110
|
exitCode: z.number(),
|
|
111
|
+
/**
|
|
112
|
+
* Fleet-level conditions the operator should know about, regardless of what
|
|
113
|
+
* they ran. Today that is exactly one thing: modules are paused
|
|
114
|
+
* (openspec/changes/module-pause-lifecycle, design D7).
|
|
115
|
+
*
|
|
116
|
+
* Attached to EVERY result, including commands unrelated to the paused
|
|
117
|
+
* modules, and that is the point — a pause switches off the alerting that
|
|
118
|
+
* would otherwise report those modules as down, so the only remaining defence
|
|
119
|
+
* is that the paused-ness is discovered incidentally, while doing something
|
|
120
|
+
* else. A warning the operator has to go looking for does not do that job.
|
|
121
|
+
*
|
|
122
|
+
* Optional so an older client parses a newer server's result unchanged.
|
|
123
|
+
*/
|
|
124
|
+
warnings: z.array(z.string()).optional(),
|
|
111
125
|
});
|
|
112
126
|
export type ResultMessage = z.infer<typeof ResultMessageSchema>;
|
|
113
127
|
|
package/src/remote-client.ts
CHANGED
|
@@ -10,8 +10,16 @@
|
|
|
10
10
|
|
|
11
11
|
import { spawn } from 'node:child_process';
|
|
12
12
|
import { Readable } from 'node:stream';
|
|
13
|
-
import {
|
|
14
|
-
|
|
13
|
+
import {
|
|
14
|
+
type DisplayWriter,
|
|
15
|
+
ProgressDisplay,
|
|
16
|
+
isCancel,
|
|
17
|
+
cancel as uiCancel,
|
|
18
|
+
confirm as uiConfirm,
|
|
19
|
+
multiselect as uiMultiselect,
|
|
20
|
+
select as uiSelect,
|
|
21
|
+
text as uiText,
|
|
22
|
+
} from '@celilo/cli-display';
|
|
15
23
|
import {
|
|
16
24
|
type ClientMessage,
|
|
17
25
|
type InterviewMessage,
|
|
@@ -40,15 +48,17 @@ export class InterviewUnanswerableError extends Error {
|
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
/**
|
|
43
|
-
* Render a forwarded interview
|
|
51
|
+
* Render a forwarded interview on the terminal, returning the operator's answer.
|
|
44
52
|
*
|
|
45
|
-
* Refuses to prompt when stdin isn't a TTY
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
53
|
+
* Refuses to prompt when stdin isn't a TTY, and keeps its own check rather than
|
|
54
|
+
* relying on the prompt layer's: this is the boundary that must produce an
|
|
55
|
+
* `unanswerable` message, not an exception. The hazard it guards against is
|
|
56
|
+
* specific — a prompt that reads keypresses off a non-terminal stdin (an MCP
|
|
57
|
+
* server's JSON-RPC stream, a CI harness) takes the very next newline as a
|
|
58
|
+
* submit at the question's `defaultValue`, and the answer is indistinguishable
|
|
59
|
+
* from a considered human decision. That is how a live rollout came to report
|
|
60
|
+
* "operator declined" for a breaking update nobody was asked about. A client
|
|
61
|
+
* with no terminal must say so instead of guessing.
|
|
52
62
|
*/
|
|
53
63
|
async function defaultRenderInterview(iv: InterviewMessage): Promise<unknown> {
|
|
54
64
|
if (!process.stdin.isTTY) {
|
|
@@ -61,30 +71,30 @@ async function defaultRenderInterview(iv: InterviewMessage): Promise<unknown> {
|
|
|
61
71
|
let result: unknown;
|
|
62
72
|
switch (iv.kind) {
|
|
63
73
|
case 'confirm':
|
|
64
|
-
result = await
|
|
74
|
+
result = await uiConfirm({
|
|
65
75
|
message: iv.message,
|
|
66
76
|
initialValue: iv.defaultValue === 'true',
|
|
67
77
|
});
|
|
68
78
|
break;
|
|
69
79
|
case 'select':
|
|
70
|
-
result = await
|
|
80
|
+
result = await uiSelect({ message: iv.message, options });
|
|
71
81
|
break;
|
|
72
82
|
case 'multiselect':
|
|
73
|
-
result = await
|
|
83
|
+
result = await uiMultiselect({
|
|
74
84
|
message: iv.message,
|
|
75
85
|
options,
|
|
76
86
|
required: iv.required ?? false,
|
|
77
87
|
});
|
|
78
88
|
break;
|
|
79
89
|
default:
|
|
80
|
-
result = await
|
|
90
|
+
result = await uiText({
|
|
81
91
|
message: iv.message,
|
|
82
92
|
defaultValue: iv.defaultValue,
|
|
83
93
|
placeholder: iv.placeholder,
|
|
84
94
|
});
|
|
85
95
|
}
|
|
86
|
-
if (
|
|
87
|
-
|
|
96
|
+
if (isCancel(result)) {
|
|
97
|
+
uiCancel('Cancelled.');
|
|
88
98
|
process.exit(130);
|
|
89
99
|
}
|
|
90
100
|
return result;
|