@trayai/tray-sync-cli 1.0.1 → 1.0.3

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,12 +19,12 @@ 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 |
26
25
  | `tray env list` | List source and target environments with connectivity status |
27
26
  | `tray env remove <name>` | Remove a target environment and its per-project mappings |
27
+ | `tray env discover [env]` | List an env's projects (id + name, default) and/or matching authentications — `env` defaults to `source` |
28
28
  | `tray env resolve <env-name>` | Report unresolved auth requirements for a target env, with candidate matches |
29
29
  | `tray auth set -w <workspace-id> -r <region> -t <token>` | Set (or overwrite) a workspace's token |
30
30
  | `tray auth remove -w <workspace-id>` | Remove a workspace's stored token |
@@ -66,9 +66,17 @@ Example:
66
66
  tray init -r us1 -w 11111111-1111-1111-1111-111111111111 -t <token> -p <project_id>
67
67
  ```
68
68
 
69
+ Don't know the project ID yet? Run `init` without `-p` (an empty scaffold is fine), then:
70
+
71
+ ```bash
72
+ tray env discover
73
+ ```
74
+
75
+ lists every project in your source workspace — id + name — so you don't have to dig it out of Tray's UI.
76
+
69
77
  A few notes on the flags:
70
78
 
71
- - **`-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.
72
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.
73
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:
74
82
  ```bash
@@ -15,14 +15,15 @@ import { PUBLIC_REGIONS, parseRegion } from "../lib/regionParsing.js";
15
15
  import { resolveToken } from "../lib/tokenResolution.js";
16
16
  import { isUuid } from "../lib/uuid.js";
17
17
  import { OutputFormats, parseFormat } from "../lib/outputFormat.js";
18
- import { listWorkspaceAuthentications, matchAuthCandidates, prioritizeAuths, } from "../lib/authDiscovery.js";
18
+ import { dedupeUsedAuths, listWorkspaceAuthentications, matchAuthCandidates, prioritizeAuths, usedAuthsInProject, } from "../lib/authDiscovery.js";
19
19
  import { isFullyResolved, resolveProjectRequirements, } from "../lib/promoteTarget.js";
20
20
  import { checkProjectDrift, DriftStatuses } from "./status.js";
21
- import { printAddedEnvironment, printConnectivityCheck, printEnvAlreadyExists, printEnvNotConfigured, printEnvResolveProject, printEnvResolveProjectFullyResolved, printInvalidEnvName, printNoTargetEnvironments, printNoTokenSkippingConnectivity, printRemovedCredentials, printRemovedEnvironment, printEnvResolveProjectError, printRemovedMapping, printSourceEnvTable, printTargetEnvHeader, printTargetEnvTable, printTokenNotSaved, printTrayYamlNotFound, } from "../views/env/env.v2.js";
21
+ import { printAddedEnvironment, printConnectivityCheck, printDiscoverAuthsHeader, printDiscoverEnvHeader, printDiscoverNoAuthsUsed, printDiscoverNoProjects, printDiscoverProjectsTable, printEnvAlreadyExists, printEnvNotConfigured, printEnvNotFound, printEnvResolveProject, printReservedEnvName, printEnvResolveProjectFullyResolved, printInvalidEnvName, printNoTargetEnvironments, printNoTokenSkippingConnectivity, printRemovedCredentials, printRemovedEnvironment, printEnvResolveProjectError, printRemovedMapping, printSourceAuthsTable, printSourceEnvTable, printTargetAuthsTable, printTargetEnvHeader, printTargetEnvTable, printTokenNotSaved, printTrayYamlNotFound, } from "../views/env/env.v2.js";
22
22
  import { printEnvListJson, printEnvResolveResultJson, } from "../views/env/shared.js";
23
23
  import { printError, printInvalidAuthMappings, printLocalDrift, printNoTargetProjectMapped, printProjectError, printProjectNoStateFile, printProjectNotPulledLocally, printTargetEnvNotFound, } from "../views/promote/promote.v2.js";
24
24
  import { FAILURE_ICON, SUCCESS_ICON } from "../views/icons.js";
25
25
  const ENV_NAME_RE = /^[a-zA-Z0-9_-]+$/;
26
+ export const SOURCE_ENV_NAME = "source";
26
27
  function emptyMapping() {
27
28
  return { authentications: {}, config: {}, connectors: [], services: [] };
28
29
  }
@@ -60,6 +61,10 @@ export async function runEnvAdd(deps, options) {
60
61
  printInvalidEnvName(stderr, ENV_NAME_RE, options.name);
61
62
  return 1;
62
63
  }
64
+ if (options.name === SOURCE_ENV_NAME) {
65
+ printReservedEnvName(stderr, options.name);
66
+ return 1;
67
+ }
63
68
  const environments = await readEnvironments(cwd);
64
69
  if (environments.environments[options.name]) {
65
70
  printEnvAlreadyExists(stderr, options.name);
@@ -194,6 +199,119 @@ export async function runEnvRemove(deps, options) {
194
199
  }
195
200
  return 0;
196
201
  }
202
+ async function listProjectsWithNames(client, workspaceId) {
203
+ const result = (await client.request("GET", `/v2/projects/workspaces/${workspaceId}`));
204
+ return result.projects;
205
+ }
206
+ function resolveDiscoverTarget(envName, trayYaml, environments) {
207
+ if (envName === SOURCE_ENV_NAME) {
208
+ return { region: trayYaml.region, workspaceId: trayYaml.workspace_id };
209
+ }
210
+ const entry = environments.environments[envName];
211
+ if (!entry)
212
+ return undefined;
213
+ return { region: entry.region, workspaceId: entry.workspace_id };
214
+ }
215
+ async function usedAuthsAcrossLocalProjects(deps, cwd, projectIds) {
216
+ const projectsRoot = path.join(cwd, "projects");
217
+ let existingDirs;
218
+ try {
219
+ existingDirs = await deps.readdir(projectsRoot);
220
+ }
221
+ catch {
222
+ existingDirs = [];
223
+ }
224
+ const used = [];
225
+ for (const projectId of projectIds) {
226
+ const dirName = existingDirs.find((name) => name.split("--")[0] === projectId);
227
+ if (!dirName)
228
+ continue;
229
+ const projectDirPath = path.join(projectsRoot, dirName);
230
+ const projectFiles = await deps.readdirRecursive(projectDirPath);
231
+ used.push(...(await usedAuthsInProject(deps.readFile, projectDirPath, projectFiles)));
232
+ }
233
+ return dedupeUsedAuths(used);
234
+ }
235
+ export async function runEnvDiscover(deps, options) {
236
+ const { cwd, env, createClient, readdir, readFile, readdirRecursive, stdout, stderr } = deps;
237
+ const trayYaml = await readTrayYaml(cwd);
238
+ if (!trayYaml) {
239
+ printTrayYamlNotFound(stderr);
240
+ return 1;
241
+ }
242
+ const environments = await readEnvironments(cwd);
243
+ const target = resolveDiscoverTarget(options.env, trayYaml, environments);
244
+ if (!target) {
245
+ printEnvNotFound(stderr, options.env);
246
+ return 1;
247
+ }
248
+ const credentials = await readCredentials(env);
249
+ let token;
250
+ try {
251
+ token = resolveToken({
252
+ tokenFlag: options.token,
253
+ credentials,
254
+ workspaceId: target.workspaceId,
255
+ envName: options.env === SOURCE_ENV_NAME ? undefined : options.env,
256
+ env,
257
+ });
258
+ }
259
+ catch (err) {
260
+ printError(stderr, err.message);
261
+ return 1;
262
+ }
263
+ const client = createClient(target.region, token);
264
+ const showProjects = options.all || options.projects || (!options.projects && !options.authentications);
265
+ const showAuths = Boolean(options.all || options.authentications);
266
+ const isSource = options.env === SOURCE_ENV_NAME;
267
+ printDiscoverEnvHeader(stdout, options.env, isSource);
268
+ if (showProjects) {
269
+ const projects = await listProjectsWithNames(client, target.workspaceId);
270
+ const rows = projects
271
+ .map((p) => ({ id: p.id, name: p.name }))
272
+ .sort((a, b) => a.name.localeCompare(b.name));
273
+ if (rows.length === 0) {
274
+ printDiscoverNoProjects(stdout, options.env);
275
+ }
276
+ else {
277
+ printDiscoverProjectsTable(stdout, rows);
278
+ }
279
+ }
280
+ if (showAuths) {
281
+ const used = await usedAuthsAcrossLocalProjects({ readdir, readFile, readdirRecursive }, cwd, trayYaml.projects);
282
+ if (used.length === 0) {
283
+ printDiscoverNoAuthsUsed(stdout);
284
+ }
285
+ else {
286
+ const { authentications } = await listWorkspaceAuthentications(client, target.region, target.workspaceId);
287
+ printDiscoverAuthsHeader(stdout);
288
+ if (isSource) {
289
+ const rows = used.map((requirement) => {
290
+ const self = authentications.find((a) => a.group === requirement.group);
291
+ return { name: self?.name ?? requirement.title, id: self?.id ?? "", service: requirement.service };
292
+ });
293
+ rows.sort((a, b) => a.service.name.localeCompare(b.service.name) || a.name.localeCompare(b.name));
294
+ printSourceAuthsTable(stdout, rows);
295
+ }
296
+ else {
297
+ const matchedTitlesByCandidateId = new Map();
298
+ for (const requirement of used) {
299
+ for (const candidate of matchAuthCandidates(requirement, authentications)) {
300
+ const titles = matchedTitlesByCandidateId.get(candidate.id) ?? [];
301
+ titles.push(requirement.title);
302
+ matchedTitlesByCandidateId.set(candidate.id, titles);
303
+ }
304
+ }
305
+ const rows = authentications
306
+ .filter((a) => matchedTitlesByCandidateId.has(a.id))
307
+ .map((a) => ({ name: a.name, id: a.id, service: a.service, matchedTitles: matchedTitlesByCandidateId.get(a.id) }));
308
+ rows.sort((a, b) => a.service.name.localeCompare(b.service.name) || a.name.localeCompare(b.name));
309
+ printTargetAuthsTable(stdout, rows);
310
+ }
311
+ }
312
+ }
313
+ return 0;
314
+ }
197
315
  function findProjectDirName(existingDirs, projectId) {
198
316
  return existingDirs.find((name) => name.split("--")[0] === projectId);
199
317
  }
@@ -310,7 +428,7 @@ export async function runEnvResolve(deps, options) {
310
428
  }
311
429
  if (resolution.status === "no_target_project_mapped") {
312
430
  if (!options.dryRun) {
313
- printNoTargetProjectMapped(stderr, sourceProjectName, projectId, options.to, resolution.mappingsPath);
431
+ printNoTargetProjectMapped(stderr, sourceProjectName, projectId, options.to, resolution.mappingsPath, "env resolve");
314
432
  return 1;
315
433
  }
316
434
  anyUnresolved = true;
@@ -518,6 +636,24 @@ envCommand
518
636
  });
519
637
  process.exitCode = exitCode;
520
638
  });
639
+ envCommand
640
+ .command("discover")
641
+ .description("List available projects and/or authentications for an env (default: source) — --projects (default) shows name+id; --authentications shows workspace auths matching what locally pulled projects use; --all shows both")
642
+ .argument("[env]", `"${SOURCE_ENV_NAME}" (default) or a target environment name (see \`tray env list\`)`, SOURCE_ENV_NAME)
643
+ .option("--projects", "list the env's projects (name + id) — the default when no flag is given")
644
+ .option("--authentications", "list authentications in the env's workspace matching what locally pulled source projects use")
645
+ .option("--all", "shorthand for --projects and --authentications together")
646
+ .option("-t, --token <token>", "Tray API token")
647
+ .action(async (envName, opts) => {
648
+ const exitCode = await runEnvDiscover(realDeps(), {
649
+ env: envName,
650
+ token: opts.token,
651
+ projects: Boolean(opts.projects),
652
+ authentications: Boolean(opts.authentications),
653
+ all: Boolean(opts.all),
654
+ });
655
+ process.exitCode = exitCode;
656
+ });
521
657
  envCommand
