@ekanos/cli 0.15.0 → 0.16.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 (113) hide show
  1. package/README.md +41 -4
  2. package/dist/auth/ekanos-api.d.ts +23 -0
  3. package/dist/auth/ekanos-api.js +33 -6
  4. package/dist/auth/ekanos-api.js.map +1 -1
  5. package/dist/commands/agents.js +3 -2
  6. package/dist/commands/agents.js.map +1 -1
  7. package/dist/commands/apps.d.ts +11 -3
  8. package/dist/commands/apps.js +86 -28
  9. package/dist/commands/apps.js.map +1 -1
  10. package/dist/commands/catalog.d.ts +4 -2
  11. package/dist/commands/catalog.js +166 -35
  12. package/dist/commands/catalog.js.map +1 -1
  13. package/dist/commands/login.d.ts +2 -0
  14. package/dist/commands/login.js +17 -0
  15. package/dist/commands/login.js.map +1 -1
  16. package/dist/commands/runs.d.ts +7 -3
  17. package/dist/commands/runs.js +21 -5
  18. package/dist/commands/runs.js.map +1 -1
  19. package/dist/commands/scope-notice.d.ts +7 -0
  20. package/dist/commands/scope-notice.js +4 -11
  21. package/dist/commands/scope-notice.js.map +1 -1
  22. package/dist/commands/sources-roles.d.ts +5 -0
  23. package/dist/commands/sources-roles.js +235 -0
  24. package/dist/commands/sources-roles.js.map +1 -0
  25. package/dist/commands/sources.d.ts +5 -0
  26. package/dist/commands/sources.js +6 -1
  27. package/dist/commands/sources.js.map +1 -1
  28. package/dist/commands/team-agent-readiness.d.ts +18 -2
  29. package/dist/commands/team-agent-readiness.js +39 -39
  30. package/dist/commands/team-agent-readiness.js.map +1 -1
  31. package/dist/commands/team-agents-agents.d.ts +5 -2
  32. package/dist/commands/team-agents-agents.js +152 -15
  33. package/dist/commands/team-agents-agents.js.map +1 -1
  34. package/dist/commands/team-agents-define.d.ts +31 -0
  35. package/dist/commands/team-agents-define.js +176 -0
  36. package/dist/commands/team-agents-define.js.map +1 -0
  37. package/dist/commands/team-agents-mcp.js +112 -10
  38. package/dist/commands/team-agents-mcp.js.map +1 -1
  39. package/dist/commands/team-agents-triggers.d.ts +1 -1
  40. package/dist/commands/team-agents-triggers.js +137 -5
  41. package/dist/commands/team-agents-triggers.js.map +1 -1
  42. package/dist/commands/team-agents.d.ts +4 -1
  43. package/dist/commands/team-agents.js +17 -4
  44. package/dist/commands/team-agents.js.map +1 -1
  45. package/dist/commands/team-llm-key.d.ts +14 -0
  46. package/dist/commands/team-llm-key.js +122 -0
  47. package/dist/commands/team-llm-key.js.map +1 -0
  48. package/dist/commands/teams-connect.js +123 -31
  49. package/dist/commands/teams-connect.js.map +1 -1
  50. package/dist/commands/teams-lifecycle.d.ts +90 -0
  51. package/dist/commands/teams-lifecycle.js +457 -0
  52. package/dist/commands/teams-lifecycle.js.map +1 -0
  53. package/dist/commands/teams-onboard.d.ts +14 -0
  54. package/dist/commands/teams-onboard.js +62 -13
  55. package/dist/commands/teams-onboard.js.map +1 -1
  56. package/dist/commands/teams-overview.js +6 -2
  57. package/dist/commands/teams-overview.js.map +1 -1
  58. package/dist/commands/teams.d.ts +25 -5
  59. package/dist/commands/teams.js +101 -39
  60. package/dist/commands/teams.js.map +1 -1
  61. package/dist/commands/workflows.d.ts +37 -0
  62. package/dist/commands/workflows.js +589 -0
  63. package/dist/commands/workflows.js.map +1 -0
  64. package/dist/connect-fields.d.ts +22 -0
  65. package/dist/connect-fields.js +115 -0
  66. package/dist/connect-fields.js.map +1 -0
  67. package/dist/context.d.ts +12 -0
  68. package/dist/context.js +32 -9
  69. package/dist/context.js.map +1 -1
  70. package/dist/index.d.ts +13 -0
  71. package/dist/index.js +357 -122
  72. package/dist/index.js.map +1 -1
  73. package/dist/invocation.d.ts +31 -0
  74. package/dist/invocation.js +58 -0
  75. package/dist/invocation.js.map +1 -0
  76. package/dist/runs-api.js +8 -3
  77. package/dist/runs-api.js.map +1 -1
  78. package/dist/safe-fs.d.ts +18 -0
  79. package/dist/safe-fs.js +57 -0
  80. package/dist/safe-fs.js.map +1 -1
  81. package/dist/secret-form.d.ts +48 -0
  82. package/dist/secret-form.js +212 -0
  83. package/dist/secret-form.js.map +1 -0
  84. package/dist/secret-input.d.ts +96 -0
  85. package/dist/secret-input.js +299 -0
  86. package/dist/secret-input.js.map +1 -0
  87. package/dist/team-connect-api.d.ts +78 -3
  88. package/dist/team-connect-api.js +17 -2
  89. package/dist/team-connect-api.js.map +1 -1
  90. package/dist/team-lifecycle-api.d.ts +435 -0
  91. package/dist/team-lifecycle-api.js +301 -0
  92. package/dist/team-lifecycle-api.js.map +1 -0
  93. package/dist/teams-api.d.ts +13 -1
  94. package/dist/teams-api.js +88 -27
  95. package/dist/teams-api.js.map +1 -1
  96. package/dist/unknown-command.d.ts +18 -0
  97. package/dist/unknown-command.js +50 -0
  98. package/dist/unknown-command.js.map +1 -0
  99. package/dist/workflows-api.d.ts +69 -0
  100. package/dist/workflows-api.js +182 -0
  101. package/dist/workflows-api.js.map +1 -0
  102. package/package.json +3 -3
  103. package/templates/AGENTS.md.tmpl +6 -4
  104. package/templates/claude-skill.md.tmpl +4 -1
  105. package/templates/package.json.tmpl +4 -4
  106. package/templates/skills/build-agent/SKILL.md +43 -10
  107. package/templates/skills/build-agent/references/agent-json.md +4 -1
  108. package/templates/skills/build-agent/references/app-setup.md +14 -7
  109. package/templates/skills/build-agent/references/rollout.md +26 -2
  110. package/templates/skills/build-agent/references/set-up-an-integration.md +46 -29
  111. package/templates/skills/build-integration/references/widget-design.md +1 -1
  112. package/templates/skills/ekanos-platform/SKILL.md +66 -14
  113. package/templates/skills/polish-widgets/references/patterns.md +3 -3
