@happy-nut/monacori 0.1.0 → 0.1.2

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
@@ -35,7 +35,7 @@ npm install -g @happy-nut/monacori
35
35
  After installation, the short command is:
36
36
 
37
37
  ```bash
38
- dg
38
+ mo
39
39
  ```
40
40
 
41
41
  Homebrew tap distribution is prepared through `Formula/monacori.rb`. Once the scoped npm package is published and the formula is copied to the `happy-nut/homebrew-monacori` tap, the intended install path is:
@@ -59,11 +59,11 @@ npm link
59
59
  Inside the repository you want to validate:
60
60
 
61
61
  ```bash
62
- dg
62
+ mo
63
63
  monacori check --include-untracked
64
64
  ```
65
65
 
66
- `dg` opens the local desktop review app for the current directory. On first run it creates `.monacori/`, updates Git ignore rules for `.monacori/`, and includes untracked files so new AI-created files show up immediately.
66
+ `mo` opens the local desktop review app for the current directory. On first run it creates `.monacori/`, updates Git ignore rules for `.monacori/`, and includes untracked files so new AI-created files show up immediately.
67
67
 
68
68
  `check` runs configured verification commands, writes a log, creates a browser diff review, and records a compact report.
69
69
 
@@ -76,7 +76,7 @@ monacori check -- npm test
76
76
  For live diff review while an AI is still editing:
77
77
 
78
78
  ```bash
79
- dg
79
+ mo
80
80
  ```
81
81
 
82
82
  ## Diff Review
@@ -100,7 +100,7 @@ The browser artifact path is still available through `monacori diff`. Add `--wat
100
100
  monacori open [--base HEAD] [--staged] [--tracked-only] [--context 12] [--no-watch]
101
101
  ```
102
102
 
103
- Opens the local desktop review app for the current directory. `dg` and bare `monacori` are aliases for this default flow. It auto-initializes local state when needed and includes untracked files by default; pass `--tracked-only` to inspect tracked changes only.
103
+ Opens the local desktop review app for the current directory. `mo` and bare `monacori` are aliases for this default flow. It auto-initializes local state when needed and includes untracked files by default; pass `--tracked-only` to inspect tracked changes only.
104
104
 
105
105
  ```bash
106
106
  monacori check [--include-untracked] [--staged] [--base HEAD] [--context 12] [--open] [--no-verify] [--no-diff] [-- <command>]
@@ -130,7 +130,7 @@ Runs configured verification commands and stores the log in `.monacori/logs/`. E
130
130
  monacori app [--base HEAD] [--staged] [--include-untracked] [--context 12] [--no-watch]
131
131
  ```
132
132
 
133
- Launches the local desktop review app. `dg`, `monacori open`, and `monacori review` are aliases. Prefer `dg` for normal use.
133
+ Launches the local desktop review app. `mo`, `monacori open`, and `monacori review` are aliases. Prefer `mo` for normal use.
134
134
 
