create-m5kdev 0.40.3 → 0.41.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-m5kdev",
3
- "version": "0.40.3",
3
+ "version": "0.41.1",
4
4
  "license": "GPL-3.0-only",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -0,0 +1,21 @@
1
+ name: check
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ check:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: pnpm/action-setup@v4
13
+ with:
14
+ version: 10.13.1
15
+ - uses: actions/setup-node@v4
16
+ with:
17
+ node-version: 24
18
+ cache: pnpm
19
+ - run: pnpm install
20
+ - run: pnpm lint
21
+ - run: pnpm check-types
@@ -22,38 +22,29 @@
22
22
  // m5k:expo:end
23
23
 
24
24
  // m5k:server:start
25
- ## Backend Conventions
26
-
27
- - Keep repository, service, and transport layers separate inside each module.
28
- - Register modules in `apps/server/src/app.ts` via `createBackendApp(config, [modules])`.
29
- - Import Kernel infrastructure (`BaseModule`, Grants, DTOs) from `@m5kdev/backend/base/*`. The `modules/base` path still re-exports.
30
- - Clay, Docx, Pdf, Social, and Video are Optional Backend Module packages (`@m5kdev/module-<name>`). They are not scaffolded; add them only when the product needs them.
31
- - Use `BaseModule` subclasses for app modules; the kernel wires repositories, services, and tRPC.
32
- - Keep tRPC files focused on input/output wiring and delegate logic to services.
33
- - Do not add service methods that merely call another service. Call the owning service directly unless the method adds a business rule, authorization, validation, orchestration, a transaction boundary, or meaningful domain translation.
34
- - Renaming a dependency method, repackaging arguments, constructing a prompt, or forwarding actor/context data alone does not justify a service wrapper.
35
- - Do not create Drizzle migrations by hand. Use the scaffolded config and your project migration workflow later if you need generated migrations.
36
- - Server event: emit through Auth from Workflow job handlers (and similar background work) when the UI must refresh a change the triggering mutation did not return. See `.cursor/rules/server-event-emit.mdc`.
37
- // m5k:server:end
25
+ ## Modules
26
+
27
+ `posts` is the reference module. Mirror its layout when adding a feature:
38
28
 
29
+ - `apps/shared/src/modules/posts/`
30
+ - `apps/server/src/modules/posts/`
39
31
  // m5k:webapp:start
40
- ## Frontend Conventions
41
-
42
- - Keep the app shell shape: `NuqsAdapter` + `BrowserRouter` + `Providers`.
43
- - Compose global providers only in `apps/webapp/src/Providers.tsx`.
44
- - Use `AppConfigProvider` and `AppTrpcQueryProvider` from `@m5kdev/frontend`; use `@m5kdev/web-ui` for web-only UI.
45
- - Define app roles in `apps/shared/src/modules/app/roles.constants.ts` and pass `roles: APP_ROLES_CONFIG` through `AppConfigProvider` and `createBackendApp` metadata.
46
- - Add role display labels in `apps/webapp/translations/app.json` using keys like `organization.role.editor` (resolved as `app:organization.role.editor`).
47
- - When adding custom roles, also extend module grants (for example `*.grants.ts`) so permissions match the new role keys.
48
- - For upgrading existing apps, see the docs site guide `apps/docs/docs/guides/custom-app-roles-migration.md`.
49
- - Use `nuqs` for URL state, React Router for routing, HeroUI for UI primitives, and Tailwind v4 for styling.
50
- - Prefer shared framework utilities from `@m5kdev/frontend` and `@m5kdev/web-ui` before adding local duplicates.
51
- - React 19 idioms: no `forwardRef` (pass `ref` as a prop), no `defaultProps`, use `startTransition`/`useDeferredValue` for low-priority updates.
52
- - Prefer HeroUI v3 components over hand-rolled HTML + Tailwind whenever an equivalent exists; Tailwind is for layout and styling on top of them.
53
- - Keep components display-focused: data fetching, mutations, URL state, and effect chains live in custom hooks under `modules/<feature>/hooks/` (small focused hooks composed into one screen-level hook, e.g. `usePostsRoute`). Ephemeral UI state (modal open, selected row) may stay in the component. Extract reusable tRPC actions (delete, publish) into their own hooks.
54
- - Fetch all server data with TanStack Query over tRPC (`useTRPC()` + `queryOptions`/`mutationOptions`); invalidate with the matching `queryFilter()` after mutations. No `fetch`/axios.
55
- - Forms are uncontrolled HeroUI `Form` + `TextField`/`TextArea` with native HTML validation (`isRequired`, `type`, `minLength`) and `FieldError`; read values from `FormData` on submit. Never add form libraries (react-hook-form, formik). Validation that HTML cannot express (e.g. confirm-password) happens in the submit handler.
56
- - All user-facing copy goes through i18next (`useTranslation`); keys live in `apps/webapp/translations/`.
57
- - The `posts` feature (`apps/webapp/src/modules/posts/`) is the reference implementation for these conventions; see `.cursor/rules/frontend-*.mdc` for the detailed guides.
32
+ - `apps/webapp/src/modules/posts/`
58
33
  // m5k:webapp:end
