terse-cli 0.9.19 → 0.9.21

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 (126) hide show
  1. package/README.md +2 -2
  2. package/dist/cliUi.d.ts +3 -1
  3. package/dist/cliUi.d.ts.map +1 -1
  4. package/dist/cliUi.js +3 -2
  5. package/dist/cliUi.js.map +1 -1
  6. package/dist/commands/actor.d.ts.map +1 -1
  7. package/dist/commands/actor.js +32 -5
  8. package/dist/commands/actor.js.map +1 -1
  9. package/dist/commands/actorGenerate.d.ts.map +1 -1
  10. package/dist/commands/actorGenerate.js +22 -86
  11. package/dist/commands/actorGenerate.js.map +1 -1
  12. package/dist/commands/attach.d.ts +0 -7
  13. package/dist/commands/attach.d.ts.map +1 -1
  14. package/dist/commands/attach.js +220 -189
  15. package/dist/commands/attach.js.map +1 -1
  16. package/dist/commands/deploy.d.ts.map +1 -1
  17. package/dist/commands/deploy.js +15 -3
  18. package/dist/commands/deploy.js.map +1 -1
  19. package/dist/commands/generate.d.ts +0 -4
  20. package/dist/commands/generate.d.ts.map +1 -1
  21. package/dist/commands/generate.js +161 -127
  22. package/dist/commands/generate.js.map +1 -1
  23. package/dist/commands/history.d.ts +0 -15
  24. package/dist/commands/history.d.ts.map +1 -1
  25. package/dist/commands/history.js +139 -105
  26. package/dist/commands/history.js.map +1 -1
  27. package/dist/commands/install.d.ts.map +1 -1
  28. package/dist/commands/install.js +6 -4
  29. package/dist/commands/install.js.map +1 -1
  30. package/dist/commands/integrate.d.ts +0 -48
  31. package/dist/commands/integrate.d.ts.map +1 -1
  32. package/dist/commands/integrate.js +897 -699
  33. package/dist/commands/integrate.js.map +1 -1
  34. package/dist/commands/integrateUse.d.ts +0 -12
  35. package/dist/commands/integrateUse.d.ts.map +1 -1
  36. package/dist/commands/integrateUse.js +132 -98
  37. package/dist/commands/integrateUse.js.map +1 -1
  38. package/dist/commands/listen.d.ts +0 -7
  39. package/dist/commands/listen.d.ts.map +1 -1
  40. package/dist/commands/listen.js +98 -81
  41. package/dist/commands/listen.js.map +1 -1
  42. package/dist/commands/memory.d.ts +0 -21
  43. package/dist/commands/memory.d.ts.map +1 -1
  44. package/dist/commands/memory.js +147 -128
  45. package/dist/commands/memory.js.map +1 -1
  46. package/dist/commands/replay.d.ts +0 -2
  47. package/dist/commands/replay.d.ts.map +1 -1
  48. package/dist/commands/replay.js +37 -28
  49. package/dist/commands/replay.js.map +1 -1
  50. package/dist/commands/resume.d.ts +0 -4
  51. package/dist/commands/resume.d.ts.map +1 -1
  52. package/dist/commands/resume.js +34 -28
  53. package/dist/commands/resume.js.map +1 -1
  54. package/dist/commands/run.d.ts +0 -2
  55. package/dist/commands/run.d.ts.map +1 -1
  56. package/dist/commands/run.js +78 -70
  57. package/dist/commands/run.js.map +1 -1
  58. package/dist/commands/state.d.ts +0 -18
  59. package/dist/commands/state.d.ts.map +1 -1
  60. package/dist/commands/state.js +129 -108
  61. package/dist/commands/state.js.map +1 -1
  62. package/dist/commands/test.d.ts +0 -28
  63. package/dist/commands/test.d.ts.map +1 -1
  64. package/dist/commands/test.js +509 -403
  65. package/dist/commands/test.js.map +1 -1
  66. package/dist/commands/toolRun.d.ts +0 -6
  67. package/dist/commands/toolRun.d.ts.map +1 -1
  68. package/dist/commands/toolRun.js +149 -134
  69. package/dist/commands/toolRun.js.map +1 -1
  70. package/dist/index.js +1 -1
  71. package/dist/index.js.map +1 -1
  72. package/dist/integrationApi.d.ts +0 -15
  73. package/dist/integrationApi.d.ts.map +1 -1
  74. package/dist/integrationApi.js +75 -49
  75. package/dist/integrationApi.js.map +1 -1
  76. package/dist/loadJob.d.ts +0 -11
  77. package/dist/loadJob.d.ts.map +1 -1
  78. package/dist/loadJob.js +54 -46
  79. package/dist/loadJob.js.map +1 -1
  80. package/dist/providers/LanguageProvider.d.ts +0 -29
  81. package/dist/providers/LanguageProvider.d.ts.map +1 -1
  82. package/dist/providers/LanguageProvider.js +3 -0
  83. package/dist/providers/LanguageProvider.js.map +1 -1
  84. package/dist/providers/typescript/TypeScriptProvider.d.ts +0 -24
  85. package/dist/providers/typescript/TypeScriptProvider.d.ts.map +1 -1
  86. package/dist/providers/typescript/TypeScriptProvider.js +113 -113
  87. package/dist/providers/typescript/TypeScriptProvider.js.map +1 -1
  88. package/dist/providers/typescript/runtimes/JobRuntime.d.ts +0 -26
  89. package/dist/providers/typescript/runtimes/JobRuntime.d.ts.map +1 -1
  90. package/dist/providers/typescript/runtimes/JobRuntime.js +51 -18
  91. package/dist/providers/typescript/runtimes/JobRuntime.js.map +1 -1
  92. package/dist/providers/typescript/runtimes/directJobRuntime.d.ts +0 -2
  93. package/dist/providers/typescript/runtimes/directJobRuntime.d.ts.map +1 -1
  94. package/dist/providers/typescript/runtimes/directJobRuntime.js +60 -49
  95. package/dist/providers/typescript/runtimes/directJobRuntime.js.map +1 -1
  96. package/dist/providers/typescript/runtimes/durableJobRuntime.d.ts +0 -2
  97. package/dist/providers/typescript/runtimes/durableJobRuntime.d.ts.map +1 -1
  98. package/dist/providers/typescript/runtimes/durableJobRuntime.js +168 -146
  99. package/dist/providers/typescript/runtimes/durableJobRuntime.js.map +1 -1
  100. package/dist/providers/typescript/runtimes/index.d.ts +0 -3
  101. package/dist/providers/typescript/runtimes/index.d.ts.map +1 -1
  102. package/dist/providers/typescript/runtimes/index.js +5 -2
  103. package/dist/providers/typescript/runtimes/index.js.map +1 -1
  104. package/dist/providers/typescript/runtimes/session.d.ts +0 -4
  105. package/dist/providers/typescript/runtimes/session.d.ts.map +1 -1
  106. package/dist/providers/typescript/runtimes/session.js +78 -64
  107. package/dist/providers/typescript/runtimes/session.js.map +1 -1
  108. package/dist/providers/typescript/terseWorkflow.d.ts +0 -7
  109. package/dist/providers/typescript/terseWorkflow.d.ts.map +1 -1
  110. package/dist/providers/typescript/terseWorkflow.js +12 -7
  111. package/dist/providers/typescript/terseWorkflow.js.map +1 -1
  112. package/dist/runLocalTestJob.d.ts +0 -19
  113. package/dist/runLocalTestJob.d.ts.map +1 -1
  114. package/dist/runLocalTestJob.js +55 -29
  115. package/dist/runLocalTestJob.js.map +1 -1
  116. package/dist/setup.js +1 -1
  117. package/dist/setup.js.map +1 -1
  118. package/dist/sourceArchive.d.ts +1 -1
  119. package/dist/sourceArchive.d.ts.map +1 -1
  120. package/dist/sourceArchive.js +1 -0
  121. package/dist/sourceArchive.js.map +1 -1
  122. package/package.json +3 -4
  123. package/templates/typescript/init/package.json.hbs +1 -2
  124. package/templates/typescript/terse-transport.d.ts +25 -0
  125. package/templates/typescript/terse-transport.js +117 -0
  126. package/templates/typescript/tsconfig.json +13 -0
