create-mercato-app 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3045.b4b3320cc2

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 CHANGED
@@ -36,6 +36,8 @@ npx create-mercato-app <app-name> [options]
36
36
  | `--app <name>` | Bootstrap an official Open Mercato ready app from `open-mercato/ready-app-<name>` |
37
37
  | `--app-url <url>` | Bootstrap a ready app from a GitHub repository URL |
38
38
  | `--skip-agentic-setup` | Skip the interactive agentic setup wizard |
39
+ | `--init-git` | Initialize a local Git repository after scaffolding |
40
+ | `--no-init-git` | Do not prompt for or initialize a local Git repository |
39
41
  | `--registry <url>` | Custom npm registry URL |
40
42
  | `--verdaccio` | Use local Verdaccio registry (http://localhost:4873) |
41
43
  | `--help`, `-h` | Show help |
@@ -61,6 +63,9 @@ npx create-mercato-app my-store --registry http://localhost:4873
61
63
 
62
64
  # Create a new app without the agentic setup wizard
63
65
  npx create-mercato-app my-store --skip-agentic-setup
66
+
67
+ # Create a new app and initialize a local Git repository
68
+ npx create-mercato-app my-store --init-git
64
69
  ```
65
70
 
66
71
  ## Ready App Behavior
@@ -73,6 +78,34 @@ npx create-mercato-app my-store --skip-agentic-setup
73
78
  - Imported ready apps skip the interactive agentic setup wizard; if you want agentic tooling later, run `yarn mercato agentic:init` inside the generated app
74
79
  - Imported ready apps must not contain `.template` files; the scaffold fails closed if template files are found
75
80
 
81
+ ## Git And GitHub
82
+
83
+ Interactive scaffolds ask whether to initialize a local Git repository after the app is created. Non-interactive scaffolds skip Git initialization unless `--init-git` is passed.
84
+
85
+ To publish the generated app to GitHub after creation:
86
+
87
+ ```bash
88
+ cd my-app
89
+ git add -A
90
+ git commit -m "Initial commit"
91
+ gh repo create --source=. --remote=origin --push
92
+ ```
93
+
94
+ If you did not initialize Git during scaffolding, run this first:
95
+
96
+ ```bash
97
+ git init -b main
98
+ ```
99
+
100
+ Without GitHub CLI, create an empty repository on GitHub and connect it manually:
101
+
102
+ ```bash
103
+ git remote add origin https://github.com/<owner>/<repo>.git
104
+ git push -u origin main
105
+ ```
106
+
107
+ The standalone dev splash also exposes a GitHub publishing panel after `yarn dev` when `gh` is installed.
108
+
76
109
  ## After Creating A Bare Scaffold
77
110
 
78
111
  1. Navigate to your app directory:
@@ -152,7 +152,7 @@ Register in `src/modules.ts`: `{ id: '<id>', from: '@app' }`
152
152
  8. **After the user adds a new module, offer to trim classic mode.** A fresh `create-mercato-app` scaffold enables every built-in module (classic mode). Once the user has added their own custom module, the defaults are usually dead weight. **Ask the user** (via a short `AskUserQuestion`) whether they want to disable built-in modules that are not needed for their project. If they say yes, invoke the `trim-unused-modules` skill — do NOT hand-craft the slimdown inside the AGENTS.md reading flow. If they say no, preserve classic mode silently.
153
153
 
154
154
  **Dashboards fallback rule.** When the user (or the `trim-unused-modules` skill) disables the `dashboards` module, you MUST update `src/app/(backend)/backend/page.tsx` so it no longer renders `<DashboardScreen />`. Replace the dashboard render with a `redirect(...)` to the first enabled backend page for the current user — preferring pages already registered in the main sidebar group and respecting the admin/superadmin role of the caller. Otherwise `/backend` will crash at build or request time because the removed module no longer ships `DashboardScreen`. Always fall back to `/backend/profile` only if no other backend page is available.
155
- 9. **New features MUST be visible to admin/superadmin immediately.** Every time you add a new feature ID (e.g. `my_module.view`, `my_module.manage`) to `src/modules/<module>/acl.ts`, you MUST also (a) add that feature to `defaultRoleFeatures` in the same module's `setup.ts` so the admin and superadmin roles get it on every tenant setup; and (b) run `yarn mercato auth sync-role-acls --all-tenants` so existing tenants pick up the new feature without a reinstall. Do this automatically unless the user has explicitly said otherwise — the user should see the features you are building, not stare at a blank admin because their role is missing a grant. Feature IDs are FROZEN once shipped; if a rename is required, add the new ID alongside, grant it, and keep the old one as a deprecated alias.
155
+ 9. **New features MUST be visible to default roles immediately.** Every time you add a new feature ID (e.g. `my_module.view`, `my_module.manage`) to `src/modules/<module>/acl.ts`, you MUST also (a) add that feature to `defaultRoleFeatures` in the same module's `setup.ts` so the admin role and any other appropriate default roles get it on every tenant setup; and (b) run `yarn mercato auth sync-role-acls` so existing tenants pick up the new feature without a reinstall. Use `--tenant <tenantId>` only when the user asks to target one tenant. Do this automatically unless the user has explicitly said otherwise — the user should see the features you are building, not stare at a blank admin because their role is missing a grant. Feature IDs are FROZEN once shipped; if a rename is required, add the new ID alongside, grant it, and keep the old one as a deprecated alias.
156
156
  10. **Strict Design System alignment for every UI change.** Any UI you add or edit MUST use the Open Mercato design system components and tokens. No hardcoded Tailwind status colors (`text-red-500`, `bg-green-100`, etc.) — use semantic tokens (`text-status-error-text`, `bg-status-success-bg`, …). No arbitrary text sizes (`text-[11px]`, `text-[13px]`) — use the Tailwind scale (`text-xs`, `text-sm`, `text-base`, `text-lg`, `text-xl`, `text-2xl`) or the `text-overline` token for 11px uppercase labels. In PAGE BODY UI, use `lucide-react` icons (never inline `<svg>`). Use `StatusBadge` for entity status, `Alert` for inline feedback, `FormField` for standalone form inputs, `SectionHeader` for detail-page section headings, `CollapsibleSection` for collapsible regions, `LoadingMessage`/`Spinner`/`DataLoader` for async states, and `EmptyState` (or DataTable's `emptyState` prop) for empty lists. For list pages, follow `.ai/skills/backend-ui-design/SKILL.md` and prefer the `DataTable` host pattern shown there (`entityId`, `apiPath`, stable `extensionTableId`, and explicit pagination props when you own the data source). Every dialog MUST support `Cmd/Ctrl+Enter` to submit and `Escape` to cancel. Every icon-only button MUST have an `aria-label`. These rules apply to `src/modules/<module>/backend/**` and `src/modules/<module>/frontend/**` alike.
157
157
  11. **BEFORE writing ANY code**, you MUST:
158
158
  - Match your task against the **Task → Context Map** above
@@ -152,7 +152,7 @@ Register in `src/modules.ts`: `{ id: '<id>', from: '@app' }`
152
152
  8. **After the user adds a new module, offer to trim classic mode.** A fresh `create-mercato-app` scaffold enables every built-in module (classic mode). Once the user has added their own custom module, the defaults are usually dead weight. **Ask the user** (via a short `AskUserQuestion`) whether they want to disable built-in modules that are not needed for their project. If they say yes, invoke the `trim-unused-modules` skill — do NOT hand-craft the slimdown inside the AGENTS.md reading flow. If they say no, preserve classic mode silently.
153
153
 
154
154
  **Dashboards fallback rule.** When the user (or the `trim-unused-modules` skill) disables the `dashboards` module, you MUST update `src/app/(backend)/backend/page.tsx` so it no longer renders `<DashboardScreen />`. Replace the dashboard render with a `redirect(...)` to the first enabled backend page for the current user — preferring pages already registered in the main sidebar group and respecting the admin/superadmin role of the caller. Otherwise `/backend` will crash at build or request time because the removed module no longer ships `DashboardScreen`. Always fall back to `/backend/profile` only if no other backend page is available.
155
- 9. **New features MUST be visible to admin/superadmin immediately.** Every time you add a new feature ID (e.g. `my_module.view`, `my_module.manage`) to `src/modules/<module>/acl.ts`, you MUST also (a) add that feature to `defaultRoleFeatures` in the same module's `setup.ts` so the admin and superadmin roles get it on every tenant setup; and (b) run `yarn mercato auth sync-role-acls --all-tenants` so existing tenants pick up the new feature without a reinstall. Do this automatically unless the user has explicitly said otherwise — the user should see the features you are building, not stare at a blank admin because their role is missing a grant. Feature IDs are FROZEN once shipped; if a rename is required, add the new ID alongside, grant it, and keep the old one as a deprecated alias.
155
+ 9. **New features MUST be visible to default roles immediately.** Every time you add a new feature ID (e.g. `my_module.view`, `my_module.manage`) to `src/modules/<module>/acl.ts`, you MUST also (a) add that feature to `defaultRoleFeatures` in the same module's `setup.ts` so the admin role and any other appropriate default roles get it on every tenant setup; and (b) run `yarn mercato auth sync-role-acls` so existing tenants pick up the new feature without a reinstall. Use `--tenant <tenantId>` only when the user asks to target one tenant. Do this automatically unless the user has explicitly said otherwise — the user should see the features you are building, not stare at a blank admin because their role is missing a grant. Feature IDs are FROZEN once shipped; if a rename is required, add the new ID alongside, grant it, and keep the old one as a deprecated alias.
156
156
  10. **Strict Design System alignment for every UI change.** Any UI you add or edit MUST use the Open Mercato design system components and tokens. No hardcoded Tailwind status colors (`text-red-500`, `bg-green-100`, etc.) — use semantic tokens (`text-status-error-text`, `bg-status-success-bg`, …). No arbitrary text sizes (`text-[11px]`, `text-[13px]`) — use the Tailwind scale (`text-xs`, `text-sm`, `text-base`, `text-lg`, `text-xl`, `text-2xl`) or the `text-overline` token for 11px uppercase labels. In PAGE BODY UI, use `lucide-react` icons (never inline `<svg>`). Use `StatusBadge` for entity status, `Alert` for inline feedback, `FormField` for standalone form inputs, `SectionHeader` for detail-page section headings, `CollapsibleSection` for collapsible regions, `LoadingMessage`/`Spinner`/`DataLoader` for async states, and `EmptyState` (or DataTable's `emptyState` prop) for empty lists. For list pages, follow `.ai/skills/backend-ui-design/SKILL.md` and prefer the `DataTable` host pattern shown there (`entityId`, `apiPath`, stable `extensionTableId`, and explicit pagination props when you own the data source). Every dialog MUST support `Cmd/Ctrl+Enter` to submit and `Escape` to cancel. Every icon-only button MUST have an `aria-label`. These rules apply to `src/modules/<module>/backend/**` and `src/modules/<module>/frontend/**` alike.
157
157
  11. **BEFORE writing ANY code**, you MUST:
158
158
  - Match your task against the **Task → Context Map** above
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  // src/index.ts
5
5
  import { createInterface } from "node:readline";
6
+ import { spawnSync } from "node:child_process";
6
7
  import { basename as basename2, dirname as dirname6, join as join7, resolve } from "node:path";
7
8
  import { existsSync as existsSync6, mkdirSync as mkdirSync6, readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync2, writeFileSync as writeFileSync7, copyFileSync as copyFileSync5 } from "node:fs";
8
9
  import { fileURLToPath as fileURLToPath6 } from "node:url";
@@ -322,7 +323,11 @@ var EMPTY_MODULES = [
322
323
  { id: "configs", from: CORE },
323
324
  { id: "entities", from: CORE },
324
325
  { id: "query_index", from: CORE },
325
- { id: "api_docs", from: CORE }
326
+ { id: "api_docs", from: CORE },
327
+ { id: "audit_logs", from: CORE },
328
+ { id: "notifications", from: CORE },
329
+ { id: "dashboards", from: CORE },
330
+ { id: "events", from: EVENTS }
326
331
  ];
327
332
  var STARTER_PRESETS = {
328
333
  classic: {
@@ -352,10 +357,7 @@ var STARTER_PRESETS = {
352
357
  add: [
353
358
  { id: "customers", from: CORE },
354
359
  { id: "dictionaries", from: CORE },
355
- { id: "feature_toggles", from: CORE },
356
- { id: "notifications", from: CORE },
357
- { id: "dashboards", from: CORE },
358
- { id: "events", from: EVENTS }
360
+ { id: "feature_toggles", from: CORE }
359
361
  ]
360
362
  },
361
363
  ui: { startPageVariant: "crm", hideDemoLinks: true },
@@ -845,7 +847,9 @@ ${pc.bold("Arguments:")}
845
847
  ${pc.bold("Options:")}
846
848
  --app <name> Bootstrap an official ready app from open-mercato/ready-app-<name>
847
849
  --app-url <url> Bootstrap a ready app from a GitHub repository URL
848
- --preset <id> Starter preset: classic (default), empty, or crm
850
+ --preset <id> Starter preset: classic, empty, or crm (omit to choose interactively)
851
+ --init-git Initialize a local Git repository after scaffolding
852
+ --no-init-git Do not prompt for or initialize a local Git repository
849
853
  --skip-agentic-setup Skip the interactive agentic setup wizard
850
854
  --registry <url> Custom npm registry URL
851
855
  --verdaccio Use local Verdaccio registry (http://localhost:4873)
@@ -854,8 +858,10 @@ ${pc.bold("Options:")}
854
858
 
855
859
  ${pc.bold("Examples:")}
856
860
  npx create-mercato-app my-store
861
+ npx create-mercato-app my-store --preset classic
857
862
  npx create-mercato-app my-store --preset empty
858
863
  npx create-mercato-app my-store --preset crm
864
+ npx create-mercato-app my-store --init-git
859
865
  npx create-mercato-app my-prm --app prm
860
866
  npx create-mercato-app my-marketplace --app-url https://github.com/some-agency/ready-app-marketplace
861
867
  npx create-mercato-app my-store --verdaccio
@@ -878,6 +884,7 @@ function parseArgs(args) {
878
884
  appUrl: void 0,
879
885
  preset: void 0,
880
886
  registry: void 0,
887
+ initGit: void 0,
881
888
  skipAgenticSetup: false,
882
889
  verdaccio: false,
883
890
  help: false,
@@ -892,6 +899,10 @@ function parseArgs(args) {
892
899
  options.version = true;
893
900
  } else if (arg === "--skip-agentic-setup") {
894
901
  options.skipAgenticSetup = true;
902
+ } else if (arg === "--init-git") {
903
+ options.initGit = true;
904
+ } else if (arg === "--no-init-git") {
905
+ options.initGit = false;
895
906
  } else if (arg === "--verdaccio") {
896
907
  options.verdaccio = true;
897
908
  } else if (arg === "--registry") {
@@ -912,6 +923,143 @@ function parseArgs(args) {
912
923
  }
913
924
  return { appName, options };
914
925
  }
926
+ var PRESET_PROMPT_OPTIONS = [
927
+ { number: "1", id: "classic", label: "Classic (default)", hint: "full demo-ready starter" },
928
+ { number: "2", id: "empty", label: "Empty", hint: "minimal builder-ready baseline" },
929
+ { number: "3", id: "crm", label: "CRM", hint: "minimal CRM starter" }
930
+ ];
931
+ function normalizePresetAnswer(answer) {
932
+ const normalized = answer.trim().toLowerCase();
933
+ if (!normalized) return DEFAULT_PRESET_ID;
934
+ const selected = PRESET_PROMPT_OPTIONS.find((option) => option.number === normalized || option.id === normalized);
935
+ if (!selected) {
936
+ throw new Error(`Unknown preset "${answer}". Choose classic, empty, or crm.`);
937
+ }
938
+ return selected.id;
939
+ }
940
+ async function promptForStarterPreset(ask) {
941
+ console.log("");
942
+ console.log("\u{1F9E9} Starter module setup");
943
+ console.log("");
944
+ console.log(" Which starter module set do you want?");
945
+ console.log("");
946
+ for (const option of PRESET_PROMPT_OPTIONS) {
947
+ console.log(` ${option.number}. ${option.label} - ${option.hint}`);
948
+ }
949
+ console.log("");
950
+ while (true) {
951
+ const answer = await ask(" Enter number [1]: ");
952
+ try {
953
+ return normalizePresetAnswer(answer);
954
+ } catch (error) {
955
+ const message = error instanceof Error ? error.message : String(error);
956
+ console.log(pc.yellow(` ${message}`));
957
+ }
958
+ }
959
+ }
960
+ async function resolveStarterPresetId(options, readyAppSource) {
961
+ if (options.preset) {
962
+ if (!VALID_PRESET_IDS.includes(options.preset)) {
963
+ throw new Error(`Unknown preset "${options.preset}". Valid presets: ${VALID_PRESET_IDS.join(", ")}`);
964
+ }
965
+ if (options.preset !== DEFAULT_PRESET_ID && readyAppSource) {
966
+ throw new Error(
967
+ `--preset ${options.preset} cannot be combined with --app or --app-url. Presets apply only to the built-in template.`
968
+ );
969
+ }
970
+ return options.preset;
971
+ }
972
+ if (readyAppSource) return DEFAULT_PRESET_ID;
973
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
974
+ console.log(pc.dim(`No starter preset selected; using ${DEFAULT_PRESET_ID}.`));
975
+ console.log("");
976
+ return DEFAULT_PRESET_ID;
977
+ }
978
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
979
+ const ask = (question) => new Promise((resolveAnswer) => rl.question(question, (answer) => resolveAnswer(answer.trim())));
980
+ try {
981
+ return await promptForStarterPreset(ask);
982
+ } finally {
983
+ rl.close();
984
+ }
985
+ }
986
+ function normalizeGitInitAnswer(answer) {
987
+ const normalized = answer.trim().toLowerCase();
988
+ if (!normalized) return true;
989
+ if (["y", "yes"].includes(normalized)) return true;
990
+ if (["n", "no"].includes(normalized)) return false;
991
+ throw new Error(`Unknown answer "${answer}". Choose yes or no.`);
992
+ }
993
+ async function promptForGitInitialization(ask) {
994
+ console.log("");
995
+ console.log("\u{1F331} Git repository setup");
996
+ console.log("");
997
+ console.log(" Initialize a local Git repository now?");
998
+ console.log(pc.dim(" You can publish it to GitHub later with `gh repo create --source=. --remote=origin --push`."));
999
+ console.log("");
1000
+ while (true) {
1001
+ const answer = await ask(" Initialize Git repository? [Y/n]: ");
1002
+ try {
1003
+ return normalizeGitInitAnswer(answer);
1004
+ } catch (error) {
1005
+ const message = error instanceof Error ? error.message : String(error);
1006
+ console.log(pc.yellow(` ${message}`));
1007
+ }
1008
+ }
1009
+ }
1010
+ async function resolveGitInitialization(options) {
1011
+ if (typeof options.initGit === "boolean") return options.initGit;
1012
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
1013
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1014
+ const ask = (question) => new Promise((resolveAnswer) => rl.question(question, (answer) => resolveAnswer(answer.trim())));
1015
+ try {
1016
+ return await promptForGitInitialization(ask);
1017
+ } finally {
1018
+ rl.close();
1019
+ }
1020
+ }
1021
+ function runGitCommand(targetDir, args) {
1022
+ return spawnSync("git", args, {
1023
+ cwd: targetDir,
1024
+ encoding: "utf8",
1025
+ stdio: ["ignore", "pipe", "pipe"],
1026
+ shell: process.platform === "win32"
1027
+ });
1028
+ }
1029
+ function initializeGitRepository(targetDir) {
1030
+ if (existsSync6(join7(targetDir, ".git"))) {
1031
+ return { status: "already-initialized" };
1032
+ }
1033
+ const initWithBranch = runGitCommand(targetDir, ["init", "-b", "main"]);
1034
+ if (initWithBranch.status === 0) {
1035
+ return { status: "initialized" };
1036
+ }
1037
+ const fallbackInit = runGitCommand(targetDir, ["init"]);
1038
+ if (fallbackInit.status !== 0) {
1039
+ const message = fallbackInit.stderr || initWithBranch.stderr || fallbackInit.stdout || initWithBranch.stdout;
1040
+ return { status: "failed", message: message.trim() || "git init failed" };
1041
+ }
1042
+ const branchRename = runGitCommand(targetDir, ["branch", "-M", "main"]);
1043
+ if (branchRename.status !== 0) {
1044
+ const message = branchRename.stderr || branchRename.stdout;
1045
+ return { status: "failed", message: message.trim() || "git branch -M main failed" };
1046
+ }
1047
+ return { status: "initialized" };
1048
+ }
1049
+ async function maybeInitializeGitRepository(targetDir, options) {
1050
+ const shouldInitialize = await resolveGitInitialization(options);
1051
+ if (!shouldInitialize) return { status: "skipped" };
1052
+ const result = initializeGitRepository(targetDir);
1053
+ if (result.status === "initialized") {
1054
+ console.log(pc.green("Initialized local Git repository."));
1055
+ } else if (result.status === "already-initialized") {
1056
+ console.log(pc.dim("Git repository already initialized."));
1057
+ } else if (result.status === "failed") {
1058
+ console.log(pc.yellow(`Could not initialize Git repository: ${result.message}`));
1059
+ }
1060
+ console.log("");
1061
+ return result;
1062
+ }
915
1063
  function buildRegistryConfig(registryUrl) {
916
1064
  let parsedRegistryUrl;
917
1065
  try {
@@ -1047,6 +1195,22 @@ function printImportedReadyAppNextSteps(appName) {
1047
1195
  console.log(pc.dim("If you want agentic tooling in the imported app later, run `yarn mercato agentic:init` inside it."));
1048
1196
  console.log("");
1049
1197
  }
1198
+ function printGitHubSyncInstructions(gitResult) {
1199
+ console.log("Optional GitHub publish:");
1200
+ console.log("");
1201
+ if (gitResult.status === "skipped" || gitResult.status === "failed") {
1202
+ console.log(pc.cyan(" git init -b main"));
1203
+ }
1204
+ console.log(pc.cyan(" git add -A"));
1205
+ console.log(pc.cyan(' git commit -m "Initial commit"'));
1206
+ console.log(pc.dim(" # With GitHub CLI:"));
1207
+ console.log(pc.cyan(" gh repo create --source=. --remote=origin --push"));
1208
+ console.log(pc.dim(" # Or create an empty GitHub repo in the browser, then:"));
1209
+ console.log(pc.cyan(" git remote add origin https://github.com/<owner>/<repo>.git"));
1210
+ console.log(pc.cyan(" git push -u origin main"));
1211
+ console.log(pc.dim(" # You can also publish from the standalone dev splash after `yarn dev`."));
1212
+ console.log("");
1213
+ }
1050
1214
  async function main(argv = process.argv.slice(2)) {
1051
1215
  const { appName: appNameArg, options } = parseArgs(argv);
1052
1216
  if (options.help) {
@@ -1070,15 +1234,7 @@ async function main(argv = process.argv.slice(2)) {
1070
1234
  throw new Error(`Directory "${appName}" already exists`);
1071
1235
  }
1072
1236
  const readyAppSource = resolveReadyAppSource(options, PACKAGE_VERSION);
1073
- const presetId = options.preset ?? DEFAULT_PRESET_ID;
1074
- if (!VALID_PRESET_IDS.includes(presetId)) {
1075
- throw new Error(`Unknown preset "${presetId}". Valid presets: ${VALID_PRESET_IDS.join(", ")}`);
1076
- }
1077
- if (presetId !== DEFAULT_PRESET_ID && readyAppSource) {
1078
- throw new Error(
1079
- `--preset ${presetId} cannot be combined with --app or --app-url. Presets apply only to the built-in template.`
1080
- );
1081
- }
1237
+ const presetId = await resolveStarterPresetId(options, readyAppSource);
1082
1238
  const registryConfig = options.verdaccio ? buildRegistryConfig("http://localhost:4873") : options.registry ? buildRegistryConfig(options.registry) : "";
1083
1239
  console.log("");
1084
1240
  console.log(pc.bold(`Creating a new Open Mercato app in ${pc.cyan(targetDir)}`));
@@ -1098,12 +1254,16 @@ async function main(argv = process.argv.slice(2)) {
1098
1254
  }
1099
1255
  console.log(pc.green("Success!") + ` Created ${pc.bold(appName)}`);
1100
1256
  console.log("");
1257
+ if (!readyAppSource) {
1258
+ await maybeRunAgenticSetup(targetDir, options.skipAgenticSetup);
1259
+ }
1260
+ const gitResult = await maybeInitializeGitRepository(targetDir, options);
1101
1261
  if (readyAppSource) {
1102
1262
  printImportedReadyAppNextSteps(appName);
1103
1263
  } else {
1104
- await maybeRunAgenticSetup(targetDir, options.skipAgenticSetup);
1105
1264
  printTemplateNextSteps(appName);
1106
1265
  }
1266
+ printGitHubSyncInstructions(gitResult);
1107
1267
  console.log(pc.dim("For more information, visit https://github.com/open-mercato/open-mercato"));
1108
1268
  console.log("");
1109
1269
  }
@@ -1116,5 +1276,6 @@ if (isEntrypoint) {
1116
1276
  });
1117
1277
  }
1118
1278
  export {
1119
- main
1279
+ main,
1280
+ normalizePresetAnswer
1120
1281
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mercato-app",
3
- "version": "0.5.1-develop.3036.f02c281f23",
3
+ "version": "0.5.1-develop.3045.b4b3320cc2",
4
4
  "type": "module",
5
5
  "description": "Create a new Open Mercato application",
6
6
  "main": "./dist/index.js",
@@ -13,7 +13,10 @@ const STATIC_TEST_IGNORES = [
13
13
  `${normalizePath(path.join(projectRoot, '.claude'))}/**`,
14
14
  `${normalizePath(path.join(projectRoot, '.codex'))}/**`,
15
15
  ];
16
- const discoveredSpecs = discoverIntegrationSpecFiles(projectRoot, path.join(projectRoot, '.ai', 'qa', 'tests'));
16
+ // `.ai/qa/tests` is retained for the shared Playwright config only.
17
+ // Executable specs must live in module-local `__integration__` folders.
18
+ const disabledLegacyIntegrationRoot = path.join(projectRoot, '.ai', 'qa', 'tests', '__legacy_disabled__');
19
+ const discoveredSpecs = discoverIntegrationSpecFiles(projectRoot, disabledLegacyIntegrationRoot);
17
20
  const discoveredSpecPaths = discoveredSpecs.map((entry) => entry.path);
18
21
 
19
22
  export default defineConfig({
@@ -218,58 +218,81 @@ TENANT_DATA_ENCRYPTION_KEY=
218
218
  # VAULT_REQUEST_TIMEOUT_MS=1000
219
219
 
220
220
  # ============================================================================
221
- # Embedding Provider Configuration (for vector search)
221
+ # AI Providers, OCR, and Vector Search
222
222
  # ============================================================================
223
- # Vector search requires ONE embedding provider to be configured.
224
- # Automatic vector indexing ships disabled by default in this example env.
225
- # Enable it by setting OM_DISABLE_VECTOR_SEARCH_AUTOINDEXING=false or removing this line,
226
- # then trigger a vector reindex from Settings > Search or the CLI.
223
+ # New apps ship with AI providers disabled. Set the provider id plus its API
224
+ # key below before using AI assistants, AI OCR, or vector embeddings.
225
+ # Docs:
226
+ # - AI Assistant: https://docs.openmercato.com/framework/ai-assistant/overview
227
+ # - AI settings: https://docs.openmercato.com/framework/ai-assistant/settings
228
+ # - Search/vector embeddings: https://docs.openmercato.com/user-guide/search
229
+
230
+ # AI Assistant provider selection. Built-in ids: anthropic, google, openai,
231
+ # deepinfra, groq, together, fireworks, azure, litellm, ollama.
232
+ OPENCODE_PROVIDER=
233
+
234
+ # Optional: override the selected assistant model.
235
+ # Examples:
236
+ # anthropic/claude-sonnet-4-6-20260107
237
+ # openai/gpt-5-mini
238
+ # deepinfra/zai-org/GLM-5.1
239
+ # OPENCODE_MODEL=
240
+
241
+ # Native LLM providers. Leave blank until you intentionally configure one.
242
+ # anthropic: set OPENCODE_PROVIDER=anthropic + ANTHROPIC_API_KEY
243
+ ANTHROPIC_API_KEY=
244
+ OPENCODE_ANTHROPIC_API_KEY=
245
+ # openai: set OPENCODE_PROVIDER=openai + OPENAI_API_KEY
246
+ OPENAI_API_KEY=
247
+ OPENCODE_OPENAI_API_KEY=
248
+ # google: set OPENCODE_PROVIDER=google + GOOGLE_GENERATIVE_AI_API_KEY
249
+ GOOGLE_GENERATIVE_AI_API_KEY=
250
+ OPENCODE_GOOGLE_API_KEY=
251
+
252
+ # OpenAI-compatible LLM providers.
253
+ # deepinfra/groq/together/fireworks: set OPENCODE_PROVIDER to the provider id
254
+ # and fill the matching *_API_KEY below.
255
+ DEEPINFRA_API_KEY=
256
+ GROQ_API_KEY=
257
+ TOGETHER_API_KEY=
258
+ FIREWORKS_API_KEY=
259
+ # azure/litellm/ollama: set OPENCODE_PROVIDER to the provider id and fill
260
+ # the API key plus base URL when that backend requires one.
261
+ AZURE_OPENAI_API_KEY=
262
+ AZURE_OPENAI_BASE_URL=
263
+ LITELLM_API_KEY=
264
+ LITELLM_BASE_URL=
265
+ OLLAMA_API_KEY=
266
+ OLLAMA_BASE_URL=
267
+
268
+ # Embedding-only providers for vector search.
269
+ MISTRAL_API_KEY=
270
+ COHERE_API_KEY=
271
+ AWS_ACCESS_KEY_ID=
272
+ AWS_SECRET_ACCESS_KEY=
273
+ AWS_REGION=
274
+
275
+ # Vector search automatic indexing ships disabled by default. After setting an
276
+ # embedding provider key above, enable this and trigger a vector reindex from
277
+ # Settings > Search or the CLI.
227
278
  OM_DISABLE_VECTOR_SEARCH_AUTOINDEXING=true
228
279
  # Legacy alias still supported: DISABLE_VECTOR_SEARCH_AUTOINDEXING=1
229
- # OpenAI is the default provider if no explicit configuration is set.
280
+ # Optional request cap for embedding providers (default: 3000 ms).
281
+ # VECTOR_EMBEDDING_TIMEOUT_MS=3000
230
282
 
231
- # OpenAI (default embedding provider)
232
- # Models: text-embedding-3-small (1536 dim), text-embedding-3-large (3072 dim)
233
- OPENAI_API_KEY=your_openai_api_key_here
234
-
235
- # Google Generative AI
236
- # Models: text-embedding-004 (768 dim), embedding-001 (768 dim)
237
- # GOOGLE_GENERATIVE_AI_API_KEY=
238
-
239
- # Mistral AI
240
- # Models: mistral-embed (1024 dim)
241
- # MISTRAL_API_KEY=
242
-
243
- # Cohere
244
- # Models: embed-english-v3.0 (1024 dim), embed-multilingual-v3.0 (1024 dim)
245
- # COHERE_API_KEY=
246
-
247
- # Amazon Bedrock
248
- # Models: amazon.titan-embed-text-v2:0 (1024 dim), cohere.embed-english-v3 (1024 dim)
249
- # AWS_ACCESS_KEY_ID=
250
- # AWS_SECRET_ACCESS_KEY=
251
- # AWS_REGION=us-east-1
252
-
253
- # Ollama (Local/Self-hosted)
254
- # Models: nomic-embed-text (768 dim), mxbai-embed-large (1024 dim), all-minilm (384 dim)
255
- # Default: http://localhost:11434 (no /api suffix needed)
256
- # OLLAMA_BASE_URL=http://localhost:11434
257
-
258
- # ============================================================================
259
- # OCR (Optical Character Recognition) Configuration
260
- # ============================================================================
261
- # Default OCR model for image and PDF text extraction (optional)
262
- # Falls back to 'gpt-4o' if not specified. Can be overridden per partition.
263
- # Supported models: gpt-4o, gpt-4o-mini
283
+ # OCR model for image and PDF text extraction. OCR currently requires
284
+ # OPENAI_API_KEY to be set above.
264
285
  OCR_MODEL=gpt-4o
265
-
266
- # Custom OCR prompt (optional, advanced)
267
- # Override the default prompt sent to the LLM for text extraction
268
286
  # OCR_DEFAULT_PROMPT="Extract all text content from this image..."
269
-
270
- # Enable/disable OCR for new partitions by default (default: true)
271
287
  # OPENMERCATO_DEFAULT_ATTACHMENT_OCR_ENABLED=true
272
288
 
289
+ # Outbound request timeouts for the AI Assistant / OpenCode stack.
290
+ # OPENCODE_REQUEST_TIMEOUT_MS=30000
291
+ # OPENCODE_SSE_CONNECT_TIMEOUT_MS=15000
292
+ # OPENCODE_SEND_MESSAGE_TIMEOUT_MS=
293
+ # AI_API_REQUEST_TIMEOUT_MS=30000
294
+ # AI_OPENAPI_FETCH_TIMEOUT_MS=10000
295
+
273
296
  # ============================================================================
274
297
  # Stripe Integration Preconfiguration
275
298
  # ============================================================================
@@ -360,28 +383,6 @@ OCR_MODEL=gpt-4o
360
383
  # OM_INTEGRATION_AKENEO_CATEGORIES_SETTINGS_JSON=
361
384
  # OM_INTEGRATION_AKENEO_ATTRIBUTES_SETTINGS_JSON=
362
385
 
363
- # ============================================================================
364
- # OpenCode AI Assistant Configuration
365
- # ============================================================================
366
- # OpenCode handles AI requests for the AI Assistant feature.
367
- # Configure the provider and set the corresponding API key.
368
-
369
- # Provider selection: anthropic, openai, or google (default: anthropic)
370
- OPENCODE_PROVIDER=anthropic
371
-
372
- # Optional: Override the default model for the selected provider
373
- # Default models by provider:
374
- # - anthropic: claude-haiku-4-5-20251001
375
- # - openai: gpt-4o-mini
376
- # - google: gemini-2.0-flash
377
- # OPENCODE_MODEL=anthropic/claude-sonnet-4-20250514
378
-
379
- # Provider API keys (set the one matching OPENCODE_PROVIDER)
380
- # Both ANTHROPIC_API_KEY and OPENCODE_ANTHROPIC_API_KEY are accepted (same for other providers)
381
- ANTHROPIC_API_KEY=your_anthropic_api_key_here
382
- # OPENAI_API_KEY=your_openai_api_key_here
383
- # GOOGLE_GENERATIVE_AI_API_KEY=your_google_api_key_here
384
-
385
386
  # ============================================================================
386
387
  # InboxOps Configuration (email-to-ERP agent)
387
388
  # ============================================================================
@@ -205,6 +205,74 @@ Practical consequences:
205
205
  4. Re-apply (`yarn db:migrate`) and commit.
206
206
  - Never hand-edit historical migrations that have shipped; add a **new** migration that performs the correction instead.
207
207
 
208
+ ## AI Assistant — adding agents, tools, UI parts, and overrides
209
+
210
+ Standalone apps consume the AI framework from `@open-mercato/ai-assistant` (in `node_modules/`). The same conventions used in the monorepo apply here:
211
+
212
+ - Add a typed agent for a new module by creating `<module>/ai-agents.ts` + `<module>/ai-tools.ts` at the **module root**. Run `yarn generate` after.
213
+ - Add inline UI widgets (record cards, custom server-emitted parts) per the [UI Parts guide](https://docs.openmercato.dev/framework/ai-assistant/ui-parts).
214
+ - Replace or disable an agent / tool that another module shipped through three paths: extra `aiAgentOverrides` / `aiToolOverrides` exports on the existing `<module>/ai-agents.ts` / `<module>/ai-tools.ts` (per-module), inline on a `ModuleEntry` in `src/modules.ts` (per-app), or programmatically via `applyAiAgentOverrides({...})` / `applyAiToolOverrides({...})` from `@open-mercato/ai-assistant`. `null` disables; a definition replaces. Resolution order is **programmatic → modules.ts → file-based → base**.
215
+
216
+ Example per-module override (preferred when the override should ship with a module):
217
+
218
+ ```ts
219
+ // src/modules/<my_module>/ai-agents.ts
220
+ import type {
221
+ AiAgentDefinition,
222
+ AiAgentOverridesMap,
223
+ } from '@open-mercato/ai-assistant'
224
+ import myAgent from './agents/my-merchandising-agent'
225
+
226
+ export const aiAgents: AiAgentDefinition[] = [/* ...your module's own agents */]
227
+
228
+ export const aiAgentOverrides: AiAgentOverridesMap = {
229
+ 'catalog.merchandising_assistant': myAgent, // replace
230
+ 'catalog.catalog_assistant': null, // disable
231
+ }
232
+ ```
233
+
234
+ Example `modules.ts` inline override (preferred for app-level decisions that don't deserve a fake module). AI lives at `overrides.ai.*`; other domains (routes, events, workers, widgets, …) reuse the same `entry.overrides` umbrella per the [unified spec](https://github.com/open-mercato/open-mercato/blob/main/.ai/specs/2026-05-04-modules-ts-unified-overrides.md) — AI is Phase 1, other domains roll out as separate PRs:
235
+
236
+ ```ts
237
+ // src/modules.ts
238
+ {
239
+ id: 'example',
240
+ from: '@app',
241
+ overrides: {
242
+ ai: {
243
+ agents: { 'catalog.catalog_assistant': null },
244
+ tools: { 'inbox_ops_accept_action': null },
245
+ },
246
+ },
247
+ },
248
+ ```
249
+
250
+ The template's `src/bootstrap.ts` already calls `applyModuleOverridesFromEnabledModules(enabledModules)` from `@open-mercato/shared/modules/overrides` for you. Importing `@open-mercato/ai-assistant` (also in bootstrap) runs the side-effect that registers the AI domain applier with the dispatcher.
251
+
252
+ Example programmatic override at boot (env-driven or test-only):
253
+
254
+ ```ts
255
+ // src/bootstrap.ts (extra)
256
+ import {
257
+ applyAiAgentOverrides,
258
+ applyAiToolOverrides,
259
+ } from '@open-mercato/ai-assistant'
260
+
261
+ // Disable an agent provided by the assistant module by default.
262
+ applyAiAgentOverrides({ 'catalog.catalog_assistant': null })
263
+ // Disable a default tool we do not use.
264
+ applyAiToolOverrides({ 'inbox_ops_accept_action': null })
265
+ ```
266
+
267
+ After editing any `aiAgentOverrides` / `aiToolOverrides` export:
268
+
269
+ ```bash
270
+ yarn generate
271
+ yarn mercato configs cache structural --all-tenants
272
+ ```
273
+
274
+ Refer to the `create-ai-agent` skill (`.ai/skills/create-ai-agent/SKILL.md`) and the public docs at `framework/ai-assistant/overrides` for the full contract, MUST rules, and the resolution order.
275
+
208
276
  ## Disabling the Dashboards Module: Update /backend
209
277
 
210
278
  The default `/backend` page (`src/app/(backend)/backend/page.tsx`) renders `<DashboardScreen />` from `@open-mercato/ui/backend/dashboard`. That component's data flow depends on the `dashboards` module being enabled — widgets, layouts, and the dashboard API routes all live there.
@@ -229,14 +297,14 @@ Do this in the same change where you disable the module — otherwise `/backend`
229
297
 
230
298
  Every time you add a new feature ID (e.g. `my_module.view`, `my_module.manage`) to `src/modules/<module>/acl.ts`, you MUST also:
231
299
 
232
- 1. **Add it to `defaultRoleFeatures`** in the same module's `setup.ts` so admin and superadmin roles receive it on every new tenant setup:
300
+ 1. **Add it to `defaultRoleFeatures`** in the same module's `setup.ts` so the admin role and any other appropriate default roles receive it on every new tenant setup:
233
301
 
234
302
  ```ts
