Skip to content

Commit 1304e5b

Browse files
Fix #358: implement ag view — lightweight ejected-component viewer (#361)
* Fix #358: implement ag view command — lightweight ejected-component viewer - New command: ag view [--port 7173] [--clean] [--no-open] - Generates .agnosticui-viewer/ Vite app per framework (React/Vue/Lit) - Always regenerates App entry on each run (keeps component list current) - Caches node_modules between runs; --clean for full rebuild - Two-pane layout: sidebar nav + main Preview/HTML/Info tabs using ag-* - Auto-imports ag-tokens.css, ag-tokens-dark.css, and ag-theme.css if present - Registers .agnosticui-viewer/ in .gitignore via ag init - Docs: ag view section added to installation.md and CLI README * Fix #358 follow-up: fix duplicate identifier and Flex import errors in ag view - Alias user component imports that conflict with viewer chrome (CopyButton, Header, Tabs) to avoid duplicate identifier errors in generated App files - Add REACT_EXPORT_OVERRIDES and VUE_EXPORT_OVERRIDES for components whose main React/Vue export name differs from React{Name}/Vue{Name} (e.g. Flex uses ReactFlexRow/VueFlexRow, not ReactFlex/VueFlex which don't exist) - Fall back to react/index when React{Name}.tsx/ts is missing in components dir - Prevent double custom element registration in Lit viewer by only importing chrome components from @ag-ref if they are not already in user components - Bump CLI version to 2.0.0-alpha.17
1 parent ff3d8b1 commit 1304e5b

File tree

9 files changed

+1270
-5
lines changed

9 files changed

+1270
-5
lines changed

v2/cli/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,37 @@ If multiple tools are detected, an interactive prompt asks which to use.
150150
The output file uses HTML sentinel markers so re-running only replaces the AgnosticUI
151151
section — anything else in the file is preserved. Re-run after adding or updating components.
152152
153+
### `ag view`
154+
155+
Launch a lightweight Vite-powered component viewer for all your installed (ejected) components.
156+
No Storybook setup required — just run the command from your project root.
157+
158+
```bash
159+
ag view [options]
160+
161+
Options:
162+
-p, --port <number> Dev server port (default: 7173)
163+
--clean Delete .agnosticui-viewer/ and rebuild from scratch
164+
--no-open Skip auto-opening the browser
165+
166+
Examples:
167+
ag view # Start viewer at http://localhost:7173
168+
ag view --port 8080 # Use a custom port
169+
ag view --clean # Full rebuild (use after ag add / ag sync)
170+
ag view --no-open # Don't auto-open browser
171+
```
172+
173+
The viewer generates a self-contained Vite app in `.agnosticui-viewer/` (gitignored) using
174+
your project's framework (React, Vue, or Lit/vanilla). It shows each installed component with
175+
a three-tab panel: **Preview**, **HTML** import snippet, and **Info** metadata.
176+
177+
CSS tokens and any `ag-theme.css` skin override in your styles directory are automatically
178+
applied so components look exactly as they do in your app.
179+
180+
`node_modules` inside `.agnosticui-viewer/` are cached between runs. The App entry file is
181+
always regenerated (cheap) so the component list stays current. Run `ag view --clean` after
182+
a `ag add` or `ag sync` when you want a guaranteed fresh install.
183+
153184
## How It Works
154185
155186
After running `ag init`, your project structure looks like this:

v2/cli/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

v2/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "agnosticui-cli",
3-
"version": "2.0.0-alpha.15",
3+
"version": "2.0.0-alpha.17",
44
"description": "CLI for AgnosticUI Local - The UI kit that lives in your codebase",
55
"type": "module",
66
"publishConfig": {

v2/cli/src/cli.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,15 @@ import { list } from "./commands/list.js";
3636
import { sync } from "./commands/sync.js";
3737
import { playbook } from "./commands/playbook.js";
3838
import { context } from "./commands/context.js";
39-
import type { Framework, SyncOptions } from "./types/index.js";
39+
import { view } from "./commands/view.js";
40+
import type { Framework, SyncOptions, ViewOptions } from "./types/index.js";
4041

4142
const program = new Command();
4243

4344
program
4445
.name("ag")
4546
.description("AgnosticUI Local - The UI kit that lives in your codebase")
46-
.version("2.0.0-alpha.15");
47+
.version("2.0.0-alpha.17");
4748

4849
// ag init command
4950
program
@@ -167,5 +168,20 @@ program
167168
await context({ output: options.output, format: options.format });
168169
});
169170

171+
// ag view command
172+
program
173+
.command("view")
174+
.description("Launch a component viewer for your installed (ejected) components")
175+
.option("-p, --port <number>", "Port for the Vite dev server", "7173")
176+
.option("--clean", "Delete .agnosticui-viewer/ and rebuild from scratch")
177+
.option("--no-open", "Skip auto-opening the browser")
178+
.action(async (options) => {
179+
await view({
180+
port: parseInt(options.port, 10),
181+
clean: options.clean ?? false,
182+
open: options.open ?? true,
183+
} as ViewOptions);
184+
});
185+
170186
// Parse arguments
171187
program.parse();

