@tiniestcloud/create 0.0.0-stage → 0.1.6

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 (42) hide show
  1. package/README.md +26 -2
  2. package/index.mjs +117 -0
  3. package/package.json +33 -4
  4. package/sdk.d.ts +290 -0
  5. package/skills/tiny-design/SKILL.md +333 -0
  6. package/skills/tiny-design/app.md +72 -0
  7. package/skills/tiny-design/dashboard.md +54 -0
  8. package/skills/tiny-design/form.md +48 -0
  9. package/skills/tiny-design/presentation.md +127 -0
  10. package/skills/tiny-design/website.md +53 -0
  11. package/templates/app/AGENTS.md +41 -0
  12. package/templates/app/CLAUDE.md +1 -0
  13. package/templates/app/README.md +10 -0
  14. package/templates/app/components.json +25 -0
  15. package/templates/app/gitignore +24 -0
  16. package/templates/app/index.html +27 -0
  17. package/templates/app/package.json +32 -0
  18. package/templates/app/src/App.tsx +39 -0
  19. package/templates/app/src/components/theme-toggle.tsx +57 -0
  20. package/templates/app/src/components/ui/button.tsx +66 -0
  21. package/templates/app/src/components/ui/tooltip.tsx +54 -0
  22. package/templates/app/src/index.css +130 -0
  23. package/templates/app/src/lib/theme.ts +39 -0
  24. package/templates/app/src/lib/tiny.tsx +59 -0
  25. package/templates/app/src/lib/utils.ts +1 -0
  26. package/templates/app/src/main.tsx +20 -0
  27. package/templates/app/src/pages/home.tsx +10 -0
  28. package/templates/app/tsconfig.app.json +29 -0
  29. package/templates/app/tsconfig.json +12 -0
  30. package/templates/app/tsconfig.node.json +24 -0
  31. package/templates/app/vite.config.ts +14 -0
  32. package/templates/site/AGENTS.md +23 -0
  33. package/templates/site/CLAUDE.md +1 -0
  34. package/templates/site/README.md +10 -0
  35. package/templates/site/astro.config.mjs +11 -0
  36. package/templates/site/gitignore +24 -0
  37. package/templates/site/package.json +22 -0
  38. package/templates/site/src/layouts/Layout.astro +30 -0
  39. package/templates/site/src/pages/about.astro +10 -0
  40. package/templates/site/src/pages/index.astro +10 -0
  41. package/templates/site/src/styles/global.css +1 -0
  42. package/templates/site/tsconfig.json +5 -0
