@josui/cli 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 ADDED
@@ -0,0 +1,75 @@
1
+ # @josui/cli
2
+
3
+ CLI tool for linking josui packages and skills to external projects.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @josui/cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ pnpm josui link packages
15
+ pnpm josui link skills
16
+ ```
17
+
18
+ Or with npx:
19
+
20
+ ```bash
21
+ npx josui link packages
22
+ ```
23
+
24
+ ### Running from source
25
+
26
+ If you're developing josui locally, you can run the CLI directly:
27
+
28
+ ```bash
29
+ node <path-to-josui>/packages/cli/dist/index.js
30
+ ```
31
+
32
+ Replace `<path-to-josui>` with the relative path to your josui checkout (e.g., `../josui`).
33
+
34
+ ## Commands
35
+
36
+ ### Link packages
37
+
38
+ Link `@josui/*` packages from your local josui checkout:
39
+
40
+ ```bash
41
+ josui link packages
42
+ ```
43
+
44
+ This replaces npm-installed packages in `node_modules/@josui/` with symlinks to your local josui packages, enabling hot reload during development.
45
+
46
+ ### Link skills
47
+
48
+ Link Claude Code skills from josui packages:
49
+
50
+ ```bash
51
+ josui link skills
52
+ ```
53
+
54
+ This creates symlinks in `.claude/skills/` for selected skills from josui packages.
55
+
56
+ ## Configuration
57
+
58
+ The CLI saves your settings to `.josui.json` in your project root:
59
+
60
+ ```json
61
+ {
62
+ "josuiPath": "../josui",
63
+ "linkedPackages": ["core", "core-web", "react", "tailwind", "tokens"],
64
+ "linkedSkills": [{ "source": "react", "skills": ["use-react-components"] }]
65
+ }
66
+ ```
67
+
68
+ When you run the CLI again, it offers to re-link using the saved config.
69
+
70
+ ## How It Works
71
+
72
+ 1. `pnpm install` creates a clean lockfile with npm versions
73
+ 2. The CLI replaces `node_modules/@josui/*` with symlinks to local packages
74
+ 3. The lockfile stays unchanged — CI works with the committed lockfile
75
+ 4. Config is saved for quick re-linking after future installs
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,381 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { select as select3 } from "@inquirer/prompts";
5
+
6
+ // src/commands/link-packages.ts
7
+ import { input, select, checkbox } from "@inquirer/prompts";
8
+ import { existsSync as existsSync2 } from "fs";
9
+ import { rm, symlink, mkdir } from "fs/promises";
10
+ import { join as join2, relative } from "path";
11
+
12
+ // src/utils/config.ts
13
+ import { readFile, writeFile } from "fs/promises";
14
+ import { existsSync } from "fs";
15
+ import { join } from "path";
16
+ var GITIGNORE_ENTRIES = ["# josui local linking config", ".josui.json", "josui-linked-*"];
17
+ var CONFIG_FILE = ".josui.json";
18
+ function getConfigPath(cwd = process.cwd()) {
19
+ return join(cwd, CONFIG_FILE);
20
+ }
21
+ async function readConfig(cwd = process.cwd()) {
22
+ const configPath = getConfigPath(cwd);
23
+ if (!existsSync(configPath)) {
24
+ return null;
25
+ }
26
+ try {
27
+ const content = await readFile(configPath, "utf-8");
28
+ return JSON.parse(content);
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ async function writeConfig(config, cwd = process.cwd()) {
34
+ const configPath = getConfigPath(cwd);
35
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
36
+ }
37
+ async function updateConfig(updates, cwd = process.cwd()) {
38
+ const existing = await readConfig(cwd) || {};
39
+ const updated = { ...existing, ...updates };
40
+ await writeConfig(updated, cwd);
41
+ return updated;
42
+ }
43
+ async function ensureGitignore(cwd = process.cwd()) {
44
+ const gitignorePath = join(cwd, ".gitignore");
45
+ const content = existsSync(gitignorePath) ? await readFile(gitignorePath, "utf-8") : "";
46
+ const entriesToCheck = GITIGNORE_ENTRIES.filter((e) => !e.startsWith("#"));
47
+ const missingEntries = entriesToCheck.filter((entry) => !content.includes(entry));
48
+ if (missingEntries.length === 0) {
49
+ return false;
50
+ }
51
+ const toAdd = missingEntries.length === entriesToCheck.length ? GITIGNORE_ENTRIES : missingEntries;
52
+ const prefix = content.length > 0 && !content.endsWith("\n\n") ? content.endsWith("\n") ? "\n" : "\n\n" : "";
53
+ await writeFile(gitignorePath, content + prefix + toAdd.join("\n") + "\n");
54
+ return true;
55
+ }
56
+
57
+ // src/commands/link-packages.ts
58
+ var AVAILABLE_PACKAGES = ["core", "core-web", "react", "tailwind", "tokens"];
59
+ async function linkPackages() {
60
+ const cwd = process.cwd();
61
+ const config = await readConfig(cwd);
62
+ if (config?.josuiPath && config?.linkedPackages?.length) {
63
+ const action = await select({
64
+ message: "Found existing configuration. What would you like to do?",
65
+ choices: [
66
+ { value: "relink", name: "Re-link existing packages" },
67
+ { value: "modify", name: "Add or remove packages" },
68
+ { value: "fresh", name: "Start fresh" }
69
+ ]
70
+ });
71
+ if (action === "relink") {
72
+ await performLink(cwd, config.josuiPath, config.linkedPackages);
73
+ return;
74
+ }
75
+ if (action === "fresh") {
76
+ } else {
77
+ const packages2 = await checkbox({
78
+ message: "Select packages to link:",
79
+ choices: AVAILABLE_PACKAGES.map((pkg) => ({
80
+ value: pkg,
81
+ name: `@josui/${pkg}`,
82
+ checked: config.linkedPackages?.includes(pkg)
83
+ }))
84
+ });
85
+ if (packages2.length === 0) {
86
+ console.log("No packages selected. Exiting.");
87
+ return;
88
+ }
89
+ await updateConfig({ linkedPackages: packages2 }, cwd);
90
+ await performLink(cwd, config.josuiPath, packages2);
91
+ return;
92
+ }
93
+ }
94
+ const defaultPath = config?.josuiPath || "../josui";
95
+ const josuiPath = await input({
96
+ message: "Where is your josui monorepo?",
97
+ default: defaultPath,
98
+ validate: (value) => {
99
+ const packagesPath = join2(cwd, value, "packages");
100
+ if (!existsSync2(packagesPath)) {
101
+ return `Could not find packages at ${packagesPath}`;
102
+ }
103
+ return true;
104
+ }
105
+ });
106
+ const packages = await checkbox({
107
+ message: "Select packages to link:",
108
+ choices: AVAILABLE_PACKAGES.map((pkg) => ({
109
+ value: pkg,
110
+ name: `@josui/${pkg}`,
111
+ checked: true
112
+ }))
113
+ });
114
+ if (packages.length === 0) {
115
+ console.log("No packages selected. Exiting.");
116
+ return;
117
+ }
118
+ await updateConfig({ josuiPath, linkedPackages: packages }, cwd);
119
+ await performLink(cwd, josuiPath, packages);
120
+ }
121
+ async function performLink(cwd, josuiPath, packages) {
122
+ const nodeModulesJosui = join2(cwd, "node_modules", "@josui");
123
+ if (!existsSync2(nodeModulesJosui)) {
124
+ await mkdir(nodeModulesJosui, { recursive: true });
125
+ }
126
+ console.log("");
127
+ for (const pkg of packages) {
128
+ const targetPath = join2(nodeModulesJosui, pkg);
129
+ const sourcePath = join2(cwd, josuiPath, "packages", pkg);
130
+ if (existsSync2(targetPath)) {
131
+ await rm(targetPath, { recursive: true, force: true });
132
+ }
133
+ const relativePath = relative(nodeModulesJosui, sourcePath);
134
+ await symlink(relativePath, targetPath);
135
+ console.log(` Linked @josui/${pkg} -> ${relativePath}`);
136
+ }
137
+ console.log(`
138
+ \u2713 Linked ${packages.length} package(s)`);
139
+ console.log("\nConfig saved to .josui.json");
140
+ const addedGitignore = await ensureGitignore(cwd);
141
+ if (addedGitignore) {
142
+ console.log("Added josui entries to .gitignore");
143
+ }
144
+ }
145
+
146
+ // src/commands/link-skills.ts
147
+ import { input as input2, select as select2, checkbox as checkbox2 } from "@inquirer/prompts";
148
+ import { existsSync as existsSync3 } from "fs";
149
+ import { readdir, rm as rm2, symlink as symlink2, mkdir as mkdir2, lstat } from "fs/promises";
150
+ import { join as join3, relative as relative2 } from "path";
151
+ async function linkSkills() {
152
+ const cwd = process.cwd();
153
+ const config = await readConfig(cwd);
154
+ if (config?.linkedSkills?.length) {
155
+ const action = await select2({
156
+ message: "Found existing skill links. What would you like to do?",
157
+ choices: [
158
+ { value: "relink", name: "Re-link existing skills" },
159
+ { value: "modify", name: "Add or remove skills" },
160
+ { value: "fresh", name: "Start fresh" }
161
+ ]
162
+ });
163
+ if (action === "relink") {
164
+ await performSkillLink(cwd, config.linkedSkills);
165
+ return;
166
+ }
167
+ if (action === "modify") {
168
+ console.log("Starting fresh configuration...\n");
169
+ }
170
+ }
171
+ const sourceType = await select2({
172
+ message: "Where are the skills you want to link?",
173
+ choices: [
174
+ { value: "josui", name: "From josui monorepo" },
175
+ { value: "package", name: "From an npm package with skills" },
176
+ { value: "custom", name: "Custom path" }
177
+ ]
178
+ });
179
+ let sourcePath;
180
+ if (sourceType === "josui") {
181
+ const defaultPath = config?.josuiPath || "../josui";
182
+ sourcePath = await input2({
183
+ message: "Where is your josui monorepo?",
184
+ default: defaultPath,
185
+ validate: (value) => {
186
+ const packagesPath = join3(cwd, value, "packages");
187
+ if (!existsSync3(packagesPath)) {
188
+ return `Could not find packages at ${packagesPath}`;
189
+ }
190
+ return true;
191
+ }
192
+ });
193
+ const skillSources = await findSkillsInJosui(cwd, sourcePath);
194
+ if (skillSources.length === 0) {
195
+ console.log("No packages with skills found in josui.");
196
+ return;
197
+ }
198
+ const allSkills = [];
199
+ for (const source of skillSources) {
200
+ for (const skill of source.skills) {
201
+ allSkills.push({
202
+ value: `${source.path}:${skill}`,
203
+ name: `${skill} (from ${source.path})`,
204
+ source: source.path
205
+ });
206
+ }
207
+ }
208
+ const selectedSkills = await checkbox2({
209
+ message: "Select skills to link:",
210
+ choices: allSkills.map((s) => ({
211
+ value: s.value,
212
+ name: s.name,
213
+ checked: true
214
+ }))
215
+ });
216
+ if (selectedSkills.length === 0) {
217
+ console.log("No skills selected. Exiting.");
218
+ return;
219
+ }
220
+ const linkedSkills = [];
221
+ for (const selected of selectedSkills) {
222
+ const [sourcePkg, skillName] = selected.split(":");
223
+ const existing = linkedSkills.find((s) => s.source === sourcePkg);
224
+ if (existing) {
225
+ existing.skills.push(skillName);
226
+ } else {
227
+ linkedSkills.push({ source: sourcePkg, skills: [skillName] });
228
+ }
229
+ }
230
+ await updateConfig({ josuiPath: sourcePath, linkedSkills }, cwd);
231
+ await performSkillLink(cwd, linkedSkills, sourcePath);
232
+ } else {
233
+ sourcePath = await input2({
234
+ message: "Enter the path to the skills directory:",
235
+ validate: (value) => {
236
+ if (!existsSync3(join3(cwd, value))) {
237
+ return `Path does not exist: ${value}`;
238
+ }
239
+ return true;
240
+ }
241
+ });
242
+ const skills = await findSkillsInDir(join3(cwd, sourcePath));
243
+ if (skills.length === 0) {
244
+ console.log("No skills found at that path.");
245
+ return;
246
+ }
247
+ const selectedSkills = await checkbox2({
248
+ message: "Select skills to link:",
249
+ choices: skills.map((s) => ({
250
+ value: s,
251
+ name: s,
252
+ checked: true
253
+ }))
254
+ });
255
+ if (selectedSkills.length === 0) {
256
+ console.log("No skills selected. Exiting.");
257
+ return;
258
+ }
259
+ const linkedSkills = [{ source: sourcePath, skills: selectedSkills }];
260
+ await updateConfig({ linkedSkills }, cwd);
261
+ await performSkillLink(cwd, linkedSkills);
262
+ }
263
+ }
264
+ async function findSkillsInJosui(cwd, josuiPath) {
265
+ const packagesDir = join3(cwd, josuiPath, "packages");
266
+ const entries = await readdir(packagesDir, { withFileTypes: true });
267
+ const sources = [];
268
+ for (const entry of entries) {
269
+ if (!entry.isDirectory()) continue;
270
+ const skillsDir = join3(packagesDir, entry.name, "skills");
271
+ if (!existsSync3(skillsDir)) continue;
272
+ const skills = await findSkillsInDir(skillsDir);
273
+ if (skills.length > 0) {
274
+ sources.push({ path: entry.name, skills });
275
+ }
276
+ }
277
+ return sources;
278
+ }
279
+ async function findSkillsInDir(dir) {
280
+ if (!existsSync3(dir)) return [];
281
+ const entries = await readdir(dir, { withFileTypes: true });
282
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
283
+ }
284
+ async function performSkillLink(cwd, linkedSkills, josuiPath) {
285
+ const targetDir = join3(cwd, ".claude", "skills");
286
+ if (!existsSync3(targetDir)) {
287
+ await mkdir2(targetDir, { recursive: true });
288
+ }
289
+ const existing = await readdir(targetDir, { withFileTypes: true });
290
+ for (const entry of existing) {
291
+ if (entry.name.startsWith("josui-linked-")) {
292
+ const entryPath = join3(targetDir, entry.name);
293
+ const stat = await lstat(entryPath);
294
+ if (stat.isSymbolicLink()) {
295
+ await rm2(entryPath);
296
+ }
297
+ }
298
+ }
299
+ console.log("");
300
+ let linked = 0;
301
+ for (const source of linkedSkills) {
302
+ for (const skill of source.skills) {
303
+ let sourcePath;
304
+ if (josuiPath) {
305
+ sourcePath = join3(cwd, josuiPath, "packages", source.source, "skills", skill);
306
+ } else {
307
+ sourcePath = join3(cwd, source.source, skill);
308
+ }
309
+ const destName = `josui-linked-${source.source}-${skill}`;
310
+ const destPath = join3(targetDir, destName);
311
+ const relativePath = relative2(targetDir, sourcePath);
312
+ await symlink2(relativePath, destPath);
313
+ console.log(` ${destName}/ -> ${relativePath}`);
314
+ linked++;
315
+ }
316
+ }
317
+ console.log(`
318
+ \u2713 Linked ${linked} skill(s) to .claude/skills/`);
319
+ console.log("\nConfig saved to .josui.json");
320
+ const addedGitignore = await ensureGitignore(cwd);
321
+ if (addedGitignore) {
322
+ console.log("Added josui entries to .gitignore");
323
+ }
324
+ }
325
+
326
+ // src/index.ts
327
+ var args = process.argv.slice(2);
328
+ async function main() {
329
+ console.log("\n josui cli\n");
330
+ const command = args[0];
331
+ const subcommand = args[1];
332
+ if (command === "link") {
333
+ if (subcommand === "packages") {
334
+ await linkPackages();
335
+ return;
336
+ }
337
+ if (subcommand === "skills") {
338
+ await linkSkills();
339
+ return;
340
+ }
341
+ const linkType = await select3({
342
+ message: "What would you like to link?",
343
+ choices: [
344
+ { value: "packages", name: "Packages - Link @josui/* packages for local development" },
345
+ { value: "skills", name: "Skills - Link Claude Code skills from josui" }
346
+ ]
347
+ });
348
+ if (linkType === "packages") {
349
+ await linkPackages();
350
+ } else {
351
+ await linkSkills();
352
+ }
353
+ return;
354
+ }
355
+ const action = await select3({
356
+ message: "What would you like to do?",
357
+ choices: [{ value: "link", name: "Link packages or skills for local development" }]
358
+ });
359
+ if (action === "link") {
360
+ const linkType = await select3({
361
+ message: "What would you like to link?",
362
+ choices: [
363
+ { value: "packages", name: "Packages - Link @josui/* packages for local development" },
364
+ { value: "skills", name: "Skills - Link Claude Code skills from josui" }
365
+ ]
366
+ });
367
+ if (linkType === "packages") {
368
+ await linkPackages();
369
+ } else {
370
+ await linkSkills();
371
+ }
372
+ }
373
+ }
374
+ main().catch((err) => {
375
+ if (err.name === "ExitPromptError") {
376
+ console.log("\nCancelled.");
377
+ process.exit(0);
378
+ }
379
+ console.error("Error:", err.message);
380
+ process.exit(1);
381
+ });
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@josui/cli",
3
+ "version": "0.1.1",
4
+ "description": "CLI tools for josui",
5
+ "type": "module",
6
+ "bin": {
7
+ "josui": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "dependencies": {
13
+ "@inquirer/prompts": "^7.0.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^20.0.0",
17
+ "tsup": "^8.0.0",
18
+ "typescript": "^5.7.0",
19
+ "@josui/typescript-config": "0.1.1"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "scripts": {
25
+ "build": "tsup src/index.ts --format esm --dts --clean",
26
+ "dev": "tsup src/index.ts --format esm --watch",
27
+ "lint": "eslint src",
28
+ "typecheck": "tsc --noEmit"
29
+ }
30
+ }