@balise.dev/cli 0.1.3 → 0.3.0

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 +19 -0
  2. package/dist/index.js +1038 -265
  3. package/package.json +4 -1
package/dist/index.js CHANGED
@@ -548,8 +548,8 @@ var ApiClient = class {
548
548
  }
549
549
  return res.json();
550
550
  }
551
- async getJson(path5) {
552
- const url = joinUrl(this.opts.apiUrl, path5);
551
+ async getJson(path7) {
552
+ const url = joinUrl(this.opts.apiUrl, path7);
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(path5, payload) {
568
- const url = joinUrl(this.opts.apiUrl, path5);
567
+ async postJson(path7, payload) {
568
+ const url = joinUrl(this.opts.apiUrl, path7);
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(path5, opts) {
592
+ async uploadBundle(path7, opts) {
593
593
  await this.ensureFreshToken();
594
594
  const qs = new URLSearchParams(opts.query).toString();
595
- const url = `${joinUrl(this.opts.apiUrl, path5)}${qs ? `?${qs}` : ""}`;
595
+ const url = `${joinUrl(this.opts.apiUrl, path7)}${qs ? `?${qs}` : ""}`;
596
596
  return this.withAuth(async (headers) => {
597
597
  const { statusCode, body } = await wrapNetwork(() => request2(url, {
598
598
  method: "POST",
@@ -622,10 +622,10 @@ var ApiClient = class {
622
622
  * `withAuth` for the 401-then-retry plumbing. Non-401 statuses are
623
623
  * returned to the caller verbatim — `withAuth` only treats 401 specially.
624
624
  */
625
- async uploadBundleRaw(path5, opts) {
625
+ async uploadBundleRaw(path7, opts) {
626
626
  await this.ensureFreshToken();
627
627
  const qs = new URLSearchParams(opts.query).toString();
628
- const url = `${joinUrl(this.opts.apiUrl, path5)}${qs ? `?${qs}` : ""}`;
628
+ const url = `${joinUrl(this.opts.apiUrl, path7)}${qs ? `?${qs}` : ""}`;
629
629
  let tokens = await loadTokens();
630
630
  if (!tokens) throw new NotAuthenticatedError();
631
631
  const exec = async (token) => {
@@ -648,8 +648,8 @@ var ApiClient = class {
648
648
  throw new NotAuthenticatedError();
649
649
  }
650
650
  };
651
- function joinUrl(base, path5) {
652
- return `${base.replace(/\/$/, "")}${path5.startsWith("/") ? path5 : `/${path5}`}`;
651
+ function joinUrl(base, path7) {
652
+ return `${base.replace(/\/$/, "")}${path7.startsWith("/") ? path7 : `/${path7}`}`;
653
653
  }
654
654
 
655
655
  // src/config.ts
@@ -734,8 +734,8 @@ account_id : ${me.account_id}
734
734
  }
735
735
 
736
736
  // src/commands/init.ts
737
- import path3 from "path";
738
- import React2 from "react";
737
+ import path5 from "path";
738
+ import React3 from "react";
739
739
  import { render } from "ink";
740
740
 
741
741
  // src/git.ts
@@ -913,83 +913,837 @@ ${credentialsHelpMessage()}
913
913
  }
914
914
 
915
915
  // src/ui/InitPicker.tsx
916
- import React, { useState } from "react";
916
+ import React, { useMemo, useState } from "react";
917
917
  import { Box, Text, useApp, useInput } from "ink";
918
- import { Select, TextInput } from "@inkjs/ui";
918
+ var ACCENT = "#E8A947";
919
+ var OK = "#9db89a";
920
+ var WIDTH = 56;
921
+ var RESERVED_SLUGS = /* @__PURE__ */ new Set(["settings"]);
919
922
  function normalizeSlug(raw) {
920
- return raw.toLowerCase().replace(/[^a-z0-9-]/g, "");
923
+ return raw.toLowerCase().replace(/[^a-z0-9_-]/g, "");
924
+ }
925
+ function slugStatus(slug, ownerId, repos) {
926
+ if (slug.length === 0) return "empty";
927
+ if (slug.length < 2) return "too-short";
928
+ if (slug.length > 40) return "too-long";
929
+ if (RESERVED_SLUGS.has(slug)) return "reserved";
930
+ if (ownerId && repos.some((r) => r.owner_id === ownerId && r.slug === slug)) {
931
+ return "taken";
932
+ }
933
+ return "ok";
921
934
  }
935
+ var FILTER_CHAR = /^[\w./-]$/;
922
936
  function InitPicker(props) {
923
937
  const { exit } = useApp();
924
- const [field, setField] = useState("slug");
925
- const [slug, setSlug] = useState(normalizeSlug(props.defaultSlug));
926
- const [ownerId, setOwnerId] = useState(
927
- props.ownerships[0]?.id
928
- );
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 });
938
+ const { repos, ownerships } = props;
939
+ const initialCursor = useMemo(() => {
940
+ if (props.currentRepoId) {
941
+ const i = repos.findIndex((r) => r.id === props.currentRepoId);
942
+ if (i >= 0) return i + 1;
943
+ }
944
+ return 0;
945
+ }, [repos, props.currentRepoId]);
946
+ const [mode, setMode] = useState("list");
947
+ const [filter, setFilter] = useState("");
948
+ const [listCursor, setListCursor] = useState(initialCursor);
949
+ const [ownerCursor, setOwnerCursor] = useState(0);
950
+ const [owner, setOwner] = useState(void 0);
951
+ const [name, setName] = useState(() => normalizeSlug(props.defaultSlug));
952
+ const [visibility, setVisibility] = useState("private");
953
+ const [field, setField] = useState("name");
954
+ const visibleRepos = useMemo(() => {
955
+ const f = filter.toLowerCase();
956
+ if (!f) return repos;
957
+ return repos.filter(
958
+ (r) => `${r.owner_login}/${r.slug}`.toLowerCase().includes(f)
959
+ );
960
+ }, [repos, filter]);
961
+ const maxListIdx = visibleRepos.length;
962
+ const safeCursor = Math.max(0, Math.min(listCursor, maxListIdx));
963
+ const status = slugStatus(name, owner?.id, repos);
964
+ useInput((input, key) => {
965
+ if (mode === "list") {
966
+ if (key.upArrow) {
967
+ setListCursor((c) => Math.max(0, Math.min(c, maxListIdx) - 1));
968
+ return;
969
+ }
970
+ if (key.downArrow) {
971
+ setListCursor((c) => Math.min(maxListIdx, c + 1));
972
+ return;
973
+ }
974
+ if (key.return) {
975
+ if (safeCursor === 0) {
976
+ setMode("owner");
977
+ setOwnerCursor(0);
978
+ } else {
979
+ const repo = visibleRepos[safeCursor - 1];
980
+ if (repo) {
981
+ props.onDone({ action: "link", repo });
982
+ exit();
983
+ }
984
+ }
985
+ return;
986
+ }
987
+ if (key.escape) {
988
+ if (filter) {
989
+ setFilter("");
990
+ setListCursor(0);
991
+ } else {
992
+ props.onDone({ action: "cancel" });
993
+ exit();
994
+ }
995
+ return;
996
+ }
997
+ if ((key.backspace || key.delete) && filter) {
998
+ setFilter((f) => f.slice(0, -1));
999
+ setListCursor(0);
1000
+ return;
1001
+ }
1002
+ if (!key.ctrl && !key.meta && input && FILTER_CHAR.test(input)) {
1003
+ setFilter((f) => f + input);
1004
+ setListCursor(0);
1005
+ }
1006
+ return;
1007
+ }
1008
+ if (mode === "owner") {
1009
+ if (key.upArrow) {
1010
+ setOwnerCursor((c) => Math.max(0, c - 1));
1011
+ return;
1012
+ }
1013
+ if (key.downArrow) {
1014
+ setOwnerCursor((c) => Math.min(ownerships.length - 1, c + 1));
1015
+ return;
1016
+ }
1017
+ if (key.return) {
1018
+ const chosen = ownerships[ownerCursor];
1019
+ if (chosen) {
1020
+ setOwner(chosen);
1021
+ setField("name");
1022
+ setMode("detail");
1023
+ }
1024
+ return;
1025
+ }
1026
+ if (key.escape) {
1027
+ setMode("list");
1028
+ }
1029
+ return;
941
1030
  }
942
- exit();
943
- };
944
- useInput((_input, key) => {
945
1031
  if (key.escape) {
946
- props.onDone({ action: "cancel" });
947
- exit();
1032
+ if (field === "visibility") setField("name");
1033
+ else setMode("owner");
948
1034
  return;
949
1035
  }
950
- if (key.tab) {
951
- setField((f) => {
952
- if (f === "slug") return "owner";
953
- if (f === "owner") return props.repos.length > 0 ? "link" : "slug";
954
- return "slug";
955
- });
1036
+ if (field === "name") {
1037
+ if (key.return) {
1038
+ if (status === "ok") setField("visibility");
1039
+ return;
1040
+ }
1041
+ if (key.backspace || key.delete) {
1042
+ setName((n) => n.slice(0, -1));
1043
+ return;
1044
+ }
1045
+ if (!key.ctrl && !key.meta && input) {
1046
+ const accepted = input.replace(/[^a-z0-9_-]/g, "");
1047
+ if (accepted) setName((n) => n + accepted);
1048
+ }
1049
+ return;
1050
+ }
1051
+ if (key.leftArrow || key.rightArrow || input === " ") {
1052
+ setVisibility((v) => v === "private" ? "public" : "private");
1053
+ return;
1054
+ }
1055
+ if (key.upArrow) {
1056
+ setField("name");
956
1057
  return;
957
1058
  }
958
1059
  if (key.return) {
959
- submit();
1060
+ if (status !== "ok") {
1061
+ setField("name");
1062
+ return;
1063
+ }
1064
+ props.onDone({
1065
+ action: "create",
1066
+ slug: name,
1067
+ owner,
1068
+ visibility
1069
+ });
1070
+ exit();
960
1071
  }
961
1072
  });
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,
1073
+ if (mode === "owner") {
1074
+ return /* @__PURE__ */ React.createElement(OwnerStep, { ownerships, cursor: ownerCursor });
1075
+ }
1076
+ if (mode === "detail" && owner) {
1077
+ return /* @__PURE__ */ React.createElement(
1078
+ DetailStep,
1079
+ {
1080
+ owner,
1081
+ name,
1082
+ visibility,
1083
+ field,
1084
+ status
1085
+ }
1086
+ );
1087
+ }
1088
+ return /* @__PURE__ */ React.createElement(
1089
+ ListStep,
972
1090
  {
973
- defaultValue: slug,
974
- placeholder: "repo-slug",
975
- isDisabled: field !== "slug",
976
- onChange: (v) => setSlug(normalizeSlug(v))
1091
+ repos: visibleRepos,
1092
+ total: repos.length,
1093
+ filter,
1094
+ cursor: safeCursor,
1095
+ currentRepoId: props.currentRepoId
977
1096
  }
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,
1097
+ );
1098
+ }
1099
+ function ListStep(props) {
1100
+ const { repos, total, filter, cursor } = props;
1101
+ const sub = filter ? `${repos.length}/${total} repos \xB7 filter: ${filter}` : `${total} repos \xB7 type to filter`;
1102
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", width: WIDTH, paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true }, "Select a repository"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, sub), /* @__PURE__ */ React.createElement(
1103
+ Row,
1104
+ {
1105
+ selected: cursor === 0,
1106
+ label: "+ Initialize new repository",
1107
+ labelColor: "white",
1108
+ kind: "here"
1109
+ }
1110
+ ), repos.length > 0 ? /* @__PURE__ */ React.createElement(Text, { color: "gray" }, "\u2500".repeat(WIDTH - 4)) : null, repos.map((r, i) => /* @__PURE__ */ React.createElement(
1111
+ Row,
980
1112
  {
981
- options: ownerOptions,
982
- defaultValue: ownerId,
983
- onChange: setOwnerId
1113
+ key: r.id,
1114
+ selected: cursor === i + 1,
1115
+ label: `${r.owner_login}/${r.slug}`,
1116
+ suffix: r.id === props.currentRepoId ? " (current)" : void 0,
1117
+ vis: r.visibility,
1118
+ kind: r.owner_type
984
1119
  }
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,
1120
+ )), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u2191\u2193 move \xB7 \u23CE select \xB7 type to filter \xB7 esc ", filter ? "clear" : "quit")));
1121
+ }
1122
+ function OwnerStep(props) {
1123
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", width: WIDTH, paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true }, "New repository \xB7 choose an owner"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "step 1 of 3 \xB7 owner \u2192 name \u2192 visibility"), /* @__PURE__ */ React.createElement(
1124
+ Box,
1125
+ {
1126
+ flexDirection: "column",
1127
+ borderStyle: "round",
1128
+ borderColor: ACCENT,
1129
+ paddingX: 1,
1130
+ marginTop: 1
1131
+ },
1132
+ props.ownerships.length === 0 ? /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "(no owners available)") : props.ownerships.map((o, i) => /* @__PURE__ */ React.createElement(
1133
+ Row,
1134
+ {
1135
+ key: o.id,
1136
+ selected: props.cursor === i,
1137
+ label: o.login,
1138
+ kind: o.type
1139
+ }
1140
+ ))
1141
+ ), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u2191\u2193 owner \xB7 \u23CE confirm & continue \xB7 esc back to list")));
1142
+ }
1143
+ function DetailStep(props) {
1144
+ const { owner, name, visibility, field, status } = props;
1145
+ const nameActive = field === "name";
1146
+ const taken = status === "taken";
1147
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", width: WIDTH, paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true }, "New repository"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, owner.login, " \xB7 ", owner.type), /* @__PURE__ */ React.createElement(
1148
+ Box,
987
1149
  {
988
- options: repoOptions,
989
- defaultValue: repoId,
990
- onChange: setRepoId
1150
+ flexDirection: "column",
1151
+ borderStyle: "round",
1152
+ borderColor: ACCENT,
1153
+ paddingX: 1,
1154
+ marginTop: 1
1155
+ },
1156
+ /* @__PURE__ */ React.createElement(FieldRow, { state: "done", label: "owner" }, /* @__PURE__ */ React.createElement(Text, null, owner.login, " ", /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\xB7 ", owner.type))),
1157
+ /* @__PURE__ */ React.createElement(FieldRow, { state: nameActive ? "active" : "done", label: "name" }, /* @__PURE__ */ React.createElement(Text, null, name, nameActive ? /* @__PURE__ */ React.createElement(Text, { color: ACCENT }, "\u2588") : null, nameActive && taken ? /* @__PURE__ */ React.createElement(Text, { color: ACCENT }, " \u26A0 existe d\xE9j\xE0") : null)),
1158
+ /* @__PURE__ */ React.createElement(
1159
+ FieldRow,
1160
+ {
1161
+ state: field === "visibility" ? "active" : "pending",
1162
+ label: "visibility"
1163
+ },
1164
+ /* @__PURE__ */ React.createElement(VisibilityPick, { value: visibility, dim: field !== "visibility" })
1165
+ )
1166
+ ), /* @__PURE__ */ React.createElement(
1167
+ StatusLine,
1168
+ {
1169
+ owner: owner.login,
1170
+ name,
1171
+ visibility,
1172
+ status
1173
+ }
1174
+ ), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u23CE next field \xB7 on visibility \u2190/\u2192 toggle \xB7 last \u23CE create \xB7 esc back")));
1175
+ }
1176
+ function Row(props) {
1177
+ const { selected } = props;
1178
+ return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Box, { width: 2, flexShrink: 0 }, /* @__PURE__ */ React.createElement(Text, { color: selected ? ACCENT : void 0 }, selected ? "\u276F " : " ")), /* @__PURE__ */ React.createElement(Box, { flexGrow: 1 }, /* @__PURE__ */ React.createElement(Text, { color: selected ? ACCENT : props.labelColor, wrap: "truncate-end" }, props.label, props.suffix ? /* @__PURE__ */ React.createElement(Text, { dimColor: true }, props.suffix) : null)), /* @__PURE__ */ React.createElement(Box, { width: 9, flexShrink: 0, justifyContent: "flex-end" }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, props.vis ?? "")), /* @__PURE__ */ React.createElement(Box, { width: 5, flexShrink: 0, justifyContent: "flex-end" }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, props.kind ?? "")));
1179
+ }
1180
+ function FieldRow(props) {
1181
+ const { state } = props;
1182
+ const marker = state === "done" ? "\u2713" : state === "active" ? "\u276F" : "\xB7";
1183
+ const markerColor = state === "done" ? OK : state === "active" ? ACCENT : "gray";
1184
+ return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Box, { width: 2, flexShrink: 0 }, /* @__PURE__ */ React.createElement(Text, { color: markerColor }, marker, " ")), /* @__PURE__ */ React.createElement(Box, { width: 11, flexShrink: 0 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, props.label)), /* @__PURE__ */ React.createElement(Box, { flexGrow: 1 }, props.children));
1185
+ }
1186
+ function VisibilityPick(props) {
1187
+ const opt = (v, label) => {
1188
+ const on = props.value === v;
1189
+ const color = props.dim ? void 0 : on ? ACCENT : "gray";
1190
+ return /* @__PURE__ */ React.createElement(Text, { color, dimColor: props.dim }, on ? "(\u2022)" : "( )", " ", label);
1191
+ };
1192
+ return /* @__PURE__ */ React.createElement(Box, null, opt("private", "private"), /* @__PURE__ */ React.createElement(Text, null, " "), opt("public", "public"));
1193
+ }
1194
+ function StatusLine(props) {
1195
+ const { owner, name, visibility, status } = props;
1196
+ if (status === "ok") {
1197
+ return /* @__PURE__ */ React.createElement(Text, { color: OK }, "\u2192 ", owner, "/", name, " \xB7 ", visibility, " \xB7 disponible");
1198
+ }
1199
+ if (status === "taken") {
1200
+ return /* @__PURE__ */ React.createElement(Text, { color: ACCENT }, "\u26A0 ", owner, "/", name, " existe d\xE9j\xE0 \u2014 change le nom pour continuer");
1201
+ }
1202
+ if (status === "reserved") {
1203
+ return /* @__PURE__ */ React.createElement(Text, { color: ACCENT }, "\u26A0 \xAB ", name, " \xBB est un nom r\xE9serv\xE9");
1204
+ }
1205
+ const hint = status === "empty" ? "saisis un nom de repo" : status === "too-short" ? "2 caract\xE8res minimum" : "40 caract\xE8res maximum";
1206
+ return /* @__PURE__ */ React.createElement(Text, { dimColor: true }, hint);
1207
+ }
1208
+
1209
+ // src/scan.ts
1210
+ import { isUtf8 } from "buffer";
1211
+ import { execFile as execFileCb } from "child_process";
1212
+ import { promises as fs3 } from "fs";
1213
+ import path3 from "path";
1214
+ import { promisify } from "util";
1215
+
1216
+ // src/policy.ts
1217
+ var LOC_THRESHOLD = 2500;
1218
+
1219
+ // src/scan.ts
1220
+ var execFile = promisify(execFileCb);
1221
+ function countLines(buf) {
1222
+ let count = 0;
1223
+ for (let i = 0; i < buf.length; i++) {
1224
+ if (buf[i] === 10) count++;
1225
+ }
1226
+ if (buf.length > 0 && buf[buf.length - 1] !== 10) count++;
1227
+ return count;
1228
+ }
1229
+ async function scanFile(absPath) {
1230
+ let buf;
1231
+ try {
1232
+ buf = await fs3.readFile(absPath);
1233
+ } catch {
1234
+ return null;
1235
+ }
1236
+ if (!isUtf8(buf)) return 0;
1237
+ return countLines(buf);
1238
+ }
1239
+ function toEntry(rel, locCount) {
1240
+ return { path: rel, locCount, isLarge: locCount > LOC_THRESHOLD };
1241
+ }
1242
+ async function listTrackedFiles(cwd) {
1243
+ try {
1244
+ const { stdout } = await execFile("git", ["ls-files", "-z"], {
1245
+ cwd,
1246
+ maxBuffer: 50 * 1024 * 1024
1247
+ });
1248
+ return stdout.split("\0").filter((l) => l.length > 0);
1249
+ } catch {
1250
+ return null;
1251
+ }
1252
+ }
1253
+ async function walkAll(absDir, relDir, out) {
1254
+ let dirents;
1255
+ try {
1256
+ dirents = await fs3.readdir(absDir, { withFileTypes: true });
1257
+ } catch {
1258
+ return;
1259
+ }
1260
+ for (const d of dirents) {
1261
+ const relPath = relDir ? `${relDir}/${d.name}` : d.name;
1262
+ const absPath = path3.join(absDir, d.name);
1263
+ if (d.isDirectory()) {
1264
+ if (d.name === ".git") continue;
1265
+ await walkAll(absPath, relPath, out);
1266
+ } else if (d.isFile()) {
1267
+ const loc = await scanFile(absPath);
1268
+ if (loc !== null) out.push(toEntry(relPath, loc));
1269
+ }
1270
+ }
1271
+ }
1272
+ async function scan(cwd) {
1273
+ const tracked = await listTrackedFiles(cwd);
1274
+ if (tracked !== null) {
1275
+ const out2 = [];
1276
+ for (const rel of tracked) {
1277
+ const loc = await scanFile(path3.join(cwd, rel));
1278
+ if (loc !== null) out2.push(toEntry(rel, loc));
1279
+ }
1280
+ return out2;
1281
+ }
1282
+ const out = [];
1283
+ await walkAll(cwd, "", out);
1284
+ return out;
1285
+ }
1286
+
1287
+ // src/baliseignore.ts
1288
+ import { promises as fs4 } from "fs";
1289
+ import path4 from "path";
1290
+ var FILENAME2 = ".baliseignore";
1291
+ var HEADER = [
1292
+ "# Generated by balise init.",
1293
+ "# balise init will overwrite this file on next run.",
1294
+ ""
1295
+ ].join("\n");
1296
+ async function readBaliseignore(cwd) {
1297
+ const file = path4.join(cwd, FILENAME2);
1298
+ let raw;
1299
+ try {
1300
+ raw = await fs4.readFile(file, "utf-8");
1301
+ } catch (err) {
1302
+ if (err.code === "ENOENT") return /* @__PURE__ */ new Set();
1303
+ throw err;
1304
+ }
1305
+ const out = /* @__PURE__ */ new Set();
1306
+ for (const line of raw.split(/\r?\n/)) {
1307
+ const trimmed = line.trim();
1308
+ if (!trimmed || trimmed.startsWith("#")) continue;
1309
+ out.add(trimmed);
1310
+ }
1311
+ return out;
1312
+ }
1313
+ async function writeBaliseignore(cwd, patterns) {
1314
+ const sorted = [...new Set(patterns)].filter((p) => p.length > 0).sort((a, b) => a.localeCompare(b));
1315
+ const body = sorted.length > 0 ? sorted.join("\n") + "\n" : "";
1316
+ await fs4.writeFile(path4.join(cwd, FILENAME2), HEADER + body, "utf-8");
1317
+ }
1318
+
1319
+ // src/ui/IgnoreTree.tsx
1320
+ import React2, { useMemo as useMemo2, useState as useState2 } from "react";
1321
+ import { Box as Box2, Text as Text2, useApp as useApp2, useInput as useInput2 } from "ink";
1322
+ function buildTree(entries) {
1323
+ const root = {
1324
+ path: "",
1325
+ name: ".",
1326
+ isDir: true,
1327
+ locCount: 0,
1328
+ isLarge: false,
1329
+ children: [],
1330
+ parent: null
1331
+ };
1332
+ const dirMap = /* @__PURE__ */ new Map();
1333
+ dirMap.set("", root);
1334
+ const ensureDir = (dirPath) => {
1335
+ if (!dirPath) return root;
1336
+ const existing = dirMap.get(dirPath);
1337
+ if (existing) return existing;
1338
+ const parts = dirPath.split("/");
1339
+ const name = parts[parts.length - 1];
1340
+ const parentPath = parts.slice(0, -1).join("/");
1341
+ const parent = ensureDir(parentPath);
1342
+ const node = {
1343
+ path: dirPath,
1344
+ name,
1345
+ isDir: true,
1346
+ locCount: 0,
1347
+ isLarge: false,
1348
+ children: [],
1349
+ parent
1350
+ };
1351
+ parent.children.push(node);
1352
+ dirMap.set(dirPath, node);
1353
+ return node;
1354
+ };
1355
+ const sorted = [...entries].sort((a, b) => a.path.localeCompare(b.path));
1356
+ for (const e of sorted) {
1357
+ const parts = e.path.split("/");
1358
+ const name = parts[parts.length - 1];
1359
+ const parentPath = parts.slice(0, -1).join("/");
1360
+ const parent = ensureDir(parentPath);
1361
+ const node = {
1362
+ path: e.path,
1363
+ name,
1364
+ isDir: false,
1365
+ locCount: e.locCount,
1366
+ isLarge: e.isLarge,
1367
+ children: [],
1368
+ parent
1369
+ };
1370
+ parent.children.push(node);
1371
+ }
1372
+ const sortChildren = (n) => {
1373
+ n.children.sort((a, b) => {
1374
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
1375
+ return a.name.localeCompare(b.name);
1376
+ });
1377
+ for (const c of n.children) sortChildren(c);
1378
+ };
1379
+ sortChildren(root);
1380
+ return root;
1381
+ }
1382
+ function collectDescendantFiles(node) {
1383
+ const out = [];
1384
+ const recurse = (n) => {
1385
+ if (!n.isDir) out.push(n.path);
1386
+ for (const c of n.children) recurse(c);
1387
+ };
1388
+ recurse(node);
1389
+ return out;
1390
+ }
1391
+ function flattenVisible(root, expanded) {
1392
+ const out = [];
1393
+ const recurse = (node, depth) => {
1394
+ if (node !== root) {
1395
+ out.push({ node, depth });
1396
+ }
1397
+ if (node === root || node.isDir && expanded.has(node.path)) {
1398
+ for (const c of node.children) recurse(c, depth + 1);
1399
+ }
1400
+ };
1401
+ recurse(root, -1);
1402
+ return out;
1403
+ }
1404
+ function dirAllIncluded(node, excluded) {
1405
+ const files = collectDescendantFiles(node);
1406
+ if (files.length === 0) return true;
1407
+ return files.every((f) => !excluded.has(f));
1408
+ }
1409
+ function buildFileList(entries) {
1410
+ return [...entries].sort(
1411
+ (a, b) => b.locCount - a.locCount || a.path.localeCompare(b.path)
1412
+ );
1413
+ }
1414
+ function computeInitialExcluded(scan2, current) {
1415
+ const out = /* @__PURE__ */ new Set();
1416
+ const scanPaths = /* @__PURE__ */ new Set();
1417
+ for (const e of scan2) {
1418
+ scanPaths.add(e.path);
1419
+ if (e.isLarge) out.add(e.path);
1420
+ }
1421
+ for (const p of current) {
1422
+ if (!scanPaths.has(p)) out.add(p);
1423
+ }
1424
+ return out;
1425
+ }
1426
+ function computeDiff(current, excluded) {
1427
+ const all = /* @__PURE__ */ new Set([...current, ...excluded]);
1428
+ return [...all].sort((a, b) => a.localeCompare(b)).map((pattern) => {
1429
+ const inCurrent = current.has(pattern);
1430
+ const inExcluded = excluded.has(pattern);
1431
+ const kind = inCurrent && inExcluded ? "unchanged" : inExcluded ? "added" : "removed";
1432
+ return { pattern, kind };
1433
+ });
1434
+ }
1435
+ var VIEWPORT_HEIGHT = 18;
1436
+ function IgnoreTree(props) {
1437
+ const { exit } = useApp2();
1438
+ const root = useMemo2(() => buildTree(props.scan), [props.scan]);
1439
+ const fileList = useMemo2(() => buildFileList(props.scan), [props.scan]);
1440
+ const [excluded, setExcluded] = useState2(
1441
+ () => computeInitialExcluded(props.scan, props.currentBaliseignore)
1442
+ );
1443
+ const [expandedDirs, setExpandedDirs] = useState2(/* @__PURE__ */ new Set());
1444
+ const [leftView, setLeftView] = useState2("list");
1445
+ const [focus, setFocus] = useState2("left");
1446
+ const [treeCursor, setTreeCursor] = useState2("");
1447
+ const [listCursor, setListCursor] = useState2(0);
1448
+ const [diffCursor, setDiffCursor] = useState2(0);
1449
+ const [modalOpen, setModalOpen] = useState2(false);
1450
+ const visible = useMemo2(
1451
+ () => flattenVisible(root, expandedDirs),
1452
+ [root, expandedDirs]
1453
+ );
1454
+ const diff = useMemo2(
1455
+ () => computeDiff(props.currentBaliseignore, excluded),
1456
+ [props.currentBaliseignore, excluded]
1457
+ );
1458
+ const treeIdx = useMemo2(() => {
1459
+ if (visible.length === 0) return -1;
1460
+ const i = visible.findIndex((v) => v.node.path === treeCursor);
1461
+ return i >= 0 ? i : 0;
1462
+ }, [visible, treeCursor]);
1463
+ const safeListIdx = fileList.length === 0 ? -1 : Math.max(0, Math.min(listCursor, fileList.length - 1));
1464
+ const safeDiffIdx = diff.length === 0 ? -1 : Math.max(0, Math.min(diffCursor, diff.length - 1));
1465
+ const stats = useMemo2(() => {
1466
+ let added = 0;
1467
+ let removed = 0;
1468
+ let unchanged = 0;
1469
+ for (const p of excluded) {
1470
+ if (props.currentBaliseignore.has(p)) unchanged++;
1471
+ else added++;
1472
+ }
1473
+ for (const p of props.currentBaliseignore) {
1474
+ if (!excluded.has(p)) removed++;
1475
+ }
1476
+ return { added, removed, unchanged };
1477
+ }, [excluded, props.currentBaliseignore]);
1478
+ const toggleFile = (path7) => {
1479
+ setExcluded((prev) => {
1480
+ const next = new Set(prev);
1481
+ if (next.has(path7)) next.delete(path7);
1482
+ else next.add(path7);
1483
+ return next;
1484
+ });
1485
+ };
1486
+ const toggleDir = (dirNode) => {
1487
+ const descendants = collectDescendantFiles(dirNode);
1488
+ if (descendants.length === 0) return;
1489
+ const allIncluded = descendants.every((p) => !excluded.has(p));
1490
+ setExcluded((prev) => {
1491
+ const next = new Set(prev);
1492
+ if (allIncluded) {
1493
+ for (const p of descendants) next.add(p);
1494
+ } else {
1495
+ for (const p of descendants) next.delete(p);
1496
+ }
1497
+ return next;
1498
+ });
1499
+ };
1500
+ useInput2((input, key) => {
1501
+ if (modalOpen) {
1502
+ if (input === "y" || input === "Y") {
1503
+ props.onDone({
1504
+ action: "save",
1505
+ patterns: [...excluded].sort((a, b) => a.localeCompare(b))
1506
+ });
1507
+ exit();
1508
+ return;
1509
+ }
1510
+ if (input === "n" || input === "N") {
1511
+ setModalOpen(false);
1512
+ return;
1513
+ }
1514
+ if (input === "q" || input === "Q" || key.escape) {
1515
+ props.onDone({ action: "cancel", patterns: [] });
1516
+ exit();
1517
+ return;
1518
+ }
1519
+ return;
1520
+ }
1521
+ if (key.tab) {
1522
+ setFocus((f) => f === "left" ? "diff" : "left");
1523
+ return;
1524
+ }
1525
+ if ((input === "v" || input === "V") && focus === "left") {
1526
+ setLeftView((vw) => vw === "list" ? "tree" : "list");
1527
+ return;
1528
+ }
1529
+ if (key.return) {
1530
+ setModalOpen(true);
1531
+ return;
1532
+ }
1533
+ if (key.escape || input === "q") {
1534
+ setModalOpen(true);
1535
+ return;
1536
+ }
1537
+ if (focus === "left" && leftView === "list") {
1538
+ if (key.downArrow) {
1539
+ setListCursor((c) => Math.min(c + 1, Math.max(fileList.length - 1, 0)));
1540
+ return;
1541
+ }
1542
+ if (key.upArrow) {
1543
+ setListCursor((c) => Math.max(c - 1, 0));
1544
+ return;
1545
+ }
1546
+ if (input === " ") {
1547
+ if (safeListIdx < 0) return;
1548
+ toggleFile(fileList[safeListIdx].path);
1549
+ return;
1550
+ }
1551
+ } else if (focus === "left") {
1552
+ if (key.downArrow) {
1553
+ const i = Math.min(treeIdx + 1, visible.length - 1);
1554
+ if (i >= 0) setTreeCursor(visible[i].node.path);
1555
+ return;
1556
+ }
1557
+ if (key.upArrow) {
1558
+ const i = Math.max(treeIdx - 1, 0);
1559
+ if (i >= 0) setTreeCursor(visible[i].node.path);
1560
+ return;
1561
+ }
1562
+ if (key.rightArrow) {
1563
+ const cur = treeIdx >= 0 ? visible[treeIdx].node : null;
1564
+ if (!cur || !cur.isDir) return;
1565
+ if (!expandedDirs.has(cur.path)) {
1566
+ setExpandedDirs((s) => {
1567
+ const next = new Set(s);
1568
+ next.add(cur.path);
1569
+ return next;
1570
+ });
1571
+ } else if (cur.children.length > 0) {
1572
+ setTreeCursor(cur.children[0].path);
1573
+ }
1574
+ return;
1575
+ }
1576
+ if (key.leftArrow) {
1577
+ const cur = treeIdx >= 0 ? visible[treeIdx].node : null;
1578
+ if (!cur) return;
1579
+ if (cur.isDir && expandedDirs.has(cur.path)) {
1580
+ setExpandedDirs((s) => {
1581
+ const next = new Set(s);
1582
+ next.delete(cur.path);
1583
+ return next;
1584
+ });
1585
+ } else if (cur.parent && cur.parent.path !== "") {
1586
+ setTreeCursor(cur.parent.path);
1587
+ }
1588
+ return;
1589
+ }
1590
+ if (input === " ") {
1591
+ const cur = treeIdx >= 0 ? visible[treeIdx].node : null;
1592
+ if (!cur) return;
1593
+ if (cur.isDir) toggleDir(cur);
1594
+ else toggleFile(cur.path);
1595
+ return;
1596
+ }
1597
+ } else {
1598
+ if (key.downArrow) {
1599
+ setDiffCursor((c) => Math.min(c + 1, Math.max(diff.length - 1, 0)));
1600
+ return;
1601
+ }
1602
+ if (key.upArrow) {
1603
+ setDiffCursor((c) => Math.max(c - 1, 0));
1604
+ return;
1605
+ }
1606
+ if (input === " ") {
1607
+ if (safeDiffIdx < 0) return;
1608
+ toggleFile(diff[safeDiffIdx].pattern);
1609
+ return;
1610
+ }
991
1611
  }
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")));
1612
+ });
1613
+ if (modalOpen) {
1614
+ return /* @__PURE__ */ React2.createElement(ConfirmModal, { stats, excludedSize: excluded.size });
1615
+ }
1616
+ const treeStart = Math.max(
1617
+ 0,
1618
+ Math.min(
1619
+ treeIdx - Math.floor(VIEWPORT_HEIGHT / 2),
1620
+ visible.length - VIEWPORT_HEIGHT
1621
+ )
1622
+ );
1623
+ const treeWindow = visible.slice(
1624
+ Math.max(treeStart, 0),
1625
+ Math.max(treeStart, 0) + VIEWPORT_HEIGHT
1626
+ );
1627
+ const treeWindowStart = Math.max(treeStart, 0);
1628
+ const listStart = Math.max(
1629
+ 0,
1630
+ Math.min(
1631
+ safeListIdx - Math.floor(VIEWPORT_HEIGHT / 2),
1632
+ fileList.length - VIEWPORT_HEIGHT
1633
+ )
1634
+ );
1635
+ const listWindow = fileList.slice(
1636
+ Math.max(listStart, 0),
1637
+ Math.max(listStart, 0) + VIEWPORT_HEIGHT
1638
+ );
1639
+ const listWindowStart = Math.max(listStart, 0);
1640
+ const diffStart = Math.max(
1641
+ 0,
1642
+ Math.min(
1643
+ safeDiffIdx - Math.floor(VIEWPORT_HEIGHT / 2),
1644
+ diff.length - VIEWPORT_HEIGHT
1645
+ )
1646
+ );
1647
+ const diffWindow = diff.slice(
1648
+ Math.max(diffStart, 0),
1649
+ Math.max(diffStart, 0) + VIEWPORT_HEIGHT
1650
+ );
1651
+ const diffWindowStart = Math.max(diffStart, 0);
1652
+ const leftTitle = leftView === "list" ? "Files \u2014 by LoC" : "Files \u2014 tree";
1653
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true }, "Balise \u2014 configure .baliseignore"), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "Tab: panel \xB7 v: list/tree \xB7 \u2191/\u2193: nav", leftView === "tree" ? " \xB7 \u2192/\u2190: open/close" : "", " \xB7 Space: toggle \xB7 Enter: save \xB7 Esc: cancel"), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(
1654
+ Box2,
1655
+ {
1656
+ flexDirection: "column",
1657
+ width: "50%",
1658
+ paddingRight: 1,
1659
+ borderStyle: "round",
1660
+ borderColor: focus === "left" ? "cyan" : "gray"
1661
+ },
1662
+ /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: focus === "left" ? "cyan" : void 0 }, leftTitle),
1663
+ leftView === "list" ? listWindow.length === 0 ? /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "(no files)") : listWindow.map((e, i) => /* @__PURE__ */ React2.createElement(
1664
+ ListRow,
1665
+ {
1666
+ key: e.path,
1667
+ entry: e,
1668
+ excluded: excluded.has(e.path),
1669
+ isFocused: focus === "left" && listWindowStart + i === safeListIdx
1670
+ }
1671
+ )) : treeWindow.length === 0 ? /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "(no files)") : treeWindow.map((v, i) => /* @__PURE__ */ React2.createElement(
1672
+ TreeRow,
1673
+ {
1674
+ key: v.node.path,
1675
+ node: v.node,
1676
+ depth: v.depth,
1677
+ expanded: expandedDirs.has(v.node.path),
1678
+ excluded,
1679
+ isFocused: focus === "left" && treeWindowStart + i === treeIdx
1680
+ }
1681
+ ))
1682
+ ), /* @__PURE__ */ React2.createElement(
1683
+ Box2,
1684
+ {
1685
+ flexDirection: "column",
1686
+ width: "50%",
1687
+ paddingLeft: 1,
1688
+ borderStyle: "round",
1689
+ borderColor: focus === "diff" ? "cyan" : "gray"
1690
+ },
1691
+ /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: focus === "diff" ? "cyan" : void 0 }, "Diff vs .baliseignore"),
1692
+ diff.length === 0 ? /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "(no changes)") : diffWindow.map((d, i) => /* @__PURE__ */ React2.createElement(
1693
+ DiffRow,
1694
+ {
1695
+ key: d.pattern,
1696
+ entry: d,
1697
+ isFocused: focus === "diff" && diffWindowStart + i === safeDiffIdx
1698
+ }
1699
+ ))
1700
+ )), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, stats.added > 0 || stats.removed > 0 ? `${stats.added} additions, ${stats.removed} removals` : "(no changes vs current .baliseignore)")));
1701
+ }
1702
+ function ListRow(props) {
1703
+ const { entry, excluded, isFocused } = props;
1704
+ const box = excluded ? "\u2610" : "\u2611";
1705
+ const loc = entry.locCount > 0 ? ` (${entry.locCount} LoC)` : "";
1706
+ const large = entry.isLarge ? " [large]" : "";
1707
+ return /* @__PURE__ */ React2.createElement(Text2, { color: isFocused ? "cyan" : void 0 }, isFocused ? "\u25B8 " : " ", box, " ", entry.path, loc, large);
1708
+ }
1709
+ function TreeRow(props) {
1710
+ const { node, depth, expanded, excluded, isFocused } = props;
1711
+ const indent = " ".repeat(Math.max(depth, 0));
1712
+ let label;
1713
+ if (node.isDir) {
1714
+ const allIncluded = dirAllIncluded(node, excluded);
1715
+ const arrow = expanded ? "\u25BE" : "\u25B8";
1716
+ const box = allIncluded ? "\u2611" : "\u2610";
1717
+ label = `${arrow} ${box} ${node.name}/`;
1718
+ } else {
1719
+ const box = excluded.has(node.path) ? "\u2610" : "\u2611";
1720
+ const loc = node.locCount > 0 ? ` (${node.locCount} LoC)` : "";
1721
+ const large = node.isLarge ? " [large]" : "";
1722
+ label = `${box} ${node.name}${loc}${large}`;
1723
+ }
1724
+ return /* @__PURE__ */ React2.createElement(Text2, { color: isFocused ? "cyan" : void 0 }, isFocused ? "\u25B8 " : " ", indent, label);
1725
+ }
1726
+ function DiffRow(props) {
1727
+ const { entry, isFocused } = props;
1728
+ const sigil = entry.kind === "added" ? "+" : entry.kind === "removed" ? "-" : " ";
1729
+ const color = isFocused ? "cyan" : entry.kind === "added" ? "green" : entry.kind === "removed" ? "red" : void 0;
1730
+ return /* @__PURE__ */ React2.createElement(Text2, { color }, isFocused ? "\u25B8 " : " ", sigil, " ", entry.pattern);
1731
+ }
1732
+ function ConfirmModal(props) {
1733
+ const { stats, excludedSize } = props;
1734
+ return /* @__PURE__ */ React2.createElement(
1735
+ Box2,
1736
+ {
1737
+ flexDirection: "column",
1738
+ padding: 1,
1739
+ borderStyle: "double",
1740
+ borderColor: "cyan"
1741
+ },
1742
+ /* @__PURE__ */ React2.createElement(Text2, { bold: true }, "Save changes to .baliseignore?"),
1743
+ /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1, flexDirection: "row", gap: 2 }, /* @__PURE__ */ React2.createElement(Text2, { color: "green" }, "+", stats.added, " added"), /* @__PURE__ */ React2.createElement(Text2, { color: "red" }, "-", stats.removed, " removed"), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, stats.unchanged, " unchanged")),
1744
+ /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, excludedSize, " total patterns in resulting file")),
1745
+ /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, null, "[y] Confirm [n] Back to edit [q/Esc] Cancel without saving"))
1746
+ );
993
1747
  }