@@ -1,700 +1,898 @@
1
- import { cancel, confirm, group, intro, isCancel, log, outro, password, select, text } from "@clack/prompts";
2
- import { input } from "@inquirer/prompts";
3
- import chalk from "chalk";
4
- import { INTEGRATION_METADATA, IntegrationType } from "terse-types";
5
- import { InstallationOptionsSchemas } from "terse-types/Integrations";
6
- import { readApiKey, readApiKeyOrBail } from "../api.js";
7
- import { CliError } from "../cliError.js";
8
- import { coerceAndValidateForSchema, isNonInteractive, parseKeyValueFlags, readFieldsFromStdin } from "../cliHelpers.js";
9
- import { createSpinner, formatSummaryList } from "../cliUi.js";
10
- import { disconnectIntegration, fetchInstallationUrl, fetchIntegrationConnections, fetchIntegrationFields, fetchIntegrations, pollForConnection, submitIntegrationForm } from "../integrationApi.js";
11
- import { openUrlInBrowser } from "../openBrowser.js";
12
- import { readProjectConfig } from "../projectConfig.js";
13
- import { fetchToolDetails } from "../toolCatalog.js";
14
- import { generate } from "./generate.js";
15
- const INTERNAL_INTEGRATION_TYPES = new Set([IntegrationType.TERSE, IntegrationType.CRON_JOB, IntegrationType.WEBMONITOR]);
16
- const SKIP_VALUE = "skip";
17
- export async function integrate(options = {}) {
18
- const showLifecycle = options.showLifecycle ?? true;
19
- const runGenerateAfterChange = options.runGenerateAfterChange ?? true;
20
- if (showLifecycle) {
21
- intro("terse integrate");
22
- }
23
- const apiKey = options.apiKey?.trim() || readApiKeyOrBail();
24
- let didChangeAnyIntegration = false;
25
- let continueLoop = true;
26
- while (continueLoop) {
27
- const result = await connectOneIntegration(apiKey);
28
- didChangeAnyIntegration = didChangeAnyIntegration || (result.status !== "unchanged" && result.status !== "skipped");
29
- if (result.status === "skipped")
30
- break;
31
- continueLoop = abortIfCancelled(await confirm({
32
- message: "Connect another integration?",
33
- initialValue: false
34
- }));
35
- }
36
- if (didChangeAnyIntegration && runGenerateAfterChange) {
37
- await generate(undefined, { apiKey });
38
- }
39
- if (showLifecycle) {
40
- outro("Done");
41
- }
42
- }
43
- export async function listAndPromptIntegrations(options = {}) {
44
- const apiKey = options.apiKey?.trim() || readApiKey();
45
- if (!apiKey)
46
- return;
47
- const s = createSpinner();
48
- s.start("Fetching integrations");
49
- let integrations;
50
- try {
51
- integrations = await fetchIntegrations(apiKey);
52
- s.stop("Fetched integrations");
53
- }
54
- catch {
55
- s.stop("Failed to fetch integrations");
56
- console.error(chalk.red("Failed to fetch integrations"));
57
- return;
58
- }
59
- const active = integrations.filter(i => i.isActive && !INTERNAL_INTEGRATION_TYPES.has(i.integrationType));
60
- if (options.nonInteractive || isNonInteractive()) {
61
- if (active.length > 0) {
62
- const names = active.map(i => INTEGRATION_METADATA[i.integrationType]?.name || i.integrationType);
63
- console.log(chalk.dim(`Connected integrations (${active.length}): ${formatSummaryList(names)}`));
64
- }
65
- else {
66
- console.log(chalk.dim("No integrated tools yet. Run `terse integrate` in an interactive terminal to connect some."));
67
- }
68
- return;
69
- }
70
- if (active.length > 0) {
71
- const names = active.map(i => INTEGRATION_METADATA[i.integrationType]?.name || i.integrationType);
72
- console.log(chalk.dim(`Connected integrations (${active.length}): ${formatSummaryList(names)}`));
73
- const addMore = abortIfCancelled(await confirm({
74
- message: "Connect another integration?",
75
- initialValue: false
76
- }));
77
- if (addMore)
78
- await integrate({ showLifecycle: false, runGenerateAfterChange: options.runGenerateAfterChange, apiKey });
79
- }
80
- else {
81
- log.warn(`${chalk.yellow.bold("No integrations connected.")} Jobs that depend on GitHub, Slack, Linear, etc. won't run until you connect one.`);
82
- const shouldConnect = abortIfCancelled(await confirm({
83
- message: "Connect an integration now?",
84
- initialValue: true
85
- }));
86
- if (shouldConnect) {
87
- await integrate({ showLifecycle: false, runGenerateAfterChange: options.runGenerateAfterChange, apiKey });
88
- }
89
- else {
90
- log.info(`Skipped. Run ${chalk.cyan("terse integrate")} anytime to connect one.`);
91
- }
92
- }
93
- }
94
- async function connectOneIntegration(apiKey) {
95
- while (true) {
96
- const integrations = await fetchUserFacingIntegrations(apiKey);
97
- if (integrations.length === 0) {
98
- log.warn("No integrations available.");
99
- return { status: "unchanged" };
100
- }
101
- const selection = await promptForIntegrationSelection(integrations);
102
- if (selection.action === "skip") {
103
- return { status: "skipped" };
104
- }
105
- if (selection.action === "back") {
106
- continue;
107
- }
108
- const selectedIntegration = integrations.find(integration => integration.integrationType === selection.integrationType);
109
- if (!selectedIntegration) {
110
- log.warn(`Integration '${selection.integrationType}' is no longer available.`);
111
- return { status: "unchanged" };
112
- }
113
- switch (selection.action) {
114
- case "keep":
115
- return { status: "unchanged", integrationType: selection.integrationType };
116
- case "disconnect": {
117
- const didDisconnect = await handleDisconnect(apiKey, selection.integrationType);
118
- return {
119
- status: didDisconnect ? "modified" : "unchanged",
120
- integrationType: selection.integrationType
121
- };
122
- }
123
- case "connect":
124
- case "refresh_permissions": {
125
- const didUpdate = await runInstallationFlow(apiKey, selection.integrationType);
126
- return {
127
- status: didUpdate ? (selectedIntegration.isActive ? "modified" : "added") : "unchanged",
128
- integrationType: selection.integrationType
129
- };
130
- }
131
- }
132
- }
133
- }
134
- async function fetchUserFacingIntegrations(apiKey) {
135
- const s = createSpinner();
136
- s.start("Fetching integrations");
137
- try {
138
- const integrations = await fetchIntegrations(apiKey);
139
- s.stop("Fetched integrations");
140
- return applyProjectPins(integrations.filter(i => !INTERNAL_INTEGRATION_TYPES.has(i.integrationType)), apiKey);
141
- }
142
- catch (err) {
143
- s.stop("Failed to fetch integrations");
144
- throw new CliError("fetch_integrations_failed", err?.message ?? "Failed to fetch integrations.");
145
- }
146
- }
147
- async function applyProjectPins(integrations, apiKey) {
148
- const pins = readProjectConfig()?.connections;
149
- if (!pins)
150
- return integrations;
151
- return Promise.all(integrations.map(async (integration) => {
152
- const pinnedId = pins[integration.integrationType];
153
- if (!pinnedId || integration.cliDisplayState.status !== "connected")
154
- return integration;
155
- if (integration.cliDisplayState.integrationId === pinnedId)
156
- return { ...integration, pin: "ok" };
157
- const connections = await fetchIntegrationConnections(apiKey, integration.integrationType).catch(() => []);
158
- const pinned = connections.find(connection => connection.id === pinnedId);
159
- return {
160
- ...integration,
161
- pin: pinned ? "ok" : "missing",
162
- cliDisplayState: {
163
- ...integration.cliDisplayState,
164
- connectionName: pinned?.name ?? pinnedId,
165
- integrationId: pinnedId
166
- }
167
- };
168
- }));
169
- }
170
- async function promptForIntegrationSelection(integrations) {
171
- const longestName = integrations.reduce((max, integration) => Math.max(max, getIntegrationDisplayName(integration).length), 0);
172
- return group({
173
- integrationType: () => select({
174
- message: "Choose an integration",
175
- options: [
176
- ...integrations.map(integration => ({
177
- value: integration.integrationType,
178
- label: formatIntegrationPickerLabel(integration, longestName)
179
- })),
180
- { value: SKIP_VALUE, label: chalk.dim("Skip — don't connect anything") }
181
- ]
182
- }),
183
- action: ({ results }) => {
184
- const selectedType = results.integrationType;
185
- if (selectedType === SKIP_VALUE) {
186
- return Promise.resolve(SKIP_VALUE);
187
- }
188
- const selectedIntegration = integrations.find(integration => integration.integrationType === selectedType);
189
- if (!selectedIntegration) {
190
- throw new Error(`Integration '${selectedType}' is no longer available`);
191
- }
192
- return select({
193
- message: getIntegrationDisplayName(selectedIntegration),
194
- options: getIntegrationActions(selectedIntegration).map(action => ({
195
- value: action,
196
- label: formatIntegrationActionLabel(action)
197
- }))
198
- });
199
- }
200
- }, {
201
- onCancel: () => {
202
- cancel("Operation cancelled.");
203
- process.exit(0);
204
- }
205
- });
206
- }
207
- function getIntegrationActions(integration) {
208
- if (integration.cliDisplayState.status === "connected") {
209
- return ["keep", "disconnect", "refresh_permissions"];
210
- }
211
- return ["connect", "back"];
212
- }
213
- function formatIntegrationActionLabel(action) {
214
- switch (action) {
215
- case "keep":
216
- return "Keep this connection";
217
- case "disconnect":
218
- return "Disconnect";
219
- case "refresh_permissions":
220
- return "Refresh permissions";
221
- case "connect":
222
- return "Connect";
223
- case "back":
224
- return "Back";
225
- default:
226
- const exhaustiveCheck = action;
227
- throw new Error(`Unhandled integration action: ${exhaustiveCheck}`);
228
- }
229
- }
230
- function formatIntegrationPickerLabel(integration, longestName) {
231
- const name = getIntegrationDisplayName(integration).padEnd(longestName);
232
- const summary = formatIntegrationDisplaySummary(integration);
233
- return `${name} ${chalk.dim(summary)}`;
234
- }
235
- function formatIntegrationDisplaySummary(integration) {
236
- const displayState = integration.cliDisplayState;
237
- if (displayState.status === "connected") {
238
- return `${displayState.connectionLabel}: ${displayState.connectionName}${pinSuffix(integration.pin)}`;
239
- }
240
- return "Not connected";
241
- }
242
- function pinSuffix(pin) {
243
- if (pin === "missing")
244
- return " (pinned, missing)";
245
- if (pin === "ok")
246
- return " (pinned)";
247
- return "";
248
- }
249
- function getIntegrationDisplayName(integration) {
250
- return INTEGRATION_METADATA[integration.integrationType]?.name || integration.integrationType;
251
- }
252
- async function runInstallationFlow(apiKey, integrationType) {
253
- const s = createSpinner();
254
- s.start("Loading integration details");
255
- try {
256
- const fieldsResponse = await fetchIntegrationFields(apiKey, integrationType);
257
- s.stop("Loaded integration details");
258
- if (fieldsResponse.installationType === "form") {
259
- return handleFormIntegration(apiKey, integrationType, fieldsResponse.fields, fieldsResponse.setup);
260
- }
261
- if (fieldsResponse.installationType === "oauth") {
262
- return handleOAuthIntegration(apiKey, integrationType, fieldsResponse.fields);
263
- }
264
- }
265
- catch (err) {
266
- s.stop("Failed to load integration details");
267
- console.error(chalk.red(` ${err.message}`));
268
- return false;
269
- }
270
- log.warn(`Integration '${integrationType}' has an unsupported installation type.`);
271
- return false;
272
- }
273
- async function handleFormIntegration(apiKey, integrationType, fields, setup) {
274
- await showFormSetupGuidance(integrationType, setup);
275
- const formValues = {};
276
- for (const field of fields) {
277
- const promptMessage = buildFieldPromptMessage(field);
278
- if (field.type === "password") {
279
- formValues[field.name] = abortIfCancelled(await password({
280
- message: promptMessage,
281
- validate: field.required ? value => ((value?.length ?? 0) > 0 ? undefined : `${field.label} is required`) : undefined
282
- }));
283
- continue;
284
- }
285
- if (field.type === "textarea") {
286
- formValues[field.name] = await input({
287
- message: promptMessage,
288
- default: field.placeholder,
289
- validate: field.required ? value => (value.length > 0 ? true : `${field.label} is required`) : undefined
290
- });
291
- continue;
292
- }
293
- formValues[field.name] = abortIfCancelled(await text({
294
- message: promptMessage,
295
- placeholder: field.placeholder,
296
- validate: field.required ? value => ((value?.length ?? 0) > 0 ? undefined : `${field.label} is required`) : undefined
297
- }));
298
- }
299
- const s = createSpinner();
300
- s.start("Connecting integration");
301
- try {
302
- const result = await submitIntegrationForm(apiKey, integrationType, formValues);
303
- if (result.success) {
304
- s.stop("Integration connected successfully");
305
- return true;
306
- }
307
- s.stop(`Failed to connect: ${result.error || "Unknown error"}`);
308
- return false;
309
- }
310
- catch (err) {
311
- s.stop("Failed to connect integration");
312
- console.error(chalk.red(` ${err.message}`));
313
- return false;
314
- }
315
- }
316
- async function showFormSetupGuidance(integrationType, setup) {
317
- if (!setup)
318
- return;
319
- log.info(chalk.bold(`${INTEGRATION_METADATA[integrationType]?.name || integrationType} setup`));
320
- console.log(chalk.cyan(setup.url));
321
- console.log(chalk.dim(setup.title));
322
- for (const instruction of setup.instructions) {
323
- console.log(` ${chalk.dim("•")} ${instruction}`);
324
- }
325
- console.log("");
326
- const shouldOpenSetupUrl = abortIfCancelled(await confirm({
327
- message: "Open setup instructions in your browser?",
328
- initialValue: false
329
- }));
330
- if (shouldOpenSetupUrl) {
331
- openUrlInBrowser(setup.url);
332
- }
333
- }
334
- function buildFieldPromptMessage(field) {
335
- return field.hint ? `${field.label} ${chalk.dim(`(${field.hint})`)}` : field.label;
336
- }
337
- async function handleOAuthIntegration(apiKey, integrationType, configFields) {
338
- let options;
339
- if (configFields.length > 0) {
340
- const raw = {};
341
- for (const field of configFields) {
342
- const value = abortIfCancelled(await select({
343
- message: field.label,
344
- options: field.options.map(option => ({ value: option.value, label: option.label }))
345
- }));
346
- raw[field.name] = value;
347
- }
348
- options = coerceAndValidateForSchema(raw, InstallationOptionsSchemas[integrationType], { integrationType });
349
- }
350
- const s = createSpinner();
351
- s.start("Getting authorization URL");
352
- let installationDetails;
353
- try {
354
- installationDetails = await fetchInstallationUrl(apiKey, integrationType, options);
355
- s.stop("Authorization URL ready");
356
- }
357
- catch (err) {
358
- s.stop("Failed to get authorization URL");
359
- console.error(chalk.red(` ${err.message}`));
360
- return false;
361
- }
362
- log.info(`Complete authorization in your browser: ${chalk.cyan(installationDetails.oauthUrl)}`);
363
- openUrlInBrowser(installationDetails.oauthUrl);
364
- s.start("Waiting for authorization to complete");
365
- const connected = await pollForConnection(apiKey, integrationType);
366
- if (connected) {
367
- s.stop("Integration connected successfully");
368
- return true;
369
- }
370
- s.stop("Authorization timed out — please try again");
371
- return false;
372
- }
373
- async function handleDisconnect(apiKey, integrationType) {
374
- const s = createSpinner();
375
- s.start("Disconnecting integration");
376
- try {
377
- const result = await disconnectIntegration(apiKey, integrationType);
378
- if (result.success) {
379
- s.stop("Integration disconnected successfully");
380
- return true;
381
- }
382
- s.stop(`Failed to disconnect: ${result.error || "Unknown error"}`);
383
- return false;
384
- }
385
- catch (err) {
386
- s.stop("Failed to disconnect integration");
387
- console.error(chalk.red(` ${err.message}`));
388
- return false;
389
- }
390
- }
391
- export function abortIfCancelled(value) {
392
- if (isCancel(value)) {
393
- cancel("Operation cancelled.");
394
- process.exit(0);
395
- }
396
- return value;
397
- }
398
- // -------------------------------------------------------------------------
399
- // Non-interactive subcommands
400
- // -------------------------------------------------------------------------
401
- export function parseIntegrationTypeOrThrow(value) {
402
- const normalized = value.toLowerCase();
403
- const match = Object.values(IntegrationType).find(v => v === normalized);
404
- if (!match) {
405
- throw new CliError("unknown_integration_type", `Unknown integration type "${value}".`, {
406
- detail: `Supported: ${Object.values(IntegrationType)
407
- .filter(v => !INTERNAL_INTEGRATION_TYPES.has(v))
408
- .join(", ")}`
409
- });
410
- }
411
- return match;
412
- }
413
- function summaryFor(displayState) {
414
- if (displayState.status === "connected") {
415
- return { connectionLabel: displayState.connectionLabel, connectionName: displayState.connectionName };
416
- }
417
- return { connectionLabel: null, connectionName: null };
418
- }
419
- export async function integrateList(opts = {}) {
420
- const apiKey = readApiKeyOrBail();
421
- const integrations = await applyProjectPins((await fetchIntegrations(apiKey)).filter(i => !INTERNAL_INTEGRATION_TYPES.has(i.integrationType)), apiKey);
422
- const filtered = integrations.filter(i => {
423
- if (opts.status === "connected")
424
- return i.isActive;
425
- if (opts.status === "disconnected")
426
- return !i.isActive;
427
- return true;
428
- });
429
- if (opts.json) {
430
- const payload = {
431
- integrations: await Promise.all(filtered.map(async (integration) => {
432
- const { connectionLabel, connectionName } = summaryFor(integration.cliDisplayState);
433
- return {
434
- type: integration.integrationType,
435
- status: integration.isActive ? "connected" : "disconnected",
436
- name: INTEGRATION_METADATA[integration.integrationType]?.name ?? integration.integrationType,
437
- connectionLabel,
438
- connectionName,
439
- ...(integration.pin ? { pin: integration.pin } : {})
440
- };
441
- }))
442
- };
443
- process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
444
- return;
445
- }
446
- if (filtered.length === 0) {
447
- process.stdout.write("No integrations.\n");
448
- return;
449
- }
450
- const longest = filtered.reduce((max, i) => Math.max(max, (INTEGRATION_METADATA[i.integrationType]?.name ?? i.integrationType).length), 0);
451
- for (const integration of filtered) {
452
- const name = (INTEGRATION_METADATA[integration.integrationType]?.name ?? integration.integrationType).padEnd(longest);
453
- const status = integration.isActive ? chalk.green("connected") : chalk.dim("not connected");
454
- const { connectionLabel, connectionName } = summaryFor(integration.cliDisplayState);
455
- const summary = connectionLabel && connectionName ? ` ${chalk.dim(`${connectionLabel}: ${connectionName}${pinSuffix(integration.pin)}`)}` : "";
456
- process.stdout.write(` ${name} ${status}${summary}\n`);
457
- }
458
- }
459
- export async function integrateDescribe(opts) {
460
- const type = parseIntegrationTypeOrThrow(opts.integrationType);
461
- const apiKey = readApiKeyOrBail();
462
- const [integrations, fieldsResponse, toolCatalog] = await Promise.all([fetchIntegrations(apiKey), fetchIntegrationFields(apiKey, type), fetchToolDetails(apiKey).catch(() => null)]);
463
- const match = integrations.find(i => i.integrationType === type);
464
- const payload = {
465
- type,
466
- name: INTEGRATION_METADATA[type]?.name ?? type,
467
- status: match?.isActive ? "connected" : "disconnected",
468
- installationType: fieldsResponse.installationType,
469
- fields: fieldsResponse.fields,
470
- setup: fieldsResponse.setup ?? null,
471
- tools: toolCatalog ? toolCatalog.filter(tool => tool.integration === type).map(toToolSummary) : null
472
- };
473
- if (opts.json) {
474
- process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
475
- return;
476
- }
477
- process.stdout.write(`${chalk.bold(payload.name)} ${payload.status === "connected" ? chalk.green("connected") : chalk.dim("not connected")}\n`);
478
- process.stdout.write(`Installation: ${payload.installationType}\n\n`);
479
- if (payload.installationType === "form") {
480
- process.stdout.write("Fields:\n");
481
- for (const field of payload.fields) {
482
- const req = field.required ? chalk.yellow(" (required)") : chalk.dim(" (optional)");
483
- const hint = field.hint ? ` — ${field.hint}` : "";
484
- process.stdout.write(` ${chalk.cyan(field.name)} [${field.type}]${req}${chalk.dim(hint)}\n`);
485
- }
486
- if (payload.setup) {
487
- process.stdout.write(`\n${chalk.yellow(payload.setup.title)}\n`);
488
- process.stdout.write(`${chalk.cyan(payload.setup.url)}\n`);
489
- for (const instruction of payload.setup.instructions) {
490
- process.stdout.write(` ${chalk.dim("•")} ${instruction}\n`);
491
- }
492
- }
493
- }
494
- else {
495
- process.stdout.write("OAuth — use `terse integrate connect <type>` in an interactive terminal, or open the auth URL manually and poll with `terse integrate wait`.\n");
496
- }
497
- printToolSummary(payload.tools);
498
- }
499
- export async function integrateTool(opts) {
500
- const type = parseIntegrationTypeOrThrow(opts.integrationType);
501
- const apiKey = readApiKeyOrBail();
502
- const catalog = await fetchToolDetails(apiKey);
503
- const tools = catalog.filter(tool => tool.integration === type);
504
- if (opts.toolName === undefined) {
505
- printToolList(type, tools, opts.json);
506
- return;
507
- }
508
- const match = tools.find(tool => tool.name === opts.toolName);
509
- if (!match) {
510
- throw new CliError("unknown_tool", `Unknown tool '${opts.toolName}' for integration '${type}'.`, {
511
- detail: tools.length > 0 ? `Available tools: ${tools.map(tool => tool.name).join(", ")}` : `Integration '${type}' exposes no tools.`
512
- });
513
- }
514
- if (opts.json) {
515
- process.stdout.write(JSON.stringify(match, null, 2) + "\n");
516
- return;
517
- }
518
- printToolDetails(match);
519
- }
520
- function printToolList(type, tools, json) {
521
- if (json) {
522
- process.stdout.write(JSON.stringify({ type, tools: tools.map(toToolSummary) }, null, 2) + "\n");
523
- return;
524
- }
525
- if (tools.length === 0) {
526
- process.stdout.write(`Integration '${type}' exposes no tools.\n`);
527
- return;
528
- }
529
- process.stdout.write("Tools:\n");
530
- for (const tool of tools) {
531
- const readOnly = tool.isReadOnly ? chalk.dim(" (read-only)") : chalk.dim(" (write)");
532
- process.stdout.write(` ${chalk.cyan(tool.name)}${readOnly} — ${tool.description}\n`);
533
- }
534
- process.stdout.write(chalk.dim(`\nRun \`terse integrate tool ${type} <tool-name> --json\` for a tool's input/output schemas.\n`));
535
- }
536
- function printToolDetails(tool) {
537
- const readOnly = tool.isReadOnly ? chalk.dim(" (read-only)") : chalk.dim(" (write)");
538
- process.stdout.write(`${chalk.bold(tool.displayName)} ${chalk.dim(`[${tool.name}]`)}${readOnly}\n`);
539
- process.stdout.write(`Integration: ${tool.integration}\n`);
540
- process.stdout.write(`Supports approval: ${tool.supportsApproval ? "yes" : "no"}\n\n`);
541
- process.stdout.write(`${tool.description}\n`);
542
- process.stdout.write(`\nInput schema:\n${formatToolSchema(tool.inputSchema)}\n`);
543
- process.stdout.write(`\nOutput schema:\n${formatToolSchema(tool.outputSchema)}\n`);
544
- }
545
- function formatToolSchema(schema) {
546
- if (schema === null)
547
- return chalk.dim(" (not available in this CLI version — update terse-cli)");
548
- return JSON.stringify(schema, null, 2);
549
- }
550
- function toToolSummary(tool) {
551
- return {
552
- name: tool.name,
553
- displayName: tool.displayName,
554
- description: tool.description,
555
- isReadOnly: tool.isReadOnly,
556
- supportsApproval: tool.supportsApproval
557
- };
558
- }
559
- function printToolSummary(tools) {
560
- if (tools === null) {
561
- process.stdout.write(chalk.dim("\nTools: unavailable (tool definitions could not be fetched)\n"));
562
- return;
563
- }
564
- if (tools.length === 0)
565
- return;
566
- process.stdout.write("\nTools:\n");
567
- for (const tool of tools) {
568
- const readOnly = tool.isReadOnly ? chalk.dim(" (read-only)") : "";
569
- process.stdout.write(` ${chalk.cyan(tool.name)}${readOnly} — ${tool.description}\n`);
570
- }
571
- process.stdout.write(chalk.dim("\nRun `terse integrate tool <type> <tool-name> --json` for a tool's input/output schemas.\n"));
572
- }
573
- async function collectRawFields(opts) {
574
- const fromFlags = parseKeyValueFlags(opts.fieldFlags);
575
- const fromStdin = opts.fieldsStdin ? await readFieldsFromStdin() : {};
576
- return { ...fromFlags, ...fromStdin };
577
- }
578
- function validateFormFields(type, declaredFields, provided) {
579
- const knownFieldNames = new Set(declaredFields.map(f => f.name));
580
- const unknown = Object.keys(provided).filter(name => !knownFieldNames.has(name));
581
- if (unknown.length > 0) {
582
- throw new CliError("unknown_fields", `Unknown field(s): ${unknown.join(", ")}`, {
583
- detail: `Valid fields: ${declaredFields.map(f => f.name).join(", ") || "(none)"}`
584
- });
585
- }
586
- const missing = declaredFields.filter(f => f.required && !provided[f.name]).map(f => f.name);
587
- if (missing.length > 0) {
588
- throw new CliError("missing_fields", `Missing required field(s): ${missing.join(", ")}`, {
589
- detail: `Run \`terse integrate describe ${type} --json\` to see the full field schema.`
590
- });
591
- }
592
- return provided;
593
- }
594
- export async function integrateConnect(opts) {
595
- const type = parseIntegrationTypeOrThrow(opts.integrationType);
596
- const apiKey = readApiKeyOrBail();
597
- const [integrations, fieldsResponse] = await Promise.all([fetchIntegrations(apiKey), fetchIntegrationFields(apiKey, type)]);
598
- const existing = integrations.find(i => i.integrationType === type);
599
- if (existing?.isActive && !opts.force) {
600
- const message = `${INTEGRATION_METADATA[type]?.name ?? type} is already connected. Pass --force to refresh.`;
601
- if (opts.json) {
602
- process.stdout.write(JSON.stringify({ type, status: "connected", changed: false, message }, null, 2) + "\n");
603
- }
604
- else {
605
- process.stdout.write(chalk.dim(message) + "\n");
606
- }
607
- return;
608
- }
609
- const raw = await collectRawFields(opts);
610
- if (fieldsResponse.installationType === "oauth") {
611
- const optionsSchema = InstallationOptionsSchemas[type];
612
- const parsed = coerceAndValidateForSchema(raw, optionsSchema, { integrationType: type });
613
- const optionsArg = Object.keys(parsed).length > 0 ? parsed : undefined;
614
- const installation = await fetchInstallationUrl(apiKey, type, optionsArg);
615
- openUrlInBrowser(installation.oauthUrl);
616
- const waitCommand = `terse integrate wait ${type}`;
617
- if (opts.json) {
618
- const payload = {
619
- handoff: "oauth",
620
- type,
621
- status: "disconnected",
622
- url: installation.oauthUrl,
623
- waitCommand,
624
- message: `Browser opened. Run \`${waitCommand}\` to block until authorization completes.`
625
- };
626
- process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
627
- process.exitCode = 2;
628
- return;
629
- }
630
- const name = INTEGRATION_METADATA[type]?.name ?? type;
631
- process.stdout.write(chalk.yellow(`ACTION REQUIRED: authorize ${name} in your browser.`) + "\n");
632
- process.stdout.write(chalk.cyan(` ${installation.oauthUrl}`) + "\n");
633
- process.stdout.write(chalk.dim(` Browser opened. Run \`${waitCommand}\` to block until authorization completes.`) + "\n");
634
- process.exitCode = 2;
635
- return;
636
- }
637
- const declaredFields = (fieldsResponse.fields ?? []);
638
- const provided = validateFormFields(type, declaredFields, raw);
639
- const result = await submitIntegrationForm(apiKey, type, provided);
640
- if (!result.success) {
641
- throw new CliError("connect_failed", result.error ?? "Failed to connect integration.");
642
- }
643
- const message = existing?.isActive ? `${type} refreshed.` : `${type} connected.`;
644
- if (opts.json) {
645
- process.stdout.write(JSON.stringify({ type, status: "connected", changed: true, message }, null, 2) + "\n");
646
- }
647
- else {
648
- process.stdout.write(chalk.green(message) + "\n");
649
- }
650
- }
651
- export async function integrateDisconnect(opts) {
652
- const type = parseIntegrationTypeOrThrow(opts.integrationType);
653
- const apiKey = readApiKeyOrBail();
654
- const result = await disconnectIntegration(apiKey, type);
655
- if (!result.success) {
656
- throw new CliError("disconnect_failed", result.error ?? "Failed to disconnect integration.");
657
- }
658
- const message = `${type} disconnected.`;
659
- if (opts.json) {
660
- process.stdout.write(JSON.stringify({ type, status: "disconnected", changed: true, message }, null, 2) + "\n");
661
- }
662
- else {
663
- process.stdout.write(chalk.green(message) + "\n");
664
- }
665
- }
666
- const WAIT_DEFAULT_TIMEOUT_S = 300;
667
- const WAIT_MAX_TIMEOUT_S = 900;
668
- const WAIT_INTERVAL_MS = 2000;
669
- export async function integrateWait(opts) {
670
- const type = parseIntegrationTypeOrThrow(opts.integrationType);
671
- const requested = opts.timeoutSeconds ?? WAIT_DEFAULT_TIMEOUT_S;
672
- const timeoutSeconds = Math.max(1, Math.min(WAIT_MAX_TIMEOUT_S, requested));
673
- const apiKey = readApiKeyOrBail();
674
- const deadline = Date.now() + timeoutSeconds * 1000;
675
- let lastError = null;
676
- while (Date.now() < deadline) {
677
- try {
678
- const integrations = await fetchIntegrations(apiKey);
679
- const match = integrations.find(i => i.integrationType === type);
680
- if (match?.isActive) {
681
- const message = `${type} is connected.`;
682
- if (opts.json) {
683
- process.stdout.write(JSON.stringify({ type, status: "connected", message }, null, 2) + "\n");
684
- }
685
- else {
686
- process.stdout.write(chalk.green(message) + "\n");
687
- }
688
- return;
689
- }
690
- }
691
- catch (err) {
692
- lastError = err instanceof Error ? err.message : String(err);
693
- }
694
- await new Promise(resolve => setTimeout(resolve, WAIT_INTERVAL_MS));
695
- }
696
- throw new CliError("wait_timeout", `Timed out after ${timeoutSeconds}s waiting for ${type} to connect.`, {
697
- detail: lastError ? `Last error: ${lastError}` : "Check the integration's connection status with `terse integrate list`."
698
- });
699
- }
1
+ "use strict";
2
+ // LEGACY-JOBS: Commented out while Terse is actors-only. Uncomment to restore; see the note in terse-sdk/src/index.ts.
3
+ // import { cancel, confirm, group, intro, isCancel, log, outro, password, select, text } from "@clack/prompts"
4
+ // import { input } from "@inquirer/prompts"
5
+ // import chalk from "chalk"
6
+ // import type { CliIntegrationDisplayState, FormIntegrationSetup, IntegrationWithStatus } from "terse-types"
7
+ // import { INTEGRATION_METADATA, IntegrationType } from "terse-types"
8
+ // import { InstallationOptionsSchemas } from "terse-types/Integrations"
9
+ //
10
+ // import { readApiKey, readApiKeyOrBail } from "../api.js"
11
+ // import { CliError } from "../cliError.js"
12
+ // import { type NonInteractiveOpts, coerceAndValidateForSchema, isNonInteractive, parseKeyValueFlags, readFieldsFromStdin } from "../cliHelpers.js"
13
+ // import { createSpinner, formatSummaryList } from "../cliUi.js"
14
+ // import {
15
+ // ConfigurationFieldDefinition,
16
+ // FormFieldDefinition,
17
+ // disconnectIntegration,
18
+ // fetchInstallationUrl,
19
+ // fetchIntegrationConnections,
20
+ // fetchIntegrationFields,
21
+ // fetchIntegrations,
22
+ // pollForConnection,
23
+ // submitIntegrationForm
24
+ // } from "../integrationApi.js"
25
+ // import { openUrlInBrowser } from "../openBrowser.js"
26
+ // import { readProjectConfig } from "../projectConfig.js"
27
+ // import { type ToolDetails, fetchToolDetails } from "../toolCatalog.js"
28
+ //
29
+ // import { generate } from "./generate.js"
30
+ //
31
+ // const INTERNAL_INTEGRATION_TYPES = new Set<string>([IntegrationType.TERSE, IntegrationType.CRON_JOB, IntegrationType.WEBMONITOR])
32
+ //
33
+ // const SKIP_VALUE = "skip" as const
34
+ //
35
+ // export async function integrate(options: IntegrateOptions = {}): Promise<void> {
36
+ // const showLifecycle = options.showLifecycle ?? true
37
+ // const runGenerateAfterChange = options.runGenerateAfterChange ?? true
38
+ //
39
+ // if (showLifecycle) {
40
+ // intro("terse integrate")
41
+ // }
42
+ //
43
+ // const apiKey = options.apiKey?.trim() || readApiKeyOrBail()
44
+ // let didChangeAnyIntegration = false
45
+ //
46
+ // let continueLoop = true
47
+ // while (continueLoop) {
48
+ // const result = await connectOneIntegration(apiKey)
49
+ // didChangeAnyIntegration = didChangeAnyIntegration || (result.status !== "unchanged" && result.status !== "skipped")
50
+ // if (result.status === "skipped") break
51
+ // continueLoop = abortIfCancelled(
52
+ // await confirm({
53
+ // message: "Connect another integration?",
54
+ // initialValue: false
55
+ // })
56
+ // )
57
+ // }
58
+ //
59
+ // if (didChangeAnyIntegration && runGenerateAfterChange) {
60
+ // await generate(undefined, { apiKey })
61
+ // }
62
+ //
63
+ // if (showLifecycle) {
64
+ // outro("Done")
65
+ // }
66
+ // }
67
+ //
68
+ // export async function listAndPromptIntegrations(options: IntegrateOptions = {}): Promise<void> {
69
+ // const apiKey = options.apiKey?.trim() || readApiKey()
70
+ // if (!apiKey) return
71
+ //
72
+ // const s = createSpinner()
73
+ // s.start("Fetching integrations")
74
+ //
75
+ // let integrations
76
+ // try {
77
+ // integrations = await fetchIntegrations(apiKey)
78
+ // s.stop("Fetched integrations")
79
+ // } catch {
80
+ // s.stop("Failed to fetch integrations")
81
+ // console.error(chalk.red("Failed to fetch integrations"))
82
+ // return
83
+ // }
84
+ //
85
+ // const active = integrations.filter(i => i.isActive && !INTERNAL_INTEGRATION_TYPES.has(i.integrationType))
86
+ //
87
+ // if (options.nonInteractive || isNonInteractive()) {
88
+ // if (active.length > 0) {
89
+ // const names = active.map(i => INTEGRATION_METADATA[i.integrationType]?.name || i.integrationType)
90
+ // console.log(chalk.dim(`Connected integrations (${active.length}): ${formatSummaryList(names)}`))
91
+ // } else {
92
+ // console.log(chalk.dim("No integrated tools yet. Run `terse integrate` in an interactive terminal to connect some."))
93
+ // }
94
+ // return
95
+ // }
96
+ //
97
+ // if (active.length > 0) {
98
+ // const names = active.map(i => INTEGRATION_METADATA[i.integrationType]?.name || i.integrationType)
99
+ // console.log(chalk.dim(`Connected integrations (${active.length}): ${formatSummaryList(names)}`))
100
+ // const addMore = abortIfCancelled(
101
+ // await confirm({
102
+ // message: "Connect another integration?",
103
+ // initialValue: false
104
+ // })
105
+ // )
106
+ // if (addMore) await integrate({ showLifecycle: false, runGenerateAfterChange: options.runGenerateAfterChange, apiKey })
107
+ // } else {
108
+ // log.warn(`${chalk.yellow.bold("No integrations connected.")} Jobs that depend on GitHub, Slack, Linear, etc. won't run until you connect one.`)
109
+ // const shouldConnect = abortIfCancelled(
110
+ // await confirm({
111
+ // message: "Connect an integration now?",
112
+ // initialValue: true
113
+ // })
114
+ // )
115
+ // if (shouldConnect) {
116
+ // await integrate({ showLifecycle: false, runGenerateAfterChange: options.runGenerateAfterChange, apiKey })
117
+ // } else {
118
+ // log.info(`Skipped. Run ${chalk.cyan("terse integrate")} anytime to connect one.`)
119
+ // }
120
+ // }
121
+ // }
122
+ //
123
+ // async function connectOneIntegration(apiKey: string): Promise<IntegrationChangeResult> {
124
+ // while (true) {
125
+ // const integrations = await fetchUserFacingIntegrations(apiKey)
126
+ //
127
+ // if (integrations.length === 0) {
128
+ // log.warn("No integrations available.")
129
+ // return { status: "unchanged" }
130
+ // }
131
+ //
132
+ // const selection = await promptForIntegrationSelection(integrations)
133
+ // if (selection.action === "skip") {
134
+ // return { status: "skipped" }
135
+ // }
136
+ // if (selection.action === "back") {
137
+ // continue
138
+ // }
139
+ //
140
+ // const selectedIntegration = integrations.find(integration => integration.integrationType === selection.integrationType)
141
+ // if (!selectedIntegration) {
142
+ // log.warn(`Integration '${selection.integrationType}' is no longer available.`)
143
+ // return { status: "unchanged" }
144
+ // }
145
+ //
146
+ // switch (selection.action) {
147
+ // case "keep":
148
+ // return { status: "unchanged", integrationType: selection.integrationType }
149
+ // case "disconnect": {
150
+ // const didDisconnect = await handleDisconnect(apiKey, selection.integrationType)
151
+ // return {
152
+ // status: didDisconnect ? "modified" : "unchanged",
153
+ // integrationType: selection.integrationType
154
+ // }
155
+ // }
156
+ // case "connect":
157
+ // case "refresh_permissions": {
158
+ // const didUpdate = await runInstallationFlow(apiKey, selection.integrationType)
159
+ // return {
160
+ // status: didUpdate ? (selectedIntegration.isActive ? "modified" : "added") : "unchanged",
161
+ // integrationType: selection.integrationType
162
+ // }
163
+ // }
164
+ // }
165
+ // }
166
+ // }
167
+ //
168
+ // async function fetchUserFacingIntegrations(apiKey: string): Promise<UserFacingIntegration[]> {
169
+ // const s = createSpinner()
170
+ // s.start("Fetching integrations")
171
+ //
172
+ // try {
173
+ // const integrations = await fetchIntegrations(apiKey)
174
+ // s.stop("Fetched integrations")
175
+ // return applyProjectPins(
176
+ // integrations.filter(i => !INTERNAL_INTEGRATION_TYPES.has(i.integrationType)),
177
+ // apiKey
178
+ // )
179
+ // } catch (err: any) {
180
+ // s.stop("Failed to fetch integrations")
181
+ // throw new CliError("fetch_integrations_failed", err?.message ?? "Failed to fetch integrations.")
182
+ // }
183
+ // }
184
+ //
185
+ // async function applyProjectPins(integrations: IntegrationWithStatus[], apiKey: string): Promise<PinAwareIntegration[]> {
186
+ // const pins = readProjectConfig()?.connections
187
+ // if (!pins) return integrations
188
+ //
189
+ // return Promise.all(
190
+ // integrations.map(async integration => {
191
+ // const pinnedId = pins[integration.integrationType]
192
+ // if (!pinnedId || integration.cliDisplayState.status !== "connected") return integration
193
+ // if (integration.cliDisplayState.integrationId === pinnedId) return { ...integration, pin: "ok" as const }
194
+ //
195
+ // const connections = await fetchIntegrationConnections(apiKey, integration.integrationType).catch(() => [])
196
+ // const pinned = connections.find(connection => connection.id === pinnedId)
197
+ // return {
198
+ // ...integration,
199
+ // pin: pinned ? ("ok" as const) : ("missing" as const),
200
+ // cliDisplayState: {
201
+ // ...integration.cliDisplayState,
202
+ // connectionName: pinned?.name ?? pinnedId,
203
+ // integrationId: pinnedId
204
+ // }
205
+ // }
206
+ // })
207
+ // )
208
+ // }
209
+ //
210
+ // async function promptForIntegrationSelection(integrations: UserFacingIntegration[]): Promise<GroupedIntegrationSelection> {
211
+ // const longestName = integrations.reduce((max, integration) => Math.max(max, getIntegrationDisplayName(integration).length), 0)
212
+ //
213
+ // return group(
214
+ // {
215
+ // integrationType: () =>
216
+ // select<IntegrationType | typeof SKIP_VALUE>({
217
+ // message: "Choose an integration",
218
+ // options: [
219
+ // ...integrations.map(integration => ({
220
+ // value: integration.integrationType,
221
+ // label: formatIntegrationPickerLabel(integration, longestName)
222
+ // })),
223
+ // { value: SKIP_VALUE, label: chalk.dim("Skip — don't connect anything") }
224
+ // ]
225
+ // }),
226
+ // action: ({ results }) => {
227
+ // const selectedType = results.integrationType as IntegrationType | typeof SKIP_VALUE
228
+ // if (selectedType === SKIP_VALUE) {
229
+ // return Promise.resolve(SKIP_VALUE)
230
+ // }
231
+ // const selectedIntegration = integrations.find(integration => integration.integrationType === selectedType)
232
+ // if (!selectedIntegration) {
233
+ // throw new Error(`Integration '${selectedType}' is no longer available`)
234
+ // }
235
+ //
236
+ // return select({
237
+ // message: getIntegrationDisplayName(selectedIntegration),
238
+ // options: getIntegrationActions(selectedIntegration).map(action => ({
239
+ // value: action,
240
+ // label: formatIntegrationActionLabel(action)
241
+ // }))
242
+ // })
243
+ // }
244
+ // },
245
+ // {
246
+ // onCancel: () => {
247
+ // cancel("Operation cancelled.")
248
+ // process.exit(0)
249
+ // }
250
+ // }
251
+ // ) as Promise<GroupedIntegrationSelection>
252
+ // }
253
+ //
254
+ // function getIntegrationActions(integration: UserFacingIntegration): IntegrationAction[] {
255
+ // if (integration.cliDisplayState.status === "connected") {
256
+ // return ["keep", "disconnect", "refresh_permissions"]
257
+ // }
258
+ //
259
+ // return ["connect", "back"]
260
+ // }
261
+ //
262
+ // function formatIntegrationActionLabel(action: IntegrationAction): string {
263
+ // switch (action) {
264
+ // case "keep":
265
+ // return "Keep this connection"
266
+ // case "disconnect":
267
+ // return "Disconnect"
268
+ // case "refresh_permissions":
269
+ // return "Refresh permissions"
270
+ // case "connect":
271
+ // return "Connect"
272
+ // case "back":
273
+ // return "Back"
274
+ // default:
275
+ // const exhaustiveCheck: never = action
276
+ // throw new Error(`Unhandled integration action: ${exhaustiveCheck}`)
277
+ // }
278
+ // }
279
+ //
280
+ // function formatIntegrationPickerLabel(integration: UserFacingIntegration, longestName: number): string {
281
+ // const name = getIntegrationDisplayName(integration).padEnd(longestName)
282
+ // const summary = formatIntegrationDisplaySummary(integration)
283
+ // return `${name} ${chalk.dim(summary)}`
284
+ // }
285
+ //
286
+ // function formatIntegrationDisplaySummary(integration: PinAwareIntegration): string {
287
+ // const displayState = integration.cliDisplayState
288
+ // if (displayState.status === "connected") {
289
+ // return `${displayState.connectionLabel}: ${displayState.connectionName}${pinSuffix(integration.pin)}`
290
+ // }
291
+ //
292
+ // return "Not connected"
293
+ // }
294
+ //
295
+ // function pinSuffix(pin: PinState | undefined): string {
296
+ // if (pin === "missing") return " (pinned, missing)"
297
+ // if (pin === "ok") return " (pinned)"
298
+ // return ""
299
+ // }
300
+ //
301
+ // function getIntegrationDisplayName(integration: UserFacingIntegration): string {
302
+ // return INTEGRATION_METADATA[integration.integrationType]?.name || integration.integrationType
303
+ // }
304
+ //
305
+ // async function runInstallationFlow(apiKey: string, integrationType: IntegrationType): Promise<boolean> {
306
+ // const s = createSpinner()
307
+ // s.start("Loading integration details")
308
+ //
309
+ // try {
310
+ // const fieldsResponse = await fetchIntegrationFields(apiKey, integrationType)
311
+ // s.stop("Loaded integration details")
312
+ //
313
+ // if (fieldsResponse.installationType === "form") {
314
+ // return handleFormIntegration(apiKey, integrationType, fieldsResponse.fields as FormFieldDefinition[], fieldsResponse.setup)
315
+ // }
316
+ //
317
+ // if (fieldsResponse.installationType === "oauth") {
318
+ // return handleOAuthIntegration(apiKey, integrationType, fieldsResponse.fields as ConfigurationFieldDefinition[])
319
+ // }
320
+ // } catch (err: any) {
321
+ // s.stop("Failed to load integration details")
322
+ // console.error(chalk.red(` ${err.message}`))
323
+ // return false
324
+ // }
325
+ //
326
+ // log.warn(`Integration '${integrationType}' has an unsupported installation type.`)
327
+ // return false
328
+ // }
329
+ //
330
+ // async function handleFormIntegration(apiKey: string, integrationType: IntegrationType, fields: FormFieldDefinition[], setup?: FormIntegrationSetup): Promise<boolean> {
331
+ // await showFormSetupGuidance(integrationType, setup)
332
+ //
333
+ // const formValues: Record<string, string> = {}
334
+ // for (const field of fields) {
335
+ // const promptMessage = buildFieldPromptMessage(field)
336
+ //
337
+ // if (field.type === "password") {
338
+ // formValues[field.name] = abortIfCancelled(
339
+ // await password({
340
+ // message: promptMessage,
341
+ // validate: field.required ? value => ((value?.length ?? 0) > 0 ? undefined : `${field.label} is required`) : undefined
342
+ // })
343
+ // )
344
+ // continue
345
+ // }
346
+ //
347
+ // if (field.type === "textarea") {
348
+ // formValues[field.name] = await input({
349
+ // message: promptMessage,
350
+ // default: field.placeholder,
351
+ // validate: field.required ? value => (value.length > 0 ? true : `${field.label} is required`) : undefined
352
+ // })
353
+ // continue
354
+ // }
355
+ //
356
+ // formValues[field.name] = abortIfCancelled(
357
+ // await text({
358
+ // message: promptMessage,
359
+ // placeholder: field.placeholder,
360
+ // validate: field.required ? value => ((value?.length ?? 0) > 0 ? undefined : `${field.label} is required`) : undefined
361
+ // })
362
+ // )
363
+ // }
364
+ //
365
+ // const s = createSpinner()
366
+ // s.start("Connecting integration")
367
+ // try {
368
+ // const result = await submitIntegrationForm(apiKey, integrationType, formValues)
369
+ // if (result.success) {
370
+ // s.stop("Integration connected successfully")
371
+ // return true
372
+ // }
373
+ // s.stop(`Failed to connect: ${result.error || "Unknown error"}`)
374
+ // return false
375
+ // } catch (err: any) {
376
+ // s.stop("Failed to connect integration")
377
+ // console.error(chalk.red(` ${err.message}`))
378
+ // return false
379
+ // }
380
+ // }
381
+ //
382
+ // async function showFormSetupGuidance(integrationType: IntegrationType, setup?: FormIntegrationSetup): Promise<void> {
383
+ // if (!setup) return
384
+ //
385
+ // log.info(chalk.bold(`${INTEGRATION_METADATA[integrationType]?.name || integrationType} setup`))
386
+ // console.log(chalk.cyan(setup.url))
387
+ // console.log(chalk.dim(setup.title))
388
+ //
389
+ // for (const instruction of setup.instructions) {
390
+ // console.log(` ${chalk.dim("•")} ${instruction}`)
391
+ // }
392
+ //
393
+ // console.log("")
394
+ //
395
+ // const shouldOpenSetupUrl = abortIfCancelled(
396
+ // await confirm({
397
+ // message: "Open setup instructions in your browser?",
398
+ // initialValue: false
399
+ // })
400
+ // )
401
+ //
402
+ // if (shouldOpenSetupUrl) {
403
+ // openUrlInBrowser(setup.url)
404
+ // }
405
+ // }
406
+ //
407
+ // function buildFieldPromptMessage(field: FormFieldDefinition): string {
408
+ // return field.hint ? `${field.label} ${chalk.dim(`(${field.hint})`)}` : field.label
409
+ // }
410
+ //
411
+ // async function handleOAuthIntegration(apiKey: string, integrationType: IntegrationType, configFields: ConfigurationFieldDefinition[]): Promise<boolean> {
412
+ // let options: Record<string, unknown> | undefined
413
+ // if (configFields.length > 0) {
414
+ // const raw: Record<string, string> = {}
415
+ // for (const field of configFields) {
416
+ // const value = abortIfCancelled(
417
+ // await select({
418
+ // message: field.label,
419
+ // options: field.options.map(option => ({ value: option.value, label: option.label }))
420
+ // })
421
+ // )
422
+ // raw[field.name] = value
423
+ // }
424
+ // options = coerceAndValidateForSchema(raw, InstallationOptionsSchemas[integrationType], { integrationType })
425
+ // }
426
+ //
427
+ // const s = createSpinner()
428
+ // s.start("Getting authorization URL")
429
+ //
430
+ // let installationDetails
431
+ // try {
432
+ // installationDetails = await fetchInstallationUrl(apiKey, integrationType, options)
433
+ // s.stop("Authorization URL ready")
434
+ // } catch (err: any) {
435
+ // s.stop("Failed to get authorization URL")
436
+ // console.error(chalk.red(` ${err.message}`))
437
+ // return false
438
+ // }
439
+ //
440
+ // log.info(`Complete authorization in your browser: ${chalk.cyan(installationDetails.oauthUrl)}`)
441
+ // openUrlInBrowser(installationDetails.oauthUrl)
442
+ //
443
+ // s.start("Waiting for authorization to complete")
444
+ // const connected = await pollForConnection(apiKey, integrationType)
445
+ //
446
+ // if (connected) {
447
+ // s.stop("Integration connected successfully")
448
+ // return true
449
+ // }
450
+ //
451
+ // s.stop("Authorization timed out — please try again")
452
+ // return false
453
+ // }
454
+ //
455
+ // async function handleDisconnect(apiKey: string, integrationType: IntegrationType): Promise<boolean> {
456
+ // const s = createSpinner()
457
+ // s.start("Disconnecting integration")
458
+ //
459
+ // try {
460
+ // const result = await disconnectIntegration(apiKey, integrationType)
461
+ // if (result.success) {
462
+ // s.stop("Integration disconnected successfully")
463
+ // return true
464
+ // }
465
+ //
466
+ // s.stop(`Failed to disconnect: ${result.error || "Unknown error"}`)
467
+ // return false
468
+ // } catch (err: any) {
469
+ // s.stop("Failed to disconnect integration")
470
+ // console.error(chalk.red(` ${err.message}`))
471
+ // return false
472
+ // }
473
+ // }
474
+ //
475
+ // export function abortIfCancelled<T>(value: T | symbol): T {
476
+ // if (isCancel(value)) {
477
+ // cancel("Operation cancelled.")
478
+ // process.exit(0)
479
+ // }
480
+ //
481
+ // return value
482
+ // }
483
+ //
484
+ // // -------------------------------------------------------------------------
485
+ // // Non-interactive subcommands
486
+ // // -------------------------------------------------------------------------
487
+ //
488
+ // export function parseIntegrationTypeOrThrow(value: string): IntegrationType {
489
+ // const normalized = value.toLowerCase()
490
+ // const match = Object.values(IntegrationType).find(v => v === normalized)
491
+ // if (!match) {
492
+ // throw new CliError("unknown_integration_type", `Unknown integration type "${value}".`, {
493
+ // detail: `Supported: ${Object.values(IntegrationType)
494
+ // .filter(v => !INTERNAL_INTEGRATION_TYPES.has(v))
495
+ // .join(", ")}`
496
+ // })
497
+ // }
498
+ // return match as IntegrationType
499
+ // }
500
+ //
501
+ // function summaryFor(displayState: CliIntegrationDisplayState): { connectionLabel: string | null; connectionName: string | null } {
502
+ // if (displayState.status === "connected") {
503
+ // return { connectionLabel: displayState.connectionLabel, connectionName: displayState.connectionName }
504
+ // }
505
+ // return { connectionLabel: null, connectionName: null }
506
+ // }
507
+ //
508
+ // export async function integrateList(opts: IntegrateListOpts = {}): Promise<void> {
509
+ // const apiKey = readApiKeyOrBail()
510
+ // const integrations = await applyProjectPins(
511
+ // (await fetchIntegrations(apiKey)).filter(i => !INTERNAL_INTEGRATION_TYPES.has(i.integrationType)),
512
+ // apiKey
513
+ // )
514
+ //
515
+ // const filtered = integrations.filter(i => {
516
+ // if (opts.status === "connected") return i.isActive
517
+ // if (opts.status === "disconnected") return !i.isActive
518
+ // return true
519
+ // })
520
+ //
521
+ // if (opts.json) {
522
+ // const payload = {
523
+ // integrations: await Promise.all(
524
+ // filtered.map(async integration => {
525
+ // const { connectionLabel, connectionName } = summaryFor(integration.cliDisplayState)
526
+ // return {
527
+ // type: integration.integrationType,
528
+ // status: integration.isActive ? "connected" : "disconnected",
529
+ // name: INTEGRATION_METADATA[integration.integrationType]?.name ?? integration.integrationType,
530
+ // connectionLabel,
531
+ // connectionName,
532
+ // ...(integration.pin ? { pin: integration.pin } : {})
533
+ // }
534
+ // })
535
+ // )
536
+ // }
537
+ // process.stdout.write(JSON.stringify(payload, null, 2) + "\n")
538
+ // return
539
+ // }
540
+ //
541
+ // if (filtered.length === 0) {
542
+ // process.stdout.write("No integrations.\n")
543
+ // return
544
+ // }
545
+ //
546
+ // const longest = filtered.reduce((max, i) => Math.max(max, (INTEGRATION_METADATA[i.integrationType]?.name ?? i.integrationType).length), 0)
547
+ // for (const integration of filtered) {
548
+ // const name = (INTEGRATION_METADATA[integration.integrationType]?.name ?? integration.integrationType).padEnd(longest)
549
+ // const status = integration.isActive ? chalk.green("connected") : chalk.dim("not connected")
550
+ // const { connectionLabel, connectionName } = summaryFor(integration.cliDisplayState)
551
+ // const summary = connectionLabel && connectionName ? ` ${chalk.dim(`${connectionLabel}: ${connectionName}${pinSuffix(integration.pin)}`)}` : ""
552
+ // process.stdout.write(` ${name} ${status}${summary}\n`)
553
+ // }
554
+ // }
555
+ //
556
+ // export async function integrateDescribe(opts: IntegrateDescribeOpts): Promise<void> {
557
+ // const type = parseIntegrationTypeOrThrow(opts.integrationType)
558
+ // const apiKey = readApiKeyOrBail()
559
+ //
560
+ // const [integrations, fieldsResponse, toolCatalog] = await Promise.all([fetchIntegrations(apiKey), fetchIntegrationFields(apiKey, type), fetchToolDetails(apiKey).catch(() => null)])
561
+ // const match = integrations.find(i => i.integrationType === type)
562
+ //
563
+ // const payload = {
564
+ // type,
565
+ // name: INTEGRATION_METADATA[type]?.name ?? type,
566
+ // status: match?.isActive ? "connected" : "disconnected",
567
+ // installationType: fieldsResponse.installationType,
568
+ // fields: fieldsResponse.fields,
569
+ // setup: fieldsResponse.setup ?? null,
570
+ // tools: toolCatalog ? toolCatalog.filter(tool => tool.integration === type).map(toToolSummary) : null
571
+ // }
572
+ //
573
+ // if (opts.json) {
574
+ // process.stdout.write(JSON.stringify(payload, null, 2) + "\n")
575
+ // return
576
+ // }
577
+ //
578
+ // process.stdout.write(`${chalk.bold(payload.name)} ${payload.status === "connected" ? chalk.green("connected") : chalk.dim("not connected")}\n`)
579
+ // process.stdout.write(`Installation: ${payload.installationType}\n\n`)
580
+ // if (payload.installationType === "form") {
581
+ // process.stdout.write("Fields:\n")
582
+ // for (const field of payload.fields as FormFieldDefinition[]) {
583
+ // const req = field.required ? chalk.yellow(" (required)") : chalk.dim(" (optional)")
584
+ // const hint = field.hint ? ` — ${field.hint}` : ""
585
+ // process.stdout.write(` ${chalk.cyan(field.name)} [${field.type}]${req}${chalk.dim(hint)}\n`)
586
+ // }
587
+ // if (payload.setup) {
588
+ // process.stdout.write(`\n${chalk.yellow(payload.setup.title)}\n`)
589
+ // process.stdout.write(`${chalk.cyan(payload.setup.url)}\n`)
590
+ // for (const instruction of payload.setup.instructions) {
591
+ // process.stdout.write(` ${chalk.dim("•")} ${instruction}\n`)
592
+ // }
593
+ // }
594
+ // } else {
595
+ // process.stdout.write("OAuth — use `terse integrate connect <type>` in an interactive terminal, or open the auth URL manually and poll with `terse integrate wait`.\n")
596
+ // }
597
+ // printToolSummary(payload.tools)
598
+ // }
599
+ //
600
+ // export async function integrateTool(opts: IntegrateToolOpts): Promise<void> {
601
+ // const type = parseIntegrationTypeOrThrow(opts.integrationType)
602
+ // const apiKey = readApiKeyOrBail()
603
+ //
604
+ // const catalog = await fetchToolDetails(apiKey)
605
+ // const tools = catalog.filter(tool => tool.integration === type)
606
+ //
607
+ // if (opts.toolName === undefined) {
608
+ // printToolList(type, tools, opts.json)
609
+ // return
610
+ // }
611
+ //
612
+ // const match = tools.find(tool => tool.name === opts.toolName)
613
+ // if (!match) {
614
+ // throw new CliError("unknown_tool", `Unknown tool '${opts.toolName}' for integration '${type}'.`, {
615
+ // detail: tools.length > 0 ? `Available tools: ${tools.map(tool => tool.name).join(", ")}` : `Integration '${type}' exposes no tools.`
616
+ // })
617
+ // }
618
+ //
619
+ // if (opts.json) {
620
+ // process.stdout.write(JSON.stringify(match, null, 2) + "\n")
621
+ // return
622
+ // }
623
+ //
624
+ // printToolDetails(match)
625
+ // }
626
+ //
627
+ // function printToolList(type: IntegrationType, tools: ToolDetails[], json: boolean | undefined): void {
628
+ // if (json) {
629
+ // process.stdout.write(JSON.stringify({ type, tools: tools.map(toToolSummary) }, null, 2) + "\n")
630
+ // return
631
+ // }
632
+ //
633
+ // if (tools.length === 0) {
634
+ // process.stdout.write(`Integration '${type}' exposes no tools.\n`)
635
+ // return
636
+ // }
637
+ //
638
+ // process.stdout.write("Tools:\n")
639
+ // for (const tool of tools) {
640
+ // const readOnly = tool.isReadOnly ? chalk.dim(" (read-only)") : chalk.dim(" (write)")
641
+ // process.stdout.write(` ${chalk.cyan(tool.name)}${readOnly} — ${tool.description}\n`)
642
+ // }
643
+ // process.stdout.write(chalk.dim(`\nRun \`terse integrate tool ${type} <tool-name> --json\` for a tool's input/output schemas.\n`))
644
+ // }
645
+ //
646
+ // function printToolDetails(tool: ToolDetails): void {
647
+ // const readOnly = tool.isReadOnly ? chalk.dim(" (read-only)") : chalk.dim(" (write)")
648
+ // process.stdout.write(`${chalk.bold(tool.displayName)} ${chalk.dim(`[${tool.name}]`)}${readOnly}\n`)
649
+ // process.stdout.write(`Integration: ${tool.integration}\n`)
650
+ // process.stdout.write(`Supports approval: ${tool.supportsApproval ? "yes" : "no"}\n\n`)
651
+ // process.stdout.write(`${tool.description}\n`)
652
+ // process.stdout.write(`\nInput schema:\n${formatToolSchema(tool.inputSchema)}\n`)
653
+ // process.stdout.write(`\nOutput schema:\n${formatToolSchema(tool.outputSchema)}\n`)
654
+ // }
655
+ //
656
+ // function formatToolSchema(schema: unknown): string {
657
+ // if (schema === null) return chalk.dim(" (not available in this CLI version — update terse-cli)")
658
+ // return JSON.stringify(schema, null, 2)
659
+ // }
660
+ //
661
+ // function toToolSummary(tool: ToolDetails): ToolSummary {
662
+ // return {
663
+ // name: tool.name,
664
+ // displayName: tool.displayName,
665
+ // description: tool.description,
666
+ // isReadOnly: tool.isReadOnly,
667
+ // supportsApproval: tool.supportsApproval
668
+ // }
669
+ // }
670
+ //
671
+ // function printToolSummary(tools: ToolSummary[] | null): void {
672
+ // if (tools === null) {
673
+ // process.stdout.write(chalk.dim("\nTools: unavailable (tool definitions could not be fetched)\n"))
674
+ // return
675
+ // }
676
+ // if (tools.length === 0) return
677
+ //
678
+ // process.stdout.write("\nTools:\n")
679
+ // for (const tool of tools) {
680
+ // const readOnly = tool.isReadOnly ? chalk.dim(" (read-only)") : ""
681
+ // process.stdout.write(` ${chalk.cyan(tool.name)}${readOnly} — ${tool.description}\n`)
682
+ // }
683
+ // process.stdout.write(chalk.dim("\nRun `terse integrate tool <type> <tool-name> --json` for a tool's input/output schemas.\n"))
684
+ // }
685
+ //
686
+ // async function collectRawFields(opts: Pick<IntegrateConnectOpts, "fieldFlags" | "fieldsStdin">): Promise<Record<string, string>> {
687
+ // const fromFlags = parseKeyValueFlags(opts.fieldFlags)
688
+ // const fromStdin = opts.fieldsStdin ? await readFieldsFromStdin() : {}
689
+ // return { ...fromFlags, ...fromStdin }
690
+ // }
691
+ //
692
+ // function validateFormFields(type: IntegrationType, declaredFields: FormFieldDefinition[], provided: Record<string, string>): Record<string, string> {
693
+ // const knownFieldNames = new Set(declaredFields.map(f => f.name))
694
+ // const unknown = Object.keys(provided).filter(name => !knownFieldNames.has(name))
695
+ // if (unknown.length > 0) {
696
+ // throw new CliError("unknown_fields", `Unknown field(s): ${unknown.join(", ")}`, {
697
+ // detail: `Valid fields: ${declaredFields.map(f => f.name).join(", ") || "(none)"}`
698
+ // })
699
+ // }
700
+ //
701
+ // const missing = declaredFields.filter(f => f.required && !provided[f.name]).map(f => f.name)
702
+ // if (missing.length > 0) {
703
+ // throw new CliError("missing_fields", `Missing required field(s): ${missing.join(", ")}`, {
704
+ // detail: `Run \`terse integrate describe ${type} --json\` to see the full field schema.`
705
+ // })
706
+ // }
707
+ //
708
+ // return provided
709
+ // }
710
+ //
711
+ // export async function integrateConnect(opts: IntegrateConnectOpts): Promise<void> {
712
+ // const type = parseIntegrationTypeOrThrow(opts.integrationType)
713
+ // const apiKey = readApiKeyOrBail()
714
+ //
715
+ // const [integrations, fieldsResponse] = await Promise.all([fetchIntegrations(apiKey), fetchIntegrationFields(apiKey, type)])
716
+ // const existing = integrations.find(i => i.integrationType === type)
717
+ //
718
+ // if (existing?.isActive && !opts.force) {
719
+ // const message = `${INTEGRATION_METADATA[type]?.name ?? type} is already connected. Pass --force to refresh.`
720
+ // if (opts.json) {
721
+ // process.stdout.write(JSON.stringify({ type, status: "connected", changed: false, message }, null, 2) + "\n")
722
+ // } else {
723
+ // process.stdout.write(chalk.dim(message) + "\n")
724
+ // }
725
+ // return
726
+ // }
727
+ //
728
+ // const raw = await collectRawFields(opts)
729
+ //
730
+ // if (fieldsResponse.installationType === "oauth") {
731
+ // const optionsSchema = InstallationOptionsSchemas[type]
732
+ // const parsed = coerceAndValidateForSchema(raw, optionsSchema, { integrationType: type })
733
+ // const optionsArg = Object.keys(parsed as Record<string, unknown>).length > 0 ? (parsed as Record<string, unknown>) : undefined
734
+ //
735
+ // const installation = await fetchInstallationUrl(apiKey, type, optionsArg)
736
+ // openUrlInBrowser(installation.oauthUrl)
737
+ //
738
+ // const waitCommand = `terse integrate wait ${type}`
739
+ //
740
+ // if (opts.json) {
741
+ // const payload = {
742
+ // handoff: "oauth",
743
+ // type,
744
+ // status: "disconnected" as const,
745
+ // url: installation.oauthUrl,
746
+ // waitCommand,
747
+ // message: `Browser opened. Run \`${waitCommand}\` to block until authorization completes.`
748
+ // }
749
+ // process.stdout.write(JSON.stringify(payload, null, 2) + "\n")
750
+ // process.exitCode = 2
751
+ // return
752
+ // }
753
+ //
754
+ // const name = INTEGRATION_METADATA[type]?.name ?? type
755
+ // process.stdout.write(chalk.yellow(`ACTION REQUIRED: authorize ${name} in your browser.`) + "\n")
756
+ // process.stdout.write(chalk.cyan(` ${installation.oauthUrl}`) + "\n")
757
+ // process.stdout.write(chalk.dim(` Browser opened. Run \`${waitCommand}\` to block until authorization completes.`) + "\n")
758
+ // process.exitCode = 2
759
+ // return
760
+ // }
761
+ //
762
+ // const declaredFields = (fieldsResponse.fields ?? []) as FormFieldDefinition[]
763
+ // const provided = validateFormFields(type, declaredFields, raw)
764
+ // const result = await submitIntegrationForm(apiKey, type, provided)
765
+ // if (!result.success) {
766
+ // throw new CliError("connect_failed", result.error ?? "Failed to connect integration.")
767
+ // }
768
+ //
769
+ // const message = existing?.isActive ? `${type} refreshed.` : `${type} connected.`
770
+ // if (opts.json) {
771
+ // process.stdout.write(JSON.stringify({ type, status: "connected", changed: true, message }, null, 2) + "\n")
772
+ // } else {
773
+ // process.stdout.write(chalk.green(message) + "\n")
774
+ // }
775
+ // }
776
+ //
777
+ // export async function integrateDisconnect(opts: IntegrateDisconnectOpts): Promise<void> {
778
+ // const type = parseIntegrationTypeOrThrow(opts.integrationType)
779
+ // const apiKey = readApiKeyOrBail()
780
+ //
781
+ // const result = await disconnectIntegration(apiKey, type)
782
+ // if (!result.success) {
783
+ // throw new CliError("disconnect_failed", result.error ?? "Failed to disconnect integration.")
784
+ // }
785
+ //
786
+ // const message = `${type} disconnected.`
787
+ // if (opts.json) {
788
+ // process.stdout.write(JSON.stringify({ type, status: "disconnected", changed: true, message }, null, 2) + "\n")
789
+ // } else {
790
+ // process.stdout.write(chalk.green(message) + "\n")
791
+ // }
792
+ // }
793
+ //
794
+ // const WAIT_DEFAULT_TIMEOUT_S = 300
795
+ // const WAIT_MAX_TIMEOUT_S = 900
796
+ // const WAIT_INTERVAL_MS = 2000
797
+ //
798
+ // export async function integrateWait(opts: IntegrateWaitOpts): Promise<void> {
799
+ // const type = parseIntegrationTypeOrThrow(opts.integrationType)
800
+ // const requested = opts.timeoutSeconds ?? WAIT_DEFAULT_TIMEOUT_S
801
+ // const timeoutSeconds = Math.max(1, Math.min(WAIT_MAX_TIMEOUT_S, requested))
802
+ // const apiKey = readApiKeyOrBail()
803
+ //
804
+ // const deadline = Date.now() + timeoutSeconds * 1000
805
+ // let lastError: string | null = null
806
+ //
807
+ // while (Date.now() < deadline) {
808
+ // try {
809
+ // const integrations = await fetchIntegrations(apiKey)
810
+ // const match = integrations.find(i => i.integrationType === type)
811
+ // if (match?.isActive) {
812
+ // const message = `${type} is connected.`
813
+ // if (opts.json) {
814
+ // process.stdout.write(JSON.stringify({ type, status: "connected", message }, null, 2) + "\n")
815
+ // } else {
816
+ // process.stdout.write(chalk.green(message) + "\n")
817
+ // }
818
+ // return
819
+ // }
820
+ // } catch (err) {
821
+ // lastError = err instanceof Error ? err.message : String(err)
822
+ // }
823
+ // await new Promise(resolve => setTimeout(resolve, WAIT_INTERVAL_MS))
824
+ // }
825
+ //
826
+ // throw new CliError("wait_timeout", `Timed out after ${timeoutSeconds}s waiting for ${type} to connect.`, {
827
+ // detail: lastError ? `Last error: ${lastError}` : "Check the integration's connection status with `terse integrate list`."
828
+ // })
829
+ // }
830
+ //
831
+ // // Types
832
+ //
833
+ // type IntegrationChangeResult = {
834
+ // status: "added" | "modified" | "skipped" | "unchanged"
835
+ // integrationType?: IntegrationType
836
+ // }
837
+ //
838
+ // type IntegrateOptions = {
839
+ // /** When set (e.g. during `terse attach`), all integration calls use this key instead of `readApiKey()`. */
840
+ // apiKey?: string
841
+ // showLifecycle?: boolean
842
+ // runGenerateAfterChange?: boolean
843
+ // nonInteractive?: boolean
844
+ // }
845
+ //
846
+ // type IntegrationAction = "back" | "connect" | "disconnect" | "keep" | "refresh_permissions"
847
+ //
848
+ // type GroupedIntegrationSelection = { action: IntegrationAction; integrationType: IntegrationType } | { action: typeof SKIP_VALUE; integrationType: typeof SKIP_VALUE }
849
+ //
850
+ // type PinState = "ok" | "missing"
851
+ //
852
+ // type PinAwareIntegration = IntegrationWithStatus & { pin?: PinState }
853
+ //
854
+ // type UserFacingIntegration = PinAwareIntegration
855
+ //
856
+ // export type IntegrateListOpts = {
857
+ // json?: boolean
858
+ // status?: "connected" | "disconnected"
859
+ // }
860
+ //
861
+ // export type IntegrateDescribeOpts = {
862
+ // integrationType: string
863
+ // json?: boolean
864
+ // }
865
+ //
866
+ // export type IntegrateToolOpts = {
867
+ // integrationType: string
868
+ // toolName?: string
869
+ // json?: boolean
870
+ // }
871
+ //
872
+ // type ToolSummary = {
873
+ // name: string
874
+ // displayName: string
875
+ // description: string
876
+ // isReadOnly: boolean
877
+ // supportsApproval: boolean
878
+ // }
879
+ //
880
+ // export type IntegrateConnectOpts = {
881
+ // integrationType: string
882
+ // fieldFlags?: string[]
883
+ // fieldsStdin?: boolean
884
+ // force?: boolean
885
+ // json?: boolean
886
+ // }
887
+ //
888
+ // export type IntegrateDisconnectOpts = {
889
+ // integrationType: string
890
+ // json?: boolean
891
+ // }
892
+ //
893
+ // export type IntegrateWaitOpts = {
894
+ // integrationType: string
895
+ // timeoutSeconds?: number
896
+ // json?: boolean
897
+ // }
700
898
  //# sourceMappingURL=integrate.js.map