522
658
  .command("resolve")
523
659
  .description("Report unresolved auth requirements for a target env, with candidate matches")
@@ -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)")
@@ -163,7 +163,7 @@ export async function runPromote(deps, options) {
163
163
  mappings_path: resolution.mappingsPath,
164
164
  });
165
165
  }
166
- printNoTargetProjectMapped(stderr, sourceProjectName, projectId, options.to, resolution.mappingsPath);
166
+ printNoTargetProjectMapped(stderr, sourceProjectName, projectId, options.to, resolution.mappingsPath, "promote");
167
167
  return 1;
168
168
  }
169
169
  if (resolution.status === "invalid_auth_mappings") {
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import path from "node:path";
2
3
  import { urlsForRegion } from "./region.js";
3
4
  const GET_AUTHENTICATIONS_QUERY = `
4
5
  query GetAuthenticationsPrivate(
@@ -73,4 +74,56 @@ export function matchAuthCandidates(requirement, authentications) {
73
74
  export function prioritizeAuths(candidates) {
74
75
  return candidates;
75
76
  }
77
+ export function extractUsedAuths(workflow) {
78
+ const steps = workflow.steps;
79
+ if (typeof steps !== "object" || steps === null) {
80
+ return [];
81
+ }
82
+ const used = [];
83
+ for (const step of Object.values(steps)) {
84
+ if (typeof step !== "object" || step === null)
85
+ continue;
86
+ const authentication = step.authentication;
87
+ if (typeof authentication !== "object" || authentication === null)
88
+ continue;
89
+ const auth = authentication;
90
+ const group = auth.group;
91
+ const title = auth.title;
92
+ const serviceName = auth.service_name;
93
+ const serviceVersion = auth.service_version;
94
+ if (typeof group !== "string" ||
95
+ typeof title !== "string" ||
96
+ typeof serviceName !== "string" ||
97
+ (typeof serviceVersion !== "string" && typeof serviceVersion !== "number")) {
98
+ continue;
99
+ }
100
+ used.push({ group, title, service: { name: serviceName, version: String(serviceVersion) } });
101
+ }
102
+ return used;
103
+ }
104
+ export function dedupeUsedAuths(used) {
105
+ const seen = new Map();
106
+ for (const auth of used) {
107
+ if (!seen.has(auth.group)) {
108
+ seen.set(auth.group, auth);
109
+ }
110
+ }
111
+ return [...seen.values()];
112
+ }
113
+ export async function usedAuthsInProject(readFile, projectDirPath, projectFiles) {
114
+ const workflowJsonPaths = projectFiles.filter((relativePath) => relativePath.startsWith(`workflows${path.sep}`) && relativePath.endsWith(`${path.sep}workflow.json`));
115
+ const used = [];
116
+ for (const relativePath of workflowJsonPaths) {
117
+ let workflow;
118
+ try {
119
+ const raw = await readFile(path.join(projectDirPath, relativePath));
120
+ workflow = JSON.parse(raw);
121
+ }
122
+ catch {
123
+ continue;
124
+ }
125
+ used.push(...extractUsedAuths(workflow));
126
+ }
127
+ return used;
128
+ }
76
129
  //# sourceMappingURL=authDiscovery.js.map
@@ -1,9 +1,12 @@
1
1
  import ansis from "ansis";
2
2
  import Table from "cli-table3";
3
- import { FAILURE_ICON, INFO_ICON, WARNING_ICON } from "../icons.js";
3
+ import { FAILURE_ICON, INFO_ICON, SUCCESS_ICON, WARNING_ICON } from "../icons.js";
4
4
  export function printInvalidEnvName(stderr, nameRe, name) {
5
5
  stderr(`${FAILURE_ICON} Environment name must match ${nameRe}: "${name}"`);
6
6
  }
7
+ export function printReservedEnvName(stderr, name) {
8
+ stderr(`${FAILURE_ICON} "${ansis.bold(name)}" is reserved (used by \`${ansis.bold("tray env discover")}\` to mean the source workspace) and cannot be a target environment name.`);
9
+ }
7
10
  export function printEnvAlreadyExists(stderr, name) {
8
11
  stderr(`${FAILURE_ICON} Environment "${ansis.bold(name)}" already exists. Run \`${ansis.bold("tray env remove")}\` first to replace it.`);
9
12
  }
