vibeadmin 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VibeAdmin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,10 @@
1
- # Temporary Holding Version
1
+ # vibeadmin
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Set up [VibeAdmin](https://www.easy-admin.solutions) from your terminal.
4
+
5
+ ```bash
6
+ npx vibeadmin init # detect your stack, create a project and a development API key
7
+ npx vibeadmin dev # verify the integration with a test user and event
8
+ ```
9
+
10
+ Run `npx vibeadmin --help` for options. License: MIT
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import("../dist/index.js")
3
+ .then((m) => m.main(process.argv.slice(2)))
4
+ .catch((e) => {
5
+ console.error(e instanceof Error ? e.message : e);
6
+ process.exit(1);
7
+ });
package/dist/api.js ADDED
@@ -0,0 +1,47 @@
1
+ export class ApiError extends Error {
2
+ status;
3
+ constructor(message, status) {
4
+ super(message);
5
+ this.status = status;
6
+ this.name = "ApiError";
7
+ }
8
+ }
9
+ /** Minimal JSON client. `token` is a session bearer token (dashboard API) or an API key (/v1). */
10
+ export function createClient(origin, token, fetchImpl = fetch) {
11
+ const base = origin.replace(/\/+$/, "");
12
+ async function call(method, path, body) {
13
+ const res = await fetchImpl(`${base}${path}`, {
14
+ method,
15
+ headers: {
16
+ accept: "application/json",
17
+ "user-agent": "vibeadmin-cli",
18
+ ...(body !== undefined ? { "content-type": "application/json" } : {}),
19
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
20
+ },
21
+ body: body === undefined ? undefined : JSON.stringify(body),
22
+ signal: AbortSignal.timeout(20_000),
23
+ });
24
+ const text = await res.text();
25
+ let json;
26
+ try {
27
+ json = text ? JSON.parse(text) : undefined;
28
+ }
29
+ catch {
30
+ json = undefined;
31
+ }
32
+ if (!res.ok) {
33
+ const j = json;
34
+ const msg = typeof j?.error === "string"
35
+ ? j.error
36
+ : (j?.error?.message ?? j?.error_description ?? `HTTP ${res.status}`);
37
+ throw new ApiError(msg, res.status);
38
+ }
39
+ return json;
40
+ }
41
+ return {
42
+ origin: base,
43
+ get: (p) => call("GET", p),
44
+ post: (p, b) => call("POST", p, b ?? {}),
45
+ del: (p) => call("DELETE", p),
46
+ };
47
+ }
package/dist/detect.js ADDED
@@ -0,0 +1,106 @@
1
+ const LABEL = {
2
+ nextjs: "Next.js",
3
+ react: "React",
4
+ node: "Node.js",
5
+ python: "Python",
6
+ other: "your app",
7
+ };
8
+ export function detectFromFiles(files) {
9
+ let pkg = {};
10
+ try {
11
+ if (files.packageJson)
12
+ pkg = JSON.parse(files.packageJson);
13
+ }
14
+ catch {
15
+ // an unreadable package.json is treated as absent
16
+ }
17
+ const deps = { ...pkg.devDependencies, ...pkg.dependencies };
18
+ const has = (...names) => names.some((n) => n in deps);
19
+ const py = `${files.requirements ?? ""}\n${files.pyproject ?? ""}`.toLowerCase();
20
+ let framework = "other";
21
+ if (has("next"))
22
+ framework = "nextjs";
23
+ else if (has("react", "vite", "react-dom"))
24
+ framework = "react";
25
+ else if (has("express", "fastify", "hono", "koa", "@nestjs/core", "@hapi/hapi"))
26
+ framework = "node";
27
+ else if (files.packageJson)
28
+ framework = "node";
29
+ else if (files.requirements || files.pyproject)
30
+ framework = "python";
31
+ return {
32
+ framework,
33
+ label: LABEL[framework],
34
+ supabase: has("@supabase/supabase-js", "@supabase/ssr") || /\bsupabase\b/.test(py),
35
+ firebase: has("firebase", "firebase-admin", "firebase-functions") || /firebase[-_]admin/.test(py),
36
+ clerk: Object.keys(deps).some((d) => d.startsWith("@clerk/")) || /\bclerk\b/.test(py),
37
+ name: pkg.name,
38
+ };
39
+ }
40
+ export function parseEnv(text) {
41
+ const out = {};
42
+ for (const raw of text.split(/\r?\n/)) {
43
+ const line = raw.trim();
44
+ if (!line || line.startsWith("#"))
45
+ continue;
46
+ const m = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
47
+ if (!m)
48
+ continue;
49
+ let v = m[2].trim();
50
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")))
51
+ v = v.slice(1, -1);
52
+ else
53
+ v = v.replace(/\s+#.*$/, "");
54
+ out[m[1]] = v;
55
+ }
56
+ return out;
57
+ }
58
+ const SUPABASE_URL_KEYS = [
59
+ "SUPABASE_URL",
60
+ "NEXT_PUBLIC_SUPABASE_URL",
61
+ "VITE_SUPABASE_URL",
62
+ "EXPO_PUBLIC_SUPABASE_URL",
63
+ ];
64
+ const SUPABASE_SERVICE_KEYS = ["SUPABASE_SERVICE_ROLE_KEY", "SUPABASE_SERVICE_KEY"];
65
+ export function findSupabase(env) {
66
+ const url = SUPABASE_URL_KEYS.map((k) => env[k]).find((v) => v && /^https:\/\//.test(v));
67
+ if (!url)
68
+ return null;
69
+ const serviceRoleKey = SUPABASE_SERVICE_KEYS.map((k) => env[k]).find(Boolean);
70
+ return { url: url.replace(/\/+$/, ""), serviceRoleKey };
71
+ }
72
+ /** Sets KEY=value, replacing an existing assignment and keeping everything else untouched. */
73
+ export function upsertEnvVar(text, key, value) {
74
+ const line = `${key}=${value}`;
75
+ const re = new RegExp(`^(?:export\\s+)?${key}\\s*=.*$`, "m");
76
+ if (re.test(text))
77
+ return text.replace(re, line);
78
+ const sep = text && !text.endsWith("\n") ? "\n" : "";
79
+ return `${text}${sep}${line}\n`;
80
+ }
81
+ /** True when .env.local (or a matching pattern) is ignored, so the key we write cannot be committed by accident. */
82
+ export function gitignoreCoversEnv(gitignore) {
83
+ if (gitignore === undefined)
84
+ return false;
85
+ return gitignore
86
+ .split(/\r?\n/)
87
+ .map((l) => l.trim())
88
+ .some((l) => [".env.local", ".env*.local", ".env*", ".env", "*.local", ".env.*"].includes(l));
89
+ }
90
+ export function nextSteps(d, ids, origin) {
91
+ const guide = `${origin}/o/${ids.org}/p/${ids.project}/development/onboarding`;
92
+ const common = [`Open ${guide} to finish setup and get a prompt for your coding agent.`];
93
+ const push = {
94
+ nextjs: "Call POST /v1/users from your signup code and POST /v1/events for key actions (server-side only).",
95
+ react: "Call the API from your backend or a serverless function, never from browser code.",
96
+ node: "Call POST /v1/users when users sign up and POST /v1/events for key actions.",
97
+ python: "Call POST /v1/users when users sign up and POST /v1/events for key actions (requests or httpx).",
98
+ other: "Call POST /v1/users when users sign up and POST /v1/events for key actions.",
99
+ }[d.framework];
100
+ const extra = [];
101
+ if (d.firebase)
102
+ extra.push("Firebase: forward new users from an auth.user().onCreate Cloud Function. See /admin-dashboard-for-firebase.");
103
+ if (d.clerk)
104
+ extra.push("Clerk: forward user.created webhooks to the API. See /admin-dashboard-for-clerk.");
105
+ return [push, ...extra, ...common, "Run `vibeadmin dev` to verify the connection."];
106
+ }
package/dist/dev.js ADDED
@@ -0,0 +1,63 @@
1
+ import { ApiError, createClient } from "./api.js";
2
+ import { parseEnv } from "./detect.js";
3
+ import { ENV_FILES } from "./fs.js";
4
+ export const PROBE_ID = "vibeadmin-dev-probe";
5
+ export function loadKey(files, env) {
6
+ const fromFiles = Object.assign({}, ...ENV_FILES.slice()
7
+ .reverse()
8
+ .map((f) => parseEnv(files.read(f) ?? "")));
9
+ return {
10
+ key: env.VIBEADMIN_API_KEY || fromFiles.VIBEADMIN_API_KEY,
11
+ origin: env.VIBEADMIN_URL || fromFiles.VIBEADMIN_URL,
12
+ };
13
+ }
14
+ /** Verifies the integration end to end with a clearly named probe user and event. Never touches real users. */
15
+ export async function runDev(opts, io) {
16
+ if (!opts.key) {
17
+ io.warn("No VIBEADMIN_API_KEY found in the environment or .env files. Run `vibeadmin init` first.");
18
+ return false;
19
+ }
20
+ if (opts.key.startsWith("adm_live_")) {
21
+ io.warn("This is a production (adm_live_) key. `vibeadmin dev` only runs against development keys.");
22
+ return false;
23
+ }
24
+ if (!opts.key.startsWith("adm_test_")) {
25
+ io.warn("That does not look like a VibeAdmin API key (expected adm_test_…).");
26
+ return false;
27
+ }
28
+ const api = createClient(opts.origin, opts.key, opts.fetch);
29
+ try {
30
+ await api.get("/v1/stats");
31
+ io.ok("API key accepted (development)");
32
+ await api.post("/v1/users", {
33
+ id: PROBE_ID,
34
+ email: "vibeadmin-dev-probe@example.invalid",
35
+ name: "VibeAdmin CLI probe",
36
+ });
37
+ io.ok("Test user created");
38
+ await api.post("/v1/events", { name: "vibeadmin.dev.check", userId: PROBE_ID });
39
+ const events = await api.get(`/v1/events?name=vibeadmin.dev.check&userId=${PROBE_ID}&limit=1`);
40
+ if (!events.data.length)
41
+ throw new Error("The test event was accepted but could not be read back.");
42
+ io.ok("Test event recorded");
43
+ try {
44
+ await api.del(`/v1/users/${PROBE_ID}`);
45
+ io.ok("Test data removed");
46
+ }
47
+ catch (e) {
48
+ if (e instanceof ApiError && e.status === 403)
49
+ io.out(` The key cannot delete users, so the user "${PROBE_ID}" was left in place. Delete it from the dashboard if you like.`);
50
+ else
51
+ throw e;
52
+ }
53
+ io.out("Your integration works.");
54
+ return true;
55
+ }
56
+ catch (e) {
57
+ const msg = e instanceof ApiError && e.status === 401
58
+ ? "The API key was rejected (revoked, or for another environment)."
59
+ : e.message;
60
+ io.warn(`Check failed: ${msg}`);
61
+ return false;
62
+ }
63
+ }
package/dist/device.js ADDED
@@ -0,0 +1,37 @@
1
+ import { ApiError } from "./api.js";
2
+ export const CLIENT_ID = "vibeadmin-cli";
3
+ const GRANT = "urn:ietf:params:oauth:grant-type:device_code";
4
+ /** OAuth device flow (RFC 8628) against Better Auth. Returns a session token usable as a bearer token. */
5
+ export async function deviceLogin(anon, show, sleep = (ms) => new Promise((r) => setTimeout(r, ms))) {
6
+ const c = await anon.post("/api/auth/device/code", { client_id: CLIENT_ID });
7
+ show({ url: c.verification_uri_complete ?? c.verification_uri, code: c.user_code });
8
+ let interval = Math.max(1, c.interval) * 1000;
9
+ const deadline = Date.now() + c.expires_in * 1000;
10
+ while (Date.now() < deadline) {
11
+ await sleep(interval);
12
+ try {
13
+ const t = await anon.post("/api/auth/device/token", {
14
+ grant_type: GRANT,
15
+ device_code: c.device_code,
16
+ client_id: CLIENT_ID,
17
+ });
18
+ if (t.access_token)
19
+ return t.access_token;
20
+ }
21
+ catch (e) {
22
+ const msg = e instanceof ApiError ? e.message : "";
23
+ if (/authorization_pending|pending/i.test(msg))
24
+ continue;
25
+ if (/slow_down/i.test(msg)) {
26
+ interval += 5000;
27
+ continue;
28
+ }
29
+ if (/access_denied|denied/i.test(msg))
30
+ throw new Error("The sign-in was denied in the browser.");
31
+ if (/expired/i.test(msg))
32
+ throw new Error("The code expired. Run the command again.");
33
+ throw e;
34
+ }
35
+ }
36
+ throw new Error("Timed out waiting for browser approval. Run the command again.");
37
+ }
package/dist/fs.js ADDED
@@ -0,0 +1,23 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ export function diskFiles(cwd) {
4
+ return {
5
+ read: (name) => {
6
+ const p = join(cwd, name);
7
+ return existsSync(p) ? readFileSync(p, "utf8") : undefined;
8
+ },
9
+ // Mode 0600: the file holds an API key.
10
+ write: (name, content) => writeFileSync(join(cwd, name), content, { mode: 0o600 }),
11
+ };
12
+ }
13
+ export function memoryFiles(initial = {}) {
14
+ const files = { ...initial };
15
+ return {
16
+ files,
17
+ read: (n) => files[n],
18
+ write: (n, c) => {
19
+ files[n] = c;
20
+ },
21
+ };
22
+ }
23
+ export const ENV_FILES = [".env.local", ".env", ".env.development.local", ".env.development"];
package/dist/index.js ADDED
@@ -0,0 +1,63 @@
1
+ import { loadKey, runDev } from "./dev.js";
2
+ import { diskFiles } from "./fs.js";
3
+ import { DEFAULT_ORIGIN, runInit } from "./init.js";
4
+ import { consoleIo } from "./io.js";
5
+ const HELP = `vibeadmin: set up the VibeAdmin admin dashboard for your app
6
+
7
+ Usage
8
+ vibeadmin init Detect your stack, create a project and a development API key
9
+ vibeadmin dev Verify the integration with a test user and event
10
+
11
+ Options
12
+ --url <origin> VibeAdmin server (default ${DEFAULT_ORIGIN}, or VIBEADMIN_URL)
13
+ --yes Accept defaults without prompting (init)
14
+ --no-env Print the key instead of writing .env.local (init)
15
+ --help Show this help
16
+ `;
17
+ export function parseArgs(argv) {
18
+ const p = { yes: false, noEnv: false, help: false };
19
+ for (let i = 0; i < argv.length; i++) {
20
+ const a = argv[i];
21
+ if (a === "--yes" || a === "-y")
22
+ p.yes = true;
23
+ else if (a === "--no-env")
24
+ p.noEnv = true;
25
+ else if (a === "--help" || a === "-h")
26
+ p.help = true;
27
+ else if (a === "--url")
28
+ p.url = argv[++i];
29
+ else if (a.startsWith("--url="))
30
+ p.url = a.slice(6);
31
+ else if (!a.startsWith("-") && !p.command)
32
+ p.command = a;
33
+ else
34
+ throw new Error(`Unknown option: ${a}\n\n${HELP}`);
35
+ }
36
+ return p;
37
+ }
38
+ export async function main(argv) {
39
+ const a = parseArgs(argv);
40
+ if (a.help || !a.command) {
41
+ console.log(HELP);
42
+ return;
43
+ }
44
+ const cwd = process.cwd();
45
+ const files = diskFiles(cwd);
46
+ const io = consoleIo({ yes: a.yes });
47
+ const saved = loadKey(files, process.env);
48
+ const origin = (a.url ?? saved.origin ?? DEFAULT_ORIGIN).replace(/\/+$/, "");
49
+ if (!/^https:\/\//.test(origin) &&
50
+ !/^http:\/\/(localhost|127\.0\.0\.1|\[::1\])(:|\/|$)/.test(origin))
51
+ throw new Error("--url must be https (http is allowed only for localhost).");
52
+ if (a.command === "init") {
53
+ await runInit({ origin, yes: a.yes, writeEnv: !a.noEnv, cwd }, files, io);
54
+ }
55
+ else if (a.command === "dev") {
56
+ const ok = await runDev({ origin, key: saved.key }, io);
57
+ if (!ok)
58
+ process.exitCode = 1;
59
+ }
60
+ else {
61
+ throw new Error(`Unknown command: ${a.command}\n\n${HELP}`);
62
+ }
63
+ }
package/dist/init.js ADDED
@@ -0,0 +1,109 @@
1
+ import { basename } from "node:path";
2
+ import { createClient } from "./api.js";
3
+ import { detectFromFiles, findSupabase, gitignoreCoversEnv, nextSteps, parseEnv, upsertEnvVar, } from "./detect.js";
4
+ import { deviceLogin } from "./device.js";
5
+ import { ENV_FILES } from "./fs.js";
6
+ // Scopes for the key init creates: enough to sync users and send events, nothing destructive.
7
+ export const INIT_SCOPES = ["users.read", "users.write", "events.write", "analytics.read"];
8
+ export const DEFAULT_ORIGIN = "https://www.easy-admin.solutions";
9
+ export async function runInit(opts, files, io) {
10
+ const d = detectFromFiles({
11
+ packageJson: files.read("package.json"),
12
+ requirements: files.read("requirements.txt"),
13
+ pyproject: files.read("pyproject.toml"),
14
+ });
15
+ io.ok(`Detected ${d.label}`);
16
+ if (d.supabase)
17
+ io.ok("Detected Supabase");
18
+ if (d.firebase)
19
+ io.ok("Detected Firebase");
20
+ if (d.clerk)
21
+ io.ok("Detected Clerk");
22
+ const anon = createClient(opts.origin, undefined, opts.fetch);
23
+ const token = await deviceLogin(anon, ({ url, code }) => {
24
+ io.out();
25
+ io.out(`Open ${url}`);
26
+ io.out(`and confirm the code: ${code}`);
27
+ io.out("Waiting for approval…");
28
+ }, opts.sleep);
29
+ const api = createClient(opts.origin, token, opts.fetch);
30
+ io.ok("Signed in");
31
+ let orgs = await api.get("/api/orgs");
32
+ let org;
33
+ if (!orgs.length) {
34
+ const name = await io.ask("Organization name", "My organization");
35
+ org = await api.post("/api/orgs", { name });
36
+ orgs = [org];
37
+ io.ok(`Created organization "${org.name}"`);
38
+ }
39
+ else if (orgs.length === 1) {
40
+ org = orgs[0];
41
+ }
42
+ else {
43
+ for (const [i, o] of orgs.entries())
44
+ io.out(` ${i + 1}. ${o.name}`);
45
+ const n = Number(await io.ask("Which organization? Enter a number", "1"));
46
+ org = orgs[Number.isInteger(n) && n >= 1 && n <= orgs.length ? n - 1 : 0];
47
+ }
48
+ const defaultName = (d.name ?? basename(opts.cwd)).replace(/^@[^/]+\//, "") || "my-app";
49
+ const projectName = await io.ask("Project name", defaultName);
50
+ const created = await api.post(`/api/orgs/${org.id}/projects`, { name: projectName });
51
+ io.ok(`Created project "${created.project.name}"`);
52
+ const dev = created.environments.find((e) => e.kind === "development");
53
+ if (!dev)
54
+ throw new Error("The new project has no development environment.");
55
+ const key = await api.post(`/api/orgs/${org.id}/environments/${dev.id}/api-keys`, {
56
+ name: `cli-${created.project.slug}`.slice(0, 120),
57
+ scopes: INIT_SCOPES,
58
+ });
59
+ io.ok("Created a development API key");
60
+ // Offer the Supabase connector only if the credentials are already on this machine; never ask for them to be typed.
61
+ if (d.supabase) {
62
+ const env = Object.assign({}, ...ENV_FILES.slice()
63
+ .reverse()
64
+ .map((f) => parseEnv(files.read(f) ?? "")));
65
+ const sb = findSupabase(env);
66
+ if (sb?.serviceRoleKey) {
67
+ const yes = await io.confirm(`Connect Supabase (${sb.url})? Your service-role key is sent to VibeAdmin, stored encrypted, and used read-only.`, false);
68
+ if (yes) {
69
+ try {
70
+ await api.post(`/api/orgs/${org.id}/environments/${dev.id}/integrations`, {
71
+ provider: "supabase",
72
+ config: { url: sb.url },
73
+ credentials: { serviceRoleKey: sb.serviceRoleKey },
74
+ grantedPermissions: ["read_users"],
75
+ });
76
+ io.ok("Connected Supabase (read-only). Users will appear after the first sync.");
77
+ }
78
+ catch (e) {
79
+ io.warn(`Could not connect Supabase: ${e.message}. You can do it from the Integrations page.`);
80
+ }
81
+ }
82
+ }
83
+ else if (sb) {
84
+ io.out("Supabase found, but no service-role key in your env files. Add it on the Integrations page to import users.");
85
+ }
86
+ }
87
+ if (opts.writeEnv) {
88
+ let text = files.read(".env.local") ?? "";
89
+ text = upsertEnvVar(text, "VIBEADMIN_API_KEY", key.secret);
90
+ if (opts.origin !== DEFAULT_ORIGIN)
91
+ text = upsertEnvVar(text, "VIBEADMIN_URL", opts.origin);
92
+ files.write(".env.local", text);
93
+ io.out();
94
+ io.ok("Added to .env.local:");
95
+ io.out(`VIBEADMIN_API_KEY=${key.secret.slice(0, 12)}…`);
96
+ if (!gitignoreCoversEnv(files.read(".gitignore")))
97
+ io.warn(".env.local is not in your .gitignore. Add it so the key is never committed.");
98
+ }
99
+ else {
100
+ io.out();
101
+ io.out("Add this to your environment (shown once):");
102
+ io.out(`VIBEADMIN_API_KEY=${key.secret}`);
103
+ }
104
+ io.out();
105
+ io.out("Next:");
106
+ for (const s of nextSteps(d, { org: org.slug, project: created.project.slug }, opts.origin))
107
+ io.out(` - ${s}`);
108
+ return { key: key.secret, slug: org.slug, project: created.project.slug };
109
+ }
package/dist/io.js ADDED
@@ -0,0 +1,48 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ export function consoleIo(opts) {
3
+ const tty = process.stdout.isTTY;
4
+ const c = (code, s) => (tty ? `\x1b[${code}m${s}\x1b[0m` : s);
5
+ let rl;
6
+ const readline = () => (rl ??= createInterface({ input: process.stdin, output: process.stdout }));
7
+ const io = {
8
+ out: (l = "") => console.log(l),
9
+ ok: (l) => console.log(`${c(32, "✓")} ${l}`),
10
+ warn: (l) => console.log(`${c(33, "!")} ${l}`),
11
+ async ask(q, fallback) {
12
+ if (opts.yes || !process.stdin.isTTY)
13
+ return fallback ?? "";
14
+ const a = (await readline().question(`${q}${fallback ? ` (${fallback})` : ""}: `)).trim();
15
+ return a || fallback || "";
16
+ },
17
+ async confirm(q, fallback) {
18
+ if (opts.yes)
19
+ return fallback;
20
+ if (!process.stdin.isTTY)
21
+ return false;
22
+ const a = (await readline().question(`${q} ${fallback ? "[Y/n]" : "[y/N]"} `))
23
+ .trim()
24
+ .toLowerCase();
25
+ return a ? a.startsWith("y") : fallback;
26
+ },
27
+ };
28
+ process.on("exit", () => rl?.close());
29
+ return io;
30
+ }
31
+ /** In-memory Io for tests: records output, answers prompts from a queue. */
32
+ export function testIo(answers = []) {
33
+ const lines = [];
34
+ const io = {
35
+ out: (l = "") => void lines.push(l),
36
+ ok: (l) => void lines.push(`ok ${l}`),
37
+ warn: (l) => void lines.push(`warn ${l}`),
38
+ ask: async (_q, fallback) => {
39
+ const a = answers.shift();
40
+ return typeof a === "string" ? a : (fallback ?? "");
41
+ },
42
+ confirm: async (_q, fallback) => {
43
+ const a = answers.shift();
44
+ return typeof a === "boolean" ? a : fallback;
45
+ },
46
+ };
47
+ return { io, lines };
48
+ }
package/dist/run.js ADDED
@@ -0,0 +1,6 @@
1
+ // Dev entry (pnpm vibeadmin …) so the CLI can run from source without a build.
2
+ import { main } from "./index.js";
3
+ main(process.argv.slice(2)).catch((e) => {
4
+ console.error(e instanceof Error ? e.message : e);
5
+ process.exit(1);
6
+ });
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "vibeadmin",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Set up VibeAdmin from your terminal: npx vibeadmin init",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "vibeadmin": "bin/vibeadmin.js"
9
+ },
10
+ "main": "dist/index.js",
11
+ "files": [
12
+ "bin",
13
+ "dist",
14
+ "README.md"
15
+ ],
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/leovegas/vibeadmin.git",
22
+ "directory": "packages/cli"
23
+ },
24
+ "homepage": "https://www.easy-admin.solutions",
25
+ "bugs": {
26
+ "url": "https://github.com/leovegas/vibeadmin/issues"
27
+ },
28
+ "keywords": [
29
+ "vibeadmin",
30
+ "admin",
31
+ "cli",
32
+ "setup"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "scripts": {
38
+ "typecheck": "tsc -p tsconfig.json",
39
+ "build": "tsc -p tsconfig.build.json"
40
+ }
6
41
  }