@parlor/scaffold 0.0.0-stage → 0.1.0

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 (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -2
  3. package/dist/add/addon.d.ts +101 -0
  4. package/dist/add/addon.d.ts.map +1 -0
  5. package/dist/add/addon.js +6 -0
  6. package/dist/add/addon.js.map +1 -0
  7. package/dist/add/plan.d.ts +72 -0
  8. package/dist/add/plan.d.ts.map +1 -0
  9. package/dist/add/plan.js +237 -0
  10. package/dist/add/plan.js.map +1 -0
  11. package/dist/add/run.d.ts +92 -0
  12. package/dist/add/run.d.ts.map +1 -0
  13. package/dist/add/run.js +167 -0
  14. package/dist/add/run.js.map +1 -0
  15. package/dist/add/transaction.d.ts +67 -0
  16. package/dist/add/transaction.d.ts.map +1 -0
  17. package/dist/add/transaction.js +181 -0
  18. package/dist/add/transaction.js.map +1 -0
  19. package/dist/addons/agent.d.ts +61 -0
  20. package/dist/addons/agent.d.ts.map +1 -0
  21. package/dist/addons/agent.js +50 -0
  22. package/dist/addons/agent.js.map +1 -0
  23. package/dist/addons/auth.d.ts +106 -0
  24. package/dist/addons/auth.d.ts.map +1 -0
  25. package/dist/addons/auth.js +93 -0
  26. package/dist/addons/auth.js.map +1 -0
  27. package/dist/addons/index.d.ts +165 -0
  28. package/dist/addons/index.d.ts.map +1 -0
  29. package/dist/addons/index.js +8 -0
  30. package/dist/addons/index.js.map +1 -0
  31. package/dist/catalog.d.ts +3 -0
  32. package/dist/catalog.d.ts.map +1 -0
  33. package/dist/catalog.js +23 -0
  34. package/dist/catalog.js.map +1 -0
  35. package/dist/create.d.ts +45 -0
  36. package/dist/create.d.ts.map +1 -0
  37. package/dist/create.js +162 -0
  38. package/dist/create.js.map +1 -0
  39. package/dist/docs.d.ts +12 -0
  40. package/dist/docs.d.ts.map +1 -0
  41. package/dist/docs.js +66 -0
  42. package/dist/docs.js.map +1 -0
  43. package/dist/edit/inspect.d.ts +15 -0
  44. package/dist/edit/inspect.d.ts.map +1 -0
  45. package/dist/edit/inspect.js +73 -0
  46. package/dist/edit/inspect.js.map +1 -0
  47. package/dist/edit/ops.d.ts +92 -0
  48. package/dist/edit/ops.d.ts.map +1 -0
  49. package/dist/edit/ops.js +516 -0
  50. package/dist/edit/ops.js.map +1 -0
  51. package/dist/edit/source.d.ts +24 -0
  52. package/dist/edit/source.d.ts.map +1 -0
  53. package/dist/edit/source.js +36 -0
  54. package/dist/edit/source.js.map +1 -0
  55. package/dist/edit/syntax.d.ts +34 -0
  56. package/dist/edit/syntax.d.ts.map +1 -0
  57. package/dist/edit/syntax.js +114 -0
  58. package/dist/edit/syntax.js.map +1 -0
  59. package/dist/errors.d.ts +5 -0
  60. package/dist/errors.d.ts.map +1 -0
  61. package/dist/errors.js +5 -0
  62. package/dist/errors.js.map +1 -0
  63. package/dist/files.d.ts +15 -0
  64. package/dist/files.d.ts.map +1 -0
  65. package/dist/files.js +65 -0
  66. package/dist/files.js.map +1 -0
  67. package/dist/fixtures.d.ts +28 -0
  68. package/dist/fixtures.d.ts.map +1 -0
  69. package/dist/fixtures.js +66 -0
  70. package/dist/fixtures.js.map +1 -0
  71. package/dist/git.d.ts +22 -0
  72. package/dist/git.d.ts.map +1 -0
  73. package/dist/git.js +56 -0
  74. package/dist/git.js.map +1 -0
  75. package/dist/identity.d.ts +9 -0
  76. package/dist/identity.d.ts.map +1 -0
  77. package/dist/identity.js +23 -0
  78. package/dist/identity.js.map +1 -0
  79. package/dist/index.d.ts +9 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +8 -0
  82. package/dist/index.js.map +1 -0
  83. package/dist/package-manager.d.ts +49 -0
  84. package/dist/package-manager.d.ts.map +1 -0
  85. package/dist/package-manager.js +118 -0
  86. package/dist/package-manager.js.map +1 -0
  87. package/dist/version.d.ts +6 -0
  88. package/dist/version.d.ts.map +1 -0
  89. package/dist/version.js +9 -0
  90. package/dist/version.js.map +1 -0
  91. package/package.json +46 -3
  92. package/templates/auth/auth.ts +20 -0
  93. package/templates/auth/schema/auth.ts +1 -0
  94. package/templates/auth/src/auth/client.ts +3 -0
  95. package/templates/auth/src/auth/sign-in-gate.tsx +17 -0
  96. package/templates/auth/src/auth/sign-out-button.tsx +17 -0
  97. package/templates/auth/src/routes/link.tsx +92 -0
  98. package/templates/auth/src/routes/sign-in.tsx +117 -0
  99. package/templates/default/_gitignore +7 -0
  100. package/templates/default/actions/index.ts +58 -0
  101. package/templates/default/actions/todos.spec.ts +38 -0
  102. package/templates/default/index.html +13 -0
  103. package/templates/default/migrations/0000_init.sql +9 -0
  104. package/templates/default/migrations/meta/0000_snapshot.json +87 -0
  105. package/templates/default/migrations/meta/_journal.json +13 -0
  106. package/templates/default/package.json +44 -0
  107. package/templates/default/parlor.config.ts +12 -0
  108. package/templates/default/schema/index.ts +1 -0
  109. package/templates/default/schema/todos.ts +13 -0
  110. package/templates/default/server.ts +13 -0
  111. package/templates/default/src/agent/agent-button.tsx +120 -0
  112. package/templates/default/src/agent/agent-sidebar.tsx +821 -0
  113. package/templates/default/src/agent/agent.css +5 -0
  114. package/templates/default/src/agent/confirmation-card.tsx +42 -0
  115. package/templates/default/src/agent/sidebar-view.ts +36 -0
  116. package/templates/default/src/agent/views.ts +9 -0
  117. package/templates/default/src/app.css +1 -0
  118. package/templates/default/src/header-actions.tsx +9 -0
  119. package/templates/default/src/main.tsx +24 -0
  120. package/templates/default/src/parlor.d.ts +9 -0
  121. package/templates/default/src/routeTree.gen.ts +59 -0
  122. package/templates/default/src/routes/__root.tsx +28 -0
  123. package/templates/default/src/routes/index.tsx +136 -0
  124. package/templates/default/tsconfig.json +8 -0
  125. package/templates/default/vitest.config.ts +5 -0
@@ -0,0 +1,118 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { readFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ export const PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
6
+ /** Dependencies whose install scripts a Parlor project needs; pnpm 10+ blocks them unless approved. */
7
+ export const BUILD_SCRIPT_DEPENDENCIES = ["esbuild"];
8
+ /**
9
+ * `npm_config_user_agent` looks like `pnpm/11.21.0 npm/? node/v24.19.0 darwin arm64`; every
10
+ * package manager sets it for the scripts and `create` initializers it runs.
11
+ */
12
+ export function fromUserAgent(userAgent) {
13
+ const [name, version] = userAgent?.split(" ")[0]?.split("/") ?? [];
14
+ if (!isPackageManagerName(name))
15
+ return undefined;
16
+ return { name, version: version && version !== "?" ? version : undefined };
17
+ }
18
+ function isPackageManagerName(name) {
19
+ return PACKAGE_MANAGERS.includes(name);
20
+ }
21
+ const LOCKFILES = [
22
+ ["pnpm-lock.yaml", "pnpm"],
23
+ ["bun.lock", "bun"],
24
+ ["bun.lockb", "bun"],
25
+ ["yarn.lock", "yarn"],
26
+ ["package-lock.json", "npm"],
27
+ ];
28
+ function fromPackageManagerField(field) {
29
+ if (typeof field !== "string")
30
+ return undefined;
31
+ const [name, rest = ""] = field.split("@");
32
+ if (!isPackageManagerName(name))
33
+ return undefined;
34
+ return { name, version: rest.split("+")[0] || undefined };
35
+ }
36
+ export async function detectProjectPackageManager(rootDir, env = process.env) {
37
+ const invoking = fromUserAgent(env.npm_config_user_agent);
38
+ if (invoking)
39
+ return invoking;
40
+ const manifest = await readFile(join(rootDir, "package.json"), "utf8").then((text) => JSON.parse(text), () => undefined);
41
+ const declared = fromPackageManagerField(manifest?.packageManager);
42
+ if (declared)
43
+ return declared;
44
+ const lockfile = LOCKFILES.find(([file]) => existsSync(join(rootDir, file)));
45
+ return { name: lockfile?.[1] ?? "npm", version: undefined };
46
+ }
47
+ export function commandsFor({ name }) {
48
+ const addVerb = name === "npm" ? "install" : "add";
49
+ const devFlag = name === "npm" ? "--save-dev" : name === "bun" ? "--dev" : "-D";
50
+ return {
51
+ install: { bin: name, args: ["install"] },
52
+ add: (packages, { dev }) => ({
53
+ bin: name,
54
+ args: [addVerb, ...(dev ? [devFlag] : []), ...packages],
55
+ }),
56
+ run: (script) => name === "pnpm" || name === "yarn" ? `${name} ${script}` : `${name} run ${script}`,
57
+ exec: (bin, args) => formatCommand(execCommand({ name, version: undefined }, bin, args)),
58
+ };
59
+ }
60
+ /** The runnable form of `commandsFor(manager).exec`. */
61
+ export function execCommand({ name }, bin, args) {
62
+ const runner = { npm: "npx", pnpm: "pnpm", yarn: "yarn", bun: "bunx" }[name];
63
+ return { bin: runner, args: [bin, ...args] };
64
+ }
65
+ export function formatCommand(command) {
66
+ return [command.bin, ...command.args].join(" ");
67
+ }
68
+ /** Yarn must not use Plug'n'Play: the CLI resolves its tools from `node_modules`. */
69
+ export function packageManagerFiles(manager) {
70
+ if (manager.name === "pnpm") {
71
+ const builds = usesOnlyBuiltDependencies(manager.version)
72
+ ? `onlyBuiltDependencies:\n${BUILD_SCRIPT_DEPENDENCIES.map((name) => ` - ${name}\n`).join("")}`
73
+ : `allowBuilds:\n${BUILD_SCRIPT_DEPENDENCIES.map((name) => ` ${name}: true\n`).join("")}`;
74
+ return new Map([["pnpm-workspace.yaml", builds]]);
75
+ }
76
+ if (manager.name === "yarn")
77
+ return new Map([[".yarnrc.yml", "nodeLinker: node-modules\n"]]);
78
+ return new Map();
79
+ }
80
+ /** pnpm 10.0 to 10.25; an unknown version gets the current form, as create-next-app does. */
81
+ function usesOnlyBuiltDependencies(version) {
82
+ const [major, minor] = (version ?? "").split(".").map(Number);
83
+ return major === 10 && minor !== undefined && minor < 26;
84
+ }
85
+ export function packageManagerField(manager) {
86
+ if (manager.name === "npm" || !manager.version)
87
+ return undefined;
88
+ return `${manager.name}@${manager.version}`;
89
+ }
90
+ /** `NODE_ENV=development`, because pnpm skips devDependencies in production. */
91
+ export async function runInstall(rootDir, command, log) {
92
+ const child = spawn(command.bin, command.args, {
93
+ cwd: rootDir,
94
+ env: { ...process.env, NODE_ENV: "development" },
95
+ stdio: ["inherit", "inherit", "pipe"],
96
+ });
97
+ let stderr = "";
98
+ child.stderr.on("data", (chunk) => {
99
+ process.stderr.write(chunk);
100
+ stderr += chunk.toString("utf8");
101
+ });
102
+ const exitCode = await new Promise((resolve) => {
103
+ child.once("error", () => resolve(null));
104
+ child.once("close", resolve);
105
+ });
106
+ if (exitCode === 0)
107
+ return { kind: "installed" };
108
+ const ignored = IGNORED_BUILDS.exec(stderr);
109
+ if (ignored) {
110
+ const packages = (ignored[1] ?? "").split(",").map((entry) => entry.trim());
111
+ const hint = "Run pnpm approve-builds to allow them.";
112
+ log(`pnpm skipped the build scripts of ${packages.join(", ")}. ${hint}`);
113
+ return { kind: "ignored-builds", packages, hint };
114
+ }
115
+ return { kind: "failed", command: formatCommand(command), exitCode };
116
+ }
117
+ const IGNORED_BUILDS = /ERR_PNPM_IGNORED_BUILDS\]?\s*Ignored build scripts:\s*([^\n]+?)\.?\s*$/m;
118
+ //# sourceMappingURL=package-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package-manager.js","sourceRoot":"","sources":["../src/package-manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /** `@parlor/*` and `create-parlor` release in lockstep, so a project pins every framework package to this. */
2
+ export declare const FRAMEWORK_VERSION: string;
3
+ /** The same folder from `src/` in the repo and from `dist/` when published. */
4
+ export declare const TEMPLATES_DIR: string;
5
+ export declare const DOCS_URL = "https://cychien.github.io/parlor";
6
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAKA,8GAA8G;AAC9G,eAAO,MAAM,iBAAiB,EAAE,MAEvB,CAAC;AAEV,+EAA+E;AAC/E,eAAO,MAAM,aAAa,EAAE,MAE3B,CAAC;AAEF,eAAO,MAAM,QAAQ,qCAAqC,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { createRequire } from "node:module";
2
+ import { fileURLToPath } from "node:url";
3
+ const require = createRequire(import.meta.url);
4
+ /** `@parlor/*` and `create-parlor` release in lockstep, so a project pins every framework package to this. */
5
+ export const FRAMEWORK_VERSION = require("../package.json").version;
6
+ /** The same folder from `src/` in the repo and from `dist/` when published. */
7
+ export const TEMPLATES_DIR = fileURLToPath(new URL("../templates/", import.meta.url));
8
+ export const DOCS_URL = "https://cychien.github.io/parlor";
9
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;AAE/C,8GAA8G;AAC9G,MAAM,CAAC,MAAM,iBAAiB,GAC5B,OAAO,CAAC,iBAAiB,CAC1B,CAAC,OAAO,CAAC;AAEV,+EAA+E;AAC/E,MAAM,CAAC,MAAM,aAAa,GAAW,aAAa,CAChD,IAAI,GAAG,CAAC,eAAe,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAC1C,CAAC;AAEF,MAAM,CAAC,MAAM,QAAQ,GAAG,kCAAkC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "@parlor/scaffold",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "What create-parlor and parlor add share: the templates, the add-on model, the syntax-tree edit engine, package manager detection, install and git",
5
+ "homepage": "https://cychien.github.io/parlor",
6
+ "bugs": "https://github.com/cychien/parlor/issues",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/cychien/parlor.git",
11
+ "directory": "packages/scaffold"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "templates/default",
16
+ "templates/auth",
17
+ "!templates/*/node_modules",
18
+ "!templates/*/.parlor"
19
+ ],
20
+ "type": "module",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js"
25
+ },
26
+ "./package.json": "./package.json"
27
+ },
28
+ "dependencies": {
29
+ "@babel/parser": "^7.29.9"
30
+ },
31
+ "devDependencies": {
32
+ "@babel/types": "^7.29.8",
33
+ "@types/node": "^24.14.0",
34
+ "tsx": "^4.23.15",
35
+ "typescript": "^7.0.2",
36
+ "vitest": "^5.0.3",
37
+ "@parlor/config": "0.1.0"
38
+ },
39
+ "engines": {
40
+ "node": ">=24"
41
+ },
42
+ "scripts": {
43
+ "build": "tsc -p tsconfig.json",
44
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.spec.json --noEmit",
45
+ "test": "vitest run",
46
+ "catalog": "tsx scripts/write-catalog.ts",
47
+ "fixtures": "tsx scripts/write-fixtures.ts"
48
+ }
6
49
  }
