trainfabric 0.1.22 → 0.1.24

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.
Files changed (2) hide show
  1. package/dist/index.cjs +94 -17
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -8816,6 +8816,11 @@ var import_node_crypto = __toESM(require("node:crypto"), 1);
8816
8816
  var import_node_fs = __toESM(require("node:fs"), 1);
8817
8817
  var import_node_os = __toESM(require("node:os"), 1);
8818
8818
  var import_node_path2 = __toESM(require("node:path"), 1);
8819
+ var idPatterns = {
8820
+ org: /^org_[A-Za-z0-9_-]+$/,
8821
+ project: /^proj_[A-Za-z0-9_-]+$/,
8822
+ run: /^run_[A-Za-z0-9_-]+$/
8823
+ };
8819
8824
  function collectRuntimeFiles(repoPath) {
8820
8825
  const manifestFiles = /* @__PURE__ */ new Set([
8821
8826
  "training.yaml",
@@ -8912,6 +8917,29 @@ function parseApiKeyScopes(input) {
8912
8917
  }
8913
8918
  return scopes;
8914
8919
  }
8920
+ function normalizeHumanName(value, label) {
8921
+ let name = String(value ?? "").trim();
8922
+ while (name.length >= 2 && (name.startsWith("'") && name.endsWith("'") || name.startsWith('"') && name.endsWith('"'))) {
8923
+ name = name.slice(1, -1).trim();
8924
+ }
8925
+ if (!/[A-Za-z0-9]/.test(name)) {
8926
+ throw new Error(`${label} is required.`);
8927
+ }
8928
+ return name;
8929
+ }
8930
+ function normalizeId(value, kind, label) {
8931
+ const id = String(value ?? "").trim();
8932
+ if (!id || !idPatterns[kind].test(id)) {
8933
+ throw new Error(`${label} is invalid.`);
8934
+ }
8935
+ return id;
8936
+ }
8937
+ function normalizeOptionalId(value, kind, label) {
8938
+ if (value === void 0) {
8939
+ return void 0;
8940
+ }
8941
+ return normalizeId(value, kind, label);
8942
+ }
8915
8943
 
8916
8944
  // src/run_input.ts
8917
8945
  var BASE_MODEL_ALIASES = {
@@ -8954,7 +8982,7 @@ function buildComputeSpec(options) {
8954
8982
 
8955
8983
  // src/index.ts
8956
8984
  var DEFAULT_TRAINFABRIC_API_URL2 = "https://api.trainfabric.com";
8957
- var CLI_VERSION = "0.1.22";
8985
+ var CLI_VERSION = "0.1.24";
8958
8986
  var CONFIG_DIR = import_node_path3.default.join(import_node_os2.default.homedir(), ".trainfabric");
8959
8987
  var CONFIG_PATH = import_node_path3.default.join(CONFIG_DIR, "config.json");
8960
8988
  var FALLBACK_SECRET_PATH = import_node_path3.default.join(CONFIG_DIR, "session.enc");
@@ -9293,7 +9321,7 @@ function visibleConfig(config) {
9293
9321
  };
9294
9322
  }
9295
9323
  function requireProjectId(options, config) {
9296
- const projectId = options.project ?? config.projectId;
9324
+ const projectId = normalizeOptionalId(options.project ?? config.projectId, "project", "Project ID");
9297
9325
  if (!projectId) {
9298
9326
  throw new Error("Project is required. Pass --project <projectId> or run `trainfabric config:set-project <projectId>`.");
9299
9327
  }
@@ -9418,6 +9446,32 @@ function createClient(config) {
9418
9446
  projectId: config.projectId
9419
9447
  });
9420
9448
  }
9449
+ function assertReadableFile(filePath, label) {
9450
+ let stats;
9451
+ try {
9452
+ stats = import_node_fs2.default.statSync(filePath);
9453
+ } catch {
9454
+ throw new Error(`${label} not found: ${filePath}`);
9455
+ }
9456
+ if (!stats.isFile()) {
9457
+ throw new Error(`${label} must be a file: ${filePath}`);
9458
+ }
9459
+ }
9460
+ function normalizeHttpBaseUrl(value) {
9461
+ let url;
9462
+ try {
9463
+ url = new URL(value);
9464
+ } catch {
9465
+ throw new Error(`Invalid base URL "${value}". Use an absolute http(s) URL such as https://api.trainfabric.com.`);
9466
+ }
9467
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
9468
+ throw new Error(`Invalid base URL "${value}". Use an absolute http(s) URL such as https://api.trainfabric.com.`);
9469
+ }
9470
+ url.pathname = url.pathname.replace(/\/+$/, "");
9471
+ url.search = "";
9472
+ url.hash = "";
9473
+ return url.toString().replace(/\/+$/, "");
9474
+ }
9421
9475
  async function login(config) {
9422
9476
  const apiKey = await promptApiKey();
9423
9477
  if (!apiKey) {
@@ -9463,20 +9517,34 @@ program2.command("config:show").description("Show the active CLI configuration")
9463
9517
  printJson(visibleConfig(loadConfig()));
9464
9518
  });
9465
9519
  program2.command("config:set-base-url").argument("<baseUrl>").description("Set the backend base URL").action((baseUrl) => {
9520
+ const normalizedBaseUrl = normalizeHttpBaseUrl(String(baseUrl));
9466
9521
  const config = updateConfig((current) => {
9467
- current.baseUrl = String(baseUrl);
9522
+ current.baseUrl = normalizedBaseUrl;
9468
9523
  });
9469
9524
  printJson(visibleConfig(config));
9470
9525
  });
9471
- program2.command("config:set-org").argument("<orgId>").description("Set the default organization ID").action((orgId) => {
9472
- const config = updateConfig((current) => {
9473
- current.orgId = String(orgId);
9526
+ program2.command("config:set-org").argument("<orgId>").description("Set the default organization ID").action(async (orgId) => {
9527
+ const normalizedOrgId = normalizeId(orgId, "org", "Organization ID");
9528
+ const currentConfig = loadConfig();
9529
+ const organization = await createClient(currentConfig).organizations.get(normalizedOrgId);
9530
+ const updatedConfig = updateConfig((current) => {
9531
+ current.orgId = organization.id;
9532
+ if (current.projectId) {
9533
+ current.projectId = void 0;
9534
+ }
9474
9535
  });
9475
- printJson(visibleConfig(config));
9536
+ printJson(visibleConfig(updatedConfig));
9476
9537
  });
9477
- program2.command("config:set-project").argument("<projectId>").description("Set the default project ID").action((projectId) => {
9538
+ program2.command("config:set-project").argument("<projectId>").description("Set the default project ID").action(async (projectId) => {
9539
+ const normalizedProjectId = normalizeId(projectId, "project", "Project ID");
9540
+ const currentConfig = loadConfig();
9541
+ const project = await createClient(currentConfig).projects.get(normalizedProjectId);
9542
+ if (currentConfig.orgId && project.organizationId !== currentConfig.orgId) {
9543
+ throw new Error(`Project ${project.id} does not belong to configured organization ${currentConfig.orgId}.`);
9544
+ }
9478
9545
  const config = updateConfig((current) => {
9479
- current.projectId = String(projectId);
9546
+ current.projectId = project.id;
9547
+ current.orgId ??= project.organizationId;
9480
9548
  });
9481
9549
  printJson(visibleConfig(config));
9482
9550
  });
@@ -9505,9 +9573,15 @@ program2.command("projects:list").description("List projects in the active organ
9505
9573
  printJson(await createClient(loadConfig()).projects.list());
9506
9574
  });
9507
9575
  program2.command("projects:create").requiredOption("--name <name>").option("--org <organizationId>").description("Create a project").action(async (options) => {
9508
- printJson(await createClient(loadConfig()).projects.create({ name: options.name, organizationId: options.org }));
9576
+ printJson(
9577
+ await createClient(loadConfig()).projects.create({
9578
+ name: normalizeHumanName(options.name, "Project name"),
9579
+ organizationId: normalizeOptionalId(options.org, "org", "Organization ID")
9580
+ })
9581
+ );
9509
9582
  });
9510
9583
  program2.command("datasets:validate").argument("<file>").description("Validate a local dataset file").action(async (file) => {
9584
+ assertReadableFile(file, "Dataset path");
9511
9585
  const validation = await createClient(loadConfig()).datasets.validate({
9512
9586
  path: file,
9513
9587
  format: "chat_jsonl"
@@ -9518,7 +9592,7 @@ program2.command("datasets:validate").argument("<file>").description("Validate a
9518
9592
  }
9519
9593
  });
9520
9594
  program2.command("datasets:list").option("--project <projectId>").description("List datasets").action(async (options) => {
9521
- printJson(await createClient(loadConfig()).datasets.list(options.project));
9595
+ printJson(await createClient(loadConfig()).datasets.list(normalizeOptionalId(options.project, "project", "Project ID")));
9522
9596
  });
9523
9597
  program2.command("datasets:get").argument("<datasetId>").description("Fetch dataset detail").action(async (datasetId) => {
9524
9598
  printJson(await createClient(loadConfig()).datasets.get(String(datasetId)));
@@ -9528,12 +9602,15 @@ program2.command("datasets:delete").argument("<datasetId>").description("Delete
9528
9602
  printJson({ deleted: true, id: String(datasetId) });
9529
9603
  });
9530
9604
  program2.command("datasets:upload").argument("<file>").option("--project <projectId>").option("--name <name>").description("Upload a dataset through upload sessions").action(async (file, options) => {
9605
+ assertReadableFile(file, "Dataset path");
9531
9606
  const config = loadConfig();
9607
+ const projectId = normalizeOptionalId(options.project ?? config.projectId, "project", "Project ID");
9608
+ const name = options.name === void 0 ? void 0 : normalizeHumanName(options.name, "Dataset name");
9532
9609
  const dataset = await createClient(config).datasets.create({
9533
9610
  path: file,
9534
9611
  format: "chat_jsonl",
9535
- name: options.name,
9536
- projectId: options.project ?? config.projectId
9612
+ name,
9613
+ projectId
9537
9614
  });
9538
9615
  printJson(dataset);
9539
9616
  });
@@ -9582,7 +9659,7 @@ program2.command("runs:quote").option("--project <projectId>").requiredOption("-
9582
9659
  }
9583
9660
  });
9584
9661
  program2.command("runs:list").option("--project <projectId>").description("List runs").action(async (options) => {
9585
- printJson(await createClient(loadConfig()).runs.list(options.project));
9662
+ printJson(await createClient(loadConfig()).runs.list(normalizeOptionalId(options.project, "project", "Project ID")));
9586
9663
  });
9587
9664
  program2.command("runs:status").argument("<runId>").description("Fetch full run detail").action(async (runId) => {
9588
9665
  printJson(await createClient(loadConfig()).runs.get(String(runId)));
@@ -9664,7 +9741,7 @@ program2.command("billing:summary").description("Fetch billing summary").action(
9664
9741
  printJson(await createClient(loadConfig()).billing.getSummary());
9665
9742
  });
9666
9743
  program2.command("billing:usage").option("--unit <unit>", "normalized_tflop_seconds, reserved_gpu_seconds, or usd").option("--run <runId>", "filter usage records to one run").description("List billing usage records").action(async (options) => {
9667
- printJson(await createClient(loadConfig()).billing.getUsage(options.unit, options.run));
9744
+ printJson(await createClient(loadConfig()).billing.getUsage(options.unit, normalizeOptionalId(options.run, "run", "Run ID")));
9668
9745
  });
9669
9746
  program2.command("billing:invoices").description("List invoices").action(async () => {
9670
9747
  printJson(await createClient(loadConfig()).billing.listInvoices());
@@ -9697,7 +9774,7 @@ program2.command("api-keys:create").requiredOption("--name <name>").option("--ow
9697
9774
  const config = loadConfig();
9698
9775
  const session = await createClient(config).auth.me();
9699
9776
  const apiKey = await createClient(config).apiKeys.create({
9700
- name: options.name,
9777
+ name: normalizeHumanName(options.name, "API key name"),
9701
9778
  ownerType: options.ownerType,
9702
9779
  ownerId: options.ownerId ?? session.user.id,
9703
9780
  scopes: parseApiKeyScopes(String(options.scopes))
@@ -9714,7 +9791,7 @@ program2.command("api-keys:revoke").argument("<apiKeyId>").description("Revoke a
9714
9791
  program2.command("service-accounts:create").requiredOption("--name <name>").option("--scopes <scopes>", "comma-separated scopes", "org:read,project:read,project:write,dataset:read,dataset:write,run:read,run:write").description("Create a service account").action(async (options) => {
9715
9792
  printJson(
9716
9793
  await createClient(loadConfig()).serviceAccounts.create({
9717
- name: options.name,
9794
+ name: normalizeHumanName(options.name, "Service account name"),
9718
9795
  scopes: parseApiKeyScopes(String(options.scopes))
9719
9796
  })
9720
9797
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trainfabric",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "Trainfabric CLI for launching GPU training jobs on the hosted Trainfabric backend.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",