bosia 0.0.0 → 0.1.1

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.
Files changed (77) hide show
  1. package/README.md +173 -7
  2. package/package.json +49 -5
  3. package/src/cli/add.ts +151 -0
  4. package/src/cli/build.ts +16 -0
  5. package/src/cli/create.ts +113 -0
  6. package/src/cli/dev.ts +14 -0
  7. package/src/cli/feat.ts +80 -0
  8. package/src/cli/index.ts +78 -0
  9. package/src/cli/start.ts +26 -0
  10. package/src/core/build.ts +157 -0
  11. package/src/core/client/App.svelte +157 -0
  12. package/src/core/client/hydrate.ts +81 -0
  13. package/src/core/client/prefetch.ts +109 -0
  14. package/src/core/client/router.svelte.ts +47 -0
  15. package/src/core/cookies.ts +68 -0
  16. package/src/core/cors.ts +60 -0
  17. package/src/core/csrf.ts +65 -0
  18. package/src/core/dev.ts +225 -0
  19. package/src/core/env.ts +153 -0
  20. package/src/core/envCodegen.ts +94 -0
  21. package/src/core/errors.ts +35 -0
  22. package/src/core/hooks.ts +92 -0
  23. package/src/core/html.ts +212 -0
  24. package/src/core/matcher.ts +80 -0
  25. package/src/core/paths.ts +32 -0
  26. package/src/core/plugin.ts +93 -0
  27. package/src/core/prerender.ts +86 -0
  28. package/src/core/renderer.ts +314 -0
  29. package/src/core/routeFile.ts +110 -0
  30. package/src/core/routeTypes.ts +106 -0
  31. package/src/core/scanner.ts +99 -0
  32. package/src/core/server.ts +414 -0
  33. package/src/core/types.ts +37 -0
  34. package/src/lib/index.ts +21 -0
  35. package/src/lib/utils.ts +24 -0
  36. package/templates/default/.env.example +75 -0
  37. package/templates/default/README.md +102 -0
  38. package/templates/default/package.json +21 -0
  39. package/templates/default/public/.gitkeep +0 -0
  40. package/templates/default/public/favicon.svg +14 -0
  41. package/templates/default/src/app.css +132 -0
  42. package/templates/default/src/app.d.ts +7 -0
  43. package/templates/default/src/lib/.gitkeep +0 -0
  44. package/templates/default/src/routes/+error.svelte +18 -0
  45. package/templates/default/src/routes/+layout.svelte +6 -0
  46. package/templates/default/src/routes/+page.svelte +36 -0
  47. package/templates/default/src/routes/about/+page.server.ts +1 -0
  48. package/templates/default/src/routes/about/+page.svelte +8 -0
  49. package/templates/default/tsconfig.json +22 -0
  50. package/templates/demo/.env.example +52 -0
  51. package/templates/demo/README.md +29 -0
  52. package/templates/demo/package.json +20 -0
  53. package/templates/demo/public/.gitkeep +0 -0
  54. package/templates/demo/public/favicon.svg +14 -0
  55. package/templates/demo/src/app.css +132 -0
  56. package/templates/demo/src/app.d.ts +7 -0
  57. package/templates/demo/src/hooks.server.ts +21 -0
  58. package/templates/demo/src/lib/utils.ts +1 -0
  59. package/templates/demo/src/routes/(public)/+layout.svelte +31 -0
  60. package/templates/demo/src/routes/(public)/+page.svelte +79 -0
  61. package/templates/demo/src/routes/(public)/about/+page.server.ts +1 -0
  62. package/templates/demo/src/routes/(public)/about/+page.svelte +31 -0
  63. package/templates/demo/src/routes/(public)/all/[...catchall]/+page.svelte +38 -0
  64. package/templates/demo/src/routes/(public)/blog/+page.svelte +55 -0
  65. package/templates/demo/src/routes/(public)/blog/[slug]/+page.server.ts +62 -0
  66. package/templates/demo/src/routes/(public)/blog/[slug]/+page.svelte +53 -0
  67. package/templates/demo/src/routes/+error.svelte +15 -0
  68. package/templates/demo/src/routes/+layout.server.ts +10 -0
  69. package/templates/demo/src/routes/+layout.svelte +6 -0
  70. package/templates/demo/src/routes/actions-test/+page.server.ts +28 -0
  71. package/templates/demo/src/routes/actions-test/+page.svelte +60 -0
  72. package/templates/demo/src/routes/api/hello/+server.ts +44 -0
  73. package/templates/demo/tsconfig.json +22 -0
  74. package/CLAUDE.md +0 -106
  75. package/bun.lock +0 -25
  76. package/index.ts +0 -1
  77. package/tsconfig.json +0 -29
