@yadurajfleetos/cli 0.13.1 → 0.14.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.
@@ -0,0 +1,131 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { parseDocument } from 'yaml';
3
+ import { CliError, EXIT, request, requireFleet } from '../api.js';
4
+ import { c } from '../render.js';
5
+ import { glyph, rule } from '../ui.js';
6
+ import { confirm } from '../prompt.js';
7
+ import { awaitRunning } from '../progress.js';
8
+ /** How a manifest writes a value the model returned as a scalar. */
9
+ function shape(field, value) {
10
+ // `health: /healthz` is the obvious way to say it and the manifest wants
11
+ // `health: { path: /healthz }`. Same normalisation the server-side review
12
+ // does, for the same reason: a difference in spelling should not cost a fix.
13
+ if (field === 'health' && typeof value === 'string')
14
+ return { path: value };
15
+ return value;
16
+ }
17
+ /** Whether a service is running right now, as the fleet sees it. */
18
+ async function statusOf(fleetId, name) {
19
+ const { body } = await request('GET', `/fleets/${fleetId}/services`);
20
+ return body.services.find((s) => s.name === name)?.current?.status ?? 'not running';
21
+ }
22
+ export const fixCommand = {
23
+ async run(args, flags) {
24
+ const service = args[0];
25
+ if (!service)
26
+ throw new CliError('usage: fleet fix <service> [--yes]', EXIT.usage);
27
+ const fleetId = await requireFleet(typeof flags.fleet === 'string' ? flags.fleet : undefined);
28
+ console.log(`\n${rule(`fix · ${service}`)}`);
29
+ console.log(`${glyph.pending} looking…`);
30
+ const { body: found } = await request('POST', `/fleets/${fleetId}/diagnose`, {
31
+ body: { question: `Why is the "${service}" service not working as it should?` },
32
+ });
33
+ if (found.status !== 'ok') {
34
+ throw new CliError(found.status === 'disabled' ? found.reason : `inconclusive — ${found.reason}`, EXIT.failure);
35
+ }
36
+ console.log(`\n${found.summary}\n`);
37
+ for (const f of found.findings) {
38
+ console.log(` ${c.bold(f.claim)}`);
39
+ console.log(` ${c.dim(f.evidence)}`);
40
+ }
41
+ const fix = found.fix;
42
+ if (!fix) {
43
+ // Most investigations do not end in one exact manifest change, and this
44
+ // is not a failure. Saying what was found and stopping is the answer.
45
+ console.log(`\n ${c.dim('No single manifest change would fix this. What to do:')}`);
46
+ for (const n of found.next)
47
+ console.log(` ${c.dim('›')} ${n}`);
48
+ console.log();
49
+ return;
50
+ }
51
+ if (!fix.applicable) {
52
+ // Reported in words, never performed. A person clicking through a prompt
53
+ // is not a guardrail; not offering the button is.
54
+ console.log(`\n${glyph.warn} ${c.bold('This one has to be done by hand')}`);
55
+ console.log(` ${fix.service}.${fix.field} → ${JSON.stringify(fix.value)}`);
56
+ console.log(` ${c.dim(fix.why)}`);
57
+ console.log(` ${c.dim(fix.reason ?? '')}`);
58
+ console.log();
59
+ return;
60
+ }
61
+ console.log(`\n${glyph.info} ${c.bold('proposed')} ${fix.service}.${fix.field} → ${JSON.stringify(fix.value)}`);
62
+ console.log(` ${c.dim(fix.why)}\n`);
63
+ if (!flags.yes && !flags.y) {
64
+ const ok = await confirm(`Apply this to fleet.yaml and redeploy ${fix.service}?`);
65
+ if (!ok)
66
+ return console.log(` ${c.dim('left alone')}\n`);
67
+ }
68
+ // Read before writing, and keep the original in memory: this is what makes
69
+ // the change reversible without a second file on disk.
70
+ const path = 'fleet.yaml';
71
+ const before = await readFile(path, 'utf8').catch(() => {
72
+ throw new CliError(`No fleet.yaml here. Run this from the project directory.`, EXIT.usage);
73
+ });
74
+ const doc = parseDocument(before);
75
+ if (!doc.hasIn(['services', fix.service])) {
76
+ throw new CliError(`fleet.yaml has no service named "${fix.service}"`, EXIT.failure);
77
+ }
78
+ // Edited as a document, so the comments `fleet init` wrote survive. A
79
+ // round trip through parse and re-serialise would take them out, and they
80
+ // are the only explanation a generated manifest carries.
81
+ if (fix.value === null)
82
+ doc.deleteIn(['services', fix.service, fix.field]);
83
+ else
84
+ doc.setIn(['services', fix.service, fix.field], shape(fix.field, fix.value));
85
+ const wasRunning = (await statusOf(fleetId, fix.service)) === 'running';
86
+ await writeFile(path, String(doc));
87
+ console.log(`${glyph.ok} fleet.yaml updated`);
88
+ const restore = async (why) => {
89
+ await writeFile(path, before);
90
+ console.log(`${glyph.warn} ${why} — fleet.yaml put back`);
91
+ console.log(` ${c.dim(`Deploy the previous manifest with: fleet up ${fix.service}`)}\n`);
92
+ };
93
+ console.log(`${glyph.pending} deploying…`);
94
+ const { body: svc } = await request('GET', `/fleets/${fleetId}/services`);
95
+ const target = svc.services.find((s) => s.name === fix.service);
96
+ if (!target) {
97
+ await restore(`"${fix.service}" is not in this fleet`);
98
+ return;
99
+ }
100
+ // Verification, and the reason the whole thing is defensible at all: a
101
+ // change that did not help is undone rather than left behind.
102
+ //
103
+ // Followed to a conclusion rather than read once. The deploy request
104
+ // returns when a node has been chosen, and the container starting is the
105
+ // agent's job afterwards -- so reading the status here would find
106
+ // "deploying" and call that success, which is a verification step that
107
+ // verifies nothing.
108
+ //
109
+ // Judged against where it started. A service that was already down and is
110
+ // still down has not been made worse by this edit, and putting the
111
+ // manifest back would take away a change that may well be right while
112
+ // leaving the real problem in place.
113
+ try {
114
+ await request('POST', `/services/${target.id}/deploy`, { body: {} });
115
+ await awaitRunning(target, { timeoutMs: 240_000 });
116
+ }
117
+ catch (err) {
118
+ if (wasRunning) {
119
+ await restore(`${fix.service} was running before and did not come back: ${err.message}`);
120
+ return;
121
+ }
122
+ console.log(`${glyph.warn} still not running — ${err.message}`);
123
+ console.log(` ${c.dim('The edit was kept: it was already down, so this did not make it worse.')}`);
124
+ console.log(` ${c.dim(`undo it with: git checkout fleet.yaml && fleet up ${fix.service}`)}\n`);
125
+ return;
126
+ }
127
+ console.log(`${glyph.ok} ${fix.service} is running`);
128
+ console.log(`\n ${c.dim('check it settled:')} fleet deployments ${fix.service}`);
129
+ console.log(` ${c.dim('undo it:')} git checkout fleet.yaml && fleet up ${fix.service}\n`);
130
+ },
131
+ };
@@ -6,6 +6,7 @@ import { configCommand, useCommand } from './config.js';
6
6
  import { doctorCommand } from './doctor.js';
