trainfabric 0.1.2 → 0.1.4

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 +148 -32
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -3462,6 +3462,7 @@ var import_node_http = __toESM(require("node:http"), 1);
3462
3462
  var import_node_os2 = __toESM(require("node:os"), 1);
3463
3463
  var import_node_path3 = __toESM(require("node:path"), 1);
3464
3464
  var import_node_child_process = require("node:child_process");
3465
+ var import_promises2 = require("node:timers/promises");
3465
3466
 
3466
3467
  // ../../node_modules/.pnpm/commander@14.0.3/node_modules/commander/esm.mjs
3467
3468
  var import_index = __toESM(require_commander(), 1);
@@ -8890,11 +8891,96 @@ function buildComputeSpec(options) {
8890
8891
 
8891
8892
  // src/index.ts
8892
8893
  var DEFAULT_TRAINFABRIC_API_URL2 = "https://api.trainfabric.com";
8893
- var CLI_VERSION = "0.1.2";
8894
+ var CLI_VERSION = "0.1.4";
8894
8895
  var CONFIG_DIR = import_node_path3.default.join(import_node_os2.default.homedir(), ".trainfabric");
8895
8896
  var CONFIG_PATH = import_node_path3.default.join(CONFIG_DIR, "config.json");
8896
8897
  var FALLBACK_SECRET_PATH = import_node_path3.default.join(CONFIG_DIR, "session.enc");
8897
8898
  var KEYCHAIN_SERVICE = "trainfabric-cli";
8899
+ var LOGIN_BANNER_LINES = [
8900
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557",
8901
+ "\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D",
8902
+ " \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2551 ",
8903
+ " \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 ",
8904
+ " \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
8905
+ " \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D"
8906
+ ];
8907
+ var LOGIN_BANNER_POOL = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*<>?+=/\\|~";
8908
+ function scrambleLine(line, progress) {
8909
+ const lockUntil = Math.floor(line.length * progress);
8910
+ return [...line].map((char, index) => {
8911
+ if (char === " " || index <= lockUntil) {
8912
+ return char;
8913
+ }
8914
+ return LOGIN_BANNER_POOL[Math.floor(Math.random() * LOGIN_BANNER_POOL.length)];
8915
+ }).join("");
8916
+ }
8917
+ async function printLoginBanner() {
8918
+ console.log("");
8919
+ if (!process.stdout.isTTY) {
8920
+ for (const row of LOGIN_BANNER_LINES) {
8921
+ console.log(row);
8922
+ }
8923
+ console.log(" TrainFabric CLI Login");
8924
+ console.log("");
8925
+ return;
8926
+ }
8927
+ process.stdout.write("\x1B[?25l");
8928
+ for (let frame = 0; frame <= 20; frame += 1) {
8929
+ if (frame > 0) {
8930
+ process.stdout.write(`\x1B[${LOGIN_BANNER_LINES.length}A`);
8931
+ }
8932
+ const progress = frame / 20;
8933
+ for (const row of LOGIN_BANNER_LINES) {
8934
+ process.stdout.write(`${scrambleLine(row, progress)}
8935
+ `);
8936
+ }
8937
+ await (0, import_promises2.setTimeout)(frame < 14 ? 32 : 18);
8938
+ }
8939
+ process.stdout.write("\x1B[?25h");
8940
+ console.log(" TrainFabric CLI Login");
8941
+ console.log("");
8942
+ }
8943
+ function printStaticLoginBanner() {
8944
+ console.log("");
8945
+ for (const row of LOGIN_BANNER_LINES) {
8946
+ console.log(row);
8947
+ }
8948
+ console.log(" TrainFabric CLI Login");
8949
+ console.log("");
8950
+ }
8951
+ function sleep(ms) {
8952
+ return (0, import_promises2.setTimeout)(ms);
8953
+ }
8954
+ async function runSpinnerUntil(label, action) {
8955
+ const frames = ["/", "-", "\\", "|"];
8956
+ let i = 0;
8957
+ const timer = setInterval(() => {
8958
+ process.stdout.write(`\r${frames[i++ % frames.length]} ${label}`);
8959
+ }, 90);
8960
+ try {
8961
+ const result = await action();
8962
+ clearInterval(timer);
8963
+ process.stdout.write(`\r[ok] ${label}... done${" ".repeat(16)}
8964
+ `);
8965
+ return result;
8966
+ } catch (error) {
8967
+ clearInterval(timer);
8968
+ process.stdout.write(`\r[x] ${label} failed${" ".repeat(16)}
8969
+ `);
8970
+ throw error;
8971
+ }
8972
+ }
8973
+ function printLoginFallbackHint(baseUrl) {
8974
+ console.error("\nCould not complete browser login (Unauthorized or callback failure).");
8975
+ console.error("If this is due to a blocked browser flow, use an API key instead:");
8976
+ console.error("");
8977
+ console.error(" export TRAINFABRIC_API_KEY=<api-key>");
8978
+ console.error(` trainfabric config:set-api-key <api-key>`);
8979
+ console.error(` trainfabric --version`);
8980
+ console.error("");
8981
+ console.error(`To verify dashboard link and API route, run: trainfabric config:set-base-url ${baseUrl}`);
8982
+ console.error("then run trainfabric login again.");
8983
+ }
8898
8984
  function ensureConfigDir() {
8899
8985
  import_node_fs2.default.mkdirSync(CONFIG_DIR, { recursive: true });
8900
8986
  import_node_fs2.default.chmodSync(CONFIG_DIR, 448);
@@ -9165,8 +9251,17 @@ function createClient(config) {
9165
9251
  });
9166
9252
  }
