trajectories-sh 1.22.0 → 1.23.1

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/cli.js CHANGED
@@ -6,7 +6,7 @@ import { readFileSync } from "fs";
6
6
  import { fileURLToPath } from "url";
7
7
  import { dirname, join } from "path";
8
8
  var __dirname = dirname(fileURLToPath(import.meta.url));
9
- var version = "1.20.0";
9
+ var version = "1.23.1";
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
12
12
  version = pkg.version;
@@ -382,6 +382,210 @@ addCommentBodyOptions(
382
382
  comment.command("update <comment-id>").description("Update the body of one of your comments").option("--json", "Print machine-readable JSON")
383
383
  ).action(updateCommentAction);
384
384
  comment.command("delete <comment-id>").description("Delete one of your comments").option("-y, --yes", "Skip confirmation prompt").option("--json", "Print machine-readable JSON").action(deleteCommentAction);
385
+ async function resolveProject(ref) {
386
+ const { listProjects, resolveProjectRef } = await import("./projects-ESYLKFUR.js");
387
+ const projects = await listProjects();
388
+ return { ...resolveProjectRef(ref, projects), projects };
389
+ }
390
+ async function listProjectsAction(opts) {
391
+ try {
392
+ const { listProjects } = await import("./projects-ESYLKFUR.js");
393
+ const projects = await listProjects();
394
+ if (opts.json) {
395
+ console.log(JSON.stringify(projects, null, 2));
396
+ return;
397
+ }
398
+ if (projects.length === 0) {
399
+ console.log("\n No projects yet. Create one with: trajectories project create <name>\n");
400
+ return;
401
+ }
402
+ const chalk = (await import("chalk")).default;
403
+ console.log(`
404
+ ${chalk.bold(`Projects (${projects.length})`)}
405
+ `);
406
+ for (const p of projects) {
407
+ const sets = `${p.n_jobs} set${p.n_jobs === 1 ? "" : "s"}`;
408
+ const trials = `${p.n_trials} trajector${p.n_trials === 1 ? "y" : "ies"}`;
409
+ const rate = p.pass_rate != null ? ` \xB7 ${(p.pass_rate * 100).toFixed(0)}%` : "";
410
+ console.log(` ${chalk.bold(p.name)} ${chalk.dim(`(${p.visibility})`)}`);
411
+ console.log(` ${chalk.dim(p.id)}`);
412
+ console.log(` ${chalk.dim(`${sets} \xB7 ${trials}${rate}`)}`);
413
+ if (p.description) console.log(` ${chalk.dim(p.description)}`);
414
+ console.log("");
415
+ }
416
+ } catch (error) {
417
+ printCommentError(error, Boolean(opts.json));
418
+ }
419
+ }
420
+ async function showProjectAction(ref, opts) {
421
+ try {
422
+ const { getProject } = await import("./projects-ESYLKFUR.js");
423
+ const { id } = await resolveProject(ref);
424
+ const project2 = await getProject(id);
425
+ if (opts.json) {
426
+ console.log(JSON.stringify(project2, null, 2));
427
+ return;
428
+ }
429
+ const chalk = (await import("chalk")).default;
430
+ console.log(`
431
+ ${chalk.bold(project2.name)} ${chalk.dim(`(${project2.visibility})`)}`);
432
+ console.log(` ${chalk.dim(project2.id)}
433
+ `);
434
+ if (project2.description) console.log(` ${project2.description}
435
+ `);
436
+ console.log(
437
+ ` ${project2.n_jobs} trajectory set${project2.n_jobs === 1 ? "" : "s"} \xB7 ${project2.n_trials} trajector${project2.n_trials === 1 ? "y" : "ies"}` + (project2.pass_rate != null ? ` \xB7 ${(project2.pass_rate * 100).toFixed(0)}% pass` : "") + "\n"
438
+ );
439
+ for (const job of project2.jobs || []) {
440
+ const trials = job.n_trials != null ? chalk.dim(` (${job.n_trials})`) : "";
441
+ console.log(` ${job.name || job.slug || job.id}${trials}`);
442
+ console.log(` ${chalk.dim(job.id)}`);
443
+ }
444
+ console.log("");
445
+ } catch (error) {
446
+ printCommentError(error, Boolean(opts.json));
447
+ }
448
+ }
449
+ async function createProjectAction(name, opts) {
450
+ try {
451
+ const { createProject } = await import("./projects-ESYLKFUR.js");
452
+ const project2 = await createProject({
453
+ name,
454
+ description: opts.description,
455
+ color: opts.color,
456
+ slug: opts.slug,
457
+ visibility: opts.visibility
458
+ });
459
+ if (opts.json) {
460
+ console.log(JSON.stringify(project2));
461
+ return;
462
+ }
463
+ console.log(`
464
+ \u2713 Created project "${project2.name}" (${project2.visibility})`);
465
+ console.log(` ${project2.id}
466
+ `);
467
+ } catch (error) {
468
+ printCommentError(error, Boolean(opts.json));
469
+ }
470
+ }
471
+ async function updateProjectAction(ref, opts) {
472
+ try {
473
+ const patch = {};
474
+ if (opts.name) patch.name = opts.name;
475
+ if (opts.description !== void 0) patch.description = opts.description;
476
+ if (opts.color) patch.color = opts.color;
477
+ if (opts.slug) patch.slug = opts.slug;
478
+ if (opts.visibility) patch.visibility = opts.visibility;
479
+ if (Object.keys(patch).length === 0) {
480
+ throw new Error("Nothing to update. Pass --name, --description, --color, --slug or --visibility");
481
+ }
482
+ const { updateProject } = await import("./projects-ESYLKFUR.js");
483
+ const { id } = await resolveProject(ref);
484
+ const project2 = await updateProject(id, patch);
485
+ if (opts.json) console.log(JSON.stringify(project2));
486
+ else console.log(`
487
+ \u2713 Updated "${project2.name}"
488
+ `);
489
+ } catch (error) {
490
+ printCommentError(error, Boolean(opts.json));
491
+ }
492
+ }
493
+ async function deleteProjectAction(ref, opts) {
494
+ try {
495
+ const { deleteProject } = await import("./projects-ESYLKFUR.js");
496
+ const { id, project: project2 } = await resolveProject(ref);
497
+ const label = project2?.name ? `"${project2.name}"` : id;
498
+ if (!opts.yes) {
499
+ if (!process.stdin.isTTY) {
500
+ throw new Error("Refusing non-interactive deletion without --yes");
501
+ }
502
+ const readline = await import("readline");
503
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
504
+ const answer = await new Promise((resolve) => {
505
+ rl.question(
506
+ ` Delete project ${label}? Its trajectory sets are ungrouped, not deleted. (y/N) `,
507
+ resolve
508
+ );
509
+ });
510
+ rl.close();
511
+ if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
512
+ if (opts.json) console.log(JSON.stringify({ status: "cancelled", project_id: id }));
513
+ else console.log(" Cancelled.");
514
+ return;
515
+ }
516
+ }
517
+ await deleteProject(id);
518
+ if (opts.json) console.log(JSON.stringify({ status: "deleted", project_id: id }));
519
+ else console.log(` \u2713 Deleted project ${label}`);
520
+ } catch (error) {
521
+ printCommentError(error, Boolean(opts.json));
522
+ }
523
+ }
524
+ async function projectAddAction(ref, sets, opts) {
525
+ try {
526
+ const { assignJobs } = await import("./projects-ESYLKFUR.js");
527
+ const { id, project: project2 } = await resolveProject(ref);
528
+ const result = await assignJobs(id, sets, "add");
529
+ if (opts.json) {
530
+ console.log(JSON.stringify(result));
531
+ return;
532
+ }
533
+ const label = project2?.name ? `"${project2.name}"` : id;
534
+ console.log(
535
+ `
536
+ \u2713 Filed ${result.assigned} trajectory set${result.assigned === 1 ? "" : "s"} into ${label}
537
+ `
538
+ );
539
+ } catch (error) {
540
+ printCommentError(error, Boolean(opts.json));
541
+ }
542
+ }
543
+ async function projectRemoveAction(refOrSet, sets, opts) {
544
+ try {
545
+ const { assignJobs } = await import("./projects-ESYLKFUR.js");
546
+ if (opts.all) {
547
+ const result2 = await assignJobs(null, [refOrSet, ...sets], "remove");
548
+ if (opts.json) console.log(JSON.stringify(result2));
549
+ else
550
+ console.log(
551
+ `
552
+ \u2713 Removed ${result2.assigned} trajectory set${result2.assigned === 1 ? "" : "s"} from all projects
553
+ `
554
+ );
555
+ return;
556
+ }
557
+ if (sets.length === 0) {
558
+ throw new Error(
559
+ "Specify which trajectory sets to remove, or pass --all to unfile them from every project"
560
+ );
561
+ }
562
+ const { id, project: project2 } = await resolveProject(refOrSet);
563
+ const result = await assignJobs(id, sets, "remove");
564
+ if (opts.json) {
565
+ console.log(JSON.stringify(result));
566
+ return;
567
+ }
568
+ const label = project2?.name ? `"${project2.name}"` : id;
569
+ console.log(
570
+ `
571
+ \u2713 Removed ${result.assigned} trajectory set${result.assigned === 1 ? "" : "s"} from ${label}
572
+ `
573
+ );
574
+ } catch (error) {
575
+ printCommentError(error, Boolean(opts.json));
576
+ }
577
+ }
578
+ var project = program.command("project").description("Manage projects and the trajectory sets filed into them");
579
+ project.command("list").description("List projects in your organization").option("--json", "Print machine-readable JSON").action(listProjectsAction);
580
+ project.command("show <project>").description("Show a project and the trajectory sets in it (id, URL, slug or name)").option("--json", "Print machine-readable JSON").action(showProjectAction);
581
+ project.command("create <name>").description("Create a project").option("-d, --description <text>", "Project description").option(
582
+ "-c, --color <color>",
583
+ "Palette key: slate, blue, green, amber, purple, pink, teal, red"
584
+ ).option("--slug <slug>", "URL slug (defaults to a slugified name)").option("--visibility <visibility>", "public, unlisted (default) or private").option("--json", "Print machine-readable JSON").action(createProjectAction);
585
+ project.command("update <project>").description("Rename a project or change its description, colour, slug or visibility").option("-n, --name <name>", "New name").option("-d, --description <text>", "New description").option("-c, --color <color>", "New palette key").option("--slug <slug>", "New URL slug").option("--visibility <visibility>", "public, unlisted or private").option("--json", "Print machine-readable JSON").action(updateProjectAction);
586
+ project.command("delete <project>").description("Delete a project (its trajectory sets are ungrouped, not deleted)").option("-y, --yes", "Skip confirmation prompt").option("--json", "Print machine-readable JSON").action(deleteProjectAction);
587
+ project.command("add <project> <sets...>").description("File one or more trajectory sets into a project (URLs or ids)").option("--json", "Print machine-readable JSON").action(projectAddAction);
588
+ project.command("remove <project|set> [sets...]").description("Remove trajectory sets from a project, or from all with --all").option("--all", "Unfile the given sets from every project").option("--json", "Print machine-readable JSON").action(projectRemoveAction);
385
589
  program.command("whoami").description("Show current authentication status").action(async () => {
386
590
  const chalk = (await import("chalk")).default;
387
591
  const { status } = await import("./auth-4UEZSE4C.js");
@@ -395,4 +599,9 @@ program.command("whoami").description("Show current authentication status").acti
395
599
  console.log(chalk.dim(" Not logged in. Run: trajectories auth login"));
396
600
  }
397
601
  });
