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