@balise.dev/cli 0.3.0 → 0.3.2

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 +4 -4
  2. package/dist/index.js +224 -219
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -32,7 +32,7 @@ balise sync
32
32
  | `balise logout` | Remove the credentials file. |
33
33
  | `balise whoami` | Print current user (`GET /v1/me`). |
34
34
  | `balise init` | Create/link a repo & write `.balise/config`. |
35
- | `balise sync` | Tarball current git repo → multipart upload → poll progress. |
35
+ | `balise sync` | Bundle current git repo → multipart upload → print a progress link. |
36
36
 
37
37
  ## Config
38
38
 
@@ -52,12 +52,12 @@ url = https://api.balise.dev
52
52
 
53
53
  Tokens are written to a plaintext JSON file with restrictive permissions:
54
54
 
55
- - **Path**: `$XDG_CONFIG_HOME/balise/credentials.json` (defaults to `~/.config/balise/credentials.json`)
56
- - **Permissions**: file `0600`, parent directory `0700` — re-enforced on every write
55
+ - **Path**: `$XDG_CONFIG_HOME/balise/credentials.json` (defaults to `~/.config/balise/credentials.json`; on **Windows**, `%APPDATA%\balise\credentials.json`)
56
+ - **Permissions**: file `0600`, parent directory `0700` — re-enforced on every write (POSIX only; on Windows the file inherits the per-user `%APPDATA%` ACL)
57
57
  - **Override path**: `BALISE_CREDENTIALS_FILE=<path>`
58
58
  - **Stateless / CI**: `BALISE_TOKEN=<jwt>` bypasses the file entirely (no file is ever read or written)
59
59
 
60
- > Windows: file storage is **not yet supported** use the `BALISE_TOKEN` env var.
60
+ > **Windows**: file storage works out of the box under `%APPDATA%`. If the write ever fails (locked profile, restricted ACLs), `balise login` prints your access token and the per-shell command to export it as `BALISE_TOKEN` (PowerShell `$env:BALISE_TOKEN`, cmd `set`, Git Bash `export`). That token is short-lived (~1h) — re-run `balise login` to refresh.
61
61
 
62
62
  If you need OS-keychain-grade protection, set `BALISE_CREDENTIALS_FILE` to a path on an encrypted volume, or feed `BALISE_TOKEN` from your existing secret manager.
63
63
 
package/dist/index.js CHANGED
@@ -192,8 +192,13 @@ function credentialsPath() {
192
192
  const fileOverride = process.env.BALISE_CREDENTIALS_FILE;
193
193
  if (fileOverride && fileOverride.length > 0) return fileOverride;
194
194
  const xdg = process.env.XDG_CONFIG_HOME;
195
- const base = xdg && xdg.length > 0 ? xdg : path.join(os.homedir(), ".config");
196
- return path.join(base, APP_DIR, FILENAME);
195
+ if (xdg && xdg.length > 0) return path.join(xdg, APP_DIR, FILENAME);
196
+ if (process.platform === "win32") {
197
+ const appData = process.env.APPDATA;
198
+ const base = appData && appData.length > 0 ? appData : path.join(os.homedir(), ".config");
199
+ return path.join(base, APP_DIR, FILENAME);
200
+ }
201
+ return path.join(os.homedir(), ".config", APP_DIR, FILENAME);
197
202
  }
198
203
  function hasEnvToken() {
199
204
  const v = process.env.BALISE_TOKEN;
@@ -260,15 +265,11 @@ async function saveTokens(t) {
260
265
  );
261
266
  return;
262
267
  }
263
- if (process.platform === "win32") {
264
- throw new CredentialsError(
265
- "Windows credential file storage not yet supported. Use BALISE_TOKEN env var instead."
266
- );
267
- }
268
+ const isWin = process.platform === "win32";
268
269
  const p = credentialsPath();
269
270
  const dir = path.dirname(p);
