@balise.dev/cli 0.2.0 → 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.
- package/dist/index.js +547 -206
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -913,84 +913,297 @@ ${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
|
-
|
|
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-
|
|
923
|
+
return raw.toLowerCase().replace(/[^a-z0-9_-]/g, "");
|
|
921
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";
|
|
934
|
+
}
|
|
935
|
+
var FILTER_CHAR = /^[\w./-]$/;
|
|
922
936
|
function InitPicker(props) {
|
|
923
937
|
const { exit } = useApp();
|
|
924
|
-
const
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
);
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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;
|
|
942
1030
|
}
|
|
943
|
-
exit();
|
|
944
|
-
};
|
|
945
|
-
useInput((_input, key) => {
|
|
946
1031
|
if (key.escape) {
|
|
947
|
-
|
|
948
|
-
|
|
1032
|
+
if (field === "visibility") setField("name");
|
|
1033
|
+
else setMode("owner");
|
|
949
1034
|
return;
|
|
950
1035
|
}
|
|
951
|
-
if (
|
|
952
|
-
|
|
953
|
-
if (
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
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");
|
|
957
1057
|
return;
|
|
958
1058
|
}
|
|
959
1059
|
if (key.return) {
|
|
960
|
-
|
|
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();
|
|
961
1071
|
}
|
|
962
1072
|
});
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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,
|
|
973
1090
|
{
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1091
|
+
repos: visibleRepos,
|
|
1092
|
+
total: repos.length,
|
|
1093
|
+
filter,
|
|
1094
|
+
cursor: safeCursor,
|
|
1095
|
+
currentRepoId: props.currentRepoId
|
|
978
1096
|
}
|
|
979
|
-
)
|
|
980
|
-
|
|
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,
|
|
981
1112
|
{
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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
|
|
985
1119
|
}
|
|
986
|
-
)
|
|
987
|
-
|
|
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,
|
|
988
1149
|
{
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
|
992
1173
|
}
|
|
993
|
-
)
|
|
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);
|
|
994
1207
|
}
|
|
995
1208
|
|
|
996
1209
|
// src/scan.ts
|
|
@@ -999,47 +1212,12 @@ import { execFile as execFileCb } from "child_process";
|
|
|
999
1212
|
import { promises as fs3 } from "fs";
|
|
1000
1213
|
import path3 from "path";
|
|
1001
1214
|
import { promisify } from "util";
|
|
1002
|
-
import picomatch from "picomatch";
|
|
1003
1215
|
|
|
1004
1216
|
// src/policy.ts
|
|
1005
|
-
var BAN_PATTERNS = [
|
|
1006
|
-
".git",
|
|
1007
|
-
"node_modules",
|
|
1008
|
-
"__pycache__",
|
|
1009
|
-
".venv",
|
|
1010
|
-
"venv",
|
|
1011
|
-
"dist",
|
|
1012
|
-
"build",
|
|
1013
|
-
".next",
|
|
1014
|
-
".turbo",
|
|
1015
|
-
".cache",
|
|
1016
|
-
"target"
|
|
1017
|
-
];
|
|
1018
|
-
var DEFAULT_PATTERNS = [
|
|
1019
|
-
"*.lock",
|
|
1020
|
-
"package-lock.json",
|
|
1021
|
-
"yarn.lock",
|
|
1022
|
-
"pnpm-lock.yaml",
|
|
1023
|
-
"*.min.js",
|
|
1024
|
-
"*.min.css",
|
|
1025
|
-
"*.svg",
|
|
1026
|
-
"*.png",
|
|
1027
|
-
"*.jpg",
|
|
1028
|
-
"*.jpeg",
|
|
1029
|
-
"*.gif",
|
|
1030
|
-
"*.ico",
|
|
1031
|
-
"*.woff",
|
|
1032
|
-
"*.woff2",
|
|
1033
|
-
"*.snap",
|
|
1034
|
-
"CHANGELOG.md"
|
|
1035
|
-
];
|
|
1036
1217
|
var LOC_THRESHOLD = 2500;
|
|
1037
|
-
var MAX_FILE_BYTES = 1e6;
|
|
1038
1218
|
|
|
1039
1219
|
// src/scan.ts
|
|
1040
1220
|
var execFile = promisify(execFileCb);
|
|
1041
|
-
var BAN_DIRS = new Set(BAN_PATTERNS);
|
|
1042
|
-
var matchesDefault = DEFAULT_PATTERNS.length > 0 ? picomatch([...DEFAULT_PATTERNS], { dot: true, matchBase: true }) : () => false;
|
|
1043
1221
|
function countLines(buf) {
|
|
1044
1222
|
let count = 0;
|
|
1045
1223
|
for (let i = 0; i < buf.length; i++) {
|
|
@@ -1049,29 +1227,30 @@ function countLines(buf) {
|
|
|
1049
1227
|
return count;
|
|
1050
1228
|
}
|
|
1051
1229
|
async function scanFile(absPath) {
|
|
1052
|
-
let
|
|
1230
|
+
let buf;
|
|
1053
1231
|
try {
|
|
1054
|
-
|
|
1232
|
+
buf = await fs3.readFile(absPath);
|
|
1055
1233
|
} catch {
|
|
1056
1234
|
return null;
|
|
1057
1235
|
}
|
|
1058
|
-
if (
|
|
1059
|
-
const buf = await fs3.readFile(absPath);
|
|
1060
|
-
if (!isUtf8(buf)) return null;
|
|
1236
|
+
if (!isUtf8(buf)) return 0;
|
|
1061
1237
|
return countLines(buf);
|
|
1062
1238
|
}
|
|
1239
|
+
function toEntry(rel, locCount) {
|
|
1240
|
+
return { path: rel, locCount, isLarge: locCount > LOC_THRESHOLD };
|
|
1241
|
+
}
|
|
1063
1242
|
async function listTrackedFiles(cwd) {
|
|
1064
1243
|
try {
|
|
1065
1244
|
const { stdout } = await execFile("git", ["ls-files", "-z"], {
|
|
1066
1245
|
cwd,
|
|
1067
1246
|
maxBuffer: 50 * 1024 * 1024
|
|
1068
1247
|
});
|
|
1069
|
-
return
|
|
1248
|
+
return stdout.split("\0").filter((l) => l.length > 0);
|
|
1070
1249
|
} catch {
|
|
1071
1250
|
return null;
|
|
1072
1251
|
}
|
|
1073
1252
|
}
|
|
1074
|
-
async function
|
|
1253
|
+
async function walkAll(absDir, relDir, out) {
|
|
1075
1254
|
let dirents;
|
|
1076
1255
|
try {
|
|
1077
1256
|
dirents = await fs3.readdir(absDir, { withFileTypes: true });
|
|
@@ -1082,35 +1261,26 @@ async function walk(absDir, relDir, out, tracked) {
|
|
|
1082
1261
|
const relPath = relDir ? `${relDir}/${d.name}` : d.name;
|
|
1083
1262
|
const absPath = path3.join(absDir, d.name);
|
|
1084
1263
|
if (d.isDirectory()) {
|
|
1085
|
-
if (
|
|
1086
|
-
|
|
1087
|
-
path: relPath,
|
|
1088
|
-
locCount: 0,
|
|
1089
|
-
isBan: true,
|
|
1090
|
-
isDefault: false,
|
|
1091
|
-
isLarge: false
|
|
1092
|
-
});
|
|
1093
|
-
continue;
|
|
1094
|
-
}
|
|
1095
|
-
await walk(absPath, relPath, out, tracked);
|
|
1264
|
+
if (d.name === ".git") continue;
|
|
1265
|
+
await walkAll(absPath, relPath, out);
|
|
1096
1266
|
} else if (d.isFile()) {
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
if (locCount === null) continue;
|
|
1100
|
-
out.push({
|
|
1101
|
-
path: relPath,
|
|
1102
|
-
locCount,
|
|
1103
|
-
isBan: false,
|
|
1104
|
-
isDefault: matchesDefault(relPath),
|
|
1105
|
-
isLarge: locCount > LOC_THRESHOLD
|
|
1106
|
-
});
|
|
1267
|
+
const loc = await scanFile(absPath);
|
|
1268
|
+
if (loc !== null) out.push(toEntry(relPath, loc));
|
|
1107
1269
|
}
|
|
1108
1270
|
}
|
|
1109
1271
|
}
|
|
1110
1272
|
async function scan(cwd) {
|
|
1111
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
|
+
}
|
|
1112
1282
|
const out = [];
|
|
1113
|
-
await
|
|
1283
|
+
await walkAll(cwd, "", out);
|
|
1114
1284
|
return out;
|
|
1115
1285
|
}
|
|
1116
1286
|
|
|
@@ -1147,16 +1317,14 @@ async function writeBaliseignore(cwd, patterns) {
|
|
|
1147
1317
|
}
|
|
1148
1318
|
|
|
1149
1319
|
// src/ui/IgnoreTree.tsx
|
|
1150
|
-
import React2, { useMemo, useState as useState2 } from "react";
|
|
1320
|
+
import React2, { useMemo as useMemo2, useState as useState2 } from "react";
|
|
1151
1321
|
import { Box as Box2, Text as Text2, useApp as useApp2, useInput as useInput2 } from "ink";
|
|
1152
1322
|
function buildTree(entries) {
|
|
1153
1323
|
const root = {
|
|
1154
1324
|
path: "",
|
|
1155
1325
|
name: ".",
|
|
1156
1326
|
isDir: true,
|
|
1157
|
-
isBan: false,
|
|
1158
1327
|
locCount: 0,
|
|
1159
|
-
isDefault: false,
|
|
1160
1328
|
isLarge: false,
|
|
1161
1329
|
children: [],
|
|
1162
1330
|
parent: null
|
|
@@ -1175,9 +1343,7 @@ function buildTree(entries) {
|
|
|
1175
1343
|
path: dirPath,
|
|
1176
1344
|
name,
|
|
1177
1345
|
isDir: true,
|
|
1178
|
-
isBan: false,
|
|
1179
1346
|
locCount: 0,
|
|
1180
|
-
isDefault: false,
|
|
1181
1347
|
isLarge: false,
|
|
1182
1348
|
children: [],
|
|
1183
1349
|
parent
|
|
@@ -1195,17 +1361,13 @@ function buildTree(entries) {
|
|
|
1195
1361
|
const node = {
|
|
1196
1362
|
path: e.path,
|
|
1197
1363
|
name,
|
|
1198
|
-
isDir:
|
|
1199
|
-
// ban entries from scan() are directories by construction
|
|
1200
|
-
isBan: e.isBan,
|
|
1364
|
+
isDir: false,
|
|
1201
1365
|
locCount: e.locCount,
|
|
1202
|
-
isDefault: e.isDefault,
|
|
1203
1366
|
isLarge: e.isLarge,
|
|
1204
1367
|
children: [],
|
|
1205
1368
|
parent
|
|
1206
1369
|
};
|
|
1207
1370
|
parent.children.push(node);
|
|
1208
|
-
if (e.isBan) dirMap.set(e.path, node);
|
|
1209
1371
|
}
|
|
1210
1372
|
const sortChildren = (n) => {
|
|
1211
1373
|
n.children.sort((a, b) => {
|
|
@@ -1220,7 +1382,7 @@ function buildTree(entries) {
|
|
|
1220
1382
|
function collectDescendantFiles(node) {
|
|
1221
1383
|
const out = [];
|
|
1222
1384
|
const recurse = (n) => {
|
|
1223
|
-
if (!n.isDir
|
|
1385
|
+
if (!n.isDir) out.push(n.path);
|
|
1224
1386
|
for (const c of n.children) recurse(c);
|
|
1225
1387
|
};
|
|
1226
1388
|
recurse(node);
|
|
@@ -1232,7 +1394,7 @@ function flattenVisible(root, expanded) {
|
|
|
1232
1394
|
if (node !== root) {
|
|
1233
1395
|
out.push({ node, depth });
|
|
1234
1396
|
}
|
|
1235
|
-
if (node === root || node.isDir &&
|
|
1397
|
+
if (node === root || node.isDir && expanded.has(node.path)) {
|
|
1236
1398
|
for (const c of node.children) recurse(c, depth + 1);
|
|
1237
1399
|
}
|
|
1238
1400
|
};
|
|
@@ -1244,13 +1406,17 @@ function dirAllIncluded(node, excluded) {
|
|
|
1244
1406
|
if (files.length === 0) return true;
|
|
1245
1407
|
return files.every((f) => !excluded.has(f));
|
|
1246
1408
|
}
|
|
1409
|
+
function buildFileList(entries) {
|
|
1410
|
+
return [...entries].sort(
|
|
1411
|
+
(a, b) => b.locCount - a.locCount || a.path.localeCompare(b.path)
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1247
1414
|
function computeInitialExcluded(scan2, current) {
|
|
1248
1415
|
const out = /* @__PURE__ */ new Set();
|
|
1249
1416
|
const scanPaths = /* @__PURE__ */ new Set();
|
|
1250
1417
|
for (const e of scan2) {
|
|
1251
|
-
if (e.isBan) continue;
|
|
1252
1418
|
scanPaths.add(e.path);
|
|
1253
|
-
if (e.
|
|
1419
|
+
if (e.isLarge) out.add(e.path);
|
|
1254
1420
|
}
|
|
1255
1421
|
for (const p of current) {
|
|
1256
1422
|
if (!scanPaths.has(p)) out.add(p);
|
|
@@ -1269,30 +1435,34 @@ function computeDiff(current, excluded) {
|
|
|
1269
1435
|
var VIEWPORT_HEIGHT = 18;
|
|
1270
1436
|
function IgnoreTree(props) {
|
|
1271
1437
|
const { exit } = useApp2();
|
|
1272
|
-
const root =
|
|
1438
|
+
const root = useMemo2(() => buildTree(props.scan), [props.scan]);
|
|
1439
|
+
const fileList = useMemo2(() => buildFileList(props.scan), [props.scan]);
|
|
1273
1440
|
const [excluded, setExcluded] = useState2(
|
|
1274
1441
|
() => computeInitialExcluded(props.scan, props.currentBaliseignore)
|
|
1275
1442
|
);
|
|
1276
1443
|
const [expandedDirs, setExpandedDirs] = useState2(/* @__PURE__ */ new Set());
|
|
1277
|
-
const [
|
|
1444
|
+
const [leftView, setLeftView] = useState2("list");
|
|
1445
|
+
const [focus, setFocus] = useState2("left");
|
|
1278
1446
|
const [treeCursor, setTreeCursor] = useState2("");
|
|
1447
|
+
const [listCursor, setListCursor] = useState2(0);
|
|
1279
1448
|
const [diffCursor, setDiffCursor] = useState2(0);
|
|
1280
1449
|
const [modalOpen, setModalOpen] = useState2(false);
|
|
1281
|
-
const visible =
|
|
1450
|
+
const visible = useMemo2(
|
|
1282
1451
|
() => flattenVisible(root, expandedDirs),
|
|
1283
1452
|
[root, expandedDirs]
|
|
1284
1453
|
);
|
|
1285
|
-
const diff =
|
|
1454
|
+
const diff = useMemo2(
|
|
1286
1455
|
() => computeDiff(props.currentBaliseignore, excluded),
|
|
1287
1456
|
[props.currentBaliseignore, excluded]
|
|
1288
1457
|
);
|
|
1289
|
-
const treeIdx =
|
|
1458
|
+
const treeIdx = useMemo2(() => {
|
|
1290
1459
|
if (visible.length === 0) return -1;
|
|
1291
1460
|
const i = visible.findIndex((v) => v.node.path === treeCursor);
|
|
1292
1461
|
return i >= 0 ? i : 0;
|
|
1293
1462
|
}, [visible, treeCursor]);
|
|
1463
|
+
const safeListIdx = fileList.length === 0 ? -1 : Math.max(0, Math.min(listCursor, fileList.length - 1));
|
|
1294
1464
|
const safeDiffIdx = diff.length === 0 ? -1 : Math.max(0, Math.min(diffCursor, diff.length - 1));
|
|
1295
|
-
const stats =
|
|
1465
|
+
const stats = useMemo2(() => {
|
|
1296
1466
|
let added = 0;
|
|
1297
1467
|
let removed = 0;
|
|
1298
1468
|
let unchanged = 0;
|
|
@@ -1349,7 +1519,11 @@ function IgnoreTree(props) {
|
|
|
1349
1519
|
return;
|
|
1350
1520
|
}
|
|
1351
1521
|
if (key.tab) {
|
|
1352
|
-
setFocus((f) => f === "
|
|
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");
|
|
1353
1527
|
return;
|
|
1354
1528
|
}
|
|
1355
1529
|
if (key.return) {
|
|
@@ -1360,7 +1534,21 @@ function IgnoreTree(props) {
|
|
|
1360
1534
|
setModalOpen(true);
|
|
1361
1535
|
return;
|
|
1362
1536
|
}
|
|
1363
|
-
if (focus === "
|
|
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") {
|
|
1364
1552
|
if (key.downArrow) {
|
|
1365
1553
|
const i = Math.min(treeIdx + 1, visible.length - 1);
|
|
1366
1554
|
if (i >= 0) setTreeCursor(visible[i].node.path);
|
|
@@ -1373,7 +1561,7 @@ function IgnoreTree(props) {
|
|
|
1373
1561
|
}
|
|
1374
1562
|
if (key.rightArrow) {
|
|
1375
1563
|
const cur = treeIdx >= 0 ? visible[treeIdx].node : null;
|
|
1376
|
-
if (!cur || !cur.isDir
|
|
1564
|
+
if (!cur || !cur.isDir) return;
|
|
1377
1565
|
if (!expandedDirs.has(cur.path)) {
|
|
1378
1566
|
setExpandedDirs((s) => {
|
|
1379
1567
|
const next = new Set(s);
|
|
@@ -1388,7 +1576,7 @@ function IgnoreTree(props) {
|
|
|
1388
1576
|
if (key.leftArrow) {
|
|
1389
1577
|
const cur = treeIdx >= 0 ? visible[treeIdx].node : null;
|
|
1390
1578
|
if (!cur) return;
|
|
1391
|
-
if (cur.isDir &&
|
|
1579
|
+
if (cur.isDir && expandedDirs.has(cur.path)) {
|
|
1392
1580
|
setExpandedDirs((s) => {
|
|
1393
1581
|
const next = new Set(s);
|
|
1394
1582
|
next.delete(cur.path);
|
|
@@ -1401,7 +1589,7 @@ function IgnoreTree(props) {
|
|
|
1401
1589
|
}
|
|
1402
1590
|
if (input === " ") {
|
|
1403
1591
|
const cur = treeIdx >= 0 ? visible[treeIdx].node : null;
|
|
1404
|
-
if (!cur
|
|
1592
|
+
if (!cur) return;
|
|
1405
1593
|
if (cur.isDir) toggleDir(cur);
|
|
1406
1594
|
else toggleFile(cur.path);
|
|
1407
1595
|
return;
|
|
@@ -1436,6 +1624,19 @@ function IgnoreTree(props) {
|
|
|
1436
1624
|
Math.max(treeStart, 0),
|
|
1437
1625
|
Math.max(treeStart, 0) + VIEWPORT_HEIGHT
|
|
1438
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);
|
|
1439
1640
|
const diffStart = Math.max(
|
|
1440
1641
|
0,
|
|
1441
1642
|
Math.min(
|
|
@@ -1447,19 +1648,27 @@ function IgnoreTree(props) {
|
|
|
1447
1648
|
Math.max(diffStart, 0),
|
|
1448
1649
|
Math.max(diffStart, 0) + VIEWPORT_HEIGHT
|
|
1449
1650
|
);
|
|
1450
|
-
const treeWindowStart = Math.max(treeStart, 0);
|
|
1451
1651
|
const diffWindowStart = Math.max(diffStart, 0);
|
|
1452
|
-
|
|
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(
|
|
1453
1654
|
Box2,
|
|
1454
1655
|
{
|
|
1455
1656
|
flexDirection: "column",
|
|
1456
1657
|
width: "50%",
|
|
1457
1658
|
paddingRight: 1,
|
|
1458
1659
|
borderStyle: "round",
|
|
1459
|
-
borderColor: focus === "
|
|
1660
|
+
borderColor: focus === "left" ? "cyan" : "gray"
|
|
1460
1661
|
},
|
|
1461
|
-
/* @__PURE__ */ React2.createElement(Text2, { bold: true, color: focus === "
|
|
1462
|
-
|
|
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(
|
|
1463
1672
|
TreeRow,
|
|
1464
1673
|
{
|
|
1465
1674
|
key: v.node.path,
|
|
@@ -1467,7 +1676,7 @@ function IgnoreTree(props) {
|
|
|
1467
1676
|
depth: v.depth,
|
|
1468
1677
|
expanded: expandedDirs.has(v.node.path),
|
|
1469
1678
|
excluded,
|
|
1470
|
-
isFocused: focus === "
|
|
1679
|
+
isFocused: focus === "left" && treeWindowStart + i === treeIdx
|
|
1471
1680
|
}
|
|
1472
1681
|
))
|
|
1473
1682
|
), /* @__PURE__ */ React2.createElement(
|
|
@@ -1490,15 +1699,18 @@ function IgnoreTree(props) {
|
|
|
1490
1699
|
))
|
|
1491
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)")));
|
|
1492
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
|
+
}
|
|
1493
1709
|
function TreeRow(props) {
|
|
1494
1710
|
const { node, depth, expanded, excluded, isFocused } = props;
|
|
1495
1711
|
const indent = " ".repeat(Math.max(depth, 0));
|
|
1496
1712
|
let label;
|
|
1497
|
-
|
|
1498
|
-
if (node.isBan) {
|
|
1499
|
-
label = `\u{1F512} ${node.name} [ban]`;
|
|
1500
|
-
dim = true;
|
|
1501
|
-
} else if (node.isDir) {
|
|
1713
|
+
if (node.isDir) {
|
|
1502
1714
|
const allIncluded = dirAllIncluded(node, excluded);
|
|
1503
1715
|
const arrow = expanded ? "\u25BE" : "\u25B8";
|
|
1504
1716
|
const box = allIncluded ? "\u2611" : "\u2610";
|
|
@@ -1506,13 +1718,10 @@ function TreeRow(props) {
|
|
|
1506
1718
|
} else {
|
|
1507
1719
|
const box = excluded.has(node.path) ? "\u2610" : "\u2611";
|
|
1508
1720
|
const loc = node.locCount > 0 ? ` (${node.locCount} LoC)` : "";
|
|
1509
|
-
const
|
|
1510
|
-
|
|
1511
|
-
if (node.isDefault) flags.push("default");
|
|
1512
|
-
const flagStr = flags.length > 0 ? ` [${flags.join(",")}]` : "";
|
|
1513
|
-
label = `${box} ${node.name}${loc}${flagStr}`;
|
|
1721
|
+
const large = node.isLarge ? " [large]" : "";
|
|
1722
|
+
label = `${box} ${node.name}${loc}${large}`;
|
|
1514
1723
|
}
|
|
1515
|
-
return /* @__PURE__ */ React2.createElement(Text2, { color: isFocused ? "cyan" : void 0
|
|
1724
|
+
return /* @__PURE__ */ React2.createElement(Text2, { color: isFocused ? "cyan" : void 0 }, isFocused ? "\u25B8 " : " ", indent, label);
|
|
1516
1725
|
}
|
|
1517
1726
|
function DiffRow(props) {
|
|
1518
1727
|
const { entry, isFocused } = props;
|
|
@@ -1618,7 +1827,8 @@ async function runInit(opts) {
|
|
|
1618
1827
|
if (result.action === "create") {
|
|
1619
1828
|
const created = await client.postJson("/v1/repos", {
|
|
1620
1829
|
owner_id: result.owner.id,
|
|
1621
|
-
slug: result.slug
|
|
1830
|
+
slug: result.slug,
|
|
1831
|
+
visibility: result.visibility
|
|
1622
1832
|
});
|
|
1623
1833
|
cfg = {
|
|
1624
1834
|
repo: {
|
|
@@ -1671,6 +1881,9 @@ async function runInit(opts) {
|
|
|
1671
1881
|
`Wrote ${treeResult.patterns.length} patterns to .baliseignore
|
|
1672
1882
|
`
|
|
1673
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
|
+
);
|
|
1674
1887
|
}
|
|
1675
1888
|
|
|
1676
1889
|
// src/commands/sync.ts
|
|
@@ -1717,6 +1930,67 @@ ${formatError(err)}
|
|
|
1717
1930
|
}
|
|
1718
1931
|
}
|
|
1719
1932
|
|
|
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();
|
|
1988
|
+
}
|
|
1989
|
+
})
|
|
1990
|
+
);
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1720
1994
|
// src/commands/sync.ts
|
|
1721
1995
|
function buildSyncQuery(opts) {
|
|
1722
1996
|
const q = { commit_sha: opts.commitSha };
|
|
@@ -1734,6 +2008,9 @@ function drainStdinBuffer(stdin) {
|
|
|
1734
2008
|
while (s.read() !== null) {
|
|
1735
2009
|
}
|
|
1736
2010
|
}
|
|
2011
|
+
function isInteractive2() {
|
|
2012
|
+
return Boolean(process.stdin.isTTY);
|
|
2013
|
+
}
|
|
1737
2014
|
async function confirmRetryWithForce(opts) {
|
|
1738
2015
|
const stderr = opts.stderr ?? process.stderr;
|
|
1739
2016
|
const synced = opts.body.synced_at ?? "unknown time";
|
|
@@ -1760,10 +2037,18 @@ async function confirmRetryWithForce(opts) {
|
|
|
1760
2037
|
}
|
|
1761
2038
|
function formatDryRunSummary(body) {
|
|
1762
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}` : "");
|
|
1763
|
-
|
|
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`;
|
|
1764
2042
|
}
|
|
1765
|
-
function
|
|
1766
|
-
|
|
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)}`;
|
|
1767
2052
|
}
|
|
1768
2053
|
async function runSync(opts) {
|
|
1769
2054
|
try {
|
|
@@ -1816,11 +2101,7 @@ async function runSyncInner(opts) {
|
|
|
1816
2101
|
process.exit(1);
|
|
1817
2102
|
}
|
|
1818
2103
|
}
|
|
1819
|
-
|
|
1820
|
-
process.stderr.write(
|
|
1821
|
-
"Warning: working tree has uncommitted changes \u2014 syncing HEAD anyway.\n"
|
|
1822
|
-
);
|
|
1823
|
-
}
|
|
2104
|
+
const dirty = await isDirty({ cwd });
|
|
1824
2105
|
const client = new ApiClient({
|
|
1825
2106
|
apiUrl: cfg.api.url,
|
|
1826
2107
|
supabaseUrl: opts.supabaseUrl
|
|
@@ -1829,53 +2110,117 @@ async function runSyncInner(opts) {
|
|
|
1829
2110
|
`Packing ${cfg.repo.owner_login}/${cfg.repo.slug} at HEAD\u2026
|
|
1830
2111
|
`
|
|
1831
2112
|
);
|
|
1832
|
-
const submit = async (force) => {
|
|
2113
|
+
const submit = async (force, dryRun) => {
|
|
1833
2114
|
const { stream, commitSha, branch } = await gitBundle({ cwd });
|
|
1834
|
-
const query = buildSyncQuery({
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
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 };
|
|
1842
2124
|
};
|
|
1843
|
-
|
|
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;
|
|
1844
2168
|
try {
|
|
1845
|
-
|
|
2169
|
+
dry = await submit(opts.force ?? false, true);
|
|
1846
2170
|
} catch (err) {
|
|
1847
2171
|
handleUploadError(err);
|
|
1848
2172
|
process.exit(1);
|
|
1849
2173
|
}
|
|
1850
|
-
if (outcome.kind === "conflict") {
|
|
1851
|
-
const
|
|
1852
|
-
body: outcome.body,
|
|
1853
|
-
autoConfirm:
|
|
2174
|
+
if (dry.outcome.kind === "conflict") {
|
|
2175
|
+
const proceed = await confirmRetryWithForce({
|
|
2176
|
+
body: dry.outcome.body,
|
|
2177
|
+
autoConfirm: false
|
|
1854
2178
|
});
|
|
1855
|
-
if (!
|
|
1856
|
-
|
|
1857
|
-
}
|
|
2179
|
+
if (!proceed) process.exit(1);
|
|
2180
|
+
let forced;
|
|
1858
2181
|
try {
|
|
1859
|
-
|
|
2182
|
+
forced = (await submit(true, false)).outcome;
|
|
1860
2183
|
} catch (err) {
|
|
1861
2184
|
handleUploadError(err);
|
|
1862
2185
|
process.exit(1);
|
|
1863
2186
|
}
|
|
2187
|
+
finishReal(forced);
|
|
1864
2188
|
}
|
|
1865
|
-
if (outcome.kind === "invalid_base") {
|
|
2189
|
+
if (dry.outcome.kind === "invalid_base") {
|
|
1866
2190
|
process.stderr.write(`invalid base ref: ${opts.base}
|
|
1867
2191
|
`);
|
|
1868
2192
|
process.exit(1);
|
|
1869
2193
|
}
|
|
1870
|
-
if (outcome.kind
|
|
1871
|
-
process.
|
|
2194
|
+
if (dry.outcome.kind !== "dry_run") {
|
|
2195
|
+
process.exit(1);
|
|
2196
|
+
}
|
|
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"
|
|
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
|
|
2211
|
+
});
|
|
2212
|
+
if (!confirmed) {
|
|
2213
|
+
process.stderr.write("Sync cancelled.\n");
|
|
1872
2214
|
process.exit(0);
|
|
1873
2215
|
}
|
|
1874
|
-
|
|
2216
|
+
let real;
|
|
2217
|
+
try {
|
|
2218
|
+
real = (await submit(opts.force ?? false, false)).outcome;
|
|
2219
|
+
} catch (err) {
|
|
2220
|
+
handleUploadError(err);
|
|
1875
2221
|
process.exit(1);
|
|
1876
2222
|
}
|
|
1877
|
-
|
|
1878
|
-
process.exit(0);
|
|
2223
|
+
finishReal(real);
|
|
1879
2224
|
}
|
|
1880
2225
|
async function uploadOnce(client, owner, slug, stream, query) {
|
|
1881
2226
|
const raw = await client.uploadBundleRaw(
|
|
@@ -1938,6 +2283,7 @@ async function withLog(name, fn) {
|
|
|
1938
2283
|
}
|
|
1939
2284
|
}
|
|
1940
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";
|
|
1941
2287
|
var loginCmd = defineCommand({
|
|
1942
2288
|
meta: { name: "login", description: "Authenticate via OAuth (PKCE loopback)." },
|
|
1943
2289
|
async run() {
|
|
@@ -1974,7 +2320,7 @@ var syncCmd = defineCommand({
|
|
|
1974
2320
|
yes: {
|
|
1975
2321
|
type: "boolean",
|
|
1976
2322
|
alias: "y",
|
|
1977
|
-
description: "
|
|
2323
|
+
description: "Auto-validate: skip the pre-run dashboard and sync directly.",
|
|
1978
2324
|
default: false
|
|
1979
2325
|
},
|
|
1980
2326
|
force: {
|
|
@@ -1985,20 +2331,15 @@ var syncCmd = defineCommand({
|
|
|
1985
2331
|
base: {
|
|
1986
2332
|
type: "string",
|
|
1987
2333
|
description: "Explicit base balise ref (branch/tag/commit) to start from. Validated server-side."
|
|
1988
|
-
},
|
|
1989
|
-
"dry-run": {
|
|
1990
|
-
type: "boolean",
|
|
1991
|
-
description: "Preview the resolution without uploading the bundle or enqueuing the worker.",
|
|
1992
|
-
default: false
|
|
1993
2334
|
}
|
|
1994
2335
|
},
|
|
1995
2336
|
async run({ args }) {
|
|
1996
2337
|
await runSync({
|
|
1997
2338
|
supabaseUrl: SUPABASE_URL,
|
|
2339
|
+
frontUrl: FRONT_URL,
|
|
1998
2340
|
autoConfirm: Boolean(args.yes),
|
|
1999
2341
|
force: Boolean(args.force),
|
|
2000
|
-
base: typeof args.base === "string" && args.base.length > 0 ? args.base : void 0
|
|
2001
|
-
dryRun: Boolean(args["dry-run"])
|
|
2342
|
+
base: typeof args.base === "string" && args.base.length > 0 ? args.base : void 0
|
|
2002
2343
|
});
|
|
2003
2344
|
}
|
|
2004
2345
|
});
|