v2/cli/src/commands/init.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ export async function init(options: InitOptions = {}): Promise<void> {
191191
const ignorePattern = `${referenceDirName}/`;
192192

193193
await updateIgnoreFile(path.join(process.cwd(), '.gitignore'), ignorePattern);
194+
await updateIgnoreFile(path.join(process.cwd(), '.gitignore'), '.agnosticui-viewer/');
194195

195196
const eslintConfigPath = path.join(process.cwd(), 'eslint.config.js');
196197
if (pathExists(eslintConfigPath)) {

v2/cli/src/commands/view.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* ag view command - Launch a lightweight component viewer for ejected components
3+
*/
4+
import path from 'node:path';
5+
import { existsSync } from 'node:fs';
6+
import { rm } from 'node:fs/promises';
7+
import { execSync, spawn } from 'node:child_process';
8+
import * as p from '@clack/prompts';
9+
import pc from 'picocolors';
10+
import { loadConfig } from '../utils/config.js';
11+
import { logger } from '../utils/logger.js';
12+
import { ensureDir } from '../utils/files.js';
13+
import { generateViewerApp } from '../utils/viewer.js';
14+
import type { ViewOptions } from '../types/index.js';
15+
16+
const VIEWER_DIR = '.agnosticui-viewer';
17+
const DEFAULT_PORT = 7173;
18+
19+
export async function view(options: ViewOptions = {}): Promise<void> {
20+
const port = options.port ?? DEFAULT_PORT;
21+
const clean = options.clean ?? false;
22+
const autoOpen = options.open ?? true;
23+
24+
p.intro(pc.bold(pc.cyan('AgnosticUI Component Viewer')));
25+
26+
// Require initialized project
27+
const config = await loadConfig();
28+
if (!config) {
29+
logger.error('AgnosticUI is not initialized in this project.');
30+
logger.info('Run ' + pc.cyan('npx agnosticui-cli init') + ' to get started.');
31+
process.exit(1);
32+
}
33+
34+
const installedComponents = Object.keys(config.components);
35+
if (installedComponents.length === 0) {
36+
logger.warn('No components installed yet.');
37+
logger.info('Run ' + pc.cyan('npx agnosticui-cli add button') + ' to add components first.');
38+
process.exit(0);
39+
}
40+
41+
const cwd = process.cwd();
42+
const viewerPath = path.join(cwd, VIEWER_DIR);
43+
const nodeModulesPath = path.join(viewerPath, 'node_modules');
44+
45+
// --clean: nuke viewer directory and start fresh
46+
if (clean && existsSync(viewerPath)) {
47+
const spinner = p.spinner();
48+
spinner.start('Cleaning viewer directory...');
49+
await rm(viewerPath, { recursive: true, force: true });
50+
spinner.stop(pc.green('✓') + ' Cleaned viewer directory');
51+
}
52+
53+
await ensureDir(viewerPath);
54+
await ensureDir(path.join(viewerPath, 'src'));
55+
56+
// Always regenerate app files (cheap — keeps component list current)
57+
const genSpinner = p.spinner();
58+
genSpinner.start('Generating viewer app...');
59+
await generateViewerApp(config, viewerPath, cwd);
60+
genSpinner.stop(pc.green('✓') + ' Viewer app ready');
61+
62+
// Install dependencies only when node_modules is absent (fast path on re-runs)
63+
if (!existsSync(nodeModulesPath)) {
64+
const installSpinner = p.spinner();
65+
installSpinner.start(
66+
'Installing viewer dependencies (first run only — subsequent runs skip this step)...'
67+
);
68+
try {
69+
execSync('npm install', { cwd: viewerPath, stdio: 'pipe' });
70+
installSpinner.stop(pc.green('✓') + ' Dependencies installed');
71+
} catch (err) {
72+
installSpinner.stop(pc.red('✖') + ' Failed to install dependencies');
73+
logger.error(`npm install failed: ${err instanceof Error ? err.message : String(err)}`);
74+
logger.info(
75+
'Try running ' + pc.cyan('ag view --clean') + ' to rebuild the viewer from scratch.'
76+
);
77+
process.exit(1);
78+
}
79+
} else {
80+
logger.info(
81+
'Using cached dependencies. Run ' +
82+
pc.cyan('ag view --clean') +
83+
' to rebuild from scratch.'
84+
);
85+
}
86+
87+
logger.newline();
88+
logger.box('Component Viewer', [
89+
pc.dim(`Framework: ${config.framework}`),
90+
pc.dim(`Components: ${installedComponents.length}`),
91+
'',
92+
pc.green(`→ http://localhost:${port}`),
93+
'',
94+
pc.dim('Press Ctrl+C to stop'),
95+
]);
96+
97+
// Spawn Vite dev server inside the viewer directory
98+
const viteProcess = spawn('npx', ['vite', '--port', String(port)], {
99+
cwd: viewerPath,
100+
stdio: 'inherit',
101+
shell: true,
102+
});
103+
104+
// Auto-open browser after a short startup delay
105+
if (autoOpen) {
106+
setTimeout(() => {
107+
const url = `http://localhost:${port}`;
108+
const openCmd =
109+
process.platform === 'darwin'
110+
? 'open'
111+
: process.platform === 'win32'
112+
? 'start'
113+
: 'xdg-open';
114+
spawn(openCmd, [url], { shell: true, detached: true });
115+
}, 1500);
116+
}
117+
118+
// Clean exit on Ctrl+C / SIGTERM
119+
const handleExit = () => {
120+
viteProcess.kill();
121+
process.exit(0);
122+
};
123+
process.on('SIGINT', handleExit);
124+
process.on('SIGTERM', handleExit);
125+
126+
viteProcess.on('close', (code) => {
127+
process.exit(code ?? 0);
128+
});
129+
}

v2/cli/src/types/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,9 @@ export interface ContextOptions {
8686
output?: string; // Explicit output file path (overrides format and auto-detect)
8787
format?: string; // AI tool format: claude, cursor, copilot, windsurf, openai, gemini, generic
8888
}
89+
90+
export interface ViewOptions {
91+
port?: number; // Dev server port (default: 7173)
92+
clean?: boolean; // Nuke .agnosticui-viewer/ and rebuild from scratch
93+
open?: boolean; // Auto-open browser (default: true)
94+
}

0 commit comments

Comments
 (0)