trainfabric 0.1.30 → 0.1.32

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
@@ -30,7 +30,7 @@ trainfabric projects:create --name <name>
30
30
  trainfabric projects:delete <projectId>
31
31
  trainfabric datasets:upload ./train.jsonl --project <projectId>
32
32
  trainfabric runs:quote --summary --project <projectId> --dataset <datasetId> --model llama-3-8b
33
- trainfabric runs:create --yes --quote <quoteId> --project <projectId> --dataset <datasetId> --model llama-3-8b
33
+ trainfabric runs:create --yes --quote <quoteId>
34
34
  trainfabric runs:watch <runId> --timeout 30m
35
35
  trainfabric runs:cost-breakdown <runId> --summary
36
36
  ```
package/dist/index.cjs CHANGED
@@ -8478,15 +8478,6 @@ var RunHandle = class {
8478
8478
  this.snapshot = detail.run;
8479
8479
  if (["completed", "failed", "canceled", "terminated"].includes(detail.run.status)) {
8480
8480
  this.close();
8481
- if (detail.run.status === "failed") {
8482
- throw new Error(detail.run.failureReason ?? "Run failed.");
8483
- }
8484
- if (detail.run.status === "canceled") {
8485
- throw new Error("Run was canceled.");
8486
- }
8487
- if (detail.run.status === "terminated") {
8488
- throw new Error("Run was terminated.");
8489
- }
8490
8481
  return detail;
8491
8482
  }
8492
8483
  if (timeoutMs !== void 0 && Date.now() - startedAt > timeoutMs) {
@@ -8677,7 +8668,7 @@ var RunsClient = class extends ResourceClient {
8677
8668
  mode: selectedModeQuote.mode
8678
8669
  };
8679
8670
  }
8680
- const parsed = runCreateSchema.parse(payload);
8671
+ const parsed = input.pricingQuoteId ? payload : runCreateSchema.parse(payload);
8681
8672
  const run = await this.requestPost("/v1/runs", parsed);
8682
8673
  return new RunHandle(this.parent, run);
8683
8674
  }
@@ -8737,7 +8728,12 @@ var RuntimeClient = class extends ResourceClient {
8737
8728
  projectId: input.projectId ?? this.parent.projectId,
8738
8729
  source: input.source
8739
8730
  });
8740
- return this.requestPost("/v1/runtime/detect", payload);
8731
+ return this.requestPost("/v1/runtime/detect", payload).catch((error) => {
8732
+ if (input.source.kind === "git" && error instanceof Error && /Request failed with status 404/.test(error.message)) {
8733
+ throw new Error("Git repository could not be resolved. Check the repository URL, branch, and access permissions.");
8734
+ }
8735
+ throw error;
8736
+ });
8741
8737
  }
8742
8738
  build(input) {
8743
8739
  const payload = runtimeBuildCreateSchema.parse({
@@ -8899,6 +8895,7 @@ var idPatterns = {
8899
8895
  org: /^org_[A-Za-z0-9_-]+$/,
8900
8896
  pool: /^pool_[A-Za-z0-9_-]+$/,
8901
8897
  project: /^proj_[A-Za-z0-9_-]+$/,
8898
+ quote: /^quote_[A-Za-z0-9_-]+$/,
8902
8899
  run: /^run_[A-Za-z0-9_-]+$/,
8903
8900
  serviceAccount: /^svc_[A-Za-z0-9_-]+$/,
8904
8901
  supplier: /^[A-Za-z][A-Za-z0-9_-]*$/
@@ -8926,7 +8923,17 @@ function collectRuntimeFiles(repoPath) {
8926
8923
  if (found.length >= maxFiles) {
8927
8924
  return;
8928
8925
  }
8929
- for (const entry of import_node_fs.default.readdirSync(currentPath, { withFileTypes: true })) {
8926
+ let entries;
8927
+ try {
8928
+ entries = import_node_fs.default.readdirSync(currentPath, { withFileTypes: true });
8929
+ } catch (error) {
8930
+ if (error.code === "EACCES" || error.code === "EPERM") {
8931
+ const relativePath = import_node_path2.default.relative(repoPath, currentPath) || ".";
8932
+ throw new Error(`Permission denied while scanning repository path: ${relativePath}`);
8933
+ }
8934
+ throw new Error(`Failed to scan repository path: ${currentPath}`);
8935
+ }
8936
+ for (const entry of entries) {
8930
8937
  const absolute = import_node_path2.default.join(currentPath, entry.name);
8931
8938
  const relative = import_node_path2.default.relative(repoPath, absolute).split(import_node_path2.default.sep).join("/");
8932
8939
  if (entry.isDirectory()) {
@@ -8938,13 +8945,30 @@ function collectRuntimeFiles(repoPath) {
8938
8945
  if (!entry.isFile() || !shouldIncludeFile(entry.name)) {
8939
8946
  continue;
8940
8947
  }
8941
- const stat = import_node_fs.default.statSync(absolute);
8948
+ let stat;
8949
+ try {
8950
+ stat = import_node_fs.default.statSync(absolute);
8951
+ } catch (error) {
8952
+ if (error.code === "EACCES" || error.code === "EPERM") {
8953
+ throw new Error(`Permission denied while scanning repository file: ${relative}`);
8954
+ }
8955
+ throw new Error(`Failed to inspect repository file: ${relative}`);
8956
+ }
8942
8957
  if (stat.size > maxFileBytes) {
8943
8958
  continue;
8944
8959
  }
8960
+ let content;
8961
+ try {
8962
+ content = import_node_fs.default.readFileSync(absolute, "utf8");
8963
+ } catch (error) {
8964
+ if (error.code === "EACCES" || error.code === "EPERM") {
8965
+ throw new Error(`Permission denied while reading repository file: ${relative}`);
8966
+ }
8967
+ throw new Error(`Failed to read repository file: ${relative}`);
8968
+ }
8945
8969
  found.push({
8946
8970
  path: relative,
8947
- content: import_node_fs.default.readFileSync(absolute, "utf8")
8971
+ content
8948
8972
  });
8949
8973
  }
8950
8974
  }
@@ -8988,7 +9012,22 @@ function validateGitBranch(value) {
8988
9012
  function validateRepoPath(repoPath) {
8989
9013
  const absolute = import_node_path2.default.resolve(repoPath);
8990
9014
  const parsed = import_node_path2.default.parse(absolute);
8991
- if (absolute === parsed.root || absolute === import_node_os.default.homedir()) {
9015
+ const unsafeRoots = /* @__PURE__ */ new Set([
9016
+ parsed.root,
9017
+ import_node_os.default.homedir(),
9018
+ import_node_path2.default.resolve(import_node_os.default.tmpdir()),
9019
+ import_node_path2.default.resolve("/tmp"),
9020
+ import_node_path2.default.resolve("/private/tmp"),
9021
+ import_node_path2.default.resolve("/var"),
9022
+ import_node_path2.default.resolve("/Users"),
9023
+ import_node_path2.default.resolve("/System"),
9024
+ import_node_path2.default.resolve("/Library"),
9025
+ import_node_path2.default.resolve("/Applications"),
9026
+ import_node_path2.default.resolve("/etc"),
9027
+ import_node_path2.default.resolve("/bin"),
9028
+ import_node_path2.default.resolve("/usr")
9029
+ ]);
9030
+ if (unsafeRoots.has(absolute)) {
8992
9031
  throw new Error(`Refusing to package unsafe repository path: ${repoPath}`);
8993
9032
  }
8994
9033
  let stat;
@@ -9210,7 +9249,7 @@ function buildComputeSpec(options) {
9210
9249
 
9211
9250
  // src/index.ts
9212
9251
  var DEFAULT_TRAINFABRIC_API_URL2 = "https://api.trainfabric.com";
9213
- var CLI_VERSION = "0.1.30";
9252
+ var CLI_VERSION = "0.1.32";
9214
9253
  var CONFIG_DIR = import_node_path3.default.join(import_node_os2.default.homedir(), ".trainfabric");
9215
9254
  var CONFIG_PATH = import_node_path3.default.join(CONFIG_DIR, "config.json");
9216
9255
  var FALLBACK_SECRET_PATH = import_node_path3.default.join(CONFIG_DIR, "session.enc");
@@ -9557,23 +9596,43 @@ function requireProjectId(options, config) {
9557
9596
  }
9558
9597
  function buildRunInput(options, config = loadConfig()) {
9559
9598
  const sourceOptions = buildSourceOptions(options);
9560
- return {
9561
- projectId: requireProjectId(options, config),
9599
+ const quoteId = options.quote === void 0 ? void 0 : normalizeId(options.quote, "quote", "Pricing quote ID");
9600
+ if (!quoteId && !options.dataset) {
9601
+ throw new Error("Dataset is required unless --quote is provided.");
9602
+ }
9603
+ if (!quoteId && !options.model) {
9604
+ throw new Error("Model is required unless --quote is provided.");
9605
+ }
9606
+ const projectId = normalizeOptionalId(options.project ?? config.projectId, "project", "Project ID");
9607
+ if (!quoteId && !projectId) {
9608
+ throw new Error("Project is required. Pass --project <projectId> or run `trainfabric config:set-project <projectId>`.");
9609
+ }
9610
+ const input = {
9562
9611
  task: "sft",
9563
9612
  method: "lora",
9564
- baseModel: normalizeBaseModel(options.model),
9565
- datasetId: normalizeId(options.dataset, "dataset", "Dataset ID"),
9613
+ pricingQuoteId: quoteId,
9566
9614
  evalDatasetId: normalizeOptionalId(options.eval, "dataset", "Eval dataset ID"),
9567
- pricingQuoteId: options.quote,
9568
9615
  ...sourceOptions,
9569
- mode: normalizeTrainingMode(options.mode),
9570
- compute: buildComputeSpec(options),
9571
- hyperparameters: {
9616
+ mode: normalizeTrainingMode(options.mode)
9617
+ };
9618
+ if (projectId) {
9619
+ input.projectId = projectId;
9620
+ }
9621
+ if (options.model) {
9622
+ input.baseModel = normalizeBaseModel(options.model);
9623
+ }
9624
+ if (options.dataset) {
9625
+ input.datasetId = normalizeId(options.dataset, "dataset", "Dataset ID");
9626
+ }
9627
+ if (!quoteId) {
9628
+ input.compute = buildComputeSpec(options);
9629
+ input.hyperparameters = {
9572
9630
  epochs: normalizeEpochs(options.epochs),
9573
9631
  lr: normalizeLearningRate(options.lr),
9574
9632
  batchSize: "auto"
9575
- }
9576
- };
9633
+ };
9634
+ }
9635
+ return input;
9577
9636
  }
9578
9637
  function assertQuoteOptions(bundle, options) {
9579
9638
  if (bundle.quotes.length > 0) {
@@ -9714,14 +9773,15 @@ function normalizeHttpBaseUrl(value) {
9714
9773
  try {
9715
9774
  url = new URL(value);
9716
9775
  } catch {
9717
- throw new Error(`Invalid base URL "${value}". Use an absolute http(s) URL such as https://api.trainfabric.com.`);
9776
+ throw new Error(`Invalid base URL "${value}". Use an absolute HTTPS origin such as https://api.trainfabric.com.`);
9777
+ }
9778
+ const isLocalDevHost = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
9779
+ if (url.protocol !== "https:" && !(isLocalDevHost && url.protocol === "http:")) {
9780
+ throw new Error(`Invalid base URL "${value}". Use HTTPS, or http://localhost for local development.`);
9718
9781
  }
