bosia 0.1.5 → 0.1.8
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/package.json +1 -1
- package/src/cli/add.ts +29 -75
- package/src/cli/create.ts +32 -5
- package/src/cli/feat.ts +55 -93
- package/src/cli/registry.ts +168 -0
- package/src/core/cookies.ts +40 -17
- package/src/core/env.ts +40 -10
- package/src/core/server.ts +1 -1
- package/templates/drizzle/package.json +3 -9
- package/templates/drizzle/template.json +3 -0
- package/templates/drizzle/drizzle.config.ts +0 -10
- package/templates/drizzle/src/features/drizzle/index.ts +0 -15
- package/templates/drizzle/src/features/drizzle/migrations/.gitkeep +0 -0
- package/templates/drizzle/src/features/drizzle/schemas.ts +0 -1
- package/templates/drizzle/src/features/drizzle/seeds/001_initial_todos.ts +0 -11
- package/templates/drizzle/src/features/drizzle/seeds/runner.ts +0 -80
- package/templates/drizzle/src/features/todo/index.ts +0 -3
- package/templates/drizzle/src/features/todo/queries.ts +0 -36
- package/templates/drizzle/src/features/todo/schemas/todo.table.ts +0 -9
- package/templates/drizzle/src/features/todo/types.ts +0 -5
- package/templates/drizzle/src/lib/components/todo/index.ts +0 -3
- package/templates/drizzle/src/lib/components/todo/todo-form.svelte +0 -23
- package/templates/drizzle/src/lib/components/todo/todo-item.svelte +0 -63
- package/templates/drizzle/src/lib/components/todo/todo-list.svelte +0 -21
- package/templates/drizzle/src/routes/api/todos/+server.ts +0 -18
- package/templates/drizzle/src/routes/api/todos/[id]/+server.ts +0 -42
- package/templates/drizzle/src/routes/todos/+page.server.ts +0 -52
- package/templates/drizzle/src/routes/todos/+page.svelte +0 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing inspired by SvelteKit. No Node.js, no Vite, no adapters.",
|
|
6
6
|
"keywords": [
|
package/src/cli/add.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { join, dirname } from "path";
|
|
2
2
|
import { mkdirSync, writeFileSync, readFileSync, existsSync } from "fs";
|
|
3
|
-
import { spawn } from "bun";
|
|
4
3
|
import * as p from "@clack/prompts";
|
|
4
|
+
import {
|
|
5
|
+
type InstallOptions,
|
|
6
|
+
REGISTRY_URL,
|
|
7
|
+
resolveLocalRegistryOrExit,
|
|
8
|
+
readRegistryJSON,
|
|
9
|
+
readRegistryFile,
|
|
10
|
+
mergePkgJson,
|
|
11
|
+
bunAdd,
|
|
12
|
+
} from "./registry.ts";
|
|
5
13
|
|
|
6
14
|
// ─── bosia add <component> ────────────────────────────────
|
|
7
15
|
// Fetches a component from the GitHub registry (or local registry
|
|
@@ -11,8 +19,6 @@ import * as p from "@clack/prompts";
|
|
|
11
19
|
// bosia add button → src/lib/components/ui/button/
|
|
12
20
|
// bosia add shop/cart → src/lib/components/shop/cart/
|
|
13
21
|
|
|
14
|
-
const REMOTE_BASE = "https://raw.githubusercontent.com/bosapi/bosia/main/registry";
|
|
15
|
-
|
|
16
22
|
interface ComponentMeta {
|
|
17
23
|
name: string;
|
|
18
24
|
description: string;
|
|
@@ -46,8 +52,7 @@ export async function runAdd(name: string | undefined, flags: string[] = []) {
|
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
if (flags.includes("--local")) {
|
|
49
|
-
|
|
50
|
-
registryRoot = resolveLocalRegistry();
|
|
55
|
+
registryRoot = resolveLocalRegistryOrExit();
|
|
51
56
|
console.log(`⬡ Using local registry: ${registryRoot}\n`);
|
|
52
57
|
}
|
|
53
58
|
|
|
@@ -88,31 +93,35 @@ async function loadIndex(): Promise<RegistryIndex | null> {
|
|
|
88
93
|
if (existsSync(path)) return JSON.parse(readFileSync(path, "utf-8"));
|
|
89
94
|
return null;
|
|
90
95
|
}
|
|
91
|
-
|
|
96
|
+
const res = await fetch(`${REGISTRY_URL}/index.json`);
|
|
97
|
+
if (!res.ok) return null;
|
|
98
|
+
return await res.json() as RegistryIndex;
|
|
92
99
|
} catch {
|
|
93
100
|
return null;
|
|
94
101
|
}
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
export async function addComponent(name: string, root = false) {
|
|
104
|
+
export async function addComponent(name: string, root = false, options?: InstallOptions) {
|
|
98
105
|
// Resolve the full path (e.g. "button" → "ui/button", "shop/cart" stays "shop/cart")
|
|
99
106
|
const fullPath = resolveDestPath(name);
|
|
100
107
|
|
|
101
108
|
if (installed.has(fullPath)) return;
|
|
102
109
|
installed.add(fullPath);
|
|
103
110
|
|
|
111
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
112
|
+
|
|
104
113
|
console.log(root ? `⬡ Installing component: ${name}\n` : ` 📦 Dependency: ${name}`);
|
|
105
114
|
|
|
106
|
-
const meta = await
|
|
115
|
+
const meta = await readRegistryJSON<ComponentMeta>(registryRoot, "components", fullPath, "meta.json");
|
|
107
116
|
|
|
108
117
|
// Install component dependencies first (recursive)
|
|
109
118
|
for (const dep of meta.dependencies) {
|
|
110
|
-
await addComponent(dep, false);
|
|
119
|
+
await addComponent(dep, false, options);
|
|
111
120
|
}
|
|
112
121
|
|
|
113
|
-
// Check if component already exists
|
|
114
|
-
const destDir = join(
|
|
115
|
-
if (existsSync(destDir)) {
|
|
122
|
+
// Check if component already exists (skip check entirely in non-interactive mode)
|
|
123
|
+
const destDir = join(cwd, "src", "lib", "components", fullPath);
|
|
124
|
+
if (!options?.skipPrompts && existsSync(destDir)) {
|
|
116
125
|
const replace = await p.confirm({
|
|
117
126
|
message: `Component "${name}" already exists at src/lib/components/${fullPath}/. Replace it?`,
|
|
118
127
|
});
|
|
@@ -126,25 +135,20 @@ export async function addComponent(name: string, root = false) {
|
|
|
126
135
|
mkdirSync(destDir, { recursive: true });
|
|
127
136
|
|
|
128
137
|
for (const file of meta.files) {
|
|
129
|
-
const content = await
|
|
138
|
+
const content = await readRegistryFile(registryRoot, "components", fullPath, file);
|
|
130
139
|
const dest = join(destDir, file);
|
|
131
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
140
|
+
if (file.includes("/")) mkdirSync(dirname(dest), { recursive: true });
|
|
132
141
|
writeFileSync(dest, content, "utf-8");
|
|
133
142
|
console.log(` ✍️ src/lib/components/${fullPath}/${file}`);
|
|
134
143
|
}
|
|
135
144
|
|
|
136
145
|
// Install npm dependencies
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
stderr: "inherit",
|
|
144
|
-
cwd: process.cwd(),
|
|
145
|
-
});
|
|
146
|
-
if ((await proc.exited) !== 0) {
|
|
147
|
-
console.warn(` ⚠️ bun add failed for: ${packages.join(", ")}`);
|
|
146
|
+
if (Object.keys(meta.npmDeps).length > 0) {
|
|
147
|
+
if (options?.skipInstall) {
|
|
148
|
+
const { addedDeps } = mergePkgJson(cwd, { deps: meta.npmDeps });
|
|
149
|
+
if (addedDeps.length > 0) console.log(` 📥 Added to package.json: ${addedDeps.join(", ")}`);
|
|
150
|
+
} else {
|
|
151
|
+
await bunAdd(cwd, meta.npmDeps);
|
|
148
152
|
}
|
|
149
153
|
}
|
|
150
154
|
|
|
@@ -169,53 +173,3 @@ function ensureUtils() {
|
|
|
169
173
|
console.log(" ✍️ src/lib/utils.ts (cn utility)\n");
|
|
170
174
|
}
|
|
171
175
|
}
|
|
172
|
-
|
|
173
|
-
// ─── Registry resolvers ──────────────────────────────────────
|
|
174
|
-
|
|
175
|
-
function resolveLocalRegistry(): string {
|
|
176
|
-
// Walk up from this file's directory to find registry/
|
|
177
|
-
let dir = dirname(new URL(import.meta.url).pathname);
|
|
178
|
-
for (let i = 0; i < 10; i++) {
|
|
179
|
-
const candidate = join(dir, "registry");
|
|
180
|
-
if (existsSync(join(candidate, "index.json"))) return candidate;
|
|
181
|
-
const parent = dirname(dir);
|
|
182
|
-
if (parent === dir) break;
|
|
183
|
-
dir = parent;
|
|
184
|
-
}
|
|
185
|
-
console.error("❌ Could not find local registry/ directory.");
|
|
186
|
-
process.exit(1);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
async function readMeta(name: string): Promise<ComponentMeta> {
|
|
190
|
-
if (registryRoot) {
|
|
191
|
-
const path = join(registryRoot, "components", name, "meta.json");
|
|
192
|
-
if (!existsSync(path)) {
|
|
193
|
-
throw new Error(`Component "${name}" not found in local registry`);
|
|
194
|
-
}
|
|
195
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
196
|
-
}
|
|
197
|
-
return fetchJSON<ComponentMeta>(`${REMOTE_BASE}/components/${name}/meta.json`);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async function readFile(name: string, file: string): Promise<string> {
|
|
201
|
-
if (registryRoot) {
|
|
202
|
-
const path = join(registryRoot, "components", name, file);
|
|
203
|
-
if (!existsSync(path)) {
|
|
204
|
-
throw new Error(`File "${file}" not found for component "${name}" in local registry`);
|
|
205
|
-
}
|
|
206
|
-
return readFileSync(path, "utf-8");
|
|
207
|
-
}
|
|
208
|
-
return fetchText(`${REMOTE_BASE}/components/${name}/${file}`);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async function fetchJSON<T>(url: string): Promise<T> {
|
|
212
|
-
const res = await fetch(url);
|
|
213
|
-
if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
|
|
214
|
-
return res.json() as Promise<T>;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async function fetchText(url: string): Promise<string> {
|
|
218
|
-
const res = await fetch(url);
|
|
219
|
-
if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
|
|
220
|
-
return res.text();
|
|
221
|
-
}
|
package/src/cli/create.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { resolve, join, basename, relative } from "path";
|
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
3
|
import { spawn } from "bun";
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
|
+
import { installFeature, initFeatRegistry, resolveLocalRegistry } from "./feat.ts";
|
|
6
|
+
import { initAddRegistry } from "./add.ts";
|
|
5
7
|
|
|
6
8
|
// ─── bosia create <name> [--template <name>] ──────────────
|
|
7
9
|
|
|
@@ -59,7 +61,32 @@ export async function runCreate(name: string | undefined, args: string[] = []) {
|
|
|
59
61
|
writeFileSync(join(targetDir, ".env"), readFileSync(join(targetDir, ".env.example"), "utf-8"));
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
// Install template features from registry
|
|
65
|
+
const templateConfigPath = join(templateDir, "template.json");
|
|
66
|
+
if (existsSync(templateConfigPath)) {
|
|
67
|
+
const config = JSON.parse(readFileSync(templateConfigPath, "utf-8"));
|
|
68
|
+
if (config.features?.length) {
|
|
69
|
+
let localRegistry: string | null = null;
|
|
70
|
+
try {
|
|
71
|
+
localRegistry = resolveLocalRegistry();
|
|
72
|
+
} catch {
|
|
73
|
+
// Local registry not found — will use remote
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await initAddRegistry(localRegistry);
|
|
77
|
+
initFeatRegistry(localRegistry);
|
|
78
|
+
|
|
79
|
+
for (const feat of config.features) {
|
|
80
|
+
await installFeature(feat, true, {
|
|
81
|
+
skipInstall: true,
|
|
82
|
+
skipPrompts: true,
|
|
83
|
+
cwd: targetDir,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(`\n✅ Project created at ${targetDir}\n`);
|
|
63
90
|
|
|
64
91
|
console.log("Installing dependencies...");
|
|
65
92
|
const proc = spawn(["bun", "install"], {
|
|
@@ -72,7 +99,7 @@ export async function runCreate(name: string | undefined, args: string[] = []) {
|
|
|
72
99
|
console.warn("⚠️ bun install failed — run it manually.");
|
|
73
100
|
} else {
|
|
74
101
|
console.log(`\n🎉 Ready!\n\ncd ${name}`);
|
|
75
|
-
|
|
102
|
+
|
|
76
103
|
const instPath = join(templateDir, "instructions.txt");
|
|
77
104
|
if (existsSync(instPath)) {
|
|
78
105
|
const instructions = readFileSync(instPath, "utf-8").trimEnd();
|
|
@@ -117,9 +144,9 @@ function copyDir(src: string, dest: string, projectName: string, isLocal: boolea
|
|
|
117
144
|
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
118
145
|
const srcPath = join(src, entry.name);
|
|
119
146
|
const destPath = join(dest, entry.name);
|
|
120
|
-
|
|
121
|
-
// Do not copy instructions.txt to the final project
|
|
122
|
-
if (entry.name === "instructions.txt") continue;
|
|
147
|
+
|
|
148
|
+
// Do not copy instructions.txt or template.json to the final project
|
|
149
|
+
if (entry.name === "instructions.txt" || entry.name === "template.json") continue;
|
|
123
150
|
|
|
124
151
|
if (entry.isDirectory()) {
|
|
125
152
|
copyDir(srcPath, destPath, projectName, isLocal);
|
package/src/cli/feat.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { join, dirname } from "path";
|
|
2
2
|
import { mkdirSync, writeFileSync, readFileSync, existsSync } from "fs";
|
|
3
|
-
import { spawn } from "bun";
|
|
4
3
|
import * as p from "@clack/prompts";
|
|
5
4
|
import { addComponent, initAddRegistry } from "./add.ts";
|
|
5
|
+
import {
|
|
6
|
+
type InstallOptions,
|
|
7
|
+
resolveLocalRegistryOrExit,
|
|
8
|
+
readRegistryJSON,
|
|
9
|
+
readRegistryFile,
|
|
10
|
+
mergePkgJson,
|
|
11
|
+
bunAdd,
|
|
12
|
+
} from "./registry.ts";
|
|
6
13
|
|
|
7
14
|
// ─── bosia feat <feature> [--local] ──────────────────────
|
|
8
15
|
// Fetches a feature scaffold from the GitHub registry (or local
|
|
9
16
|
// registry with --local) and copies route/lib files, installs npm deps.
|
|
10
17
|
// Supports nested feature dependencies (e.g. todo → drizzle).
|
|
11
18
|
|
|
12
|
-
const REGISTRY_BASE = "https://raw.githubusercontent.com/bosapi/bosia/main/registry";
|
|
13
|
-
|
|
14
19
|
interface FeatureMeta {
|
|
15
20
|
name: string;
|
|
16
21
|
description: string;
|
|
@@ -19,6 +24,7 @@ interface FeatureMeta {
|
|
|
19
24
|
files: string[]; // source filenames in the registry feature dir
|
|
20
25
|
targets: string[]; // destination paths relative to project root
|
|
21
26
|
npmDeps: Record<string, string>;
|
|
27
|
+
npmDevDeps?: Record<string, string>;
|
|
22
28
|
scripts?: Record<string, string>; // package.json scripts to add
|
|
23
29
|
envVars?: Record<string, string>; // env vars to append to .env if missing
|
|
24
30
|
}
|
|
@@ -35,7 +41,7 @@ export async function runFeat(name: string | undefined, flags: string[] = []) {
|
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
if (flags.includes("--local")) {
|
|
38
|
-
registryRoot =
|
|
44
|
+
registryRoot = resolveLocalRegistryOrExit();
|
|
39
45
|
console.log(`⬡ Using local registry: ${registryRoot}\n`);
|
|
40
46
|
}
|
|
41
47
|
|
|
@@ -45,18 +51,25 @@ export async function runFeat(name: string | undefined, flags: string[] = []) {
|
|
|
45
51
|
await installFeature(name, true);
|
|
46
52
|
}
|
|
47
53
|
|
|
48
|
-
|
|
54
|
+
/** Set the registry root for feature resolution. Called by create.ts for template features. */
|
|
55
|
+
export function initFeatRegistry(root: string | null) {
|
|
56
|
+
registryRoot = root;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function installFeature(name: string, isRoot: boolean, options?: InstallOptions) {
|
|
49
60
|
if (installedFeats.has(name)) return;
|
|
50
61
|
installedFeats.add(name);
|
|
51
62
|
|
|
63
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
64
|
+
|
|
52
65
|
console.log(isRoot ? `⬡ Installing feature: ${name}\n` : `\n⬡ Installing dependency feature: ${name}\n`);
|
|
53
66
|
|
|
54
|
-
const meta = await
|
|
67
|
+
const meta = await readRegistryJSON<FeatureMeta>(registryRoot, "features", name, "meta.json");
|
|
55
68
|
|
|
56
69
|
// Install required feature dependencies first (recursive)
|
|
57
70
|
if (meta.features && meta.features.length > 0) {
|
|
58
71
|
for (const feat of meta.features) {
|
|
59
|
-
await installFeature(feat, false);
|
|
72
|
+
await installFeature(feat, false, options);
|
|
60
73
|
}
|
|
61
74
|
}
|
|
62
75
|
|
|
@@ -64,19 +77,20 @@ async function installFeature(name: string, isRoot: boolean) {
|
|
|
64
77
|
if (meta.components.length > 0) {
|
|
65
78
|
console.log("📦 Installing required components...");
|
|
66
79
|
for (const comp of meta.components) {
|
|
67
|
-
await addComponent(comp, false);
|
|
80
|
+
await addComponent(comp, false, options);
|
|
68
81
|
}
|
|
69
82
|
console.log("");
|
|
70
83
|
}
|
|
71
84
|
|
|
72
85
|
// Copy feature files to their target paths
|
|
86
|
+
const createdDirs = new Set<string>();
|
|
73
87
|
for (let i = 0; i < meta.files.length; i++) {
|
|
74
88
|
const file = meta.files[i]!;
|
|
75
89
|
const target = meta.targets[i] ?? file;
|
|
76
|
-
const dest = join(
|
|
90
|
+
const dest = join(cwd, target);
|
|
77
91
|
|
|
78
|
-
// Prompt before overwriting existing files
|
|
79
|
-
if (existsSync(dest)) {
|
|
92
|
+
// Prompt before overwriting existing files (skip check entirely in non-interactive mode)
|
|
93
|
+
if (!options?.skipPrompts && existsSync(dest)) {
|
|
80
94
|
const replace = await p.confirm({
|
|
81
95
|
message: `File "${target}" already exists. Replace it?`,
|
|
82
96
|
});
|
|
@@ -86,52 +100,46 @@ async function installFeature(name: string, isRoot: boolean) {
|
|
|
86
100
|
}
|
|
87
101
|
}
|
|
88
102
|
|
|
89
|
-
const content = await
|
|
90
|
-
|
|
103
|
+
const content = await readRegistryFile(registryRoot, "features", name, file);
|
|
104
|
+
const dir = dirname(dest);
|
|
105
|
+
if (!createdDirs.has(dir)) {
|
|
106
|
+
mkdirSync(dir, { recursive: true });
|
|
107
|
+
createdDirs.add(dir);
|
|
108
|
+
}
|
|
91
109
|
writeFileSync(dest, content, "utf-8");
|
|
92
110
|
console.log(` ✍️ ${target}`);
|
|
93
111
|
}
|
|
94
112
|
|
|
95
113
|
// Install npm dependencies
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (existsSync(pkgPath)) {
|
|
115
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
116
|
-
pkg.scripts = pkg.scripts ?? {};
|
|
117
|
-
const added: string[] = [];
|
|
118
|
-
for (const [key, val] of scriptEntries) {
|
|
119
|
-
if (!pkg.scripts[key]) {
|
|
120
|
-
pkg.scripts[key] = val;
|
|
121
|
-
added.push(key);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
if (added.length > 0) {
|
|
125
|
-
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
126
|
-
console.log(`\n📜 Added scripts: ${added.join(", ")}`);
|
|
114
|
+
const hasDeps = Object.keys(meta.npmDeps).length > 0;
|
|
115
|
+
const hasDevDeps = Object.keys(meta.npmDevDeps ?? {}).length > 0;
|
|
116
|
+
const hasScripts = Object.keys(meta.scripts ?? {}).length > 0;
|
|
117
|
+
|
|
118
|
+
if (hasDeps || hasDevDeps) {
|
|
119
|
+
if (options?.skipInstall) {
|
|
120
|
+
const { addedDeps, addedScripts } = mergePkgJson(cwd, {
|
|
121
|
+
deps: meta.npmDeps,
|
|
122
|
+
devDeps: meta.npmDevDeps,
|
|
123
|
+
scripts: meta.scripts,
|
|
124
|
+
});
|
|
125
|
+
if (addedDeps.length > 0) console.log(`\n📥 Added to package.json: ${addedDeps.join(", ")}`);
|
|
126
|
+
if (addedScripts.length > 0) console.log(`\n📜 Added scripts: ${addedScripts.join(", ")}`);
|
|
127
|
+
} else {
|
|
128
|
+
await bunAdd(cwd, meta.npmDeps, meta.npmDevDeps);
|
|
129
|
+
if (hasScripts) {
|
|
130
|
+
const { addedScripts } = mergePkgJson(cwd, { scripts: meta.scripts });
|
|
131
|
+
if (addedScripts.length > 0) console.log(`\n📜 Added scripts: ${addedScripts.join(", ")}`);
|
|
127
132
|
}
|
|
128
133
|
}
|
|
134
|
+
} else if (hasScripts) {
|
|
135
|
+
const { addedScripts } = mergePkgJson(cwd, { scripts: meta.scripts });
|
|
136
|
+
if (addedScripts.length > 0) console.log(`\n📜 Added scripts: ${addedScripts.join(", ")}`);
|
|
129
137
|
}
|
|
130
138
|
|
|
131
139
|
// Append env vars to .env if missing
|
|
132
140
|
const envEntries = Object.entries(meta.envVars ?? {});
|
|
133
141
|
if (envEntries.length > 0) {
|
|
134
|
-
const envPath = join(
|
|
142
|
+
const envPath = join(cwd, ".env");
|
|
135
143
|
const existing = existsSync(envPath) ? readFileSync(envPath, "utf-8") : "";
|
|
136
144
|
const toAdd: string[] = [];
|
|
137
145
|
for (const [key, val] of envEntries) {
|
|
@@ -154,51 +162,5 @@ async function installFeature(name: string, isRoot: boolean) {
|
|
|
154
162
|
}
|
|
155
163
|
}
|
|
156
164
|
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
function resolveLocalRegistry(): string {
|
|
160
|
-
let dir = dirname(new URL(import.meta.url).pathname);
|
|
161
|
-
for (let i = 0; i < 10; i++) {
|
|
162
|
-
const candidate = join(dir, "registry");
|
|
163
|
-
if (existsSync(join(candidate, "index.json"))) return candidate;
|
|
164
|
-
const parent = dirname(dir);
|
|
165
|
-
if (parent === dir) break;
|
|
166
|
-
dir = parent;
|
|
167
|
-
}
|
|
168
|
-
console.error("❌ Could not find local registry/ directory.");
|
|
169
|
-
process.exit(1);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async function readMeta(name: string): Promise<FeatureMeta> {
|
|
173
|
-
if (registryRoot) {
|
|
174
|
-
const path = join(registryRoot, "features", name, "meta.json");
|
|
175
|
-
if (!existsSync(path)) {
|
|
176
|
-
throw new Error(`Feature "${name}" not found in local registry`);
|
|
177
|
-
}
|
|
178
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
179
|
-
}
|
|
180
|
-
return fetchJSON<FeatureMeta>(`${REGISTRY_BASE}/features/${name}/meta.json`);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async function readFile(name: string, file: string): Promise<string> {
|
|
184
|
-
if (registryRoot) {
|
|
185
|
-
const path = join(registryRoot, "features", name, file);
|
|
186
|
-
if (!existsSync(path)) {
|
|
187
|
-
throw new Error(`File "${file}" not found for feature "${name}" in local registry`);
|
|
188
|
-
}
|
|
189
|
-
return readFileSync(path, "utf-8");
|
|
190
|
-
}
|
|
191
|
-
return fetchText(`${REGISTRY_BASE}/features/${name}/${file}`);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async function fetchJSON<T>(url: string): Promise<T> {
|
|
195
|
-
const res = await fetch(url);
|
|
196
|
-
if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
|
|
197
|
-
return res.json() as Promise<T>;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async function fetchText(url: string): Promise<string> {
|
|
201
|
-
const res = await fetch(url);
|
|
202
|
-
if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
|
|
203
|
-
return res.text();
|
|
204
|
-
}
|
|
165
|
+
// Re-exports for create.ts
|
|
166
|
+
export { resolveLocalRegistry, type InstallOptions } from "./registry.ts";
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { join, dirname } from "path";
|
|
2
|
+
import { writeFileSync, readFileSync, existsSync } from "fs";
|
|
3
|
+
import { spawn } from "bun";
|
|
4
|
+
|
|
5
|
+
// ─── Shared registry utilities for feat.ts and add.ts ─────
|
|
6
|
+
|
|
7
|
+
export const REGISTRY_URL = "https://raw.githubusercontent.com/bosapi/bosia/main/registry";
|
|
8
|
+
|
|
9
|
+
export interface InstallOptions {
|
|
10
|
+
skipInstall?: boolean; // write deps to package.json instead of `bun add`
|
|
11
|
+
skipPrompts?: boolean; // auto-overwrite, no interactive prompts
|
|
12
|
+
cwd?: string; // override process.cwd() for file operations
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ─── Local registry resolution ────────────────────────────
|
|
16
|
+
|
|
17
|
+
export function resolveLocalRegistry(): string {
|
|
18
|
+
let dir = dirname(new URL(import.meta.url).pathname);
|
|
19
|
+
for (let i = 0; i < 10; i++) {
|
|
20
|
+
const candidate = join(dir, "registry");
|
|
21
|
+
if (existsSync(join(candidate, "index.json"))) return candidate;
|
|
22
|
+
const parent = dirname(dir);
|
|
23
|
+
if (parent === dir) break;
|
|
24
|
+
dir = parent;
|
|
25
|
+
}
|
|
26
|
+
throw new Error("Could not find local registry/ directory.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Resolve local registry, exiting with error message on failure. For CLI entry points. */
|
|
30
|
+
export function resolveLocalRegistryOrExit(): string {
|
|
31
|
+
try {
|
|
32
|
+
return resolveLocalRegistry();
|
|
33
|
+
} catch {
|
|
34
|
+
console.error("❌ Could not find local registry/ directory.");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Registry file readers ────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/** Read and parse a JSON file from the registry (local or remote). */
|
|
42
|
+
export async function readRegistryJSON<T>(
|
|
43
|
+
registryRoot: string | null,
|
|
44
|
+
category: string,
|
|
45
|
+
name: string,
|
|
46
|
+
file: string,
|
|
47
|
+
): Promise<T> {
|
|
48
|
+
if (registryRoot) {
|
|
49
|
+
const path = join(registryRoot, category, name, file);
|
|
50
|
+
if (!existsSync(path)) {
|
|
51
|
+
throw new Error(`"${file}" not found for ${category.slice(0, -1)} "${name}" in local registry`);
|
|
52
|
+
}
|
|
53
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
54
|
+
}
|
|
55
|
+
return fetchJSON<T>(`${REGISTRY_URL}/${category}/${name}/${file}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Read a text file from the registry (local or remote). */
|
|
59
|
+
export async function readRegistryFile(
|
|
60
|
+
registryRoot: string | null,
|
|
61
|
+
category: string,
|
|
62
|
+
name: string,
|
|
63
|
+
file: string,
|
|
64
|
+
): Promise<string> {
|
|
65
|
+
if (registryRoot) {
|
|
66
|
+
const path = join(registryRoot, category, name, file);
|
|
67
|
+
if (!existsSync(path)) {
|
|
68
|
+
throw new Error(`File "${file}" not found for ${category.slice(0, -1)} "${name}" in local registry`);
|
|
69
|
+
}
|
|
70
|
+
return readFileSync(path, "utf-8");
|
|
71
|
+
}
|
|
72
|
+
return fetchText(`${REGISTRY_URL}/${category}/${name}/${file}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── package.json helpers ─────────────────────────────────
|
|
76
|
+
|
|
77
|
+
export interface PkgDeps {
|
|
78
|
+
deps?: Record<string, string>;
|
|
79
|
+
devDeps?: Record<string, string>;
|
|
80
|
+
scripts?: Record<string, string>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Merge dependencies and scripts into package.json in a single read/write.
|
|
85
|
+
* Returns the list of added keys, or empty arrays if nothing changed.
|
|
86
|
+
*/
|
|
87
|
+
export function mergePkgJson(cwd: string, changes: PkgDeps): { addedDeps: string[]; addedScripts: string[] } {
|
|
88
|
+
const pkgPath = join(cwd, "package.json");
|
|
89
|
+
if (!existsSync(pkgPath)) return { addedDeps: [], addedScripts: [] };
|
|
90
|
+
|
|
91
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
92
|
+
let changed = false;
|
|
93
|
+
const addedDeps: string[] = [];
|
|
94
|
+
const addedScripts: string[] = [];
|
|
95
|
+
|
|
96
|
+
if (changes.deps && Object.keys(changes.deps).length > 0) {
|
|
97
|
+
pkg.dependencies = pkg.dependencies ?? {};
|
|
98
|
+
for (const [name, ver] of Object.entries(changes.deps)) {
|
|
99
|
+
if (!pkg.dependencies[name]) {
|
|
100
|
+
pkg.dependencies[name] = ver;
|
|
101
|
+
addedDeps.push(name);
|
|
102
|
+
changed = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (changes.devDeps && Object.keys(changes.devDeps).length > 0) {
|
|
108
|
+
pkg.devDependencies = pkg.devDependencies ?? {};
|
|
109
|
+
for (const [name, ver] of Object.entries(changes.devDeps)) {
|
|
110
|
+
if (!pkg.devDependencies[name]) {
|
|
111
|
+
pkg.devDependencies[name] = ver;
|
|
112
|
+
addedDeps.push(name);
|
|
113
|
+
changed = true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (changes.scripts && Object.keys(changes.scripts).length > 0) {
|
|
119
|
+
pkg.scripts = pkg.scripts ?? {};
|
|
120
|
+
for (const [key, val] of Object.entries(changes.scripts)) {
|
|
121
|
+
if (!pkg.scripts[key]) {
|
|
122
|
+
pkg.scripts[key] = val;
|
|
123
|
+
addedScripts.push(key);
|
|
124
|
+
changed = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (changed) {
|
|
130
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { addedDeps, addedScripts };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Run `bun add` for deps and optionally `bun add --dev` for devDeps. */
|
|
137
|
+
export async function bunAdd(cwd: string, deps: Record<string, string>, devDeps?: Record<string, string>): Promise<void> {
|
|
138
|
+
const packages = Object.entries(deps).map(([pkg, ver]) => (ver ? `${pkg}@${ver}` : pkg));
|
|
139
|
+
if (packages.length > 0) {
|
|
140
|
+
console.log(`\n📥 npm: ${packages.join(", ")}`);
|
|
141
|
+
const proc = spawn(["bun", "add", ...packages], { stdout: "inherit", stderr: "inherit", cwd });
|
|
142
|
+
if ((await proc.exited) !== 0) {
|
|
143
|
+
console.warn(`⚠️ bun add failed for: ${packages.join(", ")}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const devPackages = Object.entries(devDeps ?? {}).map(([pkg, ver]) => (ver ? `${pkg}@${ver}` : pkg));
|
|
147
|
+
if (devPackages.length > 0) {
|
|
148
|
+
console.log(`\n📥 npm (dev): ${devPackages.join(", ")}`);
|
|
149
|
+
const proc = spawn(["bun", "add", "--dev", ...devPackages], { stdout: "inherit", stderr: "inherit", cwd });
|
|
150
|
+
if ((await proc.exited) !== 0) {
|
|
151
|
+
console.warn(`⚠️ bun add --dev failed for: ${devPackages.join(", ")}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── HTTP helpers ─────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
async function fetchJSON<T>(url: string): Promise<T> {
|
|
159
|
+
const res = await fetch(url);
|
|
160
|
+
if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
|
|
161
|
+
return res.json() as Promise<T>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function fetchText(url: string): Promise<string> {
|
|
165
|
+
const res = await fetch(url);
|
|
166
|
+
if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
|
|
167
|
+
return res.text();
|
|
168
|
+
}
|