@archon-research/uikit-cli 0.2.2 → 0.3.0-rohit-improve-cli.2

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 +36 -10
  2. package/dist/cli.js +256 -16
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -10,12 +10,35 @@ npm install --save-dev @archon-research/uikit-cli
10
10
 
11
11
  ## Usage
12
12
 
13
+ ### Run lint and format tools without downstream installs
14
+
15
+ From any consumer workspace:
16
+
17
+ ```bash
18
+ uikit-cli lint -c ./.oxlintrc.ts src panda.config.ts vite.config.ts
19
+ uikit-cli format -c ./.oxfmtrc.ts --write "src/**/*.ts" "src/**/*.tsx" panda.config.ts vite.config.ts
20
+ ```
21
+
22
+ The CLI runs pinned `oxlint` and `oxfmt` versions internally, so downstream workspaces do not
23
+ need to declare those tool packages directly.
24
+
25
+ ### Register local uikit packages for downstream consumers
26
+
27
+ From the uikit repository:
28
+
29
+ ```bash
30
+ uikit-cli register
31
+ ```
32
+
33
+ This registers local public `@archon-research/*` packages globally via `npm link`, so downstream
34
+ consumer repositories can link by package name without needing package paths.
35
+
13
36
  ### Link uikit packages into a consumer repository
14
37
 
15
38
  From your consumer repository:
16
39
 
17
40
  ```bash
18
- npm run uikit:link
41
+ uikit-cli link
19
42
  ```
20
43
 
21
44
  This command links all `@archon-research/*` packages from your local uikit monorepo into your consumer project, allowing you to develop packages and see changes immediately.
@@ -36,19 +59,21 @@ Add this script to your consumer's `package.json`:
36
59
  When co-development is complete, restore published versions from npm:
37
60
 
38
61
  ```bash
39
- npm run uikit:unlink
62
+ uikit-cli unlink
40
63
  ```
41
64
 
42
65
  ## How it works
43
66
 
44
- The CLI manages workspace package links by:
67
+ The CLI manages local development links by:
45
68
 
46
- 1. Discovering linked `@archon-research` packages in your local uikit monorepo
47
- 2. Creating file links in your consumer repository's `node_modules`
48
- 3. Reversing the process with `unlink` to restore registry-installed packages
69
+ 1. Auto-registering local `@archon-research/*` packages from your uikit checkout
70
+ 2. Linking only consumer workspaces that actually depend on those packages
71
+ 3. Reversing links with `unlink`, restoring registry installs when available
49
72
 
50
73
  The CLI automatically detects the consumer workspace root and all dependent packages, working from any directory within the workspace.
51
74
 
75
+ The CLI auto-discovers the local uikit monorepo for typical sibling-checkout layouts.
76
+
52
77
  ## Requirements
53
78
 
54
79
  - Local clone of the uikit monorepo
@@ -57,19 +82,20 @@ The CLI automatically detects the consumer workspace root and all dependent pack
57
82
  ## See also
58
83
 
59
84
  - [Development guide](../../DEVELOPMENT.md#local-co-development-with-a-consumer-repository) for detailed local development workflow
60
- 3. **Linking**: Uses `npm link` to establish local package resolution
61
- 4. **Graceful fallback**: On unlink, checks if packages are published; if not, keeps local links to prevent breaking changes
62
85
 
63
86
  ## Development workflow
64
87
 
65
88
  In a synome workspace:
66
89
 
67
90
  ```bash
91
+ # Optional: run once in uikit repo to pre-register packages
92
+ uikit-cli register
93
+
68
94
  # Link uikit packages for local development
69
- npm run uikit:link
95
+ uikit-cli link
70
96
 
71
97
  # Later, restore registry versions (or keep local links if not published)
72
- npm run uikit:unlink
98
+ uikit-cli unlink
73
99
  ```
74
100
 
75
101
  ## Scripts in synome package.json
package/dist/cli.js CHANGED
@@ -1,8 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync } from 'node:child_process';
3
- import { readFileSync, readdirSync } from 'node:fs';
3
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, symlinkSync, } from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
+ const OXLINT_VERSION = '1.62.0';
7
+ const OXFMT_VERSION = '0.47.0';
8
+ function shellEscape(value) {
9
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
10
+ }
6
11
  function run(command, cwd) {
7
12
  console.log(`> (${cwd}) ${command}`);
8
13
  execSync(command, { stdio: 'inherit', cwd });
@@ -22,6 +27,52 @@ function tryRun(command, cwd, quiet = false) {
22
27
  return false;
23
28
  }
24
29
  }
