@tallpond/cli 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +33 -0
  2. package/cli.js +54 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -49,6 +49,39 @@ apply automatically; destructive ones are blocked pending an explicit migration.
49
49
  The schema file is TypeScript: the CLI compiles it in-process (via esbuild) so it
50
50
  runs on any supported Node — no bun or global TS loader required.
51
51
 
52
+ ### Functions
53
+
54
+ If the project has a `functions/` directory, `tallpond deploy` also deploys it.
55
+ The convention: **each `.ts`/`.js` file directly under `functions/` is one
56
+ invokable function**, named after the file, and must `export default` its
57
+ handler:
58
+
59
+ ```ts
60
+ // functions/submitMove.ts — invoked as tallpond.functions.invoke('submitMove', args)
61
+ import type { FunctionContext } from '@tallpond/sdk' // type-only; nothing bundled
62
+
63
+ export default async (ctx: FunctionContext, args: { resourceId: string; move: unknown }) => {
64
+ await ctx.db.query({
65
+ scope: { kind: 'resource', resourceId: args.resourceId },
66
+ table: 'moves',
67
+ op: 'insert',
68
+ values: { move: args.move },
69
+ })
70
+ return { ok: true }
71
+ }
72
+ ```
73
+
74
+ The ctx surface is exactly `userId`, `resourceId`, `fn`, `invocationId`,
75
+ `db.query`/`db.batch` (raw `/v1/db` wire shape), and `gateway(path, body)` —
76
+ annotate with `FunctionContext` so the compiler enforces it. Full contract:
77
+ <https://tallpond.com/docs/functions/>.
78
+
79
+ File names must be identifiers (letters/digits/underscores). Shared helpers and
80
+ types belong in a subdirectory (e.g. `functions/lib/`) — subdirectories aren't
81
+ scanned, and handlers can import from them freely (everything is bundled). A
82
+ top-level file without a default export is skipped with a warning rather than
83
+ deployed.
84
+
52
85
  Full command reference: <https://tallpond.com> → docs → CLI.
53
86
 
54
87
  ## License
package/cli.js CHANGED
@@ -5,7 +5,7 @@ import { parseArgs } from "node:util";
5
5
  import { spawn } from "node:child_process";
6
6
  import { existsSync as existsSync2 } from "node:fs";
7
7
  import { writeFile as writeFile3 } from "node:fs/promises";
8
- import { resolve as resolve4 } from "node:path";
8
+ import { basename as basename2, resolve as resolve4 } from "node:path";
9
9
 
10
10
  // src/deploy.ts
11
11
  import { extname, join, resolve } from "node:path";
@@ -436,7 +436,33 @@ export function createHandler(functions) {
436
436
  }
437
437
  }
438
438
  `;
439
- async function bundleFunctions(entries) {
439
+ async function partitionByDefaultExport(entries) {
440
+ if (entries.length === 0) return { handlers: [], skipped: [] };
441
+ const { build } = await import("esbuild");
442
+ const result = await build({
443
+ entryPoints: entries.map((e) => e.file),
444
+ bundle: false,
445
+ format: "esm",
446
+ metafile: true,
447
+ write: false,
448
+ outdir: ".",
449
+ logLevel: "silent"
450
+ });
451
+ const withDefault = /* @__PURE__ */ new Set();
452
+ for (const out of Object.values(result.metafile.outputs)) {
453
+ if (out.entryPoint && out.exports.includes("default")) {
454
+ withDefault.add(resolve3(out.entryPoint));
455
+ }
456
+ }
457
+ const handlers = entries.filter((e) => withDefault.has(e.file));
458
+ const skipped = entries.filter((e) => !withDefault.has(e.file));
459
+ return { handlers, skipped };
460
+ }
461
+ async function bundleFunctions(all) {
462
+ const { handlers: entries, skipped } = await partitionByDefaultExport(all);
463
+ if (entries.length === 0) {
464
+ return { script: "", manifest: [], skipped };
465
+ }
440
466
  const dir = await mkdtemp2(join4(tmpdir2(), "tallpond-functions-"));
441
467
  try {
442
468
  await writeFile2(join4(dir, "runtime.mjs"), RUNTIME_SOURCE);
@@ -463,7 +489,8 @@ ${table}
463
489
  });
464
490
  return {
465
491
  script: result.outputFiles[0].text,
466
- manifest: entries.map((e) => ({ name: e.name }))
492
+ manifest: entries.map((e) => ({ name: e.name })),
493
+ skipped
467
494
  };
468
495
  } finally {
469
496
  await rm2(dir, { recursive: true, force: true });
@@ -694,19 +721,33 @@ ${renderBlockedChanges(result.changes)}
694
721
  }
695
722
  const fnEntries = discoverFunctions(resolve4(process.cwd(), "functions"));
696
723
  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})
724
+ const { skipped, ...bundle } = await bundleFunctions(fnEntries);
725
+ for (const s of skipped) {
726
+ process.stderr.write(
727
+ `functions: skipping ${basename2(s.file)} \u2014 no default export (each file under functions/ is one handler; shared code belongs in a subdirectory)
728
+ `
729
+ );
730
+ }
731
+ if (bundle.manifest.length > 0) {
732
+ const version2 = `f${Date.now().toString(36)}`;
733
+ try {
734
+ const res = await deployFunctionsAsDev(gatewayUrl, token, project.appId, { version: version2, ...bundle });
735
+ process.stdout.write(`functions: ${res.functions.join(", ")} (version ${res.version})
702
736
  `);
703
- } catch (e) {
704
- if (e instanceof DevApiError) {
705
- process.stderr.write(`functions deploy failed: ${e.message}
737
+ if (res.invocable === false) {
738
+ process.stderr.write(
739
+ `warning: functions deployed but NOT invocable \u2014 ${res.invocable_detail ?? "the gateway reports functions are disabled"}
740
+ `
741
+ );
742
+ }
743
+ } catch (e) {
744
+ if (e instanceof DevApiError) {
745
+ process.stderr.write(`functions deploy failed: ${e.message}
706
746
  `);
707
- return 1;
747
+ return 1;
748
+ }
749
+ throw e;
708
750
  }
709
- throw e;
710
751
  }
711
752
  }
712
753
  const dir = values.dir ?? (existsSync2(resolve4(process.cwd(), "dist")) ? "dist" : void 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tallpond/cli",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "The tallpond developer CLI — sign in, register an app, and deploy schema + frontend with one command.",
5
5
  "license": "MIT",
6
6
  "repository": {