59
34
 
35
+ Read the matching `.cursor/rules` guide before editing a module file.
36
+ // m5k:server:end
37
+
38
+ ## Agent skills
39
+
40
+ ### Issue tracker
41
+
42
+ Issues live in GitHub Issues. Use the `gh` CLI. See `docs/agents/issue-tracker.md`.
43
+
44
+ ### Triage labels
45
+
46
+ Default role labels: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. See `docs/agents/triage-labels.md`.
47
+
48
+ ### Domain docs
49
+
50
+ Single-context: one root `GLOSSARY.md` and `docs/adr/`. See `docs/agents/domain.md`.
@@ -139,6 +139,20 @@ The framework's detailed conventions live in `.cursor/rules/*.mdc`. Cursor appli
139
139
  | `.cursor/rules/frontend-i18n-guide.mdc` | webapp user-facing copy |
140
140
  // m5k:webapp:end
141
141
 
142
- `AGENTS.md` (repo root and per-app) carries a compact summary of the same conventions for other agents; this file plus the guides above are the authoritative version for Claude.
142
+ `AGENTS.md` (repo root and per-app) points at this file and `.cursor/rules`. This file plus the guides above are the authoritative conventions.
143
143
  // m5k:server:end
144
144
 
145
+ ## Agent skills
146
+
147
+ ### Issue tracker
148
+
149
+ Issues live in GitHub Issues. Use the `gh` CLI. See `docs/agents/issue-tracker.md`.
150
+
151
+ ### Triage labels
152
+
153
+ Default role labels: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. See `docs/agents/triage-labels.md`.
154
+
155
+ ### Domain docs
156
+
157
+ Single-context: one root `GLOSSARY.md` and `docs/adr/`. See `docs/agents/domain.md`.
158
+
@@ -0,0 +1,181 @@
1
+ # {{APP_NAME}}
2
+
3
+ {{APP_DESCRIPTION}}
4
+
5
+ ## Language
6
+ # m5k:server:start
7
+
8
+ ### Tenancy
9
+
10
+ **Organization**:
11
+ The default tenancy unit. Signup creates one; a Closed Organization cannot be used.
12
+ _Avoid_: Workspace, tenant, account, company, Team
13
+
14
+ **Membership**:
15
+ A durable seat in an Organization. Invite can create it before a User exists; leave, cancel, and expiry soft-delete it; rejoin revives the same MemberId. An Organization has exactly one Owner.
16
+ _Avoid_: OrgUser; treating Invitation as the Membership; Seat (that is Seat billing)
17
+
18
+ **Member**:
19
+ The Membership principal used to attribute and authorize org-scoped assets, including invited Memberships that do not yet have a User.
20
+ _Avoid_: User (when you mean the membership), Invitation
21
+
22
+ **MemberId**:
23
+ The Membership id stamped on org-scoped rows. In organization context, `"own"` grants compare this, not UserId.
24
+ _Avoid_: UserId as the org ownership key
25
+
26
+ **User**:
27
+ The global sign-in identity. Owns personal resources that are not org tenancy. A Closed User cannot sign in.
28
+ _Avoid_: Account, Customer, Client, Member
29
+
30
+ **UserId**:
31
+ The User id. Correct key for personal resources. Not the org-scoped ownership key.
32
+ _Avoid_: MemberId
33
+
34
+ **Owner**:
35
+ The Organization Role `owner`. Exactly one per Organization. Invite and org self-service cannot grant or transfer it.
36
+ _Avoid_: multiple Owners as a product feature; inviting Owner
37
+
38
+ **Invitation**:
39
+ The accept token that attaches a User to an invited Membership. Not a person and not assignable.
40
+ _Avoid_: Waitlist code, pending Member
41
+
42
+ **Closed**:
43
+ A User or Organization in the restorable lockout state.
44
+ _Avoid_: soft-delete (that is Membership leave), deactivated, suspended, banned
45
+
46
+ **Restore**:
47
+ Returning a Closed User or Organization to usable state before Purge. Admin panel only.
48
+ _Avoid_: undelete, reactivate
49
+
50
+ **Purge**:
51
+ Permanent removal of a Closed User or Organization after the grace window.
52
+ _Avoid_: hard-delete as the User-facing noun
53
+
54
+ ### Identity and access
55
+
56
+ **Actor**:
57
+ Who a server call is made on behalf of: User, Organization, or Admin. Organization scope requires a live Membership and an Organization that is not Closed. Invited Members are not Actors.
58
+ _Avoid_: Session, Principal, Request, TeamActor
59
+
60
+ **Grant**:
61
+ A permission tuple: resource, level (`user` | `organization`), role, Action, Access.
62
+ _Avoid_: Permission, Policy, ACL
63
+
64
+ **Action**:
65
+ Canonical Grant verbs: `read`, `write`, `delete`, `publish`.
66
+ _Avoid_: update, create, patch, remove
67
+
68
+ **Access**:
69
+ How wide a Grant is: `all`, `own`, `org`, `none`.
70
+ _Avoid_: Scope, Role
71
+
72
+ **Role**:
73
+ A named key at User or Organization scope. Defaults: User `user`/`admin`; Organization `member`/`admin`/`owner`.
74
+ _Avoid_: Grant, Access, permission
75
+
76
+ ### Product surfaces
77
+
78
+ **Admin panel**:
79
+ The User-role admin UI at `/admin`. Organization Role `admin` does not see it.
80
+ _Avoid_: org Role admin, Billing Portal
81
+ # m5k:files:start
82
+
83
+ **File**:
84
+ An inventoried object in storage: a row plus the bytes. Personal Files stamp UserId; org-scoped Files stamp MemberId.
85
+ _Avoid_: Upload, Asset, Attachment, Blob
86
+ # m5k:files:end
87
+ # m5k:billing:start
88
+
89
+ **Plan**:
90
+ Named commercial offering in app code. Not a Stripe Product.
91
+ _Avoid_: Product, Tier, Subscription
92
+
93
+ **Subscription**:
94
+ The billed relationship for an Organization, not a User.
95
+ _Avoid_: Plan, personal User subscription
96
+
97
+ **Checkout**:
98
+ Hosted start of a Subscription when the Organization has none.
99
+ _Avoid_: Billing Portal
100
+
101
+ **Billing Portal**:
102
+ Hosted management of an existing Subscription.
103
+ _Avoid_: Checkout
104
+
105
+ **Seat billing**:
106
+ When on, billable quantity is the count of Memberships. Off (default): quantity is one.
107
+ _Avoid_: Seat as a synonym for Membership
108
+ # m5k:billing:end
109
+ # m5k:ai:start
110
+
111
+ **Conversation**:
112
+ Ordered UI transcript of AI turns.
113
+ _Avoid_: Chat, Thread (that is the store)
114
+
115
+ **Thread**:
116
+ The persisted AI memory thread behind a Conversation.
117
+ _Avoid_: using Thread for the UI surface
118
+
119
+ **Agent**:
120
+ A named AI agent the app registers. A Conversation selects which Agent answers.
121
+ _Avoid_: Assistant, Bot, MCP client
122
+ # m5k:ai:end
123
+ # m5k:mcp:start
124
+
125
+ **MCP client**:
126
+ A remote Model Context Protocol client that calls the app on behalf of a User.
127
+ _Avoid_: Agent, Connection
128
+
129
+ **MCP call**:
130
+ A tRPC procedure opted into the MCP catalog. Grant checks always run.
131
+ _Avoid_: a second tool catalog, Workflow job
132
+
133
+ **MCP allowlist**:
134
+ The Organizations a given OAuth client may pass as organizationId for a User.
135
+ _Avoid_: treating allowlist as permission to write
136
+ # m5k:mcp:end
137
+ # m5k:notifications:start
138
+
139
+ **Notification**:
140
+ A persisted inbox instance owned by a Member, classified by a Notification kind.
141
+ _Avoid_: UserId as the ownership key
142
+
143
+ **Notification kind**:
144
+ A developer-declared class of Notifications. Mute applies to the kind.
145
+ _Avoid_: Notification (that is the instance)
146
+
147
+ **Channel**:
148
+ A delivery means for a Notification: in-app, web push, mobile push, or email.
149
+ _Avoid_: Server event, Device
150
+ # m5k:notifications:end
151
+ # m5k:workflows:start
152
+
153
+ **Workflow**:
154
+ A persisted background run. Org-scoped runs stamp MemberId.
155
+ _Avoid_: Job, Queue, Task, Recurrence
156
+ # m5k:workflows:end
157
+ # m5k:tags:start
158
+
159
+ **Tag**:
160
+ A polymorphic label. Ownership is UserId, MemberId, or organizationId.
161
+ _Avoid_: Label, Category
162
+ # m5k:tags:end
163
+ # m5k:connect:start
164
+
165
+ **Connection**:
166
+ A linked third-party API account. Personal; keyed by UserId. The module id is `connect`.
167
+ _Avoid_: Connect as the product noun; login OAuth
168
+ # m5k:connect:end
169
+ # m5k:webhook:start
170
+
171
+ **Inbound callback**:
172
+ A one-shot inbound callback with an awaitable payload. Unattributed. The module id is `webhook`.
173
+ _Avoid_: Webhook (when you mean this vs Stripe Subscription sync); Connection
174
+ # m5k:webhook:end
175
+ # m5k:recurrence:start
176
+
177
+ **Recurrence**:
178
+ A repeating calendar pattern stored as a row. Not a Workflow.
179
+ _Avoid_: Workflow, cron
180
+ # m5k:recurrence:end
181
+ # m5k:server:end
@@ -39,6 +39,8 @@ Database tables are registered by hand in `apps/server/src/schema.ts`; after cha
39
39
 
