admin-ui-starter-kit 0.1.1 → 0.1.3

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.
@@ -0,0 +1,326 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * audit-consumer — checks a consuming app for common admin-ui-starter-kit
4
+ * migration mistakes:
5
+ *
6
+ * - stale copied import paths such as @/components/ui/base/buttons
7
+ * - invalid package imports not listed in package.json exports
8
+ * - local files that only re-export package components
9
+ *
10
+ * Run from a consumer app:
11
+ *
12
+ * npx admin-ui-starter-kit-audit
13
+ * npx admin-ui-starter-kit-audit --fix
14
+ */
15
+ import { promises as fs } from 'node:fs';
16
+ import path from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const PACKAGE_NAME = 'admin-ui-starter-kit';
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const PACKAGE_ROOT = path.resolve(path.dirname(__filename), '..');
22
+ const PACKAGE_JSON = JSON.parse(
23
+ await fs.readFile(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'),
24
+ );
25
+ const PUBLIC_EXPORTS = new Set(Object.keys(PACKAGE_JSON.exports ?? {}));
26
+
27
+ const DEFAULT_PATHS = ['src', 'app', 'resources/js', 'components', 'pages'];
28
+ const CODE_EXTENSIONS = new Set(['.cjs', '.cts', '.js', '.jsx', '.mjs', '.mts', '.ts', '.tsx']);
29
+ const SKIP_DIRS = new Set([
30
+ '.git',
31
+ '.next',
32
+ '.nuxt',
33
+ '.turbo',
34
+ '.vercel',
35
+ 'build',
36
+ 'coverage',
37
+ 'dist',
38
+ 'node_modules',
39
+ 'public',
40
+ 'vendor',
41
+ ]);
42
+
43
+ function parseArgs(argv) {
44
+ const opts = { fix: false, json: false, help: false, paths: [] };
45
+ for (const arg of argv.slice(2)) {
46
+ if (arg === '--fix') {
47
+ opts.fix = true;
48
+ } else if (arg === '--json') {
49
+ opts.json = true;
50
+ } else if (arg === '--help' || arg === '-h') {
51
+ opts.help = true;
52
+ } else if (arg.startsWith('--paths=')) {
53
+ opts.paths.push(...arg.slice('--paths='.length).split(',').filter(Boolean));
54
+ } else if (arg.startsWith('-')) {
55
+ throw new Error(`Unknown option: ${arg}`);
56
+ } else {
57
+ opts.paths.push(arg);
58
+ }
59
+ }
60
+ return opts;
61
+ }
62
+
63
+ function printHelp() {
64
+ console.log(
65
+ `Usage: npx ${PACKAGE_NAME}-audit [options] [paths...]\n\n` +
66
+ `Audits a consuming app for stale local UI imports, invalid package subpaths,\n` +
67
+ `and local files that only mirror ${PACKAGE_NAME} exports.\n\n` +
68
+ `Options:\n` +
69
+ ` --fix Rewrite known copied import paths to package imports\n` +
70
+ ` --paths=a,b Comma-separated paths to scan\n` +
71
+ ` --json Print machine-readable JSON\n` +
72
+ ` --help, -h Show this message\n`,
73
+ );
74
+ }
75
+
76
+ async function exists(p) {
77
+ try {
78
+ await fs.access(p);
79
+ return true;
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+
85
+ async function walk(dir, files = []) {
86
+ const entries = await fs.readdir(dir, { withFileTypes: true });
87
+ for (const entry of entries) {
88
+ if (SKIP_DIRS.has(entry.name)) continue;
89
+ const full = path.join(dir, entry.name);
90
+ if (entry.isDirectory()) {
91
+ await walk(full, files);
92
+ } else if (entry.isFile() && CODE_EXTENSIONS.has(path.extname(entry.name))) {
93
+ files.push(full);
94
+ }
95
+ }
96
+ return files;
97
+ }
98
+
99
+ async function getScanFiles(cwd, requestedPaths) {
100
+ const roots = requestedPaths.length > 0 ? requestedPaths : DEFAULT_PATHS;
101
+ const files = [];
102
+ for (const relative of roots) {
103
+ const full = path.resolve(cwd, relative);
104
+ if (!(await exists(full))) continue;
105
+ const stat = await fs.stat(full);
106
+ if (stat.isDirectory()) {
107
+ await walk(full, files);
108
+ } else if (stat.isFile() && CODE_EXTENSIONS.has(path.extname(full))) {
109
+ files.push(full);
110
+ }
111
+ }
112
+ return [...new Set(files)].sort();
113
+ }
114
+
115
+ function getImportSpecifiers(source) {
116
+ const specifiers = [];
117
+ const patterns = [
118
+ /\bimport\s+(?:type\s+)?(?:[^'"]*?\s+from\s+)?['"]([^'"]+)['"]/g,
119
+ /\bexport\s+(?:type\s+)?[^'"]*?\s+from\s+['"]([^'"]+)['"]/g,
120
+ /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
121
+ ];
122
+ for (const pattern of patterns) {
123
+ for (const match of source.matchAll(pattern)) specifiers.push(match[1]);
124
+ }
125
+ return specifiers;
126
+ }
127
+
128
+ function normalizePackageKey(specifier) {
129
+ if (specifier === PACKAGE_NAME) return '.';
130
+ if (!specifier.startsWith(`${PACKAGE_NAME}/`)) return null;
131
+ return `.${specifier.slice(PACKAGE_NAME.length)}`;
132
+ }
133
+
134
+ function packageImportIssue(specifier) {
135
+ const key = normalizePackageKey(specifier);
136
+ if (key === null) return null;
137
+ if (PUBLIC_EXPORTS.has(key)) return null;
138
+ return {
139
+ kind: 'invalid-package-import',
140
+ specifier,
141
+ message: `${specifier} is not a public ${PACKAGE_NAME} export`,
142
+ };
143
+ }
144
+
145
+ function mapOldSpecifier(specifier) {
146
+ const typographyPrefixes = [
147
+ '@/components/ui/base/typography',
148
+ '@/components/ui/typography',
149
+ '@/components/typography',
150
+ ];
151
+ if (typographyPrefixes.some((prefix) => specifier === prefix || specifier.startsWith(`${prefix}/`))) {
152
+ return `${PACKAGE_NAME}/typography`;
153
+ }
154
+
155
+ const layerMappings = [
156
+ { prefix: '@/components/ui/base/', target: 'base' },
157
+ { prefix: '@/components/base/', target: 'base' },
158
+ { prefix: '@/components/composed/', target: 'composed' },
159
+ { prefix: '@/components/features/', target: 'features' },
160
+ { prefix: '@/components/layout/', target: 'layout' },
161
+ ];
162
+
163
+ for (const mapping of layerMappings) {
164
+ if (!specifier.startsWith(mapping.prefix)) continue;
165
+ const rest = specifier.slice(mapping.prefix.length);
166
+ const parts = rest.split('/').filter(Boolean);
167
+ if (parts.length === 0) continue;
168
+
169
+ if (mapping.target === 'base' && parts[0] === 'display' && parts[1] === 'metadata') {
170
+ return `${PACKAGE_NAME}/base/display/metadata`;
171
+ }
172
+
173
+ const candidate = `${PACKAGE_NAME}/${mapping.target}/${parts[0]}`;
174
+ if (PUBLIC_EXPORTS.has(normalizePackageKey(candidate))) return candidate;
175
+ }
176
+
177
+ if (specifier === '@/components/layout') return `${PACKAGE_NAME}/layout`;
178
+
179
+ return null;
180
+ }
181
+
182
+ function isKnownCopiedImport(specifier) {
183
+ return mapOldSpecifier(specifier) !== null;
184
+ }
185
+
186
+ function isMirrorFile(source) {
187
+ const withoutComments = source
188
+ .replace(/\/\*[\s\S]*?\*\//g, '')
189
+ .replace(/^\s*\/\/.*$/gm, '')
190
+ .trim();
191
+ if (!withoutComments) return false;
192
+
193
+ const statements = withoutComments
194
+ .split(';')
195
+ .map((statement) => statement.trim())
196
+ .filter(Boolean);
197
+ if (statements.length === 0) return false;
198
+
199
+ return statements.every((statement) =>
200
+ /^export\s+(?:type\s+)?(?:\{[\s\S]*\}|\*)\s+from\s+['"]admin-ui-starter-kit(?:\/[^'"]+)?['"]$/m.test(
201
+ statement,
202
+ ),
203
+ );
204
+ }
205
+
206
+ function replaceSpecifiers(source) {
207
+ let changed = false;
208
+ const next = source.replace(
209
+ /(['"])(@\/components\/(?:ui\/base|ui\/typography|base|typography|composed|features|layout)(?:\/[^'"]*)?)\1/g,
210
+ (match, quote, specifier) => {
211
+ const replacement = mapOldSpecifier(specifier);
212
+ if (!replacement) return match;
213
+ changed = true;
214
+ return `${quote}${replacement}${quote}`;
215
+ },
216
+ );
217
+ return { changed, source: next };
218
+ }
219
+
220
+ async function audit(opts) {
221
+ const cwd = process.cwd();
222
+ const files = await getScanFiles(cwd, opts.paths);
223
+ const issues = [];
224
+ const fixedFiles = [];
225
+
226
+ for (const file of files) {
227
+ const source = await fs.readFile(file, 'utf8');
228
+ const relative = path.relative(cwd, file);
229
+ const specifiers = getImportSpecifiers(source);
230
+
231
+ for (const specifier of specifiers) {
232
+ if (isKnownCopiedImport(specifier)) {
233
+ issues.push({
234
+ kind: 'stale-local-import',
235
+ file: relative,
236
+ specifier,
237
+ suggested: mapOldSpecifier(specifier),
238
+ message: `${specifier} should import directly from ${PACKAGE_NAME}`,
239
+ });
240
+ }
241
+
242
+ const invalidPackageImport = packageImportIssue(specifier);
243
+ if (invalidPackageImport) {
244
+ issues.push({ ...invalidPackageImport, file: relative });
245
+ }
246
+ }
247
+
248
+ if (isMirrorFile(source)) {
249
+ issues.push({
250
+ kind: 'local-package-mirror',
251
+ file: relative,
252
+ message: 'local file only re-exports admin-ui-starter-kit; replace call-site imports directly',
253
+ });
254
+ }
255
+
256
+ if (opts.fix) {
257
+ const replacement = replaceSpecifiers(source);
258
+ if (replacement.changed) {
259
+ await fs.writeFile(file, replacement.source);
260
+ fixedFiles.push(relative);
261
+ }
262
+ }
263
+ }
264
+
265
+ return { filesScanned: files.length, issues, fixedFiles: [...new Set(fixedFiles)].sort() };
266
+ }
267
+
268
+ function printReport(result, opts) {
269
+ if (opts.json) {
270
+ console.log(JSON.stringify(result, null, 2));
271
+ return;
272
+ }
273
+
274
+ if (result.fixedFiles.length > 0) {
275
+ console.log(`Rewrote imports in ${result.fixedFiles.length} file(s):`);
276
+ for (const file of result.fixedFiles) console.log(` - ${file}`);
277
+ console.log('');
278
+ }
279
+
280
+ if (result.issues.length === 0) {
281
+ console.log(`Admin UI audit passed (${result.filesScanned} files scanned).`);
282
+ return;
283
+ }
284
+
285
+ console.error(`Admin UI audit found ${result.issues.length} issue(s):`);
286
+ for (const issue of result.issues) {
287
+ const suggestion = issue.suggested ? ` -> ${issue.suggested}` : '';
288
+ console.error(`- ${issue.file}: ${issue.message}${suggestion}`);
289
+ }
290
+
291
+ if (!opts.fix) {
292
+ console.error('\nRun with --fix to rewrite known copied import paths.');
293
+ }
294
+ }
295
+
296
+ async function main() {
297
+ const opts = parseArgs(process.argv);
298
+ if (opts.help) {
299
+ printHelp();
300
+ return;
301
+ }
302
+
303
+ const first = await audit(opts);
304
+ if (!opts.fix || first.fixedFiles.length === 0) {
305
+ printReport(first, opts);
306
+ process.exit(first.issues.length > 0 ? 1 : 0);
307
+ }
308
+
309
+ const second = await audit({ ...opts, fix: false });
310
+ const merged = {
311
+ filesScanned: second.filesScanned,
312
+ issues: second.issues,
313
+ fixedFiles: first.fixedFiles,
314
+ };
315
+ printReport(merged, opts);
316
+ process.exit(second.issues.length > 0 ? 1 : 0);
317
+ }
318
+
319
+ main().catch((error) => {
320
+ if (error instanceof Error) {
321
+ console.error(`admin-ui-starter-kit-audit: ${error.message}`);
322
+ } else {
323
+ console.error(error);
324
+ }
325
+ process.exit(2);
326
+ });
@@ -1,16 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * install-skill — consumer-facing installer for the
4
- * `component-library-rules` skill that ships inside the
5
- * `admin-ui-starter-kit` npm package.
3
+ * install-skill — consumer-facing installer for the AI skills that ship
4
+ * inside the `admin-ui-starter-kit` npm package.
6
5
  *
7
6
  * Run from a consumer project (after `npm install admin-ui-starter-kit`):
8
7
  *
9
- * npx admin-ui-starter-kit-install-skill [--target=claude|agents|both] [--force]
8
+ * npx admin-ui-starter-kit-install-skill [--skill=name|all] [--target=claude|agents|both] [--force]
10
9
  *
11
10
  * Default behaviour: copies the skill into BOTH
12
- * <cwd>/.claude/skills/component-library-rules/
13
- * <cwd>/.agents/skills/component-library-rules/
11
+ * <cwd>/.claude/skills/<skill-name>/
12
+ * <cwd>/.agents/skills/<skill-name>/
14
13
  *
15
14
  * The script is idempotent. It refuses to run inside the library itself
16
15
  * (the maintainer has `scripts/publish-skill.mjs` for that workflow).
@@ -20,18 +19,16 @@ import path from 'node:path';
20
19
  import readline from 'node:readline';
21
20
  import { fileURLToPath } from 'node:url';
22
21
 
23
- const SKILL_NAME = 'component-library-rules';
22
+ const SKILL_NAMES = ['component-library-rules', 'admin-ui-consumer-migration'];
24
23
  const PACKAGE_NAME = 'admin-ui-starter-kit';
25
24
 
26
25
  const __filename = fileURLToPath(import.meta.url);
27
26
  // scripts/install-skill.mjs lives at <pkg>/scripts/, so package root is one up.
28
27
  const PACKAGE_ROOT = path.resolve(path.dirname(__filename), '..');
29
- const SOURCE = path.join(PACKAGE_ROOT, '.agents', 'skills', SKILL_NAME);
30
-
31
28
  const CWD = process.cwd();
32
29
 
33
30
  function parseArgs(argv) {
34
- const opts = { target: 'both', force: false, help: false };
31
+ const opts = { target: 'both', skill: 'all', force: false, help: false };
35
32
  for (const arg of argv.slice(2)) {
36
33
  if (arg === '--force' || arg === '-f') {
37
34
  opts.force = true;
@@ -44,6 +41,15 @@ function parseArgs(argv) {
44
41
  process.exit(2);
45
42
  }
46
43
  opts.target = value;
44
+ } else if (arg.startsWith('--skill=')) {
45
+ const value = arg.slice('--skill='.length);
46
+ if (value !== 'all' && !SKILL_NAMES.includes(value)) {
47
+ console.error(
48
+ `✖ Invalid --skill=${value}. Expected one of: all, ${SKILL_NAMES.join(', ')}.`,
49
+ );
50
+ process.exit(2);
51
+ }
52
+ opts.skill = value;
47
53
  } else {
48
54
  console.error(`✖ Unknown argument: ${arg}`);
49
55
  opts.help = true;
@@ -55,8 +61,9 @@ function parseArgs(argv) {
55
61
  function printHelp() {
56
62
  console.log(
57
63
  `Usage: npx ${PACKAGE_NAME}-install-skill [options]\n\n` +
58
- `Installs the "${SKILL_NAME}" skill into the current project.\n\n` +
64
+ `Installs ${PACKAGE_NAME} AI skills into the current project.\n\n` +
59
65
  `Options:\n` +
66
+ ` --skill=all|${SKILL_NAMES.join('|')} Which skill to install (default: all)\n` +
60
67
  ` --target=claude|agents|both Where to install (default: both)\n` +
61
68
  ` --force, -f Overwrite existing skill dir without prompting\n` +
62
69
  ` --help, -h Show this message\n`,
@@ -107,7 +114,7 @@ function confirm(question) {
107
114
  });
108
115
  }
109
116
 
110
- async function installInto(targetDir, force) {
117
+ async function installInto(skillName, sourceDir, targetDir, force) {
111
118
  // Safety: the only thing we ever delete is the existing skill dir at the
112
119
  // exact target path. We never touch the parent directory or siblings.
113
120
  if (await exists(targetDir)) {
@@ -120,8 +127,8 @@ async function installInto(targetDir, force) {
120
127
  }
121
128
  await fs.rm(targetDir, { recursive: true, force: true });
122
129
  }
123
- const count = await copyDir(SOURCE, targetDir);
124
- console.log(` ✓ Installed ${SKILL_NAME} → ${targetDir} (${count} files)`);
130
+ const count = await copyDir(sourceDir, targetDir);
131
+ console.log(` ✓ Installed ${skillName} → ${targetDir} (${count} files)`);
125
132
  return count;
126
133
  }
127
134
 
@@ -143,31 +150,38 @@ async function main() {
143
150
  process.exit(2);
144
151
  }
145
152
 
146
- if (!(await exists(SOURCE))) {
147
- console.error(
148
- `✖ Source skill not found at ${SOURCE}\n` +
149
- ` This usually means the ${PACKAGE_NAME} package is missing the skill files.\n` +
150
- ` Try reinstalling: npm install ${PACKAGE_NAME}@latest`,
151
- );
152
- process.exit(1);
153
- }
153
+ const skillNames = opts.skill === 'all' ? SKILL_NAMES : [opts.skill];
154
154
 
155
- const targets = [];
156
- if (opts.target === 'claude' || opts.target === 'both') {
157
- targets.push(path.join(CWD, '.claude', 'skills', SKILL_NAME));
158
- }
159
- if (opts.target === 'agents' || opts.target === 'both') {
160
- targets.push(path.join(CWD, '.agents', 'skills', SKILL_NAME));
161
- }
155
+ console.log(`Installing ${skillNames.length === 1 ? `"${skillNames[0]}"` : 'AI skills'} from ${PACKAGE_NAME}\n`);
162
156
 
163
- console.log(`Installing "${SKILL_NAME}" from ${PACKAGE_NAME}\n`);
164
- for (const target of targets) {
165
- try {
166
- await installInto(target, opts.force);
167
- } catch (err) {
168
- console.error(` ${target}`);
169
- console.error(` ${err instanceof Error ? err.message : String(err)}`);
157
+ for (const skillName of skillNames) {
158
+ const source = path.join(PACKAGE_ROOT, '.agents', 'skills', skillName);
159
+ if (!(await exists(source))) {
160
+ console.error(
161
+ `✖ Source skill not found at ${source}\n` +
162
+ ` This usually means the ${PACKAGE_NAME} package is missing the skill files.\n` +
163
+ ` Try reinstalling: npm install ${PACKAGE_NAME}@latest`,
164
+ );
170
165
  process.exitCode = 1;
166
+ continue;
167
+ }
168
+
169
+ const targets = [];
170
+ if (opts.target === 'claude' || opts.target === 'both') {
171
+ targets.push(path.join(CWD, '.claude', 'skills', skillName));
172
+ }
173
+ if (opts.target === 'agents' || opts.target === 'both') {
174
+ targets.push(path.join(CWD, '.agents', 'skills', skillName));
175
+ }
176
+
177
+ for (const target of targets) {
178
+ try {
179
+ await installInto(skillName, source, target, opts.force);
180
+ } catch (err) {
181
+ console.error(` ✖ ${target}`);
182
+ console.error(` ${err instanceof Error ? err.message : String(err)}`);
183
+ process.exitCode = 1;
184
+ }
171
185
  }
172
186
  }
173
187