@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.
@@ -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
@@ -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 { computeNextVersionNumber, createProjectVersion, getLatestProjectVersion, isFullyResolved, resolveProjectRequirements, scaffoldUnresolvedAuthentications, } from "../lib/promoteTarget.js";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trayai/tray-sync-cli",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "CLI tool to clone Tray projects and related assets to a local directory, and promote them between Tray environments",
5
5
  "bin": {
6
6
  "tray": "dist/cli.js"