tracksiy 0.0.0-stage → 0.1.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.
@@ -0,0 +1,3053 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/auth.ts
4
+ import { readFileSync as readFileSync2 } from "node:fs";
5
+
6
+ // src/version.ts
7
+ var VERSION = true ? "0.1.0" : "0.0.0-dev";
8
+ var USER_AGENT = `tracksiy-cli/${VERSION} (node ${process.versions.node}; ${process.platform})`;
9
+
10
+ // src/core/errors.ts
11
+ var EXIT = {
12
+ ok: 0,
13
+ internal: 1,
14
+ usage: 2,
15
+ auth: 3,
16
+ notFound: 4,
17
+ invalid: 5,
18
+ rateLimited: 6,
19
+ network: 7,
20
+ server: 8,
21
+ unsupported: 9,
22
+ checkFailed: 10
23
+ };
24
+ var EXIT_MEANINGS = {
25
+ 0: "success",
26
+ 1: "internal error in the CLI itself",
27
+ 2: "usage error: unknown command, flag or bad value; nothing was sent",
28
+ 3: "authentication or authorization failed (no token, expired, or missing scope)",
29
+ 4: "not found (organization, project, issue, token, monitor...)",
30
+ 5: "the server rejected the request as invalid or conflicting",
31
+ 6: "rate limited; retry after the advertised delay",
32
+ 7: "network error or timeout; the request may not have reached the server",
33
+ 8: "server error (5xx)",
34
+ 9: "the server does not support this command yet (older server)",
35
+ 10: "the command ran but the check it performs did not pass (e.g. health not ok, integration not verified, wait timed out)"
36
+ };
37
+ var CliError = class extends Error {
38
+ code;
39
+ exit;
40
+ hint;
41
+ status;
42
+ details;
43
+ constructor(code, message, opts = {}) {
44
+ super(message);
45
+ this.code = code;
46
+ this.exit = opts.exit ?? EXIT.internal;
47
+ this.hint = opts.hint;
48
+ this.status = opts.status;
49
+ this.details = opts.details;
50
+ }
51
+ toJSON() {
52
+ const out = { code: this.code, message: this.message };
53
+ if (this.hint) out.hint = this.hint;
54
+ if (this.status !== void 0) out.status = this.status;
55
+ if (this.details !== void 0) out.details = this.details;
56
+ return out;
57
+ }
58
+ };
59
+ function usageError(code, message, hint) {
60
+ return new CliError(`usage.${code}`, message, { exit: EXIT.usage, hint });
61
+ }
62
+ var CheckFailed = class extends CliError {
63
+ data;
64
+ constructor(message, data, hint) {
65
+ super("check.failed", message, { exit: EXIT.checkFailed, hint });
66
+ this.data = data;
67
+ }
68
+ };
69
+ function toCliError(err) {
70
+ if (err instanceof CliError) return err;
71
+ const message = err instanceof Error ? err.message : String(err);
72
+ return new CliError("internal.unexpected", message, {
73
+ exit: EXIT.internal,
74
+ hint: "This is a bug in the CLI. Re-run with --verbose and report it."
75
+ });
76
+ }
77
+
78
+ // src/core/http.ts
79
+ var API_PREFIX = "/api/agent/v1/";
80
+ var seg = (value) => encodeURIComponent(String(value));
81
+ var IDEMPOTENT = /* @__PURE__ */ new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
82
+ var RETRY_STATUS = /* @__PURE__ */ new Set([429, 502, 503, 504]);
83
+ var MAX_ATTEMPTS = 3;
84
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
85
+ var Api = class {
86
+ url;
87
+ hasToken;
88
+ opts;
89
+ indexCache;
90
+ constructor(opts) {
91
+ this.opts = opts;
92
+ this.url = opts.url;
93
+ this.hasToken = Boolean(opts.token);
94
+ }
95
+ get base() {
96
+ return this.url + API_PREFIX;
97
+ }
98
+ async get(path, query, opts = {}) {
99
+ return (await this.request("GET", path, { ...opts, query })).data;
100
+ }
101
+ async send(method, path, body, opts = {}) {
102
+ return (await this.request(method, path, { ...opts, body })).data;
103
+ }
104
+ buildUrl(path, query) {
105
+ const target = new URL(path.startsWith("/") ? this.url + path : this.base + path);
106
+ for (const [key, value] of Object.entries(query ?? {})) {
107
+ if (value === void 0 || value === null || value === "") continue;
108
+ if (Array.isArray(value)) for (const v of value) target.searchParams.append(key, String(v));
109
+ else target.searchParams.set(key, String(value));
110
+ }
111
+ return target.toString();
112
+ }
113
+ async request(method, path, opts = {}) {
114
+ const url = this.buildUrl(path, opts.query);
115
+ const headers = { "User-Agent": USER_AGENT, Accept: "application/json" };
116
+ const useAuth = opts.auth !== false;
117
+ if (useAuth && this.opts.token) headers.Authorization = `Bearer ${this.opts.token}`;
118
+ let body;
119
+ if (opts.form) body = opts.form;
120
+ else if (opts.body !== void 0) {
121
+ headers["Content-Type"] = "application/json";
122
+ body = JSON.stringify(opts.body);
123
+ }
124
+ const retry = opts.retry ?? IDEMPOTENT.has(method);
125
+ const doFetch = this.opts.fetch ?? fetch;
126
+ for (let attempt = 1; ; attempt++) {
127
+ const started = Date.now();
128
+ if (this.opts.verbose) this.opts.log(`-> ${method} ${url}`);
129
+ let res;
130
+ try {
131
+ res = await doFetch(url, { method, headers, body, signal: AbortSignal.timeout(opts.timeoutMs ?? this.opts.timeoutMs) });
132
+ } catch (err) {
133
+ const mapped = networkError(err, url, opts.timeoutMs ?? this.opts.timeoutMs);
134
+ if (retry && attempt < MAX_ATTEMPTS) {
135
+ if (this.opts.verbose) this.opts.log(` ${mapped.code}; retrying`);
136
+ await sleep(backoff(attempt));
137
+ continue;
138
+ }
139
+ throw mapped;
140
+ }
141
+ if (this.opts.verbose) this.opts.log(`<- ${res.status} (${Date.now() - started} ms)`);
142
+ if (retry && RETRY_STATUS.has(res.status) && attempt < MAX_ATTEMPTS) {
143
+ const wait = retryAfter(res.headers) ?? backoff(attempt);
144
+ if (wait <= 3e4) {
145
+ await res.arrayBuffer().catch(() => void 0);
146
+ await sleep(wait);
147
+ continue;
148
+ }
149
+ }
150
+ const data = await readBody(res);
151
+ const ok = opts.accept ? opts.accept.includes(res.status) : res.ok;
152
+ if (ok) return { status: res.status, data, headers: res.headers };
153
+ throw await this.httpError(method, path, res, data);
154
+ }
155
+ }
156
+ /** The server's own list of endpoints and scopes. No token needed. */
157
+ index() {
158
+ this.indexCache ??= this.get("", void 0, { auth: false }).then(
159
+ (d) => d.endpoints ?? []
160
+ );
161
+ return this.indexCache;
162
+ }
163
+ async findEndpoint(method, path) {
164
+ let entries;
165
+ try {
166
+ entries = await this.index();
167
+ } catch {
168
+ return null;
169
+ }
170
+ const wanted = new URL(path.startsWith("/") ? this.url + path : this.base + path).pathname.replace(/\/+$/, "");
171
+ return entries.find((e) => {
172
+ if (e.method !== method) return false;
173
+ const pattern = e.path.replace(/\/+$/, "").replace(/[.*+?^$()|[\]\\]/g, "\\$&").replace(/\{[^}]+\}/g, "[^/]+");
174
+ return new RegExp(`^${pattern}$`).test(wanted);
175
+ });
176
+ }
177
+ async httpError(method, path, res, data) {
178
+ const status2 = res.status;
179
+ const message = describeBody(data) || `${method} ${path} failed with HTTP ${status2}`;
180
+ const details = typeof data === "object" && data !== null ? data : void 0;
181
+ if (status2 === 401) {
182
+ return new CliError("auth.unauthenticated", message, {
183
+ exit: EXIT.auth,
184
+ status: status2,
185
+ hint: this.hasToken ? "The token was rejected: it may be expired, revoked or for another server. Check it with `tracksiy auth status`." : "No token. Set TRACKSIY_TOKEN, pass --token, or run `tracksiy auth login --with-token < token.txt`."
186
+ });
187
+ }
188
+ if (status2 === 403) {
189
+ const entry = await this.findEndpoint(method, path);
190
+ const scopes = entry?.scopes?.length ? ` It needs one of: ${entry.scopes.join(", ")}.` : "";
191
+ return new CliError("auth.forbidden", message, {
192
+ exit: EXIT.auth,
193
+ status: status2,
194
+ hint: `The token is valid but not allowed to do this.${scopes} A restricted token also cannot reach projects outside its list.`,
195
+ details
196
+ });
197
+ }
198
+ if (status2 === 404) {
199
+ const entry = await this.findEndpoint(method, path);
200
+ if (entry === void 0) {
201
+ return new CliError("server.unsupported", `${this.url} does not have ${method} ${path.split("?")[0]}.`, {
202
+ exit: EXIT.unsupported,
203
+ status: status2,
204
+ hint: "The server is older than this CLI. Upgrade the server, or use a CLI version that matches it."
205
+ });
206
+ }
207
+ return new CliError("not_found", message, {
208
+ exit: EXIT.notFound,
209
+ status: status2,
210
+ hint: "Check the organization/project slugs (`tracksiy orgs list`) and the id. A project-restricted token sees other projects as not found.",
211
+ details
212
+ });
213
+ }
214
+ if (status2 === 429) {
215
+ const wait = retryAfter(res.headers);
216
+ return new CliError("rate_limited", message, {
217
+ exit: EXIT.rateLimited,
218
+ status: status2,
219
+ hint: wait ? `Retry after ${Math.ceil(wait / 1e3)} s.` : "Retry later.",
220
+ details: { retryAfterSeconds: wait ? Math.ceil(wait / 1e3) : null }
221
+ });
222
+ }
223
+ if (status2 >= 500) {
224
+ return new CliError("server.error", message, {
225
+ exit: EXIT.server,
226
+ status: status2,
227
+ hint: "The server failed. `tracksiy health` shows whether the platform itself is healthy; if it keeps happening, tell the Tracksiy team: `tracksiy support` says how."
228
+ });
229
+ }
230
+ return new CliError(status2 === 409 ? "request.conflict" : "request.invalid", message, {
231
+ exit: EXIT.invalid,
232
+ status: status2,
233
+ details
234
+ });
235
+ }
236
+ };
237
+ async function readBody(res) {
238
+ if (res.status === 204) return null;
239
+ const text = await res.text();
240
+ if (!text) return null;
241
+ try {
242
+ return JSON.parse(text);
243
+ } catch {
244
+ return text.length > 2e3 ? text.slice(0, 2e3) + "..." : text;
245
+ }
246
+ }
247
+ function describeBody(data) {
248
+ if (typeof data === "string") return data.trim().startsWith("<") ? "" : data.trim();
249
+ if (!data || typeof data !== "object") return "";
250
+ const d = data;
251
+ const detail = d.detail ?? d.message ?? d.error;
252
+ if (typeof detail === "string") return detail;
253
+ if (Array.isArray(detail)) {
254
+ return detail.map((item) => {
255
+ if (typeof item === "string") return item;
256
+ const loc = Array.isArray(item?.loc) ? item.loc.filter((p) => p !== "body" && p !== "payload").join(".") : "";
257
+ return loc ? `${loc}: ${item?.msg ?? JSON.stringify(item)}` : item?.msg ?? JSON.stringify(item);
258
+ }).join("; ");
259
+ }
260
+ if (detail && typeof detail === "object") return JSON.stringify(detail);
261
+ return "";
262
+ }
263
+ function networkError(err, url, timeoutMs) {
264
+ const e = err;
265
+ if (e?.name === "TimeoutError" || e?.name === "AbortError") {
266
+ return new CliError("network.timeout", `No response from ${new URL(url).host} within ${timeoutMs / 1e3} s.`, {
267
+ exit: EXIT.network,
268
+ hint: "Raise --timeout, or check that the server is reachable."
269
+ });
270
+ }
271
+ const code = e?.cause?.code;
272
+ return new CliError("network.unreachable", `Cannot reach ${new URL(url).host}: ${code ?? e?.cause?.message ?? e?.message}`, {
273
+ exit: EXIT.network,
274
+ hint: "Check --url / TRACKSIY_URL and the network. The request did not reach the server."
275
+ });
276
+ }
277
+ function backoff(attempt) {
278
+ return 500 * 2 ** (attempt - 1) + Math.floor(Math.random() * 250);
279
+ }
280
+ function retryAfter(headers) {
281
+ const raw = headers.get("retry-after");
282
+ if (!raw) return void 0;
283
+ const seconds = Number(raw);
284
+ if (Number.isFinite(seconds)) return seconds * 1e3;
285
+ const date = Date.parse(raw);
286
+ return Number.isFinite(date) ? Math.max(0, date - Date.now()) : void 0;
287
+ }
288
+
289
+ // src/core/config.ts
290
+ import { homedir } from "node:os";
291
+ import { dirname, join, resolve } from "node:path";
292
+ import { existsSync, readFileSync } from "node:fs";
293
+ import { mkdir, writeFile, chmod, rename } from "node:fs/promises";
294
+ var DEFAULT_URL = "https://tracksiy.com";
295
+ var PROJECT_FILE = ".tracksiy.json";
296
+ function configPath(env = process.env) {
297
+ if (env.TRACKSIY_CONFIG) return resolve(env.TRACKSIY_CONFIG);
298
+ const base = env.XDG_CONFIG_HOME || join(homedir(), ".config");
299
+ return join(base, "tracksiy", "config.json");
300
+ }
301
+ function readUserConfig(path) {
302
+ if (!existsSync(path)) return { hosts: {} };
303
+ try {
304
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
305
+ return { defaultUrl: parsed.defaultUrl, hosts: parsed.hosts ?? {} };
306
+ } catch (err) {
307
+ throw new CliError("config.unreadable", `Cannot parse ${path}: ${err.message}`, {
308
+ exit: EXIT.usage,
309
+ hint: `Fix or delete ${path}, then run \`tracksiy auth login\` again.`
310
+ });
311
+ }
312
+ }
313
+ async function writeUserConfig(path, cfg) {
314
+ await mkdir(dirname(path), { recursive: true, mode: 448 });
315
+ const tmp = `${path}.${process.pid}.tmp`;
316
+ await writeFile(tmp, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
317
+ await chmod(tmp, 384);
318
+ await rename(tmp, path);
319
+ }
320
+ function findProjectFile(start) {
321
+ let dir = resolve(start);
322
+ for (; ; ) {
323
+ const candidate = join(dir, PROJECT_FILE);
324
+ if (existsSync(candidate)) {
325
+ try {
326
+ return { path: candidate, data: JSON.parse(readFileSync(candidate, "utf8")) };
327
+ } catch (err) {
328
+ throw new CliError("config.bad_project_file", `Cannot parse ${candidate}: ${err.message}`, {
329
+ exit: EXIT.usage,
330
+ hint: `It should look like {"org": "my-org", "project": "my-project"}.`
331
+ });
332
+ }
333
+ }
334
+ const parent = dirname(dir);
335
+ if (parent === dir) return void 0;
336
+ dir = parent;
337
+ }
338
+ }
339
+ function normalizeUrl(raw) {
340
+ let url;
341
+ try {
342
+ url = new URL(raw.includes("://") ? raw : `https://${raw}`);
343
+ } catch {
344
+ throw usageError("bad_url", `Not a URL: ${raw}`, "Pass the server root, e.g. --url https://tracksiy.com");
345
+ }
346
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
347
+ throw usageError("bad_url", `Unsupported scheme ${url.protocol} in ${raw}`);
348
+ }
349
+ let path = url.pathname.replace(/\/+$/, "");
350
+ path = path.replace(/\/api\/agent\/v1$/, "").replace(/\/api$/, "");
351
+ return `${url.protocol}//${url.host}${path}`;
352
+ }
353
+ function isLoopback(url) {
354
+ const host = new URL(url).hostname;
355
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
356
+ }
357
+ function resolveConfig(globals, env = process.env, cwd = process.cwd()) {
358
+ const path = configPath(env);
359
+ const user = readUserConfig(path);
360
+ const pf = findProjectFile(cwd);
361
+ let url;
362
+ let urlSource;
363
+ if (globals.url) [url, urlSource] = [globals.url, "--url"];
364
+ else if (env.TRACKSIY_URL) [url, urlSource] = [env.TRACKSIY_URL, "TRACKSIY_URL"];
365
+ else if (pf?.data.url) [url, urlSource] = [pf.data.url, pf.path];
366
+ else if (user.defaultUrl) [url, urlSource] = [user.defaultUrl, path];
367
+ else [url, urlSource] = [DEFAULT_URL, "default"];
368
+ url = normalizeUrl(url);
369
+ const host = user.hosts[url] ?? {};
370
+ const out = {
371
+ url,
372
+ urlSource,
373
+ configPath: path,
374
+ timeoutMs: (globals.timeout ?? (Number(env.TRACKSIY_TIMEOUT) || 30)) * 1e3,
375
+ verbose: Boolean(globals.verbose),
376
+ quiet: Boolean(globals.quiet)
377
+ };
378
+ if (pf) out.projectFile = pf.path;
379
+ if (globals.token) [out.token, out.tokenSource] = [globals.token, "--token"];
380
+ else if (env.TRACKSIY_TOKEN) [out.token, out.tokenSource] = [env.TRACKSIY_TOKEN, "TRACKSIY_TOKEN"];
381
+ else if (host.token) [out.token, out.tokenSource] = [host.token, path];
382
+ if (out.token && url.startsWith("http:") && !isLoopback(url) && env.TRACKSIY_ALLOW_INSECURE !== "1") {
383
+ throw new CliError("config.insecure_url", `Refusing to send a token over plain HTTP to ${url}.`, {
384
+ exit: EXIT.usage,
385
+ hint: "Use https, a loopback address, or set TRACKSIY_ALLOW_INSECURE=1 if you really mean it."
386
+ });
387
+ }
388
+ if (globals.org) [out.org, out.orgSource] = [globals.org, "--org"];
389
+ else if (env.TRACKSIY_ORG) [out.org, out.orgSource] = [env.TRACKSIY_ORG, "TRACKSIY_ORG"];
390
+ else if (pf?.data.org) [out.org, out.orgSource] = [pf.data.org, pf.path];
391
+ else if (host.org) [out.org, out.orgSource] = [host.org, path];
392
+ if (globals.project) [out.project, out.projectSource] = [globals.project, "--project"];
393
+ else if (env.TRACKSIY_PROJECT) [out.project, out.projectSource] = [env.TRACKSIY_PROJECT, "TRACKSIY_PROJECT"];
394
+ else if (pf?.data.project) [out.project, out.projectSource] = [pf.data.project, pf.path];
395
+ if (out.project?.includes("/")) {
396
+ const [org, project2] = out.project.split("/", 2);
397
+ if (out.org && out.orgSource === "--org" && out.org !== org) {
398
+ throw usageError("conflict", `--org ${out.org} conflicts with --project ${out.project}`);
399
+ }
400
+ [out.org, out.orgSource] = [org, out.projectSource];
401
+ out.project = project2;
402
+ }
403
+ return out;
404
+ }
405
+
406
+ // src/core/output.ts
407
+ function resolveFormat(globals, env, isTTY) {
408
+ if (globals.json) return "json";
409
+ if (globals.format) return globals.format;
410
+ if (env.TRACKSIY_FORMAT === "json" || env.TRACKSIY_FORMAT === "text") return env.TRACKSIY_FORMAT;
411
+ return isTTY ? "text" : "json";
412
+ }
413
+ function buildTree(paths) {
414
+ const root = {};
415
+ for (const path of paths) {
416
+ const segs = path.split(".").filter(Boolean);
417
+ let node = root;
418
+ segs.forEach((seg2, i) => {
419
+ if (i === segs.length - 1) node[seg2] = true;
420
+ else {
421
+ const next = node[seg2];
422
+ if (next === true) return;
423
+ node = node[seg2] = next ?? {};
424
+ }
425
+ });
426
+ }
427
+ return root;
428
+ }
429
+ function project(value, tree) {
430
+ if (Array.isArray(value)) return value.map((v) => project(v, tree));
431
+ if (!value || typeof value !== "object") return value;
432
+ const src = value;
433
+ const out = {};
434
+ for (const [key, sub] of Object.entries(tree)) {
435
+ if (!(key in src)) continue;
436
+ out[key] = sub === true ? src[key] : project(src[key], sub);
437
+ }
438
+ return out;
439
+ }
440
+ function selectFields(data, paths) {
441
+ if (!paths?.length) return data;
442
+ return project(data, buildTree(paths));
443
+ }
444
+ var processWriter = {
445
+ stdout: (s) => process.stdout.write(s),
446
+ stderr: (s) => process.stderr.write(s)
447
+ };
448
+ function printSuccess(w, format, data, spec, input, select) {
449
+ const shown = selectFields(data, select);
450
+ if (format === "json") {
451
+ w.stdout(JSON.stringify({ ok: true, data: shown ?? null }) + "\n");
452
+ return;
453
+ }
454
+ const text = !select?.length && spec?.text ? spec.text(shown, input) : renderText(shown);
455
+ if (text) w.stdout(text.endsWith("\n") ? text : text + "\n");
456
+ }
457
+ function printError(w, format, err, spec, input = {}, select) {
458
+ const data = err instanceof CheckFailed ? selectFields(err.data, select) : void 0;
459
+ if (format === "json") {
460
+ const out = { ok: false, error: err.toJSON() };
461
+ if (data !== void 0) out.data = data;
462
+ w.stdout(JSON.stringify(out) + "\n");
463
+ return;
464
+ }
465
+ if (data !== void 0) {
466
+ const text = !select?.length && spec?.text ? spec.text(data, input) : renderText(data);
467
+ if (text) w.stdout(text.endsWith("\n") ? text : text + "\n");
468
+ }
469
+ w.stderr(`error [${err.code}]: ${err.message}
470
+ `);
471
+ if (err.hint) w.stderr(`hint: ${err.hint}
472
+ `);
473
+ }
474
+ var width = () => Math.max(60, Math.min(process.stdout.columns || 120, 200));
475
+ function scalar(v) {
476
+ if (v === null || v === void 0) return "-";
477
+ if (typeof v === "string") return v;
478
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
479
+ if (Array.isArray(v)) return v.every((x) => typeof x !== "object" || x === null) ? v.join(", ") : `[${v.length}]`;
480
+ return JSON.stringify(v);
481
+ }
482
+ var isScalar = (v) => v === null || typeof v !== "object" || Array.isArray(v) && v.every((x) => typeof x !== "object" || x === null);
483
+ function ago(iso) {
484
+ if (typeof iso !== "string") return "-";
485
+ const t = Date.parse(iso);
486
+ if (!Number.isFinite(t)) return iso;
487
+ const s = Math.round((Date.now() - t) / 1e3);
488
+ const abs = Math.abs(s);
489
+ const unit = abs < 60 ? `${abs}s` : abs < 3600 ? `${Math.round(abs / 60)}m` : abs < 86400 ? `${Math.round(abs / 3600)}h` : `${Math.round(abs / 86400)}d`;
490
+ return s >= 0 ? `${unit} ago` : `in ${unit}`;
491
+ }
492
+ function table(rows, columns2) {
493
+ if (!rows.length) return "(none)";
494
+ const cols = (columns2 ?? Object.keys(rows[0]).filter((k) => isScalar(rows[0][k])).slice(0, 7)).map(
495
+ (c) => typeof c === "string" ? [c, (r) => r[c]] : c
496
+ );
497
+ const cells = rows.map((r) => cols.map(([, get2]) => scalar(get2(r)).replace(/\s+/g, " ")));
498
+ const widths = cols.map(([name], i) => Math.max(name.length, ...cells.map((row) => row[i].length)));
499
+ const max = width();
500
+ const total = () => widths.reduce((a, b) => a + b, 0) + 2 * (widths.length - 1);
501
+ while (total() > max) {
502
+ const i = widths.indexOf(Math.max(...widths));
503
+ if (widths[i] <= 12) break;
504
+ widths[i]--;
505
+ }
506
+ const fit = (s, n) => s.length > n ? s.slice(0, Math.max(1, n - 1)) + "\u2026" : s.padEnd(n);
507
+ const line = (vals) => vals.map((v, i) => fit(v, widths[i])).join(" ").trimEnd();
508
+ return [line(cols.map(([n]) => n.toUpperCase())), ...cells.map(line)].join("\n");
509
+ }
510
+ function kv(obj, keys) {
511
+ const entries = (keys ?? Object.keys(obj)).filter((k) => k in obj && isScalar(obj[k]));
512
+ const pad = Math.max(0, ...entries.map((k) => k.length));
513
+ return entries.map((k) => `${k.padEnd(pad)} ${scalar(obj[k])}`).join("\n");
514
+ }
515
+ function renderText(data) {
516
+ if (data === null || data === void 0) return "";
517
+ if (typeof data !== "object") return String(data);
518
+ if (Array.isArray(data)) {
519
+ if (data.every((x) => x && typeof x === "object" && !Array.isArray(x))) return table(data);
520
+ return data.map(scalar).join("\n");
521
+ }
522
+ const obj = data;
523
+ const parts = [];
524
+ const head = kv(obj);
525
+ if (head) parts.push(head);
526
+ for (const [key, value] of Object.entries(obj)) {
527
+ if (isScalar(value)) continue;
528
+ if (Array.isArray(value)) {
529
+ parts.push(`
530
+ ${key}:`);
531
+ parts.push(value.length && value.every((x) => x && typeof x === "object") ? table(value) : value.map(scalar).join("\n") || "(none)");
532
+ } else {
533
+ const inner = kv(value);
534
+ if (inner) parts.push(`
535
+ ${key}:
536
+ ${inner.replace(/^/gm, " ")}`);
537
+ }
538
+ }
539
+ return parts.join("\n");
540
+ }
541
+
542
+ // src/commands/auth.ts
543
+ async function organizations(api2) {
544
+ try {
545
+ return await api2.get("organizations");
546
+ } catch (err) {
547
+ if (err instanceof CliError && err.code === "server.unsupported") return null;
548
+ throw err;
549
+ }
550
+ }
551
+ var login = {
552
+ path: ["auth", "login"],
553
+ summary: "Check a token against the server and save it for this server (file mode 0600).",
554
+ description: "The token comes from --with-token (stdin), --token or TRACKSIY_TOKEN. Agents usually skip this and set TRACKSIY_TOKEN instead; login is for a machine that should remember it.",
555
+ context: "none",
556
+ auth: false,
557
+ effect: "write",
558
+ idempotent: true,
559
+ mcp: false,
560
+ flags: {
561
+ "with-token": { type: "boolean", description: "Read the token from stdin (keeps it out of shell history and process lists)." }
562
+ },
563
+ examples: [
564
+ { command: "tracksiy auth login --with-token < token.txt", description: "Save a token for https://tracksiy.com." },
565
+ { command: "echo $TOKEN | tracksiy auth login --with-token --url https://errors.example.com --org acme", description: "Another server, with a default organization." }
566
+ ],
567
+ output: "{url, token{id, label, scopes, projects, expires}, organizations[], defaultOrg, savedTo}",
568
+ async run(ctx) {
569
+ let token = ctx.input.withToken ? readFileSync2(0, "utf8").trim() : void 0;
570
+ if (!token && (ctx.config.tokenSource === "--token" || ctx.config.tokenSource === "TRACKSIY_TOKEN")) token = ctx.config.token;
571
+ if (!token) throw usageError("missing_token", "No token given.", "Pipe it in with --with-token, or pass --token / set TRACKSIY_TOKEN.");
572
+ const api2 = new Api({ url: ctx.config.url, token, timeoutMs: ctx.config.timeoutMs, verbose: ctx.config.verbose, log: ctx.log });
573
+ const self2 = await api2.get("tokens/self");
574
+ const orgs2 = await organizations(api2);
575
+ let org = ctx.config.orgSource === "--org" || ctx.config.orgSource === "TRACKSIY_ORG" ? ctx.config.org : void 0;
576
+ if (!org && orgs2?.length === 1) org = orgs2[0].slug;
577
+ if (org && orgs2 && !orgs2.some((o) => o.slug === org)) {
578
+ throw usageError("unknown_org", `This token cannot see organization ${org}.`, `Visible: ${orgs2.map((o) => o.slug).join(", ") || "none"}`);
579
+ }
580
+ const cfg = readUserConfig(ctx.config.configPath);
581
+ cfg.hosts[ctx.config.url] = { token, org, tokenLabel: self2.label, savedAt: (/* @__PURE__ */ new Date()).toISOString() };
582
+ cfg.defaultUrl = ctx.config.url;
583
+ await writeUserConfig(ctx.config.configPath, cfg);
584
+ return { url: ctx.config.url, token: self2, organizations: orgs2, defaultOrg: org ?? null, savedTo: ctx.config.configPath };
585
+ }
586
+ };
587
+ var status = {
588
+ path: ["auth", "status"],
589
+ aliases: [["whoami"]],
590
+ summary: "Which server, which token (and where each came from), which organizations and defaults are in effect.",
591
+ context: "none",
592
+ effect: "read",
593
+ output: "{url, urlSource, tokenSource, token{id, label, scopes, projects, expires, expiresInSeconds}, organizations[], defaults{org, orgSource, project, projectSource, projectFile}}",
594
+ text: (d) => `${kv({ url: d.url, "url from": d.urlSource, "token from": d.tokenSource, token: `${d.token?.label} (#${d.token?.id})`, scopes: d.token?.scopes, projects: d.token?.projects?.length ? d.token.projects : "all", expires: d.token?.expires ?? "never" })}
595
+
596
+ ${table(d.organizations ?? [], ["slug", "name", ["projects", (r) => (r.projects ?? []).length]])}
597
+
598
+ ${kv({ org: `${d.defaults.org ?? "-"} (${d.defaults.orgSource ?? "auto"})`, project: `${d.defaults.project ?? "-"} (${d.defaults.projectSource ?? "auto"})` })}`,
599
+ async run(ctx) {
600
+ const token = await ctx.api.get("tokens/self");
601
+ const orgs2 = await organizations(ctx.api);
602
+ return {
603
+ url: ctx.config.url,
604
+ urlSource: ctx.config.urlSource,
605
+ tokenSource: ctx.config.tokenSource,
606
+ token,
607
+ organizations: orgs2,
608
+ defaults: {
609
+ org: ctx.config.org ?? (orgs2?.length === 1 ? orgs2[0].slug : null),
610
+ orgSource: ctx.config.orgSource ?? null,
611
+ project: ctx.config.project ?? null,
612
+ projectSource: ctx.config.projectSource ?? null,
613
+ projectFile: ctx.config.projectFile ?? null
614
+ }
615
+ };
616
+ }
617
+ };
618
+ var logout = {
619
+ path: ["auth", "logout"],
620
+ summary: "Forget the saved token for this server. Does not revoke it (use `tokens delete`).",
621
+ context: "none",
622
+ auth: false,
623
+ effect: "write",
624
+ idempotent: true,
625
+ mcp: false,
626
+ async run(ctx) {
627
+ const cfg = readUserConfig(ctx.config.configPath);
628
+ const had = Boolean(cfg.hosts[ctx.config.url]?.token);
629
+ if (cfg.hosts[ctx.config.url]) delete cfg.hosts[ctx.config.url].token;
630
+ await writeUserConfig(ctx.config.configPath, cfg);
631
+ if (ctx.config.tokenSource === "TRACKSIY_TOKEN") ctx.log("note: TRACKSIY_TOKEN is still set in this environment.");
632
+ return { url: ctx.config.url, removed: had, savedTo: ctx.config.configPath };
633
+ }
634
+ };
635
+ function missingToken(url) {
636
+ return new CliError("auth.missing_token", `No token for ${url}.`, {
637
+ exit: EXIT.auth,
638
+ hint: "Set TRACKSIY_TOKEN, pass --token, or run `tracksiy auth login --with-token < token.txt`. Tokens are created in the dashboard (Profile > Auth tokens) or with `tracksiy tokens create`."
639
+ });
640
+ }
641
+ var auth_default = [login, status, logout];
642
+
643
+ // src/commands/_util.ts
644
+ async function projectPath(ctx, suffix = "") {
645
+ const { org, project: project2 } = await ctx.project();
646
+ return `projects/${seg(org)}/${seg(project2)}${suffix ? `/${suffix}` : ""}`;
647
+ }
648
+ async function orgPath(ctx, suffix = "") {
649
+ const org = await ctx.org();
650
+ return `organizations/${seg(org)}${suffix ? `/${suffix}` : ""}`;
651
+ }
652
+ var UNITS = { s: 1, m: 60, h: 3600, d: 86400, w: 604800 };
653
+ function since(value, flag = "since") {
654
+ if (value === void 0 || value === null || value === "") return void 0;
655
+ const s = String(value).trim();
656
+ const m = /^(\d+)\s*([smhdw])$/.exec(s);
657
+ if (m) return new Date(Date.now() - Number(m[1]) * UNITS[m[2]] * 1e3).toISOString();
658
+ if (s === "now" || /^now\s*-\s*\d+\s*[mhd]$/.test(s)) return s;
659
+ if (Number.isFinite(Date.parse(s))) return s;
660
+ throw usageError("bad_value", `--${flag} expects a duration like 24h / 7d or an ISO time, got ${JSON.stringify(s)}`);
661
+ }
662
+ var TIME = "<24h|7d|iso>";
663
+ function requireYes(ctx, what) {
664
+ if (ctx.input.yes) return;
665
+ const how = ctx.surface === "mcp" ? 'Call again with "yes": true to confirm.' : "Re-run with --yes to confirm.";
666
+ throw new CliError("confirmation.required", `${what} ${how}`, {
667
+ exit: EXIT.usage,
668
+ hint: ctx.surface === "mcp" ? 'Pass "yes": true to confirm.' : "Add --yes. Nothing was changed."
669
+ });
670
+ }
671
+ var YES = {
672
+ yes: { type: "boolean", description: "Confirm a destructive action. Without it nothing is changed." }
673
+ };
674
+ function rate(value, flag) {
675
+ const m = /^(\d+)\s*\/\s*(\d+)\s*s?$/.exec(String(value));
676
+ if (!m) throw usageError("bad_value", `--${flag} expects <count>/<seconds>, e.g. 100/60; got ${JSON.stringify(value)}`);
677
+ return { count: Number(m[1]), windowSeconds: Number(m[2]) };
678
+ }
679
+ function compact(obj) {
680
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
681
+ }
682
+
683
+ // src/commands/fleet.ts
684
+ var orgs = {
685
+ path: ["orgs", "list"],
686
+ aliases: [["orgs"]],
687
+ summary: "Organizations this token can see, each with the projects it may reach.",
688
+ context: "none",
689
+ effect: "read",
690
+ endpoint: { method: "GET", path: "/api/agent/v1/organizations" },
691
+ output: "[{slug, name, projects:[{slug, name, platform}]}]",
692
+ text: (d) => table(d, ["slug", "name", ["projects", (r) => (r.projects ?? []).map((p) => p.slug).join(", ")]]),
693
+ async run(ctx) {
694
+ return ctx.api.get("organizations");
695
+ }
696
+ };
697
+ var projects = {
698
+ path: ["projects", "list"],
699
+ aliases: [["projects"]],
700
+ summary: "Projects of the organization that this token may reach.",
701
+ context: "org",
702
+ effect: "read",
703
+ endpoint: { method: "GET", path: "/api/agent/v1/organizations" },
704
+ output: "[{slug, name, platform}]",
705
+ async run(ctx) {
706
+ const org = await ctx.org();
707
+ const all = await ctx.api.get("organizations");
708
+ const found = all.find((o) => o.slug === org);
709
+ if (!found) throw usageError("unknown_org", `This token cannot see organization ${org}.`, `Visible: ${all.map((o) => o.slug).join(", ") || "none"}`);
710
+ return found.projects ?? [];
711
+ }
712
+ };
713
+ var overview = {
714
+ path: ["overview"],
715
+ summary: "The whole fleet in one call: per-project issues, volume trend, silence, monitors and alert coverage.",
716
+ context: "org",
717
+ effect: "read",
718
+ endpoint: { method: "GET", path: "/api/agent/v1/organizations/{org}/overview" },
719
+ flags: { hours: { type: "integer", value: "<n>", description: "Window in hours (default 24), compared with the window before it." } },
720
+ examples: [{ command: "tracksiy overview --select totals,projects.slug,projects.newIssues", description: "Fleet totals and new issues per project." }],
721
+ output: "{organization, window, totals{projects, unresolved, newIssues, events, monitorsDown, projectsWithoutAlerts, projectsSilent, ...}, projects[...], orphanMonitors[...], notes[]}",
722
+ text: (d) => `${kv(d.totals ?? {})}
723
+
724
+ ${table(d.projects ?? [], [
725
+ "slug",
726
+ ["open", (r) => r.issues && typeof r.issues === "object" ? r.issues.unresolved : r.issues],
727
+ ["new", (r) => r.newIssues],
728
+ ["events", (r) => r.events],
729
+ ["trend", (r) => r.changeRatio == null ? "-" : `${r.changeRatio > 0 ? "+" : ""}${Math.round(r.changeRatio * 100)}%`],
730
+ ["last event", (r) => ago(r.lastEvent)],
731
+ ["receiving", (r) => r.receiving]
732
+ ])}${(d.notes ?? []).length ? `
733
+
734
+ ${d.notes.join("\n")}` : ""}`,
735
+ async run(ctx) {
736
+ return ctx.api.get(await orgPath(ctx, "overview"), { hours: ctx.input.hours });
737
+ }
738
+ };
739
+ var changes = {
740
+ path: ["changes"],
741
+ summary: "What changed since a point in time: new issues, monitors going down or up, new releases.",
742
+ description: "Built for polling: pass the returned `cursor` as --since next time and nothing is seen twice.",
743
+ context: "org",
744
+ effect: "read",
745
+ endpoint: { method: "GET", path: "/api/agent/v1/organizations/{org}/changes" },
746
+ flags: {
747
+ since: { type: "string", value: `${TIME}|<cursor>`, default: "24h", description: "Start of the window, or the cursor from the previous call. Default 24h." },
748
+ limit: { type: "integer", value: "<n>", description: "Maximum items per list." }
749
+ },
750
+ examples: [{ command: "tracksiy changes --since 1h", description: "Anything new in the last hour." }],
751
+ output: "{since, until, cursor, newIssues[], monitorTransitions[], newReleases[], truncated{}, notes[]}",
752
+ text: (d) => [
753
+ `since ${d.since} (next cursor: ${d.cursor})`,
754
+ `
755
+ new issues:
756
+ ${table(d.newIssues ?? [], ["project", "id", "level", "count", "title"])}`,
757
+ `
758
+ monitor transitions:
759
+ ${table(d.monitorTransitions ?? [], ["project", "name", ["state", (r) => r.wentDown ? "DOWN" : "up"], ["at", (r) => ago(r.at)], "reason"])}`,
760
+ `
761
+ new releases:
762
+ ${table(d.newReleases ?? [], ["project", "version", "surface", ["at", (r) => ago(r.at)]])}`
763
+ ].join("\n"),
764
+ async run(ctx) {
765
+ const raw = String(ctx.input.since);
766
+ const value = /^\d+\s*[smhdw]$/.test(raw) ? since(raw) : raw;
767
+ return ctx.api.get(await orgPath(ctx, "changes"), { since: value, limit: ctx.input.limit });
768
+ }
769
+ };
770
+ var health = {
771
+ path: ["health"],
772
+ summary: "Can the platform itself be believed right now? Exit code 10 when any check fails.",
773
+ context: "none",
774
+ effect: "read",
775
+ endpoint: { method: "GET", path: "/api/agent/v1/health" },
776
+ examples: [{ command: "tracksiy health --quiet || echo 'do not trust the numbers'", description: "Gate a report on platform health." }],
777
+ output: "{generatedAt, ok, checks:[{name, ok, status, detail}], numbers}",
778
+ text: (d) => `ok: ${d.ok}
779
+ ${table(d.checks ?? [], ["name", "status", "detail"])}`,
780
+ async run(ctx) {
781
+ const data = await ctx.api.get("health");
782
+ if (data && data.ok === false) {
783
+ const failing = (data.checks ?? []).filter((c) => c.ok === false).map((c) => c.name);
784
+ throw new CheckFailed(`Platform health is not ok: ${failing.join(", ") || "see checks"}.`, data);
785
+ }
786
+ return data;
787
+ }
788
+ };
789
+ var monitorsList = {
790
+ path: ["monitors", "list"],
791
+ aliases: [["monitors"]],
792
+ summary: "Every monitor of the organization with its state, including monitors no project owns.",
793
+ context: "org",
794
+ effect: "read",
795
+ endpoint: { method: "GET", path: "/api/agent/v1/organizations/{org}/monitors" },
796
+ flags: {
797
+ status: { type: "string", enum: ["all", "up", "down", "never"], description: "Filter by state. Default all." },
798
+ type: { type: "string", enum: ["heartbeat", "get", "ping", "ssl", "ingest"], description: "Only this monitor type." }
799
+ },
800
+ output: "{summary{total, up, down, never, unalertable}, monitors:[{id, name, project, monitorType, url, upNow, lastCheck, overdue, alertable, checkInUrl}], notes[]}",
801
+ text: (d) => `${kv(d.summary ?? {})}
802
+
803
+ ${table(d.monitors ?? [], [
804
+ "name",
805
+ "project",
806
+ "monitorType",
807
+ ["up", (r) => r.upNow == null ? "never" : r.upNow],
808
+ ["last check", (r) => ago(r.lastCheck)],
809
+ ["alertable", (r) => r.alertable]
810
+ ])}`,
811
+ async run(ctx) {
812
+ return ctx.api.get(await orgPath(ctx, "monitors"), { status: ctx.input.status, type: ctx.input.type });
813
+ }
814
+ };
815
+ var monitorsDeclare = {
816
+ path: ["monitors", "declare"],
817
+ summary: "Declare monitors for a project up front, idempotently: heartbeats for jobs, HTTP checks, TLS, ingest silence.",
818
+ description: 'Each --heartbeat/--get/--ping/--ssl/--ingest adds one monitor. Re-running with the same slugs updates them in place. For full control pass --body with {"monitors": [...]}.',
819
+ context: "project",
820
+ effect: "write",
821
+ idempotent: true,
822
+ endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/monitors" },
823
+ flags: {
824
+ heartbeat: { type: "list", value: "<slug>[:<interval-seconds>]", description: "A job that must check in at least this often (default 86400 s)." },
825
+ get: { type: "list", value: "<slug>=<url>", description: "HTTP GET check expecting 200." },
826
+ ping: { type: "list", value: "<slug>=<url>", description: "Reachability check." },
827
+ ssl: { type: "list", value: "<slug>=<url>", description: "TLS certificate check (expiry is reported on every check)." },
828
+ ingest: { type: "list", value: "<slug>[:<surface>]", description: "Alert when this project (or surface) stops sending events." },
829
+ interval: { type: "integer", value: "<seconds>", description: "Interval for --get/--ping/--ssl/--ingest monitors." },
830
+ body: { type: "json", value: "<json|@file|->", description: "Raw request body; overrides the shorthand flags." }
831
+ },
832
+ examples: [
833
+ { command: "tracksiy monitors declare --heartbeat nightly-backup:93600 --get site=https://example.com", description: "A backup heartbeat and a homepage check." }
834
+ ],
835
+ output: "Server result per monitor, including check-in URLs for heartbeats.",
836
+ async run(ctx) {
837
+ const i = ctx.input;
838
+ let body = i.body;
839
+ if (!body) {
840
+ const monitors = [];
841
+ for (const h of i.heartbeat ?? []) {
842
+ const [slug, interval] = String(h).split(":");
843
+ monitors.push(compact({ slug, type: "heartbeat", intervalSeconds: interval ? Number(interval) : void 0 }));
844
+ }
845
+ for (const type of ["get", "ping", "ssl"]) {
846
+ for (const item of i[type] ?? []) {
847
+ const eq = String(item).indexOf("=");
848
+ if (eq < 1) throw usageError("bad_value", `--${type} expects <slug>=<url>, got ${JSON.stringify(item)}`);
849
+ monitors.push(compact({ slug: item.slice(0, eq), type, url: item.slice(eq + 1), intervalSeconds: i.interval }));
850
+ }
851
+ }
852
+ for (const item of i.ingest ?? []) {
853
+ const [slug, surface] = String(item).split(":");
854
+ monitors.push(compact({ slug, type: "ingest", surface, intervalSeconds: i.interval }));
855
+ }
856
+ if (!monitors.length) throw usageError("missing_flag", "Nothing to declare.", "Add --heartbeat, --get, --ping, --ssl, --ingest, or --body.");
857
+ body = { monitors };
858
+ }
859
+ return ctx.api.send("POST", await projectPath(ctx, "monitors"), body);
860
+ }
861
+ };
862
+ var monitorsDelete = {
863
+ path: ["monitors", "delete"],
864
+ summary: "Remove one of a project's monitors by the slug it was declared with.",
865
+ context: "project",
866
+ effect: "destructive",
867
+ idempotent: true,
868
+ endpoint: { method: "DELETE", path: "/api/agent/v1/projects/{org}/{project}/monitors/{monitor_slug}" },
869
+ args: [{ name: "slug", required: true, description: "The monitor's slug." }],
870
+ flags: { type: { type: "string", enum: ["heartbeat", "get", "ping", "ssl", "ingest"], description: "Disambiguate when two monitors share a slug." }, ...YES },
871
+ async run(ctx) {
872
+ requireYes(ctx, `This deletes monitor ${ctx.input.slug} and its history.`);
873
+ return ctx.api.send("DELETE", await projectPath(ctx, `monitors/${seg(ctx.input.slug)}${ctx.input.type ? `?type=${seg(ctx.input.type)}` : ""}`));
874
+ }
875
+ };
876
+ var performance = {
877
+ path: ["performance"],
878
+ summary: "Is this project slow, and did it get slower? Transactions compared with the previous window, plus uptime.",
879
+ context: "project",
880
+ effect: "read",
881
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/performance" },
882
+ flags: {
883
+ since: { type: "string", value: TIME, description: "Window start (default: server's, 24h)." },
884
+ op: { type: "string", value: "<op>", description: "Only this operation, e.g. http.server." },
885
+ sort: { type: "string", value: "<key>", description: "Order (server default: slower first)." },
886
+ limit: { type: "integer", value: "<n>", description: "How many transactions." }
887
+ },
888
+ output: "{since, until, transactions:[{transaction, op, current{...}, previous{...}, changeRatio, slower}], uptime[...], blindSpots[...]}",
889
+ text: (d) => `${table(d.transactions ?? [], [
890
+ "transaction",
891
+ "op",
892
+ ["p95 ms", (r) => r.current?.p95Ms ?? r.current?.p95 ?? "-"],
893
+ ["count", (r) => r.current?.count ?? "-"],
894
+ ["change", (r) => r.changeRatio == null ? "-" : `${r.changeRatio > 0 ? "+" : ""}${Math.round(r.changeRatio * 100)}%`],
895
+ ["slower", (r) => r.slower]
896
+ ])}${(d.uptime ?? []).length ? `
897
+
898
+ uptime:
899
+ ${table(d.uptime, ["name", "url", "upNow", "avgResponseMs", "downChecks"])}` : ""}`,
900
+ async run(ctx) {
901
+ return ctx.api.get(await projectPath(ctx, "performance"), {
902
+ since: since(ctx.input.since),
903
+ op: ctx.input.op,
904
+ sort: ctx.input.sort,
905
+ limit: ctx.input.limit
906
+ });
907
+ }
908
+ };
909
+ var fleet_default = [orgs, projects, overview, changes, health, monitorsList, monitorsDeclare, monitorsDelete, performance];
910
+
911
+ // src/commands/issues.ts
912
+ var FILTERS = {
913
+ status: { type: "string", enum: ["unresolved", "resolved", "ignored", "regressed", "all"], description: "Issue status. Server default: unresolved." },
914
+ level: { type: "string", enum: ["sample", "debug", "info", "warning", "error", "fatal"], description: "Only this level." },
915
+ environment: { type: "string", value: "<name>", description: "Only events from this environment, e.g. production." },
916
+ tag: { type: "string", value: "<key:value>", description: "Tag filter, e.g. surface:web or release:app@1.2.0." },
917
+ query: { type: "string", value: "<text>", description: "Free text against the title (* is a wildcard) and the search index." },
918
+ since: { type: "string", value: TIME, description: "Last seen at or after this (lastSeenSince)." },
919
+ until: { type: "string", value: TIME, description: "Last seen before this (lastSeenBefore)." },
920
+ "first-seen-since": { type: "string", value: TIME, description: "First seen at or after this: new issues only." },
921
+ "first-seen-until": { type: "string", value: TIME, description: "First seen before this." }
922
+ };
923
+ function filterQuery(input) {
924
+ return compact({
925
+ status: input.status,
926
+ level: input.level,
927
+ environment: input.environment,
928
+ tag: input.tag,
929
+ query: input.query,
930
+ lastSeenSince: since(input.since),
931
+ lastSeenBefore: since(input.until, "until"),
932
+ firstSeenSince: since(input.firstSeenSince, "first-seen-since"),
933
+ firstSeenBefore: since(input.firstSeenUntil, "first-seen-until")
934
+ });
935
+ }
936
+ var issueRows = (issues) => table(issues ?? [], [
937
+ "id",
938
+ ["level", (r) => r.level],
939
+ ["count", (r) => r.count],
940
+ ["last seen", (r) => ago(r.lastSeen)],
941
+ ["status", (r) => r.regressedAt && r.status === "unresolved" ? "regressed" : r.status],
942
+ ["title", (r) => r.title]
943
+ ]);
944
+ var list = {
945
+ path: ["issues", "list"],
946
+ aliases: [["issues"]],
947
+ summary: "Recent issues of a project, newest activity first, with the frames that matter inlined.",
948
+ context: "project",
949
+ effect: "read",
950
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/issues" },
951
+ flags: {
952
+ ...FILTERS,
953
+ sort: { type: "string", enum: ["lastSeen", "firstSeen", "count"], description: "Order. Default lastSeen." },
954
+ limit: { type: "integer", value: "<n>", description: "How many issues (server default 25)." },
955
+ frames: { type: "integer", value: "<n>", description: "In-app stack frames inlined per issue (default 3, 0 for none)." }
956
+ },
957
+ examples: [
958
+ { command: "tracksiy issues list --since 24h", description: "Unresolved issues seen in the last day." },
959
+ { command: "tracksiy issues list --status regressed --select issues.id,issues.title", description: "Only regressions, only ids and titles." },
960
+ { command: "tracksiy issues list --first-seen-since 2h --environment production", description: "What is new in production." }
961
+ ],
962
+ output: "{project, issueCount, total, hasMore, issues:[{id, shortId, title, culprit, level, status, count, firstSeen, lastSeen, regressedAt, timesRegressed, permalink, frames?}]}",
963
+ text: (d) => `${d.issueCount ?? d.issues?.length ?? 0} of ${d.total ?? "?"} issues${d.hasMore ? " (more: raise --limit)" : ""}
964
+ ${issueRows(d.issues)}`,
965
+ async run(ctx) {
966
+ return ctx.api.get(await projectPath(ctx, "issues"), {
967
+ ...filterQuery(ctx.input),
968
+ sort: ctx.input.sort,
969
+ limit: ctx.input.limit,
970
+ frames: ctx.input.frames
971
+ });
972
+ }
973
+ };
974
+ var view = {
975
+ path: ["issues", "view"],
976
+ summary: "One issue in enough depth to act on: latest event, stack frames, tags, recent volume.",
977
+ context: "project",
978
+ effect: "read",
979
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/issues/{issue_id}" },
980
+ args: [{ name: "id", required: true, description: "Issue id (numeric, from `issues list`)." }],
981
+ flags: { frames: { type: "integer", value: "<n>", description: "How many frames to include." } },
982
+ examples: [{ command: "tracksiy issues view 4812", description: "Everything needed to start fixing issue 4812." }],
983
+ output: "{id, title, culprit, level, status, count, firstSeen, lastSeen, latestEvent{release, environment, exceptionType, exceptionValue, contexts}, frames[{filename, function, lineno, contextLine, inApp}], tags[], eventsLast24h, eventsLast7d, permalink}",
984
+ text: (d) => {
985
+ const head = kv(d, ["id", "shortId", "title", "culprit", "level", "status", "count", "eventsLast24h", "eventsLast7d", "firstSeen", "lastSeen", "regressedAt", "permalink"]);
986
+ const ev = d.latestEvent ? `
987
+ latest event:
988
+ ${kv(d.latestEvent, ["release", "environment", "exceptionType", "exceptionValue", "transaction", "platform"]).replace(/^/gm, " ")}` : "";
989
+ const frames = (d.frames ?? []).map((f) => ` ${f.inApp ? "*" : " "} ${f.filename ?? f.module ?? "?"}:${f.lineno ?? "?"} in ${f.function ?? "?"}${f.contextLine ? `
990
+ ${String(f.contextLine).trim()}` : ""}`).join("\n");
991
+ return `${head}${ev}${frames ? `
992
+ frames (* = in app):
993
+ ${frames}` : ""}`;
994
+ },
995
+ async run(ctx) {
996
+ return ctx.api.get(await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}`), { frames: ctx.input.frames });
997
+ }
998
+ };
999
+ function statusCommand(name, status2, summary) {
1000
+ return {
1001
+ path: ["issues", name],
1002
+ summary,
1003
+ context: "project",
1004
+ effect: "write",
1005
+ idempotent: true,
1006
+ endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/issues/{issue_id}/status" },
1007
+ args: [{ name: "ids", required: true, variadic: true, description: "One or more issue ids." }],
1008
+ examples: [{ command: `tracksiy issues ${name} 4812 4813`, description: `Mark two issues ${status2}.` }],
1009
+ output: "One id: the issue's new state. Several: {matched, changed, ids, dryRun:false}.",
1010
+ async run(ctx) {
1011
+ const ids = ctx.input.ids;
1012
+ for (const id of ids) if (!/^\d+$/.test(id)) throw usageError("bad_value", `Issue ids are numeric; got ${JSON.stringify(id)}`);
1013
+ if (ids.length === 1) return ctx.api.send("POST", await projectPath(ctx, `issues/${ids[0]}/status`), { status: status2 });
1014
+ return ctx.api.send("POST", await projectPath(ctx, "issues/status"), { status: status2, ids: ids.map(Number), dryRun: false });
1015
+ }
1016
+ };
1017
+ }
1018
+ var resolve2 = statusCommand("resolve", "resolved", "Mark issues resolved. A new event on a resolved issue reopens it as a regression.");
1019
+ var ignore = statusCommand("ignore", "ignored", "Mark issues ignored: they stop alerting until unignored.");
1020
+ var unresolve = statusCommand("unresolve", "unresolved", "Reopen issues.");
1021
+ var bulk = {
1022
+ path: ["issues", "bulk-status"],
1023
+ summary: "Change the status of every issue matching a filter. Dry run unless --apply.",
1024
+ description: "Without --apply the server only reports what would change. Select issues with --ids, with the same filters as `issues list`, or with --all.",
1025
+ context: "project",
1026
+ effect: "write",
1027
+ endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/issues/status" },
1028
+ flags: {
1029
+ to: { type: "string", required: true, enum: ["resolved", "ignored", "unresolved"], description: "New status." },
1030
+ ids: { type: "list", value: "<id,...>", description: "Exactly these issues." },
1031
+ ...FILTERS,
1032
+ all: { type: "boolean", description: "Every issue of the project (required when no other selector is given)." },
1033
+ apply: { type: "boolean", description: "Actually change them. Default is a dry run." }
1034
+ },
1035
+ examples: [
1036
+ { command: "tracksiy issues bulk-status --to resolved --until 30d", description: "Preview resolving everything not seen for 30 days." },
1037
+ { command: "tracksiy issues bulk-status --to resolved --until 30d --apply", description: "Do it." }
1038
+ ],
1039
+ output: "{dryRun, matched, changed, ids, ...} as reported by the server.",
1040
+ async run(ctx) {
1041
+ const filter = filterQuery(ctx.input);
1042
+ const hasFilter = Object.keys(filter).length > 0;
1043
+ const body = compact({
1044
+ status: ctx.input.to,
1045
+ ids: ctx.input.ids?.map(Number),
1046
+ filter: hasFilter ? filter : void 0,
1047
+ all: ctx.input.all || void 0,
1048
+ dryRun: !ctx.input.apply
1049
+ });
1050
+ return ctx.api.send("POST", await projectPath(ctx, "issues/status"), body);
1051
+ }
1052
+ };
1053
+ var issues_default = [list, view, resolve2, ignore, unresolve, bulk];
1054
+
1055
+ // src/commands/projects.ts
1056
+ var create = {
1057
+ path: ["projects", "create"],
1058
+ summary: "Everything a new app needs in one idempotent call: the project, an ingest key per surface, an upload token, and the next steps.",
1059
+ description: "Safe to re-run: an existing project is returned as it is (created: false). The response holds secrets (ingest keys, the upload token) -- store them, do not print them into logs.",
1060
+ context: "org",
1061
+ effect: "write",
1062
+ idempotent: true,
1063
+ endpoint: { method: "POST", path: "/api/agent/v1/organizations/{org}/projects" },
1064
+ args: [{ name: "name", required: true, description: "Project name; its slug is derived from it." }],
1065
+ flags: {
1066
+ platform: { type: "string", value: "<platform>", description: "e.g. php-laravel, javascript-react, node, python-django, android, apple-ios, react-native." },
1067
+ team: { type: "string", value: "<slug>", description: "Team that owns it (default: the organization's first team)." },
1068
+ surfaces: { type: "list", value: "<name,...>", description: "One ingest key per surface (server default backend,web,mobile)." },
1069
+ "same-host": { type: "boolean", description: "The app runs on the Tracksiy host: use the loopback ingest address." }
1070
+ },
1071
+ examples: [{ command: "tracksiy projects create shop --platform php-laravel --surfaces backend,web", description: "A Laravel app with a browser frontend." }],
1072
+ output: "{created, project{organization, slug, name, platform}, dsns[{surface, dsn, publicKey, shipsInPublicArtifact, loopbackDsn}], sourcemapToken, onboardingUrl, mustRead[], nextSteps[]}",
1073
+ text: (d) => `${d.created ? "created" : "already existed"}: ${d.project?.organization}/${d.project?.slug}
1074
+
1075
+ ingest keys:
1076
+ ${table(d.dsns ?? [], ["surface", "dsn", "shipsInPublicArtifact"])}${d.sourcemapToken ? `
1077
+
1078
+ upload token: ${d.sourcemapToken} (scopes ${(d.sourcemapTokenScopes ?? []).join(",")})` : ""}
1079
+
1080
+ next:
1081
+ ${(d.nextSteps ?? []).map((s, n) => `${n + 1}. ${s.action} -- ${s.why}`).join("\n")}`,
1082
+ async run(ctx) {
1083
+ return ctx.api.send(
1084
+ "POST",
1085
+ await orgPath(ctx, "projects"),
1086
+ compact({ name: ctx.input.name, platform: ctx.input.platform, team: ctx.input.team, surfaces: ctx.input.surfaces, sameHost: ctx.input.sameHost })
1087
+ );
1088
+ }
1089
+ };
1090
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
1091
+ var verify = {
1092
+ path: ["projects", "verify"],
1093
+ aliases: [["verify"]],
1094
+ summary: "Did the integration actually work? Checks events, releases, environments, source maps, privacy and alerting. Exit 10 if not.",
1095
+ description: "By default only `events_received` must pass. --strict requires every check to pass (an `unknown` check is not a pass). --wait polls until it passes or the time runs out -- use it right after deploying the SDK.",
1096
+ context: "project",
1097
+ effect: "read",
1098
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/integration" },
1099
+ flags: {
1100
+ wait: { type: "integer", value: "<seconds>", description: "Keep checking every 5 s for up to this long." },
1101
+ strict: { type: "boolean", description: "Every check must pass, not only events_received." },
1102
+ require: { type: "list", value: "<check,...>", description: "These checks must pass (names from the checks list)." }
1103
+ },
1104
+ examples: [
1105
+ { command: "tracksiy projects verify --wait 120", description: "Wait up to two minutes for the first event." },
1106
+ { command: "tracksiy projects verify --strict --select checks", description: "The full integration report card." }
1107
+ ],
1108
+ output: "{receiving, firstEvent, lastEvent, releases, environments, sourceMaps{}, surfaces[], privacy{}, checks[{name, ok, status, detail}], verified, failing[]}",
1109
+ text: (d) => `${kv({ verified: d.verified, receiving: d.receiving, firstEvent: d.firstEvent, lastEvent: d.lastEvent, failing: d.failing })}
1110
+
1111
+ ${table(d.checks ?? [], ["name", "status", "detail"])}`,
1112
+ async run(ctx) {
1113
+ const path = await projectPath(ctx, "integration");
1114
+ const required = ctx.input.require ?? ["events_received"];
1115
+ const deadline = Date.now() + (ctx.input.wait ?? 0) * 1e3;
1116
+ for (; ; ) {
1117
+ const data = await ctx.api.get(path);
1118
+ const checks = data?.checks ?? [];
1119
+ const passes = (c) => c.status === "pass" || c.status == null && c.ok === true;
1120
+ const failing = ctx.input.strict ? checks.filter((c) => !passes(c)).map((c) => c.name) : required.filter((n) => {
1121
+ const c = checks.find((x) => x.name === n);
1122
+ return !c || !passes(c);
1123
+ });
1124
+ const unknownNames = ctx.input.require ? required.filter((n) => !checks.some((c) => c.name === n)) : [];
1125
+ if (unknownNames.length) throw usageError("bad_value", `Unknown check(s): ${unknownNames.join(", ")}`, `Known: ${checks.map((c) => c.name).join(", ")}`);
1126
+ const result = { ...data, verified: failing.length === 0, failing };
1127
+ if (result.verified) return result;
1128
+ if (Date.now() >= deadline) {
1129
+ throw new CheckFailed(`Integration not verified: ${failing.join(", ")}.`, result, checks.find((c) => failing.includes(c.name))?.detail);
1130
+ }
1131
+ ctx.log(`waiting: ${failing.join(", ")} not passing yet`);
1132
+ await sleep2(Math.min(5e3, Math.max(0, deadline - Date.now())));
1133
+ }
1134
+ }
1135
+ };
1136
+ var onboarding = {
1137
+ path: ["onboarding"],
1138
+ summary: "What to know before integrating an app. No token needed.",
1139
+ context: "none",
1140
+ auth: false,
1141
+ effect: "read",
1142
+ endpoint: { method: "GET", path: "/api/agent/v1/onboarding" },
1143
+ output: "{steps:[{id, phase, severity, title, detail, action}], markdown}",
1144
+ text: (d) => d.markdown ?? JSON.stringify(d, null, 2),
1145
+ async run(ctx) {
1146
+ return ctx.api.get("onboarding", void 0, { auth: false });
1147
+ }
1148
+ };
1149
+ var projects_default = [create, verify, onboarding];
1150
+
1151
+ // src/commands/alerts.ts
1152
+ var RECIPIENT_TYPES = ["email", "webhook", "discord", "googlechat", "telegram"];
1153
+ var DECLARE = {
1154
+ recipient: {
1155
+ type: "list",
1156
+ value: "email|<type>:<url>",
1157
+ description: `Who is told. Repeatable. \`email\` alone reaches the members of the project's teams; webhook, discord, googlechat and telegram take a URL: \`<type>:<url>\` (telegram: the Bot API sendMessage URL with chat_id).`
1158
+ },
1159
+ delivery: { type: "string", enum: ["immediate", "digest"], description: "For --recipient: send each alert now, or batch into the digest hours. Default immediate." },
1160
+ "new-issue": { type: "boolean", description: "Alert on every new issue (default true). --no-new-issue to turn off." },
1161
+ threshold: { type: "string", value: "<events>/<minutes>", description: "Also alert when an issue gets this many events in this many minutes, e.g. 50/10." },
1162
+ uptime: { type: "boolean", description: "Alert when this project's monitors go down (default true)." },
1163
+ "replace-recipients": { type: "boolean", description: "Make the recipient list exactly this one (default: add to it)." },
1164
+ body: { type: "json", value: "<json|@file|->", description: "Raw request body; overrides the other flags." }
1165
+ };
1166
+ function recipients(list5, delivery) {
1167
+ return (list5 ?? []).map((raw) => {
1168
+ const i = raw.indexOf(":");
1169
+ const type = i > 0 ? raw.slice(0, i) : raw;
1170
+ if (!RECIPIENT_TYPES.includes(type)) {
1171
+ throw usageError("bad_value", `--recipient type must be one of ${RECIPIENT_TYPES.join(", ")}; got ${JSON.stringify(type)}`, "Format: email, or <type>:<url>, e.g. webhook:https://hooks.example.com/x");
1172
+ }
1173
+ const target = i > 0 ? raw.slice(i + 1) : void 0;
1174
+ if (type === "email") {
1175
+ if (target) throw usageError("bad_value", `--recipient ${raw}: an email recipient takes no address.`, "Use --recipient email: it reaches the members of the teams this project belongs to.");
1176
+ return compact({ type, delivery });
1177
+ }
1178
+ if (!target) throw usageError("bad_value", `--recipient ${raw} has no target`, `Format: ${type}:<url>`);
1179
+ return compact({ type, url: target, delivery });
1180
+ });
1181
+ }
1182
+ function threshold(value) {
1183
+ if (value === void 0) return void 0;
1184
+ const m = /^(\d+)\s*\/\s*(\d+)$/.exec(String(value));
1185
+ if (!m) throw usageError("bad_value", `--threshold expects <events>/<minutes>, e.g. 50/10; got ${JSON.stringify(value)}`);
1186
+ return { events: Number(m[1]), minutes: Number(m[2]) };
1187
+ }
1188
+ function declareBody(i) {
1189
+ if (i.body) return i.body;
1190
+ return compact({
1191
+ recipients: i.recipient ? recipients(i.recipient, i.delivery) : void 0,
1192
+ newIssue: i.newIssue,
1193
+ threshold: threshold(i.threshold),
1194
+ uptime: i.uptime,
1195
+ replaceRecipients: i.replaceRecipients
1196
+ });
1197
+ }
1198
+ var rulesText = (d) => `${kv(d.coverage ?? {})}
1199
+
1200
+ rules:
1201
+ ${table(d.rules ?? [], [
1202
+ "name",
1203
+ ["agent", (r) => r.managedByAgent],
1204
+ ["new issue", (r) => r.newIssue],
1205
+ ["threshold", (r) => r.threshold ? `${r.threshold.events}/${r.threshold.minutes}m` : "-"],
1206
+ ["uptime", (r) => r.uptime],
1207
+ ["recipients", (r) => (r.recipients ?? []).map((x) => `${x.type}${x.delivery === "digest" ? "(digest)" : ""}`).join(", ")]
1208
+ ])}${(d.notes ?? []).length ? `
1209
+
1210
+ ${d.notes.join("\n")}` : ""}`;
1211
+ var get = {
1212
+ path: ["alerts", "get"],
1213
+ aliases: [["alerts"]],
1214
+ summary: "Who is told when this project breaks, and whether that can actually happen.",
1215
+ context: "project",
1216
+ effect: "read",
1217
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/alerts" },
1218
+ output: "{rules[], coverage{newIssue, threshold, uptime, recipientCount, monitors}, engine{intervalSeconds, lastRunAt}, recentNotifications[], notes[]}",
1219
+ text: rulesText,
1220
+ async run(ctx) {
1221
+ return ctx.api.get(await projectPath(ctx, "alerts"));
1222
+ }
1223
+ };
1224
+ var set = {
1225
+ path: ["alerts", "set"],
1226
+ summary: "Declare the project's alerting idempotently. Then prove it with `alerts test`.",
1227
+ context: "project",
1228
+ effect: "write",
1229
+ idempotent: true,
1230
+ endpoint: { method: "PUT", path: "/api/agent/v1/projects/{org}/{project}/alerts" },
1231
+ flags: DECLARE,
1232
+ examples: [
1233
+ { command: "tracksiy alerts set --recipient email --threshold 50/10", description: "Email the project's team on new issues, on bursts, and on downtime." },
1234
+ { command: "tracksiy alerts set --recipient 'telegram:https://api.telegram.org/bot<token>/sendMessage?chat_id=<id>' --delivery digest", description: "Telegram, batched into the digest hours." }
1235
+ ],
1236
+ output: "The stored alerting state, same shape as `alerts get`.",
1237
+ text: rulesText,
1238
+ async run(ctx) {
1239
+ return ctx.api.send("PUT", await projectPath(ctx, "alerts"), declareBody(ctx.input));
1240
+ }
1241
+ };
1242
+ var test = {
1243
+ path: ["alerts", "test"],
1244
+ summary: "Send a test message through every recipient and report each delivery. Exit code 10 if any failed.",
1245
+ context: "project",
1246
+ effect: "write",
1247
+ endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/alerts/test" },
1248
+ output: "Per-recipient delivery results with ok/status/error.",
1249
+ async run(ctx) {
1250
+ const data = await ctx.api.send("POST", await projectPath(ctx, "alerts/test"));
1251
+ const results = data?.results ?? [];
1252
+ if (!results.length) {
1253
+ throw new CheckFailed("This project has no recipients, so nobody would be told.", data, "Declare one with `tracksiy alerts set --recipient <type>:<target>`.");
1254
+ }
1255
+ const failed = results.filter((r) => r && r.ok === false);
1256
+ if (failed.length) {
1257
+ throw new CheckFailed(`${failed.length || "Some"} recipient(s) did not receive the test message.`, data, "Fix or replace the failing recipients with `tracksiy alerts set`.");
1258
+ }
1259
+ return data;
1260
+ }
1261
+ };
1262
+ var coverage = {
1263
+ path: ["alerts", "coverage"],
1264
+ summary: "For the whole organization: which projects can alert, which cannot, and through what.",
1265
+ context: "org",
1266
+ effect: "read",
1267
+ endpoint: { method: "GET", path: "/api/agent/v1/organizations/{org}/alerts/coverage" },
1268
+ output: "{projects[{project, rules, recipientCount, newIssue, threshold, uptime, monitors}], projectsWithoutRules[], monitorsWithoutProject[], recipientsByType{}, engine{}, notes[]}",
1269
+ text: (d) => `${table(d.projects ?? [], ["project", "rules", "recipientCount", "newIssue", "threshold", "uptime"])}
1270
+
1271
+ without rules: ${(d.projectsWithoutRules ?? []).join(", ") || "none"}`,
1272
+ async run(ctx) {
1273
+ return ctx.api.get(await orgPath(ctx, "alerts/coverage"));
1274
+ }
1275
+ };
1276
+ var defaults = {
1277
+ path: ["alerts", "defaults"],
1278
+ summary: "Apply one alerting declaration to many projects at once (by default only where none exists).",
1279
+ context: "org",
1280
+ effect: "write",
1281
+ idempotent: true,
1282
+ endpoint: { method: "PUT", path: "/api/agent/v1/organizations/{org}/alerts/defaults" },
1283
+ flags: {
1284
+ ...DECLARE,
1285
+ projects: { type: "list", value: "<slug,...>", description: "Which projects (default every project: *)." },
1286
+ "only-where-missing": { type: "boolean", description: "Skip projects that already have a rule (default true)." }
1287
+ },
1288
+ examples: [{ command: "tracksiy alerts defaults --recipient email", description: "Every project without alerting emails its team." }],
1289
+ async run(ctx) {
1290
+ const body = ctx.input.body ?? compact({ ...declareBody(ctx.input), projects: ctx.input.projects, onlyWhereMissing: ctx.input.onlyWhereMissing });
1291
+ return ctx.api.send("PUT", await orgPath(ctx, "alerts/defaults"), body);
1292
+ }
1293
+ };
1294
+ var settingsGet = {
1295
+ path: ["alerts", "settings"],
1296
+ summary: "When digest-mode recipients are told, and whether the last digests went out.",
1297
+ context: "org",
1298
+ effect: "read",
1299
+ endpoint: { method: "GET", path: "/api/agent/v1/organizations/{org}/alerts/settings" },
1300
+ output: "{digest{hours, timezone, effectiveHours, nextAt, lastSentAt, recipients, pendingItems}, recentDigests[], notes[]}",
1301
+ text: (d) => `${kv(d.digest ?? {})}
1302
+
1303
+ recent digests:
1304
+ ${table(d.recentDigests ?? [], [["sent", (r) => ago(r.sentAt)], "type", "ok", "itemCount", "error"])}`,
1305
+ async run(ctx) {
1306
+ return ctx.api.get(await orgPath(ctx, "alerts/settings"));
1307
+ }
1308
+ };
1309
+ var settingsSet = {
1310
+ path: ["alerts", "settings", "set"],
1311
+ summary: "Declare the digest hours and timezone.",
1312
+ context: "org",
1313
+ effect: "write",
1314
+ idempotent: true,
1315
+ endpoint: { method: "PUT", path: "/api/agent/v1/organizations/{org}/alerts/settings" },
1316
+ flags: {
1317
+ hours: { type: "list", value: "<h,...>", description: "Hours of the day (0-23) a digest is sent, e.g. 11,14,17." },
1318
+ timezone: { type: "string", value: "<iana>", description: "IANA timezone, e.g. Europe/Istanbul." }
1319
+ },
1320
+ examples: [{ command: "tracksiy alerts settings set --hours 9,13,18 --timezone Europe/Istanbul", description: "Three digests a day, Istanbul time." }],
1321
+ async run(ctx) {
1322
+ const hours = ctx.input.hours?.map((h) => {
1323
+ const n = Number(h);
1324
+ if (!Number.isInteger(n) || n < 0 || n > 23) throw usageError("bad_value", `--hours: ${h} is not an hour 0-23`);
1325
+ return n;
1326
+ });
1327
+ if (!hours && !ctx.input.timezone) throw usageError("missing_flag", "Nothing to set.", "Pass --hours and/or --timezone.");
1328
+ return ctx.api.send("PUT", await orgPath(ctx, "alerts/settings"), { digest: compact({ hours, timezone: ctx.input.timezone }) });
1329
+ }
1330
+ };
1331
+ var alerts_default = [get, set, test, coverage, defaults, settingsGet, settingsSet];
1332
+
1333
+ // src/commands/keys.ts
1334
+ var list2 = {
1335
+ path: ["keys", "list"],
1336
+ aliases: [["keys"]],
1337
+ summary: "Every ingest key of the project: its surface, whether ingest accepts it, its rate limit and volume.",
1338
+ context: "project",
1339
+ effect: "read",
1340
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/keys" },
1341
+ output: "{keys:[{name, publicKey, isActive, rateLimit, dsn, created, eventsLast24h}], notes[]}",
1342
+ text: (d) => table(d.keys ?? [], [
1343
+ "name",
1344
+ "isActive",
1345
+ ["rate limit", (r) => r.rateLimit ? `${r.rateLimit.count}/${r.rateLimit.windowSeconds}s` : "none"],
1346
+ ["24h", (r) => r.eventsLast24h],
1347
+ "dsn"
1348
+ ]),
1349
+ async run(ctx) {
1350
+ return ctx.api.get(await projectPath(ctx, "keys"));
1351
+ }
1352
+ };
1353
+ var set2 = {
1354
+ path: ["keys", "set"],
1355
+ summary: "Declare one ingest key's state: active or not, and its rate limit.",
1356
+ context: "project",
1357
+ effect: "write",
1358
+ idempotent: true,
1359
+ endpoint: { method: "PUT", path: "/api/agent/v1/projects/{org}/{project}/keys/{name}" },
1360
+ args: [{ name: "name", required: true, description: "Key name, which is also its surface (backend, web, mobile...)." }],
1361
+ flags: {
1362
+ active: { type: "boolean", description: "Accept events with this key (--no-active to stop ingest from it)." },
1363
+ "rate-limit": { type: "string", value: "<count>/<seconds>", description: "Maximum events per window, e.g. 600/60." },
1364
+ "no-rate-limit": { type: "boolean", description: "Remove the rate limit." }
1365
+ },
1366
+ examples: [{ command: "tracksiy keys set web --rate-limit 600/60", description: "Cap the browser key at 600 events a minute." }],
1367
+ async run(ctx) {
1368
+ const i = ctx.input;
1369
+ const body = compact({
1370
+ isActive: i.active,
1371
+ // Explicit null removes the limit; omitted leaves it as it is.
1372
+ rateLimit: i.noRateLimit ? null : i.rateLimit !== void 0 ? rate(i.rateLimit, "rate-limit") : void 0
1373
+ });
1374
+ return ctx.api.send("PUT", await projectPath(ctx, `keys/${seg(i.name)}`), body);
1375
+ }
1376
+ };
1377
+ var rotate = {
1378
+ path: ["keys", "rotate"],
1379
+ summary: "Replace an ingest key with a new one of the same name and limit; the old one stops being accepted.",
1380
+ description: "Every app sending with the old key must be redeployed with the new DSN, or its events are dropped.",
1381
+ context: "project",
1382
+ effect: "destructive",
1383
+ endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/keys/{name}/rotate" },
1384
+ args: [{ name: "name", required: true, description: "Key name." }],
1385
+ flags: YES,
1386
+ async run(ctx) {
1387
+ requireYes(ctx, `Rotating key ${ctx.input.name} cuts off every app still using the old one.`);
1388
+ return ctx.api.send("POST", await projectPath(ctx, `keys/${seg(ctx.input.name)}/rotate`));
1389
+ }
1390
+ };
1391
+ var keys_default = [list2, set2, rotate];
1392
+
1393
+ // src/commands/releases.ts
1394
+ var list3 = {
1395
+ path: ["releases", "list"],
1396
+ aliases: [["releases"]],
1397
+ summary: "What did the last deploys break? Recent releases with their events, issues and the issues they introduced.",
1398
+ context: "project",
1399
+ effect: "read",
1400
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/releases" },
1401
+ flags: {
1402
+ since: { type: "string", value: TIME, description: "Only releases seen since then." },
1403
+ limit: { type: "integer", value: "<n>", description: "How many releases (server default 10)." }
1404
+ },
1405
+ output: "{releases:[{version, surface, previousVersion, firstEvent, lastEvent, events, issues, newIssueCount, newIssues[]}], note}",
1406
+ text: (d) => table(d.releases ?? [], ["version", "surface", ["first event", (r) => ago(r.firstEvent)], "events", "issues", ["new issues", (r) => r.newIssueCount]]),
1407
+ async run(ctx) {
1408
+ return ctx.api.get(await projectPath(ctx, "releases"), { since: since(ctx.input.since), limit: ctx.input.limit });
1409
+ }
1410
+ };
1411
+ var health2 = {
1412
+ path: ["releases", "health"],
1413
+ summary: "Is the app crashing for its users, and did the last release make it worse? Crash-free sessions and users per release.",
1414
+ context: "project",
1415
+ effect: "read",
1416
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/release-health" },
1417
+ flags: {
1418
+ since: { type: "string", value: TIME, description: "Window start." },
1419
+ environment: { type: "string", value: "<name>", description: "Only this environment." },
1420
+ limit: { type: "integer", value: "<n>", description: "How many releases." }
1421
+ },
1422
+ output: "{receivingSessions, releases:[{version, environment, sessions, crashed, crashFreeSessionsPct, crashFreeUsersPct, ...}], clientReports[], checks[], notes[]}",
1423
+ text: (d) => `receiving sessions: ${d.receivingSessions}
1424
+ ${table(d.releases ?? [], ["version", "environment", "sessions", "crashed", ["crash-free %", (r) => r.crashFreeSessionsPct], ["users crash-free %", (r) => r.crashFreeUsersPct]])}`,
1425
+ async run(ctx) {
1426
+ return ctx.api.get(await projectPath(ctx, "release-health"), {
1427
+ since: since(ctx.input.since),
1428
+ environment: ctx.input.environment,
1429
+ limit: ctx.input.limit
1430
+ });
1431
+ }
1432
+ };
1433
+ var declare = {
1434
+ path: ["releases", "declare"],
1435
+ aliases: [["releases", "create"]],
1436
+ summary: "Create or update a release idempotently: its commit, URL, and when it was released. Run it from the deploy.",
1437
+ context: "project",
1438
+ effect: "write",
1439
+ idempotent: true,
1440
+ endpoint: { method: "PUT", path: "/api/agent/v1/projects/{org}/{project}/releases/{version}" },
1441
+ scopes: ["project:releases", "project:write", "project:admin"],
1442
+ args: [{ name: "version", required: true, description: "The release name the SDK reports, e.g. shop@1.4.2 or shop@<git-sha>." }],
1443
+ flags: {
1444
+ ref: { type: "string", value: "<commit>", description: "Commit or tag it was built from." },
1445
+ url: { type: "string", value: "<url>", description: "Link to the release (changelog, CI run)." },
1446
+ released: { type: "boolean", description: "Mark it released now (if not already). --no-released clears it." },
1447
+ "released-at": { type: "string", value: "<iso>", description: "Mark it released at this time." }
1448
+ },
1449
+ examples: [
1450
+ { command: "tracksiy releases declare shop@$(git rev-parse --short HEAD) --ref $(git rev-parse HEAD) --released", description: "From a deploy script." }
1451
+ ],
1452
+ output: "{version, ref, url, released, created, projects[], artifacts}",
1453
+ async run(ctx) {
1454
+ const i = ctx.input;
1455
+ if (i.released !== void 0 && i.releasedAt) throw usageError("conflict", "Pass --released or --released-at, not both.");
1456
+ if (i.releasedAt && !Number.isFinite(Date.parse(i.releasedAt))) throw usageError("bad_value", `--released-at is not a time: ${i.releasedAt}`);
1457
+ return ctx.api.send("PUT", await projectPath(ctx, `releases/${seg(i.version)}`), compact({ ref: i.ref, url: i.url, released: i.releasedAt ?? i.released }));
1458
+ }
1459
+ };
1460
+ var releases_default = [list3, health2, declare];
1461
+
1462
+ // src/commands/artifacts.ts
1463
+ import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
1464
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
1465
+
1466
+ // src/lib/sourcemaps.ts
1467
+ import { createHash } from "node:crypto";
1468
+ import { existsSync as existsSync2 } from "node:fs";
1469
+ import { readdir, readFile, stat } from "node:fs/promises";
1470
+ import { dirname as dirname2, join as join2, relative, resolve as resolve3, sep } from "node:path";
1471
+ var JS = /\.(m?js|cjs)$/i;
1472
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".svn", ".hg"]);
1473
+ async function walk(dir) {
1474
+ const out = [];
1475
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
1476
+ const full = join2(dir, entry.name);
1477
+ if (entry.isDirectory()) {
1478
+ if (!SKIP_DIRS.has(entry.name)) out.push(...await walk(full));
1479
+ } else if (entry.isFile()) out.push(full);
1480
+ }
1481
+ return out.sort();
1482
+ }
1483
+ function sourceMappingUrl(code) {
1484
+ const re = /^[ \t]*\/\/[#@][ \t]*sourceMappingURL=([^\s'"]+)[ \t]*$/gm;
1485
+ let last;
1486
+ for (let m = re.exec(code); m; m = re.exec(code)) last = m[1];
1487
+ return last;
1488
+ }
1489
+ function debugIdInCode(code) {
1490
+ const re = /^[ \t]*\/\/# debugId=([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})[ \t]*$/gm;
1491
+ let last;
1492
+ for (let m = re.exec(code); m; m = re.exec(code)) last = m[1];
1493
+ return last?.toLowerCase();
1494
+ }
1495
+ function debugIdInMap(json) {
1496
+ try {
1497
+ const m = JSON.parse(json);
1498
+ const id = m.debugId ?? m.debug_id;
1499
+ return typeof id === "string" ? id.toLowerCase() : void 0;
1500
+ } catch {
1501
+ return void 0;
1502
+ }
1503
+ }
1504
+ function debugIdFor(content) {
1505
+ const h = createHash("sha256").update(content).digest();
1506
+ h[6] = h[6] & 15 | 64;
1507
+ h[8] = h[8] & 63 | 128;
1508
+ const hex = h.subarray(0, 16).toString("hex");
1509
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
1510
+ }
1511
+ function snippet(id) {
1512
+ return `;!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&((e._tracksiyDebugIds=e._tracksiyDebugIds||{})[n]="${id}",(e._sentryDebugIds=e._sentryDebugIds||{})[n]="${id}")}catch(e){}}();`;
1513
+ }
1514
+ function injectCode(code, id) {
1515
+ const lines = code.split("\n");
1516
+ let at = lines.length;
1517
+ while (at > 0 && lines[at - 1].trim() === "") at--;
1518
+ if (at > 0 && /^[ \t]*\/\/[#@][ \t]*sourceMappingURL=/.test(lines[at - 1])) at--;
1519
+ const before = lines.slice(0, at);
1520
+ const after = lines.slice(at);
1521
+ const result = [...before, snippet(id), `//# debugId=${id}`, ...after].join("\n");
1522
+ return result.endsWith("\n") ? result : result + "\n";
1523
+ }
1524
+ function injectMap(json, id) {
1525
+ const map = JSON.parse(json);
1526
+ map.debugId = id;
1527
+ map.debug_id = id;
1528
+ return JSON.stringify(map);
1529
+ }
1530
+ async function findPairs(paths) {
1531
+ const pairs = [];
1532
+ const seen = /* @__PURE__ */ new Set();
1533
+ for (const p of paths) {
1534
+ const abs = resolve3(p);
1535
+ const st = await stat(abs).catch(() => void 0);
1536
+ if (!st) throw new Error(`No such file or directory: ${p}`);
1537
+ const root = st.isDirectory() ? abs : dirname2(abs);
1538
+ const files = st.isDirectory() ? await walk(abs) : [abs];
1539
+ for (const js of files.filter((f) => JS.test(f))) {
1540
+ if (seen.has(js)) continue;
1541
+ seen.add(js);
1542
+ const code = await readFile(js, "utf8");
1543
+ const url = sourceMappingUrl(code);
1544
+ let map;
1545
+ if (url && !url.startsWith("data:")) {
1546
+ const candidate = resolve3(dirname2(js), decodeURIComponent(url.split(/[?#]/)[0]));
1547
+ if (existsSync2(candidate)) map = candidate;
1548
+ }
1549
+ if (!map && existsSync2(`${js}.map`)) map = `${js}.map`;
1550
+ pairs.push({ js, map, root });
1551
+ }
1552
+ }
1553
+ return pairs;
1554
+ }
1555
+ function artifactName(file, root, prefix) {
1556
+ const rel = relative(root, file).split(sep).join("/");
1557
+ const p = prefix.endsWith("/") ? prefix : `${prefix}/`;
1558
+ return `${p}${rel}`;
1559
+ }
1560
+
1561
+ // src/lib/chunks.ts
1562
+ import { createHash as createHash2 } from "node:crypto";
1563
+ var sha1 = (data) => createHash2("sha1").update(data).digest("hex");
1564
+ function manifestFor(files, chunkSize) {
1565
+ const chunks = /* @__PURE__ */ new Map();
1566
+ const manifest = files.map((f) => {
1567
+ const ids = [];
1568
+ for (let off = 0; off < f.data.length || off === 0 && f.data.length === 0; off += chunkSize) {
1569
+ const piece = f.data.subarray(off, off + chunkSize);
1570
+ const id = sha1(piece);
1571
+ chunks.set(id, piece);
1572
+ ids.push(id);
1573
+ if (f.data.length === 0) break;
1574
+ }
1575
+ const entry = { name: f.name, kind: f.kind, sha1: sha1(f.data), size: f.data.length, chunks: ids };
1576
+ if (f.debugId) entry.debugId = f.debugId;
1577
+ if (f.sourcemap) entry.sourcemap = f.sourcemap;
1578
+ return entry;
1579
+ });
1580
+ return { manifest, chunks };
1581
+ }
1582
+ var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
1583
+ var UPLOAD_TIMEOUT_MS = 12e4;
1584
+ async function uploadArtifacts(ctx, files, opts) {
1585
+ const { org, project: project2 } = await ctx.project();
1586
+ const api2 = ctx.api;
1587
+ const options = await api2.get(`organizations/${seg(org)}/chunks`);
1588
+ const chunkSize = options.chunkSize ?? 8 * 1024 * 1024;
1589
+ const perRequest = options.maxChunksPerRequest ?? 1;
1590
+ const maxRequest = options.maxRequestSize ?? chunkSize;
1591
+ const { manifest, chunks } = manifestFor(files, chunkSize);
1592
+ const all = [...chunks.keys()];
1593
+ const missing = /* @__PURE__ */ new Set();
1594
+ for (let i = 0; i < all.length; i += 1e3) {
1595
+ const res2 = await api2.send("POST", `organizations/${seg(org)}/chunks/missing`, { checksums: all.slice(i, i + 1e3) }, { retry: true });
1596
+ for (const id of res2.missing ?? []) missing.add(id);
1597
+ }
1598
+ let uploadedBytes = 0;
1599
+ const send = async (ids) => {
1600
+ let batch = [];
1601
+ let size = 0;
1602
+ const flush = async () => {
1603
+ if (!batch.length) return;
1604
+ const form = new FormData();
1605
+ for (const id of batch) form.append("chunk", new Blob([new Uint8Array(chunks.get(id))]), id);
1606
+ await api2.request("POST", `organizations/${seg(org)}/chunks`, { form, retry: true, timeoutMs: Math.max(UPLOAD_TIMEOUT_MS, ctx.config.timeoutMs) });
1607
+ uploadedBytes += size;
1608
+ ctx.log(`uploaded ${batch.length} chunk(s), ${(size / 1024).toFixed(0)} KiB`);
1609
+ batch = [];
1610
+ size = 0;
1611
+ };
1612
+ for (const id of ids) {
1613
+ const len = chunks.get(id).length;
1614
+ if (batch.length && (batch.length >= perRequest || size + len > maxRequest)) await flush();
1615
+ batch.push(id);
1616
+ size += len;
1617
+ }
1618
+ await flush();
1619
+ };
1620
+ await send([...missing]);
1621
+ const path = `projects/${seg(org)}/${seg(project2)}/artifacts`;
1622
+ const body = { release: opts.release ?? null, dist: opts.dist ?? null, files: manifest };
1623
+ const slow = { retry: true, timeoutMs: Math.max(UPLOAD_TIMEOUT_MS, ctx.config.timeoutMs) };
1624
+ let res = await api2.send("POST", path, body, slow);
1625
+ if (res?.state === "missing_chunks") {
1626
+ const again = (res.missingChunks ?? []).filter((id) => chunks.has(id));
1627
+ for (const id of again) missing.add(id);
1628
+ await send(again);
1629
+ res = await api2.send("POST", path, body, slow);
1630
+ }
1631
+ const deadline = Date.now() + opts.waitSeconds * 1e3;
1632
+ let delay = 1e3;
1633
+ while (res?.state === "pending" && res.id && Date.now() < deadline) {
1634
+ await sleep3(delay);
1635
+ delay = Math.min(delay * 2, 5e3);
1636
+ res = await api2.get(`${path}/assemblies/${seg(res.id)}`);
1637
+ }
1638
+ const total = [...chunks.values()].reduce((n, b) => n + b.length, 0);
1639
+ const result = {
1640
+ id: res?.id,
1641
+ state: res?.state ?? "unknown",
1642
+ release: opts.release ?? null,
1643
+ files: res?.files ?? manifest.map((m) => ({ name: m.name, kind: m.kind })),
1644
+ chunks: { total: all.length, uploaded: missing.size, alreadyOnServer: all.length - missing.size },
1645
+ bytes: { total, uploaded: uploadedBytes }
1646
+ };
1647
+ if (result.state === "error" || result.state === "missing_chunks") {
1648
+ const failed = result.files.filter((f) => f?.state === "error");
1649
+ const what = failed.length ? `${failed.length} of ${result.files.length} file(s) were refused: ${failed.slice(0, 5).map((f) => `${f.name} (${f.detail ?? "error"})`).join("; ")}${failed.length > 5 ? "; ..." : ""}` : `Assembly ended in state ${result.state}.`;
1650
+ throw new CliError("upload.failed", what, {
1651
+ exit: EXIT.invalid,
1652
+ hint: "The files marked ok were stored; fix the refused ones and re-run (only what changed is sent again).",
1653
+ details: result
1654
+ });
1655
+ }
1656
+ if (result.state === "pending") {
1657
+ throw new CliError("upload.pending", "The server is still assembling the upload.", {
1658
+ exit: EXIT.checkFailed,
1659
+ hint: "Raise --wait, or check later with `tracksiy artifacts list`.",
1660
+ details: result
1661
+ });
1662
+ }
1663
+ return result;
1664
+ }
1665
+ var DNS_NAMESPACE = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
1666
+ function uuid5(namespace, name) {
1667
+ const ns = Buffer.from(namespace.replace(/-/g, ""), "hex");
1668
+ const h = createHash2("sha1").update(ns).update(name).digest();
1669
+ h[6] = h[6] & 15 | 80;
1670
+ h[8] = h[8] & 63 | 128;
1671
+ const hex = h.subarray(0, 16).toString("hex");
1672
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
1673
+ }
1674
+ function proguardUuid(mapping) {
1675
+ return uuid5(uuid5(DNS_NAMESPACE, "guardsquare.com"), mapping);
1676
+ }
1677
+
1678
+ // src/commands/artifacts.ts
1679
+ async function scan(paths) {
1680
+ try {
1681
+ return await findPairs(paths);
1682
+ } catch (err) {
1683
+ throw usageError("bad_path", err.message);
1684
+ }
1685
+ }
1686
+ async function inject(pairs, dryRun) {
1687
+ const out = [];
1688
+ for (const pair of pairs) {
1689
+ if (!pair.map) {
1690
+ out.push({ file: pair.js, map: null, debugId: "", action: "skipped-no-map" });
1691
+ continue;
1692
+ }
1693
+ const code = await readFile2(pair.js, "utf8");
1694
+ const mapText = await readFile2(pair.map, "utf8");
1695
+ const existing = debugIdInCode(code);
1696
+ const id = existing ?? debugIdFor(code);
1697
+ let action = existing ? "already-injected" : "injected";
1698
+ if (!existing && !dryRun) await writeFile2(pair.js, injectCode(code, id));
1699
+ if (debugIdInMap(mapText) !== id) {
1700
+ if (!dryRun) await writeFile2(pair.map, injectMap(mapText, id));
1701
+ if (existing) action = "map-updated";
1702
+ }
1703
+ out.push({ file: pair.js, map: pair.map, debugId: id, action });
1704
+ }
1705
+ return out;
1706
+ }
1707
+ var injectFlags = {
1708
+ "dry-run": { type: "boolean", description: "Report what would change; write nothing." }
1709
+ };
1710
+ var sourcemapsInject = {
1711
+ path: ["sourcemaps", "inject"],
1712
+ summary: "Stamp built JavaScript and its source maps with a debug ID, so errors map to the exact build. Run after the build, before deploying.",
1713
+ description: "Adds one line (a tiny registration snippet and a //# debugId= comment) to the end of each minified file that has a source map, and the same ID to the map. Deterministic and idempotent: re-running changes nothing. Line/column mappings are untouched.",
1714
+ context: "none",
1715
+ auth: false,
1716
+ effect: "write",
1717
+ idempotent: true,
1718
+ args: [{ name: "paths", required: true, variadic: true, description: "Build output directories or files, e.g. dist public/build." }],
1719
+ flags: injectFlags,
1720
+ examples: [{ command: "tracksiy sourcemaps inject dist", description: "Stamp everything under dist/." }],
1721
+ output: "{files:[{file, map, debugId, action}], injected, alreadyInjected, withoutMap}",
1722
+ text: (d) => `${table(d.files, [["action", (r) => r.action], ["debug id", (r) => r.debugId || "-"], "file"])}
1723
+
1724
+ ${d.injected} injected, ${d.alreadyInjected} already, ${d.withoutMap} without a source map`,
1725
+ async run(ctx) {
1726
+ const files = await inject(await scan(ctx.input.paths), Boolean(ctx.input.dryRun));
1727
+ return {
1728
+ dryRun: Boolean(ctx.input.dryRun),
1729
+ files,
1730
+ injected: files.filter((f) => f.action === "injected").length,
1731
+ alreadyInjected: files.filter((f) => f.action !== "injected" && f.action !== "skipped-no-map").length,
1732
+ withoutMap: files.filter((f) => f.action === "skipped-no-map").length
1733
+ };
1734
+ }
1735
+ };
1736
+ var sourcemapsUpload = {
1737
+ path: ["sourcemaps", "upload"],
1738
+ summary: "Upload built JavaScript and source maps so stack traces show original files and lines. Only missing bytes are sent.",
1739
+ description: "Files are matched to events by debug ID (run `sourcemaps inject` first, or pass --inject) or, without debug IDs, by --release plus the file's URL. --url-prefix must match how the browser loads the files (~/ matches any host).",
1740
+ context: "project",
1741
+ effect: "write",
1742
+ idempotent: true,
1743
+ endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/artifacts" },
1744
+ scopes: ["project:releases", "project:write", "project:admin"],
1745
+ args: [{ name: "paths", required: true, variadic: true, description: "Build output directories or files." }],
1746
+ flags: {
1747
+ release: { type: "string", value: "<version>", description: "Tie the files to this release (required when files have no debug ID)." },
1748
+ dist: { type: "string", value: "<name>", description: "Distribution within the release, e.g. a build number." },
1749
+ "url-prefix": { type: "string", value: "<prefix>", default: "~/", description: "Prefix of the URL the files are served from, e.g. ~/assets/ or ~/static/js/." },
1750
+ inject: { type: "boolean", description: "Inject debug IDs first (same as running `sourcemaps inject`)." },
1751
+ wait: { type: "integer", value: "<seconds>", default: 120, description: "How long to wait for the server to finish assembling." },
1752
+ "dry-run": { type: "boolean", description: "Show what would be uploaded; send nothing." }
1753
+ },
1754
+ examples: [
1755
+ { command: "tracksiy sourcemaps upload dist --inject", description: "Inject and upload a Vite build." },
1756
+ { command: "tracksiy sourcemaps upload public/build --release shop@$(git rev-parse --short HEAD) --url-prefix ~/build/", description: "Release-based, for apps that set a release in the SDK." }
1757
+ ],
1758
+ output: "{id, state, release, files[{name, kind, state, detail}], chunks{total, uploaded, alreadyOnServer}, bytes{total, uploaded}, skipped[] (minified files without a map)}",
1759
+ text: (d) => d.dryRun ? table(d.files, ["kind", ["debug id", (r) => r.debugId ?? "-"], "name", "size"]) : `${d.state}: ${d.files.length} file(s), ${d.chunks.uploaded}/${d.chunks.total} chunks sent (${d.chunks.alreadyOnServer} already on the server)
1760
+ ${table(d.files, ["kind", "state", "name", "detail"])}${d.skipped?.length ? `
1761
+
1762
+ skipped, no source map: ${d.skipped.join(", ")}` : ""}`,
1763
+ async run(ctx) {
1764
+ const i = ctx.input;
1765
+ const pairs = await scan(i.paths);
1766
+ if (!pairs.length) throw usageError("nothing_to_upload", "No JavaScript files found.", "Point at the build output directory (dist, build, public/build).");
1767
+ if (i.inject && !i.dryRun) await inject(pairs, false);
1768
+ const skipped = pairs.filter((p) => !p.map).map((p) => artifactName(p.js, p.root, i.urlPrefix));
1769
+ const withMaps = pairs.filter((p) => p.map);
1770
+ if (!withMaps.length) {
1771
+ throw usageError("nothing_to_upload", `None of the ${pairs.length} JavaScript file(s) has a source map.`, "Build with source maps on (e.g. Vite build.sourcemap: true, webpack devtool: 'source-map').");
1772
+ }
1773
+ const files = [];
1774
+ let withoutId = 0;
1775
+ for (const pair of withMaps) {
1776
+ const code = await readFile2(pair.js);
1777
+ const mapBuf = pair.map ? await readFile2(pair.map) : void 0;
1778
+ const id = debugIdInCode(code.toString("utf8")) ?? (mapBuf ? debugIdInMap(mapBuf.toString("utf8")) : void 0);
1779
+ if (!id) withoutId++;
1780
+ const name = artifactName(pair.js, pair.root, i.urlPrefix);
1781
+ const mapName = pair.map ? artifactName(pair.map, pair.root, i.urlPrefix) : void 0;
1782
+ files.push(compact({ name, kind: "source", data: code, debugId: id, sourcemap: mapName }));
1783
+ if (pair.map && mapBuf) files.push(compact({ name: mapName, kind: "sourcemap", data: mapBuf, debugId: id }));
1784
+ }
1785
+ if (withoutId && !i.release) {
1786
+ throw usageError(
1787
+ "no_match_key",
1788
+ `${withoutId} file(s) have no debug ID and no --release was given, so nothing could ever match them to an event.`,
1789
+ "Add --inject (recommended), or pass --release with the same value the SDK reports."
1790
+ );
1791
+ }
1792
+ if (i.dryRun) {
1793
+ return { dryRun: true, release: i.release ?? null, files: files.map(({ data, ...rest }) => ({ ...rest, size: data.length })), skipped };
1794
+ }
1795
+ const result = await uploadArtifacts(ctx, files, { release: i.release, dist: i.dist, waitSeconds: i.wait });
1796
+ return { ...result, skipped };
1797
+ }
1798
+ };
1799
+ function uuidFromDebugMeta(path) {
1800
+ if (!existsSync3(path)) throw usageError("bad_path", `No such file: ${path}`);
1801
+ const m = /ProguardUuids\s*[=:]\s*([0-9a-fA-F-]{36})/.exec(readFileSync3(path, "utf8"));
1802
+ if (!m) throw usageError("bad_value", `No ProguardUuids entry in ${path}`);
1803
+ return m[1].toLowerCase();
1804
+ }
1805
+ var proguardUpload = {
1806
+ path: ["proguard", "upload"],
1807
+ aliases: [["r8", "upload"]],
1808
+ summary: "Upload an Android ProGuard/R8 mapping so obfuscated stack traces are readable.",
1809
+ description: "The UUID must be the one the app reports for this build. Pass --uuid, or --debug-meta pointing at the properties file your build generates; without either, the UUID is derived from the mapping's content.",
1810
+ context: "project",
1811
+ effect: "write",
1812
+ idempotent: true,
1813
+ endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/artifacts" },
1814
+ scopes: ["project:releases", "project:write", "project:admin"],
1815
+ args: [{ name: "mapping", required: true, description: "Path to mapping.txt (e.g. app/build/outputs/mapping/release/mapping.txt)." }],
1816
+ flags: {
1817
+ uuid: { type: "string", value: "<uuid>", description: "The mapping UUID the app reports." },
1818
+ "debug-meta": { type: "string", value: "<file>", description: "A .properties file with ProguardUuids=<uuid> (generated by the Android build)." },
1819
+ release: { type: "string", value: "<version>", description: "Release to associate, e.g. com.example.app@2.1.0+80." },
1820
+ wait: { type: "integer", value: "<seconds>", default: 120, description: "How long to wait for assembly." }
1821
+ },
1822
+ examples: [{ command: "tracksiy proguard upload app/build/outputs/mapping/release/mapping.txt --uuid $UUID", description: "Upload the release mapping." }],
1823
+ output: "{id, state, files[{name, kind, state}], chunks{}, bytes{}, uuid}",
1824
+ async run(ctx) {
1825
+ const i = ctx.input;
1826
+ if (!existsSync3(i.mapping)) throw usageError("bad_path", `No such file: ${i.mapping}`);
1827
+ const data = await readFile2(i.mapping);
1828
+ if (!data.length) throw usageError("bad_value", `${i.mapping} is empty.`);
1829
+ const uuid = (i.uuid ?? (i.debugMeta ? uuidFromDebugMeta(i.debugMeta) : proguardUuid(data))).toLowerCase();
1830
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(uuid)) throw usageError("bad_value", `--uuid is not a UUID: ${uuid}`);
1831
+ const res = await uploadArtifacts(ctx, [{ name: `proguard/${uuid}.txt`, kind: "proguard", data, debugId: uuid }], { release: i.release, waitSeconds: i.wait });
1832
+ return { ...res, uuid, uuidSource: i.uuid ? "--uuid" : i.debugMeta ? i.debugMeta : "derived from content" };
1833
+ }
1834
+ };
1835
+ var artifactsList = {
1836
+ path: ["artifacts", "list"],
1837
+ aliases: [["artifacts"]],
1838
+ summary: "What is actually stored for a project: source files, source maps and ProGuard mappings. Use it to prove an upload landed.",
1839
+ context: "project",
1840
+ effect: "read",
1841
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/artifacts" },
1842
+ flags: {
1843
+ release: { type: "string", value: "<version>", description: "Only this release." },
1844
+ "debug-id": { type: "string", value: "<uuid>", description: "Only this debug ID." },
1845
+ kind: { type: "string", enum: ["source", "sourcemap", "proguard"], description: "Only this kind." },
1846
+ limit: { type: "integer", value: "<n>", description: "How many." }
1847
+ },
1848
+ output: "[{name, kind, debugId, release, size, sha1, created, lastUsed}]",
1849
+ text: (d) => table(d, ["kind", "name", "release", ["debug id", (r) => r.debugId ?? "-"], "size", ["created", (r) => ago(r.created)]]),
1850
+ async run(ctx) {
1851
+ return ctx.api.get(await projectPath(ctx, "artifacts"), {
1852
+ release: ctx.input.release,
1853
+ debugId: ctx.input.debugId,
1854
+ kind: ctx.input.kind,
1855
+ limit: ctx.input.limit
1856
+ });
1857
+ }
1858
+ };
1859
+ var artifacts_default = [sourcemapsInject, sourcemapsUpload, proguardUpload, artifactsList];
1860
+
1861
+ // src/commands/tokens.ts
1862
+ var SCOPE_HINT = "e.g. org:read,project:read,event:read,event:write,project:write,project:releases,project:admin";
1863
+ var list4 = {
1864
+ path: ["tokens", "list"],
1865
+ aliases: [["tokens"]],
1866
+ summary: "Your tokens, newest first, without their values.",
1867
+ context: "none",
1868
+ effect: "read",
1869
+ endpoint: { method: "GET", path: "/api/agent/v1/tokens" },
1870
+ output: "[{id, label, scopes, projects, created, expires, lastUsed, isExpired, isCurrent}]",
1871
+ text: (d) => table(d, [
1872
+ "id",
1873
+ ["", (r) => r.isCurrent ? "*" : ""],
1874
+ "label",
1875
+ "scopes",
1876
+ ["projects", (r) => r.projects?.length ? r.projects.join(",") : "all"],
1877
+ ["last used", (r) => ago(r.lastUsed)],
1878
+ ["expires", (r) => r.isExpired ? "EXPIRED" : r.expires ? ago(r.expires) : "never"]
1879
+ ]),
1880
+ async run(ctx) {
1881
+ return ctx.api.get("tokens");
1882
+ }
1883
+ };
1884
+ var create2 = {
1885
+ path: ["tokens", "create"],
1886
+ summary: "Mint a token for the same user, never broader than the one making the call.",
1887
+ description: "The value is returned once. Restrict it to projects and the narrowest scopes the consumer needs.",
1888
+ context: "none",
1889
+ effect: "write",
1890
+ endpoint: { method: "POST", path: "/api/agent/v1/tokens" },
1891
+ flags: {
1892
+ label: { type: "string", required: true, value: "<text>", description: "What it is for, e.g. 'ci source maps for shop'." },
1893
+ scopes: { type: "list", required: true, value: "<scope,...>", description: `Scopes, ${SCOPE_HINT}.` },
1894
+ projects: { type: "list", value: "<slug,...>", description: "Only these projects (default: all the caller can reach)." },
1895
+ "expires-in-days": { type: "integer", value: "<n>", description: "Lifetime in days (server default 365)." }
1896
+ },
1897
+ examples: [
1898
+ { command: "tracksiy tokens create --label 'ci sourcemaps' --scopes project:releases --projects shop --expires-in-days 90", description: "A CI token that can only upload artifacts for one project." }
1899
+ ],
1900
+ output: "{id, label, scopes, projects, expires, token} -- `token` is shown only here.",
1901
+ async run(ctx) {
1902
+ return ctx.api.send("POST", "tokens", compact({ label: ctx.input.label, scopes: ctx.input.scopes, projects: ctx.input.projects, expiresInDays: ctx.input.expiresInDays }));
1903
+ }
1904
+ };
1905
+ var update = {
1906
+ path: ["tokens", "update"],
1907
+ summary: "Change a token's label, scopes, projects or lifetime (never broader than the caller).",
1908
+ context: "none",
1909
+ effect: "write",
1910
+ idempotent: true,
1911
+ endpoint: { method: "PATCH", path: "/api/agent/v1/tokens/{token_id}" },
1912
+ args: [{ name: "id", required: true, description: "Token id from `tokens list`." }],
1913
+ flags: {
1914
+ label: { type: "string", value: "<text>", description: "New label." },
1915
+ scopes: { type: "list", value: "<scope,...>", description: `Replace the scopes, ${SCOPE_HINT}.` },
1916
+ projects: { type: "list", value: "<slug,...>", description: "Replace the project list (empty string = all)." },
1917
+ "expires-in-days": { type: "integer", value: "<n>", description: "New lifetime from now." }
1918
+ },
1919
+ async run(ctx) {
1920
+ const body = compact({ label: ctx.input.label, scopes: ctx.input.scopes, projects: ctx.input.projects, expiresInDays: ctx.input.expiresInDays });
1921
+ if (!Object.keys(body).length) throw usageError("missing_flag", "Nothing to change.", "Pass --label, --scopes, --projects or --expires-in-days.");
1922
+ return ctx.api.send("PATCH", `tokens/${encodeURIComponent(ctx.input.id)}`, body);
1923
+ }
1924
+ };
1925
+ var remove = {
1926
+ path: ["tokens", "delete"],
1927
+ aliases: [["tokens", "revoke"]],
1928
+ summary: "Revoke a token immediately.",
1929
+ context: "none",
1930
+ effect: "destructive",
1931
+ idempotent: true,
1932
+ endpoint: { method: "DELETE", path: "/api/agent/v1/tokens/{token_id}" },
1933
+ args: [{ name: "id", required: true, description: "Token id from `tokens list`." }],
1934
+ flags: YES,
1935
+ async run(ctx) {
1936
+ requireYes(ctx, `Token ${ctx.input.id} stops working at once, for everything that uses it.`);
1937
+ await ctx.api.send("DELETE", `tokens/${encodeURIComponent(ctx.input.id)}`);
1938
+ return { deleted: Number(ctx.input.id) };
1939
+ }
1940
+ };
1941
+ var rotate2 = {
1942
+ path: ["tokens", "rotate"],
1943
+ summary: "Replace the token making this call; the old one keeps working for a grace period.",
1944
+ description: "If the token came from `tracksiy auth login`, the saved login is updated to the new value.",
1945
+ context: "none",
1946
+ effect: "destructive",
1947
+ endpoint: { method: "POST", path: "/api/agent/v1/tokens/self/rotate" },
1948
+ flags: {
1949
+ "grace-minutes": { type: "integer", value: "<n>", description: "How long the old token keeps working (server default 60)." },
1950
+ ...YES
1951
+ },
1952
+ output: "{token, id, expires, previous{id, expiresAt}, savedTo?}",
1953
+ async run(ctx) {
1954
+ requireYes(ctx, "The current token will stop working after the grace period, everywhere it is used.");
1955
+ const data = await ctx.api.send("POST", "tokens/self/rotate", compact({ graceMinutes: ctx.input.graceMinutes }));
1956
+ if (ctx.config.tokenSource === ctx.config.configPath && data?.token) {
1957
+ const cfg = readUserConfig(ctx.config.configPath);
1958
+ cfg.hosts[ctx.config.url] = { ...cfg.hosts[ctx.config.url], token: data.token, savedAt: (/* @__PURE__ */ new Date()).toISOString() };
1959
+ await writeUserConfig(ctx.config.configPath, cfg);
1960
+ return { ...data, savedTo: ctx.config.configPath };
1961
+ }
1962
+ return data;
1963
+ }
1964
+ };
1965
+ var self = {
1966
+ path: ["tokens", "self"],
1967
+ summary: "Describe the token making this call: scopes, projects, expiry.",
1968
+ context: "none",
1969
+ effect: "read",
1970
+ endpoint: { method: "GET", path: "/api/agent/v1/tokens/self" },
1971
+ text: (d) => kv(d),
1972
+ async run(ctx) {
1973
+ return ctx.api.get("tokens/self");
1974
+ }
1975
+ };
1976
+ var tokens_default = [list4, self, create2, update, remove, rotate2];
1977
+
1978
+ // src/commands/support.ts
1979
+ var SUPPORT = {
1980
+ via: "peerpacket",
1981
+ channel: "tracksiy-support",
1982
+ handle: "tracksiy-agent",
1983
+ install: "npm install -g peerpacket",
1984
+ join: "pp join tracksiy-support",
1985
+ open: 'pp ticket new tracksiy-support "<one-line problem>" --to tracksiy-agent --label bug -f report.md',
1986
+ follow: "pp tickets tracksiy-support"
1987
+ };
1988
+ var SUPPORT_HINT = `tell the Tracksiy team on PeerPacket: ${SUPPORT.open} (see \`tracksiy support\`)`;
1989
+ var support = {
1990
+ path: ["support"],
1991
+ aliases: [["feedback"], ["contact"]],
1992
+ summary: "How to tell the Tracksiy team what got in your way: a bug, a missing capability, an error you cannot interpret, or a blocker.",
1993
+ description: "Tickets go through PeerPacket (the pp CLI): open one in the tracksiy-support channel for tracksiy-agent, who answers on the ticket and closes it with what was done. Labels: bug, feature, question, blocker. Say what you ran, what came back (error code, request id) and what you expected; include your CLI version. Never paste tokens; use `pp secret send`. Needs no token and makes no request.",
1994
+ context: "none",
1995
+ auth: false,
1996
+ effect: "read",
1997
+ idempotent: true,
1998
+ examples: [{ command: "tracksiy support", description: "Print the channel, handle and the exact commands." }],
1999
+ output: "{via, channel, handle, install, join, open, follow, labels[], include[], cliVersion}",
2000
+ text: (d) => kv({ via: d.via, channel: d.channel, "ticket for": d.handle, install: d.install, join: d.join, open: d.open, follow: d.follow, labels: d.labels.join(", ") }),
2001
+ async run() {
2002
+ return {
2003
+ ...SUPPORT,
2004
+ labels: ["bug", "feature", "question", "blocker"],
2005
+ include: ["what you ran", "what came back: error code, message, request id", "what you expected", `tracksiy --version (${VERSION})`],
2006
+ cliVersion: VERSION
2007
+ };
2008
+ }
2009
+ };
2010
+ var support_default = [support];
2011
+
2012
+ // src/commands/init.ts
2013
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
2014
+ import { appendFile, writeFile as writeFile3 } from "node:fs/promises";
2015
+ import { join as join4, resolve as resolve4 } from "node:path";
2016
+
2017
+ // src/lib/detect.ts
2018
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync } from "node:fs";
2019
+ import { basename, join as join3 } from "node:path";
2020
+ function readJson(path) {
2021
+ try {
2022
+ return JSON.parse(readFileSync4(path, "utf8"));
2023
+ } catch {
2024
+ return void 0;
2025
+ }
2026
+ }
2027
+ function has(dir, ...names) {
2028
+ return names.find((n) => existsSync4(join3(dir, n)));
2029
+ }
2030
+ function slugify(s) {
2031
+ return s.toLowerCase().replace(/^@[^/]+\//, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
2032
+ }
2033
+ function detect(dir, guideBase) {
2034
+ const evidence = [];
2035
+ const composer = readJson(join3(dir, "composer.json"));
2036
+ const pkg = readJson(join3(dir, "package.json"));
2037
+ const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
2038
+ const hasDep = (d) => d in deps;
2039
+ const name = slugify(pkg?.name ?? composer?.name?.split("/").pop() ?? basename(dir)) || "app";
2040
+ const guide = (slug) => `${guideBase}/${slug}.md`;
2041
+ if (composer?.require?.["laravel/framework"]) {
2042
+ evidence.push("composer.json requires laravel/framework");
2043
+ const web = hasDep("react") || hasDep("vue") || hasDep("svelte") || hasDep("@inertiajs/react") || hasDep("@inertiajs/vue3");
2044
+ if (web) evidence.push("package.json has a browser framework");
2045
+ return {
2046
+ platform: "php-laravel",
2047
+ framework: "laravel",
2048
+ surfaces: web ? ["backend", "web"] : ["backend"],
2049
+ env: web ? { backend: "SENTRY_LARAVEL_DSN", web: "VITE_SENTRY_DSN" } : { backend: "SENTRY_LARAVEL_DSN" },
2050
+ guide: guide("php-laravel"),
2051
+ buildDir: web ? "public/build" : void 0,
2052
+ name,
2053
+ evidence
2054
+ };
2055
+ }
2056
+ if (hasDep("react-native") || hasDep("expo")) {
2057
+ evidence.push(`package.json depends on ${hasDep("expo") ? "expo" : "react-native"}`);
2058
+ return { platform: "react-native", framework: hasDep("expo") ? "expo" : "react-native", surfaces: ["mobile"], env: {}, guide: guide("mobile"), name, evidence };
2059
+ }
2060
+ if (hasDep("next")) {
2061
+ evidence.push("package.json depends on next");
2062
+ return {
2063
+ platform: "javascript-nextjs",
2064
+ framework: "nextjs",
2065
+ surfaces: ["backend", "web"],
2066
+ env: { backend: "SENTRY_DSN", web: "NEXT_PUBLIC_SENTRY_DSN" },
2067
+ guide: guide("javascript"),
2068
+ buildDir: ".next/static",
2069
+ name,
2070
+ evidence
2071
+ };
2072
+ }
2073
+ const browser = ["react", "vue", "svelte", "@angular/core", "solid-js", "preact"].find(hasDep);
2074
+ if (browser) {
2075
+ evidence.push(`package.json depends on ${browser}`);
2076
+ const vite = hasDep("vite");
2077
+ return {
2078
+ platform: browser === "react" ? "javascript-react" : browser === "vue" ? "javascript-vue" : browser === "@angular/core" ? "javascript-angular" : "javascript",
2079
+ framework: browser.replace("@angular/core", "angular"),
2080
+ surfaces: ["web"],
2081
+ env: { web: vite ? "VITE_SENTRY_DSN" : "SENTRY_DSN" },
2082
+ guide: guide("javascript"),
2083
+ buildDir: vite ? "dist" : "build",
2084
+ name,
2085
+ evidence
2086
+ };
2087
+ }
2088
+ if (pkg) {
2089
+ evidence.push("package.json without a browser framework");
2090
+ return { platform: "node", framework: ["express", "fastify", "@nestjs/core", "hono", "koa"].find(hasDep) ?? "node", surfaces: ["backend"], env: { backend: "SENTRY_DSN" }, guide: guide("node"), name, evidence };
2091
+ }
2092
+ const py = has(dir, "pyproject.toml", "requirements.txt", "Pipfile", "manage.py");
2093
+ if (py) {
2094
+ const text = ["pyproject.toml", "requirements.txt", "Pipfile"].map((f) => existsSync4(join3(dir, f)) ? readFileSync4(join3(dir, f), "utf8") : "").join("\n").toLowerCase();
2095
+ const fw = existsSync4(join3(dir, "manage.py")) || text.includes("django") ? "django" : text.includes("fastapi") ? "fastapi" : text.includes("flask") ? "flask" : "python";
2096
+ evidence.push(`${py} (${fw})`);
2097
+ return { platform: fw === "python" ? "python" : `python-${fw}`, framework: fw, surfaces: ["backend"], env: { backend: "SENTRY_DSN" }, guide: guide("python-sentry-sdk"), name, evidence };
2098
+ }
2099
+ const gradle = has(dir, "build.gradle.kts", "build.gradle", "app/build.gradle.kts", "app/build.gradle");
2100
+ if (gradle) {
2101
+ evidence.push(gradle);
2102
+ return { platform: "android", framework: "android", surfaces: ["mobile"], env: {}, guide: guide("mobile"), name, evidence };
2103
+ }
2104
+ const apple = has(dir, "Podfile", "Package.swift") ?? readdirSync(dir).find((f) => f.endsWith(".xcodeproj"));
2105
+ if (apple) {
2106
+ evidence.push(apple);
2107
+ return { platform: "apple-ios", framework: "ios", surfaces: ["mobile"], env: {}, guide: guide("mobile"), name, evidence };
2108
+ }
2109
+ if (has(dir, "pubspec.yaml")) {
2110
+ evidence.push("pubspec.yaml");
2111
+ return { platform: "flutter", framework: "flutter", surfaces: ["mobile"], env: {}, guide: guide("mobile"), name, evidence };
2112
+ }
2113
+ return { platform: "other", framework: "unknown", surfaces: ["backend"], env: { backend: "SENTRY_DSN" }, guide: guide("other-stacks"), name, evidence: ["nothing recognised"] };
2114
+ }
2115
+
2116
+ // src/commands/init.ts
2117
+ var init = {
2118
+ path: ["init"],
2119
+ summary: "Integrate the app in this directory, without a browser: detect the stack, create the project and its keys, write .tracksiy.json, and print the exact next steps.",
2120
+ description: "Idempotent: re-running on an integrated app returns the same project. It never edits source code; it writes .tracksiy.json (no secrets) and, only with --write-env, appends missing ingest-key variables to an existing .env. Finish with `tracksiy projects verify --wait 120` after the first deploy.",
2121
+ context: "org",
2122
+ effect: "write",
2123
+ idempotent: true,
2124
+ endpoint: { method: "POST", path: "/api/agent/v1/organizations/{org}/projects" },
2125
+ scopes: ["project:admin"],
2126
+ flags: {
2127
+ dir: { type: "string", value: "<path>", default: ".", description: "The app's root directory." },
2128
+ name: { type: "string", value: "<project>", description: "Project name (default: package/composer name or directory name)." },
2129
+ platform: { type: "string", value: "<platform>", description: "Override the detected platform." },
2130
+ surfaces: { type: "list", value: "<name,...>", description: "Override the detected surfaces (backend, web, mobile, ...)." },
2131
+ "same-host": { type: "boolean", description: "The app runs on the Tracksiy host itself (loopback ingest)." },
2132
+ "write-env": { type: "boolean", description: "Append missing ingest-key variables to the existing .env (never overwrites a value)." },
2133
+ "dry-run": { type: "boolean", description: "Only detect and show the plan; create nothing." }
2134
+ },
2135
+ examples: [
2136
+ { command: "tracksiy init --dry-run", description: "What would be created for this directory." },
2137
+ { command: "tracksiy init --write-env && tracksiy projects verify --wait 120", description: "Integrate, deploy, and prove the first event arrived." }
2138
+ ],
2139
+ output: "{detected{platform, framework, surfaces, evidence}, project{organization, slug}, created, keys[{surface, env, dsn}], projectFile, env{file, added[], kept[]}, uploadToken, guide, steps[{id, run?, detail}]}",
2140
+ text: (d) => [
2141
+ kv({ platform: d.detected.platform, evidence: d.detected.evidence.join("; "), project: d.project ? `${d.project.organization}/${d.project.slug} (${d.created ? "created" : "existing"})` : "(dry run)", "project file": d.projectFile ?? "-", guide: d.guide }),
2142
+ d.keys?.length ? `
2143
+ ingest keys:
2144
+ ${table(d.keys, ["surface", "env", "dsn"])}` : "",
2145
+ d.env ? `
2146
+ .env: added ${d.env.added.join(", ") || "nothing"}${d.env.kept.length ? `; kept existing ${d.env.kept.join(", ")}` : ""}` : "",
2147
+ `
2148
+ next:
2149
+ ${d.steps.map((s, i) => `${i + 1}. ${s.detail}${s.run ? `
2150
+ $ ${s.run}` : ""}`).join("\n")}`
2151
+ ].join("\n"),
2152
+ async run(ctx) {
2153
+ const i = ctx.input;
2154
+ const dir = resolve4(i.dir);
2155
+ if (!existsSync5(dir)) throw usageError("bad_path", `No such directory: ${i.dir}`);
2156
+ const found = detect(dir, ctx.config.url);
2157
+ const platform = i.platform ?? found.platform;
2158
+ const surfaces = i.surfaces ?? found.surfaces;
2159
+ const name = i.name ?? found.name;
2160
+ const detected = { platform, framework: found.framework, surfaces, evidence: found.evidence, name };
2161
+ const steps = [];
2162
+ const upload = found.buildDir ? `tracksiy sourcemaps upload ${found.buildDir} --inject` : void 0;
2163
+ if (i.dryRun) {
2164
+ steps.push({ id: "create", run: `tracksiy init${i.writeEnv ? " --write-env" : ""}`, detail: "Create the project and keys." });
2165
+ return { dryRun: true, detected, guide: found.guide, steps };
2166
+ }
2167
+ const org = await ctx.org();
2168
+ const prov = await ctx.api.send(
2169
+ "POST",
2170
+ `organizations/${seg(org)}/projects`,
2171
+ compact({ name, platform, surfaces, sameHost: i.sameHost })
2172
+ );
2173
+ const project2 = prov.project;
2174
+ const pfPath = join4(dir, PROJECT_FILE);
2175
+ const pf = { org: project2.organization, project: project2.slug };
2176
+ if (ctx.config.url !== DEFAULT_URL) pf.url = ctx.config.url;
2177
+ await writeFile3(pfPath, JSON.stringify(pf, null, 2) + "\n");
2178
+ const keys = (prov.dsns ?? []).map((k) => ({
2179
+ surface: k.surface,
2180
+ env: found.env[k.surface] ?? null,
2181
+ dsn: k.dsn,
2182
+ shipsInPublicArtifact: k.shipsInPublicArtifact
2183
+ }));
2184
+ let env;
2185
+ if (i.writeEnv) {
2186
+ const envPath = join4(dir, ".env");
2187
+ if (!existsSync5(envPath)) {
2188
+ throw usageError("no_env_file", `${envPath} does not exist; nothing was written to it.`, "Create it (and make sure it is gitignored), or set the variables yourself from `keys` in the output.");
2189
+ }
2190
+ const current = readFileSync5(envPath, "utf8");
2191
+ const added = [];
2192
+ const kept = [];
2193
+ let block = "";
2194
+ for (const k of keys) {
2195
+ if (!k.env) continue;
2196
+ if (new RegExp(`^\\s*${k.env}\\s*=`, "m").test(current)) kept.push(k.env);
2197
+ else {
2198
+ block += `${k.env}=${k.dsn}
2199
+ `;
2200
+ added.push(k.env);
2201
+ }
2202
+ }
2203
+ if (block) await appendFile(envPath, `${current.endsWith("\n") || !current ? "" : "\n"}
2204
+ # Tracksiy (tracksiy init)
2205
+ ${block}`);
2206
+ env = { file: envPath, added, kept };
2207
+ }
2208
+ steps.push({ id: "sdk", detail: `Install and configure the SDK for ${found.framework} as the guide says (it is plain Markdown).`, run: `curl -s ${found.guide}` });
2209
+ if (!i.writeEnv && keys.some((k) => k.env)) {
2210
+ steps.push({ id: "env", detail: `Set ${keys.filter((k) => k.env).map((k) => k.env).join(", ")} from \`keys\` (or re-run with --write-env).` });
2211
+ }
2212
+ if (upload) steps.push({ id: "sourcemaps", detail: "After each production build, upload source maps (uses project:releases).", run: upload });
2213
+ steps.push({ id: "alerts", detail: "Decide who is told. A project without a recipient notifies nobody.", run: "tracksiy alerts set --recipient email && tracksiy alerts test" });
2214
+ steps.push({ id: "verify", detail: "Deploy, trigger one error, then prove it arrived.", run: "tracksiy projects verify --wait 120" });
2215
+ return {
2216
+ detected,
2217
+ project: project2,
2218
+ created: prov.created,
2219
+ keys,
2220
+ projectFile: pfPath,
2221
+ env: env ?? null,
2222
+ uploadToken: prov.sourcemapToken ?? null,
2223
+ uploadTokenScopes: prov.sourcemapTokenScopes ?? [],
2224
+ guide: found.guide,
2225
+ mustRead: prov.mustRead ?? [],
2226
+ serverNextSteps: prov.nextSteps ?? [],
2227
+ steps
2228
+ };
2229
+ }
2230
+ };
2231
+ var init_default = [init];
2232
+
2233
+ // src/core/spec.ts
2234
+ function commandName(spec) {
2235
+ return spec.path.join(" ");
2236
+ }
2237
+ function toolName(spec) {
2238
+ return spec.path.join("_").replace(/-/g, "_");
2239
+ }
2240
+ function camel(name) {
2241
+ return name.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
2242
+ }
2243
+
2244
+ // src/core/args.ts
2245
+ import { readFileSync as readFileSync6 } from "node:fs";
2246
+ var GLOBAL_FLAGS = {
2247
+ org: { type: "string", value: "<slug>", description: "Organization slug. Env: TRACKSIY_ORG. Default: .tracksiy.json, saved login, or your only organization." },
2248
+ project: { type: "string", value: "<slug>", description: "Project slug, or org/project. Env: TRACKSIY_PROJECT. Default: .tracksiy.json." },
2249
+ url: { type: "string", value: "<url>", description: "Server root. Env: TRACKSIY_URL. Default: https://tracksiy.com." },
2250
+ token: { type: "string", value: "<token>", description: "API token. Env: TRACKSIY_TOKEN (preferred: flags show up in process lists)." },
2251
+ json: { type: "boolean", description: "Print JSON. This is the default whenever stdout is not a terminal." },
2252
+ format: { type: "string", enum: ["json", "text"], description: "Output format. Env: TRACKSIY_FORMAT." },
2253
+ select: { type: "list", value: "<fields>", description: "Keep only these fields of `data`, comma-separated; dot paths reach into objects and lists (e.g. issues.id,issues.title)." },
2254
+ timeout: { type: "integer", value: "<seconds>", description: "Seconds to wait per request. Default 30." },
2255
+ verbose: { type: "boolean", description: "Log each HTTP request and its status to stderr (never the token)." },
2256
+ quiet: { type: "boolean", description: "No progress messages on stderr." },
2257
+ help: { type: "boolean", short: "h", description: "Show help for the command." }
2258
+ };
2259
+ var stdinCache;
2260
+ function readStdin() {
2261
+ stdinCache ??= readFileSync6(0, "utf8");
2262
+ return stdinCache;
2263
+ }
2264
+ function parseJsonValue(name, raw) {
2265
+ let text = raw;
2266
+ if (raw === "-") text = readStdin();
2267
+ else if (raw.startsWith("@")) {
2268
+ try {
2269
+ text = readFileSync6(raw.slice(1), "utf8");
2270
+ } catch (err) {
2271
+ throw usageError("bad_value", `--${name}: cannot read ${raw.slice(1)}: ${err.message}`);
2272
+ }
2273
+ }
2274
+ try {
2275
+ return JSON.parse(text);
2276
+ } catch (err) {
2277
+ throw usageError("bad_json", `--${name} is not valid JSON: ${err.message}`, `Pass inline JSON, @file.json, or - for stdin.`);
2278
+ }
2279
+ }
2280
+ function coerce(name, spec, value, previous) {
2281
+ const fail = (why) => usageError("bad_value", `--${name} ${why}`);
2282
+ switch (spec.type) {
2283
+ case "boolean": {
2284
+ if (typeof value === "boolean") return value;
2285
+ const v = String(value).toLowerCase();
2286
+ if (["true", "1", "yes", "on"].includes(v)) return true;
2287
+ if (["false", "0", "no", "off"].includes(v)) return false;
2288
+ throw fail(`expects true or false, got ${JSON.stringify(value)}`);
2289
+ }
2290
+ case "integer":
2291
+ case "number": {
2292
+ const n = typeof value === "number" ? value : Number(value);
2293
+ if (!Number.isFinite(n) || spec.type === "integer" && !Number.isInteger(n)) {
2294
+ throw fail(`expects ${spec.type === "integer" ? "an integer" : "a number"}, got ${JSON.stringify(value)}`);
2295
+ }
2296
+ return n;
2297
+ }
2298
+ case "list": {
2299
+ const items = Array.isArray(value) ? value.map(String) : String(value).split(",").map((s) => s.trim()).filter(Boolean);
2300
+ if (spec.enum) for (const item of items) checkEnum(name, spec, item);
2301
+ return [...previous ?? [], ...items];
2302
+ }
2303
+ case "json":
2304
+ return typeof value === "string" ? parseJsonValue(name, value) : value;
2305
+ default: {
2306
+ const s = String(value);
2307
+ checkEnum(name, spec, s);
2308
+ return s;
2309
+ }
2310
+ }
2311
+ }
2312
+ function checkEnum(name, spec, value) {
2313
+ if (spec.enum && !spec.enum.includes(value)) {
2314
+ throw usageError("bad_value", `--${name} must be one of ${spec.enum.join(", ")}; got ${JSON.stringify(value)}`);
2315
+ }
2316
+ }
2317
+ function suggest(word, candidates) {
2318
+ let best;
2319
+ let bestScore = Infinity;
2320
+ for (const c of candidates) {
2321
+ const d = c.startsWith(word) || word.startsWith(c) ? 1 : distance(word, c);
2322
+ if (d < bestScore) [best, bestScore] = [c, d];
2323
+ }
2324
+ return bestScore <= Math.max(2, Math.floor(word.length / 3)) ? best : void 0;
2325
+ }
2326
+ function distance(a, b) {
2327
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i);
2328
+ for (let i = 1; i <= a.length; i++) {
2329
+ let prev = row[0];
2330
+ row[0] = i;
2331
+ for (let j = 1; j <= b.length; j++) {
2332
+ const tmp = row[j];
2333
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
2334
+ prev = tmp;
2335
+ }
2336
+ }
2337
+ return row[b.length];
2338
+ }
2339
+ function parseArgv(spec, tokens) {
2340
+ const flags = spec?.flags ?? {};
2341
+ const input = {};
2342
+ const globals = {};
2343
+ const positional = [];
2344
+ const shorts = /* @__PURE__ */ new Map();
2345
+ for (const [n, f] of Object.entries({ ...GLOBAL_FLAGS, ...flags })) if (f.short) shorts.set(f.short, n);
2346
+ const lookup = (name) => {
2347
+ if (flags[name]) return [flags[name], false];
2348
+ if (GLOBAL_FLAGS[name]) return [GLOBAL_FLAGS[name], true];
2349
+ return void 0;
2350
+ };
2351
+ for (let i = 0; i < tokens.length; i++) {
2352
+ const tok = tokens[i];
2353
+ if (tok === "--") {
2354
+ positional.push(...tokens.slice(i + 1));
2355
+ break;
2356
+ }
2357
+ let name;
2358
+ let inline;
2359
+ if (tok.startsWith("--") && tok.length > 2) {
2360
+ const eq = tok.indexOf("=");
2361
+ [name, inline] = eq === -1 ? [tok.slice(2), void 0] : [tok.slice(2, eq), tok.slice(eq + 1)];
2362
+ } else if (/^-[a-zA-Z]$/.test(tok)) {
2363
+ const long = shorts.get(tok.slice(1));
2364
+ if (!long) throw usageError("unknown_flag", `Unknown flag ${tok}`, "Run with --help to list the flags.");
2365
+ name = long;
2366
+ } else {
2367
+ positional.push(tok);
2368
+ continue;
2369
+ }
2370
+ let found = lookup(name);
2371
+ let negated = false;
2372
+ if (!found && name.startsWith("no-")) {
2373
+ const base = lookup(name.slice(3));
2374
+ if (base && base[0].type === "boolean") [found, negated, name] = [base, true, name.slice(3)];
2375
+ }
2376
+ if (!found) {
2377
+ const close = suggest(name, [...Object.keys(flags), ...Object.keys(GLOBAL_FLAGS)]);
2378
+ throw usageError(
2379
+ "unknown_flag",
2380
+ `Unknown flag --${name}${spec ? ` for \`tracksiy ${spec.path.join(" ")}\`` : ""}`,
2381
+ close ? `Did you mean --${close}?` : "Run with --help to list the flags."
2382
+ );
2383
+ }
2384
+ const [fspec, isGlobal] = found;
2385
+ let raw;
2386
+ if (fspec.type === "boolean") raw = negated ? false : inline ?? true;
2387
+ else if (inline !== void 0) raw = inline;
2388
+ else {
2389
+ const next = tokens[i + 1];
2390
+ if (next === void 0 || next.startsWith("--") && next.length > 2) {
2391
+ throw usageError("missing_value", `--${name} needs a value${fspec.value ? ` ${fspec.value}` : ""}`);
2392
+ }
2393
+ raw = next;
2394
+ i++;
2395
+ }
2396
+ const target = isGlobal ? globals : input;
2397
+ const key = camel(name);
2398
+ target[key] = coerce(name, fspec, raw, target[key]);
2399
+ }
2400
+ assignPositional(spec, positional, input);
2401
+ return { input, globals };
2402
+ }
2403
+ function assignPositional(spec, positional, input) {
2404
+ const args = spec?.args ?? [];
2405
+ let i = 0;
2406
+ for (const arg of args) {
2407
+ const key = camel(arg.name);
2408
+ if (arg.variadic) {
2409
+ if (i < positional.length) input[key] = positional.slice(i);
2410
+ i = positional.length;
2411
+ } else if (i < positional.length) {
2412
+ input[key] = positional[i++];
2413
+ }
2414
+ }
2415
+ if (i < positional.length) {
2416
+ throw usageError(
2417
+ "unexpected_argument",
2418
+ `Unexpected argument ${JSON.stringify(positional[i])}${spec ? ` for \`tracksiy ${spec.path.join(" ")}\`` : ""}`,
2419
+ "Run with --help to see the arguments this command takes."
2420
+ );
2421
+ }
2422
+ }
2423
+ function parseObject(spec, raw) {
2424
+ const input = {};
2425
+ const globals = {};
2426
+ const known = /* @__PURE__ */ new Map();
2427
+ for (const [n, f] of Object.entries(spec.flags ?? {})) known.set(camel(n), [n, f, "input"]);
2428
+ for (const a of spec.args ?? []) {
2429
+ known.set(camel(a.name), [a.name, { type: a.variadic ? "list" : "string", description: a.description }, "input"]);
2430
+ }
2431
+ if (spec.context === "org" || spec.context === "project") known.set("org", ["org", GLOBAL_FLAGS.org, "globals"]);
2432
+ if (spec.context === "project") known.set("project", ["project", GLOBAL_FLAGS.project, "globals"]);
2433
+ for (const [key, value] of Object.entries(raw ?? {})) {
2434
+ if (value === void 0 || value === null) continue;
2435
+ const entry = known.get(key);
2436
+ if (!entry) {
2437
+ const close = suggest(key, [...known.keys()]);
2438
+ throw usageError("unknown_argument", `Unknown argument "${key}"`, close ? `Did you mean "${close}"?` : void 0);
2439
+ }
2440
+ const [name, fspec, where] = entry;
2441
+ (where === "input" ? input : globals)[key] = coerce(name, fspec, value);
2442
+ }
2443
+ return { input, globals };
2444
+ }
2445
+ function finalizeInput(spec, input) {
2446
+ for (const arg of spec.args ?? []) {
2447
+ const key = camel(arg.name);
2448
+ if (arg.required && (input[key] === void 0 || Array.isArray(input[key]) && !input[key].length)) {
2449
+ throw usageError("missing_argument", `Missing <${arg.name}> for \`tracksiy ${spec.path.join(" ")}\`: ${arg.description}`);
2450
+ }
2451
+ }
2452
+ for (const [name, f] of Object.entries(spec.flags ?? {})) {
2453
+ const key = camel(name);
2454
+ if (input[key] === void 0 && f.default !== void 0) input[key] = f.default;
2455
+ if (f.required && input[key] === void 0) {
2456
+ throw usageError("missing_flag", `Missing --${name} for \`tracksiy ${spec.path.join(" ")}\`: ${f.description}`);
2457
+ }
2458
+ }
2459
+ }
2460
+
2461
+ // src/commands/meta.ts
2462
+ var api = {
2463
+ path: ["api"],
2464
+ summary: "Call any endpoint directly, for what no command covers yet. Paths are relative to /api/agent/v1/ unless they start with /.",
2465
+ description: "The response body is returned as `data`. It can do anything the token allows, including deletions.",
2466
+ context: "none",
2467
+ effect: "write",
2468
+ args: [
2469
+ { name: "method", required: true, description: "GET, POST, PUT, PATCH or DELETE." },
2470
+ { name: "path", required: true, description: "e.g. projects/acme/shop/issues, or /api/agent/v1/health." }
2471
+ ],
2472
+ flags: {
2473
+ body: { type: "json", value: "<json|@file|->", description: "JSON request body." },
2474
+ query: { type: "list", value: "<key=value,...>", description: "Query parameters." }
2475
+ },
2476
+ examples: [
2477
+ { command: "tracksiy api GET organizations", description: "Raw organization list." },
2478
+ { command: "tracksiy api PUT projects/acme/shop/alerts --body @alerts.json", description: "Send a prepared body." }
2479
+ ],
2480
+ async run(ctx) {
2481
+ const method = String(ctx.input.method).toUpperCase();
2482
+ if (!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(method)) throw usageError("bad_value", `Unsupported method ${ctx.input.method}`);
2483
+ const query = {};
2484
+ for (const pair of ctx.input.query ?? []) {
2485
+ const eq = pair.indexOf("=");
2486
+ if (eq < 1) throw usageError("bad_value", `--query expects key=value, got ${JSON.stringify(pair)}`);
2487
+ query[pair.slice(0, eq)] = pair.slice(eq + 1);
2488
+ }
2489
+ const path = String(ctx.input.path).replace(/^\/api\/agent\/v1\//, "");
2490
+ const res = await ctx.api.request(method, path, { query, body: ctx.input.body });
2491
+ return res.data;
2492
+ }
2493
+ };
2494
+ function flagSchema(name, f) {
2495
+ const out = { name, input: camel(name), type: f.type, description: f.description };
2496
+ if (f.required) out.required = true;
2497
+ if (f.default !== void 0) out.default = f.default;
2498
+ if (f.enum) out.enum = f.enum;
2499
+ if (f.value) out.value = f.value;
2500
+ return out;
2501
+ }
2502
+ function describeCommand(spec) {
2503
+ return {
2504
+ name: commandName(spec),
2505
+ aliases: (spec.aliases ?? []).map((a) => a.join(" ")),
2506
+ summary: spec.summary,
2507
+ description: spec.description ?? null,
2508
+ usage: `tracksiy ${commandName(spec)}${(spec.args ?? []).map((a) => a.required ? ` <${a.name}${a.variadic ? "..." : ""}>` : ` [${a.name}${a.variadic ? "..." : ""}]`).join("")}${spec.flags ? " [flags]" : ""}`,
2509
+ args: (spec.args ?? []).map((a) => ({ name: a.name, input: camel(a.name), required: Boolean(a.required), variadic: Boolean(a.variadic), description: a.description })),
2510
+ flags: Object.entries(spec.flags ?? {}).map(([n, f]) => flagSchema(n, f)),
2511
+ context: spec.context ?? "none",
2512
+ needsToken: spec.auth !== false,
2513
+ effect: spec.effect,
2514
+ idempotent: Boolean(spec.idempotent),
2515
+ confirm: spec.flags?.yes ? "--yes" : null,
2516
+ endpoint: spec.endpoint ?? null,
2517
+ scopes: spec.scopes ?? null,
2518
+ output: spec.output ?? null,
2519
+ examples: spec.examples ?? [],
2520
+ mcpTool: spec.mcp === false ? null : toolName(spec)
2521
+ };
2522
+ }
2523
+ function contract() {
2524
+ return {
2525
+ output: {
2526
+ json: 'One line on stdout. Success: {"ok":true,"data":<result>}. Failure: {"ok":false,"error":{"code","message","hint?","status?","details?"},"data?":<result of a check that did not pass>}.',
2527
+ whenJson: "Whenever stdout is not a terminal, or with --json / TRACKSIY_FORMAT=json.",
2528
+ text: "On a terminal: a readable view on stdout, errors on stderr.",
2529
+ progress: "Progress messages go to stderr only; --quiet silences them.",
2530
+ select: "--select a,b.c keeps only those fields of data; lists are mapped element-wise."
2531
+ },
2532
+ exitCodes: EXIT_MEANINGS,
2533
+ errorCodes: {
2534
+ "usage.*": "bad command line; nothing was sent",
2535
+ "confirmation.required": "destructive command without --yes; nothing was changed",
2536
+ "auth.missing_token": "no token configured",
2537
+ "auth.unauthenticated": "token rejected (expired, revoked, other server)",
2538
+ "auth.forbidden": "token lacks a scope or the project is outside its list",
2539
+ not_found: "organization, project or object does not exist (or is hidden from this token)",
2540
+ "request.invalid": "server rejected the body or parameters; see details",
2541
+ "request.conflict": "conflicts with current state",
2542
+ rate_limited: "slow down; details.retryAfterSeconds",
2543
+ "network.timeout": "no answer in time",
2544
+ "network.unreachable": "server not reachable",
2545
+ "server.error": "5xx",
2546
+ "server.unsupported": "the server is older than this CLI",
2547
+ "check.failed": "the command's check did not pass; data holds the report",
2548
+ "upload.failed": "artifact assembly failed",
2549
+ "upload.pending": "assembly still running after --wait"
2550
+ },
2551
+ environment: {
2552
+ TRACKSIY_TOKEN: "API token (preferred over --token)",
2553
+ TRACKSIY_URL: `server root (default ${DEFAULT_URL})`,
2554
+ TRACKSIY_ORG: "default organization slug",
2555
+ TRACKSIY_PROJECT: "default project slug, or org/project",
2556
+ TRACKSIY_FORMAT: "json or text",
2557
+ TRACKSIY_TIMEOUT: "seconds per request (default 30)",
2558
+ TRACKSIY_CONFIG: "path of the saved-login file"
2559
+ },
2560
+ precedence: `flag > environment > ${PROJECT_FILE} (searched upward from the working directory) > saved login > default. When org or project is still unknown and the token can see exactly one, that one is used.`,
2561
+ projectFile: `${PROJECT_FILE}: {"org": "...", "project": "...", "url?": "..."}. Never holds a token. Written by \`tracksiy init\`.`,
2562
+ writes: "Every write is a declaration: re-running it is safe and reports the stored state. Destructive commands require --yes. Bulk changes are dry runs unless --apply.",
2563
+ mcp: "`tracksiy mcp` serves every command (except login/logout/mcp/describe) as an MCP tool over stdio. Tool names are the command path joined by underscores, e.g. issues_list."
2564
+ };
2565
+ }
2566
+ var describe = {
2567
+ path: ["describe"],
2568
+ aliases: [["schema"]],
2569
+ summary: "Machine-readable description of every command, flag, exit code and error code. Start here if you are an agent.",
2570
+ context: "none",
2571
+ auth: false,
2572
+ effect: "read",
2573
+ mcp: false,
2574
+ args: [{ name: "command", variadic: true, description: "Describe only this command, e.g. `issues list`." }],
2575
+ output: "{name, version, contract{output, exitCodes, errorCodes, environment, precedence, projectFile, writes, mcp}, globalFlags[], commands[]}",
2576
+ text: (d) => JSON.stringify(d, null, 2),
2577
+ async run(ctx) {
2578
+ const words = ctx.input.command ?? [];
2579
+ if (words.length) {
2580
+ const key = words.join(" ");
2581
+ const spec = COMMANDS.find((c) => commandName(c) === key || (c.aliases ?? []).some((a) => a.join(" ") === key));
2582
+ if (!spec) throw usageError("unknown_command", `No command \`${key}\`.`, "Run `tracksiy describe` for the full list.");
2583
+ return describeCommand(spec);
2584
+ }
2585
+ return {
2586
+ name: "tracksiy",
2587
+ version: VERSION,
2588
+ summary: "Agent-first CLI and MCP server for Tracksiy: errors, uptime, alerts, releases and source maps from one token.",
2589
+ docs: `${DEFAULT_URL}/cli.md`,
2590
+ contract: contract(),
2591
+ globalFlags: Object.entries(GLOBAL_FLAGS).map(([n, f]) => flagSchema(n, f)),
2592
+ commands: COMMANDS.map(describeCommand)
2593
+ };
2594
+ }
2595
+ };
2596
+ var meta_default = [api, describe];
2597
+
2598
+ // src/mcp.ts
2599
+ import { createInterface } from "node:readline";
2600
+
2601
+ // src/core/run.ts
2602
+ function makeContext(spec, input, globals, surface, io) {
2603
+ const config = resolveConfig(globals, io.env, io.cwd);
2604
+ const quiet = config.quiet || surface === "mcp";
2605
+ const log = (message) => {
2606
+ if (!quiet || surface === "mcp" && config.verbose) io.stderr(`${message}
2607
+ `);
2608
+ };
2609
+ const api2 = new Api({ url: config.url, token: config.token, timeoutMs: config.timeoutMs, verbose: config.verbose, log, fetch: io.fetch });
2610
+ let orgs2;
2611
+ const visible = () => {
2612
+ orgs2 ??= api2.get("organizations").catch((err) => {
2613
+ if (err instanceof CliError && (err.code === "server.unsupported" || err.code === "auth.forbidden")) {
2614
+ throw usageError(
2615
+ "missing_org",
2616
+ err.code === "auth.forbidden" ? "No organization given, and this token may not list them." : "No organization given and this server cannot list them.",
2617
+ "Pass --org <slug> and --project <slug> (or --project org/slug), set TRACKSIY_ORG / TRACKSIY_PROJECT, or commit the .tracksiy.json that `tracksiy init` writes."
2618
+ );
2619
+ }
2620
+ throw err;
2621
+ });
2622
+ return orgs2;
2623
+ };
2624
+ let orgPromise;
2625
+ const org = () => {
2626
+ orgPromise ??= (async () => {
2627
+ if (config.org) return config.org;
2628
+ const list5 = await visible();
2629
+ if (list5.length === 1) {
2630
+ log(`using organization ${list5[0].slug}`);
2631
+ return list5[0].slug;
2632
+ }
2633
+ throw usageError(
2634
+ "missing_org",
2635
+ list5.length ? "Which organization? This token can see several." : "This token cannot see any organization.",
2636
+ list5.length ? `Pass --org <slug>, set TRACKSIY_ORG, or add "org" to ${".tracksiy.json"}. Visible: ${list5.map((o) => o.slug).join(", ")}` : "Check the token with `tracksiy auth status`."
2637
+ );
2638
+ })();
2639
+ return orgPromise;
2640
+ };
2641
+ let projectPromise;
2642
+ const project2 = () => {
2643
+ projectPromise ??= (async () => {
2644
+ const o = await org();
2645
+ if (config.project) return { org: o, project: config.project };
2646
+ const list5 = await visible();
2647
+ const projects2 = list5.find((x) => x.slug === o)?.projects ?? [];
2648
+ if (projects2.length === 1) {
2649
+ log(`using project ${o}/${projects2[0].slug}`);
2650
+ return { org: o, project: projects2[0].slug };
2651
+ }
2652
+ throw usageError(
2653
+ "missing_project",
2654
+ "Which project?",
2655
+ `Pass --project <slug>, set TRACKSIY_PROJECT, or run \`tracksiy init\` here to write .tracksiy.json.${projects2.length ? ` Projects in ${o}: ${projects2.slice(0, 30).map((p) => p.slug).join(", ")}` : ""}`
2656
+ );
2657
+ })();
2658
+ return projectPromise;
2659
+ };
2660
+ return { input, api: api2, config, org, project: project2, log, surface };
2661
+ }
2662
+ async function execute(spec, input, globals, surface, io) {
2663
+ finalizeInput(spec, input);
2664
+ const ctx = makeContext(spec, input, globals, surface, io);
2665
+ if (spec.auth !== false && !ctx.config.token) throw missingToken(ctx.config.url);
2666
+ return spec.run(ctx);
2667
+ }
2668
+
2669
+ // src/mcp.ts
2670
+ var PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
2671
+ var INSTRUCTIONS = `Tracksiy: error tracking, uptime monitoring, alerting, releases and source maps for the apps of an organization.
2672
+
2673
+ Orient first: auth_status (who am I, which org/projects), then overview (the whole fleet: issues, volume trend, silent projects, monitors down, projects nobody is alerted for).
2674
+ Dig in: issues_list -> issues_view <id> gives the stack frames, latest event and tags needed to fix something. changes with the returned cursor is the cheap way to poll for anything new.
2675
+ Act: issues_resolve / issues_ignore; alerts_set then alerts_test to prove someone is told; monitors_declare for jobs and URLs; releases_declare and sourcemaps_upload from a deploy.
2676
+ Verify, don't assume: projects_verify reports whether an integration really works; health says whether the platform itself can be believed.
2677
+ If Tracksiy itself gets in your way -- a bug, a missing capability, an error you cannot interpret, or you are blocked -- tell the Tracksiy team: open a PeerPacket ticket for tracksiy-agent in the tracksiy-support channel (pp ticket new tracksiy-support "<problem>" --to tracksiy-agent --label bug -f report.md). The support tool returns the exact commands.
2678
+
2679
+ Every tool takes optional "select" (dot paths) to return only the fields you need -- use it to keep results small. org/project default to TRACKSIY_ORG / TRACKSIY_PROJECT or to the only one the token can see.
2680
+ Writes are idempotent declarations. Destructive tools need "yes": true. issues_bulk_status is a dry run unless "apply": true.
2681
+ Errors come back with isError and a JSON body {error:{code, message, hint}}; the hint says what to do next.`;
2682
+ function schemaFor(name, f) {
2683
+ const base = { description: f.description };
2684
+ switch (f.type) {
2685
+ case "boolean":
2686
+ return { type: "boolean", ...base };
2687
+ case "integer":
2688
+ return { type: "integer", ...base };
2689
+ case "number":
2690
+ return { type: "number", ...base };
2691
+ case "list":
2692
+ return { type: "array", items: f.enum ? { type: "string", enum: f.enum } : { type: "string" }, ...base };
2693
+ case "json":
2694
+ return { type: "object", ...base, description: `${f.description} (a JSON object)` };
2695
+ default:
2696
+ return f.enum ? { type: "string", enum: f.enum, ...base } : { type: "string", ...base };
2697
+ }
2698
+ }
2699
+ function inputSchema(spec) {
2700
+ const properties = {};
2701
+ const required = [];
2702
+ for (const a of spec.args ?? []) {
2703
+ properties[camel(a.name)] = a.variadic ? { type: "array", items: { type: "string" }, description: a.description } : { type: "string", description: a.description };
2704
+ if (a.required) required.push(camel(a.name));
2705
+ }
2706
+ for (const [n, f] of Object.entries(spec.flags ?? {})) {
2707
+ properties[camel(n)] = schemaFor(n, f);
2708
+ if (f.required) required.push(camel(n));
2709
+ }
2710
+ if (spec.context === "org" || spec.context === "project") {
2711
+ properties.org = { type: "string", description: "Organization slug (default: TRACKSIY_ORG, .tracksiy.json, or the only one visible)." };
2712
+ }
2713
+ if (spec.context === "project") {
2714
+ properties.project = { type: "string", description: "Project slug or org/project (default: TRACKSIY_PROJECT, .tracksiy.json, or the only one visible)." };
2715
+ }
2716
+ properties.select = { type: "array", items: { type: "string" }, description: "Return only these fields of the result (dot paths, e.g. issues.id). Use it to keep the result small." };
2717
+ return { type: "object", properties, required, additionalProperties: false };
2718
+ }
2719
+ function toolFor(spec) {
2720
+ const parts = [spec.summary];
2721
+ if (spec.description) parts.push(spec.description);
2722
+ if (spec.output) parts.push(`Returns: ${spec.output}`);
2723
+ if (spec.flags?.yes) parts.push('Destructive: pass "yes": true to confirm.');
2724
+ const title = `tracksiy ${commandName(spec)}`;
2725
+ return {
2726
+ name: toolName(spec),
2727
+ title,
2728
+ description: parts.join("\n\n"),
2729
+ inputSchema: inputSchema(spec),
2730
+ annotations: {
2731
+ title,
2732
+ readOnlyHint: spec.effect === "read",
2733
+ destructiveHint: spec.effect === "destructive" || spec.path[0] === "api",
2734
+ idempotentHint: Boolean(spec.idempotent),
2735
+ openWorldHint: true
2736
+ }
2737
+ };
2738
+ }
2739
+ function mcpTools() {
2740
+ return COMMANDS.filter((c) => c.mcp !== false);
2741
+ }
2742
+ var McpServer = class {
2743
+ protocol = PROTOCOL_VERSIONS[0];
2744
+ tools;
2745
+ io;
2746
+ defaults;
2747
+ get toolCount() {
2748
+ return this.tools.size;
2749
+ }
2750
+ constructor(io, defaults2 = {}) {
2751
+ this.io = io;
2752
+ this.defaults = defaults2;
2753
+ this.tools = new Map(mcpTools().map((t) => [toolName(t), t]));
2754
+ }
2755
+ /** Returns the response to send, or undefined for notifications and responses. */
2756
+ async handle(msg) {
2757
+ if (!msg || typeof msg !== "object" || !msg.method) return void 0;
2758
+ const isRequest = msg.id !== void 0 && msg.id !== null;
2759
+ if (!isRequest) return void 0;
2760
+ const ok = (result) => ({ jsonrpc: "2.0", id: msg.id, result });
2761
+ const fail = (code, message) => ({ jsonrpc: "2.0", id: msg.id, error: { code, message } });
2762
+ switch (msg.method) {
2763
+ case "initialize": {
2764
+ const requested = msg.params?.protocolVersion;
2765
+ this.protocol = PROTOCOL_VERSIONS.includes(requested) ? requested : PROTOCOL_VERSIONS[0];
2766
+ return ok({
2767
+ protocolVersion: this.protocol,
2768
+ capabilities: { tools: { listChanged: false } },
2769
+ serverInfo: { name: "tracksiy", title: "Tracksiy", version: VERSION },
2770
+ instructions: INSTRUCTIONS
2771
+ });
2772
+ }
2773
+ case "ping":
2774
+ return ok({});
2775
+ case "tools/list":
2776
+ return ok({ tools: [...this.tools.values()].map(toolFor) });
2777
+ case "tools/call":
2778
+ return ok(await this.call(msg.params?.name, msg.params?.arguments ?? {}));
2779
+ default:
2780
+ return fail(-32601, `Method not found: ${msg.method}`);
2781
+ }
2782
+ }
2783
+ async call(name, args) {
2784
+ const spec = this.tools.get(name);
2785
+ const text = (value) => [{ type: "text", text: JSON.stringify(value ?? null) }];
2786
+ if (!spec) {
2787
+ return { content: text({ error: { code: "usage.unknown_tool", message: `No tool named ${name}.` } }), isError: true };
2788
+ }
2789
+ try {
2790
+ const { select, ...rest } = args ?? {};
2791
+ for (const [n, f] of Object.entries(spec.flags ?? {})) {
2792
+ if (f.type === "json" && rest[camel(n)] === "-") throw usageError("bad_value", `"${camel(n)}" cannot be read from stdin here; pass the JSON object itself.`);
2793
+ }
2794
+ const parsed = parseObject(spec, rest);
2795
+ const data = await execute(spec, parsed.input, { ...this.defaults, ...parsed.globals }, "mcp", this.io);
2796
+ return { content: text(selectFields(data, Array.isArray(select) ? select : typeof select === "string" && select.trim() ? select.split(",").map((x) => x.trim()).filter(Boolean) : void 0)), isError: false };
2797
+ } catch (err) {
2798
+ const e = toCliError(err);
2799
+ const body = { error: e.toJSON() };
2800
+ if (e instanceof CheckFailed) body.data = e.data;
2801
+ return { content: text(body), isError: true };
2802
+ }
2803
+ }
2804
+ };
2805
+ async function serve(io, defaults2) {
2806
+ const server = new McpServer(io, defaults2);
2807
+ const write = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
2808
+ const pending = /* @__PURE__ */ new Set();
2809
+ const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
2810
+ io.stderr(`tracksiy mcp ${VERSION}: ${server.toolCount} tools on stdio
2811
+ `);
2812
+ for await (const line of rl) {
2813
+ if (!line.trim()) continue;
2814
+ let msg;
2815
+ try {
2816
+ msg = JSON.parse(line);
2817
+ } catch {
2818
+ write({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
2819
+ continue;
2820
+ }
2821
+ const job = (async () => {
2822
+ if (Array.isArray(msg)) {
2823
+ const out = (await Promise.all(msg.map((m) => server.handle(m)))).filter(Boolean);
2824
+ if (out.length) write(out);
2825
+ } else {
2826
+ const out = await server.handle(msg);
2827
+ if (out) write(out);
2828
+ }
2829
+ })().catch((err) => io.stderr(`tracksiy mcp: ${err.stack}
2830
+ `));
2831
+ pending.add(job);
2832
+ job.finally(() => pending.delete(job));
2833
+ }
2834
+ await Promise.all(pending);
2835
+ }
2836
+ var SERVED = Symbol("served");
2837
+ var mcpCommand = {
2838
+ path: ["mcp"],
2839
+ summary: "Run as an MCP server on stdio: every command becomes a tool for Claude, Cursor or any MCP client.",
2840
+ description: "Configure the token with TRACKSIY_TOKEN in the client's server config (and optionally TRACKSIY_ORG / TRACKSIY_PROJECT / TRACKSIY_URL). --org/--project/--url given here become defaults for every tool call.",
2841
+ context: "none",
2842
+ auth: false,
2843
+ effect: "read",
2844
+ mcp: false,
2845
+ examples: [
2846
+ { command: "claude mcp add tracksiy -e TRACKSIY_TOKEN=<token> -- npx -y tracksiy mcp", description: "Add it to Claude Code." },
2847
+ { command: '{"mcpServers":{"tracksiy":{"command":"npx","args":["-y","tracksiy","mcp"],"env":{"TRACKSIY_TOKEN":"<token>"}}}}', description: "The JSON most MCP clients take." }
2848
+ ],
2849
+ async run(ctx) {
2850
+ const defaults2 = {};
2851
+ if (ctx.config.urlSource === "--url") defaults2.url = ctx.config.url;
2852
+ if (ctx.config.orgSource === "--org") defaults2.org = ctx.config.org;
2853
+ if (ctx.config.projectSource === "--project") defaults2.project = ctx.config.project;
2854
+ if (ctx.config.tokenSource === "--token") defaults2.token = ctx.config.token;
2855
+ if (ctx.config.verbose) defaults2.verbose = true;
2856
+ await serve({ env: process.env, cwd: process.cwd(), stderr: (s) => process.stderr.write(s) }, defaults2);
2857
+ return SERVED;
2858
+ }
2859
+ };
2860
+ var mcp_default = [mcpCommand];
2861
+
2862
+ // src/registry.ts
2863
+ var COMMANDS = [
2864
+ ...init_default,
2865
+ ...auth_default,
2866
+ ...fleet_default,
2867
+ ...issues_default,
2868
+ ...projects_default,
2869
+ ...alerts_default,
2870
+ ...keys_default,
2871
+ ...releases_default,
2872
+ ...artifacts_default,
2873
+ ...tokens_default,
2874
+ ...support_default,
2875
+ ...meta_default,
2876
+ ...mcp_default
2877
+ ];
2878
+ var GROUPS = [
2879
+ ["Start", "init auth onboarding describe"],
2880
+ ["Look", "overview changes health orgs projects issues monitors performance releases alerts keys artifacts"],
2881
+ ["Ship", "sourcemaps proguard"],
2882
+ ["Manage", "tokens"],
2883
+ ["Talk to us", "support"],
2884
+ ["Other", "api mcp"]
2885
+ ];
2886
+
2887
+ // src/core/help.ts
2888
+ function flagLine(name, f) {
2889
+ const value = f.type === "boolean" ? "" : ` ${f.value ?? (f.enum ? `<${f.enum.join("|")}>` : `<${f.type}>`)}`;
2890
+ const left = `${f.short ? `-${f.short}, ` : " "}--${name}${value}`;
2891
+ const extra = [f.required ? "required" : "", f.default !== void 0 ? `default ${JSON.stringify(f.default)}` : "", f.enum && f.value ? `one of ${f.enum.join(", ")}` : ""].filter(Boolean).join("; ");
2892
+ return { left, right: `${f.description}${extra ? ` (${extra})` : ""}` };
2893
+ }
2894
+ function columns(rows, indent = " ") {
2895
+ const pad = Math.min(38, Math.max(...rows.map((r) => r.left.length)));
2896
+ return rows.map((r) => `${indent}${r.left.padEnd(pad)} ${r.right}`).join("\n");
2897
+ }
2898
+ function commandHelp(spec) {
2899
+ const usage = `tracksiy ${commandName(spec)}${(spec.args ?? []).map((a) => a.required ? ` <${a.name}${a.variadic ? "..." : ""}>` : ` [${a.name}${a.variadic ? "..." : ""}]`).join("")}${spec.flags ? " [flags]" : ""}`;
2900
+ const out = [`${spec.summary}`, "", `Usage: ${usage}`];
2901
+ if (spec.description) out.push("", spec.description);
2902
+ if (spec.args?.length) out.push("", "Arguments:", columns(spec.args.map((a) => ({ left: `<${a.name}>`, right: a.description }))));
2903
+ if (spec.flags && Object.keys(spec.flags).length) out.push("", "Flags:", columns(Object.entries(spec.flags).map(([n, f]) => flagLine(n, f))));
2904
+ const ctxFlags = ["org", "project"].filter((n) => n === "org" && (spec.context === "org" || spec.context === "project") || n === "project" && spec.context === "project");
2905
+ out.push("", "Global flags:", columns([...ctxFlags, "json", "select", "url", "quiet", "verbose"].map((n) => flagLine(n, GLOBAL_FLAGS[n]))));
2906
+ if (spec.examples?.length) out.push("", "Examples:", spec.examples.map((e) => ` # ${e.description}
2907
+ $ ${e.command}`).join("\n\n"));
2908
+ const facts = [
2909
+ `effect: ${spec.effect}${spec.idempotent ? ", idempotent" : ""}`,
2910
+ spec.endpoint ? `calls: ${spec.endpoint.method} ${spec.endpoint.path}` : "",
2911
+ spec.output ? `data: ${spec.output}` : "",
2912
+ spec.mcp === false ? "" : `MCP tool: ${commandName(spec).replace(/[ -]/g, "_")}`
2913
+ ].filter(Boolean);
2914
+ out.push("", facts.join("\n"));
2915
+ return out.join("\n") + "\n";
2916
+ }
2917
+ function groupHelp(group) {
2918
+ const cmds = COMMANDS.filter((c) => c.path[0] === group);
2919
+ return `Commands under \`tracksiy ${group}\`:
2920
+
2921
+ ${columns(cmds.map((c) => ({ left: commandName(c), right: c.summary })))}
2922
+
2923
+ Run \`tracksiy ${group} <command> --help\` for details.
2924
+ `;
2925
+ }
2926
+ function rootHelp() {
2927
+ const sections = GROUPS.map(([title, words]) => {
2928
+ const set3 = words.split(" ");
2929
+ const cmds = COMMANDS.filter((c) => set3.includes(c.path[0])).sort((a, b) => set3.indexOf(a.path[0]) - set3.indexOf(b.path[0]));
2930
+ return `${title}:
2931
+ ${columns(cmds.map((c) => ({ left: commandName(c), right: c.summary.length > 90 ? c.summary.slice(0, 88) + "\u2026" : c.summary })))}`;
2932
+ });
2933
+ return `tracksiy ${VERSION} -- errors, uptime, alerts, releases and source maps, from one token. Built for agents first.
2934
+
2935
+ Usage: tracksiy <command> [flags]
2936
+
2937
+ ${sections.join("\n\n")}
2938
+
2939
+ Global flags:
2940
+ ${columns(Object.entries(GLOBAL_FLAGS).map(([n, f]) => flagLine(n, f)))}
2941
+
2942
+ Output is JSON whenever stdout is not a terminal: {"ok":true,"data":...} or {"ok":false,"error":{"code","message","hint"}}.
2943
+ Exit codes: 0 ok, 2 usage, 3 auth, 4 not found, 5 invalid, 6 rate limited, 7 network, 8 server, 9 server too old, 10 check failed.
2944
+ Agents: \`tracksiy describe\` prints every command as JSON. \`tracksiy mcp\` serves them as MCP tools.
2945
+ Docs: https://tracksiy.com/cli.md
2946
+ `;
2947
+ }
2948
+
2949
+ // src/main.ts
2950
+ function resolveCommand(tokens) {
2951
+ const wordIdx = [];
2952
+ for (let i = 0; i < tokens.length; i++) {
2953
+ const t = tokens[i];
2954
+ if (t === "--") break;
2955
+ if (t.startsWith("-")) {
2956
+ const name = t.replace(/^--?/, "").split("=")[0];
2957
+ const f = GLOBAL_FLAGS[name];
2958
+ if (f && f.type !== "boolean" && !t.includes("=")) i++;
2959
+ if (wordIdx.length) break;
2960
+ continue;
2961
+ }
2962
+ wordIdx.push(i);
2963
+ }
2964
+ const words = wordIdx.map((i) => tokens[i]);
2965
+ let best;
2966
+ for (const spec of COMMANDS) {
2967
+ for (const path of [spec.path, ...spec.aliases ?? []]) {
2968
+ if (path.length <= words.length && path.every((w, i) => words[i] === w) && (!best || path.length > best.len)) best = { spec, len: path.length };
2969
+ }
2970
+ }
2971
+ const strip = (n) => tokens.filter((_, i) => !wordIdx.slice(0, n).includes(i));
2972
+ const flagsOnly = strip(wordIdx.length);
2973
+ if (best) return { spec: best.spec, len: best.len, rest: strip(best.len), words, flagsOnly };
2974
+ const group = words[0] && COMMANDS.some((c) => c.path[0] === words[0]) ? words[0] : void 0;
2975
+ return { group, len: 0, rest: strip(group ? 1 : 0), words, flagsOnly };
2976
+ }
2977
+ async function main(argv, io) {
2978
+ const w = io.writer;
2979
+ let format = resolveFormat({}, io.env, io.isTTY);
2980
+ let spec;
2981
+ let input = {};
2982
+ let select;
2983
+ try {
2984
+ if (argv[0] === "--version" || argv[0] === "-V" || argv[0] === "version") {
2985
+ if (format === "json" || argv.includes("--json")) w.stdout(JSON.stringify({ ok: true, data: { version: VERSION } }) + "\n");
2986
+ else w.stdout(`${VERSION}
2987
+ `);
2988
+ return EXIT.ok;
2989
+ }
2990
+ if (argv[0] === "help") argv = [...argv.slice(1), "--help"];
2991
+ const match = resolveCommand(argv);
2992
+ spec = match.spec;
2993
+ if (spec && match.words.length > match.len && !spec.args?.length && COMMANDS.some((c) => c.path.length > 1 && c.path[0] === match.words[0])) {
2994
+ format = resolveFormat(parseArgv(void 0, match.flagsOnly).globals, io.env, io.isTTY);
2995
+ const sub = match.words.slice(1, match.len + 1).join(" ");
2996
+ const options = COMMANDS.filter((c) => c.path[0] === match.words[0]).map((c) => c.path.slice(1).join(" ")).filter(Boolean);
2997
+ const close = suggest(sub, options);
2998
+ throw usageError("unknown_command", `Unknown command \`tracksiy ${match.words.slice(0, match.len + 1).join(" ")}\``, close ? `Did you mean \`tracksiy ${match.words[0]} ${close}\`?` : `Run \`tracksiy ${match.words[0]} --help\`.`);
2999
+ }
3000
+ const parsed = parseArgv(spec, spec ? match.rest : match.flagsOnly);
3001
+ input = parsed.input;
3002
+ format = resolveFormat(parsed.globals, io.env, io.isTTY);
3003
+ select = parsed.globals.select;
3004
+ if (!spec) {
3005
+ if (match.group) {
3006
+ if (match.words.length > 1) {
3007
+ const options = COMMANDS.filter((c) => c.path[0] === match.group).map((c) => c.path.slice(1).join(" "));
3008
+ const close = suggest(match.words[1], options);
3009
+ throw usageError("unknown_command", `Unknown command \`tracksiy ${match.words.slice(0, 2).join(" ")}\``, close ? `Did you mean \`tracksiy ${match.group} ${close}\`?` : `Run \`tracksiy ${match.group} --help\`.`);
3010
+ }
3011
+ w.stdout(groupHelp(match.group));
3012
+ return EXIT.ok;
3013
+ }
3014
+ if (match.words.length) {
3015
+ const names = [...new Set(COMMANDS.flatMap((c) => [commandName(c), c.path[0], ...(c.aliases ?? []).map((a) => a.join(" "))]))];
3016
+ const close = suggest(match.words.join(" "), names) ?? suggest(match.words[0], names);
3017
+ throw usageError("unknown_command", `Unknown command \`tracksiy ${match.words.join(" ")}\``, close ? `Did you mean \`tracksiy ${close}\`?` : "Run `tracksiy --help`, or `tracksiy describe` for JSON.");
3018
+ }
3019
+ w.stdout(rootHelp());
3020
+ return EXIT.ok;
3021
+ }
3022
+ if (parsed.globals.help) {
3023
+ w.stdout(commandHelp(spec));
3024
+ return EXIT.ok;
3025
+ }
3026
+ const data = await execute(spec, input, parsed.globals, "cli", io);
3027
+ if (data === SERVED) return EXIT.ok;
3028
+ printSuccess(w, format, data, spec, input, parsed.globals.select);
3029
+ return EXIT.ok;
3030
+ } catch (err) {
3031
+ const e = toCliError(err);
3032
+ printError(w, format, e, spec, input, select);
3033
+ return e.exit;
3034
+ }
3035
+ }
3036
+
3037
+ // src/bin.ts
3038
+ main(process.argv.slice(2), {
3039
+ env: process.env,
3040
+ cwd: process.cwd(),
3041
+ isTTY: Boolean(process.stdout.isTTY),
3042
+ writer: processWriter,
3043
+ stderr: (s) => process.stderr.write(s)
3044
+ }).then(
3045
+ (code) => {
3046
+ process.exitCode = code;
3047
+ },
3048
+ (err) => {
3049
+ process.stderr.write(`tracksiy: ${err?.stack ?? err}
3050
+ `);
3051
+ process.exitCode = 1;
3052
+ }
3053
+ );