sickbay 1.3.0 → 1.3.1

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.
Files changed (43) hide show
  1. package/README.md +244 -0
  2. package/dist/DiffApp-E2AQ3IOB.js +172 -0
  3. package/dist/DoctorApp-ZFBKXFR4.js +440 -0
  4. package/dist/FixApp-AEO2W2EP.js +352 -0
  5. package/dist/StatsApp-IHRVYLPF.js +356 -0
  6. package/dist/TrendApp-JJ76OGDZ.js +106 -0
  7. package/dist/TuiApp-YXSV6ZYY.js +946 -0
  8. package/dist/ai-7DGOLNJX.js +64 -0
  9. package/dist/badge-KQ73KEIN.js +41 -0
  10. package/dist/chunk-3OR2GFVE.js +90 -0
  11. package/dist/chunk-7CHSSJZH.js +60 -0
  12. package/dist/chunk-MBVA75EM.js +19 -0
  13. package/dist/chunk-POUHUMJN.js +21 -0
  14. package/dist/chunk-SHO3ZXTH.js +23 -0
  15. package/dist/chunk-WNCDYCPO.js +3976 -0
  16. package/dist/chunk-WS67R6X3.js +19 -0
  17. package/dist/dist-YIK3YE5B.js +36 -0
  18. package/dist/history-XLNVZEDI.js +14 -0
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.js +537 -0
  21. package/dist/init-VMFU77S5.js +51 -0
  22. package/dist/resolve-package-57YPNPWD.js +9 -0
  23. package/dist/web/assets/ChatDrawer-Dqewz3Yz.js +2 -0
  24. package/dist/web/assets/ChatDrawer-Dqewz3Yz.js.map +1 -0
  25. package/dist/web/assets/DependencyGraph-DS5sXNvY.js +2 -0
  26. package/dist/web/assets/DependencyGraph-DS5sXNvY.js.map +1 -0
  27. package/dist/web/assets/MonorepoOverview-vTq9TL11.js +2 -0
  28. package/dist/web/assets/MonorepoOverview-vTq9TL11.js.map +1 -0
  29. package/dist/web/assets/graph-viz-Ls9Hbzox.js +2 -0
  30. package/dist/web/assets/graph-viz-Ls9Hbzox.js.map +1 -0
  31. package/dist/web/assets/index-BM8n1Sbm.js +15 -0
  32. package/dist/web/assets/index-BM8n1Sbm.js.map +1 -0
  33. package/dist/web/assets/index-_UXbDrsj.css +2 -0
  34. package/dist/web/assets/markdown-BkROfETq.js +23 -0
  35. package/dist/web/assets/markdown-BkROfETq.js.map +1 -0
  36. package/dist/web/assets/react-BEi4PGB1.js +16 -0
  37. package/dist/web/assets/react-BEi4PGB1.js.map +1 -0
  38. package/dist/web/assets/react-CHpVij2M.css +1 -0
  39. package/dist/web/assets/rolldown-runtime-B1FJdls4.js +1 -0
  40. package/dist/web/index.html +22 -0
  41. package/dist/web-XBUBGSTP.js +196 -0
  42. package/package.json +45 -16
  43. package/bin.mjs +0 -2
