@happyvertical/smrt-cli 0.30.0

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/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@happyvertical/smrt-cli",
3
+ "version": "0.30.0",
4
+ "description": "Developer CLI for SMRT framework - introspection, testing, and project management",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "smrt": "./bin/smrt.js",
10
+ "smrt-cli": "./bin/smrt.js"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "dist",
15
+ "scripts",
16
+ "README.md",
17
+ "CLAUDE.md",
18
+ "AGENTS.md"
19
+ ],
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js"
24
+ }
25
+ },
26
+ "dependencies": {
27
+ "@happyvertical/ai": "^0.74.7",
28
+ "@happyvertical/files": "^0.74.7",
29
+ "@happyvertical/logger": "^0.74.7",
30
+ "@happyvertical/sql": "^0.74.7",
31
+ "@happyvertical/utils": "^0.74.7",
32
+ "acorn": "^8.15.0",
33
+ "fast-glob": "3.3.3",
34
+ "tar": "^7.5.2",
35
+ "@happyvertical/smrt-config": "0.30.0",
36
+ "@happyvertical/smrt-agents": "0.30.0",
37
+ "@happyvertical/smrt-core": "0.30.0",
38
+ "@happyvertical/smrt-dev-mcp": "0.30.0",
39
+ "@happyvertical/smrt-playground": "0.30.0",
40
+ "@happyvertical/smrt-types": "0.30.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "25.0.9",
44
+ "tsx": "^4.21.0",
45
+ "typescript": "^5.9.3",
46
+ "vite": "^7.3.1",
47
+ "vite-plugin-dts": "4.5.4",
48
+ "vitest": "^4.0.17"
49
+ },
50
+ "keywords": [
51
+ "smrt",
52
+ "cli",
53
+ "developer-tools",
54
+ "introspection",
55
+ "testing",
56
+ "ai-agents"
57
+ ],
58
+ "author": "HappyVertical",
59
+ "license": "MIT",
60
+ "publishConfig": {
61
+ "registry": "https://registry.npmjs.org",
62
+ "access": "public"
63
+ },
64
+ "repository": {
65
+ "type": "git",
66
+ "url": "https://github.com/happyvertical/smrt.git",
67
+ "directory": "packages/cli"
68
+ },
69
+ "scripts": {
70
+ "build": "vite build",
71
+ "build:watch": "vite build --watch",
72
+ "clean": "rm -rf dist",
73
+ "dev": "npm run build:watch",
74
+ "test": "vitest run",
75
+ "test:watch": "vitest",
76
+ "typecheck": "tsc -p tsconfig.typecheck.json",
77
+ "cli": "tsx src/index.ts"
78
+ }
79
+ }
@@ -0,0 +1,573 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from 'node:child_process';
4
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { dirname, join, relative, resolve, sep } from 'node:path';
7
+ import { parse } from 'acorn';
8
+
9
+ function fail(message) {
10
+ throw new Error(message);
11
+ }
12
+
13
+ function run(command, args, options = {}) {
14
+ const result = spawnSync(command, args, {
15
+ encoding: 'utf8',
16
+ stdio: ['ignore', 'pipe', 'pipe'],
17
+ ...options,
18
+ });
19
+
20
+ if (result.error) {
21
+ fail(`Failed to run ${command}: ${result.error.message}`);
22
+ }
23
+
24
+ if (result.status !== 0) {
25
+ const stderr = result.stderr?.trim();
26
+ fail(
27
+ `${command} ${args.join(' ')} exited with code ${result.status}${
28
+ stderr ? `\n${stderr}` : ''
29
+ }`,
30
+ );
31
+ }
32
+
33
+ return result.stdout;
34
+ }
35
+
36
+ function collectTypePaths(packageJson) {
37
+ const typePaths = new Set();
38
+
39
+ if (typeof packageJson.types === 'string') {
40
+ typePaths.add(packageJson.types);
41
+ }
42
+
43
+ const visit = (value) => {
44
+ if (!value || typeof value !== 'object') {
45
+ return;
46
+ }
47
+
48
+ if (Array.isArray(value)) {
49
+ for (const item of value) {
50
+ visit(item);
51
+ }
52
+ return;
53
+ }
54
+
55
+ for (const [key, child] of Object.entries(value)) {
56
+ if (key === 'types' && typeof child === 'string') {
57
+ typePaths.add(child);
58
+ continue;
59
+ }
60
+
61
+ visit(child);
62
+ }
63
+ };
64
+
65
+ visit(packageJson.exports);
66
+
67
+ return [...typePaths].map((filePath) => filePath.replace(/^\.\//, ''));
68
+ }
69
+
70
+ function collectRuntimePaths(packageJson) {
71
+ const runtimePaths = new Set();
72
+
73
+ const addPath = (filePath) => {
74
+ if (typeof filePath === 'string') {
75
+ runtimePaths.add(filePath.replace(/^\.\//, ''));
76
+ }
77
+ };
78
+
79
+ if (typeof packageJson.main === 'string') {
80
+ addPath(packageJson.main);
81
+ }
82
+
83
+ if (typeof packageJson.module === 'string') {
84
+ addPath(packageJson.module);
85
+ }
86
+
87
+ if (typeof packageJson.svelte === 'string') {
88
+ addPath(packageJson.svelte);
89
+ }
90
+
91
+ const visit = (value, currentKey) => {
92
+ if (!value || typeof value !== 'object') {
93
+ if (typeof value === 'string' && currentKey !== 'types') {
94
+ addPath(value);
95
+ }
96
+ return;
97
+ }
98
+
99
+ if (Array.isArray(value)) {
100
+ for (const item of value) {
101
+ visit(item, currentKey);
102
+ }
103
+ return;
104
+ }
105
+
106
+ for (const [key, child] of Object.entries(value)) {
107
+ visit(child, key);
108
+ }
109
+ };
110
+
111
+ visit(packageJson.exports);
112
+
113
+ return [...runtimePaths];
114
+ }
115
+
116
+ function escapeRegex(source) {
117
+ return source.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
118
+ }
119
+
120
+ function archiveHasPath(archiveEntries, relativePath) {
121
+ const archivePath = `package/${relativePath}`;
122
+
123
+ if (!archivePath.includes('*')) {
124
+ return archiveEntries.has(archivePath);
125
+ }
126
+
127
+ const pattern = new RegExp(
128
+ `^${escapeRegex(archivePath).replaceAll('*', '[^/]+')}$`,
129
+ );
130
+
131
+ return [...archiveEntries].some((entry) => pattern.test(entry));
132
+ }
133
+
134
+ function stripComments(source) {
135
+ return source
136
+ .replace(/\/\*[\s\S]*?\*\//g, '')
137
+ .replace(/(^|[^:])\/\/.*$/gm, '$1');
138
+ }
139
+
140
+ function getPathStem(filePath) {
141
+ return filePath.replace(
142
+ /\.(?:d\.(?:cts|mts|ts)|[cm]?ts|[cm]?js|svelte)$/,
143
+ '',
144
+ );
145
+ }
146
+
147
+ function isRuntimeSourceFile(filePath) {
148
+ return /\.(?:[cm]?js|svelte)$/.test(filePath);
149
+ }
150
+
151
+ function isTypeSourceFile(filePath) {
152
+ return /\.d\.(?:cts|mts|ts)$/.test(filePath);
153
+ }
154
+
155
+ function resolveRuntimeCandidates(basePath) {
156
+ const stems = new Set([basePath, getPathStem(basePath)]);
157
+ const candidates = new Set();
158
+
159
+ for (const stem of stems) {
160
+ candidates.add(stem);
161
+ candidates.add(`${stem}.js`);
162
+ candidates.add(`${stem}.mjs`);
163
+ candidates.add(`${stem}.cjs`);
164
+ candidates.add(`${stem}.svelte`);
165
+ candidates.add(join(stem, 'index.js'));
166
+ candidates.add(join(stem, 'index.mjs'));
167
+ candidates.add(join(stem, 'index.cjs'));
168
+ candidates.add(join(stem, 'index.svelte'));
169
+ }
170
+
171
+ return [...candidates];
172
+ }
173
+
174
+ function resolveTypeCandidates(basePath) {
175
+ const stems = new Set([basePath, getPathStem(basePath)]);
176
+ const candidates = new Set();
177
+
178
+ for (const stem of stems) {
179
+ candidates.add(stem);
180
+ candidates.add(`${stem}.d.ts`);
181
+ candidates.add(`${stem}.d.mts`);
182
+ candidates.add(`${stem}.d.cts`);
183
+ candidates.add(`${stem}.ts`);
184
+ candidates.add(`${stem}.mts`);
185
+ candidates.add(`${stem}.cts`);
186
+ candidates.add(`${stem}.js`);
187
+ candidates.add(`${stem}.mjs`);
188
+ candidates.add(`${stem}.cjs`);
189
+ candidates.add(`${stem}.svelte`);
190
+ candidates.add(`${stem}.svelte.d.ts`);
191
+ candidates.add(join(stem, 'index.d.ts'));
192
+ candidates.add(join(stem, 'index.d.mts'));
193
+ candidates.add(join(stem, 'index.d.cts'));
194
+ candidates.add(join(stem, 'index.ts'));
195
+ candidates.add(join(stem, 'index.mts'));
196
+ candidates.add(join(stem, 'index.cts'));
197
+ candidates.add(join(stem, 'index.js'));
198
+ candidates.add(join(stem, 'index.mjs'));
199
+ candidates.add(join(stem, 'index.cjs'));
200
+ candidates.add(join(stem, 'index.svelte'));
201
+ candidates.add(join(stem, 'index.svelte.d.ts'));
202
+ }
203
+
204
+ return [...candidates];
205
+ }
206
+
207
+ function isWithinPackageRoot(packageRoot, targetPath) {
208
+ const relativePath = relative(packageRoot, targetPath);
209
+ return (
210
+ targetPath === packageRoot ||
211
+ (relativePath !== '..' &&
212
+ !relativePath.startsWith(`..${sep}`) &&
213
+ relativePath !== '')
214
+ );
215
+ }
216
+
217
+ function findExistingPackageCandidate(
218
+ packageRoot,
219
+ basePath,
220
+ candidateResolver,
221
+ predicate = () => true,
222
+ ) {
223
+ for (const candidate of candidateResolver(basePath)) {
224
+ if (
225
+ isWithinPackageRoot(packageRoot, candidate) &&
226
+ existsSync(candidate) &&
227
+ predicate(candidate)
228
+ ) {
229
+ return candidate;
230
+ }
231
+ }
232
+
233
+ return null;
234
+ }
235
+
236
+ function collectPackageEntryFiles(
237
+ packageRoot,
238
+ exportPaths,
239
+ candidateResolver,
240
+ predicate,
241
+ ) {
242
+ const entryFiles = [];
243
+ const seen = new Set();
244
+
245
+ for (const exportPath of exportPaths) {
246
+ if (exportPath.includes('*')) {
247
+ continue;
248
+ }
249
+
250
+ const resolved = findExistingPackageCandidate(
251
+ packageRoot,
252
+ resolve(packageRoot, exportPath),
253
+ candidateResolver,
254
+ predicate,
255
+ );
256
+
257
+ if (!resolved || seen.has(resolved)) {
258
+ continue;
259
+ }
260
+
261
+ seen.add(resolved);
262
+ entryFiles.push(resolved);
263
+ }
264
+
265
+ return entryFiles;
266
+ }
267
+
268
+ function collectRelativeEsmSpecifiers(source) {
269
+ const program = parse(source, {
270
+ ecmaVersion: 'latest',
271
+ sourceType: 'module',
272
+ });
273
+ const specifiers = [];
274
+ const pending = [program];
275
+
276
+ while (pending.length > 0) {
277
+ const current = pending.pop();
278
+
279
+ if (!current || typeof current !== 'object') {
280
+ continue;
281
+ }
282
+
283
+ switch (current.type) {
284
+ case 'ImportDeclaration':
285
+ case 'ExportAllDeclaration':
286
+ case 'ExportNamedDeclaration': {
287
+ const specifier = current.source?.value;
288
+ if (typeof specifier === 'string' && specifier.startsWith('.')) {
289
+ specifiers.push(specifier);
290
+ }
291
+ break;
292
+ }
293
+ case 'ImportExpression': {
294
+ const specifier = current.source?.value;
295
+ if (typeof specifier === 'string' && specifier.startsWith('.')) {
296
+ specifiers.push(specifier);
297
+ }
298
+ break;
299
+ }
300
+ default:
301
+ break;
302
+ }
303
+
304
+ for (const value of Object.values(current)) {
305
+ if (!value || typeof value !== 'object') {
306
+ continue;
307
+ }
308
+
309
+ if (Array.isArray(value)) {
310
+ for (const entry of value) {
311
+ pending.push(entry);
312
+ }
313
+ continue;
314
+ }
315
+
316
+ pending.push(value);
317
+ }
318
+ }
319
+
320
+ return specifiers;
321
+ }
322
+
323
+ function collectMissingRelativeRuntimeImports(packageRoot, runtimeEntryFiles) {
324
+ const missing = [];
325
+ const importPattern =
326
+ /(?:import|export)\s+(?:[^'"]*?\s+from\s+)?['"]([^'"]+)['"]|import\(['"]([^'"]+)['"]\)/g;
327
+ const pending = [...runtimeEntryFiles];
328
+ const scanned = new Set();
329
+
330
+ while (pending.length > 0) {
331
+ const filePath = pending.pop();
332
+ if (!filePath || scanned.has(filePath)) {
333
+ continue;
334
+ }
335
+
336
+ scanned.add(filePath);
337
+ const rawSource = readFileSync(filePath, 'utf8');
338
+ const source = filePath.endsWith('.svelte')
339
+ ? stripComments(rawSource)
340
+ : rawSource;
341
+ const specifiers = filePath.endsWith('.svelte')
342
+ ? [...source.matchAll(importPattern)]
343
+ .map((match) => {
344
+ const specifier = match[1] || match[2];
345
+ const fullMatch = match[0] ?? '';
346
+ const isTypeOnlyImport =
347
+ /^\s*(?:import|export)\s+type\b/.test(fullMatch) ||
348
+ /^\s*import\s*\{\s*type\b/.test(fullMatch);
349
+
350
+ if (typeof specifier !== 'string' || !specifier.startsWith('.')) {
351
+ return null;
352
+ }
353
+
354
+ return {
355
+ specifier,
356
+ isTypeOnlyImport,
357
+ };
358
+ })
359
+ .filter(Boolean)
360
+ : collectRelativeEsmSpecifiers(source).map((specifier) => ({
361
+ specifier,
362
+ isTypeOnlyImport: false,
363
+ }));
364
+ const seenSpecifiers = new Set();
365
+
366
+ for (const entry of specifiers) {
367
+ if (!entry) {
368
+ continue;
369
+ }
370
+
371
+ const key = `${entry.specifier}:${entry.isTypeOnlyImport ? 'type' : 'runtime'}`;
372
+ if (seenSpecifiers.has(key)) {
373
+ continue;
374
+ }
375
+ seenSpecifiers.add(key);
376
+
377
+ const specifier = entry.specifier;
378
+ const basePath = resolve(dirname(filePath), specifier);
379
+ if (!isWithinPackageRoot(packageRoot, basePath)) {
380
+ missing.push(
381
+ `${filePath.replace(`${packageRoot}/`, '')} -> ${specifier}`,
382
+ );
383
+ continue;
384
+ }
385
+ const candidateResolver = entry.isTypeOnlyImport
386
+ ? resolveTypeCandidates
387
+ : resolveRuntimeCandidates;
388
+ const targetPath = findExistingPackageCandidate(
389
+ packageRoot,
390
+ basePath,
391
+ candidateResolver,
392
+ );
393
+
394
+ if (!targetPath) {
395
+ missing.push(
396
+ `${filePath.replace(`${packageRoot}/`, '')} -> ${specifier}`,
397
+ );
398
+ continue;
399
+ }
400
+
401
+ if (!entry.isTypeOnlyImport && isRuntimeSourceFile(targetPath)) {
402
+ pending.push(targetPath);
403
+ }
404
+ }
405
+ }
406
+
407
+ return missing;
408
+ }
409
+
410
+ function collectMissingTypeImports(packageRoot, typeEntryFiles) {
411
+ const missing = [];
412
+ const importPattern =
413
+ /(?:import|export)\s+(?:[^'"]*?\s+from\s+)?['"]([^'"]+)['"]|import\(['"]([^'"]+)['"]\)/g;
414
+ const pending = [...typeEntryFiles];
415
+ const scanned = new Set();
416
+
417
+ while (pending.length > 0) {
418
+ const filePath = pending.pop();
419
+ if (!filePath || scanned.has(filePath)) {
420
+ continue;
421
+ }
422
+
423
+ scanned.add(filePath);
424
+ const source = stripComments(readFileSync(filePath, 'utf8'));
425
+ const seenSpecifiers = new Set();
426
+
427
+ for (const match of source.matchAll(importPattern)) {
428
+ const specifier = match[1] || match[2];
429
+ if (!specifier || !specifier.startsWith('.')) {
430
+ continue;
431
+ }
432
+
433
+ if (seenSpecifiers.has(specifier)) {
434
+ continue;
435
+ }
436
+ seenSpecifiers.add(specifier);
437
+
438
+ const basePath = resolve(dirname(filePath), specifier);
439
+ if (!isWithinPackageRoot(packageRoot, basePath)) {
440
+ missing.push(
441
+ `${filePath.replace(`${packageRoot}/`, '')} -> ${specifier}`,
442
+ );
443
+ continue;
444
+ }
445
+ const targetPath = findExistingPackageCandidate(
446
+ packageRoot,
447
+ basePath,
448
+ resolveTypeCandidates,
449
+ );
450
+
451
+ if (!targetPath) {
452
+ missing.push(
453
+ `${filePath.replace(`${packageRoot}/`, '')} -> ${specifier}`,
454
+ );
455
+ continue;
456
+ }
457
+
458
+ if (isTypeSourceFile(targetPath)) {
459
+ pending.push(targetPath);
460
+ }
461
+ }
462
+ }
463
+
464
+ return missing;
465
+ }
466
+
467
+ const packageDir = resolve(process.cwd(), process.argv[2] ?? '.');
468
+ const packageJsonPath = join(packageDir, 'package.json');
469
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
470
+ const typePaths = collectTypePaths(packageJson);
471
+ const runtimePaths = collectRuntimePaths(packageJson);
472
+ const npmConfigDryRunKey = 'npm_config_dry_run';
473
+ const npmConfigDryRunUpperKey = 'NPM_CONFIG_DRY_RUN';
474
+
475
+ if (typePaths.length === 0 && runtimePaths.length === 0) {
476
+ console.log(
477
+ `ℹ️ No exported pack verification paths declared for ${packageJson.name}`,
478
+ );
479
+ process.exit(0);
480
+ }
481
+
482
+ const tempDir = mkdtempSync(join(tmpdir(), 'smrt-pack-'));
483
+
484
+ try {
485
+ const packOutput = run(
486
+ 'npm',
487
+ ['pack', '--json', '--ignore-scripts', '--pack-destination', tempDir],
488
+ {
489
+ cwd: packageDir,
490
+ env: {
491
+ ...process.env,
492
+ [npmConfigDryRunKey]: 'false',
493
+ [npmConfigDryRunUpperKey]: 'false',
494
+ },
495
+ },
496
+ );
497
+ const packResult = JSON.parse(packOutput);
498
+ const tarballName = packResult[0]?.filename;
499
+
500
+ if (!tarballName) {
501
+ fail(`npm pack did not return a tarball filename for ${packageJson.name}`);
502
+ }
503
+
504
+ const tarballPath = join(tempDir, tarballName);
505
+ const archiveListing = run('tar', ['-tf', tarballPath]);
506
+ const archiveEntries = new Set(
507
+ archiveListing
508
+ .split('\n')
509
+ .map((entry) => entry.trim())
510
+ .filter(Boolean),
511
+ );
512
+
513
+ const missing = typePaths.filter(
514
+ (typePath) => !archiveHasPath(archiveEntries, typePath),
515
+ );
516
+
517
+ const missingRuntime = runtimePaths.filter(
518
+ (runtimePath) => !archiveHasPath(archiveEntries, runtimePath),
519
+ );
520
+
521
+ run('tar', ['-xf', tarballPath, '-C', tempDir]);
522
+ const packageRoot = join(tempDir, 'package');
523
+ const runtimeEntryFiles = collectPackageEntryFiles(
524
+ packageRoot,
525
+ runtimePaths,
526
+ resolveRuntimeCandidates,
527
+ isRuntimeSourceFile,
528
+ );
529
+ const typeEntryFiles = collectPackageEntryFiles(
530
+ packageRoot,
531
+ typePaths,
532
+ resolveTypeCandidates,
533
+ isTypeSourceFile,
534
+ );
535
+ const missingRelativeRuntimeImports = collectMissingRelativeRuntimeImports(
536
+ packageRoot,
537
+ runtimeEntryFiles,
538
+ );
539
+ const missingTypeImports = collectMissingTypeImports(
540
+ packageRoot,
541
+ typeEntryFiles,
542
+ );
543
+
544
+ if (
545
+ missing.length > 0 ||
546
+ missingRuntime.length > 0 ||
547
+ missingRelativeRuntimeImports.length > 0 ||
548
+ missingTypeImports.length > 0
549
+ ) {
550
+ fail(
551
+ `${packageJson.name} is missing required package artifacts in the packed artifact:\n${[
552
+ ...missing.map((typePath) => `- types: ${typePath}`),
553
+ ...missingRuntime.map((runtimePath) => `- runtime: ${runtimePath}`),
554
+ ...missingRelativeRuntimeImports.map(
555
+ (entry) => `- relative import target: ${entry}`,
556
+ ),
557
+ ...missingTypeImports.map((entry) => `- type import target: ${entry}`),
558
+ ].join('\n')}`,
559
+ );
560
+ }
561
+
562
+ console.log(
563
+ `✅ Verified packed exports for ${packageJson.name}: ${[
564
+ ...typePaths.map((typePath) => `types=${typePath}`),
565
+ ...runtimePaths.map((runtimePath) => `runtime=${runtimePath}`),
566
+ ].join(', ')}`,
567
+ );
568
+ } catch (error) {
569
+ console.error(`❌ ${error instanceof Error ? error.message : String(error)}`);
570
+ process.exitCode = 1;
571
+ } finally {
572
+ rmSync(tempDir, { recursive: true, force: true });
573
+ }