@yopem-ui/cli 0.0.2 → 0.1.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 CHANGED
@@ -1,19 +1,137 @@
1
1
  # @yopem-ui/cli
2
2
 
3
3
  Copy Yopem UI components and configure StyleX in an existing React project.
4
- Requires Bun. Until the registry is deployed, run the docs server on
5
- `http://localhost:3100` before using the CLI.
4
+ Requires Bun. Registry requests default to `https://ui.yopem.com/r`.
6
5
 
7
6
  ```sh
8
7
  bunx @yopem-ui/cli init
9
8
  bunx @yopem-ui/cli add button
10
9
  bunx @yopem-ui/cli update button
10
+ bunx @yopem-ui/cli --help
11
+ bunx @yopem-ui/cli --version
11
12
  ```
12
13
 
14
+ No arguments or `--help` (`-h`) prints usage; `--version` (`-v`) prints the
15
+ installed CLI version. These commands need no project and exit successfully.
16
+ Invalid commands or arguments exit with status 1.
17
+
13
18
  `init` supports Vite, TanStack Router, TanStack Start, React Router, Next.js App
14
19
  Router, and Astro. Pass `--framework <name>` if autodetection is ambiguous.
15
20
  Installed source changes stay intact on `init` and `add`; `update` rejects
16
- modified files unless `--force` is supplied.
21
+ modified files unless `--force` is supplied. Installed files and their hashes
22
+ are tracked in `ui.json`.
23
+
24
+ ## Dry run
25
+
26
+ Preview `add` or `update` before installing:
27
+
28
+ ```sh
29
+ bunx @yopem-ui/cli add button --dry-run
30
+ bunx @yopem-ui/cli update button --dry-run
31
+ bunx @yopem-ui/cli update button --dry-run --force
32
+ ```
33
+
34
+ The plan lists file writes (including tracking in `ui.json`), skips, all local
35
+ file conflicts, and runtime/dev dependency additions. Forced source overwrites
36
+ are marked explicitly. As in normal mode, `add` skips modified tracked files;
37
+ `update` reports them as conflicts without `--force`. Conflicts block a real
38
+ install until resolved or forced. Dependency lists show the package arguments
39
+ the installer would request, not package-manager version resolution.
40
+
41
+ Dry runs still read the registry and validate URLs, schemas, paths, and file
42
+ integrity. They write no files, create no directories or temporary files, never
43
+ run a package manager, and do not create or change `ui.json`, project config,
44
+ package manifests, lockfiles, or dependencies. `--cwd` and `--registry` work as
45
+ usual. `init --dry-run` and `initProject({ dryRun: true })` are rejected before
46
+ any changes.
47
+
48
+ Programmatic callers use `installItem(name, { dryRun: true })` through
49
+ `@yopem-ui/cli/install`. The result adds `preview.files` (`path`, `action`,
50
+ optional `reason` and `forced`), `preview.dependencies`, and
51
+ `preview.devDependencies`; `installed` is zero. Normal calls retain their
52
+ `{ installed, skipped }` result. Previewed writes apply when no conflicts block
53
+ the command and files have not changed before a subsequent real install.
54
+
55
+ ## Registry
56
+
57
+ Pass `--registry <URL>` to `init`, `add`, or `update` to use another registry.
58
+ The override applies to that command only; it is not stored in `ui.json`. URLs
59
+ must use HTTPS, except HTTP on `localhost` or loopback addresses. Credentials,
60
+ query strings, and fragments are rejected.
61
+
62
+ For local development, start this repository's docs server with `bun run dev`:
63
+
64
+ ```sh
65
+ bunx @yopem-ui/cli init --registry http://localhost:3100/r
66
+ bunx @yopem-ui/cli add button --registry http://localhost:3100/r
67
+ bunx @yopem-ui/cli update button --registry http://localhost:3100/r
68
+ ```
69
+
70
+ Each request has a 30-second timeout covering connection and JSON body reads.
71
+ Failures report timeout, network, HTTP status, or malformed JSON details without
72
+ writing source, configuration, or dependencies. Programmatic callers can set
73
+ `InstallOptions.registryUrl` and `requestTimeoutMs` (milliseconds).
74
+
75
+ ## Monorepos
76
+
77
+ Target an app from the repository root with `--cwd`. Package manager detection
78
+ uses the app and its workspace root; dependencies stay in the target package.
79
+ Bun, npm, pnpm, and Yarn workspaces are supported.
80
+
81
+ ```sh
82
+ bunx @yopem-ui/cli init --cwd apps/web
83
+ bunx @yopem-ui/cli add button --cwd apps/web
84
+ ```
85
+
86
+ For shared components, use an existing named React package in the same
87
+ workspace. `--ui` is relative to the target app, not the repository root.
88
+
89
+ ```sh
90
+ bunx @yopem-ui/cli init --cwd apps/web --ui ../../packages/ui
91
+ bunx @yopem-ui/cli add button --cwd packages/ui
92
+ bunx @yopem-ui/cli update button --cwd packages/ui
93
+ ```
94
+
95
+ `init --ui` installs base files in the UI package, adds source subpath exports,
96
+ links the app, and configures StyleX to scan both packages. Next.js also gets
97
+ `transpilePackages`. Init registers shared package imports with the Yopem UI
98
+ Oxlint rules, preserving existing rule options and explicit opt-outs. Shared
99
+ source imports use the UI package name, such as `@acme/ui/components/ui/button`;
100
+ later `add` and `update` reuse that name from its `ui.json`. Run `init --ui` for
101
+ each consuming app and keep passing `--ui` when repeating init. Existing
102
+ conflicting exports or build configuration require manual review; init does not
103
+ migrate app-local components.
104
+
105
+ ## Development checks
106
+
107
+ From the repository root, generate registry fixtures and build the lint plugin
108
+ before running CLI tests:
109
+
110
+ ```sh
111
+ bun run registry:build
112
+ bun run --cwd packages/oxlint-plugin build
113
+ bun test packages/cli
114
+ ```
115
+
116
+ `bun run --cwd packages/cli test` also runs the CLI suite. Tests use `bun:test`,
117
+ not browsers; packaged installation checks explicitly pass a local `--registry`
118
+ and serve registry JSON with Bun when port 3100 has no registry server. Registry
119
+ network tests use isolated local Bun servers for successful workflows, header
120
+ and body stalls, malformed JSON, and HTTP/network failures. Command logs and
121
+ configuration artifacts are saved in `packages/cli/test-results/`.
122
+
123
+ `bun test packages/cli/test/dry-run.test.ts` checks real fixture projects with
124
+ local registries, whole-project file/directory snapshots, dependency-call
125
+ tracking, conflict/force previews, shared import prefixes, validation failures,
126
+ and CLI argument rules. Snapshot and preview evidence is saved in
127
+ `step5-dry-run.json` under the results directory.
128
+
129
+ `bun test packages/cli/test/public.test.ts` runs packed CLI production smoke
130
+ checks: help, version matching the packed manifest, dry-run previews, init, add
131
+ Button, lint, and build in Vite and Next.js App Router apps, including shared UI
132
+ workspaces. Requires Node.js for Next.js and network access for fixture
133
+ dependencies. Tests verify build artifacts and compiled StyleX CSS, then save
134
+ `packaged-*.log`, `packaged-*.html`, and `packaged-*.css` evidence.
17
135
 
