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 +3 -0
- package/dist/tracksiy.mjs +355 -56
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
|
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 = [
|
|
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(
|
|
2178
|
-
return (
|
|
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
|
|
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 = [
|
|
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
|
|
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 = [
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2877
|
+
async function walk2(dir) {
|
|
2580
2878
|
const out = [];
|
|
2581
2879
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
2582
|
-
const full =
|
|
2880
|
+
const full = join3(dir, entry.name);
|
|
2583
2881
|
if (entry.isDirectory()) {
|
|
2584
|
-
if (!SKIP_DIRS.has(entry.name)) out.push(...await
|
|
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
|
|
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 (
|
|
2951
|
+
if (existsSync3(candidate)) map = candidate;
|
|
2654
2952
|
}
|
|
2655
|
-
if (!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 =
|
|
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 (!
|
|
2945
|
-
const m = /ProguardUuids\s*[=:]\s*([0-9a-fA-F-]{36})/.exec(
|
|
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 (!
|
|
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
|
|
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 = [
|
|
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
|
|
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
|
|
3457
|
+
import { join as join5, resolve as resolve4 } from "node:path";
|
|
3160
3458
|
|
|
3161
3459
|
// src/lib/detect.ts
|
|
3162
|
-
import { existsSync as
|
|
3163
|
-
import { basename, join as
|
|
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(
|
|
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) =>
|
|
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(
|
|
3180
|
-
const pkg = readJson(
|
|
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() ??
|
|
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) =>
|
|
3239
|
-
const fw =
|
|
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") ??
|
|
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 (!
|
|
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
|
|
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 =
|
|
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 =
|
|
3331
|
-
if (!
|
|
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 =
|
|
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 (
|
|
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
|
|
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 ??=
|
|
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 =
|
|
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
|
|
3785
|
-
if (
|
|
3786
|
-
log(`using organization ${
|
|
3787
|
-
return
|
|
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
|
-
|
|
3792
|
-
|
|
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
|
|
3803
|
-
const projects2 =
|
|
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"]
|