@roboticela/devkit 1.0.2

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,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import chalk from "chalk";
4
+ import { createCommand } from "./commands/create.js";
5
+ import { initCommand } from "./commands/init.js";
6
+ import { addCommand } from "./commands/add.js";
7
+ import { removeCommand } from "./commands/remove.js";
8
+ import { listCommand } from "./commands/list.js";
9
+ import { infoCommand } from "./commands/info.js";
10
+ import { updateCommand, upgradeAllCommand } from "./commands/update.js";
11
+ import { doctorCommand } from "./commands/doctor.js";
12
+ import { themeApplyCommand, themePreviewCommand, themePresetCommand, themeSetCommand, themeListCommand, themeAuditCommand, } from "./commands/theme.js";
13
+ const program = new Command();
14
+ program
15
+ .name("devkit")
16
+ .description("Roboticela DevKit — scaffold and extend full-stack projects")
17
+ .version("1.0.0");
18
+ // ── devkit create ─────────────────────────────────────────────────────────────
19
+ program
20
+ .command("create [name]")
21
+ .description("Create a new project from a DevKit template")
22
+ .option("--template <id>", "Template ID (nextjs-compact | vite-express-tauri)")
23
+ .option("--preset <name>", "Theme preset (default | minimal | bold | playful | corporate)")
24
+ .option("--primary <hex>", "Primary brand color override (e.g. #e11d48)")
25
+ .option("--git", "Initialize git repository")
26
+ .option("--no-git", "Skip git initialization")
27
+ .option("--install", "Run npm install after scaffolding")
28
+ .option("--no-install", "Skip npm install")
29
+ .option("--add <components>", "Comma-separated components to add immediately (e.g. auth,hero-section)")
30
+ .option("-y, --yes", "Accept all defaults, skip prompts")
31
+ .action((name, opts) => createCommand(name, opts));
32
+ // ── devkit init ───────────────────────────────────────────────────────────────
33
+ program
34
+ .command("init")
35
+ .description("Initialize DevKit in an existing project")
36
+ .action(() => initCommand());
37
+ // ── devkit add ────────────────────────────────────────────────────────────────
38
+ program
39
+ .command("add <components...>")
40
+ .description("Install one or more components (e.g. devkit add auth hero-section)")
41
+ .option("--variant <id>", "Variant to install for components that have variants")
42
+ .option("--dry-run", "Preview what would be installed without writing files")
43
+ .action((names, opts) => addCommand(names, opts));
44
+ // ── devkit remove ─────────────────────────────────────────────────────────────
45
+ program
46
+ .command("remove <name>")
47
+ .description("Uninstall a component and its managed files")
48
+ .option("--keep-files", "Remove from DevKit tracking but keep the files")
49
+ .action((name, opts) => removeCommand(name, opts));
50
+ // ── devkit list ───────────────────────────────────────────────────────────────
51
+ program
52
+ .command("list")
53
+ .description("List all available components from the registry")
54
+ .option("--template <id>", "Filter by template")
55
+ .option("--category <cat>", "Filter by category")
56
+ .option("--installed", "Show only installed components")
57
+ .action((opts) => listCommand(opts));
58
+ // ── devkit info ───────────────────────────────────────────────────────────────
59
+ program
60
+ .command("info <name>")
61
+ .description("Show details about a component")
62
+ .action((name) => infoCommand(name));
63
+ // ── devkit update ─────────────────────────────────────────────────────────────
64
+ program
65
+ .command("update [component]")
66
+ .description("Update a component (or all components with --all)")
67
+ .option("--all", "Update all installed components")
68
+ .action((comp, opts) => {
69
+ if (opts.all || !comp)
70
+ return upgradeAllCommand();
71
+ return updateCommand(comp);
72
+ });
73
+ // ── devkit upgrade ────────────────────────────────────────────────────────────
74
+ program
75
+ .command("upgrade")
76
+ .description("Upgrade all installed components to their latest versions")
77
+ .action(() => upgradeAllCommand());
78
+ // ── devkit doctor ─────────────────────────────────────────────────────────────
79
+ program
80
+ .command("doctor")
81
+ .description("Check project configuration and installed components for issues")
82
+ .action(() => doctorCommand());
83
+ // ── devkit theme ──────────────────────────────────────────────────────────────
84
+ const theme = program.command("theme").description("Manage the global design token system");
85
+ theme.command("apply")
86
+ .description("Regenerate globals.css from current devkit.config.json theme settings")
87
+ .action(() => themeApplyCommand());
88
+ theme.command("preview")
89
+ .description("Preview the generated CSS without writing it")
90
+ .action(() => themePreviewCommand());
91
+ theme.command("preset <name>")
92
+ .description("Switch to a named theme preset (default | minimal | bold | playful | corporate)")
93
+ .action((name) => themePresetCommand(name));
94
+ theme.command("set <key> <value>")
95
+ .description("Set a single theme token (e.g. colors.primary #e11d48)")
96
+ .action((key, value) => themeSetCommand(key, value));
97
+ theme.command("list")
98
+ .description("Show all current theme settings")
99
+ .action(() => themeListCommand());
100
+ theme.command("audit")
101
+ .description("Scan component files for hardcoded colors (anti-pattern detector)")
102
+ .action(() => themeAuditCommand());
103
+ // ── devkit install (CI use) ───────────────────────────────────────────────────
104
+ program
105
+ .command("install")
106
+ .description("Install all components from devkit.lock.json (for CI/deployment)")
107
+ .action(async () => {
108
+ const { readLock, readConfig } = await import("./lib/config.js");
109
+ const { installComponent } = await import("./lib/installer.js");
110
+ const { log } = await import("./lib/logger.js");
111
+ const lock = readLock();
112
+ const config = readConfig();
113
+ for (const [name, entry] of Object.entries(lock.components)) {
114
+ const s = (await import("ora")).default(`Installing ${name}…`).start();
115
+ try {
116
+ await installComponent(name, config.template, entry.version, entry.variant ?? undefined);
117
+ s.succeed(`${name}@${entry.version}`);
118
+ }
119
+ catch (e) {
120
+ s.fail(`${name}: ${e.message}`);
121
+ }
122
+ }
123
+ log.success("All components installed.");
124
+ });
125
+ // ── devkit eject ──────────────────────────────────────────────────────────────
126
+ program
127
+ .command("eject <name>")
128
+ .description("Stop DevKit from tracking a component (files are kept, no more updates via DevKit)")
129
+ .action(async (name) => {
130
+ const { readLock, writeLock } = await import("./lib/config.js");
131
+ const { unlinkSync, existsSync } = await import("fs");
132
+ const { join } = await import("path");
133
+ const { log } = await import("./lib/logger.js");
134
+ const manifestPath = join(process.cwd(), ".devkit", `${name}.manifest.json`);
135
+ if (existsSync(manifestPath))
136
+ unlinkSync(manifestPath);
137
+ const lock = readLock();
138
+ delete lock.components[name];
139
+ writeLock(lock);
140
+ log.success(`${name} ejected. Files are yours — DevKit will no longer manage them.`);
141
+ });
142
+ // ── Help footer ───────────────────────────────────────────────────────────────
143
+ program.addHelpText("after", `
144
+ ${chalk.bold("Examples:")}
145
+ ${chalk.cyan("devkit create my-app")} Interactive project creation
146
+ ${chalk.cyan("devkit create my-app --template=nextjs-compact --yes")} Non-interactive
147
+ ${chalk.cyan("devkit add auth")} Install auth component
148
+ ${chalk.cyan("devkit add hero-section --variant=split-image")}
149
+ ${chalk.cyan("devkit theme set colors.primary #e11d48")} Change primary color
150
+ ${chalk.cyan("devkit doctor")} Check configuration
151
+ ${chalk.cyan("devkit list")} Browse all components
152
+
153
+ ${chalk.dim("Registry:")} ${process.env.DEVKIT_REGISTRY ?? "https://api.devkit.roboticela.com"}
154
+ `);
155
+ program.parse();
@@ -0,0 +1,51 @@
1
+ export interface DevKitConfig {
2
+ $schema?: string;
3
+ devkit: string;
4
+ template: "nextjs-compact" | "vite-express-tauri";
5
+ site: {
6
+ name: string;
7
+ url: string;
8
+ icon?: string;
9
+ description?: string;
10
+ };
11
+ theme?: {
12
+ preset?: string;
13
+ colors?: {
14
+ primary?: string;
15
+ secondary?: string;
16
+ };
17
+ fonts?: {
18
+ sans?: string;
19
+ mono?: string;
20
+ display?: string;
21
+ };
22
+ radius?: string;
23
+ darkMode?: boolean;
24
+ darkModeStrategy?: "class" | "media";
25
+ };
26
+ auth?: Record<string, unknown>;
27
+ subscriptions?: Record<string, unknown>;
28
+ storage?: Record<string, unknown>;
29
+ database?: {
30
+ url?: string;
31
+ };
32
+ }
33
+ export interface DevKitLock {
34
+ lockVersion: number;
35
+ template: string;
36
+ components: Record<string, {
37
+ version: string;
38
+ variant?: string | null;
39
+ resolved: string;
40
+ integrity?: string;
41
+ installedAt: string;
42
+ }>;
43
+ }
44
+ export declare function configExists(cwd?: string): boolean;
45
+ export declare function readConfig(cwd?: string): DevKitConfig;
46
+ export declare function writeConfig(config: DevKitConfig, cwd?: string): void;
47
+ export declare function readLock(cwd?: string): DevKitLock;
48
+ export declare function writeLock(lock: DevKitLock, cwd?: string): void;
49
+ export declare function getInstalledComponents(cwd?: string): string[];
50
+ export declare function isComponentInstalled(name: string, cwd?: string): boolean;
51
+ export declare function defaultConfig(template: DevKitConfig["template"], siteName: string, siteUrl: string): DevKitConfig;
@@ -0,0 +1,48 @@
1
+ import { readFileSync, writeFileSync, existsSync } from "fs";
2
+ import { join } from "path";
3
+ const CONFIG_FILE = "devkit.config.json";
4
+ const LOCK_FILE = "devkit.lock.json";
5
+ export function configExists(cwd = process.cwd()) {
6
+ return existsSync(join(cwd, CONFIG_FILE));
7
+ }
8
+ export function readConfig(cwd = process.cwd()) {
9
+ const path = join(cwd, CONFIG_FILE);
10
+ if (!existsSync(path))
11
+ throw new Error(`devkit.config.json not found. Run 'devkit init' first.`);
12
+ return JSON.parse(readFileSync(path, "utf-8"));
13
+ }
14
+ export function writeConfig(config, cwd = process.cwd()) {
15
+ writeFileSync(join(cwd, CONFIG_FILE), JSON.stringify(config, null, 2) + "\n");
16
+ }
17
+ export function readLock(cwd = process.cwd()) {
18
+ const path = join(cwd, LOCK_FILE);
19
+ if (!existsSync(path))
20
+ return { lockVersion: 1, template: "", components: {} };
21
+ return JSON.parse(readFileSync(path, "utf-8"));
22
+ }
23
+ export function writeLock(lock, cwd = process.cwd()) {
24
+ writeFileSync(join(cwd, LOCK_FILE), JSON.stringify(lock, null, 2) + "\n");
25
+ }
26
+ export function getInstalledComponents(cwd = process.cwd()) {
27
+ const lock = readLock(cwd);
28
+ return Object.keys(lock.components);
29
+ }
30
+ export function isComponentInstalled(name, cwd = process.cwd()) {
31
+ return name in readLock(cwd).components;
32
+ }
33
+ export function defaultConfig(template, siteName, siteUrl) {
34
+ return {
35
+ $schema: "https://registry.roboticela.com/schemas/devkit-config.json",
36
+ devkit: "1.0",
37
+ template,
38
+ site: { name: siteName, url: siteUrl },
39
+ theme: {
40
+ preset: "default",
41
+ colors: { primary: "#6366f1", secondary: "#f59e0b" },
42
+ fonts: { sans: "Inter", mono: "JetBrains Mono", display: "Inter" },
43
+ radius: "md",
44
+ darkMode: true,
45
+ darkModeStrategy: "class",
46
+ },
47
+ };
48
+ }
@@ -0,0 +1,2 @@
1
+ export type TemplateId = "nextjs-compact" | "vite-express-tauri" | "unknown";
2
+ export declare function detectTemplate(cwd?: string): TemplateId;
@@ -0,0 +1,18 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { join } from "path";
3
+ export function detectTemplate(cwd = process.cwd()) {
4
+ const pkgPath = join(cwd, "package.json");
5
+ if (!existsSync(pkgPath))
6
+ return "unknown";
7
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
8
+ const deps = { ...(pkg["dependencies"] ?? {}), ...(pkg["devDependencies"] ?? {}) };
9
+ // Next.js check
10
+ if ("next" in deps)
11
+ return "nextjs-compact";
12
+ // Vite + Tauri check
13
+ if ("vite" in deps && existsSync(join(cwd, "src-tauri")))
14
+ return "vite-express-tauri";
15
+ if ("vite" in deps && existsSync(join(cwd, "server")))
16
+ return "vite-express-tauri";
17
+ return "unknown";
18
+ }
@@ -0,0 +1,2 @@
1
+ export declare function installComponent(name: string, template: string, version: string | undefined, variant: string | undefined, cwd?: string): Promise<void>;
2
+ export declare function removeComponent(name: string, cwd?: string): void;
@@ -0,0 +1,178 @@
1
+ import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, createWriteStream } from "fs";
2
+ import { join, dirname } from "path";
3
+ import { execSync } from "child_process";
4
+ import { createHash } from "crypto";
5
+ import { x as extractTar } from "tar";
6
+ import { pipeline } from "stream/promises";
7
+ import { getManifest } from "./registry.js";
8
+ import { readLock, writeLock } from "./config.js";
9
+ import { log } from "./logger.js";
10
+ const REGISTRY_URL = process.env.DEVKIT_REGISTRY ?? "https://api.devkit.roboticela.com";
11
+ function hashFile(path) {
12
+ const content = readFileSync(path);
13
+ return createHash("sha256").update(content).digest("hex");
14
+ }
15
+ export async function installComponent(name, template, version = "latest", variant, cwd = process.cwd()) {
16
+ // 1. Fetch manifest from registry
17
+ const result = await getManifest(name, template, version, variant);
18
+ const manifest = result.manifest;
19
+ const resolvedVersion = result.version;
20
+ // 2. Download component files from registry
21
+ const tmpDir = join(cwd, ".devkit", `.tmp-${name}`);
22
+ await downloadComponentFiles(name, template, resolvedVersion, variant, tmpDir);
23
+ // 3. Copy files into project
24
+ const managedFiles = [];
25
+ for (const fileEntry of manifest.files) {
26
+ const src = join(tmpDir, fileEntry.source);
27
+ const dest = join(cwd, fileEntry.destination);
28
+ // Check if file exists and was user-modified
29
+ let userModified = false;
30
+ if (existsSync(dest)) {
31
+ const existingHash = hashFile(dest);
32
+ const lock = readLock(cwd);
33
+ const lockedComp = lock.components[name];
34
+ if (lockedComp) {
35
+ const manifestPath = join(cwd, ".devkit", `${name}.manifest.json`);
36
+ if (existsSync(manifestPath)) {
37
+ const existingManifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
38
+ const tracked = (existingManifest.managedFiles ?? []).find((f) => f.path === fileEntry.destination);
39
+ if (tracked && tracked.hash !== existingHash)
40
+ userModified = true;
41
+ }
42
+ }
43
+ }
44
+ if (userModified) {
45
+ log.warn(`Skipped (user-modified): ${fileEntry.destination}`);
46
+ }
47
+ else {
48
+ mkdirSync(dirname(dest), { recursive: true });
49
+ if (existsSync(src)) {
50
+ cpSync(src, dest);
51
+ const hash = hashFile(dest);
52
+ managedFiles.push({ path: fileEntry.destination, hash, userModified: false });
53
+ log.step(`Created ${fileEntry.destination}`);
54
+ }
55
+ }
56
+ }
57
+ // 4. Apply injections
58
+ for (const injection of manifest.injections ?? []) {
59
+ applyInjection(injection, cwd);
60
+ }
61
+ // 5. Install npm dependencies
62
+ const deps = manifest.dependencies ?? {};
63
+ if ((deps.frontend?.length ?? 0) > 0) {
64
+ log.step(`npm install ${deps.frontend.join(" ")}`);
65
+ execSync(`npm install ${deps.frontend.join(" ")}`, { cwd, stdio: "inherit" });
66
+ }
67
+ if ((deps.devFrontend?.length ?? 0) > 0) {
68
+ execSync(`npm install -D ${deps.devFrontend.join(" ")}`, { cwd, stdio: "inherit" });
69
+ }
70
+ if ((deps.backend?.length ?? 0) > 0 && existsSync(join(cwd, "server"))) {
71
+ log.step(`npm install (server) ${deps.backend.join(" ")}`);
72
+ execSync(`npm install ${deps.backend.join(" ")}`, { cwd: join(cwd, "server"), stdio: "inherit" });
73
+ }
74
+ // 6. Write component manifest
75
+ const compManifest = {
76
+ name,
77
+ version: resolvedVersion,
78
+ template,
79
+ variant: variant ?? null,
80
+ installedAt: new Date().toISOString(),
81
+ managedFiles,
82
+ injections: manifest.injections ?? [],
83
+ dependencies: manifest.dependencies ?? {},
84
+ envVars: manifest.envVars ?? [],
85
+ };
86
+ mkdirSync(join(cwd, ".devkit"), { recursive: true });
87
+ writeFileSync(join(cwd, ".devkit", `${name}.manifest.json`), JSON.stringify(compManifest, null, 2) + "\n");
88
+ // 7. Update lock file
89
+ const lock = readLock(cwd);
90
+ lock.components[name] = {
91
+ version: resolvedVersion,
92
+ variant: variant ?? null,
93
+ resolved: `${REGISTRY_URL}/api/v1/components/${name}/download/${resolvedVersion}/${template}`,
94
+ installedAt: new Date().toISOString(),
95
+ };
96
+ writeLock(lock, cwd);
97
+ // 8. Print required env vars
98
+ if ((manifest.envVars?.length ?? 0) > 0) {
99
+ log.blank();
100
+ log.warn(`Required env vars for ${name}:`);
101
+ for (const envVar of manifest.envVars) {
102
+ log.step(envVar);
103
+ }
104
+ }
105
+ // Cleanup temp dir
106
+ const { rmSync } = await import("fs");
107
+ rmSync(tmpDir, { recursive: true, force: true });
108
+ }
109
+ export function removeComponent(name, cwd = process.cwd()) {
110
+ const manifestPath = join(cwd, ".devkit", `${name}.manifest.json`);
111
+ if (!existsSync(manifestPath))
112
+ throw new Error(`Component '${name}' is not installed.`);
113
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
114
+ for (const file of manifest.managedFiles ?? []) {
115
+ if (!file.userModified && existsSync(join(cwd, file.path))) {
116
+ unlinkSync(join(cwd, file.path));
117
+ log.step(`Removed ${file.path}`);
118
+ }
119
+ else if (file.userModified) {
120
+ log.warn(`Kept (user-modified): ${file.path}`);
121
+ }
122
+ }
123
+ unlinkSync(manifestPath);
124
+ const lock = readLock(cwd);
125
+ delete lock.components[name];
126
+ writeLock(lock, cwd);
127
+ }
128
+ async function downloadComponentFiles(name, template, version, variant, tmpDir) {
129
+ mkdirSync(tmpDir, { recursive: true });
130
+ // For local development with the registry server, we serve files directly.
131
+ // In production this would stream a tarball. For now we copy from local registry.
132
+ const registryDir = join(dirname(new URL(import.meta.url).pathname), "../../../registry/components");
133
+ const major = `v${version.split(".")[0]}`;
134
+ const compDir = variant
135
+ ? join(registryDir, name, template, major, "variants", variant)
136
+ : join(registryDir, name, template, major);
137
+ if (existsSync(compDir)) {
138
+ cpSync(compDir, tmpDir, { recursive: true });
139
+ }
140
+ else {
141
+ // Fallback: try to fetch a tarball from registry
142
+ const qs = new URLSearchParams({ template, version });
143
+ if (variant)
144
+ qs.set("variant", variant);
145
+ const url = `${REGISTRY_URL}/api/v1/components/${name}/download?${qs}`;
146
+ const res = await fetch(url);
147
+ if (!res.ok)
148
+ throw new Error(`Failed to download component ${name}: HTTP ${res.status}`);
149
+ const tmpTar = join(tmpDir, ".component.tar.gz");
150
+ const writer = createWriteStream(tmpTar);
151
+ // @ts-expect-error — Node stream
152
+ await pipeline(res.body, writer);
153
+ await extractTar({ file: tmpTar, cwd: tmpDir, strip: 0 });
154
+ const { unlinkSync } = await import("fs");
155
+ unlinkSync(tmpTar);
156
+ }
157
+ }
158
+ function applyInjection(injection, cwd) {
159
+ const filePath = join(cwd, injection.file);
160
+ if (!existsSync(filePath)) {
161
+ log.warn(`Injection target not found: ${injection.file} — add manually`);
162
+ return;
163
+ }
164
+ let content = readFileSync(filePath, "utf-8");
165
+ // Add import if not present
166
+ if (injection.importLine && !content.includes(injection.importLine)) {
167
+ content = injection.importLine + "\n" + content;
168
+ }
169
+ // Add route or component line at marker
170
+ if (injection.marker && injection.routeLine && content.includes(injection.marker)) {
171
+ content = content.replace(injection.marker, `${injection.marker}\n ${injection.routeLine}`);
172
+ }
173
+ if (injection.marker && injection.componentLine && content.includes(injection.marker)) {
174
+ content = content.replace(injection.marker, `${injection.marker}\n ${injection.componentLine}`);
175
+ }
176
+ writeFileSync(filePath, content);
177
+ log.step(`Injected into ${injection.file}`);
178
+ }
@@ -0,0 +1,10 @@
1
+ export declare const log: {
2
+ info: (msg: string) => void;
3
+ success: (msg: string) => void;
4
+ warn: (msg: string) => void;
5
+ error: (msg: string) => void;
6
+ step: (msg: string) => void;
7
+ blank: () => void;
8
+ header: (msg: string) => void;
9
+ divider: () => void;
10
+ };
@@ -0,0 +1,15 @@
1
+ import chalk from "chalk";
2
+ export const log = {
3
+ info: (msg) => console.log(` ${chalk.cyan("ℹ")} ${msg}`),
4
+ success: (msg) => console.log(` ${chalk.green("✓")} ${msg}`),
5
+ warn: (msg) => console.log(` ${chalk.yellow("⚠")} ${msg}`),
6
+ error: (msg) => console.log(` ${chalk.red("✗")} ${msg}`),
7
+ step: (msg) => console.log(` ${chalk.gray("→")} ${chalk.dim(msg)}`),
8
+ blank: () => console.log(),
9
+ header: (msg) => {
10
+ console.log();
11
+ console.log(chalk.bold.white(` ${msg}`));
12
+ console.log();
13
+ },
14
+ divider: () => console.log(chalk.gray(" " + "─".repeat(56))),
15
+ };
@@ -0,0 +1,26 @@
1
+ export interface ComponentEntry {
2
+ name: string;
3
+ displayName: string;
4
+ description: string;
5
+ category: string;
6
+ templates: string[];
7
+ platforms: string[];
8
+ hasVariants: boolean;
9
+ variants?: {
10
+ id: string;
11
+ label: string;
12
+ description: string;
13
+ }[];
14
+ latestVersion: string;
15
+ tags: string[];
16
+ }
17
+ export declare function listComponents(template?: string): Promise<ComponentEntry[]>;
18
+ export declare function getComponentInfo(name: string): Promise<Record<string, unknown>>;
19
+ export declare function getManifest(name: string, template: string, version?: string, variant?: string): Promise<{
20
+ name: string;
21
+ template: string;
22
+ version: string;
23
+ variant: string | null;
24
+ manifest: Record<string, unknown>;
25
+ }>;
26
+ export declare function healthCheck(): Promise<boolean>;
@@ -0,0 +1,30 @@
1
+ const REGISTRY_URL = process.env.DEVKIT_REGISTRY ?? "https://api.devkit.roboticela.com";
2
+ async function get(path) {
3
+ const res = await fetch(`${REGISTRY_URL}${path}`);
4
+ if (!res.ok)
5
+ throw new Error(`Registry error ${res.status}: ${path}`);
6
+ return res.json();
7
+ }
8
+ export async function listComponents(template) {
9
+ const qs = template ? `?template=${template}` : "";
10
+ const data = await get(`/api/v1/components${qs}`);
11
+ return data.components;
12
+ }
13
+ export async function getComponentInfo(name) {
14
+ return get(`/api/v1/components/${name}`);
15
+ }
16
+ export async function getManifest(name, template, version = "latest", variant) {
17
+ const qs = new URLSearchParams({ template, version });
18
+ if (variant)
19
+ qs.set("variant", variant);
20
+ return get(`/api/v1/components/${name}/manifest?${qs}`);
21
+ }
22
+ export async function healthCheck() {
23
+ try {
24
+ await get("/health");
25
+ return true;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
@@ -0,0 +1 @@
1
+ export declare function downloadTemplate(templateId: string, destDir: string): Promise<void>;
@@ -0,0 +1,56 @@
1
+ import { createWriteStream, mkdirSync, existsSync } from "fs";
2
+ import { join } from "path";
3
+ import { pipeline } from "stream/promises";
4
+ import { x as extractTar } from "tar";
5
+ import { execSync } from "child_process";
6
+ const TEMPLATE_REPOS = {
7
+ "nextjs-compact": "Roboticela/NextJS-Template-DevKit",
8
+ "vite-express-tauri": "Roboticela/Vite-ExpressJS-Tauri-Template-DevKit",
9
+ };
10
+ export async function downloadTemplate(templateId, destDir) {
11
+ const repo = TEMPLATE_REPOS[templateId];
12
+ if (!repo)
13
+ throw new Error(`Unknown template: ${templateId}`);
14
+ const tarballUrl = `https://api.github.com/repos/${repo}/tarball/main`;
15
+ // Try GitHub tarball API first
16
+ try {
17
+ await downloadViaTarball(tarballUrl, destDir);
18
+ return;
19
+ }
20
+ catch (e) {
21
+ console.warn(` Tarball download failed, falling back to git clone... (${e.message})`);
22
+ }
23
+ // Fallback: git clone --depth=1
24
+ try {
25
+ await cloneViaGit(`https://github.com/${repo}.git`, destDir);
26
+ }
27
+ catch {
28
+ throw new Error(`Failed to download template '${templateId}'. Check your internet connection.`);
29
+ }
30
+ }
31
+ async function downloadViaTarball(url, destDir) {
32
+ const response = await fetch(url, {
33
+ redirect: "follow",
34
+ headers: { "User-Agent": "@roboticela/devkit" },
35
+ });
36
+ if (!response.ok)
37
+ throw new Error(`HTTP ${response.status}`);
38
+ if (!response.body)
39
+ throw new Error("Empty response body");
40
+ if (!existsSync(destDir))
41
+ mkdirSync(destDir, { recursive: true });
42
+ const tmpTar = join(destDir, ".devkit-template.tar.gz");
43
+ const writer = createWriteStream(tmpTar);
44
+ await pipeline(response.body, writer);
45
+ // Extract, stripping the top-level GitHub-generated folder prefix
46
+ await extractTar({ file: tmpTar, cwd: destDir, strip: 1 });
47
+ // Clean up
48
+ const { unlinkSync } = await import("fs");
49
+ unlinkSync(tmpTar);
50
+ }
51
+ async function cloneViaGit(repoUrl, destDir) {
52
+ execSync(`git clone --depth=1 ${repoUrl} "${destDir}"`, { stdio: "inherit" });
53
+ // Remove .git history so the user starts fresh
54
+ const { rmSync } = await import("fs");
55
+ rmSync(join(destDir, ".git"), { recursive: true, force: true });
56
+ }
@@ -0,0 +1,2 @@
1
+ import { DevKitConfig } from "./config.js";
2
+ export declare function generateThemeCSS(config: DevKitConfig): string;