trainfabric 0.1.28 → 0.1.30

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 (3) hide show
  1. package/README.md +2 -0
  2. package/dist/index.cjs +125 -30
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -26,6 +26,8 @@ Secrets are stored in macOS Keychain when available. Other platforms use an encr
26
26
 
27
27
  ```bash
28
28
  trainfabric projects:list
29
+ trainfabric projects:create --name <name>
30
+ trainfabric projects:delete <projectId>
29
31
  trainfabric datasets:upload ./train.jsonl --project <projectId>
30
32
  trainfabric runs:quote --summary --project <projectId> --dataset <datasetId> --model llama-3-8b
31
33
  trainfabric runs:create --yes --quote <quoteId> --project <projectId> --dataset <datasetId> --model llama-3-8b
package/dist/index.cjs CHANGED
@@ -8077,6 +8077,15 @@ var ResourceClient = class {
8077
8077
  };
8078
8078
 
8079
8079
  // ../sdk/dist/client/admin.js
8080
+ var apiKeyIdPattern = /^key_[A-Za-z0-9_-]+$/;
8081
+ var serviceAccountIdPattern = /^svc_[A-Za-z0-9_-]+$/;
8082
+ function normalizeRequiredId(value, pattern, label) {
8083
+ const id = String(value ?? "").trim();
8084
+ if (!pattern.test(id)) {
8085
+ throw new Error(`${label} is invalid.`);
8086
+ }
8087
+ return encodeURIComponent(id);
8088
+ }
8080
8089
  var ApiKeysClient = class extends ResourceClient {
8081
8090
  create(input) {
8082
8091
  apiKeyCreateSchema.parse(input);
@@ -8086,7 +8095,7 @@ var ApiKeysClient = class extends ResourceClient {
8086
8095
  return this.requestGet("/v1/api-keys");
8087
8096
  }
8088
8097
  revoke(apiKeyId) {
8089
- return this.requestDelete(`/v1/api-keys/${apiKeyId}`);
8098
+ return this.requestDelete(`/v1/api-keys/${normalizeRequiredId(apiKeyId, apiKeyIdPattern, "API key ID")}`);
8090
8099
  }
8091
8100
  };
8092
8101
  var ServiceAccountsClient = class extends ResourceClient {
@@ -8098,7 +8107,7 @@ var ServiceAccountsClient = class extends ResourceClient {
8098
8107
  return this.requestGet("/v1/service-accounts");
8099
8108
  }
8100
8109
  revoke(serviceAccountId) {
8101
- return this.requestDelete(`/v1/service-accounts/${serviceAccountId}`);
8110
+ return this.requestDelete(`/v1/service-accounts/${normalizeRequiredId(serviceAccountId, serviceAccountIdPattern, "Service account ID")}`);
8102
8111
  }
8103
8112
  };
8104
8113
 
@@ -8164,6 +8173,14 @@ var ClusterClient = class extends ResourceClient {
8164
8173
  // ../sdk/dist/client/datasets.js
8165
8174
  var import_promises = __toESM(require("node:fs/promises"), 1);
8166
8175
  var import_node_path = __toESM(require("node:path"), 1);
8176
+ var datasetIdPattern = /^ds_[A-Za-z0-9_-]+$/;
8177
+ function encodeDatasetId(datasetId) {
8178
+ const id = String(datasetId ?? "").trim();
8179
+ if (!datasetIdPattern.test(id)) {
8180
+ throw new Error("Dataset ID is invalid.");
8181
+ }
8182
+ return encodeURIComponent(id);
8183
+ }
8167
8184
  var DatasetsClient = class extends ResourceClient {
8168
8185
  async create(input) {
8169
8186
  const validation = await this.validate({
@@ -8178,7 +8195,7 @@ var DatasetsClient = class extends ResourceClient {
8178
8195
  throw new Error("projectId is required.");
8179
8196
  }
8180
8197
  const fileName = import_node_path.default.basename(input.path);
8181
- const fileBuffer = await import_promises.default.readFile(input.path);
8198
+ const fileBuffer = await readDatasetFile(input.path);
8182
8199
  const uploadSessionInput = uploadSessionCreateSchema.parse({
8183
8200
  projectId,
8184
8201
  fileName,
@@ -8204,21 +8221,46 @@ var DatasetsClient = class extends ResourceClient {
8204
8221
  if (input.format !== "chat_jsonl") {
8205
8222
  throw new Error("Only chat_jsonl datasets are supported.");
8206
8223
  }
8207
- return validateChatJsonl(await import_promises.default.readFile(input.path, "utf8"));
8224
+ return validateChatJsonl(await readDatasetFile(input.path, "utf8"));
8208
8225
  }
8209
8226
  get(datasetId) {
8210
- return this.requestGet(`/v1/datasets/${datasetId}`);
8227
+ return this.requestGet(`/v1/datasets/${encodeDatasetId(datasetId)}`);
8211
8228
  }
8212
8229
  list(projectId) {
8213
8230
  const effectiveProjectId = projectId ?? this.parent.projectId;
8214
- return this.requestGet(`/v1/datasets${effectiveProjectId ? `?projectId=${effectiveProjectId}` : ""}`);
8231
+ const params = new URLSearchParams();
8232
+ if (effectiveProjectId) {
8233
+ params.set("projectId", effectiveProjectId);
8234
+ }
8235
+ const query = params.toString();
8236
+ return this.requestGet(`/v1/datasets${query ? `?${query}` : ""}`);
8215
8237
  }
8216
8238
  delete(datasetId) {
8217
- return this.requestDelete(`/v1/datasets/${datasetId}`);
8239
+ return this.requestDelete(`/v1/datasets/${encodeDatasetId(datasetId)}`);
8218
8240
  }
8219
8241
  };
8242
+ async function readDatasetFile(pathname, encoding) {
8243
+ let stats;
8244
+ try {
8245
+ stats = await import_promises.default.stat(pathname);
8246
+ } catch {
8247
+ throw new Error(`Dataset file not found: ${pathname}`);
8248
+ }
8249
+ if (!stats.isFile()) {
8250
+ throw new Error(`Dataset path must be a file: ${pathname}`);
8251
+ }
8252
+ return encoding ? import_promises.default.readFile(pathname, encoding) : import_promises.default.readFile(pathname);
8253
+ }
8220
8254
 
8221
8255
  // ../sdk/dist/client/deployments.js
8256
+ var deploymentIdPattern = /^dep_[A-Za-z0-9_-]+$/;
8257
+ function encodeDeploymentId(deploymentId) {
8258
+ const id = String(deploymentId ?? "").trim();
8259
+ if (!deploymentIdPattern.test(id)) {
8260
+ throw new Error("Deployment ID is invalid.");
8261
+ }
8262
+ return encodeURIComponent(id);
8263
+ }
8222
8264
  var DeploymentsClient = class extends ResourceClient {
8223
8265
  list() {
8224
8266
  return this.requestGet("/v1/deployments");
@@ -8227,23 +8269,31 @@ var DeploymentsClient = class extends ResourceClient {
8227
8269
  return this.requestPost("/v1/deployments", input);
8228
8270
  }
8229
8271
  get(id) {
8230
- return this.requestGet(`/v1/deployments/${id}`);
8272
+ return this.requestGet(`/v1/deployments/${encodeDeploymentId(id)}`);
8231
8273
  }
8232
8274
  delete(id) {
8233
- return this.requestDelete(`/v1/deployments/${id}`);
8275
+ return this.requestDelete(`/v1/deployments/${encodeDeploymentId(id)}`);
8234
8276
  }
8235
8277
  };
8236
8278
 
8237
8279
  // ../sdk/dist/client/models.js
8280
+ var modelIdPattern = /^mdl_[A-Za-z0-9_-]+$/;
8281
+ function normalizeModelId(modelId) {
8282
+ const normalized = String(modelId ?? "").trim();
8283
+ if (!modelIdPattern.test(normalized)) {
8284
+ throw new Error("Model ID is invalid.");
8285
+ }
8286
+ return encodeURIComponent(normalized);
8287
+ }
8238
8288
  var ModelsClient = class extends ResourceClient {
8239
8289
  list() {
8240
8290
  return this.requestGet("/v1/models");
8241
8291
  }
8242
8292
  get(modelId) {
8243
- return this.requestGet(`/v1/models/${modelId}`);
8293
+ return this.requestGet(`/v1/models/${normalizeModelId(modelId)}`);
8244
8294
  }
8245
8295
  export(modelId) {
8246
- return this.requestPost(`/v1/models/${modelId}/export`);
8296
+ return this.requestPost(`/v1/models/${normalizeModelId(modelId)}/export`);
8247
8297
  }
8248
8298
  };
8249
8299
 
@@ -8255,12 +8305,20 @@ var ProjectsClient = class extends ResourceClient {
8255
8305
  organizationId: input.organizationId ?? this.parent.orgId
8256
8306
  });
8257
8307
  }
8308
+ delete(projectId) {
8309
+ return this.requestDelete(`/v1/projects/${projectId}`);
8310
+ }
8258
8311
  get(projectId) {
8259
8312
  return this.requestGet(`/v1/projects/${projectId}`);
8260
8313
  }
8261
8314
  list(organizationId) {
8262
8315
  const suffix = organizationId ?? this.parent.orgId;
8263
- return this.requestGet(`/v1/projects${suffix ? `?organizationId=${suffix}` : ""}`);
8316
+ const params = new URLSearchParams();
8317
+ if (suffix) {
8318
+ params.set("organizationId", suffix);
8319
+ }
8320
+ const query = params.toString();
8321
+ return this.requestGet(`/v1/projects${query ? `?${query}` : ""}`);
8264
8322
  }
8265
8323
  };
8266
8324
  var OrganizationsClient = class extends ResourceClient {
@@ -8514,6 +8572,14 @@ var RunHandle = class {
8514
8572
  };
8515
8573
 
8516
8574
  // ../sdk/dist/client/runs.js
8575
+ var runIdPattern = /^run_[A-Za-z0-9_-]+$/;
8576
+ function encodeRunId(runId) {
8577
+ const id = String(runId ?? "").trim();
8578
+ if (!runIdPattern.test(id)) {
8579
+ throw new Error("Run ID is invalid.");
8580
+ }
8581
+ return encodeURIComponent(id);
8582
+ }
8517
8583
  var RunsClient = class extends ResourceClient {
8518
8584
  describeEmptyQuoteFailure(input) {
8519
8585
  const compute = input.compute && input.compute !== "auto" && input.compute !== "cpu-sim" ? input.compute : void 0;
@@ -8616,42 +8682,47 @@ var RunsClient = class extends ResourceClient {
8616
8682
  return new RunHandle(this.parent, run);
8617
8683
  }
8618
8684
  get(runId) {
8619
- return this.requestGet(`/v1/runs/${runId}`);
8685
+ return this.requestGet(`/v1/runs/${encodeRunId(runId)}`);
8620
8686
  }
8621
8687
  list(projectId) {
8622
8688
  const effectiveProjectId = projectId ?? this.parent.projectId;
8623
- return this.requestGet(`/v1/runs${effectiveProjectId ? `?projectId=${effectiveProjectId}` : ""}`);
8689
+ const params = new URLSearchParams();
8690
+ if (effectiveProjectId) {
8691
+ params.set("projectId", effectiveProjectId);
8692
+ }
8693
+ const query = params.toString();
8694
+ return this.requestGet(`/v1/runs${query ? `?${query}` : ""}`);
8624
8695
  }
8625
8696
  cancel(runId) {
8626
- return this.requestPost(`/v1/runs/${runId}/cancel`);
8697
+ return this.requestPost(`/v1/runs/${encodeRunId(runId)}/cancel`);
8627
8698
  }
8628
8699
  resume(runId) {
8629
- return this.requestPost(`/v1/runs/${runId}/resume`);
8700
+ return this.requestPost(`/v1/runs/${encodeRunId(runId)}/resume`);
8630
8701
  }
8631
8702
  terminate(runId) {
8632
- return this.requestPost(`/v1/runs/${runId}/terminate`);
8703
+ return this.requestPost(`/v1/runs/${encodeRunId(runId)}/terminate`);
8633
8704
  }
8634
8705
  logs(runId) {
8635
- return this.requestGet(`/v1/runs/${runId}/logs`);
8706
+ return this.requestGet(`/v1/runs/${encodeRunId(runId)}/logs`);
8636
8707
  }
8637
8708
  async watch(runId) {
8638
8709
  const detail = await this.get(runId);
8639
8710
  return new RunHandle(this.parent, detail.run);
8640
8711
  }
8641
8712
  allocation(runId) {
8642
- return this.requestGet(`/v1/runs/${runId}/allocation`);
8713
+ return this.requestGet(`/v1/runs/${encodeRunId(runId)}/allocation`);
8643
8714
  }
8644
8715
  usage(runId) {
8645
- return this.requestGet(`/v1/runs/${runId}/usage`);
8716
+ return this.requestGet(`/v1/runs/${encodeRunId(runId)}/usage`);
8646
8717
  }
8647
8718
  costBreakdown(runId) {
8648
- return this.requestGet(`/v1/runs/${runId}/cost-breakdown`);
8719
+ return this.requestGet(`/v1/runs/${encodeRunId(runId)}/cost-breakdown`);
8649
8720
  }
8650
8721
  configuration(runId) {
8651
- return this.requestGet(`/v1/runs/${runId}/configuration`);
8722
+ return this.requestGet(`/v1/runs/${encodeRunId(runId)}/configuration`);
8652
8723
  }
8653
8724
  explanation(runId) {
8654
- return this.requestGet(`/v1/runs/${runId}/explanation`);
8725
+ return this.requestGet(`/v1/runs/${encodeRunId(runId)}/explanation`);
8655
8726
  }
8656
8727
  async wait(runId, options) {
8657
8728
  const handle = await this.watch(runId);
@@ -8823,6 +8894,7 @@ var idPatterns = {
8823
8894
  apiKey: /^key_[A-Za-z0-9_-]+$/,
8824
8895
  dataset: /^ds_[A-Za-z0-9_-]+$/,
8825
8896
  deployment: /^dep_[A-Za-z0-9_-]+$/,
8897
+ detection: /^det_[A-Za-z0-9_-]+$/,
8826
8898
  model: /^mdl_[A-Za-z0-9_-]+$/,
8827
8899
  org: /^org_[A-Za-z0-9_-]+$/,
8828
8900
  pool: /^pool_[A-Za-z0-9_-]+$/,
@@ -8919,7 +8991,12 @@ function validateRepoPath(repoPath) {
8919
8991
  if (absolute === parsed.root || absolute === import_node_os.default.homedir()) {
8920
8992
  throw new Error(`Refusing to package unsafe repository path: ${repoPath}`);
8921
8993
  }
8922
- const stat = import_node_fs.default.statSync(absolute);
8994
+ let stat;
8995
+ try {
8996
+ stat = import_node_fs.default.statSync(absolute);
8997
+ } catch {
8998
+ throw new Error(`Repository path not found: ${repoPath}`);
8999
+ }
8923
9000
  if (!stat.isDirectory()) {
8924
9001
  throw new Error(`Repository path must be a directory: ${repoPath}`);
8925
9002
  }
@@ -9133,7 +9210,7 @@ function buildComputeSpec(options) {
9133
9210
 
9134
9211
  // src/index.ts
9135
9212
  var DEFAULT_TRAINFABRIC_API_URL2 = "https://api.trainfabric.com";
9136
- var CLI_VERSION = "0.1.28";
9213
+ var CLI_VERSION = "0.1.30";
9137
9214
  var CONFIG_DIR = import_node_path3.default.join(import_node_os2.default.homedir(), ".trainfabric");
9138
9215
  var CONFIG_PATH = import_node_path3.default.join(CONFIG_DIR, "config.json");
9139
9216
  var FALLBACK_SECRET_PATH = import_node_path3.default.join(CONFIG_DIR, "session.enc");
@@ -9755,6 +9832,11 @@ program2.command("projects:create").requiredOption("--name <name>").option("--or
9755
9832
  })
9756
9833
  );
9757
9834
  });
9835
+ program2.command("projects:delete").argument("<projectId>").description("Delete an empty project").action(async (projectId) => {
9836
+ const normalizedProjectId = normalizeId(projectId, "project", "Project ID");
9837
+ await createClient(loadConfig()).projects.delete(normalizedProjectId);
9838
+ printJson({ deleted: true, id: normalizedProjectId });
9839
+ });
9758
9840
  program2.command("datasets:validate").argument("<file>").description("Validate a local dataset file").action(async (file) => {
9759
9841
  assertReadableFile(file, "Dataset path");
9760
9842
  const validation = await createClient(loadConfig()).datasets.validate({
@@ -9806,10 +9888,23 @@ program2.command("runtime:detect").option("--project <projectId>").option("--rep
9806
9888
  throw new Error("Provide --repo or --git.");
9807
9889
  }
9808
9890
  const config = loadConfig();
9809
- const detection = await createClient(config).runtime.detect({
9810
- projectId: requireProjectId(options, config),
9811
- source: sourceOptions.source
9812
- });
9891
+ let detection;
9892
+ try {
9893
+ detection = await createClient(config).runtime.detect({
9894
+ projectId: requireProjectId(options, config),
9895
+ source: sourceOptions.source
9896
+ });
9897
+ } catch (error) {
9898
+ if (sourceOptions.source.kind === "git" && error instanceof Error && /Request failed with status 404/.test(error.message)) {
9899
+ throw new Error("Git repository could not be resolved. Check the repository URL, branch, and access permissions.");
9900
+ }
9901
+ throw error;
9902
+ }
9903
+ if (sourceOptions.source.kind === "git" && detection.status !== "detected") {
9904
+ throw new Error(
9905
+ `Runtime source is ${detection.status}. Git repository could not be resolved into a supported runtime source.`
9906
+ );
9907
+ }
9813
9908
  printJson(redactInlineSource(detection));
9814
9909
  });
9815
9910
  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) => {
@@ -9817,7 +9912,7 @@ program2.command("runtime:build").option("--project <projectId>").requiredOption
9817
9912
  printJson(
9818
9913
  await createClient(config).runtime.build({
9819
9914
  projectId: requireProjectId(options, config),
9820
- detectionId: options.detection
9915
+ detectionId: normalizeId(options.detection, "detection", "Runtime detection ID")
9821
9916
  })
9822
9917
  );
9823
9918
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trainfabric",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
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",