40
40
  - Email: `admin@{{APP_SLUG}}.local`
41
41
  - Password: `password1234`
42
+
43
+ After sign-in, the index route (`/`) is the Posts list.
42
44
  // m5k:server:end
43
45
 
44
46
  ## Typical Commands
@@ -58,7 +58,7 @@ async function saveMemberEdit(page: Page, row: Locator, changes: { name?: string
58
58
  await dialog.getByLabel(/^name$/i).fill(changes.name);
59
59
  }
60
60
  if (changes.role !== undefined) {
61
- await dialog.getByRole("button", { name: /^role$/i }).click();
61
+ await dialog.getByRole("button", { name: /role/i }).click();
62
62
  await page.getByRole("option", { name: new RegExp(`^${changes.role}$`) }).click();
63
63
  }
64
64
 
@@ -211,7 +211,10 @@ test("duplicate signup with an existing email is rejected", async ({ page, reque
211
211
  expect(
212
212
  response.ok(),
213
213
  `${response.status()} ${response.statusText()}: ${await response.text()}`
214
- ).toBe(false);
214
+ ).toBe(true);
215
+ expect(await response.json()).toMatchObject({ token: null });
216
+ const state = await getUserState(request, profile, email);
217
+ expect(state.user.emailVerified).toBe(true);
215
218
  });
216
219
 
217
220
  test("organization invite email lets a new user join without creating a separate org", async ({
@@ -357,7 +360,7 @@ test("organization members page lets an admin edit a member name", async ({ page
357
360
  expect(afterName?.id).toBe(invitedMemberId);
358
361
  expect(afterName?.name).toBe("Kitchen lead");
359
362
  expect(afterName?.userId).toBeNull();
360
- await expect(invitedRow.getByRole("cell").first()).toHaveText("Kitchen lead");
363
+ await expect(page.getByRole("row").filter({ hasText: "Kitchen lead" })).toBeVisible();
361
364
  });
362
365
 
363
366
  test("organization members page resends, updates, cancels, and revives an invited seat", async ({
@@ -491,9 +494,11 @@ test("admin creates, bans, unbans, and authenticates a user", async ({ page, req
491
494
  await dialog.getByPlaceholder(/enter user's email/i).fill(email);
492
495
  await dialog.getByPlaceholder(/enter password/i).fill(password);
493
496
  await dialog.getByRole("button", { name: /^create user$/i }).click();
497
+ await expect(dialog).toBeHidden();
494
498
 
499
+ await page.locator('input[name="search"]').fill(email);
495
500
  const createdRow = page.getByRole("row").filter({ hasText: email });
496
- await expect(createdRow).toBeVisible();
501
+ await expect(createdRow).toBeVisible({ timeout: 10_000 });
497
502
  await expect(createdRow.getByText(/^verified$/i)).toBeVisible();
498
503
  await createdRow.getByLabel("User actions").click();
499
504
  await page.getByRole("menuitem", { name: /^ban$/i }).click();
@@ -529,21 +534,6 @@ test("admin creates, bans, unbans, and authenticates a user", async ({ page, req
529
534
  .toBe("admin");
530
535
 
531
536
  await logout(page);
532
- const verificationResponse = await request.post(
533
- `${profiles.standard.serverUrl}/api/auth/send-verification-email`,
534
- {
535
- data: {
536
- email,
537
- callbackURL: `${profiles.standard.webUrl}/login`,
538
- },
539
- }
540
- );
541
- expect(verificationResponse.ok()).toBe(true);
542
- const verification = await latestEmail(request, profile, {
543
- to: email,
544
- templateId: "verification",
545
- });
546
- await request.get(emailUrl(verification));
547
537
  await login(page, email, password);
548
538
  const adminListResponse = await authFetch(page, profile, "/api/auth/admin/list-users");
549
539
  expect(
@@ -561,6 +551,7 @@ test("admin verifies email, changes user role, and sets password from users list
561
551
  const newPassword = "password5678";
562
552
 
563
553
  await signUp(page, email, password);
554
+ await expect(page.getByText(/verification email sent/i)).toBeVisible();
564
555
  const before = await getUserState(request, profile, email);
565
556
  expect(before.user.emailVerified).toBe(false);
566
557
  expect(before.user.role).toBe("user");
@@ -603,9 +594,7 @@ test("admin verifies email, changes user role, and sets password from users list
603
594
  await page.locator('input[name="search"]').fill(profiles.standard.adminEmail);
604
595
  const adminRow = page.getByRole("row").filter({ hasText: profiles.standard.adminEmail });
605
596
  await expect(adminRow).toBeVisible();
606
- await expect(
607
- adminRow.getByRole("button", { name: `Role for ${profiles.standard.adminEmail}` })
608
- ).toBeDisabled();
597
+ await expect(adminRow.locator('[data-slot="select-trigger"]')).toBeDisabled();
609
598
 
610
599
  await logout(page);
611
600
  await login(page, email, newPassword);
@@ -0,0 +1,45 @@
1
+ DROP INDEX `users_stripe_customer_id_unique`;--> statement-breakpoint
2
+ ALTER TABLE `users` ADD `closed_at` integer;--> statement-breakpoint
3
+ ALTER TABLE `users` DROP COLUMN `stripe_customer_id`;--> statement-breakpoint
4
+ PRAGMA foreign_keys=OFF;--> statement-breakpoint
5
+ CREATE TABLE `__new_apikeys` (
6
+ `id` text PRIMARY KEY NOT NULL,
7
+ `name` text,
8
+ `start` text,
9
+ `prefix` text,
10
+ `key` text NOT NULL,
11
+ `config_id` text DEFAULT 'default' NOT NULL,
12
+ `reference_id` text NOT NULL,
13
+ `user_id` text,
14
+ `refill_interval` integer,
15
+ `refill_amount` integer,
16
+ `last_refill_at` integer,
17
+ `enabled` integer NOT NULL,
18
+ `rate_limit_enabled` integer NOT NULL,
19
+ `rate_limit_time_window` integer,
20
+ `rate_limit_max` integer,
21
+ `request_count` integer NOT NULL,
22
+ `remaining` integer,
23
+ `last_request` integer,
24
+ `expires_at` integer,
25
+ `created_at` integer NOT NULL,
26
+ `updated_at` integer NOT NULL,
27
+ `permissions` text,
28
+ `metadata` text,
29
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
30
+ );
31
+ --> statement-breakpoint
32
+ INSERT INTO `__new_apikeys`("id", "name", "start", "prefix", "key", "config_id", "reference_id", "user_id", "refill_interval", "refill_amount", "last_refill_at", "enabled", "rate_limit_enabled", "rate_limit_time_window", "rate_limit_max", "request_count", "remaining", "last_request", "expires_at", "created_at", "updated_at", "permissions", "metadata") SELECT "id", "name", "start", "prefix", "key", 'default', "user_id", "user_id", "refill_interval", "refill_amount", "last_refill_at", "enabled", "rate_limit_enabled", "rate_limit_time_window", "rate_limit_max", "request_count", "remaining", "last_request", "expires_at", "created_at", "updated_at", "permissions", "metadata" FROM `apikeys`;--> statement-breakpoint
33
+ DROP TABLE `apikeys`;--> statement-breakpoint
34
+ ALTER TABLE `__new_apikeys` RENAME TO `apikeys`;--> statement-breakpoint
35
+ PRAGMA foreign_keys=ON;--> statement-breakpoint
36
+ ALTER TABLE `organizations` ADD `currency` text;--> statement-breakpoint
37
+ ALTER TABLE `organizations` ADD `stripe_customer_id` text;--> statement-breakpoint
38
+ ALTER TABLE `organizations` ADD `stripe_sandbox_customer_id` text;--> statement-breakpoint
39
+ ALTER TABLE `organizations` ADD `billing_exempt` integer DEFAULT false NOT NULL;--> statement-breakpoint
40
+ ALTER TABLE `organizations` ADD `allow_cardless_trial` integer DEFAULT false NOT NULL;--> statement-breakpoint
41
+ ALTER TABLE `organizations` ADD `cardless_trial_consumed` text DEFAULT '{}';--> statement-breakpoint
42
+ ALTER TABLE `organizations` ADD `closed_at` integer;--> statement-breakpoint
43
+ CREATE UNIQUE INDEX `organizations_stripe_customer_id_unique` ON `organizations` (`stripe_customer_id`);--> statement-breakpoint
44
+ CREATE UNIQUE INDEX `organizations_stripe_sandbox_customer_id_unique` ON `organizations` (`stripe_sandbox_customer_id`);--> statement-breakpoint
45
+ ALTER TABLE `files` DROP COLUMN `team_id`;