nativeui-cli 1.0.0-beta.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.
@@ -0,0 +1,44 @@
1
+ // src/commands/list.ts
2
+ // ─────────────────────────────────────────────────────────────
3
+ // `native-ui list [--category <cat>]`
4
+ //
5
+ // Reads available components from the static COMPONENTS array.
6
+ // Reads installed components from native-ui.json → `components[]`.
7
+ // No network call unless --details flag is passed.
8
+ // ─────────────────────────────────────────────────────────────
9
+ import pc from 'picocolors';
10
+ import { COMPONENTS } from '../registry.js';
11
+ import { configExists, readConfig } from '../config.js';
12
+ import { logError } from '../utils.js';
13
+ export async function listCommand(opts) {
14
+ if (!configExists()) {
15
+ logError('native-ui.json not found. Run `nativeui-cli init` first.');
16
+ process.exit(1);
17
+ }
18
+ const config = readConfig();
19
+ const installedKeys = new Set(config.components); // from native-ui.json
20
+ const filterCat = opts.category?.toLowerCase();
21
+ const keys = filterCat
22
+ ? [...COMPONENTS].filter((k) => k.startsWith(filterCat))
23
+ : [...COMPONENTS];
24
+ if (keys.length === 0) {
25
+ console.log(pc.yellow(`No components found${filterCat ? ` for category: ${filterCat}` : ''}.`));
26
+ process.exit(0);
27
+ }
28
+ const installedCount = keys.filter((k) => installedKeys.has(k)).length;
29
+ console.log();
30
+ console.log(pc.bgCyan(pc.black(' native-ui ')) + pc.dim(' available components'));
31
+ console.log();
32
+ console.log(` ${pc.dim('total')} ${keys.length}` +
33
+ ` ${pc.dim('installed')} ${pc.green(String(installedCount))}` +
34
+ ` ${pc.dim('available')} ${keys.length - installedCount}`);
35
+ console.log();
36
+ for (const key of [...keys].sort()) {
37
+ const isInstalled = installedKeys.has(key);
38
+ const status = isInstalled ? pc.green('✔') : pc.dim('○');
39
+ console.log(` ${status} ${pc.bold(key)}`);
40
+ }
41
+ console.log();
42
+ console.log(pc.dim(` Run ${pc.reset(pc.bold('nativeui-cli add <component>'))} to install a component.`));
43
+ console.log();
44
+ }
@@ -0,0 +1 @@
1
+ export declare function removeCommand(componentArgs: string[]): Promise<void>;
@@ -0,0 +1,97 @@
1
+ // src/commands/remove.ts
2
+ // ─────────────────────────────────────────────────────────────
3
+ // `native-ui remove [components...]`
4
+ //
5
+ // Removes local component files and updates native-ui.json.
6
+ // ─────────────────────────────────────────────────────────────
7
+ import { intro, outro, multiselect, confirm, log } from "@clack/prompts";
8
+ import pc from "picocolors";
9
+ import fs from "fs";
10
+ import { getInstalledComponents, unmarkInstalled, configExists, readConfig, } from "../config.js";
11
+ import { fetchRegistryEntries, logError, logSuccess, logWarn, resolveComponentDest, } from "../utils.js";
12
+ export async function removeCommand(componentArgs) {
13
+ intro(pc.bgCyan(pc.black(" native-ui ")) + pc.dim(" remove components"));
14
+ // ── Config guard ────────────────────────────────────────────
15
+ if (!configExists()) {
16
+ logError("No native-ui.json found. Run `nativeui-cli init` first.");
17
+ process.exit(1);
18
+ }
19
+ const installedKeys = getInstalledComponents();
20
+ const config = readConfig();
21
+ if (installedKeys.length === 0) {
22
+ log.info("No components are installed.");
23
+ process.exit(0);
24
+ }
25
+ const installedEntries = new Map((await fetchRegistryEntries(installedKeys)).map((entry) => [
26
+ entry.key.toLowerCase(),
27
+ entry,
28
+ ]));
29
+ // ── Resolve which to remove ──────────────────────────────────
30
+ let selectedKeys;
31
+ if (componentArgs.length > 0) {
32
+ selectedKeys = componentArgs.map((c) => c.toLowerCase());
33
+ const unknown = selectedKeys.filter((k) => !installedEntries.has(k));
34
+ if (unknown.length) {
35
+ logError(`Unknown component(s): ${unknown.join(", ")}`);
36
+ process.exit(1);
37
+ }
38
+ const notInstalled = selectedKeys.filter((k) => !installedKeys.includes(k));
39
+ if (notInstalled.length) {
40
+ logWarn(`Not installed: ${notInstalled.map((k) => installedEntries.get(k)?.title ?? k).join(", ")}`);
41
+ }
42
+ selectedKeys = selectedKeys.filter((k) => installedKeys.includes(k));
43
+ }
44
+ else {
45
+ const installed = installedKeys.filter((k) => installedEntries.has(k));
46
+ const selected = await multiselect({
47
+ message: "Which components do you want to remove?",
48
+ options: installed.map((key) => ({
49
+ value: key,
50
+ label: pc.bold(installedEntries.get(key)?.title ?? key) +
51
+ pc.dim(` — ${installedEntries.get(key)?.description ?? ""}`),
52
+ })),
53
+ required: true,
54
+ });
55
+ if (typeof selected === "symbol") {
56
+ log.info("Cancelled.");
57
+ process.exit(0);
58
+ }
59
+ selectedKeys = selected;
60
+ }
61
+ if (selectedKeys.length === 0) {
62
+ log.info("Nothing to remove.");
63
+ process.exit(0);
64
+ }
65
+ const selectedEntries = new Map((await fetchRegistryEntries(selectedKeys)).map((entry) => [
66
+ entry.key.toLowerCase(),
67
+ entry,
68
+ ]));
69
+ // ── Confirm ──────────────────────────────────────────────────
70
+ const names = selectedKeys
71
+ .map((k) => selectedEntries.get(k)?.title ?? k)
72
+ .join(", ");
73
+ const ok = await confirm({
74
+ message: `Remove ${pc.bold(names)}? This deletes the local file(s).`,
75
+ initialValue: false,
76
+ });
77
+ if (!ok || typeof ok === "symbol") {
78
+ log.info("Aborted.");
79
+ process.exit(0);
80
+ }
81
+ // ── Delete files & update config ─────────────────────────────
82
+ for (const key of selectedKeys) {
83
+ const comp = selectedEntries.get(key);
84
+ const filePath = resolveComponentDest(comp?.files?.[0], config, key);
85
+ if (fs.existsSync(filePath)) {
86
+ fs.unlinkSync(filePath);
87
+ logSuccess(`Removed ${comp?.title ?? key} (${filePath})`);
88
+ }
89
+ else {
90
+ logWarn(`${comp?.title ?? key}: file not found at ${filePath}`);
91
+ }
92
+ }
93
+ for (const key of selectedKeys) {
94
+ unmarkInstalled(key);
95
+ }
96
+ outro(pc.cyan(`Removed ${selectedKeys.length} component(s).`));
97
+ }
@@ -0,0 +1,26 @@
1
+ export declare const CONFIG_FILE = "native-ui.json";
2
+ export interface NativeConfig {
3
+ $schema: string;
4
+ typescript: boolean;
5
+ /** Relative path from project root where components are written */
6
+ outputDir: string;
7
+ /** Which Expo runner to use when installing npm packages */
8
+ expoRunner: 'npx' | 'yarn' | 'bunx' | 'pnpm';
9
+ /** Base peer deps installed during `init` */
10
+ baseDependencies: string[];
11
+ /** Keys of components the user has added to this project */
12
+ components: string[];
13
+ }
14
+ export declare const DEFAULT_BASE_DEPENDENCIES: string[];
15
+ export declare const DEFAULT_CONFIG: NativeConfig;
16
+ export declare function getConfigPath(): string;
17
+ export declare function configExists(): boolean;
18
+ export declare function readConfig(): NativeConfig;
19
+ export declare function writeConfig(config: NativeConfig): void;
20
+ export declare function updateConfig(partial: Partial<NativeConfig>): NativeConfig;
21
+ export declare function getInstalledComponents(): string[];
22
+ /** Records a component as installed in native-ui.json. Idempotent. */
23
+ export declare function markInstalled(key: string): void;
24
+ /** Removes a component from the installed list in native-ui.json. */
25
+ export declare function unmarkInstalled(key: string): void;
26
+ export declare function getOutputDir(config: NativeConfig): string;
package/dist/config.js ADDED
@@ -0,0 +1,115 @@
1
+ // src/config.ts
2
+ // ─────────────────────────────────────────────────────────────
3
+ // Reads and writes `native-ui.json`.
4
+ //
5
+ // The config stores project settings + the list of components
6
+ // the user has installed. That's it — no registry cache, no
7
+ // hidden state files.
8
+ //
9
+ // native-ui.json shape:
10
+ // {
11
+ // "$schema": "...",
12
+ // "typescript": true,
13
+ // "outputDir": "components/ui",
14
+ // "expoRunner": "npx",
15
+ // "baseDependencies": ["react-native-reanimated", ...],
16
+ // "components": ["button", "input"] ← only what the user installed
17
+ // }
18
+ // ─────────────────────────────────────────────────────────────
19
+ import fs from 'fs';
20
+ import path from 'path';
21
+ export const CONFIG_FILE = 'native-ui.json';
22
+ export const DEFAULT_BASE_DEPENDENCIES = [
23
+ 'react-native-reanimated',
24
+ 'react-native-worklets',
25
+ '@rn-primitives/portal',
26
+ ];
27
+ export const DEFAULT_CONFIG = {
28
+ $schema: 'https://nativeui.qzz.io/schema.json',
29
+ typescript: true,
30
+ outputDir: 'components/ui',
31
+ expoRunner: 'npx',
32
+ baseDependencies: [...DEFAULT_BASE_DEPENDENCIES],
33
+ components: [],
34
+ };
35
+ // ─── Path helpers ─────────────────────────────────────────────
36
+ export function getConfigPath() {
37
+ return path.join(process.cwd(), CONFIG_FILE);
38
+ }
39
+ export function configExists() {
40
+ return fs.existsSync(getConfigPath());
41
+ }
42
+ // ─── Read / write ─────────────────────────────────────────────
43
+ export function readConfig() {
44
+ const configPath = getConfigPath();
45
+ if (!fs.existsSync(configPath)) {
46
+ throw new Error('native-ui.json not found. Run `nativeui-cli init` first.');
47
+ }
48
+ const raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
49
+ // Migrate legacy `packageManager` → `expoRunner`
50
+ const expoRunner = raw.expoRunner ??
51
+ (raw.packageManager === 'bun' ? 'bunx'
52
+ : raw.packageManager === 'yarn' ? 'yarn'
53
+ : raw.packageManager === 'pnpm' ? 'pnpm'
54
+ : 'npx');
55
+ let components = Array.isArray(raw.components)
56
+ ? raw.components.map((k) => k.toLowerCase())
57
+ : [];
58
+ const statePath = path.join(process.cwd(), '.native-ui-state.json');
59
+ if (fs.existsSync(statePath)) {
60
+ try {
61
+ const stateRaw = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
62
+ const stateComponents = Array.isArray(stateRaw.components)
63
+ ? stateRaw.components.map((k) => k.toLowerCase())
64
+ : [];
65
+ components = [...new Set([...components, ...stateComponents])];
66
+ fs.unlinkSync(statePath); // remove the old state file permanently
67
+ }
68
+ catch {
69
+ // corrupt state file — ignore
70
+ }
71
+ }
72
+ return {
73
+ $schema: raw.$schema ?? DEFAULT_CONFIG.$schema,
74
+ typescript: typeof raw.typescript === 'boolean' ? raw.typescript : true,
75
+ outputDir: typeof raw.outputDir === 'string' && raw.outputDir.trim()
76
+ ? raw.outputDir.trim()
77
+ : DEFAULT_CONFIG.outputDir,
78
+ expoRunner,
79
+ baseDependencies: Array.isArray(raw.baseDependencies) && raw.baseDependencies.length > 0
80
+ ? raw.baseDependencies
81
+ : [...DEFAULT_BASE_DEPENDENCIES],
82
+ components,
83
+ };
84
+ }
85
+ export function writeConfig(config) {
86
+ fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + '\n', 'utf-8');
87
+ }
88
+ export function updateConfig(partial) {
89
+ const config = { ...readConfig(), ...partial };
90
+ writeConfig(config);
91
+ return config;
92
+ }
93
+ // ─── Component tracking (reads/writes native-ui.json) ─────────
94
+ export function getInstalledComponents() {
95
+ return readConfig().components;
96
+ }
97
+ /** Records a component as installed in native-ui.json. Idempotent. */
98
+ export function markInstalled(key) {
99
+ const normalized = key.toLowerCase();
100
+ const config = readConfig();
101
+ if (!config.components.includes(normalized)) {
102
+ config.components = [...config.components, normalized].sort();
103
+ writeConfig(config);
104
+ }
105
+ }
106
+ /** Removes a component from the installed list in native-ui.json. */
107
+ export function unmarkInstalled(key) {
108
+ const normalized = key.toLowerCase();
109
+ const config = readConfig();
110
+ config.components = config.components.filter((k) => k !== normalized);
111
+ writeConfig(config);
112
+ }
113
+ export function getOutputDir(config) {
114
+ return path.join(process.cwd(), config.outputDir);
115
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ // src/index.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+ // Entry point. Wires up all commands via Commander.
5
+ //
6
+ // Usage:
7
+ // native-ui init
8
+ // native-ui add [components...]
9
+ // native-ui remove [components...]
10
+ // native-ui list [--category <cat>]
11
+ // native-ui diff [component]
12
+ // ─────────────────────────────────────────────────────────────
13
+ import { Command } from 'commander';
14
+ import pc from 'picocolors';
15
+ import { createRequire } from 'module';
16
+ import { initCommand } from './commands/init.js';
17
+ import { addCommand } from './commands/add.js';
18
+ import { removeCommand } from './commands/remove.js';
19
+ import { listCommand } from './commands/list.js';
20
+ import { diffCommand } from './commands/diff.js';
21
+ // Read version from package.json at runtime
22
+ const require = createRequire(import.meta.url);
23
+ const pkg = require('../package.json');
24
+ const program = new Command();
25
+ program
26
+ .name('nativeui-cli')
27
+ .description(pc.cyan('Add beautiful React Native / Expo components to your project.'))
28
+ .version(pkg.version, '-v, --version', 'Display the current version');
29
+ // ─── init ─────────────────────────────────────────────────────
30
+ program
31
+ .command('init')
32
+ .description('Initialise native-ui in your project')
33
+ .option('-y, --yes', 'Skip all prompts and use defaults')
34
+ .option('-f, --force', 'Overwrite existing native-ui.json')
35
+ .action(async (opts) => {
36
+ await initCommand(opts);
37
+ });
38
+ // ─── add ──────────────────────────────────────────────────────
39
+ program
40
+ .command('add [components...]')
41
+ .description('Add one or more components to your project')
42
+ .option('-o, --overwrite', 'Overwrite existing files without prompting')
43
+ .option('-a, --all', 'Add every available component')
44
+ .action(async (components, opts) => {
45
+ await addCommand(components, opts);
46
+ });
47
+ // ─── remove ───────────────────────────────────────────────────
48
+ program
49
+ .command('remove [components...]')
50
+ .alias('rm')
51
+ .description('Remove installed components')
52
+ .action(async (components) => {
53
+ await removeCommand(components);
54
+ });
55
+ // ─── list ─────────────────────────────────────────────────────
56
+ program
57
+ .command('list')
58
+ .alias('ls')
59
+ .description('List all available components')
60
+ .option('-c, --category <category>', 'Filter by category (primitives, forms, navigation, feedback, layout, typography)')
61
+ .action(async (opts) => {
62
+ await listCommand(opts);
63
+ });
64
+ // ─── diff ─────────────────────────────────────────────────────
65
+ program
66
+ .command('diff [component]')
67
+ .description('Show differences between your local files and the registry')
68
+ .action(async (component) => {
69
+ await diffCommand(component);
70
+ });
71
+ // ─── Global error handler ─────────────────────────────────────
72
+ process.on('uncaughtException', (err) => {
73
+ console.error(pc.red(`\n Error: ${err.message}\n`));
74
+ process.exit(1);
75
+ });
76
+ process.on('unhandledRejection', (reason) => {
77
+ console.error(pc.red(`\n Unhandled rejection: ${reason}\n`));
78
+ process.exit(1);
79
+ });
80
+ program.parse(process.argv);
81
+ // Show help if no command given
82
+ if (!process.argv.slice(2).length) {
83
+ program.outputHelp();
84
+ }
@@ -0,0 +1,19 @@
1
+ export type ComponentCategory = 'primitives' | 'forms' | 'navigation' | 'feedback' | 'layout' | 'typography';
2
+ export interface RegistryEntry {
3
+ key: string;
4
+ title?: string;
5
+ description?: string;
6
+ category?: ComponentCategory;
7
+ registryDependencies?: string[];
8
+ dependencies?: string[];
9
+ files?: RegistryFile[];
10
+ }
11
+ export interface RegistryFile {
12
+ path?: string;
13
+ target?: string;
14
+ content?: string;
15
+ }
16
+ export declare const COMPONENTS: readonly ["accordion", "alert-dialog", "alert", "aspect-ratio", "avatar", "badge", "button-group", "button", "calendar", "card", "carousel", "checkbox", "date-picker", "dialog", "empty", "field", "input-otp", "input", "label", "progress", "radio-group", "select", "separator", "skeleton", "sonner", "spinner", "switch", "table", "textarea", "typography"];
17
+ export type ComponentKey = (typeof COMPONENTS)[number];
18
+ export declare const CATEGORY_LABELS: Record<ComponentCategory, string>;
19
+ export declare function entryName(entry: Pick<RegistryEntry, 'key' | 'title'>): string;
@@ -0,0 +1,53 @@
1
+ // src/registry.ts
2
+ // ─────────────────────────────────────────────────────────────
3
+ // Static list of every component the CLI knows about.
4
+ // This is the single source of truth for what can be installed.
5
+ // File *contents* are fetched from the GraphQL backend at install time.
6
+ // ─────────────────────────────────────────────────────────────
7
+ // ─── All available components ─────────────────────────────────
8
+ // Add new component keys here — they instantly appear in `add` and `list`.
9
+ export const COMPONENTS = [
10
+ 'accordion',
11
+ 'alert-dialog',
12
+ 'alert',
13
+ 'aspect-ratio',
14
+ 'avatar',
15
+ 'badge',
16
+ 'button-group',
17
+ 'button',
18
+ 'calendar',
19
+ 'card',
20
+ 'carousel',
21
+ 'checkbox',
22
+ 'date-picker',
23
+ 'dialog',
24
+ 'empty',
25
+ 'field',
26
+ 'input-otp',
27
+ 'input',
28
+ 'label',
29
+ 'progress',
30
+ 'radio-group',
31
+ 'select',
32
+ 'separator',
33
+ 'skeleton',
34
+ 'sonner',
35
+ 'spinner',
36
+ 'switch',
37
+ 'table',
38
+ 'textarea',
39
+ 'typography',
40
+ ];
41
+ // ─── Category labels for `list` display ──────────────────────
42
+ export const CATEGORY_LABELS = {
43
+ primitives: '◆ Primitives',
44
+ forms: '◆ Forms',
45
+ navigation: '◆ Navigation',
46
+ feedback: '◆ Feedback',
47
+ layout: '◆ Layout',
48
+ typography: '◆ Typography',
49
+ };
50
+ // ─── Helpers ──────────────────────────────────────────────────
51
+ export function entryName(entry) {
52
+ return entry.title ?? entry.key;
53
+ }
@@ -0,0 +1,70 @@
1
+ import type { NativeConfig } from "./config.js";
2
+ type RegistryFile = {
3
+ path?: string;
4
+ target?: string;
5
+ content?: string;
6
+ };
7
+ type RegistryManifest = {
8
+ title?: string;
9
+ description?: string;
10
+ registryDependencies?: string[];
11
+ dependencies?: string[];
12
+ category?: string;
13
+ files?: RegistryFile[];
14
+ };
15
+ type RegistryEntry = RegistryManifest & {
16
+ key: string;
17
+ };
18
+ export declare function fetchRegistryEntries(keys: string[]): Promise<RegistryEntry[]>;
19
+ export declare function fetchRegistryIndex(): Promise<string[]>;
20
+ export declare function fetchRegistryClosure(keys: string[]): Promise<RegistryEntry[]>;
21
+ export declare function logSuccess(msg: string): void;
22
+ export declare function logError(msg: string): void;
23
+ export declare function logWarn(msg: string): void;
24
+ export declare function logInfo(msg: string): void;
25
+ /** Print a labelled key → value pair (used in `init` summary). */
26
+ export declare function logKV(key: string, value: string): void;
27
+ /**
28
+ * Fetches the raw source of a component from its registry URL.
29
+ * Throws a descriptive error on non-200 responses.
30
+ */
31
+ export declare function fetchComponent(name: string, url: string): Promise<string>;
32
+ /** Ensures a directory exists, creating it recursively if needed. */
33
+ export declare function ensureDir(dir: string): void;
34
+ /** Writes text to a file, creating parent dirs as needed. */
35
+ export declare function writeFile(filePath: string, content: string): void;
36
+ /** Returns file content or null if the file doesn't exist. */
37
+ export declare function readFileSafe(filePath: string): string | null;
38
+ /** Returns the install command for the given package manager. */
39
+ export declare function buildInstallCmd(runner: NativeConfig["expoRunner"], packages: string[]): string;
40
+ /**
41
+ * Installs Expo packages in a single batched command.
42
+ * Returns true on success, false on failure.
43
+ */
44
+ export declare function installPackages(packages: string[], runner: NativeConfig["expoRunner"], cwd: string, onProgress?: (pkg: string, index: number, total: number) => void): boolean;
45
+ /** Detect which Expo runner should be used in the cwd. */
46
+ export declare function detectExpoRunner(): NativeConfig["expoRunner"];
47
+ /** Returns true if this looks like an Expo project. */
48
+ export declare function isExpoProject(): boolean;
49
+ /**
50
+ * Very minimal line diff — returns a coloured unified-diff-style string.
51
+ * For a production CLI you'd swap this for the `diff` package.
52
+ */
53
+ export declare function lineDiff(a: string, b: string): string;
54
+ /** Returns true if two strings are identical (ignoring line-ending differences). */
55
+ export declare function isUpToDate(a: string, b: string): boolean;
56
+ /** * Resolves the absolute destination path for a registry file, honouring
57
+ * the user-configured `outputDir` from native-ui.json.
58
+ *
59
+ * Registry entries store paths relative to the default output dir
60
+ * (e.g. "components/ui/button.tsx"). When the user has changed
61
+ * `outputDir` to something like "src/ui", this function strips the
62
+ * default prefix and replaces it with the configured one so the file
63
+ * lands in the right place.
64
+ */
65
+ export declare function resolveComponentDest(file: {
66
+ path?: string;
67
+ target?: string;
68
+ } | undefined, config: NativeConfig, fallback: string): string;
69
+ export declare function getMissingPackages(packages: string[], cwd: string): string[];
70
+ export {};