@uptimerobot/cli 0.1.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 (108) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +179 -0
  3. package/bin/run.js +43 -0
  4. package/dist/api/client.js +95 -0
  5. package/dist/api/errors.js +26 -0
  6. package/dist/commands/alert-contacts/create.js +10 -0
  7. package/dist/commands/alert-contacts/delete.js +10 -0
  8. package/dist/commands/alert-contacts/get.js +10 -0
  9. package/dist/commands/alert-contacts/list.js +10 -0
  10. package/dist/commands/alert-contacts/update.js +10 -0
  11. package/dist/commands/auth/login.js +51 -0
  12. package/dist/commands/auth/logout.js +33 -0
  13. package/dist/commands/auth/status.js +38 -0
  14. package/dist/commands/auth/whoami.js +6 -0
  15. package/dist/commands/incidents/activity-log.js +10 -0
  16. package/dist/commands/incidents/alerts.js +10 -0
  17. package/dist/commands/incidents/comments/create.js +10 -0
  18. package/dist/commands/incidents/comments/delete.js +10 -0
  19. package/dist/commands/incidents/comments/list.js +10 -0
  20. package/dist/commands/incidents/comments/update.js +10 -0
  21. package/dist/commands/incidents/get.js +10 -0
  22. package/dist/commands/incidents/list.js +10 -0
  23. package/dist/commands/integrations/create.js +10 -0
  24. package/dist/commands/integrations/delete.js +10 -0
  25. package/dist/commands/integrations/get.js +10 -0
  26. package/dist/commands/integrations/list.js +10 -0
  27. package/dist/commands/integrations/update.js +10 -0
  28. package/dist/commands/maintenance-windows/create.js +10 -0
  29. package/dist/commands/maintenance-windows/delete.js +10 -0
  30. package/dist/commands/maintenance-windows/get.js +10 -0
  31. package/dist/commands/maintenance-windows/list.js +10 -0
  32. package/dist/commands/maintenance-windows/update.js +10 -0
  33. package/dist/commands/monitor-groups/create.js +10 -0
  34. package/dist/commands/monitor-groups/delete.js +10 -0
  35. package/dist/commands/monitor-groups/get.js +10 -0
  36. package/dist/commands/monitor-groups/list.js +10 -0
  37. package/dist/commands/monitor-groups/update.js +10 -0
  38. package/dist/commands/monitors/bulk/pause.js +10 -0
  39. package/dist/commands/monitors/bulk/start.js +10 -0
  40. package/dist/commands/monitors/bulk/update.js +10 -0
  41. package/dist/commands/monitors/create/api.js +10 -0
  42. package/dist/commands/monitors/create/dns.js +10 -0
  43. package/dist/commands/monitors/create/heartbeat.js +10 -0
  44. package/dist/commands/monitors/create/http.js +10 -0
  45. package/dist/commands/monitors/create/keyword.js +10 -0
  46. package/dist/commands/monitors/create/ping.js +10 -0
  47. package/dist/commands/monitors/create/port.js +10 -0
  48. package/dist/commands/monitors/create/udp.js +10 -0
  49. package/dist/commands/monitors/create/visual-comparison.js +10 -0
  50. package/dist/commands/monitors/delete.js +10 -0
  51. package/dist/commands/monitors/get.js +10 -0
  52. package/dist/commands/monitors/list.js +10 -0
  53. package/dist/commands/monitors/pause.js +10 -0
  54. package/dist/commands/monitors/reset.js +10 -0
  55. package/dist/commands/monitors/schema.js +57 -0
  56. package/dist/commands/monitors/start.js +10 -0
  57. package/dist/commands/monitors/stats/response-time/all.js +10 -0
  58. package/dist/commands/monitors/stats/response-time.js +10 -0
  59. package/dist/commands/monitors/stats/uptime.js +10 -0
  60. package/dist/commands/monitors/update.js +10 -0
  61. package/dist/commands/monitors/uptime-stats.js +10 -0
  62. package/dist/commands/status-pages/announcements/create.js +10 -0
  63. package/dist/commands/status-pages/announcements/get.js +10 -0
  64. package/dist/commands/status-pages/announcements/list.js +10 -0
  65. package/dist/commands/status-pages/announcements/pin.js +10 -0
  66. package/dist/commands/status-pages/announcements/unpin.js +10 -0
  67. package/dist/commands/status-pages/announcements/update.js +10 -0
  68. package/dist/commands/status-pages/create.js +10 -0
  69. package/dist/commands/status-pages/delete.js +10 -0
  70. package/dist/commands/status-pages/get.js +10 -0
  71. package/dist/commands/status-pages/list.js +10 -0
  72. package/dist/commands/status-pages/update.js +10 -0
  73. package/dist/commands/tags/delete.js +10 -0
  74. package/dist/commands/tags/list.js +10 -0
  75. package/dist/commands/user/alert-contacts.js +10 -0
  76. package/dist/commands/user/all-alert-contacts.js +10 -0
  77. package/dist/commands/user/get.js +5 -0
  78. package/dist/commands/user/me.js +10 -0
  79. package/dist/generated/operations.js +7631 -0
  80. package/dist/lib/auth.js +60 -0
  81. package/dist/lib/base-command.js +21 -0
  82. package/dist/lib/cli-errors.js +80 -0
  83. package/dist/lib/command-suggestions.js +44 -0
  84. package/dist/lib/credential-store.js +152 -0
  85. package/dist/lib/flag-name.js +11 -0
  86. package/dist/lib/invocation.js +74 -0
  87. package/dist/lib/objects.js +34 -0
  88. package/dist/lib/operation-command.js +486 -0
  89. package/dist/lib/operation-parser-error.js +28 -0
  90. package/dist/lib/request-curation.js +76 -0
  91. package/dist/lib/request-example.js +82 -0
  92. package/dist/lib/request-preview.js +25 -0
  93. package/dist/lib/request.js +301 -0
  94. package/dist/lib/sensitive-fields.js +19 -0
  95. package/dist/lib/types.js +1 -0
  96. package/dist/output/columns.js +221 -0
  97. package/dist/output/display-width.js +34 -0
  98. package/dist/output/normalize-result.js +28 -0
  99. package/dist/output/pagination-notice.js +27 -0
  100. package/dist/output/renderer.js +145 -0
  101. package/dist/output/request-progress.js +15 -0
  102. package/dist/output/resolve-format.js +32 -0
  103. package/dist/output/style.js +57 -0
  104. package/dist/runtime/parse-input.js +35 -0
  105. package/dist/runtime/types.js +1 -0
  106. package/dist/runtime/validate-input.js +63 -0
  107. package/openapi/openapi.yaml +9034 -0
  108. package/package.json +135 -0
