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