@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celilo/core",
3
- "version": "0.6.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.1.10",
28
- "@clack/prompts": "^1.1.0",
27
+ "@celilo/cli-display": "^0.2.0",
29
28
  "zod": "^3.24.1"
30
29
  }
31
30
  }
@@ -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: 'Module id or built-in check name' }],
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: 'Module id or built-in check name' }],
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
 
@@ -10,8 +10,16 @@
10
10
 
11
11
  import { spawn } from 'node:child_process';
12
12
  import { Readable } from 'node:stream';
13
- import { type DisplayWriter, ProgressDisplay } from '@celilo/cli-display';
14
- import * as clack from '@clack/prompts';
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 via clack, returning the operator's answer.
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. clack reads keypresses off stdin
46
- * whether or not it is a terminal, so on a piped stdin (an MCP server's
47
- * JSON-RPC stream, a CI harness) the very next newline submits the prompt at
48
- * its `initialValue` — i.e. the question's `defaultValue` — and the answer
49
- * looks exactly like a considered human decision. That is how a live rollout
50
- * came to report "operator declined" for a breaking update nobody was asked
51
- * about. A client with no terminal must say so instead of guessing.
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 clack.confirm({
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 clack.select({ message: iv.message, options });
80
+ result = await uiSelect({ message: iv.message, options });
71
81
  break;
72
82
  case 'multiselect':
73
- result = await clack.multiselect({
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 clack.text({
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 (clack.isCancel(result)) {
87
- clack.cancel('Cancelled.');
96
+ if (isCancel(result)) {
97
+ uiCancel('Cancelled.');
88
98
  process.exit(130);
89
99
  }
90
100
  return result;