9719
- if (url.protocol !== "https:" && url.protocol !== "http:") {
9720
- throw new Error(`Invalid base URL "${value}". Use an absolute http(s) URL such as https://api.trainfabric.com.`);
9782
+ if (url.pathname !== "/" || url.search || url.hash) {
9783
+ throw new Error(`Invalid base URL "${value}". Use the API origin without a path, query, or hash.`);
9721
9784
  }
9722
- url.pathname = url.pathname.replace(/\/+$/, "");
9723
- url.search = "";
9724
- url.hash = "";
9725
9785
  return url.toString().replace(/\/+$/, "");
9726
9786
  }
9727
9787
  async function login(config) {
@@ -9872,7 +9932,7 @@ program2.command("datasets:upload").argument("<file>").option("--project <projec
9872
9932
  });
9873
9933
  printJson(dataset);
9874
9934
  });
9875
- program2.command("runs:create").option("--project <projectId>").requiredOption("--dataset <datasetId>").requiredOption("--model <baseModel>").option("--eval <evalDatasetId>").option("--epochs <epochs>", "number of epochs", "3").option("--lr <lr>", "learning rate", "0.0002").option("--gpus <gpuCount>", "gpu count", "1").option("--nodes <nodeCount>", "node count", "1").option("--accelerator <acceleratorClass>", "optional hard accelerator constraint (for example: a10g, a100, h100)").option("--min-memory <gigabytes>", "minimum GPU memory in GB").option("--precision <precision>", "fp16_bf16, fp8, or fp32").option("--interconnect <interconnect>", "pcie or nvlink").option("--mode <mode>", "efficient, balanced, or power", "balanced").option("--quote <pricingQuoteId>", "launch the exact accepted quote id from runs:quote").option("--repo <path>", "local repo path for runtime autodetect").option("--git <url>", "git repo URL for runtime metadata").option("--branch <branch>", "git branch for runtime metadata").option("--yes", "confirm that you reviewed pricing with runs:quote --summary and accept fluctuating realized usage").description("Create a training run").action(async (options) => {
9935
+ program2.command("runs:create").option("--project <projectId>").option("--dataset <datasetId>").option("--model <baseModel>").option("--eval <evalDatasetId>").option("--epochs <epochs>", "number of epochs", "3").option("--lr <lr>", "learning rate", "0.0002").option("--gpus <gpuCount>", "gpu count", "1").option("--nodes <nodeCount>", "node count", "1").option("--accelerator <acceleratorClass>", "optional hard accelerator constraint (for example: a10g, a100, h100)").option("--min-memory <gigabytes>", "minimum GPU memory in GB").option("--precision <precision>", "fp16_bf16, fp8, or fp32").option("--interconnect <interconnect>", "pcie or nvlink").option("--mode <mode>", "efficient, balanced, or power").option("--quote <pricingQuoteId>", "launch the exact accepted quote id from runs:quote").option("--repo <path>", "local repo path for runtime autodetect").option("--git <url>", "git repo URL for runtime metadata").option("--branch <branch>", "git branch for runtime metadata").option("--yes", "confirm that you reviewed pricing with runs:quote --summary and accept fluctuating realized usage").description("Create a training run").action(async (options) => {
9876
9936
  const config = loadConfig();
9877
9937
  const runInput = buildRunInput(options, config);
9878
9938
  if (!options.yes) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trainfabric",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
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",