@@ -0,0 +1,96 @@
1
+ import type { CliContext } from './context.js';
2
+ import { type SecretFormField } from './secret-form.js';
3
+ /**
4
+ * How every command that takes a secret gets it — `apps set`, `apps
5
+ * connect`, `teams app set`, `teams connect`, `teams mcp token|add`,
6
+ * `teams llm-key set` — so a coding agent or a script can run them as well
7
+ * as a person at a terminal, and none of them ever has the secret in argv.
8
+ *
9
+ * In order:
10
+ *
11
+ * 1. **`--browser`**: a one-shot form on this computer (`secret-form.ts`).
12
+ * The person types it in their browser; the run that asked never sees it.
13
+ * 2. **A named source**: `--secret-env NAME`, `--secret-file PATH` (a
14
+ * 0600 file of yours, never through a symlink) or `--secret-stdin` (piped)
15
+ * for the one secret; `--set-secret <field>=env:NAME|file:PATH|stdin` for
16
+ * each field of a form. Nothing else is asked.
17
+ * 3. **A terminal**: hidden input, as before.
18
+ * 4. Otherwise a usage error naming the forms above.
19
+ *
20
+ * A literal (`--set-secret field=sk-…`) is refused: argv lands in shell
21
+ * history and the process list. No error, log or output here ever carries a
22
+ * value — not even a path or a name that might be one by mistake.
23
+ *
24
+ * `--browser` is never chosen for you. A run with no terminal is usually an
25
+ * agent or CI, and the CLI cannot tell whether anyone is at the desktop: a
26
+ * form nobody fills would stall the run for its whole timeout. So it is
27
+ * explicit, refused up front where it cannot work (CI, SSH, no display),
28
+ * and bounded by a timeout everywhere.
29
+ */
30
+ export interface SecretFlags {
31
+ /** `--secret-stdin` */
32
+ stdin: boolean;
33
+ /** `--secret-env <NAME>` */
34
+ env?: string;
35
+ /** `--secret-file <path>` */
36
+ file?: string;
37
+ /** `--set-secret <field>=env:NAME|file:PATH|stdin`, repeatable. */
38
+ sets: string[];
39
+ /** `--browser` */
40
+ browser: boolean;
41
+ }
42
+ /** The flags, from a command's parsed flags. */
43
+ export declare function readSecretFlags(flags: Record<string, string | boolean | string[]> | undefined): SecretFlags;
44
+ /** Whether any way of giving a secret without a terminal was passed. */
45
+ export declare function secretInputGiven(flags: SecretFlags): boolean;
46
+ /** The forms, as a hint names them. */
47
+ export declare const SINGLE_SECRET_FORMS = "--secret-env <NAME>, --secret-file <path> (a file only you can read), --secret-stdin (piped in), or --browser (a form in your browser on this computer)";
48
+ export declare const FIELD_SECRET_FORMS = "--set-secret <field>=env:<NAME>|file:<path>|stdin for each secret (--secret-env, --secret-file or --secret-stdin when there is one), or --browser (a form in your browser on this computer)";
49
+ export interface SecretIO {
50
+ env: Record<string, string | undefined>;
51
+ stdinIsTTY: boolean;
52
+ /** Test seam: the whole of stdin. */
53
+ readStdin?: () => Promise<string>;
54
+ /** Test seam: the browser opener for `--browser`. */
55
+ openUrl?: (url: string) => void;
56
+ /** Test seam: how long `--browser` waits. */
57
+ formTimeoutMs?: number;
58
+ /** Test seam: the platform `--browser` checks for a desktop. */
59
+ platform?: NodeJS.Platform;
60
+ /** Where a relative `--secret-file` resolves (default: the working directory). */
61
+ cwd?: string;
62
+ }
63
+ export type SecretField = SecretFormField;
64
+ export interface CollectSecretsInput {
65
+ /** Every field this run would ask, in order. */
66
+ fields: readonly SecretField[];
67
+ flags: SecretFlags;
68
+ io: SecretIO;
69
+ /** A human at a terminal (not `--json`, stdin a TTY). */
70
+ interactive: boolean;
71
+ /** The terminal question for one field — hidden for a secret. */
72
+ prompt: (field: SecretField) => Promise<string>;
73
+ /** The secure form's heading. */
74
+ title: string;
75
+ /** What is needed, when nothing can be asked: the refusal's message. */
76
+ refusal: string;
77
+ /** The command, without `npx @ekanos/cli`, for hints. */
78
+ again: string;
79
+ /**
80
+ * `--set <field>=<value>` for a field that is not a secret (a subdomain, a
81
+ * client ID): taken as given, and never asked. A secret here is refused.
82
+ */
83
+ plain?: Record<string, string>;
84
+ }
85
+ /**
86
+ * `--set <field>=<value>` pairs. A malformed one is refused without echoing
87
+ * it — it may be a secret typed where a field name was meant.
88
+ */
89
+ export declare function parsePlainValues(raw: string | boolean | string[] | undefined, example?: string): Record<string, string>;
90
+ /**
91
+ * Each field's value by name; a field left empty is left out (its saved
92
+ * value stays). Refuses a required field that is neither saved nor given.
93
+ */
94
+ export declare function collectSecrets(ctx: CliContext, input: CollectSecretsInput): Promise<Record<string, string>>;
95
+ /** Why a browser form cannot be filled here, or null when it can. */
96
+ export declare function desktopProblem(env: Record<string, string | undefined>, platform: NodeJS.Platform): string | null;
@@ -0,0 +1,299 @@
1
+ import * as path from 'node:path';
2
+ import { notFoundError, preconditionError, usageError, validationError, } from './errors.js';
3
+ import { openInBrowser } from './open-url.js';
4
+ import { readPrivateFileNoFollow } from './safe-fs.js';
5
+ import { collectWithSecretForm } from './secret-form.js';
6
+ /** The flags, from a command's parsed flags. */
7
+ export function readSecretFlags(flags) {
8
+ const text = (name) => {
9
+ const value = flags === null || flags === void 0 ? void 0 : flags[name];
10
+ return typeof value === 'string' ? value : undefined;
11
+ };
12
+ const sets = flags === null || flags === void 0 ? void 0 : flags['set-secret'];
13
+ return {
14
+ stdin: (flags === null || flags === void 0 ? void 0 : flags['secret-stdin']) === true,
15
+ env: text('secret-env'),
16
+ file: text('secret-file'),
17
+ sets: Array.isArray(sets) ? sets : typeof sets === 'string' ? [sets] : [],
18
+ browser: (flags === null || flags === void 0 ? void 0 : flags.browser) === true,
19
+ };
20
+ }
21
+ /** Whether any way of giving a secret without a terminal was passed. */
22
+ export function secretInputGiven(flags) {
23
+ return (flags.browser ||
24
+ flags.stdin ||
25
+ flags.env !== undefined ||
26
+ flags.file !== undefined ||
27
+ flags.sets.length > 0);
28
+ }
29
+ /** The forms, as a hint names them. */
30
+ export const SINGLE_SECRET_FORMS = '--secret-env <NAME>, --secret-file <path> (a file only you can read), --secret-stdin (piped in), or --browser (a form in your browser on this computer)';
31
+ export const FIELD_SECRET_FORMS = '--set-secret <field>=env:<NAME>|file:<path>|stdin for each secret (--secret-env, --secret-file or --secret-stdin when there is one), or --browser (a form in your browser on this computer)';
32
+ /**
33
+ * `--set <field>=<value>` pairs. A malformed one is refused without echoing
34
+ * it — it may be a secret typed where a field name was meant.
35
+ */
36
+ export function parsePlainValues(raw, example = 'subdomain=acme') {
37
+ const pairs = Array.isArray(raw) ? raw : typeof raw === 'string' ? [raw] : [];
38
+ const values = {};
39
+ for (const pair of pairs) {
40
+ const at = pair.indexOf('=');
41
+ const name = at > 0 ? pair.slice(0, at).trim() : '';
42
+ if (!name) {
43
+ throw usageError('--set takes <field>=<value>, for a field that is not a secret.', `For example: --set ${example}. A secret is never passed as an argument: use --set-secret <field>=env:<NAME>|file:<path>|stdin.`);
44
+ }
45
+ values[name] = pair.slice(at + 1).trim();
46
+ }
47
+ return values;
48
+ }
49
+ const MAX_SECRET_BYTES = 64 * 1024;
50
+ const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
51
+ const FIELD_NAME = /^[A-Za-z0-9_.-]{1,64}$/;
52
+ /**
53
+ * Each field's value by name; a field left empty is left out (its saved
54
+ * value stays). Refuses a required field that is neither saved nor given.
55
+ */
56
+ export async function collectSecrets(ctx, input) {
57
+ var _a;
58
+ const { flags } = input;
59
+ const plain = checkPlain(input.fields, (_a = input.plain) !== null && _a !== void 0 ? _a : {});
60
+ // What is left to ask: every field `--set` did not give.
61
+ const fields = input.fields.filter((field) => plain[field.name] === undefined);
62
+ const singles = [
63
+ flags.stdin,
64
+ flags.env !== undefined,
65
+ flags.file !== undefined,
66
+ ].filter(Boolean).length;
67
+ if (singles > 1) {
68
+ throw usageError('Pass one of --secret-env, --secret-file and --secret-stdin, not several.', `Each names where the one secret comes from. For several, use --set-secret <field>=… per field.`);
69
+ }
70
+ if (flags.browser && (singles > 0 || flags.sets.length > 0)) {
71
+ throw usageError('--browser asks for every field in the form; it does not mix with --secret-* or --set-secret.', 'Drop --browser to pass them from the environment, a file or stdin, or drop those to use the form.');
72
+ }
73
+ const rest = Object.assign(Object.assign({}, input), { fields });
74
+ let values;
75
+ if (fields.length === 0) {
76
+ values = {};
77
+ }
78
+ else if (flags.browser) {
79
+ values = await viaBrowser(ctx, rest);
80
+ }
81
+ else if (singles > 0 || flags.sets.length > 0) {
82
+ values = await viaSources(rest);
83
+ }
84
+ else if (input.interactive) {
85
+ values = {};
86
+ for (const field of fields) {
87
+ const value = (await input.prompt(field)).trim();
88
+ if (value)
89
+ values[field.name] = value;
90
+ }
91
+ }
92
+ else if (Object.keys(plain).length > 0) {
93
+ // Only non-secret values were given: the rest keep what is saved.
94
+ values = {};
95
+ }
96
+ else {
97
+ throw usageError(input.refusal, `Without a terminal, pass ${fields.filter((field) => field.sensitive).length > 1 ? FIELD_SECRET_FORMS : SINGLE_SECRET_FORMS}. Or run it yourself in your terminal: npx @ekanos/cli ${input.again}`);
98
+ }
99
+ const missing = fields.filter((field) => field.required && !field.set && !values[field.name]);
100
+ if (missing.length > 0) {
101
+ throw validationError(`${missing.map((field) => field.label).join(', ')} ${missing.length === 1 ? 'is' : 'are'} needed; nothing was saved.`, flags.browser || input.interactive
102
+ ? `Run "ekanos ${input.again}" again and fill ${missing.length === 1 ? 'it' : 'them'} in.`
103
+ : `Pass ${missing.map((field) => (field.sensitive ? `--set-secret ${field.name}=env:<NAME>` : `--set ${field.name}=<value>`)).join(' ')}.`);
104
+ }
105
+ return Object.assign(Object.assign({}, plainGiven(plain)), values);
106
+ }
107
+ /** `--set` values, checked against the fields: known, and never a secret. */
108
+ function checkPlain(fields, plain) {
109
+ const byName = new Map(fields.map((field) => [field.name, field]));
110
+ for (const name of Object.keys(plain)) {
111
+ const field = byName.get(name);
112
+ const shown = FIELD_NAME.test(name) ? `"${name}"` : 'That field';
113
+ if (!field) {
114
+ throw usageError(`${shown} is not a field this asks for.`, `Its fields are: ${fields.map((entry) => entry.name).join(', ') || 'none'}.`);
115
+ }
116
+ if (field.sensitive) {
117
+ throw usageError(`${shown} is a secret: it is never passed as an argument.`, `Pass it as --set-secret ${name}=env:<NAME>|file:<path>|stdin, or use --browser.`);
118
+ }
119
+ }
120
+ return plain;
121
+ }
122
+ /** An empty `--set name=` means nothing to change, as Enter does at a prompt. */
123
+ function plainGiven(plain) {
124
+ return Object.fromEntries(Object.entries(plain).filter(([, value]) => value !== ''));
125
+ }
126
+ async function viaBrowser(ctx, input) {
127
+ var _a, _b;
128
+ const problem = desktopProblem(input.io.env, (_a = input.io.platform) !== null && _a !== void 0 ? _a : process.platform);
129
+ if (problem) {
130
+ throw preconditionError(`--browser needs a desktop to open a form on: ${problem}`, `Pass it as ${input.fields.filter((field) => field.sensitive).length > 1 ? '--set-secret <field>=env:<NAME>|file:<path>|stdin' : '--secret-env <NAME>, --secret-file <path> or --secret-stdin'} instead.`);
131
+ }
132
+ ctx.log(`Opened a secure form in your browser for ${input.title}… (waiting; if no browser opened, re-run with --secret-env, --secret-file or --secret-stdin)`);
133
+ return collectWithSecretForm({
134
+ title: input.title,
135
+ fields: input.fields,
136
+ open: (_b = input.io.openUrl) !== null && _b !== void 0 ? _b : openInBrowser,
137
+ timeoutMs: input.io.formTimeoutMs,
138
+ });
139
+ }
140
+ /** Why a browser form cannot be filled here, or null when it can. */
141
+ export function desktopProblem(env, platform) {
142
+ var _a;
143
+ const ci = (_a = env.CI) === null || _a === void 0 ? void 0 : _a.trim().toLowerCase();
144
+ if (ci && ci !== '0' && ci !== 'false') {
145
+ return 'this is a CI run (CI is set), with no one to fill it in.';
146
+ }
147
+ if (env.SSH_CONNECTION || env.SSH_TTY) {
148
+ return 'this is an SSH session, and a browser on this machine is not in front of you.';
149
+ }
150
+ if (platform !== 'darwin' &&
151
+ platform !== 'win32' &&
152
+ !env.DISPLAY &&
153
+ !env.WAYLAND_DISPLAY) {
154
+ return 'there is no desktop session (DISPLAY or WAYLAND_DISPLAY) to open a browser in.';
155
+ }
156
+ return null;
157
+ }
158
+ async function viaSources(input) {
159
+ var _a;
160
+ var _b;
161
+ const { flags, fields } = input;
162
+ const byName = new Map(fields.map((field) => [field.name, field]));
163
+ const specs = new Map();
164
+ for (const entry of flags.sets) {
165
+ const at = entry.indexOf('=');
166
+ const name = at > 0 ? entry.slice(0, at).trim() : '';
167
+ if (!name) {
168
+ throw usageError('--set-secret takes <field>=env:<NAME>, <field>=file:<path> or <field>=stdin.', `For example: --set-secret ${(_b = (_a = fields.find((field) => field.sensitive)) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : 'client_secret'}=env:MY_SECRET.`);
169
+ }
170
+ const shown = FIELD_NAME.test(name) ? `"${name}"` : 'that field';
171
+ const field = byName.get(name);
172
+ if (!field) {
173
+ throw usageError(`${shown} is not a field this asks for.`, `Its fields are: ${fields.map((entry) => entry.name).join(', ') || 'none'}.`);
174
+ }
175
+ if (specs.has(name)) {
176
+ throw usageError(`--set-secret names ${shown} twice.`, 'Pass each field once.');
177
+ }
178
+ specs.set(name, parseSpec(entry.slice(at + 1), `--set-secret ${name}`));
179
+ }
180
+ const single = flags.stdin
181
+ ? { kind: 'stdin' }
182
+ : flags.env !== undefined
183
+ ? envSpec(flags.env, '--secret-env')
184
+ : flags.file !== undefined
185
+ ? { kind: 'file', path: flags.file }
186
+ : null;
187
+ if (single) {
188
+ const secrets = fields.filter((field) => field.sensitive && !specs.has(field.name));
189
+ if (secrets.length !== 1) {
190
+ throw usageError(secrets.length === 0
191
+ ? 'There is no secret here for --secret-env, --secret-file or --secret-stdin to fill.'
192
+ : `This asks for ${secrets.length} secrets (${secrets.map((field) => field.name).join(', ')}), so name each one.`, secrets.length === 0
193
+ ? 'Drop the flag.'
194
+ : `Pass --set-secret <field>=env:<NAME>|file:<path>|stdin for each, e.g. --set-secret ${secrets[0].name}=env:MY_SECRET.`);
195
+ }
196
+ specs.set(secrets[0].name, single);
197
+ }
198
+ if ([...specs.values()].filter((spec) => spec.kind === 'stdin').length > 1) {
199
+ throw usageError('Only one secret can come from stdin.', 'Take the others from the environment (env:<NAME>) or a file (file:<path>).');
200
+ }
201
+ const values = {};
202
+ for (const [name, spec] of specs) {
203
+ const value = (await resolveSpec(spec, name, input.io)).trim();
204
+ if (!value) {
205
+ throw preconditionError(`The value for ${name} (from ${describe(spec)}) is empty; nothing was saved.`, 'Put the secret there, then run the command again.');
206
+ }
207
+ values[name] = value;
208
+ }
209
+ return values;
210
+ }
211
+ function parseSpec(raw, flag) {
212
+ if (raw === 'stdin')
213
+ return { kind: 'stdin' };
214
+ if (raw.startsWith('env:'))
215
+ return envSpec(raw.slice(4), flag);
216
+ if (raw.startsWith('file:') && raw.length > 5) {
217
+ return { kind: 'file', path: raw.slice(5) };
218
+ }
219
+ throw usageError(`${flag}=… is not env:<NAME>, file:<path> or stdin. A secret is never passed as an argument: it would land in your shell history and the process list.`, `Put it in an environment variable (${flag}=env:MY_SECRET), a file only you can read (${flag}=file:./secret.txt), or pipe it in (${flag}=stdin).`);
220
+ }
221
+ function envSpec(name, flag) {
222
+ if (ENV_NAME.test(name))
223
+ return { kind: 'env', name };
224
+ // Never echoed: a value that is not a name may be the secret itself.
225
+ throw usageError(`${flag} takes the NAME of an environment variable (letters, digits and _), never the secret itself.`, `For example: MY_SECRET=… then ${flag}${flag.startsWith('--set-secret') ? '=env:' : ' '}MY_SECRET.`);
226
+ }
227
+ function describe(spec) {
228
+ if (spec.kind === 'env')
229
+ return `$${spec.name}`;
230
+ if (spec.kind === 'file')
231
+ return 'the file';
232
+ return 'stdin';
233
+ }
234
+ async function resolveSpec(spec, field, io) {
235
+ if (spec.kind === 'env') {
236
+ const value = io.env[spec.name];
237
+ if (value === undefined || value === '') {
238
+ throw preconditionError(`The environment variable ${spec.name} (for ${field}) is not set; nothing was saved.`, `Set it in the environment the command runs in, then run it again.`);
239
+ }
240
+ return value;
241
+ }
242
+ if (spec.kind === 'file')
243
+ return readSecretFile(spec.path, field, io.cwd);
244
+ return readStdinSecret(io);
245
+ }
246
+ function readSecretFile(given, field, cwd = process.cwd()) {
247
+ var _a;
248
+ const result = readPrivateFileNoFollow(path.resolve(cwd, given), MAX_SECRET_BYTES);
249
+ if (result.ok)
250
+ return stripNewline(result.bytes.toString('utf8'));
251
+ // The path is not echoed: a mistyped flag may have put the secret there.
252
+ const what = `The file given for ${field}`;
253
+ switch (result.problem) {
254
+ case 'missing':
255
+ throw notFoundError(`${what} does not exist; nothing was saved.`, 'Check the path, then run the command again.');
256
+ case 'symlink':
257
+ throw preconditionError(`${what} is a symlink, and the CLI never reads a secret through one.`, 'Name the real file instead.');
258
+ case 'not-a-file':
259
+ throw preconditionError(`${what} is not a regular file.`, 'Name a file holding just the secret.');
260
+ case 'readable-by-others':
261
+ throw preconditionError(`${what} can be read by other users on this machine (mode ${((_a = result.mode) !== null && _a !== void 0 ? _a : 0).toString(8).padStart(3, '0')}), so it was not read.`, 'Make it yours alone with "chmod 600 <file>", then run the command again.');
262
+ case 'not-yours':
263
+ throw preconditionError(`${what} belongs to another user, so it was not read.`, 'Use a file you own, readable only by you (chmod 600).');
264
+ case 'too-large':
265
+ throw preconditionError(`${what} is larger than a secret (over 64 KB).`, 'Name a file holding just the secret.');
266
+ }
267
+ }
268
+ async function readStdinSecret(io) {
269
+ var _a;
270
+ if (io.stdinIsTTY) {
271
+ throw usageError('stdin is a terminal, so there is no piped secret to read.', 'Pipe it in (e.g. `printf %s "$MY_SECRET" | ekanos …`), or use --secret-env or --secret-file.');
272
+ }
273
+ const raw = await ((_a = io.readStdin) !== null && _a !== void 0 ? _a : readAllStdin)();
274
+ return stripNewline(raw);
275
+ }
276
+ function readAllStdin() {
277
+ return new Promise((resolve, reject) => {
278
+ const chunks = [];
279
+ let size = 0;
280
+ const stdin = process.stdin;
281
+ stdin.on('data', (chunk) => {
282
+ const buffer = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
283
+ size += buffer.length;
284
+ if (size > MAX_SECRET_BYTES) {
285
+ stdin.destroy();
286
+ reject(usageError('stdin holds more than a secret (over 64 KB).', 'Pipe in just the secret.'));
287
+ return;
288
+ }
289
+ chunks.push(buffer);
290
+ });
291
+ stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
292
+ stdin.on('error', reject);
293
+ });
294
+ }
295
+ /** One trailing newline (`echo`, an editor's final line) is not part of it. */
296
+ function stripNewline(value) {
297
+ return value.replace(/\r?\n$/, '');
298
+ }
299
+ //# sourceMappingURL=secret-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret-input.js","sourceRoot":"","sources":["../src/secret-input.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'node:path';\n\nimport type { CliContext } from './context';\nimport {\n notFoundError,\n preconditionError,\n usageError,\n validationError,\n} from './errors';\nimport { openInBrowser } from './open-url';\nimport { readPrivateFileNoFollow } from './safe-fs';\nimport { type SecretFormField, collectWithSecretForm } from './secret-form';\n\n/**\n * How every command that takes a secret gets it — `apps set`, `apps\n * connect`, `teams app set`, `teams connect`, `teams mcp token|add`,\n * `teams llm-key set` — so a coding agent or a script can run them as well\n * as a person at a terminal, and none of them ever has the secret in argv.\n *\n * In order:\n *\n * 1. **`--browser`**: a one-shot form on this computer (`secret-form.ts`).\n * The person types it in their browser; the run that asked never sees it.\n * 2. **A named source**: `--secret-env NAME`, `--secret-file PATH` (a\n * 0600 file of yours, never through a symlink) or `--secret-stdin` (piped)\n * for the one secret; `--set-secret <field>=env:NAME|file:PATH|stdin` for\n * each field of a form. Nothing else is asked.\n * 3. **A terminal**: hidden input, as before.\n * 4. Otherwise a usage error naming the forms above.\n *\n * A literal (`--set-secret field=sk-…`) is refused: argv lands in shell\n * history and the process list. No error, log or output here ever carries a\n * value — not even a path or a name that might be one by mistake.\n *\n * `--browser` is never chosen for you. A run with no terminal is usually an\n * agent or CI, and the CLI cannot tell whether anyone is at the desktop: a\n * form nobody fills would stall the run for its whole timeout. So it is\n * explicit, refused up front where it cannot work (CI, SSH, no display),\n * and bounded by a timeout everywhere.\n */\n\nexport interface SecretFlags {\n /** `--secret-stdin` */\n stdin: boolean;\n /** `--secret-env <NAME>` */\n env?: string;\n /** `--secret-file <path>` */\n file?: string;\n /** `--set-secret <field>=env:NAME|file:PATH|stdin`, repeatable. */\n sets: string[];\n /** `--browser` */\n browser: boolean;\n}\n\n/** The flags, from a command's parsed flags. */\nexport function readSecretFlags(\n flags: Record<string, string | boolean | string[]> | undefined,\n): SecretFlags {\n const text = (name: string) => {\n const value = flags?.[name];\n return typeof value === 'string' ? value : undefined;\n };\n const sets = flags?.['set-secret'];\n return {\n stdin: flags?.['secret-stdin'] === true,\n env: text('secret-env'),\n file: text('secret-file'),\n sets: Array.isArray(sets) ? sets : typeof sets === 'string' ? [sets] : [],\n browser: flags?.browser === true,\n };\n}\n\n/** Whether any way of giving a secret without a terminal was passed. */\nexport function secretInputGiven(flags: SecretFlags): boolean {\n return (\n flags.browser ||\n flags.stdin ||\n flags.env !== undefined ||\n flags.file !== undefined ||\n flags.sets.length > 0\n );\n}\n\n/** The forms, as a hint names them. */\nexport const SINGLE_SECRET_FORMS =\n '--secret-env <NAME>, --secret-file <path> (a file only you can read), --secret-stdin (piped in), or --browser (a form in your browser on this computer)';\nexport const FIELD_SECRET_FORMS =\n '--set-secret <field>=env:<NAME>|file:<path>|stdin for each secret (--secret-env, --secret-file or --secret-stdin when there is one), or --browser (a form in your browser on this computer)';\n\nexport interface SecretIO {\n env: Record<string, string | undefined>;\n stdinIsTTY: boolean;\n /** Test seam: the whole of stdin. */\n readStdin?: () => Promise<string>;\n /** Test seam: the browser opener for `--browser`. */\n openUrl?: (url: string) => void;\n /** Test seam: how long `--browser` waits. */\n formTimeoutMs?: number;\n /** Test seam: the platform `--browser` checks for a desktop. */\n platform?: NodeJS.Platform;\n /** Where a relative `--secret-file` resolves (default: the working directory). */\n cwd?: string;\n}\n\nexport type SecretField = SecretFormField;\n\nexport interface CollectSecretsInput {\n /** Every field this run would ask, in order. */\n fields: readonly SecretField[];\n flags: SecretFlags;\n io: SecretIO;\n /** A human at a terminal (not `--json`, stdin a TTY). */\n interactive: boolean;\n /** The terminal question for one field — hidden for a secret. */\n prompt: (field: SecretField) => Promise<string>;\n /** The secure form's heading. */\n title: string;\n /** What is needed, when nothing can be asked: the refusal's message. */\n refusal: string;\n /** The command, without `npx @ekanos/cli`, for hints. */\n again: string;\n /**\n * `--set <field>=<value>` for a field that is not a secret (a subdomain, a\n * client ID): taken as given, and never asked. A secret here is refused.\n */\n plain?: Record<string, string>;\n}\n\n/**\n * `--set <field>=<value>` pairs. A malformed one is refused without echoing\n * it — it may be a secret typed where a field name was meant.\n */\nexport function parsePlainValues(\n raw: string | boolean | string[] | undefined,\n example = 'subdomain=acme',\n): Record<string, string> {\n const pairs = Array.isArray(raw) ? raw : typeof raw === 'string' ? [raw] : [];\n const values: Record<string, string> = {};\n for (const pair of pairs) {\n const at = pair.indexOf('=');\n const name = at > 0 ? pair.slice(0, at).trim() : '';\n if (!name) {\n throw usageError(\n '--set takes <field>=<value>, for a field that is not a secret.',\n `For example: --set ${example}. A secret is never passed as an argument: use --set-secret <field>=env:<NAME>|file:<path>|stdin.`,\n );\n }\n values[name] = pair.slice(at + 1).trim();\n }\n return values;\n}\n\nconst MAX_SECRET_BYTES = 64 * 1024;\nconst ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;\nconst FIELD_NAME = /^[A-Za-z0-9_.-]{1,64}$/;\n\ntype Spec =\n | { kind: 'env'; name: string }\n | { kind: 'file'; path: string }\n | { kind: 'stdin' };\n\n/**\n * Each field's value by name; a field left empty is left out (its saved\n * value stays). Refuses a required field that is neither saved nor given.\n */\nexport async function collectSecrets(\n ctx: CliContext,\n input: CollectSecretsInput,\n): Promise<Record<string, string>> {\n const { flags } = input;\n const plain = checkPlain(input.fields, input.plain ?? {});\n // What is left to ask: every field `--set` did not give.\n const fields = input.fields.filter(\n (field) => plain[field.name] === undefined,\n );\n const singles = [\n flags.stdin,\n flags.env !== undefined,\n flags.file !== undefined,\n ].filter(Boolean).length;\n if (singles > 1) {\n throw usageError(\n 'Pass one of --secret-env, --secret-file and --secret-stdin, not several.',\n `Each names where the one secret comes from. For several, use --set-secret <field>=… per field.`,\n );\n }\n if (flags.browser && (singles > 0 || flags.sets.length > 0)) {\n throw usageError(\n '--browser asks for every field in the form; it does not mix with --secret-* or --set-secret.',\n 'Drop --browser to pass them from the environment, a file or stdin, or drop those to use the form.',\n );\n }\n\n const rest = { ...input, fields };\n let values: Record<string, string>;\n if (fields.length === 0) {\n values = {};\n } else if (flags.browser) {\n values = await viaBrowser(ctx, rest);\n } else if (singles > 0 || flags.sets.length > 0) {\n values = await viaSources(rest);\n } else if (input.interactive) {\n values = {};\n for (const field of fields) {\n const value = (await input.prompt(field)).trim();\n if (value) values[field.name] = value;\n }\n } else if (Object.keys(plain).length > 0) {\n // Only non-secret values were given: the rest keep what is saved.\n values = {};\n } else {\n throw usageError(\n input.refusal,\n `Without a terminal, pass ${fields.filter((field) => field.sensitive).length > 1 ? FIELD_SECRET_FORMS : SINGLE_SECRET_FORMS}. Or run it yourself in your terminal: npx @ekanos/cli ${input.again}`,\n );\n }\n\n const missing = fields.filter(\n (field) => field.required && !field.set && !values[field.name],\n );\n if (missing.length > 0) {\n throw validationError(\n `${missing.map((field) => field.label).join(', ')} ${missing.length === 1 ? 'is' : 'are'} needed; nothing was saved.`,\n flags.browser || input.interactive\n ? `Run \"ekanos ${input.again}\" again and fill ${missing.length === 1 ? 'it' : 'them'} in.`\n : `Pass ${missing.map((field) => (field.sensitive ? `--set-secret ${field.name}=env:<NAME>` : `--set ${field.name}=<value>`)).join(' ')}.`,\n );\n }\n return { ...plainGiven(plain), ...values };\n}\n\n/** `--set` values, checked against the fields: known, and never a secret. */\nfunction checkPlain(\n fields: readonly SecretField[],\n plain: Record<string, string>,\n): Record<string, string> {\n const byName = new Map(fields.map((field) => [field.name, field]));\n for (const name of Object.keys(plain)) {\n const field = byName.get(name);\n const shown = FIELD_NAME.test(name) ? `\"${name}\"` : 'That field';\n if (!field) {\n throw usageError(\n `${shown} is not a field this asks for.`,\n `Its fields are: ${fields.map((entry) => entry.name).join(', ') || 'none'}.`,\n );\n }\n if (field.sensitive) {\n throw usageError(\n `${shown} is a secret: it is never passed as an argument.`,\n `Pass it as --set-secret ${name}=env:<NAME>|file:<path>|stdin, or use --browser.`,\n );\n }\n }\n return plain;\n}\n\n/** An empty `--set name=` means nothing to change, as Enter does at a prompt. */\nfunction plainGiven(plain: Record<string, string>): Record<string, string> {\n return Object.fromEntries(\n Object.entries(plain).filter(([, value]) => value !== ''),\n );\n}\n\nasync function viaBrowser(\n ctx: CliContext,\n input: CollectSecretsInput,\n): Promise<Record<string, string>> {\n const problem = desktopProblem(\n input.io.env,\n input.io.platform ?? process.platform,\n );\n if (problem) {\n throw preconditionError(\n `--browser needs a desktop to open a form on: ${problem}`,\n `Pass it as ${input.fields.filter((field) => field.sensitive).length > 1 ? '--set-secret <field>=env:<NAME>|file:<path>|stdin' : '--secret-env <NAME>, --secret-file <path> or --secret-stdin'} instead.`,\n );\n }\n ctx.log(\n `Opened a secure form in your browser for ${input.title}… (waiting; if no browser opened, re-run with --secret-env, --secret-file or --secret-stdin)`,\n );\n return collectWithSecretForm({\n title: input.title,\n fields: input.fields,\n open: input.io.openUrl ?? openInBrowser,\n timeoutMs: input.io.formTimeoutMs,\n });\n}\n\n/** Why a browser form cannot be filled here, or null when it can. */\nexport function desktopProblem(\n env: Record<string, string | undefined>,\n platform: NodeJS.Platform,\n): string | null {\n const ci = env.CI?.trim().toLowerCase();\n if (ci && ci !== '0' && ci !== 'false') {\n return 'this is a CI run (CI is set), with no one to fill it in.';\n }\n if (env.SSH_CONNECTION || env.SSH_TTY) {\n return 'this is an SSH session, and a browser on this machine is not in front of you.';\n }\n if (\n platform !== 'darwin' &&\n platform !== 'win32' &&\n !env.DISPLAY &&\n !env.WAYLAND_DISPLAY\n ) {\n return 'there is no desktop session (DISPLAY or WAYLAND_DISPLAY) to open a browser in.';\n }\n return null;\n}\n\nasync function viaSources(\n input: CollectSecretsInput,\n): Promise<Record<string, string>> {\n const { flags, fields } = input;\n const byName = new Map(fields.map((field) => [field.name, field]));\n const specs = new Map<string, Spec>();\n\n for (const entry of flags.sets) {\n const at = entry.indexOf('=');\n const name = at > 0 ? entry.slice(0, at).trim() : '';\n if (!name) {\n throw usageError(\n '--set-secret takes <field>=env:<NAME>, <field>=file:<path> or <field>=stdin.',\n `For example: --set-secret ${fields.find((field) => field.sensitive)?.name ?? 'client_secret'}=env:MY_SECRET.`,\n );\n }\n const shown = FIELD_NAME.test(name) ? `\"${name}\"` : 'that field';\n const field = byName.get(name);\n if (!field) {\n throw usageError(\n `${shown} is not a field this asks for.`,\n `Its fields are: ${fields.map((entry) => entry.name).join(', ') || 'none'}.`,\n );\n }\n if (specs.has(name)) {\n throw usageError(\n `--set-secret names ${shown} twice.`,\n 'Pass each field once.',\n );\n }\n specs.set(name, parseSpec(entry.slice(at + 1), `--set-secret ${name}`));\n }\n\n const single: Spec | null = flags.stdin\n ? { kind: 'stdin' }\n : flags.env !== undefined\n ? envSpec(flags.env, '--secret-env')\n : flags.file !== undefined\n ? { kind: 'file', path: flags.file }\n : null;\n if (single) {\n const secrets = fields.filter(\n (field) => field.sensitive && !specs.has(field.name),\n );\n if (secrets.length !== 1) {\n throw usageError(\n secrets.length === 0\n ? 'There is no secret here for --secret-env, --secret-file or --secret-stdin to fill.'\n : `This asks for ${secrets.length} secrets (${secrets.map((field) => field.name).join(', ')}), so name each one.`,\n secrets.length === 0\n ? 'Drop the flag.'\n : `Pass --set-secret <field>=env:<NAME>|file:<path>|stdin for each, e.g. --set-secret ${secrets[0]!.name}=env:MY_SECRET.`,\n );\n }\n specs.set(secrets[0]!.name, single);\n }\n\n if ([...specs.values()].filter((spec) => spec.kind === 'stdin').length > 1) {\n throw usageError(\n 'Only one secret can come from stdin.',\n 'Take the others from the environment (env:<NAME>) or a file (file:<path>).',\n );\n }\n\n const values: Record<string, string> = {};\n for (const [name, spec] of specs) {\n const value = (await resolveSpec(spec, name, input.io)).trim();\n if (!value) {\n throw preconditionError(\n `The value for ${name} (from ${describe(spec)}) is empty; nothing was saved.`,\n 'Put the secret there, then run the command again.',\n );\n }\n values[name] = value;\n }\n return values;\n}\n\nfunction parseSpec(raw: string, flag: string): Spec {\n if (raw === 'stdin') return { kind: 'stdin' };\n if (raw.startsWith('env:')) return envSpec(raw.slice(4), flag);\n if (raw.startsWith('file:') && raw.length > 5) {\n return { kind: 'file', path: raw.slice(5) };\n }\n throw usageError(\n `${flag}=… is not env:<NAME>, file:<path> or stdin. A secret is never passed as an argument: it would land in your shell history and the process list.`,\n `Put it in an environment variable (${flag}=env:MY_SECRET), a file only you can read (${flag}=file:./secret.txt), or pipe it in (${flag}=stdin).`,\n );\n}\n\nfunction envSpec(name: string, flag: string): Spec {\n if (ENV_NAME.test(name)) return { kind: 'env', name };\n // Never echoed: a value that is not a name may be the secret itself.\n throw usageError(\n `${flag} takes the NAME of an environment variable (letters, digits and _), never the secret itself.`,\n `For example: MY_SECRET=… then ${flag}${flag.startsWith('--set-secret') ? '=env:' : ' '}MY_SECRET.`,\n );\n}\n\nfunction describe(spec: Spec): string {\n if (spec.kind === 'env') return `$${spec.name}`;\n if (spec.kind === 'file') return 'the file';\n return 'stdin';\n}\n\nasync function resolveSpec(\n spec: Spec,\n field: string,\n io: SecretIO,\n): Promise<string> {\n if (spec.kind === 'env') {\n const value = io.env[spec.name];\n if (value === undefined || value === '') {\n throw preconditionError(\n `The environment variable ${spec.name} (for ${field}) is not set; nothing was saved.`,\n `Set it in the environment the command runs in, then run it again.`,\n );\n }\n return value;\n }\n if (spec.kind === 'file') return readSecretFile(spec.path, field, io.cwd);\n return readStdinSecret(io);\n}\n\nfunction readSecretFile(\n given: string,\n field: string,\n cwd: string = process.cwd(),\n): string {\n const result = readPrivateFileNoFollow(\n path.resolve(cwd, given),\n MAX_SECRET_BYTES,\n );\n if (result.ok) return stripNewline(result.bytes.toString('utf8'));\n // The path is not echoed: a mistyped flag may have put the secret there.\n const what = `The file given for ${field}`;\n switch (result.problem) {\n case 'missing':\n throw notFoundError(\n `${what} does not exist; nothing was saved.`,\n 'Check the path, then run the command again.',\n );\n case 'symlink':\n throw preconditionError(\n `${what} is a symlink, and the CLI never reads a secret through one.`,\n 'Name the real file instead.',\n );\n case 'not-a-file':\n throw preconditionError(\n `${what} is not a regular file.`,\n 'Name a file holding just the secret.',\n );\n case 'readable-by-others':\n throw preconditionError(\n `${what} can be read by other users on this machine (mode ${(result.mode ?? 0).toString(8).padStart(3, '0')}), so it was not read.`,\n 'Make it yours alone with \"chmod 600 <file>\", then run the command again.',\n );\n case 'not-yours':\n throw preconditionError(\n `${what} belongs to another user, so it was not read.`,\n 'Use a file you own, readable only by you (chmod 600).',\n );\n case 'too-large':\n throw preconditionError(\n `${what} is larger than a secret (over 64 KB).`,\n 'Name a file holding just the secret.',\n );\n }\n}\n\nasync function readStdinSecret(io: SecretIO): Promise<string> {\n if (io.stdinIsTTY) {\n throw usageError(\n 'stdin is a terminal, so there is no piped secret to read.',\n 'Pipe it in (e.g. `printf %s \"$MY_SECRET\" | ekanos …`), or use --secret-env or --secret-file.',\n );\n }\n const raw = await (io.readStdin ?? readAllStdin)();\n return stripNewline(raw);\n}\n\nfunction readAllStdin(): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n const stdin = process.stdin;\n stdin.on('data', (chunk: Buffer | string) => {\n const buffer = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;\n size += buffer.length;\n if (size > MAX_SECRET_BYTES) {\n stdin.destroy();\n reject(\n usageError(\n 'stdin holds more than a secret (over 64 KB).',\n 'Pipe in just the secret.',\n ),\n );\n return;\n }\n chunks.push(buffer);\n });\n stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));\n stdin.on('error', reject);\n });\n}\n\n/** One trailing newline (`echo`, an editor's final line) is not part of it. */\nfunction stripNewline(value: string): string {\n return value.replace(/\\r?\\n$/, '');\n}\n"]}
@@ -7,9 +7,9 @@ import { type TeamsApiResult } from './teams-api.js';
7
7
  * GET /api/partner/teams/<team>/integrations/<product>/connect?source=<slug>[&as=<email>][&connection=<id>]