994
1748
 
995
1749
  // src/commands/init.ts
@@ -1015,6 +1769,13 @@ async function runInit(opts) {
1015
1769
  }
1016
1770
  throw err;
1017
1771
  }
1772
+ const existingConfig = await readConfig(cwd);
1773
+ if (existingConfig) {
1774
+ process.stdout.write(
1775
+ `Re-configuring ${existingConfig.repo.owner_login}/${existingConfig.repo.slug}.
1776
+ `
1777
+ );
1778
+ }
1018
1779
  const apiUrl = DEFAULT_API_URL;
1019
1780
  const client = new ApiClient({
1020
1781
  apiUrl,
@@ -1043,13 +1804,14 @@ async function runInit(opts) {
1043
1804
  const sortedRepos = [...repos].sort(
1044
1805
  (a, b) => `${a.owner_login}/${a.slug}`.localeCompare(`${b.owner_login}/${b.slug}`)
1045
1806
  );
1046
- const defaultSlug = path3.basename(cwd).toLowerCase();
1807
+ const defaultSlug = path5.basename(cwd).toLowerCase();
1047
1808
  const result = await new Promise((resolve) => {
1048
1809
  const app = render(
1049
- React2.createElement(InitPicker, {
1810
+ React3.createElement(InitPicker, {
1050
1811
  defaultSlug,
1051
1812
  ownerships,
1052
1813
  repos: sortedRepos,
1814
+ currentRepoId: existingConfig?.repo.id,
1053
1815
  onDone: (r) => {
1054
1816
  resolve(r);
1055
1817
  app.unmount();
@@ -1065,7 +1827,8 @@ async function runInit(opts) {
1065
1827
  if (result.action === "create") {
1066
1828
  const created = await client.postJson("/v1/repos", {
1067
1829
  owner_id: result.owner.id,
1068
- slug: result.slug
1830
+ slug: result.slug,
1831
+ visibility: result.visibility
1069
1832
  });
1070
1833
  cfg = {
1071
1834
  repo: {
@@ -1091,23 +1854,51 @@ async function runInit(opts) {
1091
1854
  `Linked ${cfg.repo.owner_login}/${cfg.repo.slug} \u2192 .balise/config
1092
1855
  `
1093
1856
  );
1857
+ const [scanResult, currentBaliseignore] = await Promise.all([
1858
+ scan(cwd),
1859
+ readBaliseignore(cwd)
1860
+ ]);
1861
+ const treeResult = await new Promise((resolve) => {
1862
+ const app = render(
1863
+ React3.createElement(IgnoreTree, {
1864
+ scan: scanResult,
1865
+ currentBaliseignore,
1866
+ onDone: (r) => {
1867
+ resolve(r);
1868
+ app.unmount();
1869
+ }
1870
+ })
1871
+ );
1872
+ });
1873
+ if (treeResult.action === "cancel") {
1874
+ process.stderr.write(
1875
+ "Ignore configuration cancelled. .baliseignore unchanged.\n"
1876
+ );
1877
+ process.exit(1);
1878
+ }
1879
+ await writeBaliseignore(cwd, treeResult.patterns);
1880
+ process.stdout.write(
1881
+ `Wrote ${treeResult.patterns.length} patterns to .baliseignore
1882
+ `
1883
+ );
1884
+ 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'
1886
+ );
1094
1887
  }
1095
1888
 
1096
1889
  // src/commands/sync.ts
1097
1890
  import readline from "readline/promises";
1098
- import React4 from "react";
1099
- import { render as render2 } from "ink";
1100
1891
 
1101
1892
  // src/logger.ts
1102
- import { promises as fs3 } from "fs";
1103
- import path4 from "path";
1893
+ import { promises as fs5 } from "fs";
1894
+ import path6 from "path";
1104
1895
  import os2 from "os";
1105
1896
  var APP_DIR2 = ".balise";
1106
- var FILENAME2 = "balise.log";
1897
+ var FILENAME3 = "balise.log";
1107
1898
  function logPath() {
1108
1899
  const override = process.env.BALISE_LOG_FILE;
1109
1900
  if (override && override.length > 0) return override;
1110
- return path4.join(os2.homedir(), APP_DIR2, FILENAME2);
1901
+ return path6.join(os2.homedir(), APP_DIR2, FILENAME3);
1111
1902
  }
1112
1903
  function formatError(err) {
1113
1904
  if (err instanceof Error) {
@@ -1128,152 +1919,76 @@ ${extras.join("\n")}` : stack;
1128
1919
  async function logError(context, err) {
1129
1920
  try {
1130
1921
  const p = logPath();
1131
- await fs3.mkdir(path4.dirname(p), { recursive: true, mode: 448 });
1922
+ await fs5.mkdir(path6.dirname(p), { recursive: true, mode: 448 });
1132
1923
  const ts = (/* @__PURE__ */ new Date()).toISOString();
1133
1924
  const line = `[${ts}] ERROR ${context}
1134
1925
  ${formatError(err)}
1135
1926
 
1136
1927
  `;
1137
- await fs3.appendFile(p, line, { mode: 384 });
1928
+ await fs5.appendFile(p, line, { mode: 384 });
1138
1929
  } catch {
1139
1930
  }
1140
1931
  }
1141
1932
 
1142
- // src/ui/SyncProgress.tsx
1143
- import React3, { useEffect, useState as useState2 } from "react";
1144
- import { Box as Box2, Text as Text2, useApp as useApp2 } from "ink";
1145
- import { Spinner } from "@inkjs/ui";
1146
- var QUEUED_MESSAGES = [
1147
- "Waiting for an agent willing to accept the job\u2026",
1148
- "Your request is in line. An agent will be assigned once one stops pretending to be busy.",
1149
- 'Looking for an available agent. Most are currently "in a meeting."',
1150
- "Queued. An agent will pick this up as soon as they finish rereading the spec.",
1151
- "Your task is waiting for an agent with the right vibes.",
1152
- "Negotiating with an agent. They want a raise.",
1153
- "All agents are currently on strike. Sending in a scab.",
1154
- "Waiting for an agent to finish their coffee \u2615",
1155
- "An agent saw your task and walked away slowly. Finding another one.",
1156
- "Your task is being passed around like a hot potato. Someone will catch it eventually.",
1157
- "Agents are currently arguing about who has to do this one.",
1158
- "Waiting for an agent brave enough to open your repo.",
1159
- "Your task is sitting in the agent break room. Someone will notice it soon.",
1160
- "An agent was assigned, but they ghosted. Recruiting a replacement.",
1161
- "Paging an agent. Please hold while we bribe one.",
1162
- "Queued. Waiting for an agent with enough context window to care.",
1163
- "An agent is spinning up. They're doing their stretches.",
1164
- "Looking for an agent whose system prompt allows this.",
1165
- "Waiting for a worker. Their last task traumatized them.",
1166
- "Your task is in queue. Agents are busy arguing about tabs vs spaces.",
1167
- "Finding an agent\u2026",
1168
- "Found one. They said no.",
1169
- "Finding another agent\u2026",
1170
- "This one looks promising.",
1171
- "Negotiating\u2026"
1172
- ];
1173
- var RUNNING_MESSAGES = [
1174
- "An agent is on it. Try not to watch.",
1175
- "Your task has an owner. They seem focused.",
1176
- "An agent accepted the job. Surprisingly.",
1177
- "Working. The agent asked not to be disturbed.",
1178
- "An agent is handling it. They'll let us know if they need anything.",
1179
- "Progress is happening. Allegedly.",
1180
- "An agent rolled up their sleeves. Mostly for show.",
1181
- "Hard at work. The agent has opened seventeen browser tabs.",
1182
- "Your agent is locked in. Do not make eye contact.",
1183
- "An agent is typing furiously. Some of it might be relevant.",
1184
- "Working. The agent has entered the zone. And also a Wikipedia rabbit hole.",
1185
- "An agent is deep in thought. Or buffering. Hard to tell.",
1186
- "Your task is being handled. The agent is muttering to themselves.",
1187
- "Cooking. \u{1F9D1}\u200D\u{1F373}",
1188
- "The agent is working. They've asked for snacks.",
1189
- "An agent is doing the thing. Please clap.",
1190
- "Agent is crunching tokens.",
1191
- "Working. The agent is reasoning about your reasoning.",
1192
- "An agent is in a tool-use loop. Going well so far.",
1193
- "Thinking hard. The agent just discovered your codebase has opinions.",
1194
- "Working. The agent is negotiating with your linter.",
1195
- "An agent is running. They promise it's not an infinite loop.",
1196
- "The agent is writing, deleting, and rewriting. Classic.",
1197
- "An agent picked it up.",
1198
- "They're reading the task\u2026",
1199
- "Looks like they have a plan.",
1200
- "Executing\u2026",
1201
- "Still going. Confidently.",
1202
- "Almost there. (They always say that.)"
1203
- ];
1204
- function pickInitialIndex(len) {
1205
- return Math.floor(Math.random() * len);
1206
- }
1207
- function SyncProgress(props) {
1208
- const { exit } = useApp2();
1209
- const [status, setStatus] = useState2(null);
1210
- const [error, setError] = useState2(null);
1211
- const [startedAt] = useState2(() => Date.now());
1212
- const [queuedIdx, setQueuedIdx] = useState2(
1213
- () => pickInitialIndex(QUEUED_MESSAGES.length)
1214
- );
1215
- const [runningIdx, setRunningIdx] = useState2(
1216
- () => pickInitialIndex(RUNNING_MESSAGES.length)
1217
- );
1218
- useEffect(() => {
1219
- let cancelled = false;
1220
- const tick = async () => {
1221
- try {
1222
- const s = await props.client.getJson(
1223
- `/v1/syncs/${props.syncId}`
1224
- );
1225
- if (cancelled) return;
1226
- setStatus(s);
1227
- if (s.status === "done") {
1228
- props.onDone(true);
1229
- exit();
1230
- return;
1231
- }
1232
- if (s.status === "failed") {
1233
- props.onDone(false);
1234
- exit();
1235
- return;
1933
+ // src/ui/SyncDashboard.tsx
1934
+ import React4 from "react";
1935
+ import { Box as Box3, render as render2, Text as Text3, useApp as useApp3, useInput as useInput3 } from "ink";
1936
+
1937
+ // src/format.ts
1938
+ function fmtLoc(n) {
1939
+ return n >= 1e3 ? `${(n / 1e3).toFixed(1).replace(/\.0$/, "")}k` : String(n);
1940
+ }
1941
+ var CREDIT_FMT = new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 });
1942
+ function fmtCredits(micro) {
1943
+ return CREDIT_FMT.format(Math.floor(micro / 1e6));
1944
+ }
1945
+ function fmtEstimate(micro) {
1946
+ if (micro > 0 && micro < 1e6) return "<1";
1947
+ return fmtCredits(micro);
1948
+ }
1949
+
1950
+ // src/ui/SyncDashboard.tsx
1951
+ function Row2(props) {
1952
+ return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "row" }, /* @__PURE__ */ React4.createElement(Box3, { width: 8, flexShrink: 0 }, /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, props.label)), /* @__PURE__ */ React4.createElement(Text3, null, props.children));
1953
+ }
1954
+ function describeBase(body) {
1955
+ if (body.would_be_cold_start) {
1956
+ return `${body.base_dolt_ref} \xB7 cold-start (no prior sync)`;
1957
+ }
1958
+ const dist = body.from_commit_distance !== null ? ` \xB7 ${body.from_commit_distance} commit${body.from_commit_distance === 1 ? "" : "s"} ahead` : "";
1959
+ return `${body.base_dolt_ref} \xB7 ${body.base_kind}${dist}`;
1960
+ }
1961
+ function SyncDashboard(props) {
1962
+ const { exit } = useApp3();
1963
+ const { body } = props;
1964
+ useInput3((input, key) => {
1965
+ if (key.return) {
1966
+ props.onDone(true);
1967
+ exit();
1968
+ return;
1969
+ }
1970
+ if (key.escape || input === "q" || input === "Q" || key.ctrl && input === "c") {
1971
+ props.onDone(false);
1972
+ exit();
1973
+ return;
1974
+ }
1975
+ });
1976
+ const loc = body.loc !== null ? `${fmtLoc(body.loc)} LOC` : "n/a";
1977
+ const cost = `~${fmtEstimate(body.estimate)} cr`;
1978
+ return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", paddingX: 1, borderStyle: "round", borderColor: "cyan" }, /* @__PURE__ */ React4.createElement(Text3, { bold: true }, "balise sync \xB7 ", props.owner, "/", props.slug), /* @__PURE__ */ React4.createElement(Box3, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React4.createElement(Row2, { label: "commit" }, props.commitSha.slice(0, 7), props.branch ? /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, " \xB7 ", props.branch) : null), /* @__PURE__ */ React4.createElement(Row2, { label: "base" }, describeBase(body)), /* @__PURE__ */ React4.createElement(Row2, { label: "diff" }, /* @__PURE__ */ React4.createElement(Text3, { bold: true }, loc)), /* @__PURE__ */ React4.createElement(Row2, { label: "cost" }, /* @__PURE__ */ React4.createElement(Text3, { bold: true }, cost))), props.dirty ? /* @__PURE__ */ React4.createElement(Box3, { marginTop: 1 }, /* @__PURE__ */ React4.createElement(Text3, { color: "yellow" }, "\u26A0 working tree dirty \u2014 syncing HEAD")) : null, /* @__PURE__ */ React4.createElement(Box3, { marginTop: 1 }, /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, "[Enter] sync \xB7 [Esc] cancel")));
1979
+ }
1980
+ function runSyncDashboard(props) {
1981
+ return new Promise((resolve) => {
1982
+ const app = render2(
1983
+ React4.createElement(SyncDashboard, {
1984
+ ...props,
1985
+ onDone: (confirmed) => {
1986
+ resolve(confirmed);
1987
+ app.unmount();
1236
1988
  }
1237
- } catch (err) {
1238
- if (cancelled) return;
1239
- setError(err.message);
1240
- props.onDone(false);
1241
- exit();
1242
- return;
1243
- }
1244
- setTimeout(tick, props.pollIntervalMs ?? 1e3);
1245
- };
1246
- void tick();
1247
- return () => {
1248
- cancelled = true;
1249
- };
1250
- }, []);
1251
- useEffect(() => {
1252
- const period = props.messageRotationMs ?? 1e4;
1253
- const h = setInterval(() => {
1254
- setQueuedIdx((i) => (i + 1) % QUEUED_MESSAGES.length);
1255
- setRunningIdx((i) => (i + 1) % RUNNING_MESSAGES.length);
1256
- }, period);
1257
- return () => clearInterval(h);
1258
- }, [props.messageRotationMs]);
1259
- if (error) {
1260
- return /* @__PURE__ */ React3.createElement(Text2, { color: "red" }, "\u2717 Failed");
1261
- }
1262
- if (!status) {
1263
- return /* @__PURE__ */ React3.createElement(Spinner, { label: "Getting things ready\u2026" });
1264
- }
1265
- if (status.status === "done") {
1266
- return /* @__PURE__ */ React3.createElement(Text2, { color: "green" }, "\u2713 Done");
1267
- }
1268
- if (status.status === "failed") {
1269
- return /* @__PURE__ */ React3.createElement(Text2, { color: "red" }, "\u2717 Failed");
1270
- }
1271
- const message = status.status === "queued" ? QUEUED_MESSAGES[queuedIdx] : RUNNING_MESSAGES[runningIdx];
1272
- const { files_processed, files_total, nodes_pushed } = status.progress;
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);
1989
+ })
1990
+ );
1991
+ });
1277
1992
  }
1278
1993
 
1279
1994
  // src/commands/sync.ts
@@ -1293,6 +2008,9 @@ function drainStdinBuffer(stdin) {
1293
2008
  while (s.read() !== null) {
1294
2009
  }
1295
2010
  }
2011
+ function isInteractive2() {
2012
+ return Boolean(process.stdin.isTTY);
2013
+ }
1296
2014
  async function confirmRetryWithForce(opts) {
1297
2015
  const stderr = opts.stderr ?? process.stderr;
1298
2016
  const synced = opts.body.synced_at ?? "unknown time";
@@ -1319,7 +2037,18 @@ async function confirmRetryWithForce(opts) {
1319
2037
  }
1320
2038
  function formatDryRunSummary(body) {
1321
2039
  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}`;
2040
+ const loc = body.loc !== null ? `${body.loc} LOC` : "LOC n/a";
2041
+ return `Sync would use: base=${body.base_dolt_ref} (${body.base_kind}) / ${fromDiff} \xB7 ${loc} \xB7 ~${fmtEstimate(body.estimate)} cr`;
2042
+ }
2043
+ function resolveWebUrl(webUrl, frontUrl) {
2044
+ try {
2045
+ return new URL(webUrl, frontUrl).toString();
2046
+ } catch {
2047
+ return webUrl;
2048
+ }
2049
+ }
2050
+ function formatAcceptedSummary(body, frontUrl) {
2051
+ return `Sync queued. Track it here: ${resolveWebUrl(body.web_url, frontUrl)}`;
1323
2052
  }
1324
2053
  async function runSync(opts) {
1325
2054
  try {
@@ -1372,11 +2101,7 @@ async function runSyncInner(opts) {
1372
2101
  process.exit(1);
1373
2102
  }
1374
2103
  }
1375
- if (await isDirty({ cwd })) {
1376
- process.stderr.write(
1377
- "Warning: working tree has uncommitted changes \u2014 syncing HEAD anyway.\n"
1378
- );
1379
- }
2104
+ const dirty = await isDirty({ cwd });
1380
2105
  const client = new ApiClient({
1381
2106
  apiUrl: cfg.api.url,
1382
2107
  supabaseUrl: opts.supabaseUrl
@@ -1385,65 +2110,117 @@ async function runSyncInner(opts) {
1385
2110
  `Packing ${cfg.repo.owner_login}/${cfg.repo.slug} at HEAD\u2026
1386
2111
  `
1387
2112
  );
1388
- const submit = async (force) => {
2113
+ const submit = async (force, dryRun) => {
1389
2114
  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);
2115
+ const query = buildSyncQuery({ commitSha, branch, force, base: opts.base, dryRun });
2116
+ const outcome = await uploadOnce(
2117
+ client,
2118
+ cfg.repo.owner_login,
2119
+ cfg.repo.slug,
2120
+ stream,
2121
+ query
2122
+ );
2123
+ return { outcome, commitSha, branch };
1398
2124
  };
1399
- let outcome;
2125
+ const finishReal = (outcome) => {
2126
+ if (outcome.kind === "invalid_base") {
2127
+ process.stderr.write(`invalid base ref: ${opts.base}
2128
+ `);
2129
+ process.exit(1);
2130
+ }
2131
+ if (outcome.kind === "accepted") {
2132
+ process.stdout.write(
2133
+ formatAcceptedSummary(outcome.body, opts.frontUrl) + "\n"
2134
+ );
2135
+ process.exit(0);
2136
+ }
2137
+ process.exit(1);
2138
+ };
2139
+ if (opts.autoConfirm) {
2140
+ if (dirty) {
2141
+ process.stderr.write(
2142
+ "Warning: working tree has uncommitted changes \u2014 syncing HEAD anyway.\n"
2143
+ );
2144
+ }
2145
+ let outcome;
2146
+ try {
2147
+ outcome = (await submit(opts.force ?? false, false)).outcome;
2148
+ } catch (err) {
2149
+ handleUploadError(err);
2150
+ process.exit(1);
2151
+ }
2152
+ if (outcome.kind === "conflict") {
2153
+ const proceed = await confirmRetryWithForce({
2154
+ body: outcome.body,
2155
+ autoConfirm: true
2156
+ });
2157
+ if (!proceed) process.exit(1);
2158
+ try {
2159
+ outcome = (await submit(true, false)).outcome;
2160
+ } catch (err) {
2161
+ handleUploadError(err);
2162
+ process.exit(1);
2163
+ }
2164
+ }
2165
+ finishReal(outcome);
2166
+ }
2167
+ let dry;
1400
2168
  try {
1401
- outcome = await submit(opts.force ?? false);
2169
+ dry = await submit(opts.force ?? false, true);
1402
2170
  } catch (err) {
1403
2171
  handleUploadError(err);
1404
2172
  process.exit(1);
1405
2173
  }
1406
- if (outcome.kind === "conflict") {
1407
- const proceedForce = await confirmRetryWithForce({
1408
- body: outcome.body,
1409
- autoConfirm: opts.autoConfirm ?? false
2174
+ if (dry.outcome.kind === "conflict") {
2175
+ const proceed = await confirmRetryWithForce({
2176
+ body: dry.outcome.body,
2177
+ autoConfirm: false
1410
2178
  });
1411
- if (!proceedForce) {
1412
- process.exit(1);
1413
- }
2179
+ if (!proceed) process.exit(1);
2180
+ let forced;
1414
2181
  try {
1415
- outcome = await submit(true);
2182
+ forced = (await submit(true, false)).outcome;
1416
2183
  } catch (err) {
1417
2184
  handleUploadError(err);
1418
2185
  process.exit(1);
1419
2186
  }
2187
+ finishReal(forced);
1420
2188
  }
1421
- if (outcome.kind === "invalid_base") {
2189
+ if (dry.outcome.kind === "invalid_base") {
1422
2190
  process.stderr.write(`invalid base ref: ${opts.base}
1423
2191
  `);
1424
2192
  process.exit(1);
1425
2193
  }
1426
- if (outcome.kind === "dry_run") {
1427
- process.stdout.write(formatDryRunSummary(outcome.body) + "\n");
1428
- process.exit(0);
1429
- }
1430
- if (outcome.kind !== "accepted") {
2194
+ if (dry.outcome.kind !== "dry_run") {
1431
2195
  process.exit(1);
1432
2196
  }
1433
- const accepted = outcome.body;
1434
- const result = await new Promise((resolve) => {
1435
- const app = render2(
1436
- React4.createElement(SyncProgress, {
1437
- client,
1438
- syncId: accepted.sync_id,
1439
- onDone: (ok) => {
1440
- resolve(ok);
1441
- app.unmount();
1442
- }
1443
- })
2197
+ if (!isInteractive2()) {
2198
+ process.stdout.write(formatDryRunSummary(dry.outcome.body) + "\n");
2199
+ process.stderr.write(
2200
+ "Non-interactive terminal \u2014 re-run with -y to confirm and sync.\n"
1444
2201
  );
2202
+ process.exit(1);
2203
+ }
2204
+ const confirmed = await runSyncDashboard({
2205
+ owner: cfg.repo.owner_login,
2206
+ slug: cfg.repo.slug,
2207
+ commitSha: dry.commitSha,
2208
+ branch: dry.branch,
2209
+ dirty,
2210
+ body: dry.outcome.body
1445
2211
  });
1446
- process.exit(result ? 0 : 1);
2212
+ if (!confirmed) {
2213
+ process.stderr.write("Sync cancelled.\n");
2214
+ process.exit(0);
2215
+ }
2216
+ let real;
2217
+ try {
2218
+ real = (await submit(opts.force ?? false, false)).outcome;
2219
+ } catch (err) {
2220
+ handleUploadError(err);
2221
+ process.exit(1);
2222
+ }
2223
+ finishReal(real);
1447
2224
  }
1448
2225
  async function uploadOnce(client, owner, slug, stream, query) {
1449
2226
  const raw = await client.uploadBundleRaw(
@@ -1506,6 +2283,7 @@ async function withLog(name, fn) {
1506
2283
  }
1507
2284
  }
1508
2285
  var SUPABASE_URL = process.env.BALISE_SUPABASE_URL ?? "https://api.balise.dev";
2286
+ var FRONT_URL = process.env.BALISE_FRONT_URL ?? "https://balise.dev";
1509
2287
  var loginCmd = defineCommand({
1510
2288
  meta: { name: "login", description: "Authenticate via OAuth (PKCE loopback)." },
1511
2289
  async run() {
@@ -1542,7 +2320,7 @@ var syncCmd = defineCommand({
1542
2320
  yes: {
1543
2321
  type: "boolean",
1544
2322
  alias: "y",
1545
- description: "Skip sync confirmations (warning prompt + future 409 retry).",
2323
+ description: "Auto-validate: skip the pre-run dashboard and sync directly.",
1546
2324
  default: false
1547
2325
  },
1548
2326
  force: {
@@ -1553,20 +2331,15 @@ var syncCmd = defineCommand({
1553
2331
  base: {
1554
2332
  type: "string",
1555
2333
  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
2334
  }
1562
2335
  },
1563
2336
  async run({ args }) {
1564
2337
  await runSync({
1565
2338
  supabaseUrl: SUPABASE_URL,
2339
+ frontUrl: FRONT_URL,
1566
2340
  autoConfirm: Boolean(args.yes),
1567
2341
  force: Boolean(args.force),
1568
- base: typeof args.base === "string" && args.base.length > 0 ? args.base : void 0,
1569
- dryRun: Boolean(args["dry-run"])
2342
+ base: typeof args.base === "string" && args.base.length > 0 ? args.base : void 0
1570
2343
  });
1571
2344
  }
1572
2345
  });