18
136
  ## Licence
19
137
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yopem-ui/cli",
3
- "version": "0.0.2",
3
+ "version": "0.1.1",
4
4
  "description": "CLI for installing Yopem UI source components and StyleX setup",
5
5
  "keywords": [
6
6
  "cli",
@@ -27,12 +27,14 @@
27
27
  "exports": {
28
28
  "./cli": "./src/cli.ts",
29
29
  "./init": "./src/init.ts",
30
- "./install": "./src/install.ts"
30
+ "./install": "./src/install.ts",
31
+ "./project": "./src/project.ts"
31
32
  },
32
33
  "publishConfig": {
33
34
  "access": "public"
34
35
  },
35
36
  "scripts": {
37
+ "test": "bun test ./test",
36
38
  "typecheck": "tsc --noEmit"
37
39
  },
38
40
  "dependencies": {
package/src/cli.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import type { InitOptions } from "./init"
3
+ import type { InitOptions } from "@yopem-ui/cli/init"
4
+ import type { JsonValue } from "@yopem-ui/cli/install"
4
5
 
5
- import { initProject } from "./init"
6
- import { installItem } from "./install"
6
+ import { initProject } from "@yopem-ui/cli/init"
7
+ import { installItem, isRecord, isString } from "@yopem-ui/cli/install"
8
+ import { resolve } from "node:path"
7
9
 
8
10
  const usage =
9
- "Usage: yopem-ui init [--framework vite|tanstack-router|tanstack-start|react-router|next|astro] | <add|update> <name> [--force]"
11
+ "Usage: yopem-ui [--help | --version] | init [--framework vite|tanstack-router|tanstack-start|react-router|next|astro] [--ui <path>] [--dry-run] [--cwd <path>] [--registry <URL>] | <add|update> <name> [--force] [--dry-run] [--cwd <path>] [--registry <URL>]"
10
12
 
11
13
  function isFramework(
12
14
  value: string | undefined,
@@ -22,47 +24,160 @@ function isFramework(
22
24
  }
23
25
 
24
26
  export async function runCli(args: string[], options: InitOptions = {}) {
25
- const [command, name, ...flags] = args
27
+ const [command, ...rest] = args
28
+
29
+ if (command === undefined || command === "--help" || command === "-h") {
30
+ if (rest.length) throw new Error(usage)
31
+ console.info(usage)
32
+
33
+ return
34
+ }
35
+
36
+ if (command === "--version" || command === "-v") {
37
+ if (rest.length) throw new Error(usage)
38
+
39
+ const manifest: JsonValue = await Bun.file(
40
+ new URL("../package.json", import.meta.url),
41
+ ).json()
42
+
43
+ if (!isRecord(manifest) || !isString(manifest.version)) {
44
+ throw new Error("Invalid CLI package version")
45
+ }
46
+
47
+ console.info(manifest.version)
48
+
49
+ return
50
+ }
51
+
52
+ const positional: string[] = []
53
+ const flags = new Map<string, string>()
54
+
55
+ for (let index = 0; index < rest.length; index++) {
56
+ const argument = rest[index]!
57
+
58
+ if (!argument.startsWith("--")) {
59
+ positional.push(argument)
60
+ continue
61
+ }
62
+
63
+ if (flags.has(argument)) throw new Error(usage)
64
+
65
+ if (argument === "--force" || argument === "--dry-run") {
66
+ flags.set(argument, "true")
67
+ continue
68
+ }
69
+
70
+ if (!["--cwd", "--ui", "--framework", "--registry"].includes(argument))
71
+ throw new Error(usage)
72
+ const value = rest[++index]
73
+
74
+ if (!value || value.startsWith("--")) throw new Error(usage)
75
+ flags.set(argument, value)
76
+ }
77
+
78
+ const cwd = resolve(options.cwd ?? process.cwd(), flags.get("--cwd") ?? ".")
79
+ const registryUrl = flags.get("--registry") ?? options.registryUrl
80
+
81
+ function onWarning(warning: string) {
82
+ console.warn(`Warning: ${warning}`)
83
+ options.onWarning?.(warning)
84
+ }
26
85
 
27
86
  if (command === "init") {
28
- const framework = name === "--framework" ? args[2] : undefined
87
+ const framework = flags.get("--framework")
29
88
 
30
89
  if (
31
- args.length !== 1 &&
32
- !(args.length === 3 && name === "--framework" && isFramework(framework))
33
- ) {
90
+ positional.length ||
91
+ flags.has("--force") ||
92
+ (framework !== undefined && !isFramework(framework))
93
+ )
34
94
  throw new Error(usage)
35
- }
36
95
 
37
96
  const result = await initProject({
38
97
  ...options,
39
- framework: isFramework(framework) ? framework : undefined,
98
+ cwd,
99
+ registryUrl,
100
+ dryRun: flags.has("--dry-run") || options.dryRun,
101
+ onWarning,
102
+ ui: flags.get("--ui") ?? options.ui,
103
+ framework: isFramework(framework) ? framework : options.framework,
40
104
  })
41
105
 
42
- console.info(
43
- `Configured ${result.framework} (${result.configured} file(s))`,
44
- )
106
+ if (result.preview) {
107
+ for (const file of result.preview.files) {
108
+ const label =
109
+ file.action === "write"
110
+ ? "Write"
111
+ : file.action === "delete"
112
+ ? "Delete"
113
+ : "Skip"
114
+
115
+ console.info(
116
+ `${label} ${file.path}${file.reason ? ` (${file.reason})` : ""}`,
117
+ )
118
+ }
119
+
120
+ for (const group of result.preview.dependencies) {
121
+ console.info(
122
+ `Runtime dependencies (${group.cwd}): ${group.dependencies.join(", ") || "none"}`,
123
+ )
124
+ console.info(
125
+ `Dev dependencies (${group.cwd}): ${group.devDependencies.join(", ") || "none"}`,
126
+ )
127
+ }
128
+
129
+ console.info(`Prerequisites: ${result.preview.prerequisites.join("; ")}`)
130
+ console.info(
131
+ `Dry run: ${result.framework}. No files written or package-manager commands run.`,
132
+ )
133
+ } else {
134
+ console.info(
135
+ `Configured ${result.framework} (${result.configured} file(s))`,
136
+ )
137
+ }
45
138
 
46
139
  return result
47
140
  }
48
141
 
49
142
  if (
50
143
  (command !== "add" && command !== "update") ||
51
- !name ||
52
- flags.some((flag) => flag !== "--force")
144
+ positional.length !== 1 ||
145
+ flags.has("--ui") ||
146
+ flags.has("--framework")
53
147
  ) {
54
148
  throw new Error(usage)
55
149
  }
56
150
 
57
- const result = await installItem(name, {
151
+ const result = await installItem(positional[0]!, {
58
152
  ...options,
153
+ cwd,
154
+ registryUrl,
59
155
  mode: command,
60
- force: flags.includes("--force"),
156
+ onWarning,
157
+ force: flags.has("--force"),
158
+ dryRun: flags.has("--dry-run") || options.dryRun,
61
159
  })
62
160
 
63
- console.info(
64
- `Installed ${result.installed} file(s), skipped ${result.skipped}`,
65
- )
161
+ if (result.preview) {
162
+ const { files, dependencies, devDependencies } = result.preview
163
+ const labels = { write: "Write", skip: "Skip", conflict: "Conflict" }
164
+
165
+ for (const file of files) {
166
+ console.info(
167
+ `${labels[file.action]} ${file.reason ?? file.path}${file.forced ? " (forced overwrite)" : ""}`,
168
+ )
169
+ }
170
+
171
+ console.info(`Runtime dependencies: ${dependencies.join(", ") || "none"}`)
172
+ console.info(`Dev dependencies: ${devDependencies.join(", ") || "none"}`)
173
+ console.info(
174
+ `Dry run: ${files.filter((file) => file.action === "write").length} file write(s), ${files.filter((file) => file.action === "skip").length} skip(s), ${files.filter((file) => file.action === "conflict").length} conflict(s). No files written.`,
175
+ )
176
+ } else {
177
+ console.info(
178
+ `Installed ${result.installed} file(s), skipped ${result.skipped}`,
179
+ )
180
+ }
66
181
 
67
182
  return result
68
183
  }