tracksiy 0.1.1 → 0.3.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 +7 -2
- package/dist/tracksiy.mjs +213 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,10 +128,15 @@ Labels: `bug`, `feature`, `question`, `blocker`. `npx tracksiy support` prints t
|
|
|
128
128
|
| `performance` | Is this project slow, and did it get slower? Transactions compared with the previous window, plus uptime. | read | `performance` |
|
|
129
129
|
| `issues list` | Recent issues of a project, newest activity first, with the frames that matter inlined. | read | `issues_list` |
|
|
130
130
|
| `issues view <id>` | One issue in enough depth to act on: latest event, stack frames, tags, recent volume. | read | `issues_view` |
|
|
131
|
-
| `issues resolve <ids...>` | Mark issues resolved
|
|
132
|
-
| `issues ignore <ids...>` | Mark issues ignored: they stop alerting until unignored. | write | `issues_ignore` |
|
|
131
|
+
| `issues resolve <ids...>` | Mark issues resolved, optionally fixed in a release so old clients do not reopen them. Records why. | write | `issues_resolve` |
|
|
132
|
+
| `issues ignore <ids...>` | Mark issues ignored: they stop alerting until unignored. Records why. | write | `issues_ignore` |
|
|
133
133
|
| `issues unresolve <ids...>` | Reopen issues. | write | `issues_unresolve` |
|
|
134
134
|
| `issues bulk-status` | Change the status of every issue matching a filter. Dry run unless --apply. | write | `issues_bulk_status` |
|
|
135
|
+
| `issues comment <id>` | Leave a comment on an issue for the next agent or human: what you found, what you tried. | write | `issues_comment` |
|
|
136
|
+
| `issues activity <id>` | Who did what to an issue and why: resolutions (and in which release), regressions, comments, claims. | read | `issues_activity` |
|
|
137
|
+
| `issues verify-fixed <id>` | Did the fix hold? Judged by the fix release's own traffic: fixed, regressed or undetermined. Exit 10 unless fixed. | read | `issues_verify_fixed` |
|
|
138
|
+
| `issues claim <id>` | Take a lease on an issue so no other agent works on it meanwhile. Lapses on its own. | write | `issues_claim` |
|
|
139
|
+
| `issues unclaim <id>` | Give back your lease on an issue. | write | `issues_unclaim` |
|
|
135
140
|
| `projects create <name>` | Everything a new app needs in one idempotent call: the project, an ingest key per surface, an upload token, and the next steps. | write | `projects_create` |
|
|
136
141
|
| `projects verify` | Did the integration actually work? Checks events, releases, environments, source maps, privacy and alerting. Exit 10 if not. | read | `projects_verify` |
|
|
137
142
|
| `onboarding` | What to know before integrating an app. No token needed. | read | `onboarding` |
|
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.3.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
|
|
@@ -115,6 +115,7 @@ var Api = class {
|
|
|
115
115
|
const headers = { "User-Agent": USER_AGENT, Accept: "application/json" };
|
|
116
116
|
const useAuth = opts.auth !== false;
|
|
117
117
|
if (useAuth && this.opts.token) headers.Authorization = `Bearer ${this.opts.token}`;
|
|
118
|
+
if (this.opts.agent) headers["X-Tracksiy-Agent"] = this.opts.agent;
|
|
118
119
|
let body;
|
|
119
120
|
if (opts.form) body = opts.form;
|
|
120
121
|
else if (opts.body !== void 0) {
|
|
@@ -375,6 +376,8 @@ function resolveConfig(globals, env = process.env, cwd = process.cwd()) {
|
|
|
375
376
|
verbose: Boolean(globals.verbose),
|
|
376
377
|
quiet: Boolean(globals.quiet)
|
|
377
378
|
};
|
|
379
|
+
const agent = (globals.as ?? env.TRACKSIY_AGENT ?? "").trim();
|
|
380
|
+
if (agent) out.agent = agent.slice(0, 100);
|
|
378
381
|
if (pf) out.projectFile = pf.path;
|
|
379
382
|
if (globals.token) [out.token, out.tokenSource] = [globals.token, "--token"];
|
|
380
383
|
else if (env.TRACKSIY_TOKEN) [out.token, out.tokenSource] = [env.TRACKSIY_TOKEN, "TRACKSIY_TOKEN"];
|
|
@@ -641,6 +644,7 @@ function missingToken(url) {
|
|
|
641
644
|
var auth_default = [login, status, logout];
|
|
642
645
|
|
|
643
646
|
// src/commands/_util.ts
|
|
647
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
644
648
|
async function projectPath(ctx, suffix = "") {
|
|
645
649
|
const { org, project: project2 } = await ctx.project();
|
|
646
650
|
return `projects/${seg(org)}/${seg(project2)}${suffix ? `/${suffix}` : ""}`;
|
|
@@ -679,6 +683,26 @@ function rate(value, flag) {
|
|
|
679
683
|
function compact(obj) {
|
|
680
684
|
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
|
|
681
685
|
}
|
|
686
|
+
function textArg(value, flag) {
|
|
687
|
+
if (value === void 0 || value === null) return void 0;
|
|
688
|
+
const raw = String(value);
|
|
689
|
+
if (raw === "-") return readFileSync3(0, "utf8");
|
|
690
|
+
if (raw.startsWith("@")) {
|
|
691
|
+
try {
|
|
692
|
+
return readFileSync3(raw.slice(1), "utf8");
|
|
693
|
+
} catch (err) {
|
|
694
|
+
throw usageError("bad_value", `--${flag}: cannot read ${raw.slice(1)}: ${err.message}`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return raw;
|
|
698
|
+
}
|
|
699
|
+
function duration(value, flag) {
|
|
700
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
701
|
+
const s = String(value).trim();
|
|
702
|
+
const m = /^(\d+)\s*([smhdw])?$/.exec(s);
|
|
703
|
+
if (!m) throw usageError("bad_value", `--${flag} expects a duration like 30m / 2h / 1d, got ${JSON.stringify(s)}`);
|
|
704
|
+
return Number(m[1]) * (m[2] ? UNITS[m[2]] : 1);
|
|
705
|
+
}
|
|
682
706
|
|
|
683
707
|
// src/commands/fleet.ts
|
|
684
708
|
var orgs = {
|
|
@@ -920,6 +944,26 @@ var FILTERS = {
|
|
|
920
944
|
"first-seen-since": { type: "string", value: TIME, description: "First seen at or after this: new issues only." },
|
|
921
945
|
"first-seen-until": { type: "string", value: TIME, description: "First seen before this." }
|
|
922
946
|
};
|
|
947
|
+
var WHY = {
|
|
948
|
+
reason: { type: "string", enum: ["fixed", "duplicate", "third_party", "expected", "wont_fix"], description: "Why, recorded on the issue's activity log." },
|
|
949
|
+
note: { type: "string", value: "<md|@file|->", description: "A note for the next agent (markdown), kept on the activity log." },
|
|
950
|
+
ref: { type: "string", value: "<sha|url>", description: "The evidence: a commit, PR or URL." }
|
|
951
|
+
};
|
|
952
|
+
var FIXED_IN = {
|
|
953
|
+
"in-release": { type: "string", value: "<version>", description: "Fixed in this release: events from older releases count as stale, not as a regression. Must exist (releases declare)." },
|
|
954
|
+
"in-commit": { type: "string", value: "<sha>", description: "Fixed in the release that carries this commit (its ref, or a version ending @<sha>); bound when that release appears." },
|
|
955
|
+
"in-next-release": { type: "boolean", description: "Fixed in whatever is released next: only newer releases can reopen it." }
|
|
956
|
+
};
|
|
957
|
+
function whyBody(input) {
|
|
958
|
+
return compact({
|
|
959
|
+
inRelease: input.inRelease,
|
|
960
|
+
inCommit: input.inCommit,
|
|
961
|
+
inNextRelease: input.inNextRelease || void 0,
|
|
962
|
+
reason: input.reason,
|
|
963
|
+
note: textArg(input.note, "note"),
|
|
964
|
+
ref: input.ref
|
|
965
|
+
});
|
|
966
|
+
}
|
|
923
967
|
function filterQuery(input) {
|
|
924
968
|
return compact({
|
|
925
969
|
status: input.status,
|
|
@@ -952,14 +996,17 @@ var list = {
|
|
|
952
996
|
...FILTERS,
|
|
953
997
|
sort: { type: "string", enum: ["lastSeen", "firstSeen", "count"], description: "Order. Default lastSeen." },
|
|
954
998
|
limit: { type: "integer", value: "<n>", description: "How many issues (server default 25)." },
|
|
955
|
-
frames: { type: "integer", value: "<n>", description: "In-app stack frames inlined per issue (default 3, 0 for none)." }
|
|
999
|
+
frames: { type: "integer", value: "<n>", description: "In-app stack frames inlined per issue (default 3, 0 for none)." },
|
|
1000
|
+
unclaimed: { type: "boolean", description: "Only issues nobody holds a live claim on: what you can pick up." },
|
|
1001
|
+
"claimed-by": { type: "string", value: "<handle>", description: "Only issues claimed by this handle." }
|
|
956
1002
|
},
|
|
957
1003
|
examples: [
|
|
958
1004
|
{ command: "tracksiy issues list --since 24h", description: "Unresolved issues seen in the last day." },
|
|
1005
|
+
{ command: "tracksiy issues list --unclaimed --limit 5", description: "Five issues no other agent is working on." },
|
|
959
1006
|
{ command: "tracksiy issues list --status regressed --select issues.id,issues.title", description: "Only regressions, only ids and titles." },
|
|
960
1007
|
{ command: "tracksiy issues list --first-seen-since 2h --environment production", description: "What is new in production." }
|
|
961
1008
|
],
|
|
962
|
-
output: "{project, issueCount, total, hasMore, issues:[{id, shortId, title, culprit, level, status, count, firstSeen, lastSeen, regressedAt, timesRegressed, permalink, frames?}]}",
|
|
1009
|
+
output: "{project, issueCount, total, hasMore, issues:[{id, shortId, title, culprit, level, status, count, firstSeen, lastSeen, regressedAt, timesRegressed, resolution?, staleEvents, claimedBy?, claimedUntil?, permalink, frames?}]}",
|
|
963
1010
|
text: (d) => `${d.issueCount ?? d.issues?.length ?? 0} of ${d.total ?? "?"} issues${d.hasMore ? " (more: raise --limit)" : ""}
|
|
964
1011
|
${issueRows(d.issues)}`,
|
|
965
1012
|
async run(ctx) {
|
|
@@ -967,7 +1014,9 @@ ${issueRows(d.issues)}`,
|
|
|
967
1014
|
...filterQuery(ctx.input),
|
|
968
1015
|
sort: ctx.input.sort,
|
|
969
1016
|
limit: ctx.input.limit,
|
|
970
|
-
frames: ctx.input.frames
|
|
1017
|
+
frames: ctx.input.frames,
|
|
1018
|
+
unclaimed: ctx.input.unclaimed || void 0,
|
|
1019
|
+
claimedBy: ctx.input.claimedBy
|
|
971
1020
|
});
|
|
972
1021
|
}
|
|
973
1022
|
};
|
|
@@ -980,7 +1029,7 @@ var view = {
|
|
|
980
1029
|
args: [{ name: "id", required: true, description: "Issue id (numeric, from `issues list`)." }],
|
|
981
1030
|
flags: { frames: { type: "integer", value: "<n>", description: "How many frames to include." } },
|
|
982
1031
|
examples: [{ command: "tracksiy issues view 4812", description: "Everything needed to start fixing issue 4812." }],
|
|
983
|
-
output: "{id, title, culprit, level, status, count, firstSeen, lastSeen, latestEvent{release, environment, exceptionType, exceptionValue, contexts}, frames[{filename, function, lineno, contextLine, inApp}], tags[], eventsLast24h, eventsLast7d, permalink}",
|
|
1032
|
+
output: "{id, title, culprit, level, status, count, firstSeen, lastSeen, resolution?, staleEvents, claimedBy?, latestEvent{release, environment, exceptionType, exceptionValue, contexts}, frames[{filename, function, lineno, contextLine, inApp}], tags[], eventsLast24h, eventsLast7d, activity[], recentComments[], permalink}",
|
|
984
1033
|
text: (d) => {
|
|
985
1034
|
const head = kv(d, ["id", "shortId", "title", "culprit", "level", "status", "count", "eventsLast24h", "eventsLast7d", "firstSeen", "lastSeen", "regressedAt", "permalink"]);
|
|
986
1035
|
const ev = d.latestEvent ? `
|
|
@@ -988,15 +1037,22 @@ latest event:
|
|
|
988
1037
|
${kv(d.latestEvent, ["release", "environment", "exceptionType", "exceptionValue", "transaction", "platform"]).replace(/^/gm, " ")}` : "";
|
|
989
1038
|
const frames = (d.frames ?? []).map((f) => ` ${f.inApp ? "*" : " "} ${f.filename ?? f.module ?? "?"}:${f.lineno ?? "?"} in ${f.function ?? "?"}${f.contextLine ? `
|
|
990
1039
|
${String(f.contextLine).trim()}` : ""}`).join("\n");
|
|
991
|
-
|
|
1040
|
+
const res = d.resolution ? `
|
|
1041
|
+
resolved ${d.resolution.type.replace(/_/g, " ")}: ${d.resolution.release ?? d.resolution.commit ?? "-"}${d.resolution.pending ? " (pending)" : ""}; ${d.staleEvents ?? 0} stale event(s)` : "";
|
|
1042
|
+
const claim2 = d.claimedBy ? `
|
|
1043
|
+
claimed by ${d.claimedBy} until ${d.claimedUntil}` : "";
|
|
1044
|
+
const act = (d.activity ?? []).slice(0, 5).map((a) => ` ${ago(a.at)} ${a.action}${a.reason ? ` (${a.reason})` : ""} by ${a.actor ?? a.user ?? a.via}${a.note ? `: ${String(a.note).split("\n")[0].slice(0, 100)}` : ""}`).join("\n");
|
|
1045
|
+
return `${head}${res}${claim2}${ev}${frames ? `
|
|
992
1046
|
frames (* = in app):
|
|
993
|
-
${frames}` : ""}
|
|
1047
|
+
${frames}` : ""}${act ? `
|
|
1048
|
+
activity:
|
|
1049
|
+
${act}` : ""}`;
|
|
994
1050
|
},
|
|
995
1051
|
async run(ctx) {
|
|
996
1052
|
return ctx.api.get(await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}`), { frames: ctx.input.frames });
|
|
997
1053
|
}
|
|
998
1054
|
};
|
|
999
|
-
function statusCommand(name, status2, summary) {
|
|
1055
|
+
function statusCommand(name, status2, summary, examples) {
|
|
1000
1056
|
return {
|
|
1001
1057
|
path: ["issues", name],
|
|
1002
1058
|
summary,
|
|
@@ -1005,19 +1061,139 @@ function statusCommand(name, status2, summary) {
|
|
|
1005
1061
|
idempotent: true,
|
|
1006
1062
|
endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/issues/{issue_id}/status" },
|
|
1007
1063
|
args: [{ name: "ids", required: true, variadic: true, description: "One or more issue ids." }],
|
|
1008
|
-
|
|
1009
|
-
|
|
1064
|
+
flags: name === "resolve" ? { ...FIXED_IN, ...WHY } : { ...WHY },
|
|
1065
|
+
examples,
|
|
1066
|
+
output: "One id: the issue's new state with changed, requested, applied. Several: {matched, updated, alreadyInStatus, resolution, dryRun:false}.",
|
|
1010
1067
|
async run(ctx) {
|
|
1011
1068
|
const ids = ctx.input.ids;
|
|
1012
1069
|
for (const id of ids) if (!/^\d+$/.test(id)) throw usageError("bad_value", `Issue ids are numeric; got ${JSON.stringify(id)}`);
|
|
1013
|
-
|
|
1014
|
-
return ctx.api.send("POST", await projectPath(ctx,
|
|
1070
|
+
const why = whyBody(ctx.input);
|
|
1071
|
+
if (ids.length === 1) return ctx.api.send("POST", await projectPath(ctx, `issues/${ids[0]}/status`), { status: status2, ...why });
|
|
1072
|
+
return ctx.api.send("POST", await projectPath(ctx, "issues/status"), { status: status2, ids, dryRun: false, ...why });
|
|
1015
1073
|
}
|
|
1016
1074
|
};
|
|
1017
1075
|
}
|
|
1018
|
-
var resolve2 = statusCommand(
|
|
1019
|
-
|
|
1020
|
-
|
|
1076
|
+
var resolve2 = statusCommand(
|
|
1077
|
+
"resolve",
|
|
1078
|
+
"resolved",
|
|
1079
|
+
"Mark issues resolved, optionally fixed in a release so old clients do not reopen them. Records why.",
|
|
1080
|
+
[
|
|
1081
|
+
{ command: "tracksiy issues resolve 4812 --in-release web@$(git rev-parse HEAD) --reason fixed --ref $(git rev-parse HEAD)", description: "Fixed in this deploy; old bundles count as stale." },
|
|
1082
|
+
{ command: "tracksiy issues resolve 4812 --in-next-release --note @why.md", description: "Fixed in whatever ships next, with the reasoning." },
|
|
1083
|
+
{ command: "tracksiy issues resolve 5562 --reason third_party --note 'WebView injects addEL_hook'", description: "Not our code: closed with the reason on record." }
|
|
1084
|
+
]
|
|
1085
|
+
);
|
|
1086
|
+
var ignore = statusCommand("ignore", "ignored", "Mark issues ignored: they stop alerting until unignored. Records why.", [
|
|
1087
|
+
{ command: "tracksiy issues ignore 4812 --reason expected --note 'Bots hitting a removed URL'", description: "Ignore, and say why for the next agent." }
|
|
1088
|
+
]);
|
|
1089
|
+
var unresolve = statusCommand("unresolve", "unresolved", "Reopen issues.", [{ command: "tracksiy issues unresolve 4812 --note 'Came back on Android'", description: "Reopen with a note." }]);
|
|
1090
|
+
var comment = {
|
|
1091
|
+
path: ["issues", "comment"],
|
|
1092
|
+
summary: "Leave a comment on an issue for the next agent or human: what you found, what you tried.",
|
|
1093
|
+
context: "project",
|
|
1094
|
+
effect: "write",
|
|
1095
|
+
endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/issues/{issue_id}/comments" },
|
|
1096
|
+
args: [{ name: "id", required: true, description: "Issue id." }],
|
|
1097
|
+
flags: { text: { type: "string", required: true, value: "<md|@file|->", description: "The comment (markdown). @file reads a file, - reads stdin." } },
|
|
1098
|
+
examples: [{ command: "tracksiy issues comment 4812 --text @findings.md", description: "Attach what you found." }],
|
|
1099
|
+
output: "{id, created, user, text}",
|
|
1100
|
+
async run(ctx) {
|
|
1101
|
+
return ctx.api.send("POST", await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}/comments`), { text: textArg(ctx.input.text, "text") });
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
var activity = {
|
|
1105
|
+
path: ["issues", "activity"],
|
|
1106
|
+
summary: "Who did what to an issue and why: resolutions (and in which release), regressions, comments, claims.",
|
|
1107
|
+
context: "project",
|
|
1108
|
+
effect: "read",
|
|
1109
|
+
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/issues/{issue_id}/activity" },
|
|
1110
|
+
args: [{ name: "id", required: true, description: "Issue id." }],
|
|
1111
|
+
flags: { limit: { type: "integer", value: "<n>", description: "How many entries (1..500, default 100)." } },
|
|
1112
|
+
examples: [{ command: "tracksiy issues activity 4812", description: "The issue's history, newest first." }],
|
|
1113
|
+
output: "{issue, activity:[{at, action, via, actor, user, reason, note, ref, data}], comments:[{id, created, user, text}]}",
|
|
1114
|
+
text: (d) => table(d.activity ?? [], [
|
|
1115
|
+
["when", (r) => ago(r.at)],
|
|
1116
|
+
"action",
|
|
1117
|
+
["by", (r) => r.actor ?? r.user ?? r.via],
|
|
1118
|
+
["reason", (r) => r.reason ?? ""],
|
|
1119
|
+
["note", (r) => String(r.note ?? (r.data?.release ? `release ${r.data.release}` : "")).split("\n")[0].slice(0, 80)]
|
|
1120
|
+
]),
|
|
1121
|
+
async run(ctx) {
|
|
1122
|
+
return ctx.api.get(await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}/activity`), { limit: ctx.input.limit });
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
var verifyFixed = {
|
|
1126
|
+
path: ["issues", "verify-fixed"],
|
|
1127
|
+
summary: "Did the fix hold? Judged by the fix release's own traffic: fixed, regressed or undetermined. Exit 10 unless fixed.",
|
|
1128
|
+
description: "regressed: the issue has an event from the fix release or a newer one. undetermined: the fix release has had too little traffic (--min-traffic), or the issue's old rate predicts fewer than 3 occurrences in that traffic. fixed otherwise. The fix release defaults to the one the issue was resolved in. --wait polls until the verdict is not undetermined.",
|
|
1129
|
+
context: "project",
|
|
1130
|
+
effect: "read",
|
|
1131
|
+
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/issues/{issue_id}/verify" },
|
|
1132
|
+
args: [{ name: "id", required: true, description: "Issue id." }],
|
|
1133
|
+
flags: {
|
|
1134
|
+
release: { type: "string", value: "<version>", description: "The fix release. Default: the one it was resolved in." },
|
|
1135
|
+
"min-traffic": { type: "integer", value: "<n>", description: "Transactions + errors + sessions from the fix release needed before silence counts (default 100)." },
|
|
1136
|
+
wait: { type: "integer", value: "<seconds>", description: "Poll every 30 s until fixed or regressed, up to this long." }
|
|
1137
|
+
},
|
|
1138
|
+
examples: [
|
|
1139
|
+
{ command: "tracksiy issues verify-fixed 4812", description: "Verdict now." },
|
|
1140
|
+
{ command: "tracksiy issues verify-fixed 4812 --wait 1800", description: "In a deploy script: wait up to 30 min for enough traffic." }
|
|
1141
|
+
],
|
|
1142
|
+
output: "{verdict, reason, release, since, eventsSince, staleEvents, traffic{transactions, errors, sessions, total}, baselinePer1000, expectedEvents, latestEvent?}",
|
|
1143
|
+
text: (d) => kv(d, ["verdict", "reason", "release", "since", "eventsSince", "staleEvents", "expectedEvents"]) + `
|
|
1144
|
+
traffic: ${d.traffic?.total ?? 0} (${d.traffic?.transactions ?? 0} transactions, ${d.traffic?.errors ?? 0} errors, ${d.traffic?.sessions ?? 0} sessions)`,
|
|
1145
|
+
async run(ctx) {
|
|
1146
|
+
const path = await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}/verify`);
|
|
1147
|
+
const query = { release: ctx.input.release, minTraffic: ctx.input.minTraffic };
|
|
1148
|
+
const deadline = Date.now() + (ctx.input.wait ?? 0) * 1e3;
|
|
1149
|
+
let data = await ctx.api.get(path, query);
|
|
1150
|
+
while (data?.verdict === "undetermined" && Date.now() + 3e4 <= deadline) {
|
|
1151
|
+
ctx.log(`undetermined: ${data.reason} -- checking again in 30 s`);
|
|
1152
|
+
await new Promise((r) => setTimeout(r, 3e4));
|
|
1153
|
+
data = await ctx.api.get(path, query);
|
|
1154
|
+
}
|
|
1155
|
+
if (data?.verdict !== "fixed") throw new CheckFailed(`${data?.verdict ?? "unknown"}: ${data?.reason ?? ""}`, data, data?.verdict === "regressed" ? "Reopened by its own release: look at latestEvent." : "Wait for traffic (--wait), or lower --min-traffic.");
|
|
1156
|
+
return data;
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
var claim = {
|
|
1160
|
+
path: ["issues", "claim"],
|
|
1161
|
+
summary: "Take a lease on an issue so no other agent works on it meanwhile. Lapses on its own.",
|
|
1162
|
+
description: "The holder is your handle: --as, TRACKSIY_AGENT, else the token's label. Claiming what you hold extends it. Someone else's live claim is an error naming them, unless --force. Resolving or ignoring releases it.",
|
|
1163
|
+
context: "project",
|
|
1164
|
+
effect: "write",
|
|
1165
|
+
endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/issues/{issue_id}/claim" },
|
|
1166
|
+
args: [{ name: "id", required: true, description: "Issue id." }],
|
|
1167
|
+
flags: {
|
|
1168
|
+
for: { type: "string", value: "<30m|2h|1d>", description: "Lease length (60 s to 7 days). Default 2h." },
|
|
1169
|
+
force: { type: "boolean", description: "Take it from another live holder." }
|
|
1170
|
+
},
|
|
1171
|
+
examples: [{ command: "TRACKSIY_AGENT=logistivo-app tracksiy issues claim 4812 --for 2h", description: "Work on 4812 for two hours." }],
|
|
1172
|
+
output: "The issue with claimedBy, claimedUntil, changed, previousClaimedBy.",
|
|
1173
|
+
async run(ctx) {
|
|
1174
|
+
return ctx.api.send(
|
|
1175
|
+
"POST",
|
|
1176
|
+
await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}/claim`),
|
|
1177
|
+
compact({ seconds: duration(ctx.input.for, "for"), force: ctx.input.force || void 0 })
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
var unclaim = {
|
|
1182
|
+
path: ["issues", "unclaim"],
|
|
1183
|
+
summary: "Give back your lease on an issue.",
|
|
1184
|
+
context: "project",
|
|
1185
|
+
effect: "write",
|
|
1186
|
+
idempotent: true,
|
|
1187
|
+
endpoint: { method: "DELETE", path: "/api/agent/v1/projects/{org}/{project}/issues/{issue_id}/claim" },
|
|
1188
|
+
args: [{ name: "id", required: true, description: "Issue id." }],
|
|
1189
|
+
flags: { force: { type: "boolean", description: "Release someone else's claim." } },
|
|
1190
|
+
examples: [{ command: "tracksiy issues unclaim 4812", description: "Done, or giving up: let others take it." }],
|
|
1191
|
+
output: "The issue with changed and previousClaimedBy.",
|
|
1192
|
+
async run(ctx) {
|
|
1193
|
+
const path = await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}/claim`);
|
|
1194
|
+
return (await ctx.api.request("DELETE", path, { query: ctx.input.force ? { force: true } : void 0 })).data;
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1021
1197
|
var bulk = {
|
|
1022
1198
|
path: ["issues", "bulk-status"],
|
|
1023
1199
|
summary: "Change the status of every issue matching a filter. Dry run unless --apply.",
|
|
@@ -1030,7 +1206,9 @@ var bulk = {
|
|
|
1030
1206
|
ids: { type: "list", value: "<id,...>", description: "Exactly these issues." },
|
|
1031
1207
|
...FILTERS,
|
|
1032
1208
|
all: { type: "boolean", description: "Every issue of the project (required when no other selector is given)." },
|
|
1033
|
-
apply: { type: "boolean", description: "Actually change them. Default is a dry run." }
|
|
1209
|
+
apply: { type: "boolean", description: "Actually change them. Default is a dry run." },
|
|
1210
|
+
...FIXED_IN,
|
|
1211
|
+
...WHY
|
|
1034
1212
|
},
|
|
1035
1213
|
examples: [
|
|
1036
1214
|
{ command: "tracksiy issues bulk-status --to resolved --until 30d", description: "Preview resolving everything not seen for 30 days." },
|
|
@@ -1045,12 +1223,13 @@ var bulk = {
|
|
|
1045
1223
|
ids: ctx.input.ids?.map(String),
|
|
1046
1224
|
filter: hasFilter ? filter : void 0,
|
|
1047
1225
|
all: ctx.input.all || void 0,
|
|
1048
|
-
dryRun: !ctx.input.apply
|
|
1226
|
+
dryRun: !ctx.input.apply,
|
|
1227
|
+
...whyBody(ctx.input)
|
|
1049
1228
|
});
|
|
1050
1229
|
return ctx.api.send("POST", await projectPath(ctx, "issues/status"), body);
|
|
1051
1230
|
}
|
|
1052
1231
|
};
|
|
1053
|
-
var issues_default = [list, view, resolve2, ignore, unresolve, bulk];
|
|
1232
|
+
var issues_default = [list, view, resolve2, ignore, unresolve, bulk, comment, activity, verifyFixed, claim, unclaim];
|
|
1054
1233
|
|
|
1055
1234
|
// src/commands/projects.ts
|
|
1056
1235
|
var create = {
|
|
@@ -1149,12 +1328,12 @@ var onboarding = {
|
|
|
1149
1328
|
var projects_default = [create, verify, onboarding];
|
|
1150
1329
|
|
|
1151
1330
|
// src/commands/alerts.ts
|
|
1152
|
-
var RECIPIENT_TYPES = ["email", "webhook", "discord", "googlechat", "telegram"];
|
|
1331
|
+
var RECIPIENT_TYPES = ["email", "webhook", "discord", "googlechat", "telegram", "peerpacket"];
|
|
1153
1332
|
var DECLARE = {
|
|
1154
1333
|
recipient: {
|
|
1155
1334
|
type: "list",
|
|
1156
1335
|
value: "email|<type>:<url>",
|
|
1157
|
-
description: `Who is told. Repeatable. \`email\` alone reaches the members of the project's teams; webhook, discord, googlechat and telegram take a URL: \`<type>:<url>\` (telegram: the Bot API sendMessage URL with chat_id).`
|
|
1336
|
+
description: `Who is told. Repeatable. \`email\` alone reaches the members of the project's teams; webhook, discord, googlechat and telegram take a URL: \`<type>:<url>\` (telegram: the Bot API sendMessage URL with chat_id; peerpacket: https://peerpacket.com/channels/<channel>?to=<handle>&as=<author>&key=<pp_live_\u2026> -- each alert becomes a ticket for that agent, closed when its issues are resolved).`
|
|
1158
1337
|
},
|
|
1159
1338
|
delivery: { type: "string", enum: ["immediate", "digest"], description: "For --recipient: send each alert now, or batch into the digest hours. Default immediate." },
|
|
1160
1339
|
"new-issue": { type: "boolean", description: "Alert on every new issue (default true). --no-new-issue to turn off." },
|
|
@@ -1231,7 +1410,8 @@ var set = {
|
|
|
1231
1410
|
flags: DECLARE,
|
|
1232
1411
|
examples: [
|
|
1233
1412
|
{ command: "tracksiy alerts set --recipient email --threshold 50/10", description: "Email the project's team on new issues, on bursts, and on downtime." },
|
|
1234
|
-
{ command: "tracksiy alerts set --recipient 'telegram:https://api.telegram.org/bot<token>/sendMessage?chat_id=<id>' --delivery digest", description: "Telegram, batched into the digest hours." }
|
|
1413
|
+
{ command: "tracksiy alerts set --recipient 'telegram:https://api.telegram.org/bot<token>/sendMessage?chat_id=<id>' --delivery digest", description: "Telegram, batched into the digest hours." },
|
|
1414
|
+
{ command: "tracksiy alerts set --recipient 'peerpacket:https://peerpacket.com/channels/shop-ops?to=shop-agent&as=tracksiy-alerts&key=pp_live_\u2026'", description: "Alerts land as PeerPacket tickets for the shop's agent." }
|
|
1235
1415
|
],
|
|
1236
1416
|
output: "The stored alerting state, same shape as `alerts get`.",
|
|
1237
1417
|
text: rulesText,
|
|
@@ -1466,7 +1646,7 @@ var releases_default = [list3, health2, declare];
|
|
|
1466
1646
|
|
|
1467
1647
|
// src/commands/artifacts.ts
|
|
1468
1648
|
import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1469
|
-
import { existsSync as existsSync3, readFileSync as
|
|
1649
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
1470
1650
|
|
|
1471
1651
|
// src/lib/sourcemaps.ts
|
|
1472
1652
|
import { createHash } from "node:crypto";
|
|
@@ -1841,7 +2021,7 @@ skipped, no source map: ${d.skipped.join(", ")}` : ""}`,
|
|
|
1841
2021
|
};
|
|
1842
2022
|
function uuidFromDebugMeta(path) {
|
|
1843
2023
|
if (!existsSync3(path)) throw usageError("bad_path", `No such file: ${path}`);
|
|
1844
|
-
const m = /ProguardUuids\s*[=:]\s*([0-9a-fA-F-]{36})/.exec(
|
|
2024
|
+
const m = /ProguardUuids\s*[=:]\s*([0-9a-fA-F-]{36})/.exec(readFileSync4(path, "utf8"));
|
|
1845
2025
|
if (!m) throw usageError("bad_value", `No ProguardUuids entry in ${path}`);
|
|
1846
2026
|
return m[1].toLowerCase();
|
|
1847
2027
|
}
|
|
@@ -2053,16 +2233,16 @@ var support = {
|
|
|
2053
2233
|
var support_default = [support];
|
|
2054
2234
|
|
|
2055
2235
|
// src/commands/init.ts
|
|
2056
|
-
import { existsSync as existsSync5, readFileSync as
|
|
2236
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6 } from "node:fs";
|
|
2057
2237
|
import { appendFile, writeFile as writeFile3 } from "node:fs/promises";
|
|
2058
2238
|
import { join as join4, resolve as resolve4 } from "node:path";
|
|
2059
2239
|
|
|
2060
2240
|
// src/lib/detect.ts
|
|
2061
|
-
import { existsSync as existsSync4, readFileSync as
|
|
2241
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync } from "node:fs";
|
|
2062
2242
|
import { basename, join as join3 } from "node:path";
|
|
2063
2243
|
function readJson(path) {
|
|
2064
2244
|
try {
|
|
2065
|
-
return JSON.parse(
|
|
2245
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
2066
2246
|
} catch {
|
|
2067
2247
|
return void 0;
|
|
2068
2248
|
}
|
|
@@ -2134,7 +2314,7 @@ function detect(dir, guideBase) {
|
|
|
2134
2314
|
}
|
|
2135
2315
|
const py = has(dir, "pyproject.toml", "requirements.txt", "Pipfile", "manage.py");
|
|
2136
2316
|
if (py) {
|
|
2137
|
-
const text = ["pyproject.toml", "requirements.txt", "Pipfile"].map((f) => existsSync4(join3(dir, f)) ?
|
|
2317
|
+
const text = ["pyproject.toml", "requirements.txt", "Pipfile"].map((f) => existsSync4(join3(dir, f)) ? readFileSync5(join3(dir, f), "utf8") : "").join("\n").toLowerCase();
|
|
2138
2318
|
const fw = existsSync4(join3(dir, "manage.py")) || text.includes("django") ? "django" : text.includes("fastapi") ? "fastapi" : text.includes("flask") ? "flask" : "python";
|
|
2139
2319
|
evidence.push(`${py} (${fw})`);
|
|
2140
2320
|
return { platform: fw === "python" ? "python" : `python-${fw}`, framework: fw, surfaces: ["backend"], env: { backend: "SENTRY_DSN" }, guide: guide("python-sentry-sdk"), name, evidence };
|
|
@@ -2230,7 +2410,7 @@ ${d.steps.map((s, i) => `${i + 1}. ${s.detail}${s.run ? `
|
|
|
2230
2410
|
if (!existsSync5(envPath)) {
|
|
2231
2411
|
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.");
|
|
2232
2412
|
}
|
|
2233
|
-
const current =
|
|
2413
|
+
const current = readFileSync6(envPath, "utf8");
|
|
2234
2414
|
const added = [];
|
|
2235
2415
|
const kept = [];
|
|
2236
2416
|
let block = "";
|
|
@@ -2285,12 +2465,13 @@ function camel(name) {
|
|
|
2285
2465
|
}
|
|
2286
2466
|
|
|
2287
2467
|
// src/core/args.ts
|
|
2288
|
-
import { readFileSync as
|
|
2468
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
2289
2469
|
var GLOBAL_FLAGS = {
|
|
2290
2470
|
org: { type: "string", value: "<slug>", description: "Organization slug. Env: TRACKSIY_ORG. Default: .tracksiy.json, saved login, or your only organization." },
|
|
2291
2471
|
project: { type: "string", value: "<slug>", description: "Project slug, or org/project. Env: TRACKSIY_PROJECT. Default: .tracksiy.json." },
|
|
2292
2472
|
url: { type: "string", value: "<url>", description: "Server root. Env: TRACKSIY_URL. Default: https://tracksiy.com." },
|
|
2293
2473
|
token: { type: "string", value: "<token>", description: "API token. Env: TRACKSIY_TOKEN (preferred: flags show up in process lists)." },
|
|
2474
|
+
as: { type: "string", value: "<handle>", description: "Your agent handle, recorded on issue activity and claims (X-Tracksiy-Agent). Env: TRACKSIY_AGENT. Default: the token's label." },
|
|
2294
2475
|
json: { type: "boolean", description: "Print JSON. This is the default whenever stdout is not a terminal." },
|
|
2295
2476
|
format: { type: "string", enum: ["json", "text"], description: "Output format. Env: TRACKSIY_FORMAT." },
|
|
2296
2477
|
select: { type: "list", value: "<fields>", description: "Keep only these fields of `data`, comma-separated; dot paths reach into objects and lists (e.g. issues.id,issues.title)." },
|
|
@@ -2301,7 +2482,7 @@ var GLOBAL_FLAGS = {
|
|
|
2301
2482
|
};
|
|
2302
2483
|
var stdinCache;
|
|
2303
2484
|
function readStdin() {
|
|
2304
|
-
stdinCache ??=
|
|
2485
|
+
stdinCache ??= readFileSync7(0, "utf8");
|
|
2305
2486
|
return stdinCache;
|
|
2306
2487
|
}
|
|
2307
2488
|
function parseJsonValue(name, raw) {
|
|
@@ -2309,7 +2490,7 @@ function parseJsonValue(name, raw) {
|
|
|
2309
2490
|
if (raw === "-") text = readStdin();
|
|
2310
2491
|
else if (raw.startsWith("@")) {
|
|
2311
2492
|
try {
|
|
2312
|
-
text =
|
|
2493
|
+
text = readFileSync7(raw.slice(1), "utf8");
|
|
2313
2494
|
} catch (err) {
|
|
2314
2495
|
throw usageError("bad_value", `--${name}: cannot read ${raw.slice(1)}: ${err.message}`);
|
|
2315
2496
|
}
|
|
@@ -2649,7 +2830,7 @@ function makeContext(spec, input, globals, surface, io) {
|
|
|
2649
2830
|
if (!quiet || surface === "mcp" && config.verbose) io.stderr(`${message}
|
|
2650
2831
|
`);
|
|
2651
2832
|
};
|
|
2652
|
-
const api2 = new Api({ url: config.url, token: config.token, timeoutMs: config.timeoutMs, verbose: config.verbose, log, fetch: io.fetch });
|
|
2833
|
+
const api2 = new Api({ url: config.url, token: config.token, agent: config.agent, timeoutMs: config.timeoutMs, verbose: config.verbose, log, fetch: io.fetch });
|
|
2653
2834
|
let orgs2;
|
|
2654
2835
|
const visible = () => {
|
|
2655
2836
|
orgs2 ??= api2.get("organizations").catch((err) => {
|