tracksiy 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -143,6 +143,13 @@ Labels: `bug`, `feature`, `question`, `blocker`. `npx tracksiy support` prints t
143
143
  | `logs patterns` | The loudest log lines, counted across their values (message template, or the body with numbers and ids blanked). | read | `logs_patterns` |
144
144
  | `logs stats` | Log counts per level per minute, hour or day. | read | `logs_stats` |
145
145
  | `issues logs <id>` | What the app logged around an error: the logs in the traces of the issue's last events. | read | `issues_logs` |
146
+ | `user-feedback` | What users wrote through the feedback widget or API: message, page or screen, release, device, the issue it is about and its screenshots. | read | `user_feedback` |
147
+ | `issues attachments <id>` | Screenshots, view hierarchies and files of an issue's recent events and of the feedback about it. | read | `issues_attachments` |
148
+ | `attachments get <id>` | Save one attachment (a screenshot, a view hierarchy) to a file -- or print a JSON one. | read | `attachments_get` |
149
+ | `issues flags <id>` | Feature flag values of an issue's events against the rest of the project; `suspect` marks a value far more common in this issue. | read | `issues_flags` |
150
+ | `build upload <path>` | Measure an app build (APK, AAB, IPA, .xcarchive, .app) on this machine and record its size: per category, the largest files, growth against the previous build, size budgets. The binary is not uploaded. | write | `build_upload` |
151
+ | `build list` | Recorded builds newest first, with their growth against the build before. | read | `build_list` |
152
+ | `build view <id>` | One build: size per category, the largest files, what changed against the previous build. | read | `build_view` |
146
153
  | `issues list` | Recent issues of a project, newest activity first, with the frames that matter inlined. | read | `issues_list` |
147
154
  | `issues view <id>` | One issue in enough depth to act on: latest event, stack frames, tags, recent volume. | read | `issues_view` |
148
155
  | `issues resolve <ids...>` | Mark issues resolved, optionally fixed in a release so old clients do not reopen them. Records why. | write | `issues_resolve` |
package/dist/tracksiy.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  import { readFileSync as readFileSync2 } from "node:fs";
5
5
 
6
6
  // src/version.ts
7
- var VERSION = true ? "0.11.0" : "0.0.0-dev";
7
+ var VERSION = true ? "0.13.0" : "0.0.0-dev";
8
8
  var USER_AGENT = `tracksiy-cli/${VERSION} (node ${process.versions.node}; ${process.platform})`;
9
9
 
10
10
  // src/core/errors.ts
@@ -149,7 +149,7 @@ var Api = class {
149
149
  continue;
150
150
  }
151
151
  }
152
- const data = await readBody(res);
152
+ const data = opts.binary && res.ok ? new Uint8Array(await res.arrayBuffer()) : await readBody(res);
153
153
  const ok = opts.accept ? opts.accept.includes(res.status) : res.ok;
154
154
  if (ok) return { status: res.status, data, headers: res.headers };
155
155
  throw await this.httpError(method, path, res, data);
