tracksiy 0.12.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
@@ -147,6 +147,9 @@ Labels: `bug`, `feature`, `question`, `blocker`. `npx tracksiy support` prints t
147
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
148
  | `attachments get <id>` | Save one attachment (a screenshot, a view hierarchy) to a file -- or print a JSON one. | read | `attachments_get` |
149
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` |
150
153
  | `issues list` | Recent issues of a project, newest activity first, with the frames that matter inlined. | read | `issues_list` |
151
154
  | `issues view <id>` | One issue in enough depth to act on: latest event, stack frames, tags, recent volume. | read | `issues_view` |
152
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.12.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
@@ -1702,6 +1702,304 @@ ${d.notes.join("\n")}` : ""}`,
1702
1702
  };
1703
1703
  var evidence_default = [feedback, issueAttachments, attachmentGet, issueFlags];
1704
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
+
1705
2003
  // src/commands/issues.ts
1706
2004
  import { writeFileSync as writeFileSync2 } from "node:fs";
1707
2005
  var FILTERS = {
@@ -1759,7 +2057,7 @@ var issueRows = (issues) => table(issues ?? [], [
1759
2057
  ["status", (r) => r.regressedAt && r.status === "unresolved" ? "regressed" : r.status],
1760
2058
  ["title", (r) => r.title]
1761
2059
  ]);
1762
- var list2 = {
2060
+ var list3 = {
1763
2061
  path: ["issues", "list"],
1764
2062
  aliases: [["issues"]],
1765
2063
  summary: "Recent issues of a project, newest activity first, with the frames that matter inlined.",
@@ -1799,7 +2097,7 @@ ${issueRows(d.issues)}`,
1799
2097
  });
1800
2098
  }
1801
2099
  };
1802
- var view = {
2100
+ var view2 = {
1803
2101
  path: ["issues", "view"],
1804
2102
  summary: "One issue in enough depth to act on: latest event, stack frames, tags, recent volume.",
1805
2103
  context: "project",
@@ -2059,7 +2357,7 @@ var bulk = {
2059
2357
  return result;
2060
2358
  }
2061
2359
  };
2062
- 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];
2063
2361
 
2064
2362
  // src/commands/projects.ts
