@trayai/tray-sync-cli 1.0.13 → 1.0.15
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/project.js +36 -0
- package/dist/commands/api/api.js +4 -0
- package/dist/commands/api/login.js +91 -0
- package/dist/commands/api/logout.js +39 -0
- package/dist/commands/api/project.js +116 -4
- package/dist/commands/api/shared.js +10 -4
- package/dist/commands/api/workflow.js +4 -4
- package/dist/commands/promote.js +2 -1
- 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/promoteTarget.js +0 -24
- package/dist/lib/region.js +1 -0
- package/dist/views/api/project.v2.js +6 -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
|
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
export async function rollbackProject(client, projectId, versionNumber) {
|
|
2
2
|
await client.request("POST", `/v1/projects/${projectId}/rollbacks/versions/${versionNumber}`);
|
|
3
3
|
}
|
|
4
|
+
export async function getLatestProjectVersion(client, targetProjectId) {
|
|
5
|
+
const result = (await client.request("GET", `/v1/projects/${targetProjectId}/versions?last=1`));
|
|
6
|
+
const latest = result.elements?.[0];
|
|
7
|
+
if (!latest)
|
|
8
|
+
return null;
|
|
9
|
+
const [major, minor] = latest.versionNumber.split(".").map(Number);
|
|
10
|
+
return { major, minor };
|
|
11
|
+
}
|
|
12
|
+
export function computeNextVersionNumber(current, mode) {
|
|
13
|
+
if (!current)
|
|
14
|
+
return "1.0";
|
|
15
|
+
if (mode === "major")
|
|
16
|
+
return `${current.major + 1}.0`;
|
|
17
|
+
return `${current.major}.${current.minor + 1}`;
|
|
18
|
+
}
|
|
19
|
+
export async function createProjectVersion(client, targetProjectId, versionNumber, title, description) {
|
|
20
|
+
const result = (await client.request("POST", `/v1/projects/${targetProjectId}/versions/${versionNumber}`, { title, description }));
|
|
21
|
+
return {
|
|
22
|
+
versionNumber: result.versionNumber,
|
|
23
|
+
title: result.title,
|
|
24
|
+
description: result.description,
|
|
25
|
+
created: result.created,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export async function updateProjectVersion(client, projectId, versionNumber, fields) {
|
|
29
|
+
const result = (await client.request("PUT", `/v1/projects/${projectId}/versions/${versionNumber}`, fields));
|
|
30
|
+
return {
|
|
31
|
+
versionNumber: result.versionNumber,
|
|
32
|
+
title: result.title,
|
|
33
|
+
description: result.description,
|
|
34
|
+
created: result.created,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export async function deleteProjectVersion(client, projectId, versionNumber) {
|
|
38
|
+
await client.request("DELETE", `/v1/projects/${projectId}/versions/${versionNumber}`);
|
|
39
|
+
}
|
|
4
40
|
export async function listProjectVersions(client, projectId, options = {}) {
|
|
5
41
|
const params = new URLSearchParams();
|
|
6
42
|
if (options.last !== undefined)
|
package/dist/commands/api/api.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { projectCommand } from "./project.js";
|
|
3
3
|
import { workflowCommand } from "./workflow.js";
|
|
4
|
+
import { loginCommand } from "./login.js";
|
|
5
|
+
import { logoutCommand } from "./logout.js";
|
|
4
6
|
export const apiCommand = new Command("api").description("Direct, authenticated access to the Tray API");
|
|
5
7
|
apiCommand.addCommand(projectCommand);
|
|
6
8
|
apiCommand.addCommand(workflowCommand);
|
|
9
|
+
apiCommand.addCommand(loginCommand);
|
|
10
|
+
apiCommand.addCommand(logoutCommand);
|
|
7
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
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import ansis from "ansis";
|
|
1
2
|
import { Command, InvalidArgumentError } from "commander";
|
|
2
|
-
import { listProjectVersions, rollbackProject } from "../../api/resources/project.js";
|
|
3
|
+
import { computeNextVersionNumber, createProjectVersion, deleteProjectVersion, getLatestProjectVersion, listProjectVersions, rollbackProject, updateProjectVersion, } from "../../api/resources/project.js";
|
|
3
4
|
import { isUuid } from "../../lib/uuid.js";
|
|
4
5
|
import { parseVersionNumber } from "../../lib/versionNumber.js";
|
|
5
6
|
import { OutputFormats } from "../../lib/outputFormat.js";
|
|
6
|
-
import { printProjectRollbackResult, printProjectVersionListTable } from "../../views/api/project.v2.js";
|
|
7
|
+
import { printProjectRollbackResult, printProjectVersionDeletedResult, printProjectVersionListTable, printProjectVersionResult, } from "../../views/api/project.v2.js";
|
|
7
8
|
import { printConfirmationJson } from "../../views/api/shared.js";
|
|
9
|
+
import { FAILURE_ICON } from "../../views/icons.js";
|
|
8
10
|
import { addCommonApiOptions, apiClientFor, realDeps, runApiAction, } from "./shared.js";
|
|
11
|
+
const DEFAULT_VERSION_CREATE_TITLE = "Created via tray-sync-cli";
|
|
9
12
|
const DEFAULT_VERSION_LIST_LIMIT = 5;
|
|
10
13
|
function parseLimit(value) {
|
|
11
14
|
const limit = Number(value);
|
|
@@ -21,7 +24,7 @@ function parseProjectId(value) {
|
|
|
21
24
|
return value;
|
|
22
25
|
}
|
|
23
26
|
export async function runProjectRollback(deps, options) {
|
|
24
|
-
const client = apiClientFor(deps, options);
|
|
27
|
+
const client = await apiClientFor(deps, options);
|
|
25
28
|
await rollbackProject(client, options.projectId, options.versionNumber);
|
|
26
29
|
if (options.format === OutputFormats.JSON) {
|
|
27
30
|
printConfirmationJson(deps.stdout, {
|
|
@@ -36,7 +39,7 @@ export async function runProjectRollback(deps, options) {
|
|
|
36
39
|
return 0;
|
|
37
40
|
}
|
|
38
41
|
export async function runProjectVersionList(deps, options) {
|
|
39
|
-
const client = apiClientFor(deps, options);
|
|
42
|
+
const client = await apiClientFor(deps, options);
|
|
40
43
|
const result = await listProjectVersions(client, options.projectId, {
|
|
41
44
|
last: options.limit ?? DEFAULT_VERSION_LIST_LIMIT,
|
|
42
45
|
});
|
|
@@ -48,6 +51,63 @@ export async function runProjectVersionList(deps, options) {
|
|
|
48
51
|
}
|
|
49
52
|
return 0;
|
|
50
53
|
}
|
|
54
|
+
function printMutuallyExclusiveMajorAndVersion(stderr) {
|
|
55
|
+
stderr(`${FAILURE_ICON} ${ansis.bold("--major")} and ${ansis.bold("--version-number")} are mutually exclusive.`);
|
|
56
|
+
}
|
|
57
|
+
export async function runProjectVersionCreate(deps, options) {
|
|
58
|
+
if (options.major && options.versionNumber) {
|
|
59
|
+
printMutuallyExclusiveMajorAndVersion(deps.stderr);
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
const client = await apiClientFor(deps, options);
|
|
63
|
+
const mode = options.major ? "major" : "minor";
|
|
64
|
+
const versionNumber = options.versionNumber ?? computeNextVersionNumber(await getLatestProjectVersion(client, options.projectId), mode);
|
|
65
|
+
const title = options.title ?? DEFAULT_VERSION_CREATE_TITLE;
|
|
66
|
+
const description = options.description ?? "";
|
|
67
|
+
const created = await createProjectVersion(client, options.projectId, versionNumber, title, description);
|
|
68
|
+
if (options.format === OutputFormats.JSON) {
|
|
69
|
+
printConfirmationJson(deps.stdout, { projectId: options.projectId, ...created });
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
printProjectVersionResult(deps.stdout, options.projectId, created, "created");
|
|
73
|
+
}
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
export async function runProjectVersionEdit(deps, options) {
|
|
77
|
+
if (options.title === undefined && options.description === undefined) {
|
|
78
|
+
deps.stderr(`${FAILURE_ICON} Nothing to edit — pass --title and/or --description.`);
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
const client = await apiClientFor(deps, options);
|
|
82
|
+
const fields = {};
|
|
83
|
+
if (options.title !== undefined)
|
|
84
|
+
fields.title = options.title;
|
|
85
|
+
if (options.description !== undefined)
|
|
86
|
+
fields.description = options.description;
|
|
87
|
+
const updated = await updateProjectVersion(client, options.projectId, options.versionNumber, fields);
|
|
88
|
+
if (options.format === OutputFormats.JSON) {
|
|
89
|
+
printConfirmationJson(deps.stdout, { projectId: options.projectId, ...updated });
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
printProjectVersionResult(deps.stdout, options.projectId, updated, "updated");
|
|
93
|
+
}
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
export async function runProjectVersionDelete(deps, options) {
|
|
97
|
+
const client = await apiClientFor(deps, options);
|
|
98
|
+
await deleteProjectVersion(client, options.projectId, options.versionNumber);
|
|
99
|
+
if (options.format === OutputFormats.JSON) {
|
|
100
|
+
printConfirmationJson(deps.stdout, {
|
|
101
|
+
projectId: options.projectId,
|
|
102
|
+
versionNumber: options.versionNumber,
|
|
103
|
+
deleted: true,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
printProjectVersionDeletedResult(deps.stdout, options.projectId, options.versionNumber);
|
|
108
|
+
}
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
51
111
|
export const projectCommand = new Command("project").description("Manage projects");
|
|
52
112
|
const versionCommand = new Command("version").description("Manage project versions");
|
|
53
113
|
projectCommand.addCommand(versionCommand);
|
|
@@ -79,4 +139,56 @@ addCommonApiOptions(versionCommand
|
|
|
79
139
|
format: opts.format,
|
|
80
140
|
}));
|
|
81
141
|
});
|
|
142
|
+
addCommonApiOptions(versionCommand
|
|
143
|
+
.command("create")
|
|
144
|
+
.description("Create a new version for a project")
|
|
145
|
+
.argument("<project-id>", "Tray project UUID", parseProjectId)
|
|
146
|
+
.option("--major", "bump the major version instead of minor")
|
|
147
|
+
.option("--version-number <major.minor>", "explicit target version number (overrides --major)", parseVersionNumber)
|
|
148
|
+
.option("--title <title>", `title for the created version (default "${DEFAULT_VERSION_CREATE_TITLE}")`)
|
|
149
|
+
.option("--description <description>", "description for the created version")).action(async (projectId, opts) => {
|
|
150
|
+
const deps = realDeps();
|
|
151
|
+
await runApiAction(deps, () => runProjectVersionCreate(deps, {
|
|
152
|
+
projectId,
|
|
153
|
+
major: opts.major,
|
|
154
|
+
versionNumber: opts.versionNumber,
|
|
155
|
+
title: opts.title,
|
|
156
|
+
description: opts.description,
|
|
157
|
+
region: opts.region,
|
|
158
|
+
token: opts.token,
|
|
159
|
+
format: opts.format,
|
|
160
|
+
}));
|
|
161
|
+
});
|
|
162
|
+
addCommonApiOptions(versionCommand
|
|
163
|
+
.command("edit")
|
|
164
|
+
.description("Edit a version's title and/or description")
|
|
165
|
+
.argument("<project-id>", "Tray project UUID", parseProjectId)
|
|
166
|
+
.requiredOption("--version-number <major.minor>", "target version number", parseVersionNumber)
|
|
167
|
+
.option("--title <title>", "new title")
|
|
168
|
+
.option("--description <description>", "new description")).action(async (projectId, opts) => {
|
|
169
|
+
const deps = realDeps();
|
|
170
|
+
await runApiAction(deps, () => runProjectVersionEdit(deps, {
|
|
171
|
+
projectId,
|
|
172
|
+
versionNumber: opts.versionNumber,
|
|
173
|
+
title: opts.title,
|
|
174
|
+
description: opts.description,
|
|
175
|
+
region: opts.region,
|
|
176
|
+
token: opts.token,
|
|
177
|
+
format: opts.format,
|
|
178
|
+
}));
|
|
179
|
+
});
|
|
180
|
+
addCommonApiOptions(versionCommand
|
|
181
|
+
.command("delete")
|
|
182
|
+
.description("Delete a version")
|
|
183
|
+
.argument("<project-id>", "Tray project UUID", parseProjectId)
|
|
184
|
+
.requiredOption("--version-number <major.minor>", "target version number", parseVersionNumber)).action(async (projectId, opts) => {
|
|
185
|
+
const deps = realDeps();
|
|
186
|
+
await runApiAction(deps, () => runProjectVersionDelete(deps, {
|
|
187
|
+
projectId,
|
|
188
|
+
versionNumber: opts.versionNumber,
|
|
189
|
+
region: opts.region,
|
|
190
|
+
token: opts.token,
|
|
191
|
+
format: opts.format,
|
|
192
|
+
}));
|
|
193
|
+
});
|
|
82
194
|
//# sourceMappingURL=project.js.map
|
|
@@ -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
|
|
@@ -9,7 +9,7 @@ import { FAILURE_ICON } from "../../views/icons.js";
|
|
|
9
9
|
import { COMMAND_COLOR } from "../../views/help.js";
|
|
10
10
|
import { addCommonApiOptions, apiClientFor, realDeps, runApiAction, } from "./shared.js";
|
|
11
11
|
async function runWorkflowSetEnabled(deps, options, enabled) {
|
|
12
|
-
const client = apiClientFor(deps, options);
|
|
12
|
+
const client = await apiClientFor(deps, options);
|
|
13
13
|
await setWorkflowEnabled(client, options.workflowId, enabled);
|
|
14
14
|
if (options.format === OutputFormats.JSON) {
|
|
15
15
|
printConfirmationJson(deps.stdout, { workflowId: options.workflowId, enabled });
|
|
@@ -26,7 +26,7 @@ export async function runWorkflowDisable(deps, options) {
|
|
|
26
26
|
return runWorkflowSetEnabled(deps, options, false);
|
|
27
27
|
}
|
|
28
28
|
export async function runWorkflowShow(deps, options) {
|
|
29
|
-
const client = apiClientFor(deps, options);
|
|
29
|
+
const client = await apiClientFor(deps, options);
|
|
30
30
|
const workflow = await getWorkflow(client, options.workflowId);
|
|
31
31
|
if (options.format === OutputFormats.JSON) {
|
|
32
32
|
deps.stdout(JSON.stringify(workflow, null, 2));
|
|
@@ -37,7 +37,7 @@ export async function runWorkflowShow(deps, options) {
|
|
|
37
37
|
return 0;
|
|
38
38
|
}
|
|
39
39
|
export async function runWorkflowDelete(deps, options) {
|
|
40
|
-
const client = apiClientFor(deps, options);
|
|
40
|
+
const client = await apiClientFor(deps, options);
|
|
41
41
|
await deleteWorkflow(client, options.workflowId);
|
|
42
42
|
if (options.format === OutputFormats.JSON) {
|
|
43
43
|
printConfirmationJson(deps.stdout, { workflowId: options.workflowId, deleted: true });
|
|
@@ -52,7 +52,7 @@ export async function runWorkflowEdit(deps, options) {
|
|
|
52
52
|
deps.stderr(`${FAILURE_ICON} Nothing to edit — payload has no fields.`);
|
|
53
53
|
return 1;
|
|
54
54
|
}
|
|
55
|
-
const client = apiClientFor(deps, options);
|
|
55
|
+
const client = await apiClientFor(deps, options);
|
|
56
56
|
await updateWorkflowUnversioned(client, options.workflowId, options.payload);
|
|
57
57
|
if (options.format === OutputFormats.JSON) {
|
|
58
58
|
printConfirmationJson(deps.stdout, { workflowId: options.workflowId, edited: true });
|
package/dist/commands/promote.js
CHANGED
|
@@ -13,7 +13,8 @@ import { resolveContainedSafe } from "../lib/pathContainment.js";
|
|
|
13
13
|
import { entityDisplayName } from "../lib/exportStructure.js";
|
|
14
14
|
import { importProject, previewImport } from "../lib/promoteExecute.js";
|
|
15
15
|
import { summarizeImpactForVersionDescription, toProjectImportImpact, } from "../lib/promoteReport.js";
|
|
16
|
-
import {
|
|
16
|
+
import { isFullyResolved, resolveProjectRequirements, scaffoldUnresolvedAuthentications, } from "../lib/promoteTarget.js";
|
|
17
|
+
import { computeNextVersionNumber, createProjectVersion, getLatestProjectVersion, } from "../api/resources/project.js";
|
|
17
18
|
import { resolveToken } from "../lib/tokenResolution.js";
|
|
18
19
|
import { isUuid } from "../lib/uuid.js";
|
|
19
20
|
import { OutputFormats, parseFormat } from "../lib/outputFormat.js";
|
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
|