9167
9253
  async function login(config) {
9168
- const client = createClient(config);
9254
+ await printLoginBanner().catch(() => {
9255
+ printStaticLoginBanner();
9256
+ });
9257
+ const loginClient = new TrainingClient({ baseUrl: config.baseUrl });
9169
9258
  const server = import_node_http.default.createServer();
9259
+ const closeServer = () => {
9260
+ try {
9261
+ server.close();
9262
+ } catch {
9263
+ }
9264
+ };
9170
9265
  const port = await new Promise((resolve) => {
9171
9266
  server.listen(0, "127.0.0.1", () => {
9172
9267
  const address = server.address();
@@ -9176,37 +9271,58 @@ async function login(config) {
9176
9271
  resolve(address.port);
9177
9272
  });
9178
9273
  });
9179
- const callbackUrl = `http://127.0.0.1:${port}/callback`;
9180
- const loginUrl = await client.auth.browserLoginUrl(callbackUrl);
9181
- const tokenResult = await new Promise((resolve, reject) => {
9182
- server.on("request", (request, response) => {
9183
- const url = new URL(request.url ?? "/", callbackUrl);
9184
- const accessToken = url.searchParams.get("access_token");
9185
- const refreshToken = url.searchParams.get("refresh_token") ?? void 0;
9186
- const organizationId = url.searchParams.get("organization_id") ?? void 0;
9187
- if (!accessToken) {
9188
- response.statusCode = 400;
9189
- response.end("Missing access token.");
9190
- reject(new Error("Login callback did not contain an access token."));
9191
- return;
9192
- }
9193
- response.end("Authentication completed. You can close this window.");
9194
- resolve({ accessToken, refreshToken, organizationId });
9195
- });
9274
+ try {
9275
+ const callbackUrl = `http://127.0.0.1:${port}/callback`;
9276
+ const loginUrl = await runSpinnerUntil("Preparing browser login", () => loginClient.auth.browserLoginUrl(callbackUrl));
9277
+ console.log("Waiting for authentication in your browser...");
9278
+ console.log(`Callback URL: ${callbackUrl}`);
9196
9279
  openBrowser(loginUrl.authorizationUrl);
9197
- });
9198
- server.close();
9199
- const authedClient = new TrainingClient({
9200
- baseUrl: config.baseUrl,
9201
- accessToken: tokenResult.accessToken
9202
- });
9203
- const session = await authedClient.auth.me();
9204
- config.accessToken = tokenResult.accessToken;
9205
- config.refreshToken = tokenResult.refreshToken;
9206
- config.orgId = tokenResult.organizationId ?? session.organizationId;
9207
- config.apiKey = void 0;
9208
- saveConfig(config);
9209
- console.log(`Logged in as ${session.user.email} for organization ${config.orgId}.`);
9280
+ const tokenResult = await Promise.race([
9281
+ new Promise((resolve, reject) => {
9282
+ const timeoutId = setTimeout(() => {
9283
+ reject(new Error("Login callback timed out after 5 minutes. Please retry."));
9284
+ }, 5 * 60 * 1e3);
9285
+ server.on("request", (request, response) => {
9286
+ const url = new URL(request.url ?? "/", callbackUrl);
9287
+ const accessToken = url.searchParams.get("access_token");
9288
+ const refreshToken = url.searchParams.get("refresh_token") ?? void 0;
9289
+ const organizationId = url.searchParams.get("organization_id") ?? void 0;
9290
+ if (!accessToken) {
9291
+ response.statusCode = 400;
9292
+ response.end("Missing access token.");
9293
+ clearTimeout(timeoutId);
9294
+ reject(new Error("Login callback did not contain an access token."));
9295
+ return;
9296
+ }
9297
+ response.end("Authentication completed. You can close this window.");
9298
+ clearTimeout(timeoutId);
9299
+ resolve({ accessToken, refreshToken, organizationId });
9300
+ });
9301
+ }),
9302
+ (0, import_promises2.setTimeout)(5 * 60 * 1e3).then(() => {
9303
+ throw new Error("Login callback timed out after 5 minutes. Please retry.");
9304
+ })
9305
+ ]);
9306
+ const authedClient = new TrainingClient({
9307
+ baseUrl: config.baseUrl,
9308
+ accessToken: tokenResult.accessToken
9309
+ });
9310
+ const session = await runSpinnerUntil("Finalizing login", async () => {
9311
+ await sleep(100);
9312
+ return authedClient.auth.me();
9313
+ });
9314
+ config.accessToken = tokenResult.accessToken;
9315
+ config.refreshToken = tokenResult.refreshToken;
9316
+ config.orgId = tokenResult.organizationId ?? session.organizationId;
9317
+ config.apiKey = void 0;
9318
+ saveConfig(config);
9319
+ console.log(`Logged in as ${session.user.email} for organization ${config.orgId}.`);
9320
+ } catch (error) {
9321
+ printLoginFallbackHint(config.baseUrl);
9322
+ throw error;
9323
+ } finally {
9324
+ closeServer();
9325
+ }
9210
9326
  }
9211
9327
  var program2 = new Command();
9212
9328
  program2.name("trainfabric").description("Trainfabric CLI for launching and monitoring GPU training jobs").version(CLI_VERSION);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trainfabric",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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",