@@ -0,0 +1,60 @@
1
+ import { credentialStore } from './credential-store.js';
2
+ export const PRODUCTION_API_URL = 'https://api.uptimerobot.com/v3';
3
+ const VALIDATION_TIMEOUT_MS = 10_000;
4
+ export class AuthenticationError extends Error {
5
+ code;
6
+ exitCode;
7
+ constructor(code, message, exitCode) {
8
+ super(message);
9
+ this.code = code;
10
+ this.exitCode = exitCode;
11
+ }
12
+ }
13
+ export async function saveValidatedApiKey(apiKey, apiUrl, attribution, store = credentialStore, fetchImplementation = fetch) {
14
+ if (apiUrl !== PRODUCTION_API_URL) {
15
+ throw new AuthenticationError('AUTH_URL_RESTRICTED', 'Persistent login is only available for api.uptimerobot.com. Use UPTIMEROBOT_API_KEY or --api-key with a custom API URL.', 2);
16
+ }
17
+ let response;
18
+ try {
19
+ response = await fetchImplementation(new URL(`${apiUrl}/user/me`), {
20
+ headers: {
21
+ accept: 'application/json',
22
+ authorization: `Bearer ${apiKey}`,
23
+ 'user-agent': `uptimerobot-cli/${attribution.version} mode/${attribution.mode} environment/${attribution.environment}`,
24
+ 'x-uptimerobot-client': 'cli',
25
+ 'x-uptimerobot-execution-environment': attribution.environment,
26
+ 'x-uptimerobot-invocation-mode': attribution.mode,
27
+ },
28
+ signal: AbortSignal.timeout(VALIDATION_TIMEOUT_MS),
29
+ });
30
+ }
31
+ catch (error) {
32
+ throw new AuthenticationError('AUTH_INVALID', `Could not validate the API key: ${error instanceof Error ? error.message : String(error)}`, 1);
33
+ }
34
+ if (!response.ok) {
35
+ throw new AuthenticationError('AUTH_INVALID', `API key validation failed with HTTP ${response.status}.`, 4);
36
+ }
37
+ try {
38
+ return await store.setApiKey(apiKey);
39
+ }
40
+ catch {
41
+ throw new AuthenticationError('AUTH_STORAGE_UNAVAILABLE', 'No credential storage is available. Use UPTIMEROBOT_API_KEY for this environment.', 1);
42
+ }
43
+ }
44
+ export async function resolveApiKey(explicitApiKey, apiUrl, store = credentialStore) {
45
+ if (typeof explicitApiKey === 'string' && explicitApiKey.length > 0)
46
+ return explicitApiKey;
47
+ if (apiUrl !== PRODUCTION_API_URL) {
48
+ throw new AuthenticationError('AUTH_URL_RESTRICTED', 'Stored credentials are only used with api.uptimerobot.com. Pass --api-key or set UPTIMEROBOT_API_KEY for a custom API URL.', 2);
49
+ }
50
+ let stored;
51
+ try {
52
+ stored = await store.getApiKey();
53
+ }
54
+ catch {
55
+ throw new AuthenticationError('AUTH_STORAGE_UNAVAILABLE', 'Credential storage is unavailable. Set UPTIMEROBOT_API_KEY or pass --api-key.', 1);
56
+ }
57
+ if (stored)
58
+ return stored.apiKey;
59
+ throw new AuthenticationError('AUTH_REQUIRED', 'Run `uptimerobot auth login --api-key <key>`, set UPTIMEROBOT_API_KEY, or pass --api-key.', 2);
60
+ }
@@ -0,0 +1,21 @@
1
+ import { Command } from '@oclif/core';
2
+ import { CliFailure, errorEnvelope, exitCodeForError, machineOutputRequested, markErrorHandled, } from './cli-errors.js';
3
+ export class BaseCommand extends Command {
4
+ async catch(error) {
5
+ const failure = error instanceof CliFailure;
6
+ const machineOutput = machineOutputRequested(this.argv);
7
+ if (!failure && !machineOutput) {
8
+ await super.catch(error);
9
+ throw error;
10
+ }
11
+ this.logToStderr(failure && !machineOutput
12
+ ? `${error.cliError.message}${error.cliError.code ? ` (${error.cliError.code})` : ''}`
13
+ : JSON.stringify(errorEnvelope(error)));
14
+ process.exitCode = exitCodeForError(error);
15
+ markErrorHandled(error);
16
+ throw error;
17
+ }
18
+ fail(error, exitCode) {
19
+ throw new CliFailure(error, exitCode);
20
+ }
21
+ }
@@ -0,0 +1,80 @@
1
+ import { outputFormatFromEnvironment, requestedFormatFromArgv } from '../output/resolve-format.js';
2
+ import { agentEnvironmentDetected } from './invocation.js';
3
+ import { isRecord } from './objects.js';
4
+ const handledErrors = new WeakSet();
5
+ export class CliFailure extends Error {
6
+ cliError;
7
+ oclif;
8
+ constructor(error, exitCode) {
9
+ super(error.message);
10
+ this.cliError = error;
11
+ this.oclif = { exit: exitCode };
12
+ }
13
+ }
14
+ export function errorEnvelope(error) {
15
+ if (error instanceof CliFailure)
16
+ return { error: error.cliError };
17
+ const metadata = isRecord(error) ? error : {};
18
+ const exitCode = exitCodeForError(error);
19
+ const code = typeof metadata.code === 'string' && metadata.code !== 'EEXIT'
20
+ ? metadata.code
21
+ : exitCode === 2
22
+ ? 'INVALID_INPUT'
23
+ : 'CLI_ERROR';
24
+ const message = typeof metadata.message === 'string'
25
+ ? metadata.message
26
+ : typeof error === 'string'
27
+ ? error
28
+ : 'The command failed.';
29
+ return {
30
+ error: {
31
+ code,
32
+ ...(metadata.details !== undefined ? { details: metadata.details } : {}),
33
+ ...(typeof metadata.expected === 'string' ? { expected: metadata.expected } : {}),
34
+ message,
35
+ ...(typeof metadata.path === 'string' ? { path: metadata.path } : {}),
36
+ ...(typeof metadata.status === 'number' ? { status: metadata.status } : {}),
37
+ ...(Array.isArray(metadata.suggestions) &&
38
+ metadata.suggestions.every((suggestion) => typeof suggestion === 'string')
39
+ ? { suggestions: metadata.suggestions }
40
+ : {}),
41
+ },
42
+ };
43
+ }
44
+ export function exitCodeForError(error) {
45
+ if (!isRecord(error))
46
+ return 1;
47
+ const oclifExit = isRecord(error.oclif) ? error.oclif.exit : undefined;
48
+ if (typeof oclifExit === 'number')
49
+ return oclifExit;
50
+ return typeof error.exitCode === 'number' ? error.exitCode : 1;
51
+ }
52
+ export function machineOutputRequested(argv = process.argv.slice(2), env = process.env, stdoutIsTTY = process.stdout.isTTY === true) {
53
+ const separator = argv.indexOf('--');
54
+ const options = separator === -1 ? argv : argv.slice(0, separator);
55
+ if (options.includes('--json') || options.includes('--raw'))
56
+ return true;
57
+ const format = requestedFormatFromArgv(options);
58
+ if (format === 'json' || format === 'jsonl')
59
+ return true;
60
+ if (format === 'plain' || format === 'table')
61
+ return false;
62
+ const environmentFormat = outputFormatFromEnvironment(env);
63
+ if (environmentFormat !== undefined) {
64
+ if (environmentFormat === 'json' || environmentFormat === 'jsonl')
65
+ return true;
66
+ if (environmentFormat === 'plain' || environmentFormat === 'table')
67
+ return false;
68
+ }
69
+ if (options.includes('--agent'))
70
+ return true;
71
+ if (agentEnvironmentDetected(env))
72
+ return true;
73
+ return !stdoutIsTTY;
74
+ }
75
+ export function markErrorHandled(error) {
76
+ handledErrors.add(error);
77
+ }
78
+ export function wasErrorHandled(error) {
79
+ return isRecord(error) && handledErrors.has(error);
80
+ }
@@ -0,0 +1,44 @@
1
+ import { operations } from '../generated/operations.js';
2
+ /**
3
+ * Commands implemented by hand rather than generated from the OpenAPI
4
+ * contract. A test asserts that each entry maps to a compiled command file,
5
+ * so renames and removals break CI instead of silently dropping suggestions.
6
+ */
7
+ export const HANDWRITTEN_COMMANDS = [
8
+ 'auth login',
9
+ 'auth logout',
10
+ 'auth status',
11
+ 'auth whoami',
12
+ 'monitors schema',
13
+ 'user get',
14
+ ];
15
+ const commands = [
16
+ ...Object.keys(operations).map((commandId) => commandId.replaceAll(':', ' ')),
17
+ ...HANDWRITTEN_COMMANDS,
18
+ ];
19
+ export function commandSuggestion(argv) {
20
+ const firstFlag = argv.findIndex((value) => value.startsWith('-'));
21
+ const input = (firstFlag === -1 ? argv : argv.slice(0, firstFlag)).join(' ');
22
+ if (!input)
23
+ return undefined;
24
+ const ranked = commands
25
+ .map((command) => ({ command, distance: editDistance(input, command) }))
26
+ .sort((left, right) => left.distance - right.distance || left.command.localeCompare(right.command));
27
+ const best = ranked[0];
28
+ if (!best || best.distance > Math.max(2, Math.floor(input.length * 0.2)))
29
+ return undefined;
30
+ if (ranked[1]?.distance === best.distance)
31
+ return undefined;
32
+ return best.command;
33
+ }
34
+ function editDistance(left, right) {
35
+ let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
36
+ for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
37
+ const current = [leftIndex];
38
+ for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
39
+ current[rightIndex] = Math.min(current[rightIndex - 1] + 1, previous[rightIndex] + 1, previous[rightIndex - 1] + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1));
40
+ }
41
+ previous = current;
42
+ }
43
+ return previous[right.length];
44
+ }
@@ -0,0 +1,152 @@
1
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ const KEYRING_ACCOUNT = 'default:api-key';
5
+ const KEYRING_SERVICE = 'com.uptimerobot.cli';
6
+ const KEYRING_TIMEOUT_MS = 3000;
7
+ const CREDENTIALS_FILENAME = 'credentials.json';
8
+ /** The directory that holds the plaintext fallback credential. */
9
+ export function configDirectory(env = process.env) {
10
+ const override = env.UPTIMEROBOT_CONFIG_DIR;
11
+ if (override)
12
+ return override;
13
+ const xdgConfigHome = env.XDG_CONFIG_HOME;
14
+ return xdgConfigHome
15
+ ? join(xdgConfigHome, 'uptimerobot')
16
+ : join(homedir(), '.config', 'uptimerobot');
17
+ }
18
+ export function credentialsFilePath(env = process.env) {
19
+ return join(configDirectory(env), CREDENTIALS_FILENAME);
20
+ }
21
+ /**
22
+ * OS credential store (macOS Keychain, Windows Credential Manager, Linux
23
+ * Secret Service or kernel keyring). Keyring operations are time-boxed
24
+ * because a missing or locked D-Bus session can hang them on Linux.
25
+ */
26
+ class KeyringCredentialStore {
27
+ async deleteApiKey() {
28
+ return withKeyringTimeout((await keyringEntry()).deleteCredential());
29
+ }
30
+ async getApiKey() {
31
+ const apiKey = await withKeyringTimeout((await keyringEntry()).getPassword());
32
+ return apiKey ? { apiKey, backend: 'keyring' } : undefined;
33
+ }
34
+ async setApiKey(apiKey) {
35
+ const credential = await keyringEntry();
36
+ await withKeyringTimeout(credential.setPassword(apiKey));
37
+ if ((await withKeyringTimeout(credential.getPassword())) !== apiKey) {
38
+ throw new Error('Credential could not be read back after saving.');
39
+ }
40
+ return 'keyring';
41
+ }
42
+ }
43
+ /**
44
+ * Plaintext fallback for environments without an OS keyring
45
+ * (minimal/headless Linux, containers): a JSON file beneath the CLI config
46
+ * directory with directory 0700 and file 0600 permissions.
47
+ */
48
+ class FileCredentialStore {
49
+ async deleteApiKey() {
50
+ try {
51
+ await rm(credentialsFilePath());
52
+ return true;
53
+ }
54
+ catch (error) {
55
+ if (error.code === 'ENOENT')
56
+ return false;
57
+ throw error;
58
+ }
59
+ }
60
+ async getApiKey() {
61
+ let raw;
62
+ try {
63
+ raw = await readFile(credentialsFilePath(), 'utf8');
64
+ }
65
+ catch (error) {
66
+ if (error.code === 'ENOENT')
67
+ return undefined;
68
+ throw error;
69
+ }
70
+ try {
71
+ const parsed = JSON.parse(raw);
72
+ const apiKey = typeof parsed === 'object' && parsed !== null
73
+ ? parsed.apiKey
74
+ : undefined;
75
+ return typeof apiKey === 'string' && apiKey.length > 0
76
+ ? { apiKey, backend: 'file' }
77
+ : undefined;
78
+ }
79
+ catch {
80
+ return undefined;
81
+ }
82
+ }
83
+ async setApiKey(apiKey) {
84
+ const directory = configDirectory();
85
+ await mkdir(directory, { mode: 0o700, recursive: true });
86
+ await chmod(directory, 0o700).catch(() => { });
87
+ const path = credentialsFilePath();
88
+ const temporary = join(directory, `.${CREDENTIALS_FILENAME}.${process.pid}.tmp`);
89
+ await writeFile(temporary, JSON.stringify({ apiKey }), { mode: 0o600 });
90
+ await rename(temporary, path);
91
+ await chmod(path, 0o600).catch(() => { });
92
+ const stored = await this.getApiKey();
93
+ if (stored?.apiKey !== apiKey) {
94
+ throw new Error('Credential could not be read back after saving.');
95
+ }
96
+ return 'file';
97
+ }
98
+ }
99
+ /**
100
+ * Prefers the OS keyring and falls back to the plaintext config file when
101
+ * the keyring is unavailable. Reads prefer the keyring so a credential
102
+ * migrated there takes precedence over a stale file.
103
+ */
104
+ class FallbackCredentialStore {
105
+ keyring;
106
+ file;
107
+ constructor(keyring, file) {
108
+ this.keyring = keyring;
109
+ this.file = file;
110
+ }
111
+ async deleteApiKey() {
112
+ const [keyring, file] = await Promise.allSettled([
113
+ this.keyring.deleteApiKey(),
114
+ this.file.deleteApiKey(),
115
+ ]);
116
+ if (keyring.status === 'rejected' && file.status === 'rejected')
117
+ throw keyring.reason;
118
+ return ((keyring.status === 'fulfilled' && keyring.value) ||
119
+ (file.status === 'fulfilled' && file.value));
120
+ }
121
+ async getApiKey() {
122
+ try {
123
+ const stored = await this.keyring.getApiKey();
124
+ if (stored)
125
+ return stored;
126
+ }
127
+ catch {
128
+ // The keyring is unavailable; fall back to the config file.
129
+ }
130
+ return this.file.getApiKey();
131
+ }
132
+ async setApiKey(apiKey) {
133
+ try {
134
+ return await this.keyring.setApiKey(apiKey);
135
+ }
136
+ catch {
137
+ return this.file.setApiKey(apiKey);
138
+ }
139
+ }
140
+ }
141
+ export const credentialStore = new FallbackCredentialStore(new KeyringCredentialStore(), new FileCredentialStore());
142
+ async function keyringEntry() {
143
+ const { AsyncEntry } = await import('@napi-rs/keyring');
144
+ return new AsyncEntry(KEYRING_SERVICE, KEYRING_ACCOUNT);
145
+ }
146
+ function withKeyringTimeout(operation) {
147
+ let timer;
148
+ const timeout = new Promise((_resolve, reject) => {
149
+ timer = setTimeout(() => reject(new Error('The OS credential store did not respond in time.')), KEYRING_TIMEOUT_MS);
150
+ });
151
+ return Promise.race([operation.finally(() => clearTimeout(timer)), timeout]);
152
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Converts an OpenAPI parameter or field name into its CLI flag spelling.
3
+ * The generator (scripts/generate-openapi.ts) and the runtime must always
4
+ * agree on this mapping, so both import it from here.
5
+ */
6
+ export function flagName(value) {
7
+ return value
8
+ .replace(/_/g, '-')
9
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
10
+ .toLowerCase();
11
+ }
@@ -0,0 +1,74 @@
1
+ import { emitKeypressEvents } from 'node:readline';
2
+ import { createInterface } from 'node:readline/promises';
3
+ /** Environment variables set by known coding-agent runtimes. */
4
+ export const AGENT_ENVIRONMENT_VARIABLES = ['CLAUDECODE', 'CODEX_SANDBOX', 'CURSOR_AGENT'];
5
+ export function isCI() {
6
+ return /^(1|true|yes)$/i.test(process.env.CI ?? '');
7
+ }
8
+ /**
9
+ * The single source of truth for agent-driven execution. UPTIMEROBOT_AGENT is
10
+ * an explicit override in both directions; otherwise a known agent runtime's
11
+ * environment marker decides.
12
+ */
13
+ export function agentEnvironmentDetected(env = process.env) {
14
+ const override = env.UPTIMEROBOT_AGENT;
15
+ if (override !== undefined)
16
+ return /^(1|true|yes)$/i.test(override);
17
+ return AGENT_ENVIRONMENT_VARIABLES.some((name) => env[name]);
18
+ }
19
+ export function detectInvocationMode(agentFlag, env = process.env) {
20
+ return agentFlag || agentEnvironmentDetected(env) ? 'agent' : 'human';
21
+ }
22
+ export async function requestConfirmation(operation, mode) {
23
+ if (mode === 'agent' || !process.stdin.isTTY || !process.stdout.isTTY)
24
+ return false;
25
+ const reader = createInterface({ input: process.stdin, output: process.stderr });
26
+ try {
27
+ const answer = await reader.question(`${operation.summary}. Continue? [y/N] `);
28
+ return /^(y|yes)$/i.test(answer.trim());
29
+ }
30
+ finally {
31
+ reader.close();
32
+ }
33
+ }
34
+ /**
35
+ * Prompts for a secret on stderr with masked echo. Returns undefined when
36
+ * there is no interactive terminal, so callers can fail with guidance instead.
37
+ */
38
+ export async function promptSecret(question) {
39
+ if (!process.stdin.isTTY || !process.stderr.isTTY)
40
+ return undefined;
41
+ process.stderr.write(question);
42
+ return new Promise((resolve) => {
43
+ let secret = '';
44
+ const finish = (value, exitCode) => {
45
+ process.stdin.setRawMode(false);
46
+ process.stdin.removeListener('keypress', onKeypress);
47
+ process.stderr.write('\n');
48
+ if (exitCode !== undefined)
49
+ process.exit(exitCode);
50
+ resolve(value);
51
+ };
52
+ const onKeypress = (str, key) => {
53
+ if (key.ctrl && key.name === 'c')
54
+ return finish(undefined, 130);
55
+ if (key.name === 'return')
56
+ return finish(secret.trim() || undefined);
57
+ if (key.name === 'backspace') {
58
+ if (secret.length > 0) {
59
+ secret = [...secret].slice(0, -1).join('');
60
+ process.stderr.write('\b \b');
61
+ }
62
+ return;
63
+ }
64
+ if (str === '' || str < ' ')
65
+ return; // arrows, escape sequences, control keys
66
+ secret += str;
67
+ process.stderr.write('*'.repeat([...str].length));
68
+ };
69
+ emitKeypressEvents(process.stdin);
70
+ process.stdin.setRawMode(true);
71
+ process.stdin.resume();
72
+ process.stdin.on('keypress', onKeypress);
73
+ });
74
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Shared record and dotted-path helpers. Keep these behavior-compatible with
3
+ * the request compiler: path traversal treats arrays as values, not records.
4
+ */
5
+ /** Any non-null object, including arrays and class instances such as Error. */
6
+ export function isRecord(value) {
7
+ return typeof value === 'object' && value !== null;
8
+ }
9
+ /** A plain object: non-null and not an array. */
10
+ export function isPlainRecord(value) {
11
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
12
+ }
13
+ export function valueAtPath(target, path) {
14
+ let value = target;
15
+ for (const segment of path.split('.')) {
16
+ if (!isPlainRecord(value))
17
+ return undefined;
18
+ value = value[segment];
19
+ }
20
+ return value;
21
+ }
22
+ export function assignPath(target, path, value) {
23
+ const segments = path.split('.').filter(Boolean);
24
+ if (segments.length === 0)
25
+ throw new Error('A field path is required.');
26
+ let cursor = target;
27
+ for (const segment of segments.slice(0, -1)) {
28
+ const existing = cursor[segment];
29
+ if (!isPlainRecord(existing))
30
+ cursor[segment] = {};
31
+ cursor = cursor[segment];
32
+ }
33
+ cursor[segments.at(-1)] = value;
34
+ }