30
+ function removeWorkspaceShadowInstall(consumerRoot, workspace, packageName) {
31
+ const packagePath = path.join(consumerRoot, workspace, 'node_modules', packageName);
32
+ if (!existsSync(packagePath)) {
33
+ return;
34
+ }
35
+ try {
36
+ const stats = lstatSync(packagePath);
37
+ if (stats.isSymbolicLink()) {
38
+ return;
39
+ }
40
+ // Remove workspace-local regular install so Node resolves to linked root package.
41
+ rmSync(packagePath, { recursive: true, force: true });
42
+ console.log(`Removed shadow install at ${workspace}/node_modules/${packageName} to preserve local links.`);
43
+ }
44
+ catch {
45
+ // Best effort cleanup; linking may still succeed via root resolution.
46
+ }
47
+ }
48
+ function ensureLinkedPath(consumerRoot, packageName, expectedTarget) {
49
+ const packagePath = path.join(consumerRoot, 'node_modules', packageName);
50
+ try {
51
+ const stats = lstatSync(packagePath);
52
+ if (stats.isSymbolicLink() && realpathSync(packagePath) === expectedTarget) {
53
+ return;
54
+ }
55
+ rmSync(packagePath, { recursive: true, force: true });
56
+ }
57
+ catch {
58
+ // Path may not exist yet; continue and create symlink.
59
+ }
60
+ mkdirSync(path.dirname(packagePath), { recursive: true });
61
+ symlinkSync(expectedTarget, packagePath, 'dir');
62
+ }
63
+ function clearWorkspaceViteCache(consumerRoot, workspace) {
64
+ const viteCachePath = path.join(consumerRoot, workspace, 'node_modules', '.vite');
65
+ if (!existsSync(viteCachePath)) {
66
+ return;
67
+ }
68
+ try {
69
+ rmSync(viteCachePath, { recursive: true, force: true });
70
+ console.log(`Cleared Vite cache at ${workspace}/node_modules/.vite.`);
71
+ }
72
+ catch {
73
+ // Best-effort cache cleanup; linking still succeeds without this.
74
+ }
75
+ }
25
76
  function readJson(filePath) {
26
77
  const content = readFileSync(filePath, 'utf8');
27
78
  return JSON.parse(content);
@@ -83,21 +134,179 @@ function findConsumerRoot(startDir) {
83
134
  current = parent;
84
135
  }
85
136
  }
