@tanishqkancharla/diffmap 0.1.3 → 0.1.4

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
@@ -5,27 +5,26 @@
5
5
  diffmap is a set of personal skills as well as a web-viewer for Markdown files. It has a unique form of showing diffs using a mixture of callstack diffs and mermaid diagram links, which I've found to be personally extremely helpful in understanding massive diffs.
6
6
 
7
7
  ```sh
8
- npx diffmap path/to/file.md
8
+ npx @tanishqkancharla/diffmap serve path/to/file.md
9
9
  ```
10
10
 
11
+ Bare `npx @tanishqkancharla/diffmap path/to/file.md` is the same as `serve`.
12
+
11
13
  Share a secret gist and print a `diffmap.dev` link (requires [`gh`](https://cli.github.com/) logged in). Does not start a local server:
12
14
 
13
15
  ```sh
14
- npx diffmap share path/to.md
16
+ npx @tanishqkancharla/diffmap share path/to.md
15
17
  ```
16
18
 
17
19
  Hosted viewer: `https://diffmap.dev/g/<gistId>` (optional `/<file.md>`; `#heading` is the table of contents). The page fetches the gist in the browser from `api.github.com`. Gists are unlisted, not private.
18
20
 
19
21
  ## Agent skills
20
22
 
21
- Install skills from this repository:
22
-
23
23
  ```sh
24
- npx skills add tanishqkancharla/diffmap --skill code-walkthrough
25
24
  npx skills add tanishqkancharla/diffmap --skill generate-spec
26
25
  ```
27
26
 
28
- `code-walkthrough` explains landed changes. `generate-spec` writes a phased spec for work that has not happened yet, then serves `specs/<name>.md` with diffmap.
27
+ Keeps a spec in `specs/` that stays true to what's planned versus already in the tree. Serve it with diffmap. Share from a GitHub PR (`https://diffmap.dev/<owner>/<repo>/pull/<n>/specs/<name>.md`) or `npx @tanishqkancharla/diffmap share`.
29
28
 
30
29
  Options:
31
30
 
@@ -41,7 +40,7 @@ Vite connections do not keep it alive.
41
40
  List every running viewer, including viewers using custom ports:
42
41
 
43
42
  ```sh
44
- npx diffmap list
43
+ npx @tanishqkancharla/diffmap list
45
44
  ```
46
45
 
47
46
  Request the page with `Accept: text/markdown` to read the current source file
@@ -54,10 +53,14 @@ curl -H 'Accept: text/markdown' http://127.0.0.1:4177/
54
53
  ## Library
55
54
 
56
55
  ```ts
57
- import { startServer, parseFence, parseViewerDocument } from "diffmap";
56
+ import {
57
+ startServer,
58
+ parseFence,
59
+ parseViewerDocument,
60
+ } from "@tanishqkancharla/diffmap";
58
61
  ```
59
62
 
60
- `parseViewerDocument` turns markdown into the page document with [md4x](https://github.com/unjs/md4x). `startServer` listens. The generate-spec and code-walkthrough skills own spec vs walkthrough section order; the viewer does not.
63
+ `parseViewerDocument` turns markdown into the page document with [md4x](https://github.com/unjs/md4x). `startServer` listens. The generate-spec skill owns document shape; the viewer does not.
61
64
 
62
65
  ## Fences
63
66
 
@@ -176,7 +179,7 @@ Resolution uses the workspace's TypeScript configuration and current files.
176
179
  Deleted files and lines that no longer match the workspace show a message;
177
180
  their call stack references still highlight the embedded old-side diff.
178
181
 
179
- Run `npx diffmap fixtures/annotations.md` for an example
182
+ Run `npx @tanishqkancharla/diffmap fixtures/annotations.md` for an example
180
183
  with old/new references, multiple files, and unchanged context.
181
184
 
182
185
  ## Link Mermaid nodes and edges
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanishqkancharla/diffmap",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Serve a spec or code walkthrough as a local page",
5
5
  "license": "MIT",
6
6
  "author": "Tanishq Kancharla",
@@ -33,7 +33,7 @@
33
33
  "lint:fix": "oxlint --fix src vite.config.ts vite.gist.config.ts bin.js",
34
34
  "prepublishOnly": "npm run typecheck && npm run lint && npm run format:check",
35
35
  "release": "npm publish",
36
- "test": "tsx --test src/gist/route.test.ts",
36
+ "test": "tsx --test src/parseViewer.test.ts src/gist/route.test.ts src/github/route.test.ts src/github/patch.test.ts src/github/fetchRepo.test.ts src/viewer/sourceHeader.test.ts",
37
37
  "typecheck": "tsc --noEmit",
38
38
  "build": "vite build",
39
39
  "build:gist": "vite build",
package/src/cli.ts CHANGED
@@ -6,65 +6,76 @@ import { listRunningDiffmaps } from "./registry.js";
6
6
  import { startServer } from "./serve.js";
7
7
  import { shareMarkdownFile } from "./share.js";
8
8
 
9
+ const serveArgs = z.object({
10
+ file: z.string().describe("Path to the markdown file"),
11
+ });
12
+
13
+ const serveOptions = z.object({
14
+ port: z.coerce.number().optional().describe("Port (default 4177)"),
15
+ root: z.string().optional().describe("Workspace root for file excerpts"),
16
+ });
17
+
18
+ const serveOutput = z.object({
19
+ url: z.string(),
20
+ file: z.string(),
21
+ });
22
+
23
+ type ServeContext = {
24
+ args: { file: string };
25
+ options: { port?: number | undefined; root?: string | undefined };
26
+ error: (input: { code: string; message: string }) => never;
27
+ };
28
+
29
+ async function* runServe(c: ServeContext) {
30
+ const started = await startServer({
31
+ filePath: resolveFromInvokeCwd(c.args.file),
32
+ workspaceRoot:
33
+ c.options.root === undefined
34
+ ? resolveFromInvokeCwd(".")
35
+ : resolveFromInvokeCwd(c.options.root),
36
+ port: c.options.port === undefined ? 4177 : c.options.port,
37
+ });
38
+ if (started instanceof Error) {
39
+ return c.error({
40
+ code: "DIFFMAP",
41
+ message: started.message,
42
+ });
43
+ }
44
+ yield { url: started.url, file: started.filePath };
45
+ await started.closed;
46
+ return { url: started.url, file: started.filePath };
47
+ }
48
+
9
49
  const cli = Cli.create("diffmap", {
10
- description: "Serve a spec or code walkthrough as a local page",
50
+ description: "Serve a spec locally, or share it as a gist",
11
51
  version: "0.1.3",
12
- args: z.object({
13
- file: z.string().describe("Path to the markdown file"),
14
- }),
15
- options: z.object({
16
- port: z.coerce.number().optional().describe("Port (default 4177)"),
17
- root: z.string().optional().describe("Workspace root for file excerpts"),
18
- }),
19
- output: z.object({
20
- url: z.string(),
21
- file: z.string(),
22
- }),
23
- async *run(c) {
24
- const started = await startServer({
25
- filePath: resolveFromInvokeCwd(c.args.file),
26
- workspaceRoot:
27
- c.options.root === undefined
28
- ? resolveFromInvokeCwd(".")
29
- : resolveFromInvokeCwd(c.options.root),
30
- port: c.options.port === undefined ? 4177 : c.options.port,
31
- });
32
- if (started instanceof Error) {
33
- return c.error({
34
- code: "DIFFMAP",
35
- message: started.message,
36
- });
37
- }
38
- yield { url: started.url, file: started.filePath };
39
- await started.closed;
40
- return { url: started.url, file: started.filePath };
41
- },
42
- })
43
- .command("list", {
44
- description: "List running diffmap viewers",
45
- output: z.object({
46
- instances: z.array(
47
- z.object({
48
- pid: z.number(),
49
- title: z.string(),
50
- url: z.string(),
51
- file: z.string(),
52
- }),
53
- ),
54
- }),
55
- async run(c) {
56
- const instances = await listRunningDiffmaps();
57
- if (instances instanceof Error) {
58
- return c.error({
59
- code: "DIFFMAP",
60
- message: instances.message,
61
- });
62
- }
63
- return c.ok({ instances });
52
+ args: serveArgs,
53
+ options: serveOptions,
54
+ output: serveOutput,
55
+ hint: "`serve <spec.md>` runs the local viewer. `share <spec.md>` publishes a secret gist.",
56
+ examples: [
57
+ {
58
+ args: { file: "spec.md" },
59
+ description: "Same as `diffmap serve spec.md`",
64
60
  },
61
+ ],
62
+ run: runServe,
63
+ })
64
+ .command("serve", {
65
+ description: "Run the local viewer server",
66
+ args: serveArgs,
67
+ options: serveOptions,
68
+ output: serveOutput,
69
+ examples: [
70
+ {
71
+ args: { file: "spec.md" },
72
+ description: "Open the local viewer",
73
+ },
74
+ ],
75
+ run: runServe,
65
76
  })
66
77
  .command("share", {
67
- description: "Upload a walkthrough gist and print the diffmap.dev URL",
78
+ description: "Publish a secret gist and print https://diffmap.dev/g/<id>",
68
79
  args: z.object({
69
80
  file: z.string().describe("Path to the markdown file"),
70
81
  }),
@@ -76,6 +87,12 @@ const cli = Cli.create("diffmap", {
76
87
  gistUrl: z.string(),
77
88
  viewerUrl: z.string(),
78
89
  }),
90
+ examples: [
91
+ {
92
+ args: { file: "spec.md" },
93
+ description: "Secret gist → https://diffmap.dev/g/<id>",
94
+ },
95
+ ],
79
96
  async run(c) {
80
97
  const shared = await shareMarkdownFile({
81
98
  filePath: resolveFromInvokeCwd(c.args.file),
@@ -89,6 +106,29 @@ const cli = Cli.create("diffmap", {
89
106
  }
90
107
  return c.ok(shared);
91
108
  },
109
+ })
110
+ .command("list", {
111
+ description: "List running diffmap viewers",
112
+ output: z.object({
113
+ instances: z.array(
114
+ z.object({
115
+ pid: z.number(),
116
+ title: z.string(),
117
+ url: z.string(),
118
+ file: z.string(),
119
+ }),
120
+ ),
121
+ }),
122
+ async run(c) {
123
+ const instances = await listRunningDiffmaps();
124
+ if (instances instanceof Error) {
125
+ return c.error({
126
+ code: "DIFFMAP",
127
+ message: instances.message,
128
+ });
129
+ }
130
+ return c.ok({ instances });
131
+ },
92
132
  });
93
133
 
94
134
  await cli.serve();
@@ -15,9 +15,11 @@ export function diffmapContentPlugin(input: {
15
15
  async load(id) {
16
16
  if (id !== virtualId) return;
17
17
  const source = await fs.readFile(input.filePath, "utf8");
18
- const document = parseViewerDocument(source);
19
- if (document instanceof Error) throw document;
20
- return `export const viewerDocument = ${JSON.stringify(document)};`;
18
+ const document = parseViewerDocument(source, input.filePath);
19
+ if (document instanceof Error) {
20
+ return `export const viewerDocument = null; export const parseError = ${JSON.stringify(document.message)};`;
21
+ }
22
+ return `export const viewerDocument = ${JSON.stringify(document)}; export const parseError = null;`;
21
23
  },
22
24
  transformIndexHtml(html) {
23
25
  return html.replaceAll(
package/src/errors.ts CHANGED
@@ -17,7 +17,7 @@ export class DiffmapRegistryError extends errore.createTaggedError({
17
17
 
18
18
  export class DiffmapMermaidError extends errore.createTaggedError({
19
19
  name: "DiffmapMermaidError",
20
- message: "diffmap could not render mermaid",
20
+ message: "diffmap could not parse mermaid in $path ($where): $reason",
21
21
  }) {}
22
22
 
23
23
  export class DiffmapParseError extends errore.createTaggedError({
@@ -6,6 +6,7 @@ import { parseViewerDocument } from "../parseViewer.ts";
6
6
  import type { ViewerDocument } from "../parseViewer.ts";
7
7
  import { ViewerApp } from "../viewer/ViewerApp.tsx";
8
8
  import { loadGistMarkdown, type GistDocument } from "./fetchGist.ts";
9
+ import { GistPinContext } from "./pin.ts";
9
10
  import { GistLanding, GistLoading, GistStatus } from "./GistStatus.tsx";
10
11
  import {
11
12
  gistPath,
@@ -151,24 +152,32 @@ export function GistApp() {
151
152
  }
152
153
 
153
154
  return (
154
- <ViewerApp
155
- key={`${view.gist.id}:${view.file}`}
156
- document={view.document}
157
- mode="gist"
158
- headerActions={
159
- <GistHeader
160
- gistId={view.gist.id}
161
- htmlUrl={view.gist.htmlUrl}
162
- files={view.gist.files}
163
- current={view.file}
164
- onSelect={(file) => {
165
- const next = gistPath(view.gist.id, file);
166
- window.history.pushState({}, "", next);
167
- setRoute(parseGistPath(next));
168
- }}
169
- />
170
- }
171
- />
155
+ <GistPinContext.Provider
156
+ value={{
157
+ gistId: view.gist.id,
158
+ file: view.file,
159
+ htmlUrl: view.gist.htmlUrl,
160
+ }}
161
+ >
162
+ <ViewerApp
163
+ key={`${view.gist.id}:${view.file}`}
164
+ document={view.document}
165
+ mode="gist"
166
+ headerActions={
167
+ <GistHeader
168
+ gistId={view.gist.id}
169
+ htmlUrl={view.gist.htmlUrl}
170
+ files={view.gist.files}
171
+ current={view.file}
172
+ onSelect={(file) => {
173
+ const next = gistPath(view.gist.id, file);
174
+ window.history.pushState({}, "", next);
175
+ setRoute(parseGistPath(next));
176
+ }}
177
+ />
178
+ }
179
+ />
180
+ </GistPinContext.Provider>
172
181
  );
173
182
  }
174
183
 
@@ -196,7 +205,10 @@ async function loadView(
196
205
  file: file ?? "",
197
206
  };
198
207
  }
199
- const parsed = parseViewerDocument(loaded.gist.chosen.content);
208
+ const parsed = parseViewerDocument(
209
+ loaded.gist.chosen.content,
210
+ loaded.gist.chosen.name,
211
+ );
200
212
  if (parsed instanceof Error) {
201
213
  return {
202
214
  kind: "parse-error",
@@ -19,6 +19,7 @@ export function GistStatus(props: {
19
19
  children: ReactNode;
20
20
  gistId?: string;
21
21
  gistUrl?: string;
22
+ linkLabel?: string;
22
23
  }) {
23
24
  const shell = useStyles(styles.shell);
24
25
  const header = useStyles(styles.header);
@@ -39,7 +40,9 @@ export function GistStatus(props: {
39
40
  {props.children}
40
41
  {gistUrl !== undefined && (
41
42
  <P>
42
- <Link href={gistUrl}>Open gist on GitHub</Link>
43
+ <Link href={gistUrl}>
44
+ {props.linkLabel ?? "Open gist on GitHub"}
45
+ </Link>
43
46
  </P>
44
47
  )}
45
48
  </main>
@@ -47,10 +50,10 @@ export function GistStatus(props: {
47
50
  );
48
51
  }
49
52
 
50
- export function GistLoading() {
53
+ export function GistLoading(props: { title?: string }) {
51
54
  const row = useStyles(styles.loading);
52
55
  return (
53
- <GistStatus title="Loading gist…">
56
+ <GistStatus title={props.title ?? "Loading gist…"}>
54
57
  <div className={row}>
55
58
  <Thinking />
56
59
  </div>
@@ -64,21 +67,32 @@ export function GistLanding() {
64
67
  <GistStatus title="diffmap">
65
68
  <P>
66
69
  A hosted viewer for diffmap walkthroughs. This page fetches a GitHub
67
- gist in your browser. There is no server holding the markdown.
70
+ gist or a public repo file in your browser. There is no server holding
71
+ the markdown.
68
72
  </P>
69
73
  <P>
70
- Canonical URL:{" "}
74
+ Canonical gist URL:{" "}
71
75
  <code className={code}>https://diffmap.dev/g/&lt;gistId&gt;</code>.
72
76
  Optional file:{" "}
73
- <code className={code}>/g/&lt;gistId&gt;/&lt;file.md&gt;</code>. Heading
74
- hashes stay in the fragment, same as the local viewer.
77
+ <code className={code}>/g/&lt;gistId&gt;/&lt;file.md&gt;</code>. GitHub
78
+ spec:{" "}
79
+ <code className={code}>
80
+ https://diffmap.dev/&lt;owner&gt;/&lt;repo&gt;/pull/&lt;n&gt;/&lt;path.md&gt;
81
+ </code>{" "}
82
+ or{" "}
83
+ <code className={code}>
84
+ /&lt;owner&gt;/&lt;repo&gt;/commit/&lt;sha&gt;/&lt;path.md&gt;
85
+ </code>
86
+ . Heading hashes stay in the fragment, same as the local viewer.
75
87
  </P>
76
88
  <P>
77
89
  Share a secret gist (requires <code className={code}>gh</code>
78
90
  ):
79
91
  </P>
80
92
  <P>
81
- <code className={code}>npx diffmap share path/to.md</code>
93
+ <code className={code}>
94
+ npx @tanishqkancharla/diffmap share path/to.md
95
+ </code>
82
96
  </P>
83
97
  <P>
84
98
  Secret gists are unlisted, not private. Anyone with the id can read
package/src/gist/main.tsx CHANGED
@@ -2,7 +2,7 @@ import { StrictMode } from "react";
2
2
  import { createRoot } from "react-dom/client";
3
3
  import { MauiProvider } from "maui";
4
4
  import { rewritePiGistUrl } from "./route.ts";
5
- import { GistApp } from "./GistApp.tsx";
5
+ import { HostApp } from "../host/HostApp.tsx";
6
6
  import "../viewer/styles.css";
7
7
 
8
8
  const rewritten = rewritePiGistUrl(
@@ -19,7 +19,7 @@ if (root === null) throw new Error("diffmap root element is missing");
19
19
  createRoot(root).render(
20
20
  <StrictMode>
21
21
  <MauiProvider>
22
- <GistApp />
22
+ <HostApp />
23
23
  </MauiProvider>
24
24
  </StrictMode>,
25
25
  );
@@ -0,0 +1,14 @@
1
+ import { createContext, useContext } from "react";
2
+
3
+ /** Gist snapshot: identify the gist file, never a fake owner/repo. */
4
+ export type GistPin = {
5
+ gistId: string;
6
+ file: string;
7
+ htmlUrl: string;
8
+ };
9
+
10
+ export const GistPinContext = createContext<GistPin | undefined>(undefined);
11
+
12
+ export function useGistPin() {
13
+ return useContext(GistPinContext);
14
+ }