@@ -1050,7 +1050,7 @@ var settingsGet = {
1050
1050
  context: "project",
1051
1051
  effect: "read",
1052
1052
  endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/settings" },
1053
- output: "{sourceRoots[], environments[], environmentPolicy, fingerprintRules[], privacyRules[], performanceIssues, storage{maxEventsPerIssue, sourcemapKeepReleases, sourcemapKeepUsedDays}, releaseHealth{crashFreeSessionsMin, minSessions, maxDropPct, velocityPct, velocityMinEvents}, logs{enabled, minLevel, dailyLimit}}",
1053
+ output: "{sourceRoots[], environments[], environmentPolicy, fingerprintRules[], privacyRules[], performanceIssues, storage{maxEventsPerIssue, sourcemapKeepReleases, sourcemapKeepUsedDays}, releaseHealth{crashFreeSessionsMin, minSessions, maxDropPct, velocityPct, velocityMinEvents}, logs{enabled, minLevel, dailyLimit}, attachments{store, maxKb, quotaMb, retentionDays}}",
1054
1054
  async run(ctx) {
1055
1055
  return ctx.api.get(await projectPath(ctx, "settings"));
1056
1056
  }
@@ -1093,6 +1093,10 @@ var settingsSet = {
1093
1093
  logs: { type: "boolean", description: "Store structured logs at all (default on). --no-logs drops them, counted as log:disabled." },
1094
1094
  "logs-min-level": { type: "string", enum: ["trace", "debug", "info", "warn", "error", "fatal"], description: "Drop log lines below this level at ingest (default trace)." },
1095
1095
  "logs-daily-limit": { type: "integer", value: "<n>", description: "At most this many log lines a day (default 100000); the rest are dropped and counted as log:daily_limit." },
1096
+ attachments: { type: "string", enum: ["off", "feedback", "all"], description: "Which attachments are kept: off, feedback (default: only what a user sent with their feedback), all (also crash screenshots, view hierarchies, files)." },
1097
+ "attachment-max-kb": { type: "integer", value: "<kb>", description: "Largest attachment kept (default 5120)." },
1098
+ "attachments-quota-mb": { type: "integer", value: "<mb>", description: "The project's attachment budget (default 500); past it new files are dropped (attachment:quota)." },
1099
+ "attachments-retention-days": { type: "integer", value: "<days>", description: "Keep attachments this long (default 30)." },
1096
1100
  body: { type: "json", value: "<json|@file|->", description: "The whole declaration; overrides the flags." }
1097
1101
  },
1098
1102
  examples: [
@@ -1102,7 +1106,7 @@ var settingsSet = {
1102
1106
  { command: "tracksiy settings set --source-root 'strip=/var/www/shop/,prepend=backend/,surface=backend'", description: "The server checkout is only the repo's backend/ directory." },
1103
1107
  { command: "tracksiy settings set --max-events-per-issue 500 --sourcemap-keep-releases 5", description: "A noisy project keeps less on disk." }
1104
1108
  ],
1105
- output: "{sourceRoots[], environments[], environmentPolicy, fingerprintRules[], privacyRules[], performanceIssues, storage{maxEventsPerIssue, sourcemapKeepReleases, sourcemapKeepUsedDays}, releaseHealth{crashFreeSessionsMin, minSessions, maxDropPct, velocityPct, velocityMinEvents}, logs{enabled, minLevel, dailyLimit}}",
1109
+ output: "{sourceRoots[], environments[], environmentPolicy, fingerprintRules[], privacyRules[], performanceIssues, storage{maxEventsPerIssue, sourcemapKeepReleases, sourcemapKeepUsedDays}, releaseHealth{crashFreeSessionsMin, minSessions, maxDropPct, velocityPct, velocityMinEvents}, logs{enabled, minLevel, dailyLimit}, attachments{store, maxKb, quotaMb, retentionDays}}",
1106
1110
  async run(ctx) {
1107
1111
  const path = await projectPath(ctx, "settings");
1108
1112
  const i = ctx.input;
@@ -1127,6 +1131,13 @@ var settingsSet = {
1127
1131
  ...i.logsMinLevel !== void 0 ? { minLevel: i.logsMinLevel } : {},
1128
1132
  ...i.logsDailyLimit !== void 0 ? { dailyLimit: i.logsDailyLimit } : {}
1129
1133
  },
1134
+ attachments: {
1135
+ ...current.attachments ?? {},
1136
+ ...i.attachments !== void 0 ? { store: i.attachments } : {},
1137
+ ...i.attachmentMaxKb !== void 0 ? { maxKb: i.attachmentMaxKb } : {},
1138
+ ...i.attachmentsQuotaMb !== void 0 ? { quotaMb: i.attachmentsQuotaMb } : {},
1139
+ ...i.attachmentsRetentionDays !== void 0 ? { retentionDays: i.attachmentsRetentionDays } : {}
1140
+ },
1130
1141
  releaseHealth: {
1131
1142
  ...current.releaseHealth ?? {},
1132
1143
  ...i.crashFreeMin !== void 0 ? { crashFreeSessionsMin: i.crashFreeMin } : {},
@@ -1601,8 +1612,396 @@ ${d.notes.join("\n")}` : ""}`,
1601
1612
  };
1602
1613
  var logs_default = [list, patterns, stats, issueLogs];
1603
1614
 
1604
- // src/commands/issues.ts
1615
+ // src/commands/evidence.ts
1605
1616
  import { writeFileSync } from "node:fs";
1617
+ var feedback = {
1618
+ path: ["user-feedback"],
1619
+ aliases: [["feedback", "list"]],
1620
+ summary: "What users wrote through the feedback widget or API: message, page or screen, release, device, the issue it is about and its screenshots.",
1621
+ context: "project",
1622
+ effect: "read",
1623
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/feedback" },
1624
+ flags: {
1625
+ since: { type: "string", value: TIME, description: "Default 30 days." },
1626
+ limit: { type: "integer", value: "<n>", description: "At most 200 (default 50)." }
1627
+ },
1628
+ examples: [{ command: "tracksiy user-feedback --since 7d", description: "This week's feedback; `attachments get <id>` saves a screenshot." }],
1629
+ output: "{feedback:[{id, created, message, name, email, url, issueId, release, environment, source, os, device, attachments[{id, kind, name, contentType, size, url}]}]}",
1630
+ text: (d) => table(d.feedback ?? [], [
1631
+ ["when", (r) => ago(r.created)],
1632
+ ["message", (r) => String(r.message ?? "").slice(0, 80)],
1633
+ ["url", (r) => r.url ?? "-"],
1634
+ ["release", (r) => r.release ?? "-"],
1635
+ ["issue", (r) => r.issueId ?? "-"],
1636
+ ["files", (r) => (r.attachments ?? []).map((a) => a.id).join(",") || "-"]
1637
+ ]),
1638
+ async run(ctx) {
1639
+ return ctx.api.get(await projectPath(ctx, "feedback"), { since: since(ctx.input.since), limit: ctx.input.limit });
1640
+ }
1641
+ };
1642
+ var issueAttachments = {
1643
+ path: ["issues", "attachments"],
1644
+ summary: "Screenshots, view hierarchies and files of an issue's recent events and of the feedback about it.",
1645
+ context: "project",
1646
+ effect: "read",
1647
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/issues/{id}/attachments" },
1648
+ args: [{ name: "id", required: true, description: "The issue id." }],
1649
+ output: "{issueId, attachments:[{id, eventId, kind, name, contentType, size, created, url}], notes[]}",
1650
+ text: (d) => `${table(d.attachments ?? [], ["id", "kind", "name", "contentType", "size", ["when", (r) => ago(r.created)]])}${(d.notes ?? []).length ? `
1651
+
1652
+ ${d.notes.join("\n")}` : ""}`,
1653
+ async run(ctx) {
1654
+ return ctx.api.get(await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}/attachments`));
1655
+ }
1656
+ };
1657
+ var attachmentGet = {
1658
+ path: ["attachments", "get"],
1659
+ summary: "Save one attachment (a screenshot, a view hierarchy) to a file -- or print a JSON one.",
1660
+ context: "project",
1661
+ effect: "read",
1662
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/attachments/{id}" },
1663
+ args: [{ name: "id", required: true, description: "The attachment id (from `feedback` or `issues attachments`)." }],
1664
+ flags: { out: { type: "string", value: "<file>", description: "Where to write it. Without it, JSON and text are printed; binary needs --out." } },
1665
+ examples: [{ command: "tracksiy attachments get 812 --out /tmp/screenshot.png", description: "Then look at the screenshot." }],
1666
+ output: "{id, bytes, out} or the JSON / text content",
1667
+ async run(ctx) {
1668
+ if (!/^\d+$/.test(String(ctx.input.id ?? ""))) {
1669
+ throw usageError("bad_value", `attachments get expects a numeric attachment id, got ${JSON.stringify(ctx.input.id)}.`, "Ids come from `user-feedback` or `issues attachments`.");
1670
+ }
1671
+ const path = await projectPath(ctx, `attachments/${encodeURIComponent(ctx.input.id)}`);
1672
+ const bytes = await ctx.api.get(path, void 0, { binary: true });
1673
+ if (ctx.input.out) {
1674
+ writeFileSync(ctx.input.out, bytes);
1675
+ return { id: ctx.input.id, bytes: bytes.length, out: ctx.input.out };
1676
+ }
1677
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
1678
+ if (/[\u0000-\u0008\u000e-\u001f]/.test(text.slice(0, 512))) {
1679
+ throw usageError("binary", `Attachment ${ctx.input.id} is binary (${bytes.length} bytes).`, "Pass --out <file> to save it.");
1680
+ }
1681
+ try {
1682
+ return JSON.parse(text);
1683
+ } catch {
1684
+ return { id: ctx.input.id, text };
1685
+ }
1686
+ }
1687
+ };
1688
+ var issueFlags = {
1689
+ path: ["issues", "flags"],
1690
+ summary: "Feature flag values of an issue's events against the rest of the project; `suspect` marks a value far more common in this issue.",
1691
+ context: "project",
1692
+ effect: "read",
1693
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/issues/{id}/flags" },
1694
+ args: [{ name: "id", required: true, description: "The issue id." }],
1695
+ output: "{issueId, issueEvents, baselineEvents, flags:[{flag, suspect, suspectValue, values:[{result, issueShare, baselineShare}]}], notes[]}",
1696
+ text: (d) => `${table(d.flags ?? [], ["flag", ["suspect", (r) => r.suspect ? `YES (${r.suspectValue})` : ""], ["values", (r) => (r.values ?? []).map((v) => `${v.result}: ${Math.round(v.issueShare * 100)}% vs ${v.baselineShare == null ? "-" : Math.round(v.baselineShare * 100) + "%"}`).join(" ")]])}${(d.notes ?? []).length ? `
1697
+
1698
+ ${d.notes.join("\n")}` : ""}`,
1699
+ async run(ctx) {
1700
+ return ctx.api.get(await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}/flags`));
1701
+ }
1702
+ };
1703
+ var evidence_default = [feedback, issueAttachments, attachmentGet, issueFlags];
1704
+
1705
+ // src/commands/builds.ts
1706
+ import { existsSync as existsSync2 } from "node:fs";
1707
+ import { basename as basename2 } from "node:path";
1708
+
1709
+ // src/lib/appsize.ts
1710
+ import { readFileSync as readFileSync4, readdirSync, statSync } from "node:fs";
1711
+ import { basename, join as join2, relative, sep } from "node:path";
1712
+ import { deflateRawSync } from "node:zlib";
1713
+ var EOCD = 101010256;
1714
+ var ZIP64_LOCATOR = 117853008;
1715
+ var ZIP64_EOCD = 101075792;
1716
+ var CENTRAL = 33639248;
1717
+ function zipEntries(buf) {
1718
+ let eocd = -1;
1719
+ for (let i = buf.length - 22; i >= Math.max(0, buf.length - 65557); i--) {
1720
+ if (buf.readUInt32LE(i) === EOCD) {
1721
+ eocd = i;
1722
+ break;
1723
+ }
1724
+ }
1725
+ if (eocd < 0) throw new Error("not a ZIP archive (no end of central directory)");
1726
+ let count = buf.readUInt16LE(eocd + 10);
1727
+ let offset = buf.readUInt32LE(eocd + 16);
1728
+ if (count === 65535 || offset === 4294967295) {
1729
+ const locator = eocd - 20;
1730
+ if (locator >= 0 && buf.readUInt32LE(locator) === ZIP64_LOCATOR) {
1731
+ const record = Number(buf.readBigUInt64LE(locator + 8));
1732
+ if (buf.readUInt32LE(record) === ZIP64_EOCD) {
1733
+ count = Number(buf.readBigUInt64LE(record + 32));
1734
+ offset = Number(buf.readBigUInt64LE(record + 48));
1735
+ }
1736
+ }
1737
+ }
1738
+ const out = [];
1739
+ let p = offset;
1740
+ for (let n = 0; n < count && p + 46 <= buf.length; n++) {
1741
+ if (buf.readUInt32LE(p) !== CENTRAL) break;
1742
+ let compressed = buf.readUInt32LE(p + 20);
1743
+ let uncompressed = buf.readUInt32LE(p + 24);
1744
+ const nameLen = buf.readUInt16LE(p + 28);
1745
+ const extraLen = buf.readUInt16LE(p + 30);
1746
+ const commentLen = buf.readUInt16LE(p + 32);
1747
+ const name = buf.toString("utf8", p + 46, p + 46 + nameLen);
1748
+ if (compressed === 4294967295 || uncompressed === 4294967295) {
1749
+ let e = p + 46 + nameLen;
1750
+ const end = e + extraLen;
1751
+ while (e + 4 <= end) {
1752
+ const id = buf.readUInt16LE(e);
1753
+ const size = buf.readUInt16LE(e + 2);
1754
+ if (id === 1) {
1755
+ let q = e + 4;
1756
+ if (uncompressed === 4294967295) {
1757
+ uncompressed = Number(buf.readBigUInt64LE(q));
1758
+ q += 8;
1759
+ }
1760
+ if (compressed === 4294967295) compressed = Number(buf.readBigUInt64LE(q));
1761
+ break;
1762
+ }
1763
+ e += 4 + size;
1764
+ }
1765
+ }
1766
+ if (!name.endsWith("/")) out.push({ path: name, download: compressed, install: uncompressed });
1767
+ p += 46 + nameLen + extraLen + commentLen;
1768
+ }
1769
+ return out;
1770
+ }
1771
+ function walk(dir) {
1772
+ const out = [];
1773
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1774
+ const full = join2(dir, entry.name);
1775
+ if (entry.isDirectory()) out.push(...walk(full));
1776
+ else if (entry.isFile()) out.push(full);
1777
+ }
1778
+ return out;
1779
+ }
1780
+ function dirEntries(dir) {
1781
+ return walk(dir).map((file) => {
1782
+ const size = statSync(file).size;
1783
+ let download = size;
1784
+ if (size > 0 && size <= 64 * 1024 * 1024) {
1785
+ try {
1786
+ download = Math.min(size, deflateRawSync(readFileSync4(file), { level: 6 }).length);
1787
+ } catch {
1788
+ download = size;
1789
+ }
1790
+ }
1791
+ return { path: relative(dir, file).split(sep).join("/"), download, install: size };
1792
+ });
1793
+ }
1794
+ function androidCategory(path, aab) {
1795
+ let p = path;
1796
+ if (aab) {
1797
+ if (p === "BundleConfig.pb" || p.startsWith("BUNDLE-METADATA/") || p.startsWith("META-INF/")) return "bundle metadata";
1798
+ const slash = p.indexOf("/");
1799
+ const module = slash > 0 ? p.slice(0, slash) : "";
1800
+ if (module && module !== "base") return `module:${module}`;
1801
+ p = slash > 0 ? p.slice(slash + 1) : p;
1802
+ if (p.startsWith("dex/")) return "dex";
1803
+ if (p === "resources.pb") return "resources";
1804
+ if (p.startsWith("manifest/")) return "manifest";
1805
+ if (p.startsWith("root/")) p = p.slice(5);
1806
+ }
1807
+ if (/^classes\d*\.dex$/.test(p)) return "dex";
1808
+ const lib = /^lib\/([^/]+)\/([^/]+)$/.exec(p);
1809
+ if (lib) {
1810
+ const [, abi, file] = lib;
1811
+ if (file === "libapp.so") return `dart code (${abi})`;
1812
+ if (file === "libflutter.so") return `flutter engine (${abi})`;
1813
+ if (/^lib(hermes|jsc|reactnative)/.test(file)) return `js engine (${abi})`;
1814
+ return `native (${abi})`;
1815
+ }
1816
+ if (p === "assets/index.android.bundle") return "js bundle";
1817
+ if (p.startsWith("assets/flutter_assets/")) return "flutter assets";
1818
+ if (p.startsWith("assets/")) return "assets";
1819
+ if (p.startsWith("res/") || p === "resources.arsc") return "resources";
1820
+ if (p.startsWith("META-INF/")) return "signing";
1821
+ if (p.startsWith("kotlin/")) return "kotlin metadata";
1822
+ if (p === "AndroidManifest.xml") return "manifest";
1823
+ return "other";
1824
+ }
1825
+ function iosCategory(path, appName) {
1826
+ if (path === appName) return "binary";
1827
+ if (path.startsWith("Frameworks/Flutter.framework/")) return "flutter engine";
1828
+ if (path.startsWith("Frameworks/App.framework/")) return "dart code";
1829
+ if (path.startsWith("Frameworks/")) return "frameworks";
1830
+ if (path.startsWith("PlugIns/") || path.startsWith("Extensions/")) return "extensions";
1831
+ if (path.startsWith("Watch/")) return "watch app";
1832
+ if (path.endsWith(".car")) return "asset catalog";
1833
+ if (path === "main.jsbundle") return "js bundle";
1834
+ if (/(^|\/)[^/]+\.lproj\//.test(path)) return "localizations";
1835
+ if (path.startsWith("_CodeSignature/") || path === "embedded.mobileprovision") return "signing";
1836
+ if (/\.bundle\//.test(path)) return "bundles";
1837
+ return "resources";
1838
+ }
1839
+ function appRelative(entries) {
1840
+ const first = entries.map((e) => /^(.*?\/)?([^/]+)\.app\//.exec(e.path)).find(Boolean);
1841
+ if (!first) return { entries, appName: "" };
1842
+ const prefix = (first[1] ?? "") + first[2] + ".app/";
1843
+ return {
1844
+ entries: entries.filter((e) => e.path.startsWith(prefix)).map((e) => ({ ...e, path: e.path.slice(prefix.length) })),
1845
+ appName: first[2]
1846
+ };
1847
+ }
1848
+ function analyze(path) {
1849
+ const isDir = statSync(path).isDirectory();
1850
+ const lower = path.toLowerCase().replace(/\/+$/, "");
1851
+ let kind = "other";
1852
+ if (lower.endsWith(".apk")) kind = "apk";
1853
+ else if (lower.endsWith(".aab")) kind = "aab";
1854
+ else if (lower.endsWith(".ipa")) kind = "ipa";
1855
+ else if (lower.endsWith(".xcarchive")) kind = "xcarchive";
1856
+ else if (lower.endsWith(".app")) kind = "app";
1857
+ const platform = kind === "apk" || kind === "aab" ? "android" : kind === "other" ? "other" : "ios";
1858
+ let entries;
1859
+ if (isDir) {
1860
+ const root = kind === "xcarchive" ? join2(path, "Products", "Applications") : path;
1861
+ entries = dirEntries(root);
1862
+ if (kind === "app") entries = entries.map((e) => ({ ...e, path: `${basename(path)}/${e.path}` }));
1863
+ } else {
1864
+ entries = zipEntries(readFileSync4(path));
1865
+ }
1866
+ let appName = "";
1867
+ if (platform === "ios") ({ entries, appName } = appRelative(entries));
1868
+ const categories = {};
1869
+ let download = 0;
1870
+ let install = 0;
1871
+ for (const e of entries) {
1872
+ const name = platform === "android" ? androidCategory(e.path, kind === "aab") : platform === "ios" ? iosCategory(e.path, appName) : e.path.split("/")[0] || "other";
1873
+ const c = categories[name] ??= { download: 0, install: 0, files: 0 };
1874
+ c.download += e.download;
1875
+ c.install += e.install;
1876
+ c.files += 1;
1877
+ download += e.download;
1878
+ install += e.install;
1879
+ }
1880
+ const largest = [...entries].sort((a, b) => b.download - a.download).slice(0, 50);
1881
+ return { kind, platform, downloadBytes: download, installBytes: install, fileCount: entries.length, categories, largest };
1882
+ }
1883
+
1884
+ // src/commands/builds.ts
1885
+ var mb2 = (n) => n === null || n === void 0 ? "-" : `${(Number(n) / 1e6).toFixed(2)} MB`;
1886
+ var signed = (n) => `${n > 0 ? "+" : ""}${(n / 1e6).toFixed(2)} MB`;
1887
+ function render(d) {
1888
+ const c = d.comparison;
1889
+ const head = kv({
1890
+ release: d.release,
1891
+ platform: `${d.platform} (${d.kind})`,
1892
+ download: mb2(d.downloadBytes),
1893
+ install: mb2(d.installBytes),
1894
+ files: d.fileCount,
1895
+ ...c ? { "vs previous": `${c.previousRelease}: download ${signed(c.downloadDelta)} (${c.downloadDeltaPct ?? "-"}%), install ${signed(c.installDelta)}` } : {},
1896
+ ...d.verdict ? { budgets: d.verdict } : {}
1897
+ });
1898
+ const categories = Object.entries(d.categories ?? {}).map(([category, v]) => ({ category, ...v })).sort((a, b) => b.download - a.download).slice(0, 15);
1899
+ const cats = categories.length ? `
1900
+
1901
+ ${table(categories, ["category", ["download", (r) => mb2(r.download)], ["install", (r) => mb2(r.install)], "files"])}` : "";
1902
+ const deltas = c?.categories?.length ? `
1903
+
1904
+ Changed:
1905
+ ${table(c.categories, ["category", ["download \u0394", (r) => signed(r.downloadDelta)], ["install \u0394", (r) => signed(r.installDelta)]])}` : "";
1906
+ const budgets = (d.budgets ?? []).length ? `
1907
+
1908
+ ${table(d.budgets, ["name", "metric", "max", ["value", (r) => r.value ?? "-"], "status"])}` : "";
1909
+ return head + cats + deltas + budgets;
1910
+ }
1911
+ var upload = {
1912
+ path: ["build", "upload"],
1913
+ summary: "Measure an app build (APK, AAB, IPA, .xcarchive, .app) on this machine and record its size: per category, the largest files, growth against the previous build, size budgets. The binary is not uploaded.",
1914
+ description: "Download size = the archive's compressed sizes (for an AAB an upper bound of what Play serves a device); install size = uncompressed. Categories: dex, native per ABI, resources, assets, the JS bundle and engine, Dart code and the Flutter engine, frameworks, the main binary, asset catalogs, localizations... Exits 10 when a size budget (`budgets set --budget 'name=size,metric=app_download_mb,max=40,op=android'`) is crossed, or the download grew more than --max-growth. An upload token (project:releases) is enough.",
1915
+ context: "project",
1916
+ effect: "write",
1917
+ endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/builds" },
1918
+ scopes: ["project:releases", "project:write", "project:admin"],
1919
+ args: [{ name: "path", required: true, description: "app-release.aab, app.apk, App.ipa, App.xcarchive or App.app." }],
1920
+ flags: {
1921
+ release: { type: "string", value: "<version>", description: "The release as the SDK names it: shop-android@1.4.2+57. Required." },
1922
+ commit: { type: "string", value: "<sha>", description: "The commit it was built from." },
1923
+ "build-configuration": { type: "string", value: "<name>", description: "Release, Debug, a flavor..." },
1924
+ "app-id": { type: "string", value: "<id>", description: "Package name / bundle id." },
1925
+ "max-growth": { type: "number", value: "<pct>", description: "Fail (exit 10) when the download grew more than this % against the previous build." },
1926
+ "dry-run": { type: "boolean", description: "Measure and print; record nothing." }
1927
+ },
1928
+ examples: [
1929
+ { command: "tracksiy build upload app/build/outputs/bundle/release/app-release.aab --release shop-android@1.4.2+57 --max-growth 5", description: "In CI after bundleRelease: record and stop on a 5 % jump." },
1930
+ { command: "tracksiy build upload build/Shop.xcarchive --release shop-ios@1.4.2+57 --dry-run", description: "Where the weight is, without recording." }
1931
+ ],
1932
+ output: "{id, release, platform, kind, downloadBytes, installBytes, fileCount, categories{...}, largest[...], comparison{previousRelease, downloadDelta, downloadDeltaPct, categories[...], newLargeFiles[...]}, budgets[...], verdict}",
1933
+ text: render,
1934
+ async run(ctx) {
1935
+ const i = ctx.input;
1936
+ if (!existsSync2(i.path)) throw usageError("not_found", `No such file or directory: ${i.path}`);
1937
+ let size;
1938
+ try {
1939
+ size = analyze(i.path);
1940
+ } catch (err) {
1941
+ throw usageError("bad_value", `Could not read ${i.path}: ${err.message}`, "Pass an .apk, .aab, .ipa, .xcarchive or .app.");
1942
+ }
1943
+ const body = {
1944
+ release: i.release,
1945
+ platform: size.platform,
1946
+ kind: size.kind,
1947
+ downloadBytes: size.downloadBytes,
1948
+ installBytes: size.installBytes,
1949
+ fileCount: size.fileCount,
1950
+ categories: size.categories,
1951
+ largest: size.largest,
1952
+ commit: i.commit ?? "",
1953
+ buildConfiguration: i.buildConfiguration ?? "",
1954
+ appId: i.appId ?? "",
1955
+ artifact: basename2(String(i.path).replace(/\/+$/, ""))
1956
+ };
1957
+ if (i.dryRun) return { dryRun: true, ...body, largest: body.largest.slice(0, 15) };
1958
+ if (!i.release) throw usageError("missing_flag", "--release is required to record a build.", "Name it as the SDK does: shop-android@1.4.2+57.");
1959
+ const data = await ctx.api.send("POST", await projectPath(ctx, "builds"), body);
1960
+ const growth = data.comparison?.downloadDeltaPct;
1961
+ if (data.verdict === "fail") {
1962
+ const names = (data.budgets ?? []).filter((b) => b.status === "fail").map((b) => `${b.name} ${b.value} > ${b.max}`).join(", ");
1963
+ throw new CheckFailed(`Size budget crossed: ${names}.`, data, "See `build view <id>` for what grew.");
1964
+ }
1965
+ if (i.maxGrowth !== void 0 && growth !== null && growth !== void 0 && growth > i.maxGrowth) {
1966
+ throw new CheckFailed(`Download grew ${growth}% against ${data.comparison.previousRelease} (max ${i.maxGrowth}%).`, data, "See the `Changed` categories and `comparison.newLargeFiles`.");
1967
+ }
1968
+ return data;
1969
+ }
1970
+ };
1971
+ var list2 = {
1972
+ path: ["build", "list"],
1973
+ aliases: [["builds"]],
1974
+ summary: "Recorded builds newest first, with their growth against the build before.",
1975
+ context: "project",
1976
+ effect: "read",
1977
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/builds" },
1978
+ flags: {
1979
+ platform: { type: "string", enum: ["android", "ios", "other"], description: "Only this platform." },
1980
+ limit: { type: "integer", value: "<n>", description: "At most 100 (default 20)." }
1981
+ },
1982
+ output: "[{id, created, release, platform, kind, downloadBytes, installBytes, comparison{...}}]",
1983
+ text: (d) => table(d ?? [], ["id", ["when", (r) => ago(r.created)], "release", "platform", ["download", (r) => mb2(r.downloadBytes)], ["\u0394", (r) => r.comparison ? signed(r.comparison.downloadDelta) : "-"], ["install", (r) => mb2(r.installBytes)]]),
1984
+ async run(ctx) {
1985
+ return ctx.api.get(await projectPath(ctx, "builds"), { platform: ctx.input.platform, limit: ctx.input.limit });
1986
+ }
1987
+ };
1988
+ var view = {
1989
+ path: ["build", "view"],
1990
+ summary: "One build: size per category, the largest files, what changed against the previous build.",
1991
+ context: "project",
1992
+ effect: "read",
1993
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/builds/{id}" },
1994
+ args: [{ name: "id", required: true, description: "The build id (from `build list`)." }],
1995
+ output: "{id, release, categories{...}, largest[...], comparison{...}}",
1996
+ text: render,
1997
+ async run(ctx) {
1998
+ return ctx.api.get(await projectPath(ctx, `builds/${encodeURIComponent(ctx.input.id)}`));
1999
+ }
2000
+ };
2001
+ var builds_default = [upload, list2, view];
2002
+
2003
+ // src/commands/issues.ts
2004
+ import { writeFileSync as writeFileSync2 } from "node:fs";
1606
2005
  var FILTERS = {
1607
2006
  status: { type: "string", enum: ["unresolved", "resolved", "ignored", "regressed", "all"], description: "Issue status. Server default: unresolved." },
1608
2007
  level: { type: "string", enum: ["sample", "debug", "info", "warning", "error", "fatal"], description: "Only this level." },
@@ -1658,7 +2057,7 @@ var issueRows = (issues) => table(issues ?? [], [
1658
2057
  ["status", (r) => r.regressedAt && r.status === "unresolved" ? "regressed" : r.status],
1659
2058
  ["title", (r) => r.title]
1660
2059
  ]);
1661
- var list2 = {
2060
+ var list3 = {
1662
2061
  path: ["issues", "list"],
1663
2062
  aliases: [["issues"]],
1664
2063
  summary: "Recent issues of a project, newest activity first, with the frames that matter inlined.",
@@ -1698,7 +2097,7 @@ ${issueRows(d.issues)}`,
1698
2097
  });
1699
2098
  }
1700
2099
  };
1701
- var view = {
2100
+ var view2 = {
1702
2101
  path: ["issues", "view"],
1703
2102
  summary: "One issue in enough depth to act on: latest event, stack frames, tags, recent volume.",
1704
2103
  context: "project",
@@ -1952,13 +2351,13 @@ var bulk = {
1952
2351
  });
1953
2352
  const result = await ctx.api.send("POST", await projectPath(ctx, "issues/status"), body);
1954
2353
  if (ctx.input.out && ctx.input.apply) {
1955
- writeFileSync(ctx.input.out, JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), status: ctx.input.to, changed: result?.changed ?? [] }, null, 2));
2354
+ writeFileSync2(ctx.input.out, JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), status: ctx.input.to, changed: result?.changed ?? [] }, null, 2));
1956
2355
  ctx.log(`wrote ${result?.changed?.length ?? 0} change(s) to ${ctx.input.out}; undo with --revert ${ctx.input.out} --apply`);
1957
2356
  }
1958
2357
  return result;
1959
2358
  }
1960
2359
  };
1961
- var issues_default = [list2, view, resolve2, ignore, unresolve, bulk, merge, comment, activity, verifyFixed, claim, unclaim];
2360
+ var issues_default = [list3, view2, resolve2, ignore, unresolve, bulk, merge, comment, activity, verifyFixed, claim, unclaim];
1962
2361
 
1963
2362
  // src/commands/projects.ts
1964
2363
  var create = {
@@ -2073,8 +2472,8 @@ var DECLARE = {
2073
2472
  "replace-recipients": { type: "boolean", description: "Make the recipient list exactly this one (default: add to it)." },
2074
2473
  body: { type: "json", value: "<json|@file|->", description: "Raw request body; overrides the other flags." }
2075
2474
  };
2076
- function recipients(list6, delivery) {
2077
- return (list6 ?? []).map((raw) => {
2475
+ function recipients(list7, delivery) {
2476
+ return (list7 ?? []).map((raw) => {
2078
2477
  const i = raw.indexOf(":");
2079
2478
  const type = i > 0 ? raw.slice(0, i) : raw;
2080
2479
  if (!RECIPIENT_TYPES.includes(type)) {
@@ -2255,7 +2654,7 @@ var settingsSet2 = {
2255
2654
  var alerts_default = [get, set, test, coverage, defaults, settingsGet2, settingsSet2];
2256
2655
 
2257
2656
  // src/commands/keys.ts
2258
- var list3 = {
2657
+ var list4 = {
2259
2658
  path: ["keys", "list"],
2260
2659
  aliases: [["keys"]],
2261
2660
  summary: "Every ingest key of the project: its surface, whether ingest accepts it, its rate limit and volume.",
@@ -2312,11 +2711,11 @@ var rotate = {
2312
2711
  return ctx.api.send("POST", await projectPath(ctx, `keys/${seg(ctx.input.name)}/rotate`));
2313
2712
  }
2314
2713
  };
2315
- var keys_default = [list3, set2, rotate];
2714
+ var keys_default = [list4, set2, rotate];
2316
2715
 
2317
2716
  // src/commands/releases.ts
2318
2717
  import { execFileSync } from "node:child_process";
2319
- var list4 = {
2718
+ var list5 = {
2320
2719
  path: ["releases", "list"],
2321
2720
  aliases: [["releases"]],
2322
2721
  summary: "What did the last deploys break? Recent releases with their events, issues and the issues they introduced.",
@@ -2462,25 +2861,25 @@ ${table(d.newIssues, ["short_id", "count", "title"])}` : ""}`,
2462
2861
  return data;
2463
2862
  }
2464
2863
  };
2465
- var releases_default = [list4, health2, declare, verdict];
2864
+ var releases_default = [list5, health2, declare, verdict];
2466
2865
 
2467
2866
  // src/commands/artifacts.ts
2468
2867
  import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
2469
- import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
2868
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
2470
2869
 
2471
2870
  // src/lib/sourcemaps.ts
2472
2871
  import { createHash } from "node:crypto";
2473
- import { existsSync as existsSync2 } from "node:fs";
2872
+ import { existsSync as existsSync3 } from "node:fs";
2474
2873
  import { readdir, readFile, stat } from "node:fs/promises";
2475
- import { dirname as dirname2, join as join2, relative, resolve as resolve3, sep } from "node:path";
2874
+ import { dirname as dirname2, join as join3, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
2476
2875
  var JS = /\.(m?js|cjs)$/i;
2477
2876
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".svn", ".hg"]);
2478
- async function walk(dir) {
2877
+ async function walk2(dir) {
2479
2878
  const out = [];
2480
2879
  for (const entry of await readdir(dir, { withFileTypes: true })) {
2481
- const full = join2(dir, entry.name);
2880
+ const full = join3(dir, entry.name);
2482
2881
  if (entry.isDirectory()) {
2483
- if (!SKIP_DIRS.has(entry.name)) out.push(...await walk(full));
2882
+ if (!SKIP_DIRS.has(entry.name)) out.push(...await walk2(full));
2484
2883
  } else if (entry.isFile()) out.push(full);
2485
2884
  }
2486
2885
  return out.sort();
@@ -2540,7 +2939,7 @@ async function findPairs(paths) {
2540
2939
  const st = await stat(abs).catch(() => void 0);
2541
2940
  if (!st) throw new Error(`No such file or directory: ${p}`);
2542
2941
  const root = st.isDirectory() ? abs : dirname2(abs);
2543
- const files = st.isDirectory() ? await walk(abs) : [abs];
2942
+ const files = st.isDirectory() ? await walk2(abs) : [abs];
2544
2943
  for (const js of files.filter((f) => JS.test(f))) {
2545
2944
  if (seen.has(js)) continue;
2546
2945
  seen.add(js);
@@ -2549,16 +2948,16 @@ async function findPairs(paths) {
2549
2948
  let map;
2550
2949
  if (url && !url.startsWith("data:")) {
2551
2950
  const candidate = resolve3(dirname2(js), decodeURIComponent(url.split(/[?#]/)[0]));
2552
- if (existsSync2(candidate)) map = candidate;
2951
+ if (existsSync3(candidate)) map = candidate;
2553
2952
  }
2554
- if (!map && existsSync2(`${js}.map`)) map = `${js}.map`;
2953
+ if (!map && existsSync3(`${js}.map`)) map = `${js}.map`;
2555
2954
  pairs2.push({ js, map, root });
2556
2955
  }
2557
2956
  }
2558
2957
  return pairs2;
2559
2958
  }
2560
2959
  function artifactName(file, root, prefix) {
2561
- const rel = relative(root, file).split(sep).join("/");
2960
+ const rel = relative2(root, file).split(sep2).join("/");
2562
2961
  const p = prefix.endsWith("/") ? prefix : `${prefix}/`;
2563
2962
  return `${p}${rel}`;
2564
2963
  }
@@ -2840,8 +3239,8 @@ skipped, no source map: ${d.skipped.join(", ")}` : ""}`,
2840
3239
  }
2841
3240
  };
2842
3241
  function uuidFromDebugMeta(path) {
2843
- if (!existsSync3(path)) throw usageError("bad_path", `No such file: ${path}`);
2844
- const m = /ProguardUuids\s*[=:]\s*([0-9a-fA-F-]{36})/.exec(readFileSync4(path, "utf8"));
3242
+ if (!existsSync4(path)) throw usageError("bad_path", `No such file: ${path}`);
3243
+ const m = /ProguardUuids\s*[=:]\s*([0-9a-fA-F-]{36})/.exec(readFileSync5(path, "utf8"));
2845
3244
  if (!m) throw usageError("bad_value", `No ProguardUuids entry in ${path}`);
2846
3245
  return m[1].toLowerCase();
2847
3246
  }
@@ -2866,7 +3265,7 @@ var proguardUpload = {
2866
3265
  output: "{id, state, files[{name, kind, state}], chunks{}, bytes{}, uuid}",
2867
3266
  async run(ctx) {
2868
3267
  const i = ctx.input;
2869
- if (!existsSync3(i.mapping)) throw usageError("bad_path", `No such file: ${i.mapping}`);
3268
+ if (!existsSync4(i.mapping)) throw usageError("bad_path", `No such file: ${i.mapping}`);
2870
3269
  const data = await readFile2(i.mapping);
2871
3270
  if (!data.length) throw usageError("bad_value", `${i.mapping} is empty.`);
2872
3271
  const uuid = (i.uuid ?? (i.debugMeta ? uuidFromDebugMeta(i.debugMeta) : proguardUuid(data))).toLowerCase();
@@ -2903,7 +3302,7 @@ var artifacts_default = [sourcemapsInject, sourcemapsUpload, proguardUpload, art
2903
3302
 
2904
3303
  // src/commands/tokens.ts
2905
3304
  var SCOPE_HINT = "e.g. org:read,project:read,event:read,event:write,project:write,project:releases,project:admin";
2906
- var list5 = {
3305
+ var list6 = {
2907
3306
  path: ["tokens", "list"],
2908
3307
  aliases: [["tokens"]],
2909
3308
  summary: "Your tokens, newest first, without their values.",
@@ -3016,7 +3415,7 @@ var self = {
3016
3415
  return ctx.api.get("tokens/self");
3017
3416
  }
3018
3417
  };
3019
- var tokens_default = [list5, self, create2, update, remove, rotate2];
3418
+ var tokens_default = [list6, self, create2, update, remove, rotate2];
3020
3419
 
3021
3420
  // src/commands/support.ts
3022
3421
  var SUPPORT = {
@@ -3053,33 +3452,33 @@ var support = {
3053
3452
  var support_default = [support];
3054
3453
 
3055
3454
  // src/commands/init.ts
3056
- import { existsSync as existsSync5, readFileSync as readFileSync6 } from "node:fs";
3455
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
3057
3456
  import { appendFile, writeFile as writeFile3 } from "node:fs/promises";
3058
- import { join as join4, resolve as resolve4 } from "node:path";
3457
+ import { join as join5, resolve as resolve4 } from "node:path";
3059
3458
 
3060
3459
  // src/lib/detect.ts
3061
- import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync } from "node:fs";
3062
- import { basename, join as join3 } from "node:path";
3460
+ import { existsSync as existsSync5, readFileSync as readFileSync6, readdirSync as readdirSync2 } from "node:fs";
3461
+ import { basename as basename3, join as join4 } from "node:path";
3063
3462
  function readJson(path) {
3064
3463
  try {
3065
- return JSON.parse(readFileSync5(path, "utf8"));
3464
+ return JSON.parse(readFileSync6(path, "utf8"));
3066
3465
  } catch {
3067
3466
  return void 0;
3068
3467
  }
3069
3468
  }
3070
3469
  function has(dir, ...names) {
3071
- return names.find((n) => existsSync4(join3(dir, n)));
3470
+ return names.find((n) => existsSync5(join4(dir, n)));
3072
3471
  }
3073
3472
  function slugify(s) {
3074
3473
  return s.toLowerCase().replace(/^@[^/]+\//, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
3075
3474
  }
3076
3475
  function detect(dir, guideBase) {
3077
3476
  const evidence = [];
3078
- const composer = readJson(join3(dir, "composer.json"));
3079
- const pkg = readJson(join3(dir, "package.json"));
3477
+ const composer = readJson(join4(dir, "composer.json"));
3478
+ const pkg = readJson(join4(dir, "package.json"));
3080
3479
  const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
3081
3480
  const hasDep = (d) => d in deps;
3082
- const name = slugify(pkg?.name ?? composer?.name?.split("/").pop() ?? basename(dir)) || "app";
3481
+ const name = slugify(pkg?.name ?? composer?.name?.split("/").pop() ?? basename3(dir)) || "app";
3083
3482
  const guide = (slug) => `${guideBase}/${slug}.md`;
3084
3483
  if (composer?.require?.["laravel/framework"]) {
3085
3484
  evidence.push("composer.json requires laravel/framework");
@@ -3134,8 +3533,8 @@ function detect(dir, guideBase) {
3134
3533
  }
3135
3534
  const py = has(dir, "pyproject.toml", "requirements.txt", "Pipfile", "manage.py");
3136
3535
  if (py) {
3137
- const text = ["pyproject.toml", "requirements.txt", "Pipfile"].map((f) => existsSync4(join3(dir, f)) ? readFileSync5(join3(dir, f), "utf8") : "").join("\n").toLowerCase();
3138
- const fw = existsSync4(join3(dir, "manage.py")) || text.includes("django") ? "django" : text.includes("fastapi") ? "fastapi" : text.includes("flask") ? "flask" : "python";
3536
+ const text = ["pyproject.toml", "requirements.txt", "Pipfile"].map((f) => existsSync5(join4(dir, f)) ? readFileSync6(join4(dir, f), "utf8") : "").join("\n").toLowerCase();
3537
+ const fw = existsSync5(join4(dir, "manage.py")) || text.includes("django") ? "django" : text.includes("fastapi") ? "fastapi" : text.includes("flask") ? "flask" : "python";
3139
3538
  evidence.push(`${py} (${fw})`);
3140
3539
  return { platform: fw === "python" ? "python" : `python-${fw}`, framework: fw, surfaces: ["backend"], env: { backend: "SENTRY_DSN" }, guide: guide("python-sentry-sdk"), name, evidence };
3141
3540
  }
@@ -3144,7 +3543,7 @@ function detect(dir, guideBase) {
3144
3543
  evidence.push(gradle);
3145
3544
  return { platform: "android", framework: "android", surfaces: ["mobile"], env: {}, guide: guide("mobile"), name, evidence };
3146
3545
  }
3147
- const apple = has(dir, "Podfile", "Package.swift") ?? readdirSync(dir).find((f) => f.endsWith(".xcodeproj"));
3546
+ const apple = has(dir, "Podfile", "Package.swift") ?? readdirSync2(dir).find((f) => f.endsWith(".xcodeproj"));
3148
3547
  if (apple) {
3149
3548
  evidence.push(apple);
3150
3549
  return { platform: "apple-ios", framework: "ios", surfaces: ["mobile"], env: {}, guide: guide("mobile"), name, evidence };
@@ -3195,14 +3594,14 @@ ${d.steps.map((s, i) => `${i + 1}. ${s.detail}${s.run ? `
3195
3594
  async run(ctx) {
3196
3595
  const i = ctx.input;
3197
3596
  const dir = resolve4(i.dir);
3198
- if (!existsSync5(dir)) throw usageError("bad_path", `No such directory: ${i.dir}`);
3597
+ if (!existsSync6(dir)) throw usageError("bad_path", `No such directory: ${i.dir}`);
3199
3598
  const found = detect(dir, ctx.config.url);
3200
3599
  const platform = i.platform ?? found.platform;
3201
3600
  const surfaces = i.surfaces ?? found.surfaces;
3202
3601
  const name = i.name ?? found.name;
3203
3602
  const detected = { platform, framework: found.framework, surfaces, evidence: found.evidence, name };
3204
3603
  const steps = [];
3205
- const upload = found.buildDir ? `tracksiy sourcemaps upload ${found.buildDir} --inject` : void 0;
3604
+ const upload2 = found.buildDir ? `tracksiy sourcemaps upload ${found.buildDir} --inject` : void 0;
3206
3605
  if (i.dryRun) {
3207
3606
  steps.push({ id: "create", run: `tracksiy init${i.writeEnv ? " --write-env" : ""}`, detail: "Create the project and keys." });
3208
3607
  return { dryRun: true, detected, guide: found.guide, steps };
@@ -3214,7 +3613,7 @@ ${d.steps.map((s, i) => `${i + 1}. ${s.detail}${s.run ? `
3214
3613
  compact({ name, platform, surfaces, sameHost: i.sameHost })
3215
3614
  );
3216
3615
  const project2 = prov.project;
3217
- const pfPath = join4(dir, PROJECT_FILE);
3616
+ const pfPath = join5(dir, PROJECT_FILE);
3218
3617
  const pf = { org: project2.organization, project: project2.slug };
3219
3618
  if (ctx.config.url !== DEFAULT_URL) pf.url = ctx.config.url;
3220
3619
  await writeFile3(pfPath, JSON.stringify(pf, null, 2) + "\n");
@@ -3226,11 +3625,11 @@ ${d.steps.map((s, i) => `${i + 1}. ${s.detail}${s.run ? `
3226
3625
  }));
3227
3626
  let env;
3228
3627
  if (i.writeEnv) {
3229
- const envPath = join4(dir, ".env");
3230
- if (!existsSync5(envPath)) {
3628
+ const envPath = join5(dir, ".env");
3629
+ if (!existsSync6(envPath)) {
3231
3630
  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.");
3232
3631
  }
3233
- const current = readFileSync6(envPath, "utf8");
3632
+ const current = readFileSync7(envPath, "utf8");
3234
3633
  const added = [];
3235
3634
  const kept = [];
3236
3635
  let block = "";
@@ -3252,7 +3651,7 @@ ${block}`);
3252
3651
  if (!i.writeEnv && keys.some((k) => k.env)) {
3253
3652
  steps.push({ id: "env", detail: `Set ${keys.filter((k) => k.env).map((k) => k.env).join(", ")} from \`keys\` (or re-run with --write-env).` });
3254
3653
  }
3255
- if (upload) steps.push({ id: "sourcemaps", detail: "After each production build, upload source maps (uses project:releases).", run: upload });
3654
+ if (upload2) steps.push({ id: "sourcemaps", detail: "After each production build, upload source maps (uses project:releases).", run: upload2 });
3256
3655
  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" });
3257
3656
  steps.push({ id: "verify", detail: "Deploy, trigger one error, then prove it arrived.", run: "tracksiy projects verify --wait 120" });
3258
3657
  return {
@@ -3285,7 +3684,7 @@ function camel(name) {
3285
3684
  }
3286
3685
 
3287
3686
  // src/core/args.ts
3288
- import { readFileSync as readFileSync7 } from "node:fs";
3687
+ import { readFileSync as readFileSync8 } from "node:fs";
3289
3688
  var GLOBAL_FLAGS = {
3290
3689
  org: { type: "string", value: "<slug>", description: "Organization slug. Env: TRACKSIY_ORG. Default: .tracksiy.json, saved login, or your only organization." },
3291
3690
  project: { type: "string", value: "<slug>", description: "Project slug, or org/project. Env: TRACKSIY_PROJECT. Default: .tracksiy.json." },
@@ -3302,7 +3701,7 @@ var GLOBAL_FLAGS = {
3302
3701
  };
3303
3702
  var stdinCache;
3304
3703
  function readStdin() {
3305
- stdinCache ??= readFileSync7(0, "utf8");
3704
+ stdinCache ??= readFileSync8(0, "utf8");
3306
3705
  return stdinCache;
3307
3706
  }
3308
3707
  function parseJsonValue(name, raw) {
@@ -3310,7 +3709,7 @@ function parseJsonValue(name, raw) {
3310
3709
  if (raw === "-") text = readStdin();
3311
3710
  else if (raw.startsWith("@")) {
3312
3711
  try {
3313
- text = readFileSync7(raw.slice(1), "utf8");
3712
+ text = readFileSync8(raw.slice(1), "utf8");
3314
3713
  } catch (err) {
3315
3714
  throw usageError("bad_value", `--${name}: cannot read ${raw.slice(1)}: ${err.message}`);
3316
3715
  }
@@ -3680,15 +4079,15 @@ function makeContext(spec, input, globals, surface, io) {
3680
4079
  const org = () => {
3681
4080
  orgPromise ??= (async () => {
3682
4081
  if (config.org) return config.org;
3683
- const list6 = await visible();
3684
- if (list6.length === 1) {
3685
- log(`using organization ${list6[0].slug}`);
3686
- return list6[0].slug;
4082
+ const list7 = await visible();
4083
+ if (list7.length === 1) {
4084
+ log(`using organization ${list7[0].slug}`);
4085
+ return list7[0].slug;
3687
4086
  }
3688
4087
  throw usageError(
3689
4088
  "missing_org",
3690
- list6.length ? "Which organization? This token can see several." : "This token cannot see any organization.",
3691
- list6.length ? `Pass --org <slug>, set TRACKSIY_ORG, or add "org" to ${".tracksiy.json"}. Visible: ${list6.map((o) => o.slug).join(", ")}` : "Check the token with `tracksiy auth status`."
4089
+ list7.length ? "Which organization? This token can see several." : "This token cannot see any organization.",
4090
+ list7.length ? `Pass --org <slug>, set TRACKSIY_ORG, or add "org" to ${".tracksiy.json"}. Visible: ${list7.map((o) => o.slug).join(", ")}` : "Check the token with `tracksiy auth status`."
3692
4091
  );
3693
4092
  })();
3694
4093
  return orgPromise;
@@ -3698,8 +4097,8 @@ function makeContext(spec, input, globals, surface, io) {
3698
4097
  projectPromise ??= (async () => {
3699
4098
  const o = await org();
3700
4099
  if (config.project) return { org: o, project: config.project };
3701
- const list6 = await visible();
3702
- const projects2 = list6.find((x) => x.slug === o)?.projects ?? [];
4100
+ const list7 = await visible();
4101
+ const projects2 = list7.find((x) => x.slug === o)?.projects ?? [];
3703
4102
  if (projects2.length === 1) {
3704
4103
  log(`using project ${o}/${projects2[0].slug}`);
3705
4104
  return { org: o, project: projects2[0].slug };
@@ -3923,6 +4322,8 @@ var COMMANDS = [
3923
4322
  ...storage_default,
3924
4323
  ...mobile_default,
3925
4324
  ...logs_default,
4325
+ ...evidence_default,
4326
+ ...builds_default,
3926
4327
  ...issues_default,
3927
4328
  ...projects_default,
3928
4329
  ...alerts_default,
@@ -3936,8 +4337,8 @@ var COMMANDS = [
3936
4337
  ];
3937
4338
  var GROUPS = [
3938
4339
  ["Start", "init auth onboarding describe"],
3939
- ["Look", "overview changes health storage orgs projects issues monitors performance mobile logs queries not-found releases alerts keys artifacts settings privacy-preview"],
3940
- ["Ship", "sourcemaps proguard"],
4340
+ ["Look", "overview changes health storage orgs projects issues monitors performance mobile logs user-feedback attachments queries not-found releases alerts keys artifacts settings privacy-preview"],
4341
+ ["Ship", "sourcemaps proguard build builds"],
3941
4342
  ["Manage", "tokens"],
3942
4343
  ["Talk to us", "support"],
3943
4344
  ["Other", "api mcp"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tracksiy",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Agent-first CLI and MCP server for Tracksiy: errors, uptime, alerts, releases and source maps, from one token.",
5
5
  "type": "module",
6
6
  "bin": {