package/README.md ADDED
@@ -0,0 +1,244 @@
1
+ # @nebulord/sickbay
2
+
3
+ The terminal interface for Sickbay. Built with [Ink](https://github.com/vadimdemedes/ink) (React for terminals) and [Commander](https://github.com/tj/commander.js).
4
+
5
+ ## Quickstart
6
+
7
+ ```bash
8
+ # Terminal
9
+ npx sickbay
10
+
11
+ # Web
12
+ npx sickbay --web
13
+
14
+ # TUI
15
+ npx sickbay --tui
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```bash
21
+ sickbay [options]
22
+ ```
23
+
24
+ ### Commands
25
+
26
+ | Command | Description |
27
+ | ------------------ | ------------------------------------------------------------------ |
28
+ | `init [options]` | Scaffold `.sickbay/`, run baseline scan, seed history |
29
+ | `fix [options]` | Interactively fix issues found by sickbay scan |
30
+ | `trend [options]` | Show score history and trends over time |
31
+ | `stats [options]` | Show a quick codebase overview and project summary |
32
+ | `doctor [options]` | Diagnose project setup and configuration issues |
33
+ | `tui [options]` | Persistent live dashboard with file watching and activity tracking |
34
+
35
+ ### Flags
36
+
37
+ | Flag | Default | Description |
38
+ | ---------------------- | --------------- | --------------------------------- |
39
+ | `-p, --path <path>` | `process.cwd()` | Path to the project to analyze |
40
+ | `-c, --checks <names>` | all | Comma-separated check IDs to run |
41
+ | `--json` | false | Output raw JSON to stdout (no UI) |
42
+ | `--web` | false | Open web dashboard after scan |
43
+ | `--verbose` | false | Show tool output during checks |
44
+ | `-V, --version` | | Print version |
45
+ | `-h, --help` | | Show help |
46
+
47
+ ### Examples
48
+
49
+ ```bash
50
+ # Analyze current directory
51
+ sickbay
52
+
53
+ # Analyze another project
54
+ sickbay -p ~/projects/my-app
55
+
56
+ # Run specific checks only
57
+ sickbay --checks knip,npm-audit,depcheck
58
+
59
+ # JSON output for CI
60
+ sickbay --json | jq '.overallScore'
61
+
62
+ # Get just the summary
63
+ sickbay --json | jq '.summary'
64
+
65
+ # List all check names and their scores
66
+ sickbay --json | jq '.checks[] | {name, score}'
67
+
68
+ # Get only failing checks
69
+ sickbay --json | jq '.checks[] | select(.status == "fail")'
70
+
71
+ # Open web dashboard
72
+ sickbay --web
73
+
74
+ # Initialize .sickbay/ folder with baseline scan
75
+ sickbay init
76
+
77
+ # Initialize for a specific project
78
+ sickbay init --path ~/projects/my-app
79
+
80
+ # Interactively fix issues
81
+ sickbay fix
82
+
83
+ # View score history and trends
84
+ sickbay trend
85
+
86
+ # Get quick project stats
87
+ sickbay stats
88
+
89
+ # Diagnose project setup
90
+ sickbay doctor
91
+
92
+ # Launch tui dashboard (current directory, file watching enabled)
93
+ sickbay tui
94
+
95
+ # TUI for a specific project, disable file watching
96
+ sickbay tui --path ~/projects/my-app --no-watch
97
+
98
+ # TUI with faster auto-refresh (60 seconds) and specific checks only
99
+ sickbay tui --path ~/projects/my-app --refresh 60 --checks knip,npm-audit,eslint
100
+ ```
101
+
102
+ ## `sickbay init` vs `sickbay`
103
+
104
+ **Run `sickbay init` once when setting up a project for the first time.**
105
+
106
+ It scaffolds the `.sickbay/` data folder, saves a `baseline.json` snapshot of the project's current health, and wires up `.gitignore` entries so `history.json` doesn't pollute your repo. Think of it as "onboarding" Sickbay to a project.
107
+
108
+ **Run `sickbay` for every subsequent scan.**
109
+
110
+ Each scan automatically appends an entry to `.sickbay/history.json`, so your score trend builds up over time without any extra steps. The History tab in the web dashboard (`sickbay --web`) reads from this file.
111
+
112
+ | | First time | Ongoing |
113
+ | ------------------------- | -------------- | -------------- |
114
+ | Command | `sickbay init` | `sickbay` |
115
+ | Creates `.sickbay/` | ✓ | ✓ (if missing) |
116
+ | Saves `baseline.json` | ✓ | ✗ |
117
+ | Updates root `.gitignore` | ✓ | ✗ |
118
+ | Appends to `history.json` | ✓ | ✓ |
119
+
120
+ > If you skip `sickbay init` and go straight to `sickbay`, history will still accumulate — you just won't have a baseline snapshot or gitignore entries for `.sickbay/`. But you can always ignore it manually.
121
+
122
+ ## TUI Dashboard
123
+
124
+ `sickbay tui` opens a persistent split-pane TUI that continuously monitors your project. Unlike a one-shot scan, it stays running, watches for file changes, and lets you interact with results in real time.
125
+
126
+ ### TUI Flags
127
+
128
+ | Flag | Default | Description |
129
+ | ----------------------- | --------------- | ------------------------------------- |
130
+ | `-p, --path <path>` | `process.cwd()` | Project path to monitor |
131
+ | `--no-watch` | watch enabled | Disable file-watching auto-refresh |
132
+ | `--refresh <seconds>` | `300` | Auto-refresh interval in seconds |
133
+ | `-c, --checks <checks>` | all | Comma-separated list of checks to run |
134
+
135
+ ### Panels
136
+
137
+ The tui displays six panels arranged in a responsive grid:
138
+
139
+ | Panel | Key | Content |
140
+ | -------------- | --- | -------------------------------------------------------------------------- |
141
+ | **Health** | `h` | All check results with status icons, names, and score bars |
142
+ | **Score** | — | Overall score (0–100), color-coded status, issue counts, score delta |
143
+ | **Trend** | `t` | Sparkline charts for overall score and each category (last 10 scans) |
144
+ | **Git** | `g` | Branch, commits ahead/behind, modified/staged/untracked files, last commit |
145
+ | **Quick Wins** | `q` | Top 5 actionable fixes prioritized by severity |
146
+ | **Activity** | `a` | Time-stamped event log (scans, file changes, regressions, git changes) |
147
+
148
+ ### Keyboard Controls
149
+
150
+ | Key | Action |
151
+ | -------- | ------------------------------------------ |
152
+ | `r` | Manually trigger a rescan |
153
+ | `w` | Launch web dashboard (without AI) |
154
+ | `W` | Launch web dashboard with AI analysis |
155
+ | `f` | Expand focused panel to fullscreen |
156
+ | `Escape` | Unfocus current panel |
157
+ | `↑ / ↓` | Scroll Health Panel results (when focused) |
158
+ | `h` | Focus Health panel |
159
+ | `g` | Focus Git panel |
160
+ | `t` | Focus Trend panel |
161
+ | `q` | Focus Quick Wins panel |
162
+ | `a` | Focus Activity panel |
163
+
164
+ ### Automatic Triggers
165
+
166
+ - **Startup** — Initial scan runs immediately
167
+ - **File watch** — Rescans when TypeScript, JavaScript, or JSON files change (debounced 2s)
168
+ - **Auto-refresh** — Periodic rescan at the configured interval (default 5 minutes)
169
+ - **Regression detection** — Activity panel flags category score decreases automatically
170
+
171
+ ## Architecture
172
+
173
+ ```
174
+ src/
175
+ ├── index.ts # Commander entry — parses flags, renders Ink <App>
176
+ ├── commands/
177
+ │ └── web.ts # HTTP server (Node built-in) for the dashboard
178
+ └── components/
179
+ ├── App.tsx # Root Ink component — manages phases & state
180
+ ├── Header.tsx # ASCII art banner + project name
181
+ ├── ProgressList.tsx # Animated check progress (pending → running → done)
182
+ ├── CheckResult.tsx # Single check: name, status, score bar, issues
183
+ ├── ScoreBar.tsx # Colored horizontal bar (green/yellow/red)
184
+ ├── Summary.tsx # Overall score + issue counts
185
+ ├── QuickWins.tsx # Top actionable fix suggestions
186
+ └── tui/
187
+ ├── TUIApp.tsx # TUI root — layout, keyboard input, state
188
+ ├── HealthPanel.tsx # Check results with status icons and score bars
189
+ ├── ScorePanel.tsx # Overall score, issue counts, delta from last scan
190
+ ├── TrendPanel.tsx # Sparkline charts for score history (last 10 scans)
191
+ ├── GitPanel.tsx # Branch, ahead/behind, staged/modified file counts
192
+ ├── QuickWinsPanel.tsx # Top 5 actionable fixes by severity
193
+ ├── ActivityPanel.tsx # Timestamped event log
194
+ ├── HotkeyBar.tsx # Fixed footer with keyboard shortcut reference
195
+ ├── PanelBorder.tsx # Focused/unfocused border styling
196
+ └── hooks/
197
+ ├── useSickbayRunner.ts # Manages check execution and scan state
198
+ ├── useFileWatcher.ts # chokidar file watcher with debounce
199
+ ├── useGitStatus.ts # Polls git status every 10 seconds
200
+ └── useTerminalSize.ts # Tracks terminal dimensions for responsive layout
201
+ ```
202
+
203
+ ### UI Phases
204
+
205
+ The `<App>` component cycles through phases:
206
+
207
+ 1. **`loading`** — Shows progress list with animated spinners while checks run
208
+ 2. **`results`** — Displays all check results + summary + quick wins
209
+ 3. **`opening-web`** — Starts HTTP server, opens browser, stays alive until Ctrl+C
210
+ 4. **`error`** — Shows error message and exits
211
+
212
+ ### `--web` flag flow
213
+
214
+ When `--web` is passed:
215
+
216
+ 1. Scan completes normally
217
+ 2. `serveWeb(report)` starts an HTTP server on port 3030 (or next free port)
218
+ 3. Server serves `packages/web/dist/` as static files
219
+ 4. Server responds to `GET /sickbay-report.json` with the in-memory report
220
+ 5. `open` package opens the browser
221
+ 6. Process stays alive until Ctrl+C
222
+
223
+ ### `--json` flag flow
224
+
225
+ Skips the Ink UI entirely, writes `JSON.stringify(report, null, 2)` to stdout, then exits.
226
+
227
+ ## Local Development
228
+
229
+ ```bash
230
+ # Watch mode — rebuilds on file changes
231
+ pnpm dev
232
+
233
+ # Test against a project
234
+ node dist/index.js --path ~/Desktop/sickbay-test-app
235
+ node dist/index.js --path ~/Desktop/sickbay-test-app --web
236
+ node dist/index.js --path ~/Desktop/sickbay-test-app --json
237
+ ```
238
+
239
+ ## Build
240
+
241
+ ```bash
242
+ pnpm build # tsup → dist/index.js + dist/web-*.js (code-split)
243
+ pnpm clean # rm -rf dist/
244
+ ```
@@ -0,0 +1,172 @@
1
+ import {
2
+ Header
3
+ } from "./chunk-WS67R6X3.js";
4
+
5
+ // src/components/DiffApp.tsx
6
+ import React, { useState, useEffect } from "react";
7
+ import { Box, Text, useApp } from "ink";
8
+ import Spinner from "ink-spinner";
9
+
10
+ // src/commands/diff.ts
11
+ import { execFileSync } from "child_process";
12
+ function loadBaseReport(projectPath, branch) {
13
+ try {
14
+ const output = execFileSync("git", ["show", `${branch}:.sickbay/last-report.json`], {
15
+ cwd: projectPath,
16
+ encoding: "utf-8",
17
+ stdio: ["pipe", "pipe", "pipe"]
18
+ });
19
+ return JSON.parse(output);
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+ var STATUS_ORDER = {
25
+ regressed: 0,
26
+ improved: 1,
27
+ new: 2,
28
+ removed: 3,
29
+ unchanged: 4
30
+ };
31
+ function compareReports(current, base, branch) {
32
+ const baseMap = new Map(base.checks.map((c) => [c.id, c]));
33
+ const currentMap = new Map(current.checks.map((c) => [c.id, c]));
34
+ const checks = [];
35
+ for (const check of current.checks) {
36
+ const baseCheck = baseMap.get(check.id);
37
+ if (!baseCheck) {
38
+ checks.push({
39
+ id: check.id,
40
+ name: check.name,
41
+ category: check.category,
42
+ currentScore: check.score,
43
+ baseScore: 0,
44
+ delta: check.score,
45
+ status: "new"
46
+ });
47
+ } else {
48
+ const delta = check.score - baseCheck.score;
49
+ checks.push({
50
+ id: check.id,
51
+ name: check.name,
52
+ category: check.category,
53
+ currentScore: check.score,
54
+ baseScore: baseCheck.score,
55
+ delta,
56
+ status: delta > 0 ? "improved" : delta < 0 ? "regressed" : "unchanged"
57
+ });
58
+ }
59
+ }
60
+ for (const check of base.checks) {
61
+ if (!currentMap.has(check.id)) {
62
+ checks.push({
63
+ id: check.id,
64
+ name: check.name,
65
+ category: check.category,
66
+ currentScore: 0,
67
+ baseScore: check.score,
68
+ delta: -check.score,
69
+ status: "removed"
70
+ });
71
+ }
72
+ }
73
+ checks.sort((a, b) => STATUS_ORDER[a.status] - STATUS_ORDER[b.status]);
74
+ const summary = {
75
+ improved: checks.filter((c) => c.status === "improved").length,
76
+ regressed: checks.filter((c) => c.status === "regressed").length,
77
+ unchanged: checks.filter((c) => c.status === "unchanged").length,
78
+ newChecks: checks.filter((c) => c.status === "new").length,
79
+ removedChecks: checks.filter((c) => c.status === "removed").length
80
+ };
81
+ return {
82
+ branch,
83
+ currentScore: current.overallScore,
84
+ baseScore: base.overallScore,
85
+ scoreDelta: current.overallScore - base.overallScore,
86
+ checks,
87
+ summary
88
+ };
89
+ }
90
+
91
+ // src/components/DiffApp.tsx
92
+ var STATUS_ICONS = {
93
+ improved: "\u2191",
94
+ regressed: "\u2193",
95
+ unchanged: "=",
96
+ new: "+",
97
+ removed: "\u2212"
98
+ };
99
+ var STATUS_COLORS = {
100
+ improved: "green",
101
+ regressed: "red",
102
+ unchanged: "gray",
103
+ new: "cyan",
104
+ removed: "yellow"
105
+ };
106
+ function formatDelta(delta) {
107
+ if (delta > 0) return `+${delta}`;
108
+ if (delta < 0) return `${delta}`;
109
+ return "0";
110
+ }
111
+ function DiffTable({ diff }) {
112
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true }, "Overall: "), /* @__PURE__ */ React.createElement(Text, { bold: true }, diff.baseScore), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React.createElement(Text, { bold: true }, diff.currentScore), /* @__PURE__ */ React.createElement(Text, null, " "), /* @__PURE__ */ React.createElement(Text, { color: diff.scoreDelta > 0 ? "green" : diff.scoreDelta < 0 ? "red" : "gray", bold: true }, "(", formatDelta(diff.scoreDelta), ")")), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2 }, /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { bold: true }, " Check".padEnd(30)), /* @__PURE__ */ React.createElement(Text, { bold: true }, "Current".padEnd(10)), /* @__PURE__ */ React.createElement(Text, { bold: true }, "Base".padEnd(10)), /* @__PURE__ */ React.createElement(Text, { bold: true }, "Delta")), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u2501".repeat(56)), diff.checks.map((check) => /* @__PURE__ */ React.createElement(Box, { key: check.id }, /* @__PURE__ */ React.createElement(Text, { color: STATUS_COLORS[check.status] }, STATUS_ICONS[check.status], " "), /* @__PURE__ */ React.createElement(Text, null, check.name.padEnd(28)), /* @__PURE__ */ React.createElement(Text, null, String(check.currentScore || "\u2014").padEnd(10)), /* @__PURE__ */ React.createElement(Text, null, String(check.baseScore || "\u2014").padEnd(10)), /* @__PURE__ */ React.createElement(Text, { color: STATUS_COLORS[check.status], bold: true }, check.status === "new" ? "new" : check.status === "removed" ? "removed" : formatDelta(check.delta))))), /* @__PURE__ */ React.createElement(Box, { marginTop: 1, marginLeft: 2 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, diff.summary.improved > 0 && /* @__PURE__ */ React.createElement(Text, { color: "green" }, diff.summary.improved, " improved"), diff.summary.improved > 0 && diff.summary.regressed > 0 && /* @__PURE__ */ React.createElement(Text, null, ", "), diff.summary.regressed > 0 && /* @__PURE__ */ React.createElement(Text, { color: "red" }, diff.summary.regressed, " regressed"), (diff.summary.improved > 0 || diff.summary.regressed > 0) && diff.summary.unchanged > 0 && /* @__PURE__ */ React.createElement(Text, null, ", "), diff.summary.unchanged > 0 && /* @__PURE__ */ React.createElement(Text, null, diff.summary.unchanged, " unchanged"), diff.summary.newChecks > 0 && /* @__PURE__ */ React.createElement(Text, { color: "cyan" }, ", ", diff.summary.newChecks, " new"), diff.summary.removedChecks > 0 && /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, ", ", diff.summary.removedChecks, " removed"))));
113
+ }
114
+ function DiffApp({ projectPath, branch, jsonOutput, checks, verbose }) {
115
+ const { exit } = useApp();
116
+ const [phase, setPhase] = useState("scanning");
117
+ const [diff, setDiff] = useState(null);
118
+ const [error, setError] = useState(null);
119
+ useEffect(() => {
120
+ (async () => {
121
+ try {
122
+ const { runSickbay } = await import("./dist-YIK3YE5B.js");
123
+ const currentReport = await runSickbay({
124
+ projectPath,
125
+ checks,
126
+ verbose
127
+ });
128
+ try {
129
+ const { saveEntry, saveLastReport } = await import("./history-XLNVZEDI.js");
130
+ saveEntry(currentReport);
131
+ saveLastReport(currentReport);
132
+ } catch {
133
+ }
134
+ setPhase("loading-base");
135
+ const baseReport = loadBaseReport(projectPath, branch);
136
+ if (!baseReport) {
137
+ setError(
138
+ `No saved report found on "${branch}". Run \`sickbay\` on that branch and commit .sickbay/last-report.json so it can be read via git.`
139
+ );
140
+ setPhase("error");
141
+ setTimeout(() => exit(), 100);
142
+ return;
143
+ }
144
+ const result = compareReports(currentReport, baseReport, branch);
145
+ setDiff(result);
146
+ setPhase("results");
147
+ if (jsonOutput) {
148
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
149
+ }
150
+ setTimeout(() => exit(), 100);
151
+ } catch (err) {
152
+ setError(err instanceof Error ? err.message : String(err));
153
+ setPhase("error");
154
+ setTimeout(() => exit(), 100);
155
+ }
156
+ })();
157
+ }, []);
158
+ if (phase === "scanning") {
159
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Header, null), /* @__PURE__ */ React.createElement(Text, null, /* @__PURE__ */ React.createElement(Text, { color: "green" }, /* @__PURE__ */ React.createElement(Spinner, { type: "dots" })), " ", "Scanning current branch..."));
160
+ }
161
+ if (phase === "loading-base") {
162
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Header, null), /* @__PURE__ */ React.createElement(Text, null, /* @__PURE__ */ React.createElement(Text, { color: "green" }, /* @__PURE__ */ React.createElement(Spinner, { type: "dots" })), " ", "Loading ", branch, " baseline..."));
163
+ }
164
+ if (phase === "error") {
165
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Header, null), /* @__PURE__ */ React.createElement(Text, { color: "red" }, "\u2717 ", error));
166
+ }
167
+ if (jsonOutput || !diff) return null;
168
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Header, null), /* @__PURE__ */ React.createElement(Text, { bold: true }, "Branch Diff: current vs ", branch), /* @__PURE__ */ React.createElement(DiffTable, { diff }));
169
+ }
170
+ export {
171
+ DiffApp
172
+ };