@balise.dev/cli 0.1.2 → 0.1.3
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/dist/index.js +335 -87
- package/package.json +2 -3
package/dist/index.js
CHANGED
|
@@ -548,8 +548,8 @@ var ApiClient = class {
|
|
|
548
548
|
}
|
|
549
549
|
return res.json();
|
|
550
550
|
}
|
|
551
|
-
async getJson(
|
|
552
|
-
const url = joinUrl(this.opts.apiUrl,
|
|
551
|
+
async getJson(path5) {
|
|
552
|
+
const url = joinUrl(this.opts.apiUrl, path5);
|
|
553
553
|
return this.withAuth(async (headers) => {
|
|
554
554
|
const { statusCode, body } = await wrapNetwork(() => request2(url, {
|
|
555
555
|
method: "GET",
|
|
@@ -564,8 +564,8 @@ var ApiClient = class {
|
|
|
564
564
|
};
|
|
565
565
|
});
|
|
566
566
|
}
|
|
567
|
-
async postJson(
|
|
568
|
-
const url = joinUrl(this.opts.apiUrl,
|
|
567
|
+
async postJson(path5, payload) {
|
|
568
|
+
const url = joinUrl(this.opts.apiUrl, path5);
|
|
569
569
|
return this.withAuth(async (headers) => {
|
|
570
570
|
const { statusCode, body } = await wrapNetwork(() => request2(url, {
|
|
571
571
|
method: "POST",
|
|
@@ -589,10 +589,10 @@ var ApiClient = class {
|
|
|
589
589
|
* metadata travels in the query string. Matches the FastAPI contract
|
|
590
590
|
* `POST /v1/repos/:owner/:slug/sync?commit_sha=...&branch=...`.
|
|
591
591
|
*/
|
|
592
|
-
async uploadBundle(
|
|
592
|
+
async uploadBundle(path5, opts) {
|
|
593
593
|
await this.ensureFreshToken();
|
|
594
594
|
const qs = new URLSearchParams(opts.query).toString();
|
|
595
|
-
const url = `${joinUrl(this.opts.apiUrl,
|
|
595
|
+
const url = `${joinUrl(this.opts.apiUrl, path5)}${qs ? `?${qs}` : ""}`;
|
|
596
596
|
return this.withAuth(async (headers) => {
|
|
597
597
|
const { statusCode, body } = await wrapNetwork(() => request2(url, {
|
|
598
598
|
method: "POST",
|
|
@@ -613,9 +613,43 @@ var ApiClient = class {
|
|
|
613
613
|
};
|
|
614
614
|
});
|
|
615
615
|
}
|
|
616
|
+
/**
|
|
617
|
+
* Same wire as `uploadBundle` but returns the raw `(status, text)` pair so
|
|
618
|
+
* callers can branch on response shape (200 dry-run, 202 happy, 409
|
|
619
|
+
* idempotency, 422 invalid base) without `ApiError` swallowing the body.
|
|
620
|
+
*
|
|
621
|
+
* Token refresh and auth attachment behave identically; we still rely on
|
|
622
|
+
* `withAuth` for the 401-then-retry plumbing. Non-401 statuses are
|
|
623
|
+
* returned to the caller verbatim — `withAuth` only treats 401 specially.
|
|
624
|
+
*/
|
|
625
|
+
async uploadBundleRaw(path5, opts) {
|
|
626
|
+
await this.ensureFreshToken();
|
|
627
|
+
const qs = new URLSearchParams(opts.query).toString();
|
|
628
|
+
const url = `${joinUrl(this.opts.apiUrl, path5)}${qs ? `?${qs}` : ""}`;
|
|
629
|
+
let tokens = await loadTokens();
|
|
630
|
+
if (!tokens) throw new NotAuthenticatedError();
|
|
631
|
+
const exec = async (token) => {
|
|
632
|
+
const { statusCode, body } = await wrapNetwork(
|
|
633
|
+
() => request2(url, {
|
|
634
|
+
method: "POST",
|
|
635
|
+
headers: {
|
|
636
|
+
Authorization: `Bearer ${token}`,
|
|
637
|
+
"Content-Type": "application/octet-stream"
|
|
638
|
+
},
|
|
639
|
+
body: opts.bundleStream,
|
|
640
|
+
dispatcher: this.opts.dispatcher
|
|
641
|
+
})
|
|
642
|
+
);
|
|
643
|
+
const text = await body.text();
|
|
644
|
+
return { status: statusCode, text };
|
|
645
|
+
};
|
|
646
|
+
const first = await exec(tokens.access_token);
|
|
647
|
+
if (first.status !== 401) return first;
|
|
648
|
+
throw new NotAuthenticatedError();
|
|
649
|
+
}
|
|
616
650
|
};
|
|
617
|
-
function joinUrl(base,
|
|
618
|
-
return `${base.replace(/\/$/, "")}${
|
|
651
|
+
function joinUrl(base, path5) {
|
|
652
|
+
return `${base.replace(/\/$/, "")}${path5.startsWith("/") ? path5 : `/${path5}`}`;
|
|
619
653
|
}
|
|
620
654
|
|
|
621
655
|
// src/config.ts
|
|
@@ -795,7 +829,6 @@ async function gitBundle(opts) {
|
|
|
795
829
|
}
|
|
796
830
|
|
|
797
831
|
// src/auth-ensure.ts
|
|
798
|
-
import readline from "readline/promises";
|
|
799
832
|
import crypto3 from "crypto";
|
|
800
833
|
import open2 from "open";
|
|
801
834
|
var LoginDeclinedError = class extends Error {
|
|
@@ -808,6 +841,7 @@ function isInteractive(stdin) {
|
|
|
808
841
|
const s = stdin ?? process.stdin;
|
|
809
842
|
return Boolean(s.isTTY);
|
|
810
843
|
}
|
|
844
|
+
var LOGIN_AUTOLAUNCH_MESSAGE = "Not logged in, launching balise login...";
|
|
811
845
|
async function ensureAuthenticated(opts) {
|
|
812
846
|
const existing = await loadTokens();
|
|
813
847
|
if (existing) return;
|
|
@@ -815,19 +849,8 @@ async function ensureAuthenticated(opts) {
|
|
|
815
849
|
if (!isInteractive(opts.stdin)) {
|
|
816
850
|
throw new LoginDeclinedError();
|
|
817
851
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
output: opts.stdout ?? process.stdout
|
|
821
|
-
});
|
|
822
|
-
let answer;
|
|
823
|
-
try {
|
|
824
|
-
answer = (await rl.question("Not logged in. Login now? (Y/n) ")).trim();
|
|
825
|
-
} finally {
|
|
826
|
-
rl.close();
|
|
827
|
-
}
|
|
828
|
-
if (answer && !/^y(es)?$/i.test(answer)) {
|
|
829
|
-
throw new LoginDeclinedError();
|
|
830
|
-
}
|
|
852
|
+
stderr.write(`${LOGIN_AUTOLAUNCH_MESSAGE}
|
|
853
|
+
`);
|
|
831
854
|
const verifier = generateCodeVerifier();
|
|
832
855
|
const challenge = codeChallengeFor(verifier);
|
|
833
856
|
const state = crypto3.randomBytes(16).toString("hex");
|
|
@@ -892,55 +915,81 @@ ${credentialsHelpMessage()}
|
|
|
892
915
|
// src/ui/InitPicker.tsx
|
|
893
916
|
import React, { useState } from "react";
|
|
894
917
|
import { Box, Text, useApp, useInput } from "ink";
|
|
918
|
+
import { Select, TextInput } from "@inkjs/ui";
|
|
919
|
+
function normalizeSlug(raw) {
|
|
920
|
+
return raw.toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
921
|
+
}
|
|
895
922
|
function InitPicker(props) {
|
|
896
923
|
const { exit } = useApp();
|
|
897
|
-
const [
|
|
898
|
-
|
|
924
|
+
const [field, setField] = useState("slug");
|
|
925
|
+
const [slug, setSlug] = useState(normalizeSlug(props.defaultSlug));
|
|
926
|
+
const [ownerId, setOwnerId] = useState(
|
|
927
|
+
props.ownerships[0]?.id
|
|
899
928
|
);
|
|
900
|
-
const [
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
929
|
+
const [repoId, setRepoId] = useState(
|
|
930
|
+
props.repos[0]?.id
|
|
931
|
+
);
|
|
932
|
+
const submit = () => {
|
|
933
|
+
if (field === "link") {
|
|
934
|
+
const repo = props.repos.find((r) => r.id === repoId);
|
|
935
|
+
if (!repo) return;
|
|
936
|
+
props.onDone({ action: "link", repo });
|
|
937
|
+
} else {
|
|
938
|
+
const owner = props.ownerships.find((o) => o.id === ownerId);
|
|
939
|
+
if (!owner || !slug) return;
|
|
940
|
+
props.onDone({ action: "create", slug, owner });
|
|
941
|
+
}
|
|
942
|
+
exit();
|
|
943
|
+
};
|
|
944
|
+
useInput((_input, key) => {
|
|
904
945
|
if (key.escape) {
|
|
905
946
|
props.onDone({ action: "cancel" });
|
|
906
947
|
exit();
|
|
907
948
|
return;
|
|
908
949
|
}
|
|
909
950
|
if (key.tab) {
|
|
910
|
-
|
|
951
|
+
setField((f) => {
|
|
952
|
+
if (f === "slug") return "owner";
|
|
953
|
+
if (f === "owner") return props.repos.length > 0 ? "link" : "slug";
|
|
954
|
+
return "slug";
|
|
955
|
+
});
|
|
911
956
|
return;
|
|
912
957
|
}
|
|
913
958
|
if (key.return) {
|
|
914
|
-
|
|
915
|
-
if (!props.ownerships[ownerIdx]) return;
|
|
916
|
-
props.onDone({
|
|
917
|
-
action: "create",
|
|
918
|
-
slug,
|
|
919
|
-
owner: props.ownerships[ownerIdx]
|
|
920
|
-
});
|
|
921
|
-
} else {
|
|
922
|
-
if (!props.repos[repoIdx]) return;
|
|
923
|
-
props.onDone({ action: "link", repo: props.repos[repoIdx] });
|
|
924
|
-
}
|
|
925
|
-
exit();
|
|
926
|
-
return;
|
|
927
|
-
}
|
|
928
|
-
if (zone === "create") {
|
|
929
|
-
if (key.upArrow)
|
|
930
|
-
setOwnerIdx((i) => (i - 1 + props.ownerships.length) % Math.max(1, props.ownerships.length));
|
|
931
|
-
else if (key.downArrow)
|
|
932
|
-
setOwnerIdx((i) => (i + 1) % Math.max(1, props.ownerships.length));
|
|
933
|
-
else if (key.backspace || key.delete) setSlug((s) => s.slice(0, -1));
|
|
934
|
-
else if (input && /^[a-z0-9-]$/i.test(input))
|
|
935
|
-
setSlug((s) => (s + input).toLowerCase());
|
|
936
|
-
} else {
|
|
937
|
-
if (key.upArrow)
|
|
938
|
-
setRepoIdx((i) => (i - 1 + props.repos.length) % Math.max(1, props.repos.length));
|
|
939
|
-
else if (key.downArrow)
|
|
940
|
-
setRepoIdx((i) => (i + 1) % Math.max(1, props.repos.length));
|
|
959
|
+
submit();
|
|
941
960
|
}
|
|
942
961
|
});
|
|
943
|
-
|
|
962
|
+
const ownerOptions = props.ownerships.map((o) => ({
|
|
963
|
+
label: `${o.login} (${o.type})`,
|
|
964
|
+
value: o.id
|
|
965
|
+
}));
|
|
966
|
+
const repoOptions = props.repos.map((r) => ({
|
|
967
|
+
label: `${r.owner_login}/${r.slug}`,
|
|
968
|
+
value: r.id
|
|
969
|
+
}));
|
|
970
|
+
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true }, "Balise \u2014 link or create a repo"), /* @__PURE__ */ React.createElement(Box, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React.createElement(Text, { color: field === "slug" ? "cyan" : "gray" }, field === "slug" ? "\u25B8 " : " ", "New repo slug"), /* @__PURE__ */ React.createElement(Box, { marginLeft: 2 }, /* @__PURE__ */ React.createElement(
|
|
971
|
+
TextInput,
|
|
972
|
+
{
|
|
973
|
+
defaultValue: slug,
|
|
974
|
+
placeholder: "repo-slug",
|
|
975
|
+
isDisabled: field !== "slug",
|
|
976
|
+
onChange: (v) => setSlug(normalizeSlug(v))
|
|
977
|
+
}
|
|
978
|
+
))), /* @__PURE__ */ React.createElement(Box, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React.createElement(Text, { color: field === "owner" ? "cyan" : "gray" }, field === "owner" ? "\u25B8 " : " ", "Owner"), /* @__PURE__ */ React.createElement(Box, { marginLeft: 2 }, field === "owner" ? /* @__PURE__ */ React.createElement(
|
|
979
|
+
Select,
|
|
980
|
+
{
|
|
981
|
+
options: ownerOptions,
|
|
982
|
+
defaultValue: ownerId,
|
|
983
|
+
onChange: setOwnerId
|
|
984
|
+
}
|
|
985
|
+
) : /* @__PURE__ */ React.createElement(Text, { dimColor: true }, ownerOptions.find((o) => o.value === ownerId)?.label ?? "\u2014"))), props.repos.length > 0 && /* @__PURE__ */ React.createElement(Box, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React.createElement(Text, { color: field === "link" ? "cyan" : "gray" }, field === "link" ? "\u25B8 " : " ", "Link existing (", props.repos.length, ")"), /* @__PURE__ */ React.createElement(Box, { marginLeft: 2 }, field === "link" ? /* @__PURE__ */ React.createElement(
|
|
986
|
+
Select,
|
|
987
|
+
{
|
|
988
|
+
options: repoOptions,
|
|
989
|
+
defaultValue: repoId,
|
|
990
|
+
onChange: setRepoId
|
|
991
|
+
}
|
|
992
|
+
) : /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "tab to browse"))), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Tab: next field \xB7 \u2191/\u2193: navigate \xB7 Enter: confirm \xB7 Esc: cancel")));
|
|
944
993
|
}
|
|
945
994
|
|
|
946
995
|
// src/commands/init.ts
|
|
@@ -1045,13 +1094,55 @@ async function runInit(opts) {
|
|
|
1045
1094
|
}
|
|
1046
1095
|
|
|
1047
1096
|
// src/commands/sync.ts
|
|
1097
|
+
import readline from "readline/promises";
|
|
1048
1098
|
import React4 from "react";
|
|
1049
1099
|
import { render as render2 } from "ink";
|
|
1050
1100
|
|
|
1101
|
+
// src/logger.ts
|
|
1102
|
+
import { promises as fs3 } from "fs";
|
|
1103
|
+
import path4 from "path";
|
|
1104
|
+
import os2 from "os";
|
|
1105
|
+
var APP_DIR2 = ".balise";
|
|
1106
|
+
var FILENAME2 = "balise.log";
|
|
1107
|
+
function logPath() {
|
|
1108
|
+
const override = process.env.BALISE_LOG_FILE;
|
|
1109
|
+
if (override && override.length > 0) return override;
|
|
1110
|
+
return path4.join(os2.homedir(), APP_DIR2, FILENAME2);
|
|
1111
|
+
}
|
|
1112
|
+
function formatError(err) {
|
|
1113
|
+
if (err instanceof Error) {
|
|
1114
|
+
const stack = err.stack ?? `${err.name}: ${err.message}`;
|
|
1115
|
+
const extras = [];
|
|
1116
|
+
const cause = err.cause;
|
|
1117
|
+
if (cause) extras.push(` caused by: ${formatError(cause)}`);
|
|
1118
|
+
return extras.length ? `${stack}
|
|
1119
|
+
${extras.join("\n")}` : stack;
|
|
1120
|
+
}
|
|
1121
|
+
if (typeof err === "string") return err;
|
|
1122
|
+
try {
|
|
1123
|
+
return JSON.stringify(err);
|
|
1124
|
+
} catch {
|
|
1125
|
+
return String(err);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
async function logError(context, err) {
|
|
1129
|
+
try {
|
|
1130
|
+
const p = logPath();
|
|
1131
|
+
await fs3.mkdir(path4.dirname(p), { recursive: true, mode: 448 });
|
|
1132
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1133
|
+
const line = `[${ts}] ERROR ${context}
|
|
1134
|
+
${formatError(err)}
|
|
1135
|
+
|
|
1136
|
+
`;
|
|
1137
|
+
await fs3.appendFile(p, line, { mode: 384 });
|
|
1138
|
+
} catch {
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1051
1142
|
// src/ui/SyncProgress.tsx
|
|
1052
1143
|
import React3, { useEffect, useState as useState2 } from "react";
|
|
1053
1144
|
import { Box as Box2, Text as Text2, useApp as useApp2 } from "ink";
|
|
1054
|
-
import Spinner from "
|
|
1145
|
+
import { Spinner } from "@inkjs/ui";
|
|
1055
1146
|
var QUEUED_MESSAGES = [
|
|
1056
1147
|
"Waiting for an agent willing to accept the job\u2026",
|
|
1057
1148
|
"Your request is in line. An agent will be assigned once one stops pretending to be busy.",
|
|
@@ -1169,7 +1260,7 @@ function SyncProgress(props) {
|
|
|
1169
1260
|
return /* @__PURE__ */ React3.createElement(Text2, { color: "red" }, "\u2717 Failed");
|
|
1170
1261
|
}
|
|
1171
1262
|
if (!status) {
|
|
1172
|
-
return /* @__PURE__ */ React3.createElement(
|
|
1263
|
+
return /* @__PURE__ */ React3.createElement(Spinner, { label: "Getting things ready\u2026" });
|
|
1173
1264
|
}
|
|
1174
1265
|
if (status.status === "done") {
|
|
1175
1266
|
return /* @__PURE__ */ React3.createElement(Text2, { color: "green" }, "\u2713 Done");
|
|
@@ -1179,21 +1270,71 @@ function SyncProgress(props) {
|
|
|
1179
1270
|
}
|
|
1180
1271
|
const message = status.status === "queued" ? QUEUED_MESSAGES[queuedIdx] : RUNNING_MESSAGES[runningIdx];
|
|
1181
1272
|
const { files_processed, files_total, nodes_pushed } = status.progress;
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1273
|
+
const isRunning = status.status === "running";
|
|
1274
|
+
const showFiles = isRunning && files_total > 0;
|
|
1275
|
+
const showNodes = isRunning && nodes_pushed > 0;
|
|
1276
|
+
return /* @__PURE__ */ React3.createElement(Box2, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Spinner, { label: message }), showFiles || showNodes ? /* @__PURE__ */ React3.createElement(Text2, { dimColor: true }, " ", showFiles ? `${files_processed}/${files_total} files` : null, showFiles && showNodes ? " \xB7 " : null, showNodes ? `${nodes_pushed} concepts` : null) : null);
|
|
1184
1277
|
}
|
|
1185
1278
|
|
|
1186
1279
|
// src/commands/sync.ts
|
|
1280
|
+
function buildSyncQuery(opts) {
|
|
1281
|
+
const q = { commit_sha: opts.commitSha };
|
|
1282
|
+
if (opts.branch) q.branch = opts.branch;
|
|
1283
|
+
q.force = opts.force ? "true" : "false";
|
|
1284
|
+
q.dry_run = opts.dryRun ? "true" : "false";
|
|
1285
|
+
if (opts.base) q.base = opts.base;
|
|
1286
|
+
return q;
|
|
1287
|
+
}
|
|
1288
|
+
var INIT_AUTOLAUNCH_MESSAGE = "Repo not initialized, launching balise init...";
|
|
1289
|
+
function drainStdinBuffer(stdin) {
|
|
1290
|
+
if (stdin !== process.stdin) return;
|
|
1291
|
+
const s = stdin;
|
|
1292
|
+
if (typeof s.read !== "function") return;
|
|
1293
|
+
while (s.read() !== null) {
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
async function confirmRetryWithForce(opts) {
|
|
1297
|
+
const stderr = opts.stderr ?? process.stderr;
|
|
1298
|
+
const synced = opts.body.synced_at ?? "unknown time";
|
|
1299
|
+
stderr.write(
|
|
1300
|
+
`commit already synced at tag ${opts.body.tag} (synced at ${synced}), re-run ?
|
|
1301
|
+
`
|
|
1302
|
+
);
|
|
1303
|
+
if (opts.autoConfirm) {
|
|
1304
|
+
return false;
|
|
1305
|
+
}
|
|
1306
|
+
const input = opts.stdin ?? process.stdin;
|
|
1307
|
+
drainStdinBuffer(input);
|
|
1308
|
+
const rl = readline.createInterface({
|
|
1309
|
+
input,
|
|
1310
|
+
output: opts.stdout ?? process.stdout
|
|
1311
|
+
});
|
|
1312
|
+
let answer;
|
|
1313
|
+
try {
|
|
1314
|
+
answer = (await rl.question("re-run ? (y/N) ")).trim();
|
|
1315
|
+
} finally {
|
|
1316
|
+
rl.close();
|
|
1317
|
+
}
|
|
1318
|
+
return /^y(es)?$/i.test(answer);
|
|
1319
|
+
}
|
|
1320
|
+
function formatDryRunSummary(body) {
|
|
1321
|
+
const fromDiff = body.would_be_cold_start ? "cold-start (no prior tag)" : `from-commit=${body.from_commit ?? "?"}` + (body.from_commit_distance !== null ? ` distance=${body.from_commit_distance}` : "");
|
|
1322
|
+
return `Sync would use: base=${body.base_dolt_ref} (${body.base_kind}) / ${fromDiff}`;
|
|
1323
|
+
}
|
|
1187
1324
|
async function runSync(opts) {
|
|
1188
1325
|
try {
|
|
1189
1326
|
await runSyncInner(opts);
|
|
1190
1327
|
} catch (err) {
|
|
1328
|
+
await logError("sync", err);
|
|
1191
1329
|
if (err instanceof ApiUnreachableError) {
|
|
1192
1330
|
process.stderr.write(
|
|
1193
1331
|
"Cannot reach the Balise service. Please try again in a moment.\n"
|
|
1194
1332
|
);
|
|
1195
1333
|
} else {
|
|
1196
|
-
process.stderr.write(
|
|
1334
|
+
process.stderr.write(
|
|
1335
|
+
`Something went wrong. Please try again. (details: ${logPath()})
|
|
1336
|
+
`
|
|
1337
|
+
);
|
|
1197
1338
|
}
|
|
1198
1339
|
process.exit(1);
|
|
1199
1340
|
}
|
|
@@ -1222,7 +1363,8 @@ async function runSyncInner(opts) {
|
|
|
1222
1363
|
}
|
|
1223
1364
|
let cfg = await readConfig(cwd);
|
|
1224
1365
|
if (!cfg) {
|
|
1225
|
-
process.stderr.write(
|
|
1366
|
+
process.stderr.write(`${INIT_AUTOLAUNCH_MESSAGE}
|
|
1367
|
+
`);
|
|
1226
1368
|
await runInit({ ...opts, cwd });
|
|
1227
1369
|
cfg = await readConfig(cwd);
|
|
1228
1370
|
if (!cfg) {
|
|
@@ -1243,32 +1385,52 @@ async function runSyncInner(opts) {
|
|
|
1243
1385
|
`Packing ${cfg.repo.owner_login}/${cfg.repo.slug} at HEAD\u2026
|
|
1244
1386
|
`
|
|
1245
1387
|
);
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1388
|
+
const submit = async (force) => {
|
|
1389
|
+
const { stream, commitSha, branch } = await gitBundle({ cwd });
|
|
1390
|
+
const query = buildSyncQuery({
|
|
1391
|
+
commitSha,
|
|
1392
|
+
branch,
|
|
1393
|
+
force,
|
|
1394
|
+
base: opts.base,
|
|
1395
|
+
dryRun: opts.dryRun ?? false
|
|
1396
|
+
});
|
|
1397
|
+
return uploadOnce(client, cfg.repo.owner_login, cfg.repo.slug, stream, query);
|
|
1398
|
+
};
|
|
1399
|
+
let outcome;
|
|
1248
1400
|
try {
|
|
1249
|
-
|
|
1250
|
-
`/v1/repos/${cfg.repo.owner_login}/${cfg.repo.slug}/sync`,
|
|
1251
|
-
{
|
|
1252
|
-
bundleStream: stream,
|
|
1253
|
-
query: { commit_sha: commitSha, branch }
|
|
1254
|
-
}
|
|
1255
|
-
);
|
|
1401
|
+
outcome = await submit(opts.force ?? false);
|
|
1256
1402
|
} catch (err) {
|
|
1257
|
-
|
|
1258
|
-
|
|
1403
|
+
handleUploadError(err);
|
|
1404
|
+
process.exit(1);
|
|
1405
|
+
}
|
|
1406
|
+
if (outcome.kind === "conflict") {
|
|
1407
|
+
const proceedForce = await confirmRetryWithForce({
|
|
1408
|
+
body: outcome.body,
|
|
1409
|
+
autoConfirm: opts.autoConfirm ?? false
|
|
1410
|
+
});
|
|
1411
|
+
if (!proceedForce) {
|
|
1259
1412
|
process.exit(1);
|
|
1260
1413
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
);
|
|
1414
|
+
try {
|
|
1415
|
+
outcome = await submit(true);
|
|
1416
|
+
} catch (err) {
|
|
1417
|
+
handleUploadError(err);
|
|
1265
1418
|
process.exit(1);
|
|
1266
1419
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1420
|
+
}
|
|
1421
|
+
if (outcome.kind === "invalid_base") {
|
|
1422
|
+
process.stderr.write(`invalid base ref: ${opts.base}
|
|
1423
|
+
`);
|
|
1424
|
+
process.exit(1);
|
|
1425
|
+
}
|
|
1426
|
+
if (outcome.kind === "dry_run") {
|
|
1427
|
+
process.stdout.write(formatDryRunSummary(outcome.body) + "\n");
|
|
1428
|
+
process.exit(0);
|
|
1429
|
+
}
|
|
1430
|
+
if (outcome.kind !== "accepted") {
|
|
1270
1431
|
process.exit(1);
|
|
1271
1432
|
}
|
|
1433
|
+
const accepted = outcome.body;
|
|
1272
1434
|
const result = await new Promise((resolve) => {
|
|
1273
1435
|
const app = render2(
|
|
1274
1436
|
React4.createElement(SyncProgress, {
|
|
@@ -1283,25 +1445,83 @@ async function runSyncInner(opts) {
|
|
|
1283
1445
|
});
|
|
1284
1446
|
process.exit(result ? 0 : 1);
|
|
1285
1447
|
}
|
|
1448
|
+
async function uploadOnce(client, owner, slug, stream, query) {
|
|
1449
|
+
const raw = await client.uploadBundleRaw(
|
|
1450
|
+
`/v1/repos/${owner}/${slug}/sync`,
|
|
1451
|
+
{
|
|
1452
|
+
bundleStream: stream,
|
|
1453
|
+
query
|
|
1454
|
+
}
|
|
1455
|
+
);
|
|
1456
|
+
if (raw.status === 202) {
|
|
1457
|
+
return { kind: "accepted", body: JSON.parse(raw.text) };
|
|
1458
|
+
}
|
|
1459
|
+
if (raw.status === 200) {
|
|
1460
|
+
return { kind: "dry_run", body: JSON.parse(raw.text) };
|
|
1461
|
+
}
|
|
1462
|
+
if (raw.status === 409) {
|
|
1463
|
+
const parsed = JSON.parse(raw.text);
|
|
1464
|
+
const body = "detail" in parsed && parsed.detail ? parsed.detail : parsed;
|
|
1465
|
+
return { kind: "conflict", body };
|
|
1466
|
+
}
|
|
1467
|
+
if (raw.status === 422) {
|
|
1468
|
+
let detail;
|
|
1469
|
+
try {
|
|
1470
|
+
const parsed = JSON.parse(raw.text);
|
|
1471
|
+
if (typeof parsed.detail === "string") detail = parsed.detail;
|
|
1472
|
+
} catch {
|
|
1473
|
+
}
|
|
1474
|
+
if (detail === "invalid_base_ref") return { kind: "invalid_base" };
|
|
1475
|
+
throw new ApiError(raw.status, raw.text);
|
|
1476
|
+
}
|
|
1477
|
+
throw new ApiError(raw.status, raw.text);
|
|
1478
|
+
}
|
|
1479
|
+
function handleUploadError(err) {
|
|
1480
|
+
if (err instanceof NotAuthenticatedError) {
|
|
1481
|
+
process.stderr.write("Not logged in \u2014 run `balise login`.\n");
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
if (err instanceof ApiUnreachableError) {
|
|
1485
|
+
process.stderr.write(
|
|
1486
|
+
"Cannot reach the Balise service. Please try again in a moment.\n"
|
|
1487
|
+
);
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
process.stderr.write(
|
|
1491
|
+
"Something went wrong while uploading. Please try again.\n"
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1286
1494
|
|
|
1287
1495
|
// src/index.ts
|
|
1496
|
+
async function withLog(name, fn) {
|
|
1497
|
+
try {
|
|
1498
|
+
await fn();
|
|
1499
|
+
} catch (err) {
|
|
1500
|
+
await logError(name, err);
|
|
1501
|
+
process.stderr.write(
|
|
1502
|
+
`Something went wrong (${name}). Details: ${logPath()}
|
|
1503
|
+
`
|
|
1504
|
+
);
|
|
1505
|
+
process.exit(1);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1288
1508
|
var SUPABASE_URL = process.env.BALISE_SUPABASE_URL ?? "https://api.balise.dev";
|
|
1289
1509
|
var loginCmd = defineCommand({
|
|
1290
1510
|
meta: { name: "login", description: "Authenticate via OAuth (PKCE loopback)." },
|
|
1291
1511
|
async run() {
|
|
1292
|
-
await runLogin({ supabaseUrl: SUPABASE_URL });
|
|
1512
|
+
await withLog("login", () => runLogin({ supabaseUrl: SUPABASE_URL }));
|
|
1293
1513
|
}
|
|
1294
1514
|
});
|
|
1295
1515
|
var logoutCmd = defineCommand({
|
|
1296
1516
|
meta: { name: "logout", description: "Clear stored credentials." },
|
|
1297
1517
|
async run() {
|
|
1298
|
-
await runLogout();
|
|
1518
|
+
await withLog("logout", () => runLogout());
|
|
1299
1519
|
}
|
|
1300
1520
|
});
|
|
1301
1521
|
var whoamiCmd = defineCommand({
|
|
1302
1522
|
meta: { name: "whoami", description: "Show current authenticated user." },
|
|
1303
1523
|
async run() {
|
|
1304
|
-
await runWhoami({ supabaseUrl: SUPABASE_URL });
|
|
1524
|
+
await withLog("whoami", () => runWhoami({ supabaseUrl: SUPABASE_URL }));
|
|
1305
1525
|
}
|
|
1306
1526
|
});
|
|
1307
1527
|
var initCmd = defineCommand({
|
|
@@ -1310,7 +1530,7 @@ var initCmd = defineCommand({
|
|
|
1310
1530
|
description: "Link or create a Balise repo and write .balise/config."
|
|
1311
1531
|
},
|
|
1312
1532
|
async run() {
|
|
1313
|
-
await runInit({ supabaseUrl: SUPABASE_URL });
|
|
1533
|
+
await withLog("init", () => runInit({ supabaseUrl: SUPABASE_URL }));
|
|
1314
1534
|
}
|
|
1315
1535
|
});
|
|
1316
1536
|
var syncCmd = defineCommand({
|
|
@@ -1318,8 +1538,36 @@ var syncCmd = defineCommand({
|
|
|
1318
1538
|
name: "sync",
|
|
1319
1539
|
description: "Tarball current repo \u2192 upload \u2192 poll extraction progress."
|
|
1320
1540
|
},
|
|
1321
|
-
|
|
1322
|
-
|
|
1541
|
+
args: {
|
|
1542
|
+
yes: {
|
|
1543
|
+
type: "boolean",
|
|
1544
|
+
alias: "y",
|
|
1545
|
+
description: "Skip sync confirmations (warning prompt + future 409 retry).",
|
|
1546
|
+
default: false
|
|
1547
|
+
},
|
|
1548
|
+
force: {
|
|
1549
|
+
type: "boolean",
|
|
1550
|
+
description: "Bypass idempotency 409 + resume-reuse: re-tag from scratch and re-resolve base.",
|
|
1551
|
+
default: false
|
|
1552
|
+
},
|
|
1553
|
+
base: {
|
|
1554
|
+
type: "string",
|
|
1555
|
+
description: "Explicit base balise ref (branch/tag/commit) to start from. Validated server-side."
|
|
1556
|
+
},
|
|
1557
|
+
"dry-run": {
|
|
1558
|
+
type: "boolean",
|
|
1559
|
+
description: "Preview the resolution without uploading the bundle or enqueuing the worker.",
|
|
1560
|
+
default: false
|
|
1561
|
+
}
|
|
1562
|
+
},
|
|
1563
|
+
async run({ args }) {
|
|
1564
|
+
await runSync({
|
|
1565
|
+
supabaseUrl: SUPABASE_URL,
|
|
1566
|
+
autoConfirm: Boolean(args.yes),
|
|
1567
|
+
force: Boolean(args.force),
|
|
1568
|
+
base: typeof args.base === "string" && args.base.length > 0 ? args.base : void 0,
|
|
1569
|
+
dryRun: Boolean(args["dry-run"])
|
|
1570
|
+
});
|
|
1323
1571
|
}
|
|
1324
1572
|
});
|
|
1325
1573
|
var main = defineCommand({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@balise.dev/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Balise CLI — push codebase to Balise backend for spec extraction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,11 +22,10 @@
|
|
|
22
22
|
"node": ">=20"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
+
"@inkjs/ui": "^2.0.0",
|
|
25
26
|
"citty": "^0.2.2",
|
|
26
27
|
"ini": "^5.0.0",
|
|
27
28
|
"ink": "^5.0.1",
|
|
28
|
-
"ink-progress-bar": "^3.0.0",
|
|
29
|
-
"ink-spinner": "^5.0.0",
|
|
30
29
|
"open": "^10.1.0",
|
|
31
30
|
"react": "^18.3.1",
|
|
32
31
|
"tar": "^7.4.3",
|