Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions v2/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,37 @@ If multiple tools are detected, an interactive prompt asks which to use.
The output file uses HTML sentinel markers so re-running only replaces the AgnosticUI
section — anything else in the file is preserved. Re-run after adding or updating components.

### `ag view`

Launch a lightweight Vite-powered component viewer for all your installed (ejected) components.
No Storybook setup required — just run the command from your project root.

```bash
ag view [options]

Options:
-p, --port <number> Dev server port (default: 7173)
--clean Delete .agnosticui-viewer/ and rebuild from scratch
--no-open Skip auto-opening the browser

Examples:
ag view # Start viewer at http://localhost:7173
ag view --port 8080 # Use a custom port
ag view --clean # Full rebuild (use after ag add / ag sync)
ag view --no-open # Don't auto-open browser
```

The viewer generates a self-contained Vite app in `.agnosticui-viewer/` (gitignored) using
your project's framework (React, Vue, or Lit/vanilla). It shows each installed component with
a three-tab panel: **Preview**, **HTML** import snippet, and **Info** metadata.

CSS tokens and any `ag-theme.css` skin override in your styles directory are automatically
applied so components look exactly as they do in your app.

`node_modules` inside `.agnosticui-viewer/` are cached between runs. The App entry file is
always regenerated (cheap) so the component list stays current. Run `ag view --clean` after
a `ag add` or `ag sync` when you want a guaranteed fresh install.

## How It Works

After running `ag init`, your project structure looks like this:
Expand Down
4 changes: 2 additions & 2 deletions v2/cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion v2/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agnosticui-cli",
"version": "2.0.0-alpha.15",
"version": "2.0.0-alpha.17",
"description": "CLI for AgnosticUI Local - The UI kit that lives in your codebase",
"type": "module",
"publishConfig": {
Expand Down
20 changes: 18 additions & 2 deletions v2/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,15 @@ import { list } from "./commands/list.js";
import { sync } from "./commands/sync.js";
import { playbook } from "./commands/playbook.js";
import { context } from "./commands/context.js";
import type { Framework, SyncOptions } from "./types/index.js";
import { view } from "./commands/view.js";
import type { Framework, SyncOptions, ViewOptions } from "./types/index.js";

const program = new Command();

program
.name("ag")
.description("AgnosticUI Local - The UI kit that lives in your codebase")
.version("2.0.0-alpha.15");
.version("2.0.0-alpha.17");

// ag init command
program
Expand Down Expand Up @@ -167,5 +168,20 @@ program
await context({ output: options.output, format: options.format });
});

// ag view command
program
.command("view")
.description("Launch a component viewer for your installed (ejected) components")
.option("-p, --port <number>", "Port for the Vite dev server", "7173")
.option("--clean", "Delete .agnosticui-viewer/ and rebuild from scratch")
.option("--no-open", "Skip auto-opening the browser")
.action(async (options) => {
await view({
port: parseInt(options.port, 10),
clean: options.clean ?? false,
open: options.open ?? true,
} as ViewOptions);
});

