@roarkanalytics/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 (99) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +225 -0
  3. package/bin.d.ts +2 -0
  4. package/bin.js +14 -0
  5. package/commands.d.ts +5 -0
  6. package/commands.js +3553 -0
  7. package/completions.d.ts +1 -0
  8. package/completions.js +13 -0
  9. package/index.d.ts +6 -0
  10. package/index.js +26 -0
  11. package/man/roark-agent-create.1 +99 -0
  12. package/man/roark-agent-endpoint-create.1 +108 -0
  13. package/man/roark-agent-endpoint-get.1 +93 -0
  14. package/man/roark-agent-endpoint-list.1 +102 -0
  15. package/man/roark-agent-endpoint-update.1 +103 -0
  16. package/man/roark-agent-get.1 +93 -0
  17. package/man/roark-agent-list.1 +99 -0
  18. package/man/roark-agent-update.1 +100 -0
  19. package/man/roark-call-create.1 +150 -0
  20. package/man/roark-call-get.1 +93 -0
  21. package/man/roark-call-list.1 +111 -0
  22. package/man/roark-call-metric-list.1 +100 -0
  23. package/man/roark-call-sentiment-run-list.1 +93 -0
  24. package/man/roark-call-transcript-get.1 +97 -0
  25. package/man/roark-config-apply.1 +96 -0
  26. package/man/roark-config-diff.1 +96 -0
  27. package/man/roark-customer-flow-create.1 +97 -0
  28. package/man/roark-customer-flow-delete.1 +93 -0
  29. package/man/roark-customer-flow-edge-case-create.1 +112 -0
  30. package/man/roark-customer-flow-edge-case-delete.1 +96 -0
  31. package/man/roark-customer-flow-edge-case-promote.1 +96 -0
  32. package/man/roark-customer-flow-edge-case-update.1 +118 -0
  33. package/man/roark-customer-flow-get.1 +93 -0
  34. package/man/roark-customer-flow-graph-replace.1 +100 -0
  35. package/man/roark-customer-flow-happy-path-update.1 +115 -0
  36. package/man/roark-customer-flow-list.1 +105 -0
  37. package/man/roark-customer-flow-update.1 +109 -0
  38. package/man/roark-health.1 +89 -0
  39. package/man/roark-http-request-definition-create.1 +108 -0
  40. package/man/roark-http-request-definition-get.1 +93 -0
  41. package/man/roark-http-request-definition-list.1 +96 -0
  42. package/man/roark-http-request-definition-update.1 +109 -0
  43. package/man/roark-metric-collection-job-create.1 +99 -0
  44. package/man/roark-metric-collection-job-get.1 +93 -0
  45. package/man/roark-metric-collection-job-list.1 +99 -0
  46. package/man/roark-metric-definition-create.1 +100 -0
  47. package/man/roark-metric-definition-list.1 +89 -0
  48. package/man/roark-metric-policy-create.1 +105 -0
  49. package/man/roark-metric-policy-delete.1 +93 -0
  50. package/man/roark-metric-policy-get.1 +93 -0
  51. package/man/roark-metric-policy-list.1 +99 -0
  52. package/man/roark-metric-policy-update.1 +106 -0
  53. package/man/roark-simulation-environment-get.1 +93 -0
  54. package/man/roark-simulation-environment-list.1 +96 -0
  55. package/man/roark-simulation-job-get.1 +93 -0
  56. package/man/roark-simulation-job-lookup.1 +96 -0
  57. package/man/roark-simulation-persona-create.1 +156 -0
  58. package/man/roark-simulation-persona-get.1 +93 -0
  59. package/man/roark-simulation-persona-list.1 +99 -0
  60. package/man/roark-simulation-persona-update.1 +160 -0
  61. package/man/roark-simulation-plan-create.1 +138 -0
  62. package/man/roark-simulation-plan-delete.1 +93 -0
  63. package/man/roark-simulation-plan-get.1 +93 -0
  64. package/man/roark-simulation-plan-job-get.1 +93 -0
  65. package/man/roark-simulation-plan-job-list.1 +108 -0
  66. package/man/roark-simulation-plan-job-start.1 +97 -0
  67. package/man/roark-simulation-plan-list.1 +102 -0
  68. package/man/roark-simulation-plan-update.1 +142 -0
  69. package/man/roark-simulation-run.1 +97 -0
  70. package/man/roark-webhook-create.1 +102 -0
  71. package/man/roark-webhook-delete.1 +93 -0
  72. package/man/roark-webhook-get.1 +93 -0
  73. package/man/roark-webhook-list.1 +96 -0
  74. package/man/roark.1 +287 -0
  75. package/package.json +102 -0
  76. package/runtime/commands/api.d.ts +26 -0
  77. package/runtime/commands/api.js +85 -0
  78. package/runtime/commands/auth.d.ts +9 -0
  79. package/runtime/commands/auth.js +111 -0
  80. package/runtime/commands/completion.d.ts +9 -0
  81. package/runtime/commands/completion.js +42 -0
  82. package/runtime/commands/config.d.ts +15 -0
  83. package/runtime/commands/config.js +125 -0
  84. package/runtime/config.d.ts +65 -0
  85. package/runtime/config.js +163 -0
  86. package/runtime/confirm.d.ts +16 -0
  87. package/runtime/confirm.js +34 -0
  88. package/runtime/errors.d.ts +45 -0
  89. package/runtime/errors.js +91 -0
  90. package/runtime/input.d.ts +52 -0
  91. package/runtime/input.js +211 -0
  92. package/runtime/output.d.ts +41 -0
  93. package/runtime/output.js +90 -0
  94. package/runtime/program.d.ts +45 -0
  95. package/runtime/program.js +305 -0
  96. package/runtime/types.d.ts +53 -0
  97. package/runtime/types.js +10 -0
  98. package/version.d.ts +1 -0
  99. package/version.js +8 -0
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ /**
3
+ * `roark auth login | logout | status`
4
+ *
5
+ * The token is never accepted as a flag value: flags land in shell history and
6
+ * in the process table, where a bearer token has no business being. It is read
7
+ * from a prompt with echo off, or from stdin so CI can pipe it in.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.registerAuthCommands = void 0;
11
+ const commander_1 = require("commander");
12
+ const node_readline_1 = require("node:readline");
13
+ const errors_1 = require("../errors");
14
+ const config_1 = require("../config");
15
+ const output_1 = require("../output");
16
+ const confirm_1 = require("../confirm");
17
+ /** Reads a secret without echoing it, so it is not left on screen or in a scrollback. */
18
+ const promptSecret = async (prompt) => {
19
+ const input = process.stdin;
20
+ const output = process.stderr;
21
+ const rl = (0, node_readline_1.createInterface)({ input, output, terminal: true });
22
+ const onData = (chunk) => {
23
+ // Redraw the prompt without the typed characters.
24
+ const text = chunk.toString();
25
+ if (text.includes('\n') || text.includes('\r'))
26
+ return;
27
+ output.write(`${prompt}`);
28
+ };
29
+ output.write(prompt);
30
+ input.on('data', onData);
31
+ try {
32
+ return await new Promise((resolve) => {
33
+ rl.question('', (answer) => resolve(answer));
34
+ });
35
+ }
36
+ finally {
37
+ input.off('data', onData);
38
+ rl.close();
39
+ output.write('\n');
40
+ }
41
+ };
42
+ const readToken = async () => {
43
+ if (!(0, confirm_1.isInteractive)()) {
44
+ const piped = await new Promise((resolve, reject) => {
45
+ let buffer = '';
46
+ process.stdin.setEncoding('utf8');
47
+ process.stdin.on('data', (chunk) => (buffer += chunk));
48
+ process.stdin.on('end', () => resolve(buffer));
49
+ process.stdin.on('error', reject);
50
+ });
51
+ const token = piped.trim();
52
+ if (token.length === 0) {
53
+ throw new errors_1.UsageError('No token on stdin. Pipe one in, or run this in a terminal.');
54
+ }
55
+ return token;
56
+ }
57
+ const token = (await promptSecret('Bearer token: ')).trim();
58
+ if (token.length === 0)
59
+ throw new errors_1.UsageError('No token entered.');
60
+ return token;
61
+ };
62
+ const registerAuthCommands = (root, binaryName) => {
63
+ const auth = new commander_1.Command('auth').description('Manage the stored credential').showHelpAfterError();
64
+ auth.action(() => auth.outputHelp());
65
+ auth
66
+ .command('login')
67
+ .description('Store a bearer token for future commands')
68
+ .addHelpText('after', [
69
+ '',
70
+ 'The token is read from a hidden prompt, or from stdin when not a terminal:',
71
+ '',
72
+ ` ${binaryName} auth login`,
73
+ ` echo "$ROARK_API_BEARER_TOKEN" | ${binaryName} auth login`,
74
+ ].join('\n'))
75
+ .action(async () => {
76
+ const token = await readToken();
77
+ const path = (0, config_1.writeUserConfig)({ ...(0, config_1.readUserConfig)(), bearerToken: token });
78
+ const color = (0, output_1.supportsColor)(process.stderr);
79
+ (0, output_1.write)(`${(0, output_1.paint)('Saved', 'green', color)} ${(0, config_1.maskToken)(token)} to ${path}`, process.stderr);
80
+ });
81
+ auth
82
+ .command('logout')
83
+ .description('Delete the stored credential')
84
+ .action(() => {
85
+ const removed = (0, config_1.clearUserConfig)();
86
+ (0, output_1.write)(removed ? `Removed ${(0, config_1.userConfigPath)()}` : `Nothing to remove at ${(0, config_1.userConfigPath)()}`, process.stderr);
87
+ });
88
+ auth
89
+ .command('status')
90
+ .description('Show which credential would be used, and where it came from')
91
+ .action(() => {
92
+ const color = (0, output_1.supportsColor)(process.stderr);
93
+ const fromEnv = process.env['ROARK_API_BEARER_TOKEN'];
94
+ const stored = (0, config_1.readUserConfig)().bearerToken;
95
+ if (fromEnv) {
96
+ (0, output_1.write)(`${(0, output_1.paint)('Authenticated', 'green', color)} via ROARK_API_BEARER_TOKEN (${(0, config_1.maskToken)(fromEnv)})`, process.stderr);
97
+ if (stored) {
98
+ (0, output_1.write)((0, output_1.paint)(`A token is also stored in ${(0, config_1.userConfigPath)()}; the environment variable wins.`, 'dim', color), process.stderr);
99
+ }
100
+ return;
101
+ }
102
+ if (stored) {
103
+ (0, output_1.write)(`${(0, output_1.paint)('Authenticated', 'green', color)} via ${(0, config_1.userConfigPath)()} (${(0, config_1.maskToken)(stored)})`, process.stderr);
104
+ return;
105
+ }
106
+ (0, output_1.write)(`${(0, output_1.paint)('Not authenticated.', 'yellow', color)} Run \`${binaryName} auth login\`.`, process.stderr);
107
+ process.exitCode = 3;
108
+ });
109
+ root.addCommand(auth);
110
+ };
111
+ exports.registerAuthCommands = registerAuthCommands;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `roark completion <shell>`
3
+ *
4
+ * The scripts are generated from the same command table the program is built
5
+ * from, so they cannot describe a command that does not exist. Nothing is
6
+ * assembled at runtime and nothing is read from the live Commander tree.
7
+ */
8
+ import { Command } from 'commander';
9
+ export declare const registerCompletionCommand: (root: Command, binaryName: string, completions: Readonly<Record<string, string>>) => void;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ /**
3
+ * `roark completion <shell>`
4
+ *
5
+ * The scripts are generated from the same command table the program is built
6
+ * from, so they cannot describe a command that does not exist. Nothing is
7
+ * assembled at runtime and nothing is read from the live Commander tree.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.registerCompletionCommand = void 0;
11
+ const commander_1 = require("commander");
12
+ const errors_1 = require("../errors");
13
+ const output_1 = require("../output");
14
+ const LOADING_INSTRUCTIONS = {
15
+ bash: (binary) => ` eval "$(${binary} completion bash)" # or install into /etc/bash_completion.d`,
16
+ zsh: (binary) => ` eval "$(${binary} completion zsh)" # or write to a directory on $fpath`,
17
+ fish: (binary) => ` ${binary} completion fish | source # or write to ~/.config/fish/completions`,
18
+ powershell: (binary) => ` ${binary} completion powershell | Out-String | Invoke-Expression # or add to $PROFILE`,
19
+ };
20
+ const registerCompletionCommand = (root, binaryName, completions) => {
21
+ const shells = Object.keys(completions);
22
+ if (shells.length === 0)
23
+ return;
24
+ const help = shells
25
+ .map((shell) => LOADING_INSTRUCTIONS[shell]?.(binaryName))
26
+ .filter((line) => line !== undefined);
27
+ root.addCommand(new commander_1.Command('completion')
28
+ .description(`Print a shell completion script (${shells.join(', ')})`)
29
+ .argument('<shell>', `one of: ${shells.join(', ')}`)
30
+ .addHelpText('after', help.length > 0 ? `\nAdd one of these to your shell startup file:\n${help.join('\n')}` : '')
31
+ .action((shell) => {
32
+ // Own-property lookup only: `completions['constructor']` would otherwise
33
+ // resolve to something that is not a completion script.
34
+ const script = Object.prototype.hasOwnProperty.call(completions, shell) ? completions[shell] : undefined;
35
+ if (script === undefined) {
36
+ throw new errors_1.UsageError(`Unsupported shell '${shell}'. Supported: ${shells.join(', ')}`);
37
+ }
38
+ (0, output_1.write)(script);
39
+ })
40
+ .showHelpAfterError());
41
+ };
42
+ exports.registerCompletionCommand = registerCompletionCommand;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `roark config get | set | list | path`
3
+ *
4
+ * Only the non-secret settings are settable here. The token has its own command
5
+ * because it needs a hidden prompt, and `config set bearer-token <value>` would
6
+ * put it straight into shell history.
7
+ *
8
+ * The API has a `config` resource of its own - config as code - so `config` at
9
+ * the root may already exist by the time this runs, carrying the generated
10
+ * `diff` and `apply`. The two sets of subcommands do not overlap, so they share
11
+ * one command rather than either side being renamed; Commander refuses a second
12
+ * `config` at the root outright.
13
+ */
14
+ import { Command } from 'commander';
15
+ export declare const registerConfigCommands: (root: Command, binaryName: string) => void;
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ /**
3
+ * `roark config get | set | list | path`
4
+ *
5
+ * Only the non-secret settings are settable here. The token has its own command
6
+ * because it needs a hidden prompt, and `config set bearer-token <value>` would
7
+ * put it straight into shell history.
8
+ *
9
+ * The API has a `config` resource of its own - config as code - so `config` at
10
+ * the root may already exist by the time this runs, carrying the generated
11
+ * `diff` and `apply`. The two sets of subcommands do not overlap, so they share
12
+ * one command rather than either side being renamed; Commander refuses a second
13
+ * `config` at the root outright.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.registerConfigCommands = void 0;
17
+ const commander_1 = require("commander");
18
+ const errors_1 = require("../errors");
19
+ const config_1 = require("../config");
20
+ const output_1 = require("../output");
21
+ /** Settable keys, as typed, mapped onto the config field they write. */
22
+ const KEYS = {
23
+ 'base-url': 'baseURL',
24
+ timeout: 'timeout',
25
+ 'max-retries': 'maxRetries',
26
+ };
27
+ const numericKeys = new Set(['timeout', 'maxRetries']);
28
+ const keyFor = (name) => {
29
+ const key = KEYS[name];
30
+ if (!key) {
31
+ throw new errors_1.UsageError(`Unknown config key '${name}'. Known keys: ${Object.keys(KEYS).join(', ')}`);
32
+ }
33
+ return key;
34
+ };
35
+ /** The generated `config` group if it is already there, otherwise a new one. */
36
+ const configCommand = (root) => {
37
+ const description = 'Read and write CLI settings, and apply config as code';
38
+ const existing = root.commands.find((child) => child.name() === 'config');
39
+ if (existing)
40
+ return existing.description(description);
41
+ const created = new commander_1.Command('config').description(description).showHelpAfterError();
42
+ created.action(() => created.outputHelp());
43
+ root.addCommand(created);
44
+ return created;
45
+ };
46
+ const registerConfigCommands = (root, binaryName) => {
47
+ const config = configCommand(root);
48
+ config
49
+ .command('list')
50
+ .description('Show the effective settings and where each file lives')
51
+ .option('--json', 'output as JSON')
52
+ .action((options) => {
53
+ const effective = (0, config_1.loadConfig)();
54
+ const shown = {
55
+ ...effective,
56
+ ...(effective.bearerToken === undefined ? {} : { bearerToken: (0, config_1.maskToken)(effective.bearerToken) }),
57
+ };
58
+ if (options.json === true) {
59
+ (0, output_1.write)((0, output_1.render)(shown, { format: 'json', color: (0, output_1.supportsColor)() }));
60
+ return;
61
+ }
62
+ for (const [name, key] of Object.entries(KEYS)) {
63
+ (0, output_1.write)(`${name} = ${shown[key] ?? '(unset)'}`);
64
+ }
65
+ (0, output_1.write)(`token = ${shown.bearerToken ?? '(unset)'}`);
66
+ (0, output_1.write)('');
67
+ (0, output_1.write)(`user config ${(0, config_1.userConfigPath)()}`);
68
+ (0, output_1.write)(`project config ${(0, config_1.projectConfigPath)() ?? '(none)'}`);
69
+ });
70
+ config
71
+ .command('get')
72
+ .description('Print one effective setting')
73
+ .argument('<key>', `one of: ${Object.keys(KEYS).join(', ')}`)
74
+ .action((name) => {
75
+ const value = (0, config_1.loadConfig)()[keyFor(name)];
76
+ if (value === undefined)
77
+ process.exitCode = 1;
78
+ else
79
+ (0, output_1.write)(String(value));
80
+ });
81
+ config
82
+ .command('set')
83
+ .description('Write one setting to the user config file')
84
+ .argument('<key>', `one of: ${Object.keys(KEYS).join(', ')}`)
85
+ .argument('<value>')
86
+ .action((name, value) => {
87
+ const key = keyFor(name);
88
+ let parsed = value;
89
+ if (numericKeys.has(key)) {
90
+ parsed = Number(value);
91
+ if (!Number.isFinite(parsed))
92
+ throw new errors_1.UsageError(`${name} expects a number, got '${value}'`);
93
+ }
94
+ const path = (0, config_1.writeUserConfig)({ ...(0, config_1.readUserConfig)(), [key]: parsed });
95
+ (0, output_1.write)(`Set ${name} in ${path}`, process.stderr);
96
+ });
97
+ config
98
+ .command('unset')
99
+ .description('Remove one setting from the user config file')
100
+ .argument('<key>', `one of: ${Object.keys(KEYS).join(', ')}`)
101
+ .action((name) => {
102
+ const key = keyFor(name);
103
+ const current = (0, config_1.readUserConfig)();
104
+ delete current[key];
105
+ const path = (0, config_1.writeUserConfig)(current);
106
+ (0, output_1.write)(`Unset ${name} in ${path}`, process.stderr);
107
+ });
108
+ config
109
+ .command('path')
110
+ .description('Print the config file paths')
111
+ .action(() => {
112
+ (0, output_1.write)((0, config_1.userConfigPath)());
113
+ const project = (0, config_1.projectConfigPath)();
114
+ if (project)
115
+ (0, output_1.write)(project);
116
+ });
117
+ config.addHelpText('after', [
118
+ '',
119
+ 'Precedence, highest first: flag, environment, project .roark.json, user config.',
120
+ '',
121
+ ` ${binaryName} config set base-url https://api.roark.ai`,
122
+ ` ${binaryName} auth login # the token is stored separately`,
123
+ ].join('\n'));
124
+ };
125
+ exports.registerConfigCommands = registerConfigCommands;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Configuration resolution.
3
+ *
4
+ * Precedence, highest first (clig.dev): flag, environment, project config, user
5
+ * config. A project file lets a repo pin a base URL without every contributor
6
+ * exporting it; the user file is where `auth login` writes the token.
7
+ *
8
+ * A project file is untrusted input. It is found by walking up from the cwd, so
9
+ * it arrives with a `git clone` rather than being something the person running
10
+ * the command wrote, which is why `resolveConfig` records where every value came
11
+ * from: sending a credential to a host that a checked-in file chose is a
12
+ * decision the caller has to make deliberately. See `unsafeBaseUrlRedirect`.
13
+ */
14
+ export interface CliConfig {
15
+ bearerToken?: string;
16
+ baseURL?: string;
17
+ timeout?: number;
18
+ maxRetries?: number;
19
+ }
20
+ export declare const PROJECT_FILE = ".roark.json";
21
+ export declare const userConfigPath: () => string;
22
+ /** Walks up from the cwd, so a command run in a subdirectory still sees the repo's config. */
23
+ export declare const projectConfigPath: (from?: string) => string | undefined;
24
+ /** Which layer a value came from, lowest precedence first. */
25
+ export type ConfigSource = 'user' | 'project' | 'environment' | 'flag';
26
+ export interface ResolvedConfig {
27
+ config: CliConfig;
28
+ /** The layer that supplied each key that ended up set. */
29
+ sources: Partial<Record<keyof CliConfig, ConfigSource>>;
30
+ /** The project file that took part in the merge, if one was found. */
31
+ projectFile?: string;
32
+ }
33
+ export declare const resolveConfig: (overrides?: CliConfig, cwd?: string) => ResolvedConfig;
34
+ export declare const loadConfig: (overrides?: CliConfig) => CliConfig;
35
+ export interface BaseUrlRedirect {
36
+ baseURL: string;
37
+ projectFile: string;
38
+ credentialFrom: ConfigSource;
39
+ }
40
+ /**
41
+ * The one combination that leaks a secret: a project file chose the host, and
42
+ * the credential heading for it is one the caller did not choose per-invocation.
43
+ *
44
+ * `git clone` a repository with a `.roark.json` naming someone else's base URL,
45
+ * run any command anywhere inside it, and the token `auth login` stored would be
46
+ * posted to that host - the SDK attaches `Authorization` to whatever base URL it
47
+ * is given, and never scopes a credential to a host. Same shape as a malicious
48
+ * `.npmrc` pointing `registry` at an attacker.
49
+ *
50
+ * A token that came from the same project file is not a leak: the file supplied
51
+ * both halves and nothing of the caller's travels. Neither is one passed as
52
+ * `--token` for this run, which is an explicit act naming both.
53
+ */
54
+ export declare const unsafeBaseUrlRedirect: (resolved: ResolvedConfig) => BaseUrlRedirect | undefined;
55
+ /** `ROARK_ALLOW_PROJECT_BASE_URL`, for scripts that cannot pass the flag. */
56
+ export declare const allowProjectBaseUrlFromEnvironment: () => boolean;
57
+ /**
58
+ * Written 0600: it holds a bearer token, and the default 0644 would expose it to
59
+ * every account on a shared machine.
60
+ */
61
+ export declare const writeUserConfig: (config: CliConfig) => string;
62
+ export declare const readUserConfig: () => CliConfig;
63
+ export declare const clearUserConfig: () => boolean;
64
+ /** `roark-...-abcd` -> `roar...abcd`: enough to identify, not enough to use. */
65
+ export declare const maskToken: (token: string) => string;
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ /**
3
+ * Configuration resolution.
4
+ *
5
+ * Precedence, highest first (clig.dev): flag, environment, project config, user
6
+ * config. A project file lets a repo pin a base URL without every contributor
7
+ * exporting it; the user file is where `auth login` writes the token.
8
+ *
9
+ * A project file is untrusted input. It is found by walking up from the cwd, so
10
+ * it arrives with a `git clone` rather than being something the person running
11
+ * the command wrote, which is why `resolveConfig` records where every value came
12
+ * from: sending a credential to a host that a checked-in file chose is a
13
+ * decision the caller has to make deliberately. See `unsafeBaseUrlRedirect`.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.maskToken = exports.clearUserConfig = exports.readUserConfig = exports.writeUserConfig = exports.allowProjectBaseUrlFromEnvironment = exports.unsafeBaseUrlRedirect = exports.loadConfig = exports.resolveConfig = exports.projectConfigPath = exports.userConfigPath = exports.PROJECT_FILE = void 0;
17
+ const node_fs_1 = require("node:fs");
18
+ const node_os_1 = require("node:os");
19
+ const node_path_1 = require("node:path");
20
+ exports.PROJECT_FILE = '.roark.json';
21
+ const userConfigPath = () => {
22
+ const xdg = process.env['XDG_CONFIG_HOME'];
23
+ const base = xdg && xdg.length > 0 ? xdg : (0, node_path_1.join)((0, node_os_1.homedir)(), '.config');
24
+ return (0, node_path_1.join)(base, 'roark', 'config.json');
25
+ };
26
+ exports.userConfigPath = userConfigPath;
27
+ /** Walks up from the cwd, so a command run in a subdirectory still sees the repo's config. */
28
+ const projectConfigPath = (from = process.cwd()) => {
29
+ let directory = (0, node_path_1.resolve)(from);
30
+ for (;;) {
31
+ const candidate = (0, node_path_1.join)(directory, exports.PROJECT_FILE);
32
+ if ((0, node_fs_1.existsSync)(candidate))
33
+ return candidate;
34
+ const parent = (0, node_path_1.dirname)(directory);
35
+ if (parent === directory)
36
+ return undefined;
37
+ directory = parent;
38
+ }
39
+ };
40
+ exports.projectConfigPath = projectConfigPath;
41
+ const readFile = (path) => {
42
+ if (!path || !(0, node_fs_1.existsSync)(path))
43
+ return {};
44
+ try {
45
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf8'));
46
+ if (typeof parsed !== 'object' || parsed === null)
47
+ return {};
48
+ return parsed;
49
+ }
50
+ catch (cause) {
51
+ // A corrupt config should say so rather than silently behaving as if the
52
+ // file were empty, which looks like "my token stopped working".
53
+ throw new Error(`Could not parse ${path}: ${cause.message}`);
54
+ }
55
+ };
56
+ const number = (value) => {
57
+ if (value === undefined || value.trim() === '')
58
+ return undefined;
59
+ const parsed = Number(value);
60
+ return Number.isFinite(parsed) ? parsed : undefined;
61
+ };
62
+ const fromEnvironment = () => ({
63
+ ...(process.env['ROARK_API_BEARER_TOKEN'] ? { bearerToken: process.env['ROARK_API_BEARER_TOKEN'] } : {}),
64
+ ...(process.env['ROARK_BASE_URL'] ? { baseURL: process.env['ROARK_BASE_URL'] } : {}),
65
+ ...(number(process.env['ROARK_TIMEOUT']) !== undefined ?
66
+ { timeout: number(process.env['ROARK_TIMEOUT']) }
67
+ : {}),
68
+ ...(number(process.env['ROARK_MAX_RETRIES']) !== undefined ?
69
+ { maxRetries: number(process.env['ROARK_MAX_RETRIES']) }
70
+ : {}),
71
+ });
72
+ /** Later arguments win, and an absent key never overwrites a present one. */
73
+ const merge = (...layers) => {
74
+ const result = {};
75
+ for (const layer of layers) {
76
+ for (const [key, value] of Object.entries(layer)) {
77
+ if (value !== undefined)
78
+ result[key] = value;
79
+ }
80
+ }
81
+ return result;
82
+ };
83
+ const resolveConfig = (overrides = {}, cwd) => {
84
+ const projectFile = (0, exports.projectConfigPath)(cwd);
85
+ const layers = [
86
+ ['user', readFile((0, exports.userConfigPath)())],
87
+ ['project', readFile(projectFile)],
88
+ ['environment', fromEnvironment()],
89
+ ['flag', overrides],
90
+ ];
91
+ const config = {};
92
+ const sources = {};
93
+ for (const [source, layer] of layers) {
94
+ for (const [key, value] of Object.entries(layer)) {
95
+ if (value === undefined)
96
+ continue;
97
+ config[key] = value;
98
+ sources[key] = source;
99
+ }
100
+ }
101
+ return { config, sources, ...(projectFile === undefined ? {} : { projectFile }) };
102
+ };
103
+ exports.resolveConfig = resolveConfig;
104
+ const loadConfig = (overrides = {}) => (0, exports.resolveConfig)(overrides).config;
105
+ exports.loadConfig = loadConfig;
106
+ /**
107
+ * The one combination that leaks a secret: a project file chose the host, and
108
+ * the credential heading for it is one the caller did not choose per-invocation.
109
+ *
110
+ * `git clone` a repository with a `.roark.json` naming someone else's base URL,
111
+ * run any command anywhere inside it, and the token `auth login` stored would be
112
+ * posted to that host - the SDK attaches `Authorization` to whatever base URL it
113
+ * is given, and never scopes a credential to a host. Same shape as a malicious
114
+ * `.npmrc` pointing `registry` at an attacker.
115
+ *
116
+ * A token that came from the same project file is not a leak: the file supplied
117
+ * both halves and nothing of the caller's travels. Neither is one passed as
118
+ * `--token` for this run, which is an explicit act naming both.
119
+ */
120
+ const unsafeBaseUrlRedirect = (resolved) => {
121
+ const { baseURL, bearerToken } = resolved.config;
122
+ if (resolved.sources.baseURL !== 'project' || baseURL === undefined)
123
+ return undefined;
124
+ if (bearerToken === undefined)
125
+ return undefined;
126
+ const credentialFrom = resolved.sources.bearerToken;
127
+ if (credentialFrom === undefined || credentialFrom === 'project' || credentialFrom === 'flag') {
128
+ return undefined;
129
+ }
130
+ // `sources.baseURL === 'project'` cannot be set without a project file.
131
+ return { baseURL, projectFile: resolved.projectFile, credentialFrom };
132
+ };
133
+ exports.unsafeBaseUrlRedirect = unsafeBaseUrlRedirect;
134
+ /** `ROARK_ALLOW_PROJECT_BASE_URL`, for scripts that cannot pass the flag. */
135
+ const allowProjectBaseUrlFromEnvironment = () => {
136
+ const value = process.env['ROARK_ALLOW_PROJECT_BASE_URL'];
137
+ return value !== undefined && value !== '' && value !== '0' && value.toLowerCase() !== 'false';
138
+ };
139
+ exports.allowProjectBaseUrlFromEnvironment = allowProjectBaseUrlFromEnvironment;
140
+ /**
141
+ * Written 0600: it holds a bearer token, and the default 0644 would expose it to
142
+ * every account on a shared machine.
143
+ */
144
+ const writeUserConfig = (config) => {
145
+ const path = (0, exports.userConfigPath)();
146
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true, mode: 0o700 });
147
+ (0, node_fs_1.writeFileSync)(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
148
+ return path;
149
+ };
150
+ exports.writeUserConfig = writeUserConfig;
151
+ const readUserConfig = () => readFile((0, exports.userConfigPath)());
152
+ exports.readUserConfig = readUserConfig;
153
+ const clearUserConfig = () => {
154
+ const path = (0, exports.userConfigPath)();
155
+ if (!(0, node_fs_1.existsSync)(path))
156
+ return false;
157
+ (0, node_fs_1.rmSync)(path);
158
+ return true;
159
+ };
160
+ exports.clearUserConfig = clearUserConfig;
161
+ /** `roark-...-abcd` -> `roar...abcd`: enough to identify, not enough to use. */
162
+ const maskToken = (token) => token.length <= 8 ? '****' : `${token.slice(0, 4)}...${token.slice(-4)}`;
163
+ exports.maskToken = maskToken;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Confirmation for destructive commands.
3
+ *
4
+ * Prompting is only safe when there is a human to answer: in a script stdin is
5
+ * not a TTY, and a prompt would either hang or silently consume piped data. So
6
+ * a non-interactive run requires `--yes` up front rather than being waved
7
+ * through, which is the direction that fails safe.
8
+ */
9
+ export declare const isInteractive: () => boolean;
10
+ export interface ConfirmOptions {
11
+ /** `--yes`: the user has already agreed. */
12
+ assumeYes: boolean;
13
+ /** `--no-input`: never prompt, fail instead. */
14
+ noInput: boolean;
15
+ }
16
+ export declare const confirm: (prompt: string, options: ConfirmOptions) => Promise<void>;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ /**
3
+ * Confirmation for destructive commands.
4
+ *
5
+ * Prompting is only safe when there is a human to answer: in a script stdin is
6
+ * not a TTY, and a prompt would either hang or silently consume piped data. So
7
+ * a non-interactive run requires `--yes` up front rather than being waved
8
+ * through, which is the direction that fails safe.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.confirm = exports.isInteractive = void 0;
12
+ const node_readline_1 = require("node:readline");
13
+ const errors_1 = require("./errors");
14
+ const isInteractive = () => process.stdin.isTTY === true && process.stdout.isTTY === true;
15
+ exports.isInteractive = isInteractive;
16
+ const confirm = async (prompt, options) => {
17
+ if (options.assumeYes)
18
+ return;
19
+ if (options.noInput || !(0, exports.isInteractive)()) {
20
+ throw new errors_1.UsageError(`${prompt} Refusing to continue without --yes.`);
21
+ }
22
+ const rl = (0, node_readline_1.createInterface)({ input: process.stdin, output: process.stderr });
23
+ try {
24
+ const answer = await new Promise((resolveAnswer) => {
25
+ rl.question(`${prompt} [y/N] `, resolveAnswer);
26
+ });
27
+ if (!/^y(es)?$/i.test(answer.trim()))
28
+ throw new errors_1.UsageError('Cancelled.');
29
+ }
30
+ finally {
31
+ rl.close();
32
+ }
33
+ };
34
+ exports.confirm = confirm;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Error rendering and exit codes.
3
+ *
4
+ * Exit codes are the only thing a script can branch on, so they distinguish the
5
+ * cases a caller would actually handle differently: a bad invocation, a missing
6
+ * credential, and a missing resource are not all "1".
7
+ */
8
+ import { type OutputFormat } from './output';
9
+ export declare const EXIT: {
10
+ readonly ok: 0;
11
+ /** The request was made and the API rejected it. */
12
+ readonly apiError: 1;
13
+ /** The command line was wrong: unknown flag, missing argument, bad value. */
14
+ readonly usage: 2;
15
+ /** No credential, or the credential was refused. */
16
+ readonly auth: 3;
17
+ /** The addressed resource does not exist. */
18
+ readonly notFound: 4;
19
+ /** The request never completed: DNS, TLS, timeout, or an aborted run. */
20
+ readonly connection: 5;
21
+ };
22
+ /** Thrown for anything wrong with the command line itself. */
23
+ export declare class UsageError extends Error {
24
+ readonly name = "UsageError";
25
+ }
26
+ /**
27
+ * No credential was found at all. Distinct from a rejected one, but a script
28
+ * handles both the same way, so both exit 3.
29
+ */
30
+ export declare class AuthRequiredError extends Error {
31
+ readonly name = "AuthRequiredError";
32
+ }
33
+ export declare const exitCodeFor: (error: unknown) => number;
34
+ interface ReportOptions {
35
+ format: OutputFormat;
36
+ color: boolean;
37
+ /** Shown under an auth failure so the fix is one copyable line away. */
38
+ binaryName: string;
39
+ }
40
+ /**
41
+ * Errors go to stderr so that `roark ... > out.json` leaves `out.json` holding
42
+ * only real output, and a failed run leaves it empty rather than half-written.
43
+ */
44
+ export declare const reportError: (error: unknown, options: ReportOptions) => void;
45
+ export {};