7
7
  import { upCommand } from './up.js';
8
8
  import { tuneCommand } from './tune.js';
9
+ import { fixCommand } from './fix.js';
9
10
  import { openCommand } from './open.js';
10
11
  import { downCommand } from './down.js';
11
12
  import { unpairCommand, agentCommand } from './unpair.js';
@@ -22,6 +23,7 @@ export const commands = {
22
23
  use: useCommand,
23
24
  doctor: doctorCommand,
24
25
  tune: tuneCommand,
26
+ fix: fixCommand,
25
27
  init: initCommand,
26
28
  diagnose: diagnoseCommand,
27
29
  import: importCommand,
package/dist/index.js CHANGED
@@ -28,6 +28,7 @@ const GROUPS = [
28
28
  ['deploy <service>', 'Build, schedule, and roll out'],
29
29
  ['explain <service>', 'Read a failed deploy and say what to do about it'],
30
30
  ['diagnose "<question>"', 'Investigate why something is wrong, and cite what it looked at'],
31
+ ['fix <service>', 'Diagnose, propose one manifest change, deploy it, and undo it if that was worse'],
31
32
  ],
32
33
  ],
33
34
  [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yadurajfleetos/cli",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "description": "Fleet OS command-line interface for deploying and orchestrating services on user-owned hardware",
5
5
  "type": "module",
6
6
  "license": "MIT",