137
+ function findUIKitRoot(startDir) {
138
+ let current = startDir;
139
+ while (true) {
140
+ if (isValidUIKitRoot(current)) {
141
+ return current;
142
+ }
143
+ const parent = path.dirname(current);
144
+ if (parent === current) {
145
+ return null;
146
+ }
147
+ current = parent;
148
+ }
149
+ }
150
+ function isValidUIKitRoot(rootDir) {
151
+ try {
152
+ const pkgPath = path.join(rootDir, 'package.json');
153
+ const pkg = readJson(pkgPath);
154
+ if (!pkg.workspaces) {
155
+ return false;
156
+ }
157
+ const workspaces = loadWorkspaces(rootDir);
158
+ return workspaces.some((ws) => ws.name === '@archon-research/design-system');
159
+ }
160
+ catch {
161
+ return false;
162
+ }
163
+ }
164
+ function findUIKitRootFromConsumer(consumerRoot) {
165
+ const candidateNames = ['uikit'];
166
+ let current = consumerRoot;
167
+ while (true) {
168
+ for (const candidateName of candidateNames) {
169
+ const candidate = path.join(current, candidateName);
170
+ if (isValidUIKitRoot(candidate)) {
171
+ return candidate;
172
+ }
173
+ }
174
+ const parent = path.dirname(current);
175
+ if (parent === current) {
176
+ return null;
177
+ }
178
+ current = parent;
179
+ }
180
+ }
181
+ function resolveUIKitRoot(explicitUIKitRoot, consumerRoot) {
182
+ if (explicitUIKitRoot) {
183
+ const resolved = path.resolve(process.cwd(), explicitUIKitRoot);
184
+ if (!isValidUIKitRoot(resolved)) {
185
+ throw new Error(`Invalid uikit root: ${resolved}. Expected a workspace root containing @archon-research/design-system.`);
186
+ }
187
+ return resolved;
188
+ }
189
+ const scriptRelativeRoot = path.resolve(scriptDir, '../../..');
190
+ if (isValidUIKitRoot(scriptRelativeRoot)) {
191
+ return scriptRelativeRoot;
192
+ }
193
+ if (consumerRoot) {
194
+ const discovered = findUIKitRootFromConsumer(consumerRoot);
195
+ if (discovered) {
196
+ return discovered;
197
+ }
198
+ }
199
+ const fromCwd = findUIKitRoot(process.cwd());
200
+ if (fromCwd) {
201
+ return fromCwd;
202
+ }
203
+ throw new Error('Could not locate local uikit workspace automatically. Run from a consumer workspace near your uikit checkout, or pass --uikit-root / set UIKIT_ROOT.');
204
+ }
86
205
  function parseArgs(argv) {
87
206
  const args = argv.slice(2);
88
- const mode = args[0] === 'unlink' ? 'unlink' : 'link';
207
+ const command = args[0];
208
+ let mode;
209
+ if (!command || command === 'link') {
210
+ mode = 'link';
211
+ }
212
+ else if (command === 'unlink') {
213
+ mode = 'unlink';
214
+ }
215
+ else if (command === 'register') {
216
+ mode = 'register';
217
+ }
218
+ else if (command === 'lint') {
219
+ mode = 'lint';
220
+ }
221
+ else if (command === 'format') {
222
+ mode = 'format';
223
+ }
224
+ else {
225
+ throw new Error(`Unknown command: ${command}. Expected one of: link, unlink, register, lint, format.`);
226
+ }
227
+ const commandArgs = args.slice(1);
89
228
  let consumerRoot = null;
229
+ let uikitRoot = process.env.UIKIT_ROOT ?? null;
90
230
  for (let i = 1; i < args.length; i += 1) {
91
231
  const arg = args[i];
92
232
  if (arg === '--consumer-root' && args[i + 1]) {
93
233
  consumerRoot = path.resolve(process.cwd(), args[i + 1]);
94
234
  i += 1;
235
+ continue;
236
+ }
237
+ if (arg === '--uikit-root' && args[i + 1]) {
238
+ uikitRoot = args[i + 1];
239
+ i += 1;
95
240
  }
96
241
  }
97
- if (!consumerRoot) {
242
+ if (mode !== 'register' &&
243
+ mode !== 'lint' &&
244
+ mode !== 'format' &&
245
+ !consumerRoot) {
98
246
  consumerRoot = findConsumerRoot(process.cwd());
99
247
  }
100
- return { mode, consumerRoot };
248
+ if (mode === 'lint' || mode === 'format') {
249
+ return {
250
+ mode,
251
+ consumerRoot: null,
252
+ uikitRoot: '',
253
+ commandArgs,
254
+ };
255
+ }
256
+ return {
257
+ mode,
258
+ consumerRoot,
259
+ uikitRoot: resolveUIKitRoot(uikitRoot, consumerRoot),
260
+ commandArgs,
261
+ };
262
+ }
263
+ function runLint(commandArgs) {
264
+ const forwarded = commandArgs.map(shellEscape).join(' ');
265
+ run(`npm exec --yes --package oxlint@${OXLINT_VERSION} -- oxlint ${forwarded}`.trim(), process.cwd());
266
+ }
267
+ function hasConfigFlag(args) {
268
+ for (let i = 0; i < args.length; i += 1) {
269
+ const arg = args[i];
270
+ if (arg === '-c' || arg === '--config' || arg.startsWith('--config=')) {
271
+ return true;
272
+ }
273
+ }
274
+ return false;
275
+ }
276
+ function runFormat(commandArgs) {
277
+ const args = [...commandArgs];
278
+ const defaultConfig = './.oxfmtrc.ts';
279
+ if (!hasConfigFlag(args) && existsSync(path.join(process.cwd(), '.oxfmtrc.ts'))) {
280
+ args.unshift(defaultConfig);
281
+ args.unshift('-c');
282
+ }
283
+ const forwarded = args.map(shellEscape).join(' ');
284
+ run(`npm exec --yes --package oxfmt@${OXFMT_VERSION} -- oxfmt ${forwarded}`.trim(), process.cwd());
285
+ }
286
+ function registerLocalPackages(uikitRoot, supportedNames) {
287
+ const uikitWorkspaces = loadWorkspaces(uikitRoot);
288
+ const uikitPackages = uikitWorkspaces.filter((ws) => String(ws.name ?? '').startsWith('@archon-research/') &&
289
+ !ws.private &&
290
+ (!supportedNames || supportedNames.has(String(ws.name))));
291
+ if (uikitPackages.length === 0) {
292
+ console.log('No public @archon-research packages found in this uikit workspace.');
293
+ return;
294
+ }
295
+ for (const pkg of uikitPackages) {
296
+ if (!pkg.name) {
297
+ continue;
298
+ }
299
+ // Ensure CLI bin target exists before linking globally.
300
+ if (pkg.name === '@archon-research/uikit-cli' &&
301
+ !existsSync(path.join(pkg.path, 'dist', 'cli.js'))) {
302
+ run('npm run build', pkg.path);
303
+ }
304
+ run('npm link', pkg.path);
305
+ }
306
+ console.log('\nRegistered local uikit packages for downstream consumers.');
307
+ }
308
+ function linkCliIntoConsumer(consumerRoot) {
309
+ run('npm link "@archon-research/uikit-cli" --package-lock=false --save=false', consumerRoot);
101
310
  }
102
311
  function collectWorkspaceRequirements(workspaces, supportedNames) {
103
312
  const neededByWorkspace = new Map();
@@ -133,24 +342,30 @@ function linkLocalPackages(consumerRoot, neededByWorkspace, dirByName) {
133
342
  allNames.add(name);
134
343
  }
135
344
  }
136
- const rootPackageArgs = [...allNames]
137
- .map((name) => dirByName.get(name))
138
- .filter((pkgDir) => Boolean(pkgDir))
139
- .map((pkgDir) => `"${pkgDir}"`)
140
- .join(' ');
345
+ const rootPackageArgs = [...allNames].map((name) => `"${name}"`).join(' ');
141
346
  if (rootPackageArgs) {
142
347
  run(`npm link ${rootPackageArgs} --package-lock=false --save=false`, consumerRoot);
143
348
  }
349
+ // npm can materialize regular installs in some workspace setups; enforce root links.
350
+ for (const name of allNames) {
351
+ const target = dirByName.get(name);
352
+ if (!target) {
353
+ continue;
354
+ }
355
+ ensureLinkedPath(consumerRoot, name, target);
356
+ }
144
357
  for (const [workspace, names] of neededByWorkspace.entries()) {
145
- const packageArgs = names
146
- .map((name) => dirByName.get(name))
147
- .filter((pkgDir) => Boolean(pkgDir))
148
- .map((pkgDir) => `"${pkgDir}"`)
149
- .join(' ');
358
+ const packageArgs = names.map((name) => `"${name}"`).join(' ');
150
359
  if (!packageArgs) {
151
360
  continue;
152
361
  }
153
362
  run(`npm link ${packageArgs} --workspace "${workspace}" --package-lock=false --save=false`, consumerRoot);
363
+ // npm --workspace link can leave a regular install in some workspace layouts.
364
+ // Removing workspace shadow installs keeps resolution aligned to linked root packages.
365
+ for (const name of names) {
366
+ removeWorkspaceShadowInstall(consumerRoot, workspace, name);
367
+ }
368
+ clearWorkspaceViteCache(consumerRoot, workspace);
154
369
  }
155
370
  }