package/README.md CHANGED
@@ -1,15 +1,181 @@
1
- # asbun
1
+ # Bosia
2
2
 
3
- To install dependencies:
3
+ > Full documentation: [bosia.bosapi.com](https://bosia.bosapi.com)
4
+
5
+ A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS.
6
+
7
+ File-based routing inspired by SvelteKit, built on top of the Bun runtime and ElysiaJS HTTP server. No Node.js, no Vite, no adapters.
8
+
9
+ ## Features
10
+
11
+ - **File-based routing** — `+page.svelte`, `+layout.svelte`, `+server.ts`, route groups, dynamic segments, catch-all routes
12
+ - **Server-side rendering** — every page is rendered on the server with full hydration
13
+ - **Server loaders** — `+page.server.ts` and `+layout.server.ts` with `parent()` data threading
14
+ - **API routes** — `+server.ts` exports HTTP verbs (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS`)
15
+ - **Middleware hooks** — `hooks.server.ts` with `sequence()` for auth, logging, locals
16
+ - **Dev server with HMR** — file watcher + SSE browser reload, no page blink
17
+ - **Tailwind CSS v4** — compiled at build time, shadcn-inspired design tokens out of the box
18
+ - **CLI** — `bosia create`, `bosia dev`, `bosia build`, `bosia add`, `bosia feat`
19
+
20
+ ## Quick Start
4
21
 
5
22
  ```bash
6
- bun install
23
+ # Scaffold a new project
24
+ bun x bosia create my-app
25
+ cd my-app
26
+
27
+ # Start development
28
+ bun run dev
29
+
30
+ # Build for production
31
+ bun run build
32
+ bun run start
7
33
  ```
8
34
 
9
- To run:
35
+ ## Tech Stack
10
36
 
11
- ```bash
12
- bun run index.ts
37
+ | Layer | Technology |
38
+ |-------|------------|
39
+ | Runtime | [Bun](https://bun.sh) |
40
+ | HTTP Server | [ElysiaJS](https://elysiajs.com) |
41
+ | UI | [Svelte 5](https://svelte.dev) (Runes) |
42
+ | CSS | [Tailwind CSS v4](https://tailwindcss.com) |
43
+ | Bundler | Bun.build |
44
+
45
+ ## Routing Conventions
46
+
47
+ Files in `src/routes/` map to URLs automatically.
48
+
49
+ | File | Purpose |
50
+ |------|---------|
51
+ | `+page.svelte` | Page component |
52
+ | `+layout.svelte` | Layout that wraps child pages |
53
+ | `+page.server.ts` | Server loader for a page |
54
+ | `+layout.server.ts` | Server loader for a layout |
55
+ | `+server.ts` | API endpoint (export HTTP verbs) |
56
+
57
+ ### Dynamic Routes
58
+
59
+ | Pattern | Matches |
60
+ |---------|---------|
61
+ | `[param]` | `/blog/hello` → `params.param = "hello"` |
62
+ | `[...rest]` | `/a/b/c` → `params.rest = "a/b/c"` |
63
+
64
+ ### Route Groups
65
+
66
+ Wrap a directory in parentheses to share a layout without affecting the URL:
67
+
68
+ ```
69
+ src/routes/
70
+ └── (marketing)/
71
+ ├── +layout.svelte # shared layout
72
+ ├── +page.svelte # /
73
+ └── about/
74
+ └── +page.svelte # /about
13
75
  ```
14
76
 
15
- This project was created using `bun init` in bun v1.3.0. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
77
+ ## Server Loaders
78
+
79
+ ```typescript
80
+ // src/routes/blog/[slug]/+page.server.ts
81
+ import type { LoadEvent } from "bosia";
82
+
83
+ export async function load({ params, url, locals, fetch, parent }: LoadEvent) {
84
+ const parentData = await parent(); // data from layout loaders above
85
+ return {
86
+ post: await getPost(params.slug),
87
+ };
88
+ }
89
+ ```
90
+
91
+ Data returned is passed as the `data` prop to `+page.svelte`:
92
+
93
+ ```svelte
94
+ <script lang="ts">
95
+ let { data } = $props();
96
+ // data.post, data.params ...
97
+ </script>
98
+ ```
99
+
100
+ ## API Routes
101
+
102
+ Export named HTTP verb functions from `+server.ts`:
103
+
104
+ ```typescript
105
+ // src/routes/api/items/+server.ts
106
+ import type { RequestEvent } from "bosia";
107
+
108
+ export function GET({ params, url, locals }: RequestEvent) {
109
+ return Response.json({ items: [] });
110
+ }
111
+
112
+ export async function POST({ request }: RequestEvent) {
113
+ const body = await request.json();
114
+ return Response.json({ created: body }, { status: 201 });
115
+ }
116
+ ```
117
+
118
+ ## Middleware Hooks
119
+
120
+ Create `src/hooks.server.ts` to intercept every request:
121
+
122
+ ```typescript
123
+ import { sequence } from "bosia";
124
+ import type { Handle } from "bosia";
125
+
126
+ const authHandle: Handle = async ({ event, resolve }) => {
127
+ event.locals.user = await getUser(event.request);
128
+ return resolve(event);
129
+ };
130
+
131
+ const loggingHandle: Handle = async ({ event, resolve }) => {
132
+ const res = await resolve(event);
133
+ console.log(`${event.request.method} ${event.url.pathname} ${res.status}`);
134
+ return res;
135
+ };
136
+
137
+ export const handle = sequence(authHandle, loggingHandle);
138
+ ```
139
+
140
+ `locals` set here are available in every loader and API handler.
141
+
142
+ ## Public API
143
+
144
+ ```typescript
145
+ import { cn, sequence } from "bosia";
146
+ import type { RequestEvent, LoadEvent, Handle } from "bosia";
147
+ ```
148
+
149
+ | Export | Description |
150
+ |--------|-------------|
151
+ | `cn(...classes)` | Tailwind class merge utility (clsx + tailwind-merge) |
152
+ | `sequence(...handlers)` | Compose multiple `Handle` middleware functions |
153
+ | `RequestEvent` | Type for API route and hook handlers |
154
+ | `LoadEvent` | Type for `load()` in `+page.server.ts` / `+layout.server.ts` |
155
+ | `Handle` | Type for a middleware function in `hooks.server.ts` |
156
+
157
+ ## Path Alias
158
+
159
+ `$lib` maps to `src/lib/` out of the box:
160
+
161
+ ```typescript
162
+ import { myUtil } from "$lib/utils";
163
+ ```
164
+
165
+ ## Project Structure
166
+
167
+ ```
168
+ my-app/
169
+ ├── src/
170
+ │ ├── app.css # Global styles + Tailwind config
171
+ │ ├── hooks.server.ts # Optional request middleware
172
+ │ ├── lib/ # Shared utilities ($lib alias)
173
+ │ └── routes/ # File-based routes
174
+ ├── public/ # Static assets (served as-is)
175
+ ├── dist/ # Build output (git-ignored)
176
+ └── package.json
177
+ ```
178
+
179
+ ## License
180
+
181
+ MIT
package/package.json CHANGED
@@ -1,12 +1,56 @@
1
1
  {
2
2
  "name": "bosia",
3
- "version": "0.0.0",
4
- "module": "index.ts",
3
+ "version": "0.1.1",
5
4
  "type": "module",
6
- "devDependencies": {
7
- "@types/bun": "latest"
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
+ "keywords": [
7
+ "bun",
8
+ "svelte",
9
+ "ssr",
10
+ "elysia",
11
+ "fullstack",
12
+ "framework"
13
+ ],
14
+ "license": "MIT",
15
+ "author": {
16
+ "name": "Jekibus",
17
+ "url": "https://github.com/jekibus"
18
+ },
19
+ "homepage": "https://github.com/bosapi/bosia#readme",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/bosapi/bosia.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/bosapi/bosia/issues"
26
+ },
27
+ "files": [
28
+ "src",
29
+ "templates",
30
+ "README.md",
31
+ "package.json"
32
+ ],
33
+ "exports": {
34
+ ".": "./src/lib/index.ts"
8
35
  },
9
- "peerDependencies": {
36
+ "bin": {
37
+ "bosia": "src/cli/index.ts"
38
+ },
39
+ "scripts": {
40
+ "check": "tsc --noEmit"
41
+ },
42
+ "devDependencies": {
43
+ "@types/bun": "latest",
10
44
  "typescript": "^5"
45
+ },
46
+ "dependencies": {
47
+ "@clack/prompts": "^1.1.0",
48
+ "@tailwindcss/cli": "^4.2.1",
49
+ "bun-plugin-svelte": "^0.0.6",
50
+ "clsx": "^2.1.1",
51
+ "elysia": "^1.4.26",
52
+ "svelte": "^5.53.6",
53
+ "tailwind-merge": "^3.5.0",
54
+ "tailwindcss": "^4.2.1"
11
55
  }
12
56
  }
package/src/cli/add.ts ADDED
@@ -0,0 +1,151 @@
1
+ import { join, dirname } from "path";
2
+ import { mkdirSync, writeFileSync, readFileSync, existsSync } from "fs";
3
+ import { spawn } from "bun";
4
+
5
+ // ─── bosia add <component> ────────────────────────────────
6
+ // Fetches a component from the GitHub registry (or local registry
7
+ // with --local) and copies it into src/lib/components/ui/<name>/.
8
+
9
+ const REMOTE_BASE = "https://raw.githubusercontent.com/bosapi/bosia/main/registry";
10
+
11
+ interface ComponentMeta {
12
+ name: string;
13
+ description: string;
14
+ dependencies: string[]; // other bosia components required
15
+ files: string[];
16
+ npmDeps: Record<string, string>;
17
+ }
18
+
19
+ // Track already-installed components within a session to avoid re-running deps
20
+ const installed = new Set<string>();
21
+
22
+ // Resolved once in runAdd, used by addComponent
23
+ let registryRoot: string | null = null;
24
+
25
+ export async function runAdd(name: string | undefined, flags: string[] = []) {
26
+ if (!name) {
27
+ console.error("❌ Please provide a component name.\n Usage: bosia add <component> [--local]");
28
+ process.exit(1);
29
+ }
30
+
31
+ if (flags.includes("--local")) {
32
+ // Walk up from this file to find the repo's registry/ directory
33
+ registryRoot = resolveLocalRegistry();
34
+ console.log(`⬡ Using local registry: ${registryRoot}\n`);
35
+ }
36
+
37
+ ensureUtils();
38
+ await addComponent(name, true);
39
+ }
40
+
41
+ export async function addComponent(name: string, root = false) {
42
+ if (installed.has(name)) return;
43
+ installed.add(name);
44
+
45
+ console.log(root ? `⬡ Installing component: ${name}\n` : ` 📦 Dependency: ${name}`);
46
+
47
+ const meta = await readMeta(name);
48
+
49
+ // Install component dependencies first (recursive)
50
+ for (const dep of meta.dependencies) {
51
+ await addComponent(dep, false);
52
+ }
53
+
54
+ // Download/copy component files into src/lib/components/ui/<name>/
55
+ const destDir = join(process.cwd(), "src", "lib", "components", "ui", name);
56
+ mkdirSync(destDir, { recursive: true });
57
+
58
+ for (const file of meta.files) {
59
+ const content = await readFile(name, file);
60
+ const dest = join(destDir, file);
61
+ mkdirSync(dirname(dest), { recursive: true });
62
+ writeFileSync(dest, content, "utf-8");
63
+ console.log(` ✍️ src/lib/components/ui/${name}/${file}`);
64
+ }
65
+
66
+ // Install npm dependencies
67
+ const npmEntries = Object.entries(meta.npmDeps);
68
+ if (npmEntries.length > 0) {
69
+ const packages = npmEntries.map(([pkg, ver]) => (ver ? `${pkg}@${ver}` : pkg));
70
+ console.log(` 📥 npm: ${packages.join(", ")}`);
71
+ const proc = spawn(["bun", "add", ...packages], {
72
+ stdout: "inherit",
73
+ stderr: "inherit",
74
+ cwd: process.cwd(),
75
+ });
76
+ if ((await proc.exited) !== 0) {
77
+ console.warn(` ⚠️ bun add failed for: ${packages.join(", ")}`);
78
+ }
79
+ }
80
+
81
+ if (root) console.log(`\n✅ ${name} installed at src/lib/components/ui/${name}/`);
82
+ }
83
+
84
+ // ─── Ensure $lib/utils.ts exists ─────────────────────────────
85
+
86
+ const UTILS_CONTENT = `import { clsx, type ClassValue } from "clsx";
87
+ import { twMerge } from "tailwind-merge";
88
+
89
+ export function cn(...inputs: ClassValue[]) {
90
+ return twMerge(clsx(inputs));
91
+ }
92
+ `;
93
+
94
+ function ensureUtils() {
95
+ const utilsPath = join(process.cwd(), "src", "lib", "utils.ts");
96
+ if (!existsSync(utilsPath)) {
97
+ mkdirSync(dirname(utilsPath), { recursive: true });
98
+ writeFileSync(utilsPath, UTILS_CONTENT, "utf-8");
99
+ console.log(" ✍️ src/lib/utils.ts (cn utility)\n");
100
+ }
101
+ }
102
+
103
+ // ─── Registry resolvers ──────────────────────────────────────
104
+
105
+ function resolveLocalRegistry(): string {
106
+ // Walk up from this file's directory to find registry/
107
+ let dir = dirname(new URL(import.meta.url).pathname);
108
+ for (let i = 0; i < 10; i++) {
109
+ const candidate = join(dir, "registry");
110
+ if (existsSync(join(candidate, "index.json"))) return candidate;
111
+ const parent = dirname(dir);
112
+ if (parent === dir) break;
113
+ dir = parent;
114
+ }
115
+ console.error("❌ Could not find local registry/ directory.");
116
+ process.exit(1);
117
+ }
118
+
119
+ async function readMeta(name: string): Promise<ComponentMeta> {
120
+ if (registryRoot) {
121
+ const path = join(registryRoot, "components", name, "meta.json");
122
+ if (!existsSync(path)) {
123
+ throw new Error(`Component "${name}" not found in local registry`);
124
+ }
125
+ return JSON.parse(readFileSync(path, "utf-8"));
126
+ }
127
+ return fetchJSON<ComponentMeta>(`${REMOTE_BASE}/components/${name}/meta.json`);
128
+ }
129
+
130
+ async function readFile(name: string, file: string): Promise<string> {
131
+ if (registryRoot) {
132
+ const path = join(registryRoot, "components", name, file);
133
+ if (!existsSync(path)) {
134
+ throw new Error(`File "${file}" not found for component "${name}" in local registry`);
135
+ }
136
+ return readFileSync(path, "utf-8");
137
+ }
138
+ return fetchText(`${REMOTE_BASE}/components/${name}/${file}`);
139
+ }
140
+
141
+ async function fetchJSON<T>(url: string): Promise<T> {
142
+ const res = await fetch(url);
143
+ if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
144
+ return res.json() as Promise<T>;
145
+ }
146
+
147
+ async function fetchText(url: string): Promise<string> {
148
+ const res = await fetch(url);
149
+ if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
150
+ return res.text();
151
+ }
@@ -0,0 +1,16 @@
1
+ import { spawn } from "bun";
2
+ import { resolve } from "path";
3
+ import { loadEnv } from "../core/env.ts";
4
+
5
+ export async function runBuild() {
6
+ loadEnv("production");
7
+ const buildScript = resolve(import.meta.dir, "../core/build.ts");
8
+ const proc = spawn(["bun", "run", buildScript], {
9
+ stdout: "inherit",
10
+ stderr: "inherit",
11
+ cwd: process.cwd(),
12
+ env: { ...process.env, NODE_ENV: process.env.NODE_ENV ?? "production" },
13
+ });
14
+ const exitCode = await proc.exited;
15
+ if (exitCode !== 0) process.exit(exitCode ?? 1);
16
+ }
@@ -0,0 +1,113 @@
1
+ import { resolve, join, basename } from "path";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { spawn } from "bun";
4
+ import * as p from "@clack/prompts";
5
+
6
+ // ─── bosia create <name> [--template <name>] ──────────────
7
+
8
+ const TEMPLATES_DIR = resolve(import.meta.dir, "../../templates");
9
+ const BOSIA_PKG = JSON.parse(readFileSync(resolve(import.meta.dir, "../../package.json"), "utf-8"));
10
+ const BOSIA_VERSION: string = BOSIA_PKG.version;
11
+
12
+ const TEMPLATE_DESCRIPTIONS: Record<string, string> = {
13
+ default: "Minimal starter with routing and Tailwind",
14
+ demo: "Full-featured demo with hooks, API routes, form actions, and more",
15
+ };
16
+
17
+ export async function runCreate(name: string | undefined, args: string[] = []) {
18
+ if (!name) {
19
+ console.error("❌ Please provide a project name.\n Usage: bosia create my-app");
20
+ process.exit(1);
21
+ }
22
+
23
+ const targetDir = resolve(process.cwd(), name);
24
+
25
+ if (existsSync(targetDir)) {
26
+ console.error(`❌ Directory already exists: ${targetDir}`);
27
+ process.exit(1);
28
+ }
29
+
30
+ // Parse --template flag
31
+ let template: string | undefined;
32
+ const templateIdx = args.indexOf("--template");
33
+ if (templateIdx !== -1 && args[templateIdx + 1]) {
34
+ template = args[templateIdx + 1];
35
+ }
36
+
37
+ // If no --template flag, prompt interactively
38
+ if (!template) {
39
+ template = await promptTemplate();
40
+ }
41
+
42
+ // Validate template exists
43
+ const templateDir = resolve(TEMPLATES_DIR, template);
44
+ if (!existsSync(templateDir)) {
45
+ const available = getAvailableTemplates().join(", ");
46
+ console.error(`❌ Unknown template: "${template}"\n Available: ${available}`);
47
+ process.exit(1);
48
+ }
49
+
50
+ console.log(`\n⬡ Creating Bosia project: ${basename(targetDir)} (template: ${template})\n`);
51
+
52
+ copyDir(templateDir, targetDir, name);
53
+
54
+ console.log(`✅ Project created at ${targetDir}\n`);
55
+
56
+ console.log("Installing dependencies...");
57
+ const proc = spawn(["bun", "install"], {
58
+ stdout: "inherit",
59
+ stderr: "inherit",
60
+ cwd: targetDir,
61
+ });
62
+ const exitCode = await proc.exited;
63
+ if (exitCode !== 0) {
64
+ console.warn("⚠️ bun install failed — run it manually.");
65
+ } else {
66
+ console.log(`\n🎉 Ready!\n\n cd ${name}\n bun x bosia dev\n`);
67
+ }
68
+ }
69
+
70
+ function getAvailableTemplates(): string[] {
71
+ return readdirSync(TEMPLATES_DIR, { withFileTypes: true })
72
+ .filter((d) => d.isDirectory())
73
+ .map((d) => d.name)
74
+ .sort((a, b) => (a === "default" ? -1 : b === "default" ? 1 : a.localeCompare(b)));
75
+ }
76
+
77
+ async function promptTemplate(): Promise<string> {
78
+ const templates = getAvailableTemplates();
79
+
80
+ if (templates.length === 1) return templates[0];
81
+
82
+ const selected = await p.select({
83
+ message: "Which template?",
84
+ options: templates.map((t) => ({
85
+ value: t,
86
+ label: t,
87
+ hint: TEMPLATE_DESCRIPTIONS[t],
88
+ })),
89
+ });
90
+
91
+ if (p.isCancel(selected)) {
92
+ p.cancel("Operation cancelled.");
93
+ process.exit(0);
94
+ }
95
+
96
+ return selected as string;
97
+ }
98
+
99
+ function copyDir(src: string, dest: string, projectName: string) {
100
+ mkdirSync(dest, { recursive: true });
101
+ for (const entry of readdirSync(src, { withFileTypes: true })) {
102
+ const srcPath = join(src, entry.name);
103
+ const destPath = join(dest, entry.name);
104
+ if (entry.isDirectory()) {
105
+ copyDir(srcPath, destPath, projectName);
106
+ } else {
107
+ const content = readFileSync(srcPath, "utf-8")
108
+ .replaceAll("{{PROJECT_NAME}}", projectName)
109
+ .replaceAll("{{BOSIA_VERSION}}", BOSIA_VERSION);
110
+ writeFileSync(destPath, content, "utf-8");
111
+ }
112
+ }
113
+ }
package/src/cli/dev.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { spawn } from "bun";
2
+ import { resolve } from "path";
3
+ import { loadEnv } from "../core/env.ts";
4
+
5
+ export async function runDev() {
6
+ loadEnv("development");
7
+ const devScript = resolve(import.meta.dir, "../core/dev.ts");
8
+ const proc = spawn(["bun", "run", devScript], {
9
+ stdout: "inherit",
10
+ stderr: "inherit",
11
+ cwd: process.cwd(),
12
+ });
13
+ await proc.exited;
14
+ }
@@ -0,0 +1,80 @@
1
+ import { join, dirname } from "path";
2
+ import { mkdirSync, writeFileSync } from "fs";
3
+ import { spawn } from "bun";
4
+ import { addComponent } from "./add.ts";
5
+
6
+ // ─── bosia feat <feature> ─────────────────────────────────
7
+ // Fetches a feature scaffold from the GitHub registry.
8
+ // Installs required components, copies route/lib files, installs npm deps.
9
+
10
+ const REGISTRY_BASE = "https://raw.githubusercontent.com/bosapi/bosia/main/registry";
11
+
12
+ interface FeatureMeta {
13
+ name: string;
14
+ description: string;
15
+ components: string[]; // bosia components to install via `bosia add`
16
+ files: string[]; // source filenames in the registry feature dir
17
+ targets: string[]; // destination paths relative to project root
18
+ npmDeps: Record<string, string>;
19
+ }
20
+
21
+ export async function runFeat(name: string | undefined) {
22
+ if (!name) {
23
+ console.error("❌ Please provide a feature name.\n Usage: bosia feat <feature>");
24
+ process.exit(1);
25
+ }
26
+
27
+ console.log(`⬡ Installing feature: ${name}\n`);
28
+
29
+ const meta = await fetchJSON<FeatureMeta>(`${REGISTRY_BASE}/features/${name}/meta.json`);
30
+
31
+ // Install required UI components
32
+ if (meta.components.length > 0) {
33
+ console.log("📦 Installing required components...");
34
+ for (const comp of meta.components) {
35
+ await addComponent(comp, false);
36
+ }
37
+ console.log("");
38
+ }
39
+
40
+ // Copy feature files to their target paths
41
+ for (let i = 0; i < meta.files.length; i++) {
42
+ const file = meta.files[i]!;
43
+ const target = meta.targets[i] ?? file;
44
+ const content = await fetchText(`${REGISTRY_BASE}/features/${name}/${file}`);
45
+ const dest = join(process.cwd(), target);
46
+ mkdirSync(dirname(dest), { recursive: true });
47
+ writeFileSync(dest, content, "utf-8");
48
+ console.log(` ✍️ ${target}`);
49
+ }
50
+
51
+ // Install npm dependencies
52
+ const npmEntries = Object.entries(meta.npmDeps);
53
+ if (npmEntries.length > 0) {
54
+ const packages = npmEntries.map(([pkg, ver]) => (ver ? `${pkg}@${ver}` : pkg));
55
+ console.log(`\n📥 npm: ${packages.join(", ")}`);
56
+ const proc = spawn(["bun", "add", ...packages], {
57
+ stdout: "inherit",
58
+ stderr: "inherit",
59
+ cwd: process.cwd(),
60
+ });
61
+ if ((await proc.exited) !== 0) {
62
+ console.warn(`⚠️ bun add failed for: ${packages.join(", ")}`);
63
+ }
64
+ }
65
+
66
+ console.log(`\n✅ Feature "${name}" scaffolded!`);
67
+ if (meta.description) console.log(` ${meta.description}`);
68
+ }
69
+
70
+ async function fetchJSON<T>(url: string): Promise<T> {
71
+ const res = await fetch(url);
72
+ if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
73
+ return res.json() as Promise<T>;
74
+ }
75
+
76
+ async function fetchText(url: string): Promise<string> {
77
+ const res = await fetch(url);
78
+ if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
79
+ return res.text();
80
+ }