235
303
  // src/modules/<module>/setup.ts
236
304
  export const setup = {
237
305
  defaultRoleFeatures: {
238
- admin: ['my_module.view', 'my_module.manage'],
239
- superadmin: ['my_module.view', 'my_module.manage'],
306
+ admin: ['my_module.view', 'my_module.manage'],
307
+ employee: ['my_module.view'],
240
308
  },
241
309
  // ...
242
310
  }
@@ -245,10 +313,10 @@ Every time you add a new feature ID (e.g. `my_module.view`, `my_module.manage`)
245
313
  2. **Reconcile existing tenants** by running the ACL sync command so existing installs pick up the new feature without a reinstall:
246
314
 
247
315
  ```bash
248
- yarn mercato auth sync-role-acls --all-tenants
316
+ yarn mercato auth sync-role-acls
249
317
  ```
250
318
 
251
- Do this automatically unless the user has explicitly said otherwise. If the current user is an admin or superadmin, they should see the feature you just built — not stare at a blank admin because their role is missing the grant.
319
+ Do this automatically unless the user has explicitly said otherwise. If the current user has a default role that should access the module, they should see the feature you just built — not stare at a blank admin because their role is missing the grant. Use `--tenant <tenantId>` only when the user asks to target one tenant.
252
320
 
