trainfabric 0.1.23 → 0.1.25

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 +175 -40
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -7613,6 +7613,7 @@ var runCreateSchema = external_exports.object({
7613
7613
  repoUrl: external_exports.string().min(1).optional(),
7614
7614
  branch: external_exports.string().min(1).optional(),
7615
7615
  commitSha: external_exports.string().min(1).optional(),
7616
+ entrypoint: external_exports.string().min(1).optional(),
7616
7617
  rootDirName: external_exports.string().min(1).optional(),
7617
7618
  files: external_exports.array(external_exports.object({
7618
7619
  path: external_exports.string().min(1),
@@ -7638,6 +7639,7 @@ var runtimeDetectSchema = external_exports.object({
7638
7639
  repoUrl: external_exports.string().min(1).optional(),
7639
7640
  branch: external_exports.string().min(1).optional(),
7640
7641
  commitSha: external_exports.string().min(1).optional(),
7642
+ entrypoint: external_exports.string().min(1).optional(),
7641
7643
  rootDirName: external_exports.string().min(1).optional(),
7642
7644
  files: external_exports.array(external_exports.object({
7643
7645
  path: external_exports.string().min(1),
@@ -7661,6 +7663,7 @@ var runQuoteSchema = external_exports.object({
7661
7663
  repoUrl: external_exports.string().min(1).optional(),
7662
7664
  branch: external_exports.string().min(1).optional(),
7663
7665
  commitSha: external_exports.string().min(1).optional(),
7666
+ entrypoint: external_exports.string().min(1).optional(),
7664
7667
  rootDirName: external_exports.string().min(1).optional(),
7665
7668
  files: external_exports.array(external_exports.object({
7666
7669
  path: external_exports.string().min(1),
@@ -8816,6 +8819,15 @@ var import_node_crypto = __toESM(require("node:crypto"), 1);
8816
8819
  var import_node_fs = __toESM(require("node:fs"), 1);
8817
8820
  var import_node_os = __toESM(require("node:os"), 1);
8818
8821
  var import_node_path2 = __toESM(require("node:path"), 1);
8822
+ var idPatterns = {
8823
+ dataset: /^ds_[A-Za-z0-9_-]+$/,
8824
+ deployment: /^dep_[A-Za-z0-9_-]+$/,
8825
+ model: /^mdl_[A-Za-z0-9_-]+$/,
8826
+ org: /^org_[A-Za-z0-9_-]+$/,
8827
+ project: /^proj_[A-Za-z0-9_-]+$/,
8828
+ run: /^run_[A-Za-z0-9_-]+$/
8829
+ };
8830
+ var allowedBillingUnits = /* @__PURE__ */ new Set(["normalized_tflop_seconds", "reserved_gpu_seconds", "usd"]);
8819
8831
  function collectRuntimeFiles(repoPath) {
8820
8832
  const manifestFiles = /* @__PURE__ */ new Set([
8821
8833
  "training.yaml",
@@ -8863,12 +8875,56 @@ function collectRuntimeFiles(repoPath) {
8863
8875
  walk(repoPath);
8864
8876
  return found.sort((left, right) => left.path.localeCompare(right.path));
8865
8877
  }
8878
+ function inferEntrypoint(files) {
8879
+ const trainingYaml = files.find((file) => file.path === "training.yaml" || file.path === "train.runtime.yaml");
8880
+ if (trainingYaml) {
8881
+ const match = trainingYaml.content.match(/entrypoint\s*[:=]\s*["']?([A-Za-z0-9_./-]+)["']?/i);
8882
+ if (match?.[1]) {
8883
+ const entry = match[1];
8884
+ const exact = files.find((file) => file.path === entry);
8885
+ if (exact) {
8886
+ return exact.path;
8887
+ }
8888
+ const nested = files.find((file) => file.path.endsWith(`/${entry}`));
8889
+ if (nested) {
8890
+ return nested.path;
8891
+ }
8892
+ return entry;
8893
+ }
8894
+ }
8895
+ return files.find((file) => /(^|\/)train\.(py|sh)$/i.test(file.path))?.path;
8896
+ }
8897
+ function validateGitUrl(value) {
8898
+ const url = new URL(value);
8899
+ if (url.protocol !== "https:" && url.protocol !== "http:" && url.protocol !== "ssh:" && url.protocol !== "git:") {
8900
+ throw new Error(`Invalid git URL "${value}". Use an absolute repository URL.`);
8901
+ }
8902
+ return value;
8903
+ }
8904
+ function validateRepoPath(repoPath) {
8905
+ const absolute = import_node_path2.default.resolve(repoPath);
8906
+ const parsed = import_node_path2.default.parse(absolute);
8907
+ if (absolute === parsed.root || absolute === import_node_os.default.homedir()) {
8908
+ throw new Error(`Refusing to package unsafe repository path: ${repoPath}`);
8909
+ }
8910
+ const stat = import_node_fs.default.statSync(absolute);
8911
+ if (!stat.isDirectory()) {
8912
+ throw new Error(`Repository path must be a directory: ${repoPath}`);
8913
+ }
8914
+ return absolute;
8915
+ }
8866
8916
  function buildSourceOptions(options) {
8917
+ if (options.repo && options.git) {
8918
+ throw new Error("Use either --repo or --git, not both.");
8919
+ }
8867
8920
  if (options.repo) {
8921
+ const repoPath = validateRepoPath(options.repo);
8922
+ const files = collectRuntimeFiles(repoPath);
8868
8923
  return {
8869
8924
  source: {
8870
8925
  kind: "inline",
8871
- files: collectRuntimeFiles(import_node_path2.default.resolve(options.repo))
8926
+ entrypoint: inferEntrypoint(files),
8927
+ files
8872
8928
  }
8873
8929
  };
8874
8930
  }
@@ -8876,7 +8932,7 @@ function buildSourceOptions(options) {
8876
8932
  return {
8877
8933
  source: {
8878
8934
  kind: "git",
8879
- repoUrl: options.git,
8935
+ repoUrl: validateGitUrl(options.git),
8880
8936
  branch: options.branch
8881
8937
  }
8882
8938
  };
@@ -8910,7 +8966,40 @@ function parseApiKeyScopes(input) {
8910
8966
  if (invalidScope) {
8911
8967
  throw new Error(`Invalid API key scope: ${invalidScope}`);
8912
8968
  }
8913
- return scopes;
8969
+ return [...new Set(scopes)];
8970
+ }
8971
+ function normalizeHumanName(value, label) {
8972
+ let name = String(value ?? "").trim();
8973
+ while (name.length >= 2 && (name.startsWith("'") && name.endsWith("'") || name.startsWith('"') && name.endsWith('"'))) {
8974
+ name = name.slice(1, -1).trim();
8975
+ }
8976
+ if (!/[A-Za-z0-9]/.test(name)) {
8977
+ throw new Error(`${label} is required.`);
8978
+ }
8979
+ return name;
8980
+ }
8981
+ function normalizeId(value, kind, label) {
8982
+ const id = String(value ?? "").trim();
8983
+ if (!id || !idPatterns[kind].test(id)) {
8984
+ throw new Error(`${label} is invalid.`);
8985
+ }
8986
+ return id;
8987
+ }
8988
+ function normalizeOptionalId(value, kind, label) {
8989
+ if (value === void 0) {
8990
+ return void 0;
8991
+ }
8992
+ return normalizeId(value, kind, label);
8993
+ }
8994
+ function normalizeOptionalBillingUnit(value) {
8995
+ if (value === void 0) {
8996
+ return void 0;
8997
+ }
8998
+ const unit = String(value).trim();
8999
+ if (!allowedBillingUnits.has(unit)) {
9000
+ throw new Error("Billing unit is invalid. Use normalized_tflop_seconds, reserved_gpu_seconds, or usd.");
9001
+ }
9002
+ return unit;
8914
9003
  }
8915
9004
 
8916
9005
  // src/run_input.ts
@@ -8954,7 +9043,7 @@ function buildComputeSpec(options) {
8954
9043
 
8955
9044
  // src/index.ts
8956
9045
  var DEFAULT_TRAINFABRIC_API_URL2 = "https://api.trainfabric.com";
8957
- var CLI_VERSION = "0.1.23";
9046
+ var CLI_VERSION = "0.1.25";
8958
9047
  var CONFIG_DIR = import_node_path3.default.join(import_node_os2.default.homedir(), ".trainfabric");
8959
9048
  var CONFIG_PATH = import_node_path3.default.join(CONFIG_DIR, "config.json");
8960
9049
  var FALLBACK_SECRET_PATH = import_node_path3.default.join(CONFIG_DIR, "session.enc");
@@ -9293,7 +9382,7 @@ function visibleConfig(config) {
9293
9382
  };
9294
9383
  }
9295
9384
  function requireProjectId(options, config) {
9296
- const projectId = options.project ?? config.projectId;
9385
+ const projectId = normalizeOptionalId(options.project ?? config.projectId, "project", "Project ID");
9297
9386
  if (!projectId) {
9298
9387
  throw new Error("Project is required. Pass --project <projectId> or run `trainfabric config:set-project <projectId>`.");
9299
9388
  }
@@ -9351,6 +9440,16 @@ function filterQuoteBundleForRequestedMode(bundle, options) {
9351
9440
  quotes: bundle.quotes.filter((item) => item.mode === options.mode || item.quote.mode === options.mode)
9352
9441
  };
9353
9442
  }
9443
+ function redactInlineSource(value) {
9444
+ return JSON.parse(
9445
+ JSON.stringify(value, (_key, current) => {
9446
+ if (current && typeof current === "object" && typeof current.path === "string" && typeof current.content === "string") {
9447
+ return { ...current, content: "<redacted>" };
9448
+ }
9449
+ return current;
9450
+ })
9451
+ );
9452
+ }
9354
9453
  async function watchRun(runId, json = false, timeoutMs, pollMs) {
9355
9454
  const handle = await createClient(loadConfig()).runs.watch(runId);
9356
9455
  if (json) {
@@ -9418,6 +9517,17 @@ function createClient(config) {
9418
9517
  projectId: config.projectId
9419
9518
  });
9420
9519
  }
9520
+ function assertReadableFile(filePath, label) {
9521
+ let stats;
9522
+ try {
9523
+ stats = import_node_fs2.default.statSync(filePath);
9524
+ } catch {
9525
+ throw new Error(`${label} not found: ${filePath}`);
9526
+ }
9527
+ if (!stats.isFile()) {
9528
+ throw new Error(`${label} must be a file: ${filePath}`);
9529
+ }
9530
+ }
9421
9531
  function normalizeHttpBaseUrl(value) {
9422
9532
  let url;
9423
9533
  try {
@@ -9485,8 +9595,9 @@ program2.command("config:set-base-url").argument("<baseUrl>").description("Set t
9485
9595
  printJson(visibleConfig(config));
9486
9596
  });
9487
9597
  program2.command("config:set-org").argument("<orgId>").description("Set the default organization ID").action(async (orgId) => {
9598
+ const normalizedOrgId = normalizeId(orgId, "org", "Organization ID");
9488
9599
  const currentConfig = loadConfig();
9489
- const organization = await createClient(currentConfig).organizations.get(String(orgId));
9600
+ const organization = await createClient(currentConfig).organizations.get(normalizedOrgId);
9490
9601
  const updatedConfig = updateConfig((current) => {
9491
9602
  current.orgId = organization.id;
9492
9603
  if (current.projectId) {
@@ -9496,8 +9607,9 @@ program2.command("config:set-org").argument("<orgId>").description("Set the defa
9496
9607
  printJson(visibleConfig(updatedConfig));
9497
9608
  });
9498
9609
  program2.command("config:set-project").argument("<projectId>").description("Set the default project ID").action(async (projectId) => {
9610
+ const normalizedProjectId = normalizeId(projectId, "project", "Project ID");
9499
9611
  const currentConfig = loadConfig();
9500
- const project = await createClient(currentConfig).projects.get(String(projectId));
9612
+ const project = await createClient(currentConfig).projects.get(normalizedProjectId);
9501
9613
  if (currentConfig.orgId && project.organizationId !== currentConfig.orgId) {
9502
9614
  throw new Error(`Project ${project.id} does not belong to configured organization ${currentConfig.orgId}.`);
9503
9615
  }
@@ -9532,9 +9644,15 @@ program2.command("projects:list").description("List projects in the active organ
9532
9644
  printJson(await createClient(loadConfig()).projects.list());
9533
9645
  });
9534
9646
  program2.command("projects:create").requiredOption("--name <name>").option("--org <organizationId>").description("Create a project").action(async (options) => {
9535
- printJson(await createClient(loadConfig()).projects.create({ name: options.name, organizationId: options.org }));
9647
+ printJson(
9648
+ await createClient(loadConfig()).projects.create({
9649
+ name: normalizeHumanName(options.name, "Project name"),
9650
+ organizationId: normalizeOptionalId(options.org, "org", "Organization ID")
9651
+ })
9652
+ );
9536
9653
  });
9537
9654
  program2.command("datasets:validate").argument("<file>").description("Validate a local dataset file").action(async (file) => {
9655
+ assertReadableFile(file, "Dataset path");
9538
9656
  const validation = await createClient(loadConfig()).datasets.validate({
9539
9657
  path: file,
9540
9658
  format: "chat_jsonl"
@@ -9545,22 +9663,26 @@ program2.command("datasets:validate").argument("<file>").description("Validate a
9545
9663
  }
9546
9664
  });
9547
9665
  program2.command("datasets:list").option("--project <projectId>").description("List datasets").action(async (options) => {
9548
- printJson(await createClient(loadConfig()).datasets.list(options.project));
9666
+ printJson(await createClient(loadConfig()).datasets.list(normalizeOptionalId(options.project, "project", "Project ID")));
9549
9667
  });
9550
9668
  program2.command("datasets:get").argument("<datasetId>").description("Fetch dataset detail").action(async (datasetId) => {
9551
- printJson(await createClient(loadConfig()).datasets.get(String(datasetId)));
9669
+ printJson(await createClient(loadConfig()).datasets.get(normalizeId(datasetId, "dataset", "Dataset ID")));
9552
9670
  });
9553
9671
  program2.command("datasets:delete").argument("<datasetId>").description("Delete a dataset").action(async (datasetId) => {
9554
- await createClient(loadConfig()).datasets.delete(String(datasetId));
9555
- printJson({ deleted: true, id: String(datasetId) });
9672
+ const normalizedDatasetId = normalizeId(datasetId, "dataset", "Dataset ID");
9673
+ await createClient(loadConfig()).datasets.delete(normalizedDatasetId);
9674
+ printJson({ deleted: true, id: normalizedDatasetId });
9556
9675
  });
9557
9676
  program2.command("datasets:upload").argument("<file>").option("--project <projectId>").option("--name <name>").description("Upload a dataset through upload sessions").action(async (file, options) => {
9677
+ assertReadableFile(file, "Dataset path");
9558
9678
  const config = loadConfig();
9679
+ const projectId = normalizeOptionalId(options.project ?? config.projectId, "project", "Project ID");
9680
+ const name = options.name === void 0 ? void 0 : normalizeHumanName(options.name, "Dataset name");
9559
9681
  const dataset = await createClient(config).datasets.create({
9560
9682
  path: file,
9561
9683
  format: "chat_jsonl",
9562
- name: options.name,
9563
- projectId: options.project ?? config.projectId
9684
+ name,
9685
+ projectId
9564
9686
  });
9565
9687
  printJson(dataset);
9566
9688
  });
@@ -9579,12 +9701,11 @@ program2.command("runtime:detect").option("--project <projectId>").option("--rep
9579
9701
  throw new Error("Provide --repo or --git.");
9580
9702
  }
9581
9703
  const config = loadConfig();
9582
- printJson(
9583
- await createClient(config).runtime.detect({
9584
- projectId: requireProjectId(options, config),
9585
- source: sourceOptions.source
9586
- })
9587
- );
9704
+ const detection = await createClient(config).runtime.detect({
9705
+ projectId: requireProjectId(options, config),
9706
+ source: sourceOptions.source
9707
+ });
9708
+ printJson(redactInlineSource(detection));
9588
9709
  });
9589
9710
  program2.command("runtime:build").option("--project <projectId>").requiredOption("--detection <detectionId>").description("Build or reuse a cached runtime image from a runtime detection").action(async (options) => {
9590
9711
  const config = loadConfig();
@@ -9609,27 +9730,28 @@ program2.command("runs:quote").option("--project <projectId>").requiredOption("-
9609
9730
  }
9610
9731
  });
9611
9732
  program2.command("runs:list").option("--project <projectId>").description("List runs").action(async (options) => {
9612
- printJson(await createClient(loadConfig()).runs.list(options.project));
9733
+ printJson(await createClient(loadConfig()).runs.list(normalizeOptionalId(options.project, "project", "Project ID")));
9613
9734
  });
9614
9735
  program2.command("runs:status").argument("<runId>").description("Fetch full run detail").action(async (runId) => {
9615
- printJson(await createClient(loadConfig()).runs.get(String(runId)));
9736
+ printJson(await createClient(loadConfig()).runs.get(normalizeId(runId, "run", "Run ID")));
9616
9737
  });
9617
9738
  program2.command("runs:logs").argument("<runId>").description("Fetch run logs").action(async (runId) => {
9618
- printJson(await createClient(loadConfig()).runs.logs(String(runId)));
9739
+ printJson(await createClient(loadConfig()).runs.logs(normalizeId(runId, "run", "Run ID")));
9619
9740
  });
9620
9741
  program2.command("runs:wait").argument("<runId>").option("--timeout <duration>", "maximum wait duration, for example 30s, 10m, or 1h").option("--poll <duration>", "poll interval, for example 1s or 500ms").description("Wait for a run to reach a terminal state").action(async (runId, options) => {
9742
+ const normalizedRunId = normalizeId(runId, "run", "Run ID");
9621
9743
  printJson(
9622
- await createClient(loadConfig()).runs.wait(String(runId), {
9744
+ await createClient(loadConfig()).runs.wait(normalizedRunId, {
9623
9745
  timeoutMs: parseDurationMs(options.timeout),
9624
9746
  pollMs: parsePositiveMs(options.poll, 1e3)
9625
9747
  })
9626
9748
  );
9627
9749
  });
9628
9750
  program2.command("runs:watch").argument("<runId>").option("--json", "emit event output as JSON").option("--timeout <duration>", "maximum watch duration, for example 30s, 10m, or 1h").option("--poll <duration>", "poll interval, for example 1s or 500ms").description("Stream a run until it completes").action(async (runId, options) => {
9629
- await watchRun(String(runId), Boolean(options.json), parseDurationMs(options.timeout), parsePositiveMs(options.poll, 1e3));
9751
+ await watchRun(normalizeId(runId, "run", "Run ID"), Boolean(options.json), parseDurationMs(options.timeout), parsePositiveMs(options.poll, 1e3));
9630
9752
  });
9631
9753
  program2.command("runs:usage").argument("<runId>").option("--summary", "print a human-readable cost summary instead of raw JSON").description("Fetch run usage summary").action(async (runId, options) => {
9632
- const usage = await createClient(loadConfig()).runs.usage(String(runId));
9754
+ const usage = await createClient(loadConfig()).runs.usage(normalizeId(runId, "run", "Run ID"));
9633
9755
  if (options.summary) {
9634
9756
  printUsageSummary(usage);
9635
9757
  } else {
@@ -9637,7 +9759,7 @@ program2.command("runs:usage").argument("<runId>").option("--summary", "print a
9637
9759
  }
9638
9760
  });
9639
9761
  program2.command("runs:cost-breakdown").argument("<runId>").option("--summary", "print a human-readable cost summary instead of raw JSON").description("Fetch realized run cost and profitability").action(async (runId, options) => {
9640
- const breakdown = await createClient(loadConfig()).runs.costBreakdown(String(runId));
9762
+ const breakdown = await createClient(loadConfig()).runs.costBreakdown(normalizeId(runId, "run", "Run ID"));
9641
9763
  if (options.summary) {
9642
9764
  printUsageSummary(breakdown);
9643
9765
  } else {
@@ -9645,22 +9767,22 @@ program2.command("runs:cost-breakdown").argument("<runId>").option("--summary",
9645
9767
  }
9646
9768
  });
9647
9769
  program2.command("runs:allocation").argument("<runId>").description("Fetch run allocation").action(async (runId) => {
9648
- printJson(await createClient(loadConfig()).runs.allocation(String(runId)));
9770
+ printJson(await createClient(loadConfig()).runs.allocation(normalizeId(runId, "run", "Run ID")));
9649
9771
  });
9650
9772
  program2.command("runs:configuration").argument("<runId>").description("Fetch run placement configuration").action(async (runId) => {
9651
- printJson(await createClient(loadConfig()).runs.configuration(String(runId)));
9773
+ printJson(await createClient(loadConfig()).runs.configuration(normalizeId(runId, "run", "Run ID")));
9652
9774
  });
9653
9775
  program2.command("runs:explanation").argument("<runId>").description("Fetch run planning explanation").action(async (runId) => {
9654
- printJson(await createClient(loadConfig()).runs.explanation(String(runId)));
9776
+ printJson(await createClient(loadConfig()).runs.explanation(normalizeId(runId, "run", "Run ID")));
9655
9777
  });
9656
9778
  program2.command("runs:cancel").argument("<runId>").description("Cancel a run").action(async (runId) => {
9657
- printJson(await createClient(loadConfig()).runs.cancel(String(runId)));
9779
+ printJson(await createClient(loadConfig()).runs.cancel(normalizeId(runId, "run", "Run ID")));
9658
9780
  });
9659
9781
  program2.command("runs:resume").argument("<runId>").description("Resume a run from the latest checkpoint").action(async (runId) => {
9660
- printJson(await createClient(loadConfig()).runs.resume(String(runId)));
9782
+ printJson(await createClient(loadConfig()).runs.resume(normalizeId(runId, "run", "Run ID")));
9661
9783
  });
9662
9784
  program2.command("runs:terminate").argument("<runId>").description("Terminate a queued, launchable, or active run").action(async (runId) => {
9663
- printJson(await createClient(loadConfig()).runs.terminate(String(runId)));
9785
+ printJson(await createClient(loadConfig()).runs.terminate(normalizeId(runId, "run", "Run ID")));
9664
9786
  });
9665
9787
  program2.command("cluster:capacity").description("Fetch cluster capacity view").action(async () => {
9666
9788
  printJson(await createClient(loadConfig()).cluster.getCapacity());
@@ -9678,20 +9800,33 @@ program2.command("deployments:list").description("List deployments").action(asyn
9678
9800
  printJson(await createClient(loadConfig()).deployments.list());
9679
9801
  });
9680
9802
  program2.command("deployments:create").requiredOption("--model <modelId>").option("--project <projectId>", "accepted for consistency; deployment project is inferred from the model").description("Create a deployment for a model").action(async (options) => {
9681
- printJson(await createClient(loadConfig()).deployments.create({ modelId: options.model }));
9803
+ const config = loadConfig();
9804
+ const client = createClient(config);
9805
+ const projectId = normalizeOptionalId(options.project, "project", "Project ID");
9806
+ if (projectId) {
9807
+ await client.projects.get(projectId);
9808
+ }
9809
+ const modelId = normalizeId(options.model, "model", "Model ID");
9810
+ printJson(await client.deployments.create({ modelId }));
9682
9811
  });
9683
9812
  program2.command("deployments:get").argument("<deploymentId>").description("Fetch a deployment").action(async (deploymentId) => {
9684
- printJson(await createClient(loadConfig()).deployments.get(String(deploymentId)));
9813
+ printJson(await createClient(loadConfig()).deployments.get(normalizeId(deploymentId, "deployment", "Deployment ID")));
9685
9814
  });
9686
9815
  program2.command("deployments:delete").argument("<deploymentId>").description("Delete a pending or cataloged deployment").action(async (deploymentId) => {
9687
- await createClient(loadConfig()).deployments.delete(String(deploymentId));
9688
- printJson({ deleted: true, id: String(deploymentId) });
9816
+ const normalizedDeploymentId = normalizeId(deploymentId, "deployment", "Deployment ID");
9817
+ await createClient(loadConfig()).deployments.delete(normalizedDeploymentId);
9818
+ printJson({ deleted: true, id: normalizedDeploymentId });
9689
9819
  });
9690
9820
  program2.command("billing:summary").description("Fetch billing summary").action(async () => {
9691
9821
  printJson(await createClient(loadConfig()).billing.getSummary());
9692
9822
  });
9693
9823
  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) => {
9694
- printJson(await createClient(loadConfig()).billing.getUsage(options.unit, options.run));
9824
+ printJson(
9825
+ await createClient(loadConfig()).billing.getUsage(
9826
+ normalizeOptionalBillingUnit(options.unit),
9827
+ normalizeOptionalId(options.run, "run", "Run ID")
9828
+ )
9829
+ );
9695
9830
  });
9696
9831
  program2.command("billing:invoices").description("List invoices").action(async () => {
9697
9832
  printJson(await createClient(loadConfig()).billing.listInvoices());
@@ -9724,7 +9859,7 @@ program2.command("api-keys:create").requiredOption("--name <name>").option("--ow
9724
9859
  const config = loadConfig();
9725
9860
  const session = await createClient(config).auth.me();
9726
9861
  const apiKey = await createClient(config).apiKeys.create({
9727
- name: options.name,
9862
+ name: normalizeHumanName(options.name, "API key name"),
9728
9863
  ownerType: options.ownerType,
9729
9864
  ownerId: options.ownerId ?? session.user.id,
9730
9865
  scopes: parseApiKeyScopes(String(options.scopes))
@@ -9741,7 +9876,7 @@ program2.command("api-keys:revoke").argument("<apiKeyId>").description("Revoke a
9741
9876
  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) => {
9742
9877
  printJson(
9743
9878
  await createClient(loadConfig()).serviceAccounts.create({
9744
- name: options.name,
9879
+ name: normalizeHumanName(options.name, "Service account name"),
9745
9880
  scopes: parseApiKeyScopes(String(options.scopes))
9746
9881
  })
9747
9882
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trainfabric",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
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",