@helloleo/plugins 0.2.13 → 0.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -24955,7 +24955,8 @@ var scaffold = tool({
24955
24955
  const typeNotes = isStart ? [
24956
24956
  "- This is a TanStack Start project. The server entry is src/server.ts. There is no index.html or client entrypoint.",
24957
24957
  "- For JSON/CRUD APIs, add server routes under src/routes/api/. Read src/routes/api/health.ts first, add routes as needed.",
24958
- "- The database schema lives in src/db/schema.ts and is the single source of truth. Migration files under drizzle/ are generated and applied automatically. Never create, edit or delete them. Prefer adding new columns/tables over renaming existing ones."
24958
+ "- The database schema lives in src/db/schema.ts and is the single source of truth. Migration files under drizzle/ are generated and applied automatically. Never create, edit or delete them. Prefer adding new columns/tables over renaming existing ones.",
24959
+ "- Persistence goes through @helloleo/runtime exclusively: database via #/db (see src/db/index.ts), key-value store via #/lib/kv (see src/lib/kv.ts), file storage + presigned browser uploads via #/lib/storage (see src/lib/storage.ts), env/secrets via env from @helloleo/runtime. Read those files before server-side data work. Never install another DB/storage client, never import from cloudflare:workers, all of it is server-side only."
24959
24960
  ] : [
24960
24961
  "- This is a TanStack Router SPA. The client entry is index.html -> src/main.tsx. There is no server: no API routes, no server functions, no database.",
24961
24962
  "- Fetch data from external APIs only. If the app needs its own persistence or server-side code, it should have been scaffolded as tanstack-start."
@@ -1,13 +1 @@
1
- {
2
- "version": "7",
3
- "dialect": "sqlite",
4
- "entries": [
5
- {
6
- "idx": 0,
7
- "version": "6",
8
- "when": 1785420910012,
9
- "tag": "0000_tidy_kang",
10
- "breakpoints": true
11
- }
12
- ]
13
- }
1
+ {"version":"7","dialect":"sqlite","entries":[]}
@@ -1,18 +1,20 @@
1
- import { sql } from 'drizzle-orm'
2
- import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
3
-
4
- // Single source of truth for the D1 / libsql schema. Sqlite dialect → the same
5
- // definition runs on the dev cf-shim (libsql .dev.db, synced via `drizzle-kit
6
- // push`) and prod D1 (migrations via `wrangler d1 migrations apply`).
1
+ // Single source of truth for the database schema. Define tables here — the
2
+ // SQL migration files under drizzle/ are generated and applied automatically
3
+ // (dev on server start, production on publish). Never hand-write SQL or touch
4
+ // drizzle/. Prefer ADDING columns/tables over renaming or deleting existing
5
+ // ones: renames cannot be migrated automatically.
7
6
  //
8
- // Add your own tables here, then `bun run db:push` (dev) or `bun run
9
- // db:generate` + deploy (prod). Do NOT hand-write SQL — this file is the schema.
10
- export const entries = sqliteTable('entries', {
11
- id: integer('id').primaryKey({ autoIncrement: true }),
12
- label: text('label').notNull(),
13
- createdAt: integer('created_at')
14
- .notNull()
15
- .default(sql`(unixepoch())`),
16
- })
17
-
18
- export type Entry = typeof entries.$inferSelect
7
+ // Example:
8
+ //
9
+ // import { sql } from 'drizzle-orm'
10
+ // import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
11
+ //
12
+ // export const notes = sqliteTable('notes', {
13
+ // id: integer('id').primaryKey({ autoIncrement: true }),
14
+ // title: text('title').notNull(),
15
+ // createdAt: integer('created_at')
16
+ // .notNull()
17
+ // .default(sql`(unixepoch())`),
18
+ // })
19
+ //
20
+ // export type Note = typeof notes.$inferSelect
@@ -0,0 +1,37 @@
1
+ import { kv } from '@helloleo/runtime'
2
+
3
+ /**
4
+ * Key-value store — @helloleo/runtime `kv`. Use this file as the base for
5
+ * caches, sessions, counters and flags. Same import in dev and production;
6
+ * never install redis/ioredis or branch on environment.
7
+ *
8
+ * The raw contract (values are ALWAYS strings):
9
+ *
10
+ * await kv.put('session:abc', value, { expirationTtl: 3600 }) // ttl in seconds, optional
11
+ * await kv.get('session:abc') // string | null
12
+ * await kv.delete('session:abc')
13
+ * await kv.list({ prefix: 'session:', limit: 100 }) // { keys, cursor, listComplete }
14
+ *
15
+ * Rules:
16
+ * - Server-side only (route handlers, loaders, server functions) — the
17
+ * binding does not exist in the browser or at app module scope.
18
+ * - KV is for small, hot values. Relational data belongs in the database
19
+ * (`#/db`); files belong in storage (`#/lib/storage`).
20
+ * - Keys are flat strings — namespace with prefixes ('cache:', 'session:').
21
+ */
22
+
23
+ /** JSON convenience wrappers — kv stores strings, most values are objects. */
24
+ export async function kvGetJson<T>(key: string): Promise<T | null> {
25
+ const raw = await kv.get(key)
26
+ return raw === null ? null : (JSON.parse(raw) as T)
27
+ }
28
+
29
+ export async function kvPutJson(
30
+ key: string,
31
+ value: unknown,
32
+ opts?: { expirationTtl?: number },
33
+ ): Promise<void> {
34
+ await kv.put(key, JSON.stringify(value), opts)
35
+ }
36
+
37
+ export { kv }
@@ -0,0 +1,44 @@
1
+ import { storage } from '@helloleo/runtime'
2
+
3
+ /**
4
+ * File/object storage — @helloleo/runtime `storage`. Use this file as the
5
+ * base for uploads, images, exports and any binary data. Same import in dev
6
+ * and production; never install @aws-sdk/*, multer, or use node:fs.
7
+ *
8
+ * The raw contract:
9
+ *
10
+ * await storage.put('avatars/u1.png', bytes, { contentType: 'image/png' })
11
+ * await storage.get('avatars/u1.png') // { body: ReadableStream, size, contentType } | null
12
+ * await storage.head('avatars/u1.png') // { size, contentType } | null
13
+ * await storage.delete('avatars/u1.png')
14
+ * await storage.list({ prefix: 'avatars/', limit: 100 }) // { keys: [{ key, size }], cursor }
15
+ *
16
+ * Rules:
17
+ * - Server-side only (route handlers, loaders, server functions).
18
+ * - Browser uploads/downloads go through PRESIGNED URLS (below) — never
19
+ * stream file bytes through your own API route when a presigned URL works:
20
+ * the browser talks to storage directly.
21
+ * - Key layout is up to the app — namespace with prefixes ('avatars/',
22
+ * 'exports/').
23
+ */
24
+
25
+ /**
26
+ * Presign an upload: the server hands the browser a short-lived URL it can
27
+ * `fetch(url, { method: 'PUT', body: file })` to directly. Pair with a DB row
28
+ * that records the key.
29
+ */
30
+ export function getUploadUrl(
31
+ key: string,
32
+ opts?: { ttlSeconds?: number; contentType?: string },
33
+ ): Promise<string> {
34
+ return storage.presignPut(key, opts?.ttlSeconds ?? 600, {
35
+ contentType: opts?.contentType,
36
+ })
37
+ }
38
+
39
+ /** Presign a download — short-lived direct GET URL for the browser. */
40
+ export function getDownloadUrl(key: string, ttlSeconds = 600): Promise<string> {
41
+ return storage.presignGet(key, ttlSeconds)
42
+ }
43
+
44
+ export { storage }
@@ -1,6 +1,6 @@
1
1
  import { createFileRoute } from '@tanstack/react-router'
2
+ import { sql } from 'drizzle-orm'
2
3
  import { db } from '#/db'
3
- import { entries } from '#/db/schema'
4
4
 
5
5
  /**
6
6
  * Server route example: GET /api/health. Use this file as the base for
@@ -39,7 +39,9 @@ export const Route = createFileRoute('/api/health')({
39
39
  runtime: 'cloudflare-workers',
40
40
  }
41
41
 
42
- // Database health: lightweight query against a known table
42
+ // Database health: schema-agnostic connectivity ping. Deliberately
43
+ // NOT a query against an app table — the health probe must keep
44
+ // working no matter how src/db/schema.ts evolves.
43
45
  let database: {
44
46
  status: 'ok' | 'error'
45
47
  latencyMs: number
@@ -47,7 +49,7 @@ export const Route = createFileRoute('/api/health')({
47
49
  }
48
50
  const dbStart = Date.now()
49
51
  try {
50
- await db.select({ id: entries.id }).from(entries).limit(1)
52
+ await db.run(sql`SELECT 1`)
51
53
  database = {
52
54
  status: 'ok',
53
55
  latencyMs: Date.now() - dbStart,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helloleo/plugins",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "description": "OpenCode plugins for HelloLeo",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -1,13 +1 @@
1
- {
2
- "version": "7",
3
- "dialect": "sqlite",
4
- "entries": [
5
- {
6
- "idx": 0,
7
- "version": "6",
8
- "when": 1785420910012,
9
- "tag": "0000_tidy_kang",
10
- "breakpoints": true
11
- }
12
- ]
13
- }
1
+ {"version":"7","dialect":"sqlite","entries":[]}
@@ -1,18 +1,20 @@
1
- import { sql } from 'drizzle-orm'
2
- import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
3
-
4
- // Single source of truth for the D1 / libsql schema. Sqlite dialect → the same
5
- // definition runs on the dev cf-shim (libsql .dev.db, synced via `drizzle-kit
6
- // push`) and prod D1 (migrations via `wrangler d1 migrations apply`).
1
+ // Single source of truth for the database schema. Define tables here — the
2
+ // SQL migration files under drizzle/ are generated and applied automatically
3
+ // (dev on server start, production on publish). Never hand-write SQL or touch
4
+ // drizzle/. Prefer ADDING columns/tables over renaming or deleting existing
5
+ // ones: renames cannot be migrated automatically.
7
6
  //
8
- // Add your own tables here, then `bun run db:push` (dev) or `bun run
9
- // db:generate` + deploy (prod). Do NOT hand-write SQL — this file is the schema.
10
- export const entries = sqliteTable('entries', {
11
- id: integer('id').primaryKey({ autoIncrement: true }),
12
- label: text('label').notNull(),
13
- createdAt: integer('created_at')
14
- .notNull()
15
- .default(sql`(unixepoch())`),
16
- })
17
-
18
- export type Entry = typeof entries.$inferSelect
7
+ // Example:
8
+ //
9
+ // import { sql } from 'drizzle-orm'
10
+ // import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
11
+ //
12
+ // export const notes = sqliteTable('notes', {
13
+ // id: integer('id').primaryKey({ autoIncrement: true }),
14
+ // title: text('title').notNull(),
15
+ // createdAt: integer('created_at')
16
+ // .notNull()
17
+ // .default(sql`(unixepoch())`),
18
+ // })
19
+ //
20
+ // export type Note = typeof notes.$inferSelect
@@ -0,0 +1,37 @@
1
+ import { kv } from '@helloleo/runtime'
2
+
3
+ /**
4
+ * Key-value store — @helloleo/runtime `kv`. Use this file as the base for
5
+ * caches, sessions, counters and flags. Same import in dev and production;
6
+ * never install redis/ioredis or branch on environment.
7
+ *
8
+ * The raw contract (values are ALWAYS strings):
9
+ *
10
+ * await kv.put('session:abc', value, { expirationTtl: 3600 }) // ttl in seconds, optional
11
+ * await kv.get('session:abc') // string | null
12
+ * await kv.delete('session:abc')
13
+ * await kv.list({ prefix: 'session:', limit: 100 }) // { keys, cursor, listComplete }
14
+ *
15
+ * Rules:
16
+ * - Server-side only (route handlers, loaders, server functions) — the
17
+ * binding does not exist in the browser or at app module scope.
18
+ * - KV is for small, hot values. Relational data belongs in the database
19
+ * (`#/db`); files belong in storage (`#/lib/storage`).
20
+ * - Keys are flat strings — namespace with prefixes ('cache:', 'session:').
21
+ */
22
+
23
+ /** JSON convenience wrappers — kv stores strings, most values are objects. */
24
+ export async function kvGetJson<T>(key: string): Promise<T | null> {
25
+ const raw = await kv.get(key)
26
+ return raw === null ? null : (JSON.parse(raw) as T)
27
+ }
28
+
29
+ export async function kvPutJson(
30
+ key: string,
31
+ value: unknown,
32
+ opts?: { expirationTtl?: number },
33
+ ): Promise<void> {
34
+ await kv.put(key, JSON.stringify(value), opts)
35
+ }
36
+
37
+ export { kv }
@@ -0,0 +1,44 @@
1
+ import { storage } from '@helloleo/runtime'
2
+
3
+ /**
4
+ * File/object storage — @helloleo/runtime `storage`. Use this file as the
5
+ * base for uploads, images, exports and any binary data. Same import in dev
6
+ * and production; never install @aws-sdk/*, multer, or use node:fs.
7
+ *
8
+ * The raw contract:
9
+ *
10
+ * await storage.put('avatars/u1.png', bytes, { contentType: 'image/png' })
11
+ * await storage.get('avatars/u1.png') // { body: ReadableStream, size, contentType } | null
12
+ * await storage.head('avatars/u1.png') // { size, contentType } | null
13
+ * await storage.delete('avatars/u1.png')
14
+ * await storage.list({ prefix: 'avatars/', limit: 100 }) // { keys: [{ key, size }], cursor }
15
+ *
16
+ * Rules:
17
+ * - Server-side only (route handlers, loaders, server functions).
18
+ * - Browser uploads/downloads go through PRESIGNED URLS (below) — never
19
+ * stream file bytes through your own API route when a presigned URL works:
20
+ * the browser talks to storage directly.
21
+ * - Key layout is up to the app — namespace with prefixes ('avatars/',
22
+ * 'exports/').
23
+ */
24
+
25
+ /**
26
+ * Presign an upload: the server hands the browser a short-lived URL it can
27
+ * `fetch(url, { method: 'PUT', body: file })` to directly. Pair with a DB row
28
+ * that records the key.
29
+ */
30
+ export function getUploadUrl(
31
+ key: string,
32
+ opts?: { ttlSeconds?: number; contentType?: string },
33
+ ): Promise<string> {
34
+ return storage.presignPut(key, opts?.ttlSeconds ?? 600, {
35
+ contentType: opts?.contentType,
36
+ })
37
+ }
38
+
39
+ /** Presign a download — short-lived direct GET URL for the browser. */
40
+ export function getDownloadUrl(key: string, ttlSeconds = 600): Promise<string> {
41
+ return storage.presignGet(key, ttlSeconds)
42
+ }
43
+
44
+ export { storage }
@@ -1,6 +1,6 @@
1
1
  import { createFileRoute } from '@tanstack/react-router'
2
+ import { sql } from 'drizzle-orm'
2
3
  import { db } from '#/db'
3
- import { entries } from '#/db/schema'
4
4
 
5
5
  /**
6
6
  * Server route example: GET /api/health. Use this file as the base for
@@ -39,7 +39,9 @@ export const Route = createFileRoute('/api/health')({
39
39
  runtime: 'cloudflare-workers',
40
40
  }
41
41
 
42
- // Database health: lightweight query against a known table
42
+ // Database health: schema-agnostic connectivity ping. Deliberately
43
+ // NOT a query against an app table — the health probe must keep
44
+ // working no matter how src/db/schema.ts evolves.
43
45
  let database: {
44
46
  status: 'ok' | 'error'
45
47
  latencyMs: number
@@ -47,7 +49,7 @@ export const Route = createFileRoute('/api/health')({
47
49
  }
48
50
  const dbStart = Date.now()
49
51
  try {
50
- await db.select({ id: entries.id }).from(entries).limit(1)
52
+ await db.run(sql`SELECT 1`)
51
53
  database = {
52
54
  status: 'ok',
53
55
  latencyMs: Date.now() - dbStart,
@@ -153,6 +153,7 @@ export const scaffold = tool({
153
153
  "- This is a TanStack Start project. The server entry is src/server.ts. There is no index.html or client entrypoint.",
154
154
  "- For JSON/CRUD APIs, add server routes under src/routes/api/. Read src/routes/api/health.ts first, add routes as needed.",
155
155
  "- The database schema lives in src/db/schema.ts and is the single source of truth. Migration files under drizzle/ are generated and applied automatically. Never create, edit or delete them. Prefer adding new columns/tables over renaming existing ones.",
156
+ "- Persistence goes through @helloleo/runtime exclusively: database via #/db (see src/db/index.ts), key-value store via #/lib/kv (see src/lib/kv.ts), file storage + presigned browser uploads via #/lib/storage (see src/lib/storage.ts), env/secrets via env from @helloleo/runtime. Read those files before server-side data work. Never install another DB/storage client, never import from cloudflare:workers, all of it is server-side only.",
156
157
  ]
157
158
  : [
158
159
  "- This is a TanStack Router SPA. The client entry is index.html -> src/main.tsx. There is no server: no API routes, no server functions, no database.",
@@ -1,5 +0,0 @@
1
- CREATE TABLE `entries` (
2
- `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
3
- `label` text NOT NULL,
4
- `created_at` integer DEFAULT (unixepoch()) NOT NULL
5
- );
@@ -1,50 +0,0 @@
1
- {
2
- "version": "6",
3
- "dialect": "sqlite",
4
- "id": "ec5ce3c5-da3c-496c-942e-72a9a9579093",
5
- "prevId": "00000000-0000-0000-0000-000000000000",
6
- "tables": {
7
- "entries": {
8
- "name": "entries",
9
- "columns": {
10
- "id": {
11
- "name": "id",
12
- "type": "integer",
13
- "primaryKey": true,
14
- "notNull": true,
15
- "autoincrement": true
16
- },
17
- "label": {
18
- "name": "label",
19
- "type": "text",
20
- "primaryKey": false,
21
- "notNull": true,
22
- "autoincrement": false
23
- },
24
- "created_at": {
25
- "name": "created_at",
26
- "type": "integer",
27
- "primaryKey": false,
28
- "notNull": true,
29
- "autoincrement": false,
30
- "default": "(unixepoch())"
31
- }
32
- },
33
- "indexes": {},
34
- "foreignKeys": {},
35
- "compositePrimaryKeys": {},
36
- "uniqueConstraints": {},
37
- "checkConstraints": {}
38
- }
39
- },
40
- "views": {},
41
- "enums": {},
42
- "_meta": {
43
- "schemas": {},
44
- "tables": {},
45
- "columns": {}
46
- },
47
- "internal": {
48
- "indexes": {}
49
- }
50
- }
@@ -1,5 +0,0 @@
1
- CREATE TABLE `entries` (
2
- `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
3
- `label` text NOT NULL,
4
- `created_at` integer DEFAULT (unixepoch()) NOT NULL
5
- );
@@ -1,50 +0,0 @@
1
- {
2
- "version": "6",
3
- "dialect": "sqlite",
4
- "id": "ec5ce3c5-da3c-496c-942e-72a9a9579093",
5
- "prevId": "00000000-0000-0000-0000-000000000000",
6
- "tables": {
7
- "entries": {
8
- "name": "entries",
9
- "columns": {
10
- "id": {
11
- "name": "id",
12
- "type": "integer",
13
- "primaryKey": true,
14
- "notNull": true,
15
- "autoincrement": true
16
- },
17
- "label": {
18
- "name": "label",
19
- "type": "text",
20
- "primaryKey": false,
21
- "notNull": true,
22
- "autoincrement": false
23
- },
24
- "created_at": {
25
- "name": "created_at",
26
- "type": "integer",
27
- "primaryKey": false,
28
- "notNull": true,
29
- "autoincrement": false,
30
- "default": "(unixepoch())"
31
- }
32
- },
33
- "indexes": {},
34
- "foreignKeys": {},
35
- "compositePrimaryKeys": {},
36
- "uniqueConstraints": {},
37
- "checkConstraints": {}
38
- }
39
- },
40
- "views": {},
41
- "enums": {},
42
- "_meta": {
43
- "schemas": {},
44
- "tables": {},
45
- "columns": {}
46
- },
47
- "internal": {
48
- "indexes": {}
49
- }
50
- }