@tallpond/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 (5) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -0
  3. package/cli.js +816 -0
  4. package/index.js +362 -0
  5. package/package.json +39 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Carson Gragg
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 ADDED
@@ -0,0 +1,56 @@
1
+ # @tallpond/cli
2
+
3
+ The `tallpond` CLI is the whole developer workflow for [tallpond](https://tallpond.com):
4
+ sign in once, register an app, and ship schema + frontend with one command. No
5
+ dashboard steps, no hand-provisioned OAuth clients, no shared secrets.
6
+
7
+ ## Install
8
+
9
+ Run it without installing:
10
+
11
+ ```sh
12
+ npx @tallpond/cli login
13
+ ```
14
+
15
+ or install globally so the `tallpond` command is on your PATH:
16
+
17
+ ```sh
18
+ npm i -g @tallpond/cli
19
+ ```
20
+
21
+ ## Quick start
22
+
23
+ ```sh
24
+ tallpond login # sign in (browser approval), saves a developer token
25
+ tallpond apps create "My App" # registers the app + its OAuth client, writes tallpond.json
26
+ tallpond deploy # applies .tallpond.schema.ts and publishes ./dist
27
+ ```
28
+
29
+ A deployed app is served at `https://<slug>.tallpond.app` with the schema live
30
+ behind the gateway.
31
+
32
+ ## `tallpond login`
33
+
34
+ Device-flow sign-in: the CLI prints an approval URL with a short confirmation
35
+ code, you approve it in the browser using your existing tallpond session, and the
36
+ CLI receives a developer token saved to `~/.config/tallpond/credentials.json`
37
+ (mode 600). The code in the URL can only *approve* — it can never mint a token,
38
+ so a leaked link doesn't compromise the login.
39
+
40
+ `TALLPOND_DEV_TOKEN` overrides the saved token (for CI / non-interactive agents).
41
+ `tallpond logout` forgets it; `tallpond whoami` prints the signed-in user.
42
+
43
+ ## `tallpond deploy [schemaFile] [--dir dist] [--no-bundle]`
44
+
45
+ Applies `.tallpond.schema.ts` (compiled locally) and uploads `./dist` as a new
46
+ immutable version, then flips the live pointer in one call. Safe schema changes
47
+ apply automatically; destructive ones are blocked pending an explicit migration.
48
+
49
+ The schema file is TypeScript: the CLI compiles it in-process (via esbuild) so it
50
+ runs on any supported Node — no bun or global TS loader required.
51
+
52
+ Full command reference: <https://tallpond.com> → docs → CLI.
53
+
54
+ ## License
55
+
56
+ MIT
package/cli.js ADDED
@@ -0,0 +1,816 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { parseArgs } from "node:util";
5
+ import { spawn } from "node:child_process";
6
+ import { existsSync as existsSync2 } from "node:fs";
7
+ import { writeFile as writeFile3 } from "node:fs/promises";
8
+ import { resolve as resolve4 } from "node:path";
9
+
10
+ // src/deploy.ts
11
+ import { extname, join, resolve } from "node:path";
12
+ import { tmpdir } from "node:os";
13
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
14
+ import { pathToFileURL } from "node:url";
15
+ function resolveConfig(flags, env) {
16
+ const gatewayUrl = flags.gatewayUrl ?? env["TALLPOND_GATEWAY_URL"];
17
+ const appId = flags.appId ?? env["TALLPOND_APP_ID"];
18
+ const token = flags.token ?? env["TALLPOND_DEPLOY_TOKEN"];
19
+ const deploymentId = flags.deploymentId ?? env["TALLPOND_DEPLOYMENT_ID"] ?? generateDeploymentId();
20
+ if (!gatewayUrl) return { error: "missing gateway URL (--gateway-url or TALLPOND_GATEWAY_URL)" };
21
+ if (!appId) return { error: "missing app id (--app-id or TALLPOND_APP_ID)" };
22
+ if (!token) return { error: "missing deploy token (--token or TALLPOND_DEPLOY_TOKEN)" };
23
+ if (!/^[A-Za-z0-9]+$/.test(deploymentId)) {
24
+ return { error: `deploymentId "${deploymentId}" must be alphanumeric` };
25
+ }
26
+ return { gatewayUrl, appId, deploymentId, token };
27
+ }
28
+ function generateDeploymentId() {
29
+ const ts = Date.now().toString(36);
30
+ const rand = Math.random().toString(36).slice(2, 8);
31
+ return `d${ts}${rand}`;
32
+ }
33
+ var TS_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts"]);
34
+ async function loadSchema(schemaPath) {
35
+ const abs = resolve(process.cwd(), schemaPath);
36
+ let importUrl = pathToFileURL(abs).href;
37
+ let cleanup;
38
+ if (TS_EXT.has(extname(abs).toLowerCase())) {
39
+ const { build } = await import("esbuild");
40
+ const result = await build({
41
+ entryPoints: [abs],
42
+ bundle: true,
43
+ format: "esm",
44
+ platform: "node",
45
+ write: false,
46
+ logLevel: "silent"
47
+ });
48
+ const dir = await mkdtemp(join(tmpdir(), "tallpond-schema-"));
49
+ const out = join(dir, "schema.mjs");
50
+ await writeFile(out, result.outputFiles[0].text);
51
+ importUrl = pathToFileURL(out).href;
52
+ cleanup = () => rm(dir, { recursive: true, force: true });
53
+ }
54
+ let mod;
55
+ try {
56
+ mod = await import(importUrl);
57
+ } catch (e) {
58
+ throw new Error(`failed to load schema at ${abs}: ${e.message}`);
59
+ } finally {
60
+ await cleanup?.();
61
+ }
62
+ const schema = mod.default;
63
+ if (!schema || typeof schema !== "object" || !Array.isArray(schema.tables)) {
64
+ throw new Error(`${abs} must \`export default defineSchema({ ... })\``);
65
+ }
66
+ return schema;
67
+ }
68
+ async function deploy(schema, config, deps = { fetch }) {
69
+ const res = await deps.fetch(new URL("/deploy", config.gatewayUrl).toString(), {
70
+ method: "POST",
71
+ headers: {
72
+ "content-type": "application/json",
73
+ "x-tallpond-deploy-token": config.token
74
+ },
75
+ body: JSON.stringify({
76
+ deploymentId: config.deploymentId,
77
+ appId: config.appId,
78
+ schema
79
+ })
80
+ });
81
+ const body = await res.json().catch(() => null);
82
+ if (res.ok) {
83
+ return {
84
+ ok: true,
85
+ version: body?.version ?? "(unknown)",
86
+ applied: body?.applied ?? 0,
87
+ noop: body?.noop ?? false
88
+ };
89
+ }
90
+ if (res.status === 409 && body?.error === "migration_blocked") {
91
+ return { ok: false, kind: "migration_blocked", changes: body.changes ?? [] };
92
+ }
93
+ return {
94
+ ok: false,
95
+ kind: "error",
96
+ status: res.status,
97
+ error: body?.error ?? `http_${res.status}`,
98
+ ...body?.detail ? { detail: body.detail } : {}
99
+ };
100
+ }
101
+ function renderBlockedChanges(changes) {
102
+ if (changes.length === 0) return " (no change details returned)";
103
+ return changes.map((c) => {
104
+ const head = ` [${c.class}] ${c.description}`;
105
+ return c.sql ? `${head}
106
+ ${c.sql}` : head;
107
+ }).join("\n");
108
+ }
109
+
110
+ // src/typegen.ts
111
+ var TS_TYPE = {
112
+ text: "string",
113
+ uuid: "string",
114
+ integer: "number",
115
+ bigint: "number",
116
+ boolean: "boolean",
117
+ jsonb: "unknown",
118
+ timestamp: "string"
119
+ };
120
+ function pascal(name) {
121
+ return name.replace(/(^|[_-])([a-z])/g, (_, __, c) => c.toUpperCase());
122
+ }
123
+ function fieldType(col) {
124
+ const base = TS_TYPE[col.type];
125
+ return col.primaryKey || col.notNull ? base : `${base} | null`;
126
+ }
127
+ function rowInterface(table) {
128
+ const name = `${pascal(table.name)}Row`;
129
+ const hasId = table.columns.some((c) => c.name === "id");
130
+ const fields = table.columns.map((c) => ` ${c.name}: ${fieldType(c)}`);
131
+ const lines = hasId ? fields : [` id: string`, ...fields];
132
+ return { name, body: `export interface ${name} {
133
+ ${lines.join("\n")}
134
+ }` };
135
+ }
136
+ function generateTypes(schema) {
137
+ const tables = [...schema.tables];
138
+ for (const res of schema.resources) tables.push(...res.tables);
139
+ tables.sort((a, b) => a.name.localeCompare(b.name));
140
+ const interfaces = tables.map(rowInterface);
141
+ const registryLines = tables.map((t, i) => ` ${t.name}: ${interfaces[i].name}`);
142
+ return `// Generated by \`tallpond typegen\`. Do not edit by hand.
143
+ /* eslint-disable */
144
+ import type {} from '@tallpond/sdk'
145
+
146
+ ${interfaces.map((i) => i.body).join("\n\n")}
147
+
148
+ declare module '@tallpond/sdk' {
149
+ interface Register {
150
+ tables: {
151
+ ${registryLines.join("\n")}
152
+ }
153
+ }
154
+ }
155
+ `;
156
+ }
157
+
158
+ // src/config.ts
159
+ import { homedir } from "node:os";
160
+ import { dirname, join as join2, resolve as resolve2 } from "node:path";
161
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
162
+ var DEFAULT_GATEWAY_URL = "https://api.tallpond.com";
163
+ function resolveGatewayUrl(flag) {
164
+ return (flag ?? process.env["TALLPOND_GATEWAY_URL"] ?? DEFAULT_GATEWAY_URL).replace(/\/$/, "");
165
+ }
166
+ function credentialsPath() {
167
+ const base = process.env["XDG_CONFIG_HOME"] ?? join2(homedir(), ".config");
168
+ return join2(base, "tallpond", "credentials.json");
169
+ }
170
+ function readCredentialsFile() {
171
+ try {
172
+ return JSON.parse(readFileSync(credentialsPath(), "utf8"));
173
+ } catch {
174
+ return {};
175
+ }
176
+ }
177
+ function savedToken(gatewayUrl) {
178
+ const fromEnv = process.env["TALLPOND_DEV_TOKEN"];
179
+ if (fromEnv) return fromEnv;
180
+ return readCredentialsFile()[gatewayUrl]?.token ?? null;
181
+ }
182
+ function saveToken(gatewayUrl, token, userId) {
183
+ const path = credentialsPath();
184
+ const all = readCredentialsFile();
185
+ all[gatewayUrl] = { token, ...userId ? { userId } : {} };
186
+ mkdirSync(dirname(path), { recursive: true });
187
+ writeFileSync(path, `${JSON.stringify(all, null, 2)}
188
+ `, { mode: 384 });
189
+ return path;
190
+ }
191
+ function forgetToken(gatewayUrl) {
192
+ const all = readCredentialsFile();
193
+ if (!(gatewayUrl in all)) return;
194
+ delete all[gatewayUrl];
195
+ writeFileSync(credentialsPath(), `${JSON.stringify(all, null, 2)}
196
+ `, { mode: 384 });
197
+ }
198
+ var PROJECT_CONFIG_FILE = "tallpond.json";
199
+ function readProjectConfig(cwd = process.cwd()) {
200
+ try {
201
+ const parsed = JSON.parse(readFileSync(resolve2(cwd, PROJECT_CONFIG_FILE), "utf8"));
202
+ return parsed.appId && parsed.slug ? parsed : null;
203
+ } catch {
204
+ return null;
205
+ }
206
+ }
207
+ function writeProjectConfig(config, cwd = process.cwd()) {
208
+ const path = resolve2(cwd, PROJECT_CONFIG_FILE);
209
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}
210
+ `);
211
+ return path;
212
+ }
213
+
214
+ // src/devapi.ts
215
+ import { readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
216
+ import { join as join3, relative } from "node:path";
217
+ var DevApiError = class extends Error {
218
+ constructor(status, code, detail) {
219
+ super(detail ? `${code}: ${detail}` : code);
220
+ this.status = status;
221
+ this.code = code;
222
+ this.detail = detail;
223
+ }
224
+ };
225
+ async function request(fetcher, gatewayUrl, token, method, path, body) {
226
+ const res = await fetcher(new URL(path, `${gatewayUrl}/`).toString(), {
227
+ method,
228
+ headers: {
229
+ ...token ? { Authorization: `Bearer ${token}` } : {},
230
+ ...body !== void 0 && !(body instanceof Uint8Array) ? { "content-type": "application/json" } : {}
231
+ },
232
+ ...body !== void 0 ? { body: body instanceof Uint8Array ? body : JSON.stringify(body) } : {}
233
+ });
234
+ const json = await res.json().catch(() => ({}));
235
+ if (!res.ok) {
236
+ throw new DevApiError(
237
+ res.status,
238
+ typeof json["error"] === "string" ? json["error"] : `http_${res.status}`,
239
+ typeof json["detail"] === "string" ? json["detail"] : void 0
240
+ );
241
+ }
242
+ return json;
243
+ }
244
+ async function startDeviceFlow(gatewayUrl, fetcher = fetch) {
245
+ return request(fetcher, gatewayUrl, null, "POST", "dev/cli/start");
246
+ }
247
+ async function pollForToken(gatewayUrl, start, deps = {}) {
248
+ const fetcher = deps.fetch ?? fetch;
249
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
250
+ const deadline = Date.now() + start.expires_in * 1e3;
251
+ while (Date.now() < deadline) {
252
+ const res = await fetcher(new URL("dev/cli/token", `${gatewayUrl}/`).toString(), {
253
+ method: "POST",
254
+ headers: { "content-type": "application/json" },
255
+ body: JSON.stringify({ device_code: start.device_code })
256
+ });
257
+ const json = await res.json().catch(() => ({}));
258
+ if (res.ok && json.token) return { token: json.token, userId: json.user_id ?? "" };
259
+ if (json.error === "access_denied") throw new DevApiError(res.status, "access_denied", "the request was denied in the browser");
260
+ if (json.error === "expired_token") break;
261
+ if (json.error !== "authorization_pending") {
262
+ throw new DevApiError(res.status, json.error ?? `http_${res.status}`);
263
+ }
264
+ await sleep(start.interval * 1e3);
265
+ }
266
+ throw new DevApiError(400, "expired_token", "sign-in timed out \u2014 run `tallpond login` again");
267
+ }
268
+ function whoami(gatewayUrl, token, fetcher = fetch) {
269
+ return request(fetcher, gatewayUrl, token, "GET", "dev/me");
270
+ }
271
+ function createApp(gatewayUrl, token, input, fetcher = fetch) {
272
+ return request(fetcher, gatewayUrl, token, "POST", "dev/apps", input);
273
+ }
274
+ async function listApps(gatewayUrl, token, fetcher = fetch) {
275
+ const res = await request(fetcher, gatewayUrl, token, "GET", "dev/apps");
276
+ return res.apps;
277
+ }
278
+ async function deploySchemaAsDev(gatewayUrl, token, appId, schema, fetcher = fetch) {
279
+ const res = await fetcher(new URL(`dev/apps/${appId}/deploy`, `${gatewayUrl}/`).toString(), {
280
+ method: "POST",
281
+ headers: { "content-type": "application/json", Authorization: `Bearer ${token}` },
282
+ body: JSON.stringify({ schema })
283
+ });
284
+ const body = await res.json().catch(() => null);
285
+ if (res.ok) {
286
+ return { ok: true, version: body?.version ?? "(unknown)", applied: body?.applied ?? 0, noop: body?.noop ?? false };
287
+ }
288
+ if (res.status === 409 && body?.error === "migration_blocked") {
289
+ return { ok: false, kind: "migration_blocked", changes: body.changes ?? [] };
290
+ }
291
+ return {
292
+ ok: false,
293
+ kind: "error",
294
+ status: res.status,
295
+ error: body?.error ?? `http_${res.status}`,
296
+ ...body?.detail ? { detail: body.detail } : {}
297
+ };
298
+ }
299
+ function deployFunctionsAsDev(gatewayUrl, token, appId, bundle, fetcher = fetch) {
300
+ return request(
301
+ fetcher,
302
+ gatewayUrl,
303
+ token,
304
+ "POST",
305
+ `dev/apps/${appId}/functions`,
306
+ bundle
307
+ );
308
+ }
309
+ function collectBundleFiles(dir) {
310
+ const out = [];
311
+ const walk = (d) => {
312
+ for (const entry of readdirSync(d, { withFileTypes: true })) {
313
+ const full = join3(d, entry.name);
314
+ if (entry.isDirectory()) walk(full);
315
+ else if (entry.isFile()) out.push(relative(dir, full).split("\\").join("/"));
316
+ }
317
+ };
318
+ walk(dir);
319
+ return out.sort();
320
+ }
321
+ async function uploadBundle(gatewayUrl, token, appId, dir, deps = {}) {
322
+ const fetcher = deps.fetch ?? fetch;
323
+ const files = collectBundleFiles(dir);
324
+ if (!files.includes("index.html")) {
325
+ throw new DevApiError(400, "invalid_bundle", `${dir} has no index.html \u2014 is it a built app?`);
326
+ }
327
+ const version = Date.now().toString(36);
328
+ for (const [i, path] of files.entries()) {
329
+ deps.onFile?.(path, i + 1, files.length);
330
+ const size = statSync(join3(dir, path)).size;
331
+ if (size > 25 * 1024 * 1024) {
332
+ throw new DevApiError(413, "file_too_large", `${path} is ${size} bytes (limit 25MB)`);
333
+ }
334
+ await request(fetcher, gatewayUrl, token, "PUT", `dev/apps/${appId}/bundle/${version}/${path}`, new Uint8Array(readFileSync2(join3(dir, path))));
335
+ }
336
+ const activated = await request(
337
+ fetcher,
338
+ gatewayUrl,
339
+ token,
340
+ "POST",
341
+ `dev/apps/${appId}/bundle/${version}/activate`,
342
+ { files }
343
+ );
344
+ return { version, files, url: activated.url };
345
+ }
346
+
347
+ // src/functions.ts
348
+ import { basename, extname as extname2, join as join4, resolve as resolve3 } from "node:path";
349
+ import { tmpdir as tmpdir2 } from "node:os";
350
+ import { existsSync, readdirSync as readdirSync2 } from "node:fs";
351
+ import { mkdtemp as mkdtemp2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
352
+ var FN_NAME = /^[a-zA-Z][a-zA-Z0-9_]*$/;
353
+ var FN_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".js", ".mjs"]);
354
+ function discoverFunctions(dir) {
355
+ const abs = resolve3(dir);
356
+ if (!existsSync(abs)) return [];
357
+ const entries = [];
358
+ for (const file of readdirSync2(abs, { withFileTypes: true })) {
359
+ if (!file.isFile()) continue;
360
+ const ext = extname2(file.name);
361
+ if (!FN_EXT.has(ext)) continue;
362
+ const name = basename(file.name, ext);
363
+ if (!FN_NAME.test(name)) {
364
+ throw new Error(`function file "${file.name}" \u2014 names must be identifiers (letters/digits/underscores)`);
365
+ }
366
+ entries.push({ name, file: join4(abs, file.name) });
367
+ }
368
+ entries.sort((a, b) => a.name.localeCompare(b.name));
369
+ const seen = /* @__PURE__ */ new Set();
370
+ for (const e of entries) {
371
+ if (seen.has(e.name)) throw new Error(`duplicate function name "${e.name}" in ${dir}`);
372
+ seen.add(e.name);
373
+ }
374
+ return entries;
375
+ }
376
+ var RUNTIME_SOURCE = `// tallpond function runtime (generated \u2014 do not edit)
377
+ function decodeInvocation(token) {
378
+ const payload = token.slice(4).split('.')[0].replace(/-/g, '+').replace(/_/g, '/')
379
+ return JSON.parse(atob(payload))
380
+ }
381
+
382
+ function makeCtx(env, token) {
383
+ const fetchImpl = env.TALLPOND_FETCH ?? fetch
384
+ const base = String(env.TALLPOND_GATEWAY_URL ?? '').replace(/\\/$/, '')
385
+ async function call(path, body) {
386
+ const res = await fetchImpl(base + path, {
387
+ method: 'POST',
388
+ headers: { 'content-type': 'application/json', authorization: 'Bearer ' + token },
389
+ body: JSON.stringify(body),
390
+ })
391
+ const data = await res.json().catch(() => null)
392
+ if (!res.ok) {
393
+ const err = new Error((data && (data.detail || data.error)) || ('gateway ' + res.status))
394
+ err.code = (data && data.error) || 'gateway_error'
395
+ throw err
396
+ }
397
+ return data
398
+ }
399
+ const inv = decodeInvocation(token)
400
+ return {
401
+ userId: inv.userId,
402
+ resourceId: inv.resourceId ?? null,
403
+ fn: inv.fn,
404
+ invocationId: inv.invocationId,
405
+ db: {
406
+ query: (request) => call('/v1/db/query', request),
407
+ batch: (operations) => call('/v1/db/batch', { operations }),
408
+ },
409
+ // Escape hatch for surfaces without a typed helper yet (ai, files, ...).
410
+ gateway: call,
411
+ }
412
+ }
413
+
414
+ export function createHandler(functions) {
415
+ return {
416
+ async fetch(req, env) {
417
+ if (req.method !== 'POST') return Response.json({ error: 'not_found' }, { status: 404 })
418
+ if (req.headers.get('x-tallpond-dispatch') !== env.TALLPOND_DISPATCH_SECRET) {
419
+ return Response.json({ error: 'forbidden' }, { status: 403 })
420
+ }
421
+ const token = req.headers.get('x-tallpond-invocation')
422
+ if (!token) return Response.json({ error: 'missing invocation token' }, { status: 401 })
423
+ const body = await req.json().catch(() => null)
424
+ const impl = body && functions[body.fn]
425
+ if (!impl) return Response.json({ error: 'not_found', detail: 'no such function' }, { status: 404 })
426
+ try {
427
+ const result = await impl(makeCtx(env, token), body.args ?? null)
428
+ return Response.json({ result: result === undefined ? null : result })
429
+ } catch (e) {
430
+ return Response.json(
431
+ { error: 'function_error', detail: String((e && e.message) || e) },
432
+ { status: 500 },
433
+ )
434
+ }
435
+ },
436
+ }
437
+ }
438
+ `;
439
+ async function bundleFunctions(entries) {
440
+ const dir = await mkdtemp2(join4(tmpdir2(), "tallpond-functions-"));
441
+ try {
442
+ await writeFile2(join4(dir, "runtime.mjs"), RUNTIME_SOURCE);
443
+ const imports = entries.map((e, i) => `import fn${i} from ${JSON.stringify(e.file)}`).join("\n");
444
+ const table = entries.map((e, i) => ` ${JSON.stringify(e.name)}: fn${i},`).join("\n");
445
+ const entrySource = `import { createHandler } from './runtime.mjs'
446
+ ${imports}
447
+ export default createHandler({
448
+ ${table}
449
+ })
450
+ `;
451
+ const entryPath = join4(dir, "entry.mjs");
452
+ await writeFile2(entryPath, entrySource);
453
+ const { build } = await import("esbuild");
454
+ const result = await build({
455
+ entryPoints: [entryPath],
456
+ bundle: true,
457
+ format: "esm",
458
+ // Workers are a browser-like runtime: no node builtins, fetch/crypto global.
459
+ platform: "browser",
460
+ conditions: ["workerd", "worker"],
461
+ write: false,
462
+ logLevel: "silent"
463
+ });
464
+ return {
465
+ script: result.outputFiles[0].text,
466
+ manifest: entries.map((e) => ({ name: e.name }))
467
+ };
468
+ } finally {
469
+ await rm2(dir, { recursive: true, force: true });
470
+ }
471
+ }
472
+
473
+ // src/cli.ts
474
+ var USAGE = `tallpond \u2014 build and ship apps on the tallpond platform
475
+
476
+ Usage:
477
+ tallpond login Sign in and save a developer token
478
+ tallpond logout Forget the saved token for this gateway
479
+ tallpond whoami Show the signed-in developer
480
+ tallpond apps create <name> Register a new app (writes ${PROJECT_CONFIG_FILE})
481
+ tallpond apps list List your apps
482
+ tallpond deploy [schemaFile] Deploy the app: schema + functions/ + built bundle
483
+ tallpond typegen [schemaFile] Generate row types (--out tallpond-env.d.ts)
484
+ tallpond help Show this help
485
+
486
+ Common flags:
487
+ --gateway-url <url> gateway base URL [env TALLPOND_GATEWAY_URL, default https://api.tallpond.com]
488
+
489
+ apps create flags:
490
+ --slug <slug> hosted subdomain (default: derived from the name)
491
+
492
+ deploy flags:
493
+ --dir <dir> built bundle to publish (default: ./dist when present)
494
+ --no-bundle deploy the schema only
495
+ schemaFile defaults to ./.tallpond.schema.ts (skipped if absent)
496
+
497
+ Platform/CI escape hatch (bypasses login; uses the shared deploy token):
498
+ --app-id <id> --deployment-id <id> --token <token>
499
+ [env TALLPOND_APP_ID, TALLPOND_DEPLOYMENT_ID, TALLPOND_DEPLOY_TOKEN]
500
+ `;
501
+ async function main(argv) {
502
+ const command = argv[0];
503
+ if (!command || command === "help" || command === "--help" || command === "-h") {
504
+ process.stdout.write(USAGE);
505
+ return command ? 0 : 1;
506
+ }
507
+ try {
508
+ if (command === "login") return await runLogin(argv.slice(1));
509
+ if (command === "logout") return runLogout(argv.slice(1));
510
+ if (command === "whoami") return await runWhoami(argv.slice(1));
511
+ if (command === "apps") return await runApps(argv.slice(1));
512
+ if (command === "create") return await runAppsCreate(argv.slice(1));
513
+ if (command === "deploy") return await runDeploy(argv.slice(1));
514
+ if (command === "typegen") return await runTypegen(argv.slice(1));
515
+ } catch (e) {
516
+ if (e instanceof DevApiError) {
517
+ if (e.status === 401) {
518
+ process.stderr.write(`error: not signed in (${e.message}) \u2014 run \`tallpond login\`
519
+ `);
520
+ } else {
521
+ process.stderr.write(`error: ${e.message}
522
+ `);
523
+ }
524
+ return 1;
525
+ }
526
+ throw e;
527
+ }
528
+ if (command === "dev") {
529
+ process.stderr.write(
530
+ "tallpond dev is not implemented yet. Point your app at the hosted gateway (or `bun run dev:gateway` from the platform repo) and use your framework dev server.\n"
531
+ );
532
+ return 1;
533
+ }
534
+ process.stderr.write(`unknown command "${command}"
535
+
536
+ ${USAGE}`);
537
+ return 1;
538
+ }
539
+ function gatewayFlag(args) {
540
+ return parseArgs({
541
+ args,
542
+ allowPositionals: true,
543
+ options: { "gateway-url": { type: "string" } }
544
+ });
545
+ }
546
+ async function runLogin(args) {
547
+ const { values } = gatewayFlag(args);
548
+ const gatewayUrl = resolveGatewayUrl(values["gateway-url"]);
549
+ const start = await startDeviceFlow(gatewayUrl);
550
+ process.stdout.write(
551
+ `Confirm this code in your browser: ${start.user_code}
552
+
553
+ ${start.verification_url}
554
+
555
+ Waiting for approval\u2026
556
+ `
557
+ );
558
+ openInBrowser(start.verification_url);
559
+ const { token, userId } = await pollForToken(gatewayUrl, start);
560
+ const path = saveToken(gatewayUrl, token, userId);
561
+ process.stdout.write(`Signed in as ${userId}. Token saved to ${path}
562
+ `);
563
+ return 0;
564
+ }
565
+ function runLogout(args) {
566
+ const { values } = gatewayFlag(args);
567
+ const gatewayUrl = resolveGatewayUrl(values["gateway-url"]);
568
+ forgetToken(gatewayUrl);
569
+ process.stdout.write(`Forgot saved token for ${gatewayUrl}
570
+ `);
571
+ return 0;
572
+ }
573
+ async function runWhoami(args) {
574
+ const { values } = gatewayFlag(args);
575
+ const gatewayUrl = resolveGatewayUrl(values["gateway-url"]);
576
+ const token = requireToken(gatewayUrl);
577
+ const me = await whoami(gatewayUrl, token);
578
+ process.stdout.write(`${me.user_id} (${gatewayUrl})
579
+ `);
580
+ return 0;
581
+ }
582
+ async function runApps(args) {
583
+ const sub = args[0];
584
+ if (sub === "create") return runAppsCreate(args.slice(1));
585
+ if (sub === "list") return runAppsList(args.slice(1));
586
+ process.stderr.write(`usage: tallpond apps <create|list>
587
+ `);
588
+ return 1;
589
+ }
590
+ async function runAppsCreate(args) {
591
+ const { values, positionals } = parseArgs({
592
+ args,
593
+ allowPositionals: true,
594
+ options: { "gateway-url": { type: "string" }, slug: { type: "string" } }
595
+ });
596
+ const name = positionals.join(" ").trim();
597
+ if (!name) {
598
+ process.stderr.write("usage: tallpond apps create <name> [--slug my-app]\n");
599
+ return 1;
600
+ }
601
+ const gatewayUrl = resolveGatewayUrl(values["gateway-url"]);
602
+ const token = requireToken(gatewayUrl);
603
+ const app = await createApp(gatewayUrl, token, {
604
+ name,
605
+ ...values["slug"] ? { slug: values["slug"] } : {}
606
+ });
607
+ const path = writeProjectConfig({
608
+ appId: app.app_id,
609
+ slug: app.slug,
610
+ name: app.name,
611
+ gatewayUrl,
612
+ url: app.url
613
+ });
614
+ process.stdout.write(
615
+ `Created app "${app.name}" (${app.app_id})
616
+ slug: ${app.slug}
617
+ ` + (app.url ? ` url: ${app.url}
618
+ ` : "") + ` wrote ${path}
619
+
620
+ Next: declare a schema in .tallpond.schema.ts, build your app, then \`tallpond deploy\`.
621
+ `
622
+ );
623
+ return 0;
624
+ }
625
+ async function runAppsList(args) {
626
+ const { values } = gatewayFlag(args);
627
+ const gatewayUrl = resolveGatewayUrl(values["gateway-url"]);
628
+ const token = requireToken(gatewayUrl);
629
+ const apps = await listApps(gatewayUrl, token);
630
+ if (apps.length === 0) {
631
+ process.stdout.write("No apps yet \u2014 `tallpond apps create <name>`.\n");
632
+ return 0;
633
+ }
634
+ for (const a of apps) {
635
+ process.stdout.write(
636
+ `${a.slug.padEnd(24)} ${a.name} ${a.url ?? ""}${a.live_version ? "" : " (not deployed)"}
637
+ `
638
+ );
639
+ }
640
+ return 0;
641
+ }
642
+ async function runDeploy(args) {
643
+ const { values, positionals } = parseArgs({
644
+ args,
645
+ allowPositionals: true,
646
+ options: {
647
+ "gateway-url": { type: "string" },
648
+ "app-id": { type: "string" },
649
+ "deployment-id": { type: "string" },
650
+ token: { type: "string" },
651
+ dir: { type: "string" },
652
+ "no-bundle": { type: "boolean" }
653
+ }
654
+ });
655
+ const platformToken = values.token ?? process.env["TALLPOND_DEPLOY_TOKEN"];
656
+ if (platformToken) return runPlatformDeploy(values, positionals, platformToken);
657
+ const project = readProjectConfig();
658
+ if (!project) {
659
+ process.stderr.write(
660
+ `error: no ${PROJECT_CONFIG_FILE} here \u2014 run \`tallpond apps create <name>\` first
661
+ (or pass the platform deploy token for the legacy path)
662
+ `
663
+ );
664
+ return 1;
665
+ }
666
+ const gatewayUrl = resolveGatewayUrl(values["gateway-url"] ?? project.gatewayUrl);
667
+ const token = requireToken(gatewayUrl);
668
+ const schemaPath = positionals[0] ?? "./.tallpond.schema.ts";
669
+ if (existsSync2(resolve4(process.cwd(), schemaPath))) {
670
+ const schema = await loadSchema(schemaPath);
671
+ const result = await deploySchemaAsDev(gatewayUrl, token, project.appId, schema);
672
+ if (!result.ok) {
673
+ if (result.kind === "migration_blocked") {
674
+ process.stderr.write(
675
+ `deploy blocked: the schema change needs an explicit migration.
676
+ ${renderBlockedChanges(result.changes)}
677
+ `
678
+ );
679
+ } else {
680
+ process.stderr.write(`schema deploy failed (${result.status}): ${result.error}${result.detail ? `: ${result.detail}` : ""}
681
+ `);
682
+ }
683
+ return 1;
684
+ }
685
+ process.stdout.write(
686
+ result.noop ? `schema: up to date (version ${result.version})
687
+ ` : `schema: applied version ${result.version} (${result.applied} statement${result.applied === 1 ? "" : "s"})
688
+ `
689
+ );
690
+ } else if (positionals[0]) {
691
+ process.stderr.write(`error: schema file ${schemaPath} not found
692
+ `);
693
+ return 1;
694
+ }
695
+ const fnEntries = discoverFunctions(resolve4(process.cwd(), "functions"));
696
+ if (fnEntries.length > 0) {
697
+ const bundle = await bundleFunctions(fnEntries);
698
+ const version2 = `f${Date.now().toString(36)}`;
699
+ try {
700
+ const res = await deployFunctionsAsDev(gatewayUrl, token, project.appId, { version: version2, ...bundle });
701
+ process.stdout.write(`functions: ${res.functions.join(", ")} (version ${res.version})
702
+ `);
703
+ } catch (e) {
704
+ if (e instanceof DevApiError) {
705
+ process.stderr.write(`functions deploy failed: ${e.message}
706
+ `);
707
+ return 1;
708
+ }
709
+ throw e;
710
+ }
711
+ }
712
+ const dir = values.dir ?? (existsSync2(resolve4(process.cwd(), "dist")) ? "dist" : void 0);
713
+ if (values["no-bundle"] || !dir) {
714
+ if (!values["no-bundle"]) {
715
+ process.stdout.write("no bundle directory found (looked for ./dist) \u2014 schema-only deploy\n");
716
+ }
717
+ return 0;
718
+ }
719
+ if (!existsSync2(resolve4(process.cwd(), dir))) {
720
+ process.stderr.write(`error: bundle directory ${dir} not found \u2014 build your app first
721
+ `);
722
+ return 1;
723
+ }
724
+ const { version, files, url } = await uploadBundle(gatewayUrl, token, project.appId, resolve4(process.cwd(), dir), {
725
+ onFile: (path, i, total) => process.stdout.write(` [${i}/${total}] ${path}
726
+ `)
727
+ });
728
+ process.stdout.write(`bundle: ${files.length} files live as version ${version}
729
+ `);
730
+ if (url) process.stdout.write(`
731
+ \u2192 ${url}
732
+ `);
733
+ return 0;
734
+ }
735
+ async function runPlatformDeploy(values, positionals, token) {
736
+ const config = resolveConfig(
737
+ {
738
+ ...typeof values["gateway-url"] === "string" ? { gatewayUrl: values["gateway-url"] } : {},
739
+ ...typeof values["app-id"] === "string" ? { appId: values["app-id"] } : {},
740
+ ...typeof values["deployment-id"] === "string" ? { deploymentId: values["deployment-id"] } : {},
741
+ token
742
+ },
743
+ process.env
744
+ );
745
+ if ("error" in config) {
746
+ process.stderr.write(`error: ${config.error}
747
+ `);
748
+ return 1;
749
+ }
750
+ const schema = await loadSchema(positionals[0] ?? "./.tallpond.schema.ts");
751
+ const result = await deploy(schema, config);
752
+ if (result.ok) {
753
+ process.stdout.write(
754
+ result.noop ? `up to date \u2014 deployment ${config.deploymentId} already at version ${result.version}
755
+ ` : `deployed ${config.deploymentId} \u2192 version ${result.version} (${result.applied} statement${result.applied === 1 ? "" : "s"} applied)
756
+ `
757
+ );
758
+ return 0;
759
+ }
760
+ if (result.kind === "migration_blocked") {
761
+ process.stderr.write(
762
+ `deploy blocked: the schema change needs an explicit migration.
763
+ ${renderBlockedChanges(result.changes)}
764
+ `
765
+ );
766
+ return 1;
767
+ }
768
+ const detail = result.detail ? `: ${result.detail}` : "";
769
+ if (result.status === 401) {
770
+ process.stderr.write(`deploy failed: unauthorized (check --token / TALLPOND_DEPLOY_TOKEN)${detail}
771
+ `);
772
+ } else {
773
+ process.stderr.write(`deploy failed (${result.status}): ${result.error}${detail}
774
+ `);
775
+ }
776
+ return 1;
777
+ }
778
+ async function runTypegen(args) {
779
+ const { values, positionals } = parseArgs({
780
+ args,
781
+ allowPositionals: true,
782
+ options: { out: { type: "string" } }
783
+ });
784
+ const schemaPath = positionals[0] ?? "./.tallpond.schema.ts";
785
+ let schema;
786
+ try {
787
+ schema = await loadSchema(schemaPath);
788
+ } catch (e) {
789
+ process.stderr.write(`error: ${e.message}
790
+ `);
791
+ return 1;
792
+ }
793
+ const outPath = resolve4(process.cwd(), values.out ?? "tallpond-env.d.ts");
794
+ await writeFile3(outPath, generateTypes(schema));
795
+ process.stdout.write(`wrote ${outPath}
796
+ `);
797
+ return 0;
798
+ }
799
+ function requireToken(gatewayUrl) {
800
+ const token = savedToken(gatewayUrl);
801
+ if (!token) throw new DevApiError(401, "not_signed_in", `no saved token for ${gatewayUrl}`);
802
+ return token;
803
+ }
804
+ function openInBrowser(url) {
805
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
806
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
807
+ try {
808
+ spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
809
+ } catch {
810
+ }
811
+ }
812
+ main(process.argv.slice(2)).then((code) => process.exit(code)).catch((e) => {
813
+ process.stderr.write(`unexpected error: ${e.stack ?? e}
814
+ `);
815
+ process.exit(1);
816
+ });
package/index.js ADDED
@@ -0,0 +1,362 @@
1
+ // src/deploy.ts
2
+ import { extname, join, resolve } from "node:path";
3
+ import { tmpdir } from "node:os";
4
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
5
+ import { pathToFileURL } from "node:url";
6
+ function resolveConfig(flags, env) {
7
+ const gatewayUrl = flags.gatewayUrl ?? env["TALLPOND_GATEWAY_URL"];
8
+ const appId = flags.appId ?? env["TALLPOND_APP_ID"];
9
+ const token = flags.token ?? env["TALLPOND_DEPLOY_TOKEN"];
10
+ const deploymentId = flags.deploymentId ?? env["TALLPOND_DEPLOYMENT_ID"] ?? generateDeploymentId();
11
+ if (!gatewayUrl) return { error: "missing gateway URL (--gateway-url or TALLPOND_GATEWAY_URL)" };
12
+ if (!appId) return { error: "missing app id (--app-id or TALLPOND_APP_ID)" };
13
+ if (!token) return { error: "missing deploy token (--token or TALLPOND_DEPLOY_TOKEN)" };
14
+ if (!/^[A-Za-z0-9]+$/.test(deploymentId)) {
15
+ return { error: `deploymentId "${deploymentId}" must be alphanumeric` };
16
+ }
17
+ return { gatewayUrl, appId, deploymentId, token };
18
+ }
19
+ function generateDeploymentId() {
20
+ const ts = Date.now().toString(36);
21
+ const rand = Math.random().toString(36).slice(2, 8);
22
+ return `d${ts}${rand}`;
23
+ }
24
+ var TS_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts"]);
25
+ async function loadSchema(schemaPath) {
26
+ const abs = resolve(process.cwd(), schemaPath);
27
+ let importUrl = pathToFileURL(abs).href;
28
+ let cleanup;
29
+ if (TS_EXT.has(extname(abs).toLowerCase())) {
30
+ const { build } = await import("esbuild");
31
+ const result = await build({
32
+ entryPoints: [abs],
33
+ bundle: true,
34
+ format: "esm",
35
+ platform: "node",
36
+ write: false,
37
+ logLevel: "silent"
38
+ });
39
+ const dir = await mkdtemp(join(tmpdir(), "tallpond-schema-"));
40
+ const out = join(dir, "schema.mjs");
41
+ await writeFile(out, result.outputFiles[0].text);
42
+ importUrl = pathToFileURL(out).href;
43
+ cleanup = () => rm(dir, { recursive: true, force: true });
44
+ }
45
+ let mod;
46
+ try {
47
+ mod = await import(importUrl);
48
+ } catch (e) {
49
+ throw new Error(`failed to load schema at ${abs}: ${e.message}`);
50
+ } finally {
51
+ await cleanup?.();
52
+ }
53
+ const schema = mod.default;
54
+ if (!schema || typeof schema !== "object" || !Array.isArray(schema.tables)) {
55
+ throw new Error(`${abs} must \`export default defineSchema({ ... })\``);
56
+ }
57
+ return schema;
58
+ }
59
+ async function deploy(schema, config, deps = { fetch }) {
60
+ const res = await deps.fetch(new URL("/deploy", config.gatewayUrl).toString(), {
61
+ method: "POST",
62
+ headers: {
63
+ "content-type": "application/json",
64
+ "x-tallpond-deploy-token": config.token
65
+ },
66
+ body: JSON.stringify({
67
+ deploymentId: config.deploymentId,
68
+ appId: config.appId,
69
+ schema
70
+ })
71
+ });
72
+ const body = await res.json().catch(() => null);
73
+ if (res.ok) {
74
+ return {
75
+ ok: true,
76
+ version: body?.version ?? "(unknown)",
77
+ applied: body?.applied ?? 0,
78
+ noop: body?.noop ?? false
79
+ };
80
+ }
81
+ if (res.status === 409 && body?.error === "migration_blocked") {
82
+ return { ok: false, kind: "migration_blocked", changes: body.changes ?? [] };
83
+ }
84
+ return {
85
+ ok: false,
86
+ kind: "error",
87
+ status: res.status,
88
+ error: body?.error ?? `http_${res.status}`,
89
+ ...body?.detail ? { detail: body.detail } : {}
90
+ };
91
+ }
92
+ function renderBlockedChanges(changes) {
93
+ if (changes.length === 0) return " (no change details returned)";
94
+ return changes.map((c) => {
95
+ const head = ` [${c.class}] ${c.description}`;
96
+ return c.sql ? `${head}
97
+ ${c.sql}` : head;
98
+ }).join("\n");
99
+ }
100
+
101
+ // src/typegen.ts
102
+ var TS_TYPE = {
103
+ text: "string",
104
+ uuid: "string",
105
+ integer: "number",
106
+ bigint: "number",
107
+ boolean: "boolean",
108
+ jsonb: "unknown",
109
+ timestamp: "string"
110
+ };
111
+ function pascal(name) {
112
+ return name.replace(/(^|[_-])([a-z])/g, (_, __, c) => c.toUpperCase());
113
+ }
114
+ function fieldType(col) {
115
+ const base = TS_TYPE[col.type];
116
+ return col.primaryKey || col.notNull ? base : `${base} | null`;
117
+ }
118
+ function rowInterface(table) {
119
+ const name = `${pascal(table.name)}Row`;
120
+ const hasId = table.columns.some((c) => c.name === "id");
121
+ const fields = table.columns.map((c) => ` ${c.name}: ${fieldType(c)}`);
122
+ const lines = hasId ? fields : [` id: string`, ...fields];
123
+ return { name, body: `export interface ${name} {
124
+ ${lines.join("\n")}
125
+ }` };
126
+ }
127
+ function generateTypes(schema) {
128
+ const tables = [...schema.tables];
129
+ for (const res of schema.resources) tables.push(...res.tables);
130
+ tables.sort((a, b) => a.name.localeCompare(b.name));
131
+ const interfaces = tables.map(rowInterface);
132
+ const registryLines = tables.map((t, i) => ` ${t.name}: ${interfaces[i].name}`);
133
+ return `// Generated by \`tallpond typegen\`. Do not edit by hand.
134
+ /* eslint-disable */
135
+ import type {} from '@tallpond/sdk'
136
+
137
+ ${interfaces.map((i) => i.body).join("\n\n")}
138
+
139
+ declare module '@tallpond/sdk' {
140
+ interface Register {
141
+ tables: {
142
+ ${registryLines.join("\n")}
143
+ }
144
+ }
145
+ }
146
+ `;
147
+ }
148
+
149
+ // src/devapi.ts
150
+ import { readdirSync, readFileSync, statSync } from "node:fs";
151
+ import { join as join2, relative } from "node:path";
152
+ var DevApiError = class extends Error {
153
+ constructor(status, code, detail) {
154
+ super(detail ? `${code}: ${detail}` : code);
155
+ this.status = status;
156
+ this.code = code;
157
+ this.detail = detail;
158
+ }
159
+ };
160
+ async function request(fetcher, gatewayUrl, token, method, path, body) {
161
+ const res = await fetcher(new URL(path, `${gatewayUrl}/`).toString(), {
162
+ method,
163
+ headers: {
164
+ ...token ? { Authorization: `Bearer ${token}` } : {},
165
+ ...body !== void 0 && !(body instanceof Uint8Array) ? { "content-type": "application/json" } : {}
166
+ },
167
+ ...body !== void 0 ? { body: body instanceof Uint8Array ? body : JSON.stringify(body) } : {}
168
+ });
169
+ const json = await res.json().catch(() => ({}));
170
+ if (!res.ok) {
171
+ throw new DevApiError(
172
+ res.status,
173
+ typeof json["error"] === "string" ? json["error"] : `http_${res.status}`,
174
+ typeof json["detail"] === "string" ? json["detail"] : void 0
175
+ );
176
+ }
177
+ return json;
178
+ }
179
+ async function startDeviceFlow(gatewayUrl, fetcher = fetch) {
180
+ return request(fetcher, gatewayUrl, null, "POST", "dev/cli/start");
181
+ }
182
+ async function pollForToken(gatewayUrl, start, deps = {}) {
183
+ const fetcher = deps.fetch ?? fetch;
184
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
185
+ const deadline = Date.now() + start.expires_in * 1e3;
186
+ while (Date.now() < deadline) {
187
+ const res = await fetcher(new URL("dev/cli/token", `${gatewayUrl}/`).toString(), {
188
+ method: "POST",
189
+ headers: { "content-type": "application/json" },
190
+ body: JSON.stringify({ device_code: start.device_code })
191
+ });
192
+ const json = await res.json().catch(() => ({}));
193
+ if (res.ok && json.token) return { token: json.token, userId: json.user_id ?? "" };
194
+ if (json.error === "access_denied") throw new DevApiError(res.status, "access_denied", "the request was denied in the browser");
195
+ if (json.error === "expired_token") break;
196
+ if (json.error !== "authorization_pending") {
197
+ throw new DevApiError(res.status, json.error ?? `http_${res.status}`);
198
+ }
199
+ await sleep(start.interval * 1e3);
200
+ }
201
+ throw new DevApiError(400, "expired_token", "sign-in timed out \u2014 run `tallpond login` again");
202
+ }
203
+ function whoami(gatewayUrl, token, fetcher = fetch) {
204
+ return request(fetcher, gatewayUrl, token, "GET", "dev/me");
205
+ }
206
+ function createApp(gatewayUrl, token, input, fetcher = fetch) {
207
+ return request(fetcher, gatewayUrl, token, "POST", "dev/apps", input);
208
+ }
209
+ async function listApps(gatewayUrl, token, fetcher = fetch) {
210
+ const res = await request(fetcher, gatewayUrl, token, "GET", "dev/apps");
211
+ return res.apps;
212
+ }
213
+ async function deploySchemaAsDev(gatewayUrl, token, appId, schema, fetcher = fetch) {
214
+ const res = await fetcher(new URL(`dev/apps/${appId}/deploy`, `${gatewayUrl}/`).toString(), {
215
+ method: "POST",
216
+ headers: { "content-type": "application/json", Authorization: `Bearer ${token}` },
217
+ body: JSON.stringify({ schema })
218
+ });
219
+ const body = await res.json().catch(() => null);
220
+ if (res.ok) {
221
+ return { ok: true, version: body?.version ?? "(unknown)", applied: body?.applied ?? 0, noop: body?.noop ?? false };
222
+ }
223
+ if (res.status === 409 && body?.error === "migration_blocked") {
224
+ return { ok: false, kind: "migration_blocked", changes: body.changes ?? [] };
225
+ }
226
+ return {
227
+ ok: false,
228
+ kind: "error",
229
+ status: res.status,
230
+ error: body?.error ?? `http_${res.status}`,
231
+ ...body?.detail ? { detail: body.detail } : {}
232
+ };
233
+ }
234
+ function deployFunctionsAsDev(gatewayUrl, token, appId, bundle, fetcher = fetch) {
235
+ return request(
236
+ fetcher,
237
+ gatewayUrl,
238
+ token,
239
+ "POST",
240
+ `dev/apps/${appId}/functions`,
241
+ bundle
242
+ );
243
+ }
244
+ function collectBundleFiles(dir) {
245
+ const out = [];
246
+ const walk = (d) => {
247
+ for (const entry of readdirSync(d, { withFileTypes: true })) {
248
+ const full = join2(d, entry.name);
249
+ if (entry.isDirectory()) walk(full);
250
+ else if (entry.isFile()) out.push(relative(dir, full).split("\\").join("/"));
251
+ }
252
+ };
253
+ walk(dir);
254
+ return out.sort();
255
+ }
256
+ async function uploadBundle(gatewayUrl, token, appId, dir, deps = {}) {
257
+ const fetcher = deps.fetch ?? fetch;
258
+ const files = collectBundleFiles(dir);
259
+ if (!files.includes("index.html")) {
260
+ throw new DevApiError(400, "invalid_bundle", `${dir} has no index.html \u2014 is it a built app?`);
261
+ }
262
+ const version = Date.now().toString(36);
263
+ for (const [i, path] of files.entries()) {
264
+ deps.onFile?.(path, i + 1, files.length);
265
+ const size = statSync(join2(dir, path)).size;
266
+ if (size > 25 * 1024 * 1024) {
267
+ throw new DevApiError(413, "file_too_large", `${path} is ${size} bytes (limit 25MB)`);
268
+ }
269
+ await request(fetcher, gatewayUrl, token, "PUT", `dev/apps/${appId}/bundle/${version}/${path}`, new Uint8Array(readFileSync(join2(dir, path))));
270
+ }
271
+ const activated = await request(
272
+ fetcher,
273
+ gatewayUrl,
274
+ token,
275
+ "POST",
276
+ `dev/apps/${appId}/bundle/${version}/activate`,
277
+ { files }
278
+ );
279
+ return { version, files, url: activated.url };
280
+ }
281
+
282
+ // src/config.ts
283
+ import { homedir } from "node:os";
284
+ import { dirname, join as join3, resolve as resolve2 } from "node:path";
285
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
286
+ var DEFAULT_GATEWAY_URL = "https://api.tallpond.com";
287
+ function resolveGatewayUrl(flag) {
288
+ return (flag ?? process.env["TALLPOND_GATEWAY_URL"] ?? DEFAULT_GATEWAY_URL).replace(/\/$/, "");
289
+ }
290
+ function credentialsPath() {
291
+ const base = process.env["XDG_CONFIG_HOME"] ?? join3(homedir(), ".config");
292
+ return join3(base, "tallpond", "credentials.json");
293
+ }
294
+ function readCredentialsFile() {
295
+ try {
296
+ return JSON.parse(readFileSync2(credentialsPath(), "utf8"));
297
+ } catch {
298
+ return {};
299
+ }
300
+ }
301
+ function savedToken(gatewayUrl) {
302
+ const fromEnv = process.env["TALLPOND_DEV_TOKEN"];
303
+ if (fromEnv) return fromEnv;
304
+ return readCredentialsFile()[gatewayUrl]?.token ?? null;
305
+ }
306
+ function saveToken(gatewayUrl, token, userId) {
307
+ const path = credentialsPath();
308
+ const all = readCredentialsFile();
309
+ all[gatewayUrl] = { token, ...userId ? { userId } : {} };
310
+ mkdirSync(dirname(path), { recursive: true });
311
+ writeFileSync(path, `${JSON.stringify(all, null, 2)}
312
+ `, { mode: 384 });
313
+ return path;
314
+ }
315
+ function forgetToken(gatewayUrl) {
316
+ const all = readCredentialsFile();
317
+ if (!(gatewayUrl in all)) return;
318
+ delete all[gatewayUrl];
319
+ writeFileSync(credentialsPath(), `${JSON.stringify(all, null, 2)}
320
+ `, { mode: 384 });
321
+ }
322
+ var PROJECT_CONFIG_FILE = "tallpond.json";
323
+ function readProjectConfig(cwd = process.cwd()) {
324
+ try {
325
+ const parsed = JSON.parse(readFileSync2(resolve2(cwd, PROJECT_CONFIG_FILE), "utf8"));
326
+ return parsed.appId && parsed.slug ? parsed : null;
327
+ } catch {
328
+ return null;
329
+ }
330
+ }
331
+ function writeProjectConfig(config, cwd = process.cwd()) {
332
+ const path = resolve2(cwd, PROJECT_CONFIG_FILE);
333
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}
334
+ `);
335
+ return path;
336
+ }
337
+ export {
338
+ DEFAULT_GATEWAY_URL,
339
+ DevApiError,
340
+ PROJECT_CONFIG_FILE,
341
+ collectBundleFiles,
342
+ createApp,
343
+ deploy,
344
+ deployFunctionsAsDev,
345
+ deploySchemaAsDev,
346
+ forgetToken,
347
+ generateDeploymentId,
348
+ generateTypes,
349
+ listApps,
350
+ loadSchema,
351
+ pollForToken,
352
+ readProjectConfig,
353
+ renderBlockedChanges,
354
+ resolveConfig,
355
+ resolveGatewayUrl,
356
+ saveToken,
357
+ savedToken,
358
+ startDeviceFlow,
359
+ uploadBundle,
360
+ whoami,
361
+ writeProjectConfig
362
+ };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@tallpond/cli",
3
+ "version": "0.0.1",
4
+ "description": "The tallpond developer CLI — sign in, register an app, and deploy schema + frontend with one command.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/CarsonJoe/tallpond.git",
9
+ "directory": "packages/cli"
10
+ },
11
+ "homepage": "https://tallpond.com",
12
+ "keywords": [
13
+ "tallpond",
14
+ "cli",
15
+ "deploy",
16
+ "oauth",
17
+ "backend"
18
+ ],
19
+ "type": "module",
20
+ "sideEffects": false,
21
+ "bin": {
22
+ "tallpond": "./cli.js"
23
+ },
24
+ "main": "./index.js",
25
+ "module": "./index.js",
26
+ "exports": {
27
+ ".": {
28
+ "import": "./index.js",
29
+ "default": "./index.js"
30
+ }
31
+ },
32
+ "dependencies": {
33
+ "@tallpond/schema": "^0.0.2",
34
+ "esbuild": "^0.24.0"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ }
39
+ }