@uetuluk/create-cli 0.0.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 (3) hide show
  1. package/README.md +68 -0
  2. package/dist/cli.js +293 -0
  3. package/package.json +49 -0
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # @uetuluk/create-cli
2
+
3
+ CLI for the [create.ritsdev.top](https://create.ritsdev.top) platform — a small self-hosted teenybase-as-a-service.
4
+
5
+ Register an account, provision a teenybase instance under your own subdomain, push schema, stream logs.
6
+
7
+ ## Quick start
8
+
9
+ ```sh
10
+ # no install — just use npx
11
+ npx @uetuluk/create-cli register
12
+ npx @uetuluk/create-cli create
13
+ npx @uetuluk/create-cli list
14
+ ```
15
+
16
+ Or install once:
17
+
18
+ ```sh
19
+ npm i -g @uetuluk/create-cli
20
+ teeny register
21
+ teeny create
22
+ ```
23
+
24
+ ## Commands
25
+
26
+ | | |
27
+ |---|---|
28
+ | `teeny register` | create an account |
29
+ | `teeny login` | log in to an existing account |
30
+ | `teeny logout` | forget local credentials |
31
+ | `teeny whoami` | show current user |
32
+ | `teeny create` | provision a new teenybase instance, returns a `<adj>-<animal>-NNNN` subdomain + admin token |
33
+ | `teeny list` | list your instances |
34
+ | `teeny status <name>` | one instance's details |
35
+ | `teeny delete <name>` | tear down an instance (db + bucket + container) |
36
+ | `teeny deploy <name>` | push your `teenybase.config.ts` to an instance |
37
+ | `teeny logs <name>` | stream live container logs |
38
+
39
+ All commands take `--server <url>` to point at a different platform; defaults to `https://create.ritsdev.top` (or `TEENY_SERVER` env).
40
+
41
+ ## Files
42
+
43
+ - Account credentials: `~/.config/teenybase-create/credentials.json`
44
+ - Per-instance admin tokens: `~/.config/teenybase-create/instances/<name>.json`
45
+
46
+ Both are written `0o600`. The admin token for an instance is shown **only once** at create time; if you lose it, delete and recreate the instance.
47
+
48
+ ## Claude Code skill
49
+
50
+ If you're using Claude Code, install the platform skill so the assistant knows how to drive this CLI:
51
+
52
+ ```sh
53
+ mkdir -p ~/.claude/skills/create-ritsdev
54
+ curl -fsSL https://create.ritsdev.top/skills/create-ritsdev/SKILL.md \
55
+ > ~/.claude/skills/create-ritsdev/SKILL.md
56
+ ```
57
+
58
+ ## What's a teenybase?
59
+
60
+ [teenybase](https://teenybase.com) is a tiny declarative backend (auth, schema, REST). Upstream targets Cloudflare Workers + D1; this platform runs a Node + Postgres + MinIO port of the same codebase under your own subdomain on `*.create.ritsdev.top`.
61
+
62
+ ## Quotas
63
+
64
+ 5 instances per account. Delete to free a slot.
65
+
66
+ ## Status
67
+
68
+ Pre-alpha. Don't put production data in it yet.
package/dist/cli.js ADDED
@@ -0,0 +1,293 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { cac } from "cac";
5
+ import colors from "picocolors";
6
+ import prompts from "prompts";
7
+
8
+ // src/api.ts
9
+ var ApiError = class extends Error {
10
+ constructor(status, message, body) {
11
+ super(message);
12
+ this.status = status;
13
+ this.body = body;
14
+ }
15
+ };
16
+ async function api(serverUrl2, path, init = {}) {
17
+ const headers = new Headers(init.headers);
18
+ if (init.body && !headers.has("content-type")) headers.set("content-type", "application/json");
19
+ if (init.token) headers.set("authorization", `Bearer ${init.token}`);
20
+ const r = await fetch(`${serverUrl2}${path}`, { ...init, headers });
21
+ const text = await r.text();
22
+ let body = text;
23
+ try {
24
+ body = JSON.parse(text);
25
+ } catch {
26
+ }
27
+ if (!r.ok) {
28
+ const msg = body && typeof body === "object" && (body.error || body.message) || `HTTP ${r.status}`;
29
+ throw new ApiError(r.status, msg, body);
30
+ }
31
+ return body;
32
+ }
33
+
34
+ // src/store.ts
35
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
36
+ import { homedir } from "node:os";
37
+ import { join } from "node:path";
38
+ var DEFAULT_SERVER_URL = "https://create.ritsdev.top";
39
+ var CONFIG_DIR = process.env.TEENY_CONFIG_DIR || join(homedir(), ".config", "teenybase-create");
40
+ var credentialsPath = () => join(CONFIG_DIR, "credentials.json");
41
+ function loadCredentials() {
42
+ const p = credentialsPath();
43
+ if (!existsSync(p)) return null;
44
+ try {
45
+ return JSON.parse(readFileSync(p, "utf8"));
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+ function saveCredentials(c) {
51
+ mkdirSync(CONFIG_DIR, { recursive: true });
52
+ const p = credentialsPath();
53
+ writeFileSync(p, JSON.stringify(c, null, 2));
54
+ try {
55
+ chmodSync(p, 384);
56
+ } catch {
57
+ }
58
+ }
59
+ function deleteCredentials() {
60
+ const p = credentialsPath();
61
+ if (!existsSync(p)) return false;
62
+ rmSync(p);
63
+ return true;
64
+ }
65
+ var instanceDir = () => join(CONFIG_DIR, "instances");
66
+ var instancePath = (name) => join(instanceDir(), `${name}.json`);
67
+ function saveInstance(c) {
68
+ mkdirSync(instanceDir(), { recursive: true });
69
+ const p = instancePath(c.name);
70
+ writeFileSync(p, JSON.stringify(c, null, 2));
71
+ try {
72
+ chmodSync(p, 384);
73
+ } catch {
74
+ }
75
+ }
76
+ function loadInstance(name) {
77
+ const p = instancePath(name);
78
+ if (!existsSync(p)) return null;
79
+ try {
80
+ return JSON.parse(readFileSync(p, "utf8"));
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+ function deleteInstance(name) {
86
+ const p = instancePath(name);
87
+ if (existsSync(p)) rmSync(p);
88
+ }
89
+
90
+ // src/cli.ts
91
+ var cli = cac("teeny");
92
+ cli.option("--server <url>", `[string] platform URL (default: ${DEFAULT_SERVER_URL} or env TEENY_SERVER)`);
93
+ function serverUrl(opts) {
94
+ return (opts.server || process.env.TEENY_SERVER || DEFAULT_SERVER_URL).replace(/\/+$/, "");
95
+ }
96
+ function requireAuth() {
97
+ const c = loadCredentials();
98
+ if (!c) {
99
+ console.error(colors.red("not logged in. run `teeny login` or `teeny register`"));
100
+ process.exit(1);
101
+ }
102
+ return c;
103
+ }
104
+ function wrap(fn) {
105
+ return async (...args) => {
106
+ try {
107
+ await fn(...args);
108
+ } catch (e) {
109
+ if (e instanceof ApiError) {
110
+ console.error(colors.red(`${e.status} ${e.message}`));
111
+ } else {
112
+ console.error(colors.red(e.message));
113
+ }
114
+ process.exit(1);
115
+ }
116
+ };
117
+ }
118
+ cli.command("register", "create an account on the platform").option("--email <email>", "[string] email").option("--username <username>", "[string] username (default: derived from email)").option("--password <password>", "[string] password (min 8 chars)").action(wrap(async (opts) => {
119
+ const url = serverUrl(opts);
120
+ const email = opts.email ?? (await prompts({ type: "text", name: "v", message: "email:", validate: (v) => /@/.test(v) || "must look like an email" })).v;
121
+ const username = opts.username ?? (await prompts({ type: "text", name: "v", message: "username:", initial: email.split("@")[0].replace(/[^a-zA-Z0-9_-]/g, "_") })).v;
122
+ const password = opts.password ?? (await prompts({ type: "password", name: "v", message: "password (min 8):", validate: (v) => v.length >= 8 || "min 8 chars" })).v;
123
+ const r = await api(url, "/auth/sign-up", {
124
+ method: "POST",
125
+ body: JSON.stringify({ email, username, name: username, password, passwordConfirm: password })
126
+ });
127
+ saveCredentials({
128
+ serverUrl: url,
129
+ email,
130
+ username: r.record.username,
131
+ userId: r.record.id,
132
+ token: r.token,
133
+ refreshToken: r.refresh_token
134
+ });
135
+ console.log(colors.green(`registered as ${username} at ${url}`));
136
+ }));
137
+ cli.command("login", "log in to the platform").option("--email <email>", "[string] email").option("--password <password>", "[string] password").action(wrap(async (opts) => {
138
+ const url = serverUrl(opts);
139
+ const email = opts.email ?? (await prompts({ type: "text", name: "v", message: "email:" })).v;
140
+ const password = opts.password ?? (await prompts({ type: "password", name: "v", message: "password:" })).v;
141
+ const r = await api(url, "/auth/login-password", {
142
+ method: "POST",
143
+ body: JSON.stringify({ email, password })
144
+ });
145
+ saveCredentials({
146
+ serverUrl: url,
147
+ email,
148
+ username: r.record.username,
149
+ userId: r.record.id,
150
+ token: r.token,
151
+ refreshToken: r.refresh_token
152
+ });
153
+ console.log(colors.green(`logged in as ${r.record.username} at ${url}`));
154
+ }));
155
+ cli.command("logout", "forget local credentials").action(wrap(async () => {
156
+ if (deleteCredentials()) console.log("logged out");
157
+ else console.log("not logged in");
158
+ }));
159
+ cli.command("whoami", "show current user").action(wrap(async () => {
160
+ const c = loadCredentials();
161
+ if (!c) {
162
+ console.log("not logged in");
163
+ return;
164
+ }
165
+ console.log(`${colors.bold(c.username)} <${c.email}>`);
166
+ console.log(`server: ${c.serverUrl}`);
167
+ }));
168
+ cli.command("create", "provision a new instance").action(wrap(async (opts) => {
169
+ const c = requireAuth();
170
+ const url = opts.server ? serverUrl(opts) : c.serverUrl;
171
+ const r = await api(url, "/platform/instances", {
172
+ method: "POST",
173
+ token: c.token
174
+ });
175
+ saveInstance({ name: r.name, url: r.url, adminToken: r.admin_token, serverUrl: url });
176
+ console.log(colors.green(`created ${colors.bold(r.name)}`));
177
+ console.log(` url: ${r.url}`);
178
+ console.log(` admin token saved to local store (only shown once)`);
179
+ }));
180
+ cli.command("list", "list your instances").action(wrap(async (opts) => {
181
+ const c = requireAuth();
182
+ const url = opts.server ? serverUrl(opts) : c.serverUrl;
183
+ const r = await api(url, "/platform/instances", { token: c.token });
184
+ if (!r.result.length) {
185
+ console.log("no instances. run `teeny create`");
186
+ return;
187
+ }
188
+ for (const inst of r.result) {
189
+ console.log(` ${colors.green(inst.name)} ${colors.dim(inst.url)}`);
190
+ console.log(` created: ${inst.created_at}`);
191
+ }
192
+ }));
193
+ cli.command("status <name>", "show instance details").action(wrap(async (name, opts) => {
194
+ const c = requireAuth();
195
+ const url = opts.server ? serverUrl(opts) : c.serverUrl;
196
+ const r = await api(url, `/platform/instances/${encodeURIComponent(name)}`, { token: c.token });
197
+ console.log(colors.bold(r.result.name));
198
+ console.log(` url: ${r.result.url}`);
199
+ console.log(` created: ${r.result.created_at}`);
200
+ }));
201
+ cli.command("delete <name>", "tear down an instance").option("-y, --yes", "[boolean] skip confirmation").action(wrap(async (name, opts) => {
202
+ const c = requireAuth();
203
+ const url = opts.server ? serverUrl(opts) : c.serverUrl;
204
+ if (!opts.yes) {
205
+ const r = await prompts({ type: "confirm", name: "v", message: `delete ${name}? this is permanent`, initial: false });
206
+ if (!r.v) {
207
+ console.log("cancelled");
208
+ return;
209
+ }
210
+ }
211
+ await api(url, `/platform/instances/${encodeURIComponent(name)}`, { method: "DELETE", token: c.token });
212
+ deleteInstance(name);
213
+ console.log(colors.green(`deleted ${name}`));
214
+ }));
215
+ cli.command("deploy <name>", "push schema + migrations to your instance").option("--config <file>", "[string] path to teenybase.config.ts (default: ./teenybase.config.ts)").action(wrap(async (name, opts) => {
216
+ requireAuth();
217
+ const inst = loadInstance(name);
218
+ if (!inst) {
219
+ console.error(colors.red(`no local credentials for instance "${name}". run \`teeny create\` first, or contact whoever provisioned it`));
220
+ process.exit(1);
221
+ }
222
+ const configPath = opts.config ?? "./teenybase.config.ts";
223
+ let settings;
224
+ try {
225
+ const mod = await import(`${process.cwd()}/${configPath.replace(/^\.\//, "")}`);
226
+ settings = mod.default ?? mod.settings ?? mod;
227
+ } catch (e) {
228
+ console.error(colors.red(`failed to load ${configPath}: ${e.message}`));
229
+ process.exit(1);
230
+ }
231
+ await api(inst.url, "/api/v1/setup-db", {
232
+ method: "POST",
233
+ token: inst.adminToken,
234
+ body: JSON.stringify({})
235
+ });
236
+ const r = await api(inst.url, "/api/v1/migrations", {
237
+ method: "POST",
238
+ token: inst.adminToken,
239
+ body: JSON.stringify({ migrations: [], settings, lastVersion: null })
240
+ });
241
+ console.log(colors.green(`deployed to ${inst.url}`));
242
+ if (r.applied?.length) console.log(` applied: ${r.applied.join(", ")}`);
243
+ }));
244
+ cli.command("logs <name>", "stream live logs from an instance").option("--tail <n>", "[number] initial tail (default: 200)").action(wrap(async (name, opts) => {
245
+ const c = requireAuth();
246
+ const url = opts.server ? serverUrl(opts) : c.serverUrl;
247
+ const tail = opts.tail || "200";
248
+ const res = await fetch(`${url}/platform/instances/${encodeURIComponent(name)}/logs?tail=${tail}`, {
249
+ headers: { authorization: `Bearer ${c.token}` }
250
+ });
251
+ if (!res.ok || !res.body) {
252
+ console.error(colors.red(`failed to stream: ${res.status}`));
253
+ process.exit(1);
254
+ }
255
+ const reader = res.body.getReader();
256
+ const decoder = new TextDecoder();
257
+ let buf = "";
258
+ while (true) {
259
+ const { done, value } = await reader.read();
260
+ if (done) break;
261
+ buf += decoder.decode(value, { stream: true });
262
+ let idx;
263
+ while ((idx = buf.indexOf("\n\n")) >= 0) {
264
+ const frame = buf.slice(0, idx);
265
+ buf = buf.slice(idx + 2);
266
+ const lines = frame.split("\n");
267
+ let event = "message";
268
+ let data = "";
269
+ for (const line of lines) {
270
+ if (line.startsWith("event: ")) event = line.slice(7);
271
+ else if (line.startsWith("data: ")) data += line.slice(6);
272
+ }
273
+ if (event === "log") {
274
+ try {
275
+ const { src, line } = JSON.parse(data);
276
+ const tag = src === "stderr" ? colors.red("err") : colors.dim("out");
277
+ console.log(`${tag} ${line}`);
278
+ } catch {
279
+ console.log(data);
280
+ }
281
+ } else if (event === "end") {
282
+ console.log(colors.dim("--- container exited ---"));
283
+ return;
284
+ } else if (event === "error") {
285
+ console.error(colors.red(`stream error: ${data}`));
286
+ return;
287
+ }
288
+ }
289
+ }
290
+ }));
291
+ cli.help();
292
+ cli.version("0.0.1");
293
+ cli.parse();
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@uetuluk/create-cli",
3
+ "version": "0.0.1",
4
+ "description": "CLI for the create.ritsdev.top platform — register, provision, and deploy teenybase instances.",
5
+ "type": "module",
6
+ "bin": {
7
+ "teeny": "./dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "keywords": ["teenybase", "platform", "cli", "ritsdev"],
20
+ "author": "uetuluk",
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/uetuluk/create-ritsdev-platform.git",
25
+ "directory": "cli"
26
+ },
27
+ "homepage": "https://github.com/uetuluk/create-ritsdev-platform/tree/main/cli#readme",
28
+ "bugs": {
29
+ "url": "https://github.com/uetuluk/create-ritsdev-platform/issues"
30
+ },
31
+ "scripts": {
32
+ "build": "esbuild src/cli.ts --bundle --outfile=dist/cli.js --platform=node --format=esm --packages=external --banner:js=\"#!/usr/bin/env node\" && chmod +x dist/cli.js",
33
+ "check": "tsc -p tsconfig.json --noEmit",
34
+ "dev": "tsx src/cli.ts",
35
+ "prepublishOnly": "npm run check && npm run build"
36
+ },
37
+ "dependencies": {
38
+ "cac": "^6.7.14",
39
+ "picocolors": "^1.1.1",
40
+ "prompts": "^2.4.2"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.0.0",
44
+ "@types/prompts": "^2.4.9",
45
+ "esbuild": "^0.24.0",
46
+ "tsx": "^4.19.0",
47
+ "typescript": "^5.6.0"
48
+ }
49
+ }