@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.
Files changed (92) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/README.md +235 -30
  3. package/bin/thinr.js +43 -25
  4. package/commands/_shared.js +154 -0
  5. package/commands/dashboard.js +17 -0
  6. package/commands/device/_shared.js +17 -34
  7. package/commands/device/console.js +1 -0
  8. package/commands/device/delete.js +53 -0
  9. package/commands/device/exec.js +7 -6
  10. package/commands/device/fs.js +193 -0
  11. package/commands/device/index.js +9 -0
  12. package/commands/device/list.js +35 -23
  13. package/commands/device/logs.js +178 -0
  14. package/commands/device/property.js +30 -28
  15. package/commands/device/proxy.js +3 -0
  16. package/commands/device/resource.js +30 -29
  17. package/commands/device/status.js +35 -14
  18. package/commands/device/transfer.js +236 -0
  19. package/commands/device/update.js +15 -10
  20. package/commands/fleet/index.js +17 -0
  21. package/commands/fleet/upgrade.js +275 -0
  22. package/commands/logout.js +10 -11
  23. package/commands/playbook/_shared.js +12 -0
  24. package/commands/playbook/index.js +18 -0
  25. package/commands/playbook/run.js +302 -0
  26. package/commands/playbook/validate.js +38 -0
  27. package/commands/product/_shared.js +8 -0
  28. package/commands/product/exec.js +243 -0
  29. package/commands/product/fs.js +218 -0
  30. package/commands/product/index.js +35 -0
  31. package/commands/product/list.js +86 -0
  32. package/commands/product/logs.js +289 -0
  33. package/commands/product/metric.js +124 -0
  34. package/commands/product/playbook.js +911 -0
  35. package/commands/product/property.js +76 -0
  36. package/commands/product/props.js +125 -0
  37. package/commands/product/push.js +184 -0
  38. package/commands/product/resource.js +76 -0
  39. package/commands/product/status.js +213 -0
  40. package/commands/profile.js +76 -8
  41. package/dist/dashboard.js +5644 -0
  42. package/lib/agent-releases.js +59 -0
  43. package/lib/agent-versions.js +73 -0
  44. package/lib/alarms.js +252 -0
  45. package/lib/api.js +8 -2
  46. package/lib/auth.js +68 -20
  47. package/lib/bucket.js +87 -0
  48. package/lib/command-stream.js +213 -0
  49. package/lib/config.js +5 -3
  50. package/lib/console.js +19 -5
  51. package/lib/constants.js +83 -0
  52. package/lib/dashboard/event-stream.js +239 -0
  53. package/lib/dashboard/metric-aggregate.js +64 -0
  54. package/lib/dashboard/metric-filter.js +39 -0
  55. package/lib/debug-log.js +93 -0
  56. package/lib/device-api.js +131 -148
  57. package/lib/devices.js +18 -21
  58. package/lib/errors.js +12 -0
  59. package/lib/fleet-upgrade.js +106 -0
  60. package/lib/format.js +114 -0
  61. package/lib/mcp/registry.js +14 -0
  62. package/lib/mcp/tools-alarms.js +510 -0
  63. package/lib/mcp/tools-buckets.js +103 -0
  64. package/lib/mcp/tools-devices.js +33 -3
  65. package/lib/mcp/tools-exec.js +30 -17
  66. package/lib/mcp/tools-filesystem.js +102 -6
  67. package/lib/mcp/tools-install.js +153 -0
  68. package/lib/mcp/tools-logs.js +225 -0
  69. package/lib/mcp/tools-playbook.js +211 -0
  70. package/lib/mcp/tools-products.js +1226 -105
  71. package/lib/mcp/tools-profile.js +634 -0
  72. package/lib/mcp/tools-tokens.js +371 -0
  73. package/lib/monitoring.js +26 -1
  74. package/lib/playbook/actions.js +447 -0
  75. package/lib/playbook/expression.js +302 -0
  76. package/lib/playbook/fleet.js +175 -0
  77. package/lib/playbook/loader.js +247 -0
  78. package/lib/playbook/runner.js +300 -0
  79. package/lib/playbook/schema.js +209 -0
  80. package/lib/playbook/vars.js +209 -0
  81. package/lib/product/log-presets.js +196 -0
  82. package/lib/product/logs.js +485 -0
  83. package/lib/product-orchestrator.js +124 -0
  84. package/lib/product.js +523 -20
  85. package/lib/profile.js +665 -0
  86. package/lib/property.js +7 -40
  87. package/lib/proxy.js +9 -8
  88. package/lib/resource.js +10 -14
  89. package/lib/status.js +78 -37
  90. package/lib/tokens.js +374 -0
  91. package/package.json +14 -3
  92. package/commands/product.js +0 -407
