@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 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: "monacori",
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", () => mainWindow?.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: "monacori",
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
+ }
@@ -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
- try { if (window.monacoriSettings && window.monacoriSettings.all && key in window.monacoriSettings.all) return window.monacoriSettings.all[key]; } catch (e) {}
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 = document.querySelector('.mc-composer .mc-input');
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 || document.querySelector('.mc-composer .mc-input');
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.9",
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": {