package/README.md CHANGED
@@ -1,3 +1,27 @@
1
- # Temporary Holding Version
1
+ # @tiniestcloud/create
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
+ Start an app or a website for [Tiniest Cloud](https://tiniest.cloud), which gives it sign-in,
4
+ storage and AI with no backend to write.
5
+
6
+ ```sh
7
+ npm create @tiniestcloud@latest my-app # an app
8
+ npm create @tiniestcloud@latest my-site -- --template site # a website
9
+ ```
10
+
11
+ - **app**: React, TypeScript, Vite, Tailwind CSS and shadcn/ui, for something people sign in
12
+ to and use. The SDK, its TypeScript types and React hooks are already wired in.
13
+ - **site**: Astro and Tailwind CSS, for pages people read. Every file in `src/pages` is a page.
14
+
15
+ Both come with the `tiny-design` skill in `.agents/skills` and `.claude/skills`, so Claude Code,
16
+ Codex, Cursor, Gemini CLI, GitHub Copilot and other coding agents pick up how it should look
17
+ without installing anything.
18
+
19
+ Then:
20
+
21
+ ```sh
22
+ cd my-app
23
+ npm install
24
+ npm run build
25
+ ```
26
+
27
+ and deploy the `dist` folder to Tiniest Cloud, from the dashboard or your coding agent.
package/index.mjs ADDED
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * npm create @tiniestcloud@latest <folder> [-- --template app|site]
4
+ *
5
+ * Copies a starter project for Tiniest Cloud into a new folder: 'app' is React, TypeScript,
6
+ * Vite, Tailwind CSS and shadcn/ui with the platform's SDK wired in; 'site' is Astro and
7
+ * Tailwind CSS. The templates are the same files the platform's get_starter tool returns.
8
+ * Beside them goes the tiny-design skill, so any coding agent opened in the project finds the
9
+ * design guidance without anyone installing it.
10
+ *
11
+ * No dependencies and no prompts: it is run by coding agents as often as by people, and an
12
+ * agent cannot answer a question on a terminal.
13
+ */
14
+ import { cp, mkdir, readdir, readFile, rename, writeFile } from "node:fs/promises";
15
+ import { existsSync } from "node:fs";
16
+ import { basename, join, resolve } from "node:path";
17
+ import { parseArgs } from "node:util";
18
+
19
+ const TEMPLATES = {
20
+ app: "React, TypeScript, Vite, Tailwind CSS and shadcn/ui",
21
+ site: "Astro and Tailwind CSS",
22
+ };
23
+ // The SDK's types, always the ones the platform serves today. The copy bundled at publish time
24
+ // is only for when that address cannot be reached.
25
+ const TYPES_URL = process.env.TINIEST_CLOUD_TYPES_URL ?? "https://app.tiniest.cloud/sdk.d.ts";
26
+ // Where coding agents look for a project's skills without being told: .agents/skills is read
27
+ // by Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and most others, .claude/skills by
28
+ // Claude Code. The skill is copied into the package when it is published (prepack).
29
+ const SKILL = join(import.meta.dirname, "skills", "tiny-design");
30
+ const SKILL_DIRS = [".agents/skills", ".claude/skills"];
31
+
32
+ const USAGE = `Usage: npm create @tiniestcloud@latest <folder> -- --template <app|site>
33
+
34
+ app ${TEMPLATES.app} — something people sign in to and use
35
+ site ${TEMPLATES.site} — pages people read`;
36
+
37
+ function fail(message) {
38
+ console.error(`${message}\n\n${USAGE}`);
39
+ process.exit(1);
40
+ }
41
+
42
+ let args;
43
+ try {
44
+ args = parseArgs({
45
+ allowPositionals: true,
46
+ options: {
47
+ template: { type: "string", short: "t" },
48
+ app: { type: "boolean" },
49
+ site: { type: "boolean" },
50
+ help: { type: "boolean", short: "h" },
51
+ },
52
+ });
53
+ } catch (err) {
54
+ fail(err.message);
55
+ }
56
+ if (args.values.help) {
57
+ console.log(USAGE);
58
+ process.exit(0);
59
+ }
60
+
61
+ const kind = args.values.template ?? (args.values.site ? "site" : "app");
62
+ if (!(kind in TEMPLATES)) fail(`"${kind}" is not a template.`);
63
+ if (args.positionals.length > 1) fail(`One folder at a time; got ${args.positionals.join(", ")}.`);
64
+
65
+ const folder = args.positionals[0] ?? (kind === "site" ? "my-site" : "my-app");
66
+ const target = resolve(folder);
67
+ if (existsSync(target) && (await readdir(target)).length > 0) {
68
+ fail(`${folder} already exists and is not empty. Choose a new folder.`);
69
+ }
70
+
71
+ async function sdkTypes() {
72
+ try {
73
+ const res = await fetch(TYPES_URL, { signal: AbortSignal.timeout(10_000) });
74
+ if (res.ok) return await res.text();
75
+ } catch {
76
+ // Falls through to the bundled copy.
77
+ }
78
+ const bundled = join(import.meta.dirname, "sdk.d.ts");
79
+ if (existsSync(bundled)) {
80
+ console.warn(`Could not reach ${TYPES_URL}; using the SDK types bundled with this package.`);
81
+ return await readFile(bundled, "utf8");
82
+ }
83
+ return null;
84
+ }
85
+
86
+ // Fetched before anything is written, so a failure leaves no half-made folder behind.
87
+ const types = kind === "app" ? await sdkTypes() : null;
88
+ if (kind === "app" && types === null) {
89
+ console.error(`Could not download the SDK's types from ${TYPES_URL}. Check the connection and run this again.`);
90
+ process.exit(1);
91
+ }
92
+
93
+ await mkdir(target, { recursive: true });
94
+ await cp(join(import.meta.dirname, "templates", kind), target, { recursive: true });
95
+ // Stored without its dot: npm leaves a .gitignore out of a published package.
96
+ await rename(join(target, "gitignore"), join(target, ".gitignore"));
97
+ if (types !== null) await writeFile(join(target, "src", "tiny.d.ts"), types);
98
+ if (existsSync(SKILL)) {
99
+ for (const dir of SKILL_DIRS) await cp(SKILL, join(target, dir, "tiny-design"), { recursive: true });
100
+ }
101
+
102
+ const pkgPath = join(target, "package.json");
103
+ const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
104
+ pkg.name = basename(target).toLowerCase().replace(/[^a-z0-9-~._]+/g, "-").replace(/^[-._]+/, "") || pkg.name;
105
+ await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
106
+
107
+ console.log(`Created ${folder}: ${TEMPLATES[kind]}.
108
+
109
+ cd ${folder}
110
+ npm install
111
+ npm run build
112
+
113
+ Then deploy the dist folder to Tiniest Cloud.${
114
+ kind === "app"
115
+ ? "\n`npm run dev` shows a notice instead of the app: sign-in and data come from Tiniest Cloud, so it runs once deployed."
116
+ : ""
117
+ }`);
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
1
  {
2
2
  "name": "@tiniestcloud/create",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.6",
4
+ "description": "Start an app or website for Tiniest Cloud: React, TypeScript, Vite, Tailwind CSS and shadcn/ui, or Astro.",
5
+ "keywords": [
6
+ "tiniest-cloud",
7
+ "create",
8
+ "starter",
9
+ "react",
10
+ "vite",
11
+ "astro",
12
+ "shadcn"
13
+ ],
14
+ "homepage": "https://tiniest.cloud",
15
+ "license": "MIT",
16
+ "type": "module",
17
+ "bin": {
18
+ "create-tiniest-cloud": "index.mjs"
19
+ },
20
+ "files": [
21
+ "index.mjs",
22
+ "sdk.d.ts",
23
+ "skills",
24
+ "templates"
25
+ ],
26
+ "engines": {
27
+ "node": ">=20.12"
28
+ },
29
+ "scripts": {
30
+ "prepack": "cp ../server/public/sdk.d.ts sdk.d.ts && rm -rf skills && mkdir skills && cp -R ../../skills/tiny-design skills/tiny-design"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ }
35
+ }
package/sdk.d.ts ADDED
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Types for the Tiniest Cloud SDK — the global `tiny` that `<script src="/sdk.js"></script>`
3
+ * defines. Drop this file into a TypeScript project (src/tiny.d.ts) and `tiny` is typed
4
+ * everywhere, with nothing to import or install.
5
+ *
6
+ * Served at /sdk.d.ts. Always the types for the SDK this deployment serves; a test fails
7
+ * when sdk.js gains a method this file does not declare.
8
+ */
9
+
10
+ declare namespace Tiny {
11
+ /** Errors the SDK throws. `status` is the HTTP status, or 0 when the platform was unreachable. */
12
+ interface TinyError extends Error {
13
+ name: "TinyError";
14
+ status: number;
15
+ }
16
+
17
+ interface User {
18
+ id: string;
19
+ email: string;
20
+ }
21
+
22
+ interface Entry<T = unknown> {
23
+ key: string;
24
+ value: T;
25
+ /** ISO timestamp of the last write. */
26
+ updated_at: string;
27
+ /** ISO timestamp of the first write. Kept by the platform; a page cannot set it. */
28
+ created_at: string;
29
+ /**
30
+ * Who created it, as an id that is the same for one person across this app's entries — not
31
+ * their account id, and different in every other app. Use it to group or filter entries by
32
+ * person (`where: { $created_by: entry.created_by }`); use `mine` for "is it yours". Kept by
33
+ * the platform. Null when no person did: a job, a webhook, an app key, a visitor with no
34
+ * account.
35
+ */
36
+ created_by: string | null;
37
+ /** Whether the signed-in person reading it created it. */
38
+ mine: boolean;
39
+ }
40
+
41
+ /** A range bound: a number compares numbers, a Date or ISO date string compares times, other text compares text. */
42
+ type Bound = number | string | Date;
43
+
44
+ type Comparison = {
45
+ eq?: unknown;
46
+ ne?: unknown;
47
+ gt?: Bound;
48
+ gte?: Bound;
49
+ lt?: Bound;
50
+ lte?: Bound;
51
+ /** Equal to any of these, compared as text. */
52
+ in?: Array<string | number | boolean>;
53
+ /** Contains this text, ignoring case. */
54
+ contains?: string;
55
+ };
56
+
57
+ /**
58
+ * A filter applied on the server, eight conditions at most. Keys are top-level fields of the
59
+ * stored value, or the entry's own `$created_at`, `$updated_at` (ranges) and `$created_by`
60
+ * ("me", or an entry's `created_by`). A bare value means equals (compared as text, so 5 matches "5"); an array
61
+ * means one of them.
62
+ */
63
+ type Where = Record<string, string | number | boolean | null | Date | Array<string | number | boolean> | Comparison>;
64
+
65
+ interface Filter {
66
+ where?: Where;
67
+ }
68
+
69
+ interface Ordered {
70
+ /**
71
+ * A field of the value to sort by — numbers as numbers, text in byte order, entries without
72
+ * it last — or `$created_at`, `$updated_at`, `$key`. A leading `-` is newest or largest first.
73
+ * Ties break on the key. Default: key order.
74
+ */
75
+ order?: string;
76
+ }
77
+
78
+ interface Page<T = unknown> {
79
+ prefix: string;
80
+ count: number;
81
+ entries: Entry<T>[];
82
+ /** Pass back for the next page; null at the end. */
83
+ cursor: string | null;
84
+ }
85
+
86
+ interface Group {
87
+ value: string | null;
88
+ count: number;
89
+ /** Present when `sum` was asked for. */
90
+ sum?: number;
91
+ avg?: number;
92
+ min?: number;
93
+ max?: number;
94
+ counted?: number;
95
+ }
96
+
97
+ interface WatchOptions extends Filter, Ordered {
98
+ /** With `order`, only the first N — "the latest 50 messages". */
99
+ limit?: number;
100
+ /** Called when the live connection cannot be made or is refused. */
101
+ onError?: (error: TinyError) => void;
102
+ }
103
+
104
+ interface DB {
105
+ /** The stored value, or null when there is none. */
106
+ get<T = unknown>(key: string): Promise<T | null>;
107
+ /** Any JSON value up to 256 KB. Resolves to the value stored. */
108
+ set<T>(key: string, value: T): Promise<T>;
109
+ /** Save only if the key is new — atomic; a racing second caller rejects with status 409. */
110
+ create<T>(key: string, value: T): Promise<T>;
111
+ /** Whether something was deleted. */
112
+ delete(key: string): Promise<boolean>;
113
+ /**
114
+ * Entries under a prefix. With `limit`, the first N (up to 1,000) in `order`; without it,
115
+ * every match, and an error past 1,000 rather than part of them.
116
+ */
117
+ list<T = unknown>(prefix?: string, options?: Filter & Ordered & { limit?: number }): Promise<Entry<T>[]>;
118
+ page<T = unknown>(prefix?: string, options?: Filter & Ordered & { cursor?: string | null; limit?: number }): Promise<Page<T>>;
119
+ /** Every entry under a prefix, paging automatically. A forward pass, not a snapshot. */
120
+ scan<T = unknown>(prefix?: string, options?: Filter & Ordered & { limit?: number }): AsyncIterable<Entry<T>>;
121
+ /** Live: calls back with the current entries on connect and after every change. Returns stop. */
122
+ watch<T = unknown>(prefix: string, onChange: (entries: Entry<T>[]) => void, options?: WatchOptions): () => void;
123
+ count(prefix?: string, options?: Filter): Promise<number>;
124
+ sum(prefix: string, field: string, options?: Filter): Promise<number>;
125
+ sumDetail(prefix: string, field: string, options?: Filter): Promise<{ prefix: string; field: string; sum: number; counted: number }>;
126
+ /** { value: count } per distinct value of `by`. Errors past 100 groups. */
127
+ countBy(prefix: string, by: string, options?: Filter): Promise<Record<string, number>>;
128
+ /** { value: total of field } per distinct value of `by`. */
129
+ sumBy(prefix: string, field: string, by: string, options?: Filter): Promise<Record<string, number>>;
130
+ /** `bucket` groups by the first N characters of `by` — 7 is per month for ISO dates. */
131
+ groupBy(prefix: string, options: Filter & { by?: string; sum?: string; bucket?: number }): Promise<Group[]>;
132
+ }
133
+
134
+ interface RoomMessage {
135
+ /** The sending connection — not a person; the same someone in two tabs is two senders. */
136
+ from: string;
137
+ data: string;
138
+ }
139
+
140
+ interface Room {
141
+ /** This connection, as others see it. Null until connected. */
142
+ readonly id: string | null;
143
+ /** Strings go as-is; anything else is JSON-encoded. Up to 4 KB. */
144
+ send(data: unknown): void;
145
+ /** Returns a function that removes the handler. */
146
+ on(handler: (message: RoomMessage) => void): () => void;
147
+ leave(): void;
148
+ }
149
+
150
+ type Effort = "low" | "medium" | "high" | "xhigh" | "max";
151
+
152
+ /** A File or Blob, what files.put returned (or its id or url), or a data: URL. */
153
+ type ImageInput = Blob | StoredFile | string;
154
+
155
+ interface GenerateOptions {
156
+ system?: string;
157
+ model?: string;
158
+ effort?: Effort;
159
+ /** Up to four. */
160
+ images?: ImageInput[];
161
+ }
162
+
163
+ type Question =
164
+ | { type: "noul"; instructions: string }
165
+ | { type: "choice"; instructions: string; criteria: Record<string, string> }
166
+ | { type: "score"; instructions: string; criteria: string[] };
167
+
168
+ type Answer<Q extends Question> = Q extends { type: "noul" }
169
+ ? { type: "noul"; noul: number }
170
+ : Q extends { type: "choice"; criteria: Record<infer K extends string, string> }
171
+ ? { type: "choice"; choice: K; confidence: number; probabilities: Record<K, number> }
172
+ : { type: "score"; score: number; confidence: number; probabilities: Record<string, number> };
173
+
174
+ interface Limits {
175
+ models: string[];
176
+ max_effort: Effort;
177
+ default_effort: Effort;
178
+ remaining: { user_hour: number; app_day: number };
179
+ judge: { available: boolean; remaining: { user_hour: number; app_day: number } };
180
+ }
181
+
182
+ interface AI {
183
+ generate(prompt: string, options?: GenerateOptions): Promise<string>;
184
+ stream(prompt: string, options?: GenerateOptions): AsyncIterable<string>;
185
+ /** Typed decisions about a text, answered as values to branch on. */
186
+ judge<Q extends Record<string, Question>>(state: unknown, questions: Q): Promise<{ [K in keyof Q]: Answer<Q[K]> }>;
187
+ limits(): Promise<Limits>;
188
+ }
189
+
190
+ interface StoredFile {
191
+ id: string;
192
+ url: string;
193
+ name: string;
194
+ type: string;
195
+ size: number;
196
+ }
197
+
198
+ interface Files {
199
+ put(file: Blob, options?: { name?: string; onProgress?: (percent: number) => void }): Promise<StoredFile>;
200
+ /** Where the bytes are, for an <img src>. */
201
+ url(ref: StoredFile | string): string;
202
+ delete(ref: StoredFile | string): Promise<boolean>;
203
+ }
204
+
205
+ interface FetchOptions {
206
+ /** "/..." or "?..."; the host is the connection's. */
207
+ path?: string;
208
+ method?: string;
209
+ headers?: Record<string, string>;
210
+ query?: Record<string, string | number | boolean>;
211
+ body?: string;
212
+ /** Any value, sent as application/json. */
213
+ json?: unknown;
214
+ }
215
+
216
+ interface FetchResult<T = any> {
217
+ ok: boolean;
218
+ status: number;
219
+ headers: Record<string, string>;
220
+ text: string;
221
+ /** Parsed when the response is JSON, else null. */
222
+ json: T | null;
223
+ }
224
+
225
+ interface Notify {
226
+ supported(): Promise<boolean>;
227
+ enabled(): Promise<boolean>;
228
+ /** Must run from a tap. Resolves true when the person said yes. */
229
+ ask(): Promise<boolean>;
230
+ stop(): Promise<void>;
231
+ test(): Promise<{ sent: number; pruned: number; skipped: number; failed: number }>;
232
+ /** Moves a subscribed device onto the server's current key. The SDK runs it on load. */
233
+ refresh(): Promise<void>;
234
+ }
235
+
236
+ /**
237
+ * What a backend function receives as its second argument — the platform, from the server
238
+ * side: `export default async (input, tiny: Tiny.Server) => value`. Reads and writes reach
239
+ * the caller's data under the app's rules, with the `function` role added.
240
+ */
241
+ interface Server {
242
+ /** Who called, or null for a visitor with no account. */
243
+ caller: { id: string; email: string; roles: string[] } | null;
244
+ db: ServerDB;
245
+ fetch<T = any>(connection: string, options?: FetchOptions): Promise<FetchResult<T>>;
246
+ ai: ServerAI;
247
+ }
248
+
249
+ interface ServerDB {
250
+ get<T = unknown>(key: string): Promise<T | null>;
251
+ set<T>(key: string, value: T): Promise<T>;
252
+ /** Save only if the key is new — atomic; a racing second call throws with status 409. */
253
+ create<T>(key: string, value: T): Promise<T>;
254
+ delete(key: string): Promise<boolean>;
255
+ list<T = unknown>(prefix?: string, options?: Filter): Promise<Entry<T>[]>;
256
+ page<T = unknown>(prefix?: string, options?: Filter & { cursor?: string | null; limit?: number }): Promise<Page<T>>;
257
+ count(prefix?: string, options?: Filter): Promise<number>;
258
+ sum(prefix: string, field: string, options?: Filter): Promise<number>;
259
+ groupBy(prefix: string, options: Filter & { by?: string; sum?: string; bucket?: number }): Promise<Group[]>;
260
+ }
261
+
262
+ interface ServerAI {
263
+ generate(prompt: string, options?: Omit<GenerateOptions, "images">): Promise<string>;
264
+ judge<Q extends Record<string, Question>>(state: unknown, questions: Q): Promise<{ [K in keyof Q]: Answer<Q[K]> }>;
265
+ }
266
+
267
+ interface SDK {
268
+ /** The signed-in visitor, or null. */
269
+ me(): Promise<User | null>;
270
+ /** Send the visitor to sign in, returning to this page afterwards. */
271
+ login(): void;
272
+ logout(): Promise<void>;
273
+ db: DB;
274
+ room(name: string): Room;
275
+ ai: AI;
276
+ files: Files;
277
+ /** Run one of the app's backend functions (`_functions/<name>.js`) and get what it returned. */
278
+ call<T = any>(name: string, input?: unknown): Promise<T>;
279
+ /** Call an API through a connection the app's owner set up. */
280
+ fetch<T = any>(connection: string, options?: FetchOptions): Promise<FetchResult<T>>;
281
+ notify: Notify;
282
+ }
283
+ }
284
+
285
+ /** Defined by <script src="/sdk.js"></script>; absent anywhere the app is not served by Tiniest Cloud. */
286
+ declare var tiny: Tiny.SDK;
287
+
288
+ interface Window {
289
+ tiny: Tiny.SDK;
290
+ }