@trayai/tray-sync-cli 1.0.14 → 1.0.16
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/project.js +36 -0
- package/dist/cli.js +16 -0
- package/dist/commands/api/project.js +114 -2
- package/dist/commands/promote.js +2 -1
- package/dist/lib/promoteTarget.js +0 -24
- package/dist/lib/versionCheck.js +48 -0
- package/dist/views/api/project.v2.js +6 -0
- package/dist/views/versionCheck.v2.js +11 -0
- package/package.json +1 -1
|
@@ -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/cli.js
CHANGED
|
@@ -12,6 +12,8 @@ import { authCommand } from "./commands/auth.js";
|
|
|
12
12
|
import { promoteCommand } from "./commands/promote.js";
|
|
13
13
|
import { apiCommand } from "./commands/api/api.js";
|
|
14
14
|
import { isApiEnabled } from "./lib/apiAuth.js";
|
|
15
|
+
import { checkVersion, VersionDeprecatedError } from "./lib/versionCheck.js";
|
|
16
|
+
import { formatDeprecationRefusal, formatOutdatedNag } from "./views/versionCheck.v2.js";
|
|
15
17
|
import { BANNER } from "./views/banner.js";
|
|
16
18
|
import { TrayHelp } from "./views/help.js";
|
|
17
19
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -41,6 +43,20 @@ if (isApiEnabled()) {
|
|
|
41
43
|
apiCommand.helpGroup("General-purpose API (growing)");
|
|
42
44
|
program.addCommand(apiCommand);
|
|
43
45
|
}
|
|
46
|
+
program.hook("preAction", async () => {
|
|
47
|
+
try {
|
|
48
|
+
await checkVersion(pkg.version, {
|
|
49
|
+
onOutdated: (latest, installed) => console.error(formatOutdatedNag(latest, installed)),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (err instanceof VersionDeprecatedError) {
|
|
54
|
+
console.error(formatDeprecationRefusal(err.message));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
44
60
|
program.parseAsync(process.argv).catch((err) => {
|
|
45
61
|
const message = err instanceof Error ? err.message : String(err);
|
|
46
62
|
console.error(message);
|
|
@@ -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);
|
|
@@ -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
|
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";
|
|
@@ -96,30 +96,6 @@ export function validateAuthenticationMappings(mapping) {
|
|
|
96
96
|
.filter(([, authenticationId]) => !isUuid(authenticationId))
|
|
97
97
|
.map(([authExportId, value]) => ({ authExportId, value }));
|
|
98
98
|
}
|
|
99
|
-
export async function getLatestProjectVersion(client, targetProjectId) {
|
|
100
|
-
const result = (await client.request("GET", `/v1/projects/${targetProjectId}/versions?last=1`));
|
|
101
|
-
const latest = result.elements?.[0];
|
|
102
|
-
if (!latest)
|
|
103
|
-
return null;
|
|
104
|
-
const [major, minor] = latest.versionNumber.split(".").map(Number);
|
|
105
|
-
return { major, minor };
|
|
106
|
-
}
|
|
107
|
-
export function computeNextVersionNumber(current, mode) {
|
|
108
|
-
if (!current)
|
|
109
|
-
return "1.0";
|
|
110
|
-
if (mode === "major")
|
|
111
|
-
return `${current.major + 1}.0`;
|
|
112
|
-
return `${current.major}.${current.minor + 1}`;
|
|
113
|
-
}
|
|
114
|
-
export async function createProjectVersion(client, targetProjectId, versionNumber, title, description) {
|
|
115
|
-
const result = (await client.request("POST", `/v1/projects/${targetProjectId}/versions/${versionNumber}`, { title, description }));
|
|
116
|
-
return {
|
|
117
|
-
versionNumber: result.versionNumber,
|
|
118
|
-
title: result.title,
|
|
119
|
-
description: result.description,
|
|
120
|
-
created: result.created,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
99
|
export async function resolveProjectRequirements(client, deps, options) {
|
|
124
100
|
const targetResolution = await resolveTargetProjectId(client, {
|
|
125
101
|
projectDirPath: options.projectDirPath,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readJson, writeJson } from "../config/io.js";
|
|
2
|
+
import { versionCheckPath } from "../config/paths.js";
|
|
3
|
+
import { NetworkError } from "../api/errors.js";
|
|
4
|
+
const REGISTRY_URL = "https://registry.npmjs.org/@trayai/tray-sync-cli";
|
|
5
|
+
const TTL_MS = 24 * 60 * 60 * 1000;
|
|
6
|
+
const FETCH_TIMEOUT_MS = 5_000;
|
|
7
|
+
export async function checkVersion(installedVersion, deps = {}) {
|
|
8
|
+
const { env, now = () => new Date(), onOutdated } = deps;
|
|
9
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
10
|
+
const cached = await readJson(versionCheckPath(env)).catch(() => undefined);
|
|
11
|
+
const isFresh = cached !== undefined && now().getTime() - Date.parse(cached.cached_at) < TTL_MS;
|
|
12
|
+
const response = isFresh
|
|
13
|
+
? cached.response
|
|
14
|
+
: await fetchAndCache(installedVersion, fetchImpl, env, now).catch(() => undefined);
|
|
15
|
+
if (response === undefined)
|
|
16
|
+
return;
|
|
17
|
+
if (response.deprecated) {
|
|
18
|
+
throw new VersionDeprecatedError(response.deprecated);
|
|
19
|
+
}
|
|
20
|
+
if (installedVersion !== response.latest) {
|
|
21
|
+
onOutdated?.(response.latest, installedVersion);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function fetchAndCache(installedVersion, fetchImpl, env, now) {
|
|
25
|
+
let raw;
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetchImpl(REGISTRY_URL, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
28
|
+
if (!res.ok)
|
|
29
|
+
throw new Error(`HTTP ${res.status}`);
|
|
30
|
+
raw = (await res.json());
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
throw new NetworkError(`Could not reach the npm registry to check for a deprecated version: ${err.message}`);
|
|
34
|
+
}
|
|
35
|
+
const latest = raw["dist-tags"]?.latest;
|
|
36
|
+
if (!latest) {
|
|
37
|
+
throw new NetworkError("Unexpected response from the npm registry: missing dist-tags.latest");
|
|
38
|
+
}
|
|
39
|
+
const response = {
|
|
40
|
+
latest,
|
|
41
|
+
deprecated: raw.versions?.[installedVersion]?.deprecated ?? null,
|
|
42
|
+
};
|
|
43
|
+
await writeJson(versionCheckPath(env), { cached_at: now().toISOString(), response });
|
|
44
|
+
return response;
|
|
45
|
+
}
|
|
46
|
+
export class VersionDeprecatedError extends Error {
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=versionCheck.js.map
|
|
@@ -21,4 +21,10 @@ export function printProjectVersionListTable(stdout, versions) {
|
|
|
21
21
|
}
|
|
22
22
|
stdout(table.toString());
|
|
23
23
|
}
|
|
24
|
+
export function printProjectVersionResult(stdout, projectId, version, verb) {
|
|
25
|
+
stdout(`${SUCCESS_ICON} Project ${ansis.bold(projectId)} version ${ansis.bold(version.versionNumber)} ${verb}: ${ansis.bold(version.title)}.`);
|
|
26
|
+
}
|
|
27
|
+
export function printProjectVersionDeletedResult(stdout, projectId, versionNumber) {
|
|
28
|
+
stdout(`${SUCCESS_ICON} Project ${ansis.bold(projectId)} version ${ansis.bold(versionNumber)} deleted.`);
|
|
29
|
+
}
|
|
24
30
|
//# sourceMappingURL=project.v2.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import ansis from "ansis";
|
|
2
|
+
import { INFO_ICON, WARNING_ICON } from "./icons.js";
|
|
3
|
+
export function formatOutdatedNag(latest, installed) {
|
|
4
|
+
return (`${INFO_ICON} A ${ansis.bold("newer")} version of tray-sync-cli is available ` +
|
|
5
|
+
`(${ansis.bold.green(latest)}, you have ${ansis.bold(installed)}).\n` +
|
|
6
|
+
`Upgrade: ${ansis.bold("npm install -g @trayai/tray-sync-cli")}`);
|
|
7
|
+
}
|
|
8
|
+
export function formatDeprecationRefusal(message) {
|
|
9
|
+
return `${WARNING_ICON} ${message}\n` + `Upgrade: ${ansis.bold("npm install -g @trayai/tray-sync-cli")}`;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=versionCheck.v2.js.map
|
package/package.json
CHANGED