create-feltkit 0.3.1 → 0.3.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.
- package/README.md +21 -3
- package/bin/create.js +30 -10
- package/package.json +3 -2
- package/scaffold.d.ts +7 -2
- package/scaffold.js +45 -9
- package/template/README.md +318 -0
- package/template/feltkit/_generated/api.ts +2 -1
- package/template/feltkit/app.ts +27 -10
- package/template/feltkit/inbox.ts +21 -0
- package/template/feltkit/projects.ts +19 -0
- package/template/feltkit/tasks.ts +11 -1
- package/template/index.html +4 -1
- package/template/package.json +6 -1
- package/template/public/apple-touch-icon.png +0 -0
- package/template/public/favicon.png +0 -0
- package/template/public/feltkit-mark.png +0 -0
- package/template/public/theme.js +7 -0
- package/template/src/App.tsx +31 -7
- package/template/src/styles.css +31 -31
- package/template/src/theme.tsx +58 -0
- package/template/tests/tasks.test.ts +42 -0
- package/template-managed/.dockerignore +7 -0
- package/template-managed/Dockerfile +24 -0
- package/template-managed/README.md +222 -0
- package/template-managed/_gitignore +7 -0
- package/template-managed/docker-compose.yml +17 -0
- package/template-managed/feltdb.contract.json +1102 -0
- package/template-managed/feltkit/_generated/api.ts +9 -0
- package/template-managed/feltkit/app.ts +40 -0
- package/template-managed/feltkit/items.ts +55 -0
- package/template-managed/feltkit/system.ts +14 -0
- package/template-managed/fly.toml +28 -0
- package/template-managed/package.json +35 -0
- package/template-managed/scripts/deploy-fly.mjs +54 -0
- package/template-managed/scripts/managed-env.mjs +103 -0
- package/template-managed/tests/config.test.ts +91 -0
- package/template-managed/tests/inventory.test.ts +69 -0
- package/template-managed/tests/managed.live.test.ts +75 -0
- package/template-managed/tsconfig.json +15 -0
- package/template/public/favicon.svg +0 -1
package/README.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/rkendel1/feltkit/main/docs/assets/feltkit-logo-dark.png">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/rkendel1/feltkit/main/docs/assets/feltkit-logo.png" alt="FeltKit" width="160">
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
1
8
|
# create-feltkit
|
|
2
9
|
|
|
3
10
|
```bash
|
|
@@ -7,8 +14,19 @@ npm install
|
|
|
7
14
|
npm run dev
|
|
8
15
|
```
|
|
9
16
|
|
|
10
|
-
Creates a FeltKit application
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
Creates a FeltKit application. Refuses to write into a non-empty directory. Has no dependencies.
|
|
18
|
+
|
|
19
|
+
| Starter | |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `tasks` (default) | A small task manager with projects, tasks, notes (AppPort Services Files), three roles, live queries, AppPort capabilities, and tests. State in local FeltDB. |
|
|
22
|
+
| `managed` | An inventory API whose state lives in **Managed FeltDB**: the FeltDB contract checked in, keys and contract publication through FeltDB's CLI, a Dockerfile, compose file and Fly.io configuration, and tests. |
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm create feltkit@latest inventory -- --template managed # or: -- --managed
|
|
26
|
+
cd inventory && npm install && npm test
|
|
27
|
+
npm run managed:setup
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The managed starter's README walks through setup, deployment and verification.
|
|
13
31
|
|
|
14
32
|
Documentation: https://github.com/rkendel1/concave#readme
|
package/bin/create.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// npm create feltkit@latest <dir>
|
|
2
|
+
// npm create feltkit@latest <dir> [-- --template managed]
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
-
import { scaffold, VERSION } from "../scaffold.js";
|
|
4
|
+
import { scaffold, TEMPLATES, VERSION } from "../scaffold.js";
|
|
5
5
|
|
|
6
6
|
const args = process.argv.slice(2);
|
|
7
7
|
const color = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
@@ -11,24 +11,44 @@ const green = (s) => (color ? `\u001b[32m${s}\u001b[39m` : s);
|
|
|
11
11
|
const red = (s) => (color ? `\u001b[31m${s}\u001b[39m` : s);
|
|
12
12
|
|
|
13
13
|
if (args.includes("--help") || args.includes("-h")) {
|
|
14
|
-
console.log(`npm create feltkit@latest <directory> [-- --name <app-name>]
|
|
14
|
+
console.log(`npm create feltkit@latest <directory> [-- --name <app-name>] [-- --template <starter>]
|
|
15
|
+
|
|
16
|
+
Creates a FeltKit application you can run immediately.
|
|
17
|
+
|
|
18
|
+
Starters (--template):
|
|
19
|
+
${Object.entries(TEMPLATES).map(([name, { description }]) => ` ${name.padEnd(9)}${description}`).join("\n")}
|
|
20
|
+
|
|
21
|
+
--managed is short for --template managed.`);
|
|
15
22
|
process.exit(0);
|
|
16
23
|
}
|
|
17
24
|
if (args.includes("--version")) {
|
|
18
25
|
console.log(VERSION);
|
|
19
26
|
process.exit(0);
|
|
20
27
|
}
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
28
|
+
const VALUE_FLAGS = ["--name", "--template"];
|
|
29
|
+
const valueOf = (flag) => {
|
|
30
|
+
const index = args.indexOf(flag);
|
|
31
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
32
|
+
};
|
|
33
|
+
const name = valueOf("--name");
|
|
34
|
+
const template = args.includes("--managed") ? "managed" : (valueOf("--template") ?? "tasks");
|
|
35
|
+
const dir = args.find((a, i) => !a.startsWith("-") && !VALUE_FLAGS.includes(args[i - 1])) ?? "my-feltkit-app";
|
|
24
36
|
|
|
25
37
|
try {
|
|
26
|
-
const result = scaffold({ target: resolve(process.cwd(), dir), ...(name ? { name } : {}) });
|
|
27
|
-
console.log(`${green("✓")} Created ${bold(result.name)} in ${result.dir}\n`);
|
|
38
|
+
const result = scaffold({ target: resolve(process.cwd(), dir), template, ...(name ? { name } : {}) });
|
|
39
|
+
console.log(`${green("✓")} Created ${bold(result.name)} in ${result.dir}${template === "tasks" ? "" : ` (${template} starter)`}\n`);
|
|
28
40
|
console.log(` cd ${dir}`);
|
|
29
41
|
console.log(" npm install");
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
if (template === "managed") {
|
|
43
|
+
console.log(" npm test # local FeltDB, no account needed");
|
|
44
|
+
console.log(" npm run managed:setup # Managed FeltDB: your account, a least-privilege key, the contract\n");
|
|
45
|
+
console.log(dim("State lives in Managed FeltDB once set up (README.md walks through setup, deploy and verification)."));
|
|
46
|
+
console.log(dim("Keys are written to .env.local and never committed; deploy with Docker or `npm run deploy:fly`."));
|
|
47
|
+
} else {
|
|
48
|
+
console.log(" npm run dev\n");
|
|
49
|
+
console.log(dim("The dev server prints your app URL and a dashboard link. State lives in .feltkit/development/state."));
|
|
50
|
+
console.log(dim("README.md covers running, testing, deploying, and every feltkit command."));
|
|
51
|
+
}
|
|
32
52
|
console.log(dim("Using a coding agent? `npx feltkit skills install` (after npm install) adds the FeltKit skills for Claude Code."));
|
|
33
53
|
} catch (error) {
|
|
34
54
|
console.error(`${red("✗")} ${error.message}`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-feltkit",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Create a FeltKit application: npm create feltkit@latest my-app",
|
|
3
|
+
"version": "0.3.3",
|
|
4
|
+
"description": "Create a FeltKit application: npm create feltkit@latest my-app (add -- --template managed for Managed FeltDB)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"scaffold.js",
|
|
27
27
|
"scaffold.d.ts",
|
|
28
28
|
"template",
|
|
29
|
+
"template-managed",
|
|
29
30
|
"README.md"
|
|
30
31
|
],
|
|
31
32
|
"scripts": {
|
package/scaffold.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type TemplateName = "tasks" | "managed";
|
|
1
2
|
export interface ScaffoldOptions {
|
|
2
3
|
/** Directory to create. Must not exist or be empty. */
|
|
3
4
|
target: string;
|
|
@@ -5,17 +6,21 @@ export interface ScaffoldOptions {
|
|
|
5
6
|
name?: string;
|
|
6
7
|
/** Version spec for feltkit; defaults to this package's version (released in lockstep). */
|
|
7
8
|
sdkSpec?: string;
|
|
9
|
+
/** Which starter: `tasks` (default, local FeltDB) or `managed` (Managed FeltDB). */
|
|
10
|
+
template?: TemplateName;
|
|
8
11
|
}
|
|
9
12
|
export interface ScaffoldResult {
|
|
10
13
|
dir: string;
|
|
11
14
|
name: string;
|
|
12
15
|
sdkSpec: string;
|
|
16
|
+
template: TemplateName;
|
|
13
17
|
}
|
|
14
18
|
export class ScaffoldError extends Error {
|
|
15
|
-
readonly code: "TARGET_NOT_EMPTY" | "TEMPLATE_MISSING" | "TEMPLATE_OUT_OF_SYNC";
|
|
19
|
+
readonly code: "TARGET_NOT_EMPTY" | "TEMPLATE_MISSING" | "TEMPLATE_OUT_OF_SYNC" | "TEMPLATE_UNKNOWN";
|
|
16
20
|
readonly hint?: string;
|
|
17
21
|
}
|
|
18
22
|
export declare const VERSION: string;
|
|
23
|
+
export declare const TEMPLATES: Readonly<Record<TemplateName, { dir: string; description: string }>>;
|
|
19
24
|
export declare function toAppName(input: string): string;
|
|
20
|
-
export declare function templateDir(): string;
|
|
25
|
+
export declare function templateDir(template?: TemplateName): string;
|
|
21
26
|
export declare function scaffold(options: ScaffoldOptions): ScaffoldResult;
|
package/scaffold.js
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
// Scaffold a FeltKit application from
|
|
1
|
+
// Scaffold a FeltKit application from a starter template. No dependencies, so `npm create` is fast.
|
|
2
2
|
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { basename, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
6
|
export const VERSION = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8")).version;
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* The starters. Both are the same FeltKit; they differ in where state lives and how it is deployed.
|
|
10
|
+
* tasks a task manager with a React frontend, AppPort Services (files, intelligence) and an agent;
|
|
11
|
+
* state in local FeltDB (`.feltkit/<env>/state`), or a FeltDB server you run.
|
|
12
|
+
* managed an inventory API whose state lives in Managed FeltDB: no AppPort Services (Managed FeltDB
|
|
13
|
+
* stores only what the application's contract declares), the contract checked in, keys from
|
|
14
|
+
* the FeltDB CLI, and a Dockerfile, compose file and Fly.io configuration.
|
|
15
|
+
*/
|
|
16
|
+
export const TEMPLATES = Object.freeze({
|
|
17
|
+
tasks: { dir: "template", description: "Task manager with a frontend; local FeltDB (default)" },
|
|
18
|
+
managed: { dir: "template-managed", description: "Inventory API on Managed FeltDB; contract, keys and deployment as code" },
|
|
19
|
+
});
|
|
20
|
+
|
|
8
21
|
export class ScaffoldError extends Error {
|
|
9
22
|
constructor(code, message, hint) {
|
|
10
23
|
super(message);
|
|
@@ -14,10 +27,14 @@ export class ScaffoldError extends Error {
|
|
|
14
27
|
}
|
|
15
28
|
}
|
|
16
29
|
|
|
17
|
-
export function templateDir() {
|
|
18
|
-
const
|
|
30
|
+
export function templateDir(template = "tasks") {
|
|
31
|
+
const entry = TEMPLATES[template];
|
|
32
|
+
if (!entry) {
|
|
33
|
+
throw new ScaffoldError("TEMPLATE_UNKNOWN", `There is no "${template}" starter.`, `Choose one of: ${Object.keys(TEMPLATES).join(", ")}.`);
|
|
34
|
+
}
|
|
35
|
+
const dir = fileURLToPath(new URL(`./${entry.dir}/`, import.meta.url));
|
|
19
36
|
if (!existsSync(join(dir, "package.json"))) {
|
|
20
|
-
throw new ScaffoldError("TEMPLATE_MISSING",
|
|
37
|
+
throw new ScaffoldError("TEMPLATE_MISSING", `The ${template} starter is missing from this installation of create-feltkit.`, "Reinstall it: npm create feltkit@latest");
|
|
21
38
|
}
|
|
22
39
|
return dir;
|
|
23
40
|
}
|
|
@@ -33,13 +50,13 @@ export function toAppName(input) {
|
|
|
33
50
|
return name || "my-app";
|
|
34
51
|
}
|
|
35
52
|
|
|
36
|
-
export function scaffold({ target, name, sdkSpec }) {
|
|
53
|
+
export function scaffold({ target, name, sdkSpec, template = "tasks" }) {
|
|
54
|
+
const source = templateDir(template);
|
|
37
55
|
if (existsSync(target) && readdirSync(target).filter((f) => f !== ".git" && f !== ".DS_Store").length > 0) {
|
|
38
56
|
throw new ScaffoldError("TARGET_NOT_EMPTY", `${target} already exists and is not empty. FeltKit will not write over existing files.`, "Choose a new directory name.");
|
|
39
57
|
}
|
|
40
58
|
const appName = toAppName(name ?? basename(target));
|
|
41
59
|
const spec = sdkSpec ?? process.env.FELTKIT_SDK_SPEC ?? `^${VERSION}`;
|
|
42
|
-
const source = templateDir();
|
|
43
60
|
mkdirSync(target, { recursive: true });
|
|
44
61
|
cpSync(source, target, { recursive: true });
|
|
45
62
|
if (existsSync(join(target, "_gitignore"))) renameSync(join(target, "_gitignore"), join(target, ".gitignore"));
|
|
@@ -52,11 +69,30 @@ export function scaffold({ target, name, sdkSpec }) {
|
|
|
52
69
|
pkg.dependencies["@feltdb/feltkit"] = spec;
|
|
53
70
|
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
54
71
|
|
|
72
|
+
// The starter's own name, wherever it names the application.
|
|
55
73
|
const appPath = join(target, "feltkit", "app.ts");
|
|
56
74
|
const app = readFileSync(appPath, "utf8");
|
|
57
|
-
|
|
75
|
+
const starterName = /feltkit\(\{\s*name: "([a-z][a-z0-9-]*)"/.exec(app)?.[1];
|
|
76
|
+
if (!starterName) {
|
|
58
77
|
throw new ScaffoldError("TEMPLATE_OUT_OF_SYNC", "The starter's feltkit/app.ts no longer declares its name; the template is out of sync.");
|
|
59
78
|
}
|
|
60
|
-
writeFileSync(appPath, app.replace(
|
|
61
|
-
|
|
79
|
+
writeFileSync(appPath, app.replace(`name: "${starterName}"`, `name: ${JSON.stringify(appName)}`));
|
|
80
|
+
// The default starter's README is titled with the starter's name and names its Docker image after it.
|
|
81
|
+
const readmePath = join(target, "README.md");
|
|
82
|
+
if (existsSync(readmePath)) {
|
|
83
|
+
const readme = readFileSync(readmePath, "utf8");
|
|
84
|
+
if (readme.startsWith(`# ${starterName}\n`)) {
|
|
85
|
+
writeFileSync(
|
|
86
|
+
readmePath,
|
|
87
|
+
readme
|
|
88
|
+
.replace(`# ${starterName}\n`, `# ${appName}\n`)
|
|
89
|
+
.replaceAll(`docker build -t ${starterName} `, `docker build -t ${appName} `)
|
|
90
|
+
.replaceAll(`-v ${starterName}-data:/data ${starterName}`, `-v ${appName}-data:/data ${appName}`)
|
|
91
|
+
.replaceAll(`--name ${starterName} --namespace ${starterName} `, `--name ${appName} --namespace ${appName} `),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const flyPath = join(target, "fly.toml");
|
|
96
|
+
if (existsSync(flyPath)) writeFileSync(flyPath, readFileSync(flyPath, "utf8").replace(/^app = ".*"$/m, `app = ${JSON.stringify(appName)}`));
|
|
97
|
+
return { dir: target, name: appName, sdkSpec: spec, template };
|
|
62
98
|
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# tasks
|
|
2
|
+
|
|
3
|
+
A FeltKit application: a small task manager with projects, tasks and notes, roles, live
|
|
4
|
+
queries, AppPort Services, an agent, and a development dashboard (Studio). Each
|
|
5
|
+
environment keeps all of its state in one FeltDB instance.
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
npm run dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`npm run dev` prints:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
App http://localhost:3000
|
|
18
|
+
Dashboard http://localhost:3000/_feltkit/#token=…
|
|
19
|
+
AppPort http://localhost:3000/appport
|
|
20
|
+
MCP http://localhost:3000/mcp
|
|
21
|
+
State …/.feltkit/development/state
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- **App**: you are signed in as a development user. **Ada** (`member`, `admin`) can do
|
|
25
|
+
everything; **Grace** (`viewer`) can only read. Switch between them from the app's header.
|
|
26
|
+
- **Dashboard** (Studio): your data, functions, capabilities, services, agent, models and
|
|
27
|
+
every operation that ran. Keep the link private: the token in it is this environment's
|
|
28
|
+
admin token.
|
|
29
|
+
- Edit anything in `feltkit/`. The backend reloads in place and keeps its state, sessions and
|
|
30
|
+
history. The frontend in `src/` hot-reloads.
|
|
31
|
+
|
|
32
|
+
Development state lives in `.feltkit/development/` (ignored by git). Delete that folder to
|
|
33
|
+
start over.
|
|
34
|
+
|
|
35
|
+
## What's in the project
|
|
36
|
+
|
|
37
|
+
| Path | What it is |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `feltkit/app.ts` | The application: its name, roles and permissions, dev users, enabled services, collections |
|
|
40
|
+
| `feltkit/projects.ts`, `feltkit/tasks.ts` | Functions: queries (read) and mutations (write). A function is `<module>:<export>`, e.g. `tasks:create` |
|
|
41
|
+
| `feltkit/_generated/api.ts` | Typed references to every function. Regenerated by `feltkit dev` and `feltkit codegen`; don't edit it |
|
|
42
|
+
| `src/` | The React frontend. Colors are tokens in `src/styles.css`, each `light-dark(light, dark)`; the top bar's switch (`src/theme.tsx`) picks light, dark, or the system setting, remembered per browser and applied before first paint by `public/theme.js` |
|
|
43
|
+
| `tests/` | Tests, each against a fresh FeltDB instance |
|
|
44
|
+
| `.feltkit/` | Local state, keys and builds, per environment. Never commit it |
|
|
45
|
+
|
|
46
|
+
## How it works
|
|
47
|
+
|
|
48
|
+
**Data.** Collections are declared in `feltkit/app.ts` with a schema; every write is
|
|
49
|
+
validated against it.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
export const tasks = app.collection("tasks", {
|
|
53
|
+
projectId: s.string(),
|
|
54
|
+
title: s.string({ minLength: 1, maxLength: 200 }),
|
|
55
|
+
done: s.boolean(),
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Functions.** A query reads, a mutation writes. Writes in one mutation commit together, or
|
|
60
|
+
not at all. `access` is the permission the caller needs; `capability` publishes the
|
|
61
|
+
function through AppPort, so agents can use it too.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
export const create = app.mutation({
|
|
65
|
+
access: "tasks.write",
|
|
66
|
+
args: { projectId: s.string(), title: s.string({ minLength: 1 }) },
|
|
67
|
+
capability: { name: "tasks.create", description: "Add a task to a project." },
|
|
68
|
+
handler: async (ctx, { projectId, title }) =>
|
|
69
|
+
ctx.db.insert(tasks, { projectId, title, done: false, createdAt: Date.now(), createdBy: ctx.auth.requirePrincipal().id }),
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Roles.** `roles` in `feltkit/app.ts` grant permissions; `"tasks.*"` grants every
|
|
74
|
+
`tasks.<x>`. The server checks them on every call, whatever the frontend shows.
|
|
75
|
+
|
|
76
|
+
**Frontend.** Queries are live: they update when the data changes.
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { useMutation, useQueryState } from "@feltdb/feltkit/react";
|
|
80
|
+
import { api } from "../feltkit/_generated/api";
|
|
81
|
+
|
|
82
|
+
const tasks = useQueryState(api.tasks.list, { projectId });
|
|
83
|
+
const create = useMutation(api.tasks.create);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Services
|
|
87
|
+
|
|
88
|
+
Every AppPort Service is enabled in `feltkit/app.ts` and stored in the same FeltDB instance:
|
|
89
|
+
Files, Notifications, Configuration, API keys, Webhooks, Jobs, Schedules and Intelligence.
|
|
90
|
+
Configure each one in **Studio → Services**: every service has its own screen, and each
|
|
91
|
+
operation there is authorized as the dev user you choose with **Act as**. From a function:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
await ctx.services.files.upload({ name: "notes.txt", content: text });
|
|
95
|
+
await ctx.services.notifications.send({ recipient: "grace", type: "task.completed", title: "Done" });
|
|
96
|
+
await ctx.services.webhooks?.emit("task.created", { id }); // mutations only
|
|
97
|
+
await ctx.services.jobs?.enqueue("projects.clear-completed", {}); // mutations only
|
|
98
|
+
const greeting = await ctx.services.configuration.get("GREETING");
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The starter already uses them: creating and completing a task emits `task.created` and
|
|
102
|
+
`task.completed` to any registered webhook endpoint; completing someone else's task notifies
|
|
103
|
+
its creator; and the job `projects.clear-completed` runs `projects:clearCompleted`.
|
|
104
|
+
|
|
105
|
+
Declare what your app sends in `feltkit/app.ts`, so Studio's forms offer it in dropdowns:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
services: {
|
|
109
|
+
notifications: { types: ["task.completed"] },
|
|
110
|
+
webhooks: { events: ["task.created", "task.completed"] },
|
|
111
|
+
jobs: { handlers: { "projects.clear-completed": "projects:clearCompleted" } },
|
|
112
|
+
// …
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
A job type runs the mutation you name, as the user who queued it, with retries. To turn a
|
|
117
|
+
service off, remove it from `services`.
|
|
118
|
+
|
|
119
|
+
## Connecting an API
|
|
120
|
+
|
|
121
|
+
**Studio → Integrations → Connect an API** reads an OpenAPI document (JSON) or lets you
|
|
122
|
+
describe the operations yourself, then writes `feltkit/<name>.ts` and grants `<name>.*` to the
|
|
123
|
+
roles you choose. Each operation becomes a capability your functions, the agent and other apps
|
|
124
|
+
can call (`<name>.<operation>`), with its permission, and every call recorded. The API key is
|
|
125
|
+
stored in FeltKit's custody; in production set `<NAME>_URL` and `FELTKIT_CREDENTIAL_<NAME>`.
|
|
126
|
+
Then narrow each operation's `output` schema in that file to what the API returns.
|
|
127
|
+
|
|
128
|
+
## Choosing a model for the agent
|
|
129
|
+
|
|
130
|
+
Open **Studio → Models**, pick a provider and model, type its API key, then **Save and use**
|
|
131
|
+
and **Test**. **Save for later** keeps more models (other providers too) under **Saved
|
|
132
|
+
models**, each with its own key; **Use** switches the agent to one. Or from a terminal:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npx feltkit model set --provider anthropic --model <model> --api-key-stdin
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The key is stored encrypted outside FeltDB (`.feltkit/development/credentials.vault`) and is
|
|
139
|
+
never shown again. Never put a key in `feltkit/`, `src/`, tests or a committed file.
|
|
140
|
+
|
|
141
|
+
## Testing
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npm test
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
runs `tests/**/*.test.ts`. Each `createTestApp()` gets its own temporary FeltDB instance,
|
|
148
|
+
and `t.as("ada")` calls functions as a dev user:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
const t = await createTestApp();
|
|
152
|
+
const projectId = await t.as("ada").mutation(api.projects.create, { name: "Launch" });
|
|
153
|
+
await assert.rejects(t.as("grace").mutation(api.tasks.create, { projectId, title: "x" }), { code: "FORBIDDEN" });
|
|
154
|
+
await t.close();
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Deploying
|
|
158
|
+
|
|
159
|
+
FeltKit doesn't host your app; it builds what you run, for the way you want to run it:
|
|
160
|
+
|
|
161
|
+
| Target | What runs | Data |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| `container` (default) | Everything in one container | FeltDB state on a volume |
|
|
164
|
+
| `hosted` | A separate app container | Managed FeltDB, hosted for you, from the artifact's contract (it declares AppPort Services' collections too) |
|
|
165
|
+
| `self-hosted` | A separate app container | A FeltDB server you operate (`feltdb-server`), in this app's namespace |
|
|
166
|
+
|
|
167
|
+
Each target's `DEPLOY.md` spells out its contract: what to set, where state lives and how to
|
|
168
|
+
recover it, what stops startup, and how to upgrade and roll back. A target never changes where
|
|
169
|
+
state is stored: the container target refuses `FELTDB_URL`, and the others refuse to start
|
|
170
|
+
without their FeltDB settings.
|
|
171
|
+
|
|
172
|
+
1. **Check what production needs.**
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
npx feltkit readiness --env production
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
2. **Build and verify.** It asks which target (or pass `--target container`, `hosted` or `self-hosted`):
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
npm run deploy
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
This builds the backend and frontend, boots the production build against throwaway state
|
|
185
|
+
to prove it serves, and writes `.feltkit/deploy/` with a `Dockerfile`, `docker-compose.yml`,
|
|
186
|
+
`fly.toml`, `.env.example` (exactly the variables it needs) and `DEPLOY.md`. Each deploy
|
|
187
|
+
replaces it, keeping production state a previous run kept inside it. It prints the commands
|
|
188
|
+
to run next.
|
|
189
|
+
|
|
190
|
+
3. **Run it.** Everything in one container:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
docker compose -f .feltkit/deploy/docker-compose.yml up --build
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Or with Node, from the artifact (data in its `.feltkit/production`):
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
cd .feltkit/deploy
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
npm ci --omit=dev
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
npm start
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
With Managed FeltDB (`--target hosted`), create the app and its key from the artifact's
|
|
211
|
+
contract (`npx feltdb init-managed --contract .feltkit/deploy/feltdb.contract.json
|
|
212
|
+
--runtime-scopes minimal`), put `FELTDB_URL`, `FELTDB_APPLICATION_ID` and `FELTDB_TOKEN` in
|
|
213
|
+
`.feltkit/deploy/.env`, then run the same `docker compose` command. With your own FeltDB
|
|
214
|
+
server (`--target self-hosted`), create a key for the app's namespace (`feltdb-server keys
|
|
215
|
+
create --keys <keys.json> --name tasks --namespace tasks --scope state:read,state:write`) and
|
|
216
|
+
put `FELTDB_URL` and `FELTDB_TOKEN` in `.env`. Either container refuses to start without its
|
|
217
|
+
settings, and checks the database answers, so it never keeps data on its own disk. For
|
|
218
|
+
Fly.io, plain Docker, or other hosts, follow `.feltkit/deploy/DEPLOY.md`.
|
|
219
|
+
|
|
220
|
+
Open <http://localhost:3000>.
|
|
221
|
+
|
|
222
|
+
4. **Sign in.** Production has no development sign-in (Ada and Grace are for development).
|
|
223
|
+
Until you connect your own login, create a session token, *with the app stopped* (only one
|
|
224
|
+
process may open the state), and paste it into the app's sign-in screen:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
npx feltkit session create --env production --type user --id you --role member
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Run it in `.feltkit/deploy` for the Node run (or anywhere with the target's `FELTDB_*`
|
|
231
|
+
variables set: then the app may keep running, since its sessions are in FeltDB). For the one-container Docker run, use a one-off container on the same
|
|
232
|
+
volume while the app's container is stopped:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
docker run --rm -v tasks-data:/data tasks node node_modules/@feltdb/feltkit/bin/feltkit.js session create --env production --type user --id you --role member
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
For real users, add `auth.authenticate` to `feltkit({ ... })` to verify requests with your
|
|
239
|
+
identity provider.
|
|
240
|
+
|
|
241
|
+
**Production settings** (from your secret manager, never in the repository). With one
|
|
242
|
+
container, none is required:
|
|
243
|
+
|
|
244
|
+
| Variable | What it's for |
|
|
245
|
+
|---|---|
|
|
246
|
+
| `FELTDB_URL`, `FELTDB_APPLICATION_ID`, `FELTDB_TOKEN` | Managed FeltDB (`--target hosted`) |
|
|
247
|
+
| `FELTDB_URL`, `FELTDB_TOKEN` | Your FeltDB server (`--target self-hosted`) |
|
|
248
|
+
| `FELTKIT_WEBHOOK_PRIVATE_NETWORKS` | `1` lets webhooks reach receivers on private networks (refused by default outside development) |
|
|
249
|
+
| `FELTKIT_API_KEY_VERIFIER_KEY` | Hosted and self-hosted: 32+ random bytes (`openssl rand -base64 32`). In one container it is generated once and kept beside the state on the volume |
|
|
250
|
+
| `FELTKIT_CREDENTIAL_<ID>` | Credential values, e.g. `FELTKIT_CREDENTIAL_ANTHROPIC` for `credential-ref:anthropic`. Keys you entered in Studio are development-only and are not deployed |
|
|
251
|
+
| `FELTKIT_STATE_DIR` | Where local state lives (the container image uses `/data/state`, on the volume) |
|
|
252
|
+
| `FELTKIT_OPERATOR_KEY` | Optional: signs operator sign-ins for production Studio |
|
|
253
|
+
| `PORT` | The port to listen on (default 3000) |
|
|
254
|
+
|
|
255
|
+
**Production Studio.** Studio isn't served in production unless `FELTKIT_DASHBOARD=1`. Then
|
|
256
|
+
operators sign in with a one-time grant you create on the host
|
|
257
|
+
(`npx feltkit operator grant --id <id> --role <role> --env production`). Production Studio
|
|
258
|
+
is for operating the app; data edits and service configuration stay development tools.
|
|
259
|
+
To move non-secret configuration between environments, use
|
|
260
|
+
`npx feltkit config export` and `npx feltkit config import <file> --env production --dry-run`.
|
|
261
|
+
|
|
262
|
+
To deploy on Managed FeltDB, where no host keeps state, start from
|
|
263
|
+
`npm create feltkit@latest <dir> -- --template managed`; its README walks through setup.
|
|
264
|
+
|
|
265
|
+
## Commands
|
|
266
|
+
|
|
267
|
+
npm scripts:
|
|
268
|
+
|
|
269
|
+
| Script | Runs |
|
|
270
|
+
|---|---|
|
|
271
|
+
| `npm run dev` | `feltkit dev`: backend, frontend, Studio and hot reload |
|
|
272
|
+
| `npm test` | `feltkit test`: the tests, against isolated test environments |
|
|
273
|
+
| `npm run build` | `feltkit build`: bundle the backend, build the frontend, verify the bundle |
|
|
274
|
+
| `npm start` | `feltkit start`: run the production build |
|
|
275
|
+
| `npm run deploy` | `feltkit deploy`: build a verified deployment artifact |
|
|
276
|
+
| `npm run inspect` | `feltkit inspect`: status, state location, functions, services, recent activity |
|
|
277
|
+
| `npm run typecheck` | `tsc --noEmit` |
|
|
278
|
+
|
|
279
|
+
Every `feltkit` command (run as `npx feltkit <command>`; `--help` on any of them for its options):
|
|
280
|
+
|
|
281
|
+
| Command | What it does |
|
|
282
|
+
|---|---|
|
|
283
|
+
| `dev [--port 3000]` | Development environment: backend, frontend, Studio, hot reload |
|
|
284
|
+
| `build` | Bundle the backend, build the frontend, load the bundle to verify it |
|
|
285
|
+
| `start [--port $PORT]` | Run the production build (the `production` environment) |
|
|
286
|
+
| `test [files…]` | Run `tests/**/*.test.ts` against isolated test environments |
|
|
287
|
+
| `deploy [--out <dir>]` | Build, verify the production build boots, write a Node/Docker artifact |
|
|
288
|
+
| `inspect [--json]` | What the app is, where its state lives, and what it's doing |
|
|
289
|
+
| `readiness [--env production]` | What an environment has and lacks before production, with fixes |
|
|
290
|
+
| `model show` / `model set` | Show or choose the model that makes agent decisions |
|
|
291
|
+
| `credential set` / `status` / `remove` | Store an API key (from a hidden prompt or stdin, never an argument), see where each credential comes from, or erase one |
|
|
292
|
+
| `config export` / `import` | Move non-secret configuration (variables, webhook endpoints, …) between environments |
|
|
293
|
+
| `session create` / `list` / `revoke` | Sessions for agents and service accounts |
|
|
294
|
+
| `operator grant` | A one-time production Studio sign-in for an operator |
|
|
295
|
+
| `codegen` | Regenerate `feltkit/_generated/api.ts` |
|
|
296
|
+
| `contract` | Write the FeltDB application contract (Managed FeltDB) |
|
|
297
|
+
| `doctor` | Check Node, dependencies and running servers for problems |
|
|
298
|
+
| `skills install` / `check` | Add or validate the FeltKit skills for coding agents (Claude Code) |
|
|
299
|
+
|
|
300
|
+
## Using a coding agent
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
npx feltkit skills install
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
adds the FeltKit skills to `.claude/skills/`, so Claude Code knows how to build, test and
|
|
307
|
+
operate this app.
|
|
308
|
+
|
|
309
|
+
## When something's wrong
|
|
310
|
+
|
|
311
|
+
- `npx feltkit doctor` checks Node, dependencies and running servers.
|
|
312
|
+
- `npx feltkit inspect` shows the environment's state, functions and recent errors.
|
|
313
|
+
- Studio → **Activity** shows every operation: who ran it, what was decided, and what FeltDB
|
|
314
|
+
committed.
|
|
315
|
+
- Errors carry a code (`FORBIDDEN`, `INVALID_ARGUMENT`, `NOT_FOUND`, …) and usually a hint with
|
|
316
|
+
the fix.
|
|
317
|
+
|
|
318
|
+
FeltKit documentation: <https://github.com/rkendel1/feltkit#readme>
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
/* eslint-disable */
|
|
3
3
|
import { createApi } from "@feltdb/feltkit/client";
|
|
4
4
|
import type * as app_module from "../app.js";
|
|
5
|
+
import type * as inbox_module from "../inbox.js";
|
|
5
6
|
import type * as projects_module from "../projects.js";
|
|
6
7
|
import type * as tasks_module from "../tasks.js";
|
|
7
8
|
|
|
8
9
|
/** Typed references to every query and mutation. Type-only imports: no backend code reaches the browser. */
|
|
9
|
-
export const api = createApi<{ "app": typeof app_module; "projects": typeof projects_module; "tasks": typeof tasks_module }>();
|
|
10
|
+
export const api = createApi<{ "app": typeof app_module; "inbox": typeof inbox_module; "projects": typeof projects_module; "tasks": typeof tasks_module }>();
|
package/template/feltkit/app.ts
CHANGED
|
@@ -12,15 +12,19 @@ export const app = feltkit({
|
|
|
12
12
|
// Roles grant AppPort permissions. "tasks.*" grants every tasks.<x> permission.
|
|
13
13
|
roles: {
|
|
14
14
|
// Members use the app and Studio's agent (feltkit.agent.*: create, approve, and follow agent tasks).
|
|
15
|
-
|
|
15
|
+
// notifications.*: a principal reads, marks, and dismisses only its own notifications.
|
|
16
|
+
// webhooks.emit + webhooks.deliver: their changes notify registered endpoints (task.created,
|
|
17
|
+
// task.completed); each delivery runs as the person whose change emitted it.
|
|
18
|
+
member: ["projects.*", "tasks.*", "files.*", "notifications.*", "webhooks.emit", "webhooks.deliver", "feltkit.agent.*"],
|
|
16
19
|
// Viewers read. They can see agent tasks but not start or approve them.
|
|
17
|
-
viewer: ["projects.read", "tasks.read", "files.read", "feltkit.agent.read"],
|
|
20
|
+
viewer: ["projects.read", "tasks.read", "files.read", "notifications.read", "feltkit.agent.read"],
|
|
18
21
|
// Agents can read, complete tasks, and leave notes. They cannot create or delete.
|
|
19
|
-
agent: ["projects.read", "tasks.read", "tasks.complete", "tasks.note", "files.read", "files.write"],
|
|
20
|
-
// Admins configure the app:
|
|
21
|
-
// (credential.*),
|
|
22
|
-
// never enters FeltDB.
|
|
23
|
-
|
|
22
|
+
agent: ["projects.read", "tasks.read", "tasks.complete", "tasks.note", "files.read", "files.write", "notifications.send", "webhooks.emit", "webhooks.deliver"],
|
|
23
|
+
// Admins configure the app in Studio → Services: the model (intelligence.*), stored
|
|
24
|
+
// credentials (credential.*), configuration, API keys, webhooks, jobs, and schedules.
|
|
25
|
+
// Credentials are stored by reference; the value never enters FeltDB.
|
|
26
|
+
// feltkit.operations.*: in production, admins are Studio's operators (npx feltkit operator grant).
|
|
27
|
+
admin: ["intelligence.*", "credential.*", "configuration.*", "apikeys.*", "webhooks.*", "jobs.*", "schedules.*", "feltkit.operations.*"],
|
|
24
28
|
},
|
|
25
29
|
// Development-only identities. Sign in as them from the app or the dashboard.
|
|
26
30
|
dev: {
|
|
@@ -29,9 +33,22 @@ export const app = feltkit({
|
|
|
29
33
|
{ id: "grace", name: "Grace Hopper", roles: ["viewer"] },
|
|
30
34
|
],
|
|
31
35
|
},
|
|
32
|
-
// AppPort Services, stored in this app's FeltDB instance
|
|
33
|
-
// model the agent decides with
|
|
34
|
-
services: {
|
|
36
|
+
// AppPort Services, stored in this app's FeltDB instance, each configured in Studio →
|
|
37
|
+
// Services. Intelligence is the model the agent decides with (Studio → Models).
|
|
38
|
+
services: {
|
|
39
|
+
files: true,
|
|
40
|
+
// The types this app sends; Studio offers them when you send one by hand.
|
|
41
|
+
notifications: { types: ["task.completed"] },
|
|
42
|
+
configuration: true,
|
|
43
|
+
// A request authenticated by an API key holds these roles.
|
|
44
|
+
apiKeys: { roles: ["member"] },
|
|
45
|
+
// The events this app emits (feltkit/tasks.ts); Studio offers them when you register an endpoint.
|
|
46
|
+
webhooks: { events: ["task.created", "task.completed"] },
|
|
47
|
+
// Job type → the mutation that runs it, as the principal that queued it, with retries.
|
|
48
|
+
jobs: { handlers: { "projects.clear-completed": "projects:clearCompleted" } },
|
|
49
|
+
schedules: true,
|
|
50
|
+
intelligence: true,
|
|
51
|
+
},
|
|
35
52
|
});
|
|
36
53
|
|
|
37
54
|
export const projects = app.collection("projects", {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { s } from "@feltdb/feltkit";
|
|
2
|
+
import { app } from "./app.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The signed-in person's notifications (AppPort Services Notifications, e.g. "task.completed"
|
|
6
|
+
* from tasks:setDone). Each principal reads and marks only its own; the service enforces it.
|
|
7
|
+
*/
|
|
8
|
+
export const list = app.query({
|
|
9
|
+
access: "notifications.read",
|
|
10
|
+
args: { unread: s.optional(s.boolean()) },
|
|
11
|
+
handler: async (ctx, { unread }) =>
|
|
12
|
+
(await ctx.services.notifications.list(unread ? { unread: true } : {})).map((n) => ({ id: n.id, type: n.type, title: n.title, data: n.data ?? null, createdAt: n.createdAt, read: Boolean(n.readAt) })),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const markRead = app.mutation({
|
|
16
|
+
access: "notifications.update",
|
|
17
|
+
args: { id: s.string() },
|
|
18
|
+
handler: async (ctx, { id }) => {
|
|
19
|
+
await ctx.services.notifications.markRead(id);
|
|
20
|
+
},
|
|
21
|
+
});
|