270
271
  try {
271
- await fs.mkdir(dir, { recursive: true, mode: 448 });
272
+ await fs.mkdir(dir, isWin ? { recursive: true } : { recursive: true, mode: 448 });
272
273
  } catch (err) {
273
274
  throw new CredentialsError(
274
275
  `Cannot create credentials directory ${dir}: ${err.message}. Set BALISE_TOKEN env var instead.`,
@@ -276,8 +277,8 @@ async function saveTokens(t) {
276
277
  );
277
278
  }
278
279
  try {
279
- await fs.writeFile(p, JSON.stringify(t), { mode: 384 });
280
- await fs.chmod(p, 384);
280
+ await fs.writeFile(p, JSON.stringify(t), isWin ? void 0 : { mode: 384 });
281
+ if (!isWin) await fs.chmod(p, 384);
281
282
  } catch (err) {
282
283
  throw new CredentialsError(
283
284
  `Cannot write credentials file ${p}: ${err.message}. Set BALISE_TOKEN env var instead.`,
@@ -315,102 +316,6 @@ function credentialsHelpMessage() {
315
316
  \u2022 Set BALISE_CREDENTIALS_FILE=<path> to override the storage location.`;
316
317
  }
317
318
 
318
- // src/commands/login.ts
319
- async function runLogin(opts) {
320
- if (process.env.BALISE_TOKEN && process.env.BALISE_TOKEN.length > 0) {
321
- process.stderr.write(
322
- "Already authenticated via BALISE_TOKEN env var. Unset it to run browser-based login.\n"
323
- );
324
- return;
325
- }
326
- const verifier = generateCodeVerifier();
327
- const challenge = codeChallengeFor(verifier);
328
- const state = crypto2.randomBytes(16).toString("hex");
329
- const { port: portPromise, waitForCode } = startLoopbackServer({
330
- expectedState: state,
331
- timeoutMs: opts.timeoutMs ?? 3e5
332
- });
333
- const port = await portPromise;
334
- const redirectUri = `http://127.0.0.1:${port}/callback`;
335
- const clientId = await registerClient({
336
- supabaseUrl: opts.supabaseUrl,
337
- clientName: "Balise CLI",
338
- redirectUri
339
- });
340
- const url = buildAuthorizeUrl({
341
- supabaseUrl: opts.supabaseUrl,
342
- clientId,
343
- redirectUri,
344
- codeChallenge: challenge,
345
- state
346
- });
347
- process.stderr.write(`Opening browser to log in\u2026
348
- ${url}
349
- `);
350
- const opener = opts.openBrowser ?? (async (u) => {
351
- await open(u);
352
- });
353
- try {
354
- await opener(url);
355
- } catch {
356
- process.stderr.write(
357
- "Could not auto-open browser. Copy the URL above manually.\n"
358
- );
359
- }
360
- const code = await waitForCode;
361
- const tokens = await exchangeCodeForTokens({
362
- supabaseUrl: opts.supabaseUrl,
363
- code,
364
- codeVerifier: verifier,
365
- redirectUri,
366
- clientId
367
- });
368
- try {
369
- await saveTokens({
370
- access_token: tokens.access_token,
371
- refresh_token: tokens.refresh_token,
372
- client_id: clientId,
373
- expires_at: Math.floor(Date.now() / 1e3) + (tokens.expires_in ?? 3600),
374
- user_id: tokens.user?.id
375
- });
376
- } catch (err) {
377
- if (err instanceof CredentialsError) {
378
- process.stderr.write(`
379
- ${err.message}
380
-
381
- ${credentialsHelpMessage()}
382
- `);
383
- process.exit(1);
384
- }
385
- throw err;
386
- }
387
- const who = tokens.user?.email ?? tokens.user?.id ?? "you";
388
- process.stdout.write(`Logged in as ${who}
389
- `);
390
- }
391
-
392
- // src/commands/logout.ts
393
- async function runLogout() {
394
- try {
395
- const existed = await clearTokens();
396
- if (existed) {
397
- process.stdout.write("Logged out \u2014 credentials file removed.\n");
398
- } else {
399
- process.stdout.write("Already logged out (no credentials file found).\n");
400
- }
401
- } catch (err) {
402
- if (err instanceof CredentialsError) {
403
- process.stderr.write(`
404
- ${err.message}
405
-
406
- ${credentialsHelpMessage()}
407
- `);
408
- process.exit(1);
409
- }
410
- throw err;
411
- }
412
- }
413
-
414
319
  // src/api.ts
415
320
  import { request as request2 } from "undici";
416
321
  var NotAuthenticatedError = class extends Error {
@@ -707,6 +612,162 @@ async function ensureGitignored(cwd = process.cwd()) {
707
612
  `, "utf8");
708
613
  }
709
614
 
615
+ // src/commands/login.ts
616
+ function printWindowsTokenFallback(token, who) {
617
+ process.stdout.write(
618
+ `
619
+ \u2713 Authenticated as ${who}
620
+
621
+ \u26A0 Windows detected \u2014 Balise can't store credentials in a file on Windows yet.
622
+ Instead, set the token below as the BALISE_TOKEN environment variable.
623
+
624
+ Your access token (valid ~1h \u2014 re-run \`balise login\` to refresh):
625
+
626
+ ${token}
627
+
628
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
629
+ Set it for your shell:
630
+
631
+ \u2022 PowerShell (current session)
632
+ $env:BALISE_TOKEN = "<token>"
633
+
634
+ \u2022 Command Prompt (current session)
635
+ set BALISE_TOKEN=<token>
636
+
637
+ \u2022 Git Bash / MSYS2 (current session)
638
+ export BALISE_TOKEN="<token>"
639
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
640
+
641
+ Verify connection with: balise whoami
642
+ `
643
+ );
644
+ }
645
+ async function runLogin(opts) {
646
+ if (process.env.BALISE_TOKEN && process.env.BALISE_TOKEN.length > 0) {
647
+ process.stderr.write(
648
+ "Already authenticated via BALISE_TOKEN env var. Unset it to run browser-based login.\n"
649
+ );
650
+ return;
651
+ }
652
+ const verifier = generateCodeVerifier();
653
+ const challenge = codeChallengeFor(verifier);
654
+ const state = crypto2.randomBytes(16).toString("hex");
655
+ const { port: portPromise, waitForCode } = startLoopbackServer({
656
+ expectedState: state,
657
+ timeoutMs: opts.timeoutMs ?? 3e5
658
+ });
659
+ const port = await portPromise;
660
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
661
+ const clientId = await registerClient({
662
+ supabaseUrl: opts.supabaseUrl,
663
+ clientName: "Balise CLI",
664
+ redirectUri
665
+ });
666
+ const url = buildAuthorizeUrl({
667
+ supabaseUrl: opts.supabaseUrl,
668
+ clientId,
669
+ redirectUri,
670
+ codeChallenge: challenge,
671
+ state
672
+ });
673
+ process.stderr.write(`Opening browser to log in\u2026
674
+ ${url}
675
+ `);
676
+ const opener = opts.openBrowser ?? (async (u) => {
677
+ await open(u);
678
+ });
679
+ try {
680
+ await opener(url);
681
+ } catch {
682
+ process.stderr.write(
683
+ "Could not auto-open browser. Copy the URL above manually.\n"
684
+ );
685
+ }
686
+ const code = await waitForCode;
687
+ const tokens = await exchangeCodeForTokens({
688
+ supabaseUrl: opts.supabaseUrl,
689
+ code,
690
+ codeVerifier: verifier,
691
+ redirectUri,
692
+ clientId
693
+ });
694
+ try {
695
+ await saveTokens({
696
+ access_token: tokens.access_token,
697
+ refresh_token: tokens.refresh_token,
698
+ client_id: clientId,
699
+ expires_at: Math.floor(Date.now() / 1e3) + (tokens.expires_in ?? 3600),
700
+ user_id: tokens.user?.id
701
+ });
702
+ } catch (err) {
703
+ if (err instanceof CredentialsError) {
704
+ if (process.platform === "win32") {
705
+ printWindowsTokenFallback(
706
+ tokens.access_token,
707
+ tokens.user?.email ?? tokens.user?.id ?? "you"
708
+ );
709
+ return;
710
+ }
711
+ process.stderr.write(`
712
+ ${err.message}
713
+
714
+ ${credentialsHelpMessage()}
715
+ `);
716
+ process.exit(1);
717
+ }
718
+ throw err;
719
+ }
720
+ let who = tokens.user?.email ?? tokens.user?.id ?? "you";
721
+ try {
722
+ const client = new ApiClient({
723
+ apiUrl: DEFAULT_API_URL,
724
+ supabaseUrl: opts.supabaseUrl
725
+ });
726
+ const me = await client.getJson("/v1/me");
727
+ who = me.login;
728
+ } catch {
729
+ }
730
+ process.stdout.write(`Logged in as ${who}
731
+ `);
732
+ }
733
+
734
+ // src/commands/logout.ts
735
+ async function runLogout() {
736
+ try {
737
+ const existed = await clearTokens();
738
+ if (existed) {
739
+ process.stdout.write("Logged out \u2014 credentials file removed.\n");
740
+ } else {
741
+ process.stdout.write("Already logged out (no credentials file found).\n");
742
+ }
743
+ } catch (err) {
744
+ if (err instanceof CredentialsError) {
745
+ process.stderr.write(`
746
+ ${err.message}
747
+
748
+ ${credentialsHelpMessage()}
749
+ `);
750
+ process.exit(1);
751
+ }
752
+ throw err;
753
+ }
754
+ }
755
+
756
+ // src/auth-ensure.ts
757
+ var NotLoggedInError = class extends Error {
758
+ constructor() {
759
+ super("not logged in");
760
+ this.name = "NotLoggedInError";
761
+ }
762
+ };
763
+ var NOT_LOGGED_IN_MESSAGE = "You're not signed in to Balise.\n Run `balise login` to authenticate via your browser.\n For CI / non-interactive env: set BALISE_TOKEN=<jwt>.";
764
+ var SESSION_EXPIRED_MESSAGE = "Your Balise session has expired \u2014 run `balise login` to sign in again.";
765
+ async function ensureAuthenticated() {
766
+ const existing = await loadTokens();
767
+ if (existing) return;
768
+ throw new NotLoggedInError();
769
+ }
770
+
710
771
  // src/commands/whoami.ts
711
772
  async function runWhoami(opts) {
712
773
  const cfg = await readConfig().catch(() => null);
@@ -726,7 +787,7 @@ account_id : ${me.account_id}
726
787
  );
727
788
  } catch (err) {
728
789
  if (err instanceof NotAuthenticatedError) {
729
- process.stderr.write("Not logged in \u2014 run `balise login`.\n");
790
+ process.stderr.write(SESSION_EXPIRED_MESSAGE + "\n");
730
791
  process.exit(1);
731
792
  }
732
793
  throw err;
@@ -806,6 +867,10 @@ async function isDirty(opts) {
806
867
  const status = await runGit(opts, ["status", "--porcelain"]);
807
868
  return status.trim().length > 0;
808
869
  }
870
+ async function isPathUncommitted(opts, relPath) {
871
+ const status = await runGit(opts, ["status", "--porcelain", "--", relPath]);
872
+ return status.trim().length > 0;
873
+ }
809
874
  async function gitBundle(opts) {
810
875
  await assertGitRepo(opts);
811
876
  const [commitSha, branch, dirty] = await Promise.all([
@@ -828,90 +893,6 @@ async function gitBundle(opts) {
828
893
  return { stream, commitSha, branch, dirty };
829
894
  }
830
895
 
831
- // src/auth-ensure.ts
832
- import crypto3 from "crypto";
833
- import open2 from "open";
834
- var LoginDeclinedError = class extends Error {
835
- constructor() {
836
- super("login declined by user");
837
- this.name = "LoginDeclinedError";
838
- }
839
- };
840
- function isInteractive(stdin) {
841
- const s = stdin ?? process.stdin;
842
- return Boolean(s.isTTY);
843
- }
844
- var LOGIN_AUTOLAUNCH_MESSAGE = "Not logged in, launching balise login...";
845
- async function ensureAuthenticated(opts) {
846
- const existing = await loadTokens();
847
- if (existing) return;
848
- const stderr = opts.stderr ?? process.stderr;
849
- if (!isInteractive(opts.stdin)) {
850
- throw new LoginDeclinedError();
851
- }
852
- stderr.write(`${LOGIN_AUTOLAUNCH_MESSAGE}
853
- `);
854
- const verifier = generateCodeVerifier();
855
- const challenge = codeChallengeFor(verifier);
856
- const state = crypto3.randomBytes(16).toString("hex");
857
- const { port: portPromise, waitForCode } = startLoopbackServer({
858
- expectedState: state,
859
- timeoutMs: opts.timeoutMs ?? 3e5
860
- });
861
- const port = await portPromise;
862
- const redirectUri = `http://127.0.0.1:${port}/callback`;
863
- const clientId = await registerClient({
864
- supabaseUrl: opts.supabaseUrl,
865
- clientName: "Balise CLI",
866
- redirectUri
867
- });
868
- const url = buildAuthorizeUrl({
869
- supabaseUrl: opts.supabaseUrl,
870
- clientId,
871
- redirectUri,
872
- codeChallenge: challenge,
873
- state
874
- });
875
- stderr.write(`Opening browser to log in\u2026
876
- ${url}
877
- `);
878
- const opener = opts.openBrowser ?? (async (u) => {
879
- await open2(u);
880
- });
881
- try {
882
- await opener(url);
883
- } catch {
884
- stderr.write("Could not auto-open browser. Copy the URL above manually.\n");
885
- }
886
- const code = await waitForCode;
887
- const tokens = await exchangeCodeForTokens({
888
- supabaseUrl: opts.supabaseUrl,
889
- code,
890
- codeVerifier: verifier,
891
- redirectUri,
892
- clientId
893
- });
894
- try {
895
- await saveTokens({
896
- access_token: tokens.access_token,
897
- refresh_token: tokens.refresh_token,
898
- client_id: clientId,
899
- expires_at: Math.floor(Date.now() / 1e3) + (tokens.expires_in ?? 3600),
900
- user_id: tokens.user?.id
901
- });
902
- } catch (err) {
903
- if (err instanceof CredentialsError) {
904
- stderr.write(`
905
- ${err.message}
906
-
907
- ${credentialsHelpMessage()}
908
- `);
909
- throw err;
910
- }
911
- throw err;
912
- }
913
- }
914
-
915
896
  // src/ui/InitPicker.tsx
916
897
  import React, { useMemo, useState } from "react";
917
898
  import { Box, Text, useApp, useInput } from "ink";
@@ -1759,12 +1740,10 @@ async function runInit(opts) {
1759
1740
  throw err;
1760
1741
  }
1761
1742
  try {
1762
- await ensureAuthenticated({
1763
- supabaseUrl: opts.supabaseUrl
1764
- });
1743
+ await ensureAuthenticated();
1765
1744
  } catch (err) {
1766
- if (err instanceof LoginDeclinedError) {
1767
- process.stderr.write("Login required. Aborting.\n");
1745
+ if (err instanceof NotLoggedInError) {
1746
+ process.stderr.write(NOT_LOGGED_IN_MESSAGE + "\n");
1768
1747
  process.exit(1);
1769
1748
  }
1770
1749
  throw err;
@@ -1796,7 +1775,7 @@ async function runInit(opts) {
1796
1775
  repos = repoList;
1797
1776
  } catch (err) {
1798
1777
  if (err instanceof NotAuthenticatedError) {
1799
- process.stderr.write("Not logged in \u2014 run `balise login` first.\n");
1778
+ process.stderr.write(SESSION_EXPIRED_MESSAGE + "\n");
1800
1779
  process.exit(1);
1801
1780
  }
1802
1781
  throw err;
@@ -1882,7 +1861,7 @@ async function runInit(opts) {
1882
1861
  `
1883
1862
  );
1884
1863
  process.stdout.write(
1885
- '\n\u26A0 .baliseignore takes effect only once committed (it\'s read per commit\n server-side). Commit it before your next `balise sync`:\n git add .baliseignore && git commit -m "chore: update .baliseignore"\n'
1864
+ '\n\u26A0 .baliseignore takes effect only once committed (it\'s read per commit server-side). Commit it before your next `balise sync`:\n git add .baliseignore && git commit -m "chore: update .baliseignore"\n'
1886
1865
  );
1887
1866
  }
1888
1867
 
@@ -2000,7 +1979,6 @@ function buildSyncQuery(opts) {
2000
1979
  if (opts.base) q.base = opts.base;
2001
1980
  return q;
2002
1981
  }
2003
- var INIT_AUTOLAUNCH_MESSAGE = "Repo not initialized, launching balise init...";
2004
1982
  function drainStdinBuffer(stdin) {
2005
1983
  if (stdin !== process.stdin) return;
2006
1984
  const s = stdin;
@@ -2008,7 +1986,7 @@ function drainStdinBuffer(stdin) {
2008
1986
  while (s.read() !== null) {
2009
1987
  }
2010
1988
  }
2011
- function isInteractive2() {
1989
+ function isInteractive() {
2012
1990
  return Boolean(process.stdin.isTTY);
2013
1991
  }
2014
1992
  async function confirmRetryWithForce(opts) {
@@ -2050,6 +2028,25 @@ function resolveWebUrl(webUrl, frontUrl) {
2050
2028
  function formatAcceptedSummary(body, frontUrl) {
2051
2029
  return `Sync queued. Track it here: ${resolveWebUrl(body.web_url, frontUrl)}`;
2052
2030
  }
2031
+ function formatInsufficientCredits(rawBody) {
2032
+ let estimate;
2033
+ let available;
2034
+ try {
2035
+ const detail = JSON.parse(rawBody).detail;
2036
+ if (detail?.error === "insufficient_credits") {
2037
+ if (typeof detail.estimate === "number") estimate = detail.estimate;
2038
+ if (typeof detail.available === "number") available = detail.available;
2039
+ }
2040
+ } catch {
2041
+ }
2042
+ const topUp = "Top up your balance, then run `balise sync` again.\n";
2043
+ if (estimate !== void 0 && available !== void 0) {
2044
+ return `Not enough credits \u2014 this sync needs ~${fmtEstimate(estimate)} cr but only ${fmtCredits(available)} cr available.
2045
+ ${topUp}`;
2046
+ }
2047
+ return `Not enough credits to run this sync.
2048
+ ${topUp}`;
2049
+ }
2053
2050
  async function runSync(opts) {
2054
2051
  try {
2055
2052
  await runSyncInner(opts);
@@ -2080,28 +2077,23 @@ async function runSyncInner(opts) {
2080
2077
  throw err;
2081
2078
  }
2082
2079
  try {
2083
- await ensureAuthenticated({
2084
- supabaseUrl: opts.supabaseUrl
2085
- });
2080
+ await ensureAuthenticated();
2086
2081
  } catch (err) {
2087
- if (err instanceof LoginDeclinedError) {
2088
- process.stderr.write("Login required. Aborting.\n");
2082
+ if (err instanceof NotLoggedInError) {
2083
+ process.stderr.write(NOT_LOGGED_IN_MESSAGE + "\n");
2089
2084
  process.exit(1);
2090
2085
  }
2091
2086
  throw err;
2092
2087
  }
2093
- let cfg = await readConfig(cwd);
2088
+ const cfg = await readConfig(cwd);
2094
2089
  if (!cfg) {
2095
- process.stderr.write(`${INIT_AUTOLAUNCH_MESSAGE}
2096
- `);
2097
- await runInit({ ...opts, cwd });
2098
- cfg = await readConfig(cwd);
2099
- if (!cfg) {
2100
- process.stderr.write("Init cancelled. Aborting sync.\n");
2101
- process.exit(1);
2102
- }
2090
+ process.stderr.write(
2091
+ "This repo isn't linked to Balise yet.\n Run `balise init` to set up your repo.\n"
2092
+ );
2093
+ process.exit(1);
2103
2094
  }
2104
2095
  const dirty = await isDirty({ cwd });
2096
+ const ignoreUncommitted = await isPathUncommitted({ cwd }, ".baliseignore");
2105
2097
  const client = new ApiClient({
2106
2098
  apiUrl: cfg.api.url,
2107
2099
  supabaseUrl: opts.supabaseUrl
@@ -2110,6 +2102,15 @@ async function runSyncInner(opts) {
2110
2102
  `Packing ${cfg.repo.owner_login}/${cfg.repo.slug} at HEAD\u2026
2111
2103
  `
2112
2104
  );
2105
+ if (ignoreUncommitted) {
2106
+ process.stderr.write(
2107
+ `\u26A0 .baliseignore has uncommitted changes \u2014 it's read per commit
2108
+ server-side, so those edits will NOT apply to this sync.
2109
+ Commit it first if you want them to take effect:
2110
+ git add .baliseignore && git commit -m "chore: update .baliseignore"
2111
+ `
2112
+ );
2113
+ }
2113
2114
  const submit = async (force, dryRun) => {
2114
2115
  const { stream, commitSha, branch } = await gitBundle({ cwd });
2115
2116
  const query = buildSyncQuery({ commitSha, branch, force, base: opts.base, dryRun });
@@ -2194,7 +2195,7 @@ async function runSyncInner(opts) {
2194
2195
  if (dry.outcome.kind !== "dry_run") {
2195
2196
  process.exit(1);
2196
2197
  }
2197
- if (!isInteractive2()) {
2198
+ if (!isInteractive()) {
2198
2199
  process.stdout.write(formatDryRunSummary(dry.outcome.body) + "\n");
2199
2200
  process.stderr.write(
2200
2201
  "Non-interactive terminal \u2014 re-run with -y to confirm and sync.\n"
@@ -2255,7 +2256,7 @@ async function uploadOnce(client, owner, slug, stream, query) {
2255
2256
  }
2256
2257
  function handleUploadError(err) {
2257
2258
  if (err instanceof NotAuthenticatedError) {
2258
- process.stderr.write("Not logged in \u2014 run `balise login`.\n");
2259
+ process.stderr.write(SESSION_EXPIRED_MESSAGE + "\n");
2259
2260
  return;
2260
2261
  }
2261
2262
  if (err instanceof ApiUnreachableError) {
@@ -2264,6 +2265,10 @@ function handleUploadError(err) {
2264
2265
  );
2265
2266
  return;
2266
2267
  }
2268
+ if (err instanceof ApiError && err.status === 402) {
2269
+ process.stderr.write(formatInsufficientCredits(err.body));
2270
+ return;
2271
+ }
2267
2272
  process.stderr.write(
2268
2273
  "Something went wrong while uploading. Please try again.\n"
2269
2274
  );
@@ -2314,7 +2319,7 @@ var initCmd = defineCommand({
2314
2319
  var syncCmd = defineCommand({
2315
2320
  meta: {
2316
2321
  name: "sync",
2317
- description: "Tarball current repo \u2192 upload \u2192 poll extraction progress."
2322
+ description: "Bundle current repo \u2192 upload \u2192 track sync progress."
2318
2323
  },
2319
2324
  args: {
2320
2325
  yes: {
@@ -2346,8 +2351,8 @@ var syncCmd = defineCommand({
2346
2351
  var main = defineCommand({
2347
2352
  meta: {
2348
2353
  name: "balise",
2349
- version: "0.1.0",
2350
- description: "Balise CLI \u2014 push codebase for spec extraction."
2354
+ version: "0.3.2",
2355
+ description: "Balise CLI \u2014 keep your spec graph in sync with your code."
2351
2356
  },
2352
2357
  subCommands: {
2353
2358
  login: loginCmd,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@balise.dev/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Balise CLI — push codebase to Balise backend for spec extraction.",
5
5
  "type": "module",
6
6
  "bin": {