@trayai/tray-sync-cli 1.0.12 → 1.0.14
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/dist/api/resources/me.js +14 -0
- package/dist/api/resources/workflow.js +38 -0
- package/dist/commands/api/api.js +6 -0
- package/dist/commands/api/login.js +91 -0
- package/dist/commands/api/logout.js +39 -0
- package/dist/commands/api/project.js +2 -2
- package/dist/commands/api/shared.js +10 -4
- package/dist/commands/api/workflow.js +166 -0
- package/dist/lib/apiAuth.js +25 -2
- package/dist/lib/apiSessionStore.js +104 -0
- package/dist/lib/browserOpener.js +24 -0
- package/dist/lib/dotenvEdit.js +31 -0
- package/dist/lib/oauth.js +125 -0
- package/dist/lib/oauthCallbackServer.js +97 -0
- package/dist/lib/oauthRefresh.js +65 -0
- package/dist/lib/region.js +1 -0
- package/dist/views/api/workflow.v2.js +21 -0
- package/node_modules/bundle-name/index.js +5 -0
- package/node_modules/bundle-name/license +9 -0
- package/node_modules/bundle-name/package.json +45 -0
- package/node_modules/bundle-name/readme.md +23 -0
- package/node_modules/default-browser/index.d.ts +29 -0
- package/node_modules/default-browser/index.js +34 -0
- package/node_modules/default-browser/license +9 -0
- package/node_modules/default-browser/package.json +54 -0
- package/node_modules/default-browser/readme.md +24 -0
- package/node_modules/default-browser/windows.js +59 -0
- package/node_modules/default-browser-id/index.js +25 -0
- package/node_modules/default-browser-id/license +9 -0
- package/node_modules/default-browser-id/package.json +41 -0
- package/node_modules/default-browser-id/readme.md +19 -0
- package/node_modules/define-lazy-prop/index.d.ts +31 -0
- package/node_modules/define-lazy-prop/index.js +18 -0
- package/node_modules/define-lazy-prop/license +9 -0
- package/node_modules/define-lazy-prop/package.json +51 -0
- package/node_modules/define-lazy-prop/readme.md +55 -0
- package/node_modules/dotenv/CHANGELOG.md +643 -0
- package/node_modules/dotenv/LICENSE +23 -0
- package/node_modules/dotenv/README-es.md +757 -0
- package/node_modules/dotenv/README.md +812 -0
- package/node_modules/dotenv/SECURITY.md +1 -0
- package/node_modules/dotenv/config.d.ts +1 -0
- package/node_modules/dotenv/config.js +9 -0
- package/node_modules/dotenv/lib/cli-options.js +17 -0
- package/node_modules/dotenv/lib/env-options.js +28 -0
- package/node_modules/dotenv/lib/main.d.ts +179 -0
- package/node_modules/dotenv/lib/main.js +423 -0
- package/node_modules/dotenv/package.json +62 -0
- package/node_modules/dotenv/skills/dotenv/SKILL.md +200 -0
- package/node_modules/dotenv/skills/dotenvx/SKILL.md +118 -0
- package/node_modules/graceful-fs/LICENSE +15 -0
- package/node_modules/graceful-fs/README.md +143 -0
- package/node_modules/graceful-fs/clone.js +23 -0
- package/node_modules/graceful-fs/graceful-fs.js +448 -0
- package/node_modules/graceful-fs/legacy-streams.js +118 -0
- package/node_modules/graceful-fs/package.json +53 -0
- package/node_modules/graceful-fs/polyfills.js +355 -0
- package/node_modules/is-docker/cli.js +5 -0
- package/node_modules/is-docker/index.d.ts +13 -0
- package/node_modules/is-docker/index.js +29 -0
- package/node_modules/is-docker/license +9 -0
- package/node_modules/is-docker/package.json +44 -0
- package/node_modules/is-docker/readme.md +27 -0
- package/node_modules/is-in-ssh/index.d.ts +15 -0
- package/node_modules/is-in-ssh/index.js +7 -0
- package/node_modules/is-in-ssh/license +9 -0
- package/node_modules/is-in-ssh/package.json +43 -0
- package/node_modules/is-in-ssh/readme.md +43 -0
- package/node_modules/is-inside-container/cli.js +5 -0
- package/node_modules/is-inside-container/index.d.ts +13 -0
- package/node_modules/is-inside-container/index.js +23 -0
- package/node_modules/is-inside-container/license +9 -0
- package/node_modules/is-inside-container/package.json +51 -0
- package/node_modules/is-inside-container/readme.md +36 -0
- package/node_modules/is-wsl/index.d.ts +15 -0
- package/node_modules/is-wsl/index.js +36 -0
- package/node_modules/is-wsl/license +9 -0
- package/node_modules/is-wsl/package.json +58 -0
- package/node_modules/is-wsl/readme.md +21 -0
- package/node_modules/open/index.d.ts +158 -0
- package/node_modules/open/index.js +425 -0
- package/node_modules/open/license +9 -0
- package/node_modules/open/package.json +70 -0
- package/node_modules/open/readme.md +197 -0
- package/node_modules/open/xdg-open +1267 -0
- package/node_modules/powershell-utils/index.d.ts +175 -0
- package/node_modules/powershell-utils/index.js +79 -0
- package/node_modules/powershell-utils/license +9 -0
- package/node_modules/powershell-utils/package.json +47 -0
- package/node_modules/powershell-utils/readme.md +209 -0
- package/node_modules/proper-lockfile/CHANGELOG.md +108 -0
- package/node_modules/proper-lockfile/LICENSE +21 -0
- package/node_modules/proper-lockfile/README.md +183 -0
- package/node_modules/proper-lockfile/index.js +40 -0
- package/node_modules/proper-lockfile/lib/adapter.js +85 -0
- package/node_modules/proper-lockfile/lib/lockfile.js +342 -0
- package/node_modules/proper-lockfile/lib/mtime-precision.js +55 -0
- package/node_modules/proper-lockfile/package.json +71 -0
- package/node_modules/retry/.npmignore +3 -0
- package/node_modules/retry/.travis.yml +15 -0
- package/node_modules/retry/License +21 -0
- package/node_modules/retry/Makefile +18 -0
- package/node_modules/retry/README.md +227 -0
- package/node_modules/retry/equation.gif +0 -0
- package/node_modules/retry/example/dns.js +31 -0
- package/node_modules/retry/example/stop.js +40 -0
- package/node_modules/retry/index.js +1 -0
- package/node_modules/retry/lib/retry.js +100 -0
- package/node_modules/retry/lib/retry_operation.js +158 -0
- package/node_modules/retry/package.json +32 -0
- package/node_modules/retry/test/common.js +10 -0
- package/node_modules/retry/test/integration/test-forever.js +24 -0
- package/node_modules/retry/test/integration/test-retry-operation.js +258 -0
- package/node_modules/retry/test/integration/test-retry-wrap.js +101 -0
- package/node_modules/retry/test/integration/test-timeouts.js +69 -0
- package/node_modules/run-applescript/index.d.ts +66 -0
- package/node_modules/run-applescript/index.js +37 -0
- package/node_modules/run-applescript/license +9 -0
- package/node_modules/run-applescript/package.json +42 -0
- package/node_modules/run-applescript/readme.md +78 -0
- package/node_modules/signal-exit/LICENSE.txt +16 -0
- package/node_modules/signal-exit/README.md +39 -0
- package/node_modules/signal-exit/index.js +202 -0
- package/node_modules/signal-exit/package.json +38 -0
- package/node_modules/signal-exit/signals.js +53 -0
- package/node_modules/wsl-utils/index.d.ts +180 -0
- package/node_modules/wsl-utils/index.js +145 -0
- package/node_modules/wsl-utils/license +9 -0
- package/node_modules/wsl-utils/node_modules/powershell-utils/index.d.ts +122 -0
- package/node_modules/wsl-utils/node_modules/powershell-utils/index.js +58 -0
- package/node_modules/wsl-utils/node_modules/powershell-utils/license +9 -0
- package/node_modules/wsl-utils/node_modules/powershell-utils/package.json +47 -0
- package/node_modules/wsl-utils/node_modules/powershell-utils/readme.md +153 -0
- package/node_modules/wsl-utils/package.json +48 -0
- package/node_modules/wsl-utils/readme.md +223 -0
- package/node_modules/wsl-utils/utilities.js +19 -0
- package/package.json +8 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AuthError } from "../errors.js";
|
|
2
|
+
export async function getMe(client) {
|
|
3
|
+
try {
|
|
4
|
+
const body = (await client.request("GET", "/v1/me"));
|
|
5
|
+
const user = body.data ?? body;
|
|
6
|
+
return { id: user.id, email: user.email, name: user.name };
|
|
7
|
+
}
|
|
8
|
+
catch (err) {
|
|
9
|
+
if (err instanceof AuthError)
|
|
10
|
+
return null;
|
|
11
|
+
throw err;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=me.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export async function getWorkflow(client, workflowId) {
|
|
2
|
+
return (await client.request("GET", `/v2/workflows/${workflowId}`));
|
|
3
|
+
}
|
|
4
|
+
export async function setWorkflowEnabled(client, workflowId, enabled) {
|
|
5
|
+
await client.request("PUT", `/v2/workflows/${workflowId}/unversioned`, { enabled });
|
|
6
|
+
}
|
|
7
|
+
export async function deleteWorkflow(client, workflowId) {
|
|
8
|
+
await client.request("DELETE", `/v2/workflows/${workflowId}`);
|
|
9
|
+
}
|
|
10
|
+
const EDITABLE_FIELDS = new Set([
|
|
11
|
+
"name",
|
|
12
|
+
"description",
|
|
13
|
+
"tags",
|
|
14
|
+
"alerting_workflow",
|
|
15
|
+
"legacy_error_handling",
|
|
16
|
+
]);
|
|
17
|
+
export function assertValidWorkflowEditPayload(payload) {
|
|
18
|
+
const unknownKeys = Object.keys(payload).filter((key) => !EDITABLE_FIELDS.has(key));
|
|
19
|
+
if (unknownKeys.length > 0) {
|
|
20
|
+
throw new Error(`Unknown field(s) in edit payload: ${unknownKeys.join(", ")}. ` +
|
|
21
|
+
`Editable fields are: ${[...EDITABLE_FIELDS].join(", ")}.`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function updateWorkflowUnversioned(client, workflowId, payload) {
|
|
25
|
+
const body = {};
|
|
26
|
+
if (payload.name !== undefined)
|
|
27
|
+
body.name = payload.name;
|
|
28
|
+
if (payload.description !== undefined)
|
|
29
|
+
body.description = { value: payload.description };
|
|
30
|
+
if (payload.tags !== undefined)
|
|
31
|
+
body.tags = payload.tags;
|
|
32
|
+
if (payload.alerting_workflow !== undefined)
|
|
33
|
+
body.alerting_workflow = { value: payload.alerting_workflow };
|
|
34
|
+
if (payload.legacy_error_handling !== undefined)
|
|
35
|
+
body.legacy_error_handling = payload.legacy_error_handling;
|
|
36
|
+
await client.request("PUT", `/v2/workflows/${workflowId}/unversioned`, body);
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=workflow.js.map
|
package/dist/commands/api/api.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { projectCommand } from "./project.js";
|
|
3
|
+
import { workflowCommand } from "./workflow.js";
|
|
4
|
+
import { loginCommand } from "./login.js";
|
|
5
|
+
import { logoutCommand } from "./logout.js";
|
|
3
6
|
export const apiCommand = new Command("api").description("Direct, authenticated access to the Tray API");
|
|
4
7
|
apiCommand.addCommand(projectCommand);
|
|
8
|
+
apiCommand.addCommand(workflowCommand);
|
|
9
|
+
apiCommand.addCommand(loginCommand);
|
|
10
|
+
apiCommand.addCommand(logoutCommand);
|
|
5
11
|
//# sourceMappingURL=api.js.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { parseRegion } from "../../lib/regionParsing.js";
|
|
3
|
+
import { generatePkcePair, generateState, authorizeUrl, exchangeAuthorizationCode, oauthClientId, OAUTH_SCOPE, apiResource } from "../../lib/oauth.js";
|
|
4
|
+
import { startCallbackServer, DEFAULT_CALLBACK_TIMEOUT_MS } from "../../lib/oauthCallbackServer.js";
|
|
5
|
+
import { openInBrowser } from "../../lib/browserOpener.js";
|
|
6
|
+
import { toApiSession } from "../../lib/oauthRefresh.js";
|
|
7
|
+
import { createApiSessionStore } from "../../lib/apiSessionStore.js";
|
|
8
|
+
import { getMe } from "../../api/resources/me.js";
|
|
9
|
+
import { createTrayClientForRegion } from "../../api/trayClient.js";
|
|
10
|
+
import { FAILURE_ICON, SUCCESS_ICON } from "../../views/icons.js";
|
|
11
|
+
export async function runApiLogin(deps, options) {
|
|
12
|
+
const { stdout, stderr } = deps;
|
|
13
|
+
if (!deps.isInteractive()) {
|
|
14
|
+
stderr(`${FAILURE_ICON} "tray api login" needs an interactive terminal: it opens a browser and waits for you to sign in. Run it directly in your terminal.`);
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
const { region } = options;
|
|
18
|
+
const { verifier, challenge } = generatePkcePair();
|
|
19
|
+
const state = generateState();
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
const onSigint = () => controller.abort();
|
|
22
|
+
const server = await deps.startCallback({
|
|
23
|
+
expectedState: state,
|
|
24
|
+
timeoutMs: DEFAULT_CALLBACK_TIMEOUT_MS,
|
|
25
|
+
signal: controller.signal,
|
|
26
|
+
});
|
|
27
|
+
process.once("SIGINT", onSigint);
|
|
28
|
+
try {
|
|
29
|
+
const url = new URL(authorizeUrl(region));
|
|
30
|
+
url.searchParams.set("response_type", "code");
|
|
31
|
+
url.searchParams.set("client_id", oauthClientId(region));
|
|
32
|
+
url.searchParams.set("redirect_uri", server.redirectUri);
|
|
33
|
+
url.searchParams.set("scope", OAUTH_SCOPE);
|
|
34
|
+
url.searchParams.set("resource", apiResource(region));
|
|
35
|
+
url.searchParams.set("state", state);
|
|
36
|
+
url.searchParams.set("code_challenge", challenge);
|
|
37
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
38
|
+
const authorizeUrlString = url.toString();
|
|
39
|
+
try {
|
|
40
|
+
await deps.openBrowser(authorizeUrlString);
|
|
41
|
+
stdout(`\n Opened your browser to sign in to Tray (${region}).`);
|
|
42
|
+
stdout(` Didn't open? Visit:\n\n ${authorizeUrlString}\n`);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
stdout(`\n Couldn't open a browser automatically. To sign in, visit:\n\n ${authorizeUrlString}\n`);
|
|
46
|
+
}
|
|
47
|
+
stdout(" Waiting for you to sign in... (Ctrl-C to cancel)");
|
|
48
|
+
const code = await server.waitForCode();
|
|
49
|
+
const pair = await deps.exchangeCode({
|
|
50
|
+
region,
|
|
51
|
+
code,
|
|
52
|
+
redirectUri: server.redirectUri,
|
|
53
|
+
codeVerifier: verifier,
|
|
54
|
+
});
|
|
55
|
+
const client = deps.createClient(region, pair.accessToken);
|
|
56
|
+
const user = await getMe(client);
|
|
57
|
+
if (user === null) {
|
|
58
|
+
stderr(`${FAILURE_ICON} Tray rejected the token from the browser sign-in. Run "tray api login" to try again.`);
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
await deps.store.withLock(() => deps.store.set(toApiSession(region, pair, deps.now())));
|
|
62
|
+
const label = user.email ?? user.name ?? user.id ?? "your account";
|
|
63
|
+
stdout(`${SUCCESS_ICON} Logged in as ${label} (${region}).`);
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
process.removeListener("SIGINT", onSigint);
|
|
68
|
+
await server.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function realDeps() {
|
|
72
|
+
return {
|
|
73
|
+
env: process.env,
|
|
74
|
+
stdout: (line) => console.log(line),
|
|
75
|
+
stderr: (line) => console.error(line),
|
|
76
|
+
store: createApiSessionStore(),
|
|
77
|
+
createClient: createTrayClientForRegion,
|
|
78
|
+
isInteractive: () => Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
79
|
+
openBrowser: openInBrowser,
|
|
80
|
+
startCallback: startCallbackServer,
|
|
81
|
+
exchangeCode: exchangeAuthorizationCode,
|
|
82
|
+
now: Date.now,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export const loginCommand = new Command("login")
|
|
86
|
+
.description("Sign in via browser (OAuth2) and store the token for tray api commands")
|
|
87
|
+
.requiredOption("-r, --region <region>", "Tray region to sign in to", parseRegion)
|
|
88
|
+
.action(async (opts) => {
|
|
89
|
+
process.exitCode = await runApiLogin(realDeps(), { region: opts.region });
|
|
90
|
+
});
|
|
91
|
+
//# sourceMappingURL=login.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { revokeToken } from "../../lib/oauth.js";
|
|
3
|
+
import { createApiSessionStore } from "../../lib/apiSessionStore.js";
|
|
4
|
+
import { SUCCESS_ICON, INFO_ICON } from "../../views/icons.js";
|
|
5
|
+
export async function runApiLogout(deps) {
|
|
6
|
+
const { stdout, store } = deps;
|
|
7
|
+
const session = await store.withLock(async () => {
|
|
8
|
+
const current = await store.get();
|
|
9
|
+
await store.clear();
|
|
10
|
+
return current;
|
|
11
|
+
});
|
|
12
|
+
if (!session) {
|
|
13
|
+
stdout(`${INFO_ICON} Not logged in.`);
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
if (session.method === "oauth") {
|
|
17
|
+
try {
|
|
18
|
+
await deps.revokeToken(session.region, session.token);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
stdout(`${SUCCESS_ICON} Logged out (${session.region}).`);
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
function realDeps() {
|
|
27
|
+
return {
|
|
28
|
+
stdout: (line) => console.log(line),
|
|
29
|
+
stderr: (line) => console.error(line),
|
|
30
|
+
store: createApiSessionStore(),
|
|
31
|
+
revokeToken,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export const logoutCommand = new Command("logout")
|
|
35
|
+
.description("Log out of tray api (remove the stored token and revoke it server-side)")
|
|
36
|
+
.action(async () => {
|
|
37
|
+
process.exitCode = await runApiLogout(realDeps());
|
|
38
|
+
});
|
|
39
|
+
//# sourceMappingURL=logout.js.map
|
|
@@ -21,7 +21,7 @@ function parseProjectId(value) {
|
|
|
21
21
|
return value;
|
|
22
22
|
}
|
|
23
23
|
export async function runProjectRollback(deps, options) {
|
|
24
|
-
const client = apiClientFor(deps, options);
|
|
24
|
+
const client = await apiClientFor(deps, options);
|
|
25
25
|
await rollbackProject(client, options.projectId, options.versionNumber);
|
|
26
26
|
if (options.format === OutputFormats.JSON) {
|
|
27
27
|
printConfirmationJson(deps.stdout, {
|
|
@@ -36,7 +36,7 @@ export async function runProjectRollback(deps, options) {
|
|
|
36
36
|
return 0;
|
|
37
37
|
}
|
|
38
38
|
export async function runProjectVersionList(deps, options) {
|
|
39
|
-
const client = apiClientFor(deps, options);
|
|
39
|
+
const client = await apiClientFor(deps, options);
|
|
40
40
|
const result = await listProjectVersions(client, options.projectId, {
|
|
41
41
|
last: options.limit ?? DEFAULT_VERSION_LIST_LIMIT,
|
|
42
42
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createTrayClientForRegion } from "../../api/trayClient.js";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveApiCredentials } from "../../lib/apiAuth.js";
|
|
3
|
+
import { createApiSessionStore } from "../../lib/apiSessionStore.js";
|
|
3
4
|
import { parseRegion } from "../../lib/regionParsing.js";
|
|
4
5
|
import { OutputFormats, parseFormat } from "../../lib/outputFormat.js";
|
|
5
6
|
import { FAILURE_ICON } from "../../views/icons.js";
|
|
@@ -9,6 +10,7 @@ export function realDeps() {
|
|
|
9
10
|
createClient: createTrayClientForRegion,
|
|
10
11
|
stdout: (line) => console.log(line),
|
|
11
12
|
stderr: (line) => console.error(line),
|
|
13
|
+
apiSessionStore: createApiSessionStore(),
|
|
12
14
|
};
|
|
13
15
|
}
|
|
14
16
|
export function addCommonApiOptions(command) {
|
|
@@ -27,9 +29,13 @@ export async function runApiAction(deps, run) {
|
|
|
27
29
|
process.exitCode = 1;
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
|
-
export function apiClientFor(deps, options) {
|
|
31
|
-
const
|
|
32
|
-
|
|
32
|
+
export async function apiClientFor(deps, options) {
|
|
33
|
+
const { token, region } = await resolveApiCredentials({
|
|
34
|
+
tokenFlag: options.token,
|
|
35
|
+
regionFlag: options.region,
|
|
36
|
+
env: deps.env,
|
|
37
|
+
store: deps.apiSessionStore,
|
|
38
|
+
});
|
|
33
39
|
return deps.createClient(region, token);
|
|
34
40
|
}
|
|
35
41
|
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { Command, InvalidArgumentError } from "commander";
|
|
2
|
+
import ansis from "ansis";
|
|
3
|
+
import { assertValidWorkflowEditPayload, deleteWorkflow, getWorkflow, setWorkflowEnabled, updateWorkflowUnversioned, } from "../../api/resources/workflow.js";
|
|
4
|
+
import { isUuid } from "../../lib/uuid.js";
|
|
5
|
+
import { OutputFormats } from "../../lib/outputFormat.js";
|
|
6
|
+
import { printWorkflowDeletedResult, printWorkflowEditedResult, printWorkflowEnabledResult, printWorkflowShowResult, } from "../../views/api/workflow.v2.js";
|
|
7
|
+
import { printConfirmationJson } from "../../views/api/shared.js";
|
|
8
|
+
import { FAILURE_ICON } from "../../views/icons.js";
|
|
9
|
+
import { COMMAND_COLOR } from "../../views/help.js";
|
|
10
|
+
import { addCommonApiOptions, apiClientFor, realDeps, runApiAction, } from "./shared.js";
|
|
11
|
+
async function runWorkflowSetEnabled(deps, options, enabled) {
|
|
12
|
+
const client = await apiClientFor(deps, options);
|
|
13
|
+
await setWorkflowEnabled(client, options.workflowId, enabled);
|
|
14
|
+
if (options.format === OutputFormats.JSON) {
|
|
15
|
+
printConfirmationJson(deps.stdout, { workflowId: options.workflowId, enabled });
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
printWorkflowEnabledResult(deps.stdout, options.workflowId, enabled);
|
|
19
|
+
}
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
export async function runWorkflowEnable(deps, options) {
|
|
23
|
+
return runWorkflowSetEnabled(deps, options, true);
|
|
24
|
+
}
|
|
25
|
+
export async function runWorkflowDisable(deps, options) {
|
|
26
|
+
return runWorkflowSetEnabled(deps, options, false);
|
|
27
|
+
}
|
|
28
|
+
export async function runWorkflowShow(deps, options) {
|
|
29
|
+
const client = await apiClientFor(deps, options);
|
|
30
|
+
const workflow = await getWorkflow(client, options.workflowId);
|
|
31
|
+
if (options.format === OutputFormats.JSON) {
|
|
32
|
+
deps.stdout(JSON.stringify(workflow, null, 2));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
printWorkflowShowResult(deps.stdout, options.workflowId, workflow);
|
|
36
|
+
}
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
export async function runWorkflowDelete(deps, options) {
|
|
40
|
+
const client = await apiClientFor(deps, options);
|
|
41
|
+
await deleteWorkflow(client, options.workflowId);
|
|
42
|
+
if (options.format === OutputFormats.JSON) {
|
|
43
|
+
printConfirmationJson(deps.stdout, { workflowId: options.workflowId, deleted: true });
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
printWorkflowDeletedResult(deps.stdout, options.workflowId);
|
|
47
|
+
}
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
export async function runWorkflowEdit(deps, options) {
|
|
51
|
+
if (Object.keys(options.payload).length === 0) {
|
|
52
|
+
deps.stderr(`${FAILURE_ICON} Nothing to edit — payload has no fields.`);
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
const client = await apiClientFor(deps, options);
|
|
56
|
+
await updateWorkflowUnversioned(client, options.workflowId, options.payload);
|
|
57
|
+
if (options.format === OutputFormats.JSON) {
|
|
58
|
+
printConfirmationJson(deps.stdout, { workflowId: options.workflowId, edited: true });
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
printWorkflowEditedResult(deps.stdout, options.workflowId);
|
|
62
|
+
}
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
function parseWorkflowId(value) {
|
|
66
|
+
if (!isUuid(value)) {
|
|
67
|
+
throw new InvalidArgumentError("workflow id must be a UUID");
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
function parseEditPayload(value) {
|
|
72
|
+
let parsed;
|
|
73
|
+
try {
|
|
74
|
+
parsed = JSON.parse(value);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
throw new InvalidArgumentError(`payload is not valid JSON: ${err.message}`);
|
|
78
|
+
}
|
|
79
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
80
|
+
throw new InvalidArgumentError("payload must be a JSON object");
|
|
81
|
+
}
|
|
82
|
+
const record = parsed;
|
|
83
|
+
try {
|
|
84
|
+
assertValidWorkflowEditPayload(record);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
throw new InvalidArgumentError(err.message);
|
|
88
|
+
}
|
|
89
|
+
return record;
|
|
90
|
+
}
|
|
91
|
+
export const workflowCommand = new Command("workflow").description("Manage workflows");
|
|
92
|
+
addCommonApiOptions(workflowCommand
|
|
93
|
+
.command("enable")
|
|
94
|
+
.description("Enable a workflow")
|
|
95
|
+
.argument("<workflow-id>", "Tray workflow UUID", parseWorkflowId)).action(async (workflowId, opts) => {
|
|
96
|
+
const deps = realDeps();
|
|
97
|
+
await runApiAction(deps, () => runWorkflowEnable(deps, {
|
|
98
|
+
workflowId,
|
|
99
|
+
region: opts.region,
|
|
100
|
+
token: opts.token,
|
|
101
|
+
format: opts.format,
|
|
102
|
+
}));
|
|
103
|
+
});
|
|
104
|
+
addCommonApiOptions(workflowCommand
|
|
105
|
+
.command("disable")
|
|
106
|
+
.description("Disable a workflow")
|
|
107
|
+
.argument("<workflow-id>", "Tray workflow UUID", parseWorkflowId)).action(async (workflowId, opts) => {
|
|
108
|
+
const deps = realDeps();
|
|
109
|
+
await runApiAction(deps, () => runWorkflowDisable(deps, {
|
|
110
|
+
workflowId,
|
|
111
|
+
region: opts.region,
|
|
112
|
+
token: opts.token,
|
|
113
|
+
format: opts.format,
|
|
114
|
+
}));
|
|
115
|
+
});
|
|
116
|
+
addCommonApiOptions(workflowCommand
|
|
117
|
+
.command("show")
|
|
118
|
+
.description("Show a workflow")
|
|
119
|
+
.argument("<workflow-id>", "Tray workflow UUID", parseWorkflowId)).action(async (workflowId, opts) => {
|
|
120
|
+
const deps = realDeps();
|
|
121
|
+
await runApiAction(deps, () => runWorkflowShow(deps, {
|
|
122
|
+
workflowId,
|
|
123
|
+
region: opts.region,
|
|
124
|
+
token: opts.token,
|
|
125
|
+
format: opts.format,
|
|
126
|
+
}));
|
|
127
|
+
});
|
|
128
|
+
addCommonApiOptions(workflowCommand
|
|
129
|
+
.command("delete")
|
|
130
|
+
.description("Delete a workflow")
|
|
131
|
+
.argument("<workflow-id>", "Tray workflow UUID", parseWorkflowId)).action(async (workflowId, opts) => {
|
|
132
|
+
const deps = realDeps();
|
|
133
|
+
await runApiAction(deps, () => runWorkflowDelete(deps, {
|
|
134
|
+
workflowId,
|
|
135
|
+
region: opts.region,
|
|
136
|
+
token: opts.token,
|
|
137
|
+
format: opts.format,
|
|
138
|
+
}));
|
|
139
|
+
});
|
|
140
|
+
const teal = ansis.hex(COMMAND_COLOR);
|
|
141
|
+
const EDIT_PAYLOAD_EXAMPLE = `${ansis.bold("Example:")}
|
|
142
|
+
$ ${teal(`tray api workflow edit <workflow-id> '{"name": "New name", "tags": ["prod", "reviewed"]}'`)}
|
|
143
|
+
|
|
144
|
+
Editable fields (all optional, only the ones you include are changed):
|
|
145
|
+
${teal("name")} string
|
|
146
|
+
${teal("description")} string, or null to clear
|
|
147
|
+
${teal("tags")} string[] — replaces the FULL tag list, does not add/remove single tags
|
|
148
|
+
${teal("alerting_workflow")} string (a workflow id), or null to clear
|
|
149
|
+
${teal("legacy_error_handling")} boolean
|
|
150
|
+
`;
|
|
151
|
+
addCommonApiOptions(workflowCommand
|
|
152
|
+
.command("edit")
|
|
153
|
+
.description("Edit a workflow's name, description, tags, alerting workflow, or legacy error handling")
|
|
154
|
+
.argument("<workflow-id>", "Tray workflow UUID", parseWorkflowId)
|
|
155
|
+
.argument("<payload>", "JSON object of fields to change", parseEditPayload)
|
|
156
|
+
.addHelpText("after", `\n${EDIT_PAYLOAD_EXAMPLE}`)).action(async (workflowId, payload, opts) => {
|
|
157
|
+
const deps = realDeps();
|
|
158
|
+
await runApiAction(deps, () => runWorkflowEdit(deps, {
|
|
159
|
+
workflowId,
|
|
160
|
+
payload,
|
|
161
|
+
region: opts.region,
|
|
162
|
+
token: opts.token,
|
|
163
|
+
format: opts.format,
|
|
164
|
+
}));
|
|
165
|
+
});
|
|
166
|
+
//# sourceMappingURL=workflow.js.map
|
package/dist/lib/apiAuth.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import ansis from "ansis";
|
|
1
2
|
import { REGIONS, PUBLIC_REGIONS } from "./regionParsing.js";
|
|
2
3
|
import { describeRegion } from "./regionDisplay.js";
|
|
4
|
+
import { createApiSessionStore } from "./apiSessionStore.js";
|
|
5
|
+
import { getValidApiSession } from "./oauthRefresh.js";
|
|
3
6
|
export function resolveApiToken(options) {
|
|
4
7
|
const { tokenFlag, env = process.env } = options;
|
|
5
8
|
if (tokenFlag)
|
|
6
9
|
return tokenFlag;
|
|
7
10
|
if (env.TRAY_API_TOKEN)
|
|
8
11
|
return env.TRAY_API_TOKEN;
|
|
9
|
-
throw new Error(
|
|
12
|
+
throw new Error(`Not authenticated. Run ${ansis.bold("tray api login -r <region>")} to sign in, pass ${ansis.bold("-t/--token <token>")}, or set the ${ansis.bold("TRAY_API_TOKEN")} environment variable in your terminal.`);
|
|
10
13
|
}
|
|
11
14
|
export function resolveApiRegion(options) {
|
|
12
15
|
const { regionFlag, env = process.env } = options;
|
|
@@ -19,9 +22,29 @@ export function resolveApiRegion(options) {
|
|
|
19
22
|
}
|
|
20
23
|
return envRegion;
|
|
21
24
|
}
|
|
22
|
-
throw new Error(
|
|
25
|
+
throw new Error(`No region provided. Run ${ansis.bold("tray api login -r <region>")} to sign in, pass ${ansis.bold("-r/--region <region>")}, or set the ${ansis.bold("TRAY_API_REGION")} environment variable in your terminal.`);
|
|
23
26
|
}
|
|
24
27
|
export function isApiEnabled(env = process.env) {
|
|
25
28
|
return env.TRAY_CLI_API_ENABLE === "true";
|
|
26
29
|
}
|
|
30
|
+
export async function resolveApiCredentials(options) {
|
|
31
|
+
const { tokenFlag, regionFlag, env = process.env } = options;
|
|
32
|
+
if (tokenFlag && regionFlag) {
|
|
33
|
+
return { token: tokenFlag, region: regionFlag };
|
|
34
|
+
}
|
|
35
|
+
if (tokenFlag || regionFlag) {
|
|
36
|
+
return {
|
|
37
|
+
token: resolveApiToken({ tokenFlag, env }),
|
|
38
|
+
region: resolveApiRegion({ regionFlag, env }),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const store = options.store ?? createApiSessionStore();
|
|
42
|
+
const session = await getValidApiSession(store);
|
|
43
|
+
if (session)
|
|
44
|
+
return session;
|
|
45
|
+
return {
|
|
46
|
+
token: resolveApiToken({ env }),
|
|
47
|
+
region: resolveApiRegion({ env }),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
27
50
|
//# sourceMappingURL=apiAuth.js.map
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { mkdir, open, readFile, rename, unlink } from "node:fs/promises";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { lock } from "proper-lockfile";
|
|
6
|
+
import { REGIONS } from "./regionParsing.js";
|
|
7
|
+
import { upsertDotEnv, removeDotEnvKey, readDotEnvKey } from "./dotenvEdit.js";
|
|
8
|
+
const KEY_TOKEN = "TCLI_TOKEN";
|
|
9
|
+
const KEY_REGION = "TCLI_REGION";
|
|
10
|
+
const KEY_REFRESH_TOKEN = "TCLI_REFRESH_TOKEN";
|
|
11
|
+
const KEY_EXPIRES_AT = "TCLI_TOKEN_EXPIRES_AT";
|
|
12
|
+
const KEY_METHOD = "TCLI_TOKEN_METHOD";
|
|
13
|
+
export function defaultApiSessionPath(env = process.env) {
|
|
14
|
+
return env.TRAY_CONFIG_PATH ?? join(homedir(), ".tray.config");
|
|
15
|
+
}
|
|
16
|
+
function writeOptionalKey(source, key, value) {
|
|
17
|
+
return value !== undefined ? upsertDotEnv(source, key, value) : removeDotEnvKey(source, key);
|
|
18
|
+
}
|
|
19
|
+
async function readFileOrEmpty(path) {
|
|
20
|
+
try {
|
|
21
|
+
return await readFile(path, "utf-8");
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (err.code === "ENOENT")
|
|
25
|
+
return "";
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function writeFileAtomic(path, content) {
|
|
30
|
+
await mkdir(dirname(path), { recursive: true });
|
|
31
|
+
const tmp = `${path}.${randomUUID()}.tmp`;
|
|
32
|
+
try {
|
|
33
|
+
const fh = await open(tmp, "w", 0o600);
|
|
34
|
+
try {
|
|
35
|
+
await fh.writeFile(content, "utf-8");
|
|
36
|
+
await fh.sync();
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
await fh.close();
|
|
40
|
+
}
|
|
41
|
+
await rename(tmp, path);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
await unlink(tmp).catch(() => { });
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function createApiSessionStore(path = defaultApiSessionPath()) {
|
|
49
|
+
async function setSession(session) {
|
|
50
|
+
let source = await readFileOrEmpty(path);
|
|
51
|
+
source = upsertDotEnv(source, KEY_TOKEN, session.token);
|
|
52
|
+
source = upsertDotEnv(source, KEY_REGION, session.region);
|
|
53
|
+
source = writeOptionalKey(source, KEY_REFRESH_TOKEN, session.refreshToken);
|
|
54
|
+
source = writeOptionalKey(source, KEY_EXPIRES_AT, session.expiresAt !== undefined ? String(session.expiresAt) : undefined);
|
|
55
|
+
source = writeOptionalKey(source, KEY_METHOD, session.method);
|
|
56
|
+
await writeFileAtomic(path, source);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
async get() {
|
|
60
|
+
const source = await readFileOrEmpty(path);
|
|
61
|
+
const token = readDotEnvKey(source, KEY_TOKEN);
|
|
62
|
+
const regionRaw = readDotEnvKey(source, KEY_REGION);
|
|
63
|
+
if (!token || !regionRaw || !REGIONS.includes(regionRaw))
|
|
64
|
+
return null;
|
|
65
|
+
const region = regionRaw;
|
|
66
|
+
const refreshToken = readDotEnvKey(source, KEY_REFRESH_TOKEN) ?? undefined;
|
|
67
|
+
const expiresAtRaw = readDotEnvKey(source, KEY_EXPIRES_AT);
|
|
68
|
+
let expiresAt;
|
|
69
|
+
if (expiresAtRaw) {
|
|
70
|
+
const parsed = Number(expiresAtRaw);
|
|
71
|
+
if (Number.isFinite(parsed))
|
|
72
|
+
expiresAt = parsed;
|
|
73
|
+
}
|
|
74
|
+
const methodRaw = readDotEnvKey(source, KEY_METHOD);
|
|
75
|
+
const method = methodRaw === "oauth" ? "oauth" : undefined;
|
|
76
|
+
return { token, region, refreshToken, expiresAt, method };
|
|
77
|
+
},
|
|
78
|
+
set: setSession,
|
|
79
|
+
async clear() {
|
|
80
|
+
let source = await readFileOrEmpty(path);
|
|
81
|
+
source = removeDotEnvKey(source, KEY_TOKEN);
|
|
82
|
+
source = removeDotEnvKey(source, KEY_REGION);
|
|
83
|
+
source = removeDotEnvKey(source, KEY_REFRESH_TOKEN);
|
|
84
|
+
source = removeDotEnvKey(source, KEY_EXPIRES_AT);
|
|
85
|
+
source = removeDotEnvKey(source, KEY_METHOD);
|
|
86
|
+
await writeFileAtomic(path, source);
|
|
87
|
+
},
|
|
88
|
+
async withLock(fn) {
|
|
89
|
+
await mkdir(dirname(path), { recursive: true });
|
|
90
|
+
const release = await lock(path, {
|
|
91
|
+
realpath: false,
|
|
92
|
+
stale: 60_000,
|
|
93
|
+
retries: { retries: 35, minTimeout: 100, maxTimeout: 2_000 },
|
|
94
|
+
});
|
|
95
|
+
try {
|
|
96
|
+
return await fn();
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
await release();
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=apiSessionStore.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class BrowserOpenError extends Error {
|
|
2
|
+
cause;
|
|
3
|
+
constructor(message, cause) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.cause = cause;
|
|
6
|
+
this.name = "BrowserOpenError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export async function openInBrowser(url) {
|
|
10
|
+
let open;
|
|
11
|
+
try {
|
|
12
|
+
open = (await import("open")).default;
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
throw new BrowserOpenError("Could not load the browser opener. Open the printed URL manually.", err);
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
await open(url);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
throw new BrowserOpenError("Could not open a browser automatically. Open the printed URL manually.", err);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=browserOpener.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { parse as dotenvParse } from "dotenv";
|
|
2
|
+
function detectEol(source) {
|
|
3
|
+
return source.includes("\r\n") ? "\r\n" : "\n";
|
|
4
|
+
}
|
|
5
|
+
function escapeKey(key) {
|
|
6
|
+
return key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7
|
+
}
|
|
8
|
+
export function upsertDotEnv(source, key, value) {
|
|
9
|
+
if (/[\r\n]/.test(value)) {
|
|
10
|
+
throw new Error(`Refusing to write a value containing a newline for ${key}`);
|
|
11
|
+
}
|
|
12
|
+
const eol = detectEol(source);
|
|
13
|
+
const line = `${key}=${value}`;
|
|
14
|
+
const re = new RegExp(`^${escapeKey(key)}=.*$`, "m");
|
|
15
|
+
if (re.test(source)) {
|
|
16
|
+
return source.replace(re, line);
|
|
17
|
+
}
|
|
18
|
+
if (source.length === 0)
|
|
19
|
+
return line + eol;
|
|
20
|
+
const endsWithNewline = source.endsWith("\n") || source.endsWith("\r");
|
|
21
|
+
return source + (endsWithNewline ? "" : eol) + line + eol;
|
|
22
|
+
}
|
|
23
|
+
export function removeDotEnvKey(source, key) {
|
|
24
|
+
const re = new RegExp(`^${escapeKey(key)}=.*(?:\\r?\\n)?`, "m");
|
|
25
|
+
return source.replace(re, "");
|
|
26
|
+
}
|
|
27
|
+
export function readDotEnvKey(source, key) {
|
|
28
|
+
const parsed = dotenvParse(source);
|
|
29
|
+
return Object.prototype.hasOwnProperty.call(parsed, key) ? parsed[key] : null;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=dotenvEdit.js.map
|