@@ -0,0 +1,20 @@
1
+ import { betterAuthProvider, parlorAuth } from "@parlor/auth";
2
+ import type { DatabaseHandle } from "@parlor/data";
3
+ import { betterAuth } from "better-auth";
4
+ import { drizzleAdapter } from "better-auth/adapters/drizzle";
5
+
6
+ import * as schema from "./schema/index.js";
7
+
8
+ export function createAuth(database: DatabaseHandle) {
9
+ const options = parlorAuth({
10
+ appName: process.env.PARLOR_APP_NAME ?? "Parlor App",
11
+ signInPage: "/sign-in",
12
+ emailAndPassword: { enabled: true },
13
+ });
14
+ return betterAuthProvider({
15
+ instance: betterAuth({
16
+ ...options,
17
+ database: drizzleAdapter(database.db, { provider: "pg", schema }),
18
+ }),
19
+ });
20
+ }
@@ -0,0 +1 @@
1
+ export * from "@parlor/auth/schema";
@@ -0,0 +1,3 @@
1
+ import { createAuthClient } from "@parlor/auth/client";
2
+
3
+ export const auth = createAuthClient();
@@ -0,0 +1,17 @@
1
+ import { useSession } from "@parlor/client/react";
2
+ import { Navigate, Outlet, useLocation } from "@tanstack/react-router";
3
+ import type { ReactNode } from "react";
4
+
5
+ export const SIGN_IN_PATH = "/sign-in";
6
+
7
+ /** Wraps the root layout: the sign-in page renders bare, everything else needs a session. */
8
+ export function withSignIn(Layout: () => ReactNode): () => ReactNode {
9
+ return function SignedIn() {
10
+ const session = useSession();
11
+ const { pathname } = useLocation();
12
+ if (pathname === SIGN_IN_PATH) return <Outlet />;
13
+ if (session.status === "loading") return null;
14
+ if (session.status === "signed-out") return <Navigate to={SIGN_IN_PATH} replace />;
15
+ return <Layout />;
16
+ };
17
+ }
@@ -0,0 +1,17 @@
1
+ import { useSession } from "@parlor/client/react";
2
+
3
+ import { auth } from "./client.js";
4
+
5
+ export function SignOutButton() {
6
+ const session = useSession();
7
+ if (session.status !== "signed-in") return null;
8
+ return (
9
+ <button
10
+ type="button"
11
+ onClick={() => void auth.signOut()}
12
+ className="rounded-md px-3 py-1.5 text-sm text-zinc-600 hover:bg-zinc-100"
13
+ >
14
+ Sign out
15
+ </button>
16
+ );
17
+ }
@@ -0,0 +1,92 @@
1
+ import { useLinkDevice, useSession } from "@parlor/client/react";
2
+ import { createFileRoute, Link } from "@tanstack/react-router";
3
+ import { useEffect, useState } from "react";
4
+
5
+ export const Route = createFileRoute("/link")({
6
+ staticData: {
7
+ description:
8
+ "Link a new device to the user's agents: the page a linked phone opens from the new device's QR code.",
9
+ },
10
+ component: LinkDevice,
11
+ });
12
+
13
+ const BUTTON =
14
+ "rounded-md border border-zinc-300 px-3 py-1.5 text-sm enabled:hover:bg-zinc-100";
15
+ const PRIMARY = "rounded-md bg-zinc-900 px-3 py-1.5 text-sm font-medium text-white";
16
+
17
+ /** The device id and commitment the QR put in the fragment, which never reaches a server. */
18
+ function scanned():
19
+ | { readonly deviceId: string; readonly commitment: string }
20
+ | undefined {
21
+ const fragment = new URLSearchParams(window.location.hash.slice(1));
22
+ const deviceId = fragment.get("d");
23
+ const commitment = fragment.get("c");
24
+ return deviceId && commitment ? { deviceId, commitment } : undefined;
25
+ }
26
+
27
+ function LinkDevice() {
28
+ const { available, state, check, confirm } = useLinkDevice();
29
+ const session = useSession();
30
+ const [target] = useState(scanned);
31
+ useEffect(() => {
32
+ if (available && target) void check(target);
33
+ }, [available, target, check]);
34
+ const account = session.status === "signed-in" ? session.user.email : "your account";
35
+
36
+ return (
37
+ <section aria-label="Link a device" className="space-y-4">
38
+ <h1 className="text-2xl font-semibold tracking-tight">Link a device</h1>
39
+ {!target ? (
40
+ <p>This link is missing the new device's code. Scan its QR code again.</p>
41
+ ) : !available ? (
42
+ <p>Linking needs sign-in on this device.</p>
43
+ ) : state.step === "confirm" ? (
44
+ <>
45
+ <p className="text-lg">Link {state.device.name}?</p>
46
+ <p className="text-zinc-600">
47
+ It will be able to send requests to the agents on your computer, signed in as{" "}
48
+ {account}. Only link a device you have with you.
49
+ </p>
50
+ <div className="flex gap-2">
51
+ <Link to="/" className={BUTTON}>
52
+ Cancel
53
+ </Link>
54
+ <button type="button" onClick={() => void confirm()} className={PRIMARY}>
55
+ Link with Face ID or Touch ID
56
+ </button>
57
+ </div>
58
+ </>
59
+ ) : state.step === "linking" ? (
60
+ <p className="text-zinc-600">Confirm with Face ID or Touch ID…</p>
61
+ ) : state.step === "linked" ? (
62
+ <>
63
+ <p className="text-lg">{state.name} is linked</p>
64
+ <p className="text-zinc-600">It can use your agents now.</p>
65
+ <Link to="/" className={PRIMARY}>
66
+ Done
67
+ </Link>
68
+ </>
69
+ ) : state.step === "on-computer" ? (
70
+ <>
71
+ <p>
72
+ This browser cannot confirm with Face ID or Touch ID. On your computer, run:
73
+ </p>
74
+ <code className="block rounded bg-zinc-100 p-2 text-sm wrap-anywhere">
75
+ {state.command}
76
+ </code>
77
+ </>
78
+ ) : state.step === "error" ? (
79
+ <>
80
+ <p role="alert" className="text-red-600">
81
+ {state.message}
82
+ </p>
83
+ <button type="button" onClick={() => void check(target)} className={BUTTON}>
84
+ Try again
85
+ </button>
86
+ </>
87
+ ) : (
88
+ <p className="text-zinc-600">Checking the new device…</p>
89
+ )}
90
+ </section>
91
+ );
92
+ }
@@ -0,0 +1,117 @@
1
+ import { AuthClientError } from "@parlor/auth/client";
2
+ import { useSession } from "@parlor/client/react";
3
+ import { createFileRoute, Navigate, useNavigate } from "@tanstack/react-router";
4
+ import { type FormEvent, useId, useState } from "react";
5
+
6
+ import { auth } from "../auth/client.js";
7
+
8
+ export const Route = createFileRoute("/sign-in")({
9
+ staticData: {
10
+ description: "Sign in, or create an account, with an email and a password.",
11
+ },
12
+ component: SignIn,
13
+ });
14
+
15
+ /** An MCP client's authorization comes back here with a signed query to resume after sign-in. */
16
+ const resumesAuthorization = () => new URLSearchParams(window.location.search).has("sig");
17
+
18
+ const FIELD =
19
+ "w-full rounded-md border border-zinc-300 bg-white px-3 py-2 focus:border-zinc-900 focus:outline-none";
20
+
21
+ function SignIn() {
22
+ const session = useSession();
23
+ const navigate = useNavigate();
24
+ const errorId = useId();
25
+ const [signingUp, setSigningUp] = useState(false);
26
+ const [error, setError] = useState("");
27
+ const [busy, setBusy] = useState(false);
28
+
29
+ if (session.status === "anonymous") return <Navigate to="/" />;
30
+ if (session.status === "signed-in" && !resumesAuthorization())
31
+ return <Navigate to="/" />;
32
+
33
+ const submit = async (event: FormEvent<HTMLFormElement>) => {
34
+ event.preventDefault();
35
+ const form = new FormData(event.currentTarget);
36
+ const email = String(form.get("email")).trim();
37
+ const password = String(form.get("password"));
38
+ setBusy(true);
39
+ setError("");
40
+ try {
41
+ if (signingUp) {
42
+ await auth.signUp({ name: String(form.get("name")).trim(), email, password });
43
+ } else {
44
+ await auth.signIn({ email, password });
45
+ }
46
+ if (!resumesAuthorization()) await navigate({ to: "/" });
47
+ } catch (failure) {
48
+ setError(
49
+ failure instanceof AuthClientError ? failure.message : "Something went wrong.",
50
+ );
51
+ } finally {
52
+ setBusy(false);
53
+ }
54
+ };
55
+
56
+ return (
57
+ <main className="mx-auto mt-24 max-w-sm space-y-6 px-6">
58
+ <h1 className="text-2xl font-semibold tracking-tight">
59
+ {signingUp ? "Create your account" : "Sign in"}
60
+ </h1>
61
+ <form
62
+ onSubmit={(event) => void submit(event)}
63
+ aria-describedby={error ? errorId : undefined}
64
+ className="space-y-3"
65
+ >
66
+ {signingUp ? (
67
+ <input
68
+ name="name"
69
+ aria-label="Name"
70
+ placeholder="Name"
71
+ autoComplete="name"
72
+ required
73
+ className={FIELD}
74
+ />
75
+ ) : null}
76
+ <input
77
+ name="email"
78
+ type="email"
79
+ aria-label="Email"
80
+ placeholder="Email"
81
+ autoComplete="email"
82
+ required
83
+ className={FIELD}
84
+ />
85
+ <input
86
+ name="password"
87
+ type="password"
88
+ aria-label="Password"
89
+ placeholder="Password"
90
+ autoComplete={signingUp ? "new-password" : "current-password"}
91
+ minLength={8}
92
+ required
93
+ className={FIELD}
94
+ />
95
+ {error ? (
96
+ <p id={errorId} role="alert" className="text-sm text-red-600">
97
+ {error}
98
+ </p>
99
+ ) : null}
100
+ <button
101
+ type="submit"
102
+ disabled={busy}
103
+ className="w-full rounded-md bg-zinc-900 px-4 py-2 font-medium text-white disabled:opacity-50"
104
+ >
105
+ {signingUp ? "Create account" : "Sign in"}
106
+ </button>
107
+ </form>
108
+ <button
109
+ type="button"
110
+ onClick={() => setSigningUp(!signingUp)}
111
+ className="text-sm text-zinc-600 underline"
112
+ >
113
+ {signingUp ? "I have an account" : "Create an account"}
114
+ </button>
115
+ </main>
116
+ );
117
+ }
@@ -0,0 +1,7 @@
1
+ node_modules
2
+ .parlor
3
+ *.log
4
+ .env
5
+ .env.*
6
+ !.env.example
7
+ .DS_Store
@@ -0,0 +1,58 @@
1
+ import { ActionError, defineAction } from "@parlor/core";
2
+ import { createRegistry } from "@parlor/core/registry";
3
+ import { and, asc, eq } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ import { todos } from "../schema/index.js";
7
+
8
+ const mine = (id: string, scope: string) => and(eq(todos.id, id), eq(todos.scope, scope));
9
+
10
+ export const registry = createRegistry({
11
+ listTodos: defineAction({
12
+ description: "List the todos, oldest first.",
13
+ input: z.object({}),
14
+ reads: [todos],
15
+ run: (_, { db, scope }) =>
16
+ db.select().from(todos).where(eq(todos.scope, scope)).orderBy(asc(todos.createdAt)),
17
+ }),
18
+ addTodo: defineAction({
19
+ description: "Add a todo.",
20
+ input: z.object({
21
+ title: z.string().trim().min(1).describe("What needs to be done"),
22
+ }),
23
+ writes: [todos],
24
+ run: async ({ title }, { db, scope }) => {
25
+ const [todo] = await db
26
+ .insert(todos)
27
+ .values({ id: crypto.randomUUID(), scope, title })
28
+ .returning();
29
+ return todo;
30
+ },
31
+ }),
32
+ toggleTodo: defineAction({
33
+ description: "Mark a todo done, or not done again.",
34
+ input: z.object({ id: z.string() }),
35
+ writes: [todos],
36
+ run: async ({ id }, { db, scope }) => {
37
+ const [current] = await db.select().from(todos).where(mine(id, scope));
38
+ if (!current) throw new ActionError("not_found", `No todo ${id}`);
39
+ const [todo] = await db
40
+ .update(todos)
41
+ .set({ done: !current.done })
42
+ .where(mine(id, scope))
43
+ .returning();
44
+ return todo;
45
+ },
46
+ }),
47
+ deleteTodo: defineAction({
48
+ description: "Delete a todo.",
49
+ input: z.object({ id: z.string() }),
50
+ writes: [todos],
51
+ confirm: true,
52
+ run: async ({ id }, { db, scope }) => {
53
+ const [todo] = await db.delete(todos).where(mine(id, scope)).returning();
54
+ if (!todo) throw new ActionError("not_found", `No todo ${id}`);
55
+ return todo;
56
+ },
57
+ }),
58
+ });
@@ -0,0 +1,38 @@
1
+ import { fileURLToPath } from "node:url";
2
+
3
+ import { createActionClient } from "@parlor/client";
4
+ import type { ActionsOf } from "@parlor/core/registry";
5
+ import { createDatabase } from "@parlor/data";
6
+ import { createServer } from "@parlor/server";
7
+ import { afterEach, expect, test } from "vitest";
8
+
9
+ import { registry } from "./index.js";
10
+
11
+ const servers: { close(): Promise<void> }[] = [];
12
+
13
+ afterEach(async () => {
14
+ await Promise.all(servers.splice(0).map((server) => server.close()));
15
+ });
16
+
17
+ async function startApp() {
18
+ const database = await createDatabase({
19
+ dataDir: "memory://",
20
+ migrationsFolder: fileURLToPath(new URL("../migrations", import.meta.url)),
21
+ });
22
+ const server = createServer({ actions: registry, database });
23
+ servers.push(server);
24
+ return createActionClient<ActionsOf<typeof registry>>({
25
+ baseUrl: "http://app.test",
26
+ fetch: (input, init) => server.fetch(new Request(input, init)),
27
+ });
28
+ }
29
+
30
+ test("a todo added through its action is in the list, not done", async () => {
31
+ const app = await startApp();
32
+
33
+ await app.call("addTodo", { title: "Buy milk" });
34
+
35
+ expect(await app.call("listTodos", {})).toMatchObject([
36
+ { title: "Buy milk", done: false },
37
+ ]);
38
+ });
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="icon" href="data:," />
7
+ <link rel="stylesheet" href="/src/app.css" />
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,9 @@
1
+ CREATE TABLE "todos" (
2
+ "id" text PRIMARY KEY NOT NULL,
3
+ "scope" text DEFAULT 'default' NOT NULL,
4
+ "title" text NOT NULL,
5
+ "done" boolean DEFAULT false NOT NULL,
6
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
7
+ );
8
+ --> statement-breakpoint
9
+ CREATE INDEX "todos_scope_created_at_idx" ON "todos" USING btree ("scope","created_at");