latchkey 2.6.1 → 2.7.1
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.
- package/README.md +50 -1
- package/dist/scripts/recordBrowserSession.js +3 -3
- package/dist/scripts/recordBrowserSession.js.map +1 -1
- package/dist/src/{apiCredentials.d.ts → apiCredentials/base.d.ts} +6 -6
- package/dist/src/apiCredentials/base.d.ts.map +1 -0
- package/dist/src/{apiCredentials.js → apiCredentials/base.js} +5 -5
- package/dist/src/apiCredentials/base.js.map +1 -0
- package/dist/src/{apiCredentialsSerialization.d.ts → apiCredentials/serialization.d.ts} +5 -5
- package/dist/src/apiCredentials/serialization.d.ts.map +1 -0
- package/dist/src/{apiCredentialsSerialization.js → apiCredentials/serialization.js} +9 -9
- package/dist/src/apiCredentials/serialization.js.map +1 -0
- package/dist/src/{apiCredentialStore.d.ts → apiCredentials/store.d.ts} +3 -3
- package/dist/src/apiCredentials/store.d.ts.map +1 -0
- package/dist/src/{apiCredentialStore.js → apiCredentials/store.js} +2 -2
- package/dist/src/apiCredentials/store.js.map +1 -0
- package/dist/src/apiCredentials/utils.d.ts +13 -0
- package/dist/src/apiCredentials/utils.d.ts.map +1 -0
- package/dist/src/apiCredentials/utils.js +27 -0
- package/dist/src/apiCredentials/utils.js.map +1 -0
- package/dist/src/browserConfig.d.ts +5 -1
- package/dist/src/browserConfig.d.ts.map +1 -1
- package/dist/src/browserConfig.js +17 -3
- package/dist/src/browserConfig.js.map +1 -1
- package/dist/src/cli.js +42 -39
- package/dist/src/cli.js.map +1 -1
- package/dist/src/cliCommands.d.ts +5 -2
- package/dist/src/cliCommands.d.ts.map +1 -1
- package/dist/src/cliCommands.js +270 -190
- package/dist/src/cliCommands.js.map +1 -1
- package/dist/src/config.d.ts +32 -2
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +107 -20
- package/dist/src/config.js.map +1 -1
- package/dist/src/configDataStore.d.ts +44 -0
- package/dist/src/configDataStore.d.ts.map +1 -1
- package/dist/src/configDataStore.js +27 -0
- package/dist/src/configDataStore.js.map +1 -1
- package/dist/src/curl.d.ts +41 -8
- package/dist/src/curl.d.ts.map +1 -1
- package/dist/src/curl.js +80 -75
- package/dist/src/curl.js.map +1 -1
- package/dist/src/curlInjection.d.ts +46 -0
- package/dist/src/curlInjection.d.ts.map +1 -0
- package/dist/src/curlInjection.js +99 -0
- package/dist/src/curlInjection.js.map +1 -0
- package/dist/src/errorMessages.d.ts +14 -0
- package/dist/src/errorMessages.d.ts.map +1 -0
- package/dist/src/errorMessages.js +22 -0
- package/dist/src/errorMessages.js.map +1 -0
- package/dist/src/gateway/client.d.ts +32 -0
- package/dist/src/gateway/client.d.ts.map +1 -0
- package/dist/src/gateway/client.js +89 -0
- package/dist/src/gateway/client.js.map +1 -0
- package/dist/src/gateway/gatewayEndpoint.d.ts +43 -0
- package/dist/src/gateway/gatewayEndpoint.d.ts.map +1 -0
- package/dist/src/gateway/gatewayEndpoint.js +297 -0
- package/dist/src/gateway/gatewayEndpoint.js.map +1 -0
- package/dist/src/gateway/latchkeyEndpoint.d.ts +105 -0
- package/dist/src/gateway/latchkeyEndpoint.d.ts.map +1 -0
- package/dist/src/gateway/latchkeyEndpoint.js +144 -0
- package/dist/src/gateway/latchkeyEndpoint.js.map +1 -0
- package/dist/src/gateway/server.d.ts +20 -0
- package/dist/src/gateway/server.d.ts.map +1 -0
- package/dist/src/gateway/server.js +90 -0
- package/dist/src/gateway/server.js.map +1 -0
- package/dist/src/index.d.ts +4 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/permissions.d.ts.map +1 -1
- package/dist/src/permissions.js +4 -2
- package/dist/src/permissions.js.map +1 -1
- package/dist/src/playwrightDownload.d.ts +1 -1
- package/dist/src/playwrightDownload.d.ts.map +1 -1
- package/dist/src/playwrightDownload.js +5 -4
- package/dist/src/playwrightDownload.js.map +1 -1
- package/dist/src/playwrightLoader.d.ts +17 -0
- package/dist/src/playwrightLoader.d.ts.map +1 -0
- package/dist/src/playwrightLoader.js +47 -0
- package/dist/src/playwrightLoader.js.map +1 -0
- package/dist/src/playwrightUtils.d.ts.map +1 -1
- package/dist/src/playwrightUtils.js +2 -1
- package/dist/src/playwrightUtils.js.map +1 -1
- package/dist/src/{registry.d.ts → serviceRegistry.d.ts} +4 -4
- package/dist/src/serviceRegistry.d.ts.map +1 -0
- package/dist/src/{registry.js → serviceRegistry.js} +4 -4
- package/dist/src/serviceRegistry.js.map +1 -0
- package/dist/src/services/aws.d.ts +2 -2
- package/dist/src/services/aws.d.ts.map +1 -1
- package/dist/src/services/aws.js +17 -10
- package/dist/src/services/aws.js.map +1 -1
- package/dist/src/services/core/base.d.ts +2 -2
- package/dist/src/services/core/base.d.ts.map +1 -1
- package/dist/src/services/core/base.js +3 -3
- package/dist/src/services/core/base.js.map +1 -1
- package/dist/src/services/core/registered.d.ts +2 -2
- package/dist/src/services/core/registered.d.ts.map +1 -1
- package/dist/src/services/core/registered.js +2 -2
- package/dist/src/services/core/registered.js.map +1 -1
- package/dist/src/services/discord.d.ts +1 -1
- package/dist/src/services/discord.d.ts.map +1 -1
- package/dist/src/services/discord.js +1 -1
- package/dist/src/services/discord.js.map +1 -1
- package/dist/src/services/dropbox.d.ts +1 -1
- package/dist/src/services/dropbox.d.ts.map +1 -1
- package/dist/src/services/dropbox.js +1 -1
- package/dist/src/services/dropbox.js.map +1 -1
- package/dist/src/services/github.d.ts +2 -2
- package/dist/src/services/github.d.ts.map +1 -1
- package/dist/src/services/github.js +2 -2
- package/dist/src/services/github.js.map +1 -1
- package/dist/src/services/google/base.d.ts +2 -2
- package/dist/src/services/google/base.d.ts.map +1 -1
- package/dist/src/services/google/base.js +3 -3
- package/dist/src/services/google/base.js.map +1 -1
- package/dist/src/services/google/directions.d.ts +1 -1
- package/dist/src/services/google/directions.d.ts.map +1 -1
- package/dist/src/services/linear.d.ts +1 -1
- package/dist/src/services/linear.d.ts.map +1 -1
- package/dist/src/services/linear.js +1 -1
- package/dist/src/services/linear.js.map +1 -1
- package/dist/src/services/notion.d.ts +1 -1
- package/dist/src/services/notion.d.ts.map +1 -1
- package/dist/src/services/notion.js +1 -1
- package/dist/src/services/notion.js.map +1 -1
- package/dist/src/services/sentry.d.ts +2 -2
- package/dist/src/services/sentry.d.ts.map +1 -1
- package/dist/src/services/sentry.js +6 -3
- package/dist/src/services/sentry.js.map +1 -1
- package/dist/src/services/slack.d.ts +3 -3
- package/dist/src/services/slack.d.ts.map +1 -1
- package/dist/src/services/slack.js +5 -5
- package/dist/src/services/slack.js.map +1 -1
- package/dist/src/services/telegram.d.ts +2 -2
- package/dist/src/services/telegram.d.ts.map +1 -1
- package/dist/src/services/telegram.js +2 -2
- package/dist/src/services/telegram.js.map +1 -1
- package/dist/src/sharedOperations.d.ts +44 -0
- package/dist/src/sharedOperations.d.ts.map +1 -0
- package/dist/src/sharedOperations.js +131 -0
- package/dist/src/sharedOperations.js.map +1 -0
- package/dist/src/version.d.ts +2 -0
- package/dist/src/version.d.ts.map +1 -0
- package/dist/src/version.js +4 -0
- package/dist/src/version.js.map +1 -0
- package/dist/tests/apiCredentialStore.test.js +2 -2
- package/dist/tests/apiCredentialStore.test.js.map +1 -1
- package/dist/tests/apiCredentials.test.js +37 -36
- package/dist/tests/apiCredentials.test.js.map +1 -1
- package/dist/tests/cli.test.js +240 -55
- package/dist/tests/cli.test.js.map +1 -1
- package/dist/tests/config.test.d.ts +2 -0
- package/dist/tests/config.test.d.ts.map +1 -0
- package/dist/tests/config.test.js +150 -0
- package/dist/tests/config.test.js.map +1 -0
- package/dist/tests/gateway.test.d.ts +2 -0
- package/dist/tests/gateway.test.d.ts.map +1 -0
- package/dist/tests/gateway.test.js +566 -0
- package/dist/tests/gateway.test.js.map +1 -0
- package/dist/tests/gatewayClient.test.d.ts +2 -0
- package/dist/tests/gatewayClient.test.d.ts.map +1 -0
- package/dist/tests/gatewayClient.test.js +85 -0
- package/dist/tests/gatewayClient.test.js.map +1 -0
- package/dist/tests/latchkeyEndpoint.test.d.ts +2 -0
- package/dist/tests/latchkeyEndpoint.test.d.ts.map +1 -0
- package/dist/tests/latchkeyEndpoint.test.js +385 -0
- package/dist/tests/latchkeyEndpoint.test.js.map +1 -0
- package/dist/tests/permissions.test.js +15 -0
- package/dist/tests/permissions.test.js.map +1 -1
- package/dist/tests/playwrightDownload.test.js +2 -2
- package/dist/tests/playwrightDownload.test.js.map +1 -1
- package/dist/tests/serviceRegistry.test.d.ts +2 -0
- package/dist/tests/serviceRegistry.test.d.ts.map +1 -0
- package/dist/tests/{registry.test.js → serviceRegistry.test.js} +17 -17
- package/dist/tests/serviceRegistry.test.js.map +1 -0
- package/dist/tests/servicesAgainstRecordings.test.js +3 -3
- package/dist/tests/servicesAgainstRecordings.test.js.map +1 -1
- package/dist/tests/sharedOperations.test.d.ts +2 -0
- package/dist/tests/sharedOperations.test.d.ts.map +1 -0
- package/dist/tests/sharedOperations.test.js +264 -0
- package/dist/tests/sharedOperations.test.js.map +1 -0
- package/package.json +10 -3
- package/dist/src/apiCredentialStore.d.ts.map +0 -1
- package/dist/src/apiCredentialStore.js.map +0 -1
- package/dist/src/apiCredentials.d.ts.map +0 -1
- package/dist/src/apiCredentials.js.map +0 -1
- package/dist/src/apiCredentialsSerialization.d.ts.map +0 -1
- package/dist/src/apiCredentialsSerialization.js.map +0 -1
- package/dist/src/registry.d.ts.map +0 -1
- package/dist/src/registry.js.map +0 -1
- package/dist/tests/registry.test.d.ts +0 -2
- package/dist/tests/registry.test.d.ts.map +0 -1
- package/dist/tests/registry.test.js.map +0 -1
package/dist/src/cliCommands.js
CHANGED
|
@@ -3,55 +3,41 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { existsSync, unlinkSync } from 'node:fs';
|
|
5
5
|
import { createInterface } from 'node:readline';
|
|
6
|
-
import { ApiCredentialStore } from './
|
|
7
|
-
import {
|
|
6
|
+
import { ApiCredentialStore } from './apiCredentials/store.js';
|
|
7
|
+
import { RawCurlCredentials } from './apiCredentials/base.js';
|
|
8
|
+
import { CredentialsExpiredError, NoCredentialsForServiceError, NoServiceForUrlError, prepareCurlInvocation, RequestNotPermittedError, UrlExtractionFailedError, } from './curlInjection.js';
|
|
8
9
|
import { BROWSER_SOURCES, BrowserNotFoundError, DEFAULT_BROWSER_SOURCES, ensureBrowser, } from './browserConfig.js';
|
|
9
10
|
import { CONFIG } from './config.js';
|
|
10
|
-
import { deleteRegisteredService,
|
|
11
|
-
import { BrowserDisabledError,
|
|
11
|
+
import { deleteRegisteredService, saveRegisteredService } from './configDataStore.js';
|
|
12
|
+
import { BrowserDisabledError, BrowserFlowsNotSupportedError, GraphicalEnvironmentNotFoundError, } from './playwrightUtils.js';
|
|
13
|
+
import { BrowserFeaturesUnavailableError, loadPlaywright } from './playwrightLoader.js';
|
|
12
14
|
import { EncryptedStorage } from './encryptedStorage.js';
|
|
13
|
-
import { DuplicateServiceNameError, InvalidServiceNameError,
|
|
15
|
+
import { DuplicateServiceNameError, InvalidServiceNameError, SERVICE_REGISTRY, canonicalizeServiceName, } from './serviceRegistry.js';
|
|
14
16
|
import { RegisteredService } from './services/core/registered.js';
|
|
15
17
|
import { LoginCancelledError, LoginFailedError, NoCurlCredentialsNotSupportedError, } from './services/index.js';
|
|
16
|
-
import { extractUrlFromCurlArguments, run as curlRun } from './curl.js';
|
|
18
|
+
import { CurlParseError, extractUrlFromCurlArguments, run as curlRun, runAsync as curlRunAsync, } from './curl.js';
|
|
17
19
|
import { checkPermission, PermissionCheckError } from './permissions.js';
|
|
20
|
+
import { ErrorMessages } from './errorMessages.js';
|
|
18
21
|
import { getSkillMdContent } from './skillMd.js';
|
|
22
|
+
import { startGateway } from './gateway/server.js';
|
|
23
|
+
import { callLatchkeyEndpoint, GatewayCommandNotSupportedError, GatewayCurlRewriteError, GatewayRequestError, rewriteCurlArgumentsForGateway, } from './gateway/client.js';
|
|
24
|
+
import { servicesList, servicesInfo, authList, authBrowser, authBrowserPrepare, UnknownServiceError, BrowserNotConfiguredError, PreparationRequiredError, } from './sharedOperations.js';
|
|
25
|
+
import { VERSION } from './version.js';
|
|
19
26
|
/**
|
|
20
27
|
* Exit code used when a request is rejected by permission rules.
|
|
21
28
|
* Uses the Unix convention for "command not permitted" (126).
|
|
22
29
|
* Curl itself does not use this exit code.
|
|
23
30
|
*/
|
|
24
31
|
export const PERMISSION_DENIED_EXIT_CODE = 126;
|
|
25
|
-
/**
|
|
26
|
-
* Try to refresh expired credentials if the service supports it.
|
|
27
|
-
* Returns refreshed credentials if successful, otherwise returns the original credentials.
|
|
28
|
-
*/
|
|
29
|
-
async function maybeRefreshCredentials(service, apiCredentials, apiCredentialStore) {
|
|
30
|
-
if (apiCredentials.isExpired() !== true || !service.refreshCredentials) {
|
|
31
|
-
return apiCredentials;
|
|
32
|
-
}
|
|
33
|
-
const refreshedCredentials = await service.refreshCredentials(apiCredentials);
|
|
34
|
-
if (refreshedCredentials !== null) {
|
|
35
|
-
apiCredentialStore.save(service.name, refreshedCredentials);
|
|
36
|
-
return refreshedCredentials;
|
|
37
|
-
}
|
|
38
|
-
return apiCredentials;
|
|
39
|
-
}
|
|
40
|
-
async function getCredentialStatus(service, credentials, apiCredentialStore) {
|
|
41
|
-
if (credentials === null) {
|
|
42
|
-
return ApiCredentialStatus.Missing;
|
|
43
|
-
}
|
|
44
|
-
const refreshed = await maybeRefreshCredentials(service, credentials, apiCredentialStore);
|
|
45
|
-
return service.checkApiCredentials(refreshed);
|
|
46
|
-
}
|
|
47
32
|
/**
|
|
48
33
|
* Default implementation of CLI dependencies.
|
|
49
34
|
*/
|
|
50
35
|
export function createDefaultDependencies() {
|
|
51
36
|
return {
|
|
52
|
-
registry:
|
|
37
|
+
registry: SERVICE_REGISTRY,
|
|
53
38
|
config: CONFIG,
|
|
54
39
|
runCurl: curlRun,
|
|
40
|
+
runCurlAsync: curlRunAsync,
|
|
55
41
|
checkPermission: checkPermission,
|
|
56
42
|
confirm: defaultConfirm,
|
|
57
43
|
exit: (code) => process.exit(code),
|
|
@@ -61,8 +47,40 @@ export function createDefaultDependencies() {
|
|
|
61
47
|
errorLog: (message) => {
|
|
62
48
|
console.error(message);
|
|
63
49
|
},
|
|
50
|
+
version: VERSION,
|
|
64
51
|
};
|
|
65
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Forward a request to the gateway's `/latchkey/` endpoint. On transport or
|
|
55
|
+
* protocol errors the CLI exits with status 1 after logging the error message.
|
|
56
|
+
*/
|
|
57
|
+
async function forwardToGateway(deps, request) {
|
|
58
|
+
const gatewayUrl = deps.config.gatewayUrl;
|
|
59
|
+
if (gatewayUrl === null) {
|
|
60
|
+
throw new GatewayCommandNotSupportedError(request.command);
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
return await callLatchkeyEndpoint(gatewayUrl, request);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error instanceof GatewayRequestError) {
|
|
67
|
+
deps.errorLog(`Error: ${error.message}`);
|
|
68
|
+
deps.exit(1);
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* If the CLI is running in gateway mode, log an error and exit. Used for
|
|
75
|
+
* commands that manage local state and cannot be meaningfully delegated.
|
|
76
|
+
*/
|
|
77
|
+
function refuseInGatewayMode(deps, commandName) {
|
|
78
|
+
if (deps.config.gatewayUrl !== null) {
|
|
79
|
+
const error = new GatewayCommandNotSupportedError(commandName);
|
|
80
|
+
deps.errorLog(`Error: ${error.message}`);
|
|
81
|
+
deps.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
66
84
|
async function defaultConfirm(message) {
|
|
67
85
|
const readline = createInterface({
|
|
68
86
|
input: process.stdin,
|
|
@@ -136,44 +154,6 @@ async function clearService(deps, serviceName) {
|
|
|
136
154
|
deps.log(`No API credentials found for ${serviceName}.`);
|
|
137
155
|
}
|
|
138
156
|
}
|
|
139
|
-
/**
|
|
140
|
-
* Check if browser login is disabled via environment variable.
|
|
141
|
-
* Exits with error if LATCHKEY_DISABLE_BROWSER is set.
|
|
142
|
-
*/
|
|
143
|
-
function checkBrowserNotDisabledOrExit(deps) {
|
|
144
|
-
if (deps.config.browserDisabled) {
|
|
145
|
-
deps.errorLog(new BrowserDisabledError().message);
|
|
146
|
-
deps.exit(1);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Check if a graphical environment is available.
|
|
151
|
-
* Exits with error if no display server (X11 or Wayland) is detected on Linux.
|
|
152
|
-
*/
|
|
153
|
-
function checkGraphicalEnvironmentOrExit(deps) {
|
|
154
|
-
if (!hasGraphicalEnvironment()) {
|
|
155
|
-
deps.errorLog(new GraphicalEnvironmentNotFoundError().message);
|
|
156
|
-
deps.exit(1);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Get the browser launch options from configuration, handling errors with CLI output.
|
|
161
|
-
* Exits with error if no valid browser config exists, if browser is disabled,
|
|
162
|
-
* or if no graphical environment is available.
|
|
163
|
-
*/
|
|
164
|
-
function getBrowserLaunchOptionsOrExit(deps) {
|
|
165
|
-
checkBrowserNotDisabledOrExit(deps);
|
|
166
|
-
checkGraphicalEnvironmentOrExit(deps);
|
|
167
|
-
const browserConfig = loadBrowserConfig(deps.config.configPath);
|
|
168
|
-
if (!browserConfig) {
|
|
169
|
-
deps.errorLog("Error: No browser configured. Run 'latchkey ensure-browser' first.");
|
|
170
|
-
deps.exit(1);
|
|
171
|
-
}
|
|
172
|
-
return {
|
|
173
|
-
browserStatePath: deps.config.browserStatePath,
|
|
174
|
-
executablePath: browserConfig.executablePath,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
157
|
/**
|
|
178
158
|
* Register all CLI commands on the given program.
|
|
179
159
|
*/
|
|
@@ -187,56 +167,45 @@ export function registerCommands(program, deps) {
|
|
|
187
167
|
.option('--builtin', 'Only list built-in services (exclude registered services)')
|
|
188
168
|
.option('--viable', 'Only list services that either have stored credentials or can be authenticated via a browser.')
|
|
189
169
|
.action(async (options) => {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (options.viable === true) {
|
|
195
|
-
const encryptedStorage = await createEncryptedStorageFromConfig(deps.config);
|
|
196
|
-
const apiCredentialStore = new ApiCredentialStore(deps.config.credentialStorePath, encryptedStorage);
|
|
197
|
-
const allCredentials = apiCredentialStore.getAll();
|
|
198
|
-
services = services.filter((service) => {
|
|
199
|
-
if (allCredentials.has(service.name)) {
|
|
200
|
-
return true;
|
|
201
|
-
}
|
|
202
|
-
const supportsBrowser = service.getSession !== undefined &&
|
|
203
|
-
!deps.config.browserDisabled &&
|
|
204
|
-
hasGraphicalEnvironment();
|
|
205
|
-
return supportsBrowser;
|
|
170
|
+
if (deps.config.gatewayUrl !== null) {
|
|
171
|
+
const result = await forwardToGateway(deps, {
|
|
172
|
+
command: 'services list',
|
|
173
|
+
params: options,
|
|
206
174
|
});
|
|
175
|
+
deps.log(JSON.stringify(result, null, 2));
|
|
176
|
+
return;
|
|
207
177
|
}
|
|
208
|
-
const
|
|
209
|
-
deps.
|
|
178
|
+
const encryptedStorage = await createEncryptedStorageFromConfig(deps.config);
|
|
179
|
+
const apiCredentialStore = new ApiCredentialStore(deps.config.credentialStorePath, encryptedStorage);
|
|
180
|
+
const result = servicesList(deps.registry, apiCredentialStore, deps.config, options);
|
|
181
|
+
deps.log(JSON.stringify(result, null, 2));
|
|
210
182
|
});
|
|
211
183
|
servicesCommand
|
|
212
184
|
.command('info')
|
|
213
185
|
.description('Show information about a service.')
|
|
214
186
|
.argument('<service_name>', 'Name of the service to get info for')
|
|
215
187
|
.action(async (serviceName) => {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
188
|
+
if (deps.config.gatewayUrl !== null) {
|
|
189
|
+
const info = await forwardToGateway(deps, {
|
|
190
|
+
command: 'services info',
|
|
191
|
+
params: { serviceName },
|
|
192
|
+
});
|
|
193
|
+
deps.log(JSON.stringify(info, null, 2));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const encryptedStorage = await createEncryptedStorageFromConfig(deps.config);
|
|
198
|
+
const apiCredentialStore = new ApiCredentialStore(deps.config.credentialStorePath, encryptedStorage);
|
|
199
|
+
const info = await servicesInfo(deps.registry, apiCredentialStore, deps.config, serviceName);
|
|
200
|
+
deps.log(JSON.stringify(info, null, 2));
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (error instanceof UnknownServiceError) {
|
|
204
|
+
deps.errorLog(`Error: ${error.message}`);
|
|
205
|
+
deps.exit(1);
|
|
206
|
+
}
|
|
207
|
+
throw error;
|
|
221
208
|
}
|
|
222
|
-
// Login options
|
|
223
|
-
const supportsBrowser = service.getSession !== undefined && !deps.config.browserDisabled;
|
|
224
|
-
const authOptions = supportsBrowser ? ['browser', 'set'] : ['set'];
|
|
225
|
-
// Credentials status
|
|
226
|
-
const encryptedStorage = await createEncryptedStorageFromConfig(deps.config);
|
|
227
|
-
const apiCredentialStore = new ApiCredentialStore(deps.config.credentialStorePath, encryptedStorage);
|
|
228
|
-
const apiCredentials = apiCredentialStore.get(serviceName);
|
|
229
|
-
const credentialStatus = await getCredentialStatus(service, apiCredentials, apiCredentialStore);
|
|
230
|
-
const serviceType = service instanceof RegisteredService ? 'user-registered' : 'built-in';
|
|
231
|
-
const info = {
|
|
232
|
-
type: serviceType,
|
|
233
|
-
baseApiUrls: service.baseApiUrls,
|
|
234
|
-
authOptions,
|
|
235
|
-
credentialStatus,
|
|
236
|
-
setCredentialsExample: service.setCredentialsExample(serviceName),
|
|
237
|
-
developerNotes: service.info,
|
|
238
|
-
};
|
|
239
|
-
deps.log(JSON.stringify(info, null, 2));
|
|
240
209
|
});
|
|
241
210
|
servicesCommand
|
|
242
211
|
.command('register')
|
|
@@ -246,6 +215,7 @@ export function registerCommands(program, deps) {
|
|
|
246
215
|
.option('--service-family <name>', 'Name of the built-in service to use as a template, if any (e.g. gitlab)')
|
|
247
216
|
.option('--login-url <url>', 'Login URL for browser-based authentication, if applicable')
|
|
248
217
|
.action((rawServiceName, options) => {
|
|
218
|
+
refuseInGatewayMode(deps, 'services register');
|
|
249
219
|
let serviceName;
|
|
250
220
|
try {
|
|
251
221
|
serviceName = canonicalizeServiceName(rawServiceName);
|
|
@@ -303,6 +273,7 @@ export function registerCommands(program, deps) {
|
|
|
303
273
|
.description('Deregister a previously registered service instance.')
|
|
304
274
|
.argument('<service_name>', 'Name of the registered service to remove')
|
|
305
275
|
.action(async (serviceName) => {
|
|
276
|
+
refuseInGatewayMode(deps, 'services deregister');
|
|
306
277
|
const service = deps.registry.getByName(serviceName);
|
|
307
278
|
if (service === null) {
|
|
308
279
|
deps.errorLog(`Error: Unknown service: ${serviceName}`);
|
|
@@ -330,6 +301,7 @@ export function registerCommands(program, deps) {
|
|
|
330
301
|
.argument('[service_name]', 'Name of the service to clear API credentials for')
|
|
331
302
|
.option('-y, --yes', 'Skip confirmation prompt when clearing all data')
|
|
332
303
|
.action(async (serviceName, options) => {
|
|
304
|
+
refuseInGatewayMode(deps, 'auth clear');
|
|
333
305
|
if (serviceName === undefined) {
|
|
334
306
|
await clearAll(deps, options.yes ?? false);
|
|
335
307
|
}
|
|
@@ -341,17 +313,14 @@ export function registerCommands(program, deps) {
|
|
|
341
313
|
.command('list')
|
|
342
314
|
.description('List all stored credentials and their status.')
|
|
343
315
|
.action(async () => {
|
|
316
|
+
if (deps.config.gatewayUrl !== null) {
|
|
317
|
+
const entries = await forwardToGateway(deps, { command: 'auth list' });
|
|
318
|
+
deps.log(JSON.stringify(entries, null, 2));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
344
321
|
const encryptedStorage = await createEncryptedStorageFromConfig(deps.config);
|
|
345
322
|
const apiCredentialStore = new ApiCredentialStore(deps.config.credentialStorePath, encryptedStorage);
|
|
346
|
-
const
|
|
347
|
-
const statusChecks = Array.from(allCredentials, async ([serviceName, credentials]) => {
|
|
348
|
-
const service = deps.registry.getByName(serviceName);
|
|
349
|
-
const credentialStatus = service !== null
|
|
350
|
-
? await getCredentialStatus(service, credentials, apiCredentialStore)
|
|
351
|
-
: ApiCredentialStatus.Valid;
|
|
352
|
-
return [serviceName, { credentialType: credentials.objectType, credentialStatus }];
|
|
353
|
-
});
|
|
354
|
-
const entries = Object.fromEntries(await Promise.all(statusChecks));
|
|
323
|
+
const entries = await authList(deps.registry, apiCredentialStore);
|
|
355
324
|
deps.log(JSON.stringify(entries, null, 2));
|
|
356
325
|
});
|
|
357
326
|
authCommand
|
|
@@ -362,6 +331,7 @@ export function registerCommands(program, deps) {
|
|
|
362
331
|
.allowUnknownOption()
|
|
363
332
|
.allowExcessArguments()
|
|
364
333
|
.action(async (_serviceName, _options, command) => {
|
|
334
|
+
refuseInGatewayMode(deps, 'auth set');
|
|
365
335
|
const [serviceName, ...curlArguments] = command.args;
|
|
366
336
|
if (serviceName === undefined) {
|
|
367
337
|
deps.errorLog('Error: Service name is required.');
|
|
@@ -394,6 +364,7 @@ export function registerCommands(program, deps) {
|
|
|
394
364
|
.addHelpText('after', `\nExample:\n $ latchkey auth set-nocurl aws <access-key-id> <secret-access-key>`)
|
|
395
365
|
.allowExcessArguments()
|
|
396
366
|
.action(async (_serviceName, _options, command) => {
|
|
367
|
+
refuseInGatewayMode(deps, 'auth set-nocurl');
|
|
397
368
|
const [serviceName, ...noCurlArguments] = command.args;
|
|
398
369
|
if (serviceName === undefined) {
|
|
399
370
|
deps.errorLog('Error: Service name is required.');
|
|
@@ -426,33 +397,45 @@ export function registerCommands(program, deps) {
|
|
|
426
397
|
.description('Login to a service via the browser and store the API credentials.')
|
|
427
398
|
.argument('<service_name>', 'Name of the service to login to')
|
|
428
399
|
.action(async (serviceName) => {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if (!session) {
|
|
437
|
-
deps.errorLog(`Service '${serviceName}' does not support browser flows. ` +
|
|
438
|
-
`Use '${service.setCredentialsExample(serviceName)}' to set credentials manually.`);
|
|
439
|
-
deps.exit(1);
|
|
440
|
-
}
|
|
441
|
-
const encryptedStorage = await createEncryptedStorageFromConfig(deps.config);
|
|
442
|
-
const apiCredentialStore = new ApiCredentialStore(deps.config.credentialStorePath, encryptedStorage);
|
|
443
|
-
const oldCredentials = apiCredentialStore.get(service.name);
|
|
444
|
-
if (session.prepare && oldCredentials === null) {
|
|
445
|
-
deps.errorLog(`Error: Service ${serviceName} requires preparation first.`);
|
|
446
|
-
deps.errorLog(`Run 'latchkey auth browser-prepare ${serviceName}' before logging in.`);
|
|
447
|
-
deps.exit(1);
|
|
400
|
+
if (deps.config.gatewayUrl !== null) {
|
|
401
|
+
await forwardToGateway(deps, {
|
|
402
|
+
command: 'auth browser',
|
|
403
|
+
params: { serviceName },
|
|
404
|
+
});
|
|
405
|
+
deps.log('Done');
|
|
406
|
+
return;
|
|
448
407
|
}
|
|
449
|
-
const launchOptions = getBrowserLaunchOptionsOrExit(deps);
|
|
450
408
|
try {
|
|
451
|
-
const
|
|
452
|
-
apiCredentialStore
|
|
409
|
+
const encryptedStorage = await createEncryptedStorageFromConfig(deps.config);
|
|
410
|
+
const apiCredentialStore = new ApiCredentialStore(deps.config.credentialStorePath, encryptedStorage);
|
|
411
|
+
await authBrowser(deps.registry, apiCredentialStore, encryptedStorage, deps.config, serviceName);
|
|
453
412
|
deps.log('Done');
|
|
454
413
|
}
|
|
455
414
|
catch (error) {
|
|
415
|
+
if (error instanceof UnknownServiceError) {
|
|
416
|
+
deps.errorLog(`Error: ${error.message}`);
|
|
417
|
+
deps.exit(1);
|
|
418
|
+
}
|
|
419
|
+
if (error instanceof BrowserFlowsNotSupportedError) {
|
|
420
|
+
deps.errorLog(error.message);
|
|
421
|
+
deps.exit(1);
|
|
422
|
+
}
|
|
423
|
+
if (error instanceof PreparationRequiredError) {
|
|
424
|
+
deps.errorLog(`Error: ${error.message}`);
|
|
425
|
+
deps.exit(1);
|
|
426
|
+
}
|
|
427
|
+
if (error instanceof BrowserDisabledError) {
|
|
428
|
+
deps.errorLog(error.message);
|
|
429
|
+
deps.exit(1);
|
|
430
|
+
}
|
|
431
|
+
if (error instanceof GraphicalEnvironmentNotFoundError) {
|
|
432
|
+
deps.errorLog(error.message);
|
|
433
|
+
deps.exit(1);
|
|
434
|
+
}
|
|
435
|
+
if (error instanceof BrowserNotConfiguredError) {
|
|
436
|
+
deps.errorLog(`Error: ${error.message}`);
|
|
437
|
+
deps.exit(1);
|
|
438
|
+
}
|
|
456
439
|
if (error instanceof LoginCancelledError) {
|
|
457
440
|
deps.errorLog('Login cancelled.');
|
|
458
441
|
deps.exit(1);
|
|
@@ -461,6 +444,10 @@ export function registerCommands(program, deps) {
|
|
|
461
444
|
deps.errorLog(error.message);
|
|
462
445
|
deps.exit(1);
|
|
463
446
|
}
|
|
447
|
+
if (error instanceof BrowserFeaturesUnavailableError) {
|
|
448
|
+
deps.errorLog(error.message);
|
|
449
|
+
deps.exit(1);
|
|
450
|
+
}
|
|
464
451
|
throw error;
|
|
465
452
|
}
|
|
466
453
|
});
|
|
@@ -469,32 +456,47 @@ export function registerCommands(program, deps) {
|
|
|
469
456
|
.description('Prepare a service to be used with the browser command.')
|
|
470
457
|
.argument('<service_name>', 'Name of the service to prepare')
|
|
471
458
|
.action(async (serviceName) => {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const encryptedStorage = await createEncryptedStorageFromConfig(deps.config);
|
|
484
|
-
const apiCredentialStore = new ApiCredentialStore(deps.config.credentialStorePath, encryptedStorage);
|
|
485
|
-
// Check if already prepared (credentials exist)
|
|
486
|
-
const existingCredentials = apiCredentialStore.get(service.name);
|
|
487
|
-
if (existingCredentials !== null) {
|
|
488
|
-
deps.log('Already prepared.');
|
|
459
|
+
if (deps.config.gatewayUrl !== null) {
|
|
460
|
+
const result = (await forwardToGateway(deps, {
|
|
461
|
+
command: 'auth browser-prepare',
|
|
462
|
+
params: { serviceName },
|
|
463
|
+
}));
|
|
464
|
+
if (result !== null && result.alreadyPrepared === true) {
|
|
465
|
+
deps.log('Already prepared.');
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
deps.log('Done');
|
|
469
|
+
}
|
|
489
470
|
return;
|
|
490
471
|
}
|
|
491
|
-
const launchOptions = getBrowserLaunchOptionsOrExit(deps);
|
|
492
472
|
try {
|
|
493
|
-
const
|
|
494
|
-
apiCredentialStore
|
|
495
|
-
deps.
|
|
473
|
+
const encryptedStorage = await createEncryptedStorageFromConfig(deps.config);
|
|
474
|
+
const apiCredentialStore = new ApiCredentialStore(deps.config.credentialStorePath, encryptedStorage);
|
|
475
|
+
const result = await authBrowserPrepare(deps.registry, apiCredentialStore, encryptedStorage, deps.config, serviceName);
|
|
476
|
+
if (result.alreadyPrepared) {
|
|
477
|
+
deps.log('Already prepared.');
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
deps.log('Done');
|
|
481
|
+
}
|
|
496
482
|
}
|
|
497
483
|
catch (error) {
|
|
484
|
+
if (error instanceof UnknownServiceError) {
|
|
485
|
+
deps.errorLog(`Error: ${error.message}`);
|
|
486
|
+
deps.exit(1);
|
|
487
|
+
}
|
|
488
|
+
if (error instanceof BrowserDisabledError) {
|
|
489
|
+
deps.errorLog(error.message);
|
|
490
|
+
deps.exit(1);
|
|
491
|
+
}
|
|
492
|
+
if (error instanceof GraphicalEnvironmentNotFoundError) {
|
|
493
|
+
deps.errorLog(error.message);
|
|
494
|
+
deps.exit(1);
|
|
495
|
+
}
|
|
496
|
+
if (error instanceof BrowserNotConfiguredError) {
|
|
497
|
+
deps.errorLog(`Error: ${error.message}`);
|
|
498
|
+
deps.exit(1);
|
|
499
|
+
}
|
|
498
500
|
if (error instanceof LoginCancelledError) {
|
|
499
501
|
deps.errorLog('Preparation cancelled.');
|
|
500
502
|
deps.exit(1);
|
|
@@ -503,6 +505,10 @@ export function registerCommands(program, deps) {
|
|
|
503
505
|
deps.errorLog(error.message);
|
|
504
506
|
deps.exit(1);
|
|
505
507
|
}
|
|
508
|
+
if (error instanceof BrowserFeaturesUnavailableError) {
|
|
509
|
+
deps.errorLog(error.message);
|
|
510
|
+
deps.exit(1);
|
|
511
|
+
}
|
|
506
512
|
throw error;
|
|
507
513
|
}
|
|
508
514
|
});
|
|
@@ -513,51 +519,106 @@ export function registerCommands(program, deps) {
|
|
|
513
519
|
.allowExcessArguments()
|
|
514
520
|
.action(async (_options, command) => {
|
|
515
521
|
const curlArguments = command.args;
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
522
|
+
if (deps.config.gatewayUrl !== null) {
|
|
523
|
+
let targetUrl;
|
|
524
|
+
try {
|
|
525
|
+
targetUrl = extractUrlFromCurlArguments(curlArguments);
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
if (error instanceof CurlParseError) {
|
|
529
|
+
if (error.message) {
|
|
530
|
+
deps.errorLog(`${ErrorMessages.couldNotExtractUrlBrief} ${error.message}`);
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
deps.errorLog(`${ErrorMessages.couldNotExtractUrl} ${error.message}`);
|
|
534
|
+
}
|
|
535
|
+
deps.exit(1);
|
|
536
|
+
}
|
|
537
|
+
throw error;
|
|
538
|
+
}
|
|
539
|
+
if (targetUrl === null) {
|
|
540
|
+
deps.errorLog(ErrorMessages.couldNotExtractUrl);
|
|
541
|
+
deps.exit(1);
|
|
542
|
+
}
|
|
543
|
+
let rewritten;
|
|
544
|
+
try {
|
|
545
|
+
rewritten = rewriteCurlArgumentsForGateway(curlArguments, targetUrl, deps.config.gatewayUrl);
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
if (error instanceof GatewayCurlRewriteError) {
|
|
549
|
+
deps.errorLog(`Error: ${error.message}`);
|
|
550
|
+
deps.exit(1);
|
|
551
|
+
}
|
|
552
|
+
throw error;
|
|
521
553
|
}
|
|
554
|
+
const result = deps.runCurl(rewritten);
|
|
555
|
+
deps.exit(result.returncode);
|
|
556
|
+
}
|
|
557
|
+
const encryptedStorage = await createEncryptedStorageFromConfig(deps.config);
|
|
558
|
+
const apiCredentialStore = new ApiCredentialStore(deps.config.credentialStorePath, encryptedStorage);
|
|
559
|
+
let finalArguments;
|
|
560
|
+
try {
|
|
561
|
+
finalArguments = await prepareCurlInvocation(curlArguments, apiCredentialStore, {
|
|
562
|
+
registry: deps.registry,
|
|
563
|
+
checkPermission: deps.checkPermission,
|
|
564
|
+
permissionsConfigPath: deps.config.permissionsConfigPath,
|
|
565
|
+
permissionsDoNotUseBuiltinSchemas: deps.config.permissionsDoNotUseBuiltinSchemas,
|
|
566
|
+
passthroughUnknown: deps.config.passthroughUnknown,
|
|
567
|
+
});
|
|
522
568
|
}
|
|
523
569
|
catch (error) {
|
|
570
|
+
if (error instanceof RequestNotPermittedError) {
|
|
571
|
+
deps.errorLog(error.message);
|
|
572
|
+
deps.exit(PERMISSION_DENIED_EXIT_CODE);
|
|
573
|
+
}
|
|
524
574
|
if (error instanceof PermissionCheckError) {
|
|
525
575
|
deps.errorLog(`Error: ${error.message}`);
|
|
526
576
|
deps.exit(PERMISSION_DENIED_EXIT_CODE);
|
|
527
577
|
}
|
|
578
|
+
if (error instanceof UrlExtractionFailedError ||
|
|
579
|
+
error instanceof NoServiceForUrlError ||
|
|
580
|
+
error instanceof NoCredentialsForServiceError ||
|
|
581
|
+
error instanceof CredentialsExpiredError) {
|
|
582
|
+
deps.errorLog(error.message);
|
|
583
|
+
deps.exit(1);
|
|
584
|
+
}
|
|
528
585
|
throw error;
|
|
529
586
|
}
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
587
|
+
const result = deps.runCurl(finalArguments);
|
|
588
|
+
deps.exit(result.returncode);
|
|
589
|
+
});
|
|
590
|
+
program
|
|
591
|
+
.command('gateway')
|
|
592
|
+
.description('Start a local HTTP gateway that proxies requests with credential injection.')
|
|
593
|
+
.option('--port <number>', `Port to listen on (default: ${deps.config.gatewayListenPort.toString()}, configurable via config.json key 'gatewayListenPort')`)
|
|
594
|
+
.option('--host <address>', `Address to bind to (default: ${deps.config.gatewayListenHost}, configurable via config.json key 'gatewayListenHost')`)
|
|
595
|
+
.option('--max-body-size <bytes>', 'Maximum request body size in bytes', String(10 * 1024 * 1024))
|
|
596
|
+
.action(async (options) => {
|
|
597
|
+
refuseInGatewayMode(deps, 'gateway');
|
|
598
|
+
const portString = options.port ?? deps.config.gatewayListenPort.toString();
|
|
599
|
+
const port = parseInt(portString, 10);
|
|
600
|
+
if (isNaN(port) || port < 0 || port > 65535) {
|
|
601
|
+
deps.errorLog(`Error: Invalid port number: ${portString}`);
|
|
533
602
|
deps.exit(1);
|
|
534
603
|
}
|
|
535
|
-
const
|
|
536
|
-
if (
|
|
537
|
-
deps.errorLog(`Error:
|
|
604
|
+
const maxBodySize = parseInt(options.maxBodySize, 10);
|
|
605
|
+
if (isNaN(maxBodySize) || maxBodySize <= 0) {
|
|
606
|
+
deps.errorLog(`Error: Invalid max body size: ${options.maxBodySize}`);
|
|
538
607
|
deps.exit(1);
|
|
539
608
|
}
|
|
540
609
|
const encryptedStorage = await createEncryptedStorageFromConfig(deps.config);
|
|
541
610
|
const apiCredentialStore = new ApiCredentialStore(deps.config.credentialStorePath, encryptedStorage);
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
deps.errorLog(`Error: Credentials for ${service.name} are expired.`);
|
|
554
|
-
deps.errorLog(`Run 'latchkey auth browser ${service.name}' or 'latchkey auth set ${service.name}' to refresh them.`);
|
|
555
|
-
deps.exit(1);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
const allArguments = apiCredentials.injectIntoCurlCall(curlArguments);
|
|
559
|
-
const result = deps.runCurl(allArguments);
|
|
560
|
-
deps.exit(result.returncode);
|
|
611
|
+
const gateway = await startGateway(deps, apiCredentialStore, encryptedStorage, {
|
|
612
|
+
port,
|
|
613
|
+
host: options.host ?? deps.config.gatewayListenHost,
|
|
614
|
+
maxBodySize,
|
|
615
|
+
});
|
|
616
|
+
const shutdown = async () => {
|
|
617
|
+
await gateway.close();
|
|
618
|
+
deps.exit(0);
|
|
619
|
+
};
|
|
620
|
+
process.on('SIGINT', () => void shutdown());
|
|
621
|
+
process.on('SIGTERM', () => void shutdown());
|
|
561
622
|
});
|
|
562
623
|
program
|
|
563
624
|
.command('skill-md')
|
|
@@ -570,6 +631,7 @@ export function registerCommands(program, deps) {
|
|
|
570
631
|
.description('Ensure a Chrome/Chromium browser is available for Latchkey to use.')
|
|
571
632
|
.option('--source <sources>', `Comma-separated list of sources to try in order: ${BROWSER_SOURCES.join(', ')}`, DEFAULT_BROWSER_SOURCES.join(','))
|
|
572
633
|
.action(async (options) => {
|
|
634
|
+
refuseInGatewayMode(deps, 'ensure-browser');
|
|
573
635
|
const configPath = deps.config.configPath;
|
|
574
636
|
// Parse and validate sources
|
|
575
637
|
const sourceList = options.source.split(',').map((s) => s.trim());
|
|
@@ -580,6 +642,20 @@ export function registerCommands(program, deps) {
|
|
|
580
642
|
deps.exit(1);
|
|
581
643
|
}
|
|
582
644
|
const sources = sourceList;
|
|
645
|
+
// In the standalone binary, playwright is not available at runtime.
|
|
646
|
+
// Refuse `ensure-browser` up front rather than silently succeeding via
|
|
647
|
+
// a stale `existing-config` source that points to a browser we could
|
|
648
|
+
// never actually launch anyway.
|
|
649
|
+
try {
|
|
650
|
+
await loadPlaywright();
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
if (error instanceof BrowserFeaturesUnavailableError) {
|
|
654
|
+
deps.errorLog(error.message);
|
|
655
|
+
deps.exit(1);
|
|
656
|
+
}
|
|
657
|
+
throw error;
|
|
658
|
+
}
|
|
583
659
|
deps.log(`Discovering browser using sources: ${sources.join(', ')}`);
|
|
584
660
|
try {
|
|
585
661
|
const { config, source } = await ensureBrowser(configPath, sources);
|
|
@@ -596,6 +672,10 @@ export function registerCommands(program, deps) {
|
|
|
596
672
|
deps.errorLog(`Error: ${error.message}`);
|
|
597
673
|
deps.exit(1);
|
|
598
674
|
}
|
|
675
|
+
if (error instanceof BrowserFeaturesUnavailableError) {
|
|
676
|
+
deps.errorLog(error.message);
|
|
677
|
+
deps.exit(1);
|
|
678
|
+
}
|
|
599
679
|
throw error;
|
|
600
680
|
}
|
|
601
681
|
});
|