@sntlr/registry-shell 1.0.0

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 (134) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -0
  3. package/dist/adapter/custom.d.ts +47 -0
  4. package/dist/adapter/custom.js +53 -0
  5. package/dist/adapter/custom.js.map +1 -0
  6. package/dist/adapter/default.d.ts +40 -0
  7. package/dist/adapter/default.js +202 -0
  8. package/dist/adapter/default.js.map +1 -0
  9. package/dist/cli/build.d.ts +1 -0
  10. package/dist/cli/build.js +31 -0
  11. package/dist/cli/build.js.map +1 -0
  12. package/dist/cli/dev.d.ts +1 -0
  13. package/dist/cli/dev.js +26 -0
  14. package/dist/cli/dev.js.map +1 -0
  15. package/dist/cli/index.d.ts +12 -0
  16. package/dist/cli/index.js +49 -0
  17. package/dist/cli/index.js.map +1 -0
  18. package/dist/cli/init.d.ts +1 -0
  19. package/dist/cli/init.js +70 -0
  20. package/dist/cli/init.js.map +1 -0
  21. package/dist/cli/shared.d.ts +33 -0
  22. package/dist/cli/shared.js +278 -0
  23. package/dist/cli/shared.js.map +1 -0
  24. package/dist/cli/start.d.ts +1 -0
  25. package/dist/cli/start.js +24 -0
  26. package/dist/cli/start.js.map +1 -0
  27. package/dist/config-loader.d.ts +49 -0
  28. package/dist/config-loader.js +140 -0
  29. package/dist/define-config.d.ts +188 -0
  30. package/dist/define-config.js +21 -0
  31. package/dist/index.d.ts +11 -0
  32. package/dist/index.js +9 -0
  33. package/package.json +124 -0
  34. package/src/adapter/custom.ts +90 -0
  35. package/src/adapter/default.ts +241 -0
  36. package/src/cli/build.ts +38 -0
  37. package/src/cli/dev.ts +38 -0
  38. package/src/cli/index.ts +52 -0
  39. package/src/cli/init.ts +76 -0
  40. package/src/cli/shared.ts +306 -0
  41. package/src/cli/start.ts +28 -0
  42. package/src/config-loader.ts +190 -0
  43. package/src/define-config.ts +206 -0
  44. package/src/index.ts +17 -0
  45. package/src/next-app/app/[...asset]/route.ts +81 -0
  46. package/src/next-app/app/_user-global.css +6 -0
  47. package/src/next-app/app/_user-sources.css +9 -0
  48. package/src/next-app/app/a11y/[name]/route.ts +19 -0
  49. package/src/next-app/app/api/search-index/route.ts +19 -0
  50. package/src/next-app/app/components/[name]/page.tsx +61 -0
  51. package/src/next-app/app/components/layout.tsx +18 -0
  52. package/src/next-app/app/docs/[slug]/page.tsx +53 -0
  53. package/src/next-app/app/docs/layout.tsx +18 -0
  54. package/src/next-app/app/globals.css +329 -0
  55. package/src/next-app/app/layout.tsx +102 -0
  56. package/src/next-app/app/page.tsx +9 -0
  57. package/src/next-app/app/preview-snapshot/[name]/page.tsx +20 -0
  58. package/src/next-app/app/preview-snapshot/layout.tsx +17 -0
  59. package/src/next-app/app/props/[name]/route.ts +19 -0
  60. package/src/next-app/app/r/[name]/route.ts +14 -0
  61. package/src/next-app/app/tests/[name]/route.ts +19 -0
  62. package/src/next-app/components/a11y-info.tsx +287 -0
  63. package/src/next-app/components/a11y-provider.tsx +39 -0
  64. package/src/next-app/components/component-breadcrumb.tsx +55 -0
  65. package/src/next-app/components/component-icon.tsx +140 -0
  66. package/src/next-app/components/component-preview.tsx +13 -0
  67. package/src/next-app/components/component-tabs.tsx +209 -0
  68. package/src/next-app/components/docs-toc.tsx +86 -0
  69. package/src/next-app/components/global-mobile-sidebar.tsx +35 -0
  70. package/src/next-app/components/header.tsx +188 -0
  71. package/src/next-app/components/heading-anchor.tsx +52 -0
  72. package/src/next-app/components/homepage-demo.tsx +180 -0
  73. package/src/next-app/components/locale-toggle.tsx +35 -0
  74. package/src/next-app/components/localized-mdx-client.tsx +14 -0
  75. package/src/next-app/components/localized-mdx.tsx +27 -0
  76. package/src/next-app/components/mobile-sidebar.tsx +22 -0
  77. package/src/next-app/components/nav-data-provider.tsx +37 -0
  78. package/src/next-app/components/navigation-progress.tsx +62 -0
  79. package/src/next-app/components/preview-canvas.tsx +368 -0
  80. package/src/next-app/components/preview-controls.tsx +94 -0
  81. package/src/next-app/components/preview-layout.tsx +218 -0
  82. package/src/next-app/components/props-table.tsx +134 -0
  83. package/src/next-app/components/resizable-preview.tsx +101 -0
  84. package/src/next-app/components/search.tsx +177 -0
  85. package/src/next-app/components/settings-modal.tsx +98 -0
  86. package/src/next-app/components/shell-ui/accordion.tsx +70 -0
  87. package/src/next-app/components/shell-ui/backdrop.tsx +29 -0
  88. package/src/next-app/components/shell-ui/badge.tsx +55 -0
  89. package/src/next-app/components/shell-ui/breadcrumb.tsx +120 -0
  90. package/src/next-app/components/shell-ui/button.tsx +64 -0
  91. package/src/next-app/components/shell-ui/card.tsx +127 -0
  92. package/src/next-app/components/shell-ui/checkbox.tsx +33 -0
  93. package/src/next-app/components/shell-ui/dialog.tsx +171 -0
  94. package/src/next-app/components/shell-ui/empty-state.tsx +66 -0
  95. package/src/next-app/components/shell-ui/input.tsx +27 -0
  96. package/src/next-app/components/shell-ui/kbd.tsx +30 -0
  97. package/src/next-app/components/shell-ui/label.tsx +25 -0
  98. package/src/next-app/components/shell-ui/select.tsx +204 -0
  99. package/src/next-app/components/shell-ui/separator.tsx +32 -0
  100. package/src/next-app/components/shell-ui/skeleton.tsx +18 -0
  101. package/src/next-app/components/shell-ui/table.tsx +124 -0
  102. package/src/next-app/components/shell-ui/tabs.tsx +102 -0
  103. package/src/next-app/components/shell-ui/toggle.tsx +56 -0
  104. package/src/next-app/components/sidebar-layout.tsx +37 -0
  105. package/src/next-app/components/sidebar-provider.tsx +75 -0
  106. package/src/next-app/components/sidebar.tsx +222 -0
  107. package/src/next-app/components/snapshot-preview.tsx +28 -0
  108. package/src/next-app/components/test-info.tsx +155 -0
  109. package/src/next-app/components/theme-provider.tsx +16 -0
  110. package/src/next-app/components/theme-toggle.tsx +21 -0
  111. package/src/next-app/components/translated-text.tsx +8 -0
  112. package/src/next-app/fallback/homepage.tsx +112 -0
  113. package/src/next-app/fallback/previews.ts +17 -0
  114. package/src/next-app/hooks/use-active-section.ts +23 -0
  115. package/src/next-app/hooks/use-controls.ts +72 -0
  116. package/src/next-app/hooks/use-mobile.ts +19 -0
  117. package/src/next-app/lib/branding.ts +52 -0
  118. package/src/next-app/lib/components-nav.ts +8 -0
  119. package/src/next-app/lib/docs.ts +16 -0
  120. package/src/next-app/lib/github.ts +38 -0
  121. package/src/next-app/lib/i18n.tsx +630 -0
  122. package/src/next-app/lib/locales.ts +17 -0
  123. package/src/next-app/lib/preview-loader.ts +7 -0
  124. package/src/next-app/lib/registry-adapter.ts +199 -0
  125. package/src/next-app/lib/utils.ts +6 -0
  126. package/src/next-app/next-env.d.ts +6 -0
  127. package/src/next-app/next.config.ts +101 -0
  128. package/src/next-app/postcss.config.mjs +7 -0
  129. package/src/next-app/public/favicon.ico +0 -0
  130. package/src/next-app/public/favicon_dark.svg +3 -0
  131. package/src/next-app/public/favicon_light.svg +3 -0
  132. package/src/next-app/registry.config.ts +50 -0
  133. package/src/next-app/tsconfig.json +29 -0
  134. package/src/next-app/user-aliases.d.ts +17 -0
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Default convention-based RegistryAdapter implementation.
3
+ *
4
+ * Builds a RegistryAdapter from a `ResolvedShellConfig`: scans the filesystem
5
+ * paths declared in the user's config, parses MDX frontmatter, and reads
6
+ * registry JSON. The `previewLoader` and `homePage` are wired separately via
7
+ * Next.js aliases (see next-app/next.config.ts) because Next's `dynamic()`
8
+ * needs string literal imports.
9
+ */
10
+ import "server-only"
11
+ import fs from "node:fs"
12
+ import path from "node:path"
13
+ import matter from "gray-matter"
14
+ import type { ResolvedShellConfig } from "../config-loader.js"
15
+
16
+ // The RegistryAdapter interface lives inside the Next app (next-app/lib).
17
+ // This file is compiled to dist/ separately and consumed at Next runtime,
18
+ // so we re-declare the types it needs here.
19
+
20
+ export interface ComponentMeta {
21
+ name: string
22
+ label: string
23
+ kind: "component" | "block"
24
+ }
25
+
26
+ export interface DocMeta {
27
+ slug: string
28
+ title: string
29
+ description: string
30
+ order: number
31
+ titles: Record<string, string>
32
+ }
33
+
34
+ export interface DocContent {
35
+ meta: Omit<DocMeta, "titles">
36
+ content: string
37
+ }
38
+
39
+ function titleCase(slug: string) {
40
+ return slug
41
+ .split("-")
42
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
43
+ .join(" ")
44
+ }
45
+
46
+ export function createDefaultAdapter(resolved: ResolvedShellConfig) {
47
+ const { paths } = resolved
48
+ const isMulti = resolved.multilocale
49
+ const defaultLocale = resolved.defaultLocale
50
+
51
+ /** In multilocale mode, return all locale subfolder names. */
52
+ function listLocales(): string[] {
53
+ if (!isMulti || !fs.existsSync(paths.docs)) return []
54
+ return fs
55
+ .readdirSync(paths.docs, { withFileTypes: true })
56
+ .filter((d) => d.isDirectory())
57
+ .map((d) => d.name)
58
+ }
59
+
60
+ /** Path to the directory holding `.mdx` files for a given locale (multi mode). */
61
+ function localeDir(locale: string): string {
62
+ return path.join(paths.docs, locale)
63
+ }
64
+
65
+ function getAllComponents(): ComponentMeta[] {
66
+ const items: ComponentMeta[] = []
67
+
68
+ if (fs.existsSync(paths.components)) {
69
+ for (const filename of fs.readdirSync(paths.components).filter((f) => f.endsWith(".tsx"))) {
70
+ const name = filename.replace(/\.tsx$/, "")
71
+ items.push({ name, label: titleCase(name), kind: "component" })
72
+ }
73
+ }
74
+
75
+ if (fs.existsSync(paths.blocks)) {
76
+ for (const dir of fs.readdirSync(paths.blocks, { withFileTypes: true })) {
77
+ if (!dir.isDirectory() || paths.skipBlocks.has(dir.name)) continue
78
+ items.push({ name: dir.name, label: titleCase(dir.name), kind: "block" })
79
+ }
80
+ }
81
+
82
+ return items.sort((a, b) => a.label.localeCompare(b.label))
83
+ }
84
+
85
+ function getAllDocs(): DocMeta[] {
86
+ if (!fs.existsSync(paths.docs)) return []
87
+
88
+ if (isMulti) {
89
+ const dir = localeDir(defaultLocale)
90
+ if (!fs.existsSync(dir)) return []
91
+ const otherLocales = listLocales().filter((l) => l !== defaultLocale)
92
+
93
+ return fs
94
+ .readdirSync(dir)
95
+ .filter((f) => f.endsWith(".mdx"))
96
+ .map((filename) => {
97
+ const slug = filename.replace(/\.mdx$/, "")
98
+ const { data } = matter(fs.readFileSync(path.join(dir, filename), "utf-8"))
99
+ const titles: Record<string, string> = { [defaultLocale]: data.title ?? slug }
100
+
101
+ for (const loc of otherLocales) {
102
+ const p = path.join(localeDir(loc), filename)
103
+ if (!fs.existsSync(p)) continue
104
+ const { data: locData } = matter(fs.readFileSync(p, "utf-8"))
105
+ if (locData.title) titles[loc] = locData.title
106
+ }
107
+
108
+ return {
109
+ slug,
110
+ title: data.title ?? slug,
111
+ description: data.description ?? "",
112
+ order: data.order ?? 999,
113
+ titles,
114
+ }
115
+ })
116
+ .sort((a, b) => a.order - b.order)
117
+ }
118
+
119
+ // Single-folder mode: one locale, no variant scanning.
120
+ return fs
121
+ .readdirSync(paths.docs)
122
+ .filter((f) => f.endsWith(".mdx"))
123
+ .map((filename) => {
124
+ const slug = filename.replace(/\.mdx$/, "")
125
+ const { data } = matter(fs.readFileSync(path.join(paths.docs, filename), "utf-8"))
126
+ return {
127
+ slug,
128
+ title: data.title ?? slug,
129
+ description: data.description ?? "",
130
+ order: data.order ?? 999,
131
+ titles: { en: data.title ?? slug },
132
+ }
133
+ })
134
+ .sort((a, b) => a.order - b.order)
135
+ }
136
+
137
+ function getDocBySlug(slug: string, locale?: string): DocContent | null {
138
+ if (isMulti) {
139
+ const want = locale && locale !== defaultLocale ? locale : defaultLocale
140
+ const candidates = [
141
+ path.join(localeDir(want), `${slug}.mdx`),
142
+ path.join(localeDir(defaultLocale), `${slug}.mdx`),
143
+ ]
144
+ return readDocFile(slug, candidates)
145
+ }
146
+
147
+ // Single-folder mode: `locale` is ignored, only `{slug}.mdx` matters.
148
+ return readDocFile(slug, [path.join(paths.docs, `${slug}.mdx`)])
149
+ }
150
+
151
+ function getDocAllLocales(slug: string): Record<string, string> {
152
+ if (!fs.existsSync(paths.docs)) return {}
153
+
154
+ if (isMulti) {
155
+ const out: Record<string, string> = {}
156
+ for (const loc of listLocales()) {
157
+ const p = path.join(localeDir(loc), `${slug}.mdx`)
158
+ if (fs.existsSync(p)) {
159
+ out[loc] = matter(fs.readFileSync(p, "utf-8")).content
160
+ }
161
+ }
162
+ return out
163
+ }
164
+
165
+ // Single-folder mode: exactly one file, keyed under `en` so the client
166
+ // renderer's fallback chain works without needing a special case.
167
+ const p = path.join(paths.docs, `${slug}.mdx`)
168
+ if (!fs.existsSync(p)) return {}
169
+ return { en: matter(fs.readFileSync(p, "utf-8")).content }
170
+ }
171
+
172
+ function getComponentSource(name: string): string | null {
173
+ const candidates = [
174
+ path.join(paths.components, `${name}.tsx`),
175
+ path.join(paths.blocks, name, `${name}.tsx`),
176
+ ]
177
+ for (const p of candidates) {
178
+ if (fs.existsSync(p)) return fs.readFileSync(p, "utf-8")
179
+ }
180
+ return null
181
+ }
182
+
183
+ async function getRegistryItem(name: string): Promise<unknown | null> {
184
+ return readJson(paths.registryJson, name)
185
+ }
186
+
187
+ async function getA11yData(name: string): Promise<unknown | null> {
188
+ return readJson(paths.a11y, name)
189
+ }
190
+
191
+ async function getTestData(name: string): Promise<unknown | null> {
192
+ return readJson(paths.tests, name)
193
+ }
194
+
195
+ async function getPropsData(name: string): Promise<unknown | null> {
196
+ return readJson(paths.props, name)
197
+ }
198
+
199
+ return {
200
+ getAllComponents,
201
+ getAllDocs,
202
+ getDocBySlug,
203
+ getDocAllLocales,
204
+ getComponentSource,
205
+ getRegistryItem,
206
+ getA11yData,
207
+ getTestData,
208
+ getPropsData,
209
+ branding: resolved.branding,
210
+ extraTranslations: resolved.extraTranslations,
211
+ }
212
+ }
213
+
214
+ async function readJson(dir: string, name: string): Promise<unknown | null> {
215
+ const base = name.replace(/\.json$/, "")
216
+ const filePath = path.join(dir, `${base}.json`)
217
+ try {
218
+ const data = await fs.promises.readFile(filePath, "utf-8")
219
+ return JSON.parse(data)
220
+ } catch {
221
+ return null
222
+ }
223
+ }
224
+
225
+ /** Try each candidate path; return the first that parses to a DocContent. */
226
+ function readDocFile(slug: string, candidates: string[]): DocContent | null {
227
+ for (const p of candidates) {
228
+ if (!fs.existsSync(p)) continue
229
+ const { data, content } = matter(fs.readFileSync(p, "utf-8"))
230
+ return {
231
+ meta: {
232
+ slug,
233
+ title: data.title ?? slug,
234
+ description: data.description ?? "",
235
+ order: data.order ?? 999,
236
+ },
237
+ content,
238
+ }
239
+ }
240
+ return null
241
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `registry-shell build` — run `next build` against the user's project.
3
+ *
4
+ * Writes the Next build output to `<user-project>/.next` via `distDir` so
5
+ * generic Next.js hosts (Vercel, self-hosted) find it where they expect.
6
+ * Without this, the output would land inside
7
+ * `node_modules/@sntlr/registry-shell/src/next-app/.next` and no external
8
+ * host could discover it.
9
+ */
10
+ import path from "node:path"
11
+ import { spawn } from "node:child_process"
12
+ import {
13
+ NEXT_BIN,
14
+ buildEnvVars,
15
+ clearStaleNextCacheIfModeChanged,
16
+ loadUserConfig,
17
+ nextAppDir,
18
+ writeUserSourcesCss,
19
+ } from "./shared.js"
20
+
21
+ export async function run(args: string[]): Promise<void> {
22
+ const loaded = loadUserConfig()
23
+ clearStaleNextCacheIfModeChanged(loaded)
24
+ writeUserSourcesCss(loaded)
25
+ const userDistDir = loaded
26
+ ? path.resolve(loaded.root, loaded.config.paths?.buildOutput ?? ".next")
27
+ : undefined
28
+ const env = {
29
+ ...process.env,
30
+ ...buildEnvVars(loaded),
31
+ ...(userDistDir ? { USER_DIST_DIR: userDistDir } : {}),
32
+ }
33
+ const child = spawn(process.execPath, [NEXT_BIN, "build", nextAppDir(), ...args], {
34
+ stdio: "inherit",
35
+ env,
36
+ })
37
+ child.on("exit", (code) => process.exit(code ?? 0))
38
+ }
package/src/cli/dev.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `registry-shell dev` — run `next dev` against the user's project.
3
+ */
4
+ import { spawn } from "node:child_process"
5
+ import {
6
+ NEXT_BIN,
7
+ buildEnvVars,
8
+ clearStaleNextCacheIfModeChanged,
9
+ loadUserConfig,
10
+ nextAppDir,
11
+ writeUserSourcesCss,
12
+ } from "./shared.js"
13
+
14
+ export async function run(args: string[]): Promise<void> {
15
+ const loaded = loadUserConfig()
16
+ if (loaded) {
17
+ console.log(`[registry-shell] Using config: ${loaded.configPath}`)
18
+ } else {
19
+ console.log(`[registry-shell] No registry-shell.config.ts found — running in shell-only mode.`)
20
+ }
21
+
22
+ clearStaleNextCacheIfModeChanged(loaded)
23
+ writeUserSourcesCss(loaded)
24
+ const env = { ...process.env, ...buildEnvVars(loaded) }
25
+ const portArgs = loaded?.config.port ? ["-p", String(loaded.config.port)] : []
26
+ // Webpack by default. Turbopack currently can't compile files reached via
27
+ // the `@user/*` cross-project aliases — it treats them as native Node ESM
28
+ // and crashes on `next/dynamic`. Set UI_SHELL_TURBOPACK=1 to opt in once
29
+ // Turbopack supports this (track https://github.com/vercel/next.js).
30
+ const turbopackArgs = process.env.UI_SHELL_TURBOPACK ? ["--turbopack"] : []
31
+ const child = spawn(
32
+ process.execPath,
33
+ [NEXT_BIN, "dev", nextAppDir(), ...turbopackArgs, ...portArgs, ...args],
34
+ { stdio: "inherit", env },
35
+ )
36
+
37
+ child.on("exit", (code) => process.exit(code ?? 0))
38
+ }
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @sntlr/registry-shell CLI entry.
4
+ *
5
+ * Commands:
6
+ * init Scaffold registry-shell.config.ts and add a script to package.json.
7
+ * dev Run `next dev` pointed at the user's current directory.
8
+ * build Run `next build` pointed at the user's current directory.
9
+ * start Run `next start` for a built shell.
10
+ */
11
+
12
+ const USAGE = `Usage: registry-shell <command> [args]
13
+
14
+ Commands:
15
+ init Scaffold registry-shell.config.ts in the current project.
16
+ dev Start the shell in dev mode on http://localhost:3000.
17
+ build Build the shell for production.
18
+ start Start the production server against a prior build.
19
+ `
20
+
21
+ async function main() {
22
+ const [, , cmd, ...args] = process.argv
23
+
24
+ if (!cmd || cmd === "--help" || cmd === "-h") {
25
+ console.log(USAGE)
26
+ process.exit(0)
27
+ }
28
+
29
+ switch (cmd) {
30
+ case "init":
31
+ await (await import("./init.js")).run(args)
32
+ break
33
+ case "dev":
34
+ await (await import("./dev.js")).run(args)
35
+ break
36
+ case "build":
37
+ await (await import("./build.js")).run(args)
38
+ break
39
+ case "start":
40
+ await (await import("./start.js")).run(args)
41
+ break
42
+ default:
43
+ console.error(`Unknown command: ${cmd}\n`)
44
+ console.log(USAGE)
45
+ process.exit(1)
46
+ }
47
+ }
48
+
49
+ main().catch((err) => {
50
+ console.error(err instanceof Error ? err.message : err)
51
+ process.exit(1)
52
+ })
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `registry-shell init` — scaffold `registry-shell.config.ts` in the current
3
+ * directory and add a `shell` script to the project's package.json.
4
+ */
5
+ import fs from "node:fs"
6
+ import path from "node:path"
7
+
8
+ const TEMPLATE = `import { defineConfig } from "@sntlr/registry-shell"
9
+
10
+ export default defineConfig({
11
+ branding: {
12
+ siteName: "My UI",
13
+ shortName: "UI",
14
+ // siteUrl: "https://ui.example.com",
15
+ // github: { owner: "my-org", repo: "my-ui" },
16
+ },
17
+
18
+ // All path overrides are optional — these are the defaults:
19
+ // paths: {
20
+ // components: "components/ui",
21
+ // blocks: "registry/new-york/blocks",
22
+ // previews: "components/previews/index.ts",
23
+ // docs: "content/docs",
24
+ // registryJson: "public/r",
25
+ // skipBlocks: [],
26
+ // // Optional: your own global CSS (brand fonts, token overrides,
27
+ // // extra @source directives). Imported after the shell's globals so
28
+ // // your :root { --primary: ... } wins the cascade.
29
+ // // globalCss: "./styles/theme.css",
30
+ // },
31
+
32
+ // Custom homepage component (optional):
33
+ // homePage: "./components/homepage",
34
+ })
35
+ `
36
+
37
+ export async function run(_args: string[]): Promise<void> {
38
+ const cwd = process.cwd()
39
+ const configPath = path.join(cwd, "registry-shell.config.ts")
40
+
41
+ if (fs.existsSync(configPath)) {
42
+ console.log(`[registry-shell] Config already exists: ${configPath}`)
43
+ } else {
44
+ fs.writeFileSync(configPath, TEMPLATE, "utf-8")
45
+ console.log(`[registry-shell] Wrote ${configPath}`)
46
+ }
47
+
48
+ // Add "shell" script to package.json if missing.
49
+ const pkgPath = path.join(cwd, "package.json")
50
+ if (fs.existsSync(pkgPath)) {
51
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as Record<string, unknown>
52
+ pkg.scripts = (pkg.scripts as Record<string, string> | undefined) ?? {}
53
+ const scripts = pkg.scripts as Record<string, string>
54
+ let changed = false
55
+ if (!scripts.shell) {
56
+ scripts.shell = "registry-shell dev"
57
+ changed = true
58
+ }
59
+ if (!scripts["shell:build"]) {
60
+ scripts["shell:build"] = "registry-shell build"
61
+ changed = true
62
+ }
63
+ if (changed) {
64
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8")
65
+ console.log(`[registry-shell] Added "shell" / "shell:build" scripts to package.json`)
66
+ }
67
+ } else {
68
+ console.log(
69
+ `[registry-shell] No package.json found — skipped script injection. Run \`npm init\` first.`,
70
+ )
71
+ }
72
+
73
+ console.log(
74
+ `\nNext steps:\n 1. Edit registry-shell.config.ts (branding at minimum)\n 2. Run: npm run shell`,
75
+ )
76
+ }