@hypoth-ui/cli 0.0.1

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 ADDED
@@ -0,0 +1,154 @@
1
+ # @hypoth-ui/cli
2
+
3
+ CLI tool for adding hypoth-ui components to your project.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npx @hypoth-ui/cli init
9
+ ```
10
+
11
+ Or install globally:
12
+
13
+ ```bash
14
+ npm install -g @hypoth-ui/cli
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ 1. Initialize hypoth-ui in your project:
20
+
21
+ ```bash
22
+ npx @hypoth-ui/cli init
23
+ ```
24
+
25
+ 2. Add components:
26
+
27
+ ```bash
28
+ npx @hypoth-ui/cli add button
29
+ npx @hypoth-ui/cli add dialog menu
30
+ ```
31
+
32
+ 3. List available components:
33
+
34
+ ```bash
35
+ npx @hypoth-ui/cli list
36
+ ```
37
+
38
+ ## Commands
39
+
40
+ ### `init`
41
+
42
+ Initialize hypoth-ui in your project. Creates `ds.config.json` and installs core dependencies.
43
+
44
+ ```bash
45
+ npx @hypoth-ui/cli init [options]
46
+ ```
47
+
48
+ Options:
49
+ - `-s, --style <style>` - Installation style: `copy` (source files) or `package` (npm packages)
50
+ - `-f, --framework <framework>` - Framework: `react`, `next`, `wc`, or `vanilla`
51
+ - `-y, --yes` - Skip prompts and use defaults
52
+
53
+ ### `add`
54
+
55
+ Add components to your project.
56
+
57
+ ```bash
58
+ npx @hypoth-ui/cli add <components...> [options]
59
+ ```
60
+
61
+ Options:
62
+ - `-o, --overwrite` - Overwrite existing components
63
+ - `-a, --all` - Add all available components
64
+
65
+ Examples:
66
+ ```bash
67
+ npx @hypoth-ui/cli add button # Add single component
68
+ npx @hypoth-ui/cli add button dialog # Add multiple components
69
+ npx @hypoth-ui/cli add --all # Add all components
70
+ npx @hypoth-ui/cli add button -o # Overwrite existing
71
+ ```
72
+
73
+ ### `list`
74
+
75
+ List all available components.
76
+
77
+ ```bash
78
+ npx @hypoth-ui/cli list [options]
79
+ ```
80
+
81
+ Options:
82
+ - `-j, --json` - Output as JSON
83
+
84
+ ### `diff`
85
+
86
+ Check for component updates.
87
+
88
+ ```bash
89
+ npx @hypoth-ui/cli diff [options]
90
+ ```
91
+
92
+ Options:
93
+ - `-j, --json` - Output as JSON
94
+
95
+ ## Installation Modes
96
+
97
+ ### Package Mode (Recommended)
98
+
99
+ Components are installed as npm packages. Easier to update, tree-shakeable.
100
+
101
+ ```bash
102
+ npx @hypoth-ui/cli init --style package
103
+ ```
104
+
105
+ ### Copy Mode
106
+
107
+ Component source files are copied to your project. Full customization, you own the code.
108
+
109
+ ```bash
110
+ npx @hypoth-ui/cli init --style copy
111
+ ```
112
+
113
+ ## Configuration
114
+
115
+ Configuration is stored in `ds.config.json`:
116
+
117
+ ```json
118
+ {
119
+ "$schema": "https://hypoth-ui.dev/schema/ds.config.json",
120
+ "style": "package",
121
+ "framework": "next",
122
+ "typescript": true,
123
+ "packageManager": "pnpm",
124
+ "paths": {
125
+ "components": "src/components/ui",
126
+ "utils": "src/lib"
127
+ },
128
+ "aliases": {
129
+ "components": "@/components/ui",
130
+ "lib": "@/lib"
131
+ },
132
+ "components": []
133
+ }
134
+ ```
135
+
136
+ ## Supported Frameworks
137
+
138
+ - **React** - Standard React applications
139
+ - **Next.js** - Next.js App Router projects
140
+ - **Web Components** - Lit-based Web Components
141
+ - **Vanilla** - Vanilla JS using Web Components
142
+
143
+ ## Supported Package Managers
144
+
145
+ - npm
146
+ - pnpm
147
+ - yarn
148
+ - bun
149
+
150
+ The CLI automatically detects your package manager from lock files.
151
+
152
+ ## License
153
+
154
+ MIT
@@ -0,0 +1,238 @@
1
+ import {
2
+ installPackages
3
+ } from "./chunk-5LTQ2XVL.js";
4
+ import {
5
+ getComponent,
6
+ getComponentNames,
7
+ getNpmDependencies,
8
+ isComponentCompatible,
9
+ loadBundledRegistry,
10
+ resolveDependencies
11
+ } from "./chunk-GJ6JOQ3Q.js";
12
+ import {
13
+ addInstalledComponent,
14
+ isComponentInstalled,
15
+ readConfig,
16
+ writeConfig
17
+ } from "./chunk-YPKFYE45.js";
18
+
19
+ // src/commands/add.ts
20
+ import * as p from "@clack/prompts";
21
+ import pc from "picocolors";
22
+
23
+ // src/utils/copy.ts
24
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
25
+ import { dirname, join } from "path";
26
+ async function copyComponentFiles(component, config, options = {}) {
27
+ const { cwd = process.cwd(), overwrite = false, sourceDir } = options;
28
+ const targetDir = join(cwd, config.paths.components);
29
+ const files = filterFilesForFramework(component.files, config.framework);
30
+ const result = {
31
+ copied: [],
32
+ skipped: [],
33
+ errors: []
34
+ };
35
+ for (const file of files) {
36
+ const targetPath = join(targetDir, component.name, file.target);
37
+ if (existsSync(targetPath) && !overwrite) {
38
+ result.skipped.push({
39
+ file: file.target,
40
+ reason: "exists"
41
+ });
42
+ continue;
43
+ }
44
+ try {
45
+ const content = await getSourceContent(file, component, sourceDir);
46
+ const transformedContent = transformImports(content, config, file.type);
47
+ const dir = dirname(targetPath);
48
+ if (!existsSync(dir)) {
49
+ mkdirSync(dir, { recursive: true });
50
+ }
51
+ writeFileSync(targetPath, transformedContent, "utf-8");
52
+ result.copied.push(targetPath);
53
+ } catch (error) {
54
+ result.errors.push({
55
+ file: file.target,
56
+ error: error.message
57
+ });
58
+ }
59
+ }
60
+ return result;
61
+ }
62
+ function filterFilesForFramework(files, framework) {
63
+ const frameworkMap = {
64
+ react: "react",
65
+ next: "react",
66
+ wc: "wc",
67
+ vanilla: "wc"
68
+ };
69
+ const targetFramework = frameworkMap[framework];
70
+ return files.filter((file) => {
71
+ if (!file.framework) {
72
+ return true;
73
+ }
74
+ return file.framework === targetFramework;
75
+ });
76
+ }
77
+ async function getSourceContent(file, component, sourceDir) {
78
+ if (sourceDir) {
79
+ const sourcePath = join(sourceDir, component.name, file.path);
80
+ if (existsSync(sourcePath)) {
81
+ return readFileSync(sourcePath, "utf-8");
82
+ }
83
+ }
84
+ const bundledPath = getBundledTemplatePath(component.name, file.path);
85
+ if (existsSync(bundledPath)) {
86
+ return readFileSync(bundledPath, "utf-8");
87
+ }
88
+ throw new Error(`Source file not found: ${file.path}`);
89
+ }
90
+ function getBundledTemplatePath(componentName, filePath) {
91
+ const __dirname2 = new URL(".", import.meta.url).pathname;
92
+ return join(__dirname2, "../../templates", componentName, filePath);
93
+ }
94
+ function transformImports(content, config, fileType) {
95
+ if (fileType !== "ts" && fileType !== "tsx") {
96
+ return content;
97
+ }
98
+ let result = content;
99
+ result = result.replace(
100
+ /@\/components\//g,
101
+ `${config.aliases.components}/`
102
+ );
103
+ result = result.replace(
104
+ /@\/lib\//g,
105
+ `${config.aliases.lib}/`
106
+ );
107
+ return result;
108
+ }
109
+ function ensureComponentsDir(config, cwd = process.cwd()) {
110
+ const targetDir = join(cwd, config.paths.components);
111
+ if (!existsSync(targetDir)) {
112
+ mkdirSync(targetDir, { recursive: true });
113
+ }
114
+ }
115
+
116
+ // src/commands/add.ts
117
+ async function addCommand(components, options = {}) {
118
+ p.intro(pc.bgCyan(pc.black(" hypoth-ui add ")));
119
+ const cwd = process.cwd();
120
+ let config;
121
+ try {
122
+ config = readConfig(cwd);
123
+ } catch (error) {
124
+ p.cancel(pc.red(error.message));
125
+ process.exit(1);
126
+ }
127
+ const registry = loadBundledRegistry();
128
+ if (options.all) {
129
+ components = getComponentNames(registry);
130
+ p.log.info(`Installing all ${components.length} components`);
131
+ }
132
+ if (components.length === 0) {
133
+ p.cancel(pc.red("No components specified. Usage: npx @hypoth-ui/cli add <component>"));
134
+ process.exit(1);
135
+ }
136
+ const invalidComponents = [];
137
+ const validComponents = [];
138
+ for (const name of components) {
139
+ const component = getComponent(registry, name);
140
+ if (!component) {
141
+ invalidComponents.push(name);
142
+ } else if (!isComponentCompatible(component, config.framework)) {
143
+ p.log.warn(`${pc.yellow(name)} is not compatible with ${config.framework}`);
144
+ } else {
145
+ validComponents.push(component);
146
+ }
147
+ }
148
+ if (invalidComponents.length > 0) {
149
+ p.log.error(`Unknown components: ${invalidComponents.join(", ")}`);
150
+ p.log.info(`Run ${pc.cyan("npx @hypoth-ui/cli list")} to see available components`);
151
+ if (validComponents.length === 0) {
152
+ process.exit(1);
153
+ }
154
+ }
155
+ const toInstall = [];
156
+ const skipped = [];
157
+ for (const component of validComponents) {
158
+ if (isComponentInstalled(config, component.name) && !options.overwrite) {
159
+ skipped.push(component.name);
160
+ } else {
161
+ toInstall.push(component);
162
+ }
163
+ }
164
+ if (skipped.length > 0) {
165
+ p.log.info(`Skipping already installed: ${skipped.join(", ")} (use --overwrite to replace)`);
166
+ }
167
+ if (toInstall.length === 0) {
168
+ p.outro(pc.yellow("No new components to install"));
169
+ return;
170
+ }
171
+ const componentsToInstall = resolveDependencies(
172
+ registry,
173
+ toInstall.map((c) => c.name)
174
+ );
175
+ const finalComponents = componentsToInstall.filter(
176
+ (c) => !isComponentInstalled(config, c.name) || options.overwrite
177
+ );
178
+ if (finalComponents.length === 0) {
179
+ p.outro(pc.yellow("All components already installed"));
180
+ return;
181
+ }
182
+ p.log.info(`Installing: ${finalComponents.map((c) => pc.cyan(c.name)).join(", ")}`);
183
+ const npmDeps = getNpmDependencies(finalComponents);
184
+ if (npmDeps.length > 0) {
185
+ const spinner3 = p.spinner();
186
+ spinner3.start(`Installing npm dependencies: ${npmDeps.join(", ")}`);
187
+ try {
188
+ await installPackages(npmDeps, config.packageManager, { cwd });
189
+ spinner3.stop(`Installed npm dependencies`);
190
+ } catch (error) {
191
+ spinner3.stop(pc.red(`Failed to install npm dependencies: ${error.message}`));
192
+ process.exit(1);
193
+ }
194
+ }
195
+ const spinner2 = p.spinner();
196
+ if (config.style === "copy") {
197
+ ensureComponentsDir(config, cwd);
198
+ for (const component of finalComponents) {
199
+ spinner2.start(`Copying ${component.name}...`);
200
+ try {
201
+ const result = await copyComponentFiles(component, config, {
202
+ cwd,
203
+ overwrite: options.overwrite
204
+ });
205
+ if (result.errors.length > 0) {
206
+ spinner2.stop(pc.red(`Failed to copy ${component.name}: ${result.errors[0]?.error ?? "Unknown error"}`));
207
+ continue;
208
+ }
209
+ config = addInstalledComponent(config, {
210
+ name: component.name,
211
+ version: component.version,
212
+ mode: "copy"
213
+ });
214
+ spinner2.stop(`Copied ${component.name}`);
215
+ } catch (error) {
216
+ spinner2.stop(pc.red(`Failed to copy ${component.name}: ${error.message}`));
217
+ }
218
+ }
219
+ } else {
220
+ for (const component of finalComponents) {
221
+ config = addInstalledComponent(config, {
222
+ name: component.name,
223
+ version: component.version,
224
+ mode: "package"
225
+ });
226
+ }
227
+ }
228
+ writeConfig(config, cwd);
229
+ const installed = finalComponents.map((c) => c.name);
230
+ p.outro(pc.green(`Added ${installed.length} component(s): ${installed.join(", ")}`));
231
+ if (config.style === "copy") {
232
+ console.log("");
233
+ console.log(`Components copied to: ${pc.dim(config.paths.components)}`);
234
+ }
235
+ }
236
+ export {
237
+ addCommand
238
+ };
@@ -0,0 +1,52 @@
1
+ // src/utils/install.ts
2
+ import { execa } from "execa";
3
+ async function installPackages(packages, packageManager, options = {}) {
4
+ const { cwd = process.cwd(), dev = false, silent = false } = options;
5
+ if (packages.length === 0) {
6
+ return;
7
+ }
8
+ const command = getCommand(packageManager);
9
+ const args = getArgs(packageManager, packages, dev);
10
+ await execa(command, args, {
11
+ cwd,
12
+ stdio: silent ? "ignore" : "inherit"
13
+ });
14
+ }
15
+ function getCommand(pm) {
16
+ return pm;
17
+ }
18
+ function getArgs(pm, packages, dev) {
19
+ const devFlag = dev ? getDevFlag(pm) : null;
20
+ const installCommand = getInstallSubcommand(pm);
21
+ const args = [installCommand, ...packages];
22
+ if (devFlag) {
23
+ args.push(devFlag);
24
+ }
25
+ return args;
26
+ }
27
+ function getInstallSubcommand(pm) {
28
+ switch (pm) {
29
+ case "yarn":
30
+ case "pnpm":
31
+ case "bun":
32
+ return "add";
33
+ case "npm":
34
+ default:
35
+ return "install";
36
+ }
37
+ }
38
+ function getDevFlag(pm) {
39
+ switch (pm) {
40
+ case "yarn":
41
+ case "pnpm":
42
+ case "bun":
43
+ return "-D";
44
+ case "npm":
45
+ default:
46
+ return "--save-dev";
47
+ }
48
+ }
49
+
50
+ export {
51
+ installPackages
52
+ };
@@ -0,0 +1,101 @@
1
+ // src/utils/registry.ts
2
+ import { readFileSync } from "fs";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+ var __dirname = dirname(fileURLToPath(import.meta.url));
6
+ var BUNDLED_REGISTRY_PATH = join(__dirname, "../registry/components.json");
7
+ var REMOTE_REGISTRY_URL = "https://hypoth-ui.dev/registry/components.json";
8
+ function loadBundledRegistry() {
9
+ try {
10
+ const content = readFileSync(BUNDLED_REGISTRY_PATH, "utf-8");
11
+ return JSON.parse(content);
12
+ } catch (error) {
13
+ throw new Error(`Failed to load bundled registry: ${error.message}`);
14
+ }
15
+ }
16
+ async function fetchRegistry() {
17
+ try {
18
+ const response = await fetch(REMOTE_REGISTRY_URL);
19
+ if (!response.ok) {
20
+ throw new Error(`HTTP ${response.status}`);
21
+ }
22
+ return await response.json();
23
+ } catch {
24
+ return loadBundledRegistry();
25
+ }
26
+ }
27
+ function getComponent(registry, name) {
28
+ const normalizedName = name.toLowerCase();
29
+ return registry.components.find((c) => c.name.toLowerCase() === normalizedName);
30
+ }
31
+ function getComponentNames(registry) {
32
+ return registry.components.map((c) => c.name);
33
+ }
34
+ function getComponentsForFramework(registry, framework) {
35
+ const frameworkMap = {
36
+ react: "react",
37
+ next: "react",
38
+ // Next.js uses React components
39
+ wc: "wc",
40
+ vanilla: "wc"
41
+ // Vanilla uses Web Components
42
+ };
43
+ const targetFramework = frameworkMap[framework];
44
+ return registry.components.filter((c) => c.frameworks.includes(targetFramework));
45
+ }
46
+ function resolveDependencies(registry, componentNames) {
47
+ const resolved = /* @__PURE__ */ new Map();
48
+ const visiting = /* @__PURE__ */ new Set();
49
+ function visit(name) {
50
+ const normalizedName = name.toLowerCase();
51
+ if (resolved.has(normalizedName)) {
52
+ return;
53
+ }
54
+ if (visiting.has(normalizedName)) {
55
+ throw new Error(`Circular dependency detected: ${name}`);
56
+ }
57
+ const component = getComponent(registry, name);
58
+ if (!component) {
59
+ throw new Error(`Component not found in registry: ${name}`);
60
+ }
61
+ visiting.add(normalizedName);
62
+ for (const dep of component.registryDependencies) {
63
+ visit(dep);
64
+ }
65
+ visiting.delete(normalizedName);
66
+ resolved.set(normalizedName, component);
67
+ }
68
+ for (const name of componentNames) {
69
+ visit(name);
70
+ }
71
+ return Array.from(resolved.values());
72
+ }
73
+ function getNpmDependencies(components) {
74
+ const deps = /* @__PURE__ */ new Set();
75
+ for (const component of components) {
76
+ for (const dep of component.dependencies) {
77
+ deps.add(dep);
78
+ }
79
+ }
80
+ return Array.from(deps);
81
+ }
82
+ function isComponentCompatible(component, framework) {
83
+ const frameworkMap = {
84
+ react: "react",
85
+ next: "react",
86
+ wc: "wc",
87
+ vanilla: "wc"
88
+ };
89
+ return component.frameworks.includes(frameworkMap[framework]);
90
+ }
91
+
92
+ export {
93
+ loadBundledRegistry,
94
+ fetchRegistry,
95
+ getComponent,
96
+ getComponentNames,
97
+ getComponentsForFramework,
98
+ resolveDependencies,
99
+ getNpmDependencies,
100
+ isComponentCompatible
101
+ };
@@ -0,0 +1,123 @@
1
+ // src/utils/config.ts
2
+ import { existsSync, readFileSync, writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ var CONFIG_FILE_NAME = "ds.config.json";
5
+ var CONFIG_SCHEMA_URL = "https://hypoth-ui.dev/schema/ds.config.json";
6
+ function configExists(cwd = process.cwd()) {
7
+ return existsSync(join(cwd, CONFIG_FILE_NAME));
8
+ }
9
+ function getConfigPath(cwd = process.cwd()) {
10
+ return join(cwd, CONFIG_FILE_NAME);
11
+ }
12
+ function readConfig(cwd = process.cwd()) {
13
+ const configPath = getConfigPath(cwd);
14
+ if (!existsSync(configPath)) {
15
+ throw new Error(
16
+ `Configuration file not found. Run 'hypoth-ui init' first.`
17
+ );
18
+ }
19
+ try {
20
+ const content = readFileSync(configPath, "utf-8");
21
+ const config = JSON.parse(content);
22
+ return validateConfig(config);
23
+ } catch (error) {
24
+ if (error instanceof SyntaxError) {
25
+ throw new Error(`Invalid JSON in ${CONFIG_FILE_NAME}: ${error.message}`);
26
+ }
27
+ throw error;
28
+ }
29
+ }
30
+ function writeConfig(config, cwd = process.cwd()) {
31
+ const configPath = getConfigPath(cwd);
32
+ const content = JSON.stringify(config, null, 2);
33
+ writeFileSync(configPath, content + "\n", "utf-8");
34
+ }
35
+ function createConfig(options) {
36
+ const config = {
37
+ $schema: CONFIG_SCHEMA_URL,
38
+ style: options.style ?? "package",
39
+ framework: options.framework ?? "react",
40
+ typescript: options.typescript ?? true,
41
+ packageManager: options.packageManager ?? "npm",
42
+ paths: options.paths ?? {
43
+ components: "src/components/ui",
44
+ utils: "src/lib"
45
+ },
46
+ aliases: options.aliases ?? {
47
+ components: "@/components/ui",
48
+ lib: "@/lib"
49
+ },
50
+ components: options.components ?? []
51
+ };
52
+ return config;
53
+ }
54
+ function validateConfig(config) {
55
+ if (!config || typeof config !== "object") {
56
+ throw new Error("Invalid configuration: expected object");
57
+ }
58
+ const cfg = config;
59
+ if (!["copy", "package"].includes(cfg.style)) {
60
+ throw new Error('Invalid configuration: style must be "copy" or "package"');
61
+ }
62
+ if (!["react", "next", "wc", "vanilla"].includes(cfg.framework)) {
63
+ throw new Error(
64
+ 'Invalid configuration: framework must be "react", "next", "wc", or "vanilla"'
65
+ );
66
+ }
67
+ if (typeof cfg.typescript !== "boolean") {
68
+ throw new Error("Invalid configuration: typescript must be a boolean");
69
+ }
70
+ if (!["npm", "pnpm", "yarn", "bun"].includes(cfg.packageManager)) {
71
+ throw new Error(
72
+ 'Invalid configuration: packageManager must be "npm", "pnpm", "yarn", or "bun"'
73
+ );
74
+ }
75
+ if (!cfg.paths || typeof cfg.paths !== "object") {
76
+ throw new Error("Invalid configuration: paths must be an object");
77
+ }
78
+ const paths = cfg.paths;
79
+ if (typeof paths.components !== "string" || typeof paths.utils !== "string") {
80
+ throw new Error(
81
+ "Invalid configuration: paths.components and paths.utils must be strings"
82
+ );
83
+ }
84
+ if (!cfg.aliases || typeof cfg.aliases !== "object") {
85
+ throw new Error("Invalid configuration: aliases must be an object");
86
+ }
87
+ const aliases = cfg.aliases;
88
+ if (typeof aliases.components !== "string" || typeof aliases.lib !== "string") {
89
+ throw new Error(
90
+ "Invalid configuration: aliases.components and aliases.lib must be strings"
91
+ );
92
+ }
93
+ if (!Array.isArray(cfg.components)) {
94
+ throw new Error("Invalid configuration: components must be an array");
95
+ }
96
+ return config;
97
+ }
98
+ function addInstalledComponent(config, component) {
99
+ const now = (/* @__PURE__ */ new Date()).toISOString();
100
+ const filtered = config.components.filter((c) => c.name !== component.name);
101
+ return {
102
+ ...config,
103
+ components: [
104
+ ...filtered,
105
+ {
106
+ ...component,
107
+ installedAt: now
108
+ }
109
+ ]
110
+ };
111
+ }
112
+ function isComponentInstalled(config, name) {
113
+ return config.components.some((c) => c.name === name);
114
+ }
115
+
116
+ export {
117
+ configExists,
118
+ readConfig,
119
+ writeConfig,
120
+ createConfig,
121
+ addInstalledComponent,
122
+ isComponentInstalled
123
+ };