@archon-research/uikit-cli 0.1.0-dev25004766367

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 +69 -0
  2. package/dist/cli.js +250 -0
  3. package/package.json +27 -0
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @archon-research/uikit-cli
2
+
3
+ CLI for managing local uikit package linking in consumer workspaces.
4
+
5
+ ## Installation
6
+
7
+ Once published to GitHub Packages, install globally:
8
+
9
+ ```bash
10
+ npm install -g @archon-research/uikit-cli
11
+ ```
12
+
13
+ For development, link locally:
14
+
15
+ ```bash
16
+ cd /path/to/uikit/packages/uikit-cli
17
+ npm link
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ### Link local uikit packages
23
+
24
+ ```bash
25
+ uikit-cli link
26
+ ```
27
+
28
+ Automatically detects the consumer workspace root by walking up to the nearest `package.json` with a `workspaces` field. Links all `@archon-research/*` packages that the consumer depends on.
29
+
30
+ ### Unlink local packages (restore registry versions)
31
+
32
+ ```bash
33
+ uikit-cli unlink
34
+ ```
35
+
36
+ Attempts to unlink all local uikit packages and restore registered versions. If packages are not yet published, keeps local links in place.
37
+
38
+ ### Working from any subdirectory
39
+
40
+ The CLI works from any directory within a workspace:
41
+
42
+ ```bash
43
+ cd my-consumer/src/explorer
44
+ uikit-cli link # auto-detects workspace root and links packages
45
+ ```
46
+
47
+ ## How it works
48
+
49
+ 1. **Auto-detection**: Walks up from current working directory to find the nearest `package.json` with a `workspaces` field
50
+ 2. **Discovery**: Queries locally available uikit packages and determines which ones are needed by consumer workspaces
51
+ 3. **Linking**: Uses `npm link` to establish local package resolution
52
+ 4. **Graceful fallback**: On unlink, checks if packages are published; if not, keeps local links to prevent breaking changes
53
+
54
+ ## Development workflow
55
+
56
+ In a synome workspace:
57
+
58
+ ```bash
59
+ # Link uikit packages for local development
60
+ npm run uikit:link
61
+
62
+ # Later, restore registry versions (or keep local links if not published)
63
+ npm run uikit:unlink
64
+ ```
65
+
66
+ ## Scripts in synome package.json
67
+
68
+ - `uikit:link` — Uses the CLI with auto-detection
69
+ - `uikit:unlink` — Unlinks using the CLI
package/dist/cli.js ADDED
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from 'node:child_process';
3
+ import { readFileSync, readdirSync } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ function run(command, cwd) {
7
+ console.log(`> (${cwd}) ${command}`);
8
+ execSync(command, { stdio: 'inherit', cwd });
9
+ }
10
+ function tryRun(command, cwd, quiet = false) {
11
+ if (!quiet) {
12
+ console.log(`> (${cwd}) ${command}`);
13
+ }
14
+ try {
15
+ execSync(command, {
16
+ stdio: quiet ? 'ignore' : 'inherit',
17
+ cwd,
18
+ });
19
+ return true;
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ function readJson(filePath) {
26
+ const content = readFileSync(filePath, 'utf8');
27
+ return JSON.parse(content);
28
+ }
29
+ function getWorkspacePatterns(rootDir) {
30
+ const pkg = readJson(path.join(rootDir, 'package.json'));
31
+ const workspaces = pkg.workspaces;
32
+ if (Array.isArray(workspaces)) {
33
+ return workspaces;
34
+ }
35
+ if (workspaces && Array.isArray(workspaces.packages)) {
36
+ return workspaces.packages;
37
+ }
38
+ return [];
39
+ }
40
+ function resolveWorkspacePattern(rootDir, pattern) {
41
+ if (pattern.endsWith('/*')) {
42
+ const base = pattern.slice(0, -2);
43
+ const absBase = path.join(rootDir, base);
44
+ return readdirSync(absBase, { withFileTypes: true })
45
+ .filter((entry) => entry.isDirectory())
46
+ .map((entry) => path.join(base, entry.name));
47
+ }
48
+ return [pattern];
49
+ }
50
+ function loadWorkspaces(rootDir) {
51
+ const patterns = getWorkspacePatterns(rootDir);
52
+ const locations = patterns.flatMap((pattern) => resolveWorkspacePattern(rootDir, pattern));
53
+ return locations
54
+ .map((location) => {
55
+ const pkgPath = path.join(rootDir, location, 'package.json');
56
+ const pkg = readJson(pkgPath);
57
+ return {
58
+ ...pkg,
59
+ location,
60
+ path: path.join(rootDir, location),
61
+ };
62
+ })
63
+ .filter((ws) => Boolean(ws.name));
64
+ }
65
+ function findConsumerRoot(startDir) {
66
+ let current = startDir;
67
+ while (true) {
68
+ const pkgPath = path.join(current, 'package.json');
69
+ try {
70
+ const content = readFileSync(pkgPath, 'utf8');
71
+ const pkg = JSON.parse(content);
72
+ if (pkg.workspaces) {
73
+ return current;
74
+ }
75
+ }
76
+ catch {
77
+ // File not found or parse error, continue up.
78
+ }
79
+ const parent = path.dirname(current);
80
+ if (parent === current) {
81
+ throw new Error('Could not find consumer workspace root (no package.json with workspaces field)');
82
+ }
83
+ current = parent;
84
+ }
85
+ }
86
+ function parseArgs(argv) {
87
+ const args = argv.slice(2);
88
+ const mode = args[0] === 'unlink' ? 'unlink' : 'link';
89
+ let consumerRoot = null;
90
+ for (let i = 1; i < args.length; i += 1) {
91
+ const arg = args[i];
92
+ if (arg === '--consumer-root' && args[i + 1]) {
93
+ consumerRoot = path.resolve(process.cwd(), args[i + 1]);
94
+ i += 1;
95
+ }
96
+ }
97
+ if (!consumerRoot) {
98
+ consumerRoot = findConsumerRoot(process.cwd());
99
+ }
100
+ return { mode, consumerRoot };
101
+ }
102
+ function collectWorkspaceRequirements(workspaces, supportedNames) {
103
+ const neededByWorkspace = new Map();
104
+ for (const ws of workspaces) {
105
+ const fields = [
106
+ ws.dependencies ?? {},
107
+ ws.devDependencies ?? {},
108
+ ws.optionalDependencies ?? {},
109
+ ws.peerDependencies ?? {},
110
+ ];
111
+ const needed = new Set();
112
+ for (const depField of fields) {
113
+ for (const depName of Object.keys(depField)) {
114
+ if (supportedNames.has(depName)) {
115
+ needed.add(depName);
116
+ }
117
+ }
118
+ }
119
+ if (needed.size > 0) {
120
+ neededByWorkspace.set(ws.location, [...needed]);
121
+ }
122
+ }
123
+ return neededByWorkspace;
124
+ }
125
+ function linkLocalPackages(consumerRoot, neededByWorkspace, dirByName) {
126
+ if (neededByWorkspace.size === 0) {
127
+ console.log('No local uikit packages referenced by this consumer workspaces.');
128
+ return;
129
+ }
130
+ const allNames = new Set();
131
+ for (const names of neededByWorkspace.values()) {
132
+ for (const name of names) {
133
+ allNames.add(name);
134
+ }
135
+ }
136
+ const rootPackageArgs = [...allNames]
137
+ .map((name) => dirByName.get(name))
138
+ .filter((pkgDir) => Boolean(pkgDir))
139
+ .map((pkgDir) => `"${pkgDir}"`)
140
+ .join(' ');
141
+ if (rootPackageArgs) {
142
+ run(`npm link ${rootPackageArgs} --package-lock=false --save=false`, consumerRoot);
143
+ }
144
+ 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(' ');
150
+ if (!packageArgs) {
151
+ continue;
152
+ }
153
+ run(`npm link ${packageArgs} --workspace "${workspace}" --package-lock=false --save=false`, consumerRoot);
154
+ }
155
+ }
156
+ function unlinkLocalPackages(consumerRoot, neededByWorkspace) {
157
+ if (neededByWorkspace.size === 0) {
158
+ console.log('No local uikit packages referenced by this consumer workspaces.');
159
+ return;
160
+ }
161
+ const allNames = new Set();
162
+ for (const names of neededByWorkspace.values()) {
163
+ for (const name of names) {
164
+ allNames.add(name);
165
+ }
166
+ }
167
+ for (const name of allNames) {
168
+ const ok = tryRun(`npm unlink "${name}" --package-lock=false --save=false`, consumerRoot);
169
+ if (!ok) {
170
+ console.warn(`Unable to unlink ${name} at root; continuing.`);
171
+ }
172
+ }
173
+ for (const [workspace, names] of neededByWorkspace.entries()) {
174
+ for (const name of names) {
175
+ const ok = tryRun(`npm unlink "${name}" --workspace "${workspace}" --package-lock=false --save=false`, consumerRoot);
176
+ if (!ok) {
177
+ console.warn(`Unable to unlink ${name} in ${workspace}; continuing with restore flow.`);
178
+ }
179
+ }
180
+ }
181
+ }
182
+ function areRegistryPackagesReady(consumerRoot, neededByWorkspace) {
183
+ for (const [workspace, names] of neededByWorkspace.entries()) {
184
+ for (const name of names) {
185
+ const ok = tryRun(`npm_config_min_release_age=0 npm ls ${name} --depth=0 --workspace "${workspace}"`, consumerRoot, true);
186
+ if (!ok) {
187
+ return false;
188
+ }
189
+ }
190
+ }
191
+ return true;
192
+ }
193
+ function arePackagesPublished(consumerRoot, neededByWorkspace) {
194
+ const uniqueNames = new Set();
195
+ for (const names of neededByWorkspace.values()) {
196
+ for (const name of names) {
197
+ uniqueNames.add(name);
198
+ }
199
+ }
200
+ for (const name of uniqueNames) {
201
+ const ok = tryRun(`npm view ${name} version --json`, consumerRoot, true);
202
+ if (!ok) {
203
+ return false;
204
+ }
205
+ }
206
+ return true;
207
+ }
208
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
209
+ const uikitRoot = path.resolve(scriptDir, '../../..');
210
+ try {
211
+ const { mode, consumerRoot } = parseArgs(process.argv);
212
+ const uikitWorkspaces = loadWorkspaces(uikitRoot);
213
+ const consumerWorkspaces = loadWorkspaces(consumerRoot);
214
+ const uikitPackages = uikitWorkspaces.filter((ws) => String(ws.name ?? '').startsWith('@archon-research/'));
215
+ const dirByName = new Map(uikitPackages.map((pkg) => [pkg.name ?? '', pkg.path]));
216
+ dirByName.delete('');
217
+ const supportedNames = new Set(dirByName.keys());
218
+ const neededByWorkspace = collectWorkspaceRequirements(consumerWorkspaces, supportedNames);
219
+ if (mode === 'link') {
220
+ linkLocalPackages(consumerRoot, neededByWorkspace, dirByName);
221
+ console.log('\nLinked local uikit packages into consumer workspaces.');
222
+ process.exit(0);
223
+ }
224
+ if (!arePackagesPublished(consumerRoot, neededByWorkspace)) {
225
+ console.warn('\nRegistry packages are not published yet; keeping local uikit links in place.');
226
+ linkLocalPackages(consumerRoot, neededByWorkspace, dirByName);
227
+ console.log('\nConsumer remains on local uikit links.');
228
+ process.exit(0);
229
+ }
230
+ unlinkLocalPackages(consumerRoot, neededByWorkspace);
231
+ let rootInstallOk = tryRun('npm_config_min_release_age=0 npm install', consumerRoot);
232
+ let installOk = true;
233
+ for (const workspace of neededByWorkspace.keys()) {
234
+ installOk =
235
+ tryRun(`npm_config_min_release_age=0 npm install --workspace "${workspace}"`, consumerRoot) &&
236
+ installOk;
237
+ }
238
+ if (!rootInstallOk ||
239
+ !installOk ||
240
+ !areRegistryPackagesReady(consumerRoot, neededByWorkspace)) {
241
+ console.warn('\nRegistry packages are not fully resolvable; falling back to local uikit linking.');
242
+ linkLocalPackages(consumerRoot, neededByWorkspace, dirByName);
243
+ }
244
+ console.log('\nUnlinked local uikit packages and restored consumer dependencies.');
245
+ }
246
+ catch (error) {
247
+ const message = error instanceof Error ? error.message : String(error);
248
+ console.error(`Error: ${message}`);
249
+ process.exit(1);
250
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@archon-research/uikit-cli",
3
+ "version": "0.1.0-dev25004766367",
4
+ "type": "module",
5
+ "description": "CLI for managing local uikit package linking in consumer workspaces",
6
+ "scripts": {
7
+ "build": "tsc -p tsconfig.json",
8
+ "clean": "rm -rf dist",
9
+ "prepare": "npm run build"
10
+ },
11
+ "bin": {
12
+ "uikit-cli": "./dist/cli.js"
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "devDependencies": {
18
+ "@archon-research/tsconfig": "*",
19
+ "@types/node": "^24.7.2"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "repository": {
25
+ "url": "https://github.com/archon-research/uikit"
26
+ }
27
+ }