@multicomma/arkcn 0.1.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 (237) hide show
  1. package/README.md +78 -0
  2. package/bin/arkcn.mjs +78 -0
  3. package/cli/commands.mjs +251 -0
  4. package/cli/project.mjs +168 -0
  5. package/cli/registry.mjs +50 -0
  6. package/cli/ui.mjs +46 -0
  7. package/mcp/server.mjs +149 -0
  8. package/package.json +201 -0
  9. package/registry/base.css +680 -0
  10. package/registry/index.json +1833 -0
  11. package/registry/items/accordion.json +18 -0
  12. package/registry/items/activity-feed.json +18 -0
  13. package/registry/items/alert-dialog.json +18 -0
  14. package/registry/items/alert.json +17 -0
  15. package/registry/items/angle-slider.json +17 -0
  16. package/registry/items/app-shell.json +28 -0
  17. package/registry/items/aspect-ratio.json +13 -0
  18. package/registry/items/attachment.json +19 -0
  19. package/registry/items/avatar.json +17 -0
  20. package/registry/items/badge.json +18 -0
  21. package/registry/items/breadcrumb.json +18 -0
  22. package/registry/items/bubble.json +18 -0
  23. package/registry/items/button-group.json +19 -0
  24. package/registry/items/button.json +18 -0
  25. package/registry/items/calendar.json +19 -0
  26. package/registry/items/canvas.json +24 -0
  27. package/registry/items/card.json +15 -0
  28. package/registry/items/carousel.json +19 -0
  29. package/registry/items/cascader.json +23 -0
  30. package/registry/items/chart.json +17 -0
  31. package/registry/items/chat.json +23 -0
  32. package/registry/items/checkbox.json +18 -0
  33. package/registry/items/clipboard.json +19 -0
  34. package/registry/items/collapsible.json +15 -0
  35. package/registry/items/color-picker.json +19 -0
  36. package/registry/items/combobox.json +20 -0
  37. package/registry/items/command.json +21 -0
  38. package/registry/items/comment-thread.json +25 -0
  39. package/registry/items/context-menu.json +18 -0
  40. package/registry/items/controllable.json +14 -0
  41. package/registry/items/data-grid.json +24 -0
  42. package/registry/items/data-table-adapter.json +14 -0
  43. package/registry/items/data-table.json +32 -0
  44. package/registry/items/date-input.json +17 -0
  45. package/registry/items/dialog.json +19 -0
  46. package/registry/items/direction.json +13 -0
  47. package/registry/items/drag-preview.json +16 -0
  48. package/registry/items/drawer.json +18 -0
  49. package/registry/items/dropdown-menu.json +18 -0
  50. package/registry/items/editable.json +17 -0
  51. package/registry/items/empty.json +17 -0
  52. package/registry/items/field.json +19 -0
  53. package/registry/items/file-upload.json +19 -0
  54. package/registry/items/floating-panel.json +19 -0
  55. package/registry/items/floating-toolbar.json +16 -0
  56. package/registry/items/form.json +14 -0
  57. package/registry/items/format.json +15 -0
  58. package/registry/items/gantt.json +22 -0
  59. package/registry/items/history.json +14 -0
  60. package/registry/items/hotkeys.json +18 -0
  61. package/registry/items/hover-card.json +17 -0
  62. package/registry/items/image-cropper.json +17 -0
  63. package/registry/items/input-group.json +20 -0
  64. package/registry/items/input-otp.json +18 -0
  65. package/registry/items/input.json +15 -0
  66. package/registry/items/item.json +19 -0
  67. package/registry/items/json-tree-view.json +17 -0
  68. package/registry/items/kanban.json +26 -0
  69. package/registry/items/kbd.json +15 -0
  70. package/registry/items/label.json +15 -0
  71. package/registry/items/live-region.json +16 -0
  72. package/registry/items/marker.json +18 -0
  73. package/registry/items/marquee.json +17 -0
  74. package/registry/items/menubar.json +19 -0
  75. package/registry/items/message.json +15 -0
  76. package/registry/items/navigation-menu.json +19 -0
  77. package/registry/items/node-graph.json +22 -0
  78. package/registry/items/number-input.json +18 -0
  79. package/registry/items/pagination.json +18 -0
  80. package/registry/items/password-input.json +18 -0
  81. package/registry/items/popconfirm.json +20 -0
  82. package/registry/items/popover.json +18 -0
  83. package/registry/items/progress.json +17 -0
  84. package/registry/items/qr-code.json +17 -0
  85. package/registry/items/query-builder.json +23 -0
  86. package/registry/items/radio-group.json +17 -0
  87. package/registry/items/rating-group.json +18 -0
  88. package/registry/items/resizable.json +17 -0
  89. package/registry/items/rich-text-editor.json +37 -0
  90. package/registry/items/row-layout.json +15 -0
  91. package/registry/items/scheduler.json +23 -0
  92. package/registry/items/scroll-area.json +17 -0
  93. package/registry/items/segment-group.json +17 -0
  94. package/registry/items/select.json +19 -0
  95. package/registry/items/separator.json +15 -0
  96. package/registry/items/sheet.json +19 -0
  97. package/registry/items/sidebar.json +27 -0
  98. package/registry/items/signature-pad.json +19 -0
  99. package/registry/items/skeleton.json +15 -0
  100. package/registry/items/slider.json +17 -0
  101. package/registry/items/sonner.json +17 -0
  102. package/registry/items/spinner.json +17 -0
  103. package/registry/items/steps.json +19 -0
  104. package/registry/items/swap.json +17 -0
  105. package/registry/items/switch.json +17 -0
  106. package/registry/items/table.json +16 -0
  107. package/registry/items/tabs.json +18 -0
  108. package/registry/items/tags-input.json +18 -0
  109. package/registry/items/textarea.json +15 -0
  110. package/registry/items/time.json +14 -0
  111. package/registry/items/timer.json +18 -0
  112. package/registry/items/toc.json +17 -0
  113. package/registry/items/toggle-group.json +19 -0
  114. package/registry/items/toggle.json +18 -0
  115. package/registry/items/tooltip.json +17 -0
  116. package/registry/items/tour.json +19 -0
  117. package/registry/items/transfer-list.json +24 -0
  118. package/registry/items/tree-select.json +24 -0
  119. package/registry/items/tree-view.json +18 -0
  120. package/registry/items/use-mobile.json +13 -0
  121. package/registry/items/utils.json +17 -0
  122. package/registry/items/virtual-list.json +16 -0
  123. package/src/components/ui/accordion.tsx +111 -0
  124. package/src/components/ui/activity-feed.tsx +228 -0
  125. package/src/components/ui/alert-dialog.tsx +204 -0
  126. package/src/components/ui/alert.tsx +55 -0
  127. package/src/components/ui/angle-slider.tsx +117 -0
  128. package/src/components/ui/app-shell.tsx +985 -0
  129. package/src/components/ui/aspect-ratio.tsx +35 -0
  130. package/src/components/ui/attachment.tsx +193 -0
  131. package/src/components/ui/avatar.tsx +94 -0
  132. package/src/components/ui/badge.tsx +45 -0
  133. package/src/components/ui/breadcrumb.tsx +93 -0
  134. package/src/components/ui/bubble.tsx +119 -0
  135. package/src/components/ui/button-group.tsx +79 -0
  136. package/src/components/ui/button.tsx +65 -0
  137. package/src/components/ui/calendar.tsx +172 -0
  138. package/src/components/ui/canvas.tsx +723 -0
  139. package/src/components/ui/card.tsx +69 -0
  140. package/src/components/ui/carousel.tsx +228 -0
  141. package/src/components/ui/cascader.tsx +754 -0
  142. package/src/components/ui/chart.tsx +303 -0
  143. package/src/components/ui/chat.tsx +544 -0
  144. package/src/components/ui/checkbox.tsx +94 -0
  145. package/src/components/ui/clipboard.tsx +94 -0
  146. package/src/components/ui/collapsible.tsx +32 -0
  147. package/src/components/ui/color-picker.tsx +441 -0
  148. package/src/components/ui/combobox.tsx +400 -0
  149. package/src/components/ui/command.tsx +292 -0
  150. package/src/components/ui/comment-thread.tsx +923 -0
  151. package/src/components/ui/context-menu.tsx +331 -0
  152. package/src/components/ui/data-grid.tsx +1227 -0
  153. package/src/components/ui/data-table.tsx +993 -0
  154. package/src/components/ui/date-input.tsx +103 -0
  155. package/src/components/ui/dialog.tsx +159 -0
  156. package/src/components/ui/direction.tsx +32 -0
  157. package/src/components/ui/drawer.tsx +200 -0
  158. package/src/components/ui/dropdown-menu.tsx +329 -0
  159. package/src/components/ui/editable.tsx +90 -0
  160. package/src/components/ui/empty.tsx +86 -0
  161. package/src/components/ui/field.tsx +218 -0
  162. package/src/components/ui/file-upload.tsx +204 -0
  163. package/src/components/ui/floating-panel.tsx +199 -0
  164. package/src/components/ui/floating-toolbar.tsx +79 -0
  165. package/src/components/ui/format.tsx +22 -0
  166. package/src/components/ui/gantt.tsx +950 -0
  167. package/src/components/ui/hotkeys.tsx +340 -0
  168. package/src/components/ui/hover-card.tsx +83 -0
  169. package/src/components/ui/image-cropper.tsx +100 -0
  170. package/src/components/ui/input-group.tsx +133 -0
  171. package/src/components/ui/input-otp.tsx +82 -0
  172. package/src/components/ui/input.tsx +18 -0
  173. package/src/components/ui/item.tsx +174 -0
  174. package/src/components/ui/json-tree-view.tsx +37 -0
  175. package/src/components/ui/kanban.tsx +884 -0
  176. package/src/components/ui/kbd.tsx +20 -0
  177. package/src/components/ui/label.tsx +30 -0
  178. package/src/components/ui/live-region.tsx +54 -0
  179. package/src/components/ui/marker.tsx +66 -0
  180. package/src/components/ui/marquee.tsx +61 -0
  181. package/src/components/ui/menubar.tsx +456 -0
  182. package/src/components/ui/message.tsx +74 -0
  183. package/src/components/ui/navigation-menu.tsx +202 -0
  184. package/src/components/ui/node-graph.tsx +1723 -0
  185. package/src/components/ui/number-input.tsx +148 -0
  186. package/src/components/ui/pagination.tsx +88 -0
  187. package/src/components/ui/password-input.tsx +104 -0
  188. package/src/components/ui/popconfirm.tsx +149 -0
  189. package/src/components/ui/popover.tsx +126 -0
  190. package/src/components/ui/progress.tsx +114 -0
  191. package/src/components/ui/qr-code.tsx +51 -0
  192. package/src/components/ui/query-builder.tsx +938 -0
  193. package/src/components/ui/radio-group.tsx +108 -0
  194. package/src/components/ui/rating-group.tsx +91 -0
  195. package/src/components/ui/resizable.tsx +41 -0
  196. package/src/components/ui/rich-text-editor.tsx +845 -0
  197. package/src/components/ui/scheduler.tsx +1164 -0
  198. package/src/components/ui/scroll-area.tsx +82 -0
  199. package/src/components/ui/segment-group.tsx +95 -0
  200. package/src/components/ui/select.tsx +354 -0
  201. package/src/components/ui/separator.tsx +32 -0
  202. package/src/components/ui/sheet.tsx +130 -0
  203. package/src/components/ui/sidebar.tsx +665 -0
  204. package/src/components/ui/signature-pad.tsx +112 -0
  205. package/src/components/ui/skeleton.tsx +7 -0
  206. package/src/components/ui/slider.tsx +183 -0
  207. package/src/components/ui/sonner.tsx +37 -0
  208. package/src/components/ui/spinner.tsx +16 -0
  209. package/src/components/ui/steps.tsx +152 -0
  210. package/src/components/ui/swap.tsx +33 -0
  211. package/src/components/ui/switch.tsx +83 -0
  212. package/src/components/ui/table.tsx +72 -0
  213. package/src/components/ui/tabs.tsx +100 -0
  214. package/src/components/ui/tags-input.tsx +153 -0
  215. package/src/components/ui/textarea.tsx +17 -0
  216. package/src/components/ui/timer.tsx +67 -0
  217. package/src/components/ui/toc.tsx +64 -0
  218. package/src/components/ui/toggle-group.tsx +89 -0
  219. package/src/components/ui/toggle.tsx +46 -0
  220. package/src/components/ui/tooltip.tsx +113 -0
  221. package/src/components/ui/tour.tsx +168 -0
  222. package/src/components/ui/transfer-list.tsx +700 -0
  223. package/src/components/ui/tree-select.tsx +638 -0
  224. package/src/components/ui/tree-view.tsx +217 -0
  225. package/src/components/ui/virtual-list.tsx +343 -0
  226. package/src/hooks/use-mobile.ts +19 -0
  227. package/src/lib/controllable.ts +31 -0
  228. package/src/lib/data-table-adapter.ts +67 -0
  229. package/src/lib/drag-preview.ts +39 -0
  230. package/src/lib/form.ts +39 -0
  231. package/src/lib/history.ts +55 -0
  232. package/src/lib/row-layout.ts +81 -0
  233. package/src/lib/table.ts +483 -0
  234. package/src/lib/time.ts +37 -0
  235. package/src/lib/utils.ts +6 -0
  236. package/src/styles/arkcn.css +764 -0
  237. package/src/styles/index.css +3 -0
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # arkcn
2
+
3
+ shadcn/ui-style React components ported to [Ark UI](https://ark-ui.com), plus data-heavy primitives:
4
+ data grid, data table engine, kanban, gantt, scheduler, node graph, query builder, rich text editor
5
+ (tiptap), tree select, cascader, transfer list, virtual list, hotkeys, and more. Tailwind v4.
6
+
7
+ Every part is exported on its own, carries a `data-slot`, and is styled through Ark's data attributes.
8
+ Triggers are polymorphic via `asChild`.
9
+
10
+ ## Install components
11
+
12
+ ```bash
13
+ npx @multicomma/arkcn init # arkcn.json, src/lib/utils.ts, base styles, "@/" alias check, base deps
14
+ npx @multicomma/arkcn add button card dialog data-grid
15
+ npx @multicomma/arkcn list # everything in the registry (--type all, `list data-grid --docs`)
16
+ npx @multicomma/arkcn diff # which installed files you changed locally
17
+ ```
18
+
19
+ `init` writes `arkcn.json` (alias, `srcDir`, component/lib/hooks dirs, stylesheet), merges the
20
+ toolkit's tokens, custom variants, utilities, and keyframes into your stylesheet after
21
+ `@import "tailwindcss"` (between `/* arkcn:base */` markers, so re-running refreshes them),
22
+ makes sure the `@/*` path alias exists, and installs the base packages (Ark UI, lucide, clsx,
23
+ tailwind-merge, cva, tw-animate-css, the typography plugin). Tailwind v4 is required.
24
+
25
+ `add` copies the component **and everything it imports** (other components, `lib`, `hooks`) into
26
+ your project with imports rewritten to your alias, appends any component-specific CSS block, and
27
+ installs the packages those files need (tiptap for the editor, pragmatic drag and drop for kanban,
28
+ and so on). Files you have edited are kept unless you pass `--overwrite`; `diff --verbose` shows
29
+ what changed. `--no-install` skips the package manager, `--all` adds every component.
30
+
31
+ The components then belong to you: edit them freely, exactly as with shadcn/ui.
32
+
33
+ ### Registry
34
+
35
+ The CLI reads a registry generated from this repo (`npm run build:registry` → `registry/`), bundled
36
+ in the npm package so a CLI version always installs a matching component set and works offline.
37
+ The same folder is committed here, so you can also read it from a URL, for example to pick up
38
+ components newer than your installed CLI:
39
+
40
+ ```bash
41
+ npx @multicomma/arkcn add kanban --registry https://raw.githubusercontent.com/itsdanreed/arkcn/main/registry
42
+ # or export ARKCN_REGISTRY=...
43
+ ```
44
+
45
+ Each item is `registry/items/<name>.json`: files with content, `registryDependencies`,
46
+ `dependencies` (with versions), an optional `css` fragment, and `docs`. `registry/index.json` is the
47
+ manifest and `registry/base.css` the init payload.
48
+
49
+ ## MCP server
50
+
51
+ The package also ships an MCP server so agents (Claude Code, Cursor, and others) can browse the
52
+ registry, read documentation and source, and copy components into a project:
53
+
54
+ ```json
55
+ { "mcpServers": { "@multicomma/arkcn": { "command": "npx", "args": ["-y", "@multicomma/arkcn", "mcp"] } } }
56
+ ```
57
+
58
+ Tools: `list_components`, `search_components`, `get_component` (source, deps, css, docs),
59
+ `get_docs`, `plan_install` (dry run against a project), `add_components` (writes files and CSS,
60
+ returns the packages to install). Pass `--registry <url>` to read a hosted registry.
61
+
62
+ ## Consume from node_modules instead
63
+
64
+ The demo app imports the package source directly: import `@multicomma/arkcn/styles.css`,
65
+ alias `@/components/ui`, `@/lib`, and `@/hooks` to `node_modules/@multicomma/arkcn/src/...`,
66
+ and add `@source "../node_modules/@multicomma/arkcn/src"` because Tailwind does not scan
67
+ `node_modules`. Heavy dependencies (tiptap, pragmatic drag and drop, recharts, embla, date-fns,
68
+ react-day-picker, input-otp, react-resizable-panels, sonner, next-themes) are optional peers.
69
+
70
+ The demo application lives in the `arkcn-demo` repository.
71
+
72
+ ## Develop
73
+
74
+ ```bash
75
+ npm install
76
+ npm run check # registry build + typecheck + lint (oxlint with Tailwind rules, quotes, Ark part coverage, Prettier) + CLI e2e test
77
+ npm publish # prepublishOnly runs the same check
78
+ ```
package/bin/arkcn.mjs ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util"
3
+ import { add, diff, init, list } from "../cli/commands.mjs"
4
+ import { createRegistry } from "../cli/registry.mjs"
5
+ import { c, fail, log } from "../cli/ui.mjs"
6
+
7
+ const help = `${c.bold("@multicomma/arkcn")} - copy Ark UI components into your project
8
+
9
+ ${c.bold("Usage")}
10
+ npx @multicomma/arkcn init [--yes] [--alias @] [--css src/index.css] [--dir src] [--no-install]
11
+ npx @multicomma/arkcn add <name...> [--all] [--overwrite] [--yes] [--no-install]
12
+ npx @multicomma/arkcn list [name...] [--type ui|lib|hook|all] [--docs]
13
+ npx @multicomma/arkcn diff [name...] [--verbose]
14
+ npx @multicomma/arkcn mcp
15
+
16
+ ${c.bold("Commands")}
17
+ init Write arkcn.json, lib/utils, the base styles, check the "@/" alias, install base deps
18
+ add Copy components (and everything they import) into your project, merge their styles, install deps
19
+ list Show the registry, or one item with --docs
20
+ diff Show which installed files were changed locally since they were added
21
+ mcp Start the Model Context Protocol server (stdio) for agents
22
+
23
+ ${c.bold("Options")}
24
+ --registry <url|dir> Read the registry from a URL or directory instead of the bundled copy
25
+ (also ARKCN_REGISTRY), e.g. the GitHub raw URL of registry/
26
+ --cwd <dir> Project directory (default: current)
27
+ `
28
+
29
+ const { values, positionals } = parseArgs({
30
+ allowPositionals: true,
31
+ allowNegative: true,
32
+ options: {
33
+ help: { type: "boolean", short: "h" },
34
+ yes: { type: "boolean", short: "y" },
35
+ all: { type: "boolean" },
36
+ overwrite: { type: "boolean" },
37
+ install: { type: "boolean", default: true },
38
+ verbose: { type: "boolean", short: "v" },
39
+ docs: { type: "boolean" },
40
+ alias: { type: "string" },
41
+ css: { type: "string" },
42
+ dir: { type: "string" },
43
+ type: { type: "string" },
44
+ cwd: { type: "string" },
45
+ registry: { type: "string" },
46
+ },
47
+ })
48
+
49
+ const [command, ...names] = positionals
50
+ const cwd = values.cwd ?? process.cwd()
51
+ const registry = createRegistry(values.registry ?? process.env.ARKCN_REGISTRY)
52
+
53
+ try {
54
+ switch (command) {
55
+ case "init":
56
+ await init(cwd, registry, values)
57
+ break
58
+ case "add":
59
+ await add(cwd, registry, names, values)
60
+ break
61
+ case "list":
62
+ await list(cwd, registry, names, values)
63
+ break
64
+ case "diff":
65
+ await diff(cwd, registry, names, values)
66
+ break
67
+ case "mcp": {
68
+ const { startServer } = await import("../mcp/server.mjs")
69
+ await startServer({ registryBase: values.registry ?? process.env.ARKCN_REGISTRY })
70
+ break
71
+ }
72
+ default:
73
+ log(help)
74
+ if (command && !values.help) fail(`Unknown command: ${command}`)
75
+ }
76
+ } catch (error) {
77
+ fail(error instanceof Error ? error.message : String(error))
78
+ }
@@ -0,0 +1,251 @@
1
+ import { existsSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import {
4
+ CONFIG_FILE,
5
+ defaultConfig,
6
+ detectBundlerAlias,
7
+ detectPackageManager,
8
+ ensureTsconfigAlias,
9
+ installDependencies,
10
+ installedVersion,
11
+ mergeBaseCss,
12
+ mergeComponentCss,
13
+ readConfig,
14
+ readPackageJson,
15
+ readProjectFile,
16
+ rewriteImports,
17
+ targetPath,
18
+ writeConfig,
19
+ writeProjectFile,
20
+ } from "./project.mjs"
21
+ import { c, confirm, fail, log, ok, table, warn } from "./ui.mjs"
22
+
23
+ /** Write an item's files. Returns what was written and what was kept. */
24
+ export function writeItem(cwd, config, item, { overwrite }) {
25
+ const written = []
26
+ const kept = []
27
+ for (const file of item.files) {
28
+ const rel = targetPath(config, file.path)
29
+ const content = rewriteImports(file.content, config)
30
+ const current = readProjectFile(cwd, rel)
31
+ if (current === content) continue
32
+ if (current !== null && !overwrite) {
33
+ kept.push(rel)
34
+ continue
35
+ }
36
+ writeProjectFile(cwd, rel, content)
37
+ written.push(rel)
38
+ }
39
+ return { written, kept }
40
+ }
41
+
42
+ /** Files that exist locally and differ from the registry version. */
43
+ export function findConflicts(cwd, config, items) {
44
+ const conflicts = []
45
+ for (const item of items)
46
+ for (const file of item.files) {
47
+ const rel = targetPath(config, file.path)
48
+ const current = readProjectFile(cwd, rel)
49
+ if (current !== null && current !== rewriteImports(file.content, config)) conflicts.push(rel)
50
+ }
51
+ return conflicts
52
+ }
53
+
54
+ /** Packages an item set needs that the project does not have yet. */
55
+ export function missingDependencies(cwd, items) {
56
+ const deps = new Map()
57
+ for (const item of items)
58
+ for (const [dep, version] of Object.entries(item.dependencies))
59
+ if (!installedVersion(cwd, dep)) deps.set(dep, version)
60
+ return [...deps].map(([dep, version]) => `${dep}@${version}`)
61
+ }
62
+
63
+ /** `init`: config file, utils, base styles, alias check, base dependencies. */
64
+ export async function init(cwd, registry, flags) {
65
+ if (!readPackageJson(cwd)) fail(`No package.json in ${cwd}. Run this inside your app.`)
66
+ const existing = readConfig(cwd)
67
+ if (existing && !flags.yes) {
68
+ const again = await confirm(`${CONFIG_FILE} already exists. Re-run init and refresh the base styles?`, {
69
+ fallback: false,
70
+ })
71
+ if (!again) return
72
+ }
73
+ const config = {
74
+ ...defaultConfig,
75
+ ...existing,
76
+ ...(flags.alias ? { alias: flags.alias } : {}),
77
+ ...(flags.css ? { css: flags.css } : {}),
78
+ ...(flags.dir
79
+ ? {
80
+ srcDir: flags.dir,
81
+ componentsDir: `${flags.dir}/components/ui`,
82
+ libDir: `${flags.dir}/lib`,
83
+ hooksDir: `${flags.dir}/hooks`,
84
+ }
85
+ : {}),
86
+ }
87
+ writeConfig(cwd, config)
88
+ ok(`Wrote ${CONFIG_FILE}`)
89
+
90
+ const tailwind = installedVersion(cwd, "tailwindcss")
91
+ if (!tailwind) warn("tailwindcss is not in package.json; it will be installed (v4 is required).")
92
+ else if (!/(\^|~)?4\./.test(tailwind)) warn(`tailwindcss ${tailwind} found; the toolkit needs v4.`)
93
+
94
+ const alias = ensureTsconfigAlias(cwd, config.alias, config.srcDir)
95
+ if (alias === "added") ok(`Added "${config.alias}/*" -> "./${config.srcDir}/*" to tsconfig paths`)
96
+ else if (alias === "ok") ok(`tsconfig already maps "${config.alias}/*"`)
97
+ else warn(`Add "paths": { "${config.alias}/*": ["./${config.srcDir}/*"] } to your tsconfig compilerOptions.`)
98
+ const bundler = detectBundlerAlias(cwd, config.alias)
99
+ if (bundler.framework === "vite" && !bundler.ok)
100
+ warn(
101
+ `Add the alias to ${bundler.file}: resolve.alias { "${config.alias}": path.resolve(__dirname, "./${config.srcDir}") }`
102
+ )
103
+
104
+ const { items } = await registry.closure(["utils"])
105
+ for (const item of items) writeItem(cwd, config, item, { overwrite: false })
106
+ ok(`Wrote ${targetPath(config, "lib/utils.ts")}`)
107
+
108
+ const result = mergeBaseCss(cwd, config, await registry.baseCss())
109
+ const verb = {
110
+ created: "Created",
111
+ updated: "Updated base styles in",
112
+ appended: "Appended base styles to",
113
+ unchanged: "Base styles already current in",
114
+ }
115
+ ok(`${verb[result]} ${config.css}`)
116
+
117
+ const index = await registry.index()
118
+ const deps = Object.entries(index.baseDependencies)
119
+ .filter(([dep]) => !installedVersion(cwd, dep))
120
+ .map(([dep, version]) => `${dep}@${version}`)
121
+ await install(cwd, deps, flags)
122
+ log(`\nNext: ${c.cyan("npx @multicomma/arkcn add button card dialog")}`)
123
+ }
124
+
125
+ async function install(cwd, deps, flags) {
126
+ if (deps.length === 0) return ok("No new dependencies")
127
+ if (flags.install === false) return log(`${c.dim("Skipped install. Needed:")} ${deps.join(" ")}`)
128
+ log(`${c.dim(`Installing with ${detectPackageManager(cwd)}:`)} ${deps.join(" ")}`)
129
+ const res = installDependencies(cwd, deps)
130
+ if (!res.ok) fail("Install failed. Install the packages above and re-run.")
131
+ }
132
+
133
+ /** `add`: closure of components, files, CSS fragments, dependencies. */
134
+ export async function add(cwd, registry, names, flags) {
135
+ const config = readConfig(cwd)
136
+ if (!config) fail(`No ${CONFIG_FILE}. Run ${c.cyan("npx @multicomma/arkcn init")} first.`)
137
+ const index = await registry.index()
138
+ const wanted = flags.all ? index.items.filter((i) => i.type === "ui").map((i) => i.name) : names
139
+ if (wanted.length === 0) fail("Nothing to add. Pass component names or --all.")
140
+ const { items, missing } = await registry.closure(wanted)
141
+ if (missing.length) fail(`Unknown: ${missing.join(", ")}. See ${c.cyan("npx @multicomma/arkcn list")}.`)
142
+
143
+ let overwrite = !!flags.overwrite
144
+ const conflicts = findConflicts(cwd, config, items)
145
+ if (conflicts.length && !overwrite) {
146
+ warn(`These files exist and differ from the registry:\n ${conflicts.join("\n ")}`)
147
+ overwrite = await confirm("Overwrite them?", { yes: flags.yes, fallback: false })
148
+ if (!overwrite) log(c.dim("Keeping your versions; only new files will be written."))
149
+ }
150
+
151
+ let cssChanges = 0
152
+ for (const item of items) {
153
+ const { written, kept } = writeItem(cwd, config, item, { overwrite })
154
+ for (const rel of written) ok(`${item.name} -> ${rel}`)
155
+ for (const rel of kept) log(`${c.dim("kept")} ${rel}`)
156
+ if (item.css) {
157
+ const r = mergeComponentCss(cwd, config, item.name, item.css)
158
+ if (r !== "unchanged") {
159
+ cssChanges++
160
+ ok(`${item.name} styles ${r === "updated" ? "updated in" : "appended to"} ${config.css}`)
161
+ }
162
+ }
163
+ }
164
+ await install(cwd, missingDependencies(cwd, items), flags)
165
+ const uiCount = items.filter((i) => i.type === "ui").length
166
+ log(
167
+ `\n${c.bold(`${uiCount} component(s), ${items.length - uiCount} helper(s)${cssChanges ? `, ${cssChanges} style block(s)` : ""}`)}`
168
+ )
169
+ }
170
+
171
+ /** `list`: everything in the registry, or one item in detail. */
172
+ export async function list(cwd, registry, names, flags) {
173
+ const index = await registry.index()
174
+ const config = readConfig(cwd)
175
+ if (names.length) {
176
+ for (const name of names) {
177
+ const item = await registry.item(name)
178
+ if (!item) return fail(`Unknown component: ${name}`)
179
+ log(`${c.bold(item.name)} ${c.dim(`(${item.type})`)}\n${item.description}\n`)
180
+ log(`${c.dim("files:")} ${item.files.map((f) => f.path).join(", ")}`)
181
+ log(`${c.dim("needs:")} ${item.registryDependencies.join(", ") || "-"}`)
182
+ log(`${c.dim("packages:")} ${Object.keys(item.dependencies).join(", ") || "-"}`)
183
+ if (item.css) log(`${c.dim("styles:")} yes`)
184
+ if (flags.docs && item.docs) log(`\n${item.docs}`)
185
+ }
186
+ return
187
+ }
188
+ const type = flags.type ?? "ui"
189
+ const rows = index.items
190
+ .filter((i) => type === "all" || i.type === type)
191
+ .map((i) => {
192
+ const installed = config && i.files.every((f) => existsSync(join(cwd, targetPath(config, f))))
193
+ return [i.name, i.type, installed ? c.green("installed") : "", i.description.slice(0, 70)]
194
+ })
195
+ table(rows, ["name", "type", "", "description"])
196
+ log(c.dim(`\n${rows.length} item(s) from ${registry.source}. \`list <name> --docs\` prints the documentation.`))
197
+ }
198
+
199
+ /** `diff`: compare installed files with the registry. */
200
+ export async function diff(cwd, registry, names, flags) {
201
+ const config = readConfig(cwd)
202
+ if (!config) fail(`No ${CONFIG_FILE}. Run init first.`)
203
+ const index = await registry.index()
204
+ const targets = names.length ? names : index.items.map((i) => i.name)
205
+ let changed = 0
206
+ for (const name of targets) {
207
+ const item = await registry.item(name)
208
+ if (!item) return fail(`Unknown component: ${name}`)
209
+ for (const file of item.files) {
210
+ const rel = targetPath(config, file.path)
211
+ const current = readProjectFile(cwd, rel)
212
+ if (current === null) {
213
+ if (names.length) log(`${c.dim("missing")} ${rel}`)
214
+ continue
215
+ }
216
+ const expected = rewriteImports(file.content, config)
217
+ if (current === expected) {
218
+ if (names.length) log(`${c.green("same")} ${rel}`)
219
+ continue
220
+ }
221
+ changed++
222
+ log(`${c.yellow("changed")} ${rel}`)
223
+ if (flags.verbose) log(lineDiff(current, expected))
224
+ }
225
+ }
226
+ if (!changed) ok("Installed files match the registry")
227
+ else log(c.dim(`\n${changed} file(s) differ. \`add <name> --overwrite\` replaces them with the registry version.`))
228
+ }
229
+
230
+ /** Minimal LCS line diff (files are small). */
231
+ export function lineDiff(current, expected) {
232
+ const a = current.split("\n")
233
+ const b = expected.split("\n")
234
+ const dp = Array.from({ length: a.length + 1 }, () => new Uint16Array(b.length + 1))
235
+ for (let i = a.length - 1; i >= 0; i--)
236
+ for (let j = b.length - 1; j >= 0; j--)
237
+ dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1])
238
+ let i = 0
239
+ let j = 0
240
+ const out = []
241
+ while (i < a.length && j < b.length) {
242
+ if (a[i] === b[j]) {
243
+ i++
244
+ j++
245
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) out.push(c.red(`- ${a[i++]}`))
246
+ else out.push(c.green(`+ ${b[j++]}`))
247
+ }
248
+ while (i < a.length) out.push(c.red(`- ${a[i++]}`))
249
+ while (j < b.length) out.push(c.green(`+ ${b[j++]}`))
250
+ return out.join("\n")
251
+ }
@@ -0,0 +1,168 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
2
+ import { dirname, join, relative } from "node:path"
3
+ import { spawnSync } from "node:child_process"
4
+
5
+ export const CONFIG_FILE = "arkcn.json"
6
+
7
+ export const defaultConfig = {
8
+ alias: "@",
9
+ /** What the alias points at; component dirs live under it. */
10
+ srcDir: "src",
11
+ componentsDir: "src/components/ui",
12
+ libDir: "src/lib",
13
+ hooksDir: "src/hooks",
14
+ css: "src/index.css",
15
+ }
16
+
17
+ export function readConfig(cwd) {
18
+ const file = join(cwd, CONFIG_FILE)
19
+ if (!existsSync(file)) return null
20
+ return { ...defaultConfig, ...JSON.parse(readFileSync(file, "utf8")) }
21
+ }
22
+
23
+ export function writeConfig(cwd, config) {
24
+ writeFileSync(join(cwd, CONFIG_FILE), JSON.stringify(config, null, 2) + "\n")
25
+ }
26
+
27
+ export function detectPackageManager(cwd) {
28
+ if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm"
29
+ if (existsSync(join(cwd, "yarn.lock"))) return "yarn"
30
+ if (existsSync(join(cwd, "bun.lock")) || existsSync(join(cwd, "bun.lockb"))) return "bun"
31
+ return "npm"
32
+ }
33
+
34
+ export function readPackageJson(cwd) {
35
+ const file = join(cwd, "package.json")
36
+ if (!existsSync(file)) return null
37
+ return JSON.parse(readFileSync(file, "utf8"))
38
+ }
39
+
40
+ export function installedVersion(cwd, dep) {
41
+ const pkg = readPackageJson(cwd)
42
+ return pkg?.dependencies?.[dep] ?? pkg?.devDependencies?.[dep] ?? null
43
+ }
44
+
45
+ /** Target path inside the project for a registry file path like "components/ui/button.tsx". */
46
+ export function targetPath(config, registryPath) {
47
+ if (registryPath.startsWith("components/ui/"))
48
+ return join(config.componentsDir, registryPath.slice("components/ui/".length))
49
+ if (registryPath.startsWith("lib/")) return join(config.libDir, registryPath.slice("lib/".length))
50
+ if (registryPath.startsWith("hooks/")) return join(config.hooksDir, registryPath.slice("hooks/".length))
51
+ return registryPath
52
+ }
53
+
54
+ /** Rewrite "@/components/ui/x", "@/lib/x", "@/hooks/x" imports to the project's alias and dirs. */
55
+ export function rewriteImports(content, config) {
56
+ const alias = config.alias.replace(/\/$/, "")
57
+ const dirs = { "components/ui": config.componentsDir, lib: config.libDir, hooks: config.hooksDir }
58
+ const rootPrefix = new RegExp(`^${config.srcDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/`)
59
+ return content.replace(/from "@\/(components\/ui|lib|hooks)\/([^"]+)"/g, (_, kind, rest) => {
60
+ const dir = dirs[kind].replace(rootPrefix, "")
61
+ return `from "${alias}/${dir}/${rest}"`
62
+ })
63
+ }
64
+
65
+ export function writeProjectFile(cwd, rel, content) {
66
+ const file = join(cwd, rel)
67
+ mkdirSync(dirname(file), { recursive: true })
68
+ writeFileSync(file, content)
69
+ return relative(cwd, file)
70
+ }
71
+
72
+ export function readProjectFile(cwd, rel) {
73
+ const file = join(cwd, rel)
74
+ return existsSync(file) ? readFileSync(file, "utf8") : null
75
+ }
76
+
77
+ const BASE_START = "/* arkcn:base */"
78
+ const BASE_END = "/* arkcn:base:end */"
79
+ const fragmentStart = (name) => `/* arkcn:component ${name} */`
80
+ const fragmentEnd = (name) => `/* arkcn:component ${name}:end */`
81
+
82
+ function replaceBlock(css, start, end, block) {
83
+ const i = css.indexOf(start)
84
+ const j = css.indexOf(end)
85
+ if (i >= 0 && j > i) {
86
+ const current = css.slice(i, j + end.length + 1)
87
+ if (current === block) return { css, result: "unchanged" }
88
+ return { css: css.slice(0, i) + block + css.slice(j + end.length).replace(/^\n/, ""), result: "updated" }
89
+ }
90
+ return { css: css.trimEnd() + "\n\n" + block, result: "appended" }
91
+ }
92
+
93
+ /** Insert or replace the base block; creates the stylesheet (with the tailwind import) when missing. */
94
+ export function mergeBaseCss(cwd, config, baseCss) {
95
+ const block = `${BASE_START}\n${baseCss.trim()}\n${BASE_END}\n`
96
+ let css = readProjectFile(cwd, config.css)
97
+ if (css === null) {
98
+ writeProjectFile(cwd, config.css, `@import "tailwindcss";\n\n${block}`)
99
+ return "created"
100
+ }
101
+ if (!/@import\s+"tailwindcss"/.test(css)) css = `@import "tailwindcss";\n` + css
102
+ const merged = replaceBlock(css, BASE_START, BASE_END, block)
103
+ if (merged.result !== "unchanged") writeProjectFile(cwd, config.css, merged.css)
104
+ return merged.result
105
+ }
106
+
107
+ export function mergeComponentCss(cwd, config, name, fragment) {
108
+ const css = readProjectFile(cwd, config.css) ?? `@import "tailwindcss";\n`
109
+ const block = `${fragmentStart(name)}\n${fragment.trim()}\n${fragmentEnd(name)}\n`
110
+ const merged = replaceBlock(css, fragmentStart(name), fragmentEnd(name), block)
111
+ if (merged.result !== "unchanged") writeProjectFile(cwd, config.css, merged.css)
112
+ return merged.result
113
+ }
114
+
115
+ export function installDependencies(cwd, deps) {
116
+ if (deps.length === 0) return { ok: true, skipped: true }
117
+ const pm = detectPackageManager(cwd)
118
+ const args = pm === "npm" ? ["install", ...deps] : ["add", ...deps]
119
+ const result = spawnSync(pm, args, { cwd, stdio: "inherit" })
120
+ return { ok: result.status === 0, pm, args }
121
+ }
122
+
123
+ /** Ensure tsconfig paths has "<alias>/*": ["./<srcDir>/*"]. Returns "ok" | "added" | "manual". */
124
+ export function ensureTsconfigAlias(cwd, alias, srcDir = "src") {
125
+ for (const name of ["tsconfig.app.json", "tsconfig.json"]) {
126
+ const file = join(cwd, name)
127
+ if (!existsSync(file)) continue
128
+ const raw = readFileSync(file, "utf8")
129
+ let json
130
+ try {
131
+ json = JSON.parse(
132
+ raw
133
+ .replace(/\/\*[\s\S]*?\*\//g, "")
134
+ .replace(/^\s*\/\/.*$/gm, "")
135
+ .replace(/,(\s*[}\]])/g, "$1")
136
+ )
137
+ } catch {
138
+ return "manual"
139
+ }
140
+ const paths = json.compilerOptions?.paths ?? {}
141
+ if (paths[`${alias}/*`]) return "ok"
142
+ if (raw.includes("//") || raw.includes("/*")) return "manual"
143
+ json.compilerOptions = { ...json.compilerOptions, paths: { ...paths, [`${alias}/*`]: [`./${srcDir}/*`] } }
144
+ writeFileSync(file, JSON.stringify(json, null, 2) + "\n")
145
+ return "added"
146
+ }
147
+ return "manual"
148
+ }
149
+
150
+ export function detectBundlerAlias(cwd, alias) {
151
+ const candidates = [
152
+ "vite.config.ts",
153
+ "vite.config.js",
154
+ "vite.config.mts",
155
+ "next.config.ts",
156
+ "next.config.js",
157
+ "next.config.mjs",
158
+ ]
159
+ for (const name of candidates) {
160
+ const file = join(cwd, name)
161
+ if (!existsSync(file)) continue
162
+ const raw = readFileSync(file, "utf8")
163
+ if (name.startsWith("next")) return { file: name, ok: true, framework: "next" }
164
+ const ok = raw.includes(`"${alias}"`) || raw.includes(`'${alias}'`) || raw.includes("tsconfigPaths")
165
+ return { file: name, ok, framework: "vite" }
166
+ }
167
+ return { file: null, ok: false, framework: "unknown" }
168
+ }
@@ -0,0 +1,50 @@
1
+ import { readFileSync } from "node:fs"
2
+ import { join } from "node:path"
3
+
4
+ /** The registry shipped inside this package (generated by scripts/build-registry.mjs). */
5
+ export const bundledDir = new URL("../registry/", import.meta.url).pathname
6
+
7
+ /**
8
+ * A registry source: the bundled directory by default, or a base URL (`--registry`,
9
+ * `ARKCN_REGISTRY`) such as https://raw.githubusercontent.com/itsdanreed/arkcn/main/registry
10
+ */
11
+ export function createRegistry(base) {
12
+ const remote = base && /^https?:\/\//.test(base)
13
+ const read = async (rel) => {
14
+ if (!remote) return readFileSync(join(base ?? bundledDir, rel), "utf8")
15
+ const url = `${base.replace(/\/$/, "")}/${rel}`
16
+ const res = await fetch(url)
17
+ if (!res.ok) throw new Error(`Registry request failed: ${res.status} ${url}`)
18
+ return res.text()
19
+ }
20
+ const cache = new Map()
21
+ const cached = (rel, parse) => {
22
+ if (!cache.has(rel)) cache.set(rel, read(rel).then(parse))
23
+ return cache.get(rel)
24
+ }
25
+ return {
26
+ source: remote ? base : "bundled",
27
+ index: () => cached("index.json", JSON.parse),
28
+ item: (name) => cached(`items/${name}.json`, JSON.parse).catch(() => null),
29
+ baseCss: () => cached("base.css", (s) => s),
30
+ /** Every item needed to install `names`, dependencies first, each once. */
31
+ async closure(names) {
32
+ const ordered = []
33
+ const seen = new Set()
34
+ const missing = []
35
+ const visit = async (name, chain) => {
36
+ if (seen.has(name)) return
37
+ const item = await this.item(name)
38
+ if (!item) {
39
+ missing.push(chain.length ? `${name} (needed by ${chain[chain.length - 1]})` : name)
40
+ return
41
+ }
42
+ seen.add(name)
43
+ for (const dep of item.registryDependencies) await visit(dep, [...chain, name])
44
+ ordered.push(item)
45
+ }
46
+ for (const name of names) await visit(name, [])
47
+ return { items: ordered, missing }
48
+ },
49
+ }
50
+ }
package/cli/ui.mjs ADDED
@@ -0,0 +1,46 @@
1
+ import { createInterface } from "node:readline/promises"
2
+ import { stdin, stdout } from "node:process"
3
+
4
+ const tty = stdout.isTTY && !process.env.NO_COLOR
5
+ const ESC = "["
6
+ const paint = (code) => (s) => (tty ? `${ESC}${code}m${s}${ESC}0m` : String(s))
7
+ export const c = {
8
+ bold: paint("1"),
9
+ dim: paint("2"),
10
+ green: paint("32"),
11
+ yellow: paint("33"),
12
+ red: paint("31"),
13
+ cyan: paint("36"),
14
+ }
15
+
16
+ export const log = (...args) => console.log(...args)
17
+ export const ok = (msg) => log(`${c.green("+")} ${msg}`)
18
+ export const warn = (msg) => log(`${c.yellow("!")} ${msg}`)
19
+ export const fail = (msg) => {
20
+ console.error(`${c.red("x")} ${msg}`)
21
+ process.exit(1)
22
+ }
23
+
24
+ export async function confirm(question, { yes = false, fallback = true } = {}) {
25
+ if (yes) return fallback
26
+ if (!stdin.isTTY) return fallback
27
+ const rl = createInterface({ input: stdin, output: stdout })
28
+ try {
29
+ const answer = (await rl.question(`${question} ${fallback ? "[Y/n]" : "[y/N]"} `)).trim().toLowerCase()
30
+ if (!answer) return fallback
31
+ return answer === "y" || answer === "yes"
32
+ } finally {
33
+ rl.close()
34
+ }
35
+ }
36
+
37
+ export function table(rows, headers) {
38
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => String(r[i] ?? "").length)))
39
+ const line = (cells) =>
40
+ cells
41
+ .map((cell, i) => String(cell ?? "").padEnd(widths[i]))
42
+ .join(" ")
43
+ .trimEnd()
44
+ log(c.bold(line(headers)))
45
+ for (const r of rows) log(line(r))
46
+ }