@goliapkg/sentori-cli 0.6.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +269 -340
- package/lib/index.js.map +1 -1
- package/lib/issue.d.ts +16 -18
- package/lib/issue.d.ts.map +1 -1
- package/lib/issue.js +29 -32
- package/lib/issue.js.map +1 -1
- package/lib/lenient.d.ts +13 -0
- package/lib/lenient.d.ts.map +1 -0
- package/lib/lenient.js +25 -0
- package/lib/lenient.js.map +1 -0
- package/lib/mcp.d.ts +4 -2
- package/lib/mcp.d.ts.map +1 -1
- package/lib/mcp.js +43 -220
- package/lib/mcp.js.map +1 -1
- package/lib/native-artifacts.d.ts +0 -1
- package/lib/native-artifacts.d.ts.map +1 -1
- package/lib/native-artifacts.js +34 -41
- package/lib/native-artifacts.js.map +1 -1
- package/lib/probes.d.ts +10 -0
- package/lib/probes.d.ts.map +1 -0
- package/lib/probes.js +75 -0
- package/lib/probes.js.map +1 -0
- package/lib/react-native.d.ts.map +1 -1
- package/lib/react-native.js +17 -9
- package/lib/react-native.js.map +1 -1
- package/lib/upload.d.ts +21 -20
- package/lib/upload.d.ts.map +1 -1
- package/lib/upload.js +65 -57
- package/lib/upload.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/lenient.test.ts +32 -0
- package/src/__tests__/mcp.test.ts +20 -112
- package/src/__tests__/native-artifacts.test.ts +43 -12
- package/src/__tests__/probes.test.ts +41 -0
- package/src/index.ts +291 -362
- package/src/issue.ts +47 -46
- package/src/lenient.ts +39 -0
- package/src/mcp.ts +46 -215
- package/src/native-artifacts.ts +42 -44
- package/src/probes.ts +75 -0
- package/src/react-native.ts +16 -9
- package/src/upload.ts +78 -68
- package/src/__tests__/issue.test.ts +0 -95
- package/src/__tests__/source-bundle-from-dir.test.ts +0 -85
- package/src/__tests__/source-bundle.test.ts +0 -105
- package/src/__tests__/upload.test.ts +0 -121
- package/src/source-bundle.ts +0 -234
package/lib/index.js
CHANGED
|
@@ -1,122 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
|
-
import { formatIssueLine,
|
|
3
|
+
import { fetchBundle, formatIssueLine, listIssues, noteIssue, resolveIssue, } from './issue.js';
|
|
4
|
+
import { isStrict, lenientFail, stripStrict } from './lenient.js';
|
|
4
5
|
import { runMcpServer } from './mcp.js';
|
|
5
6
|
import { uploadDsym, uploadMapping } from './native-artifacts.js';
|
|
7
|
+
import { scanProbes, syncProbes } from './probes.js';
|
|
6
8
|
import { parseJsonArg, pushCredsDelete, pushCredsList, pushCredsSet, pushReceipt, pushSend, } from './push.js';
|
|
7
9
|
import { reactNativeUpload } from './react-native.js';
|
|
8
|
-
import {
|
|
9
|
-
import { uploadSourcemaps } from './upload.js';
|
|
10
|
+
import { uploadArtifact } from './upload.js';
|
|
10
11
|
const HELP = `sentori-cli — Sentori command-line interface
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
already composed packager + Hermes maps into the final
|
|
21
|
-
\`$SOURCEMAP_FILE\` and deleted the intermediates.
|
|
22
|
-
Composed-then-uploaded vs raw-then-server-composed yield
|
|
23
|
-
identical symbolication — the server stores the same shape
|
|
24
|
-
either way.
|
|
13
|
+
Symbolication artifacts (api-scope token; failures NEVER block your
|
|
14
|
+
build — exit 0 with a friendly note unless --strict):
|
|
15
|
+
sentori-cli upload sourcemap --release <r> --token <t> <path...>
|
|
16
|
+
sentori-cli upload dsym --release <r> --token <t> <path.dSYM>
|
|
17
|
+
sentori-cli upload mapping --release <r> --token <t> mapping.txt
|
|
18
|
+
(mapping = the R8/proguard map; stored as kind "proguard")
|
|
19
|
+
sentori-cli react-native upload --release <r> --token <t> \\
|
|
20
|
+
--metro-map <m> --hermes-map <h> [--bundle <b>]
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
disk — typical Android release path where the gradle
|
|
31
|
-
\`bundleReleaseJsAndAssets\` task leaves both maps untouched.
|
|
32
|
-
Requires --metro-map and --hermes-map. On iOS the build phase
|
|
33
|
-
deletes the intermediates, so use \`upload sourcemap\` instead.
|
|
22
|
+
Regression tripwires (design: probes):
|
|
23
|
+
sentori-cli probes sync --release <r> --token <t> [--dir .]
|
|
24
|
+
Statically scans source for sentori.probe('REF') call sites and
|
|
25
|
+
registers them, so a silent probe is visibly alive.
|
|
34
26
|
|
|
35
|
-
|
|
36
|
-
sentori-cli
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
dwarfdump (useful in Linux CI where the toolchain isn't there).
|
|
27
|
+
CI triage (the same /api surface an AI agent uses):
|
|
28
|
+
sentori-cli issue list [--status open] [--kind error]
|
|
29
|
+
sentori-cli issue resolve <issue-id> [--in-release <r>]
|
|
30
|
+
sentori-cli issue note <issue-id> --body "fixed in abc123"
|
|
31
|
+
sentori-cli issue bundle <issue-id>
|
|
41
32
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
with a "# pg_map_id:" line the server sniffs the debug-id from
|
|
45
|
-
it; otherwise you can pass it explicitly.
|
|
33
|
+
MCP (for Claude Code and friends):
|
|
34
|
+
sentori-cli mcp serve --token <api-token> [--api-url <url>]
|
|
46
35
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
dashboard can render inline source for native (Swift / Kotlin /
|
|
50
|
-
Objective-C) frames the way it already does for JS via source
|
|
51
|
-
maps. Build the archive yourself:
|
|
52
|
-
tar -czf ios-source.tar.gz Sources/
|
|
53
|
-
Pass --module to upload multiple bundles per (release, platform)
|
|
54
|
-
— e.g. \`--module main\`, \`--module watch-ext\`. Omitting --module
|
|
55
|
-
reuses the v1.3 single-bundle slot (re-uploading replaces it).
|
|
36
|
+
Push (carried):
|
|
37
|
+
sentori-cli push send / receipt / creds ...
|
|
56
38
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
LLM agents (MCP):
|
|
63
|
-
sentori-cli mcp serve --project <uuid> [--token <t>] [--api-url <url>]
|
|
64
|
-
Run a stdio MCP server. Connect from Claude Code / any MCP
|
|
65
|
-
client by pointing at \`sentori-cli mcp serve …\` as the command.
|
|
66
|
-
Exposes sentori_issue_list / _get / _comment / _transition /
|
|
67
|
-
_assign / _set_priority / _set_labels / _watch tools.
|
|
68
|
-
|
|
69
|
-
Options (upload commands):
|
|
70
|
-
--release <r> release identifier — MUST equal the value the SDK
|
|
71
|
-
reports via init({ release }). Required.
|
|
72
|
-
--token <t> Sentori token (or set $SENTORI_TOKEN).
|
|
73
|
-
--api-url <url> Sentori API base (default https://sentori.golia.jp,
|
|
74
|
-
or $SENTORI_API_URL). For a self-hosted instance, your
|
|
75
|
-
host. (Accepts --ingest-url as an alias.)
|
|
76
|
-
--dry-run describe what would be uploaded; don't upload.
|
|
77
|
-
-h, --help show this help.
|
|
78
|
-
|
|
79
|
-
Options (react-native upload):
|
|
80
|
-
--metro-map <p> the *.packager.map Metro emits (--sourcemap-output).
|
|
81
|
-
--hermes-map <p> the *.hbc.map the Hermes compiler emits.
|
|
82
|
-
--bundle <p> optional: also upload the bundle (.jsbundle / .bundle).
|
|
83
|
-
|
|
84
|
-
Options (issue commands):
|
|
85
|
-
--project <uuid> project id (or set $SENTORI_PROJECT_ID).
|
|
86
|
-
--token <t> admin token, sk_… prefix (or $SENTORI_ADMIN_TOKEN /
|
|
87
|
-
$SENTORI_TOKEN). The ingest st_pk_ token may also work
|
|
88
|
-
on a self-hosted instance.
|
|
89
|
-
--api-url <url> Sentori API base (same as above).
|
|
90
|
-
--in-release <r> (resolve only) mark this release as where the fix
|
|
91
|
-
landed; the regression detector flips the issue back
|
|
92
|
-
to "regressed" if a matching event lands later.
|
|
93
|
-
|
|
94
|
-
Hermes release build, by hand:
|
|
95
|
-
|
|
96
|
-
Android (raw maps still on disk after \`./gradlew bundleRelease\`):
|
|
97
|
-
npx @goliapkg/sentori-cli react-native upload \\
|
|
98
|
-
--release "<app>@<version>+<build>" --token "$SENTORI_TOKEN" \\
|
|
99
|
-
--metro-map android/app/build/intermediates/sourcemaps/react/release/index.android.bundle.packager.map \\
|
|
100
|
-
--hermes-map android/app/build/intermediates/sourcemaps/react/release/index.android.bundle.compiler.map \\
|
|
101
|
-
--bundle android/app/build/generated/assets/react/release/index.android.bundle
|
|
102
|
-
|
|
103
|
-
iOS (already-composed map after \`xcodebuild archive\`; the build
|
|
104
|
-
phase deletes intermediates so you only have the composed map):
|
|
105
|
-
npx @goliapkg/sentori-cli upload sourcemap \\
|
|
106
|
-
--release "<app>@<version>+<build>" --token "$SENTORI_TOKEN" \\
|
|
107
|
-
"$BUILT_PRODUCTS_DIR/main.jsbundle.map" \\
|
|
108
|
-
"$BUILT_PRODUCTS_DIR/main.jsbundle"
|
|
39
|
+
Common options:
|
|
40
|
+
--token api-scope token (or $SENTORI_TOKEN)
|
|
41
|
+
--api-url instance URL (or $SENTORI_API_URL; default https://sentori.golia.jp)
|
|
42
|
+
--strict upload commands: exit non-zero on failure
|
|
109
43
|
`;
|
|
110
|
-
/** Parse the shared options, or print an error + return null. */
|
|
111
44
|
function parseCommon(values) {
|
|
112
45
|
const release = typeof values.release === 'string' ? values.release : undefined;
|
|
113
46
|
if (!release) {
|
|
114
|
-
console.error(
|
|
47
|
+
console.error("error: --release is required (must match the SDK's init({ release }))");
|
|
115
48
|
return null;
|
|
116
49
|
}
|
|
117
|
-
const dryRun = values['dry-run'] === true;
|
|
118
50
|
const token = (typeof values.token === 'string' ? values.token : undefined) ?? process.env.SENTORI_TOKEN;
|
|
119
|
-
if (!token
|
|
51
|
+
if (!token) {
|
|
120
52
|
console.error('error: --token (or $SENTORI_TOKEN) is required');
|
|
121
53
|
return null;
|
|
122
54
|
}
|
|
@@ -124,27 +56,95 @@ function parseCommon(values) {
|
|
|
124
56
|
(typeof values['ingest-url'] === 'string' ? values['ingest-url'] : undefined) ??
|
|
125
57
|
process.env.SENTORI_API_URL ??
|
|
126
58
|
'https://sentori.golia.jp';
|
|
127
|
-
return { apiUrl,
|
|
59
|
+
return { apiUrl, release, token };
|
|
128
60
|
}
|
|
61
|
+
function parseApiCfg(values) {
|
|
62
|
+
const token = (typeof values.token === 'string' ? values.token : undefined) ??
|
|
63
|
+
process.env.SENTORI_ADMIN_TOKEN ??
|
|
64
|
+
process.env.SENTORI_TOKEN;
|
|
65
|
+
if (!token) {
|
|
66
|
+
console.error('error: --token (or $SENTORI_TOKEN) is required');
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const apiUrl = (typeof values['api-url'] === 'string' ? values['api-url'] : undefined) ??
|
|
70
|
+
(typeof values['ingest-url'] === 'string' ? values['ingest-url'] : undefined) ??
|
|
71
|
+
process.env.SENTORI_API_URL ??
|
|
72
|
+
'https://sentori.golia.jp';
|
|
73
|
+
return { apiUrl, token };
|
|
74
|
+
}
|
|
75
|
+
function parseAdminCfg(values) {
|
|
76
|
+
const projectId = (typeof values.project === 'string' ? values.project : undefined) ??
|
|
77
|
+
process.env.SENTORI_PROJECT_ID;
|
|
78
|
+
if (!projectId) {
|
|
79
|
+
console.error('error: --project <uuid> (or $SENTORI_PROJECT_ID) is required');
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const api = parseApiCfg(values);
|
|
83
|
+
if (!api)
|
|
84
|
+
return null;
|
|
85
|
+
return { apiUrl: api.apiUrl, projectId, token: api.token };
|
|
86
|
+
}
|
|
87
|
+
const UPLOAD_OPTS = {
|
|
88
|
+
'api-url': { type: 'string' },
|
|
89
|
+
help: { short: 'h', type: 'boolean' },
|
|
90
|
+
'ingest-url': { type: 'string' },
|
|
91
|
+
release: { type: 'string' },
|
|
92
|
+
token: { type: 'string' },
|
|
93
|
+
};
|
|
94
|
+
// ── upload commands (lenient by contract) ─────────────────────────
|
|
129
95
|
async function cmdUploadSourcemap(argv) {
|
|
96
|
+
const strict = isStrict(argv);
|
|
97
|
+
let parsed;
|
|
98
|
+
try {
|
|
99
|
+
parsed = parseArgs({ allowPositionals: true, args: stripStrict(argv), options: UPLOAD_OPTS });
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
console.error(`error: ${e.message}\n${HELP}`);
|
|
103
|
+
return 2;
|
|
104
|
+
}
|
|
105
|
+
if (parsed.values.help) {
|
|
106
|
+
console.log(HELP);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
const c = parseCommon(parsed.values);
|
|
110
|
+
if (!c)
|
|
111
|
+
return 2;
|
|
112
|
+
if (parsed.positionals.length === 0) {
|
|
113
|
+
console.error('error: at least one sourcemap path is required');
|
|
114
|
+
return 2;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
for (const p of parsed.positionals) {
|
|
118
|
+
await uploadArtifact({ ...c, kind: 'sourcemap', path: p });
|
|
119
|
+
}
|
|
120
|
+
console.log(`uploaded ${parsed.positionals.length} sourcemap(s) for "${c.release}" — minified stacks on this release now resolve to source.`);
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
return lenientFail(strict, {
|
|
125
|
+
failure: `sourcemap upload failed (${e.message})`,
|
|
126
|
+
impact: `crashes from ${c.release} will show minified stacks until the map is uploaded.`,
|
|
127
|
+
retry: `sentori-cli upload sourcemap --release "${c.release}" --token <t> ${parsed.positionals.join(' ')}`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function cmdUploadDsym(argv) {
|
|
132
|
+
const strict = isStrict(argv);
|
|
130
133
|
let parsed;
|
|
131
134
|
try {
|
|
132
135
|
parsed = parseArgs({
|
|
133
136
|
allowPositionals: true,
|
|
134
|
-
args: argv,
|
|
137
|
+
args: stripStrict(argv),
|
|
135
138
|
options: {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
'
|
|
140
|
-
release: { type: 'string' },
|
|
141
|
-
token: { type: 'string' },
|
|
139
|
+
...UPLOAD_OPTS,
|
|
140
|
+
arch: { type: 'string' },
|
|
141
|
+
'debug-id': { type: 'string' },
|
|
142
|
+
'object-name': { type: 'string' },
|
|
142
143
|
},
|
|
143
144
|
});
|
|
144
145
|
}
|
|
145
146
|
catch (e) {
|
|
146
|
-
console.error(`error: ${e.message}\n`);
|
|
147
|
-
console.error(HELP);
|
|
147
|
+
console.error(`error: ${e.message}\n${HELP}`);
|
|
148
148
|
return 2;
|
|
149
149
|
}
|
|
150
150
|
if (parsed.values.help) {
|
|
@@ -154,47 +154,104 @@ async function cmdUploadSourcemap(argv) {
|
|
|
154
154
|
const c = parseCommon(parsed.values);
|
|
155
155
|
if (!c)
|
|
156
156
|
return 2;
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
const path = parsed.positionals[0];
|
|
158
|
+
if (!path) {
|
|
159
|
+
console.error('error: a path to a .dSYM bundle or DWARF binary is required');
|
|
160
|
+
return 2;
|
|
161
|
+
}
|
|
162
|
+
const debugId = parsed.values['debug-id'];
|
|
163
|
+
const arch = parsed.values.arch;
|
|
164
|
+
if ((debugId && !arch) || (arch && !debugId)) {
|
|
165
|
+
console.error('error: --debug-id and --arch must be passed together (or both omitted)');
|
|
159
166
|
return 2;
|
|
160
167
|
}
|
|
161
168
|
try {
|
|
162
|
-
const
|
|
169
|
+
const r = await uploadDsym({
|
|
163
170
|
apiUrl: c.apiUrl,
|
|
164
|
-
|
|
165
|
-
|
|
171
|
+
arch: typeof arch === 'string' ? arch : undefined,
|
|
172
|
+
debugId: typeof debugId === 'string' ? debugId : undefined,
|
|
173
|
+
objectName: typeof parsed.values['object-name'] === 'string'
|
|
174
|
+
? parsed.values['object-name']
|
|
175
|
+
: undefined,
|
|
176
|
+
path,
|
|
166
177
|
release: c.release,
|
|
167
178
|
token: c.token,
|
|
168
179
|
});
|
|
169
|
-
|
|
180
|
+
console.log(`uploaded ${r.slices.length} dSYM slice(s):`);
|
|
181
|
+
for (const s of r.slices)
|
|
182
|
+
console.log(` ${s.debugId} (${s.arch})`);
|
|
170
183
|
return 0;
|
|
171
184
|
}
|
|
172
185
|
catch (e) {
|
|
173
|
-
|
|
174
|
-
|
|
186
|
+
return lenientFail(strict, {
|
|
187
|
+
failure: `dSYM upload failed (${e.message})`,
|
|
188
|
+
impact: `native iOS stacks from ${c.release} stay unsymbolicated until the dSYM lands.`,
|
|
189
|
+
retry: `sentori-cli upload dsym --release "${c.release}" --token <t> ${path}`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async function cmdUploadMapping(argv) {
|
|
194
|
+
const strict = isStrict(argv);
|
|
195
|
+
let parsed;
|
|
196
|
+
try {
|
|
197
|
+
parsed = parseArgs({
|
|
198
|
+
allowPositionals: true,
|
|
199
|
+
args: stripStrict(argv),
|
|
200
|
+
options: { ...UPLOAD_OPTS, 'debug-id': { type: 'string' } },
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch (e) {
|
|
204
|
+
console.error(`error: ${e.message}\n${HELP}`);
|
|
205
|
+
return 2;
|
|
206
|
+
}
|
|
207
|
+
if (parsed.values.help) {
|
|
208
|
+
console.log(HELP);
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
const c = parseCommon(parsed.values);
|
|
212
|
+
if (!c)
|
|
213
|
+
return 2;
|
|
214
|
+
const path = parsed.positionals[0];
|
|
215
|
+
if (!path) {
|
|
216
|
+
console.error('error: a path to mapping.txt is required');
|
|
217
|
+
return 2;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
await uploadMapping({
|
|
221
|
+
apiUrl: c.apiUrl,
|
|
222
|
+
debugId: typeof parsed.values['debug-id'] === 'string' ? parsed.values['debug-id'] : undefined,
|
|
223
|
+
path,
|
|
224
|
+
release: c.release,
|
|
225
|
+
token: c.token,
|
|
226
|
+
});
|
|
227
|
+
console.log(`uploaded mapping for "${c.release}" — R8 names on this release now demangle.`);
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
return lenientFail(strict, {
|
|
232
|
+
failure: `mapping upload failed (${e.message})`,
|
|
233
|
+
impact: `Android stacks from ${c.release} stay R8-obfuscated until the mapping lands.`,
|
|
234
|
+
retry: `sentori-cli upload mapping --release "${c.release}" --token <t> ${path}`,
|
|
235
|
+
});
|
|
175
236
|
}
|
|
176
237
|
}
|
|
177
238
|
async function cmdReactNativeUpload(argv) {
|
|
239
|
+
const strict = isStrict(argv);
|
|
178
240
|
let parsed;
|
|
179
241
|
try {
|
|
180
242
|
parsed = parseArgs({
|
|
181
|
-
args: argv,
|
|
243
|
+
args: stripStrict(argv),
|
|
182
244
|
options: {
|
|
183
|
-
|
|
245
|
+
...UPLOAD_OPTS,
|
|
184
246
|
bundle: { type: 'string' },
|
|
185
247
|
'dry-run': { type: 'boolean' },
|
|
186
|
-
help: { short: 'h', type: 'boolean' },
|
|
187
248
|
'hermes-map': { type: 'string' },
|
|
188
|
-
'ingest-url': { type: 'string' },
|
|
189
249
|
'metro-map': { type: 'string' },
|
|
190
|
-
release: { type: 'string' },
|
|
191
|
-
token: { type: 'string' },
|
|
192
250
|
},
|
|
193
251
|
});
|
|
194
252
|
}
|
|
195
253
|
catch (e) {
|
|
196
|
-
console.error(`error: ${e.message}\n`);
|
|
197
|
-
console.error(HELP);
|
|
254
|
+
console.error(`error: ${e.message}\n${HELP}`);
|
|
198
255
|
return 2;
|
|
199
256
|
}
|
|
200
257
|
if (parsed.values.help) {
|
|
@@ -214,65 +271,31 @@ async function cmdReactNativeUpload(argv) {
|
|
|
214
271
|
const result = await reactNativeUpload({
|
|
215
272
|
apiUrl: c.apiUrl,
|
|
216
273
|
bundle: typeof parsed.values.bundle === 'string' ? parsed.values.bundle : undefined,
|
|
217
|
-
dryRun:
|
|
274
|
+
dryRun: parsed.values['dry-run'] === true,
|
|
218
275
|
hermesMap,
|
|
219
276
|
metroMap,
|
|
220
277
|
release: c.release,
|
|
221
278
|
token: c.token,
|
|
222
279
|
});
|
|
223
|
-
|
|
280
|
+
console.log(`uploaded ${result.uploaded ?? result.files.length} file(s) for "${c.release}".`);
|
|
224
281
|
return 0;
|
|
225
282
|
}
|
|
226
283
|
catch (e) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (c.dryRun) {
|
|
233
|
-
console.log(`would upload ${result.files.length} file(s) to ${c.apiUrl.replace(/\/+$/, '')}/admin/api/releases/${encodeURIComponent(c.release)}/sourcemaps:`);
|
|
234
|
-
for (const f of result.files)
|
|
235
|
-
console.log(` ${f}`);
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
console.log(`uploaded ${result.uploaded ?? result.files.length} file(s) for release "${c.release}" — minified stacks on this release will now resolve to source.`);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
function parseAdminCfg(values) {
|
|
242
|
-
const projectId = (typeof values.project === 'string' ? values.project : undefined) ??
|
|
243
|
-
process.env.SENTORI_PROJECT_ID;
|
|
244
|
-
if (!projectId) {
|
|
245
|
-
console.error('error: --project <uuid> (or $SENTORI_PROJECT_ID) is required');
|
|
246
|
-
return null;
|
|
247
|
-
}
|
|
248
|
-
const token = (typeof values.token === 'string' ? values.token : undefined) ??
|
|
249
|
-
process.env.SENTORI_ADMIN_TOKEN ??
|
|
250
|
-
process.env.SENTORI_TOKEN;
|
|
251
|
-
if (!token) {
|
|
252
|
-
console.error('error: --token (or $SENTORI_ADMIN_TOKEN / $SENTORI_TOKEN) is required for issue commands');
|
|
253
|
-
return null;
|
|
284
|
+
return lenientFail(strict, {
|
|
285
|
+
failure: `react-native upload failed (${e.message})`,
|
|
286
|
+
impact: `Hermes stacks from ${c.release} stay unsymbolicated until the composed map lands.`,
|
|
287
|
+
retry: `sentori-cli react-native upload --release "${c.release}" --token <t> --metro-map ${metroMap} --hermes-map ${hermesMap}`,
|
|
288
|
+
});
|
|
254
289
|
}
|
|
255
|
-
const apiUrl = (typeof values['api-url'] === 'string' ? values['api-url'] : undefined) ??
|
|
256
|
-
(typeof values['ingest-url'] === 'string' ? values['ingest-url'] : undefined) ??
|
|
257
|
-
process.env.SENTORI_API_URL ??
|
|
258
|
-
'https://sentori.golia.jp';
|
|
259
|
-
return { apiUrl, projectId, token };
|
|
260
290
|
}
|
|
261
|
-
|
|
291
|
+
// ── probes sync ───────────────────────────────────────────────────
|
|
292
|
+
async function cmdProbesSync(argv) {
|
|
293
|
+
const strict = isStrict(argv);
|
|
262
294
|
let parsed;
|
|
263
295
|
try {
|
|
264
296
|
parsed = parseArgs({
|
|
265
|
-
args: argv,
|
|
266
|
-
options: {
|
|
267
|
-
'api-url': { type: 'string' },
|
|
268
|
-
'error-type': { type: 'string' },
|
|
269
|
-
help: { short: 'h', type: 'boolean' },
|
|
270
|
-
'ingest-url': { type: 'string' },
|
|
271
|
-
limit: { type: 'string' },
|
|
272
|
-
project: { type: 'string' },
|
|
273
|
-
status: { type: 'string' },
|
|
274
|
-
token: { type: 'string' },
|
|
275
|
-
},
|
|
297
|
+
args: stripStrict(argv),
|
|
298
|
+
options: { ...UPLOAD_OPTS, dir: { type: 'string' } },
|
|
276
299
|
});
|
|
277
300
|
}
|
|
278
301
|
catch (e) {
|
|
@@ -283,50 +306,41 @@ async function cmdIssueList(argv) {
|
|
|
283
306
|
console.log(HELP);
|
|
284
307
|
return 0;
|
|
285
308
|
}
|
|
286
|
-
const
|
|
287
|
-
if (!
|
|
288
|
-
return 2;
|
|
289
|
-
const status = parsed.values.status;
|
|
290
|
-
if (status && !['active', 'closed', 'resolved', 'silenced'].includes(status)) {
|
|
291
|
-
console.error(`error: --status must be one of: active, silenced, resolved, closed`);
|
|
309
|
+
const c = parseCommon(parsed.values);
|
|
310
|
+
if (!c)
|
|
292
311
|
return 2;
|
|
312
|
+
const dir = typeof parsed.values.dir === 'string' ? parsed.values.dir : '.';
|
|
313
|
+
const refs = scanProbes(dir);
|
|
314
|
+
if (refs.length === 0) {
|
|
315
|
+
console.log(`no sentori.probe() call sites found under ${dir} — nothing to register.`);
|
|
316
|
+
return 0;
|
|
293
317
|
}
|
|
294
|
-
const limitStr = parsed.values.limit;
|
|
295
|
-
const limit = limitStr ? Number.parseInt(limitStr, 10) : undefined;
|
|
296
318
|
try {
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
errorType: parsed.values['error-type'],
|
|
300
|
-
limit,
|
|
301
|
-
status: status,
|
|
302
|
-
});
|
|
303
|
-
if (rows.length === 0) {
|
|
304
|
-
console.log('(no matching issues)');
|
|
305
|
-
return 0;
|
|
306
|
-
}
|
|
307
|
-
for (const r of rows)
|
|
308
|
-
console.log(formatIssueLine(r));
|
|
319
|
+
const r = await syncProbes({ apiUrl: c.apiUrl, token: c.token, release: c.release, refs });
|
|
320
|
+
console.log(`registered ${r.registered} probe(s) for "${c.release}": ${refs.join(', ')}`);
|
|
309
321
|
return 0;
|
|
310
322
|
}
|
|
311
323
|
catch (e) {
|
|
312
|
-
|
|
313
|
-
|
|
324
|
+
return lenientFail(strict, {
|
|
325
|
+
failure: `probes sync failed (${e.message})`,
|
|
326
|
+
impact: `silent probes on ${c.release} can't be told apart from deleted code until registered.`,
|
|
327
|
+
retry: `sentori-cli probes sync --release "${c.release}" --token <t> --dir ${dir}`,
|
|
328
|
+
});
|
|
314
329
|
}
|
|
315
330
|
}
|
|
316
|
-
|
|
331
|
+
// ── issue commands (the /api surface) ─────────────────────────────
|
|
332
|
+
const ISSUE_OPTS = {
|
|
333
|
+
'api-url': { type: 'string' },
|
|
334
|
+
help: { short: 'h', type: 'boolean' },
|
|
335
|
+
'ingest-url': { type: 'string' },
|
|
336
|
+
token: { type: 'string' },
|
|
337
|
+
};
|
|
338
|
+
async function cmdIssueList(argv) {
|
|
317
339
|
let parsed;
|
|
318
340
|
try {
|
|
319
341
|
parsed = parseArgs({
|
|
320
|
-
allowPositionals: true,
|
|
321
342
|
args: argv,
|
|
322
|
-
options: {
|
|
323
|
-
'api-url': { type: 'string' },
|
|
324
|
-
help: { short: 'h', type: 'boolean' },
|
|
325
|
-
'in-release': { type: 'string' },
|
|
326
|
-
'ingest-url': { type: 'string' },
|
|
327
|
-
project: { type: 'string' },
|
|
328
|
-
token: { type: 'string' },
|
|
329
|
-
},
|
|
343
|
+
options: { ...ISSUE_OPTS, kind: { type: 'string' }, status: { type: 'string' } },
|
|
330
344
|
});
|
|
331
345
|
}
|
|
332
346
|
catch (e) {
|
|
@@ -337,45 +351,34 @@ async function cmdIssuePatch(argv, body, verb) {
|
|
|
337
351
|
console.log(HELP);
|
|
338
352
|
return 0;
|
|
339
353
|
}
|
|
340
|
-
const cfg =
|
|
354
|
+
const cfg = parseApiCfg(parsed.values);
|
|
341
355
|
if (!cfg)
|
|
342
356
|
return 2;
|
|
343
|
-
const issueId = parsed.positionals[0];
|
|
344
|
-
if (!issueId) {
|
|
345
|
-
console.error('error: <issue-uuid> is required');
|
|
346
|
-
return 2;
|
|
347
|
-
}
|
|
348
|
-
if (verb === 'resolved' && typeof parsed.values['in-release'] === 'string') {
|
|
349
|
-
body.resolvedInRelease = parsed.values['in-release'];
|
|
350
|
-
}
|
|
351
357
|
try {
|
|
352
|
-
const
|
|
353
|
-
|
|
358
|
+
const rows = await listIssues(cfg, {
|
|
359
|
+
kind: parsed.values.kind,
|
|
360
|
+
status: parsed.values.status ?? 'open',
|
|
361
|
+
});
|
|
362
|
+
if (rows.length === 0) {
|
|
363
|
+
console.log('(no matching issues)');
|
|
364
|
+
return 0;
|
|
365
|
+
}
|
|
366
|
+
for (const r of rows)
|
|
367
|
+
console.log(formatIssueLine(r));
|
|
354
368
|
return 0;
|
|
355
369
|
}
|
|
356
370
|
catch (e) {
|
|
357
|
-
console.error(`issue
|
|
371
|
+
console.error(`issue list failed: ${e.message}`);
|
|
358
372
|
return 1;
|
|
359
373
|
}
|
|
360
374
|
}
|
|
361
|
-
|
|
362
|
-
async function cmdUploadDsym(argv) {
|
|
375
|
+
async function cmdIssueResolve(argv) {
|
|
363
376
|
let parsed;
|
|
364
377
|
try {
|
|
365
378
|
parsed = parseArgs({
|
|
366
379
|
allowPositionals: true,
|
|
367
380
|
args: argv,
|
|
368
|
-
options: {
|
|
369
|
-
'api-url': { type: 'string' },
|
|
370
|
-
arch: { type: 'string' },
|
|
371
|
-
'debug-id': { type: 'string' },
|
|
372
|
-
help: { short: 'h', type: 'boolean' },
|
|
373
|
-
'ingest-url': { type: 'string' },
|
|
374
|
-
'object-name': { type: 'string' },
|
|
375
|
-
project: { type: 'string' },
|
|
376
|
-
release: { type: 'string' },
|
|
377
|
-
token: { type: 'string' },
|
|
378
|
-
},
|
|
381
|
+
options: { ...ISSUE_OPTS, 'in-release': { type: 'string' } },
|
|
379
382
|
});
|
|
380
383
|
}
|
|
381
384
|
catch (e) {
|
|
@@ -386,56 +389,31 @@ async function cmdUploadDsym(argv) {
|
|
|
386
389
|
console.log(HELP);
|
|
387
390
|
return 0;
|
|
388
391
|
}
|
|
389
|
-
const cfg =
|
|
392
|
+
const cfg = parseApiCfg(parsed.values);
|
|
390
393
|
if (!cfg)
|
|
391
394
|
return 2;
|
|
392
|
-
const
|
|
393
|
-
if (!
|
|
394
|
-
console.error('error:
|
|
395
|
-
return 2;
|
|
396
|
-
}
|
|
397
|
-
const debugId = parsed.values['debug-id'];
|
|
398
|
-
const arch = parsed.values.arch;
|
|
399
|
-
if ((debugId && !arch) || (arch && !debugId)) {
|
|
400
|
-
console.error('error: --debug-id and --arch must be passed together (or both omitted)');
|
|
395
|
+
const issueId = parsed.positionals[0];
|
|
396
|
+
if (!issueId) {
|
|
397
|
+
console.error('error: <issue-id> is required');
|
|
401
398
|
return 2;
|
|
402
399
|
}
|
|
403
400
|
try {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
arch: typeof arch === 'string' ? arch : undefined,
|
|
407
|
-
debugId: typeof debugId === 'string' ? debugId : undefined,
|
|
408
|
-
objectName: typeof parsed.values['object-name'] === 'string' ? parsed.values['object-name'] : undefined,
|
|
409
|
-
path,
|
|
410
|
-
projectId: cfg.projectId,
|
|
411
|
-
release: typeof parsed.values.release === 'string' ? parsed.values.release : undefined,
|
|
412
|
-
token: cfg.token,
|
|
413
|
-
});
|
|
414
|
-
console.log(`uploaded ${r.slices.length} dSYM slice(s):`);
|
|
415
|
-
for (const s of r.slices)
|
|
416
|
-
console.log(` ${s.debugId} (${s.arch})`);
|
|
401
|
+
await resolveIssue(cfg, issueId, parsed.values['in-release']);
|
|
402
|
+
console.log(`${issueId} → resolved${parsed.values['in-release'] ? ` (in ${parsed.values['in-release']})` : ''}`);
|
|
417
403
|
return 0;
|
|
418
404
|
}
|
|
419
405
|
catch (e) {
|
|
420
|
-
console.error(`
|
|
406
|
+
console.error(`issue resolve failed: ${e.message}`);
|
|
421
407
|
return 1;
|
|
422
408
|
}
|
|
423
409
|
}
|
|
424
|
-
async function
|
|
410
|
+
async function cmdIssueNote(argv) {
|
|
425
411
|
let parsed;
|
|
426
412
|
try {
|
|
427
413
|
parsed = parseArgs({
|
|
428
414
|
allowPositionals: true,
|
|
429
415
|
args: argv,
|
|
430
|
-
options: {
|
|
431
|
-
'api-url': { type: 'string' },
|
|
432
|
-
'debug-id': { type: 'string' },
|
|
433
|
-
help: { short: 'h', type: 'boolean' },
|
|
434
|
-
'ingest-url': { type: 'string' },
|
|
435
|
-
project: { type: 'string' },
|
|
436
|
-
release: { type: 'string' },
|
|
437
|
-
token: { type: 'string' },
|
|
438
|
-
},
|
|
416
|
+
options: { ...ISSUE_OPTS, body: { type: 'string' } },
|
|
439
417
|
});
|
|
440
418
|
}
|
|
441
419
|
catch (e) {
|
|
@@ -446,51 +424,29 @@ async function cmdUploadMapping(argv) {
|
|
|
446
424
|
console.log(HELP);
|
|
447
425
|
return 0;
|
|
448
426
|
}
|
|
449
|
-
const cfg =
|
|
427
|
+
const cfg = parseApiCfg(parsed.values);
|
|
450
428
|
if (!cfg)
|
|
451
429
|
return 2;
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
430
|
+
const issueId = parsed.positionals[0];
|
|
431
|
+
const body = parsed.values.body;
|
|
432
|
+
if (!issueId || !body) {
|
|
433
|
+
console.error('error: <issue-id> and --body are required');
|
|
455
434
|
return 2;
|
|
456
435
|
}
|
|
457
436
|
try {
|
|
458
|
-
await
|
|
459
|
-
|
|
460
|
-
debugId: typeof parsed.values['debug-id'] === 'string' ? parsed.values['debug-id'] : undefined,
|
|
461
|
-
path,
|
|
462
|
-
projectId: cfg.projectId,
|
|
463
|
-
release: typeof parsed.values.release === 'string' ? parsed.values.release : undefined,
|
|
464
|
-
token: cfg.token,
|
|
465
|
-
});
|
|
466
|
-
console.log(`uploaded mapping for project ${cfg.projectId}${parsed.values.release ? ` / ${parsed.values.release}` : ''}`);
|
|
437
|
+
await noteIssue(cfg, issueId, body);
|
|
438
|
+
console.log(`${issueId} ← note added`);
|
|
467
439
|
return 0;
|
|
468
440
|
}
|
|
469
441
|
catch (e) {
|
|
470
|
-
console.error(`
|
|
442
|
+
console.error(`issue note failed: ${e.message}`);
|
|
471
443
|
return 1;
|
|
472
444
|
}
|
|
473
445
|
}
|
|
474
|
-
async function
|
|
446
|
+
async function cmdIssueBundle(argv) {
|
|
475
447
|
let parsed;
|
|
476
448
|
try {
|
|
477
|
-
parsed = parseArgs({
|
|
478
|
-
allowPositionals: true,
|
|
479
|
-
args: argv,
|
|
480
|
-
options: {
|
|
481
|
-
'api-url': { type: 'string' },
|
|
482
|
-
help: { short: 'h', type: 'boolean' },
|
|
483
|
-
'ingest-url': { type: 'string' },
|
|
484
|
-
// v1.4 W26 — optional module label so polyrepo apps can
|
|
485
|
-
// upload multiple bundles per (release, platform) without
|
|
486
|
-
// clobbering each other (main vs watch-ext vs share-ext…).
|
|
487
|
-
module: { type: 'string' },
|
|
488
|
-
platform: { type: 'string' },
|
|
489
|
-
project: { type: 'string' },
|
|
490
|
-
release: { type: 'string' },
|
|
491
|
-
token: { type: 'string' },
|
|
492
|
-
},
|
|
493
|
-
});
|
|
449
|
+
parsed = parseArgs({ allowPositionals: true, args: argv, options: ISSUE_OPTS });
|
|
494
450
|
}
|
|
495
451
|
catch (e) {
|
|
496
452
|
console.error(`error: ${e.message}\n${HELP}`);
|
|
@@ -500,55 +456,28 @@ async function cmdUploadSourceBundle(argv) {
|
|
|
500
456
|
console.log(HELP);
|
|
501
457
|
return 0;
|
|
502
458
|
}
|
|
503
|
-
const cfg =
|
|
459
|
+
const cfg = parseApiCfg(parsed.values);
|
|
504
460
|
if (!cfg)
|
|
505
461
|
return 2;
|
|
506
|
-
const
|
|
507
|
-
if (!
|
|
508
|
-
console.error('error:
|
|
509
|
-
return 2;
|
|
510
|
-
}
|
|
511
|
-
const platform = parsed.values.platform;
|
|
512
|
-
if (platform !== 'ios' && platform !== 'android') {
|
|
513
|
-
console.error('error: --platform must be ios or android');
|
|
514
|
-
return 2;
|
|
515
|
-
}
|
|
516
|
-
const release = typeof parsed.values.release === 'string' ? parsed.values.release : undefined;
|
|
517
|
-
if (!release) {
|
|
518
|
-
console.error('error: --release is required for source-bundle uploads');
|
|
462
|
+
const issueId = parsed.positionals[0];
|
|
463
|
+
if (!issueId) {
|
|
464
|
+
console.error('error: <issue-id> is required');
|
|
519
465
|
return 2;
|
|
520
466
|
}
|
|
521
467
|
try {
|
|
522
|
-
|
|
523
|
-
apiUrl: cfg.apiUrl,
|
|
524
|
-
module: typeof parsed.values.module === 'string' ? parsed.values.module : undefined,
|
|
525
|
-
path,
|
|
526
|
-
platform,
|
|
527
|
-
projectId: cfg.projectId,
|
|
528
|
-
release,
|
|
529
|
-
token: cfg.token,
|
|
530
|
-
});
|
|
531
|
-
console.log(`uploaded ${r.kind} (${r.sizeBytes} bytes, sha256:${r.contentHash.slice(0, 12)}…)`);
|
|
468
|
+
console.log(await fetchBundle(cfg, issueId));
|
|
532
469
|
return 0;
|
|
533
470
|
}
|
|
534
471
|
catch (e) {
|
|
535
|
-
console.error(`
|
|
472
|
+
console.error(`issue bundle failed: ${e.message}`);
|
|
536
473
|
return 1;
|
|
537
474
|
}
|
|
538
475
|
}
|
|
476
|
+
// ── mcp ───────────────────────────────────────────────────────────
|
|
539
477
|
async function cmdMcpServe(argv) {
|
|
540
478
|
let parsed;
|
|
541
479
|
try {
|
|
542
|
-
parsed = parseArgs({
|
|
543
|
-
args: argv,
|
|
544
|
-
options: {
|
|
545
|
-
'api-url': { type: 'string' },
|
|
546
|
-
help: { short: 'h', type: 'boolean' },
|
|
547
|
-
'ingest-url': { type: 'string' },
|
|
548
|
-
project: { type: 'string' },
|
|
549
|
-
token: { type: 'string' },
|
|
550
|
-
},
|
|
551
|
-
});
|
|
480
|
+
parsed = parseArgs({ args: argv, options: ISSUE_OPTS });
|
|
552
481
|
}
|
|
553
482
|
catch (e) {
|
|
554
483
|
console.error(`error: ${e.message}\n${HELP}`);
|
|
@@ -558,11 +487,11 @@ async function cmdMcpServe(argv) {
|
|
|
558
487
|
console.log(HELP);
|
|
559
488
|
return 0;
|
|
560
489
|
}
|
|
561
|
-
const cfg =
|
|
490
|
+
const cfg = parseApiCfg(parsed.values);
|
|
562
491
|
if (!cfg)
|
|
563
492
|
return 2;
|
|
564
493
|
try {
|
|
565
|
-
await runMcpServer({ apiUrl: cfg.apiUrl,
|
|
494
|
+
await runMcpServer({ apiUrl: cfg.apiUrl, token: cfg.token });
|
|
566
495
|
return 0;
|
|
567
496
|
}
|
|
568
497
|
catch (e) {
|
|
@@ -582,20 +511,20 @@ async function main(argv) {
|
|
|
582
511
|
return cmdUploadDsym(rest);
|
|
583
512
|
if (a === 'upload' && b === 'mapping')
|
|
584
513
|
return cmdUploadMapping(rest);
|
|
585
|
-
if (a === 'upload' && b === 'source-bundle')
|
|
586
|
-
return cmdUploadSourceBundle(rest);
|
|
587
|
-
if (a === 'mcp' && b === 'serve')
|
|
588
|
-
return cmdMcpServe(rest);
|
|
589
514
|
if (a === 'react-native' && b === 'upload')
|
|
590
515
|
return cmdReactNativeUpload(rest);
|
|
516
|
+
if (a === 'probes' && b === 'sync')
|
|
517
|
+
return cmdProbesSync(rest);
|
|
518
|
+
if (a === 'mcp' && b === 'serve')
|
|
519
|
+
return cmdMcpServe(rest);
|
|
591
520
|
if (a === 'issue' && b === 'list')
|
|
592
521
|
return cmdIssueList(rest);
|
|
593
522
|
if (a === 'issue' && b === 'resolve')
|
|
594
|
-
return
|
|
595
|
-
if (a === 'issue' && b === '
|
|
596
|
-
return
|
|
597
|
-
if (a === 'issue' && b === '
|
|
598
|
-
return
|
|
523
|
+
return cmdIssueResolve(rest);
|
|
524
|
+
if (a === 'issue' && b === 'note')
|
|
525
|
+
return cmdIssueNote(rest);
|
|
526
|
+
if (a === 'issue' && b === 'bundle')
|
|
527
|
+
return cmdIssueBundle(rest);
|
|
599
528
|
if (a === 'push' && b === 'send')
|
|
600
529
|
return cmdPushSend(rest);
|
|
601
530
|
if (a === 'push' && b === 'receipt')
|