253
321
  Feature IDs are FROZEN once shipped (they are stored in the DB as `role_features.feature_id`). If a rename is required, add the new ID, grant it, and keep the old one alongside as a deprecated alias until downstream data can be migrated.
254
322
 
@@ -101,7 +101,7 @@
101
101
  "tailwind-merge": "^3.4.0",
102
102
  "zod": "^4.1.13",
103
103
  "@stripe/react-stripe-js": "^6.2.0",
104
- "@stripe/stripe-js": "^9.2.0",
104
+ "@stripe/stripe-js": "^9.3.1",
105
105
  "@open-mercato/gateway-stripe": "{{PACKAGE_VERSION}}",
106
106
  "@open-mercato/sync-akeneo": "{{PACKAGE_VERSION}}"
107
107
  },
@@ -10,7 +10,6 @@ import { APP_VERSION } from '@open-mercato/shared/lib/version'
10
10
  import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
11
11
  import { PageInjectionBoundary } from '@open-mercato/ui/backend/injection/PageInjectionBoundary'
12
12
  import { DemoFeedbackWidget } from '@/components/DemoFeedbackWidget'
13
- import OrganizationSwitcher from '@/components/OrganizationSwitcher'
14
13
  import { BackendHeaderChrome } from '@/components/BackendHeaderChrome'
