@trayai/tray-sync-cli 1.0.2 → 1.0.4

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
@@ -19,7 +19,6 @@ npm install -g @trayai/tray-sync-cli
19
19
  | `tray pull` | Export tracked projects from Tray and materialise them on disk |
20
20
  | `tray promote <env>` | Reconstruct local projects and import them into a target Tray environment |
21
21
  | `tray status` | Local checksum-based drift detection (zero API calls) |
22
- | `tray project add <project-id>` | Track a project (project scope only) |
23
22
  | `tray project remove <project-id>` | Untrack a project and delete its local directory (project scope only) |
24
23
  | `tray project list` | List managed projects with directory names and last-pulled timestamps |
25
24
  | `tray env add <name>` | Add a target environment |
@@ -77,7 +76,7 @@ lists every project in your source workspace — id + name — so you don't have
77
76
 
78
77
  A few notes on the flags:
79
78
 
80
- - **`-p <project-id>`** pulls that project immediately after scaffolding. Omit it and `init` just scaffolds an empty repository — you can pull specific projects later with: `tray pull -p <project_id>` OR `tray project add <project-id>` followed by `tray pull`.
79
+ - **`-p <project-id>`** pulls that project immediately after scaffolding. Omit it and `init` just scaffolds an empty repository — you can pull specific projects later with `tray pull -p <project_id>`, which tracks and pulls it in one step.
81
80
  - **`-s workspace`** (i.e. `tray init -s workspace -r <region> -w <id> -t <token>`, omitting `-p`) switches to workspace scope: every project in the workspace is pulled automatically, and `tray pull` keeps that mirror in sync going forward (including removing projects deleted from Tray). `-p` is ignored in this scope.
82
81
  - **`-t <token>`** is optional at `init` time. If you skip it (or need to replace it later — tokens expire), set or refresh it any time with:
83
82
  ```bash
@@ -1,15 +1,12 @@
1
1
  import path from "node:path";
2
2
  import { readdir as fsReaddir, rm as fsRm } from "node:fs/promises";
3
3
  import { Command, InvalidArgumentError } from "commander";
4
- import { createTrayClientForRegion } from "../api/trayClient.js";
5
- import { readCredentials } from "../config/credentials.js";
6
4
  import { readProjectState } from "../config/state.js";
7
5
  import { readTrayYaml, writeTrayYaml } from "../config/trayYaml.js";
8
6
  import { Scopes } from "../config/types.js";
9
- import { resolveToken } from "../lib/tokenResolution.js";
10
7
  import { isUuid } from "../lib/uuid.js";
11
8
  import { OutputFormats, parseFormat } from "../lib/outputFormat.js";
12
- import { printAddedProject, printAlreadyTracked, printNoProjectsTracked, printNotTracked, printProjectListTable, printRemovedProject, printTrayYamlNotFound, printValidationSkippedOrFailed, printValidationSucceeded, printWorkspaceScopeDenied, } from "../views/project/project.v2.js";
9
+ import { printNoProjectsTracked, printNotTracked, printProjectListTable, printRemovedProject, printTrayYamlNotFound, printWorkspaceScopeDenied, } from "../views/project/project.v2.js";
13
10
  import { printProjectListJson } from "../views/project/shared.js";
14
11
  function findProjectDir(existingDirs, projectId) {
15
12
  return existingDirs.find((name) => name.split("--")[0] === projectId);
@@ -36,37 +33,6 @@ async function listProjectDirs(readdir, projectsRoot) {
36
33
  return [];
37
34
  }
38
35
  }
39
- export async function runProjectAdd(deps, options) {
40
- const { cwd, env, createClient, stdout, stderr } = deps;
41
- const trayYaml = await requireTrayYaml(deps);
42
- if (!trayYaml)
43
- return 1;
44
- if (refuseIfWorkspaceScope(trayYaml, stderr))
45
- return 1;
46
- if (trayYaml.projects.includes(options.projectId)) {
47
- printAlreadyTracked(stdout, options.projectId);
48
- return 0;
49
- }
50
- const credentials = await readCredentials(env);
51
- try {
52
- const token = resolveToken({
53
- tokenFlag: options.token,
54
- credentials,
55
- workspaceId: trayYaml.workspace_id,
56
- env,
57
- });
58
- const client = createClient(trayYaml.region, token);
59
- await client.request("GET", `/v2/projects/${options.projectId}`);
60
- printValidationSucceeded(stdout, options.projectId);
61
- }
62
- catch (err) {
63
- printValidationSkippedOrFailed(stdout, err.message);
64
- }
65
- const projects = [...trayYaml.projects, options.projectId].sort();
66
- await writeTrayYaml({ ...trayYaml, projects }, cwd);
67
- printAddedProject(stdout, options.projectId);
68
- return 0;
69
- }
70
36
  export async function runProjectRemove(deps, options) {
71
37
  const { cwd, readdir, rm, stdout, stderr } = deps;
72
38
  const trayYaml = await requireTrayYaml(deps);
@@ -136,27 +102,13 @@ function parseUuid(value, flagName) {
136
102
  function realDeps() {
137
103
  return {
138
104
  cwd: process.cwd(),
139
- env: process.env,
140
- createClient: (region, token) => createTrayClientForRegion(region, token),
141
105
  readdir: (dir) => fsReaddir(dir),
142
106
  rm: (dir) => fsRm(dir, { recursive: true, force: true }),
143
107
  stdout: (line) => console.log(line),
144
108
  stderr: (line) => console.error(line),
145
109
  };
146
110
  }
147
- export const projectCommand = new Command("project").description("Manage tray.yaml.projects (project scope only for add/remove)");
148
- projectCommand
149
- .command("add")
150
- .description("Track a project (project scope only)")
151
- .argument("<project-id>", "Tray project UUID", (value) => parseUuid(value, "project-id"))
152
- .option("-t, --token <token>", "Tray API token, for best-effort validation")
153
- .action(async (projectId, opts) => {
154
- const exitCode = await runProjectAdd(realDeps(), {
155
- projectId,
156
- token: opts.token,
157
- });
158
- process.exitCode = exitCode;
159
- });
111
+ export const projectCommand = new Command("project").description("Manage tray.yaml.projects (project scope only for remove; use `tray pull -p <id>` to add)");
160
112
  projectCommand
161
113
  .command("remove")
162
114
  .description("Untrack a project and delete its local directory (project scope only)")
@@ -9,7 +9,7 @@ import { readPullProgress, writePullProgress } from "../config/pullProgress.js";
9
9
  import { readProjectState, writeProjectState } from "../config/state.js";
10
10
  import { readTrayYaml, writeTrayYaml } from "../config/trayYaml.js";
11
11
  import { Scopes, TrayTypes, } from "../config/types.js";
12
- import { entityDirName } from "../lib/slug.js";
12
+ import { entityDirName, findDirsWithStaleSlug } from "../lib/slug.js";
13
13
  import { entityDisplayName, parseExportStructure, } from "../lib/exportStructure.js";
14
14
  import { extractScripts } from "../lib/scriptExtraction.js";
15
15
  import { sha256 } from "../lib/checksum.js";
@@ -74,10 +74,22 @@ export async function writeExportedProject(projectDirPath, exportStructure, stdo
74
74
  const resources = {};
75
75
  for (const { field, dirName, fileName, trayType } of ASSET_DIRS) {
76
76
  const entities = exportStructure[field] ?? [];
77
+ const entityParentDir = path.join(projectDirPath, dirName);
77
78
  for (const entity of entities) {
78
79
  const entityId = requireUuid(entity.id, `exportProject: ${field}[].id`);
79
80
  const entityDirName_ = entityDirName(entityId, entityDisplayName(entity));
80
- const entityDir = resolveContained(path.join(projectDirPath, dirName), entityDirName_);
81
+ const entityDir = resolveContained(entityParentDir, entityDirName_);
82
+ let staleDirNames;
83
+ try {
84
+ staleDirNames = findDirsWithStaleSlug(await fsReaddir(entityParentDir), entityId, entityDirName_);
85
+ }
86
+ catch {
87
+ staleDirNames = [];
88
+ }
89
+ for (const staleDirName of staleDirNames) {
90
+ const staleDir = await resolveContainedSafe(entityParentDir, staleDirName);
91
+ await fsRm(staleDir, { recursive: true, force: true });
92
+ }
81
93
  const entityJsonPath = path.join(entityDir, fileName);
82
94
  await assertNoSymlinkInPath(projectDirPath, entityJsonPath);
83
95
  await writeJson(entityJsonPath, entity);
@@ -195,7 +207,7 @@ export async function runPull(deps, options) {
195
207
  const dir = await resolveContainedSafe(projectsRoot, newDirName);
196
208
  let existingDirName;
197
209
  try {
198
- existingDirName = (await readdir(projectsRoot)).find((name) => name.split("--")[0] === validatedProjectId && name !== newDirName);
210
+ existingDirName = findDirsWithStaleSlug(await readdir(projectsRoot), validatedProjectId, newDirName)[0];
199
211
  }
200
212
  catch {
201
213
  existingDirName = undefined;
package/dist/lib/slug.js CHANGED
@@ -11,4 +11,7 @@ export function slugify(name) {
11
11
  export function entityDirName(uuid, name) {
12
12
  return `${uuid}--${slugify(name)}`;
13
13
  }
14
+ export function findDirsWithStaleSlug(dirNames, uuid, currentDirName) {
15
+ return dirNames.filter((name) => name.split("--")[0] === uuid && name !== currentDirName);
16
+ }
14
17
  //# sourceMappingURL=slug.js.map
@@ -5,20 +5,8 @@ export function printTrayYamlNotFound(stderr) {
5
5
  stderr(`${FAILURE_ICON} tray.yaml not found. Run \`${ansis.bold("tray init")}\` first.`);
6
6
  }
7
7
  export function printWorkspaceScopeDenied(stderr) {
8
- stderr(`${INFO_ICON} Operation denied. Project add/removal is not allowed in workspace scope.`);
9
- stderr(` Create your project with ${ansis.bold("--scope project")} (or omit --scope) in a new directory to use \`${ansis.bold("tray project add/remove")}\` commands.`);
10
- }
11
- export function printAlreadyTracked(stdout, projectId) {
12
- stdout(`${ansis.bold(projectId)} is already tracked.`);
13
- }
14
- export function printValidationSucceeded(stdout, projectId) {
15
- stdout(`Validation: ✓ (${ansis.bold(projectId)} found in Tray)`);
16
- }
17
- export function printValidationSkippedOrFailed(stdout, message) {
18
- stdout(`Validation: skipped or failed (${message})`);
19
- }
20
- export function printAddedProject(stdout, projectId) {
21
- stdout(`${ansis.bold("Added")} ${ansis.bold(projectId)} to tray.yaml.projects.`);
8
+ stderr(`${INFO_ICON} Operation denied. Project removal is not allowed in workspace scope.`);
9
+ stderr(` Create your project with ${ansis.bold("--scope project")} (or omit --scope) in a new directory to use \`${ansis.bold("tray project remove")}\`.`);
22
10
  }
23
11
  export function printNotTracked(stdout, projectId) {
24
12
  stdout(`${ansis.bold(projectId)} is not tracked.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trayai/tray-sync-cli",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
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"