@@ -0,0 +1,218 @@
1
+ // @ts-check
2
+ import Table from 'cli-table3';
3
+ import { createDeviceAPI } from '../../lib/device-api.js';
4
+ import { runProductFanOut } from '../../lib/product-orchestrator.js';
5
+ import {
6
+ label,
7
+ muted,
8
+ hint,
9
+ success,
10
+ error as errorStyle,
11
+ warning,
12
+ } from '../../lib/format.js';
13
+ import {
14
+ isJsonMode,
15
+ printOk,
16
+ printErr,
17
+ createSpinner,
18
+ classifyError,
19
+ } from '../../lib/output.js';
20
+ import {
21
+ applyJsonFlag,
22
+ ensureConfigured,
23
+ getGlobalUser,
24
+ parsePositiveInt,
25
+ ProgressSpinner,
26
+ } from '../_shared.js';
27
+
28
+ /**
29
+ * Factory for product-level fan-out filesystem subcommands.
30
+ * Captures the repetitive boilerplate (fetch product devices,
31
+ * optionally filter offline, fan out through the shared orchestrator,
32
+ * render the same cli-table3 summary) so each verb only has to
33
+ * describe its per-device operation.
34
+ */
35
+ function registerProductFsCommand(product, { name, description, args, build, perDeviceLabel }) {
36
+ const cmd = product
37
+ .command(`${name} ${args}`)
38
+ .helpGroup('Filesystem:')
39
+ .description(description)
40
+ .option('-j, --json', 'Output as JSON')
41
+ .option('-g, --group <group>', 'Filter devices by asset group')
42
+ .option(
43
+ '-c, --concurrency <n>',
44
+ 'Max parallel operations (default: 10)',
45
+ parsePositiveInt('concurrency'),
46
+ 10,
47
+ )
48
+ .option('--fail-fast', 'Stop dequeueing new devices on the first failure')
49
+ .option('-a, --all', 'Include offline devices (by default, only active)');
50
+
51
+ cmd.action(async (...commandArgs) => {
52
+ const opts = commandArgs[commandArgs.length - 2];
53
+ const commanderCmd = commandArgs[commandArgs.length - 1];
54
+ const positionalArgs = commandArgs.slice(0, -2);
55
+ const [productId, ...rest] = positionalArgs;
56
+
57
+ applyJsonFlag(opts);
58
+ ensureConfigured();
59
+ const user = getGlobalUser(commanderCmd);
60
+
61
+ const fetchSpinner = createSpinner(
62
+ `Retrieving devices for product ${productId}...`,
63
+ ).start();
64
+ /** @type {ProgressSpinner | null} */
65
+ let progress = null;
66
+
67
+ let result;
68
+ try {
69
+ result = await runProductFanOut({
70
+ product: productId,
71
+ group: opts.group,
72
+ includeOffline: !!opts.all,
73
+ user,
74
+ concurrency: opts.concurrency,
75
+ failFast: !!opts.failFast,
76
+ worker: async (device) => {
77
+ const api = createDeviceAPI(device.device, { user });
78
+ const perStart = Date.now();
79
+ try {
80
+ await build(api, rest, opts);
81
+ return {
82
+ device: device.device,
83
+ ok: true,
84
+ durationMs: Date.now() - perStart,
85
+ };
86
+ } catch (err) {
87
+ return {
88
+ device: device.device,
89
+ ok: false,
90
+ error: err instanceof Error ? err.message : String(err),
91
+ durationMs: Date.now() - perStart,
92
+ };
93
+ }
94
+ },
95
+ skipped: (device, firstFailure) => ({
96
+ device: device.device,
97
+ ok: false,
98
+ error: `skipped (fail-fast after ${firstFailure})`,
99
+ durationMs: 0,
100
+ }),
101
+ isFailure: (entry) => !entry.ok,
102
+ onDevicesResolved: (devices) => {
103
+ fetchSpinner.succeed(
104
+ `Found ${devices.length} device(s) in product ${productId}`,
105
+ );
106
+ if (devices.length > 0 && !isJsonMode()) {
107
+ progress = new ProgressSpinner(
108
+ createSpinner('').start(),
109
+ devices.length,
110
+ ({ done, total, inFlight }) =>
111
+ `${perDeviceLabel(rest)} on ${total} device(s) — ` +
112
+ `${done}/${total} done · ${inFlight} in flight` +
113
+ (opts.failFast ? ' · fail-fast' : ''),
114
+ );
115
+ progress.render();
116
+ }
117
+ },
118
+ onItemStart: () => progress?.startItem(),
119
+ onItemFinish: () => progress?.finishItem(),
120
+ });
121
+ } catch (error) {
122
+ fetchSpinner.fail(`Failed to retrieve devices for product ${productId}`);
123
+ const { message, code } = classifyError(error);
124
+ printErr(message, { code });
125
+ return;
126
+ }
127
+ progress?.stop();
128
+
129
+ const { devices, entries, durationMs } = result;
130
+
131
+ if (devices.length === 0) {
132
+ if (isJsonMode()) {
133
+ printOk({
134
+ product: productId,
135
+ summary: { total: 0, ok: 0, failed: 0 },
136
+ results: [],
137
+ });
138
+ } else {
139
+ console.log(warning('No devices to target.'));
140
+ }
141
+ return;
142
+ }
143
+
144
+ const okCount = entries.filter((e) => e.ok).length;
145
+ const failCount = entries.length - okCount;
146
+
147
+ if (isJsonMode()) {
148
+ printOk({
149
+ product: productId,
150
+ summary: {
151
+ total: entries.length,
152
+ ok: okCount,
153
+ failed: failCount,
154
+ durationMs,
155
+ },
156
+ results: entries,
157
+ });
158
+ return;
159
+ }
160
+
161
+ const table = new Table({
162
+ head: ['Device', 'Status', 'Detail', 'Duration'].map((h) => label(h)),
163
+ style: { head: [], border: ['gray'] },
164
+ colAligns: ['left', 'left', 'left', 'right'],
165
+ });
166
+ for (const e of entries) {
167
+ table.push([
168
+ e.device,
169
+ e.ok ? success('ok') : errorStyle('fail'),
170
+ e.ok ? muted('—') : e.error || '',
171
+ `${e.durationMs}ms`,
172
+ ]);
173
+ }
174
+ console.log(table.toString());
175
+ console.log(
176
+ hint(`${entries.length} total · ${okCount} ok · ${failCount} failed · ${durationMs}ms`),
177
+ );
178
+ });
179
+
180
+ return cmd;
181
+ }
182
+
183
+ export function registerProductFsCommands(product) {
184
+ registerProductFsCommand(product, {
185
+ name: 'mkdir',
186
+ description: 'Create a directory on every active device of the product',
187
+ args: '<productId> <path>',
188
+ perDeviceLabel: ([path]) => `mkdir ${path}`,
189
+ build: async (api, [path]) => {
190
+ await api.mkdir(path);
191
+ },
192
+ });
193
+
194
+ registerProductFsCommand(product, {
195
+ name: 'rm',
196
+ description:
197
+ 'Delete a file or directory on every active device of the product (DESTRUCTIVE — double-check the path).',
198
+ args: '<productId> <path>',
199
+ perDeviceLabel: ([path]) => `rm ${path}`,
200
+ build: async (api, [path], opts) => {
201
+ await api.delete(path, opts.recursive !== false);
202
+ },
203
+ }).option(
204
+ '-r, --recursive',
205
+ 'Recursively delete non-empty directories (default: true)',
206
+ true,
207
+ ).option('--no-recursive', 'Fail on non-empty directories');
208
+
209
+ registerProductFsCommand(product, {
210
+ name: 'mv',
211
+ description: 'Move or rename a path on every active device of the product',
212
+ args: '<productId> <source> <destination>',
213
+ perDeviceLabel: ([src, dst]) => `mv ${src} → ${dst}`,
214
+ build: async (api, [src, dst], opts) => {
215
+ await api.move(src, dst, !!opts.force);
216
+ },
217
+ }).option('-f, --force', 'Overwrite destination if it already exists');
218
+ }
@@ -0,0 +1,35 @@
1
+ // @ts-check
2
+ import { registerProductListCommand } from './list.js';
3
+ import { registerProductPropertyCommand } from './property.js';
4
+ import { registerProductPropertyAdminCommands } from './props.js';
5
+ import { registerProductMetricCommands } from './metric.js';
6
+ import { registerProductResourceCommand } from './resource.js';
7
+ import { registerProductExecCommand } from './exec.js';
8
+ import { registerProductStatusCommand } from './status.js';
9
+ import { registerProductFsCommands } from './fs.js';
10
+ import { registerProductPushCommand } from './push.js';
11
+ import { registerProductPlaybookCommand } from './playbook.js';
12
+ import { registerProductLogsCommand } from './logs.js';
13
+
14
+ /**
15
+ * `thinr product <subcommand> <productId> …` — fan-out commands across
16
+ * every device that belongs to a product.
17
+ */
18
+ export function productCommand(program) {
19
+ const product = program
20
+ .command('product')
21
+ .helpGroup('Operations:')
22
+ .description('Product commands (subcommand-first: thinr product <action> <productId>)');
23
+
24
+ registerProductListCommand(product);
25
+ registerProductStatusCommand(product);
26
+ registerProductPropertyCommand(product);
27
+ registerProductPropertyAdminCommands(product);
28
+ registerProductMetricCommands(product);
29
+ registerProductResourceCommand(product);
30
+ registerProductExecCommand(product);
31
+ registerProductFsCommands(product);
32
+ registerProductPushCommand(product);
33
+ registerProductPlaybookCommand(product);
34
+ registerProductLogsCommand(product);
35
+ }
@@ -0,0 +1,86 @@
1
+ // @ts-check
2
+ import { getProducts, getProductApi } from '../../lib/product.js';
3
+ import { runPool } from '../../lib/concurrency.js';
4
+ import { label, hint, success } from '../../lib/format.js';
5
+ import {
6
+ isJsonMode,
7
+ printOk,
8
+ printErr,
9
+ createSpinner,
10
+ classifyError,
11
+ } from '../../lib/output.js';
12
+ import {
13
+ applyJsonFlag,
14
+ ensureConfigured,
15
+ getGlobalUser,
16
+ parsePositiveInt,
17
+ } from '../_shared.js';
18
+
19
+ export function registerProductListCommand(product) {
20
+ product
21
+ .command('list')
22
+ .helpGroup('Discovery:')
23
+ .description('List every product configured on the server')
24
+ .option('-j, --json', 'Output as JSON')
25
+ .option(
26
+ '-c, --concurrency <n>',
27
+ 'Max parallel api-resource lookups (default: 10)',
28
+ parsePositiveInt('concurrency'),
29
+ 10,
30
+ )
31
+ .action(async (opts, cmd) => {
32
+ applyJsonFlag(opts);
33
+ ensureConfigured();
34
+ const user = getGlobalUser(cmd);
35
+
36
+ const spinner = createSpinner('Fetching products...').start();
37
+ let products;
38
+ try {
39
+ products = await getProducts(user);
40
+ } catch (error) {
41
+ spinner.fail('Failed to fetch products');
42
+ const { message, code } = classifyError(error);
43
+ printErr(message, { code });
44
+ return;
45
+ }
46
+
47
+ // Script counts come from each product's api resources map.
48
+ // Parallelise so a large catalogue doesn't crawl serially.
49
+ const scriptResults = await runPool(products, opts.concurrency, async (p) => {
50
+ try {
51
+ const apis = await getProductApi(p.product, user);
52
+ return Object.keys(apis || {}).length;
53
+ } catch {
54
+ return null;
55
+ }
56
+ });
57
+ const rows = products.map((p, i) => ({
58
+ product: p.product,
59
+ name: p.name && p.name !== p.product ? p.name : null,
60
+ enabled: !!p.enabled,
61
+ scripts: scriptResults[i]?.ok ? scriptResults[i].value : null,
62
+ }));
63
+
64
+ spinner.succeed(`Found ${rows.length} product(s)`);
65
+
66
+ if (isJsonMode()) {
67
+ printOk({ products: rows });
68
+ return;
69
+ }
70
+
71
+ if (rows.length === 0) return;
72
+
73
+ // Pad the product id column so the trailing name/scripts
74
+ // bits align even when ids have very different widths.
75
+ const idWidth = rows.reduce((w, r) => Math.max(w, r.product.length), 0);
76
+ for (const r of rows) {
77
+ const flag = r.enabled ? success('enabled ') : hint('disabled');
78
+ const id = label(r.product.padEnd(idWidth));
79
+ const name = r.name ? hint(`(${r.name})`) : '';
80
+ const scripts =
81
+ r.scripts != null && r.scripts > 0 ? hint(`${r.scripts} script(s)`) : '';
82
+ const extras = [name, scripts].filter(Boolean).join(' ');
83
+ console.log(` ${flag} ${id}${extras ? ' ' + extras : ''}`);
84
+ }
85
+ });
86
+ }
@@ -0,0 +1,289 @@
1
+ // @ts-check
2
+ import Table from 'cli-table3';
3
+ import {
4
+ addLogSource,
5
+ getProductLogs,
6
+ listPresets,
7
+ removeLogSource,
8
+ setDefaultLogSource,
9
+ } from '../../lib/product/logs.js';
10
+ import { hint, info, label, muted, success } from '../../lib/format.js';
11
+ import { classifyError, isJsonMode, printErr, printOk } from '../../lib/output.js';
12
+ import { applyJsonFlag, ensureConfigured, getGlobalUser } from '../_shared.js';
13
+
14
+ function truncate(value, max = 60) {
15
+ if (typeof value !== 'string') return '';
16
+ return value.length > max ? `${value.slice(0, max - 1)}…` : value;
17
+ }
18
+
19
+ function registerList(logs) {
20
+ logs
21
+ .command('list <productId>')
22
+ .helpGroup('Log sources:')
23
+ .description('List the log sources configured on a product')
24
+ .option('-j, --json', 'Output as JSON')
25
+ .action(async (productId, opts, cmd) => {
26
+ applyJsonFlag(opts);
27
+ ensureConfigured();
28
+ const user = getGlobalUser(cmd);
29
+ try {
30
+ const cfg = await getProductLogs(productId, user);
31
+ const fallback = !!(/** @type {any} */ (cfg).__fallback);
32
+ const def = cfg.default || cfg.sources[0]?.name || null;
33
+
34
+ if (isJsonMode()) {
35
+ printOk({
36
+ product: productId,
37
+ fallback,
38
+ default: def,
39
+ sources: cfg.sources,
40
+ });
41
+ return;
42
+ }
43
+
44
+ if (fallback) {
45
+ console.log(
46
+ `${muted('No logs property set on')} ${info(productId)}${muted('. Showing the synthetic fallback:')}`,
47
+ );
48
+ } else {
49
+ console.log(`${cfg.sources.length} source(s) on ${info(productId)}:`);
50
+ }
51
+ const table = new Table({
52
+ head: ['Name', 'Command', 'Pattern', 'Default'].map((h) => label(h)),
53
+ style: { head: [], border: ['gray'] },
54
+ });
55
+ for (const s of cfg.sources) {
56
+ const pat = s.preset
57
+ ? muted(`preset:${s.preset}`)
58
+ : s.pattern
59
+ ? truncate(s.pattern, 30)
60
+ : muted('—');
61
+ table.push([
62
+ s.name,
63
+ truncate(s.command),
64
+ pat,
65
+ s.name === def ? success('✓') : muted('—'),
66
+ ]);
67
+ }
68
+ console.log(table.toString());
69
+ } catch (err) {
70
+ const { message, code } = classifyError(err);
71
+ printErr(message, { code });
72
+ }
73
+ });
74
+ }
75
+
76
+ function registerAdd(logs) {
77
+ logs
78
+ .command('add <productId> <name>')
79
+ .helpGroup('Log sources:')
80
+ .description('Add or replace a log source on a product')
81
+ .requiredOption('-c, --command <cmd>', 'Shell command the agent should exec-stream for this source')
82
+ .option('--default', 'Mark this source as the active default')
83
+ .option(
84
+ '--pattern <regex>',
85
+ 'Custom regex with named groups (time, level, msg) for structured rendering',
86
+ )
87
+ .option(
88
+ '--preset <name>',
89
+ 'Named preset for structured rendering (see `thinr product logs presets`)',
90
+ )
91
+ .option('-j, --json', 'Output as JSON')
92
+ .action(async (productId, name, opts, cmd) => {
93
+ applyJsonFlag(opts);
94
+ ensureConfigured();
95
+ const user = getGlobalUser(cmd);
96
+ try {
97
+ const { config, action } = await addLogSource(
98
+ productId,
99
+ {
100
+ name,
101
+ command: opts.command,
102
+ makeDefault: !!opts.default,
103
+ pattern: opts.pattern,
104
+ preset: opts.preset,
105
+ },
106
+ user,
107
+ );
108
+ if (isJsonMode()) {
109
+ printOk({ product: productId, action, name, config });
110
+ return;
111
+ }
112
+ console.log(
113
+ `${success(action === 'added' ? 'Added' : 'Updated')} source ${info(name)} on product ${info(productId)}` +
114
+ (opts.default ? ` ${muted('(default)')}` : ''),
115
+ );
116
+ console.log(` ${muted('command:')} ${truncate(opts.command, 80)}`);
117
+ if (opts.preset) console.log(` ${muted('preset:')} ${opts.preset}`);
118
+ if (opts.pattern) console.log(` ${muted('pattern:')} ${truncate(opts.pattern, 80)}`);
119
+ console.log(
120
+ ` ${muted('total sources:')} ${config.sources.length}${
121
+ config.default ? ` ${muted('default:')} ${config.default}` : ''
122
+ }`,
123
+ );
124
+ } catch (err) {
125
+ const { message, code } = classifyError(err);
126
+ printErr(message, { code: code === 'error' ? 'input_error' : code });
127
+ }
128
+ });
129
+ }
130
+
131
+ function registerRemove(logs) {
132
+ logs
133
+ .command('remove <productId> <name>')
134
+ .alias('rm')
135
+ .helpGroup('Log sources:')
136
+ .description('Remove a log source from a product (idempotent)')
137
+ .option('-j, --json', 'Output as JSON')
138
+ .action(async (productId, name, opts, cmd) => {
139
+ applyJsonFlag(opts);
140
+ ensureConfigured();
141
+ const user = getGlobalUser(cmd);
142
+ try {
143
+ const { removed, config } = await removeLogSource(productId, name, user);
144
+ if (isJsonMode()) {
145
+ printOk({
146
+ product: productId,
147
+ name,
148
+ removed,
149
+ config: config || null,
150
+ cleared: removed && config === null,
151
+ });
152
+ return;
153
+ }
154
+ if (!removed) {
155
+ console.log(
156
+ `Source ${info(name)} was not configured on ${info(productId)}.`,
157
+ );
158
+ return;
159
+ }
160
+ if (config === null) {
161
+ console.log(
162
+ `${success('Removed')} source ${info(name)} (last one). ${muted('Logs property cleared — falling back to system journal.')}`,
163
+ );
164
+ return;
165
+ }
166
+ console.log(
167
+ `${success('Removed')} source ${info(name)}. ${muted(`${config.sources.length} source(s) remain`)}` +
168
+ (config.default ? ` ${muted(`· default: ${config.default}`)}` : ''),
169
+ );
170
+ } catch (err) {
171
+ const { message, code } = classifyError(err);
172
+ printErr(message, { code: code === 'error' ? 'input_error' : code });
173
+ }
174
+ });
175
+ }
176
+
177
+ function registerSetDefault(logs) {
178
+ logs
179
+ .command('set-default <productId> <name>')
180
+ .helpGroup('Log sources:')
181
+ .description('Set the active default source on a product')
182
+ .option('-j, --json', 'Output as JSON')
183
+ .action(async (productId, name, opts, cmd) => {
184
+ applyJsonFlag(opts);
185
+ ensureConfigured();
186
+ const user = getGlobalUser(cmd);
187
+ try {
188
+ const config = await setDefaultLogSource(productId, name, user);
189
+ if (isJsonMode()) {
190
+ printOk({ product: productId, default: config.default, config });
191
+ return;
192
+ }
193
+ console.log(
194
+ `${success('Default set')}: ${info(name)} on product ${info(productId)}.`,
195
+ );
196
+ } catch (err) {
197
+ const { message, code } = classifyError(err);
198
+ printErr(message, { code: code === 'error' ? 'input_error' : code });
199
+ }
200
+ });
201
+ }
202
+
203
+ function registerShow(logs) {
204
+ logs
205
+ .command('show <productId> <name>')
206
+ .helpGroup('Log sources:')
207
+ .description('Show a single log source (full command, untruncated)')
208
+ .option('-j, --json', 'Output as JSON')
209
+ .action(async (productId, name, opts, cmd) => {
210
+ applyJsonFlag(opts);
211
+ ensureConfigured();
212
+ const user = getGlobalUser(cmd);
213
+ try {
214
+ const cfg = await getProductLogs(productId, user);
215
+ const source = cfg.sources.find((s) => s.name === name);
216
+ if (!source) {
217
+ const available = cfg.sources.map((s) => s.name).join(', ') || '(none)';
218
+ printErr(
219
+ `Source "${name}" is not configured on product "${productId}". Available: ${available}`,
220
+ { code: 'not_found' },
221
+ );
222
+ return;
223
+ }
224
+ const fallback = !!(/** @type {any} */ (cfg).__fallback);
225
+ const isDefault = (cfg.default || cfg.sources[0]?.name) === name;
226
+ if (isJsonMode()) {
227
+ printOk({
228
+ product: productId,
229
+ fallback,
230
+ name: source.name,
231
+ command: source.command,
232
+ pattern: source.pattern || null,
233
+ preset: source.preset || null,
234
+ default: isDefault,
235
+ });
236
+ return;
237
+ }
238
+ console.log(`${label('Source')}: ${info(source.name)}${isDefault ? ` ${muted('(default)')}` : ''}`);
239
+ console.log(`${label('Command')}: ${source.command}`);
240
+ if (source.preset) console.log(`${label('Preset')}: ${source.preset}`);
241
+ if (source.pattern) console.log(`${label('Pattern')}: ${source.pattern}`);
242
+ if (fallback) console.log(hint('(synthetic fallback — no logs property set)'));
243
+ } catch (err) {
244
+ const { message, code } = classifyError(err);
245
+ printErr(message, { code });
246
+ }
247
+ });
248
+ }
249
+
250
+ function registerPresets(logs) {
251
+ logs
252
+ .command('presets')
253
+ .helpGroup('Log sources:')
254
+ .description('List the named log line patterns shipped with the CLI')
255
+ .option('-j, --json', 'Output as JSON')
256
+ .action(async (opts) => {
257
+ applyJsonFlag(opts);
258
+ const presets = listPresets();
259
+ if (isJsonMode()) {
260
+ printOk({ presets });
261
+ return;
262
+ }
263
+ const table = new Table({
264
+ head: ['Name', 'Description', 'Pattern'].map((h) => label(h)),
265
+ style: { head: [], border: ['gray'] },
266
+ });
267
+ for (const p of presets) {
268
+ table.push([p.name, p.description, truncate(p.pattern, 60)]);
269
+ }
270
+ console.log(`${presets.length} preset(s) available:`);
271
+ console.log(table.toString());
272
+ });
273
+ }
274
+
275
+ export function registerProductLogsCommand(product) {
276
+ const logs = product
277
+ .command('logs')
278
+ .helpGroup('Log sources:')
279
+ .description(
280
+ `Manage log sources stored on a product. ${hint('Subcommands: list, add, remove, set-default, show, presets.')}`,
281
+ );
282
+
283
+ registerList(logs);
284
+ registerAdd(logs);
285
+ registerRemove(logs);
286
+ registerSetDefault(logs);
287
+ registerShow(logs);
288
+ registerPresets(logs);
289
+ }