15
14
 
16
15
  registerBackendRouteManifests(backendRoutes)
@@ -98,7 +97,6 @@ export default async function BackendLayout({
98
97
  return (
99
98
  <I18nProvider locale={locale} dict={dict}>
100
99
  <AppShell
101
- key={path}
102
100
  productName={productName}
103
101
  email={auth?.email}
104
102
  groups={[]}
@@ -114,7 +112,6 @@ export default async function BackendLayout({
114
112
  organizationId={auth?.orgId ?? null}
115
113
  />
116
114
  )}
117
- mobileSidebarSlot={<OrganizationSwitcher compact />}
118
115
  adminNavApi="/api/auth/admin/nav"
119
116
  version={APP_VERSION}
120
117
  settingsPathPrefixes={collectStaticSettingsPathPrefixes()}
@@ -1,5 +1,5 @@
1
1
  import { NextResponse, type NextRequest } from 'next/server'
2
- import { findApiRouteManifestMatch, registerBackendRouteManifests, registerFrontendRouteManifests, type HttpMethod } from '@open-mercato/shared/modules/registry'
2
+ import { findApiRouteManifestMatch, registerApiRouteManifests, registerBackendRouteManifests, registerFrontendRouteManifests, type HttpMethod } from '@open-mercato/shared/modules/registry'
3
3
  import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
4
4
  import { apiRoutes } from '@/.mercato/generated/api-routes.generated'
5
5
  import { backendRoutes } from '@/.mercato/generated/backend-routes.generated'
@@ -11,6 +11,7 @@ import { bootstrap } from '@/bootstrap'
11
11
  bootstrap()
12
12
  registerBackendRouteManifests(backendRoutes)
13
13
  registerFrontendRouteManifests(frontendRoutes)
14
+ registerApiRouteManifests(apiRoutes)
14
15
  import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
15
16
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
16
17
  import { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
@@ -545,6 +545,14 @@ body[data-column-chooser-open="true"] .om-demo-feedback-floating {
545
545
  display: none !important;
546
546
  }
547
547
 
548
+ /* Hide floating demo feedback button while an AI chat (e.g. the merchandising
549
+ assistant sheet) is open — the FAB sits at z-[60] and otherwise overlaps
550
+ the chat composer's send button. The data attribute is set/cleared by the
551
+ shared <AiChat> component. */
552
+ body[data-ai-chat-open="true"] .om-demo-feedback-floating {
553
+ display: none !important;
554
+ }
555
+
548
556
  /* Hide native scrollbar while keeping scroll behavior — used by sticky sidebars */
549
557
  .scrollbar-hide {
550
558
  scrollbar-width: none;
@@ -28,6 +28,16 @@ registerAppDictionaryLoader(async (locale: Locale): Promise<Record<string, unkno
28
28
  }
29
29
  })
30
30
 
31
+ // modules.ts inline overrides (replace/disable any contract a module
32
+ // presents — AI today, other domains rolling out per the unified spec).
33
+ // Importing @open-mercato/ai-assistant here also runs the side-effect
34
+ // that registers the AI domain applier with the umbrella dispatcher.
35
+ import { enabledModules } from '@/modules'
36
+ import { applyModuleOverridesFromEnabledModules } from '@open-mercato/shared/modules/overrides'
37
+ import '@open-mercato/ai-assistant'
38
+
39
+ applyModuleOverridesFromEnabledModules(enabledModules)
40
+
31
41
  // Generated imports (static - works with bundlers)
32
42
  import { modules } from '@/.mercato/generated/modules.app.generated'
33
43
  import { entities } from '@/.mercato/generated/entities.generated'
@@ -75,6 +75,10 @@ export function BackendHeaderChrome({
75
75
  () => hasVisibleRoute(payload?.groups, '/backend/messages'),
76
76
  [payload?.groups],
77
77
  )
78
+ const showNotifications = React.useMemo(
79
+ () => hasFeature(grantedFeatures, 'notifications.view'),
80
+ [grantedFeatures],
81
+ )
78
82
 
79
83
  return (
80
84
  <>
@@ -89,13 +93,11 @@ export function BackendHeaderChrome({
89
93
  missingConfigMessage={missingConfigMessage}
90
94
  />
91
95
  ) : null}
92
- <div className="hidden lg:contents">
93
- {isReady ? <LazyOrganizationSwitcher /> : null}
94
- </div>
96
+ {isReady ? <LazyOrganizationSwitcher /> : null}
95
97
  {showIntegrationsButton ? <IntegrationsButton /> : null}
96
98
  <SettingsButton />
97
99
  <ProfileDropdown email={email} />
98
- {isReady ? <LazyNotificationBellWrapper /> : null}
100
+ {isReady && showNotifications ? <LazyNotificationBellWrapper /> : null}
99
101
  {isReady && showMessages ? <LazyMessagesIcon /> : null}
100
102
  </>
101
103
  )
@@ -10,6 +10,7 @@ import { Input } from '@open-mercato/ui/primitives/input'
10
10
  import { Textarea } from '@open-mercato/ui/primitives/textarea'
11
11
  import { Checkbox } from '@open-mercato/ui/primitives/checkbox'
12
12
  import { Spinner } from '@open-mercato/ui/primitives/spinner'
13
+ import { useAiDock } from '@open-mercato/ui/ai'
13
14
  import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
14
15
  import { useT } from '@open-mercato/shared/lib/i18n/context'
15
16
 
@@ -41,6 +42,8 @@ type SubmitState = 'idle' | 'sending' | 'sent' | 'error'
41
42
 
42
43
  export function DemoFeedbackWidget({ demoModeEnabled }: { demoModeEnabled: boolean }) {
43
44
  const t = useT()
45
+ const aiDock = useAiDock()
46
+ const aiDockActive = Boolean(aiDock.state.assistant)
44
47
  const [open, setOpen] = useState(false)
45
48
  const [captionIndex, setCaptionIndex] = useState(0)
46
49
  const [mounted, setMounted] = useState(false)
@@ -91,9 +94,13 @@ export function DemoFeedbackWidget({ demoModeEnabled }: { demoModeEnabled: boole
91
94
  return () => clearInterval(interval)
92
95
  }, [])
93
96
 
94
- // Auto-popup after 30s inactivity (once per day, unless suppressed)
97
+ // Auto-popup after 30s inactivity (once per day, unless suppressed).
98
+ // Skip the inactivity prompt entirely while the AI dock is open — the
99
+ // operator is mid-conversation with an assistant and a popup would be
100
+ // disruptive on top of (or competing with) the dock surface.
95
101
  useEffect(() => {
96
102
  if (!demoModeEnabled || !mounted) return
103
+ if (aiDockActive) return
97
104
  if (getCookie(SUPPRESS_COOKIE) === '1') return
98
105
  if (getCookie(SHOWN_TODAY_COOKIE) === todayKey()) return
99
106
 
@@ -126,7 +133,7 @@ export function DemoFeedbackWidget({ demoModeEnabled }: { demoModeEnabled: boole
126
133
  events.forEach((ev) => window.removeEventListener(ev, resetTimer))
127
134
  if (inactivityTimer.current) clearTimeout(inactivityTimer.current)
128
135
  }
129
- }, [demoModeEnabled, mounted])
136
+ }, [demoModeEnabled, mounted, aiDockActive])
130
137
 
131
138
  const handleSubmit = useCallback(async () => {
132
139
  setFieldErrors({})
@@ -208,15 +215,22 @@ export function DemoFeedbackWidget({ demoModeEnabled }: { demoModeEnabled: boole
208
215
  if (otherModalOpen && !open) return null
209
216
 
210
217
  // Brand-gradient floating CTA. Uses brand CSS vars (no hardcoded hex) +
211
- // z-banner token (no z-[60]) so it stays DS-compliant while keeping the
212
- // bespoke 135deg / 0-50-100 gradient that the marketing visual depends on
213
- // (FancyButton's primary variant uses 161.7deg / 0-35.36-70.72 which
214
- // truncates the violet-to-end transition and looks banded).
215
- const floatingButton = (
218
+ // z-banner so it stays DS-compliant while keeping the bespoke 135deg /
219
+ // 0-50-100 gradient that the marketing visual depends on. The text is
220
+ // pinned to `text-black` because the gradient (lime → yellow → violet)
221
+ // is a fixed light surface in BOTH themes — `text-foreground` flips to
222
+ // near-white in dark mode and disappears against the pale gradient.
223
+ // Mirrors the `FancyButton` primitive's `text-white` precedent on its
224
+ // fixed dark gradient. The `om-demo-feedback-floating` class hooks into
225
+ // `body[data-ai-chat-open="true"] .om-demo-feedback-floating` in
226
+ // globals.css so the FAB hides while the AI dock surface is open
227
+ // (anchored on the right edge of the viewport); the same `aiDockActive`
228
+ // gate hides the FAB outright when the dock is mounted in this app shell.
229
+ const floatingButton = aiDockActive ? null : (
216
230
  <button
217
231
  type="button"
218
232
  onClick={() => { setOpen(true); if (submitState === 'sent') resetForm() }}
219
- className="fixed bottom-6 right-6 z-banner flex items-center gap-2 rounded-full px-5 py-3 text-sm font-semibold text-foreground shadow-xl transition-all hover:scale-105 hover:shadow-2xl active:scale-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 animate-[subtle-bounce_2s_ease-in-out_infinite]"
233
+ className="om-demo-feedback-floating fixed bottom-6 right-6 z-banner flex items-center gap-2 rounded-full px-5 py-3 text-sm font-semibold text-black shadow-xl transition-all hover:scale-105 hover:shadow-2xl active:scale-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 animate-[subtle-bounce_2s_ease-in-out_infinite]"
220
234
  style={{
221
235
  backgroundImage: 'linear-gradient(135deg, var(--brand-lime, #B4F372) 0%, #EEFB63 50%, var(--brand-violet, #BC9AFF) 100%)',
222
236
  }}
@@ -234,7 +248,7 @@ export function DemoFeedbackWidget({ demoModeEnabled }: { demoModeEnabled: boole
234
248
 
235
249
  return (
236
250
  <>
237
- {createPortal(floatingButton, document.body)}
251
+ {floatingButton ? createPortal(floatingButton, document.body) : null}
238
252
  <Dialog open={open} onOpenChange={handleOpenChange}>
239
253
  <DialogContent className="sm:max-w-md" onKeyDown={handleKeyDown}>
240
254
  <DialogHeader className="items-center gap-3">
@@ -365,12 +379,15 @@ export function DemoFeedbackWidget({ demoModeEnabled }: { demoModeEnabled: boole
365
379
 
366
380
  <Button
367
381
  type="button"
368
- className="mt-1 w-full gap-2 text-foreground"
382
+ className="mt-1 w-full gap-2 text-black"
369
383
  disabled={submitState === 'sending'}
370
384
  onClick={handleSubmit}
371
385
  style={{
372
386
  // Same brand-gradient as the floating CTA (135deg / 0-50-100,
373
387
  // brand vars instead of hex literals to satisfy DS rules).
388
+ // Pin text to `text-black` — the gradient is a fixed light
389
+ // surface in both themes; `text-foreground` would flip white
390
+ // in dark mode and vanish against the pale gradient.
374
391
  backgroundImage: 'linear-gradient(135deg, var(--brand-lime, #B4F372) 0%, #EEFB63 50%, var(--brand-violet, #BC9AFF) 100%)',
375
392
  }}
376
393
  >
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import '@testing-library/jest-dom'
6
+ import { render, screen } from '@testing-library/react'
7
+ import { BackendHeaderChrome } from '../BackendHeaderChrome'
8
+
9
+ jest.mock('next/dynamic', () => (loader: () => Promise<unknown>) => {
10
+ const source = loader.toString()
11
+ const isOrganizationSwitcher = source.includes('OrganizationSwitcher')
12
+ const Lazy = () =>
13
+ isOrganizationSwitcher ? (
14
+ <div data-testid="lazy-organization-switcher" />
15
+ ) : (
16
+ <div data-testid="lazy-other" />
17
+ )
18
+ return Lazy
19
+ })
20
+
21
+ jest.mock('@open-mercato/ui/backend/BackendChromeProvider', () => ({
22
+ useBackendChrome: () => ({ payload: { groups: [], grantedFeatures: [] }, isReady: true }),
23
+ }))
24
+
25
+ jest.mock('@open-mercato/ui/backend/IntegrationsButton', () => ({
26
+ IntegrationsButton: () => <div data-testid="integrations-button" />,
27
+ }))
28
+
29
+ jest.mock('@open-mercato/ui/backend/ProfileDropdown', () => ({
30
+ ProfileDropdown: () => <div data-testid="profile-dropdown" />,
31
+ }))
32
+
33
+ jest.mock('@open-mercato/ui/backend/SettingsButton', () => ({
34
+ SettingsButton: () => <div data-testid="settings-button" />,
35
+ }))
36
+
37
+ jest.mock('@/components/AiAssistantShellIntegration', () => ({
38
+ AiAssistantShellIntegration: ({ children }: { children: React.ReactNode }) => <>{children}</>,
39
+ }))
40
+
41
+ describe('BackendHeaderChrome', () => {
42
+ it('renders the organization switcher in the topbar without a viewport-gated wrapper', () => {
43
+ const { container } = render(
44
+ <BackendHeaderChrome
45
+ email="demo@example.com"
46
+ embeddingConfigured={false}
47
+ missingConfigMessage=""
48
+ tenantId={null}
49
+ organizationId={null}
50
+ />,
51
+ )
52
+
53
+ const switcher = screen.getByTestId('lazy-organization-switcher')
54
+ expect(switcher).toBeInTheDocument()
55
+
56
+ // Regression for issue #1795: the topbar OrganizationSwitcher must not be
57
+ // wrapped in a viewport-gated container that hides it at narrow widths.
58
+ // Previously `<div className="hidden lg:contents">` removed it below 1024px,
59
+ // which combined with `mobileSidebarSlot={<OrganizationSwitcher compact />}`
60
+ // caused the dropdown to reappear inside the mobile sidebar drawer.
61
+ const hiddenWrappers = container.querySelectorAll('.hidden')
62
+ for (const wrapper of Array.from(hiddenWrappers)) {
63
+ expect(wrapper.contains(switcher)).toBe(false)
64
+ }
65
+ })
66
+ })
@@ -194,6 +194,8 @@
194
194
  "dashboard.action.done": "Fertig",
195
195
  "dashboard.addWidget": "Widget hinzufügen",
196
196
  "dashboard.empty.configurable": "Noch keine Widgets ausgewählt. Verwende \"Widget hinzufügen\", um dein Dashboard zu gestalten.",
197
+ "dashboard.empty.noWidgets.description": "Dashboard-Widgets erscheinen hier, sobald du das erste Modul hinzufügst, das sie bereitstellt.",
198
+ "dashboard.empty.noWidgets.title": "Noch keine Dashboard-Widgets",
197
199
  "dashboard.empty.readonly": "Für dein Konto sind noch keine Widgets verfügbar.",
198
200
  "dashboard.error.partial": "Einige Änderungen wurden nicht gespeichert",
199
201
  "dashboard.error.reload": "Daten neu laden",
@@ -194,6 +194,8 @@
194
194
  "dashboard.action.done": "Done",
195
195
  "dashboard.addWidget": "Add a widget",
196
196
  "dashboard.empty.configurable": "No widgets selected yet. Use \"Add a widget\" to start building your dashboard.",
197
+ "dashboard.empty.noWidgets.description": "Dashboard widgets will appear here after you add the first module that exposes them.",
198
+ "dashboard.empty.noWidgets.title": "No dashboard widgets yet",
197
199
  "dashboard.empty.readonly": "No widgets are available for your account yet.",
198
200
  "dashboard.error.partial": "Some changes were not saved",
199
201
  "dashboard.error.reload": "Reload data",
@@ -194,6 +194,8 @@
194
194
  "dashboard.action.done": "Listo",
195
195
  "dashboard.addWidget": "Agregar widget",
196
196
  "dashboard.empty.configurable": "Aún no has seleccionado widgets. Usa \"Agregar widget\" para construir tu panel.",
197
+ "dashboard.empty.noWidgets.description": "Los widgets del panel aparecerán aquí cuando agregues el primer módulo que los exponga.",
198
+ "dashboard.empty.noWidgets.title": "Aún no hay widgets del panel",
197
199
  "dashboard.empty.readonly": "Todavía no hay widgets disponibles para tu cuenta.",
198
200
  "dashboard.error.partial": "Algunos cambios no se guardaron",
199
201
  "dashboard.error.reload": "Recargar datos",
@@ -194,6 +194,8 @@
194
194
  "dashboard.action.done": "Gotowe",
195
195
  "dashboard.addWidget": "Dodaj widżet",
196
196
  "dashboard.empty.configurable": "Nie wybrano jeszcze żadnych widżetów. Użyj \"Dodaj widżet\", aby zbudować pulpit.",
197
+ "dashboard.empty.noWidgets.description": "Widżety pulpitu pojawią się tutaj po dodaniu pierwszego modułu, który je udostępnia.",
198
+ "dashboard.empty.noWidgets.title": "Brak widżetów pulpitu",
197
199
  "dashboard.empty.readonly": "Brak dostępnych widżetów dla Twojego konta.",
198
200
  "dashboard.error.partial": "Niektóre zmiany nie zostały zapisane",
199
201
  "dashboard.error.reload": "Ponownie wczytaj dane",
@@ -1,9 +1,19 @@
1
1
  // Central place to enable modules and their source.
2
2
  // - id: module id (plural snake_case; special cases: 'auth')
3
3
  // - from: '@open-mercato/core' | '@app' | custom alias/path in future
4
+ // - overrides: optional unified per-app override surface — replace or
5
+ // disable any contract a module presents. AI is wired today (Phase 1);
6
+ // other domains are stubbed and emit a one-shot warning if used.
7
+ // See `.ai/specs/2026-05-04-modules-ts-unified-overrides.md` and
8
+ // `apps/docs/docs/framework/ai-assistant/overrides.mdx`.
4
9
  import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
10
+ import type { ModuleOverrides } from '@open-mercato/shared/modules/overrides'
5
11
 
6
- export type ModuleEntry = { id: string; from?: '@open-mercato/core' | '@app' | string }
12
+ export type ModuleEntry = {
13
+ id: string
14
+ from?: '@open-mercato/core' | '@app' | string
15
+ overrides?: ModuleOverrides
16
+ }
7
17
 
8
18
  export const enabledModules: ModuleEntry[] = [
9
19
  { id: 'dashboards', from: '@open-mercato/core' },