@thinremote/thinr-cli 1.2.0-alpha.1 → 1.3.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/CHANGELOG.md +63 -1
- package/README.md +235 -30
- package/bin/thinr.js +43 -25
- package/commands/_shared.js +154 -0
- package/commands/dashboard.js +17 -0
- package/commands/device/_shared.js +17 -34
- package/commands/device/console.js +1 -0
- package/commands/device/delete.js +53 -0
- package/commands/device/exec.js +7 -6
- package/commands/device/fs.js +193 -0
- package/commands/device/index.js +9 -0
- package/commands/device/list.js +35 -23
- package/commands/device/logs.js +178 -0
- package/commands/device/property.js +30 -28
- package/commands/device/proxy.js +3 -0
- package/commands/device/resource.js +30 -29
- package/commands/device/status.js +35 -14
- package/commands/device/transfer.js +236 -0
- package/commands/device/update.js +15 -10
- package/commands/fleet/index.js +17 -0
- package/commands/fleet/upgrade.js +275 -0
- package/commands/logout.js +10 -11
- package/commands/playbook/_shared.js +12 -0
- package/commands/playbook/index.js +18 -0
- package/commands/playbook/run.js +302 -0
- package/commands/playbook/validate.js +38 -0
- package/commands/product/_shared.js +8 -0
- package/commands/product/exec.js +243 -0
- package/commands/product/fs.js +218 -0
- package/commands/product/index.js +35 -0
- package/commands/product/list.js +86 -0
- package/commands/product/logs.js +289 -0
- package/commands/product/metric.js +124 -0
- package/commands/product/playbook.js +911 -0
- package/commands/product/property.js +76 -0
- package/commands/product/props.js +125 -0
- package/commands/product/push.js +184 -0
- package/commands/product/resource.js +76 -0
- package/commands/product/status.js +213 -0
- package/commands/profile.js +76 -8
- package/dist/dashboard.js +5644 -0
- package/lib/agent-releases.js +59 -0
- package/lib/agent-versions.js +73 -0
- package/lib/alarms.js +252 -0
- package/lib/api.js +8 -2
- package/lib/auth.js +68 -20
- package/lib/bucket.js +87 -0
- package/lib/command-stream.js +213 -0
- package/lib/config.js +5 -3
- package/lib/console.js +19 -5
- package/lib/constants.js +83 -0
- package/lib/dashboard/event-stream.js +239 -0
- package/lib/dashboard/metric-aggregate.js +64 -0
- package/lib/dashboard/metric-filter.js +39 -0
- package/lib/debug-log.js +93 -0
- package/lib/device-api.js +131 -148
- package/lib/devices.js +18 -21
- package/lib/errors.js +12 -0
- package/lib/fleet-upgrade.js +106 -0
- package/lib/format.js +114 -0
- package/lib/mcp/registry.js +14 -0
- package/lib/mcp/tools-alarms.js +510 -0
- package/lib/mcp/tools-buckets.js +103 -0
- package/lib/mcp/tools-devices.js +33 -3
- package/lib/mcp/tools-exec.js +30 -17
- package/lib/mcp/tools-filesystem.js +102 -6
- package/lib/mcp/tools-install.js +153 -0
- package/lib/mcp/tools-logs.js +225 -0
- package/lib/mcp/tools-playbook.js +211 -0
- package/lib/mcp/tools-products.js +1226 -105
- package/lib/mcp/tools-profile.js +634 -0
- package/lib/mcp/tools-tokens.js +371 -0
- package/lib/monitoring.js +26 -1
- package/lib/playbook/actions.js +447 -0
- package/lib/playbook/expression.js +302 -0
- package/lib/playbook/fleet.js +175 -0
- package/lib/playbook/loader.js +247 -0
- package/lib/playbook/runner.js +300 -0
- package/lib/playbook/schema.js +209 -0
- package/lib/playbook/vars.js +209 -0
- package/lib/product/log-presets.js +196 -0
- package/lib/product/logs.js +485 -0
- package/lib/product-orchestrator.js +124 -0
- package/lib/product.js +523 -20
- package/lib/profile.js +665 -0
- package/lib/property.js +7 -40
- package/lib/proxy.js +9 -8
- package/lib/resource.js +10 -14
- package/lib/status.js +78 -37
- package/lib/tokens.js +374 -0
- package/package.json +14 -3
- package/commands/product.js +0 -407
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
import chalk from 'chalk';
|
|
3
2
|
import { createDeviceAPI } from '../../lib/device-api.js';
|
|
4
3
|
import { isJsonMode, printOk, printErr, createSpinner, classifyError } from '../../lib/output.js';
|
|
4
|
+
import { label as labelText, accent } from '../../lib/format.js';
|
|
5
|
+
import { TIMEOUTS } from '../../lib/constants.js';
|
|
5
6
|
import { applyJsonFlag, ensureConfigured, getGlobalUser } from './_shared.js';
|
|
6
7
|
|
|
7
8
|
const runUpdate = async (action, deviceId, opts, cmd) => {
|
|
@@ -9,33 +10,37 @@ const runUpdate = async (action, deviceId, opts, cmd) => {
|
|
|
9
10
|
ensureConfigured();
|
|
10
11
|
const user = getGlobalUser(cmd);
|
|
11
12
|
const channel = opts.channel || 'latest';
|
|
12
|
-
const
|
|
13
|
-
const spinner = createSpinner(`${
|
|
13
|
+
const phase = action === 'check' ? 'Checking for updates' : 'Applying update';
|
|
14
|
+
const spinner = createSpinner(`${phase} on ${deviceId}...`).start();
|
|
14
15
|
try {
|
|
15
16
|
const api = createDeviceAPI(deviceId, { user });
|
|
16
|
-
const timeout =
|
|
17
|
+
const timeout =
|
|
18
|
+
action === 'apply' ? TIMEOUTS.DEVICE_UPDATE_APPLY_MS : TIMEOUTS.DEVICE_RESOURCE_CALL_MS;
|
|
17
19
|
const result = await api.callResource('update', { action, channel }, { timeout });
|
|
18
|
-
spinner.succeed(`${
|
|
20
|
+
spinner.succeed(`${phase} finished`);
|
|
19
21
|
if (isJsonMode()) {
|
|
20
22
|
printOk(result);
|
|
21
23
|
} else if (result && typeof result === 'object') {
|
|
22
|
-
if (result.current) console.log(` current: ${
|
|
23
|
-
if (result.latest) console.log(` latest: ${
|
|
24
|
+
if (result.current) console.log(` current: ${labelText(result.current)}`);
|
|
25
|
+
if (result.latest) console.log(` latest: ${labelText(result.latest)}`);
|
|
24
26
|
if (result.arch) console.log(` arch: ${result.arch}`);
|
|
25
|
-
if (result.status) console.log(` status: ${
|
|
27
|
+
if (result.status) console.log(` status: ${accent(result.status)}`);
|
|
26
28
|
if (result.message) console.log(` message: ${result.message}`);
|
|
27
29
|
} else {
|
|
28
30
|
console.log(result);
|
|
29
31
|
}
|
|
30
32
|
} catch (error) {
|
|
31
|
-
spinner.fail(`${
|
|
33
|
+
spinner.fail(`${phase} failed`);
|
|
32
34
|
const { message, code } = classifyError(error);
|
|
33
35
|
printErr(message, { code });
|
|
34
36
|
}
|
|
35
37
|
};
|
|
36
38
|
|
|
37
39
|
export function registerUpdateCommand(device) {
|
|
38
|
-
const update = device
|
|
40
|
+
const update = device
|
|
41
|
+
.command('update')
|
|
42
|
+
.helpGroup('Execution:')
|
|
43
|
+
.description('Check for or apply agent updates');
|
|
39
44
|
|
|
40
45
|
update
|
|
41
46
|
.command('check <deviceId>')
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { registerUpgradeCommand } from './upgrade.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `thinr fleet <subcommand>` — operations that treat the whole device
|
|
6
|
+
* fleet as the unit of work, not a single device. Today we only expose
|
|
7
|
+
* `upgrade`; future fan-out commands (status summary, health probe,
|
|
8
|
+
* rollback, etc.) would live alongside it.
|
|
9
|
+
*/
|
|
10
|
+
export function fleetCommand(program) {
|
|
11
|
+
const fleet = program
|
|
12
|
+
.command('fleet')
|
|
13
|
+
.helpGroup('Operations:')
|
|
14
|
+
.description('Fleet-wide commands (run once, touch many devices)');
|
|
15
|
+
|
|
16
|
+
registerUpgradeCommand(fleet);
|
|
17
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { confirm } from '@inquirer/prompts';
|
|
3
|
+
import { getDevices } from '../../lib/devices.js';
|
|
4
|
+
import { getMonitoringData } from '../../lib/monitoring.js';
|
|
5
|
+
import { fetchLatestAgentVersion } from '../../lib/agent-releases.js';
|
|
6
|
+
import { runFleetUpgrade, summarize } from '../../lib/fleet-upgrade.js';
|
|
7
|
+
import { classifyAgainst, normalizeAgentVersion } from '../../lib/agent-versions.js';
|
|
8
|
+
import {
|
|
9
|
+
isJsonMode,
|
|
10
|
+
printOk,
|
|
11
|
+
printErr,
|
|
12
|
+
createSpinner,
|
|
13
|
+
classifyError,
|
|
14
|
+
} from '../../lib/output.js';
|
|
15
|
+
import {
|
|
16
|
+
success,
|
|
17
|
+
error as errorStyle,
|
|
18
|
+
warning,
|
|
19
|
+
hint,
|
|
20
|
+
label,
|
|
21
|
+
muted,
|
|
22
|
+
accent,
|
|
23
|
+
} from '../../lib/format.js';
|
|
24
|
+
import {
|
|
25
|
+
applyJsonFlag,
|
|
26
|
+
ensureConfigured,
|
|
27
|
+
getGlobalUser,
|
|
28
|
+
parsePositiveInt,
|
|
29
|
+
} from '../_shared.js';
|
|
30
|
+
|
|
31
|
+
export function registerUpgradeCommand(fleet) {
|
|
32
|
+
fleet
|
|
33
|
+
.command('upgrade')
|
|
34
|
+
.helpGroup('Fleet:')
|
|
35
|
+
.description('Apply an agent update across every outdated device in the fleet')
|
|
36
|
+
.option('-j, --json', 'Output as JSON')
|
|
37
|
+
.option('--channel <name>', 'Release channel to target', 'latest')
|
|
38
|
+
.option('-p, --product <id>', 'Restrict to devices belonging to this product')
|
|
39
|
+
.option('-g, --group <id>', 'Restrict to devices in this asset group')
|
|
40
|
+
.option(
|
|
41
|
+
'--batch-size <n>',
|
|
42
|
+
'Devices updated in parallel once the canary passes',
|
|
43
|
+
parsePositiveInt('batch-size'),
|
|
44
|
+
5,
|
|
45
|
+
)
|
|
46
|
+
.option('--no-canary', 'Skip the canary phase (upgrade everything in batches from the start)')
|
|
47
|
+
.option('--continue-on-error', 'Keep rolling after a batch failure instead of aborting')
|
|
48
|
+
.option('--dry-run', 'List what would be upgraded without sending any update request')
|
|
49
|
+
.option('-y, --yes', 'Skip the confirmation prompt (required in non-interactive shells)')
|
|
50
|
+
.action(async (opts, cmd) => {
|
|
51
|
+
applyJsonFlag(opts);
|
|
52
|
+
ensureConfigured();
|
|
53
|
+
const user = getGlobalUser(cmd);
|
|
54
|
+
|
|
55
|
+
// ── 1. Target version ──────────────────────────────────────
|
|
56
|
+
const latestSpinner = createSpinner(
|
|
57
|
+
`Resolving ${opts.channel} release…`,
|
|
58
|
+
).start();
|
|
59
|
+
let target;
|
|
60
|
+
try {
|
|
61
|
+
target = await fetchLatestAgentVersion({ channel: opts.channel });
|
|
62
|
+
latestSpinner.succeed(`Channel ${accent(opts.channel)} → ${success(target)}`);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
latestSpinner.fail(`Failed to resolve channel ${opts.channel}`);
|
|
65
|
+
const { message, code } = classifyError(err);
|
|
66
|
+
printErr(message, { code });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── 2. Fleet inventory ─────────────────────────────────────
|
|
71
|
+
const listSpinner = createSpinner('Listing fleet…').start();
|
|
72
|
+
let devices;
|
|
73
|
+
try {
|
|
74
|
+
const filter = {};
|
|
75
|
+
if (opts.product) filter.product = opts.product;
|
|
76
|
+
if (opts.group) filter.asset_group = opts.group;
|
|
77
|
+
devices = await getDevices(filter, user);
|
|
78
|
+
listSpinner.succeed(`Fleet size: ${label(devices.length)} device(s)`);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
listSpinner.fail('Failed to list devices');
|
|
81
|
+
const { message, code } = classifyError(err);
|
|
82
|
+
printErr(message, { code });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (devices.length === 0) {
|
|
86
|
+
if (isJsonMode()) {
|
|
87
|
+
printOk({ target, outdated: [], results: [] });
|
|
88
|
+
} else {
|
|
89
|
+
console.log(hint('No devices match the filter.'));
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── 3. Current versions ────────────────────────────────────
|
|
95
|
+
// Fleet-wide monitoring snapshot trimmed to `agent.version`.
|
|
96
|
+
// The backend may return several rows per device (history
|
|
97
|
+
// tail); with `sort=desc` the first row we see per device is
|
|
98
|
+
// the most recent one, so we ignore later repeats. We also
|
|
99
|
+
// scope to the filtered fleet so monitoring data from devices
|
|
100
|
+
// outside the product/group filter doesn't leak into the count.
|
|
101
|
+
const versionsSpinner = createSpinner('Reading agent versions…').start();
|
|
102
|
+
const fleetIds = new Set(devices.map((d) => d.device));
|
|
103
|
+
const deviceVersions = {};
|
|
104
|
+
for (const d of devices) deviceVersions[d.device] = null;
|
|
105
|
+
try {
|
|
106
|
+
const rows = await getMonitoringData({
|
|
107
|
+
user,
|
|
108
|
+
items: Math.max(500, devices.length),
|
|
109
|
+
sort: 'desc',
|
|
110
|
+
fields: 'agent.version',
|
|
111
|
+
});
|
|
112
|
+
if (Array.isArray(rows)) {
|
|
113
|
+
for (const row of rows) {
|
|
114
|
+
const id = row?.device;
|
|
115
|
+
const v = row?.agent?.version;
|
|
116
|
+
if (!id || !v || !fleetIds.has(id)) continue;
|
|
117
|
+
if (deviceVersions[id]) continue;
|
|
118
|
+
deviceVersions[id] = v;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const known = Object.values(deviceVersions).filter(Boolean).length;
|
|
122
|
+
versionsSpinner.succeed(`Read versions for ${label(known)} of ${devices.length} device(s)`);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
versionsSpinner.fail('Failed to read monitoring data');
|
|
125
|
+
const { message, code } = classifyError(err);
|
|
126
|
+
printErr(message, { code });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const { outdated, current, unknown } = classifyAgainst(target, deviceVersions);
|
|
131
|
+
|
|
132
|
+
// ── 4. Render summary ──────────────────────────────────────
|
|
133
|
+
const versionCounts = countVersions(deviceVersions);
|
|
134
|
+
if (!isJsonMode()) {
|
|
135
|
+
console.log();
|
|
136
|
+
console.log(label('Agent versions in fleet:'));
|
|
137
|
+
for (const [v, c] of versionCounts) {
|
|
138
|
+
const tag = v === target ? success('●') : warning('●');
|
|
139
|
+
console.log(` ${tag} ${v.padEnd(12)} ${muted(String(c))}`);
|
|
140
|
+
}
|
|
141
|
+
if (unknown.length) {
|
|
142
|
+
console.log(` ${muted('?')} ${'unknown'.padEnd(12)} ${muted(String(unknown.length))}`);
|
|
143
|
+
}
|
|
144
|
+
console.log();
|
|
145
|
+
console.log(
|
|
146
|
+
`Target: ${success(target)} · ` +
|
|
147
|
+
`outdated: ${label(outdated.length)} · ` +
|
|
148
|
+
`current: ${muted(current.length)}` +
|
|
149
|
+
(unknown.length ? ` · unknown: ${muted(unknown.length)}` : ''),
|
|
150
|
+
);
|
|
151
|
+
console.log();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (outdated.length === 0) {
|
|
155
|
+
if (isJsonMode()) {
|
|
156
|
+
printOk({
|
|
157
|
+
target,
|
|
158
|
+
summary: { outdated: 0, current: current.length, unknown: unknown.length },
|
|
159
|
+
results: [],
|
|
160
|
+
});
|
|
161
|
+
} else {
|
|
162
|
+
console.log(success('All devices are already on the target version.'));
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── 5. Dry-run ─────────────────────────────────────────────
|
|
168
|
+
if (opts.dryRun) {
|
|
169
|
+
if (isJsonMode()) {
|
|
170
|
+
printOk({
|
|
171
|
+
target,
|
|
172
|
+
dryRun: true,
|
|
173
|
+
outdated,
|
|
174
|
+
current,
|
|
175
|
+
unknown,
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
console.log(label(`Would upgrade ${outdated.length} device(s):`));
|
|
180
|
+
for (const id of outdated) {
|
|
181
|
+
console.log(` - ${id} ${muted(`(${normalizeAgentVersion(deviceVersions[id]) || '?'})`)}`);
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── 6. Confirmation ────────────────────────────────────────
|
|
187
|
+
if (!opts.yes && !isJsonMode()) {
|
|
188
|
+
if (!process.stdin.isTTY) {
|
|
189
|
+
printErr(
|
|
190
|
+
'Refusing to upgrade without --yes in a non-interactive shell',
|
|
191
|
+
{ code: 'needs_confirm' },
|
|
192
|
+
);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const plan = opts.canary ? 'canary first, then batches' : 'all in batches';
|
|
196
|
+
const failure = opts.continueOnError ? 'continue on failure' : 'abort on failure';
|
|
197
|
+
const ok = await confirm({
|
|
198
|
+
message: `Upgrade ${outdated.length} device(s) to ${target}? (${plan} · ${failure})`,
|
|
199
|
+
default: false,
|
|
200
|
+
});
|
|
201
|
+
if (!ok) {
|
|
202
|
+
console.log(hint('Cancelled.'));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── 7. Rollout ─────────────────────────────────────────────
|
|
208
|
+
const runSpinner = createSpinner('').start();
|
|
209
|
+
const setRunText = (done, total) => {
|
|
210
|
+
runSpinner.text = `Upgrading · ${done}/${total} device(s) processed`;
|
|
211
|
+
};
|
|
212
|
+
setRunText(0, outdated.length);
|
|
213
|
+
|
|
214
|
+
const perDevice = [];
|
|
215
|
+
const { results, aborted, reason } = await runFleetUpgrade({
|
|
216
|
+
deviceIds: outdated,
|
|
217
|
+
channel: opts.channel,
|
|
218
|
+
user,
|
|
219
|
+
canary: opts.canary,
|
|
220
|
+
abortOnFailure: !opts.continueOnError,
|
|
221
|
+
batchSize: opts.batchSize,
|
|
222
|
+
onProgress: ({ done, total }) => setRunText(done, total),
|
|
223
|
+
onDeviceResult: (r) => {
|
|
224
|
+
perDevice.push(r);
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
runSpinner.stop();
|
|
228
|
+
|
|
229
|
+
// ── 8. Results ─────────────────────────────────────────────
|
|
230
|
+
const sum = summarize(results);
|
|
231
|
+
if (isJsonMode()) {
|
|
232
|
+
printOk({
|
|
233
|
+
target,
|
|
234
|
+
channel: opts.channel,
|
|
235
|
+
aborted,
|
|
236
|
+
reason: aborted ? reason : undefined,
|
|
237
|
+
summary: sum,
|
|
238
|
+
results,
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
for (const r of results) {
|
|
244
|
+
if (r.ok) {
|
|
245
|
+
console.log(` ${success('✓')} ${r.deviceId}`);
|
|
246
|
+
} else {
|
|
247
|
+
console.log(` ${errorStyle('✗')} ${r.deviceId} ${muted(r.error)}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.log();
|
|
252
|
+
const verdict = aborted
|
|
253
|
+
? `${warning('aborted')} (${reason}) · ${sum.ok} ok · ${sum.failed} failed`
|
|
254
|
+
: `${success('done')} · ${sum.ok} ok · ${sum.failed} failed`;
|
|
255
|
+
console.log(label(`Rollout ${verdict}`));
|
|
256
|
+
|
|
257
|
+
if (aborted || sum.failed > 0) {
|
|
258
|
+
// Scripts can use the exit code to gate follow-up work — a
|
|
259
|
+
// partial rollout is a "soft failure" worth surfacing.
|
|
260
|
+
process.exitCode = 1;
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function countVersions(deviceVersions) {
|
|
266
|
+
const counts = new Map();
|
|
267
|
+
for (const raw of Object.values(deviceVersions)) {
|
|
268
|
+
const v = normalizeAgentVersion(raw);
|
|
269
|
+
if (!v) continue;
|
|
270
|
+
counts.set(v, (counts.get(v) || 0) + 1);
|
|
271
|
+
}
|
|
272
|
+
// Newest first by semver — reuse the comparator via dynamic import
|
|
273
|
+
// would be overkill here; the caller only needs a stable human order.
|
|
274
|
+
return [...counts.entries()].sort((a, b) => b[0].localeCompare(a[0], undefined, { numeric: true }));
|
|
275
|
+
}
|
package/commands/logout.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
1
|
import ora from 'ora';
|
|
3
2
|
import { deleteConfig, configExists } from '../lib/config.js';
|
|
3
|
+
import { warning, error, hint } from '../lib/format.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Register the logout command
|
|
@@ -9,11 +9,12 @@ import { deleteConfig, configExists } from '../lib/config.js';
|
|
|
9
9
|
export function logoutCommand(program) {
|
|
10
10
|
program
|
|
11
11
|
.command('logout')
|
|
12
|
+
.helpGroup('Configuration:')
|
|
12
13
|
.description('Remove stored credentials and configuration')
|
|
13
14
|
.action(async () => {
|
|
14
15
|
// Check if configured
|
|
15
16
|
if (!configExists()) {
|
|
16
|
-
console.log(
|
|
17
|
+
console.log(warning('No configuration found. Already logged out.'));
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -22,27 +23,25 @@ export function logoutCommand(program) {
|
|
|
22
23
|
try {
|
|
23
24
|
// Delete local configuration
|
|
24
25
|
spinner.text = 'Removing local configuration...';
|
|
25
|
-
const
|
|
26
|
+
const ok = deleteConfig();
|
|
26
27
|
|
|
27
|
-
if (
|
|
28
|
+
if (ok) {
|
|
28
29
|
spinner.succeed('Logged out successfully. Configuration removed.');
|
|
29
30
|
} else {
|
|
30
31
|
spinner.fail('Failed to remove configuration.');
|
|
31
32
|
console.error(
|
|
32
|
-
|
|
33
|
+
error(
|
|
33
34
|
'Error: Unable to delete configuration file. You may need to remove it manually.',
|
|
34
35
|
),
|
|
35
36
|
);
|
|
36
37
|
console.error(
|
|
37
|
-
|
|
38
|
+
hint('Configuration is stored in ~/.config/thinr-cli/config.json'),
|
|
38
39
|
);
|
|
39
40
|
}
|
|
40
|
-
} catch (
|
|
41
|
+
} catch (err) {
|
|
41
42
|
spinner.fail('Failed to remove configuration.');
|
|
42
|
-
console.error(
|
|
43
|
-
console.error(
|
|
44
|
-
chalk.gray('Configuration is stored in ~/.config/thinr-cli/config.json'),
|
|
45
|
-
);
|
|
43
|
+
console.error(error(`Error: ${err.message}`));
|
|
44
|
+
console.error(hint('Configuration is stored in ~/.config/thinr-cli/config.json'));
|
|
46
45
|
}
|
|
47
46
|
});
|
|
48
47
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { registerRunCommand } from './run.js';
|
|
3
|
+
import { registerValidateCommand } from './validate.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `thinr playbook <subcommand>` — declarative, repeatable sequences of
|
|
7
|
+
* agent actions over a fleet. See `lib/playbook/schema.js` for the
|
|
8
|
+
* accepted format.
|
|
9
|
+
*/
|
|
10
|
+
export function playbookCommand(program) {
|
|
11
|
+
const playbook = program
|
|
12
|
+
.command('playbook')
|
|
13
|
+
.helpGroup('Operations:')
|
|
14
|
+
.description('Run declarative YAML playbooks against the fleet');
|
|
15
|
+
|
|
16
|
+
registerRunCommand(playbook);
|
|
17
|
+
registerValidateCommand(playbook);
|
|
18
|
+
}
|