@trayai/tray-sync-cli 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # tray-sync-cli
2
- CLI tool to clone tray projects and related assets on local machine which then can be used with version control system or promoted to different tray environment
2
+
3
+ #### Tray CLI tool allows users to
4
+ - General-purpose, authenticated proxy to the Tray API
5
+ - Clone, review and promote Tray projects and related assets on a local machine or a version controlled pipeline
3
6
 
4
7
  ## Requirements
5
8
 
package/dist/cli.js CHANGED
@@ -10,22 +10,37 @@ import { projectCommand } from "./commands/project.js";
10
10
  import { envCommand } from "./commands/env.js";
11
11
  import { authCommand } from "./commands/auth.js";
12
12
  import { promoteCommand } from "./commands/promote.js";
13
+ import { apiCommand } from "./commands/api/api.js";
14
+ import { isApiEnabled } from "./lib/apiAuth.js";
13
15
  import { BANNER } from "./views/banner.js";
16
+ import { TrayHelp } from "./views/help.js";
14
17
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
18
  const pkg = JSON.parse(readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
19
+ Command.prototype.createHelp = () => new TrayHelp();
16
20
  const program = new Command();
21
+ const description = `⭐ Mirror Tray projects to local files, promote them between Tray environments${isApiEnabled() ? ', and run general-purpose API requests' : ''}\nšŸ“š Readme available at https://www.npmjs.com/package/@trayai/tray-sync-cli`;
17
22
  program
18
23
  .name("tray")
19
- .description("Mirror Tray projects to local files and promote them between Tray environments")
24
+ .description(description)
20
25
  .version(pkg.version, "-v, --version")
26
+ .helpCommand(false)
21
27
  .addHelpText("beforeAll", (context) => (context.command === program ? BANNER : ""));
22
- program.addCommand(initCommand);
23
- program.addCommand(pullCommand);
24
- program.addCommand(statusCommand);
25
- program.addCommand(projectCommand);
26
- program.addCommand(envCommand);
27
- program.addCommand(authCommand);
28
- program.addCommand(promoteCommand);
28
+ for (const cmd of [
29
+ initCommand,
30
+ pullCommand,
31
+ statusCommand,
32
+ projectCommand,
33
+ envCommand,
34
+ authCommand,
35
+ promoteCommand,
36
+ ]) {
37
+ cmd.helpGroup("Local project management");
38
+ program.addCommand(cmd);
39
+ }
40
+ if (isApiEnabled()) {
41
+ apiCommand.helpGroup("General-purpose API (growing)");
42
+ program.addCommand(apiCommand);
43
+ }
29
44
  program.parseAsync(process.argv).catch((err) => {
30
45
  const message = err instanceof Error ? err.message : String(err);
31
46
  console.error(message);
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export const apiCommand = new Command("api").description("Direct, authenticated access to the Tray API");
3
+ //# sourceMappingURL=api.js.map
@@ -59,6 +59,25 @@ export async function listProjectIds(client, workspaceId) {
59
59
  const result = (await client.request("GET", `/v2/projects/workspaces/${workspaceId}`));
60
60
  return result.projects.map((p) => requireUuid(p.id, "listProjects: projects[].id"));
61
61
  }
62
+ async function removeOrphanedEntityDirs(projectDirPath, entityParentDir, currentIds) {
63
+ await assertNoSymlinkInPath(projectDirPath, entityParentDir);
64
+ let existingDirNames;
65
+ try {
66
+ existingDirNames = await fsReaddir(entityParentDir);
67
+ }
68
+ catch {
69
+ existingDirNames = [];
70
+ }
71
+ for (const dirName_ of existingDirNames) {
72
+ const prefix = dirName_.split("--")[0];
73
+ if (!isUuid(prefix) || currentIds.has(prefix)) {
74
+ continue;
75
+ }
76
+ const staleDir = resolveContained(entityParentDir, dirName_);
77
+ await assertNoSymlinkInPath(projectDirPath, staleDir);
78
+ await fsRm(staleDir, { recursive: true, force: true });
79
+ }
80
+ }
62
81
  export async function writeExportedProject(projectDirPath, exportStructure, stdout) {
63
82
  const project = exportStructure.projects[0];
64
83
  const projectJsonPath = path.join(projectDirPath, "project.json");
@@ -75,6 +94,7 @@ export async function writeExportedProject(projectDirPath, exportStructure, stdo
75
94
  for (const { field, dirName, fileName, trayType } of ASSET_DIRS) {
76
95
  const entities = exportStructure[field] ?? [];
77
96
  const entityParentDir = path.join(projectDirPath, dirName);
97
+ const currentIds = new Set(entities.map((entity) => entity.id));
78
98
  for (const entity of entities) {
79
99
  const entityId = requireUuid(entity.id, `exportProject: ${field}[].id`);
80
100
  const entityDirName_ = entityDirName(entityId, entityDisplayName(entity));
@@ -113,6 +133,7 @@ export async function writeExportedProject(projectDirPath, exportStructure, stdo
113
133
  }
114
134
  }
115
135
  }
136
+ await removeOrphanedEntityDirs(projectDirPath, entityParentDir, currentIds);
116
137
  }
117
138
  const solutionDropped = Boolean(exportStructure.solution);
118
139
  if (solutionDropped) {
@@ -252,6 +273,10 @@ export async function runPull(deps, options) {
252
273
  }
253
274
  }
254
275
  if (unchanged) {
276
+ for (const { field, dirName } of ASSET_DIRS) {
277
+ const entities = exportStructure[field] ?? [];
278
+ await removeOrphanedEntityDirs(dir, path.join(dir, dirName), new Set(entities.map((e) => e.id)));
279
+ }
255
280
  await writeProjectState(dir, { ...existingState, last_sync: new Date().toISOString() });
256
281
  if (asJson) {
257
282
  events.push({ project_id: projectId, project_name: projectName, action: "pulled_unchanged", renamed_dir: renamedDir });
@@ -0,0 +1,27 @@
1
+ import { REGIONS, PUBLIC_REGIONS } from "./regionParsing.js";
2
+ import { describeRegion } from "./regionDisplay.js";
3
+ export function resolveApiToken(options) {
4
+ const { tokenFlag, env = process.env } = options;
5
+ if (tokenFlag)
6
+ return tokenFlag;
7
+ if (env.TRAY_API_TOKEN)
8
+ return env.TRAY_API_TOKEN;
9
+ throw new Error("No API token provided. Pass -t/--token <token> or set TRAY_API_TOKEN.");
10
+ }
11
+ export function resolveApiRegion(options) {
12
+ const { regionFlag, env = process.env } = options;
13
+ if (regionFlag)
14
+ return regionFlag;
15
+ const envRegion = env.TRAY_API_REGION;
16
+ if (envRegion) {
17
+ if (!REGIONS.includes(envRegion)) {
18
+ throw new Error(`TRAY_API_REGION="${envRegion}" is not a supported region. Must be one of: ${PUBLIC_REGIONS.map(describeRegion).join(", ")}.`);
19
+ }
20
+ return envRegion;
21
+ }
22
+ throw new Error("No region provided. Pass -r/--region <region> or set TRAY_API_REGION.");
23
+ }
24
+ export function isApiEnabled(env = process.env) {
25
+ return env.TRAY_CLI_API_ENABLE === "true";
26
+ }
27
+ //# sourceMappingURL=apiAuth.js.map
@@ -0,0 +1,25 @@
1
+ import { Help } from "commander";
2
+ import ansis from "ansis";
3
+ export const COMMAND_COLOR = "#5FD4D4";
4
+ export class TrayHelp extends Help {
5
+ styleTitle(str) {
6
+ return ansis.bold(str.replace(/:$/, "").toUpperCase());
7
+ }
8
+ styleSubcommandText(str) {
9
+ return str === "[command]" ? str : ansis.hex(COMMAND_COLOR)(str);
10
+ }
11
+ styleCommandText(str) {
12
+ return ansis.hex(COMMAND_COLOR)(str);
13
+ }
14
+ styleUsage(str) {
15
+ return `\n $ ${super.styleUsage(str)}`;
16
+ }
17
+ formatHelp(cmd, helper) {
18
+ const full = super.formatHelp(cmd, helper).replace(/^(.*?) \n/, "$1\n");
19
+ if (helper.commandDescription(cmd).length === 0)
20
+ return full;
21
+ const [usageBlock, descriptionBlock, ...rest] = full.split("\n\n");
22
+ return [descriptionBlock, usageBlock, ...rest].join("\n\n");
23
+ }
24
+ }
25
+ //# sourceMappingURL=help.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trayai/tray-sync-cli",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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"
@@ -22,7 +22,7 @@
22
22
  "build": "rm -rf dist && tsc -p tsconfig.json && chmod +x dist/cli.js",
23
23
  "prepack": "npm run build",
24
24
  "pretest": "tsc -p tsconfig.test.json",
25
- "test": "node --test dist-test/test/**/*.test.js",
25
+ "test": "node --test \"dist-test/test/**/*.test.js\"",
26
26
  "typecheck": "tsc -p tsconfig.json --noEmit",
27
27
  "build-and-reinstall": "npm uninstall -g @trayai/tray-sync-cli --silent || true; npm run build && npm pack --silent | xargs -I{} sh -c 'npm install -g ./{} && rm ./{}'"
28
28
  },