@beignet/cli 0.0.43 → 0.0.45

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 (81) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +61 -8
  3. package/dist/choices.d.ts +4 -0
  4. package/dist/choices.d.ts.map +1 -1
  5. package/dist/choices.js +9 -0
  6. package/dist/choices.js.map +1 -1
  7. package/dist/db.d.ts.map +1 -1
  8. package/dist/db.js +8 -4
  9. package/dist/db.js.map +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +13 -13
  12. package/dist/index.js.map +1 -1
  13. package/dist/inspect.js +78 -21
  14. package/dist/inspect.js.map +1 -1
  15. package/dist/lib.d.ts +4 -1
  16. package/dist/lib.d.ts.map +1 -1
  17. package/dist/lib.js +1 -1
  18. package/dist/lib.js.map +1 -1
  19. package/dist/mcp.d.ts +2 -1
  20. package/dist/mcp.d.ts.map +1 -1
  21. package/dist/mcp.js +354 -3
  22. package/dist/mcp.js.map +1 -1
  23. package/dist/operational-lifecycle.d.ts +12 -0
  24. package/dist/operational-lifecycle.d.ts.map +1 -0
  25. package/dist/operational-lifecycle.js +39 -0
  26. package/dist/operational-lifecycle.js.map +1 -0
  27. package/dist/operational-path.d.ts +3 -0
  28. package/dist/operational-path.d.ts.map +1 -0
  29. package/dist/operational-path.js +26 -0
  30. package/dist/operational-path.js.map +1 -0
  31. package/dist/operational-process.d.ts +78 -0
  32. package/dist/operational-process.d.ts.map +1 -0
  33. package/dist/operational-process.js +228 -0
  34. package/dist/operational-process.js.map +1 -0
  35. package/dist/operational-runner.d.ts +2 -0
  36. package/dist/operational-runner.d.ts.map +1 -0
  37. package/dist/operational-runner.js +168 -0
  38. package/dist/operational-runner.js.map +1 -0
  39. package/dist/outbox.d.ts +14 -6
  40. package/dist/outbox.d.ts.map +1 -1
  41. package/dist/outbox.js +140 -124
  42. package/dist/outbox.js.map +1 -1
  43. package/dist/schedule.d.ts +2 -1
  44. package/dist/schedule.d.ts.map +1 -1
  45. package/dist/schedule.js +79 -81
  46. package/dist/schedule.js.map +1 -1
  47. package/dist/task.d.ts +2 -1
  48. package/dist/task.d.ts.map +1 -1
  49. package/dist/task.js +57 -61
  50. package/dist/task.js.map +1 -1
  51. package/dist/templates/agents.d.ts.map +1 -1
  52. package/dist/templates/agents.js +21 -8
  53. package/dist/templates/agents.js.map +1 -1
  54. package/dist/templates/base.d.ts.map +1 -1
  55. package/dist/templates/base.js +2 -0
  56. package/dist/templates/base.js.map +1 -1
  57. package/dist/templates/server.d.ts.map +1 -1
  58. package/dist/templates/server.js +9 -2
  59. package/dist/templates/server.js.map +1 -1
  60. package/dist/templates/shadcn.d.ts.map +1 -1
  61. package/dist/templates/shadcn.js +3 -1
  62. package/dist/templates/shadcn.js.map +1 -1
  63. package/package.json +2 -2
  64. package/skills/app-structure/SKILL.md +19 -6
  65. package/src/choices.ts +19 -0
  66. package/src/db.ts +8 -4
  67. package/src/index.ts +19 -16
  68. package/src/inspect.ts +114 -26
  69. package/src/lib.ts +27 -1
  70. package/src/mcp.ts +457 -2
  71. package/src/operational-lifecycle.ts +56 -0
  72. package/src/operational-path.ts +35 -0
  73. package/src/operational-process.ts +454 -0
  74. package/src/operational-runner.ts +221 -0
  75. package/src/outbox.ts +154 -128
  76. package/src/schedule.ts +84 -89
  77. package/src/task.ts +58 -62
  78. package/src/templates/agents.ts +21 -8
  79. package/src/templates/base.ts +2 -0
  80. package/src/templates/server.ts +9 -2
  81. package/src/templates/shadcn.ts +3 -1