8
8
  * → { plan }
9
9
  * POST /api/partner/teams/<team>/integrations/<product>/connect?source=<slug>
10
- * { as?, fields?, connection? }
10
+ * { as?, fields?, set?, connection? }
11
11
  * → { plan, result: "activated" } fields / no-fields
12
- * → { plan, result: "consent", authorizeUrl, expiresAt } oauth
12
+ * → { plan, result: "consent", authorizeUrl, expiresAt } oauth (fields too, for signIn "app")
13
13
  *
14
14
  * The plan's `mode`, decided by the host, says how the product is
15
15
  * connected. The shapes are read here rather than from
@@ -49,6 +49,10 @@ export declare const TeamConnectPlanSchema: z.ZodObject<{
49
49
  unsupported: "unsupported";
50
50
  "works-as-shipped": "works-as-shipped";
51
51
  }>;
52
+ signIn: z.ZodOptional<z.ZodEnum<{
53
+ app: "app";
54
+ partner: "partner";
55
+ }>>;
52
56
  reason: z.ZodNullable<z.ZodString>;
53
57
  fields: z.ZodArray<z.ZodObject<{
54
58
  name: z.ZodString;
@@ -56,6 +60,19 @@ export declare const TeamConnectPlanSchema: z.ZodObject<{
56
60
  description: z.ZodNullable<z.ZodString>;
57
61
  sensitive: z.ZodBoolean;
58
62
  required: z.ZodBoolean;
63
+ type: z.ZodDefault<z.ZodEnum<{
64
+ boolean: "boolean";
65
+ json: "json";
66
+ list: "list";
67
+ number: "number";
68
+ select: "select";
69
+ text: "text";
70
+ }>>;
71
+ options: z.ZodDefault<z.ZodNullable<z.ZodArray<z.ZodString>>>;
72
+ itemType: z.ZodDefault<z.ZodNullable<z.ZodEnum<{
73
+ number: "number";
74
+ text: "text";
75
+ }>>>;
59
76
  }, z.core.$strip>>;
60
77
  teamApp: z.ZodBoolean;
61
78
  member: z.ZodNullable<z.ZodObject<{
@@ -78,6 +95,11 @@ export declare const TeamConnectPlanSchema: z.ZodObject<{
78
95
  }, z.core.$strip>>;
79
96
  }, z.core.$strip>;
80
97
  export type TeamConnectPlan = z.infer<typeof TeamConnectPlanSchema>;
98
+ export type TeamConnectField = TeamConnectPlan['fields'][number];
99
+ /** A field's value as sent: text, or typed by `connect-fields.ts`. */
100
+ export type FieldValue = string | number | boolean | null | FieldValue[] | {
101
+ [key: string]: FieldValue;
102
+ };
81
103
  declare const ShowSchema: z.ZodObject<{
82
104
  plan: z.ZodObject<{
83
105
  source: z.ZodObject<{
@@ -112,6 +134,10 @@ declare const ShowSchema: z.ZodObject<{
112
134
  unsupported: "unsupported";
113
135
  "works-as-shipped": "works-as-shipped";
114
136
  }>;
137
+ signIn: z.ZodOptional<z.ZodEnum<{
138
+ app: "app";
139
+ partner: "partner";
140
+ }>>;
115
141
  reason: z.ZodNullable<z.ZodString>;
116
142
  fields: z.ZodArray<z.ZodObject<{
117
143
  name: z.ZodString;
@@ -119,6 +145,19 @@ declare const ShowSchema: z.ZodObject<{
119
145
  description: z.ZodNullable<z.ZodString>;
120
146
  sensitive: z.ZodBoolean;
121
147
  required: z.ZodBoolean;
148
+ type: z.ZodDefault<z.ZodEnum<{
149
+ boolean: "boolean";
150
+ json: "json";
151
+ list: "list";
152
+ number: "number";
153
+ select: "select";
154
+ text: "text";
155
+ }>>;
156
+ options: z.ZodDefault<z.ZodNullable<z.ZodArray<z.ZodString>>>;
157
+ itemType: z.ZodDefault<z.ZodNullable<z.ZodEnum<{
158
+ number: "number";
159
+ text: "text";
160
+ }>>>;
122
161
  }, z.core.$strip>>;
123
162
  teamApp: z.ZodBoolean;
124
163
  member: z.ZodNullable<z.ZodObject<{
@@ -175,6 +214,10 @@ declare const StartSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
175
214
  unsupported: "unsupported";
176
215
  "works-as-shipped": "works-as-shipped";
177
216
  }>;
217
+ signIn: z.ZodOptional<z.ZodEnum<{
218
+ app: "app";
219
+ partner: "partner";
220
+ }>>;
178
221
  reason: z.ZodNullable<z.ZodString>;
179
222
  fields: z.ZodArray<z.ZodObject<{
180
223
  name: z.ZodString;
@@ -182,6 +225,19 @@ declare const StartSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
182
225
  description: z.ZodNullable<z.ZodString>;
183
226
  sensitive: z.ZodBoolean;
184
227
  required: z.ZodBoolean;
228
+ type: z.ZodDefault<z.ZodEnum<{
229
+ boolean: "boolean";
230
+ json: "json";
231
+ list: "list";
232
+ number: "number";
233
+ select: "select";
234
+ text: "text";
235
+ }>>;
236
+ options: z.ZodDefault<z.ZodNullable<z.ZodArray<z.ZodString>>>;
237
+ itemType: z.ZodDefault<z.ZodNullable<z.ZodEnum<{
238
+ number: "number";
239
+ text: "text";
240
+ }>>>;
185
241
  }, z.core.$strip>>;
186
242
  teamApp: z.ZodBoolean;
187
243
  member: z.ZodNullable<z.ZodObject<{
@@ -238,6 +294,10 @@ declare const StartSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
238
294
  unsupported: "unsupported";
239
295
  "works-as-shipped": "works-as-shipped";
240
296
  }>;
297
+ signIn: z.ZodOptional<z.ZodEnum<{
298
+ app: "app";
299
+ partner: "partner";
300
+ }>>;
241
301
  reason: z.ZodNullable<z.ZodString>;
242
302
  fields: z.ZodArray<z.ZodObject<{
243
303
  name: z.ZodString;
@@ -245,6 +305,19 @@ declare const StartSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
245
305
  description: z.ZodNullable<z.ZodString>;
246
306
  sensitive: z.ZodBoolean;
247
307
  required: z.ZodBoolean;
308
+ type: z.ZodDefault<z.ZodEnum<{
309
+ boolean: "boolean";
310
+ json: "json";
311
+ list: "list";
312
+ number: "number";
313
+ select: "select";
314
+ text: "text";
315
+ }>>;
316
+ options: z.ZodDefault<z.ZodNullable<z.ZodArray<z.ZodString>>>;
317
+ itemType: z.ZodDefault<z.ZodNullable<z.ZodEnum<{
318
+ number: "number";
319
+ text: "text";
320
+ }>>>;
248
321
  }, z.core.$strip>>;
249
322
  teamApp: z.ZodBoolean;
250
323
  member: z.ZodNullable<z.ZodObject<{
@@ -281,6 +354,8 @@ interface Target {
281
354
  export declare function showTeamConnect(host: string, accessToken: string, target: Target, asEmail?: string): Promise<TeamsApiResult<z.infer<typeof ShowSchema>>>;
282
355
  export declare function startTeamConnect(host: string, accessToken: string, target: Target, body: {
283
356
  as?: string;
284
- fields?: Record<string, string>;
357
+ fields?: Record<string, FieldValue>;
358
+ /** The field names given by `--set`, not a prompt: the host refuses a secret among them. */
359
+ set?: string[];
285
360
  }): Promise<TeamsApiResult<TeamConnectStart>>;
286
361
  export {};