@vigilbase/cli 0.2.3 → 0.2.5

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 (75) hide show
  1. package/CLOUDFLARE.md +2 -2
  2. package/COMMANDS.md +111 -44
  3. package/LOCAL.md +99 -38
  4. package/ONE.md +2 -2
  5. package/README.md +215 -67
  6. package/dist/auth/acl.d.ts +6 -0
  7. package/dist/auth/acl.js +45 -0
  8. package/dist/auth/device-auth.js +1 -0
  9. package/dist/auth/provider.js +4 -4
  10. package/dist/auth/refresh.d.ts +5 -4
  11. package/dist/auth/refresh.js +82 -41
  12. package/dist/auth/store.d.ts +25 -0
  13. package/dist/auth/store.js +91 -11
  14. package/dist/bin.js +13 -1
  15. package/dist/cli.js +205 -81
  16. package/dist/commands/account.js +11 -7
  17. package/dist/commands/auth.d.ts +6 -0
  18. package/dist/commands/auth.js +75 -24
  19. package/dist/commands/billing.js +2 -2
  20. package/dist/commands/cloudflare.d.ts +1 -1
  21. package/dist/commands/cloudflare.js +31 -17
  22. package/dist/commands/helpers.js +3 -3
  23. package/dist/commands/index.d.ts +1 -0
  24. package/dist/commands/index.js +2 -1
  25. package/dist/commands/legacy-validation.d.ts +20 -0
  26. package/dist/commands/legacy-validation.js +77 -3
  27. package/dist/commands/local.js +186 -65
  28. package/dist/commands/one.js +16 -13
  29. package/dist/commands/public-tools.js +52 -27
  30. package/dist/commands/runs.js +5 -5
  31. package/dist/commands/security.d.ts +1 -1
  32. package/dist/commands/security.js +41 -37
  33. package/dist/commands/watchdog.js +5 -5
  34. package/dist/lib/banner.d.ts +5 -11
  35. package/dist/lib/banner.js +11 -31
  36. package/dist/lib/client.js +19 -1
  37. package/dist/lib/cloudflare-client.d.ts +2 -2
  38. package/dist/lib/config.d.ts +16 -0
  39. package/dist/lib/config.js +77 -19
  40. package/dist/lib/format.d.ts +1 -1
  41. package/dist/lib/format.js +75 -24
  42. package/dist/lib/help.js +57 -6
  43. package/dist/lib/network.d.ts +12 -3
  44. package/dist/lib/network.js +73 -9
  45. package/dist/lib/one-client.js +9 -2
  46. package/dist/lib/one-error-hints.d.ts +1 -0
  47. package/dist/lib/one-error-hints.js +10 -0
  48. package/dist/lib/open-url.js +2 -1
  49. package/dist/lib/platform.d.ts +2 -0
  50. package/dist/lib/platform.js +5 -0
  51. package/dist/lib/progress.d.ts +18 -3
  52. package/dist/lib/progress.js +72 -10
  53. package/dist/lib/system-trust.d.ts +2 -0
  54. package/dist/lib/system-trust.js +6 -0
  55. package/dist/lib/tui.d.ts +1 -1
  56. package/dist/lib/tui.js +5 -7
  57. package/dist/local/cloudflare-checkup/cf/child-env.js +2 -4
  58. package/dist/local/cloudflare-checkup/cf/install.d.ts +4 -2
  59. package/dist/local/cloudflare-checkup/cf/install.js +55 -24
  60. package/dist/local/cloudflare-checkup/cf/locate.d.ts +2 -0
  61. package/dist/local/cloudflare-checkup/cf/locate.js +10 -1
  62. package/dist/local/cloudflare-checkup/cf/run-cf.d.ts +18 -3
  63. package/dist/local/cloudflare-checkup/cf/run-cf.js +75 -17
  64. package/dist/local/cloudflare-checkup/contract.d.ts +16 -9
  65. package/dist/local/cloudflare-checkup/contract.js +80 -14
  66. package/dist/local/cloudflare-checkup/errors.d.ts +34 -1
  67. package/dist/local/cloudflare-checkup/errors.js +36 -5
  68. package/dist/local/cloudflare-checkup/output.d.ts +6 -0
  69. package/dist/local/cloudflare-checkup/output.js +9 -0
  70. package/dist/local/cloudflare-checkup/snapshot-file.d.ts +6 -2
  71. package/dist/local/cloudflare-checkup/snapshot-file.js +25 -11
  72. package/dist/local/cloudflare-checkup/submit-client.js +7 -1
  73. package/dist/types.d.ts +2 -1
  74. package/esm-only.cjs +3 -0
  75. package/package.json +7 -4
@@ -1,13 +1,14 @@
1
- import { z } from 'zod';
2
- import { toolNameSchema, validateArguments } from './legacy-validation.js';
1
+ import { InvalidArgumentError, targetValue, toolNameValue, validateArguments } from './legacy-validation.js';
3
2
  import { NetworkError } from '../lib/network.js';
4
3
  import { PUBLIC_TOOLS_PATH, VigilbaseApiError } from '../lib/client.js';
5
4
  import { getOneAccessToken, getOptionalOneAccessToken, hasUsableSignIn, readCredentials } from '../auth/index.js';
6
- import { SignInUnsupportedError } from '../auth/store.js';
5
+ import { CredentialsPathError, NotSignedInError, SignInUnsupportedError } from '../auth/store.js';
7
6
  import { formatExecuteResult, formatToolDescription, formatToolsList, isSuccessfulScan, } from '../lib/format.js';
8
7
  import { isOneAuthRequiredTool, isOneExecutableTool, ONE_AUTH_REQUIRED_TOOLS, resolveToolDefinition, VigilbaseOneClient, } from '../lib/one-client.js';
9
8
  import { EXIT, ok, outputJson, withSpinner, } from './helpers.js';
10
- const ONE_SIGN_IN_REQUIRED = 'This tool needs a Vigilbase One sign-in: run `vigilbase login`.';
9
+ function unknownTool(toolName) {
10
+ return new InvalidArgumentError(`Unknown tool "${toolName}". Run \`vigilbase list\` for the allowlist.`);
11
+ }
11
12
  /**
12
13
  * For --via one and auth-required tools only: any sign-in problem fails. Specific errors keep their
13
14
  * message; those that don't already point to `vigilbase login` get it as their hint.
@@ -18,14 +19,15 @@ async function requireOneSignIn(config) {
18
19
  accessToken = await getOneAccessToken(config);
19
20
  }
20
21
  catch (error) {
21
- // Windows cannot sign in at all, so pointing to login would mislead.
22
- if (error instanceof Error && !(error instanceof SignInUnsupportedError) && !error.message.includes('`vigilbase login`')) {
23
- Object.assign(error, { hint: ONE_SIGN_IN_REQUIRED });
22
+ // Windows cannot sign in at all, and login stops on an unsafe credentials path too, so pointing to it would mislead.
23
+ const isLoginBlocked = error instanceof SignInUnsupportedError || error instanceof CredentialsPathError;
24
+ if (error instanceof Error && !isLoginBlocked && !error.message.includes('`vigilbase login`')) {
25
+ Object.assign(error, { hint: new NotSignedInError().message });
24
26
  }
25
27
  throw error;
26
28
  }
27
29
  if (!accessToken)
28
- throw new Error(ONE_SIGN_IN_REQUIRED);
30
+ throw new NotSignedInError();
29
31
  return accessToken;
30
32
  }
31
33
  /** Public tools work without login. Auth-required tools need Vigilbase One. */