602
+ program.showHelpAfterError("(run: trajectories help)");
603
+ if (process.argv.length <= 2) {
604
+ program.outputHelp();
605
+ process.exit(1);
606
+ }
398
607
  await program.parseAsync();
@@ -0,0 +1,148 @@
1
+ import {
2
+ parseJobId
3
+ } from "./chunk-IUSBK45Q.js";
4
+ import "./chunk-35B24E3O.js";
5
+ import {
6
+ getApiUrl,
7
+ getAuthHeader
8
+ } from "./chunk-Q2ZEN43L.js";
9
+
10
+ // src/projects.ts
11
+ var PROJECT_COLORS = [
12
+ "slate",
13
+ "blue",
14
+ "green",
15
+ "amber",
16
+ "purple",
17
+ "pink",
18
+ "teal",
19
+ "red"
20
+ ];
21
+ var PROJECT_VISIBILITIES = ["public", "unlisted", "private"];
22
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
23
+ async function apiRequest(path, init = {}) {
24
+ const auth = await getAuthHeader();
25
+ if (!auth) {
26
+ throw new Error("Not authenticated. Run: trajectories auth login");
27
+ }
28
+ const headers = {
29
+ ...init.headers,
30
+ Authorization: auth
31
+ };
32
+ const response = await fetch(`${getApiUrl()}${path}`, { ...init, headers });
33
+ if (!response.ok) {
34
+ const responseText = await response.text();
35
+ let detail = responseText;
36
+ try {
37
+ const payload = JSON.parse(responseText);
38
+ if (typeof payload?.detail === "string") detail = payload.detail;
39
+ } catch {
40
+ }
41
+ const message = detail || `Request failed (${response.status})`;
42
+ const error = new Error(message);
43
+ error.status = response.status;
44
+ throw error;
45
+ }
46
+ if (response.status === 204) return void 0;
47
+ return response.json();
48
+ }
49
+ async function listProjects() {
50
+ return apiRequest("/api/org/projects");
51
+ }
52
+ async function getProject(projectId) {
53
+ return apiRequest(`/api/org/projects/${encodeURIComponent(projectId)}`);
54
+ }
55
+ function resolveProjectRef(ref, projects) {
56
+ const trimmed = (ref || "").trim();
57
+ if (!trimmed) throw new Error("A project is required");
58
+ const urlMatch = trimmed.match(/\/projects\/([0-9a-f-]{36})/i);
59
+ if (urlMatch) return { id: urlMatch[1], project: projects.find((p) => p.id === urlMatch[1]) };
60
+ if (UUID_RE.test(trimmed) && !trimmed.includes("/")) {
61
+ return { id: trimmed, project: projects.find((p) => p.id === trimmed) };
62
+ }
63
+ const needle = trimmed.toLowerCase();
64
+ const bySlug = projects.filter((p) => (p.slug || "").toLowerCase() === needle);
65
+ if (bySlug.length === 1) return { id: bySlug[0].id, project: bySlug[0] };
66
+ const byName = projects.filter((p) => (p.name || "").toLowerCase() === needle);
67
+ if (byName.length === 1) return { id: byName[0].id, project: byName[0] };
68
+ if (byName.length > 1) {
69
+ throw new Error(
70
+ `${byName.length} projects are named "${trimmed}". Use the project id or URL instead:
71
+ ` + byName.map((p) => ` ${p.id} ${p.name}`).join("\n")
72
+ );
73
+ }
74
+ const partial = projects.filter((p) => (p.name || "").toLowerCase().includes(needle));
75
+ if (partial.length === 1) return { id: partial[0].id, project: partial[0] };
76
+ if (partial.length > 1) {
77
+ throw new Error(
78
+ `"${trimmed}" matches ${partial.length} projects. Be more specific, or use the id:
79
+ ` + partial.map((p) => ` ${p.id} ${p.name}`).join("\n")
80
+ );
81
+ }
82
+ throw new Error(
83
+ `No project matching "${trimmed}". Run: trajectories project list`
84
+ );
85
+ }
86
+ async function createProject(input) {
87
+ const color = input.color ?? "slate";
88
+ if (!PROJECT_COLORS.includes(color)) {
89
+ throw new Error(`--color must be one of: ${PROJECT_COLORS.join(", ")}`);
90
+ }
91
+ const visibility = input.visibility ?? "unlisted";
92
+ if (!PROJECT_VISIBILITIES.includes(visibility)) {
93
+ throw new Error(`--visibility must be one of: ${PROJECT_VISIBILITIES.join(", ")}`);
94
+ }
95
+ return apiRequest("/api/org/projects", {
96
+ method: "POST",
97
+ headers: { "Content-Type": "application/json" },
98
+ body: JSON.stringify({
99
+ name: input.name,
100
+ description: input.description ?? "",
101
+ color,
102
+ visibility,
103
+ ...input.slug ? { slug: input.slug } : {}
104
+ })
105
+ });
106
+ }
107
+ async function updateProject(projectId, patch) {
108
+ if (patch.color && !PROJECT_COLORS.includes(patch.color)) {
109
+ throw new Error(`--color must be one of: ${PROJECT_COLORS.join(", ")}`);
110
+ }
111
+ if (patch.visibility && !PROJECT_VISIBILITIES.includes(patch.visibility)) {
112
+ throw new Error(`--visibility must be one of: ${PROJECT_VISIBILITIES.join(", ")}`);
113
+ }
114
+ return apiRequest(`/api/org/projects/${encodeURIComponent(projectId)}`, {
115
+ method: "PUT",
116
+ headers: { "Content-Type": "application/json" },
117
+ body: JSON.stringify(patch)
118
+ });
119
+ }
120
+ async function deleteProject(projectId) {
121
+ await apiRequest(`/api/org/projects/${encodeURIComponent(projectId)}`, {
122
+ method: "DELETE"
123
+ });
124
+ }
125
+ async function assignJobs(projectId, jobRefs, action) {
126
+ const jobIds = jobRefs.map((ref) => parseJobId(ref));
127
+ const unique = [...new Set(jobIds)];
128
+ if (unique.length === 0) throw new Error("At least one trajectory set is required");
129
+ if (unique.length > 500) {
130
+ throw new Error(`Cannot file more than 500 trajectory sets at once (got ${unique.length})`);
131
+ }
132
+ return apiRequest("/api/org/projects/assign", {
133
+ method: "POST",
134
+ headers: { "Content-Type": "application/json" },
135
+ body: JSON.stringify({ job_ids: unique, project_id: projectId, action })
136
+ });
137
+ }
138
+ export {
139
+ PROJECT_COLORS,
140
+ PROJECT_VISIBILITIES,
141
+ assignJobs,
142
+ createProject,
143
+ deleteProject,
144
+ getProject,
145
+ listProjects,
146
+ resolveProjectRef,
147
+ updateProject
148
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trajectories-sh",
3
- "version": "1.22.0",
3
+ "version": "1.23.1",
4
4
  "description": "CLI for uploading trajectory jobs to trajectories.sh",
5
5
  "type": "module",
6
6
  "bin": {