@headband/cli 0.6.1 → 0.7.1
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 +27 -10
- package/dist/index.js +202 -66
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,6 +83,7 @@ A script, CI job or agent does not need to log in. Give it a key:
|
|
|
83
83
|
| `HEADBAND_API_KEY` | `--token` | The API key to send. Used instead of any stored login. |
|
|
84
84
|
| `HEADBAND_URL` | `--url` | The Headband URL. |
|
|
85
85
|
| `HEADBAND_PROFILE` | `--profile` | Which stored login to use. |
|
|
86
|
+
| `HEADBAND_API_URL` | | Where `/v1` calls go, if not the default below. |
|
|
86
87
|
|
|
87
88
|
Precedence, highest first:
|
|
88
89
|
|
|
@@ -92,6 +93,10 @@ Precedence, highest first:
|
|
|
92
93
|
name the profile with `--profile`.
|
|
93
94
|
- **Profile:** `--profile`, then `HEADBAND_PROFILE`, then the current profile,
|
|
94
95
|
then the only stored profile when there is one.
|
|
96
|
+
- **API host:** for `https://headband.dev`, every `/v1` call goes to
|
|
97
|
+
`https://api.headband.dev`, the API host on the servers that run the engines
|
|
98
|
+
(sign-in stays on `headband.dev`). Any other URL is used as is.
|
|
99
|
+
`HEADBAND_API_URL` overrides both.
|
|
95
100
|
|
|
96
101
|
A stored key is only ever sent to the URL it was issued for. `--url` with no
|
|
97
102
|
`--profile` picks the stored profile for that URL; `--url` with a `--profile`
|
|
@@ -187,7 +192,8 @@ has its own indexes and its own keys.
|
|
|
187
192
|
|
|
188
193
|
```bash
|
|
189
194
|
headband branch list [--project <id>] [--json]
|
|
190
|
-
headband branch create <name> --empty [--parent <name>] [--expires-in <2h|7d>] [--project <id>] [--json]
|
|
195
|
+
headband branch create <name> [--empty] [--parent <name>] [--expires-in <2h|7d>] [--project <id>] [--json]
|
|
196
|
+
headband branch ensure <name> [--from <parent>] [--expires-in <7d>] [--env-out <file>] [--project <id>] [--json]
|
|
191
197
|
headband branch delete <name> [--project <id>] [--yes] [--json]
|
|
192
198
|
headband branch protect <name> [--project <id>] [--json]
|
|
193
199
|
headband branch unprotect <name> [--project <id>] [--json]
|
|
@@ -197,13 +203,25 @@ headband branch checkout <name> [--project <id>] [--json]
|
|
|
197
203
|
|
|
198
204
|
- The project comes from `--project`, else the directory's checkout (below).
|
|
199
205
|
With neither, the command is a usage error.
|
|
200
|
-
- `create`
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
206
|
+
- `create` forks the parent (`--parent`, default: the primary branch): the
|
|
207
|
+
new branch starts with the parent's indexes, documents and settings,
|
|
208
|
+
copy-on-write. `--empty` makes a branch with no indexes instead. A parent
|
|
209
|
+
with an index that cannot fork (Typesense, Visor, your own engine) fails
|
|
210
|
+
with exit `6` (code `fork_not_supported`). Until forking is on for your
|
|
211
|
+
organization, a fork fails with exit `6` (code `fork_not_available`), and
|
|
212
|
+
until branches other than main open, every `create` fails with exit `6`
|
|
213
|
+
(code `branches_not_open`). `--expires-in` deletes the branch on its own
|
|
214
|
+
after that long. Forking by default is new in CLI 0.7.0: before it,
|
|
215
|
+
`create` needed `--empty`.
|
|
216
|
+
- `ensure` is for preview deploys, such as a Vercel build step. It forks the
|
|
217
|
+
branch from `--from` (default: the primary) unless it exists, pushes its
|
|
218
|
+
expiry `--expires-in` out (default `7d`), mints a search key and an admin
|
|
219
|
+
key on it, and writes `HEADBAND_URL`, `HEADBAND_PROJECT_ID`,
|
|
220
|
+
`HEADBAND_BRANCH`, `HEADBAND_BRANCH_ID`, `HEADBAND_SEARCH_KEY`,
|
|
221
|
+
`HEADBAND_ADMIN_KEY` and one `HEADBAND_<ENGINE>_URL` per engine to
|
|
222
|
+
`--env-out <file>`, replacing any `HEADBAND_*` lines there. Without
|
|
223
|
+
`--env-out` it prints them. The name is made branch-safe first, so a git
|
|
224
|
+
branch name works as is. It refuses the primary branch (CLI 0.7.0).
|
|
207
225
|
- `delete` removes the branch with its indexes, keys and engines. It asks
|
|
208
226
|
first. The primary branch, a protected branch, a branch with live child
|
|
209
227
|
branches and a branch with Sync-owned indexes are refused (exit `6`).
|
|
@@ -269,8 +287,7 @@ error, so a name is never matched across projects by accident.
|
|
|
269
287
|
project of their own and would ignore it. The exception is `keys create`, which
|
|
270
288
|
sends the project in its body (see Keys).
|
|
271
289
|
|
|
272
|
-
|
|
273
|
-
parent), `reset`, `rollback`, `restore` and `set-primary`.
|
|
290
|
+
Not available yet: `reset`, `rollback`, `restore` and `set-primary`.
|
|
274
291
|
|
|
275
292
|
### Keys
|
|
276
293
|
|
package/dist/index.js
CHANGED
|
@@ -42,8 +42,8 @@ __export(index_exports, {
|
|
|
42
42
|
syncClientFromFlags: () => syncClientFromFlags
|
|
43
43
|
});
|
|
44
44
|
module.exports = __toCommonJS(index_exports);
|
|
45
|
-
var import_fs11 = require("fs");
|
|
46
45
|
var import_fs12 = require("fs");
|
|
46
|
+
var import_fs13 = require("fs");
|
|
47
47
|
var import_path11 = require("path");
|
|
48
48
|
var import_prompts2 = __toESM(require("prompts"));
|
|
49
49
|
var import_picocolors3 = __toESM(require("picocolors"));
|
|
@@ -528,7 +528,7 @@ function errorMessageOf(status, errorBody) {
|
|
|
528
528
|
}
|
|
529
529
|
return message;
|
|
530
530
|
}
|
|
531
|
-
var defaultSleep = (ms) => new Promise((
|
|
531
|
+
var defaultSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
532
532
|
async function readJson(response) {
|
|
533
533
|
const text = await response.text();
|
|
534
534
|
if (!text) return {};
|
|
@@ -548,7 +548,7 @@ async function apiRequest(baseUrl, path2, init2 = {}) {
|
|
|
548
548
|
headers.set("authorization", `Bearer ${init2.apiKey}`);
|
|
549
549
|
}
|
|
550
550
|
const base = normalizeUrl(baseUrl);
|
|
551
|
-
const { rateLimitRetries = 0, sleep = defaultSleep, apiKey: _apiKey, ...fetchInit } = init2;
|
|
551
|
+
const { rateLimitRetries = 0, sleep: sleep2 = defaultSleep, apiKey: _apiKey, ...fetchInit } = init2;
|
|
552
552
|
void _apiKey;
|
|
553
553
|
let response;
|
|
554
554
|
try {
|
|
@@ -561,7 +561,7 @@ async function apiRequest(baseUrl, path2, init2 = {}) {
|
|
|
561
561
|
const wait = response.status === 429 ? retryAfterSeconds(response) : null;
|
|
562
562
|
if (wait === null || attempt >= rateLimitRetries) break;
|
|
563
563
|
await response.body?.cancel().catch(() => void 0);
|
|
564
|
-
await
|
|
564
|
+
await sleep2(wait * 1e3);
|
|
565
565
|
}
|
|
566
566
|
} catch (error) {
|
|
567
567
|
const cause = error?.cause;
|
|
@@ -583,8 +583,16 @@ async function apiRequest(baseUrl, path2, init2 = {}) {
|
|
|
583
583
|
}
|
|
584
584
|
return body;
|
|
585
585
|
}
|
|
586
|
+
var DEFAULT_API_URL = "https://api.headband.dev";
|
|
587
|
+
function apiUrlFor(url, env = process.env) {
|
|
588
|
+
const override = env.HEADBAND_API_URL?.trim();
|
|
589
|
+
if (override) return normalizeUrl(override);
|
|
590
|
+
const normalized = normalizeUrl(url);
|
|
591
|
+
return normalized === DEFAULT_URL ? DEFAULT_API_URL : normalized;
|
|
592
|
+
}
|
|
586
593
|
function createApiClient(url, profile) {
|
|
587
|
-
const
|
|
594
|
+
const apiUrl = apiUrlFor(url);
|
|
595
|
+
const request = (path2, init2 = {}) => apiRequest(apiUrl, path2, {
|
|
588
596
|
...init2,
|
|
589
597
|
apiKey: profile.apiKey,
|
|
590
598
|
headers: profile.headers,
|
|
@@ -600,7 +608,7 @@ function createApiClient(url, profile) {
|
|
|
600
608
|
}
|
|
601
609
|
|
|
602
610
|
// src/auth.ts
|
|
603
|
-
var defaultSleep2 = (ms) => new Promise((
|
|
611
|
+
var defaultSleep2 = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
604
612
|
function openBrowser(url) {
|
|
605
613
|
try {
|
|
606
614
|
if (process.platform === "darwin") {
|
|
@@ -615,7 +623,7 @@ function openBrowser(url) {
|
|
|
615
623
|
}
|
|
616
624
|
async function runDeviceFlow(url, io, options = {}) {
|
|
617
625
|
const baseUrl = normalizeUrl(url);
|
|
618
|
-
const
|
|
626
|
+
const sleep2 = options.sleep || defaultSleep2;
|
|
619
627
|
const now = options.now || Date.now;
|
|
620
628
|
const device = await apiRequest(baseUrl, "/api/cli/device", {
|
|
621
629
|
method: "POST",
|
|
@@ -630,7 +638,7 @@ async function runDeviceFlow(url, io, options = {}) {
|
|
|
630
638
|
let interval = device.data.interval;
|
|
631
639
|
const deadline = now() + device.data.expiresIn * 1e3;
|
|
632
640
|
while (now() < deadline) {
|
|
633
|
-
await
|
|
641
|
+
await sleep2(interval * 1e3);
|
|
634
642
|
try {
|
|
635
643
|
const token = await apiRequest(baseUrl, "/api/cli/token", {
|
|
636
644
|
method: "POST",
|
|
@@ -685,6 +693,7 @@ async function login(url, io, options = {}) {
|
|
|
685
693
|
}
|
|
686
694
|
|
|
687
695
|
// src/branch.ts
|
|
696
|
+
var import_fs4 = require("fs");
|
|
688
697
|
var import_path5 = require("path");
|
|
689
698
|
|
|
690
699
|
// src/output.ts
|
|
@@ -894,14 +903,22 @@ var PART_2 = /* @__PURE__ */ new Set(["reset", "rollback", "restore", "set-prima
|
|
|
894
903
|
var BRANCH_USAGE = [
|
|
895
904
|
"Usage:",
|
|
896
905
|
" headband branch list [--project <id>] [--json]",
|
|
897
|
-
" headband branch create <name> --empty [--parent <name>] [--expires-in <2h|7d>] [--project <id>] [--json]",
|
|
906
|
+
" headband branch create <name> [--empty] [--parent <name>] [--expires-in <2h|7d>] [--project <id>] [--json]",
|
|
907
|
+
" headband branch ensure <name> [--from <parent>] [--expires-in <7d>] [--env-out <file>] [--project <id>] [--json]",
|
|
898
908
|
" headband branch delete <name> [--project <id>] [--yes] [--json]",
|
|
899
909
|
" headband branch protect <name> [--project <id>] [--json]",
|
|
900
910
|
" headband branch unprotect <name> [--project <id>] [--json]",
|
|
901
911
|
" headband branch expire <name> (--in <2h|7d> | --at <iso> | --clear) [--project <id>] [--json]",
|
|
902
912
|
" headband branch checkout <name> [--project <id>] [--json]",
|
|
903
|
-
" headband branch
|
|
904
|
-
"
|
|
913
|
+
" headband branch reset|rollback|restore|set-primary <name> not available yet",
|
|
914
|
+
"",
|
|
915
|
+
"create without --empty forks the parent (default: main): its indexes, documents and settings,",
|
|
916
|
+
"copy-on-write. --empty makes a branch with no indexes.",
|
|
917
|
+
"",
|
|
918
|
+
"ensure is for preview deploys: it forks the branch unless it exists, pushes its expiry out",
|
|
919
|
+
"(--expires-in, default 7d), mints a search key and an admin key for it, and writes HEADBAND_*",
|
|
920
|
+
"variables to --env-out (or prints them). The name is made branch-safe first: feature/Foo_Bar",
|
|
921
|
+
"becomes feature/foo_bar. It refuses the primary branch.",
|
|
905
922
|
"",
|
|
906
923
|
"The project comes from --project, else the directory's checkout (.headband/project.json).",
|
|
907
924
|
"Durations take s, m, h, d or w: 30m, 2h, 7d.",
|
|
@@ -1000,10 +1017,7 @@ async function runBranch(args, flags, deps) {
|
|
|
1000
1017
|
const action = args[0];
|
|
1001
1018
|
const json = flags["--json"] === true;
|
|
1002
1019
|
if (action && PART_2.has(action)) {
|
|
1003
|
-
throw new UsageError(
|
|
1004
|
-
`headband branch ${action} is coming with bucket storage.`,
|
|
1005
|
-
"not_available"
|
|
1006
|
-
);
|
|
1020
|
+
throw new UsageError(`headband branch ${action} is not available yet.`, "not_available");
|
|
1007
1021
|
}
|
|
1008
1022
|
const client = createApiClient(auth.url, { apiKey: auth.apiKey });
|
|
1009
1023
|
const checkout = () => checkoutFor(cwd, auth.profile?.orgId);
|
|
@@ -1032,18 +1046,35 @@ async function runBranch(args, flags, deps) {
|
|
|
1032
1046
|
if (error instanceof ApiError && error.code === "branches_not_open") {
|
|
1033
1047
|
throw restate(error, "Branches other than main are not open yet.");
|
|
1034
1048
|
}
|
|
1035
|
-
|
|
1036
|
-
throw restate(
|
|
1037
|
-
error,
|
|
1038
|
-
"Forking arrives with bucket storage. Pass --empty for an empty branch."
|
|
1039
|
-
);
|
|
1040
|
-
}
|
|
1041
|
-
throw error;
|
|
1049
|
+
throw restateCreate(error);
|
|
1042
1050
|
}
|
|
1043
1051
|
if (json) printJson(io, response);
|
|
1044
1052
|
else printTable(io, [response.data], COLUMNS);
|
|
1045
1053
|
return;
|
|
1046
1054
|
}
|
|
1055
|
+
if (action === "ensure") {
|
|
1056
|
+
const result = await ensureBranch(client, {
|
|
1057
|
+
name: branchNameFrom(requiredName(args[1])),
|
|
1058
|
+
project: projectFor(flags, checkout),
|
|
1059
|
+
from: stringFlag2(flags, "--from"),
|
|
1060
|
+
expiresInMs: parseDuration(stringFlag2(flags, "--expires-in") ?? "7d"),
|
|
1061
|
+
url: auth.url
|
|
1062
|
+
});
|
|
1063
|
+
const envOut = stringFlag2(flags, "--env-out");
|
|
1064
|
+
if (envOut && envOut !== "-") writeEnvFile((0, import_path5.resolve)(cwd, envOut), result.env);
|
|
1065
|
+
if (json) {
|
|
1066
|
+
printJson(io, { branch: result.branch, created: result.created, env: result.env });
|
|
1067
|
+
} else if (envOut && envOut !== "-") {
|
|
1068
|
+
const engines = result.branch.instances?.length ?? 0;
|
|
1069
|
+
io.stdout(
|
|
1070
|
+
`${result.created ? "Forked" : "Reused"} branch ${result.branch.name} (${engines} engine${engines === 1 ? "" : "s"}), expires ${result.branch.expiresAt}.`
|
|
1071
|
+
);
|
|
1072
|
+
io.stdout(`Wrote ${Object.keys(result.env).length} HEADBAND_* variables to ${envOut}.`);
|
|
1073
|
+
} else {
|
|
1074
|
+
io.stdout(dotenv(result.env));
|
|
1075
|
+
}
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1047
1078
|
if (action === "delete") {
|
|
1048
1079
|
const name = requiredName(args[1]);
|
|
1049
1080
|
const project = projectFor(flags, checkout);
|
|
@@ -1115,9 +1146,109 @@ async function runBranch(args, flags, deps) {
|
|
|
1115
1146
|
io.stdout(BRANCH_USAGE);
|
|
1116
1147
|
throw new UsageError("Unknown branch command");
|
|
1117
1148
|
}
|
|
1149
|
+
function restateCreate(error) {
|
|
1150
|
+
if (error instanceof ApiError && error.code === "branches_not_open") {
|
|
1151
|
+
return restate(error, "Branches other than main are not open yet.");
|
|
1152
|
+
}
|
|
1153
|
+
if (error instanceof ApiError && error.code === "fork_not_available") {
|
|
1154
|
+
return restate(error, "Forking is not open yet. Pass --empty for an empty branch.");
|
|
1155
|
+
}
|
|
1156
|
+
return error;
|
|
1157
|
+
}
|
|
1158
|
+
function branchNameFrom(ref) {
|
|
1159
|
+
const name = ref.trim().toLowerCase().replace(/[^a-z0-9._/-]+/g, "-").slice(0, 63).replace(/^[^a-z0-9]+/, "").replace(/[^a-z0-9]+$/, "");
|
|
1160
|
+
if (!name) throw new UsageError(`"${ref}" has no letters or digits to make a branch name from.`);
|
|
1161
|
+
return name;
|
|
1162
|
+
}
|
|
1163
|
+
var ENSURE_WAIT_MS = 6e4;
|
|
1164
|
+
var ENSURE_POLL_MS = 2e3;
|
|
1165
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1166
|
+
var engineVar = (engine) => `HEADBAND_${engine.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_URL`;
|
|
1167
|
+
async function ensureBranch(client, o, now = Date.now) {
|
|
1168
|
+
var _a;
|
|
1169
|
+
const path2 = `/v1/branches/${encodeURIComponent(o.name)}?project=${encodeURIComponent(o.project)}`;
|
|
1170
|
+
const read = async () => {
|
|
1171
|
+
try {
|
|
1172
|
+
return (await client.get(path2)).data;
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
if (error instanceof ApiError && error.status === 404 && error.code === "branch_not_found") {
|
|
1175
|
+
return null;
|
|
1176
|
+
}
|
|
1177
|
+
throw error;
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
const expiresAt = () => new Date(now() + o.expiresInMs).toISOString();
|
|
1181
|
+
let branch = await read();
|
|
1182
|
+
let created = false;
|
|
1183
|
+
if (!branch) {
|
|
1184
|
+
const body = {
|
|
1185
|
+
name: o.name,
|
|
1186
|
+
projectId: o.project,
|
|
1187
|
+
expiresAt: expiresAt()
|
|
1188
|
+
};
|
|
1189
|
+
if (o.from) body.parent = o.from;
|
|
1190
|
+
try {
|
|
1191
|
+
branch = (await client.post("/v1/branches", body)).data;
|
|
1192
|
+
created = true;
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
if (!(error instanceof ApiError && error.code === "branch_exists"))
|
|
1195
|
+
throw restateCreate(error);
|
|
1196
|
+
branch = await read();
|
|
1197
|
+
if (!branch) throw error;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
if (branch.primary) {
|
|
1201
|
+
throw new UsageError(
|
|
1202
|
+
`${branch.name} is the primary branch. ensure is for preview branches; run it only on preview builds.`
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
for (const until = now() + ENSURE_WAIT_MS; branch.state === "creating" && now() < until; ) {
|
|
1206
|
+
await sleep(ENSURE_POLL_MS);
|
|
1207
|
+
branch = await read() ?? branch;
|
|
1208
|
+
}
|
|
1209
|
+
if (branch.state !== "ready") {
|
|
1210
|
+
throw new CliError(`Branch ${branch.name} is ${branch.state}, not ready. Try again shortly.`, {
|
|
1211
|
+
exitCode: EXIT_CODES.conflict,
|
|
1212
|
+
code: "branch_not_ready"
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
if (!created && !branch.protected) {
|
|
1216
|
+
branch = {
|
|
1217
|
+
...branch,
|
|
1218
|
+
...(await client.patch(path2, { expiresAt: expiresAt() })).data
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
if (!branch.instances)
|
|
1222
|
+
branch = { ...branch, ...(await client.get(path2)).data };
|
|
1223
|
+
const mint = async (type) => (await client.post("/v1/keys", {
|
|
1224
|
+
name: `preview ${branch.name} ${type}`,
|
|
1225
|
+
type,
|
|
1226
|
+
projectId: o.project,
|
|
1227
|
+
branch: branch.id
|
|
1228
|
+
})).data.apiKey;
|
|
1229
|
+
const env = {
|
|
1230
|
+
HEADBAND_URL: o.url,
|
|
1231
|
+
HEADBAND_PROJECT_ID: o.project,
|
|
1232
|
+
HEADBAND_BRANCH: branch.name,
|
|
1233
|
+
HEADBAND_BRANCH_ID: branch.id,
|
|
1234
|
+
HEADBAND_SEARCH_KEY: await mint("search"),
|
|
1235
|
+
HEADBAND_ADMIN_KEY: await mint("admin")
|
|
1236
|
+
};
|
|
1237
|
+
for (const i of branch.instances ?? []) env[_a = engineVar(i.engine)] ?? (env[_a] = i.endpoint);
|
|
1238
|
+
return { branch, created, env };
|
|
1239
|
+
}
|
|
1240
|
+
var quote = (v) => /^[A-Za-z0-9_./:@+-]*$/.test(v) ? v : JSON.stringify(v);
|
|
1241
|
+
function dotenv(env) {
|
|
1242
|
+
return Object.entries(env).map(([k, v]) => `${k}=${quote(v)}`).join("\n");
|
|
1243
|
+
}
|
|
1244
|
+
function writeEnvFile(file, env) {
|
|
1245
|
+
const kept = (0, import_fs4.existsSync)(file) ? (0, import_fs4.readFileSync)(file, "utf8").split("\n").filter((line) => line.trim() !== "" && !/^\s*(export\s+)?HEADBAND_[A-Z0-9_]*=/.test(line)) : [];
|
|
1246
|
+
(0, import_fs4.writeFileSync)(file, [...kept, dotenv(env)].join("\n") + "\n", { mode: 384 });
|
|
1247
|
+
(0, import_fs4.chmodSync)(file, 384);
|
|
1248
|
+
}
|
|
1118
1249
|
|
|
1119
1250
|
// src/config-load.ts
|
|
1120
|
-
var
|
|
1251
|
+
var import_fs5 = require("fs");
|
|
1121
1252
|
var import_path6 = require("path");
|
|
1122
1253
|
var import_url = require("url");
|
|
1123
1254
|
var import_jiti = require("jiti");
|
|
@@ -1253,7 +1384,7 @@ var SYNC_CONFIG_SHAPE = 'a headband.config.ts whose default export is defineConf
|
|
|
1253
1384
|
function findConfigFile(cwd) {
|
|
1254
1385
|
for (const name of CONFIG_FILENAMES) {
|
|
1255
1386
|
const path2 = (0, import_path6.join)(cwd, name);
|
|
1256
|
-
if ((0,
|
|
1387
|
+
if ((0, import_fs5.existsSync)(path2)) return path2;
|
|
1257
1388
|
}
|
|
1258
1389
|
return void 0;
|
|
1259
1390
|
}
|
|
@@ -1373,10 +1504,10 @@ function missingAdapterError(command, why) {
|
|
|
1373
1504
|
|
|
1374
1505
|
// src/docs.ts
|
|
1375
1506
|
var import_path7 = require("path");
|
|
1376
|
-
var
|
|
1507
|
+
var import_fs6 = require("fs");
|
|
1377
1508
|
var DOCS_BATCH_SIZE = 1e3;
|
|
1378
1509
|
function parseDocumentsFile(path2) {
|
|
1379
|
-
const content = (0,
|
|
1510
|
+
const content = (0, import_fs6.readFileSync)(path2, "utf-8");
|
|
1380
1511
|
const ext = (0, import_path7.extname)(path2).toLowerCase();
|
|
1381
1512
|
if (ext === ".jsonl") {
|
|
1382
1513
|
return content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
|
|
@@ -1501,7 +1632,7 @@ function refusedWalk(error) {
|
|
|
1501
1632
|
var DECLARE_WAIT_MS = 15 * 6e4;
|
|
1502
1633
|
var DECLARE_POLL_MS = 3e3;
|
|
1503
1634
|
async function declareExportKey(client, index, io, opts = {}) {
|
|
1504
|
-
const
|
|
1635
|
+
const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
1505
1636
|
const now = opts.now ?? Date.now;
|
|
1506
1637
|
const probe = () => client.get(`/v1/documents?index=${encodeURIComponent(index)}&limit=1&cursor=`);
|
|
1507
1638
|
let refused;
|
|
@@ -1547,7 +1678,7 @@ async function declareExportKey(client, index, io, opts = {}) {
|
|
|
1547
1678
|
`\`${pk}\` is still not declared on ${index} after ${Math.round((opts.waitMs ?? DECLARE_WAIT_MS) / 6e4)} minutes. Visor may still be rebuilding it: run headband docs export again later.`
|
|
1548
1679
|
);
|
|
1549
1680
|
}
|
|
1550
|
-
await
|
|
1681
|
+
await sleep2(DECLARE_POLL_MS);
|
|
1551
1682
|
}
|
|
1552
1683
|
}
|
|
1553
1684
|
function exportRefusalAdvice(error, index) {
|
|
@@ -1613,6 +1744,7 @@ function documentsMatch(expected, remote, primaryKey) {
|
|
|
1613
1744
|
}
|
|
1614
1745
|
|
|
1615
1746
|
// ../../src/lib/density/catalog-constants.ts
|
|
1747
|
+
var FREE_ALWAYS_ON_CLOSED_AT = Date.parse("2026-11-10T00:00:00Z");
|
|
1616
1748
|
function formatMemory(mb) {
|
|
1617
1749
|
return mb >= 1024 ? `${mb / 1024} GB` : `${mb} MB`;
|
|
1618
1750
|
}
|
|
@@ -2167,7 +2299,7 @@ async function runSwitchEngine(client, opts, io, chatter) {
|
|
|
2167
2299
|
);
|
|
2168
2300
|
return;
|
|
2169
2301
|
}
|
|
2170
|
-
const
|
|
2302
|
+
const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
2171
2303
|
let lastPhase = "";
|
|
2172
2304
|
for (; ; ) {
|
|
2173
2305
|
if (view.phase !== lastPhase) {
|
|
@@ -2175,7 +2307,7 @@ async function runSwitchEngine(client, opts, io, chatter) {
|
|
|
2175
2307
|
chatter.stdout(`${opts.uid}: ${PHASES[view.phase] ?? view.phase}`);
|
|
2176
2308
|
}
|
|
2177
2309
|
if (view.status === "succeeded" || view.status === "failed") break;
|
|
2178
|
-
await
|
|
2310
|
+
await sleep2(opts.pollMs ?? 3e3);
|
|
2179
2311
|
const status = await client.get(path2);
|
|
2180
2312
|
if (!status.switch || status.switch.switchId !== view.switchId) {
|
|
2181
2313
|
throw new CliError("The switch is no longer listed for this index.", {
|
|
@@ -2312,11 +2444,11 @@ function planMigration(config, applied, opts) {
|
|
|
2312
2444
|
}
|
|
2313
2445
|
|
|
2314
2446
|
// src/schema.ts
|
|
2315
|
-
var
|
|
2447
|
+
var import_fs8 = require("fs");
|
|
2316
2448
|
var import_path9 = require("path");
|
|
2317
2449
|
|
|
2318
2450
|
// ../sync/src/migrations.ts
|
|
2319
|
-
var
|
|
2451
|
+
var import_fs7 = require("fs");
|
|
2320
2452
|
var import_path8 = require("path");
|
|
2321
2453
|
var FILE_NAME_RE = /^(\d{4})_(.+)\.json$/;
|
|
2322
2454
|
function sanitizeSlug(slug) {
|
|
@@ -2324,7 +2456,7 @@ function sanitizeSlug(slug) {
|
|
|
2324
2456
|
return sanitized || "migration";
|
|
2325
2457
|
}
|
|
2326
2458
|
function generateMigrationFile(dir, ops, meta, now = () => /* @__PURE__ */ new Date()) {
|
|
2327
|
-
(0,
|
|
2459
|
+
(0, import_fs7.mkdirSync)(dir, { recursive: true });
|
|
2328
2460
|
const existing = readMigrations(dir);
|
|
2329
2461
|
const nextVersion = existing.length === 0 ? 1 : existing[existing.length - 1].version + 1;
|
|
2330
2462
|
const safeSlug = sanitizeSlug(meta.slug);
|
|
@@ -2337,13 +2469,13 @@ function generateMigrationFile(dir, ops, meta, now = () => /* @__PURE__ */ new D
|
|
|
2337
2469
|
...meta.targetHashes ? { targetHashes: meta.targetHashes } : {},
|
|
2338
2470
|
ops
|
|
2339
2471
|
};
|
|
2340
|
-
(0,
|
|
2472
|
+
(0, import_fs7.writeFileSync)(path2, JSON.stringify(file, null, 2));
|
|
2341
2473
|
return path2;
|
|
2342
2474
|
}
|
|
2343
2475
|
function readMigrations(dir) {
|
|
2344
2476
|
let entries;
|
|
2345
2477
|
try {
|
|
2346
|
-
entries = (0,
|
|
2478
|
+
entries = (0, import_fs7.readdirSync)(dir);
|
|
2347
2479
|
} catch {
|
|
2348
2480
|
return [];
|
|
2349
2481
|
}
|
|
@@ -2352,7 +2484,7 @@ function readMigrations(dir) {
|
|
|
2352
2484
|
const match = FILE_NAME_RE.exec(entry);
|
|
2353
2485
|
if (!match) continue;
|
|
2354
2486
|
const path2 = (0, import_path8.join)(dir, entry);
|
|
2355
|
-
const raw = (0,
|
|
2487
|
+
const raw = (0, import_fs7.readFileSync)(path2, "utf-8");
|
|
2356
2488
|
let parsed;
|
|
2357
2489
|
try {
|
|
2358
2490
|
parsed = JSON.parse(raw);
|
|
@@ -2382,7 +2514,7 @@ function pendingMigrations(files, appliedVersion) {
|
|
|
2382
2514
|
var BASE_BACKOFF_MS = 500;
|
|
2383
2515
|
var DEFAULT_MAX_RETRIES = 5;
|
|
2384
2516
|
function defaultSleep3(ms) {
|
|
2385
|
-
return new Promise((
|
|
2517
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
2386
2518
|
}
|
|
2387
2519
|
var SchemaCasError = class extends Error {
|
|
2388
2520
|
constructor(message) {
|
|
@@ -2549,17 +2681,17 @@ function printOpsTable(io, ops) {
|
|
|
2549
2681
|
}
|
|
2550
2682
|
function readAppliedMarker(dir) {
|
|
2551
2683
|
const path2 = (0, import_path9.join)(dir, APPLIED_MARKER_FILE);
|
|
2552
|
-
if (!(0,
|
|
2684
|
+
if (!(0, import_fs8.existsSync)(path2)) return 0;
|
|
2553
2685
|
try {
|
|
2554
|
-
const parsed = JSON.parse((0,
|
|
2686
|
+
const parsed = JSON.parse((0, import_fs8.readFileSync)(path2, "utf-8"));
|
|
2555
2687
|
return typeof parsed.version === "number" ? parsed.version : 0;
|
|
2556
2688
|
} catch {
|
|
2557
2689
|
return 0;
|
|
2558
2690
|
}
|
|
2559
2691
|
}
|
|
2560
2692
|
function writeAppliedMarker(dir, version) {
|
|
2561
|
-
(0,
|
|
2562
|
-
(0,
|
|
2693
|
+
(0, import_fs8.mkdirSync)(dir, { recursive: true });
|
|
2694
|
+
(0, import_fs8.writeFileSync)((0, import_path9.join)(dir, APPLIED_MARKER_FILE), JSON.stringify({ version }, null, 2));
|
|
2563
2695
|
}
|
|
2564
2696
|
function configHashes(config, index) {
|
|
2565
2697
|
const def = config.indexes.find((d) => d.name === index);
|
|
@@ -3068,7 +3200,7 @@ async function runSchemaForget(deps) {
|
|
|
3068
3200
|
}
|
|
3069
3201
|
|
|
3070
3202
|
// src/sync.ts
|
|
3071
|
-
var
|
|
3203
|
+
var import_fs9 = require("fs");
|
|
3072
3204
|
var import_path10 = require("path");
|
|
3073
3205
|
var import_crypto = require("crypto");
|
|
3074
3206
|
var import_pg = require("pg");
|
|
@@ -3359,7 +3491,7 @@ function rowOutcomes(rows, resolved, failures) {
|
|
|
3359
3491
|
return { done, retry, deadLetter };
|
|
3360
3492
|
}
|
|
3361
3493
|
function defaultSleep4(ms) {
|
|
3362
|
-
return new Promise((
|
|
3494
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
3363
3495
|
}
|
|
3364
3496
|
function errorMessage(err) {
|
|
3365
3497
|
if (!(err instanceof Error)) return String(err);
|
|
@@ -3399,7 +3531,7 @@ async function runSyncWorker(opts) {
|
|
|
3399
3531
|
batchSize = DEFAULT_BATCH_SIZE,
|
|
3400
3532
|
stopSignal,
|
|
3401
3533
|
now = Date.now,
|
|
3402
|
-
sleep = defaultSleep4,
|
|
3534
|
+
sleep: sleep2 = defaultSleep4,
|
|
3403
3535
|
onHeartbeat,
|
|
3404
3536
|
log = (message) => console.warn(message)
|
|
3405
3537
|
} = opts;
|
|
@@ -3426,7 +3558,7 @@ async function runSyncWorker(opts) {
|
|
|
3426
3558
|
log(`[headband] could not record this worker run: ${errorMessage(err)}`);
|
|
3427
3559
|
}
|
|
3428
3560
|
};
|
|
3429
|
-
const backoff = (attempt) =>
|
|
3561
|
+
const backoff = (attempt) => sleep2(Math.min(failureDelayMs * 2 ** (attempt - 1), MAX_FAILURE_DELAY_MS));
|
|
3430
3562
|
const processBatch = async (batch) => {
|
|
3431
3563
|
const changes = batch.map((row) => ({
|
|
3432
3564
|
model: row.model,
|
|
@@ -3468,7 +3600,7 @@ async function runSyncWorker(opts) {
|
|
|
3468
3600
|
const rows = await outbox.claim(batchSize);
|
|
3469
3601
|
if (rows.length === 0) {
|
|
3470
3602
|
if (!loop) break;
|
|
3471
|
-
await
|
|
3603
|
+
await sleep2(idleDelayMs);
|
|
3472
3604
|
continue;
|
|
3473
3605
|
}
|
|
3474
3606
|
let outcome;
|
|
@@ -3627,8 +3759,8 @@ async function runSyncInit(deps) {
|
|
|
3627
3759
|
deps.io.stdout(ddl);
|
|
3628
3760
|
if (deps.write) {
|
|
3629
3761
|
const path2 = (0, import_path10.join)(deps.cwd ?? process.cwd(), OUTBOX_SQL_RELATIVE_PATH);
|
|
3630
|
-
(0,
|
|
3631
|
-
(0,
|
|
3762
|
+
(0, import_fs9.mkdirSync)((0, import_path10.join)(deps.cwd ?? process.cwd(), "headband"), { recursive: true });
|
|
3763
|
+
(0, import_fs9.writeFileSync)(path2, `${ddl}
|
|
3632
3764
|
`);
|
|
3633
3765
|
deps.io.stdout(`Wrote ${path2}`);
|
|
3634
3766
|
}
|
|
@@ -4014,10 +4146,10 @@ async function runSelfHostedSyncStatus(deps) {
|
|
|
4014
4146
|
}
|
|
4015
4147
|
function runSyncScaffoldDocker(deps) {
|
|
4016
4148
|
const path2 = (0, import_path10.join)(deps.cwd, DOCKERFILE_NAME);
|
|
4017
|
-
if ((0,
|
|
4149
|
+
if ((0, import_fs9.existsSync)(path2) && !deps.force) {
|
|
4018
4150
|
throw new CliError(`${DOCKERFILE_NAME} already exists at ${path2}. Pass --force to overwrite.`);
|
|
4019
4151
|
}
|
|
4020
|
-
(0,
|
|
4152
|
+
(0, import_fs9.writeFileSync)(path2, dockerfileContent(deps.cliVersion));
|
|
4021
4153
|
deps.io.stdout(`Wrote ${path2}`);
|
|
4022
4154
|
deps.io.stdout("Add this service to your docker-compose.yml:");
|
|
4023
4155
|
deps.io.stdout(COMPOSE_SNIPPET);
|
|
@@ -4194,10 +4326,10 @@ ${CONFIG_NOTE}`,
|
|
|
4194
4326
|
};
|
|
4195
4327
|
|
|
4196
4328
|
// src/sync-indexes.ts
|
|
4197
|
-
var
|
|
4329
|
+
var import_fs11 = require("fs");
|
|
4198
4330
|
|
|
4199
4331
|
// src/sync-sources.ts
|
|
4200
|
-
var
|
|
4332
|
+
var import_fs10 = require("fs");
|
|
4201
4333
|
var import_pg2 = require("pg");
|
|
4202
4334
|
|
|
4203
4335
|
// ../../src/lib/sync/copy.ts
|
|
@@ -4264,7 +4396,7 @@ var SOURCE_USAGE = [
|
|
|
4264
4396
|
function readCaFile(deps, file) {
|
|
4265
4397
|
let text;
|
|
4266
4398
|
try {
|
|
4267
|
-
text = (deps.readFile ?? ((p) => (0,
|
|
4399
|
+
text = (deps.readFile ?? ((p) => (0, import_fs10.readFileSync)(p, "utf8")))(file);
|
|
4268
4400
|
} catch {
|
|
4269
4401
|
throw new CliError("Cannot read the --ca-file.");
|
|
4270
4402
|
}
|
|
@@ -4905,7 +5037,7 @@ var PREVIEW_MAX_IDS = 20;
|
|
|
4905
5037
|
var ATTENTION_INDEX = /* @__PURE__ */ new Set(["error", "needs_rebuild", "awaiting_setup", "degraded"]);
|
|
4906
5038
|
var ATTENTION_SOURCE = /* @__PURE__ */ new Set(["error", "awaiting_setup"]);
|
|
4907
5039
|
var path = (id, suffix = "") => `/v1/sync/indexes/${encodeURIComponent(id)}${suffix}`;
|
|
4908
|
-
function readDefinition(file, readFile = (p) => (0,
|
|
5040
|
+
function readDefinition(file, readFile = (p) => (0, import_fs11.readFileSync)(p, "utf8")) {
|
|
4909
5041
|
let text;
|
|
4910
5042
|
try {
|
|
4911
5043
|
text = readFile(file);
|
|
@@ -5324,6 +5456,8 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
5324
5456
|
"--branch",
|
|
5325
5457
|
"--parent",
|
|
5326
5458
|
"--expires-in",
|
|
5459
|
+
"--env-out",
|
|
5460
|
+
"--from",
|
|
5327
5461
|
"--in",
|
|
5328
5462
|
"--at",
|
|
5329
5463
|
"--level",
|
|
@@ -5453,7 +5587,8 @@ function syncClientFromFlags(flags, cwd, options = {}) {
|
|
|
5453
5587
|
return fetch(input, { ...init2, headers: merged });
|
|
5454
5588
|
} : void 0;
|
|
5455
5589
|
return new HeadbandClient({
|
|
5456
|
-
|
|
5590
|
+
// The sync client calls /v1 only: the same API host as every other /v1 call (api.ts).
|
|
5591
|
+
url: apiUrlFor(auth.url),
|
|
5457
5592
|
adminKey: auth.apiKey,
|
|
5458
5593
|
...fetchImpl ? { fetchImpl } : {}
|
|
5459
5594
|
});
|
|
@@ -5496,12 +5631,13 @@ Commands:
|
|
|
5496
5631
|
projects create <name> --engine <meilisearch|typesense|elasticsearch|visor> [--json]
|
|
5497
5632
|
projects delete <id> [--yes] [--json]
|
|
5498
5633
|
branch list [--project <id>] [--json]
|
|
5499
|
-
branch create <name> --empty [--parent <name>] [--expires-in <2h|7d>] [--project <id>] [--json]
|
|
5634
|
+
branch create <name> [--empty] [--parent <name>] [--expires-in <2h|7d>] [--project <id>] [--json]
|
|
5635
|
+
branch ensure <name> [--from <parent>] [--expires-in <7d>] [--env-out <file>] [--project <id>] [--json]
|
|
5500
5636
|
branch delete <name> [--project <id>] [--yes] [--json]
|
|
5501
5637
|
branch protect|unprotect <name> [--project <id>] [--json]
|
|
5502
5638
|
branch expire <name> (--in <2h|7d> | --at <iso> | --clear) [--project <id>] [--json]
|
|
5503
5639
|
branch checkout <name> [--project <id>] [--json]
|
|
5504
|
-
branch reset|rollback|restore|set-primary (
|
|
5640
|
+
branch reset|rollback|restore|set-primary (not available yet)
|
|
5505
5641
|
keys list [--json]
|
|
5506
5642
|
keys create <name> --type <admin|search> [--project <id>] [--branch <name>] [--instances <id,id>] [--indexes <uid,uid>] [--permissions <a,b>] [--expires <30d|ISO date>] [--json]
|
|
5507
5643
|
keys revoke <id> [--yes] [--json]
|
|
@@ -6054,7 +6190,7 @@ async function runIndexes(args, flags, io, confirm, cwd) {
|
|
|
6054
6190
|
}
|
|
6055
6191
|
if (settingsAction === "set") {
|
|
6056
6192
|
const file = requiredFlag(flags, "--file");
|
|
6057
|
-
const settings = JSON.parse((0,
|
|
6193
|
+
const settings = JSON.parse((0, import_fs13.readFileSync)(file, "utf-8"));
|
|
6058
6194
|
const response = await client.patch(`/v1/indexes/${encode(uid)}/settings`, settings);
|
|
6059
6195
|
printJson(io, response);
|
|
6060
6196
|
return;
|
|
@@ -6115,7 +6251,7 @@ async function runDocsExport(flags, context, cwd) {
|
|
|
6115
6251
|
return;
|
|
6116
6252
|
}
|
|
6117
6253
|
const partial = `${out}.partial`;
|
|
6118
|
-
const fd = (0,
|
|
6254
|
+
const fd = (0, import_fs12.openSync)(partial, "w", 384);
|
|
6119
6255
|
let exported;
|
|
6120
6256
|
try {
|
|
6121
6257
|
exported = await exportDocuments(client, {
|
|
@@ -6123,26 +6259,26 @@ async function runDocsExport(flags, context, cwd) {
|
|
|
6123
6259
|
format,
|
|
6124
6260
|
batchSize,
|
|
6125
6261
|
write: (line) => {
|
|
6126
|
-
(0,
|
|
6262
|
+
(0, import_fs12.writeSync)(fd, `${line}
|
|
6127
6263
|
`);
|
|
6128
6264
|
},
|
|
6129
6265
|
onPage: (count, total) => io.stderr(`Exported ${count}${total === null ? "" : `/${total}`} documents`),
|
|
6130
6266
|
finishOnError: true
|
|
6131
6267
|
});
|
|
6132
|
-
(0,
|
|
6268
|
+
(0, import_fs12.closeSync)(fd);
|
|
6133
6269
|
} catch (error) {
|
|
6134
|
-
(0,
|
|
6270
|
+
(0, import_fs12.closeSync)(fd);
|
|
6135
6271
|
const kept = error instanceof ExportInterruptedError ? error.exported : 0;
|
|
6136
6272
|
if (kept > 0) {
|
|
6137
6273
|
io.stderr(
|
|
6138
6274
|
`Kept the ${kept} ${kept === 1 ? "document" : "documents"} exported before the error in ${partial}. ${out} was not written.`
|
|
6139
6275
|
);
|
|
6140
6276
|
} else {
|
|
6141
|
-
(0,
|
|
6277
|
+
(0, import_fs12.rmSync)(partial, { force: true });
|
|
6142
6278
|
}
|
|
6143
6279
|
throw exportFailure(error instanceof ExportInterruptedError ? error.cause : error, index);
|
|
6144
6280
|
}
|
|
6145
|
-
(0,
|
|
6281
|
+
(0, import_fs12.renameSync)(partial, out);
|
|
6146
6282
|
if (boolFlag(flags, "--json")) printJson(io, { index, out, format, exported });
|
|
6147
6283
|
else io.stdout(`Exported ${exported} documents to ${out}.`);
|
|
6148
6284
|
}
|
|
@@ -6513,7 +6649,7 @@ async function runSync(args, flags, io, confirm, cwd) {
|
|
|
6513
6649
|
printCommandHelp("sync", io);
|
|
6514
6650
|
throw new UsageError("Unknown sync command");
|
|
6515
6651
|
}
|
|
6516
|
-
var CLI_VERSION = true ? "0.
|
|
6652
|
+
var CLI_VERSION = true ? "0.7.1" : "dev";
|
|
6517
6653
|
async function run(argv, context = { io: defaultIO }) {
|
|
6518
6654
|
if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "version")) {
|
|
6519
6655
|
context.io.stdout(CLI_VERSION);
|
|
@@ -6615,7 +6751,7 @@ async function main(argv, context = { io: defaultIO }) {
|
|
|
6615
6751
|
function stdoutLineWriter(line) {
|
|
6616
6752
|
if (process.stdout.write(`${line}
|
|
6617
6753
|
`)) return;
|
|
6618
|
-
return new Promise((
|
|
6754
|
+
return new Promise((resolve3) => process.stdout.once("drain", () => resolve3()));
|
|
6619
6755
|
}
|
|
6620
6756
|
function isDirectRun() {
|
|
6621
6757
|
return typeof require !== "undefined" && typeof module !== "undefined" && require.main === module;
|