@@ -39,6 +41,7 @@ export const handleList = async (ctx) => {
39
41
  ...catalog,
40
42
  authenticated,
41
43
  authRequiredTools: [...ONE_AUTH_REQUIRED_TOOLS],
44
+ // Signed out, nothing runs through One.
42
45
  oneExecutableTools: authenticated
43
46
  ? [
44
47
  ...new Set([
@@ -46,7 +49,7 @@ export const handleList = async (ctx) => {
46
49
  ...ONE_AUTH_REQUIRED_TOOLS,
47
50
  ]),
48
51
  ]
49
- : catalog.tools.map((tool) => tool.toolName),
52
+ : [],
50
53
  });
51
54
  }
52
55
  else {
@@ -78,17 +81,23 @@ export const handleRun = async (ctx) => {
78
81
  }
79
82
  if (!toolName || !target)
80
83
  throw new Error(`Usage: ${usage}`);
81
- toolName = toolNameSchema.parse(toolName);
82
- target = z.string().trim().min(1).max(2048).refine((value) => !/[\u0000-\u001f\u007f]/.test(value), 'Target must not contain control characters.').parse(target);
84
+ toolName = toolNameValue(toolName);
85
+ target = targetValue(target);
86
+ // Known tools are those One can run: the public ones and the One-only ones.
87
+ if (!isOneExecutableTool(toolName))
88
+ throw unknownTool(toolName);
83
89
  const via = ctx.values.via;
84
90
  if (ctx.values.isPublic && via === 'one') {
85
91
  throw new Error('--public cannot be combined with --via one.');
86
92
  }
87
- if (via && via !== 'one' && via !== 'public') {
88
- throw new Error('--via must be one of: public, one');
93
+ if (via !== undefined && via !== 'one' && via !== 'public') {
94
+ throw new InvalidArgumentError('--via must be one of: public, one.');
89
95
  }
90
96
  const forcePublic = Boolean(ctx.values.isPublic) || via === 'public';
91
97
  const forceOne = via === 'one';
98
+ if (forcePublic && isOneAuthRequiredTool(toolName)) {
99
+ throw new Error(`${toolName} runs only through Vigilbase One: drop ${ctx.values.isPublic ? '--public' : '--via public'} and run \`vigilbase login\`.`);
100
+ }
92
101
  // Public tools stay free unless forced onto One. Auth-required tools need login.
93
102
  // The public route is anonymous and never reads the credential store.
94
103
  const useOne = !forcePublic && (forceOne || isOneAuthRequiredTool(toolName));
@@ -123,15 +132,14 @@ export const handleRun = async (ctx) => {
123
132
  };
124
133
  export const handleDescribe = async (ctx) => {
125
134
  validateArguments(ctx, 1, ['isNoSkill'], 'vigilbase describe <tool>');
126
- toolNameSchema.parse(ctx.rest[0]);
135
+ toolNameValue(ctx.rest[0]);
127
136
  const toolName = ctx.rest[0];
128
137
  if (!toolName)
129
138
  throw new Error('Usage: vigilbase describe <tool>');
130
139
  const catalog = await withSpinner(ctx, 'Loading tool catalog', () => ctx.publicClient.listTools());
131
140
  const tool = resolveToolDefinition(toolName, catalog.tools, ctx.config.oneBaseUrl);
132
- if (!tool) {
133
- throw new Error(`Unknown tool "${toolName}". Run \`vigilbase list\` for the allowlist.`);
134
- }
141
+ if (!tool)
142
+ throw unknownTool(toolName);
135
143
  let skillMarkdown;
136
144
  if (!ctx.values.isNoSkill) {
137
145
  try {
@@ -146,18 +154,18 @@ export const handleDescribe = async (ctx) => {
146
154
  };
147
155
  export const handleSkill = async (ctx) => {
148
156
  validateArguments(ctx, 1, [], 'vigilbase skill <tool>');
149
- toolNameSchema.parse(ctx.rest[0]);
157
+ toolNameValue(ctx.rest[0]);
150
158
  const toolName = ctx.rest[0];
151
159
  if (!toolName)
152
160
  throw new Error('Usage: vigilbase skill <tool>');
153
161
  const catalog = await withSpinner(ctx, 'Loading tool catalog', () => ctx.publicClient.listTools());
154
162
  const definition = resolveToolDefinition(toolName, catalog.tools, ctx.config.oneBaseUrl);
155
- if (!definition) {
156
- throw new Error(`Unknown tool "${toolName}". Run \`vigilbase list\` for the allowlist.`);
157
- }
163
+ if (!definition)
164
+ throw unknownTool(toolName);
158
165
  const skillMarkdown = await withSpinner(ctx, 'Fetching skill docs', () => ctx.publicClient.fetchSkillMarkdown(definition.skillUrl));
159
166
  if (ctx.mode === 'json') {
160
167
  outputJson(ctx, {
168
+ success: true,
161
169
  toolName: definition.toolName,
162
170
  skillUrl: definition.skillUrl,
163
171
  content: skillMarkdown,
@@ -168,18 +176,35 @@ export const handleSkill = async (ctx) => {
168
176
  }
169
177
  return ok();
170
178
  };
179
+ /** The badge route's domain rule (apps/com/app/api/badge/[domain]/route.ts). */
180
+ const BADGE_DOMAIN = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
181
+ /** The hostname the badge route accepts: a URL gives its hostname; case, a trailing dot and www. don't matter. */
182
+ function badgeDomain(value) {
183
+ let domain = value.trim();
184
+ try {
185
+ const url = new URL(domain);
186
+ if (url.protocol === 'https:' || url.protocol === 'http:')
187
+ domain = url.hostname;
188
+ }
189
+ catch {
190
+ // Not a URL: a bare domain.
191
+ }
192
+ domain = domain.toLowerCase().replace(/\.$/, '').replace(/^www\./, '');
193
+ if (domain.length > 253 || !BADGE_DOMAIN.test(domain)) {
194
+ throw new InvalidArgumentError('Enter a domain such as vigilbase.com. Usage: vigilbase badge <domain>');
195
+ }
196
+ return domain;
197
+ }
171
198
  export const handleBadge = async (ctx) => {
172
199
  validateArguments(ctx, 1, ['style'], 'vigilbase badge <domain>');
173
- const domain = ctx.rest[0];
174
- if (!domain)
175
- throw new Error('Usage: vigilbase badge <domain>');
200
+ const domain = badgeDomain(ctx.rest[0] ?? '');
176
201
  const styleRaw = ctx.values.style ?? 'default';
177
202
  if (styleRaw !== 'default' && styleRaw !== 'compact' && styleRaw !== 'mini') {
178
- throw new Error('--style must be one of: default, compact, mini');
203
+ throw new InvalidArgumentError('--style must be one of: default, compact, mini.');
179
204
  }
180
205
  const url = ctx.publicClient.badgeUrl(domain, styleRaw);
181
206
  if (ctx.mode === 'json') {
182
- outputJson(ctx, { domain, style: styleRaw, url });
207
+ outputJson(ctx, { success: true, domain, style: styleRaw, url });
183
208
  }
184
209
  else {
185
210
  ctx.writer.stdout(url);
@@ -246,7 +271,7 @@ export const handleDoctor = async (ctx) => {
246
271
  const payload = {
247
272
  ok: isOk,
248
273
  success: isOk,
249
- ...(firstFailure ? { error: firstFailure.error, status: firstFailure.status } : {}),
274
+ ...(firstFailure ? { error: firstFailure.error, status: firstFailure.status, ...(firstFailure.code ? { code: firstFailure.code } : {}) } : {}),
250
275
  publicBaseUrl: ctx.config.publicBaseUrl,
251
276
  oneBaseUrl: ctx.config.oneBaseUrl,
252
277
  clientId: ctx.config.clientId,
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { limitValue, toolNameSchema, validateArguments } from './legacy-validation.js';
2
+ import { limitValue, parseArgument, toolNameValue, unknownSubcommand, validateArguments } from './legacy-validation.js';
3
3
  import { asArray, asRecord, ok, outputJson, requireOneClient, withSpinner, } from './helpers.js';
4
4
  export const handleRuns = async (ctx) => {
5
5
  const subcommand = (ctx.rest[0] ?? 'list').toLowerCase();
@@ -7,16 +7,16 @@ export const handleRuns = async (ctx) => {
7
7
  if (subcommand === 'list' || subcommand === 'ls') {
8
8
  validateArguments(ctx, ctx.rest[0] === undefined ? 0 : 1, ['tool', 'limit']);
9
9
  limit = limitValue(ctx.values.limit, 50, 200);
10
- toolName = toolNameSchema.optional().parse(ctx.values.tool);
10
+ toolName = ctx.values.tool === undefined ? undefined : toolNameValue(ctx.values.tool);
11
11
  }
12
12
  else if (subcommand === 'get' || subcommand === 'show') {
13
13
  validateArguments(ctx, 2, [], `vigilbase runs ${subcommand} <UUID>`);
14
- z.string().uuid().parse(ctx.rest[1]);
14
+ parseArgument(z.string().uuid(), ctx.rest[1], 'A valid UUID is required. Copy the full run ID from `vigilbase runs list`.');
15
15
  }
16
16
  else if (z.string().uuid().safeParse(subcommand).success)
17
17
  validateArguments(ctx, 1);
18
18
  else
19
- throw new Error('Usage: vigilbase runs [list|get <UUID>]');
19
+ throw unknownSubcommand('runs', ctx.rest[0] ?? '');
20
20
  const one = await requireOneClient(ctx);
21
21
  if (subcommand === 'list' || subcommand === 'ls') {
22
22
  const payload = await withSpinner(ctx, 'Loading tool runs', () => one.listToolRuns({
@@ -36,7 +36,7 @@ export const handleRuns = async (ctx) => {
36
36
  const run = asRecord(raw);
37
37
  if (!run)
38
38
  continue;
39
- ctx.writer.stdout(`${String(run.id).slice(0, 8)} ${String(run.tool_name).padEnd(18)} ${String(run.status).padEnd(10)} ${String(run.grade ?? '-')} ${String(run.target)}`);
39
+ ctx.writer.stdout(`${String(run.id)} ${String(run.tool_name).padEnd(18)} ${String(run.status).padEnd(10)} ${String(run.grade ?? '-')} ${String(run.target)}`);
40
40
  }
41
41
  return ok();
42
42
  }
@@ -2,4 +2,4 @@ import { type SecurityRequest } from '../lib/security-client.js';
2
2
  import { type CommandContext, type CommandHandler } from './helpers.js';
3
3
  export declare function buildSecurityRequest(ctx: CommandContext): SecurityRequest;
4
4
  export declare const handleSecurity: CommandHandler;
5
- export declare const SECURITY_HELP = "vigilbase security \u2014 managed security in Vigilbase One\n\n [overview]\n incidents [list] --status --severity --attack-category\n --order-by --order-direction --limit --offset\n incidents show <id>\n incidents timeline <id> --limit --before\n incidents fabric <id> --dataset --min-weight --limit\n incidents share-links <id>\n incidents update <id> <status> --confirm [--note]\n alerts [list] --limit --asset-id --start\n alerts show <id>\n logs --limit --asset-id --provider --dataset --search\n --start --end --sort --frs-min --cursor\n audit --limit --cursor --action-prefix --outcome\n assets\n assets <allowlist|blocklist|remediation|autonomy|datasets> <asset-id>\n assets graph <asset-id> --dataset --hours --min-weight --limit\n fabric default-asset\n detections --asset-id <asset-id>\n providers\n connections [list]\n connections show <id>\n connections <available-assets|remediation> <id>\n connections <logpush-status|worker-stats> <connection-id> <job-id>\n analytics [overview] --metrics + time-window flags\n analytics <security-analysis|bot-activity|top-attacker-ips|bot-asns|dns-security|page-shield>\n analytics provider-events --provider --dataset --start --end\n [--asset-id --search --limit]\n zero-trust <device-health|app-tests|access|sessions|device-posture>\n zero-trust gateway <dns|http|network>\n cloudflare-account [inventory]\n cloudflare-account <audit|ingestion|posture>\n notifications [list] --limit\n notifications <settings|preferences|slack-channels>\n reports [list] --limit --offset\n reports show <id>\n reports settings\n intel quota\n intel <ip|asn|cidr|ja3|ja4> <query>\n settings <lifecycle-emails|broadcast-emails|applications>\n\nTime-window flags: --hours (1\u2013720, default 24) or --start/--end ISO timestamps;\noptional --asset-id. Apply to analytics (except provider-events), zero-trust,\nand cloudflare-account audit/ingestion/posture. Inventory has no window flags.\nLimits: list incidents/reports/audit 1\u2013100 (default 50); alerts/notifications\n1\u2013100 (default 100); timeline 1\u2013200 (default 100); logs 1\u20131000 (default 100);\nprovider-events 1\u2013500 (default 200); graph 1\u20131000 (default 200).\nOffsets: 0\u20131000000. Graph min-weight: 1\u20131000000 (default 1).\nAll: --json, --timeout <ms>, --customer-id <One customer UUID> (internal View-as).\nAll operations read data except confirmed incident status updates.\nUpdate status: acknowledged, mitigating, resolved, false_positive.\nSee SECURITY.md for every endpoint, supported value, alias, and pagination contract.\n";
5
+ export declare const SECURITY_HELP = "vigilbase security \u2014 managed security in Vigilbase One\n\n [overview]\n incidents [list] --status --severity --attack-category\n --order-by --order-direction\n --limit --offset\n incidents show <id>\n incidents timeline <id> --limit --before\n incidents fabric <id> --dataset --min-weight --limit\n incidents share-links <id>\n incidents update <id> <status> --confirm [--note]\n alerts [list] --limit --asset-id --start\n alerts show <id>\n logs --start --end --limit --cursor --sort\n --asset-id --provider --dataset\n --search --frs-min\n audit --limit --cursor\n --action-prefix --outcome\n assets\n assets <allowlist|blocklist|remediation|autonomy|datasets> <asset-id>\n assets graph <asset-id> --dataset --hours --min-weight --limit\n fabric default-asset\n detections --asset-id <asset-id>\n providers\n connections [list]\n connections show <id>\n connections <available-assets|remediation> <id>\n connections <logpush-status|worker-stats> <connection-id> <job-id>\n analytics [overview] --metrics + time-window flags\n analytics <security-analysis|bot-activity|top-attacker-ips>\n analytics <bot-asns|dns-security|page-shield>\n analytics provider-events --provider --dataset --start --end\n [--asset-id --search --limit]\n zero-trust <device-health|app-tests|access|sessions|device-posture>\n zero-trust gateway <dns|http|network>\n cloudflare-account [inventory]\n cloudflare-account <audit|ingestion|posture>\n notifications [list] --limit\n notifications <settings|preferences|slack-channels>\n reports [list] --limit --offset\n reports show <id>\n reports settings\n intel quota\n intel <ip|asn|cidr|ja3|ja4> <query>\n settings <lifecycle-emails|broadcast-emails|applications>\n\nTime-window flags: --hours (1\u2013720, default 24) or --start/--end ISO timestamps;\noptional --asset-id. Apply to analytics (except provider-events), zero-trust,\nand cloudflare-account audit/ingestion/posture. Inventory has no window flags.\nLimits: list incidents/reports/audit 1\u2013100 (default 50); alerts/notifications\n1\u2013100 (default 100); timeline 1\u2013200 (default 100); logs 1\u20131000 (default 100);\nprovider-events 1\u2013500 (default 200); graph 1\u20131000 (default 200).\nOffsets: 0\u20131000000. Graph min-weight: 1\u20131000000 (default 1).\nAll: --json, --timeout <ms>.\nInternal View-as: --customer-id <One customer UUID>.\nAll operations read data except confirmed incident status updates.\nUpdate status: acknowledged, mitigating, resolved, false_positive.\nSee SECURITY.md for endpoints, supported values, aliases and pagination.\n";
@@ -1,22 +1,20 @@
1
1
  import { z } from 'zod';
2
2
  import { SecurityClient } from '../lib/security-client.js';
3
3
  import { assertCommandFlags, asArray, asRecord, ok, outputJson, requireOneClient, withSpinner } from './helpers.js';
4
- import { missingArguments } from './legacy-validation.js';
4
+ import { InvalidArgumentError, inRange, isoDateTime, missingArguments, oneOf, parseArgument, textValue, unknownSubcommand } from './legacy-validation.js';
5
5
  const statuses = ['open', 'active', 'acknowledged', 'investigating', 'mitigating', 'mitigated', 'resolved', 'false_positive'];
6
6
  const updateStatuses = ['acknowledged', 'mitigating', 'resolved', 'false_positive'];
7
7
  function textFlag(ctx, flag) {
8
8
  const value = ctx.values[flag];
9
- if (value === undefined)
10
- return undefined;
11
- return z.string().trim().min(1, `--${flag} must not be empty.`).max(2048).parse(value);
9
+ return value === undefined ? undefined : textValue(`--${flag}`, String(value), 2048);
12
10
  }
13
11
  function integer(ctx, flag, max, fallback) {
14
12
  const value = textFlag(ctx, flag);
15
13
  if (value === undefined)
16
14
  return fallback;
17
15
  if (!/^\d+$/.test(value))
18
- throw new Error(`--${flag} must be an integer.`);
19
- return z.number().int().min(['limit', 'hours', 'min-weight'].includes(flag) ? 1 : 0).max(max).parse(Number(value));
16
+ throw new InvalidArgumentError(`--${flag} must be an integer.`);
17
+ return inRange(`--${flag}`, Number(value), ['limit', 'hours', 'min-weight'].includes(flag) ? 1 : 0, max);
20
18
  }
21
19
  function allowed(ctx, flags) {
22
20
  assertCommandFlags(ctx, [...flags, 'customer-id']);
@@ -28,6 +26,10 @@ function arity(ctx, length, usage) {
28
26
  if (ctx.rest.length > length)
29
27
  throw new Error('Unexpected security arguments. Run `vigilbase security --help` for usage.');
30
28
  }
29
+ /** The entry of a command table named by user input; never an inherited Object.prototype member. */
30
+ function own(table, name) {
31
+ return name !== undefined && Object.hasOwn(table, name) ? table[name] : undefined;
32
+ }
31
33
  function uuid(value) {
32
34
  const parsed = z.string().uuid().safeParse(value);
33
35
  if (!parsed.success)
@@ -44,8 +46,8 @@ function extendedSecurityRequest(ctx, scope) {
44
46
  if (Boolean(start) !== Boolean(end))
45
47
  throw new Error('--start and --end must be supplied together as ISO timestamps.');
46
48
  if (start && end) {
47
- z.iso.datetime({ offset: true }).parse(start);
48
- z.iso.datetime({ offset: true }).parse(end);
49
+ isoDateTime('--start', start);
50
+ isoDateTime('--end', end);
49
51
  if (Date.parse(start) >= Date.parse(end))
50
52
  throw new Error('--start must be before --end.');
51
53
  if (ctx.values.hours !== undefined)
@@ -79,9 +81,9 @@ function extendedSecurityRequest(ctx, scope) {
79
81
  throw new Error('--search must be at most 200 characters for provider events.');
80
82
  return request('/analytics/provider-events', 'providerEvents', { provider_id: provider, dataset_id: dataset, start: query.start, end: query.end, asset_ids: query.asset_id, search, limit: integer(ctx, 'limit', 500, 200) });
81
83
  }
82
- const view = views[operation];
84
+ const view = own(views, operation);
83
85
  if (!view)
84
- throw new Error(`Unknown analytics command: ${operation}`);
86
+ throw unknownSubcommand('security analytics', operation);
85
87
  const query = window(operation === 'overview' ? ['metrics'] : []);
86
88
  if (operation === 'overview') {
87
89
  const metrics = textFlag(ctx, 'metrics') ?? 'hourly,by_domain,by_dataset';
@@ -94,26 +96,28 @@ function extendedSecurityRequest(ctx, scope) {
94
96
  }
95
97
  if (group === 'zero-trust') {
96
98
  const views = { 'device-health': 'deviceHealth', 'app-tests': 'appTests', gateway: 'gateway', access: 'access', sessions: 'sessions', 'device-posture': 'devicePosture' };
97
- if (!sub || !views[sub])
99
+ const view = own(views, sub);
100
+ if (!sub || !view)
98
101
  throw new Error('Choose a zero-trust command: device-health, app-tests, gateway, access, sessions, device-posture.');
99
102
  arity(ctx, sub === 'gateway' ? 3 : 2, 'zero-trust gateway <dns|http|network>');
100
103
  const query = window();
101
104
  if (sub === 'gateway')
102
- query.surface = z.enum(['dns', 'http', 'network']).parse(id);
103
- return request(`/zero-trust/${sub}`, views[sub], query);
105
+ query.surface = oneOf('Gateway surface', ['dns', 'http', 'network'], id);
106
+ return request(`/zero-trust/${sub}`, view, query);
104
107
  }
105
108
  if (group === 'cloudflare-account') {
106
109
  const operation = sub ?? 'inventory';
107
110
  const views = { inventory: 'accountInventory', audit: 'accountAudit', ingestion: 'accountIngestion', posture: 'accountPosture' };
108
- if (!views[operation])
109
- throw new Error(`Unknown Cloudflare account security command: ${operation}`);
111
+ const view = own(views, operation);
112
+ if (!view)
113
+ throw unknownSubcommand('security cloudflare-account', operation);
110
114
  arity(ctx, sub === undefined ? 1 : 2);
111
115
  let query = {};
112
116
  if (operation === 'inventory')
113
117
  allowed(ctx, []);
114
118
  else
115
119
  query = window();
116
- return request(`/cloudflare-account/${operation}`, views[operation], query);
120
+ return request(`/cloudflare-account/${operation}`, view, query);
117
121
  }
118
122
  if (group === 'providers') {
119
123
  arity(ctx, 1);
@@ -123,9 +127,9 @@ function extendedSecurityRequest(ctx, scope) {
123
127
  if (group === 'notifications') {
124
128
  const operation = sub ?? 'list';
125
129
  const routes = { list: ['/notifications', 'notifications'], settings: ['/notifications/settings', 'notificationSettings'], preferences: ['/notifications/preferences', 'preferences'], 'slack-channels': ['/notifications/slack/channels', 'slackChannels'] };
126
- const route = routes[operation];
130
+ const route = own(routes, operation);
127
131
  if (!route)
128
- throw new Error(`Unknown notifications command: ${operation}`);
132
+ throw unknownSubcommand('security notifications', operation);
129
133
  arity(ctx, sub === undefined ? 1 : 2);
130
134
  allowed(ctx, operation === 'list' ? ['limit'] : []);
131
135
  return request(...route, operation === 'list' ? { limit: integer(ctx, 'limit', 100, 100) } : {});
@@ -136,9 +140,9 @@ function extendedSecurityRequest(ctx, scope) {
136
140
  remediation: (assetId) => [`/assets/${assetId}/remediation-settings`, 'assetRemediation'], autonomy: (assetId) => [`/assets/${assetId}/vigil-autonomy`, 'autonomy'],
137
141
  datasets: (assetId) => [`/fabric/assets/${assetId}/datasets`, 'datasets'], graph: (assetId) => [`/fabric/assets/${assetId}/graph`, 'graph'],
138
142
  };
139
- const route = Object.hasOwn(routes, sub) ? routes[sub] : undefined;
143
+ const route = own(routes, sub);
140
144
  if (!route)
141
- throw new Error(`Unknown assets command: ${sub}`);
145
+ throw unknownSubcommand('security assets', sub);
142
146
  arity(ctx, 3, `assets ${sub} <asset-id>`);
143
147
  if (sub !== 'graph')
144
148
  allowed(ctx, []);
@@ -174,12 +178,12 @@ function extendedSecurityRequest(ctx, scope) {
174
178
  return request(`/alerts/${uuid(id)}`, 'alert');
175
179
  }
176
180
  if (sub !== undefined && sub !== 'list')
177
- throw new Error(`Unknown alerts command: ${sub}`);
181
+ throw unknownSubcommand('security alerts', sub);
178
182
  arity(ctx, sub === undefined ? 1 : 2);
179
183
  allowed(ctx, ['limit', 'asset-id', 'start']);
180
184
  const since = textFlag(ctx, 'start');
181
185
  if (since)
182
- z.iso.datetime({ offset: true }).parse(since);
186
+ isoDateTime('--start', since);
183
187
  return request('/alerts', 'alerts', { limit: integer(ctx, 'limit', 100, 100), asset_id: ctx.values['asset-id'] !== undefined ? uuid(ctx.values['asset-id']) : undefined, since });
184
188
  }
185
189
  if (group === 'incidents' && (sub === 'fabric' || sub === 'share-links')) {
@@ -190,7 +194,7 @@ function extendedSecurityRequest(ctx, scope) {
190
194
  }
191
195
  if (group === 'settings') {
192
196
  const routes = { 'lifecycle-emails': ['/settings/lifecycle-emails', 'lifecycleEmails'], 'broadcast-emails': ['/settings/broadcast-emails', 'broadcastEmails'], applications: ['/integrations/grants', 'applications'] };
193
- const route = sub && routes[sub];
197
+ const route = own(routes, sub);
194
198
  if (!route)
195
199
  throw new Error('Choose settings lifecycle-emails, broadcast-emails, or applications.');
196
200
  arity(ctx, 2);
@@ -228,13 +232,13 @@ export function buildSecurityRequest(ctx) {
228
232
  arity(ctx, 4, 'incidents update <id> <status>');
229
233
  allowed(ctx, ['note', 'isConfirm']);
230
234
  const id = uuid(ctx.rest[2]);
231
- const status = z.enum(updateStatuses).parse(ctx.rest[3]);
235
+ const status = oneOf('Status', updateStatuses, ctx.rest[3]);
232
236
  if (!ctx.values.isConfirm)
233
237
  throw new Error('Updating an incident requires --confirm. Your One permissions still apply.');
234
238
  return { ...request(`/incidents/${id}`, 'update'), body: { status, ...(ctx.values.note !== undefined ? { note: textFlag(ctx, 'note') } : {}) } };
235
239
  }
236
240
  if (command !== 'list' && command !== 'ls')
237
- throw new Error(`Unknown security ${group} command: ${command}`);
241
+ throw unknownSubcommand(`security ${group}`, command);
238
242
  arity(ctx, ctx.rest[1] === undefined ? 1 : 2);
239
243
  if (group === 'connections') {
240
244
  allowed(ctx, []);
@@ -247,11 +251,11 @@ export function buildSecurityRequest(ctx) {
247
251
  allowed(ctx, ['limit', 'offset', 'status', 'severity', 'attack-category', 'order-by', 'order-direction']);
248
252
  return request('/incidents', 'incidents', {
249
253
  limit: integer(ctx, 'limit', 100, 50), offset: integer(ctx, 'offset', 1_000_000),
250
- status: z.enum(statuses).optional().parse(textFlag(ctx, 'status')),
251
- severity: z.enum(['critical', 'high', 'medium', 'low']).optional().parse(textFlag(ctx, 'severity')),
254
+ status: oneOf('--status', statuses, textFlag(ctx, 'status')),
255
+ severity: oneOf('--severity', ['critical', 'high', 'medium', 'low'], textFlag(ctx, 'severity')),
252
256
  attack_category: textFlag(ctx, 'attack-category'),
253
- order_by: z.enum(['last_alert_at', 'first_alert_at', 'created_at', 'severity_rank', 'combined_score', 'alert_count']).optional().parse(textFlag(ctx, 'order-by')),
254
- order_direction: z.enum(['asc', 'desc']).optional().parse(textFlag(ctx, 'order-direction')),
257
+ order_by: oneOf('--order-by', ['last_alert_at', 'first_alert_at', 'created_at', 'severity_rank', 'combined_score', 'alert_count'], textFlag(ctx, 'order-by')),
258
+ order_direction: oneOf('--order-direction', ['asc', 'desc'], textFlag(ctx, 'order-direction')),
255
259
  });
256
260
  }
257
261
  if (group === 'assets') {
@@ -274,8 +278,8 @@ export function buildSecurityRequest(ctx) {
274
278
  if (Boolean(start) !== Boolean(end))
275
279
  throw new Error('--start and --end must be supplied together as ISO timestamps.');
276
280
  if (start && end) {
277
- z.iso.datetime({ offset: true }).parse(start);
278
- z.iso.datetime({ offset: true }).parse(end);
281
+ isoDateTime('--start', start);
282
+ isoDateTime('--end', end);
279
283
  if (Date.parse(start) >= Date.parse(end))
280
284
  throw new Error('--start must be before --end.');
281
285
  }
@@ -283,7 +287,7 @@ export function buildSecurityRequest(ctx) {
283
287
  limit: integer(ctx, 'limit', 1000, 100), asset_id: ctx.values['asset-id'] !== undefined ? uuid(ctx.values['asset-id']) : undefined,
284
288
  provider: textFlag(ctx, 'provider'), dataset: textFlag(ctx, 'dataset'), search: textFlag(ctx, 'search'),
285
289
  start_time: start, end_time: end, cursor: textFlag(ctx, 'cursor'),
286
- sort: z.enum(['ts', 'frs', 'frs_desc']).optional().parse(textFlag(ctx, 'sort')),
290
+ sort: oneOf('--sort', ['ts', 'frs', 'frs_desc'], textFlag(ctx, 'sort')),
287
291
  frs_min: integer(ctx, 'frs-min', 100),
288
292
  });
289
293
  }
@@ -292,7 +296,7 @@ export function buildSecurityRequest(ctx) {
292
296
  allowed(ctx, ['limit', 'cursor', 'action-prefix', 'outcome']);
293
297
  return request('/vigilbase-one/audit-events', 'audit', {
294
298
  limit: integer(ctx, 'limit', 100, 50), cursor: textFlag(ctx, 'cursor'), action_prefix: textFlag(ctx, 'action-prefix'),
295
- outcome: z.enum(['success', 'failure', 'denied']).optional().parse(textFlag(ctx, 'outcome')),
299
+ outcome: oneOf('--outcome', ['success', 'failure', 'denied'], textFlag(ctx, 'outcome')),
296
300
  });
297
301
  }
298
302
  if (group === 'intel') {
@@ -302,11 +306,11 @@ export function buildSecurityRequest(ctx) {
302
306
  return request('/intel/quota', 'quota');
303
307
  }
304
308
  arity(ctx, 3, 'intel <ip|asn|cidr|ja3|ja4> <query>');
305
- const type = z.enum(['ip', 'asn', 'cidr', 'ja3', 'ja4']).parse(command);
306
- const q = z.string().trim().min(1).max(512).parse(ctx.rest[2]);
309
+ const type = oneOf('Lookup type', ['ip', 'asn', 'cidr', 'ja3', 'ja4'], command);
310
+ const q = parseArgument(z.string().trim().min(1).max(512), ctx.rest[2], 'The intel query must be 1-512 characters.');
307
311
  return request('/intel/lookup', 'intel', { q, type });
308
312
  }
309
- throw new Error(`Unknown security command: ${group}. Run \`vigilbase security --help\`.`);
313
+ throw unknownSubcommand('security', group);
310
314
  }
311
315
  function cell(value) {
312
316
  // Security evidence may contain attacker-controlled text; keep each terminal row literal.
@@ -399,4 +403,4 @@ export const handleSecurity = async (ctx) => {
399
403
  display(ctx, request, payload);
400
404
  return ok();
401
405
  };
402
- export const SECURITY_HELP = "vigilbase security \u2014 managed security in Vigilbase One\n\n [overview]\n incidents [list] --status --severity --attack-category\n --order-by --order-direction --limit --offset\n incidents show <id>\n incidents timeline <id> --limit --before\n incidents fabric <id> --dataset --min-weight --limit\n incidents share-links <id>\n incidents update <id> <status> --confirm [--note]\n alerts [list] --limit --asset-id --start\n alerts show <id>\n logs --limit --asset-id --provider --dataset --search\n --start --end --sort --frs-min --cursor\n audit --limit --cursor --action-prefix --outcome\n assets\n assets <allowlist|blocklist|remediation|autonomy|datasets> <asset-id>\n assets graph <asset-id> --dataset --hours --min-weight --limit\n fabric default-asset\n detections --asset-id <asset-id>\n providers\n connections [list]\n connections show <id>\n connections <available-assets|remediation> <id>\n connections <logpush-status|worker-stats> <connection-id> <job-id>\n analytics [overview] --metrics + time-window flags\n analytics <security-analysis|bot-activity|top-attacker-ips|bot-asns|dns-security|page-shield>\n analytics provider-events --provider --dataset --start --end\n [--asset-id --search --limit]\n zero-trust <device-health|app-tests|access|sessions|device-posture>\n zero-trust gateway <dns|http|network>\n cloudflare-account [inventory]\n cloudflare-account <audit|ingestion|posture>\n notifications [list] --limit\n notifications <settings|preferences|slack-channels>\n reports [list] --limit --offset\n reports show <id>\n reports settings\n intel quota\n intel <ip|asn|cidr|ja3|ja4> <query>\n settings <lifecycle-emails|broadcast-emails|applications>\n\nTime-window flags: --hours (1\u2013720, default 24) or --start/--end ISO timestamps;\noptional --asset-id. Apply to analytics (except provider-events), zero-trust,\nand cloudflare-account audit/ingestion/posture. Inventory has no window flags.\nLimits: list incidents/reports/audit 1\u2013100 (default 50); alerts/notifications\n1\u2013100 (default 100); timeline 1\u2013200 (default 100); logs 1\u20131000 (default 100);\nprovider-events 1\u2013500 (default 200); graph 1\u20131000 (default 200).\nOffsets: 0\u20131000000. Graph min-weight: 1\u20131000000 (default 1).\nAll: --json, --timeout <ms>, --customer-id <One customer UUID> (internal View-as).\nAll operations read data except confirmed incident status updates.\nUpdate status: acknowledged, mitigating, resolved, false_positive.\nSee SECURITY.md for every endpoint, supported value, alias, and pagination contract.\n";
406
+ export const SECURITY_HELP = "vigilbase security \u2014 managed security in Vigilbase One\n\n [overview]\n incidents [list] --status --severity --attack-category\n --order-by --order-direction\n --limit --offset\n incidents show <id>\n incidents timeline <id> --limit --before\n incidents fabric <id> --dataset --min-weight --limit\n incidents share-links <id>\n incidents update <id> <status> --confirm [--note]\n alerts [list] --limit --asset-id --start\n alerts show <id>\n logs --start --end --limit --cursor --sort\n --asset-id --provider --dataset\n --search --frs-min\n audit --limit --cursor\n --action-prefix --outcome\n assets\n assets <allowlist|blocklist|remediation|autonomy|datasets> <asset-id>\n assets graph <asset-id> --dataset --hours --min-weight --limit\n fabric default-asset\n detections --asset-id <asset-id>\n providers\n connections [list]\n connections show <id>\n connections <available-assets|remediation> <id>\n connections <logpush-status|worker-stats> <connection-id> <job-id>\n analytics [overview] --metrics + time-window flags\n analytics <security-analysis|bot-activity|top-attacker-ips>\n analytics <bot-asns|dns-security|page-shield>\n analytics provider-events --provider --dataset --start --end\n [--asset-id --search --limit]\n zero-trust <device-health|app-tests|access|sessions|device-posture>\n zero-trust gateway <dns|http|network>\n cloudflare-account [inventory]\n cloudflare-account <audit|ingestion|posture>\n notifications [list] --limit\n notifications <settings|preferences|slack-channels>\n reports [list] --limit --offset\n reports show <id>\n reports settings\n intel quota\n intel <ip|asn|cidr|ja3|ja4> <query>\n settings <lifecycle-emails|broadcast-emails|applications>\n\nTime-window flags: --hours (1\u2013720, default 24) or --start/--end ISO timestamps;\noptional --asset-id. Apply to analytics (except provider-events), zero-trust,\nand cloudflare-account audit/ingestion/posture. Inventory has no window flags.\nLimits: list incidents/reports/audit 1\u2013100 (default 50); alerts/notifications\n1\u2013100 (default 100); timeline 1\u2013200 (default 100); logs 1\u20131000 (default 100);\nprovider-events 1\u2013500 (default 200); graph 1\u20131000 (default 200).\nOffsets: 0\u20131000000. Graph min-weight: 1\u20131000000 (default 1).\nAll: --json, --timeout <ms>.\nInternal View-as: --customer-id <One customer UUID>.\nAll operations read data except confirmed incident status updates.\nUpdate status: acknowledged, mitigating, resolved, false_positive.\nSee SECURITY.md for endpoints, supported values, aliases and pagination.\n";
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { limitValue, validateArguments } from './legacy-validation.js';
2
+ import { limitValue, parseArgument, unknownSubcommand, validateArguments } from './legacy-validation.js';
3
3
  import { asArray, asRecord, ok, outputJson, requireOneClient, withSpinner, } from './helpers.js';
4
4
  export const handleWatchdog = async (ctx) => {
5
5
  const subcommand = (ctx.rest[0] ?? 'list').toLowerCase();
@@ -7,12 +7,12 @@ export const handleWatchdog = async (ctx) => {
7
7
  if (subcommand === 'scans' || subcommand === 'scan') {
8
8
  validateArguments(ctx, 1, ['target-id', 'limit']);
9
9
  limit = limitValue(ctx.values.limit, 20, 100);
10
- targetId = z.string().uuid().optional().parse(ctx.values['target-id']);
10
+ targetId = parseArgument(z.string().uuid().optional(), ctx.values['target-id'], '--target-id must be a UUID. Copy the full ID from its list command.');
11
11
  }
12
12
  else if (['list', 'targets', 'status'].includes(subcommand))
13
13
  validateArguments(ctx, ctx.rest[0] === undefined ? 0 : 1);
14
14
  else
15
- throw new Error('Usage: vigilbase watchdog [list|scans]');
15
+ throw unknownSubcommand('watchdog', ctx.rest[0] ?? '');
16
16
  const one = await requireOneClient(ctx);
17
17
  if (subcommand === 'list' || subcommand === 'targets' || subcommand === 'status') {
18
18
  const payload = await withSpinner(ctx, 'Loading watchdog targets', () => one.getWatchdogTargets());
@@ -29,7 +29,7 @@ export const handleWatchdog = async (ctx) => {
29
29
  const target = asRecord(raw);
30
30
  if (!target)
31
31
  continue;
32
- ctx.writer.stdout(`${String(target.domain).padEnd(28)} ${String(target.uptimeStatus || '-')}${typeof target.uptime24h === 'number'
32
+ ctx.writer.stdout(`${String(target.id ?? '-')} ${String(target.domain).padEnd(28)} ${String(target.uptimeStatus || '-')}${typeof target.uptime24h === 'number'
33
33
  ? ` 24h=${Math.round(target.uptime24h * 100) / 100}`
34
34
  : ''}`);
35
35
  }
@@ -53,7 +53,7 @@ export const handleWatchdog = async (ctx) => {
53
53
  const scan = asRecord(raw);
54
54
  if (!scan)
55
55
  continue;
56
- ctx.writer.stdout(`${String(scan.id).slice(0, 8)} ${String(scan.status || '-').padEnd(10)} ${String(scan.scanType || '-')} ${String(scan.createdAt || '')}`);
56
+ ctx.writer.stdout(`${String(scan.id)} ${String(scan.status || '-').padEnd(10)} ${String(scan.scanType || '-')} ${String(scan.createdAt || '')}`);
57
57
  }
58
58
  return ok();
59
59
  }
@@ -2,27 +2,21 @@
2
2
  * Compact Vigilbase flare mark + wordmark for human CLI surfaces.
3
3
  * Static strings only — no I/O, no deps, skipped entirely in --json mode.
4
4
  */
5
- /**
6
- * Compact square Vigilbase flare (inverted V).
7
- * Left wing (full block) = terminal default fg; right wing (medium shade) = brand yellow.
8
- */
9
- /** Compact flare used on help/version (keeps the first viewport short). */
5
+ /** Compact square Vigilbase flare (inverted V), used on help/version (keeps the first viewport short). */
10
6
  export declare const VIGILBASE_ASCII_MARK: string;
11
7
  export type BannerOptions = {
12
8
  /** Include version line under the mark. */
13
9
  version?: string;
14
10
  /** One-line tagline. */
15
11
  tagline?: string;
16
- /** Force ANSI on/off. Default: auto from TTY + env. */
17
- useColor?: boolean;
18
12
  };
13
+ /** Whether the checkup terminal UI (tui.ts) may colour this stream. */
19
14
  export declare function shouldUseColor(stream: {
20
15
  isTTY?: boolean;
21
16
  } | undefined, env?: NodeJS.ProcessEnv): boolean;
22
17
  export declare function shouldShowBanner(mode?: 'text' | 'json', env?: NodeJS.ProcessEnv): boolean;
23
18
  /**
24
- * Banner composition (flare + wordmark + tagline).
19
+ * Banner composition (flare + wordmark + tagline). Plain text: the writer strips terminal
20
+ * sequences from every line, so the banner carries no colour.
25
21
  */
26
- export declare function renderBanner(options?: BannerOptions, stream?: {
27
- isTTY?: boolean;
28
- } | undefined, env?: NodeJS.ProcessEnv): string;
22
+ export declare function renderBanner(options?: BannerOptions): string;
@@ -2,11 +2,8 @@
2
2
  * Compact Vigilbase flare mark + wordmark for human CLI surfaces.
3
3
  * Static strings only — no I/O, no deps, skipped entirely in --json mode.
4
4
  */
5
- /**
6
- * Compact square Vigilbase flare (inverted V).
7
- * Left wing (full block) = terminal default fg; right wing (medium shade) = brand yellow.
8
- */
9
- /** Compact flare used on help/version (keeps the first viewport short). */
5
+ import { isCi } from './platform.js';
6
+ /** Compact square Vigilbase flare (inverted V), used on help/version (keeps the first viewport short). */
10
7
  export const VIGILBASE_ASCII_MARK = [
11
8
  '\u2588\u2588\u2588\u2588\u2588 \u2592\u2592\u2592',
12
9
  ' \u2588\u2588\u2588\u2588 \u2592\u2592\u2592\u2592',
@@ -14,13 +11,7 @@ export const VIGILBASE_ASCII_MARK = [
14
11
  ' \u2588\u2592\u2592\u2592\u2592 ',
15
12
  ' \u2592\u2592\u2592 ',
16
13
  ].join('\n');
17
- const ANSI = {
18
- reset: '\u001b[0m',
19
- dim: '\u001b[2m',
20
- bold: '\u001b[1m',
21
- /** Brand yellow (#f9e75e / #FFE74D family) — 256-color approximation. */
22
- brand: '\u001b[38;5;221m',
23
- };
14
+ /** Whether the checkup terminal UI (tui.ts) may colour this stream. */
24
15
  export function shouldUseColor(stream, env = process.env) {
25
16
  if (env.NO_COLOR != null && env.NO_COLOR !== '')
26
17
  return false;
@@ -28,7 +19,7 @@ export function shouldUseColor(stream, env = process.env) {
28
19
  return false;
29
20
  if (env.FORCE_COLOR && env.FORCE_COLOR !== '0')
30
21
  return true;
31
- if (env.CI === '1' || env.CI === 'true')
22
+ if (isCi(env))
32
23
  return false;
33
24
  return Boolean(stream?.isTTY);
34
25
  }
@@ -39,29 +30,18 @@ export function shouldShowBanner(mode = 'text', env = process.env) {
39
30
  return false;
40
31
  return true;
41
32
  }
42
- function paintMarkLine(line, useColor) {
43
- if (!useColor)
44
- return line;
45
- // Left wing stays terminal default fg; right wing is brand yellow.
46
- return line.replace(/\u2592+/g, (run) => `${ANSI.brand}${run}${ANSI.reset}`);
47
- }
48
33
  /**
49
- * Banner composition (flare + wordmark + tagline).
34
+ * Banner composition (flare + wordmark + tagline). Plain text: the writer strips terminal
35
+ * sequences from every line, so the banner carries no colour.
50
36
  */
51
- export function renderBanner(options = {}, stream = process.stdout, env = process.env) {
52
- const useColor = options.useColor ?? shouldUseColor(stream, env);
37
+ export function renderBanner(options = {}) {
53
38
  const tagline = options.tagline ?? 'one \u00b7 tools';
54
39
  const version = options.version ? ` \u00b7 v${options.version}` : '';
55
- const mark = VIGILBASE_ASCII_MARK.split('\n');
56
- const brand = (text) => useColor ? `${ANSI.brand}${ANSI.bold}${text}${ANSI.reset}` : text;
57
- const dim = (text) => useColor ? `${ANSI.dim}${text}${ANSI.reset}` : text;
58
- const painted = mark.map((line) => paintMarkLine(line, useColor));
59
- const lines = painted.map((line, index) => {
40
+ return VIGILBASE_ASCII_MARK.split('\n').map((line, index) => {
60
41
  if (index === 0)
61
- return `${line} ${brand('vigilbase')}`;
42
+ return `${line} vigilbase`;
62
43
  if (index === 1)
63
- return `${line} ${dim(`${tagline}${version}`)}`;
44
+ return `${line} ${tagline}${version}`;
64
45
  return line;
65
- });
66
- return lines.join('\n');
46
+ }).join('\n');
67
47
  }