@dofu-lab/simui-cli 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 (3) hide show
  1. package/README.md +148 -0
  2. package/dist/index.js +177 -0
  3. package/package.json +40 -0
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # @simui/cli
2
+
3
+ A CLI to add [SimUI](https://simui.dev) Angular components to your project instantly.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ # Using npx (no install required)
9
+ npx @simui/cli add accordion-01
10
+
11
+ # Or install globally
12
+ npm install -g @simui/cli
13
+ simui add accordion-01
14
+ ```
15
+
16
+ ## Commands
17
+
18
+ ### `simui add <component>`
19
+
20
+ Fetches a component from the SimUI registry and writes it to your project.
21
+
22
+ ```bash
23
+ simui add select-01
24
+ simui add dialog-03
25
+ simui add button-07
26
+ ```
27
+
28
+ **Options:**
29
+
30
+ | Flag | Default | Description |
31
+ |------|---------|-------------|
32
+ | `-p, --path <dir>` | `src/app/components` | Output directory (relative to cwd) |
33
+
34
+ **Examples:**
35
+
36
+ ```bash
37
+ # Default path: src/app/components/accordion-01.component.ts
38
+ simui add accordion-01
39
+
40
+ # Custom output directory
41
+ simui add badge-05 --path src/shared/ui
42
+
43
+ # Short flag alias
44
+ simui add input-12 -p src/features/auth
45
+ ```
46
+
47
+ ## What it does
48
+
49
+ 1. **Fetches** the component TypeScript source from `https://simui.dev/registry/<name>.json`
50
+ 2. **Writes** the file to the resolved output path
51
+ 3. **Detects** required `@spartan-ng/helm/*` and `@ng-icons/*` imports
52
+ 4. **Prompts** you to install them:
53
+ - Spartan UI packages — installed via `npx @spartan-ng/cli@latest add <package>`
54
+ - `@ng-icons/*` packages — installed via your detected package manager (pnpm / yarn / npm)
55
+
56
+ ## Requirements
57
+
58
+ - Node.js ≥ 18
59
+ - An Angular project with [Spartan UI](https://spartan.ng) configured
60
+
61
+ ## Browse components
62
+
63
+ Visit [simui.dev](https://simui.dev) to browse all available components and their previews.
64
+
65
+ ## Publishing
66
+
67
+ Follow these steps to publish a new version of `@simui/cli` to the npm registry.
68
+
69
+ 1. Run tests and linters
70
+
71
+ ```bash
72
+ # from packages/cli
73
+ npm test # if you have tests
74
+ npm run lint # if you have a lint script
75
+ ```
76
+
77
+ 2. Bump the package version (creates a git commit and tag)
78
+
79
+ ```bash
80
+ cd packages/cli
81
+ npm version patch # or `minor` / `major`
82
+ # push the commit and tag afterwards
83
+ ```
84
+
85
+ 3. Update changelog (optional)
86
+
87
+ Edit `packages/cli/CHANGELOG.md` or add release notes for the new version.
88
+
89
+ 4. Build the package
90
+
91
+ ```bash
92
+ cd packages/cli
93
+ npm run build
94
+ ```
95
+
96
+ 5. Dry-run publish / pack
97
+
98
+ ```bash
99
+ # create a tarball for inspection
100
+ npm pack
101
+
102
+ # simulate publish (shows what will be published)
103
+ npm publish --dry-run
104
+ ```
105
+
106
+ 6. Login to npm (if needed)
107
+
108
+ ```bash
109
+ npm whoami || npm login
110
+ ```
111
+
112
+ 7. Publish to npm
113
+
114
+ ```bash
115
+ cd packages/cli
116
+ npm publish --access public
117
+ ```
118
+
119
+ 8. Push changes and tags to the remote repo
120
+
121
+ ```bash
122
+ # from repo root
123
+ git push origin main
124
+ git push --tags
125
+ ```
126
+
127
+ 9. Create a GitHub release (optional)
128
+
129
+ ```bash
130
+ gh release create vX.Y.Z --title "vX.Y.Z" --notes "Short changelog"
131
+ ```
132
+
133
+ 10. Verify installation
134
+
135
+ ```bash
136
+ # try via npx
137
+ npx @simui/cli@latest add accordion-01
138
+
139
+ # or install globally
140
+ npm install -g @simui/cli
141
+ simui add accordion-01
142
+ ```
143
+
144
+ Notes:
145
+ - If your package is scoped (e.g. `@simui/cli`) publishing publicly requires `--access public`.
146
+ - If your npm account has 2FA enabled, you'll be prompted for an OTP when publishing.
147
+ - For CI-based publishing, use an `NPM_TOKEN` and run `npm publish` from the pipeline.
148
+
package/dist/index.js ADDED
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/add.ts
7
+ import * as p from "@clack/prompts";
8
+ import chalk from "chalk";
9
+ import { existsSync as existsSync2, mkdirSync, writeFileSync } from "fs";
10
+ import { join as join2, resolve } from "path";
11
+ import { execSync } from "child_process";
12
+
13
+ // src/utils/registry.ts
14
+ var REGISTRY_BASE_URL = "https://simui.dev/registry";
15
+ async function fetchComponent(name) {
16
+ const url = `${REGISTRY_BASE_URL}/${name}.json`;
17
+ let response;
18
+ try {
19
+ response = await fetch(url);
20
+ } catch {
21
+ throw new Error(`Network error: could not reach ${url}`);
22
+ }
23
+ if (!response.ok) {
24
+ if (response.status === 404 || response.status >= 500) {
25
+ throw new Error(
26
+ `Component "${name}" was not found in the SimUI registry.
27
+ Browse available components at https://simui.dev`
28
+ );
29
+ }
30
+ throw new Error(`Failed to fetch "${name}" (HTTP ${response.status})`);
31
+ }
32
+ let data;
33
+ try {
34
+ data = await response.json();
35
+ } catch {
36
+ throw new Error(`Invalid response from registry for "${name}"`);
37
+ }
38
+ if (typeof data !== "object" || data === null || !("content" in data) || typeof data.content !== "string") {
39
+ throw new Error(`Unexpected registry format for "${name}" \u2014 expected { content: string }`);
40
+ }
41
+ return data.content;
42
+ }
43
+
44
+ // src/utils/deps.ts
45
+ import { existsSync } from "fs";
46
+ import { join } from "path";
47
+ function parseDeps(content) {
48
+ const spartanHelmSet = /* @__PURE__ */ new Set();
49
+ const ngIconsSet = /* @__PURE__ */ new Set();
50
+ const spartanRegex = /from\s+['"]@spartan-ng\/helm\/([a-z0-9-]+)['"]/g;
51
+ let match;
52
+ while ((match = spartanRegex.exec(content)) !== null) {
53
+ spartanHelmSet.add(match[1]);
54
+ }
55
+ const ngIconsRegex = /from\s+['"](@ng-icons\/[a-z0-9-]+)['"]/g;
56
+ while ((match = ngIconsRegex.exec(content)) !== null) {
57
+ ngIconsSet.add(match[1]);
58
+ }
59
+ return {
60
+ spartanHelm: [...spartanHelmSet],
61
+ ngIcons: [...ngIconsSet]
62
+ };
63
+ }
64
+ function detectPackageManager(cwd) {
65
+ if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
66
+ if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
67
+ return "npm";
68
+ }
69
+
70
+ // src/commands/add.ts
71
+ async function addComponent(componentName, options) {
72
+ const cwd = process.cwd();
73
+ p.intro(chalk.bold.cyan(`SimUI`) + chalk.dim(` \u2014 adding ${componentName}`));
74
+ const spinner2 = p.spinner();
75
+ spinner2.start("Fetching component from registry\u2026");
76
+ let content;
77
+ try {
78
+ content = await fetchComponent(componentName);
79
+ spinner2.stop(chalk.green("Component fetched"));
80
+ } catch (err) {
81
+ spinner2.stop(chalk.red("Failed to fetch component"));
82
+ p.outro(chalk.red(String(err instanceof Error ? err.message : err)));
83
+ process.exit(1);
84
+ }
85
+ const outputDir = options.path ? resolve(cwd, options.path) : join2(cwd, "src", "app", "components");
86
+ const outputFile = join2(outputDir, `${componentName}.component.ts`);
87
+ if (existsSync2(outputFile)) {
88
+ const overwrite = await p.confirm({
89
+ message: `${chalk.yellow(outputFile.replace(cwd + "/", ""))} already exists. Overwrite?`,
90
+ initialValue: false
91
+ });
92
+ if (p.isCancel(overwrite) || !overwrite) {
93
+ p.outro(chalk.dim("Aborted."));
94
+ process.exit(0);
95
+ }
96
+ }
97
+ try {
98
+ mkdirSync(outputDir, { recursive: true });
99
+ writeFileSync(outputFile, content, "utf-8");
100
+ } catch (err) {
101
+ p.outro(chalk.red(`Could not write file: ${err instanceof Error ? err.message : err}`));
102
+ process.exit(1);
103
+ }
104
+ const relativePath = outputFile.replace(cwd + "/", "");
105
+ p.log.success(`Created ${chalk.cyan(relativePath)}`);
106
+ const { spartanHelm, ngIcons } = parseDeps(content);
107
+ const totalDeps = spartanHelm.length + ngIcons.length;
108
+ if (totalDeps === 0) {
109
+ p.outro(chalk.green("Done! No extra dependencies detected."));
110
+ return;
111
+ }
112
+ p.log.info(chalk.bold("Dependencies detected in this component:"));
113
+ for (const d of spartanHelm) {
114
+ p.log.message(` ${chalk.cyan("@spartan-ng/helm/")}${d} ${chalk.dim("(via Spartan CLI)")}`);
115
+ }
116
+ for (const d of ngIcons) {
117
+ p.log.message(` ${chalk.cyan(d)}`);
118
+ }
119
+ const shouldInstall = await p.confirm({
120
+ message: `Install ${totalDeps} dependenc${totalDeps === 1 ? "y" : "ies"} now?`,
121
+ initialValue: true
122
+ });
123
+ if (p.isCancel(shouldInstall) || !shouldInstall) {
124
+ p.log.warn("Skipped dependency installation. Install them manually before using the component.");
125
+ p.outro(chalk.dim("Done."));
126
+ return;
127
+ }
128
+ const pm = detectPackageManager(cwd);
129
+ p.log.info(`Using ${chalk.bold(pm)}`);
130
+ for (const helmPkg of spartanHelm) {
131
+ const installSpinner = p.spinner();
132
+ installSpinner.start(`Installing @spartan-ng/helm/${helmPkg}\u2026`);
133
+ try {
134
+ execSync(`npx @spartan-ng/cli@latest add ${helmPkg}`, {
135
+ cwd,
136
+ stdio: "pipe"
137
+ });
138
+ installSpinner.stop(chalk.green(`@spartan-ng/helm/${helmPkg} installed`));
139
+ } catch (err) {
140
+ installSpinner.stop(chalk.red(`Failed to install @spartan-ng/helm/${helmPkg}`));
141
+ const stderr = err instanceof Error && "stderr" in err ? String(err.stderr) : "";
142
+ p.log.warn(chalk.dim(stderr.trim() || String(err)));
143
+ }
144
+ }
145
+ if (ngIcons.length > 0) {
146
+ const ngIconsStr = ngIcons.join(" ");
147
+ const installCmd = pm === "pnpm" ? `pnpm add ${ngIconsStr}` : pm === "yarn" ? `yarn add ${ngIconsStr}` : `npm install ${ngIconsStr}`;
148
+ const installSpinner = p.spinner();
149
+ installSpinner.start(`Installing ${ngIconsStr}\u2026`);
150
+ try {
151
+ execSync(installCmd, { cwd, stdio: "pipe" });
152
+ installSpinner.stop(chalk.green(`${ngIconsStr} installed`));
153
+ } catch (err) {
154
+ installSpinner.stop(chalk.red(`Failed to install ${ngIconsStr}`));
155
+ p.log.warn(`Run manually: ${chalk.bold(installCmd)}`);
156
+ }
157
+ }
158
+ p.outro(chalk.green("All done! Your component is ready to use."));
159
+ }
160
+
161
+ // src/index.ts
162
+ import { createRequire } from "module";
163
+ import { fileURLToPath } from "url";
164
+ import { dirname as dirname2, join as join3 } from "path";
165
+ var require2 = createRequire(import.meta.url);
166
+ var __filename = fileURLToPath(import.meta.url);
167
+ var __dirname = dirname2(__filename);
168
+ var pkg = require2(join3(__dirname, "..", "package.json"));
169
+ var program = new Command();
170
+ program.name("simui").description("Add SimUI Angular components to your project").version(pkg.version);
171
+ program.command("add <component>").description("Fetch a SimUI component from the registry and add it to your project").option(
172
+ "-p, --path <path>",
173
+ "Output directory relative to cwd (default: src/app/components)"
174
+ ).action(async (component, opts) => {
175
+ await addComponent(component, { path: opts.path });
176
+ });
177
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@dofu-lab/simui-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI to add SimUI components to your Angular project",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "bin": {
8
+ "simui": "./dist/index.js"
9
+ },
10
+ "files": ["dist"],
11
+ "scripts": {
12
+ "build": "tsup",
13
+ "dev": "tsup --watch",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "dependencies": {
17
+ "@clack/prompts": "^0.9.1",
18
+ "chalk": "^5.4.1",
19
+ "commander": "^13.1.0"
20
+ },
21
+ "devDependencies": {
22
+ "tsup": "^8.4.0",
23
+ "typescript": "^5.8.3"
24
+ },
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "keywords": [
29
+ "angular",
30
+ "simui",
31
+ "spartan",
32
+ "cli",
33
+ "components"
34
+ ],
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/tdphuong/simui"
39
+ }
40
+ }