@@ -34,6 +37,9 @@ export function printConnectivityCheck(stdout, description) {
34
37
  export function printTrayYamlNotFound(stderr) {
35
38
  stderr(`${FAILURE_ICON} tray.yaml not found. Run \`${ansis.bold("tray init")}\` first.`);
36
39
  }
40
+ export function printEnvNotFound(stderr, name) {
41
+ stderr(`${FAILURE_ICON} "${ansis.bold(name)}" is not "${ansis.bold("source")}" and is not a configured target environment. See \`${ansis.bold("tray env list")}\` for configured names.`);
42
+ }
37
43
  function envTable() {
38
44
  return new Table({
39
45
  head: ["Environment", "Region", "Workspace ID", "Status"].map((h) => ansis.bold(h)),
@@ -73,7 +79,7 @@ export function printRemovedCredentials(stdout, workspaceId) {
73
79
  stdout(`${ansis.bold("Removed")} credentials for workspace ${workspaceId}.`);
74
80
  }
75
81
  export function printEnvResolveProjectFullyResolved(stdout, projectId, sourceProjectName) {
76
- stdout(`${ansis.bold(`${sourceProjectName} (${projectId})`)}: fully resolved, nothing to do`);
82
+ stdout(`${SUCCESS_ICON} ${ansis.bold(`${sourceProjectName} (${projectId})`)}: fully resolved, nothing to do`);
77
83
  }
78
84
  export function printEnvResolveProjectError(stdout, projectId, sourceProjectName, error) {
79
85
  stdout(`${ansis.bold(`${sourceProjectName} (${projectId})`)}:`);
@@ -112,4 +118,51 @@ export function printEnvResolveProject(stdout, project) {
112
118
  stdout(` new config keys require mapping — see mappings.json's config entry`);
113
119
  }
114
120
  }
121
+ export function printDiscoverEnvHeader(stdout, envName, isSource) {
122
+ stdout(ansis.bold(isSource ? "Environment: source" : `Environment: target (${envName})`));
123
+ stdout("");
124
+ }
125
+ export function printDiscoverNoProjects(stdout, envName) {
126
+ stdout(`${INFO_ICON} No projects found in "${ansis.bold(envName)}".`);
127
+ }
128
+ export function printDiscoverProjectsTable(stdout, rows) {
129
+ stdout("[Projects]");
130
+ const table = new Table({
131
+ head: ["Name", "ID"].map((h) => ansis.bold(h)),
132
+ style: { head: [] },
133
+ });
134
+ for (const row of rows) {
135
+ table.push([row.name, row.id]);
136
+ }
137
+ stdout(table.toString());
138
+ stdout("");
139
+ }
140
+ export function printDiscoverNoAuthsUsed(stdout) {
141
+ stdout(`${INFO_ICON} No locally pulled project uses an authentication.`);
142
+ }
143
+ export function printDiscoverAuthsHeader(stdout) {
144
+ stdout("[Available Authentications]");
145
+ }
146
+ export function printSourceAuthsTable(stdout, rows) {
147
+ const table = new Table({
148
+ head: ["Name", "ID", "Service"].map((h) => ansis.bold(h)),
149
+ style: { head: [] },
150
+ });
151
+ for (const row of rows) {
152
+ table.push([row.name, row.id, `${row.service.name} v${row.service.version}`]);
153
+ }
154
+ stdout(table.toString());
155
+ stdout("");
156
+ }
157
+ export function printTargetAuthsTable(stdout, rows) {
158
+ const table = new Table({
159
+ head: ["Name", "Service", "ID", "Match"].map((h) => ansis.bold(h)),
160
+ style: { head: [] },
161
+ });
162
+ for (const row of rows) {
163
+ table.push([row.name, `${row.service.name} v${row.service.version}`, row.id, row.matchedTitles.join("\n")]);
164
+ }
165
+ stdout(table.toString());
166
+ stdout("");
167
+ }
115
168
  //# sourceMappingURL=env.v2.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.`);
@@ -22,9 +22,17 @@ export function printProjectNoStateFile(stderr, projectId) {
22
22
  export function printLocalDrift(stderr, projectId, dirName, dirty) {
23
23
  printIssueBlock(stderr, `Project ${ansis.bold(`${projectId} (${dirName})`)} has local drift; refusing to promote:`, dirty.map((d) => `${d.status}\t${d.path}`), `Run \`${ansis.bold("tray pull")}\` to refresh, or resolve the drift, then retry.`);
24
24
  }
25
- export function printNoTargetProjectMapped(stderr, sourceProjectName, projectId, envName, mappingsPath) {
26
- stderr(`${FAILURE_ICON} ${ansis.bold(`"${sourceProjectName}" (${projectId})`)} has no target project mapped for "${ansis.bold(envName)}".`);
27
- stderr(`Fill in target_project_id in ${mappingsPath}, or re-run with ${ansis.bold("--auto-create-projects")}.`);
25
+ export function printNoTargetProjectMapped(stderr, sourceProjectName, projectId, envName, mappingsPath, command) {
26
+ if (command === "promote") {
27
+ stderr(`${FAILURE_ICON} ${ansis.bold(`"${sourceProjectName}" (${projectId})`)} has no target project mapped for "${ansis.bold(envName)}".`);
28
+ stderr(`Fill in target_project_id in ${mappingsPath}, or re-run with ${ansis.bold("--auto-create-projects")}.`);
29
+ return;
30
+ }
31
+ stderr("");
32
+ stderr("Your source environment is missing the following resolution:");
33
+ stderr(`${FAILURE_ICON} Project ${ansis.bold(`"${sourceProjectName}" (${projectId})`)} has no target project mapped for "${ansis.bold(envName)}".`);
34
+ stderr(`Fill in ${ansis.bold("target_project_id")} in ${mappingsPath}.`);
35
+ stderr(`${INFO_ICON} Run \`${ansis.bold(`tray env discover ${envName}`)}\` to see available target projects.`);
28
36
  }
29
37
  export function printInvalidAuthMappings(stderr, projectId, sourceProjectName, invalidAuthMappings) {
30
38
  printIssueBlock(stderr, `Project ${ansis.bold(`${projectId} (${sourceProjectName})`)} has unresolved authentication mappings:`, invalidAuthMappings.map((invalid) => `${ansis.bold(invalid.authExportId)}: "${invalid.value}" is not a valid target authentication id`), "Fill in a real target authentication id for each entry above, then retry.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trayai/tray-sync-cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
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"