@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.
- package/README.md +19 -0
- package/dist/index.js +1038 -265
- 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(
|
|
552
|
-
const url = joinUrl(this.opts.apiUrl,
|
|
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(
|
|
568
|
-
const url = joinUrl(this.opts.apiUrl,
|
|
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(
|
|
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,
|
|
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(
|
|
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,
|
|
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,
|
|
652
|
-
return `${base.replace(/\/$/, "")}${
|
|
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
|
|
738
|
-
import
|
|
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
|
-
|
|
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, "");
|
|
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
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
);
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
-
|
|
947
|
-
|
|
1032
|
+
if (field === "visibility") setField("name");
|
|
1033
|
+
else setMode("owner");
|
|
948
1034
|
return;
|
|
949
1035
|
}
|
|
950
|
-
if (
|
|
951
|
-
|
|
952
|
-
if (
|
|
953
|
-
|
|
954
|
-
|
|
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
|
-
|
|
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
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1091
|
+
repos: visibleRepos,
|
|
1092
|
+
total: repos.length,
|
|
1093
|
+
filter,
|
|
1094
|
+
cursor: safeCursor,
|
|
1095
|
+
currentRepoId: props.currentRepoId
|
|
977
1096
|
}
|
|
978
|
-
)
|
|
979
|
-
|
|
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
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
-
)
|
|
986
|
-
|
|
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
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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
|
-
|
|
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 =
|
|
1807
|
+
const defaultSlug = path5.basename(cwd).toLowerCase();
|
|
1047
1808
|
const result = await new Promise((resolve) => {
|
|
1048
1809
|
const app = render(
|
|
1049
|
-
|
|
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
|
|
1103
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1928
|
+
await fs5.appendFile(p, line, { mode: 384 });
|
|
1138
1929
|
} catch {
|
|
1139
1930
|
}
|
|
1140
1931
|
}
|
|
1141
1932
|
|
|
1142
|
-
// src/ui/
|
|
1143
|
-
import
|
|
1144
|
-
import { Box as
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
"
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
"
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
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
|
-
}
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1408
|
-
body: outcome.body,
|
|
1409
|
-
autoConfirm:
|
|
2174
|
+
if (dry.outcome.kind === "conflict") {
|
|
2175
|
+
const proceed = await confirmRetryWithForce({
|
|
2176
|
+
body: dry.outcome.body,
|
|
2177
|
+
autoConfirm: false
|
|
1410
2178
|
});
|
|
1411
|
-
if (!
|
|
1412
|
-
|
|
1413
|
-
}
|
|
2179
|
+
if (!proceed) process.exit(1);
|
|
2180
|
+
let forced;
|
|
1414
2181
|
try {
|
|
1415
|
-
|
|
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
|
|
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
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
});
|