@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.
- package/dist/commands/add.d.ts +6 -0
- package/dist/commands/add.js +27 -0
- package/dist/commands/create.d.ts +11 -0
- package/dist/commands/create.js +181 -0
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +76 -0
- package/dist/commands/info.d.ts +1 -0
- package/dist/commands/info.js +53 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +65 -0
- package/dist/commands/list.d.ts +7 -0
- package/dist/commands/list.js +52 -0
- package/dist/commands/remove.d.ts +3 -0
- package/dist/commands/remove.js +22 -0
- package/dist/commands/theme.d.ts +6 -0
- package/dist/commands/theme.js +82 -0
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +45 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +155 -0
- package/dist/lib/config.d.ts +51 -0
- package/dist/lib/config.js +48 -0
- package/dist/lib/detector.d.ts +2 -0
- package/dist/lib/detector.js +18 -0
- package/dist/lib/installer.d.ts +2 -0
- package/dist/lib/installer.js +178 -0
- package/dist/lib/logger.d.ts +10 -0
- package/dist/lib/logger.js +15 -0
- package/dist/lib/registry.d.ts +26 -0
- package/dist/lib/registry.js +30 -0
- package/dist/lib/template.d.ts +1 -0
- package/dist/lib/template.js +56 -0
- package/dist/lib/theme.d.ts +2 -0
- package/dist/lib/theme.js +221 -0
- package/package.json +58 -0
package/dist/index.d.ts
ADDED
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,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,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
|
+
}
|