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 +33 -0
- package/agentic/shared/AGENTS.md.template +1 -1
- package/dist/agentic/shared/AGENTS.md.template +1 -1
- package/dist/index.js +178 -17
- package/package.json +1 -1
- package/template/.ai/qa/tests/playwright.config.ts +4 -1
- package/template/.env.example +67 -66
- package/template/AGENTS.md +73 -5
- package/template/package.json.template +1 -1
- package/template/src/app/(backend)/backend/layout.tsx +0 -3
- package/template/src/app/api/[...slug]/route.ts +2 -1
- package/template/src/app/globals.css +8 -0
- package/template/src/bootstrap.ts +10 -0
- package/template/src/components/BackendHeaderChrome.tsx +6 -4
- package/template/src/components/DemoFeedbackWidget.tsx +27 -10
- package/template/src/components/__tests__/BackendHeaderChrome.test.tsx +66 -0
- package/template/src/i18n/de.json +2 -0
- package/template/src/i18n/en.json +2 -0
- package/template/src/i18n/es.json +2 -0
- package/template/src/i18n/pl.json +2 -0
- package/template/src/modules.ts +11 -1
- /package/template/{.ai/qa/tests → src/modules/example/__integration__}/TC-APP-001-metadata.spec.ts +0 -0
- /package/template/{.ai/qa/tests/integration → src/modules/example/__integration__}/TC-CLI-001-agentic-init.spec.ts +0 -0
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
|
|
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
|
|
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
|
|
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
|
|
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
|
@@ -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
|
-
|
|
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({
|
package/template/.env.example
CHANGED
|
@@ -218,58 +218,81 @@ TENANT_DATA_ENCRYPTION_KEY=
|
|
|
218
218
|
# VAULT_REQUEST_TIMEOUT_MS=1000
|
|
219
219
|
|
|
220
220
|
# ============================================================================
|
|
221
|
-
#
|
|
221
|
+
# AI Providers, OCR, and Vector Search
|
|
222
222
|
# ============================================================================
|
|
223
|
-
#
|
|
224
|
-
#
|
|
225
|
-
#
|
|
226
|
-
#
|
|
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
|
-
#
|
|
280
|
+
# Optional request cap for embedding providers (default: 3000 ms).
|
|
281
|
+
# VECTOR_EMBEDDING_TIMEOUT_MS=3000
|
|
230
282
|
|
|
231
|
-
#
|
|
232
|
-
#
|
|
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
|
# ============================================================================
|
package/template/AGENTS.md
CHANGED
|
@@ -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
|
|
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:
|
|
239
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
<
|
|
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
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
|
|
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-
|
|
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-
|
|
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",
|
package/template/src/modules.ts
CHANGED
|
@@ -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 = {
|
|
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' },
|
/package/template/{.ai/qa/tests → src/modules/example/__integration__}/TC-APP-001-metadata.spec.ts
RENAMED
|
File without changes
|
|
File without changes
|