@goliapkg/sentori-cli 1.8.0 → 1.10.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/LINT.md +237 -0
- package/README.md +65 -8
- package/examples/insight-lint.toml +32 -0
- package/lib/artifacts.d.ts +7 -0
- package/lib/artifacts.d.ts.map +1 -1
- package/lib/artifacts.js +1 -1
- package/lib/artifacts.js.map +1 -1
- package/lib/compatibility.js +1 -1
- package/lib/compatibility.js.map +1 -1
- package/lib/index.js +13 -5
- package/lib/index.js.map +1 -1
- package/lib/issue.d.ts +13 -2
- package/lib/issue.d.ts.map +1 -1
- package/lib/issue.js +12 -1
- package/lib/issue.js.map +1 -1
- package/lib/lint/config.d.ts +19 -0
- package/lib/lint/config.d.ts.map +1 -0
- package/lib/lint/config.js +149 -0
- package/lib/lint/config.js.map +1 -0
- package/lib/lint/index.d.ts +4 -0
- package/lib/lint/index.d.ts.map +1 -0
- package/lib/lint/index.js +204 -0
- package/lib/lint/index.js.map +1 -0
- package/lib/lint/report.d.ts +48 -0
- package/lib/lint/report.d.ts.map +1 -0
- package/lib/lint/report.js +136 -0
- package/lib/lint/report.js.map +1 -0
- package/lib/lint/rules.d.ts +28 -0
- package/lib/lint/rules.d.ts.map +1 -0
- package/lib/lint/rules.js +57 -0
- package/lib/lint/rules.js.map +1 -0
- package/lib/mcp.d.ts +6 -0
- package/lib/mcp.d.ts.map +1 -1
- package/lib/mcp.js +228 -65
- package/lib/mcp.js.map +1 -1
- package/lib/workflow.d.ts.map +1 -1
- package/lib/workflow.js +12 -2
- package/lib/workflow.js.map +1 -1
- package/package.json +4 -2
- package/src/__tests__/issue.test.ts +27 -0
- package/src/__tests__/lint.test.ts +185 -0
- package/src/__tests__/mcp.test.ts +128 -1
- package/src/__tests__/workflow.test.ts +41 -1
- package/src/artifacts.ts +3 -1
- package/src/compatibility.ts +1 -1
- package/src/index.ts +12 -5
- package/src/issue.ts +16 -5
- package/src/lint/config.ts +102 -0
- package/src/lint/index.ts +163 -0
- package/src/lint/report.ts +129 -0
- package/src/lint/rules.ts +64 -0
- package/src/mcp.ts +205 -67
- package/src/workflow.ts +7 -2
package/LINT.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Swift and Kotlin lint preview 0.3.0
|
|
2
|
+
|
|
3
|
+
`sentori lint` is a local quality gate. The existing `sentori-cli` command remains
|
|
4
|
+
available, including `sentori-cli lint`. This preview checks Swift and Kotlin sources. There is no upload, build plugin, or server dependency.
|
|
5
|
+
|
|
6
|
+
The analyzer is a separate developer tool. Installing the iOS runtime SDK does
|
|
7
|
+
not add SwiftSyntax or change the SDK's supported Swift version.
|
|
8
|
+
|
|
9
|
+
## Build and run
|
|
10
|
+
|
|
11
|
+
From a Sentori source checkout, with Node 18+ and the toolchain for each language
|
|
12
|
+
you scan (Swift 6.4; Java 17+ for Kotlin):
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
swift build --package-path sdk/native/ios/Lint -c release --jobs 2
|
|
16
|
+
export SENTORI_LINT_SWIFT="$PWD/sdk/native/ios/Lint/.build/release/sentori-lint-swift"
|
|
17
|
+
# Kotlin analyzer (standalone; no Android SDK or Gradle project needed):
|
|
18
|
+
(cd sdk/native/android/lint && ./gradlew --no-daemon check installDist)
|
|
19
|
+
export SENTORI_LINT_KOTLIN="$PWD/sdk/native/android/lint/build/install/sentori-lint-kotlin/bin/sentori-lint-kotlin"
|
|
20
|
+
bun run --cwd sdk/cli build
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The Swift analyzer's only package dependency is the official `swiftlang/swift-syntax`,
|
|
24
|
+
locked to stable 604.0.0. It reads a versioned JSON request on stdin and emits a
|
|
25
|
+
single JSON response on stdout. `sentori-lint-swift --version` prints its version.
|
|
26
|
+
The CLI rejects incompatible, incomplete, or malformed analyzer responses.
|
|
27
|
+
|
|
28
|
+
After installing `@goliapkg/sentori-cli`, use `sentori lint`. From a source checkout,
|
|
29
|
+
invoke `node /path/to/sentori/sdk/cli/lib/index.js lint`.
|
|
30
|
+
`--swift-analyzer /absolute/path/to/sentori-lint-swift` overrides the environment;
|
|
31
|
+
otherwise the CLI looks for `sentori-lint-swift` on PATH. No binary is downloaded
|
|
32
|
+
automatically. `--kotlin-analyzer /absolute/path/to/sentori-lint-kotlin` similarly
|
|
33
|
+
overrides `SENTORI_LINT_KOTLIN`, then PATH. Keep the entire Gradle `installDist`
|
|
34
|
+
directory together: its launcher needs the jars in the adjacent `lib/` directory.
|
|
35
|
+
The Kotlin analyzer uses the official Kotlin compiler PSI 2.4.21 plus its standard
|
|
36
|
+
library; it neither loads the application's Gradle build nor runs compiler plugins.
|
|
37
|
+
Only analyzers for selected languages are required. Both return findings to the
|
|
38
|
+
CLI, which applies policy, emits text/JSON/SARIF and determines the gate exit code.
|
|
39
|
+
|
|
40
|
+
Place `sentori-lint.toml` at the repository root. Then:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
sentori lint
|
|
44
|
+
sentori lint --format sarif > sentori-lint.sarif
|
|
45
|
+
sentori lint --explain SWCON001
|
|
46
|
+
sentori lint --changed HEAD
|
|
47
|
+
sentori lint Sources/Feature
|
|
48
|
+
sentori lint --fix
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Files come from Git's tracked and untracked, non-ignored file lists. Paths are
|
|
52
|
+
relative to the Git root, and positional paths are resolved from the working
|
|
53
|
+
directory. Deleted files are omitted; source symlinks are rejected. Generated sources are
|
|
54
|
+
excluded only when explicitly configured. Configured test files participate in function-length checks only; other rules skip them. All `#if`
|
|
55
|
+
branches are analyzed, independent of the platform running the tool. The analyzer
|
|
56
|
+
parses syntax, not types or runtime behavior.
|
|
57
|
+
|
|
58
|
+
`--fix` delegates formatting to the installed toolchain's `swift format` and
|
|
59
|
+
rescans afterward. It never rewrites forced casts, error handling, or concurrency
|
|
60
|
+
semantics. A scan selecting Kotlin sources rejects `--fix` before modifying any file;
|
|
61
|
+
select Swift paths explicitly to format only those sources. Inspect the resulting source diff before committing it.
|
|
62
|
+
|
|
63
|
+
Exit codes are **0** for a passing gate, **1** for rule/file count regressions or hard-limit violations,
|
|
64
|
+
and **2** for invalid configuration, parse failures, missing tools, or other
|
|
65
|
+
execution errors. A full scan selecting no Swift or Kotlin files fails explicitly. An empty
|
|
66
|
+
incremental scan succeeds and reports zero files. Both `.swift` and `.kt` files participate; Kotlin scripts (`.kts`) are outside this preview.
|
|
67
|
+
|
|
68
|
+
## Configuration
|
|
69
|
+
|
|
70
|
+
This preview supports a strict TOML subset: single-line quoted string values,
|
|
71
|
+
single-line arrays of double-quoted strings, `version = 1`, a `[rules]` table,
|
|
72
|
+
and `[[allow]]` entries. Comments are supported outside strings. Unsupported
|
|
73
|
+
syntax, unknown keys, duplicate keys, and unknown rule IDs fail rather than
|
|
74
|
+
silently changing policy. There is no runtime TOML library dependency.
|
|
75
|
+
|
|
76
|
+
```toml
|
|
77
|
+
version = 1
|
|
78
|
+
include = ["Sources/**"]
|
|
79
|
+
exclude = []
|
|
80
|
+
tests = ["**/Tests/**", "**/*Tests.swift", "**/*Test.swift"]
|
|
81
|
+
excludePathRegex = ["generated"]
|
|
82
|
+
|
|
83
|
+
[rules]
|
|
84
|
+
SWSAFE001 = "error"
|
|
85
|
+
SWSAFE002 = "error"
|
|
86
|
+
|
|
87
|
+
[[allow]]
|
|
88
|
+
rule = "SWCON002"
|
|
89
|
+
path = "Sources/LockedBox.swift"
|
|
90
|
+
reason = "All mutable state is protected by the instance lock; reviewed in PR 123."
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Path patterns support `*`, `**`, and `?`, use `/`, and match the entire
|
|
94
|
+
repository-relative path. Brace expansion and character classes are rejected.
|
|
95
|
+
`excludePathRegex` contains case-insensitive JavaScript regular expressions,
|
|
96
|
+
matched anywhere in the repository-relative path. An allowance needs a known
|
|
97
|
+
rule, a relative path pattern, and a nonempty reason. Allowances remain visible
|
|
98
|
+
in text/JSON and as external SARIF suppressions. Source comments cannot suppress
|
|
99
|
+
findings. Safety, concurrency and length rules default to `error`; parameter and Android style policies default to `warn`. The three
|
|
100
|
+
`SWSAFE` rules can be `off` or `warn`;
|
|
101
|
+
the `SWCON` and `KTCON` rules always require a reviewed allowance to bypass the gate.
|
|
102
|
+
`SWAPI001` and `SWAPI002` default to `warn`. Enabled length rules always treat hard-limit
|
|
103
|
+
violations as errors, even when their target checks are configured as warnings.
|
|
104
|
+
|
|
105
|
+
[The insight example](examples/insight-lint.toml) restricts scanning to its four
|
|
106
|
+
active trees, applies only function-length checks to configured test files, and
|
|
107
|
+
excludes any path containing `generated`, case-insensitively. It deliberately includes no allowances.
|
|
108
|
+
|
|
109
|
+
## Rules
|
|
110
|
+
|
|
111
|
+
These are **project policies informed by official documentation**, not claims
|
|
112
|
+
that either language prohibits these language features. `--explain` supplies the official
|
|
113
|
+
reference, section, group, rationale, severity, and fixability for each rule.
|
|
114
|
+
|
|
115
|
+
| Rule | Check | Baseline permitted |
|
|
116
|
+
| --- | --- | --- |
|
|
117
|
+
| SWSAFE001 | `try!` expressions | Yes |
|
|
118
|
+
| SWSAFE002 | `as!` expressions | Yes |
|
|
119
|
+
| SWCON001 | `nonisolated(unsafe)` modifiers | No |
|
|
120
|
+
| SWCON002 | `@unchecked Sendable` conformances | No |
|
|
121
|
+
| SWCON003 | `Task.detached` / `Swift.Task.detached` calls | No |
|
|
122
|
+
| SWMAINT001 | Function length: target 50, hard limit 200 lines | Target only |
|
|
123
|
+
| SWMAINT002 | File length: target 300, hard limit 500 lines | Target only |
|
|
124
|
+
| KTSAFE001 | Kotlin postfix not-null assertions (`value!!`) | Yes |
|
|
125
|
+
| KTCON001 | Imported/qualified `kotlinx.coroutines.GlobalScope` uses | No |
|
|
126
|
+
| KTCON002 | Imported/qualified `kotlinx.coroutines.runBlocking` calls in main sources | No |
|
|
127
|
+
| KTMAINT001 | Kotlin function length: target 50, hard limit 200 lines | Target only |
|
|
128
|
+
| KTMAINT002 | Kotlin file length: target 300, hard limit 500 lines | Target only |
|
|
129
|
+
| KTAPI001 | More than 5 function/constructor parameters (warning) | If configured as error |
|
|
130
|
+
| KTAPI002 | Explicit non-nullable `Boolean` parameters (warning) | If configured as error |
|
|
131
|
+
| AKS001 | Wildcard imports (warning) | If configured as error |
|
|
132
|
+
| AKS002 | Control-flow braces, with single-line branch exceptions (warning) | If configured as error |
|
|
133
|
+
| AKS003 | ASCII `const val` names outside UPPER_SNAKE_CASE (warning) | If configured as error |
|
|
134
|
+
| SWSAFE003 | Postfix force unwrap expressions (`value!`) | Yes |
|
|
135
|
+
| SWAPI001 | More than 5 explicit declaration parameters (default warning) | If configured as error |
|
|
136
|
+
| SWAPI002 | Explicit `Bool` parameters (default warning) | If configured as error |
|
|
137
|
+
|
|
138
|
+
The detached-task rule recognizes explicit names, including generic arguments;
|
|
139
|
+
it does not resolve shadowed `Task` names, aliases, or indirect calls. An
|
|
140
|
+
allowance can document a same-spelled unrelated type. Findings use 1-based UTF-16
|
|
141
|
+
columns, including Unicode and CRLF source files. Strings and comments are not
|
|
142
|
+
analyzed as code.
|
|
143
|
+
|
|
144
|
+
Function length starts at the `func` keyword's physical line and ends at its
|
|
145
|
+
body's closing brace, counting blank lines and comments. Attribute lines before
|
|
146
|
+
`func` are not counted. Named nested/operator functions and test functions are
|
|
147
|
+
included. Initializers, accessors, and closures are not measured by this rule.
|
|
148
|
+
File length counts physical lines, including comments and blanks, without a
|
|
149
|
+
phantom line after the final terminator. LF, CRLF, and lone CR are supported.
|
|
150
|
+
The numeric limits are project policies, not official Swift limits.
|
|
151
|
+
|
|
152
|
+
Parameter count includes functions, initializers, and subscripts, including
|
|
153
|
+
parameters with defaults and variadics; it excludes closure parameters and
|
|
154
|
+
function-type argument lists. The Bool policy matches `Bool`, `Swift.Bool`, and
|
|
155
|
+
attributed forms such as `inout Bool`, but not optional or collection types,
|
|
156
|
+
aliases, or closure parameters. Force unwraps use expression nodes and never
|
|
157
|
+
count IUO type declarations (`var value: String!`) or logical negation.
|
|
158
|
+
|
|
159
|
+
Kotlin functions are measured from `fun` through the end of the body, including
|
|
160
|
+
named nested and expression-body functions. Constructors, accessors and lambdas
|
|
161
|
+
are excluded. File and function limits use the same physical-line policy as Swift.
|
|
162
|
+
Configured tests participate only in function length, including its hard limit.
|
|
163
|
+
|
|
164
|
+
Coroutine checks recognize explicit, aliased and wildcard imports and fully
|
|
165
|
+
qualified names; they do not resolve local shadowing, indirect calls or type aliases.
|
|
166
|
+
Imports themselves are not GlobalScope usages. `!!` checks parse expression nodes,
|
|
167
|
+
so comments and string text do not create debt; code inside string interpolation
|
|
168
|
+
does. Kotlin findings also use 1-based UTF-16 columns with LF, CRLF and lone CR support.
|
|
169
|
+
|
|
170
|
+
SPM/Gradle integration plugins and server uploads are not part of this preview.
|
|
171
|
+
|
|
172
|
+
## Baselines
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
sentori lint --baseline sentori-lint-baseline.json --init-baseline
|
|
176
|
+
sentori lint --baseline sentori-lint-baseline.json
|
|
177
|
+
sentori lint --baseline sentori-lint-baseline.json --update-baseline
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Initialization requires a nonexistent destination and a successful full scan.
|
|
181
|
+
Resolve or explicitly allow concurrency escape hatches and hard-limit
|
|
182
|
+
violations before initializing.
|
|
183
|
+
Thereafter, updates can only lower existing error counts or remove resolved
|
|
184
|
+
entries; they cannot add files/rules or increase counts. A new file starts at
|
|
185
|
+
zero. Warnings do not fail the gate and are not baseline debt. Partial scans and
|
|
186
|
+
`--changed` cannot initialize/update baselines. Tool or parse errors never write
|
|
187
|
+
a baseline, and rule regressions leave it unchanged.
|
|
188
|
+
|
|
189
|
+
Length targets participate in the count ratchet. Their hard limits are checked
|
|
190
|
+
independently: a baselined 51-line function growing to 201 lines fails even though
|
|
191
|
+
its rule/file count stays at one. Hard-limit findings never enter a baseline;
|
|
192
|
+
only an explicit allowance with a reason can waive them. JSON findings include
|
|
193
|
+
`measured` and `hardLimitExceeded`; aggregate regressions distinguish `count`
|
|
194
|
+
from `hard-limit`. SARIF preserves these measurements in result properties.
|
|
195
|
+
|
|
196
|
+
Counts are tracked by rule and file, not diagnostic fingerprint. Replacing one
|
|
197
|
+
finding with another in the same rule/file bucket is therefore not detected.
|
|
198
|
+
Renamed files are treated as new files. Full scans can drop debt for deleted files.
|
|
199
|
+
|
|
200
|
+
The baseline records the ruleset and a configuration digest. Changing exclusions,
|
|
201
|
+
allowances, or severities requires an explicit policy migration: retain the old
|
|
202
|
+
baseline, initialize a different file under the reviewed new policy, and review
|
|
203
|
+
the count differences before replacing it. `--update-baseline` does not perform
|
|
204
|
+
that migration or accept a policy change silently. Updates use a lock and atomic
|
|
205
|
+
replacement. A lock left by a killed writer must be removed only after confirming
|
|
206
|
+
that writer has exited.
|
|
207
|
+
|
|
208
|
+
SARIF 2.1.0 output includes **all enabled findings**, including baseline debt;
|
|
209
|
+
a count baseline cannot truthfully mark individual findings as unchanged.
|
|
210
|
+
Aggregate regressions appear in run properties. Consumers should use the CLI
|
|
211
|
+
exit status for the ratchet gate rather than treating every SARIF error-level
|
|
212
|
+
finding as a new violation. JSON includes scanned files, scope completeness,
|
|
213
|
+
findings, allowances, regressions, and whether the baseline can decrease.
|
|
214
|
+
|
|
215
|
+
## Moving from preview 0.1.0
|
|
216
|
+
|
|
217
|
+
The 0.2.1 analyzer and CLI use the `swift-quality-v2.1` ruleset. Pair matching
|
|
218
|
+
versions; a mismatched analyzer or a 0.1.0 baseline fails explicitly. Retain the
|
|
219
|
+
old baseline, initialize a separate 0.2.1 baseline, and review the old rule counts
|
|
220
|
+
as well as the new debt before adopting it. This is an explicit policy migration,
|
|
221
|
+
not a way to increase the old rules' allowances unnoticed. Test files now
|
|
222
|
+
participate in function-length checks, so the total scanned-file count increases
|
|
223
|
+
while the previous five rules still exclude configured tests.
|
|
224
|
+
|
|
225
|
+
Preview 0.2.1 also changes the parameter-count default from the 0.2.0 error
|
|
226
|
+
level to warning. Its ruleset revision rejects old baselines so this policy
|
|
227
|
+
change cannot silently erase error debt. Explicit `error` remains supported.
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
## Moving to preview 0.3.0
|
|
231
|
+
|
|
232
|
+
Swift's analyzer protocol remains `swift-syntax-604.0.0/swift-quality-v2.1`;
|
|
233
|
+
Kotlin uses `kotlin-2.4.21/kotlin-quality-v1`. The CLI verifies each independently.
|
|
234
|
+
Reports and baselines record the combined ruleset. Retain any 0.2.1 baseline,
|
|
235
|
+
initialize a separate 0.3.0 baseline, and compare the Swift counts before adopting
|
|
236
|
+
it alongside the new Kotlin debt. An old baseline is rejected explicitly; updating
|
|
237
|
+
never silently migrates policies. JSON `language` is `swift`, `kotlin`, or `mixed`.
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ machine and does not add runtime work to the application.
|
|
|
7
7
|
|
|
8
8
|
## Install the correct version
|
|
9
9
|
|
|
10
|
-
This guide requires **CLI 1.
|
|
10
|
+
This guide requires **CLI 1.10.0**. Check registry availability before updating
|
|
11
11
|
your build pipeline. The older 1.5.0 package has only the
|
|
12
12
|
`sentori-cli` executable and does not have `init`, `doctor`, `upload all`, ZIP
|
|
13
13
|
packaging or exact-file `verify`. Do not install 1.5.0 and run this guide's commands.
|
|
@@ -16,17 +16,17 @@ Use Node **18.17 or newer**; use a maintained Node release for CI. Pin the CLI a
|
|
|
16
16
|
development dependency and commit the package lock:
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
|
-
npm view @goliapkg/sentori-cli@1.
|
|
20
|
-
npm install --save-dev --save-exact @goliapkg/sentori-cli@1.
|
|
19
|
+
npm view @goliapkg/sentori-cli@1.10.0 version
|
|
20
|
+
npm install --save-dev --save-exact @goliapkg/sentori-cli@1.10.0
|
|
21
21
|
npx --no-install sentori --version
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
The last command must print `sentori 1.
|
|
24
|
+
The last command must print `sentori 1.10.0`. If npm cannot find that version, use
|
|
25
25
|
the tested package supplied by the Sentori team, without assuming `latest` has
|
|
26
26
|
these features:
|
|
27
27
|
|
|
28
28
|
```sh
|
|
29
|
-
npm install --save-dev --save-exact /absolute/path/goliapkg-sentori-cli-1.
|
|
29
|
+
npm install --save-dev --save-exact /absolute/path/goliapkg-sentori-cli-1.10.0.tgz
|
|
30
30
|
npx --no-install sentori --version
|
|
31
31
|
```
|
|
32
32
|
|
|
@@ -49,7 +49,7 @@ package if the local executable is missing. The server maintenance binary
|
|
|
49
49
|
|
|
50
50
|
| Value | Meaning |
|
|
51
51
|
| --- | --- |
|
|
52
|
-
| CLI `1.
|
|
52
|
+
| CLI `1.10.0` | Build-tool package version, shown by `sentori --version` |
|
|
53
53
|
| Server version | Running server binary, reported by `sentori doctor` |
|
|
54
54
|
| SDK version | Independently versioned app library; it need not equal the CLI or server |
|
|
55
55
|
| Config `version: 1` | Upload configuration schema, not a product version |
|
|
@@ -277,7 +277,7 @@ Native tooling. A Metro map alone does not resolve Hermes bytecode offsets.
|
|
|
277
277
|
|
|
278
278
|
| Result | Action |
|
|
279
279
|
| --- | --- |
|
|
280
|
-
| No `sentori` binary / unknown `init` | Check the installed package version; use 1.
|
|
280
|
+
| No `sentori` binary / unknown `init` | Check the installed package version; use 1.10.0 or its supplied tarball, with `npx --no-install` |
|
|
281
281
|
| `doctor` rejects ZIP capability | Upgrade/deploy the compatible server; CLI installation cannot enable server support |
|
|
282
282
|
| 401 / `admin_token_required` | Use an API-scope token for the correct project and instance |
|
|
283
283
|
| Local file missing | Build first; resolve paths relative to the configuration file |
|
|
@@ -302,7 +302,7 @@ an API token into browser or mobile bundles.
|
|
|
302
302
|
export SENTORI_API_TOKEN=st_...
|
|
303
303
|
npx --no-install sentori project show --json
|
|
304
304
|
npx --no-install sentori app list --json
|
|
305
|
-
npx --no-install sentori issue list --app <app-uuid> --environment production --json
|
|
305
|
+
npx --no-install sentori issue list --app <app-uuid> --environment production --json --limit 50 --offset 0
|
|
306
306
|
```
|
|
307
307
|
|
|
308
308
|
These project/app commands require the CLI release shipped with server 5.1.17;
|
|
@@ -349,3 +349,60 @@ or `runs --id` returns zero for passed, one for site failed and two for monitori
|
|
|
349
349
|
errors, unfinished/cancelled runs or transport errors. `--wait` requires `--app`
|
|
350
350
|
and waits up to 330 seconds. API and invalid-command errors always return two.
|
|
351
351
|
The configuration contract is documented in [the protocol](../../docs/protocol.md#web-monitoring).
|
|
352
|
+
|
|
353
|
+
Issue lists use offset pagination: `--limit` accepts 1–200 (default 50), and
|
|
354
|
+
`--offset` is a non-negative integer (default 0). JSON output includes `issues`,
|
|
355
|
+
`total`, `limit`, `offset`, and `nextOffset`; request the returned `nextOffset`
|
|
356
|
+
until it is null. These metadata fields and offset support require a server
|
|
357
|
+
with API pagination support. Ordering reflects live impact, so rows can move
|
|
358
|
+
between pages while new events arrive; this is not a snapshot export.
|
|
359
|
+
|
|
360
|
+
## Issue automation (MCP)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
```sh
|
|
364
|
+
npx --no-install sentori mcp serve --api-url "$SENTORI_API_URL" --token "$SENTORI_TOKEN"
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Then point a MCP-compatible agent (Claude Code config / Cursor / etc.)
|
|
368
|
+
at the spawned stdio server. Use a project `api`-scope token; an SDK ingest
|
|
369
|
+
token cannot read issues.
|
|
370
|
+
|
|
371
|
+
- `sentori_issue_watch`: durable changes with resumable `cursor`, bounded
|
|
372
|
+
`limit` and long polling through `waitSeconds` (0–25).
|
|
373
|
+
- `sentori_issue_read`: structured evidence, including missing context,
|
|
374
|
+
activity and authenticated attachment links.
|
|
375
|
+
- `sentori_issue_bundle`: preserves its Markdown default; also accepts
|
|
376
|
+
`format: "json"`.
|
|
377
|
+
- `sentori_issue_attachment`: read screenshot images or text/JSON diagnostics
|
|
378
|
+
from the bundle, with a 1 MiB inline limit.
|
|
379
|
+
- `sentori_issue_list`, `sentori_issue_note`, `sentori_issue_resolve`: ranked
|
|
380
|
+
triage and investigation/fix writeback.
|
|
381
|
+
|
|
382
|
+
Persist `nextCursor` only after durably enqueueing the page. An empty page
|
|
383
|
+
is a heartbeat; keep watching. Deduplicate replayed changes by cursor and
|
|
384
|
+
fetch the current bundle before acting. Watch is a change feed, not an
|
|
385
|
+
exclusive work queue. JSON results include MCP `structuredContent`.
|
|
386
|
+
|
|
387
|
+
The same interfaces work directly over HTTP with bearer authentication,
|
|
388
|
+
without MCP or a browser. See the [machine consumption contract](../../docs/protocol.md#machine-issue-consumption-read-and-watch)
|
|
389
|
+
for replay, errors, evidence limits, and the consumer checkpoint rules.
|
|
390
|
+
|
|
391
|
+
→ Full reference: `sentori-cli --help`
|
|
392
|
+
→ Recipes: [sentori.golia.jp/docs/recipes/sourcemap-upload](https://sentori.golia.jp/docs/recipes/sourcemap-upload)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
`sentori verify` and the verification after `sentori upload all` require server
|
|
396
|
+
5.3.0 or newer. They request `GET /v1/releases/{release}/artifacts?verify=true`,
|
|
397
|
+
which rereads stored blobs, compares their SHA256 and byte size, and repeats
|
|
398
|
+
format validation. Inventory metadata alone cannot pass verification. Missing,
|
|
399
|
+
truncated or changed blobs fail verification; rerunning the upload repairs a
|
|
400
|
+
corrupt blob from the original bytes. Verification describes storage at the
|
|
401
|
+
moment of the check and does not prove that an event matches this build.
|
|
402
|
+
|
|
403
|
+
## Local Swift and Kotlin quality checks
|
|
404
|
+
|
|
405
|
+
`sentori lint` checks safety, concurrency and structure with standalone SwiftSyntax
|
|
406
|
+
and Kotlin PSI analyzers, a decreasing baseline, and JSON/SARIF reports. See
|
|
407
|
+
[LINT.md](LINT.md) for installation, rule metadata, configuration, and limitations.
|
|
408
|
+
Lint runs locally and does not need a server or API token.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
include = ["insight-ios/**", "insight-android/**", "insight-common-ios/**", "insight-common-android/**"]
|
|
3
|
+
exclude = ["insight-consumer-*/**"]
|
|
4
|
+
# Test files participate in function-length checks only.
|
|
5
|
+
tests = ["insight-common-ios/Tests/**", "**/*Tests.swift", "**/*Test.swift", "**/src/test/**", "**/src/androidTest/**", "insight-common-android/contract/src/**"]
|
|
6
|
+
# Case-insensitive regular expressions matched against repository-relative paths.
|
|
7
|
+
excludePathRegex = ["generated"]
|
|
8
|
+
|
|
9
|
+
[rules]
|
|
10
|
+
SWSAFE001 = "error"
|
|
11
|
+
SWSAFE002 = "error"
|
|
12
|
+
SWCON001 = "error"
|
|
13
|
+
SWCON002 = "error"
|
|
14
|
+
SWCON003 = "error"
|
|
15
|
+
SWMAINT001 = "error"
|
|
16
|
+
SWMAINT002 = "error"
|
|
17
|
+
SWSAFE003 = "error"
|
|
18
|
+
SWAPI001 = "warn"
|
|
19
|
+
SWAPI002 = "warn"
|
|
20
|
+
KTSAFE001 = "error"
|
|
21
|
+
KTCON001 = "error"
|
|
22
|
+
KTCON002 = "error"
|
|
23
|
+
KTMAINT001 = "error"
|
|
24
|
+
KTMAINT002 = "error"
|
|
25
|
+
KTAPI001 = "warn"
|
|
26
|
+
KTAPI002 = "warn"
|
|
27
|
+
AKS001 = "warn"
|
|
28
|
+
AKS002 = "warn"
|
|
29
|
+
AKS003 = "warn"
|
|
30
|
+
|
|
31
|
+
# Allowances use [[allow]] entries with rule, path, and a nonempty reason.
|
|
32
|
+
# Add only reviewed exceptions. No source comment can suppress a finding.
|
package/lib/artifacts.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ export type ArtifactRow = {
|
|
|
10
10
|
* the check existed — "never looked at", not "looked at and
|
|
11
11
|
* fine". `false` means it stored and will symbolicate nothing. */
|
|
12
12
|
usable?: boolean | null;
|
|
13
|
+
storageVerification?: {
|
|
14
|
+
integrity: string;
|
|
15
|
+
actualContentHash?: string;
|
|
16
|
+
actualSizeBytes?: number;
|
|
17
|
+
usable?: boolean | null;
|
|
18
|
+
};
|
|
13
19
|
};
|
|
14
20
|
export type ArtifactsResponse = {
|
|
15
21
|
/** Additive capability advertisement. Absent on older servers. */
|
|
@@ -32,5 +38,6 @@ export declare function fetchArtifacts(opts: {
|
|
|
32
38
|
apiUrl: string;
|
|
33
39
|
release: string;
|
|
34
40
|
token: string;
|
|
41
|
+
verify?: boolean;
|
|
35
42
|
}): Promise<ArtifactsResponse>;
|
|
36
43
|
//# sourceMappingURL=artifacts.d.ts.map
|
package/lib/artifacts.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"artifacts.d.ts","sourceRoot":"","sources":["../src/artifacts.ts"],"names":[],"mappings":"AAqBA,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,kEAAkE;IAClE,OAAO,EAAE,IAAI,GAAG,MAAM,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB;;uEAEmE;IACnE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"artifacts.d.ts","sourceRoot":"","sources":["../src/artifacts.ts"],"names":[],"mappings":"AAqBA,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,kEAAkE;IAClE,OAAO,EAAE,IAAI,GAAG,MAAM,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB;;uEAEmE;IACnE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,mBAAmB,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;KAAE,CAAA;CAC3H,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,kEAAkE;IAClE,aAAa,CAAC,EAAE;QACd,aAAa,EAAE,MAAM,CAAA;QACrB,gBAAgB,EAAE,MAAM,CAAA;QACxB,mBAAmB,EAAE,MAAM,EAAE,CAAA;QAC7B,YAAY,EAAE,MAAM,EAAE,CAAA;KACvB,CAAA;IACD,OAAO,EAAE,MAAM,CAAA;IACf;;sEAEkE;IAClE,KAAK,EAAE,OAAO,CAAA;IACd,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7B,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,SAAS,EAAE,WAAW,EAAE,CAAA;CACzB,CAAA;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE;IACzC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAkB7B"}
|
package/lib/artifacts.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// readable. It does not re-group them.
|
|
21
21
|
export async function fetchArtifacts(opts) {
|
|
22
22
|
const url = `${opts.apiUrl.replace(/\/+$/, '')}/v1/releases/` +
|
|
23
|
-
`${encodeURIComponent(opts.release)}/artifacts`;
|
|
23
|
+
`${encodeURIComponent(opts.release)}/artifacts${opts.verify ? "?verify=true" : ""}`;
|
|
24
24
|
const resp = await fetch(url, {
|
|
25
25
|
headers: { Authorization: `Bearer ${opts.token}` },
|
|
26
26
|
});
|
package/lib/artifacts.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"artifacts.js","sourceRoot":"","sources":["../src/artifacts.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,EAAE;AACF,qEAAqE;AACrE,iEAAiE;AACjE,iEAAiE;AACjE,+DAA+D;AAC/D,sEAAsE;AACtE,oEAAoE;AACpE,kCAAkC;AAClC,EAAE;AACF,iEAAiE;AACjE,kEAAkE;AAClE,mEAAmE;AACnE,mEAAmE;AACnE,6CAA6C;AAC7C,EAAE;AACF,gEAAgE;AAChE,+DAA+D;AAC/D,4DAA4D;AAC5D,uCAAuC;
|
|
1
|
+
{"version":3,"file":"artifacts.js","sourceRoot":"","sources":["../src/artifacts.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,EAAE;AACF,qEAAqE;AACrE,iEAAiE;AACjE,iEAAiE;AACjE,+DAA+D;AAC/D,sEAAsE;AACtE,oEAAoE;AACpE,kCAAkC;AAClC,EAAE;AACF,iEAAiE;AACjE,kEAAkE;AAClE,mEAAmE;AACnE,mEAAmE;AACnE,6CAA6C;AAC7C,EAAE;AACF,gEAAgE;AAChE,+DAA+D;AAC/D,4DAA4D;AAC5D,uCAAuC;AAmCvC,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAKpC;IACC,MAAM,GAAG,GACP,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,eAAe;QACjD,GAAG,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,EAAE,CAAA;IACrF,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAC5B,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,IAAI,CAAC,KAAK,EAAE,EAAE;KACnD,CAAC,CAAA;IACF,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QACb,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAA;QAChD,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CACjF,CAAA;IACH,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,EAAuB,CAAA;IACrD,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,OAAO,KAAK,IAAI,CAAC,OAAO,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;QACxH,MAAM,IAAI,KAAK,CAAC,0GAA0G,CAAC,CAAA;IAC7H,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC"}
|
package/lib/compatibility.js
CHANGED
|
@@ -7,7 +7,7 @@ export function checkCompatibility(result, sourceZip) {
|
|
|
7
7
|
if (info === undefined) {
|
|
8
8
|
if (sourceZip)
|
|
9
9
|
throw new Error('This server does not advertise source ZIP support. No files were uploaded. Upgrade the Sentori server to a build advertising artifact protocol 1 and sourceBundleFormats: ["zip"], then run sentori doctor. Existing CLI 1.5 pipelines may continue using legacy JSON sources.');
|
|
10
|
-
return 'Legacy artifact API: server capabilities are not advertised. Source maps, dSYM and mapping uploads remain available; verification
|
|
10
|
+
return 'Legacy artifact API: server capabilities are not advertised. Source maps, dSYM and mapping uploads remain available; verification requires a server that supplies fresh storageVerification proof; inventory metadata alone cannot pass.';
|
|
11
11
|
}
|
|
12
12
|
if (!info || typeof info.serverVersion !== 'string' || info.artifactProtocol !== 1
|
|
13
13
|
|| !Array.isArray(info.sourceBundleFormats) || !Array.isArray(info.verification)
|
package/lib/compatibility.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compatibility.js","sourceRoot":"","sources":["../src/compatibility.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAGtC,mFAAmF;AACnF,MAAM,CAAC,MAAM,WAAW,GAAW,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,iBAAiB,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAA;AAExH,mFAAmF;AACnF,MAAM,UAAU,kBAAkB,CAAC,MAAyB,EAAE,SAAkB;IAC9E,MAAM,IAAI,GAAG,MAAM,CAAC,aAAa,CAAA;IACjC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,IAAI,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,gRAAgR,CAAC,CAAA;QAChT,OAAO,
|
|
1
|
+
{"version":3,"file":"compatibility.js","sourceRoot":"","sources":["../src/compatibility.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAGtC,mFAAmF;AACnF,MAAM,CAAC,MAAM,WAAW,GAAW,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,iBAAiB,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAA;AAExH,mFAAmF;AACnF,MAAM,UAAU,kBAAkB,CAAC,MAAyB,EAAE,SAAkB;IAC9E,MAAM,IAAI,GAAG,MAAM,CAAC,aAAa,CAAA;IACjC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,IAAI,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,gRAAgR,CAAC,CAAA;QAChT,OAAO,0OAA0O,CAAA;IACnP,CAAC;IACD,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,CAAC,aAAa,KAAK,QAAQ,IAAI,IAAI,CAAC,gBAAgB,KAAK,CAAC;WAC7E,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC;WAC7E,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpF,MAAM,IAAI,KAAK,CAAC,0LAA0L,CAAC,CAAA;IAC7M,CAAC;IACD,IAAI,SAAS,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,IAAI,CAAC,aAAa,qGAAqG,CAAC,CAAA;IACtN,OAAO,eAAe,WAAW,YAAY,IAAI,CAAC,aAAa,qCAAqC,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,aAAa,GAAG,CAAA;AAC/K,CAAC"}
|
package/lib/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { parseArgs } from 'node:util';
|
|
|
4
4
|
import { runWorkflow, runSourcesPack, WORKFLOW_HELP } from './workflow.js';
|
|
5
5
|
import { checkCompatibility, CLI_VERSION } from './compatibility.js';
|
|
6
6
|
import { listApps, showProject } from './apps.js';
|
|
7
|
-
import { fetchBundle, formatIssueLine,
|
|
7
|
+
import { fetchBundle, formatIssueLine, listIssuesPage, noteIssue, resolveIssue, } from './issue.js';
|
|
8
8
|
import { isStrict, lenientFail, stripStrict } from './lenient.js';
|
|
9
9
|
import { runMcpServer } from './mcp.js';
|
|
10
10
|
import { uploadDsym, uploadMapping } from './native-artifacts.js';
|
|
@@ -34,6 +34,9 @@ Legacy kind-presence gate (use verify for exact planned files):
|
|
|
34
34
|
catches the case a local "we ran the upload" ledger cannot:
|
|
35
35
|
the upload step that stopped being called at all.
|
|
36
36
|
|
|
37
|
+
Local Swift and Kotlin quality gate (fails on regressions or tool errors):
|
|
38
|
+
sentori lint [paths...] --help
|
|
39
|
+
|
|
37
40
|
Regression tripwires (design: probes):
|
|
38
41
|
sentori probes sync --release <r> --token <t> [--dir .]
|
|
39
42
|
Statically scans source for sentori.probe('REF') call sites and
|
|
@@ -43,7 +46,7 @@ CI triage (the same /api surface an AI agent uses):
|
|
|
43
46
|
sentori project show [--json] project selected by the API token
|
|
44
47
|
sentori app list [--json] apps within that project
|
|
45
48
|
sentori issue list [--status open] [--kind error] [--app <uuid>]
|
|
46
|
-
[--platform <platform>] [--environment <env>] [--release <release>] [--json]
|
|
49
|
+
[--platform <platform>] [--environment <env>] [--release <release>] [--limit 50] [--offset 0] [--json]
|
|
47
50
|
sentori issue resolve <issue-id> [--in-release <r>]
|
|
48
51
|
sentori issue note <issue-id> --body "fixed in abc123"
|
|
49
52
|
sentori issue bundle <issue-id>
|
|
@@ -454,7 +457,7 @@ async function cmdIssueList(argv) {
|
|
|
454
457
|
try {
|
|
455
458
|
parsed = parseArgs({
|
|
456
459
|
args: argv,
|
|
457
|
-
options: { ...ISSUE_OPTS, kind: { type: 'string' }, status: { type: 'string' }, app: { type: 'string' }, platform: { type: 'string' }, environment: { type: 'string' }, release: { type: 'string' }, json: { type: 'boolean' } },
|
|
460
|
+
options: { ...ISSUE_OPTS, kind: { type: 'string' }, status: { type: 'string' }, app: { type: 'string' }, platform: { type: 'string' }, environment: { type: 'string' }, release: { type: 'string' }, limit: { type: 'string' }, offset: { type: 'string' }, json: { type: 'boolean' } },
|
|
458
461
|
});
|
|
459
462
|
}
|
|
460
463
|
catch (e) {
|
|
@@ -469,18 +472,21 @@ async function cmdIssueList(argv) {
|
|
|
469
472
|
if (!cfg)
|
|
470
473
|
return 2;
|
|
471
474
|
try {
|
|
472
|
-
const
|
|
475
|
+
const page = await listIssuesPage(cfg, {
|
|
473
476
|
kind: parsed.values.kind,
|
|
474
477
|
status: parsed.values.status ?? 'open',
|
|
475
478
|
app: parsed.values.app,
|
|
476
479
|
platform: parsed.values.platform,
|
|
477
480
|
environment: parsed.values.environment,
|
|
478
481
|
release: parsed.values.release,
|
|
482
|
+
limit: parsed.values.limit === undefined ? undefined : Number(parsed.values.limit),
|
|
483
|
+
offset: parsed.values.offset === undefined ? undefined : Number(parsed.values.offset),
|
|
479
484
|
});
|
|
480
485
|
if (parsed.values.json) {
|
|
481
|
-
console.log(JSON.stringify(
|
|
486
|
+
console.log(JSON.stringify(page, null, 2));
|
|
482
487
|
return 0;
|
|
483
488
|
}
|
|
489
|
+
const rows = page.issues;
|
|
484
490
|
if (rows.length === 0) {
|
|
485
491
|
console.log('(no matching issues)');
|
|
486
492
|
return 0;
|
|
@@ -710,6 +716,8 @@ async function main(argv) {
|
|
|
710
716
|
console.log(HELP);
|
|
711
717
|
return 0;
|
|
712
718
|
}
|
|
719
|
+
if (argv[0] === 'lint')
|
|
720
|
+
return (await import('./lint/index.js')).cmdLint(argv.slice(1));
|
|
713
721
|
const [a, b, ...rest] = argv;
|
|
714
722
|
if (a === 'web')
|
|
715
723
|
return runWeb(argv.slice(1));
|