idcmd 0.0.1 → 0.0.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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -2
  3. package/package.json +52 -6
  4. package/public/_idcmd/live-reload.js +18 -0
  5. package/public/_idcmd/llm-menu.js +153 -0
  6. package/public/_idcmd/nav-prefetch.js +30 -0
  7. package/public/_idcmd/right-rail-scrollspy.js +262 -0
  8. package/public/anthropic-black.svg +16 -0
  9. package/public/anthropic-white.svg +16 -0
  10. package/public/favicon.svg +13 -0
  11. package/public/live-reload.js +18 -0
  12. package/public/llm-menu.js +153 -0
  13. package/public/openai-black.svg +15 -0
  14. package/public/openai-white.svg +15 -0
  15. package/public/right-rail-scrollspy.js +262 -0
  16. package/src/build.ts +230 -0
  17. package/src/cli/args.ts +101 -0
  18. package/src/cli/commands/build.ts +43 -0
  19. package/src/cli/commands/deploy.ts +82 -0
  20. package/src/cli/commands/dev.ts +79 -0
  21. package/src/cli/commands/init.ts +211 -0
  22. package/src/cli/commands/preview.ts +57 -0
  23. package/src/cli/fs.ts +47 -0
  24. package/src/cli/main.ts +120 -0
  25. package/src/cli/normalize.ts +26 -0
  26. package/src/cli/path.ts +30 -0
  27. package/src/cli/prompt.ts +74 -0
  28. package/src/cli/run.ts +17 -0
  29. package/src/cli/version.ts +12 -0
  30. package/src/cli.ts +6 -0
  31. package/src/client/index.ts +7 -0
  32. package/src/content/components/expand.ts +351 -0
  33. package/src/content/components/install-tabs.ts +120 -0
  34. package/src/content/components/registry.ts +12 -0
  35. package/src/content/components/types.ts +21 -0
  36. package/src/content/frontmatter.ts +89 -0
  37. package/src/content/icons.ts +78 -0
  38. package/src/content/llms.ts +94 -0
  39. package/src/content/meta.ts +92 -0
  40. package/src/content/navigation.ts +156 -0
  41. package/src/content/paths.ts +34 -0
  42. package/src/content/store.ts +10 -0
  43. package/src/project/paths.ts +86 -0
  44. package/src/render/layout-loader.ts +46 -0
  45. package/src/render/layout.tsx +340 -0
  46. package/src/render/markdown.ts +14 -0
  47. package/src/render/page-renderer.ts +321 -0
  48. package/src/render/right-rail.tsx +250 -0
  49. package/src/render/toc.ts +66 -0
  50. package/src/search/api.ts +76 -0
  51. package/src/search/contract.ts +44 -0
  52. package/src/search/index.ts +265 -0
  53. package/src/search/page.tsx +96 -0
  54. package/src/search/server-page.ts +99 -0
  55. package/src/seo/files.ts +124 -0
  56. package/src/seo/server.ts +103 -0
  57. package/src/server/headers.ts +10 -0
  58. package/src/server/live-reload.ts +121 -0
  59. package/src/server/static.ts +59 -0
  60. package/src/server/user-routes.ts +209 -0
  61. package/src/server.ts +234 -0
  62. package/src/site/config.ts +244 -0
  63. package/src/site/url-policy.ts +60 -0
  64. package/src/site/urls.ts +46 -0
  65. package/templates/default/README.md +26 -0
  66. package/templates/default/package.json +29 -0
  67. package/templates/default/site/client/layout.tsx +2 -0
  68. package/templates/default/site/client/right-rail.tsx +1 -0
  69. package/templates/default/site/client/search-page.tsx +1 -0
  70. package/templates/default/site/content/404.md +8 -0
  71. package/templates/default/site/content/about.md +10 -0
  72. package/templates/default/site/content/index.md +10 -0
  73. package/templates/default/site/icons/file.svg +1 -0
  74. package/templates/default/site/icons/home.svg +1 -0
  75. package/templates/default/site/icons/info.svg +1 -0
  76. package/templates/default/site/public/_idcmd/live-reload.js +18 -0
  77. package/templates/default/site/public/_idcmd/llm-menu.js +153 -0
  78. package/templates/default/site/public/_idcmd/nav-prefetch.js +30 -0
  79. package/templates/default/site/public/_idcmd/right-rail-scrollspy.js +262 -0
  80. package/templates/default/site/public/anthropic-white.svg +16 -0
  81. package/templates/default/site/public/favicon.svg +13 -0
  82. package/templates/default/site/public/openai-white.svg +15 -0
  83. package/templates/default/site/server/routes/api/hello.ts +2 -0
  84. package/templates/default/site/server/server.ts +4 -0
  85. package/templates/default/site/site.jsonc +21 -0
  86. package/templates/default/site/styles/tailwind.css +452 -0
  87. package/templates/default/tsconfig.json +23 -0
  88. package/templates/default/vercel.json +7 -0
  89. package/index.js +0 -2
