@happy-nut/monacori 0.1.9 → 0.1.10
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/README.md +47 -0
- package/dist/app-main.js +17 -4
- package/dist/commands.js +4 -0
- package/dist/util.d.ts +3 -0
- package/dist/util.js +18 -0
- package/dist/viewer.client.js +27 -3
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -71,6 +71,53 @@ mo --base main # compare against a specific base
|
|
|
71
71
|
mo --context 20 # show more context around each hunk
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
+
## Development
|
|
75
|
+
|
|
76
|
+
Working on monacori itself? The globally-installed `mo` runs the **published** package, not your
|
|
77
|
+
checkout — local edits won't appear until you build and run locally.
|
|
78
|
+
|
|
79
|
+
Run your checkout directly (builds, then launches in the foreground with DevTools open):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm run dev
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This reviews the monacori repo itself. To point a local build at another project:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
MONACORI_DEV=1 node /path/to/monacori/bin/monacori.js --foreground
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Which build is running?** A dev build titles its window `monacori (dev)` and opens DevTools, and
|
|
92
|
+
every launch prints its app path — so a local checkout is distinguishable from the installed package
|
|
93
|
+
even when their version numbers match:
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
monacori: launching /…/repos/monacori/dist/app-main.js # local checkout
|
|
97
|
+
monacori: launching /…/lib/node_modules/@happy-nut/monacori/dist/app-main.js # installed package
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Prefer the `mo` command pointed at your checkout? `npm link` once, then rebuild after each change:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npm link # global `mo` now runs this checkout
|
|
104
|
+
npm run build # rebuild dist/ after editing src/
|
|
105
|
+
npm unlink -g @happy-nut/monacori # restore the published `mo`
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`src/viewer.client.js` and `src/viewer.css` are copied (not compiled) into `dist/` by the build, so
|
|
109
|
+
re-run `npm run build` (or `npm run dev`) after editing them.
|
|
110
|
+
|
|
111
|
+
### Tests
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npm test
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`npm test` builds, then runs the jsdom regression suite (`test/*.test.mjs`) against the built `dist/`.
|
|
118
|
+
It guards the core user flows end to end — see [test/USER_FLOWS.md](test/USER_FLOWS.md) — and the same
|
|
119
|
+
suite gates every release.
|
|
120
|
+
|
|
74
121
|
## Local State
|
|
75
122
|
|
|
76
123
|
`monacori init` creates a git-ignored `.monacori/` directory for generated diff reviews, local config, comments, logs, and validation notes. Keep it ignored unless your team intentionally wants to version review artifacts.
|
package/dist/app-main.js
CHANGED
|
@@ -4,7 +4,13 @@ import { dirname, join, resolve } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { app, BrowserWindow, ipcMain, Menu, nativeImage } from "electron";
|
|
6
6
|
import { buildDiffReview, performHttpRequest } from "./cli.js";
|
|
7
|
+
import { sanitizeTerminalEnv } from "./util.js";
|
|
7
8
|
import { spawn as spawnPty } from "node-pty";
|
|
9
|
+
// `npm run dev` sets MONACORI_DEV=1 so a locally-built app announces itself — a window-title suffix
|
|
10
|
+
// plus a boot log with its on-disk path — making it obvious whether `mo` launched THIS checkout or
|
|
11
|
+
// the globally-installed package (their version numbers can be identical; the path is the tell).
|
|
12
|
+
const DEV_BUILD = process.env.MONACORI_DEV === "1";
|
|
13
|
+
const APP_TITLE = DEV_BUILD ? "monacori (dev)" : "monacori";
|
|
8
14
|
const FLOW_DIR = ".monacori";
|
|
9
15
|
const REVIEW_FILE = "app-review.html";
|
|
10
16
|
const WATCH_INTERVAL_MS = 1000;
|
|
@@ -61,7 +67,7 @@ ipcMain.handle("monacori:pty-spawn", (_event, size) => {
|
|
|
61
67
|
cols: size?.cols ?? 80,
|
|
62
68
|
rows: size?.rows ?? 24,
|
|
63
69
|
cwd: options.root,
|
|
64
|
-
env: process.env,
|
|
70
|
+
env: sanitizeTerminalEnv(process.env),
|
|
65
71
|
});
|
|
66
72
|
terms.set(id, t);
|
|
67
73
|
// mainWindow?. only guards null, NOT a *destroyed* window — sending to a closed window's webContents
|
|
@@ -137,6 +143,9 @@ if (!existsSync(options.root)) {
|
|
|
137
143
|
throw new Error(`Repository path does not exist: ${options.root}`);
|
|
138
144
|
}
|
|
139
145
|
app.whenReady().then(async () => {
|
|
146
|
+
// Foreground (`npm run dev` / `mo --foreground`) surfaces this in the terminal; detached `mo` drops
|
|
147
|
+
// it. Either way the path disambiguates a local checkout from the installed package.
|
|
148
|
+
console.error(`[monacori] ${DEV_BUILD ? "DEV build" : "build"} — ${app.getAppPath()} (electron ${process.versions.electron})`);
|
|
140
149
|
process.chdir(options.root);
|
|
141
150
|
mkdirSync(FLOW_DIR, { recursive: true });
|
|
142
151
|
// Keep the standard Edit/Window roles so Cmd+C/V/X/A (copy comments into prompts) and Cmd+Q work.
|
|
@@ -208,7 +217,7 @@ app.whenReady().then(async () => {
|
|
|
208
217
|
minWidth: 960,
|
|
209
218
|
minHeight: 640,
|
|
210
219
|
show: false,
|
|
211
|
-
title:
|
|
220
|
+
title: APP_TITLE,
|
|
212
221
|
icon: iconPath,
|
|
213
222
|
backgroundColor: "#2b2b2b",
|
|
214
223
|
autoHideMenuBar: true,
|
|
@@ -221,7 +230,11 @@ app.whenReady().then(async () => {
|
|
|
221
230
|
},
|
|
222
231
|
});
|
|
223
232
|
mainWindow.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
|
|
224
|
-
mainWindow.once("ready-to-show", () =>
|
|
233
|
+
mainWindow.once("ready-to-show", () => {
|
|
234
|
+
mainWindow?.show();
|
|
235
|
+
if (DEV_BUILD)
|
|
236
|
+
mainWindow?.webContents.openDevTools({ mode: "detach" });
|
|
237
|
+
});
|
|
225
238
|
// Paint the window with a spinner immediately, then build the (potentially heavy) review off the first
|
|
226
239
|
// paint and swap it in. The first build used to run synchronously *before* the window existed, so the
|
|
227
240
|
// screen stayed blank for the first few seconds of startup; now the user sees a loading screen instead.
|
|
@@ -289,7 +302,7 @@ function writeReviewFile(input) {
|
|
|
289
302
|
staged: input.staged,
|
|
290
303
|
includeUntracked: input.includeUntracked,
|
|
291
304
|
context: input.context,
|
|
292
|
-
title:
|
|
305
|
+
title: APP_TITLE,
|
|
293
306
|
ignoreWhitespace: input.ignoreWhitespace,
|
|
294
307
|
lazyLoad: true, // Electron streams per-file bodies/source over IPC (monacori:get-file / get-source)
|
|
295
308
|
app: true, // gate the integrated terminal (xterm) into the HTML — Electron only
|
package/dist/commands.js
CHANGED
|
@@ -124,6 +124,10 @@ function launchReviewApp(args) {
|
|
|
124
124
|
if (args.includes("--no-watch"))
|
|
125
125
|
appArgs.push("--no-watch");
|
|
126
126
|
const electronBinary = resolveElectronBinary();
|
|
127
|
+
// Tell the user which build is about to run. The app-main path disambiguates a local checkout from
|
|
128
|
+
// the installed package (their versions can match); printed on the shell that ran `mo`, even when
|
|
129
|
+
// the app itself is spawned detached.
|
|
130
|
+
console.error(`monacori: launching ${appMainPath()}`);
|
|
127
131
|
if (args.includes("--foreground")) {
|
|
128
132
|
const result = spawnSync(electronBinary, appArgs, { stdio: "inherit" });
|
|
129
133
|
process.exit(result.status ?? 0);
|
package/dist/util.d.ts
CHANGED
|
@@ -16,3 +16,6 @@ export declare function jsonForScript(value: unknown): string;
|
|
|
16
16
|
export declare function escapeAttr(value: string): string;
|
|
17
17
|
export declare function formatBytes(bytes: number): string;
|
|
18
18
|
export declare function listRecentFiles(dir: string, limit: number): string[];
|
|
19
|
+
export declare function sanitizeTerminalEnv(env: NodeJS.ProcessEnv): {
|
|
20
|
+
[key: string]: string;
|
|
21
|
+
};
|
package/dist/util.js
CHANGED
|
@@ -142,3 +142,21 @@ export function listRecentFiles(dir, limit) {
|
|
|
142
142
|
.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)
|
|
143
143
|
.slice(0, limit);
|
|
144
144
|
}
|
|
145
|
+
// The integrated terminal should behave like the user's own login shell — not like a child of however
|
|
146
|
+
// monacori was launched. When started through npm (`npm run dev`, or a global install run via an npm
|
|
147
|
+
// shim), npm injects npm_config_* / npm_lifecycle_* / npm_package_* vars into our process. Inheriting
|
|
148
|
+
// them into the pty leaks our run's npm config into the user's shell and, with nvm, triggers:
|
|
149
|
+
// "nvm is not compatible with the npm_config_prefix environment variable …"
|
|
150
|
+
// Strip every npm_*-injected var (npm_config_prefix is the one nvm rejects) and drop undefined holes,
|
|
151
|
+
// so the shell starts clean. Returns a fresh object; the input is not mutated.
|
|
152
|
+
export function sanitizeTerminalEnv(env) {
|
|
153
|
+
const out = {};
|
|
154
|
+
for (const [key, value] of Object.entries(env)) {
|
|
155
|
+
if (value === undefined)
|
|
156
|
+
continue;
|
|
157
|
+
if (key.startsWith("npm_"))
|
|
158
|
+
continue;
|
|
159
|
+
out[key] = value;
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
package/dist/viewer.client.js
CHANGED
|
@@ -124,7 +124,16 @@ var I18N = JSON.parse(document.getElementById('i18n-data')?.textContent || '{}')
|
|
|
124
124
|
// app restart; file:// localStorage doesn't); browser/serve falls back to localStorage. persistRead
|
|
125
125
|
// returns the bridge value (native) if present, else undefined so callers parse localStorage themselves.
|
|
126
126
|
function persistRead(key) {
|
|
127
|
-
|
|
127
|
+
// window.monacoriSettings.all crosses Electron's contextBridge, which DEEP-FREEZES every value it
|
|
128
|
+
// exposes. Returning that frozen object/array directly breaks callers that mutate the result —
|
|
129
|
+
// reviewComments.push(...) and mergePrompts[kind]=... both throw "object is not extensible". Hand
|
|
130
|
+
// back a mutable deep copy so the persisted snapshot is a starting point, not a locked one.
|
|
131
|
+
try {
|
|
132
|
+
if (window.monacoriSettings && window.monacoriSettings.all && key in window.monacoriSettings.all) {
|
|
133
|
+
var v = window.monacoriSettings.all[key];
|
|
134
|
+
return v && typeof v === 'object' ? JSON.parse(JSON.stringify(v)) : v;
|
|
135
|
+
}
|
|
136
|
+
} catch (e) {}
|
|
128
137
|
return undefined;
|
|
129
138
|
}
|
|
130
139
|
function persistSave(key, value) {
|
|
@@ -1847,7 +1856,7 @@ function refreshComments() {
|
|
|
1847
1856
|
if (composerState) {
|
|
1848
1857
|
var composerFocusTries = 0;
|
|
1849
1858
|
var tryFocusComposer = function () {
|
|
1850
|
-
var ta =
|
|
1859
|
+
var ta = activeComposerInput();
|
|
1851
1860
|
if (!ta) return true; // composer gone — stop retrying
|
|
1852
1861
|
if (document.activeElement === ta) return true; // already focused — done
|
|
1853
1862
|
try { ta.focus({ preventScroll: true }); } catch (e) { try { ta.focus(); } catch (e2) {} }
|
|
@@ -1879,9 +1888,24 @@ function closeComposer() {
|
|
|
1879
1888
|
composerState = null;
|
|
1880
1889
|
refreshComments();
|
|
1881
1890
|
}
|
|
1891
|
+
// The composer is injected into BOTH the diff and source views (refreshComments renders comments in
|
|
1892
|
+
// each), but only one view is on screen at a time — the other lives inside a `.hidden` container with
|
|
1893
|
+
// its own, empty textarea. Pick the textarea in the *visible* view so save/auto-focus never grab the
|
|
1894
|
+
// off-screen duplicate. This was the "Comment doesn't save" bug: clicking Save ran
|
|
1895
|
+
// document.querySelector('.mc-composer .mc-input'), which returns the hidden diff-view textarea first
|
|
1896
|
+
// (it precedes #source-viewer in the DOM), so addComment got its empty value and bailed.
|
|
1897
|
+
function activeComposerInput() {
|
|
1898
|
+
var inputs = document.querySelectorAll('.mc-composer .mc-input');
|
|
1899
|
+
for (var i = 0; i < inputs.length; i++) {
|
|
1900
|
+
if (inputs[i].closest('#diff-view') && !isDiffViewVisible()) continue;
|
|
1901
|
+
if (inputs[i].closest('#source-viewer') && !isSourceViewerVisible()) continue;
|
|
1902
|
+
return inputs[i];
|
|
1903
|
+
}
|
|
1904
|
+
return inputs[0] || null;
|
|
1905
|
+
}
|
|
1882
1906
|
function saveComposer(ta) {
|
|
1883
1907
|
if (!composerState) return;
|
|
1884
|
-
var box = ta ||
|
|
1908
|
+
var box = ta || activeComposerInput();
|
|
1885
1909
|
if (!box) return;
|
|
1886
1910
|
addComment(composerState.kind, composerState.path, composerState.line, composerState.code, box.value);
|
|
1887
1911
|
composerState = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happy-nut/monacori",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Validation control plane for AI-generated code changes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -29,10 +29,13 @@
|
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "tsc -p tsconfig.json && node scripts/copy-viewer-assets.mjs",
|
|
32
|
+
"dev": "npm run build && MONACORI_DEV=1 node bin/monacori.js --foreground",
|
|
32
33
|
"icon": "node scripts/gen-icon.mjs",
|
|
33
34
|
"postinstall": "node scripts/patch-electron-name.mjs && node scripts/fix-pty-spawn-helper.mjs",
|
|
34
35
|
"prepare": "npm run build",
|
|
35
|
-
"smoke": "npm run build && node dist/cli.js --help"
|
|
36
|
+
"smoke": "npm run build && node dist/cli.js --help",
|
|
37
|
+
"pretest": "npm run build",
|
|
38
|
+
"test": "node --test test/*.test.mjs"
|
|
36
39
|
},
|
|
37
40
|
"keywords": [
|
|
38
41
|
"ai",
|
|
@@ -45,6 +48,7 @@
|
|
|
45
48
|
"license": "MIT",
|
|
46
49
|
"devDependencies": {
|
|
47
50
|
"@types/node": "^22.15.21",
|
|
51
|
+
"jsdom": "^29.1.1",
|
|
48
52
|
"typescript": "^5.8.3"
|
|
49
53
|
},
|
|
50
54
|
"engines": {
|