@balise.dev/cli 0.1.2 → 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 +907 -227
- package/package.json +5 -3
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",
|
|
@@ -613,9 +613,43 @@ var ApiClient = class {
|
|
|
613
613
|
};
|
|
614
614
|
});
|
|
615
615
|
}
|
|
616
|
+
/**
|
|
617
|
+
* Same wire as `uploadBundle` but returns the raw `(status, text)` pair so
|
|
618
|
+
* callers can branch on response shape (200 dry-run, 202 happy, 409
|
|
619
|
+
* idempotency, 422 invalid base) without `ApiError` swallowing the body.
|
|
620
|
+
*
|
|
621
|
+
* Token refresh and auth attachment behave identically; we still rely on
|
|
622
|
+
* `withAuth` for the 401-then-retry plumbing. Non-401 statuses are
|
|
623
|
+
* returned to the caller verbatim — `withAuth` only treats 401 specially.
|
|
624
|
+
*/
|
|
625
|
+
async uploadBundleRaw(path7, opts) {
|
|
626
|
+
await this.ensureFreshToken();
|
|
627
|
+
const qs = new URLSearchParams(opts.query).toString();
|
|
628
|
+
const url = `${joinUrl(this.opts.apiUrl, path7)}${qs ? `?${qs}` : ""}`;
|
|
629
|
+
let tokens = await loadTokens();
|
|
630
|
+
if (!tokens) throw new NotAuthenticatedError();
|
|
631
|
+
const exec = async (token) => {
|
|
632
|
+
const { statusCode, body } = await wrapNetwork(
|
|
633
|
+
() => request2(url, {
|
|
634
|
+
method: "POST",
|
|
635
|
+
headers: {
|
|
636
|
+
Authorization: `Bearer ${token}`,
|
|
637
|
+
"Content-Type": "application/octet-stream"
|
|
638
|
+
},
|
|
639
|
+
body: opts.bundleStream,
|
|
640
|
+
dispatcher: this.opts.dispatcher
|
|
641
|
+
})
|
|
642
|
+
);
|
|
643
|
+
const text = await body.text();
|
|
644
|
+
return { status: statusCode, text };
|
|
645
|
+
};
|
|
646
|
+
const first = await exec(tokens.access_token);
|
|
647
|
+
if (first.status !== 401) return first;
|
|
648
|
+
throw new NotAuthenticatedError();
|
|
649
|
+
}
|
|
616
650
|
};
|
|
617
|
-
function joinUrl(base,
|
|
618
|
-
return `${base.replace(/\/$/, "")}${
|
|
651
|
+
function joinUrl(base, path7) {
|
|
652
|
+
return `${base.replace(/\/$/, "")}${path7.startsWith("/") ? path7 : `/${path7}`}`;
|
|
619
653
|
}
|
|
620
654
|
|
|
621
655
|
// src/config.ts
|
|
@@ -700,8 +734,8 @@ account_id : ${me.account_id}
|
|
|
700
734
|
}
|
|
701
735
|
|
|
702
736
|
// src/commands/init.ts
|
|
703
|
-
import
|
|
704
|
-
import
|
|
737
|
+
import path5 from "path";
|
|
738
|
+
import React3 from "react";
|
|
705
739
|
import { render } from "ink";
|
|
706
740
|
|
|
707
741
|
// src/git.ts
|
|
@@ -795,7 +829,6 @@ async function gitBundle(opts) {
|
|
|
795
829
|
}
|
|
796
830
|
|
|
797
831
|
// src/auth-ensure.ts
|
|
798
|
-
import readline from "readline/promises";
|
|
799
832
|
import crypto3 from "crypto";
|
|
800
833
|
import open2 from "open";
|
|
801
834
|
var LoginDeclinedError = class extends Error {
|
|
@@ -808,6 +841,7 @@ function isInteractive(stdin) {
|
|
|
808
841
|
const s = stdin ?? process.stdin;
|
|
809
842
|
return Boolean(s.isTTY);
|
|
810
843
|
}
|
|
844
|
+
var LOGIN_AUTOLAUNCH_MESSAGE = "Not logged in, launching balise login...";
|
|
811
845
|
async function ensureAuthenticated(opts) {
|
|
812
846
|
const existing = await loadTokens();
|
|
813
847
|
if (existing) return;
|
|
@@ -815,19 +849,8 @@ async function ensureAuthenticated(opts) {
|
|
|
815
849
|
if (!isInteractive(opts.stdin)) {
|
|
816
850
|
throw new LoginDeclinedError();
|
|
817
851
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
output: opts.stdout ?? process.stdout
|
|
821
|
-
});
|
|
822
|
-
let answer;
|
|
823
|
-
try {
|
|
824
|
-
answer = (await rl.question("Not logged in. Login now? (Y/n) ")).trim();
|
|
825
|
-
} finally {
|
|
826
|
-
rl.close();
|
|
827
|
-
}
|
|
828
|
-
if (answer && !/^y(es)?$/i.test(answer)) {
|
|
829
|
-
throw new LoginDeclinedError();
|
|
830
|
-
}
|
|
852
|
+
stderr.write(`${LOGIN_AUTOLAUNCH_MESSAGE}
|
|
853
|
+
`);
|
|
831
854
|
const verifier = generateCodeVerifier();
|
|
832
855
|
const challenge = codeChallengeFor(verifier);
|
|
833
856
|
const state = crypto3.randomBytes(16).toString("hex");
|
|
@@ -892,55 +915,626 @@ ${credentialsHelpMessage()}
|
|
|
892
915
|
// src/ui/InitPicker.tsx
|
|
893
916
|
import React, { useState } from "react";
|
|
894
917
|
import { Box, Text, useApp, useInput } from "ink";
|
|
918
|
+
import { Select, TextInput } from "@inkjs/ui";
|
|
919
|
+
function normalizeSlug(raw) {
|
|
920
|
+
return raw.toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
921
|
+
}
|
|
895
922
|
function InitPicker(props) {
|
|
896
923
|
const { exit } = useApp();
|
|
897
|
-
const
|
|
898
|
-
|
|
924
|
+
const currentMatches = props.currentRepoId !== void 0 && props.repos.some((r) => r.id === props.currentRepoId);
|
|
925
|
+
const [field, setField] = useState(currentMatches ? "link" : "slug");
|
|
926
|
+
const [slug, setSlug] = useState(normalizeSlug(props.defaultSlug));
|
|
927
|
+
const [ownerId, setOwnerId] = useState(
|
|
928
|
+
props.ownerships[0]?.id
|
|
899
929
|
);
|
|
900
|
-
const [
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
930
|
+
const [repoId, setRepoId] = useState(
|
|
931
|
+
currentMatches ? props.currentRepoId : props.repos[0]?.id
|
|
932
|
+
);
|
|
933
|
+
const submit = () => {
|
|
934
|
+
if (field === "link") {
|
|
935
|
+
const repo = props.repos.find((r) => r.id === repoId);
|
|
936
|
+
if (!repo) return;
|
|
937
|
+
props.onDone({ action: "link", repo });
|
|
938
|
+
} else {
|
|
939
|
+
const owner = props.ownerships.find((o) => o.id === ownerId);
|
|
940
|
+
if (!owner || !slug) return;
|
|
941
|
+
props.onDone({ action: "create", slug, owner });
|
|
942
|
+
}
|
|
943
|
+
exit();
|
|
944
|
+
};
|
|
945
|
+
useInput((_input, key) => {
|
|
904
946
|
if (key.escape) {
|
|
905
947
|
props.onDone({ action: "cancel" });
|
|
906
948
|
exit();
|
|
907
949
|
return;
|
|
908
950
|
}
|
|
909
951
|
if (key.tab) {
|
|
910
|
-
|
|
952
|
+
setField((f) => {
|
|
953
|
+
if (f === "slug") return "owner";
|
|
954
|
+
if (f === "owner") return props.repos.length > 0 ? "link" : "slug";
|
|
955
|
+
return "slug";
|
|
956
|
+
});
|
|
911
957
|
return;
|
|
912
958
|
}
|
|
913
959
|
if (key.return) {
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
960
|
+
submit();
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
const ownerOptions = props.ownerships.map((o) => ({
|
|
964
|
+
label: `${o.login} (${o.type})`,
|
|
965
|
+
value: o.id
|
|
966
|
+
}));
|
|
967
|
+
const repoOptions = props.repos.map((r) => ({
|
|
968
|
+
label: r.id === props.currentRepoId ? `${r.owner_login}/${r.slug} (current)` : `${r.owner_login}/${r.slug}`,
|
|
969
|
+
value: r.id
|
|
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(
|
|
972
|
+
TextInput,
|
|
973
|
+
{
|
|
974
|
+
defaultValue: slug,
|
|
975
|
+
placeholder: "repo-slug",
|
|
976
|
+
isDisabled: field !== "slug",
|
|
977
|
+
onChange: (v) => setSlug(normalizeSlug(v))
|
|
978
|
+
}
|
|
979
|
+
))), /* @__PURE__ */ React.createElement(Box, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React.createElement(Text, { color: field === "owner" ? "cyan" : "gray" }, field === "owner" ? "\u25B8 " : " ", "Owner"), /* @__PURE__ */ React.createElement(Box, { marginLeft: 2 }, field === "owner" ? /* @__PURE__ */ React.createElement(
|
|
980
|
+
Select,
|
|
981
|
+
{
|
|
982
|
+
options: ownerOptions,
|
|
983
|
+
defaultValue: ownerId,
|
|
984
|
+
onChange: setOwnerId
|
|
985
|
+
}
|
|
986
|
+
) : /* @__PURE__ */ React.createElement(Text, { dimColor: true }, ownerOptions.find((o) => o.value === ownerId)?.label ?? "\u2014"))), props.repos.length > 0 && /* @__PURE__ */ React.createElement(Box, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React.createElement(Text, { color: field === "link" ? "cyan" : "gray" }, field === "link" ? "\u25B8 " : " ", "Link existing (", props.repos.length, ")"), /* @__PURE__ */ React.createElement(Box, { marginLeft: 2 }, field === "link" ? /* @__PURE__ */ React.createElement(
|
|
987
|
+
Select,
|
|
988
|
+
{
|
|
989
|
+
options: repoOptions,
|
|
990
|
+
defaultValue: repoId,
|
|
991
|
+
onChange: setRepoId
|
|
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")));
|
|
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
|
|
920
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);
|
|
921
1324
|
} else {
|
|
922
|
-
|
|
923
|
-
|
|
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;
|
|
924
1348
|
}
|
|
925
|
-
exit();
|
|
926
1349
|
return;
|
|
927
1350
|
}
|
|
928
|
-
if (
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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
|
+
}
|
|
936
1409
|
} else {
|
|
937
|
-
if (key.
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
+
}
|
|
941
1423
|
}
|
|
942
1424
|
});
|
|
943
|
-
|
|
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
|
+
);
|
|
944
1538
|
}
|
|
945
1539
|
|
|
946
1540
|
// src/commands/init.ts
|
|
@@ -966,6 +1560,13 @@ async function runInit(opts) {
|
|
|
966
1560
|
}
|
|
967
1561
|
throw err;
|
|
968
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
|
+
}
|
|
969
1570
|
const apiUrl = DEFAULT_API_URL;
|
|
970
1571
|
const client = new ApiClient({
|
|
971
1572
|
apiUrl,
|
|
@@ -994,13 +1595,14 @@ async function runInit(opts) {
|
|
|
994
1595
|
const sortedRepos = [...repos].sort(
|
|
995
1596
|
(a, b) => `${a.owner_login}/${a.slug}`.localeCompare(`${b.owner_login}/${b.slug}`)
|
|
996
1597
|
);
|
|
997
|
-
const defaultSlug =
|
|
1598
|
+
const defaultSlug = path5.basename(cwd).toLowerCase();
|
|
998
1599
|
const result = await new Promise((resolve) => {
|
|
999
1600
|
const app = render(
|
|
1000
|
-
|
|
1601
|
+
React3.createElement(InitPicker, {
|
|
1001
1602
|
defaultSlug,
|
|
1002
1603
|
ownerships,
|
|
1003
1604
|
repos: sortedRepos,
|
|
1605
|
+
currentRepoId: existingConfig?.repo.id,
|
|
1004
1606
|
onDone: (r) => {
|
|
1005
1607
|
resolve(r);
|
|
1006
1608
|
app.unmount();
|
|
@@ -1040,160 +1642,143 @@ async function runInit(opts) {
|
|
|
1040
1642
|
await ensureGitignored(cwd);
|
|
1041
1643
|
process.stdout.write(
|
|
1042
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
|
|
1043
1672
|
`
|
|
1044
1673
|
);
|
|
1045
1674
|
}
|
|
1046
1675
|
|
|
1047
1676
|
// src/commands/sync.ts
|
|
1048
|
-
import
|
|
1049
|
-
import { render as render2 } from "ink";
|
|
1677
|
+
import readline from "readline/promises";
|
|
1050
1678
|
|
|
1051
|
-
// src/
|
|
1052
|
-
import
|
|
1053
|
-
import
|
|
1054
|
-
import
|
|
1055
|
-
var
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
"Negotiating with an agent. They want a raise.",
|
|
1062
|
-
"All agents are currently on strike. Sending in a scab.",
|
|
1063
|
-
"Waiting for an agent to finish their coffee \u2615",
|
|
1064
|
-
"An agent saw your task and walked away slowly. Finding another one.",
|
|
1065
|
-
"Your task is being passed around like a hot potato. Someone will catch it eventually.",
|
|
1066
|
-
"Agents are currently arguing about who has to do this one.",
|
|
1067
|
-
"Waiting for an agent brave enough to open your repo.",
|
|
1068
|
-
"Your task is sitting in the agent break room. Someone will notice it soon.",
|
|
1069
|
-
"An agent was assigned, but they ghosted. Recruiting a replacement.",
|
|
1070
|
-
"Paging an agent. Please hold while we bribe one.",
|
|
1071
|
-
"Queued. Waiting for an agent with enough context window to care.",
|
|
1072
|
-
"An agent is spinning up. They're doing their stretches.",
|
|
1073
|
-
"Looking for an agent whose system prompt allows this.",
|
|
1074
|
-
"Waiting for a worker. Their last task traumatized them.",
|
|
1075
|
-
"Your task is in queue. Agents are busy arguing about tabs vs spaces.",
|
|
1076
|
-
"Finding an agent\u2026",
|
|
1077
|
-
"Found one. They said no.",
|
|
1078
|
-
"Finding another agent\u2026",
|
|
1079
|
-
"This one looks promising.",
|
|
1080
|
-
"Negotiating\u2026"
|
|
1081
|
-
];
|
|
1082
|
-
var RUNNING_MESSAGES = [
|
|
1083
|
-
"An agent is on it. Try not to watch.",
|
|
1084
|
-
"Your task has an owner. They seem focused.",
|
|
1085
|
-
"An agent accepted the job. Surprisingly.",
|
|
1086
|
-
"Working. The agent asked not to be disturbed.",
|
|
1087
|
-
"An agent is handling it. They'll let us know if they need anything.",
|
|
1088
|
-
"Progress is happening. Allegedly.",
|
|
1089
|
-
"An agent rolled up their sleeves. Mostly for show.",
|
|
1090
|
-
"Hard at work. The agent has opened seventeen browser tabs.",
|
|
1091
|
-
"Your agent is locked in. Do not make eye contact.",
|
|
1092
|
-
"An agent is typing furiously. Some of it might be relevant.",
|
|
1093
|
-
"Working. The agent has entered the zone. And also a Wikipedia rabbit hole.",
|
|
1094
|
-
"An agent is deep in thought. Or buffering. Hard to tell.",
|
|
1095
|
-
"Your task is being handled. The agent is muttering to themselves.",
|
|
1096
|
-
"Cooking. \u{1F9D1}\u200D\u{1F373}",
|
|
1097
|
-
"The agent is working. They've asked for snacks.",
|
|
1098
|
-
"An agent is doing the thing. Please clap.",
|
|
1099
|
-
"Agent is crunching tokens.",
|
|
1100
|
-
"Working. The agent is reasoning about your reasoning.",
|
|
1101
|
-
"An agent is in a tool-use loop. Going well so far.",
|
|
1102
|
-
"Thinking hard. The agent just discovered your codebase has opinions.",
|
|
1103
|
-
"Working. The agent is negotiating with your linter.",
|
|
1104
|
-
"An agent is running. They promise it's not an infinite loop.",
|
|
1105
|
-
"The agent is writing, deleting, and rewriting. Classic.",
|
|
1106
|
-
"An agent picked it up.",
|
|
1107
|
-
"They're reading the task\u2026",
|
|
1108
|
-
"Looks like they have a plan.",
|
|
1109
|
-
"Executing\u2026",
|
|
1110
|
-
"Still going. Confidently.",
|
|
1111
|
-
"Almost there. (They always say that.)"
|
|
1112
|
-
];
|
|
1113
|
-
function pickInitialIndex(len) {
|
|
1114
|
-
return Math.floor(Math.random() * len);
|
|
1679
|
+
// src/logger.ts
|
|
1680
|
+
import { promises as fs5 } from "fs";
|
|
1681
|
+
import path6 from "path";
|
|
1682
|
+
import os2 from "os";
|
|
1683
|
+
var APP_DIR2 = ".balise";
|
|
1684
|
+
var FILENAME3 = "balise.log";
|
|
1685
|
+
function logPath() {
|
|
1686
|
+
const override = process.env.BALISE_LOG_FILE;
|
|
1687
|
+
if (override && override.length > 0) return override;
|
|
1688
|
+
return path6.join(os2.homedir(), APP_DIR2, FILENAME3);
|
|
1115
1689
|
}
|
|
1116
|
-
function
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
return;
|
|
1145
|
-
}
|
|
1146
|
-
} catch (err) {
|
|
1147
|
-
if (cancelled) return;
|
|
1148
|
-
setError(err.message);
|
|
1149
|
-
props.onDone(false);
|
|
1150
|
-
exit();
|
|
1151
|
-
return;
|
|
1152
|
-
}
|
|
1153
|
-
setTimeout(tick, props.pollIntervalMs ?? 1e3);
|
|
1154
|
-
};
|
|
1155
|
-
void tick();
|
|
1156
|
-
return () => {
|
|
1157
|
-
cancelled = true;
|
|
1158
|
-
};
|
|
1159
|
-
}, []);
|
|
1160
|
-
useEffect(() => {
|
|
1161
|
-
const period = props.messageRotationMs ?? 1e4;
|
|
1162
|
-
const h = setInterval(() => {
|
|
1163
|
-
setQueuedIdx((i) => (i + 1) % QUEUED_MESSAGES.length);
|
|
1164
|
-
setRunningIdx((i) => (i + 1) % RUNNING_MESSAGES.length);
|
|
1165
|
-
}, period);
|
|
1166
|
-
return () => clearInterval(h);
|
|
1167
|
-
}, [props.messageRotationMs]);
|
|
1168
|
-
if (error) {
|
|
1169
|
-
return /* @__PURE__ */ React3.createElement(Text2, { color: "red" }, "\u2717 Failed");
|
|
1170
|
-
}
|
|
1171
|
-
if (!status) {
|
|
1172
|
-
return /* @__PURE__ */ React3.createElement(Box2, null, /* @__PURE__ */ React3.createElement(Text2, { color: "cyan" }, /* @__PURE__ */ React3.createElement(Spinner, { type: "dots" })), /* @__PURE__ */ React3.createElement(Text2, null, " Getting things ready\u2026"));
|
|
1173
|
-
}
|
|
1174
|
-
if (status.status === "done") {
|
|
1175
|
-
return /* @__PURE__ */ React3.createElement(Text2, { color: "green" }, "\u2713 Done");
|
|
1176
|
-
}
|
|
1177
|
-
if (status.status === "failed") {
|
|
1178
|
-
return /* @__PURE__ */ React3.createElement(Text2, { color: "red" }, "\u2717 Failed");
|
|
1179
|
-
}
|
|
1180
|
-
const message = status.status === "queued" ? QUEUED_MESSAGES[queuedIdx] : RUNNING_MESSAGES[runningIdx];
|
|
1181
|
-
const { files_processed, files_total, nodes_pushed } = status.progress;
|
|
1182
|
-
const hasCounters = status.status === "running" && (files_total > 0 || nodes_pushed > 0);
|
|
1183
|
-
return /* @__PURE__ */ React3.createElement(Box2, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Box2, null, /* @__PURE__ */ React3.createElement(Text2, { color: "cyan" }, /* @__PURE__ */ React3.createElement(Spinner, { type: "dots" })), /* @__PURE__ */ React3.createElement(Text2, null, " ", message)), hasCounters ? /* @__PURE__ */ React3.createElement(Text2, { dimColor: true }, " ", files_processed, "/", files_total, " files \xB7 ", nodes_pushed, " concepts") : null);
|
|
1690
|
+
function formatError(err) {
|
|
1691
|
+
if (err instanceof Error) {
|
|
1692
|
+
const stack = err.stack ?? `${err.name}: ${err.message}`;
|
|
1693
|
+
const extras = [];
|
|
1694
|
+
const cause = err.cause;
|
|
1695
|
+
if (cause) extras.push(` caused by: ${formatError(cause)}`);
|
|
1696
|
+
return extras.length ? `${stack}
|
|
1697
|
+
${extras.join("\n")}` : stack;
|
|
1698
|
+
}
|
|
1699
|
+
if (typeof err === "string") return err;
|
|
1700
|
+
try {
|
|
1701
|
+
return JSON.stringify(err);
|
|
1702
|
+
} catch {
|
|
1703
|
+
return String(err);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
async function logError(context, err) {
|
|
1707
|
+
try {
|
|
1708
|
+
const p = logPath();
|
|
1709
|
+
await fs5.mkdir(path6.dirname(p), { recursive: true, mode: 448 });
|
|
1710
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1711
|
+
const line = `[${ts}] ERROR ${context}
|
|
1712
|
+
${formatError(err)}
|
|
1713
|
+
|
|
1714
|
+
`;
|
|
1715
|
+
await fs5.appendFile(p, line, { mode: 384 });
|
|
1716
|
+
} catch {
|
|
1717
|
+
}
|
|
1184
1718
|
}
|
|
1185
1719
|
|
|
1186
1720
|
// src/commands/sync.ts
|
|
1721
|
+
function buildSyncQuery(opts) {
|
|
1722
|
+
const q = { commit_sha: opts.commitSha };
|
|
1723
|
+
if (opts.branch) q.branch = opts.branch;
|
|
1724
|
+
q.force = opts.force ? "true" : "false";
|
|
1725
|
+
q.dry_run = opts.dryRun ? "true" : "false";
|
|
1726
|
+
if (opts.base) q.base = opts.base;
|
|
1727
|
+
return q;
|
|
1728
|
+
}
|
|
1729
|
+
var INIT_AUTOLAUNCH_MESSAGE = "Repo not initialized, launching balise init...";
|
|
1730
|
+
function drainStdinBuffer(stdin) {
|
|
1731
|
+
if (stdin !== process.stdin) return;
|
|
1732
|
+
const s = stdin;
|
|
1733
|
+
if (typeof s.read !== "function") return;
|
|
1734
|
+
while (s.read() !== null) {
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
async function confirmRetryWithForce(opts) {
|
|
1738
|
+
const stderr = opts.stderr ?? process.stderr;
|
|
1739
|
+
const synced = opts.body.synced_at ?? "unknown time";
|
|
1740
|
+
stderr.write(
|
|
1741
|
+
`commit already synced at tag ${opts.body.tag} (synced at ${synced}), re-run ?
|
|
1742
|
+
`
|
|
1743
|
+
);
|
|
1744
|
+
if (opts.autoConfirm) {
|
|
1745
|
+
return false;
|
|
1746
|
+
}
|
|
1747
|
+
const input = opts.stdin ?? process.stdin;
|
|
1748
|
+
drainStdinBuffer(input);
|
|
1749
|
+
const rl = readline.createInterface({
|
|
1750
|
+
input,
|
|
1751
|
+
output: opts.stdout ?? process.stdout
|
|
1752
|
+
});
|
|
1753
|
+
let answer;
|
|
1754
|
+
try {
|
|
1755
|
+
answer = (await rl.question("re-run ? (y/N) ")).trim();
|
|
1756
|
+
} finally {
|
|
1757
|
+
rl.close();
|
|
1758
|
+
}
|
|
1759
|
+
return /^y(es)?$/i.test(answer);
|
|
1760
|
+
}
|
|
1761
|
+
function formatDryRunSummary(body) {
|
|
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}` : "");
|
|
1763
|
+
return `Sync would use: base=${body.base_dolt_ref} (${body.base_kind}) / ${fromDiff}`;
|
|
1764
|
+
}
|
|
1765
|
+
function formatAcceptedSummary(body) {
|
|
1766
|
+
return `Sync queued. Track it here: ${body.web_url}`;
|
|
1767
|
+
}
|
|
1187
1768
|
async function runSync(opts) {
|
|
1188
1769
|
try {
|
|
1189
1770
|
await runSyncInner(opts);
|
|
1190
1771
|
} catch (err) {
|
|
1772
|
+
await logError("sync", err);
|
|
1191
1773
|
if (err instanceof ApiUnreachableError) {
|
|
1192
1774
|
process.stderr.write(
|
|
1193
1775
|
"Cannot reach the Balise service. Please try again in a moment.\n"
|
|
1194
1776
|
);
|
|
1195
1777
|
} else {
|
|
1196
|
-
process.stderr.write(
|
|
1778
|
+
process.stderr.write(
|
|
1779
|
+
`Something went wrong. Please try again. (details: ${logPath()})
|
|
1780
|
+
`
|
|
1781
|
+
);
|
|
1197
1782
|
}
|
|
1198
1783
|
process.exit(1);
|
|
1199
1784
|
}
|
|
@@ -1222,7 +1807,8 @@ async function runSyncInner(opts) {
|
|
|
1222
1807
|
}
|
|
1223
1808
|
let cfg = await readConfig(cwd);
|
|
1224
1809
|
if (!cfg) {
|
|
1225
|
-
process.stderr.write(
|
|
1810
|
+
process.stderr.write(`${INIT_AUTOLAUNCH_MESSAGE}
|
|
1811
|
+
`);
|
|
1226
1812
|
await runInit({ ...opts, cwd });
|
|
1227
1813
|
cfg = await readConfig(cwd);
|
|
1228
1814
|
if (!cfg) {
|
|
@@ -1243,65 +1829,131 @@ async function runSyncInner(opts) {
|
|
|
1243
1829
|
`Packing ${cfg.repo.owner_login}/${cfg.repo.slug} at HEAD\u2026
|
|
1244
1830
|
`
|
|
1245
1831
|
);
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1832
|
+
const submit = async (force) => {
|
|
1833
|
+
const { stream, commitSha, branch } = await gitBundle({ cwd });
|
|
1834
|
+
const query = buildSyncQuery({
|
|
1835
|
+
commitSha,
|
|
1836
|
+
branch,
|
|
1837
|
+
force,
|
|
1838
|
+
base: opts.base,
|
|
1839
|
+
dryRun: opts.dryRun ?? false
|
|
1840
|
+
});
|
|
1841
|
+
return uploadOnce(client, cfg.repo.owner_login, cfg.repo.slug, stream, query);
|
|
1842
|
+
};
|
|
1843
|
+
let outcome;
|
|
1248
1844
|
try {
|
|
1249
|
-
|
|
1250
|
-
`/v1/repos/${cfg.repo.owner_login}/${cfg.repo.slug}/sync`,
|
|
1251
|
-
{
|
|
1252
|
-
bundleStream: stream,
|
|
1253
|
-
query: { commit_sha: commitSha, branch }
|
|
1254
|
-
}
|
|
1255
|
-
);
|
|
1845
|
+
outcome = await submit(opts.force ?? false);
|
|
1256
1846
|
} catch (err) {
|
|
1257
|
-
|
|
1258
|
-
|
|
1847
|
+
handleUploadError(err);
|
|
1848
|
+
process.exit(1);
|
|
1849
|
+
}
|
|
1850
|
+
if (outcome.kind === "conflict") {
|
|
1851
|
+
const proceedForce = await confirmRetryWithForce({
|
|
1852
|
+
body: outcome.body,
|
|
1853
|
+
autoConfirm: opts.autoConfirm ?? false
|
|
1854
|
+
});
|
|
1855
|
+
if (!proceedForce) {
|
|
1259
1856
|
process.exit(1);
|
|
1260
1857
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
);
|
|
1858
|
+
try {
|
|
1859
|
+
outcome = await submit(true);
|
|
1860
|
+
} catch (err) {
|
|
1861
|
+
handleUploadError(err);
|
|
1265
1862
|
process.exit(1);
|
|
1266
1863
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1864
|
+
}
|
|
1865
|
+
if (outcome.kind === "invalid_base") {
|
|
1866
|
+
process.stderr.write(`invalid base ref: ${opts.base}
|
|
1867
|
+
`);
|
|
1270
1868
|
process.exit(1);
|
|
1271
1869
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1870
|
+
if (outcome.kind === "dry_run") {
|
|
1871
|
+
process.stdout.write(formatDryRunSummary(outcome.body) + "\n");
|
|
1872
|
+
process.exit(0);
|
|
1873
|
+
}
|
|
1874
|
+
if (outcome.kind !== "accepted") {
|
|
1875
|
+
process.exit(1);
|
|
1876
|
+
}
|
|
1877
|
+
process.stdout.write(formatAcceptedSummary(outcome.body) + "\n");
|
|
1878
|
+
process.exit(0);
|
|
1879
|
+
}
|
|
1880
|
+
async function uploadOnce(client, owner, slug, stream, query) {
|
|
1881
|
+
const raw = await client.uploadBundleRaw(
|
|
1882
|
+
`/v1/repos/${owner}/${slug}/sync`,
|
|
1883
|
+
{
|
|
1884
|
+
bundleStream: stream,
|
|
1885
|
+
query
|
|
1886
|
+
}
|
|
1887
|
+
);
|
|
1888
|
+
if (raw.status === 202) {
|
|
1889
|
+
return { kind: "accepted", body: JSON.parse(raw.text) };
|
|
1890
|
+
}
|
|
1891
|
+
if (raw.status === 200) {
|
|
1892
|
+
return { kind: "dry_run", body: JSON.parse(raw.text) };
|
|
1893
|
+
}
|
|
1894
|
+
if (raw.status === 409) {
|
|
1895
|
+
const parsed = JSON.parse(raw.text);
|
|
1896
|
+
const body = "detail" in parsed && parsed.detail ? parsed.detail : parsed;
|
|
1897
|
+
return { kind: "conflict", body };
|
|
1898
|
+
}
|
|
1899
|
+
if (raw.status === 422) {
|
|
1900
|
+
let detail;
|
|
1901
|
+
try {
|
|
1902
|
+
const parsed = JSON.parse(raw.text);
|
|
1903
|
+
if (typeof parsed.detail === "string") detail = parsed.detail;
|
|
1904
|
+
} catch {
|
|
1905
|
+
}
|
|
1906
|
+
if (detail === "invalid_base_ref") return { kind: "invalid_base" };
|
|
1907
|
+
throw new ApiError(raw.status, raw.text);
|
|
1908
|
+
}
|
|
1909
|
+
throw new ApiError(raw.status, raw.text);
|
|
1910
|
+
}
|
|
1911
|
+
function handleUploadError(err) {
|
|
1912
|
+
if (err instanceof NotAuthenticatedError) {
|
|
1913
|
+
process.stderr.write("Not logged in \u2014 run `balise login`.\n");
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
if (err instanceof ApiUnreachableError) {
|
|
1917
|
+
process.stderr.write(
|
|
1918
|
+
"Cannot reach the Balise service. Please try again in a moment.\n"
|
|
1282
1919
|
);
|
|
1283
|
-
|
|
1284
|
-
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
process.stderr.write(
|
|
1923
|
+
"Something went wrong while uploading. Please try again.\n"
|
|
1924
|
+
);
|
|
1285
1925
|
}
|
|
1286
1926
|
|
|
1287
1927
|
// src/index.ts
|
|
1928
|
+
async function withLog(name, fn) {
|
|
1929
|
+
try {
|
|
1930
|
+
await fn();
|
|
1931
|
+
} catch (err) {
|
|
1932
|
+
await logError(name, err);
|
|
1933
|
+
process.stderr.write(
|
|
1934
|
+
`Something went wrong (${name}). Details: ${logPath()}
|
|
1935
|
+
`
|
|
1936
|
+
);
|
|
1937
|
+
process.exit(1);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1288
1940
|
var SUPABASE_URL = process.env.BALISE_SUPABASE_URL ?? "https://api.balise.dev";
|
|
1289
1941
|
var loginCmd = defineCommand({
|
|
1290
1942
|
meta: { name: "login", description: "Authenticate via OAuth (PKCE loopback)." },
|
|
1291
1943
|
async run() {
|
|
1292
|
-
await runLogin({ supabaseUrl: SUPABASE_URL });
|
|
1944
|
+
await withLog("login", () => runLogin({ supabaseUrl: SUPABASE_URL }));
|
|
1293
1945
|
}
|
|
1294
1946
|
});
|
|
1295
1947
|
var logoutCmd = defineCommand({
|
|
1296
1948
|
meta: { name: "logout", description: "Clear stored credentials." },
|
|
1297
1949
|
async run() {
|
|
1298
|
-
await runLogout();
|
|
1950
|
+
await withLog("logout", () => runLogout());
|
|
1299
1951
|
}
|
|
1300
1952
|
});
|
|
1301
1953
|
var whoamiCmd = defineCommand({
|
|
1302
1954
|
meta: { name: "whoami", description: "Show current authenticated user." },
|
|
1303
1955
|
async run() {
|
|
1304
|
-
await runWhoami({ supabaseUrl: SUPABASE_URL });
|
|
1956
|
+
await withLog("whoami", () => runWhoami({ supabaseUrl: SUPABASE_URL }));
|
|
1305
1957
|
}
|
|
1306
1958
|
});
|
|
1307
1959
|
var initCmd = defineCommand({
|
|
@@ -1310,7 +1962,7 @@ var initCmd = defineCommand({
|
|
|
1310
1962
|
description: "Link or create a Balise repo and write .balise/config."
|
|
1311
1963
|
},
|
|
1312
1964
|
async run() {
|
|
1313
|
-
await runInit({ supabaseUrl: SUPABASE_URL });
|
|
1965
|
+
await withLog("init", () => runInit({ supabaseUrl: SUPABASE_URL }));
|
|
1314
1966
|
}
|
|
1315
1967
|
});
|
|
1316
1968
|
var syncCmd = defineCommand({
|
|
@@ -1318,8 +1970,36 @@ var syncCmd = defineCommand({
|
|
|
1318
1970
|
name: "sync",
|
|
1319
1971
|
description: "Tarball current repo \u2192 upload \u2192 poll extraction progress."
|
|
1320
1972
|
},
|
|
1321
|
-
|
|
1322
|
-
|
|
1973
|
+
args: {
|
|
1974
|
+
yes: {
|
|
1975
|
+
type: "boolean",
|
|
1976
|
+
alias: "y",
|
|
1977
|
+
description: "Skip sync confirmations (warning prompt + future 409 retry).",
|
|
1978
|
+
default: false
|
|
1979
|
+
},
|
|
1980
|
+
force: {
|
|
1981
|
+
type: "boolean",
|
|
1982
|
+
description: "Bypass idempotency 409 + resume-reuse: re-tag from scratch and re-resolve base.",
|
|
1983
|
+
default: false
|
|
1984
|
+
},
|
|
1985
|
+
base: {
|
|
1986
|
+
type: "string",
|
|
1987
|
+
description: "Explicit base balise ref (branch/tag/commit) to start from. Validated server-side."
|
|
1988
|
+
},
|
|
1989
|
+
"dry-run": {
|
|
1990
|
+
type: "boolean",
|
|
1991
|
+
description: "Preview the resolution without uploading the bundle or enqueuing the worker.",
|
|
1992
|
+
default: false
|
|
1993
|
+
}
|
|
1994
|
+
},
|
|
1995
|
+
async run({ args }) {
|
|
1996
|
+
await runSync({
|
|
1997
|
+
supabaseUrl: SUPABASE_URL,
|
|
1998
|
+
autoConfirm: Boolean(args.yes),
|
|
1999
|
+
force: Boolean(args.force),
|
|
2000
|
+
base: typeof args.base === "string" && args.base.length > 0 ? args.base : void 0,
|
|
2001
|
+
dryRun: Boolean(args["dry-run"])
|
|
2002
|
+
});
|
|
1323
2003
|
}
|
|
1324
2004
|
});
|
|
1325
2005
|
var main = defineCommand({
|
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": {
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
"node": ">=20"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
+
"@inkjs/ui": "^2.0.0",
|
|
25
26
|
"citty": "^0.2.2",
|
|
26
27
|
"ini": "^5.0.0",
|
|
27
28
|
"ink": "^5.0.1",
|
|
28
|
-
"ink-progress-bar": "^3.0.0",
|
|
29
|
-
"ink-spinner": "^5.0.0",
|
|
30
29
|
"open": "^10.1.0",
|
|
30
|
+
"picomatch": "^4.0.2",
|
|
31
31
|
"react": "^18.3.1",
|
|
32
32
|
"tar": "^7.4.3",
|
|
33
33
|
"undici": "^6.19.8"
|
|
@@ -35,8 +35,10 @@
|
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/ini": "^4.1.1",
|
|
37
37
|
"@types/node": "^20.14.0",
|
|
38
|
+
"@types/picomatch": "^3.0.2",
|
|
38
39
|
"@types/react": "^18.3.3",
|
|
39
40
|
"@types/tar": "^6.1.13",
|
|
41
|
+
"ink-testing-library": "^4.0.0",
|
|
40
42
|
"tsup": "^8.2.4",
|
|
41
43
|
"typescript": "^5.5.4",
|
|
42
44
|
"vitest": "^2.0.5"
|