@@ -0,0 +1,10 @@
1
+ import { getMarkdownFilePath } from "./paths";
2
+
3
+ export const getMarkdownFile = async (slug: string): Promise<string | null> => {
4
+ const filePath = await getMarkdownFilePath(slug);
5
+ const file = Bun.file(filePath);
6
+ if (await file.exists()) {
7
+ return file.text();
8
+ }
9
+ return null;
10
+ };
@@ -0,0 +1,86 @@
1
+ const DEFAULT_SITE_DIR = "site";
2
+ const DEFAULT_DIST_DIR = "dist";
3
+ const ASSET_PREFIX = "/_idcmd" as const;
4
+
5
+ export interface ProjectPaths {
6
+ assetPrefix: typeof ASSET_PREFIX;
7
+ contentDir: string;
8
+ distDir: string;
9
+ iconsDir: string;
10
+ publicDir: string;
11
+ routesDir: string;
12
+ siteConfigPath: string;
13
+ siteDir: string | null;
14
+ }
15
+
16
+ export interface ResolveProjectPathsOptions {
17
+ cwd?: string;
18
+ distDir?: string;
19
+ siteDir?: string;
20
+ }
21
+
22
+ const trimTrailingSlash = (value: string): string =>
23
+ value.endsWith("/") ? value.replaceAll(/\/+$/g, "") : value;
24
+
25
+ const joinPath = (...parts: string[]): string => {
26
+ const filtered = parts.filter((p) => p.length > 0);
27
+ if (filtered.length === 0) {
28
+ return ".";
29
+ }
30
+
31
+ const normalized = filtered.map((p, idx) => {
32
+ const trimmed =
33
+ idx === 0 ? trimTrailingSlash(p) : p.replaceAll(/^\/+/g, "");
34
+ return trimTrailingSlash(trimmed);
35
+ });
36
+
37
+ return normalized.join("/");
38
+ };
39
+
40
+ const hasNewLayout = (cwd: string, siteDirName: string): Promise<boolean> =>
41
+ Bun.file(joinPath(cwd, siteDirName, "site.jsonc")).exists();
42
+
43
+ const buildPaths = (
44
+ cwd: string,
45
+ rootDir: string,
46
+ distDirName: string
47
+ ): ProjectPaths => {
48
+ const distDir = joinPath(cwd, distDirName);
49
+ const contentDir = joinPath(rootDir, "content");
50
+ const publicDir = joinPath(rootDir, "public");
51
+ const iconsDir = joinPath(rootDir, "icons");
52
+ const routesDir = joinPath(rootDir, "server", "routes");
53
+ const siteConfigPath = joinPath(rootDir, "site.jsonc");
54
+
55
+ const siteDir = rootDir === cwd ? null : rootDir;
56
+
57
+ return {
58
+ assetPrefix: ASSET_PREFIX,
59
+ contentDir,
60
+ distDir,
61
+ iconsDir,
62
+ publicDir,
63
+ routesDir,
64
+ siteConfigPath,
65
+ siteDir,
66
+ };
67
+ };
68
+
69
+ export const resolveProjectPaths = async (
70
+ options: ResolveProjectPathsOptions = {}
71
+ ): Promise<ProjectPaths> => {
72
+ const cwd = trimTrailingSlash(options.cwd ?? process.cwd());
73
+ const distDirName = options.distDir ?? DEFAULT_DIST_DIR;
74
+ const siteDirName = options.siteDir ?? DEFAULT_SITE_DIR;
75
+
76
+ const siteRoot = joinPath(cwd, siteDirName);
77
+ const rootDir = (await hasNewLayout(cwd, siteDirName)) ? siteRoot : cwd;
78
+ return buildPaths(cwd, rootDir, distDirName);
79
+ };
80
+
81
+ let cached: Promise<ProjectPaths> | null = null;
82
+
83
+ export const getProjectPaths = (): Promise<ProjectPaths> => {
84
+ cached ??= resolveProjectPaths();
85
+ return cached;
86
+ };
@@ -0,0 +1,46 @@
1
+ import type { LayoutProps } from "./layout";
2
+
3
+ import { renderLayout as defaultRenderLayout } from "./layout";
4
+
5
+ export type RenderLayout = (props: LayoutProps) => string;
6
+
7
+ const USER_LAYOUT_PATH = "site/client/layout.tsx";
8
+
9
+ const loadUserLayout = async (
10
+ filePath: string
11
+ ): Promise<RenderLayout | null> => {
12
+ try {
13
+ const url = Bun.pathToFileURL(filePath);
14
+ const mod = (await import(url.toString())) as unknown as {
15
+ renderLayout?: RenderLayout;
16
+ };
17
+ return mod.renderLayout ?? null;
18
+ } catch (error) {
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ console.warn(
21
+ `[layout] Failed to load user layout from ${filePath}: ${message}`
22
+ );
23
+ return null;
24
+ }
25
+ };
26
+
27
+ const tryLoadUserRenderLayout = async (): Promise<RenderLayout | null> => {
28
+ const exists = await Bun.file(USER_LAYOUT_PATH).exists();
29
+ if (!exists) {
30
+ return null;
31
+ }
32
+ return loadUserLayout(USER_LAYOUT_PATH);
33
+ };
34
+
35
+ let cached: RenderLayout | null = null;
36
+ let attempted = false;
37
+
38
+ export const getRenderLayout = async (): Promise<RenderLayout> => {
39
+ if (attempted) {
40
+ return cached ?? defaultRenderLayout;
41
+ }
42
+
43
+ attempted = true;
44
+ cached = await tryLoadUserRenderLayout();
45
+ return cached ?? defaultRenderLayout;
46
+ };
@@ -0,0 +1,340 @@
1
+ /* eslint-disable react/no-danger */
2
+ import type { JSX } from "preact";
3
+
4
+ import { render } from "preact-render-to-string";
5
+
6
+ import type { NavGroup, NavItem } from "@/content/navigation";
7
+ import type { ResolvedRightRailConfig } from "@/site/config";
8
+
9
+ import type { TocItem } from "./toc";
10
+
11
+ import { RightRail } from "./right-rail";
12
+
13
+ export interface LayoutProps {
14
+ title: string;
15
+ siteName: string;
16
+ description?: string;
17
+ canonicalUrl?: string;
18
+ content: string;
19
+ cssPath?: string;
20
+ inlineCss?: string;
21
+ currentPath: string;
22
+ navigation: NavGroup[];
23
+ scriptPaths?: string[];
24
+ searchQuery?: string;
25
+ showRightRail?: boolean;
26
+ rightRail: ResolvedRightRailConfig;
27
+ tocItems: TocItem[];
28
+ }
29
+
30
+ const Icon = ({ svg }: { svg: string }): JSX.Element => (
31
+ <span
32
+ class="inline-flex w-[18px] h-[18px]"
33
+ dangerouslySetInnerHTML={{ __html: svg }}
34
+ />
35
+ );
36
+
37
+ const isActiveLink = (item: NavItem, currentPath: string): boolean =>
38
+ currentPath === item.href ||
39
+ (item.href !== "/" && currentPath.startsWith(item.href));
40
+
41
+ const NavLink = ({
42
+ item,
43
+ currentPath,
44
+ }: {
45
+ item: NavItem;
46
+ currentPath: string;
47
+ }): JSX.Element => {
48
+ const activeClass = isActiveLink(item, currentPath)
49
+ ? "border-l-2 border-sidebar-primary font-medium text-sidebar-foreground"
50
+ : "border-l-2 border-transparent";
51
+
52
+ return (
53
+ <a
54
+ href={item.href}
55
+ data-prefetch="hover"
56
+ class={`flex items-center gap-3 px-3 py-1.5 text-sm hover:text-sidebar-foreground transition-colors ${activeClass}`}
57
+ >
58
+ <Icon svg={item.iconSvg} />
59
+ <span>{item.title}</span>
60
+ </a>
61
+ );
62
+ };
63
+
64
+ const NavGroupComponent = ({
65
+ group,
66
+ currentPath,
67
+ }: {
68
+ group: NavGroup;
69
+ currentPath: string;
70
+ }): JSX.Element => (
71
+ <div class="py-2">
72
+ <div class="px-3 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
73
+ {group.label}
74
+ </div>
75
+ <nav class="space-y-1">
76
+ {group.items.map((item) => (
77
+ <NavLink key={item.href} item={item} currentPath={currentPath} />
78
+ ))}
79
+ </nav>
80
+ </div>
81
+ );
82
+
83
+ const Sidebar = ({
84
+ siteName,
85
+ navigation,
86
+ currentPath,
87
+ }: {
88
+ siteName: string;
89
+ navigation: NavGroup[];
90
+ currentPath: string;
91
+ }): JSX.Element => (
92
+ <aside class="sidebar">
93
+ <div class="sidebar-header">
94
+ <a
95
+ href="/"
96
+ class="text-sm font-medium tracking-tight"
97
+ data-prefetch="hover"
98
+ >
99
+ <span class="text-muted-foreground">~/</span>
100
+ {siteName}
101
+ </a>
102
+ </div>
103
+ <div class="sidebar-content">
104
+ {navigation.map((group) => (
105
+ <NavGroupComponent
106
+ key={group.id}
107
+ group={group}
108
+ currentPath={currentPath}
109
+ />
110
+ ))}
111
+ </div>
112
+ </aside>
113
+ );
114
+
115
+ const SearchForm = ({ query }: { query?: string }): JSX.Element => (
116
+ <form
117
+ method="get"
118
+ action="/search/"
119
+ class="flex w-full items-center"
120
+ role="search"
121
+ noValidate
122
+ >
123
+ <label htmlFor="site-search" class="sr-only">
124
+ Search pages
125
+ </label>
126
+ <input
127
+ id="site-search"
128
+ name="q"
129
+ type="search"
130
+ autoComplete="off"
131
+ spellcheck={false}
132
+ placeholder="Search..."
133
+ defaultValue={query ?? ""}
134
+ class="w-full border-b border-input bg-transparent px-1 py-1.5 text-sm placeholder:text-muted-foreground focus:border-foreground focus:outline-none transition-colors"
135
+ />
136
+ </form>
137
+ );
138
+
139
+ const TopNavbar = ({
140
+ query,
141
+ siteName,
142
+ }: {
143
+ query?: string;
144
+ siteName: string;
145
+ }): JSX.Element => (
146
+ <header class="sticky top-0 z-30 border-b border-border bg-background/80 backdrop-blur-sm">
147
+ <div class="mx-auto max-w-6xl px-8 py-3">
148
+ <div class="flex items-center gap-4">
149
+ <a
150
+ href="/"
151
+ class="text-sm font-medium tracking-tight font-mono md:hidden"
152
+ data-prefetch="hover"
153
+ >
154
+ <span class="text-muted-foreground">~/</span>
155
+ {siteName}
156
+ </a>
157
+ <div class="not-prose w-full max-w-xs ml-auto">
158
+ <SearchForm query={query} />
159
+ </div>
160
+ </div>
161
+ </div>
162
+ </header>
163
+ );
164
+
165
+ interface DocumentHeadProps {
166
+ canonicalUrl?: string;
167
+ description?: string;
168
+ inlineCss?: string;
169
+ resolvedCssPath?: string;
170
+ title: string;
171
+ }
172
+
173
+ const DocumentHead = ({
174
+ canonicalUrl,
175
+ description,
176
+ inlineCss,
177
+ resolvedCssPath,
178
+ title,
179
+ }: DocumentHeadProps): JSX.Element => (
180
+ <head>
181
+ <meta charset="utf-8" />
182
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
183
+ <title>{title}</title>
184
+ {description ? <meta name="description" content={description} /> : null}
185
+ {canonicalUrl ? <link rel="canonical" href={canonicalUrl} /> : null}
186
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
187
+ <link
188
+ rel="preconnect"
189
+ href="https://fonts.gstatic.com"
190
+ crossOrigin="anonymous"
191
+ />
192
+ <link
193
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap"
194
+ rel="stylesheet"
195
+ />
196
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
197
+ {inlineCss ? <style>{inlineCss}</style> : null}
198
+ {resolvedCssPath ? <link rel="stylesheet" href={resolvedCssPath} /> : null}
199
+ </head>
200
+ );
201
+
202
+ const buildHtmlClass = (smoothScroll: boolean): string =>
203
+ smoothScroll ? "dark smooth-scroll" : "dark";
204
+
205
+ interface ScrollSpyDataset {
206
+ scrollspy?: string;
207
+ scrollspyCenter?: string;
208
+ scrollspyUpdateHash?: string;
209
+ }
210
+
211
+ const buildScrollSpyDataset = (
212
+ isScrollSpyEnabled: boolean,
213
+ rightRail: ResolvedRightRailConfig
214
+ ): ScrollSpyDataset =>
215
+ isScrollSpyEnabled
216
+ ? {
217
+ scrollspy: "1",
218
+ scrollspyCenter: rightRail.scrollSpy.centerActiveItem ? "1" : undefined,
219
+ scrollspyUpdateHash: rightRail.scrollSpy.updateHash,
220
+ }
221
+ : {};
222
+
223
+ interface DocumentBodyProps {
224
+ canonicalUrl?: string;
225
+ content: string;
226
+ currentPath: string;
227
+ navigation: NavGroup[];
228
+ rightRail: ResolvedRightRailConfig;
229
+ scriptPaths: string[];
230
+ scrollSpyDataset: ScrollSpyDataset;
231
+ searchQuery?: string;
232
+ shouldShowRightRail: boolean;
233
+ siteName: string;
234
+ tocItems: TocItem[];
235
+ }
236
+
237
+ const DocumentBody = ({
238
+ canonicalUrl,
239
+ content,
240
+ currentPath,
241
+ navigation,
242
+ rightRail,
243
+ scriptPaths,
244
+ scrollSpyDataset,
245
+ searchQuery,
246
+ shouldShowRightRail,
247
+ siteName,
248
+ tocItems,
249
+ }: DocumentBodyProps): JSX.Element => (
250
+ <body
251
+ class="bg-background text-foreground font-sans"
252
+ data-scrollspy={scrollSpyDataset.scrollspy}
253
+ data-scrollspy-center={scrollSpyDataset.scrollspyCenter}
254
+ data-scrollspy-update-hash={scrollSpyDataset.scrollspyUpdateHash}
255
+ >
256
+ <Sidebar
257
+ siteName={siteName}
258
+ navigation={navigation}
259
+ currentPath={currentPath}
260
+ />
261
+ <div class="main-wrapper">
262
+ <TopNavbar query={searchQuery} siteName={siteName} />
263
+ <main class="main-content">
264
+ <div class="mx-auto flex w-full max-w-6xl items-start gap-10">
265
+ <article
266
+ class={`prose min-w-0 flex-1${currentPath === "/" ? " prose-home" : ""}`}
267
+ dangerouslySetInnerHTML={{ __html: content }}
268
+ />
269
+ {shouldShowRightRail ? (
270
+ <RightRail
271
+ canonicalUrl={canonicalUrl}
272
+ currentPath={currentPath}
273
+ tocItems={tocItems}
274
+ rightRailConfig={rightRail}
275
+ />
276
+ ) : null}
277
+ </div>
278
+ </main>
279
+ <footer class="site-footer">
280
+ Built with Preact SSR + Tailwind &nbsp;|&nbsp; Zero JavaScript on
281
+ content pages
282
+ </footer>
283
+ </div>
284
+ {scriptPaths.map((scriptPath) => (
285
+ <script key={scriptPath} defer src={scriptPath} />
286
+ ))}
287
+ </body>
288
+ );
289
+
290
+ const Layout = ({
291
+ title,
292
+ siteName,
293
+ description,
294
+ canonicalUrl,
295
+ content,
296
+ cssPath,
297
+ inlineCss,
298
+ currentPath,
299
+ navigation,
300
+ scriptPaths = [],
301
+ searchQuery,
302
+ showRightRail = true,
303
+ rightRail,
304
+ tocItems,
305
+ }: LayoutProps): JSX.Element => {
306
+ const resolvedCssPath = inlineCss ? undefined : (cssPath ?? "/styles.css");
307
+ const shouldShowRightRail = showRightRail && rightRail.enabled;
308
+ const isScrollSpyEnabled =
309
+ shouldShowRightRail && rightRail.scrollSpy.enabled && tocItems.length > 0;
310
+ const htmlClass = buildHtmlClass(rightRail.smoothScroll);
311
+ const scrollSpyDataset = buildScrollSpyDataset(isScrollSpyEnabled, rightRail);
312
+
313
+ return (
314
+ <html lang="en" class={htmlClass}>
315
+ <DocumentHead
316
+ canonicalUrl={canonicalUrl}
317
+ description={description}
318
+ inlineCss={inlineCss}
319
+ resolvedCssPath={resolvedCssPath}
320
+ title={title}
321
+ />
322
+ <DocumentBody
323
+ canonicalUrl={canonicalUrl}
324
+ content={content}
325
+ currentPath={currentPath}
326
+ navigation={navigation}
327
+ rightRail={rightRail}
328
+ scriptPaths={scriptPaths}
329
+ scrollSpyDataset={scrollSpyDataset}
330
+ searchQuery={searchQuery}
331
+ shouldShowRightRail={shouldShowRightRail}
332
+ siteName={siteName}
333
+ tocItems={tocItems}
334
+ />
335
+ </html>
336
+ );
337
+ };
338
+
339
+ export const renderLayout = (props: LayoutProps): string =>
340
+ `<!DOCTYPE html>${render(<Layout {...props} />)}`;
@@ -0,0 +1,14 @@
1
+ const MARKDOWN_OPTIONS = {
2
+ autolinks: true,
3
+ hardSoftBreaks: true,
4
+ headings: true,
5
+ latexMath: true,
6
+ strikethrough: true,
7
+ tables: true,
8
+ tasklists: true,
9
+ underline: true,
10
+ wikiLinks: true,
11
+ } satisfies Bun.markdown.Options;
12
+
13
+ export const renderMarkdownToHtml = (markdown: string): string =>
14
+ Bun.markdown.html(markdown, MARKDOWN_OPTIONS);