package/src/task.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { readFile, stat } from "node:fs/promises";
1
+ import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import type { TaskDef, TaskRunContextArgs } from "@beignet/core/tasks";
4
4
  import { createJiti } from "jiti";
@@ -8,6 +8,8 @@ import {
8
8
  type OperationalErrorReportingContext,
9
9
  reportOperationalFailure,
10
10
  } from "./operational-error-reporting.js";
11
+ import { runWithOperationalCleanup } from "./operational-lifecycle.js";
12
+ import { resolveAppOperationalModulePath } from "./operational-path.js";
11
13
 
12
14
  /**
13
15
  * Options for running an app-owned operational task.
@@ -15,7 +17,8 @@ import {
15
17
  export type RunAppTaskOptions = {
16
18
  name: string;
17
19
  cwd?: string;
18
- input?: string | Record<string, unknown>;
20
+ /** Pre-parsed input validated by the registered task schema. */
21
+ input?: unknown;
19
22
  tenant?: string;
20
23
  modulePath?: string;
21
24
  };
@@ -49,7 +52,7 @@ export async function runAppTask(
49
52
  const cwd = path.resolve(options.cwd ?? process.cwd());
50
53
  const config = await loadBeignetConfig(cwd);
51
54
  const modulePath = normalizePath(options.modulePath ?? config.paths.tasks);
52
- const rawInput = parseTaskInputFlag(options.input);
55
+ const rawInput = options.input === undefined ? {} : options.input;
53
56
  const startedAt = performance.now();
54
57
  const taskModule = await loadTaskModule(cwd, modulePath);
55
58
  const task = findTask(taskModule.tasks, options.name, modulePath);
@@ -67,51 +70,56 @@ export async function runAppTask(
67
70
  ? await taskModule.createTaskContext(contextArgs)
68
71
  : {};
69
72
 
70
- try {
71
- let output: unknown;
72
- try {
73
- output = await runTask(task, {
74
- input: parsedInput,
75
- ctx,
76
- });
77
- } catch (error) {
78
- await reportOperationalFailure({
79
- ctx: toOperationalContext(ctx),
80
- error,
81
- reportOptions: {
82
- level: "error",
83
- mechanism: "beignet.task.cli",
84
- handled: false,
85
- tags: {
86
- "beignet.kind": "task",
87
- "beignet.task": task.name,
88
- },
89
- contexts: {
90
- task: {
91
- name: task.name,
92
- tenant: options.tenant ?? null,
93
- source: "beignet-cli",
73
+ return runWithOperationalCleanup({
74
+ operation: `Task "${task.name}"`,
75
+ cleanupLabel: "stopTaskContext",
76
+ run: async () => {
77
+ let output: unknown;
78
+ try {
79
+ output = await runTask(task, {
80
+ input: parsedInput,
81
+ ctx,
82
+ });
83
+ } catch (error) {
84
+ await reportOperationalFailure({
85
+ ctx: toOperationalContext(ctx),
86
+ error,
87
+ reportOptions: {
88
+ level: "error",
89
+ mechanism: "beignet.task.cli",
90
+ handled: false,
91
+ tags: {
92
+ "beignet.kind": "task",
93
+ "beignet.task": task.name,
94
+ },
95
+ contexts: {
96
+ task: {
97
+ name: task.name,
98
+ tenant: options.tenant ?? null,
99
+ source: "beignet-cli",
100
+ },
94
101
  },
95
102
  },
96
- },
97
- });
98
- throw error;
99
- }
100
-
101
- return {
102
- schemaVersion: 1,
103
- name: task.name,
104
- cwd,
105
- modulePath,
106
- input: parsedInput,
107
- ...(options.tenant !== undefined ? { tenant: options.tenant } : {}),
108
- output,
109
- durationMs: Math.round(performance.now() - startedAt),
110
- };
111
- } finally {
112
- await flushOperationalErrorReporter(toOperationalContext(ctx));
113
- await taskModule.stopTaskContext?.(ctx, contextArgs);
114
- }
103
+ });
104
+ throw error;
105
+ }
106
+
107
+ return {
108
+ schemaVersion: 1,
109
+ name: task.name,
110
+ cwd,
111
+ modulePath,
112
+ input: parsedInput,
113
+ ...(options.tenant !== undefined ? { tenant: options.tenant } : {}),
114
+ output,
115
+ durationMs: Math.round(performance.now() - startedAt),
116
+ };
117
+ },
118
+ cleanup: async () => {
119
+ await flushOperationalErrorReporter(toOperationalContext(ctx));
120
+ await taskModule.stopTaskContext?.(ctx, contextArgs);
121
+ },
122
+ });
115
123
  }
116
124
 
117
125
  function toOperationalContext(ctx: unknown): OperationalErrorReportingContext {
@@ -120,27 +128,15 @@ function toOperationalContext(ctx: unknown): OperationalErrorReportingContext {
120
128
  : {};
121
129
  }
122
130
 
123
- function parseTaskInputFlag(input: RunAppTaskOptions["input"]): unknown {
124
- if (input === undefined) return {};
125
- if (typeof input !== "string") return input;
126
-
127
- try {
128
- return JSON.parse(input) as unknown;
129
- } catch (error) {
130
- throw new Error(
131
- `Invalid --input JSON: ${error instanceof Error ? error.message : String(error)}`,
132
- );
133
- }
134
- }
135
-
136
131
  async function loadTaskModule(
137
132
  cwd: string,
138
133
  modulePath: string,
139
134
  ): Promise<TaskModule> {
140
- const absolutePath = path.join(cwd, modulePath);
135
+ let absolutePath: string;
141
136
  try {
142
- await stat(absolutePath);
143
- } catch {
137
+ absolutePath = await resolveAppOperationalModulePath(cwd, modulePath);
138
+ } catch (error) {
139
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
144
140
  throw new Error(
145
141
  `Could not find app task registry at ${modulePath}. Create it with beignet make task <feature.name> or configure paths.tasks in beignet.config.ts.`,
146
142
  );
@@ -31,8 +31,8 @@ export function agentsMd(ctx: TemplateContext): string {
31
31
  "\n| Server Component request context | Use an app-owned server-only helper such as `lib/server-context.ts` that wraps `server.createContextFromNext()` in React `cache(...)`. Layouts may read `ctx.auth`, `ctx.tenant`, and request metadata for redirects and shell state; feature data still belongs behind use cases. |" +
32
32
  "\n| Server Component React Query prefetch | Use an app-owned helper such as `lib/server-react-query.ts` on top of the request context: keep the `rq(contract).queryOptions(...)` key, replace only the server `queryFn` with the direct use case call, and do not create a parallel `rqServer` abstraction. |";
33
33
  const specificSkills = ctx.api
34
- ? "`@beignet/next#routes-server`, `@beignet/provider-db-drizzle#database-provider`, `@beignet/provider-auth-better-auth#auth-provider`, or `@beignet/cli#app-structure`"
35
- : "`@beignet/next#routes-server`, `@beignet/react-query#client`, `@beignet/react-hook-form#forms`, `@beignet/provider-db-drizzle#database-provider`, `@beignet/provider-auth-better-auth#auth-provider`, or `@beignet/cli#app-structure`";
34
+ ? "`@beignet/next#routes-server`, `@beignet/web#fetch-server`, `@beignet/devtools#runtime-safety`, `@beignet/provider-db-drizzle#database-provider`, `@beignet/provider-auth-better-auth#auth-provider`, or `@beignet/cli#app-structure`"
35
+ : "`@beignet/next#routes-server`, `@beignet/web#fetch-server`, `@beignet/devtools#runtime-safety`, `@beignet/react-query#client`, `@beignet/react-hook-form#forms`, `@beignet/provider-db-drizzle#database-provider`, `@beignet/provider-auth-better-auth#auth-provider`, or `@beignet/cli#app-structure`";
36
36
 
37
37
  return `# ${ctx.name} — agent guide
38
38
 
@@ -81,7 +81,9 @@ events, listener registration, jobs, and outbox wiring. After changing the Drizz
81
81
  \`infra/db/schema/\`, run \`${cli} db generate\` then \`${cli} db migrate\`.
82
82
  When MCP is available, use \`db_schema_sync\` for Beignet provider table
83
83
  re-exports and \`db\` for \`generate\`, \`migrate\`, \`seed\`, or \`reset\`
84
- instead of falling back to a shell.
84
+ instead of falling back to a shell. Use \`task_run\`, \`schedule_run\`,
85
+ \`outbox_inspect\`, and \`outbox_run\` for registered operational workflows
86
+ and outbox recovery.
85
87
 
86
88
  ## The framework already solves these
87
89
 
@@ -94,7 +96,7 @@ apps have reimplemented by accident:
94
96
  | Ports outside a request — auth callbacks, module-level helpers | \`const { ports } = await getServer()\` (dynamic \`import("@/server")\` breaks module cycles). Do not construct parallel provider clients or fall back to \`console.*\` when \`ports.logger\` exists. |
95
97
  | Routes that cannot be contracts — webhooks, third-party callbacks, streaming | \`createWebhookRoute\`, \`createPaymentWebhookRoute\`, \`createScheduleRoute\`, \`createOutboxDrainRoute\` from \`@beignet/next\`; \`server.rawRoute(...)\` for anything else. All run the hooks pipeline — never hand-enforce rate limits in a route body. |
96
98
  | Rate limiting or idempotency on a route | Declare \`metadata.rateLimit\` / \`metadata.idempotency\` on the contract (or the \`pipeline\` option on raw routes); hooks enforce it. |
97
- | Tenant-owned repository access | Pass \`TenantScope\` through app-facing repository methods and unwrap it with \`tenantScopeId(scope)\` in adapters. Raw provider-correlation lookups such as webhook customer IDs are not tenant authorization. |
99
+ | Tenant-owned repository access | Resolve the request tenant from membership-backed app state or provider claims the app explicitly treats as authoritative and current; never trust caller-supplied tenant IDs or unverified session fields. Pass \`TenantScope\` through app-facing repository methods and unwrap it with \`tenantScopeId(scope)\` in adapters. Raw provider-correlation lookups such as webhook customer IDs are not tenant authorization. |
98
100
  | Route-level tests that exercise real hooks | \`createTestApp\` / \`createTestRequester\` from \`@beignet/web/testing\`. Bind the rate-limit and idempotency ports in the test app when asserting 429s or replay. |
99
101
  | Environment configuration | \`lib/env.ts\` (\`createEnv\`), never ad-hoc \`process.env\` reads in app code. |
100
102
  | The same lookup runs several times in one request — context, policy, use case | Wrap the repository read in \`createMemo(...)\` from \`@beignet/core/memo\` where the adapter is wired in infra. The cache lives for exactly one request; pair mutations with \`.invalidate(...)\`. Never hand-roll per-request caches. |
@@ -159,10 +161,13 @@ skill-loading block.
159
161
  ## MCP server
160
162
 
161
163
  \`.mcp.json\` registers the app-local \`@beignet/cli\` bin at
162
- \`./node_modules/.bin/beignet mcp\`, which exposes the app map, validation, and
163
- generators as structured tools named exactly: \`app_map\`, \`explain\`,
164
- \`check\`, \`db\`, \`db_schema_sync\`, \`routes\`, \`doctor\`,
164
+ \`./node_modules/.bin/beignet mcp\`, which exposes app context, validation,
165
+ generation, and operations as structured tools named exactly: \`app_map\`,
166
+ \`explain\`, \`check\`, \`db\`, \`db_schema_sync\`, \`task_run\`,
167
+ \`schedule_run\`, \`outbox_inspect\`, \`outbox_run\`, \`routes\`, \`doctor\`,
165
168
  \`doctor_fix_plan\`, \`doctor_fix\`, \`lint\`, \`make\`, \`provider_add\`.
169
+ It also publishes \`beignet://app/guidance\` and focused
170
+ \`beignet://app/features/{feature}\` resources.
166
171
  Use \`doctor_fix_plan\` to inspect
167
172
  stable operation IDs, hashes, exact patches, and current diagnostics before
168
173
  passing its \`planId\` and optional \`fixIds\` to \`doctor_fix\`. Guarded
@@ -180,7 +185,15 @@ with \`generate\` and \`migrate\`; database output is bounded, commands time
180
185
  out, and cancellation stops the active process tree. Treat \`seed\` and
181
186
  especially \`reset\` as app-owned mutations. Lifecycle \`dryRun\` validates
182
187
  and reports the script without executing it; it does not simulate SQL or data
183
- changes. Clients that do not read \`.mcp.json\` can use the same command from
188
+ changes. Use \`task_run\` and \`schedule_run\` for registered operational
189
+ workflows, \`outbox_inspect\` for read-only \`list\` and \`show\`, and
190
+ \`outbox_run\` for \`drain\`, \`requeue\`, \`purge\`, or \`prune\`; purge and
191
+ prune support \`dryRun\`. Operational commands run in isolated process trees
192
+ with bounded results, cancellation, and timeouts. Optional \`module\` overrides
193
+ must remain inside the app root where the MCP server started. Cancellation and
194
+ timeouts cannot roll back side effects that already completed. Inspect app
195
+ state before retrying an interrupted operation or one that reports a cleanup
196
+ failure. Clients that do not read \`.mcp.json\` can use the same command from
184
197
  the app root; use \`${cli} mcp\` only for terminal debugging.
185
198
  `;
186
199
  }
@@ -122,7 +122,9 @@ export function packageJson(ctx: TemplateContext): string {
122
122
  function intentSkillPackages(ctx: TemplateContext): string[] {
123
123
  const packages = [
124
124
  "@beignet/core",
125
+ "@beignet/devtools",
125
126
  "@beignet/next",
127
+ "@beignet/web",
126
128
  "@beignet/cli",
127
129
  "@beignet/provider-auth-better-auth",
128
130
  "@beignet/provider-db-drizzle",
@@ -601,7 +601,9 @@ export type TenantResolutionInput = {
601
601
  export function resolveRequestTenant({
602
602
  auth,
603
603
  }: TenantResolutionInput): ActivityTenant | undefined {
604
- const tenantId = auth ? tenantIdFromAuth(auth) : undefined;
604
+ const tenantId = auth
605
+ ? tenantIdFromAuthoritativeAuthClaims(auth)
606
+ : undefined;
605
607
 
606
608
  return tenantId ? createTenant(tenantId) : undefined;
607
609
  }
@@ -614,7 +616,12 @@ export function resolveServiceTenant(
614
616
  return normalizedTenantId ? createTenant(normalizedTenantId) : undefined;
615
617
  }
616
618
 
617
- function tenantIdFromAuth(auth: AuthSession) {
619
+ /**
620
+ * Resolve only provider-issued claims that the application treats as
621
+ * authoritative. Multi-tenant apps must replace this starter seam with a
622
+ * membership-backed lookup before authorizing tenant-owned resources.
623
+ */
624
+ function tenantIdFromAuthoritativeAuthClaims(auth: AuthSession) {
618
625
  return (
619
626
  stringProperty(auth.session, "tenantId") ??
620
627
  stringProperty(auth.session, "organizationId") ??
@@ -186,7 +186,9 @@ const appGlobalsCss = `@import "tailwindcss";
186
186
  }
187
187
  `;
188
188
 
189
- const uiButton = `import { cva, type VariantProps } from "class-variance-authority";
189
+ const uiButton = `"use client";
190
+
191
+ import { cva, type VariantProps } from "class-variance-authority";
190
192
  import { Slot } from "radix-ui";
191
193
  import type * as React from "react";
192
194