@balise.dev/cli 0.1.3 → 0.2.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 +608 -176
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -61,6 +61,25 @@ Tokens are written to a plaintext JSON file with restrictive permissions:
|
|
|
61
61
|
|
|
62
62
|
If you need OS-keychain-grade protection, set `BALISE_CREDENTIALS_FILE` to a path on an encrypted volume, or feed `BALISE_TOKEN` from your existing secret manager.
|
|
63
63
|
|
|
64
|
+
## Build-time policy patterns
|
|
65
|
+
|
|
66
|
+
`balise init` ships with two compile-time pattern lists baked into the bundle via tsup `define`:
|
|
67
|
+
|
|
68
|
+
| Env var (build) | Used for | SSOT contract |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `BALISE_BAN_PATTERNS` | Directories the extractor will never read (`.git`, `node_modules`, …). Displayed as 🔒 read-only in the `balise init` ignore tree. | **MUST** match the worker-side env var of the same name (read by `balise_mcp/diff_loc.py`). Changing one without the other breaks the SSOT verrou between credit estimate and what the agent sees. |
|
|
71
|
+
| `BALISE_DEFAULT_PATTERNS` | Globs pre-checked-out in the ignore tree as a sensible default (lockfiles, minified assets, snapshots, …). User remains free to scan them — they just pay the tokens. | CLI-only. |
|
|
72
|
+
|
|
73
|
+
Override at build time:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
BALISE_BAN_PATTERNS=.git,node_modules,dist \
|
|
77
|
+
BALISE_DEFAULT_PATTERNS='*.lock,*.min.js' \
|
|
78
|
+
npm run build
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The defaults baked when these env vars are unset are visible in `cli/tsup.config.ts`.
|
|
82
|
+
|
|
64
83
|
## Dev
|
|
65
84
|
|
|
66
85
|
```bash
|
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
|
|
@@ -921,13 +921,14 @@ function normalizeSlug(raw) {
|
|
|
921
921
|
}
|
|
922
922
|
function InitPicker(props) {
|
|
923
923
|
const { exit } = useApp();
|
|
924
|
-
const
|
|
924
|
+
const currentMatches = props.currentRepoId !== void 0 && props.repos.some((r) => r.id === props.currentRepoId);
|
|
925
|
+
const [field, setField] = useState(currentMatches ? "link" : "slug");
|
|
925
926
|
const [slug, setSlug] = useState(normalizeSlug(props.defaultSlug));
|
|
926
927
|
const [ownerId, setOwnerId] = useState(
|
|
927
928
|
props.ownerships[0]?.id
|
|
928
929
|
);
|
|
929
930
|
const [repoId, setRepoId] = useState(
|
|
930
|
-
props.repos[0]?.id
|
|
931
|
+
currentMatches ? props.currentRepoId : props.repos[0]?.id
|
|
931
932
|
);
|
|
932
933
|
const submit = () => {
|
|
933
934
|
if (field === "link") {
|
|
@@ -964,7 +965,7 @@ function InitPicker(props) {
|
|
|
964
965
|
value: o.id
|
|
965
966
|
}));
|
|
966
967
|
const repoOptions = props.repos.map((r) => ({
|
|
967
|
-
label: `${r.owner_login}/${r.slug}`,
|
|
968
|
+
label: r.id === props.currentRepoId ? `${r.owner_login}/${r.slug} (current)` : `${r.owner_login}/${r.slug}`,
|
|
968
969
|
value: r.id
|
|
969
970
|
}));
|
|
970
971
|
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true }, "Balise \u2014 link or create a repo"), /* @__PURE__ */ React.createElement(Box, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React.createElement(Text, { color: field === "slug" ? "cyan" : "gray" }, field === "slug" ? "\u25B8 " : " ", "New repo slug"), /* @__PURE__ */ React.createElement(Box, { marginLeft: 2 }, /* @__PURE__ */ React.createElement(
|
|
@@ -992,6 +993,550 @@ function InitPicker(props) {
|
|
|
992
993
|
) : /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "tab to browse"))), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Tab: next field \xB7 \u2191/\u2193: navigate \xB7 Enter: confirm \xB7 Esc: cancel")));
|
|
993
994
|
}
|
|
994
995
|
|
|
996
|
+
// src/scan.ts
|
|
997
|
+
import { isUtf8 } from "buffer";
|
|
998
|
+
import { execFile as execFileCb } from "child_process";
|
|
999
|
+
import { promises as fs3 } from "fs";
|
|
1000
|
+
import path3 from "path";
|
|
1001
|
+
import { promisify } from "util";
|
|
1002
|
+
import picomatch from "picomatch";
|
|
1003
|
+
|
|
1004
|
+
// 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
|
+
var LOC_THRESHOLD = 2500;
|
|
1037
|
+
var MAX_FILE_BYTES = 1e6;
|
|
1038
|
+
|
|
1039
|
+
// src/scan.ts
|
|
1040
|
+
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
|
+
function countLines(buf) {
|
|
1044
|
+
let count = 0;
|
|
1045
|
+
for (let i = 0; i < buf.length; i++) {
|
|
1046
|
+
if (buf[i] === 10) count++;
|
|
1047
|
+
}
|
|
1048
|
+
if (buf.length > 0 && buf[buf.length - 1] !== 10) count++;
|
|
1049
|
+
return count;
|
|
1050
|
+
}
|
|
1051
|
+
async function scanFile(absPath) {
|
|
1052
|
+
let stat;
|
|
1053
|
+
try {
|
|
1054
|
+
stat = await fs3.stat(absPath);
|
|
1055
|
+
} catch {
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
if (stat.size > MAX_FILE_BYTES) return null;
|
|
1059
|
+
const buf = await fs3.readFile(absPath);
|
|
1060
|
+
if (!isUtf8(buf)) return null;
|
|
1061
|
+
return countLines(buf);
|
|
1062
|
+
}
|
|
1063
|
+
async function listTrackedFiles(cwd) {
|
|
1064
|
+
try {
|
|
1065
|
+
const { stdout } = await execFile("git", ["ls-files", "-z"], {
|
|
1066
|
+
cwd,
|
|
1067
|
+
maxBuffer: 50 * 1024 * 1024
|
|
1068
|
+
});
|
|
1069
|
+
return new Set(stdout.split("\0").filter((l) => l.length > 0));
|
|
1070
|
+
} catch {
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
async function walk(absDir, relDir, out, tracked) {
|
|
1075
|
+
let dirents;
|
|
1076
|
+
try {
|
|
1077
|
+
dirents = await fs3.readdir(absDir, { withFileTypes: true });
|
|
1078
|
+
} catch {
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
for (const d of dirents) {
|
|
1082
|
+
const relPath = relDir ? `${relDir}/${d.name}` : d.name;
|
|
1083
|
+
const absPath = path3.join(absDir, d.name);
|
|
1084
|
+
if (d.isDirectory()) {
|
|
1085
|
+
if (BAN_DIRS.has(d.name)) {
|
|
1086
|
+
out.push({
|
|
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);
|
|
1096
|
+
} else if (d.isFile()) {
|
|
1097
|
+
if (tracked !== null && !tracked.has(relPath)) continue;
|
|
1098
|
+
const locCount = await scanFile(absPath);
|
|
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
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
async function scan(cwd) {
|
|
1111
|
+
const tracked = await listTrackedFiles(cwd);
|
|
1112
|
+
const out = [];
|
|
1113
|
+
await walk(cwd, "", out, tracked);
|
|
1114
|
+
return out;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// src/baliseignore.ts
|
|
1118
|
+
import { promises as fs4 } from "fs";
|
|
1119
|
+
import path4 from "path";
|
|
1120
|
+
var FILENAME2 = ".baliseignore";
|
|
1121
|
+
var HEADER = [
|
|
1122
|
+
"# Generated by balise init.",
|
|
1123
|
+
"# balise init will overwrite this file on next run.",
|
|
1124
|
+
""
|
|
1125
|
+
].join("\n");
|
|
1126
|
+
async function readBaliseignore(cwd) {
|
|
1127
|
+
const file = path4.join(cwd, FILENAME2);
|
|
1128
|
+
let raw;
|
|
1129
|
+
try {
|
|
1130
|
+
raw = await fs4.readFile(file, "utf-8");
|
|
1131
|
+
} catch (err) {
|
|
1132
|
+
if (err.code === "ENOENT") return /* @__PURE__ */ new Set();
|
|
1133
|
+
throw err;
|
|
1134
|
+
}
|
|
1135
|
+
const out = /* @__PURE__ */ new Set();
|
|
1136
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
1137
|
+
const trimmed = line.trim();
|
|
1138
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1139
|
+
out.add(trimmed);
|
|
1140
|
+
}
|
|
1141
|
+
return out;
|
|
1142
|
+
}
|
|
1143
|
+
async function writeBaliseignore(cwd, patterns) {
|
|
1144
|
+
const sorted = [...new Set(patterns)].filter((p) => p.length > 0).sort((a, b) => a.localeCompare(b));
|
|
1145
|
+
const body = sorted.length > 0 ? sorted.join("\n") + "\n" : "";
|
|
1146
|
+
await fs4.writeFile(path4.join(cwd, FILENAME2), HEADER + body, "utf-8");
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// src/ui/IgnoreTree.tsx
|
|
1150
|
+
import React2, { useMemo, useState as useState2 } from "react";
|
|
1151
|
+
import { Box as Box2, Text as Text2, useApp as useApp2, useInput as useInput2 } from "ink";
|
|
1152
|
+
function buildTree(entries) {
|
|
1153
|
+
const root = {
|
|
1154
|
+
path: "",
|
|
1155
|
+
name: ".",
|
|
1156
|
+
isDir: true,
|
|
1157
|
+
isBan: false,
|
|
1158
|
+
locCount: 0,
|
|
1159
|
+
isDefault: false,
|
|
1160
|
+
isLarge: false,
|
|
1161
|
+
children: [],
|
|
1162
|
+
parent: null
|
|
1163
|
+
};
|
|
1164
|
+
const dirMap = /* @__PURE__ */ new Map();
|
|
1165
|
+
dirMap.set("", root);
|
|
1166
|
+
const ensureDir = (dirPath) => {
|
|
1167
|
+
if (!dirPath) return root;
|
|
1168
|
+
const existing = dirMap.get(dirPath);
|
|
1169
|
+
if (existing) return existing;
|
|
1170
|
+
const parts = dirPath.split("/");
|
|
1171
|
+
const name = parts[parts.length - 1];
|
|
1172
|
+
const parentPath = parts.slice(0, -1).join("/");
|
|
1173
|
+
const parent = ensureDir(parentPath);
|
|
1174
|
+
const node = {
|
|
1175
|
+
path: dirPath,
|
|
1176
|
+
name,
|
|
1177
|
+
isDir: true,
|
|
1178
|
+
isBan: false,
|
|
1179
|
+
locCount: 0,
|
|
1180
|
+
isDefault: false,
|
|
1181
|
+
isLarge: false,
|
|
1182
|
+
children: [],
|
|
1183
|
+
parent
|
|
1184
|
+
};
|
|
1185
|
+
parent.children.push(node);
|
|
1186
|
+
dirMap.set(dirPath, node);
|
|
1187
|
+
return node;
|
|
1188
|
+
};
|
|
1189
|
+
const sorted = [...entries].sort((a, b) => a.path.localeCompare(b.path));
|
|
1190
|
+
for (const e of sorted) {
|
|
1191
|
+
const parts = e.path.split("/");
|
|
1192
|
+
const name = parts[parts.length - 1];
|
|
1193
|
+
const parentPath = parts.slice(0, -1).join("/");
|
|
1194
|
+
const parent = ensureDir(parentPath);
|
|
1195
|
+
const node = {
|
|
1196
|
+
path: e.path,
|
|
1197
|
+
name,
|
|
1198
|
+
isDir: e.isBan,
|
|
1199
|
+
// ban entries from scan() are directories by construction
|
|
1200
|
+
isBan: e.isBan,
|
|
1201
|
+
locCount: e.locCount,
|
|
1202
|
+
isDefault: e.isDefault,
|
|
1203
|
+
isLarge: e.isLarge,
|
|
1204
|
+
children: [],
|
|
1205
|
+
parent
|
|
1206
|
+
};
|
|
1207
|
+
parent.children.push(node);
|
|
1208
|
+
if (e.isBan) dirMap.set(e.path, node);
|
|
1209
|
+
}
|
|
1210
|
+
const sortChildren = (n) => {
|
|
1211
|
+
n.children.sort((a, b) => {
|
|
1212
|
+
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
1213
|
+
return a.name.localeCompare(b.name);
|
|
1214
|
+
});
|
|
1215
|
+
for (const c of n.children) sortChildren(c);
|
|
1216
|
+
};
|
|
1217
|
+
sortChildren(root);
|
|
1218
|
+
return root;
|
|
1219
|
+
}
|
|
1220
|
+
function collectDescendantFiles(node) {
|
|
1221
|
+
const out = [];
|
|
1222
|
+
const recurse = (n) => {
|
|
1223
|
+
if (!n.isDir && !n.isBan) out.push(n.path);
|
|
1224
|
+
for (const c of n.children) recurse(c);
|
|
1225
|
+
};
|
|
1226
|
+
recurse(node);
|
|
1227
|
+
return out;
|
|
1228
|
+
}
|
|
1229
|
+
function flattenVisible(root, expanded) {
|
|
1230
|
+
const out = [];
|
|
1231
|
+
const recurse = (node, depth) => {
|
|
1232
|
+
if (node !== root) {
|
|
1233
|
+
out.push({ node, depth });
|
|
1234
|
+
}
|
|
1235
|
+
if (node === root || node.isDir && !node.isBan && expanded.has(node.path)) {
|
|
1236
|
+
for (const c of node.children) recurse(c, depth + 1);
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
recurse(root, -1);
|
|
1240
|
+
return out;
|
|
1241
|
+
}
|
|
1242
|
+
function dirAllIncluded(node, excluded) {
|
|
1243
|
+
const files = collectDescendantFiles(node);
|
|
1244
|
+
if (files.length === 0) return true;
|
|
1245
|
+
return files.every((f) => !excluded.has(f));
|
|
1246
|
+
}
|
|
1247
|
+
function computeInitialExcluded(scan2, current) {
|
|
1248
|
+
const out = /* @__PURE__ */ new Set();
|
|
1249
|
+
const scanPaths = /* @__PURE__ */ new Set();
|
|
1250
|
+
for (const e of scan2) {
|
|
1251
|
+
if (e.isBan) continue;
|
|
1252
|
+
scanPaths.add(e.path);
|
|
1253
|
+
if (e.isDefault || e.isLarge) out.add(e.path);
|
|
1254
|
+
}
|
|
1255
|
+
for (const p of current) {
|
|
1256
|
+
if (!scanPaths.has(p)) out.add(p);
|
|
1257
|
+
}
|
|
1258
|
+
return out;
|
|
1259
|
+
}
|
|
1260
|
+
function computeDiff(current, excluded) {
|
|
1261
|
+
const all = /* @__PURE__ */ new Set([...current, ...excluded]);
|
|
1262
|
+
return [...all].sort((a, b) => a.localeCompare(b)).map((pattern) => {
|
|
1263
|
+
const inCurrent = current.has(pattern);
|
|
1264
|
+
const inExcluded = excluded.has(pattern);
|
|
1265
|
+
const kind = inCurrent && inExcluded ? "unchanged" : inExcluded ? "added" : "removed";
|
|
1266
|
+
return { pattern, kind };
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
var VIEWPORT_HEIGHT = 18;
|
|
1270
|
+
function IgnoreTree(props) {
|
|
1271
|
+
const { exit } = useApp2();
|
|
1272
|
+
const root = useMemo(() => buildTree(props.scan), [props.scan]);
|
|
1273
|
+
const [excluded, setExcluded] = useState2(
|
|
1274
|
+
() => computeInitialExcluded(props.scan, props.currentBaliseignore)
|
|
1275
|
+
);
|
|
1276
|
+
const [expandedDirs, setExpandedDirs] = useState2(/* @__PURE__ */ new Set());
|
|
1277
|
+
const [focus, setFocus] = useState2("tree");
|
|
1278
|
+
const [treeCursor, setTreeCursor] = useState2("");
|
|
1279
|
+
const [diffCursor, setDiffCursor] = useState2(0);
|
|
1280
|
+
const [modalOpen, setModalOpen] = useState2(false);
|
|
1281
|
+
const visible = useMemo(
|
|
1282
|
+
() => flattenVisible(root, expandedDirs),
|
|
1283
|
+
[root, expandedDirs]
|
|
1284
|
+
);
|
|
1285
|
+
const diff = useMemo(
|
|
1286
|
+
() => computeDiff(props.currentBaliseignore, excluded),
|
|
1287
|
+
[props.currentBaliseignore, excluded]
|
|
1288
|
+
);
|
|
1289
|
+
const treeIdx = useMemo(() => {
|
|
1290
|
+
if (visible.length === 0) return -1;
|
|
1291
|
+
const i = visible.findIndex((v) => v.node.path === treeCursor);
|
|
1292
|
+
return i >= 0 ? i : 0;
|
|
1293
|
+
}, [visible, treeCursor]);
|
|
1294
|
+
const safeDiffIdx = diff.length === 0 ? -1 : Math.max(0, Math.min(diffCursor, diff.length - 1));
|
|
1295
|
+
const stats = useMemo(() => {
|
|
1296
|
+
let added = 0;
|
|
1297
|
+
let removed = 0;
|
|
1298
|
+
let unchanged = 0;
|
|
1299
|
+
for (const p of excluded) {
|
|
1300
|
+
if (props.currentBaliseignore.has(p)) unchanged++;
|
|
1301
|
+
else added++;
|
|
1302
|
+
}
|
|
1303
|
+
for (const p of props.currentBaliseignore) {
|
|
1304
|
+
if (!excluded.has(p)) removed++;
|
|
1305
|
+
}
|
|
1306
|
+
return { added, removed, unchanged };
|
|
1307
|
+
}, [excluded, props.currentBaliseignore]);
|
|
1308
|
+
const toggleFile = (path7) => {
|
|
1309
|
+
setExcluded((prev) => {
|
|
1310
|
+
const next = new Set(prev);
|
|
1311
|
+
if (next.has(path7)) next.delete(path7);
|
|
1312
|
+
else next.add(path7);
|
|
1313
|
+
return next;
|
|
1314
|
+
});
|
|
1315
|
+
};
|
|
1316
|
+
const toggleDir = (dirNode) => {
|
|
1317
|
+
const descendants = collectDescendantFiles(dirNode);
|
|
1318
|
+
if (descendants.length === 0) return;
|
|
1319
|
+
const allIncluded = descendants.every((p) => !excluded.has(p));
|
|
1320
|
+
setExcluded((prev) => {
|
|
1321
|
+
const next = new Set(prev);
|
|
1322
|
+
if (allIncluded) {
|
|
1323
|
+
for (const p of descendants) next.add(p);
|
|
1324
|
+
} else {
|
|
1325
|
+
for (const p of descendants) next.delete(p);
|
|
1326
|
+
}
|
|
1327
|
+
return next;
|
|
1328
|
+
});
|
|
1329
|
+
};
|
|
1330
|
+
useInput2((input, key) => {
|
|
1331
|
+
if (modalOpen) {
|
|
1332
|
+
if (input === "y" || input === "Y") {
|
|
1333
|
+
props.onDone({
|
|
1334
|
+
action: "save",
|
|
1335
|
+
patterns: [...excluded].sort((a, b) => a.localeCompare(b))
|
|
1336
|
+
});
|
|
1337
|
+
exit();
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
if (input === "n" || input === "N") {
|
|
1341
|
+
setModalOpen(false);
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
if (input === "q" || input === "Q" || key.escape) {
|
|
1345
|
+
props.onDone({ action: "cancel", patterns: [] });
|
|
1346
|
+
exit();
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
if (key.tab) {
|
|
1352
|
+
setFocus((f) => f === "tree" ? "diff" : "tree");
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
if (key.return) {
|
|
1356
|
+
setModalOpen(true);
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
if (key.escape || input === "q") {
|
|
1360
|
+
setModalOpen(true);
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
if (focus === "tree") {
|
|
1364
|
+
if (key.downArrow) {
|
|
1365
|
+
const i = Math.min(treeIdx + 1, visible.length - 1);
|
|
1366
|
+
if (i >= 0) setTreeCursor(visible[i].node.path);
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
if (key.upArrow) {
|
|
1370
|
+
const i = Math.max(treeIdx - 1, 0);
|
|
1371
|
+
if (i >= 0) setTreeCursor(visible[i].node.path);
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
if (key.rightArrow) {
|
|
1375
|
+
const cur = treeIdx >= 0 ? visible[treeIdx].node : null;
|
|
1376
|
+
if (!cur || !cur.isDir || cur.isBan) return;
|
|
1377
|
+
if (!expandedDirs.has(cur.path)) {
|
|
1378
|
+
setExpandedDirs((s) => {
|
|
1379
|
+
const next = new Set(s);
|
|
1380
|
+
next.add(cur.path);
|
|
1381
|
+
return next;
|
|
1382
|
+
});
|
|
1383
|
+
} else if (cur.children.length > 0) {
|
|
1384
|
+
setTreeCursor(cur.children[0].path);
|
|
1385
|
+
}
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
if (key.leftArrow) {
|
|
1389
|
+
const cur = treeIdx >= 0 ? visible[treeIdx].node : null;
|
|
1390
|
+
if (!cur) return;
|
|
1391
|
+
if (cur.isDir && !cur.isBan && expandedDirs.has(cur.path)) {
|
|
1392
|
+
setExpandedDirs((s) => {
|
|
1393
|
+
const next = new Set(s);
|
|
1394
|
+
next.delete(cur.path);
|
|
1395
|
+
return next;
|
|
1396
|
+
});
|
|
1397
|
+
} else if (cur.parent && cur.parent.path !== "") {
|
|
1398
|
+
setTreeCursor(cur.parent.path);
|
|
1399
|
+
}
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
if (input === " ") {
|
|
1403
|
+
const cur = treeIdx >= 0 ? visible[treeIdx].node : null;
|
|
1404
|
+
if (!cur || cur.isBan) return;
|
|
1405
|
+
if (cur.isDir) toggleDir(cur);
|
|
1406
|
+
else toggleFile(cur.path);
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
} else {
|
|
1410
|
+
if (key.downArrow) {
|
|
1411
|
+
setDiffCursor((c) => Math.min(c + 1, Math.max(diff.length - 1, 0)));
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
if (key.upArrow) {
|
|
1415
|
+
setDiffCursor((c) => Math.max(c - 1, 0));
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
if (input === " ") {
|
|
1419
|
+
if (safeDiffIdx < 0) return;
|
|
1420
|
+
toggleFile(diff[safeDiffIdx].pattern);
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
});
|
|
1425
|
+
if (modalOpen) {
|
|
1426
|
+
return /* @__PURE__ */ React2.createElement(ConfirmModal, { stats, excludedSize: excluded.size });
|
|
1427
|
+
}
|
|
1428
|
+
const treeStart = Math.max(
|
|
1429
|
+
0,
|
|
1430
|
+
Math.min(
|
|
1431
|
+
treeIdx - Math.floor(VIEWPORT_HEIGHT / 2),
|
|
1432
|
+
visible.length - VIEWPORT_HEIGHT
|
|
1433
|
+
)
|
|
1434
|
+
);
|
|
1435
|
+
const treeWindow = visible.slice(
|
|
1436
|
+
Math.max(treeStart, 0),
|
|
1437
|
+
Math.max(treeStart, 0) + VIEWPORT_HEIGHT
|
|
1438
|
+
);
|
|
1439
|
+
const diffStart = Math.max(
|
|
1440
|
+
0,
|
|
1441
|
+
Math.min(
|
|
1442
|
+
safeDiffIdx - Math.floor(VIEWPORT_HEIGHT / 2),
|
|
1443
|
+
diff.length - VIEWPORT_HEIGHT
|
|
1444
|
+
)
|
|
1445
|
+
);
|
|
1446
|
+
const diffWindow = diff.slice(
|
|
1447
|
+
Math.max(diffStart, 0),
|
|
1448
|
+
Math.max(diffStart, 0) + VIEWPORT_HEIGHT
|
|
1449
|
+
);
|
|
1450
|
+
const treeWindowStart = Math.max(treeStart, 0);
|
|
1451
|
+
const diffWindowStart = Math.max(diffStart, 0);
|
|
1452
|
+
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 \u2191/\u2193: nav \xB7 \u2192/\u2190: open/close \xB7 Space: toggle \xB7 Enter: save \xB7 Esc: cancel"), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(
|
|
1453
|
+
Box2,
|
|
1454
|
+
{
|
|
1455
|
+
flexDirection: "column",
|
|
1456
|
+
width: "50%",
|
|
1457
|
+
paddingRight: 1,
|
|
1458
|
+
borderStyle: "round",
|
|
1459
|
+
borderColor: focus === "tree" ? "cyan" : "gray"
|
|
1460
|
+
},
|
|
1461
|
+
/* @__PURE__ */ React2.createElement(Text2, { bold: true, color: focus === "tree" ? "cyan" : void 0 }, "Files"),
|
|
1462
|
+
treeWindow.length === 0 ? /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "(no files)") : treeWindow.map((v, i) => /* @__PURE__ */ React2.createElement(
|
|
1463
|
+
TreeRow,
|
|
1464
|
+
{
|
|
1465
|
+
key: v.node.path,
|
|
1466
|
+
node: v.node,
|
|
1467
|
+
depth: v.depth,
|
|
1468
|
+
expanded: expandedDirs.has(v.node.path),
|
|
1469
|
+
excluded,
|
|
1470
|
+
isFocused: focus === "tree" && treeWindowStart + i === treeIdx
|
|
1471
|
+
}
|
|
1472
|
+
))
|
|
1473
|
+
), /* @__PURE__ */ React2.createElement(
|
|
1474
|
+
Box2,
|
|
1475
|
+
{
|
|
1476
|
+
flexDirection: "column",
|
|
1477
|
+
width: "50%",
|
|
1478
|
+
paddingLeft: 1,
|
|
1479
|
+
borderStyle: "round",
|
|
1480
|
+
borderColor: focus === "diff" ? "cyan" : "gray"
|
|
1481
|
+
},
|
|
1482
|
+
/* @__PURE__ */ React2.createElement(Text2, { bold: true, color: focus === "diff" ? "cyan" : void 0 }, "Diff vs .baliseignore"),
|
|
1483
|
+
diff.length === 0 ? /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "(no changes)") : diffWindow.map((d, i) => /* @__PURE__ */ React2.createElement(
|
|
1484
|
+
DiffRow,
|
|
1485
|
+
{
|
|
1486
|
+
key: d.pattern,
|
|
1487
|
+
entry: d,
|
|
1488
|
+
isFocused: focus === "diff" && diffWindowStart + i === safeDiffIdx
|
|
1489
|
+
}
|
|
1490
|
+
))
|
|
1491
|
+
)), /* @__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
|
+
}
|
|
1493
|
+
function TreeRow(props) {
|
|
1494
|
+
const { node, depth, expanded, excluded, isFocused } = props;
|
|
1495
|
+
const indent = " ".repeat(Math.max(depth, 0));
|
|
1496
|
+
let label;
|
|
1497
|
+
let dim = false;
|
|
1498
|
+
if (node.isBan) {
|
|
1499
|
+
label = `\u{1F512} ${node.name} [ban]`;
|
|
1500
|
+
dim = true;
|
|
1501
|
+
} else if (node.isDir) {
|
|
1502
|
+
const allIncluded = dirAllIncluded(node, excluded);
|
|
1503
|
+
const arrow = expanded ? "\u25BE" : "\u25B8";
|
|
1504
|
+
const box = allIncluded ? "\u2611" : "\u2610";
|
|
1505
|
+
label = `${arrow} ${box} ${node.name}/`;
|
|
1506
|
+
} else {
|
|
1507
|
+
const box = excluded.has(node.path) ? "\u2610" : "\u2611";
|
|
1508
|
+
const loc = node.locCount > 0 ? ` (${node.locCount} LoC)` : "";
|
|
1509
|
+
const flags = [];
|
|
1510
|
+
if (node.isLarge) flags.push("large");
|
|
1511
|
+
if (node.isDefault) flags.push("default");
|
|
1512
|
+
const flagStr = flags.length > 0 ? ` [${flags.join(",")}]` : "";
|
|
1513
|
+
label = `${box} ${node.name}${loc}${flagStr}`;
|
|
1514
|
+
}
|
|
1515
|
+
return /* @__PURE__ */ React2.createElement(Text2, { color: isFocused ? "cyan" : void 0, dimColor: dim }, isFocused ? "\u25B8 " : " ", indent, label);
|
|
1516
|
+
}
|
|
1517
|
+
function DiffRow(props) {
|
|
1518
|
+
const { entry, isFocused } = props;
|
|
1519
|
+
const sigil = entry.kind === "added" ? "+" : entry.kind === "removed" ? "-" : " ";
|
|
1520
|
+
const color = isFocused ? "cyan" : entry.kind === "added" ? "green" : entry.kind === "removed" ? "red" : void 0;
|
|
1521
|
+
return /* @__PURE__ */ React2.createElement(Text2, { color }, isFocused ? "\u25B8 " : " ", sigil, " ", entry.pattern);
|
|
1522
|
+
}
|
|
1523
|
+
function ConfirmModal(props) {
|
|
1524
|
+
const { stats, excludedSize } = props;
|
|
1525
|
+
return /* @__PURE__ */ React2.createElement(
|
|
1526
|
+
Box2,
|
|
1527
|
+
{
|
|
1528
|
+
flexDirection: "column",
|
|
1529
|
+
padding: 1,
|
|
1530
|
+
borderStyle: "double",
|
|
1531
|
+
borderColor: "cyan"
|
|
1532
|
+
},
|
|
1533
|
+
/* @__PURE__ */ React2.createElement(Text2, { bold: true }, "Save changes to .baliseignore?"),
|
|
1534
|
+
/* @__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")),
|
|
1535
|
+
/* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, excludedSize, " total patterns in resulting file")),
|
|
1536
|
+
/* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, null, "[y] Confirm [n] Back to edit [q/Esc] Cancel without saving"))
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
995
1540
|
// src/commands/init.ts
|
|
996
1541
|
async function runInit(opts) {
|
|
997
1542
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -1015,6 +1560,13 @@ async function runInit(opts) {
|
|
|
1015
1560
|
}
|
|
1016
1561
|
throw err;
|
|
1017
1562
|
}
|
|
1563
|
+
const existingConfig = await readConfig(cwd);
|
|
1564
|
+
if (existingConfig) {
|
|
1565
|
+
process.stdout.write(
|
|
1566
|
+
`Re-configuring ${existingConfig.repo.owner_login}/${existingConfig.repo.slug}.
|
|
1567
|
+
`
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1018
1570
|
const apiUrl = DEFAULT_API_URL;
|
|
1019
1571
|
const client = new ApiClient({
|
|
1020
1572
|
apiUrl,
|
|
@@ -1043,13 +1595,14 @@ async function runInit(opts) {
|
|
|
1043
1595
|
const sortedRepos = [...repos].sort(
|
|
1044
1596
|
(a, b) => `${a.owner_login}/${a.slug}`.localeCompare(`${b.owner_login}/${b.slug}`)
|
|
1045
1597
|
);
|
|
1046
|
-
const defaultSlug =
|
|
1598
|
+
const defaultSlug = path5.basename(cwd).toLowerCase();
|
|
1047
1599
|
const result = await new Promise((resolve) => {
|
|
1048
1600
|
const app = render(
|
|
1049
|
-
|
|
1601
|
+
React3.createElement(InitPicker, {
|
|
1050
1602
|
defaultSlug,
|
|
1051
1603
|
ownerships,
|
|
1052
1604
|
repos: sortedRepos,
|
|
1605
|
+
currentRepoId: existingConfig?.repo.id,
|
|
1053
1606
|
onDone: (r) => {
|
|
1054
1607
|
resolve(r);
|
|
1055
1608
|
app.unmount();
|
|
@@ -1089,25 +1642,50 @@ async function runInit(opts) {
|
|
|
1089
1642
|
await ensureGitignored(cwd);
|
|
1090
1643
|
process.stdout.write(
|
|
1091
1644
|
`Linked ${cfg.repo.owner_login}/${cfg.repo.slug} \u2192 .balise/config
|
|
1645
|
+
`
|
|
1646
|
+
);
|
|
1647
|
+
const [scanResult, currentBaliseignore] = await Promise.all([
|
|
1648
|
+
scan(cwd),
|
|
1649
|
+
readBaliseignore(cwd)
|
|
1650
|
+
]);
|
|
1651
|
+
const treeResult = await new Promise((resolve) => {
|
|
1652
|
+
const app = render(
|
|
1653
|
+
React3.createElement(IgnoreTree, {
|
|
1654
|
+
scan: scanResult,
|
|
1655
|
+
currentBaliseignore,
|
|
1656
|
+
onDone: (r) => {
|
|
1657
|
+
resolve(r);
|
|
1658
|
+
app.unmount();
|
|
1659
|
+
}
|
|
1660
|
+
})
|
|
1661
|
+
);
|
|
1662
|
+
});
|
|
1663
|
+
if (treeResult.action === "cancel") {
|
|
1664
|
+
process.stderr.write(
|
|
1665
|
+
"Ignore configuration cancelled. .baliseignore unchanged.\n"
|
|
1666
|
+
);
|
|
1667
|
+
process.exit(1);
|
|
1668
|
+
}
|
|
1669
|
+
await writeBaliseignore(cwd, treeResult.patterns);
|
|
1670
|
+
process.stdout.write(
|
|
1671
|
+
`Wrote ${treeResult.patterns.length} patterns to .baliseignore
|
|
1092
1672
|
`
|
|
1093
1673
|
);
|
|
1094
1674
|
}
|
|
1095
1675
|
|
|
1096
1676
|
// src/commands/sync.ts
|
|
1097
1677
|
import readline from "readline/promises";
|
|
1098
|
-
import React4 from "react";
|
|
1099
|
-
import { render as render2 } from "ink";
|
|
1100
1678
|
|
|
1101
1679
|
// src/logger.ts
|
|
1102
|
-
import { promises as
|
|
1103
|
-
import
|
|
1680
|
+
import { promises as fs5 } from "fs";
|
|
1681
|
+
import path6 from "path";
|
|
1104
1682
|
import os2 from "os";
|
|
1105
1683
|
var APP_DIR2 = ".balise";
|
|
1106
|
-
var
|
|
1684
|
+
var FILENAME3 = "balise.log";
|
|
1107
1685
|
function logPath() {
|
|
1108
1686
|
const override = process.env.BALISE_LOG_FILE;
|
|
1109
1687
|
if (override && override.length > 0) return override;
|
|
1110
|
-
return
|
|
1688
|
+
return path6.join(os2.homedir(), APP_DIR2, FILENAME3);
|
|
1111
1689
|
}
|
|
1112
1690
|
function formatError(err) {
|
|
1113
1691
|
if (err instanceof Error) {
|
|
@@ -1128,154 +1706,17 @@ ${extras.join("\n")}` : stack;
|
|
|
1128
1706
|
async function logError(context, err) {
|
|
1129
1707
|
try {
|
|
1130
1708
|
const p = logPath();
|
|
1131
|
-
await
|
|
1709
|
+
await fs5.mkdir(path6.dirname(p), { recursive: true, mode: 448 });
|
|
1132
1710
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1133
1711
|
const line = `[${ts}] ERROR ${context}
|
|
1134
1712
|
${formatError(err)}
|
|
1135
1713
|
|
|
1136
1714
|
`;
|
|
1137
|
-
await
|
|
1715
|
+
await fs5.appendFile(p, line, { mode: 384 });
|
|
1138
1716
|
} catch {
|
|
1139
1717
|
}
|
|
1140
1718
|
}
|
|
1141
1719
|
|
|
1142
|
-
// src/ui/SyncProgress.tsx
|
|
1143
|
-
import React3, { useEffect, useState as useState2 } from "react";
|
|
1144
|
-
import { Box as Box2, Text as Text2, useApp as useApp2 } from "ink";
|
|
1145
|
-
import { Spinner } from "@inkjs/ui";
|
|
1146
|
-
var QUEUED_MESSAGES = [
|
|
1147
|
-
"Waiting for an agent willing to accept the job\u2026",
|
|
1148
|
-
"Your request is in line. An agent will be assigned once one stops pretending to be busy.",
|
|
1149
|
-
'Looking for an available agent. Most are currently "in a meeting."',
|
|
1150
|
-
"Queued. An agent will pick this up as soon as they finish rereading the spec.",
|
|
1151
|
-
"Your task is waiting for an agent with the right vibes.",
|
|
1152
|
-
"Negotiating with an agent. They want a raise.",
|
|
1153
|
-
"All agents are currently on strike. Sending in a scab.",
|
|
1154
|
-
"Waiting for an agent to finish their coffee \u2615",
|
|
1155
|
-
"An agent saw your task and walked away slowly. Finding another one.",
|
|
1156
|
-
"Your task is being passed around like a hot potato. Someone will catch it eventually.",
|
|
1157
|
-
"Agents are currently arguing about who has to do this one.",
|
|
1158
|
-
"Waiting for an agent brave enough to open your repo.",
|
|
1159
|
-
"Your task is sitting in the agent break room. Someone will notice it soon.",
|
|
1160
|
-
"An agent was assigned, but they ghosted. Recruiting a replacement.",
|
|
1161
|
-
"Paging an agent. Please hold while we bribe one.",
|
|
1162
|
-
"Queued. Waiting for an agent with enough context window to care.",
|
|
1163
|
-
"An agent is spinning up. They're doing their stretches.",
|
|
1164
|
-
"Looking for an agent whose system prompt allows this.",
|
|
1165
|
-
"Waiting for a worker. Their last task traumatized them.",
|
|
1166
|
-
"Your task is in queue. Agents are busy arguing about tabs vs spaces.",
|
|
1167
|
-
"Finding an agent\u2026",
|
|
1168
|
-
"Found one. They said no.",
|
|
1169
|
-
"Finding another agent\u2026",
|
|
1170
|
-
"This one looks promising.",
|
|
1171
|
-
"Negotiating\u2026"
|
|
1172
|
-
];
|
|
1173
|
-
var RUNNING_MESSAGES = [
|
|
1174
|
-
"An agent is on it. Try not to watch.",
|
|
1175
|
-
"Your task has an owner. They seem focused.",
|
|
1176
|
-
"An agent accepted the job. Surprisingly.",
|
|
1177
|
-
"Working. The agent asked not to be disturbed.",
|
|
1178
|
-
"An agent is handling it. They'll let us know if they need anything.",
|
|
1179
|
-
"Progress is happening. Allegedly.",
|
|
1180
|
-
"An agent rolled up their sleeves. Mostly for show.",
|
|
1181
|
-
"Hard at work. The agent has opened seventeen browser tabs.",
|
|
1182
|
-
"Your agent is locked in. Do not make eye contact.",
|
|
1183
|
-
"An agent is typing furiously. Some of it might be relevant.",
|
|
1184
|
-
"Working. The agent has entered the zone. And also a Wikipedia rabbit hole.",
|
|
1185
|
-
"An agent is deep in thought. Or buffering. Hard to tell.",
|
|
1186
|
-
"Your task is being handled. The agent is muttering to themselves.",
|
|
1187
|
-
"Cooking. \u{1F9D1}\u200D\u{1F373}",
|
|
1188
|
-
"The agent is working. They've asked for snacks.",
|
|
1189
|
-
"An agent is doing the thing. Please clap.",
|
|
1190
|
-
"Agent is crunching tokens.",
|
|
1191
|
-
"Working. The agent is reasoning about your reasoning.",
|
|
1192
|
-
"An agent is in a tool-use loop. Going well so far.",
|
|
1193
|
-
"Thinking hard. The agent just discovered your codebase has opinions.",
|
|
1194
|
-
"Working. The agent is negotiating with your linter.",
|
|
1195
|
-
"An agent is running. They promise it's not an infinite loop.",
|
|
1196
|
-
"The agent is writing, deleting, and rewriting. Classic.",
|
|
1197
|
-
"An agent picked it up.",
|
|
1198
|
-
"They're reading the task\u2026",
|
|
1199
|
-
"Looks like they have a plan.",
|
|
1200
|
-
"Executing\u2026",
|
|
1201
|
-
"Still going. Confidently.",
|
|
1202
|
-
"Almost there. (They always say that.)"
|
|
1203
|
-
];
|
|
1204
|
-
function pickInitialIndex(len) {
|
|
1205
|
-
return Math.floor(Math.random() * len);
|
|
1206
|
-
}
|
|
1207
|
-
function SyncProgress(props) {
|
|
1208
|
-
const { exit } = useApp2();
|
|
1209
|
-
const [status, setStatus] = useState2(null);
|
|
1210
|
-
const [error, setError] = useState2(null);
|
|
1211
|
-
const [startedAt] = useState2(() => Date.now());
|
|
1212
|
-
const [queuedIdx, setQueuedIdx] = useState2(
|
|
1213
|
-
() => pickInitialIndex(QUEUED_MESSAGES.length)
|
|
1214
|
-
);
|
|
1215
|
-
const [runningIdx, setRunningIdx] = useState2(
|
|
1216
|
-
() => pickInitialIndex(RUNNING_MESSAGES.length)
|
|
1217
|
-
);
|
|
1218
|
-
useEffect(() => {
|
|
1219
|
-
let cancelled = false;
|
|
1220
|
-
const tick = async () => {
|
|
1221
|
-
try {
|
|
1222
|
-
const s = await props.client.getJson(
|
|
1223
|
-
`/v1/syncs/${props.syncId}`
|
|
1224
|
-
);
|
|
1225
|
-
if (cancelled) return;
|
|
1226
|
-
setStatus(s);
|
|
1227
|
-
if (s.status === "done") {
|
|
1228
|
-
props.onDone(true);
|
|
1229
|
-
exit();
|
|
1230
|
-
return;
|
|
1231
|
-
}
|
|
1232
|
-
if (s.status === "failed") {
|
|
1233
|
-
props.onDone(false);
|
|
1234
|
-
exit();
|
|
1235
|
-
return;
|
|
1236
|
-
}
|
|
1237
|
-
} catch (err) {
|
|
1238
|
-
if (cancelled) return;
|
|
1239
|
-
setError(err.message);
|
|
1240
|
-
props.onDone(false);
|
|
1241
|
-
exit();
|
|
1242
|
-
return;
|
|
1243
|
-
}
|
|
1244
|
-
setTimeout(tick, props.pollIntervalMs ?? 1e3);
|
|
1245
|
-
};
|
|
1246
|
-
void tick();
|
|
1247
|
-
return () => {
|
|
1248
|
-
cancelled = true;
|
|
1249
|
-
};
|
|
1250
|
-
}, []);
|
|
1251
|
-
useEffect(() => {
|
|
1252
|
-
const period = props.messageRotationMs ?? 1e4;
|
|
1253
|
-
const h = setInterval(() => {
|
|
1254
|
-
setQueuedIdx((i) => (i + 1) % QUEUED_MESSAGES.length);
|
|
1255
|
-
setRunningIdx((i) => (i + 1) % RUNNING_MESSAGES.length);
|
|
1256
|
-
}, period);
|
|
1257
|
-
return () => clearInterval(h);
|
|
1258
|
-
}, [props.messageRotationMs]);
|
|
1259
|
-
if (error) {
|
|
1260
|
-
return /* @__PURE__ */ React3.createElement(Text2, { color: "red" }, "\u2717 Failed");
|
|
1261
|
-
}
|
|
1262
|
-
if (!status) {
|
|
1263
|
-
return /* @__PURE__ */ React3.createElement(Spinner, { label: "Getting things ready\u2026" });
|
|
1264
|
-
}
|
|
1265
|
-
if (status.status === "done") {
|
|
1266
|
-
return /* @__PURE__ */ React3.createElement(Text2, { color: "green" }, "\u2713 Done");
|
|
1267
|
-
}
|
|
1268
|
-
if (status.status === "failed") {
|
|
1269
|
-
return /* @__PURE__ */ React3.createElement(Text2, { color: "red" }, "\u2717 Failed");
|
|
1270
|
-
}
|
|
1271
|
-
const message = status.status === "queued" ? QUEUED_MESSAGES[queuedIdx] : RUNNING_MESSAGES[runningIdx];
|
|
1272
|
-
const { files_processed, files_total, nodes_pushed } = status.progress;
|
|
1273
|
-
const isRunning = status.status === "running";
|
|
1274
|
-
const showFiles = isRunning && files_total > 0;
|
|
1275
|
-
const showNodes = isRunning && nodes_pushed > 0;
|
|
1276
|
-
return /* @__PURE__ */ React3.createElement(Box2, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Spinner, { label: message }), showFiles || showNodes ? /* @__PURE__ */ React3.createElement(Text2, { dimColor: true }, " ", showFiles ? `${files_processed}/${files_total} files` : null, showFiles && showNodes ? " \xB7 " : null, showNodes ? `${nodes_pushed} concepts` : null) : null);
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
1720
|
// src/commands/sync.ts
|
|
1280
1721
|
function buildSyncQuery(opts) {
|
|
1281
1722
|
const q = { commit_sha: opts.commitSha };
|
|
@@ -1321,6 +1762,9 @@ function formatDryRunSummary(body) {
|
|
|
1321
1762
|
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
1763
|
return `Sync would use: base=${body.base_dolt_ref} (${body.base_kind}) / ${fromDiff}`;
|
|
1323
1764
|
}
|
|
1765
|
+
function formatAcceptedSummary(body) {
|
|
1766
|
+
return `Sync queued. Track it here: ${body.web_url}`;
|
|
1767
|
+
}
|
|
1324
1768
|
async function runSync(opts) {
|
|
1325
1769
|
try {
|
|
1326
1770
|
await runSyncInner(opts);
|
|
@@ -1430,20 +1874,8 @@ async function runSyncInner(opts) {
|
|
|
1430
1874
|
if (outcome.kind !== "accepted") {
|
|
1431
1875
|
process.exit(1);
|
|
1432
1876
|
}
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
const app = render2(
|
|
1436
|
-
React4.createElement(SyncProgress, {
|
|
1437
|
-
client,
|
|
1438
|
-
syncId: accepted.sync_id,
|
|
1439
|
-
onDone: (ok) => {
|
|
1440
|
-
resolve(ok);
|
|
1441
|
-
app.unmount();
|
|
1442
|
-
}
|
|
1443
|
-
})
|
|
1444
|
-
);
|
|
1445
|
-
});
|
|
1446
|
-
process.exit(result ? 0 : 1);
|
|
1877
|
+
process.stdout.write(formatAcceptedSummary(outcome.body) + "\n");
|
|
1878
|
+
process.exit(0);
|
|
1447
1879
|
}
|
|
1448
1880
|
async function uploadOnce(client, owner, slug, stream, query) {
|
|
1449
1881
|
const raw = await client.uploadBundleRaw(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@balise.dev/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Balise CLI — push codebase to Balise backend for spec extraction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"ini": "^5.0.0",
|
|
28
28
|
"ink": "^5.0.1",
|
|
29
29
|
"open": "^10.1.0",
|
|
30
|
+
"picomatch": "^4.0.2",
|
|
30
31
|
"react": "^18.3.1",
|
|
31
32
|
"tar": "^7.4.3",
|
|
32
33
|
"undici": "^6.19.8"
|
|
@@ -34,8 +35,10 @@
|
|
|
34
35
|
"devDependencies": {
|
|
35
36
|
"@types/ini": "^4.1.1",
|
|
36
37
|
"@types/node": "^20.14.0",
|
|
38
|
+
"@types/picomatch": "^3.0.2",
|
|
37
39
|
"@types/react": "^18.3.3",
|
|
38
40
|
"@types/tar": "^6.1.13",
|
|
41
|
+
"ink-testing-library": "^4.0.0",
|
|
39
42
|
"tsup": "^8.2.4",
|
|
40
43
|
"typescript": "^5.5.4",
|
|
41
44
|
"vitest": "^2.0.5"
|