@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.
- package/README.md +4 -4
- package/dist/index.js +224 -219
- 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` |
|
|
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
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
1767
|
-
process.stderr.write(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
2088
|
-
process.stderr.write(
|
|
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
|
-
|
|
2088
|
+
const cfg = await readConfig(cwd);
|
|
2094
2089
|
if (!cfg) {
|
|
2095
|
-
process.stderr.write(
|
|
2096
|
-
`
|
|
2097
|
-
|
|
2098
|
-
|
|
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 (!
|
|
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(
|
|
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: "
|
|
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.
|
|
2350
|
-
description: "Balise CLI \u2014
|
|
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,
|