2065
2363
  var create = {
@@ -2174,8 +2472,8 @@ var DECLARE = {
2174
2472
  "replace-recipients": { type: "boolean", description: "Make the recipient list exactly this one (default: add to it)." },
2175
2473
  body: { type: "json", value: "<json|@file|->", description: "Raw request body; overrides the other flags." }
2176
2474
  };
2177
- function recipients(list6, delivery) {
2178
- return (list6 ?? []).map((raw) => {
2475
+ function recipients(list7, delivery) {
2476
+ return (list7 ?? []).map((raw) => {
2179
2477
  const i = raw.indexOf(":");
2180
2478
  const type = i > 0 ? raw.slice(0, i) : raw;
2181
2479
  if (!RECIPIENT_TYPES.includes(type)) {
@@ -2356,7 +2654,7 @@ var settingsSet2 = {
2356
2654
  var alerts_default = [get, set, test, coverage, defaults, settingsGet2, settingsSet2];
2357
2655
 
2358
2656
  // src/commands/keys.ts
2359
- var list3 = {
2657
+ var list4 = {
2360
2658
  path: ["keys", "list"],
2361
2659
  aliases: [["keys"]],
2362
2660
  summary: "Every ingest key of the project: its surface, whether ingest accepts it, its rate limit and volume.",
@@ -2413,11 +2711,11 @@ var rotate = {
2413
2711
  return ctx.api.send("POST", await projectPath(ctx, `keys/${seg(ctx.input.name)}/rotate`));
2414
2712
  }
2415
2713
  };
2416
- var keys_default = [list3, set2, rotate];
2714
+ var keys_default = [list4, set2, rotate];
2417
2715
 
2418
2716
  // src/commands/releases.ts
2419
2717
  import { execFileSync } from "node:child_process";
2420
- var list4 = {
2718
+ var list5 = {
2421
2719
  path: ["releases", "list"],
2422
2720
  aliases: [["releases"]],
2423
2721
  summary: "What did the last deploys break? Recent releases with their events, issues and the issues they introduced.",
@@ -2563,25 +2861,25 @@ ${table(d.newIssues, ["short_id", "count", "title"])}` : ""}`,
2563
2861
  return data;
2564
2862
  }
2565
2863
  };
2566
- var releases_default = [list4, health2, declare, verdict];
2864
+ var releases_default = [list5, health2, declare, verdict];
2567
2865
 
2568
2866
  // src/commands/artifacts.ts
2569
2867
  import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
2570
- import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
2868
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
2571
2869
 
2572
2870
  // src/lib/sourcemaps.ts
2573
2871
  import { createHash } from "node:crypto";
2574
- import { existsSync as existsSync2 } from "node:fs";
2872
+ import { existsSync as existsSync3 } from "node:fs";
2575
2873
  import { readdir, readFile, stat } from "node:fs/promises";
2576
- 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";
2577
2875
  var JS = /\.(m?js|cjs)$/i;
2578
2876
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".svn", ".hg"]);
2579
- async function walk(dir) {
2877
+ async function walk2(dir) {
2580
2878
  const out = [];
2581
2879
  for (const entry of await readdir(dir, { withFileTypes: true })) {
2582
- const full = join2(dir, entry.name);
2880
+ const full = join3(dir, entry.name);
2583
2881
  if (entry.isDirectory()) {
2584
- if (!SKIP_DIRS.has(entry.name)) out.push(...await walk(full));
2882
+ if (!SKIP_DIRS.has(entry.name)) out.push(...await walk2(full));
2585
2883
  } else if (entry.isFile()) out.push(full);
2586
2884
  }
2587
2885
  return out.sort();
@@ -2641,7 +2939,7 @@ async function findPairs(paths) {
2641
2939
  const st = await stat(abs).catch(() => void 0);
2642
2940
  if (!st) throw new Error(`No such file or directory: ${p}`);
2643
2941
  const root = st.isDirectory() ? abs : dirname2(abs);
2644
- const files = st.isDirectory() ? await walk(abs) : [abs];
2942
+ const files = st.isDirectory() ? await walk2(abs) : [abs];
2645
2943
  for (const js of files.filter((f) => JS.test(f))) {
2646
2944
  if (seen.has(js)) continue;
2647
2945
  seen.add(js);
@@ -2650,16 +2948,16 @@ async function findPairs(paths) {
2650
2948
  let map;
2651
2949
  if (url && !url.startsWith("data:")) {
2652
2950
  const candidate = resolve3(dirname2(js), decodeURIComponent(url.split(/[?#]/)[0]));
2653
- if (existsSync2(candidate)) map = candidate;
2951
+ if (existsSync3(candidate)) map = candidate;
2654
2952
  }
2655
- if (!map && existsSync2(`${js}.map`)) map = `${js}.map`;
2953
+ if (!map && existsSync3(`${js}.map`)) map = `${js}.map`;
2656
2954
  pairs2.push({ js, map, root });
2657
2955
  }
2658
2956
  }
2659
2957
  return pairs2;
2660
2958
  }
2661
2959
  function artifactName(file, root, prefix) {
2662
- const rel = relative(root, file).split(sep).join("/");
2960
+ const rel = relative2(root, file).split(sep2).join("/");
2663
2961
  const p = prefix.endsWith("/") ? prefix : `${prefix}/`;
2664
2962
  return `${p}${rel}`;
2665
2963
  }
@@ -2941,8 +3239,8 @@ skipped, no source map: ${d.skipped.join(", ")}` : ""}`,
2941
3239
  }
2942
3240
  };
2943
3241
  function uuidFromDebugMeta(path) {
2944
- if (!existsSync3(path)) throw usageError("bad_path", `No such file: ${path}`);
2945
- 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"));
2946
3244
  if (!m) throw usageError("bad_value", `No ProguardUuids entry in ${path}`);
2947
3245
  return m[1].toLowerCase();
2948
3246
  }
@@ -2967,7 +3265,7 @@ var proguardUpload = {
2967
3265
  output: "{id, state, files[{name, kind, state}], chunks{}, bytes{}, uuid}",
2968
3266
  async run(ctx) {
2969
3267
  const i = ctx.input;
2970
- 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}`);
2971
3269
  const data = await readFile2(i.mapping);
2972
3270
  if (!data.length) throw usageError("bad_value", `${i.mapping} is empty.`);
2973
3271
  const uuid = (i.uuid ?? (i.debugMeta ? uuidFromDebugMeta(i.debugMeta) : proguardUuid(data))).toLowerCase();
@@ -3004,7 +3302,7 @@ var artifacts_default = [sourcemapsInject, sourcemapsUpload, proguardUpload, art
3004
3302
 
3005
3303
  // src/commands/tokens.ts
3006
3304
  var SCOPE_HINT = "e.g. org:read,project:read,event:read,event:write,project:write,project:releases,project:admin";
3007
- var list5 = {
3305
+ var list6 = {
3008
3306
  path: ["tokens", "list"],
3009
3307
  aliases: [["tokens"]],
3010
3308
  summary: "Your tokens, newest first, without their values.",
@@ -3117,7 +3415,7 @@ var self = {
3117
3415
  return ctx.api.get("tokens/self");
3118
3416
  }
3119
3417
  };
3120
- var tokens_default = [list5, self, create2, update, remove, rotate2];
3418
+ var tokens_default = [list6, self, create2, update, remove, rotate2];
3121
3419
 
3122
3420
  // src/commands/support.ts
3123
3421
  var SUPPORT = {
@@ -3154,33 +3452,33 @@ var support = {
3154
3452
  var support_default = [support];
3155
3453
 
3156
3454
  // src/commands/init.ts
3157
- import { existsSync as existsSync5, readFileSync as readFileSync6 } from "node:fs";
3455
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
3158
3456
  import { appendFile, writeFile as writeFile3 } from "node:fs/promises";
3159
- import { join as join4, resolve as resolve4 } from "node:path";
3457
+ import { join as join5, resolve as resolve4 } from "node:path";
3160
3458
 
3161
3459
  // src/lib/detect.ts
3162
- import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync } from "node:fs";
3163
- 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";
3164
3462
  function readJson(path) {
3165
3463
  try {
3166
- return JSON.parse(readFileSync5(path, "utf8"));
3464
+ return JSON.parse(readFileSync6(path, "utf8"));
3167
3465
  } catch {
3168
3466
  return void 0;
3169
3467
  }
3170
3468
  }
3171
3469
  function has(dir, ...names) {
3172
- return names.find((n) => existsSync4(join3(dir, n)));
3470
+ return names.find((n) => existsSync5(join4(dir, n)));
3173
3471
  }
3174
3472
  function slugify(s) {
3175
3473
  return s.toLowerCase().replace(/^@[^/]+\//, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
3176
3474
  }
3177
3475
  function detect(dir, guideBase) {
3178
3476
  const evidence = [];
3179
- const composer = readJson(join3(dir, "composer.json"));
3180
- const pkg = readJson(join3(dir, "package.json"));
3477
+ const composer = readJson(join4(dir, "composer.json"));
3478
+ const pkg = readJson(join4(dir, "package.json"));
3181
3479
  const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
3182
3480
  const hasDep = (d) => d in deps;
3183
- 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";
3184
3482
  const guide = (slug) => `${guideBase}/${slug}.md`;
3185
3483
  if (composer?.require?.["laravel/framework"]) {
3186
3484
  evidence.push("composer.json requires laravel/framework");
@@ -3235,8 +3533,8 @@ function detect(dir, guideBase) {
3235
3533
  }
3236
3534
  const py = has(dir, "pyproject.toml", "requirements.txt", "Pipfile", "manage.py");
3237
3535
  if (py) {
3238
- const text = ["pyproject.toml", "requirements.txt", "Pipfile"].map((f) => existsSync4(join3(dir, f)) ? readFileSync5(join3(dir, f), "utf8") : "").join("\n").toLowerCase();
3239
- 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";
3240
3538
  evidence.push(`${py} (${fw})`);
3241
3539
  return { platform: fw === "python" ? "python" : `python-${fw}`, framework: fw, surfaces: ["backend"], env: { backend: "SENTRY_DSN" }, guide: guide("python-sentry-sdk"), name, evidence };
3242
3540
  }
@@ -3245,7 +3543,7 @@ function detect(dir, guideBase) {
3245
3543
  evidence.push(gradle);
3246
3544
  return { platform: "android", framework: "android", surfaces: ["mobile"], env: {}, guide: guide("mobile"), name, evidence };
3247
3545
  }
3248
- 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"));
3249
3547
  if (apple) {
3250
3548
  evidence.push(apple);
3251
3549
  return { platform: "apple-ios", framework: "ios", surfaces: ["mobile"], env: {}, guide: guide("mobile"), name, evidence };
@@ -3296,14 +3594,14 @@ ${d.steps.map((s, i) => `${i + 1}. ${s.detail}${s.run ? `
3296
3594
  async run(ctx) {
3297
3595
  const i = ctx.input;
3298
3596
  const dir = resolve4(i.dir);
3299
- 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}`);
3300
3598
  const found = detect(dir, ctx.config.url);
3301
3599
  const platform = i.platform ?? found.platform;
3302
3600
  const surfaces = i.surfaces ?? found.surfaces;
3303
3601
  const name = i.name ?? found.name;
3304
3602
  const detected = { platform, framework: found.framework, surfaces, evidence: found.evidence, name };
3305
3603
  const steps = [];
3306
- 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;
3307
3605
  if (i.dryRun) {
3308
3606
  steps.push({ id: "create", run: `tracksiy init${i.writeEnv ? " --write-env" : ""}`, detail: "Create the project and keys." });
3309
3607
  return { dryRun: true, detected, guide: found.guide, steps };
@@ -3315,7 +3613,7 @@ ${d.steps.map((s, i) => `${i + 1}. ${s.detail}${s.run ? `
3315
3613
  compact({ name, platform, surfaces, sameHost: i.sameHost })
3316
3614
  );
3317
3615
  const project2 = prov.project;
3318
- const pfPath = join4(dir, PROJECT_FILE);
3616
+ const pfPath = join5(dir, PROJECT_FILE);
3319
3617
  const pf = { org: project2.organization, project: project2.slug };
3320
3618
  if (ctx.config.url !== DEFAULT_URL) pf.url = ctx.config.url;
3321
3619
  await writeFile3(pfPath, JSON.stringify(pf, null, 2) + "\n");
@@ -3327,11 +3625,11 @@ ${d.steps.map((s, i) => `${i + 1}. ${s.detail}${s.run ? `
3327
3625
  }));
3328
3626
  let env;
3329
3627
  if (i.writeEnv) {
3330
- const envPath = join4(dir, ".env");
3331
- if (!existsSync5(envPath)) {
3628
+ const envPath = join5(dir, ".env");
3629
+ if (!existsSync6(envPath)) {
3332
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.");
3333
3631
  }
3334
- const current = readFileSync6(envPath, "utf8");
3632
+ const current = readFileSync7(envPath, "utf8");
3335
3633
  const added = [];
3336
3634
  const kept = [];
3337
3635
  let block = "";
@@ -3353,7 +3651,7 @@ ${block}`);
3353
3651
  if (!i.writeEnv && keys.some((k) => k.env)) {
3354
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).` });
3355
3653
  }
3356
- 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 });
3357
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" });
3358
3656
  steps.push({ id: "verify", detail: "Deploy, trigger one error, then prove it arrived.", run: "tracksiy projects verify --wait 120" });
3359
3657
  return {
@@ -3386,7 +3684,7 @@ function camel(name) {
3386
3684
  }
3387
3685
 
3388
3686
  // src/core/args.ts
3389
- import { readFileSync as readFileSync7 } from "node:fs";
3687
+ import { readFileSync as readFileSync8 } from "node:fs";
3390
3688
  var GLOBAL_FLAGS = {
3391
3689
  org: { type: "string", value: "<slug>", description: "Organization slug. Env: TRACKSIY_ORG. Default: .tracksiy.json, saved login, or your only organization." },
3392
3690
  project: { type: "string", value: "<slug>", description: "Project slug, or org/project. Env: TRACKSIY_PROJECT. Default: .tracksiy.json." },
@@ -3403,7 +3701,7 @@ var GLOBAL_FLAGS = {
3403
3701
  };
3404
3702
  var stdinCache;
3405
3703
  function readStdin() {
3406
- stdinCache ??= readFileSync7(0, "utf8");
3704
+ stdinCache ??= readFileSync8(0, "utf8");
3407
3705
  return stdinCache;
3408
3706
  }
3409
3707
  function parseJsonValue(name, raw) {
@@ -3411,7 +3709,7 @@ function parseJsonValue(name, raw) {
3411
3709
  if (raw === "-") text = readStdin();
3412
3710
  else if (raw.startsWith("@")) {
3413
3711
  try {
3414
- text = readFileSync7(raw.slice(1), "utf8");
3712
+ text = readFileSync8(raw.slice(1), "utf8");
3415
3713
  } catch (err) {
3416
3714
  throw usageError("bad_value", `--${name}: cannot read ${raw.slice(1)}: ${err.message}`);
3417
3715
  }
@@ -3781,15 +4079,15 @@ function makeContext(spec, input, globals, surface, io) {
3781
4079
  const org = () => {
3782
4080
  orgPromise ??= (async () => {
3783
4081
  if (config.org) return config.org;
3784
- const list6 = await visible();
3785
- if (list6.length === 1) {
3786
- log(`using organization ${list6[0].slug}`);
3787
- 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;
3788
4086
  }
3789
4087
  throw usageError(
3790
4088
  "missing_org",
3791
- list6.length ? "Which organization? This token can see several." : "This token cannot see any organization.",
3792
- 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`."
3793
4091
  );
3794
4092
  })();
3795
4093
  return orgPromise;
@@ -3799,8 +4097,8 @@ function makeContext(spec, input, globals, surface, io) {
3799
4097
  projectPromise ??= (async () => {
3800
4098
  const o = await org();
3801
4099
  if (config.project) return { org: o, project: config.project };
3802
- const list6 = await visible();
3803
- const projects2 = list6.find((x) => x.slug === o)?.projects ?? [];
4100
+ const list7 = await visible();
4101
+ const projects2 = list7.find((x) => x.slug === o)?.projects ?? [];
3804
4102
  if (projects2.length === 1) {
3805
4103
  log(`using project ${o}/${projects2[0].slug}`);
3806
4104
  return { org: o, project: projects2[0].slug };
@@ -4025,6 +4323,7 @@ var COMMANDS = [
4025
4323
  ...mobile_default,
4026
4324
  ...logs_default,
4027
4325
  ...evidence_default,
4326
+ ...builds_default,
4028
4327
  ...issues_default,
4029
4328
  ...projects_default,
4030
4329
  ...alerts_default,
@@ -4039,7 +4338,7 @@ var COMMANDS = [
4039
4338
  var GROUPS = [
4040
4339
  ["Start", "init auth onboarding describe"],
4041
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"],
4042
- ["Ship", "sourcemaps proguard"],
4341
+ ["Ship", "sourcemaps proguard build builds"],
4043
4342
  ["Manage", "tokens"],
4044
4343
  ["Talk to us", "support"],
4045
4344
  ["Other", "api mcp"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tracksiy",
3
- "version": "0.12.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": {