135
135
  ```bash
136
136
  monacori diff [--base HEAD] [--staged] [--include-untracked] [--context 12] [--output review.html] [--open] [--watch] [--port 0]
package/assets/icon.png CHANGED
Binary file
package/dist/app-main.js CHANGED
@@ -1,13 +1,25 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { app, BrowserWindow, Menu, nativeImage } from "electron";
5
- import { buildDiffReview } from "./cli.js";
4
+ import { app, BrowserWindow, ipcMain, Menu, nativeImage } from "electron";
5
+ import { buildDiffReview, performHttpRequest } from "./cli.js";
6
6
  const FLOW_DIR = ".monacori";
7
7
  const REVIEW_FILE = "app-review.html";
8
8
  const WATCH_INTERVAL_MS = 1000;
9
9
  app.setName("monacori");
10
+ ipcMain.handle("monacori:http-send", (_event, request) => performHttpRequest(request));
11
+ // Phase 2 lazy-LOAD: serve a single file's diff body to the renderer on demand. Retained from the
12
+ // most recent writeReviewFile() build so navigation/scroll can materialize bodies without embedding.
13
+ let currentBodies = [];
14
+ let currentSourceData = "[]";
15
+ ipcMain.handle("monacori:get-file", (_event, request) => {
16
+ const i = Number(request?.index);
17
+ return Number.isInteger(i) && i >= 0 && i < currentBodies.length ? currentBodies[i] : "";
18
+ });
19
+ // Phase 2b lazy-LOAD: serve the full source files JSON (with content) on demand.
20
+ ipcMain.handle("monacori:get-source-data", () => currentSourceData);
10
21
  const iconPath = join(dirname(fileURLToPath(import.meta.url)), "..", "assets", "icon.png");
22
+ const preloadPath = join(dirname(fileURLToPath(import.meta.url)), "preload.cjs");
11
23
  const options = parseArgs(process.argv.slice(2));
12
24
  let mainWindow;
13
25
  let currentSignature = "";
@@ -19,7 +31,38 @@ if (!existsSync(options.root)) {
19
31
  app.whenReady().then(async () => {
20
32
  process.chdir(options.root);
21
33
  mkdirSync(FLOW_DIR, { recursive: true });
22
- Menu.setApplicationMenu(null);
34
+ // Keep the standard Edit/Window roles so Cmd+C/V/X/A (copy comments into prompts) and Cmd+Q work.
35
+ // The in-window menu bar stays hidden on Windows/Linux via autoHideMenuBar; macOS shows it in the top bar.
36
+ const sendMerged = (kind) => mainWindow?.webContents.send("monacori:merged-view", kind);
37
+ const menuTemplate = [];
38
+ if (process.platform === "darwin")
39
+ menuTemplate.push({ role: "appMenu" });
40
+ menuTemplate.push({ role: "editMenu" });
41
+ // Claim Cmd/Ctrl+Shift+/ ("?") and Cmd/Ctrl+Shift+. (">") as menu accelerators so macOS does not
42
+ // swallow Cmd+? for its Help search; clicking routes to the renderer's merged comment views.
43
+ menuTemplate.push({
44
+ label: "Review",
45
+ submenu: [
46
+ { label: "All questions", accelerator: "CommandOrControl+Shift+/", click: () => sendMerged("q") },
47
+ { label: "All change requests", accelerator: "CommandOrControl+Shift+.", click: () => sendMerged("c") },
48
+ { type: "separator" },
49
+ // Whitespace-ignore re-runs git diff with --ignore-all-space and reloads (main-process action,
50
+ // so a menu checkbox is simpler than a renderer IPC round-trip).
51
+ {
52
+ label: "Ignore whitespace",
53
+ type: "checkbox",
54
+ checked: options.ignoreWhitespace,
55
+ accelerator: "CommandOrControl+Shift+W",
56
+ click: (item) => {
57
+ options.ignoreWhitespace = item.checked;
58
+ currentSignature = writeReviewFile(options).signature;
59
+ mainWindow?.webContents.reloadIgnoringCache();
60
+ },
61
+ },
62
+ ],
63
+ });
64
+ menuTemplate.push({ role: "windowMenu" });
65
+ Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplate));
23
66
  const appIcon = nativeImage.createFromPath(iconPath);
24
67
  if (process.platform === "darwin" && app.dock && !appIcon.isEmpty()) {
25
68
  app.dock.setIcon(appIcon);
@@ -37,6 +80,7 @@ app.whenReady().then(async () => {
37
80
  backgroundColor: "#2b2b2b",
38
81
  autoHideMenuBar: true,
39
82
  webPreferences: {
83
+ preload: preloadPath,
40
84
  contextIsolation: true,
41
85
  nodeIntegration: false,
42
86
  sandbox: true,
@@ -83,8 +127,12 @@ function writeReviewFile(input) {
83
127
  includeUntracked: input.includeUntracked,
84
128
  context: input.context,
85
129
  title: "monacori",
130
+ ignoreWhitespace: input.ignoreWhitespace,
131
+ lazyLoad: true, // Electron streams per-file bodies/source over IPC (monacori:get-file / get-source)
86
132
  });
87
133
  writeFileSync(reviewPath(), build.html);
134
+ currentBodies = build.lazyBodies ?? [];
135
+ currentSourceData = build.lazySourceData ?? "[]";
88
136
  return { signature: build.signature };
89
137
  }
90
138
  function reviewPath() {
@@ -100,6 +148,7 @@ function parseArgs(args) {
100
148
  includeUntracked: args.includes("--include-untracked"),
101
149
  context: contextValue ? parsePositiveInteger(contextValue, "--context") : 12,
102
150
  watch: !args.includes("--no-watch"),
151
+ ignoreWhitespace: args.includes("--ignore-whitespace"),
103
152
  };
104
153
  }
105
154
  function readOption(args, name) {
@@ -0,0 +1,4 @@
1
+ export declare function readViewerAsset(name: string): string;
2
+ export declare function diff2HtmlCss(): string;
3
+ export declare function diffCss(): string;
4
+ export declare function diffScript(): string;
package/dist/assets.js ADDED
@@ -0,0 +1,30 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { createRequire } from "node:module";
5
+ const nodeRequire = createRequire(import.meta.url);
6
+ const viewerAssetCache = new Map();
7
+ // Client viewer script/stylesheet live in sibling files (copied to dist/ at build) so this
8
+ // module stays small and the client code can use template literals freely (no String.raw).
9
+ export function readViewerAsset(name) {
10
+ let cached = viewerAssetCache.get(name);
11
+ if (cached === undefined) {
12
+ cached = readFileSync(join(dirname(fileURLToPath(import.meta.url)), name), "utf8");
13
+ viewerAssetCache.set(name, cached);
14
+ }
15
+ return cached;
16
+ }
17
+ export function diff2HtmlCss() {
18
+ try {
19
+ return readFileSync(nodeRequire.resolve("diff2html/bundles/css/diff2html.min.css"), "utf8");
20
+ }
21
+ catch {
22
+ return "";
23
+ }
24
+ }
25
+ export function diffCss() {
26
+ return readViewerAsset("viewer.css");
27
+ }
28
+ export function diffScript() {
29
+ return readViewerAsset("viewer.client.js");
30
+ }
@@ -0,0 +1,12 @@
1
+ import type { DiffReviewBuild } from "./types.js";
2
+ export declare function buildDiffReview(input: {
3
+ base?: string;
4
+ staged: boolean;
5
+ includeUntracked: boolean;
6
+ context: number;
7
+ title: string;
8
+ watch?: boolean;
9
+ ignoreWhitespace?: boolean;
10
+ lazy?: boolean;
11
+ lazyLoad?: boolean;
12
+ }): DiffReviewBuild;
package/dist/build.js ADDED
@@ -0,0 +1,74 @@
1
+ import { createHash } from "node:crypto";
2
+ import { basename } from "node:path";
3
+ import { isGitRepository } from "./git.js";
4
+ import { collectHttpEnvironments, collectReviewFileStates, collectSourceFiles, parseUnifiedDiff, readUnifiedDiff } from "./diff.js";
5
+ import { renderDiff2Html } from "./highlight.js";
6
+ import { diffSubtitle, renderDiffHtml, renderNotGitRepoHtml, shouldLazyRender, splitDiffForLazy } from "./render.js";
7
+ export function buildDiffReview(input) {
8
+ if (!isGitRepository(process.cwd())) {
9
+ return {
10
+ html: renderNotGitRepoHtml(process.cwd()),
11
+ files: 0,
12
+ hunks: 0,
13
+ signature: "not-a-git-repo",
14
+ generatedAt: new Date().toISOString(),
15
+ };
16
+ }
17
+ const diffText = readUnifiedDiff({
18
+ base: input.base,
19
+ staged: input.staged,
20
+ context: input.context,
21
+ includeUntracked: input.includeUntracked,
22
+ ignoreWhitespace: input.ignoreWhitespace,
23
+ });
24
+ const files = parseUnifiedDiff(diffText);
25
+ const sourceFiles = collectSourceFiles(files);
26
+ const fileStates = collectReviewFileStates(files, sourceFiles);
27
+ const httpEnvironments = collectHttpEnvironments(process.cwd());
28
+ const hunks = files.reduce((sum, file) => sum + file.hunks.length, 0);
29
+ const generatedAt = new Date().toISOString();
30
+ const diffHtml = renderDiff2Html(diffText);
31
+ const totalLines = files.reduce((sum, file) => sum + file.hunks.reduce((t, h) => t + h.lines.length, 0), 0);
32
+ // lazy-LOAD (Phase 2) serves each file body + source on demand instead of embedding them; it implies
33
+ // lazy (shells). Gated by size: only big reviews lazy-LOAD — small ones embed (no IPC round-trips, no
34
+ // "Loading source…" flash). The transport opts in (serve/Electron pass lazyLoad:true); standalone has
35
+ // no server. A big standalone review still lazy-materializes from embedded islands (Phase 1).
36
+ const big = shouldLazyRender(files.length, totalLines);
37
+ const lazyLoad = (input.lazyLoad ?? false) && big;
38
+ const lazy = lazyLoad || (input.lazy ?? big);
39
+ const diffSplit = lazy ? splitDiffForLazy(diffHtml, files) : { container: diffHtml, islands: "", bodies: [] };
40
+ const signature = createHash("sha1")
41
+ .update(diffText)
42
+ .update("\n")
43
+ .update(sourceFiles.map((file) => `${file.path}\0${file.size}\0${file.embedded ? file.content : file.skippedReason ?? ""}`).join("\n"))
44
+ .update("\n")
45
+ .update(JSON.stringify(httpEnvironments))
46
+ .digest("hex");
47
+ const html = renderDiffHtml({
48
+ files,
49
+ diffHtml: diffSplit.container,
50
+ diffIslands: lazyLoad ? "" : diffSplit.islands,
51
+ lazy,
52
+ lazyLoad,
53
+ sourceFiles,
54
+ fileStates,
55
+ httpEnvironments,
56
+ title: input.title,
57
+ subtitle: diffSubtitle(input),
58
+ projectName: basename(process.cwd()),
59
+ projectPath: process.cwd(),
60
+ watch: Boolean(input.watch),
61
+ ignoreWhitespace: Boolean(input.ignoreWhitespace),
62
+ signature,
63
+ generatedAt,
64
+ });
65
+ return {
66
+ html,
67
+ files: files.length,
68
+ hunks,
69
+ signature,
70
+ generatedAt,
71
+ lazyBodies: diffSplit.bodies,
72
+ lazySourceData: lazyLoad ? JSON.stringify(sourceFiles) : undefined,
73
+ };
74
+ }
package/dist/cli.d.ts CHANGED
@@ -1,34 +1,6 @@
1
1
  #!/usr/bin/env node
2
- export type HttpSendRequest = {
3
- method: string;
4
- url: string;
5
- headers: Record<string, string>;
6
- body?: string;
7
- };
8
- export type HttpSendResult = {
9
- ok: boolean;
10
- status?: number;
11
- statusText?: string;
12
- headers?: Record<string, string>;
13
- body?: string;
14
- error?: string;
15
- durationMs: number;
16
- };
17
- type DiffReviewBuild = {
18
- html: string;
19
- files: number;
20
- hunks: number;
21
- signature: string;
22
- generatedAt: string;
23
- };
24
- export declare function main(): void;
25
- export declare function buildDiffReview(input: {
26
- base?: string;
27
- staged: boolean;
28
- includeUntracked: boolean;
29
- context: number;
30
- title: string;
31
- watch?: boolean;
32
- }): DiffReviewBuild;
33
- export declare function performHttpRequest(request: HttpSendRequest): Promise<HttpSendResult>;
34
- export {};
2
+ import { main } from "./commands.js";
3
+ export { main };
4
+ export { buildDiffReview } from "./build.js";
5
+ export { performHttpRequest } from "./server.js";
6
+ export type { HttpSendRequest, HttpSendResult } from "./types.js";