@goliapkg/sentori-cli 0.6.0 → 1.0.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 +268 -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 +30 -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 +10 -21
- package/lib/upload.d.ts.map +1 -1
- package/lib/upload.js +20 -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 +5 -12
- package/src/__tests__/probes.test.ts +41 -0
- package/src/index.ts +290 -362
- package/src/issue.ts +47 -46
- package/src/lenient.ts +39 -0
- package/src/mcp.ts +46 -215
- package/src/native-artifacts.ts +37 -44
- package/src/probes.ts +75 -0
- package/src/react-native.ts +16 -9
- package/src/upload.ts +30 -71
- 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,53 @@
|
|
|
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
|
-
- iOS post-\`react-native-xcode.sh\` where the build phase has
|
|
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
|
+
sentori-cli react-native upload --release <r> --token <t> \\
|
|
19
|
+
--metro-map <m> --hermes-map <h> [--bundle <b>]
|
|
25
20
|
|
|
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.
|
|
21
|
+
Regression tripwires (design: probes):
|
|
22
|
+
sentori-cli probes sync --release <r> --token <t> [--dir .]
|
|
23
|
+
Statically scans source for sentori.probe('REF') call sites and
|
|
24
|
+
registers them, so a silent probe is visibly alive.
|
|
34
25
|
|
|
35
|
-
|
|
36
|
-
sentori-cli
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
dwarfdump (useful in Linux CI where the toolchain isn't there).
|
|
26
|
+
CI triage (the same /api surface an AI agent uses):
|
|
27
|
+
sentori-cli issue list [--status open] [--kind error]
|
|
28
|
+
sentori-cli issue resolve <issue-id> [--in-release <r>]
|
|
29
|
+
sentori-cli issue note <issue-id> --body "fixed in abc123"
|
|
30
|
+
sentori-cli issue bundle <issue-id>
|
|
41
31
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
with a "# pg_map_id:" line the server sniffs the debug-id from
|
|
45
|
-
it; otherwise you can pass it explicitly.
|
|
32
|
+
MCP (for Claude Code and friends):
|
|
33
|
+
sentori-cli mcp serve --token <api-token> [--api-url <url>]
|
|
46
34
|
|
|
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).
|
|
35
|
+
Push (carried):
|
|
36
|
+
sentori-cli push send / receipt / creds ...
|
|
56
37
|
|
|
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"
|
|
38
|
+
Common options:
|
|
39
|
+
--token api-scope token (or $SENTORI_TOKEN)
|
|
40
|
+
--api-url instance URL (or $SENTORI_API_URL; default https://sentori.golia.jp)
|
|
41
|
+
--strict upload commands: exit non-zero on failure
|
|
109
42
|
`;
|
|
110
|
-
/** Parse the shared options, or print an error + return null. */
|
|
111
43
|
function parseCommon(values) {
|
|
112
44
|
const release = typeof values.release === 'string' ? values.release : undefined;
|
|
113
45
|
if (!release) {
|
|
114
|
-
console.error(
|
|
46
|
+
console.error("error: --release is required (must match the SDK's init({ release }))");
|
|
115
47
|
return null;
|
|
116
48
|
}
|
|
117
|
-
const dryRun = values['dry-run'] === true;
|
|
118
49
|
const token = (typeof values.token === 'string' ? values.token : undefined) ?? process.env.SENTORI_TOKEN;
|
|
119
|
-
if (!token
|
|
50
|
+
if (!token) {
|
|
120
51
|
console.error('error: --token (or $SENTORI_TOKEN) is required');
|
|
121
52
|
return null;
|
|
122
53
|
}
|
|
@@ -124,27 +55,95 @@ function parseCommon(values) {
|
|
|
124
55
|
(typeof values['ingest-url'] === 'string' ? values['ingest-url'] : undefined) ??
|
|
125
56
|
process.env.SENTORI_API_URL ??
|
|
126
57
|
'https://sentori.golia.jp';
|
|
127
|
-
return { apiUrl,
|
|
58
|
+
return { apiUrl, release, token };
|
|
128
59
|
}
|
|
60
|
+
function parseApiCfg(values) {
|
|
61
|
+
const token = (typeof values.token === 'string' ? values.token : undefined) ??
|
|
62
|
+
process.env.SENTORI_ADMIN_TOKEN ??
|
|
63
|
+
process.env.SENTORI_TOKEN;
|
|
64
|
+
if (!token) {
|
|
65
|
+
console.error('error: --token (or $SENTORI_TOKEN) is required');
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const apiUrl = (typeof values['api-url'] === 'string' ? values['api-url'] : undefined) ??
|
|
69
|
+
(typeof values['ingest-url'] === 'string' ? values['ingest-url'] : undefined) ??
|
|
70
|
+
process.env.SENTORI_API_URL ??
|
|
71
|
+
'https://sentori.golia.jp';
|
|
72
|
+
return { apiUrl, token };
|
|
73
|
+
}
|
|
74
|
+
function parseAdminCfg(values) {
|
|
75
|
+
const projectId = (typeof values.project === 'string' ? values.project : undefined) ??
|
|
76
|
+
process.env.SENTORI_PROJECT_ID;
|
|
77
|
+
if (!projectId) {
|
|
78
|
+
console.error('error: --project <uuid> (or $SENTORI_PROJECT_ID) is required');
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const api = parseApiCfg(values);
|
|
82
|
+
if (!api)
|
|
83
|
+
return null;
|
|
84
|
+
return { apiUrl: api.apiUrl, projectId, token: api.token };
|
|
85
|
+
}
|
|
86
|
+
const UPLOAD_OPTS = {
|
|
87
|
+
'api-url': { type: 'string' },
|
|
88
|
+
help: { short: 'h', type: 'boolean' },
|
|
89
|
+
'ingest-url': { type: 'string' },
|
|
90
|
+
release: { type: 'string' },
|
|
91
|
+
token: { type: 'string' },
|
|
92
|
+
};
|
|
93
|
+
// ── upload commands (lenient by contract) ─────────────────────────
|
|
129
94
|
async function cmdUploadSourcemap(argv) {
|
|
95
|
+
const strict = isStrict(argv);
|
|
96
|
+
let parsed;
|
|
97
|
+
try {
|
|
98
|
+
parsed = parseArgs({ allowPositionals: true, args: stripStrict(argv), options: UPLOAD_OPTS });
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
console.error(`error: ${e.message}\n${HELP}`);
|
|
102
|
+
return 2;
|
|
103
|
+
}
|
|
104
|
+
if (parsed.values.help) {
|
|
105
|
+
console.log(HELP);
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
const c = parseCommon(parsed.values);
|
|
109
|
+
if (!c)
|
|
110
|
+
return 2;
|
|
111
|
+
if (parsed.positionals.length === 0) {
|
|
112
|
+
console.error('error: at least one sourcemap path is required');
|
|
113
|
+
return 2;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
for (const p of parsed.positionals) {
|
|
117
|
+
await uploadArtifact({ ...c, kind: 'sourcemap', path: p });
|
|
118
|
+
}
|
|
119
|
+
console.log(`uploaded ${parsed.positionals.length} sourcemap(s) for "${c.release}" — minified stacks on this release now resolve to source.`);
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
return lenientFail(strict, {
|
|
124
|
+
failure: `sourcemap upload failed (${e.message})`,
|
|
125
|
+
impact: `crashes from ${c.release} will show minified stacks until the map is uploaded.`,
|
|
126
|
+
retry: `sentori-cli upload sourcemap --release "${c.release}" --token <t> ${parsed.positionals.join(' ')}`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function cmdUploadDsym(argv) {
|
|
131
|
+
const strict = isStrict(argv);
|
|
130
132
|
let parsed;
|
|
131
133
|
try {
|
|
132
134
|
parsed = parseArgs({
|
|
133
135
|
allowPositionals: true,
|
|
134
|
-
args: argv,
|
|
136
|
+
args: stripStrict(argv),
|
|
135
137
|
options: {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
'
|
|
140
|
-
release: { type: 'string' },
|
|
141
|
-
token: { type: 'string' },
|
|
138
|
+
...UPLOAD_OPTS,
|
|
139
|
+
arch: { type: 'string' },
|
|
140
|
+
'debug-id': { type: 'string' },
|
|
141
|
+
'object-name': { type: 'string' },
|
|
142
142
|
},
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
145
|
catch (e) {
|
|
146
|
-
console.error(`error: ${e.message}\n`);
|
|
147
|
-
console.error(HELP);
|
|
146
|
+
console.error(`error: ${e.message}\n${HELP}`);
|
|
148
147
|
return 2;
|
|
149
148
|
}
|
|
150
149
|
if (parsed.values.help) {
|
|
@@ -154,47 +153,104 @@ async function cmdUploadSourcemap(argv) {
|
|
|
154
153
|
const c = parseCommon(parsed.values);
|
|
155
154
|
if (!c)
|
|
156
155
|
return 2;
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
const path = parsed.positionals[0];
|
|
157
|
+
if (!path) {
|
|
158
|
+
console.error('error: a path to a .dSYM bundle or DWARF binary is required');
|
|
159
|
+
return 2;
|
|
160
|
+
}
|
|
161
|
+
const debugId = parsed.values['debug-id'];
|
|
162
|
+
const arch = parsed.values.arch;
|
|
163
|
+
if ((debugId && !arch) || (arch && !debugId)) {
|
|
164
|
+
console.error('error: --debug-id and --arch must be passed together (or both omitted)');
|
|
159
165
|
return 2;
|
|
160
166
|
}
|
|
161
167
|
try {
|
|
162
|
-
const
|
|
168
|
+
const r = await uploadDsym({
|
|
163
169
|
apiUrl: c.apiUrl,
|
|
164
|
-
|
|
165
|
-
|
|
170
|
+
arch: typeof arch === 'string' ? arch : undefined,
|
|
171
|
+
debugId: typeof debugId === 'string' ? debugId : undefined,
|
|
172
|
+
objectName: typeof parsed.values['object-name'] === 'string'
|
|
173
|
+
? parsed.values['object-name']
|
|
174
|
+
: undefined,
|
|
175
|
+
path,
|
|
166
176
|
release: c.release,
|
|
167
177
|
token: c.token,
|
|
168
178
|
});
|
|
169
|
-
|
|
179
|
+
console.log(`uploaded ${r.slices.length} dSYM slice(s):`);
|
|
180
|
+
for (const s of r.slices)
|
|
181
|
+
console.log(` ${s.debugId} (${s.arch})`);
|
|
170
182
|
return 0;
|
|
171
183
|
}
|
|
172
184
|
catch (e) {
|
|
173
|
-
|
|
174
|
-
|
|
185
|
+
return lenientFail(strict, {
|
|
186
|
+
failure: `dSYM upload failed (${e.message})`,
|
|
187
|
+
impact: `native iOS stacks from ${c.release} stay unsymbolicated until the dSYM lands.`,
|
|
188
|
+
retry: `sentori-cli upload dsym --release "${c.release}" --token <t> ${path}`,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function cmdUploadMapping(argv) {
|
|
193
|
+
const strict = isStrict(argv);
|
|
194
|
+
let parsed;
|
|
195
|
+
try {
|
|
196
|
+
parsed = parseArgs({
|
|
197
|
+
allowPositionals: true,
|
|
198
|
+
args: stripStrict(argv),
|
|
199
|
+
options: { ...UPLOAD_OPTS, 'debug-id': { type: 'string' } },
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
console.error(`error: ${e.message}\n${HELP}`);
|
|
204
|
+
return 2;
|
|
205
|
+
}
|
|
206
|
+
if (parsed.values.help) {
|
|
207
|
+
console.log(HELP);
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
const c = parseCommon(parsed.values);
|
|
211
|
+
if (!c)
|
|
212
|
+
return 2;
|
|
213
|
+
const path = parsed.positionals[0];
|
|
214
|
+
if (!path) {
|
|
215
|
+
console.error('error: a path to mapping.txt is required');
|
|
216
|
+
return 2;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
await uploadMapping({
|
|
220
|
+
apiUrl: c.apiUrl,
|
|
221
|
+
debugId: typeof parsed.values['debug-id'] === 'string' ? parsed.values['debug-id'] : undefined,
|
|
222
|
+
path,
|
|
223
|
+
release: c.release,
|
|
224
|
+
token: c.token,
|
|
225
|
+
});
|
|
226
|
+
console.log(`uploaded mapping for "${c.release}" — R8 names on this release now demangle.`);
|
|
227
|
+
return 0;
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
return lenientFail(strict, {
|
|
231
|
+
failure: `mapping upload failed (${e.message})`,
|
|
232
|
+
impact: `Android stacks from ${c.release} stay R8-obfuscated until the mapping lands.`,
|
|
233
|
+
retry: `sentori-cli upload mapping --release "${c.release}" --token <t> ${path}`,
|
|
234
|
+
});
|
|
175
235
|
}
|
|
176
236
|
}
|
|
177
237
|
async function cmdReactNativeUpload(argv) {
|
|
238
|
+
const strict = isStrict(argv);
|
|
178
239
|
let parsed;
|
|
179
240
|
try {
|
|
180
241
|
parsed = parseArgs({
|
|
181
|
-
args: argv,
|
|
242
|
+
args: stripStrict(argv),
|
|
182
243
|
options: {
|
|
183
|
-
|
|
244
|
+
...UPLOAD_OPTS,
|
|
184
245
|
bundle: { type: 'string' },
|
|
185
246
|
'dry-run': { type: 'boolean' },
|
|
186
|
-
help: { short: 'h', type: 'boolean' },
|
|
187
247
|
'hermes-map': { type: 'string' },
|
|
188
|
-
'ingest-url': { type: 'string' },
|
|
189
248
|
'metro-map': { type: 'string' },
|
|
190
|
-
release: { type: 'string' },
|
|
191
|
-
token: { type: 'string' },
|
|
192
249
|
},
|
|
193
250
|
});
|
|
194
251
|
}
|
|
195
252
|
catch (e) {
|
|
196
|
-
console.error(`error: ${e.message}\n`);
|
|
197
|
-
console.error(HELP);
|
|
253
|
+
console.error(`error: ${e.message}\n${HELP}`);
|
|
198
254
|
return 2;
|
|
199
255
|
}
|
|
200
256
|
if (parsed.values.help) {
|
|
@@ -214,65 +270,31 @@ async function cmdReactNativeUpload(argv) {
|
|
|
214
270
|
const result = await reactNativeUpload({
|
|
215
271
|
apiUrl: c.apiUrl,
|
|
216
272
|
bundle: typeof parsed.values.bundle === 'string' ? parsed.values.bundle : undefined,
|
|
217
|
-
dryRun:
|
|
273
|
+
dryRun: parsed.values['dry-run'] === true,
|
|
218
274
|
hermesMap,
|
|
219
275
|
metroMap,
|
|
220
276
|
release: c.release,
|
|
221
277
|
token: c.token,
|
|
222
278
|
});
|
|
223
|
-
|
|
279
|
+
console.log(`uploaded ${result.uploaded ?? result.files.length} file(s) for "${c.release}".`);
|
|
224
280
|
return 0;
|
|
225
281
|
}
|
|
226
282
|
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;
|
|
283
|
+
return lenientFail(strict, {
|
|
284
|
+
failure: `react-native upload failed (${e.message})`,
|
|
285
|
+
impact: `Hermes stacks from ${c.release} stay unsymbolicated until the composed map lands.`,
|
|
286
|
+
retry: `sentori-cli react-native upload --release "${c.release}" --token <t> --metro-map ${metroMap} --hermes-map ${hermesMap}`,
|
|
287
|
+
});
|
|
254
288
|
}
|
|
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
289
|
}
|
|
261
|
-
|
|
290
|
+
// ── probes sync ───────────────────────────────────────────────────
|
|
291
|
+
async function cmdProbesSync(argv) {
|
|
292
|
+
const strict = isStrict(argv);
|
|
262
293
|
let parsed;
|
|
263
294
|
try {
|
|
264
295
|
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
|
-
},
|
|
296
|
+
args: stripStrict(argv),
|
|
297
|
+
options: { ...UPLOAD_OPTS, dir: { type: 'string' } },
|
|
276
298
|
});
|
|
277
299
|
}
|
|
278
300
|
catch (e) {
|
|
@@ -283,50 +305,41 @@ async function cmdIssueList(argv) {
|
|
|
283
305
|
console.log(HELP);
|
|
284
306
|
return 0;
|
|
285
307
|
}
|
|
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`);
|
|
308
|
+
const c = parseCommon(parsed.values);
|
|
309
|
+
if (!c)
|
|
292
310
|
return 2;
|
|
311
|
+
const dir = typeof parsed.values.dir === 'string' ? parsed.values.dir : '.';
|
|
312
|
+
const refs = scanProbes(dir);
|
|
313
|
+
if (refs.length === 0) {
|
|
314
|
+
console.log(`no sentori.probe() call sites found under ${dir} — nothing to register.`);
|
|
315
|
+
return 0;
|
|
293
316
|
}
|
|
294
|
-
const limitStr = parsed.values.limit;
|
|
295
|
-
const limit = limitStr ? Number.parseInt(limitStr, 10) : undefined;
|
|
296
317
|
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));
|
|
318
|
+
const r = await syncProbes({ apiUrl: c.apiUrl, token: c.token, release: c.release, refs });
|
|
319
|
+
console.log(`registered ${r.registered} probe(s) for "${c.release}": ${refs.join(', ')}`);
|
|
309
320
|
return 0;
|
|
310
321
|
}
|
|
311
322
|
catch (e) {
|
|
312
|
-
|
|
313
|
-
|
|
323
|
+
return lenientFail(strict, {
|
|
324
|
+
failure: `probes sync failed (${e.message})`,
|
|
325
|
+
impact: `silent probes on ${c.release} can't be told apart from deleted code until registered.`,
|
|
326
|
+
retry: `sentori-cli probes sync --release "${c.release}" --token <t> --dir ${dir}`,
|
|
327
|
+
});
|
|
314
328
|
}
|
|
315
329
|
}
|
|
316
|
-
|
|
330
|
+
// ── issue commands (the /api surface) ─────────────────────────────
|
|
331
|
+
const ISSUE_OPTS = {
|
|
332
|
+
'api-url': { type: 'string' },
|
|
333
|
+
help: { short: 'h', type: 'boolean' },
|
|
334
|
+
'ingest-url': { type: 'string' },
|
|
335
|
+
token: { type: 'string' },
|
|
336
|
+
};
|
|
337
|
+
async function cmdIssueList(argv) {
|
|
317
338
|
let parsed;
|
|
318
339
|
try {
|
|
319
340
|
parsed = parseArgs({
|
|
320
|
-
allowPositionals: true,
|
|
321
341
|
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
|
-
},
|
|
342
|
+
options: { ...ISSUE_OPTS, kind: { type: 'string' }, status: { type: 'string' } },
|
|
330
343
|
});
|
|
331
344
|
}
|
|
332
345
|
catch (e) {
|
|
@@ -337,45 +350,34 @@ async function cmdIssuePatch(argv, body, verb) {
|
|
|
337
350
|
console.log(HELP);
|
|
338
351
|
return 0;
|
|
339
352
|
}
|
|
340
|
-
const cfg =
|
|
353
|
+
const cfg = parseApiCfg(parsed.values);
|
|
341
354
|
if (!cfg)
|
|
342
355
|
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
356
|
try {
|
|
352
|
-
const
|
|
353
|
-
|
|
357
|
+
const rows = await listIssues(cfg, {
|
|
358
|
+
kind: parsed.values.kind,
|
|
359
|
+
status: parsed.values.status ?? 'open',
|
|
360
|
+
});
|
|
361
|
+
if (rows.length === 0) {
|
|
362
|
+
console.log('(no matching issues)');
|
|
363
|
+
return 0;
|
|
364
|
+
}
|
|
365
|
+
for (const r of rows)
|
|
366
|
+
console.log(formatIssueLine(r));
|
|
354
367
|
return 0;
|
|
355
368
|
}
|
|
356
369
|
catch (e) {
|
|
357
|
-
console.error(`issue
|
|
370
|
+
console.error(`issue list failed: ${e.message}`);
|
|
358
371
|
return 1;
|
|
359
372
|
}
|
|
360
373
|
}
|
|
361
|
-
|
|
362
|
-
async function cmdUploadDsym(argv) {
|
|
374
|
+
async function cmdIssueResolve(argv) {
|
|
363
375
|
let parsed;
|
|
364
376
|
try {
|
|
365
377
|
parsed = parseArgs({
|
|
366
378
|
allowPositionals: true,
|
|
367
379
|
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
|
-
},
|
|
380
|
+
options: { ...ISSUE_OPTS, 'in-release': { type: 'string' } },
|
|
379
381
|
});
|
|
380
382
|
}
|
|
381
383
|
catch (e) {
|
|
@@ -386,56 +388,31 @@ async function cmdUploadDsym(argv) {
|
|
|
386
388
|
console.log(HELP);
|
|
387
389
|
return 0;
|
|
388
390
|
}
|
|
389
|
-
const cfg =
|
|
391
|
+
const cfg = parseApiCfg(parsed.values);
|
|
390
392
|
if (!cfg)
|
|
391
393
|
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)');
|
|
394
|
+
const issueId = parsed.positionals[0];
|
|
395
|
+
if (!issueId) {
|
|
396
|
+
console.error('error: <issue-id> is required');
|
|
401
397
|
return 2;
|
|
402
398
|
}
|
|
403
399
|
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})`);
|
|
400
|
+
await resolveIssue(cfg, issueId, parsed.values['in-release']);
|
|
401
|
+
console.log(`${issueId} → resolved${parsed.values['in-release'] ? ` (in ${parsed.values['in-release']})` : ''}`);
|
|
417
402
|
return 0;
|
|
418
403
|
}
|
|
419
404
|
catch (e) {
|
|
420
|
-
console.error(`
|
|
405
|
+
console.error(`issue resolve failed: ${e.message}`);
|
|
421
406
|
return 1;
|
|
422
407
|
}
|
|
423
408
|
}
|
|
424
|
-
async function
|
|
409
|
+
async function cmdIssueNote(argv) {
|
|
425
410
|
let parsed;
|
|
426
411
|
try {
|
|
427
412
|
parsed = parseArgs({
|
|
428
413
|
allowPositionals: true,
|
|
429
414
|
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
|
-
},
|
|
415
|
+
options: { ...ISSUE_OPTS, body: { type: 'string' } },
|
|
439
416
|
});
|
|
440
417
|
}
|
|
441
418
|
catch (e) {
|
|
@@ -446,51 +423,29 @@ async function cmdUploadMapping(argv) {
|
|
|
446
423
|
console.log(HELP);
|
|
447
424
|
return 0;
|
|
448
425
|
}
|
|
449
|
-
const cfg =
|
|
426
|
+
const cfg = parseApiCfg(parsed.values);
|
|
450
427
|
if (!cfg)
|
|
451
428
|
return 2;
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
429
|
+
const issueId = parsed.positionals[0];
|
|
430
|
+
const body = parsed.values.body;
|
|
431
|
+
if (!issueId || !body) {
|
|
432
|
+
console.error('error: <issue-id> and --body are required');
|
|
455
433
|
return 2;
|
|
456
434
|
}
|
|
457
435
|
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}` : ''}`);
|
|
436
|
+
await noteIssue(cfg, issueId, body);
|
|
437
|
+
console.log(`${issueId} ← note added`);
|
|
467
438
|
return 0;
|
|
468
439
|
}
|
|
469
440
|
catch (e) {
|
|
470
|
-
console.error(`
|
|
441
|
+
console.error(`issue note failed: ${e.message}`);
|
|
471
442
|
return 1;
|
|
472
443
|
}
|
|
473
444
|
}
|
|
474
|
-
async function
|
|
445
|
+
async function cmdIssueBundle(argv) {
|
|
475
446
|
let parsed;
|
|
476
447
|
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
|
-
});
|
|
448
|
+
parsed = parseArgs({ allowPositionals: true, args: argv, options: ISSUE_OPTS });
|
|
494
449
|
}
|
|
495
450
|
catch (e) {
|
|
496
451
|
console.error(`error: ${e.message}\n${HELP}`);
|
|
@@ -500,55 +455,28 @@ async function cmdUploadSourceBundle(argv) {
|
|
|
500
455
|
console.log(HELP);
|
|
501
456
|
return 0;
|
|
502
457
|
}
|
|
503
|
-
const cfg =
|
|
458
|
+
const cfg = parseApiCfg(parsed.values);
|
|
504
459
|
if (!cfg)
|
|
505
460
|
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');
|
|
461
|
+
const issueId = parsed.positionals[0];
|
|
462
|
+
if (!issueId) {
|
|
463
|
+
console.error('error: <issue-id> is required');
|
|
519
464
|
return 2;
|
|
520
465
|
}
|
|
521
466
|
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)}…)`);
|
|
467
|
+
console.log(await fetchBundle(cfg, issueId));
|
|
532
468
|
return 0;
|
|
533
469
|
}
|
|
534
470
|
catch (e) {
|
|
535
|
-
console.error(`
|
|
471
|
+
console.error(`issue bundle failed: ${e.message}`);
|
|
536
472
|
return 1;
|
|
537
473
|
}
|
|
538
474
|
}
|
|
475
|
+
// ── mcp ───────────────────────────────────────────────────────────
|
|
539
476
|
async function cmdMcpServe(argv) {
|
|
540
477
|
let parsed;
|
|
541
478
|
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
|
-
});
|
|
479
|
+
parsed = parseArgs({ args: argv, options: ISSUE_OPTS });
|
|
552
480
|
}
|
|
553
481
|
catch (e) {
|
|
554
482
|
console.error(`error: ${e.message}\n${HELP}`);
|
|
@@ -558,11 +486,11 @@ async function cmdMcpServe(argv) {
|
|
|
558
486
|
console.log(HELP);
|
|
559
487
|
return 0;
|
|
560
488
|
}
|
|
561
|
-
const cfg =
|
|
489
|
+
const cfg = parseApiCfg(parsed.values);
|
|
562
490
|
if (!cfg)
|
|
563
491
|
return 2;
|
|
564
492
|
try {
|
|
565
|
-
await runMcpServer({ apiUrl: cfg.apiUrl,
|
|
493
|
+
await runMcpServer({ apiUrl: cfg.apiUrl, token: cfg.token });
|
|
566
494
|
return 0;
|
|
567
495
|
}
|
|
568
496
|
catch (e) {
|
|
@@ -582,20 +510,20 @@ async function main(argv) {
|
|
|
582
510
|
return cmdUploadDsym(rest);
|
|
583
511
|
if (a === 'upload' && b === 'mapping')
|
|
584
512
|
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
513
|
if (a === 'react-native' && b === 'upload')
|
|
590
514
|
return cmdReactNativeUpload(rest);
|
|
515
|
+
if (a === 'probes' && b === 'sync')
|
|
516
|
+
return cmdProbesSync(rest);
|
|
517
|
+
if (a === 'mcp' && b === 'serve')
|
|
518
|
+
return cmdMcpServe(rest);
|
|
591
519
|
if (a === 'issue' && b === 'list')
|
|
592
520
|
return cmdIssueList(rest);
|
|
593
521
|
if (a === 'issue' && b === 'resolve')
|
|
594
|
-
return
|
|
595
|
-
if (a === 'issue' && b === '
|
|
596
|
-
return
|
|
597
|
-
if (a === 'issue' && b === '
|
|
598
|
-
return
|
|
522
|
+
return cmdIssueResolve(rest);
|
|
523
|
+
if (a === 'issue' && b === 'note')
|
|
524
|
+
return cmdIssueNote(rest);
|
|
525
|
+
if (a === 'issue' && b === 'bundle')
|
|
526
|
+
return cmdIssueBundle(rest);
|
|
599
527
|
if (a === 'push' && b === 'send')
|
|
600
528
|
return cmdPushSend(rest);
|
|
601
529
|
if (a === 'push' && b === 'receipt')
|