156
371
  function unlinkLocalPackages(consumerRoot, neededByWorkspace) {
@@ -171,12 +386,19 @@ function unlinkLocalPackages(consumerRoot, neededByWorkspace) {
171
386
  }
172
387
  }
173
388
  for (const [workspace, names] of neededByWorkspace.entries()) {
389
+ const workspaceDir = path.join(consumerRoot, workspace);
174
390
  for (const name of names) {
175
391
  const ok = tryRun(`npm unlink "${name}" --workspace "${workspace}" --package-lock=false --save=false`, consumerRoot);
176
392
  if (!ok) {
177
393
  console.warn(`Unable to unlink ${name} in ${workspace}; continuing with restore flow.`);
178
394
  }
395
+ // Fallback for cases where --workspace unlink does not clear workspace-local links.
396
+ const fallbackOk = tryRun(`npm unlink "${name}" --package-lock=false --save=false`, workspaceDir, true);
397
+ if (!fallbackOk) {
398
+ console.warn(`Unable to unlink ${name} directly in ${workspace}; continuing with restore flow.`);
399
+ }
179
400
  }
401
+ clearWorkspaceViteCache(consumerRoot, workspace);
180
402
  }
181
403
  }
182
404
  function areRegistryPackagesReady(consumerRoot, neededByWorkspace) {
@@ -206,9 +428,23 @@ function arePackagesPublished(consumerRoot, neededByWorkspace) {
206
428
  return true;
207
429
  }
208
430
  const scriptDir = path.dirname(fileURLToPath(import.meta.url));
209
- const uikitRoot = path.resolve(scriptDir, '../../..');
210
431
  try {
211
- const { mode, consumerRoot } = parseArgs(process.argv);
432
+ const { mode, consumerRoot, uikitRoot, commandArgs } = parseArgs(process.argv);
433
+ if (mode === 'lint') {
434
+ runLint(commandArgs);
435
+ process.exit(0);
436
+ }
437
+ if (mode === 'format') {
438
+ runFormat(commandArgs);
439
+ process.exit(0);
440
+ }
441
+ if (mode === 'register') {
442
+ registerLocalPackages(uikitRoot);
443
+ process.exit(0);
444
+ }
445
+ if (!consumerRoot) {
446
+ throw new Error('Consumer root is required for link/unlink operations.');
447
+ }
212
448
  const uikitWorkspaces = loadWorkspaces(uikitRoot);
213
449
  const consumerWorkspaces = loadWorkspaces(consumerRoot);
214
450
  const uikitPackages = uikitWorkspaces.filter((ws) => String(ws.name ?? '').startsWith('@archon-research/'));
@@ -216,6 +452,8 @@ try {
216
452
  dirByName.delete('');
217
453
  const supportedNames = new Set(dirByName.keys());
218
454
  const neededByWorkspace = collectWorkspaceRequirements(consumerWorkspaces, supportedNames);
455
+ registerLocalPackages(uikitRoot, supportedNames);
456
+ linkCliIntoConsumer(consumerRoot);
219
457
  if (mode === 'link') {
220
458
  linkLocalPackages(consumerRoot, neededByWorkspace, dirByName);
221
459
  console.log('\nLinked local uikit packages into consumer workspaces.');
@@ -241,6 +479,8 @@ try {
241
479
  console.warn('\nRegistry packages are not fully resolvable; falling back to local uikit linking.');
242
480
  linkLocalPackages(consumerRoot, neededByWorkspace, dirByName);
243
481
  }
482
+ // Keep consumer on the local CLI implementation so repeated link/unlink stays stable.
483
+ linkCliIntoConsumer(consumerRoot);
244
484
  console.log('\nUnlinked local uikit packages and restored consumer dependencies.');
245
485
  }
246
486
  catch (error) {
package/package.json CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "@archon-research/uikit-cli",
3
+ "version": "0.3.0-rohit-improve-cli.2",
3
4
  "type": "module",
4
5
  "description": "CLI for managing local uikit package linking in consumer workspaces",
5
6
  "scripts": {
@@ -21,7 +22,6 @@
21
22
  "files": [
22
23
  "dist"
23
24
  ],
24
- "version": "0.2.2",
25
25
  "repository": {
26
26
  "url": "https://github.com/archon-research/uikit"
27
27
  }