@bli-cockpit/cli 0.1.9 → 0.1.11
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 +7 -3
- package/dist/adapters/agent-image-evidence.js +97 -0
- package/dist/adapters/agent-image-records.js +147 -0
- package/dist/adapters/agent-image-validation.js +88 -0
- package/dist/adapters/raw-evidence.js +145 -16
- package/dist/autostart.js +43 -0
- package/dist/commands/local-args.js +383 -0
- package/dist/commands/local.js +7 -877
- package/dist/commands/session-sync.js +504 -0
- package/dist/evidence-upload-client.js +7 -0
- package/dist/local-state.js +1 -1
- package/dist/upload.js +152 -3
- package/package.json +2 -2
package/dist/autostart.js
CHANGED
|
@@ -36,6 +36,10 @@ export async function installAutostartAgent(options) {
|
|
|
36
36
|
const plistPath = plistPathFor(homeDir);
|
|
37
37
|
const stdoutPath = path.join(paths.state_dir, "sync.log");
|
|
38
38
|
const stderrPath = path.join(paths.state_dir, "sync.err.log");
|
|
39
|
+
// launchd will not reliably watch a path that does not exist at load time, so
|
|
40
|
+
// only feed it the transcript dirs that are present right now. A missing dir
|
|
41
|
+
// is fine — the StartInterval floor still covers it.
|
|
42
|
+
const watchPaths = await existingWatchPaths(homeDir);
|
|
39
43
|
await mkdir(path.dirname(plistPath), { recursive: true });
|
|
40
44
|
await mkdir(paths.state_dir, { recursive: true });
|
|
41
45
|
await writeFile(plistPath, renderPlist({
|
|
@@ -44,6 +48,7 @@ export async function installAutostartAgent(options) {
|
|
|
44
48
|
intervalSeconds,
|
|
45
49
|
stdoutPath,
|
|
46
50
|
stderrPath,
|
|
51
|
+
watchPaths,
|
|
47
52
|
}), "utf8");
|
|
48
53
|
// Unload first so a changed plist is actually picked up; a not-yet-loaded
|
|
49
54
|
// agent makes unload fail harmlessly, so the error is ignored.
|
|
@@ -103,6 +108,23 @@ export async function autostartStatus(options) {
|
|
|
103
108
|
plist_path: plistPath,
|
|
104
109
|
};
|
|
105
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* The transcript directories whose changes should re-trigger a sync, in the
|
|
113
|
+
* order they appear in WatchPaths. Resolved from the same homeDir as the plist
|
|
114
|
+
* and log paths so `--home` redirects them together.
|
|
115
|
+
*/
|
|
116
|
+
function watchPathCandidates(homeDir) {
|
|
117
|
+
return [
|
|
118
|
+
path.join(homeDir, ".claude", "projects"),
|
|
119
|
+
path.join(homeDir, ".codex", "sessions"),
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
/** Subset of the transcript dirs that exist now (see the WatchPaths comment). */
|
|
123
|
+
async function existingWatchPaths(homeDir) {
|
|
124
|
+
const candidates = watchPathCandidates(homeDir);
|
|
125
|
+
const present = await Promise.all(candidates.map((dir) => fileExists(dir)));
|
|
126
|
+
return candidates.filter((_, i) => present[i]);
|
|
127
|
+
}
|
|
106
128
|
function renderPlist(options) {
|
|
107
129
|
// The repo path is shell-quoted because it lands inside a `/bin/zsh -lc "…"`
|
|
108
130
|
// command string; the whole command is then XML-escaped for the <string>.
|
|
@@ -124,6 +146,12 @@ function renderPlist(options) {
|
|
|
124
146
|
` <integer>${options.intervalSeconds}</integer>`,
|
|
125
147
|
" <key>RunAtLoad</key>",
|
|
126
148
|
" <true/>",
|
|
149
|
+
// WatchPaths makes a transcript write fire `cockpit sync` within seconds, so
|
|
150
|
+
// captures land near-real-time; StartInterval above is the safety-net floor.
|
|
151
|
+
// It fires often, but launchd single-flights the job, sync holds its own
|
|
152
|
+
// lock, and growing-transcript re-upload is damped — so cost stays bounded
|
|
153
|
+
// and no extra debounce is needed.
|
|
154
|
+
...watchPathsBlock(options.watchPaths),
|
|
127
155
|
" <key>EnvironmentVariables</key>",
|
|
128
156
|
" <dict>",
|
|
129
157
|
" <key>PATH</key>",
|
|
@@ -138,6 +166,21 @@ function renderPlist(options) {
|
|
|
138
166
|
"",
|
|
139
167
|
].join("\n");
|
|
140
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Renders the `<key>WatchPaths</key><array>…</array>` lines, or nothing when no
|
|
171
|
+
* transcript dir exists yet (an empty array would tell launchd to watch
|
|
172
|
+
* everything-and-nothing; omitting the key leaves StartInterval as the floor).
|
|
173
|
+
*/
|
|
174
|
+
function watchPathsBlock(watchPaths) {
|
|
175
|
+
if (watchPaths.length === 0)
|
|
176
|
+
return [];
|
|
177
|
+
return [
|
|
178
|
+
" <key>WatchPaths</key>",
|
|
179
|
+
" <array>",
|
|
180
|
+
...watchPaths.map((dir) => ` <string>${xmlEscape(dir)}</string>`),
|
|
181
|
+
" </array>",
|
|
182
|
+
];
|
|
183
|
+
}
|
|
141
184
|
function shellQuote(value) {
|
|
142
185
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
143
186
|
}
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// CLI argument parsing for the local cockpit collector. Split out of
|
|
2
|
+
// commands/local.ts so the command runners read as a table of contents and
|
|
3
|
+
// the parser (pure, independently testable) can change at its own rate.
|
|
4
|
+
//
|
|
5
|
+
// Behavior-preserving extraction: functions moved verbatim, no logic change.
|
|
6
|
+
import { DEFAULT_DASHBOARD_URL } from "../local-state.js";
|
|
7
|
+
export function parseLocalArgs(argv) {
|
|
8
|
+
const command = argv[0];
|
|
9
|
+
switch (command) {
|
|
10
|
+
case "onboard":
|
|
11
|
+
return parseOnboardArgs(argv.slice(1));
|
|
12
|
+
case "install":
|
|
13
|
+
return parseInstallArgs(argv.slice(1));
|
|
14
|
+
case "login":
|
|
15
|
+
case "pair":
|
|
16
|
+
return parseLoginArgs(argv.slice(1));
|
|
17
|
+
case "logout":
|
|
18
|
+
return parseLogoutArgs(argv.slice(1));
|
|
19
|
+
case "start":
|
|
20
|
+
return parseStartArgs(argv.slice(1));
|
|
21
|
+
case "sync":
|
|
22
|
+
return parseSyncArgs(argv.slice(1));
|
|
23
|
+
case "status":
|
|
24
|
+
return parseStatusArgs(argv.slice(1));
|
|
25
|
+
case "sessions":
|
|
26
|
+
return parseSessionsArgs(argv.slice(1));
|
|
27
|
+
case "serve":
|
|
28
|
+
return parseServeArgs(argv.slice(1));
|
|
29
|
+
case "autostart":
|
|
30
|
+
return parseAutostartArgs(argv.slice(1));
|
|
31
|
+
default:
|
|
32
|
+
throw new Error(`Unknown local command: ${command ?? ""}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function parseOnboardArgs(args) {
|
|
36
|
+
const values = parseNamedArgs(args, {
|
|
37
|
+
allowedFlags: [
|
|
38
|
+
"--home",
|
|
39
|
+
"--repo",
|
|
40
|
+
"--dashboard-url",
|
|
41
|
+
"--email",
|
|
42
|
+
"--device-name",
|
|
43
|
+
"--ticket",
|
|
44
|
+
"--branch",
|
|
45
|
+
"--json",
|
|
46
|
+
"--poll-interval-ms",
|
|
47
|
+
"--timeout-ms",
|
|
48
|
+
"--max-depth",
|
|
49
|
+
"--max-repos",
|
|
50
|
+
],
|
|
51
|
+
valueFlags: [
|
|
52
|
+
"--home",
|
|
53
|
+
"--repo",
|
|
54
|
+
"--dashboard-url",
|
|
55
|
+
"--email",
|
|
56
|
+
"--device-name",
|
|
57
|
+
"--ticket",
|
|
58
|
+
"--branch",
|
|
59
|
+
"--poll-interval-ms",
|
|
60
|
+
"--timeout-ms",
|
|
61
|
+
"--max-depth",
|
|
62
|
+
"--max-repos",
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
assertNoPositionals(values.positionals, "onboard");
|
|
66
|
+
return {
|
|
67
|
+
kind: "onboard",
|
|
68
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
69
|
+
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
70
|
+
dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
|
|
71
|
+
claimedOwnerEmail: optionalEmail(values.flags.get("--email")),
|
|
72
|
+
deviceName: optionalNonEmpty(values.flags.get("--device-name")),
|
|
73
|
+
activeTicketId: optionalNonEmpty(values.flags.get("--ticket")),
|
|
74
|
+
branch: optionalNonEmpty(values.flags.get("--branch")),
|
|
75
|
+
json: values.booleans.has("--json"),
|
|
76
|
+
pollIntervalMs: optionalPositiveInteger(values.flags.get("--poll-interval-ms"), "--poll-interval-ms"),
|
|
77
|
+
timeoutMs: optionalPositiveInteger(values.flags.get("--timeout-ms"), "--timeout-ms"),
|
|
78
|
+
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
79
|
+
maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function parseInstallArgs(args) {
|
|
83
|
+
const values = parseNamedArgs(args, {
|
|
84
|
+
allowedFlags: [
|
|
85
|
+
"--home",
|
|
86
|
+
"--repo",
|
|
87
|
+
"--dashboard-url",
|
|
88
|
+
"--supabase-url",
|
|
89
|
+
"--json",
|
|
90
|
+
],
|
|
91
|
+
valueFlags: ["--home", "--repo", "--dashboard-url", "--supabase-url"],
|
|
92
|
+
});
|
|
93
|
+
assertNoPositionals(values.positionals, "install");
|
|
94
|
+
return {
|
|
95
|
+
kind: "install",
|
|
96
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
97
|
+
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
98
|
+
dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
|
|
99
|
+
supabaseUrl: optionalNonEmpty(values.flags.get("--supabase-url")),
|
|
100
|
+
json: values.booleans.has("--json"),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function parseLoginArgs(args) {
|
|
104
|
+
const values = parseNamedArgs(args, {
|
|
105
|
+
allowedFlags: [
|
|
106
|
+
"--home",
|
|
107
|
+
"--dashboard-url",
|
|
108
|
+
"--email",
|
|
109
|
+
"--device-name",
|
|
110
|
+
"--json",
|
|
111
|
+
"--poll-interval-ms",
|
|
112
|
+
"--timeout-ms",
|
|
113
|
+
],
|
|
114
|
+
valueFlags: [
|
|
115
|
+
"--home",
|
|
116
|
+
"--dashboard-url",
|
|
117
|
+
"--email",
|
|
118
|
+
"--device-name",
|
|
119
|
+
"--poll-interval-ms",
|
|
120
|
+
"--timeout-ms",
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
assertNoPositionals(values.positionals, "login");
|
|
124
|
+
return {
|
|
125
|
+
kind: "login",
|
|
126
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
127
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
128
|
+
claimedOwnerEmail: optionalEmail(values.flags.get("--email")),
|
|
129
|
+
deviceName: optionalNonEmpty(values.flags.get("--device-name")),
|
|
130
|
+
json: values.booleans.has("--json"),
|
|
131
|
+
pollIntervalMs: optionalPositiveInteger(values.flags.get("--poll-interval-ms"), "--poll-interval-ms"),
|
|
132
|
+
timeoutMs: optionalPositiveInteger(values.flags.get("--timeout-ms"), "--timeout-ms"),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function parseLogoutArgs(args) {
|
|
136
|
+
const values = parseNamedArgs(args, {
|
|
137
|
+
allowedFlags: ["--home", "--json"],
|
|
138
|
+
valueFlags: ["--home"],
|
|
139
|
+
});
|
|
140
|
+
assertNoPositionals(values.positionals, "logout");
|
|
141
|
+
return {
|
|
142
|
+
kind: "logout",
|
|
143
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
144
|
+
json: values.booleans.has("--json"),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function parseStartArgs(args) {
|
|
148
|
+
const values = parseNamedArgs(args, {
|
|
149
|
+
allowedFlags: [
|
|
150
|
+
"--home",
|
|
151
|
+
"--repo",
|
|
152
|
+
"--branch",
|
|
153
|
+
"--ticket",
|
|
154
|
+
"--operator-id",
|
|
155
|
+
"--session-id",
|
|
156
|
+
"--json",
|
|
157
|
+
"--max-depth",
|
|
158
|
+
"--max-repos",
|
|
159
|
+
],
|
|
160
|
+
valueFlags: [
|
|
161
|
+
"--home",
|
|
162
|
+
"--repo",
|
|
163
|
+
"--branch",
|
|
164
|
+
"--ticket",
|
|
165
|
+
"--operator-id",
|
|
166
|
+
"--session-id",
|
|
167
|
+
"--max-depth",
|
|
168
|
+
"--max-repos",
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
assertNoPositionals(values.positionals, "start");
|
|
172
|
+
return {
|
|
173
|
+
kind: "start",
|
|
174
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
175
|
+
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
176
|
+
branch: optionalNonEmpty(values.flags.get("--branch")),
|
|
177
|
+
activeTicketId: optionalNonEmpty(values.flags.get("--ticket")),
|
|
178
|
+
operatorId: optionalNonEmpty(values.flags.get("--operator-id")),
|
|
179
|
+
sessionId: optionalNonEmpty(values.flags.get("--session-id")),
|
|
180
|
+
json: values.booleans.has("--json"),
|
|
181
|
+
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
182
|
+
maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function parseSyncArgs(args) {
|
|
186
|
+
const values = parseNamedArgs(args, {
|
|
187
|
+
allowedFlags: ["--home", "--repo", "--dashboard-url", "--json", "--max-depth", "--max-repos"],
|
|
188
|
+
valueFlags: ["--home", "--repo", "--dashboard-url", "--max-depth", "--max-repos"],
|
|
189
|
+
});
|
|
190
|
+
assertNoPositionals(values.positionals, "sync");
|
|
191
|
+
return {
|
|
192
|
+
kind: "sync",
|
|
193
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
194
|
+
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
195
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
196
|
+
json: values.booleans.has("--json"),
|
|
197
|
+
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
198
|
+
maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function parseStatusArgs(args) {
|
|
202
|
+
const values = parseNamedArgs(args, {
|
|
203
|
+
allowedFlags: ["--home", "--repo", "--json", "--max-depth", "--max-repos"],
|
|
204
|
+
valueFlags: ["--home", "--repo", "--max-depth", "--max-repos"],
|
|
205
|
+
});
|
|
206
|
+
assertNoPositionals(values.positionals, "status");
|
|
207
|
+
return {
|
|
208
|
+
kind: "status",
|
|
209
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
210
|
+
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
211
|
+
json: values.booleans.has("--json"),
|
|
212
|
+
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
213
|
+
maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function parseSessionsArgs(args) {
|
|
217
|
+
const values = parseNamedArgs(args, {
|
|
218
|
+
allowedFlags: ["--home", "--repo", "--source", "--json", "--max-depth", "--max-repos"],
|
|
219
|
+
valueFlags: ["--home", "--repo", "--source", "--max-depth", "--max-repos"],
|
|
220
|
+
});
|
|
221
|
+
assertNoPositionals(values.positionals, "sessions");
|
|
222
|
+
const source = values.flags.get("--source");
|
|
223
|
+
if (source !== undefined && source !== "codex" && source !== "claude") {
|
|
224
|
+
throw new Error("--source must be 'codex' or 'claude'.");
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
kind: "sessions",
|
|
228
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
229
|
+
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
230
|
+
source,
|
|
231
|
+
json: values.booleans.has("--json"),
|
|
232
|
+
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
233
|
+
maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function parseServeArgs(args) {
|
|
237
|
+
const values = parseNamedArgs(args, {
|
|
238
|
+
allowedFlags: ["--home", "--repo", "--port"],
|
|
239
|
+
valueFlags: ["--home", "--repo", "--port"],
|
|
240
|
+
});
|
|
241
|
+
assertNoPositionals(values.positionals, "serve");
|
|
242
|
+
const port = Number(values.flags.get("--port") ?? "4174");
|
|
243
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
244
|
+
throw new Error("--port must be a TCP port between 1 and 65535.");
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
kind: "serve",
|
|
248
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
249
|
+
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
250
|
+
port,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function parseAutostartArgs(args) {
|
|
254
|
+
const values = parseNamedArgs(args, {
|
|
255
|
+
allowedFlags: [
|
|
256
|
+
"--home",
|
|
257
|
+
"--repo",
|
|
258
|
+
"--dashboard-url",
|
|
259
|
+
"--interval-seconds",
|
|
260
|
+
"--json",
|
|
261
|
+
],
|
|
262
|
+
valueFlags: ["--home", "--repo", "--dashboard-url", "--interval-seconds"],
|
|
263
|
+
});
|
|
264
|
+
if (values.positionals.length > 1) {
|
|
265
|
+
throw new Error("autostart accepts at most one action (install|uninstall|status).");
|
|
266
|
+
}
|
|
267
|
+
const action = values.positionals[0] ?? "install";
|
|
268
|
+
if (action !== "install" && action !== "uninstall" && action !== "status") {
|
|
269
|
+
throw new Error("autostart action must be install, uninstall, or status.");
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
kind: "autostart",
|
|
273
|
+
action,
|
|
274
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
275
|
+
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
276
|
+
dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
|
|
277
|
+
intervalSeconds: optionalPositiveInteger(values.flags.get("--interval-seconds"), "--interval-seconds") ?? 1800,
|
|
278
|
+
json: values.booleans.has("--json"),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function parseNamedArgs(args, options) {
|
|
282
|
+
const allowed = new Set(options.allowedFlags);
|
|
283
|
+
const valueFlags = new Set(options.valueFlags);
|
|
284
|
+
const flags = new Map();
|
|
285
|
+
const booleans = new Set();
|
|
286
|
+
const positionals = [];
|
|
287
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
288
|
+
const arg = args[index] ?? "";
|
|
289
|
+
rejectServiceRoleLikeArgument(arg);
|
|
290
|
+
if (!arg.startsWith("--")) {
|
|
291
|
+
positionals.push(arg);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const [flag, inlineValue] = arg.split("=", 2);
|
|
295
|
+
if (!allowed.has(flag))
|
|
296
|
+
throw new Error(`Unknown flag: ${flag}`);
|
|
297
|
+
if (valueFlags.has(flag)) {
|
|
298
|
+
const value = inlineValue ?? args[index + 1];
|
|
299
|
+
if (!value || value.startsWith("--")) {
|
|
300
|
+
throw new Error(`${flag} requires a value.`);
|
|
301
|
+
}
|
|
302
|
+
rejectServiceRoleLikeArgument(value);
|
|
303
|
+
flags.set(flag, value);
|
|
304
|
+
if (inlineValue === undefined)
|
|
305
|
+
index += 1;
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
if (inlineValue !== undefined)
|
|
309
|
+
throw new Error(`${flag} does not accept a value.`);
|
|
310
|
+
booleans.add(flag);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return { flags, booleans, positionals };
|
|
314
|
+
}
|
|
315
|
+
function assertNoPositionals(positionals, command) {
|
|
316
|
+
if (positionals.length > 0) {
|
|
317
|
+
throw new Error(`${command} does not accept positional arguments.`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function optionalNonEmpty(value) {
|
|
321
|
+
const trimmed = value?.trim();
|
|
322
|
+
return trimmed ? trimmed : undefined;
|
|
323
|
+
}
|
|
324
|
+
function optionalUrl(value) {
|
|
325
|
+
return value === undefined ? undefined : normalizeUrl(value);
|
|
326
|
+
}
|
|
327
|
+
function optionalEmail(value) {
|
|
328
|
+
const trimmed = value?.trim().toLowerCase();
|
|
329
|
+
if (!trimmed)
|
|
330
|
+
return undefined;
|
|
331
|
+
if (!trimmed.includes("@")) {
|
|
332
|
+
throw new Error("--email must be a valid email address.");
|
|
333
|
+
}
|
|
334
|
+
return trimmed;
|
|
335
|
+
}
|
|
336
|
+
function optionalPositiveInteger(value, flag) {
|
|
337
|
+
if (value === undefined)
|
|
338
|
+
return undefined;
|
|
339
|
+
const parsed = Number(value);
|
|
340
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
341
|
+
throw new Error(`${flag} must be a positive integer.`);
|
|
342
|
+
}
|
|
343
|
+
return parsed;
|
|
344
|
+
}
|
|
345
|
+
export function normalizeUrl(value) {
|
|
346
|
+
const trimmed = value.trim().replace(/\/+$/, "");
|
|
347
|
+
if (!trimmed)
|
|
348
|
+
throw new Error("URL value cannot be empty.");
|
|
349
|
+
return trimmed;
|
|
350
|
+
}
|
|
351
|
+
function rejectServiceRoleLikeArgument(value) {
|
|
352
|
+
if (!looksLikeServiceRoleSecret(value))
|
|
353
|
+
return;
|
|
354
|
+
throw new Error("Service-role credentials are not accepted by local collector commands.");
|
|
355
|
+
}
|
|
356
|
+
function looksLikeServiceRoleSecret(value) {
|
|
357
|
+
if (serviceCredentialNamePattern().test(value))
|
|
358
|
+
return true;
|
|
359
|
+
const parts = value.split(".");
|
|
360
|
+
if (parts.length !== 3)
|
|
361
|
+
return false;
|
|
362
|
+
try {
|
|
363
|
+
const payload = Buffer.from(base64UrlToBase64(parts[1] ?? ""), "base64").toString("utf8");
|
|
364
|
+
return serviceCredentialPayloadPattern().test(payload);
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function serviceCredentialNamePattern() {
|
|
371
|
+
return new RegExp([
|
|
372
|
+
["SUPABASE", "SERVICE", "ROLE", "KEY"].join("[_-]?"),
|
|
373
|
+
["service", "role"].join("[_-]?"),
|
|
374
|
+
].join("|"), "i");
|
|
375
|
+
}
|
|
376
|
+
function serviceCredentialPayloadPattern() {
|
|
377
|
+
const privilegedRole = ["service", "role"].join("_");
|
|
378
|
+
return new RegExp(`"role"\\s*:\\s*"${privilegedRole}"`);
|
|
379
|
+
}
|
|
380
|
+
function base64UrlToBase64(value) {
|
|
381
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
382
|
+
return `${normalized}${"=".repeat((4 - (normalized.length % 4)) % 4)}`;
|
|
383
|
+
}
|