@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.
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/cli.js +816 -0
- package/index.js +362 -0
- 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
|
+
}
|