// Parse arguments
program.parse();
1 change: 1 addition & 0 deletions v2/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export async function init(options: InitOptions = {}): Promise<void> {
const ignorePattern = `${referenceDirName}/`;

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

const eslintConfigPath = path.join(process.cwd(), 'eslint.config.js');
if (pathExists(eslintConfigPath)) {
Expand Down
129 changes: 129 additions & 0 deletions v2/cli/src/commands/view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/**
* ag view command - Launch a lightweight component viewer for ejected components
*/
import path from 'node:path';
import { existsSync } from 'node:fs';
import { rm } from 'node:fs/promises';
import { execSync, spawn } from 'node:child_process';
import * as p from '@clack/prompts';
import pc from 'picocolors';
import { loadConfig } from '../utils/config.js';
import { logger } from '../utils/logger.js';
import { ensureDir } from '../utils/files.js';
import { generateViewerApp } from '../utils/viewer.js';
import type { ViewOptions } from '../types/index.js';

const VIEWER_DIR = '.agnosticui-viewer';
const DEFAULT_PORT = 7173;

export async function view(options: ViewOptions = {}): Promise<void> {
const port = options.port ?? DEFAULT_PORT;
const clean = options.clean ?? false;
const autoOpen = options.open ?? true;

p.intro(pc.bold(pc.cyan('AgnosticUI Component Viewer')));

// Require initialized project
const config = await loadConfig();
if (!config) {
logger.error('AgnosticUI is not initialized in this project.');
logger.info('Run ' + pc.cyan('npx agnosticui-cli init') + ' to get started.');
process.exit(1);
}

const installedComponents = Object.keys(config.components);
if (installedComponents.length === 0) {
logger.warn('No components installed yet.');
logger.info('Run ' + pc.cyan('npx agnosticui-cli add button') + ' to add components first.');
process.exit(0);
}

const cwd = process.cwd();
const viewerPath = path.join(cwd, VIEWER_DIR);
const nodeModulesPath = path.join(viewerPath, 'node_modules');

// --clean: nuke viewer directory and start fresh
if (clean && existsSync(viewerPath)) {
const spinner = p.spinner();
spinner.start('Cleaning viewer directory...');
await rm(viewerPath, { recursive: true, force: true });
spinner.stop(pc.green('✓') + ' Cleaned viewer directory');
}

await ensureDir(viewerPath);
await ensureDir(path.join(viewerPath, 'src'));

// Always regenerate app files (cheap — keeps component list current)
const genSpinner = p.spinner();
genSpinner.start('Generating viewer app...');
await generateViewerApp(config, viewerPath, cwd);
genSpinner.stop(pc.green('✓') + ' Viewer app ready');

// Install dependencies only when node_modules is absent (fast path on re-runs)
if (!existsSync(nodeModulesPath)) {
const installSpinner = p.spinner();
installSpinner.start(
'Installing viewer dependencies (first run only — subsequent runs skip this step)...'
);
try {
execSync('npm install', { cwd: viewerPath, stdio: 'pipe' });
installSpinner.stop(pc.green('✓') + ' Dependencies installed');
} catch (err) {
installSpinner.stop(pc.red('✖') + ' Failed to install dependencies');
logger.error(`npm install failed: ${err instanceof Error ? err.message : String(err)}`);
logger.info(
'Try running ' + pc.cyan('ag view --clean') + ' to rebuild the viewer from scratch.'
);
process.exit(1);
}
} else {
logger.info(
'Using cached dependencies. Run ' +
pc.cyan('ag view --clean') +
' to rebuild from scratch.'
);
}

logger.newline();
logger.box('Component Viewer', [
pc.dim(`Framework: ${config.framework}`),
pc.dim(`Components: ${installedComponents.length}`),
'',
pc.green(`→ http://localhost:${port}`),
'',
pc.dim('Press Ctrl+C to stop'),
]);

// Spawn Vite dev server inside the viewer directory
const viteProcess = spawn('npx', ['vite', '--port', String(port)], {
cwd: viewerPath,
stdio: 'inherit',
shell: true,
});

// Auto-open browser after a short startup delay
if (autoOpen) {
setTimeout(() => {
const url = `http://localhost:${port}`;
const openCmd =
process.platform === 'darwin'
? 'open'
: process.platform === 'win32'
? 'start'
: 'xdg-open';
spawn(openCmd, [url], { shell: true, detached: true });
}, 1500);
}

// Clean exit on Ctrl+C / SIGTERM
const handleExit = () => {
viteProcess.kill();
process.exit(0);
};
process.on('SIGINT', handleExit);
process.on('SIGTERM', handleExit);

viteProcess.on('close', (code) => {
process.exit(code ?? 0);
});
}
6 changes: 6 additions & 0 deletions v2/cli/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,9 @@ export interface ContextOptions {
output?: string; // Explicit output file path (overrides format and auto-detect)
format?: string; // AI tool format: claude, cursor, copilot, windsurf, openai, gemini, generic
}

export interface ViewOptions {
port?: number; // Dev server port (default: 7173)
clean?: boolean; // Nuke .agnosticui-viewer/ and rebuild from scratch
open?: boolean; // Auto-open browser (default: true)
}
Loading
Loading