fimo 0.10.1-staging.4 → 0.11.0-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,6 +17,10 @@ When you are about to do Fimo work:
17
17
  1. List what's available: `npx fimo@latest skills list`
18
18
  2. Load the relevant skill: `npx fimo@latest skills get <name>` (add `--ref <doc>` for a reference doc)
19
19
 
20
+ Reference paths mentioned in streamed skill output, such as `references/media.md`, are identifiers,
21
+ not files in the project. Load them through the CLI, for example
22
+ `npx fimo@latest skills get fimo-cli --ref media`. Do not use a filesystem read tool for them.
23
+
20
24
  Pick by task:
21
25
 
22
26
  - Writing code — schema-driven JSX, schemas/forms JSON, `t()` labels, `fimo/ui` media → `fimo-code`
@@ -73,7 +73,7 @@ something else (e.g. `handle`, `permalink`).
73
73
  | `reference` | `{ "id": "..." }`, `null`, or an array of reference objects when `many: true` | References to another schema |
74
74
  | `blocks` | ordered array of objects | Fixed groups, repeated sections, or selectable page blocks |
75
75
 
76
- A `date` field currently generates as `any` in the TS client, so for typed access (and the `<Date>` component) prefer a `string` field holding an ISO date — which is what the `publishDate` examples below do.
76
+ A `date` field generates as `string` in the TS client and wraps in `FimoString` at read time — the same source-tracked shape as `string`/`text` — so `<Date value={post.someDate} />` and plain string methods (`.slice()`, template literals, …) both work directly.
77
77
 
78
78
  ## Enumerated strings
79
79
 
@@ -167,6 +167,7 @@ A field may declare `showIf` (every clause must match) or `showIfAny` (one claus
167
167
  - `min` and `max` constrain the total number of blocks. Use `min: 1` to require at least one block; `required: true` has the same minimum-one behavior.
168
168
  - Fimo creates a stable `_key` for each item. A `oneOf` item also carries `_type`, matching the variant `uid`.
169
169
  - Nested fields support the same defaults and validators as top-level fields.
170
+ - **Each block item's own scalar fields wrap the same way their field type wraps at the top level** — a nested `string`/`text` field is a `FimoString`, a nested `richtext` field is a `FimoRichText`, a nested `media` field is a `FimoMedia`, a nested `boolean` is a `FimoBoolean`, a nested `date` is a `FimoString`, a nested `json` field is a shallow-cloned, source-tagged object, a nested `number` stays a plain `number`, and a nested `blocks` field recurses the same way. Pass those nested values to the matching `fimo/ui` component exactly like a top-level field — `<Text value={section.heading} />` or `<Json value={section.metadata} />` inside a `.map()` over `page.sections` stays inline-editable.
170
171
  - Blocks inherit the owning entry's locale.
171
172
  - Nested references use one fixed `target`. Unpopulated reads return `{ id, documentId }`, like top-level references.
172
173
  - Nested population and filtering are not supported yet.
@@ -516,22 +517,25 @@ Populate is depth-one. It only works for reference fields; trying to populate a
516
517
 
517
518
  The entry objects a hook returns are **not** plain JS values. The generated client wraps each _trackable_ field in a `fimo/ui` primitive that carries source metadata for inline editing. Which fields wrap, and which stay plain:
518
519
 
519
- | Schema field type | Field value you get | Notes |
520
- | ----------------- | ----------------------------------- | ------------------------------------------------------------------------------------- |
521
- | `string` | `FimoString` | `extends String` — **not** a primitive `string` |
522
- | `text` | `FimoString` | Multiline plain text with the same source tracking as `string` |
523
- | `richtext` | `FimoRichText` | Tiptap JSONContent + source metadata |
524
- | `boolean` | `FimoBoolean` | class; `.valueOf()` → `boolean` |
525
- | `media` | `FimoMedia` / `FimoMedia[]` | shape + rendering in `references/assets.md` |
526
- | `number` | `number` | plain — not wrapped |
527
- | `json` | `Record<string, any>` | plain — not wrapped |
528
- | `date` | `any` (raw ISO date string) | not wrapped (`FimoDate` is not emitted — see below) |
529
- | `reference` | `FimoReference` / `FimoReference[]` | reference object(s) with target `id`; use `populate` when embedded targets are needed |
520
+ | Schema field type | Field value you get | Notes |
521
+ | ----------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
522
+ | `string` | `FimoString` | `extends String` — **not** a primitive `string` |
523
+ | `text` | `FimoString` | Multiline plain text with the same source tracking as `string` |
524
+ | `richtext` | `FimoRichText` | Tiptap JSONContent + source metadata |
525
+ | `boolean` | `FimoBoolean` | class; `.valueOf()` → `boolean` |
526
+ | `media` | `FimoMedia` / `FimoMedia[]` | shape + rendering in `references/assets.md` |
527
+ | `number` | `number` | plain — not wrapped. A wrapper would break `<span>{post.views}</span>` (React renders a boxed `String` directly but throws on a boxed `Number`), so it stays untracked and unclickable in the editing surfaces |
528
+ | `json` | `Record<string, any>` | the same shape you wrote, plus a hidden (non-enumerable, symbol-keyed) source tag — render with `<Json>`, see below |
529
+ | `date` | `FimoString` | same wrapper as `string`/`text`, **not** `FimoDate` — see below |
530
+ | `blocks` | array of plain objects | the array/objects themselves are plain; each item's own scalar fields are wrapped by their own field type (a nested `string` is still `FimoString`, a nested `number` stays a plain `number`, …) — see "Blocks" above |
531
+ | `reference` | `FimoReference` / `FimoReference[]` | reference object(s) with target `id`; use `populate` when embedded targets are needed |
530
532
 
531
533
  Rules of thumb:
532
534
 
533
- - **Pass the whole primitive to the matching `fimo/ui` component** — `<Text value={post.title} />`, `<Image value={post.cover} />`, `<RichText value={post.body} />`, `<Boolean value={p.inStock} />`. Don't unwrap first; the component reads the source metadata so the field stays inline-editable in the dashboard.
535
+ - **Pass the whole primitive to the matching `fimo/ui` component** — `<Text value={post.title} />`, `<Image value={post.cover} />`, `<RichText value={post.body} />`, `<Boolean value={p.inStock} />`, `<Date value={post.publishDate} />`, `<Json value={post.metadata} />`. Don't unwrap first; the component reads the source metadata so the field stays inline-editable in the dashboard.
534
536
  - **`FimoString extends String`**, so `typeof post.title === 'object'` and `post.title === 'Hi'` is `false`. It stringifies fine inside JSX and template literals; call `.toString()` only when something genuinely needs a primitive `string`.
537
+ - **`number` fields are the one scalar type left unwrapped** — `post.views` is a plain `number`, so `{post.views}` renders directly in JSX, `typeof`/`===`/arithmetic all behave normally, and there is no `<Number>` component. This is deliberate, not a gap: wrapping it would break direct JSX rendering in every existing site.
538
+ - **`json` fields are a shallow clone of what you wrote**, tagged with a hidden source symbol — not the original object, and not a class instance. Reading it behaves exactly like plain JSON (`JSON.stringify` round-trips it unchanged; `for...in`/`Object.keys` see only your own keys). Render it with `<Json value={post.metadata} />` (default renders as a `<span>`; pass `as="pre"` for a formatted block, or a children render function for specific keys) so it stays inline-editable and live-updates in the dashboard preview like every other field.
535
539
  - **Never hand-construct** a `FimoString` / `FimoMedia` / etc. — that bypasses source tracking. For a literal string use `t()`; for a literal image use `<StaticImage>`.
536
540
  - `t()` also returns a `FimoString` (see `references/labels.md`).
537
541
 
@@ -553,7 +557,7 @@ class FimoBoolean {
553
557
  }
554
558
  ```
555
559
 
556
- > **`FimoDate`:** exported from `fimo/ui` (it's the `<Date>` component's value type), but the generated schema client does **not** wrap `date` fields in it today — they arrive as a raw ISO string. `<Date value={post.someDate} />` accepts that at runtime either way.
560
+ > **`FimoDate`:** exported from `fimo/ui` (it's one of the `<Date>` component's accepted value types), but the generated schema client does **not** wrap `date` fields in it — a `date` field wraps as `FimoString` instead (see the table above), because the generated TypeScript type for a `date` field is `string`, and `FimoString` is what keeps that contract's string methods (`.slice()`, `.includes()`, …) working. `<Date value={post.someDate} />` accepts a `FimoString`, a `FimoDate`, a raw `Date`, or a raw string — all four render correctly.
557
561
 
558
562
  ## UI code pattern
559
563
 
@@ -579,7 +583,7 @@ export function BlogList() {
579
583
  }
580
584
  ```
581
585
 
582
- For the `<Text>` / `<RichText>` / `<Image>` / `<Date>` components and the rules of thumb, see `references/ui.md`.
586
+ For the `<Text>` / `<RichText>` / `<Image>` / `<Date>` / `<Json>` components and the rules of thumb, see `references/ui.md`.
583
587
 
584
588
  ## Read path (published app, no auth)
585
589
 
@@ -17,6 +17,7 @@ Use these to render content-type field values consistently. They carry source me
17
17
  | `<Video>` | Schema `media` video fields | `<Video value={post.heroVideo} />` |
18
18
  | `<Date>` | Schema date fields | `<Date value={post.publishDate} />` |
19
19
  | `<Boolean>` | Schema boolean fields | `<Boolean value={product.inStock} />` |
20
+ | `<Json>` | Schema `json` fields | `<Json value={post.metadata} as="pre" />` |
20
21
  | `` fimo`...` `` | Compose tracked values (mix `t()` + schema text) | ``<Text value={fimo`${t('by')} ${post.author}`} />`` |
21
22
  | `useLabels()` | `t('key')` hook | see `references/labels.md` |
22
23
  | `useLocale()` | Active locale and route locale metadata | see `references/locales.md` |
@@ -29,6 +30,7 @@ Use these to render content-type field values consistently. They carry source me
29
30
  4. **Label text → `<Text value={t('key')} />`**, not `<h1>{t(...)}</h1>`. Label values live in the DB for `i18n.defaultLocale` from `fimo-config.json` unless a non-default locale is explicitly targeted. See `references/labels.md`.
30
31
  5. **Mixed schema + translated text** → the `` fimo`...` `` template literal inside `<Text>`.
31
32
  6. **Never hand-roll richtext rendering** — always `<RichText>`. To restyle a node, pass `components={{ heading: ..., link: ..., ... }}`.
33
+ 7. **Schema `json` → `<Json>`**, not a hand-rolled `<pre data-fimo-source={getFimoSource(post.metadata)}>{JSON.stringify(post.metadata)}</pre>`. Default renders as a `<span>`; pass `as="pre"` (with a `whitespace-pre-wrap` class) for a formatted block, or a children render function for custom display of specific keys.
32
34
 
33
35
  ## App code conventions
34
36
 
@@ -78,6 +80,7 @@ The template uses **Vite + React 19 + TypeScript + Tailwind v4 + shadcn/ui + rea
78
80
 
79
81
  - Raw `<a href>` for internal nav → use `<FimoLink>` for locale-aware paths or React Router `<Link>` / `<NavLink>` only when intentionally bypassing Fimo locale helpers.
80
82
  - Raw `<img>` for schema media → use `<Image>` (with width/height).
83
+ - Raw `<pre data-fimo-source={getFimoSource(...)}>` for schema `json` fields → use `<Json>`.
81
84
  - Hardcoded strings in JSX → wrap in `t('key')` and add the DB value with `fimo labels set`.
82
85
  - Calling `t()` with a dynamic first argument → key must be a string literal.
83
86
  - Editing generated `.ts` files under `src/schemas/` or `src/forms/` → regenerate them through `fimo validate` or the matching push command.
@@ -33,6 +33,14 @@ To halt the AI mid-generation, use the **Stop button** in the input bar — whil
33
33
 
34
34
  While the AI works, you see small badges in the chat showing what it is doing. Each badge displays a tool name and its current status. Examples include reading files, writing code, searching your project, or generating images. These badges help you follow along without needing to understand the technical details.
35
35
 
36
+ ## Reviewing Changes That Could Not Merge
37
+
38
+ Chat changes are prepared separately and then applied to the environment you are viewing. Most changes merge automatically.
39
+
40
+ If the project changed in the same place while the AI was working, the response shows a **Review changes** card instead. Open it to inspect the proposal, resolve any conflicts, and merge when it is ready. Until you merge it, the proposal is not part of the environment and later chat messages will not see it.
41
+
42
+ Discarding the merge leaves the current environment unchanged and keeps the proposal separate.
43
+
36
44
  ## Tips for Better Results
37
45
 
38
46
  - **Be specific**: Mention colors, layout, text content, and spacing when you can.
@@ -16,6 +16,12 @@ To push your latest changes live, open the publish panel from the header **Publi
16
16
 
17
17
  > **Tip:** Before your site has ever been published, the chat shows a one-click **Publish** button that deploys immediately without opening the panel.
18
18
 
19
+ ## Publishing Changes That Need Review
20
+
21
+ If an AI chat change cannot merge automatically, use the response's **Review changes** card first. Resolve the conflicts and merge the proposal into `main`.
22
+
23
+ When the AI requested publication, the review dialog offers **Merge and publish** after the conflicts are resolved. Publishing starts only after the merge succeeds, so unresolved or discarded proposals never reach the live site.
24
+
19
25
  ## What You See When You Click Publish
20
26
 
21
27
  The header **Publish** button itself only shows the label "Publish" (or a spinner while a deploy is running). Open the panel to see the current state:
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "bundled": true,
3
3
  "bundler": "esbuild",
4
- "bundledAt": "2026-07-28T17:34:40.817Z",
4
+ "bundledAt": "2026-07-30T08:13:16.978Z",
5
5
  "cliVersion": "0.10.0",
6
6
  "external": [
7
7
  "oxc-parser",
package/dist/cli/index.js CHANGED
@@ -587,7 +587,7 @@ ${V2(this.state)} ${opts.message}
587
587
  ${removed.map(r=>` ${r.skill} \u2192 ${r.agent}`).join(`
588
588
  `)}`,"render")})}function listReferenceDocs(skillDir){let refsDir=path31.join(skillDir,"references");if(!fs31.existsSync(refsDir))return[];let out=[],walk=__name((cur,prefix)=>{for(let entry of fs31.readdirSync(cur,{withFileTypes:!0})){let rel=prefix?`${prefix}/${entry.name}`:entry.name;entry.isDirectory()?walk(path31.join(cur,entry.name),rel):entry.isFile()&&entry.name.endsWith(".md")&&out.push(rel.slice(0,-3))}},"walk");return walk(refsDir,""),out.sort()}function discoverBundledSkills(){if(!fs31.existsSync(SKILLS_ROOT))return[];let skills2=[];for(let entry of fs31.readdirSync(SKILLS_ROOT,{withFileTypes:!0})){if(!entry.isDirectory()||entry.name===BOOTSTRAP_SKILL_NAME)continue;let dir=path31.join(SKILLS_ROOT,entry.name),skillMd=path31.join(dir,"SKILL.md");if(!fs31.existsSync(skillMd))continue;let{data:data2}=parseFrontmatter(fs31.readFileSync(skillMd,"utf8")),description=typeof data2.description=="string"?data2.description.trim():"";skills2.push({name:entry.name,description,refs:listReferenceDocs(dir),dir})}return skills2.sort((a,b)=>a.name.localeCompare(b.name))}function firstLine(text3){let line=text3.split(`
589
589
  `,1)[0]?.trim()??"";return line.length>80?`${line.slice(0,79)}\u2026`:line}function skillsListCommand(){let skills2=discoverBundledSkills();if(skills2.length===0){ui.error({command:"skills list",code:"BUNDLED_SKILL_MISSING",message:`No bundled skills found at ${SKILLS_ROOT}.`,hint:"Try reinstalling fimo, or report this as a bug."});return}ui.result({command:"skills list",data:{skills:skills2.map(s=>({name:s.name,description:s.description,refs:s.refs}))},render:__name(()=>{let nameWidth=Math.max(4,...skills2.map(s=>s.name.length)),header=`${"NAME".padEnd(nameWidth)} REFS DESCRIPTION`,lines=skills2.map(s=>{let refs=String(s.refs.length).padStart(4);return`${s.name.padEnd(nameWidth)} ${refs} ${firstLine(s.description)}`});return[header,...lines].join(`
590
- `)},"render"),hint:"Run `fimo skills get <name>` for a skill, or add `--ref <doc>` for a reference."})}function normalizeRef(ref){return ref.trim().replace(/\\/g,"/").replace(/^references\//,"").replace(/\.md$/,"")}function skillsGetCommand(name,options={}){let skills2=discoverBundledSkills(),skill=skills2.find(s=>s.name===name);if(!skill){ui.error({command:"skills get",code:"SKILL_NOT_FOUND",message:`Unknown skill "${name}".`,hint:skills2.length?`Available: ${skills2.map(s=>s.name).join(", ")}. Run \`fimo skills list\`.`:"No bundled skills found \u2014 try reinstalling fimo."});return}let targetPath,refId=null;if(options.ref){refId=normalizeRef(options.ref);let refsRoot=path31.resolve(skill.dir,"references"),candidate=path31.resolve(refsRoot,`${refId}.md`);if(!(candidate===refsRoot||candidate.startsWith(refsRoot+path31.sep))||!fs31.existsSync(candidate)){ui.error({command:"skills get",code:"SKILL_REF_NOT_FOUND",message:`Reference "${options.ref}" not found in skill "${skill.name}".`,hint:skill.refs.length?`Available refs: ${skill.refs.join(", ")}.`:"This skill has no reference docs."});return}targetPath=candidate}else targetPath=path31.join(skill.dir,"SKILL.md");let content2=fs31.readFileSync(targetPath,"utf8");ui.result({command:"skills get",data:{skill:skill.name,ref:refId,path:targetPath,content:content2},render:__name(()=>content2,"render")})}var SKILLS_ROOT,BOOTSTRAP_SKILL_NAME,BOOTSTRAP_SKILL_SOURCE,STALE_GLOBAL_SKILL_NAMES,DETECTABLE_FIMO_SKILL_NAMES,init_skills=__esm({"src/cli/commands/skills.ts"(){init_dist5();init_dist();init_dist7();init_config();init_agents();SKILLS_ROOT=path31.resolve(CLI_ROOT,"assets","skills"),BOOTSTRAP_SKILL_NAME="fimo",BOOTSTRAP_SKILL_SOURCE=path31.join(SKILLS_ROOT,BOOTSTRAP_SKILL_NAME),STALE_GLOBAL_SKILL_NAMES=["fimo-cli","fimo-studio","fimo-migration","fimo-migrate","fimo-source-migrate"],DETECTABLE_FIMO_SKILL_NAMES=[BOOTSTRAP_SKILL_NAME,...STALE_GLOBAL_SKILL_NAMES];__name(isPrettyMode,"isPrettyMode");__name(runSkillsInstall,"runSkillsInstall");__name(renderSkillsInstall,"renderSkillsInstall");__name(skillsInstallCommand,"skillsInstallCommand");__name(skillsSyncCommand,"skillsSyncCommand");__name(detectAgentsWithFimoSkills,"detectAgentsWithFimoSkills");__name(removeDirIfExists,"removeDirIfExists");__name(removeGlobalSkillDirs,"removeGlobalSkillDirs");__name(hasGlobalSkillInstalled,"hasGlobalSkillInstalled");__name(removeCanonicalSkillDirs,"removeCanonicalSkillDirs");__name(cleanupStaleGlobalSkills,"cleanupStaleGlobalSkills");__name(skillsUninstallCommand,"skillsUninstallCommand");__name(listReferenceDocs,"listReferenceDocs");__name(discoverBundledSkills,"discoverBundledSkills");__name(firstLine,"firstLine");__name(skillsListCommand,"skillsListCommand");__name(normalizeRef,"normalizeRef");__name(skillsGetCommand,"skillsGetCommand")}});import{execFile as execFile8,spawnSync as spawnSync2}from"node:child_process";import path32 from"node:path";import{promisify as promisify8}from"node:util";function explainCreateProjectError(error){return error instanceof ApiRequestError&&error.detail.includes('insert into "projects"')&&(error.detail.includes('"subdomain"')||error.detail.includes('"subdomain_updated_at"'))?["Project creation failed because the local API database schema looks out of date.","The server is trying to write the `projects.subdomain` fields, but your database likely has not applied the latest migration yet.","Run `cd apps/api && pnpm db:migrate`, restart the API if needed, then retry."].join(`
590
+ `)},"render"),hint:"Run `fimo skills get <name>` for a skill, or add `--ref <doc>` for a reference."})}function normalizeRef(ref){return ref.trim().replace(/\\/g,"/").replace(/^references\//,"").replace(/\.md$/,"")}function skillsGetCommand(name,options={}){let skills2=discoverBundledSkills(),skill=skills2.find(s=>s.name===name);if(!skill){ui.error({command:"skills get",code:"SKILL_NOT_FOUND",message:`Unknown skill "${name}".`,hint:skills2.length?`Available: ${skills2.map(s=>s.name).join(", ")}. Run \`fimo skills list\`.`:"No bundled skills found \u2014 try reinstalling fimo."});return}let targetPath,refId=null;if(options.ref){refId=normalizeRef(options.ref);let refsRoot=path31.resolve(skill.dir,"references"),candidate=path31.resolve(refsRoot,`${refId}.md`);if(!(candidate===refsRoot||candidate.startsWith(refsRoot+path31.sep))||!fs31.existsSync(candidate)){ui.error({command:"skills get",code:"SKILL_REF_NOT_FOUND",message:`Reference "${options.ref}" not found in skill "${skill.name}".`,hint:skill.refs.length?`Available refs: ${skill.refs.join(", ")}.`:"This skill has no reference docs."});return}targetPath=candidate}else targetPath=path31.join(skill.dir,"SKILL.md");let content2=fs31.readFileSync(targetPath,"utf8");ui.result({command:"skills get",data:{skill:skill.name,ref:refId,path:targetPath,content:content2},render:__name(()=>content2,"render"),hint:STREAMED_REFERENCE_HINT})}var SKILLS_ROOT,BOOTSTRAP_SKILL_NAME,BOOTSTRAP_SKILL_SOURCE,STREAMED_REFERENCE_HINT,STALE_GLOBAL_SKILL_NAMES,DETECTABLE_FIMO_SKILL_NAMES,init_skills=__esm({"src/cli/commands/skills.ts"(){init_dist5();init_dist();init_dist7();init_config();init_agents();SKILLS_ROOT=path31.resolve(CLI_ROOT,"assets","skills"),BOOTSTRAP_SKILL_NAME="fimo",BOOTSTRAP_SKILL_SOURCE=path31.join(SKILLS_ROOT,BOOTSTRAP_SKILL_NAME),STREAMED_REFERENCE_HINT="Reference paths in this document are identifiers, not project files. Load one with `fimo skills get <skill> --ref <reference>`.",STALE_GLOBAL_SKILL_NAMES=["fimo-cli","fimo-studio","fimo-migration","fimo-migrate","fimo-source-migrate"],DETECTABLE_FIMO_SKILL_NAMES=[BOOTSTRAP_SKILL_NAME,...STALE_GLOBAL_SKILL_NAMES];__name(isPrettyMode,"isPrettyMode");__name(runSkillsInstall,"runSkillsInstall");__name(renderSkillsInstall,"renderSkillsInstall");__name(skillsInstallCommand,"skillsInstallCommand");__name(skillsSyncCommand,"skillsSyncCommand");__name(detectAgentsWithFimoSkills,"detectAgentsWithFimoSkills");__name(removeDirIfExists,"removeDirIfExists");__name(removeGlobalSkillDirs,"removeGlobalSkillDirs");__name(hasGlobalSkillInstalled,"hasGlobalSkillInstalled");__name(removeCanonicalSkillDirs,"removeCanonicalSkillDirs");__name(cleanupStaleGlobalSkills,"cleanupStaleGlobalSkills");__name(skillsUninstallCommand,"skillsUninstallCommand");__name(listReferenceDocs,"listReferenceDocs");__name(discoverBundledSkills,"discoverBundledSkills");__name(firstLine,"firstLine");__name(skillsListCommand,"skillsListCommand");__name(normalizeRef,"normalizeRef");__name(skillsGetCommand,"skillsGetCommand")}});import{execFile as execFile8,spawnSync as spawnSync2}from"node:child_process";import path32 from"node:path";import{promisify as promisify8}from"node:util";function explainCreateProjectError(error){return error instanceof ApiRequestError&&error.detail.includes('insert into "projects"')&&(error.detail.includes('"subdomain"')||error.detail.includes('"subdomain_updated_at"'))?["Project creation failed because the local API database schema looks out of date.","The server is trying to write the `projects.subdomain` fields, but your database likely has not applied the latest migration yet.","Run `cd apps/api && pnpm db:migrate`, restart the API if needed, then retry."].join(`
591
591
  `):error instanceof Error?error.message:String(error)}async function scaffoldNewProject(args){let{api,cfg,targetDir,displayName,options}=args,organizationId=await resolveOrganization(api,options.org),createSpinner=spinner("Creating Fimo project"),project;try{project=await api.createProject({organizationId,displayName:displayName??path32.basename(targetDir)})}catch(error){throw createSpinner.fail("Project creation failed"),new ScaffoldError("PROJECT_CREATE_FAILED",explainCreateProjectError(error))}createSpinner.succeed(`Project ${project.displayName??project.name} created (${project.id})`);let releaseAbort=installAbortHandler(api,project.id,project.name);try{return{scaffold:await scaffoldIntoProject({project,api,cfg,targetDir,options}),releaseAbort}}catch(error){throw releaseAbort(),error}}async function scaffoldIntoProject(args){let{project,api,cfg,targetDir,options}=args,remoteSp=spinner("Fetching git remote"),remote=await api.getGitRemote(project.id,MAIN_BRANCH);remoteSp.succeed("Remote ready");let selectedAgents;try{selectedAgents=await selectAgents(options,{promptMessage:"Which agents do you want to set up?",detectFn:detectAgentsWithFimoSkills})}catch(err){throw new ScaffoldError("AGENT_SELECTION_FAILED",err instanceof Error?err.message:String(err))}let scaffoldSp=spinner("Scaffolding template"),templateRoot=options.templateRoot?path32.resolve(options.templateRoot):process.env.FIMO_TEMPLATE_ROOT?path32.resolve(process.env.FIMO_TEMPLATE_ROOT):resolveTemplateRoot();await copyTemplate(templateRoot,targetDir),seedSiteName({targetDir,project}),writeLocalProjectFiles({targetDir,project,apiUrl:cfg.apiUrl,tenantApiUrl:remote.tenantApiUrl});let gitSp=spinner("Initializing git repository");await runGit(["init","-b",MAIN_BRANCH],targetDir),await ensureFimoRemote(targetDir,remote.url),gitSp.succeed("Git initialized with `fimo` remote"),scaffoldSp.succeed("Template scaffolded");let relativeTarget=path32.relative(process.cwd(),targetDir)||".";return{project,targetDir,relativeTarget,selectedAgents,tenantApiUrl:remote.tenantApiUrl}}async function finalizeInitialCommit(args){let{targetDir}=args,gitSp=spinner("Creating the initial commit");await runGit(["add","-A"],targetDir),await runGit(["commit","-m","chore: scaffold Fimo project"],targetDir).catch(()=>{}),gitSp.succeed("Initial commit created");let pushSp=spinner("Pushing initial commit to Fimo");try{return await runGit(["push","fimo",`HEAD:${MAIN_BRANCH}`],targetDir),pushSp.succeed("Initial commit pushed"),{pushedInitial:!0}}catch(error){pushSp.fail("Initial push failed");let failure=classifyPushError(error),retryHint=failure.kind==="auth"?"Sign in with `fimo login`, then run `fimo deploy` to push it.":"Run `fimo deploy` later to retry.";return O2.error(`Could not push the initial commit. ${retryHint}
592
592
  ${failure.stderr||""}`.trim()),{pushedInitial:!1}}}function installAbortHandler(api,projectId,projectName){let cleaningUp=!1,handler=__name(()=>{cleaningUp&&(process.stderr.write(`
593
593
  Force-exiting. Project may not have been fully cleaned up.
@@ -1150,7 +1150,7 @@ This breaks source tracking for inline editing. Translation keys must be statica
1150
1150
  \u274C Found ${errors.length} error(s) in ${reactFiles.length} files.
1151
1151
  `),process.exit(1)}}import fs21 from"node:fs";import path24 from"node:path";var SCAN_EXTENSIONS=[".ts",".tsx",".js",".jsx",".mjs"],SCAN_EXTENSIONS_SET=new Set(SCAN_EXTENSIONS),SKIP_DIRS=new Set(["node_modules","dist",".next",".fimo",".git","public"]),GLOBAL_PATTERNS=[/\.css$/,/\.scss$/,/tailwind\.config/,/postcss\.config/,/vite\.config/,/tsconfig/,/package\.json$/,/src\/main\.[tj]sx?$/,/src\/index\.[tj]sx?$/,/src\/root\.[tj]sx?$/,/react-router\.config/];function isGlobalFile(filePath){return GLOBAL_PATTERNS.some(p2=>p2.test(filePath))}__name(isGlobalFile,"isGlobalFile");function collectFiles(dir){let files=[];if(!fs21.existsSync(dir))return files;let entries2=fs21.readdirSync(dir,{withFileTypes:!0});for(let entry of entries2){if(SKIP_DIRS.has(entry.name))continue;let fullPath=path24.join(dir,entry.name);entry.isDirectory()?files.push(...collectFiles(fullPath)):SCAN_EXTENSIONS_SET.has(path24.extname(entry.name))&&files.push(fullPath)}return files}__name(collectFiles,"collectFiles");function extractImports(content2){let imports=[],staticRegex=/(?:import|export)\s+(?:[\s\S]*?)\s+from\s+['"]([^'"]+)['"]/g;for(let match of content2.matchAll(staticRegex))imports.push(match[1]);let dynamicRegex=/import\(\s*['"]([^'"]+)['"]\s*\)/g;for(let match of content2.matchAll(dynamicRegex))imports.push(match[1]);let requireRegex=/require\(\s*['"]([^'"]+)['"]\s*\)/g;for(let match of content2.matchAll(requireRegex))imports.push(match[1]);return imports.filter(s=>s.startsWith(".")||s.startsWith("@/"))}__name(extractImports,"extractImports");function resolveImport(specifier,fromDir,srcDir){let targetPath;if(specifier.startsWith("@/"))targetPath=path24.resolve(srcDir,specifier.slice(2));else if(specifier.startsWith("."))targetPath=path24.resolve(fromDir,specifier);else return null;if(fs21.existsSync(targetPath)&&fs21.statSync(targetPath).isFile())return targetPath;for(let ext of SCAN_EXTENSIONS){let withExt=targetPath+ext;if(fs21.existsSync(withExt))return withExt}for(let ext of SCAN_EXTENSIONS){let indexFile=path24.join(targetPath,"index"+ext);if(fs21.existsSync(indexFile))return indexFile}return null}__name(resolveImport,"resolveImport");function discoverRoutes(srcDir,root2){let routesFilePath=path24.resolve(srcDir,"routes.ts");if(!fs21.existsSync(routesFilePath))return new Map;let source=fs21.readFileSync(routesFilePath,"utf-8"),parsed=parseRoutesSource(source),fileToRoutes=new Map;for(let r of parsed){if(r.isLayout||!r.path||!r.file)continue;let resolved=resolveImport(r.file,srcDir,srcDir);if(!resolved)continue;let rel=path24.relative(root2,resolved);fileToRoutes.has(rel)||fileToRoutes.set(rel,new Set),fileToRoutes.get(rel).add(r.path)}return fileToRoutes}__name(discoverRoutes,"discoverRoutes");function buildReverseImportGraph(srcDir,root2){let fileToImporters=new Map,allFiles=collectFiles(srcDir);for(let filePath of allFiles){let content2;try{content2=fs21.readFileSync(filePath,"utf-8")}catch{continue}let imports=extractImports(content2);for(let specifier of imports){let resolved=resolveImport(specifier,path24.dirname(filePath),srcDir);if(!resolved)continue;let resolvedRel=path24.relative(root2,resolved),importerRel=path24.relative(root2,filePath);fileToImporters.has(resolvedRel)||fileToImporters.set(resolvedRel,new Set),fileToImporters.get(resolvedRel).add(importerRel)}}return fileToImporters}__name(buildReverseImportGraph,"buildReverseImportGraph");function resolveAffectedRoutes(file,fileToImporters,fileToRoutes){let visited=new Set,queue=[file],affectedRoutes=new Set;for(;queue.length>0;){let current=queue.shift();if(visited.has(current))continue;visited.add(current);let routes=fileToRoutes.get(current);if(routes)for(let route of routes)affectedRoutes.add(route);let importers=fileToImporters.get(current);if(importers)for(let importer of importers)visited.has(importer)||queue.push(importer)}return Array.from(affectedRoutes)}__name(resolveAffectedRoutes,"resolveAffectedRoutes");function resolveRoutes(changedFiles,options={}){let root2=options.cwd??process.cwd(),srcDir=path24.resolve(root2,"src"),normalizedChangedFiles=changedFiles.map(f=>f.replace(/^\/+/,""));if(normalizedChangedFiles.length===0)return{routeMap:{},routes:[],unresolvedFiles:[]};let fileToRoutes=discoverRoutes(srcDir,root2),fileToImporters=buildReverseImportGraph(srcDir,root2),routeMap={},unresolvedFiles=[];for(let file of normalizedChangedFiles){if(isGlobalFile(file)){routeMap[file]=["*"];continue}let routes=resolveAffectedRoutes(file,fileToImporters,fileToRoutes);routes.length>0?routeMap[file]=routes:unresolvedFiles.push(file)}let allRoutes=[...new Set(Object.values(routeMap).flat().filter(r=>r!=="*"))];return{routeMap,routes:allRoutes,unresolvedFiles}}__name(resolveRoutes,"resolveRoutes");init_sync_forms();init_sync_schemas();function resolveCwd(options){return options.cwd?resolve6(options.cwd):process.cwd()}__name(resolveCwd,"resolveCwd");function renderPrettyJson4(data2){return JSON.stringify(data2,null,2)}__name(renderPrettyJson4,"renderPrettyJson");function syncSchemasCommand(options){let result=syncSchemas({cwd:resolveCwd(options)});ui.result({command:"scripts sync-schemas",data:result,render:__name(()=>renderPrettyJson4(result),"render")})}__name(syncSchemasCommand,"syncSchemasCommand");function syncFormsCommand(options){let result=syncForms({cwd:resolveCwd(options)});ui.result({command:"scripts sync-forms",data:result,render:__name(()=>renderPrettyJson4(result),"render")})}__name(syncFormsCommand,"syncFormsCommand");async function exportStaticCommand(options){let result=await exportStatic({cwd:resolveCwd(options)});ui.result({command:"export-static",data:result??{ok:!0},render:__name(()=>renderPrettyJson4(result??{ok:!0}),"render")})}__name(exportStaticCommand,"exportStaticCommand");function validateRouteMetadataCommand(options){let result=validateRouteMetadata({cwd:resolveCwd(options)});result.errors.length>0&&(ui.error({command:"scripts validate-route-metadata",code:"ROUTE_METADATA_INVALID",message:`Route metadata validation failed (${result.errors.length} issue(s)):
1152
1152
  ${result.errors.map(e=>` - ${e}`).join(`
1153
- `)}`}),process.exit(1)),ui.ok({command:"scripts validate-route-metadata",headline:`Route metadata validation passed (${result.routesChecked} route${result.routesChecked===1?"":"s"}).`,details:{routes_checked:result.routesChecked}})}__name(validateRouteMetadataCommand,"validateRouteMetadataCommand");function extractTranslationsCommand(options){let cwd=resolveCwd(options),data2={keys:extractTranslations({cwd}).keys.length};ui.result({command:"scripts extract-translations",data:data2,render:__name(()=>JSON.stringify(data2),"render")})}__name(extractTranslationsCommand,"extractTranslationsCommand");function resolveRoutesCommand(changedFiles,options){let cwd=resolveCwd(options),files=[];if(changedFiles.length===1&&changedFiles[0].startsWith("{"))try{files=JSON.parse(changedFiles[0]).files??[]}catch{files=[]}else files=changedFiles;let output=resolveRoutes(files,{cwd});ui.result({command:"resolve-routes",data:output,render:__name(()=>JSON.stringify(output),"render")})}__name(resolveRoutesCommand,"resolveRoutesCommand");function lintProject(opts){let files=opts.targetPath?[resolve6(opts.cwd,opts.targetPath)]:collectFiles2(resolve6(opts.cwd,"src")),errors=[],filesChecked=0;for(let file of files)if(/\.(tsx?|jsx?)$/.test(file))try{let code2=readFileSync8(file,"utf-8");errors.push(...lintTranslationKeys(file,code2)),filesChecked++}catch{}return{errors,filesChecked}}__name(lintProject,"lintProject");function lintCommand(targetPath,options){let cwd=resolveCwd(options),result=lintProject({cwd,targetPath});ui.result({command:"scripts lint",data:result,render:__name(()=>JSON.stringify(result),"render")}),result.errors.length>0&&process.exit(1)}__name(lintCommand,"lintCommand");function collectFiles2(root2){if(!existsSync7(root2))return[];let results=[];for(let name of readdirSync6(root2)){if(name==="node_modules"||name==="dist"||name===".git")continue;let full=join9(root2,name);statSync3(full).isDirectory()?results.push(...collectFiles2(full)):results.push(full)}return results}__name(collectFiles2,"collectFiles");async function validateCommand(options,cmd){let cwd=resolveProjectCwd(options.cwd),envOverride2=envOverrideFromCommand(cmd),report=await runValidate({cwd,envOverride:envOverride2}),warnings=[getDbLabelsRuntimeWarning(cwd)].filter(warning=>!!warning);ui.result({command:"validate",data:report,render:__name(()=>formatReportHuman(report),"render"),warnings:warnings.length>0?warnings:void 0}),report.ok||(process.exitCode=report.exitCode)}__name(validateCommand,"validateCommand");async function runValidate(opts){let{cwd,envOverride:envOverride2}=opts,checks=await runChecksPhase({cwd}),codegen,labelKeys=null;if(checks.errors.length>0)codegen=skippedPhase("Codegen","static checks failed");else{let codegenResult=await runCodegenPhase({cwd});codegen=codegenResult.phase,labelKeys=codegenResult.labelKeys}let sync;return checks.errors.length>0?sync=skippedPhase("Labels","static checks failed"):labelKeys===null?sync=skippedPhase("Labels","label key scan failed"):sync=await runLabelsPhase({cwd,labelKeys,envOverride:envOverride2}),finalizeReport({phases:{checks,codegen,sync}})}__name(runValidate,"runValidate");async function runChecksPhase(opts){let{cwd}=opts,[lintResult,routeResult,agentsResult,schemasResult]=await Promise.allSettled([Promise.resolve().then(()=>lintProject({cwd})),Promise.resolve().then(()=>runRouteMetadataCheck({cwd})),Promise.resolve().then(()=>runAgentsCheck({cwd})),Promise.resolve().then(()=>validateSchemas({cwd}))]),errors=[],stats={};if(lintResult.status==="fulfilled"){stats.filesLinted=lintResult.value.filesChecked;for(let err of lintResult.value.errors)errors.push(authorError({rule:`lint/${err.rule}`,message:err.message,file:err.file,line:err.line,column:err.column}))}else errors.push(infraErrorFromException("lint",lintResult.reason));if(routeResult.status==="fulfilled"){if(routeResult.value){stats.routesChecked=routeResult.value.routesChecked;for(let message of routeResult.value.errors)errors.push(authorError({rule:"route-metadata",message}))}}else errors.push(infraErrorFromException("route-metadata",routeResult.reason));if(agentsResult.status==="fulfilled"?(Object.assign(stats,agentsResult.value.stats),errors.push(...agentsResult.value.errors)):errors.push(infraErrorFromException("agents",agentsResult.reason)),schemasResult.status==="fulfilled"){stats.schemasChecked=schemasResult.value.schemasChecked;for(let err of schemasResult.value.errors)errors.push(authorError({rule:err.rule,message:err.message,file:err.file}))}else errors.push(infraErrorFromException("schemas",schemasResult.reason));return{name:"Checks",ok:errors.length===0,errors,stats}}__name(runChecksPhase,"runChecksPhase");async function runAgentsCheck(opts){let{cwd}=opts,report=await runValidateAgents({cwd,json:!0}),errors=[];if(report.policy&&!report.policy.ok)for(let issue of report.policy.issues)errors.push(authorError({rule:"agents/policy",message:issue.message,file:issue.pointer}));for(let bundle of report.bundles)for(let phase of["parse","capabilities","composition","semantics"])for(let issue of bundle.phases[phase].issues)errors.push(authorError({rule:`agents/${phase}`,message:`${bundle.name}: ${issue.message}`,file:issue.pointer}));return{errors,stats:{agentsChecked:report.bundles.length}}}__name(runAgentsCheck,"runAgentsCheck");function runRouteMetadataCheck(opts){return validateRouteMetadata({cwd:opts.cwd})}__name(runRouteMetadataCheck,"runRouteMetadataCheck");async function runCodegenPhase(opts){let{cwd}=opts,[schemasResult,formsResult,labelsResult]=await Promise.allSettled([Promise.resolve().then(()=>syncSchemas({cwd,verbose:!1})),Promise.resolve().then(()=>syncForms({cwd,verbose:!1})),Promise.resolve().then(()=>extractLabelKeys({cwd}))]),errors=[],stats={};schemasResult.status==="fulfilled"?(stats.schemasCreated=schemasResult.value.created,stats.schemasUpdated=schemasResult.value.updated,stats.schemasDeleted=schemasResult.value.deleted):errors.push(infraErrorFromException("sync-schemas",schemasResult.reason)),formsResult.status==="fulfilled"?(stats.formsCreated=formsResult.value.created,stats.formsUpdated=formsResult.value.updated,stats.formsDeleted=formsResult.value.deleted):errors.push(infraErrorFromException("sync-forms",formsResult.reason));let labelKeys=null;return labelsResult.status==="fulfilled"?(labelKeys=labelsResult.value,stats.labelsDeclared=labelKeys.length):errors.push(infraErrorFromException("extract-label-keys",labelsResult.reason)),{phase:{name:"Codegen",ok:errors.length===0,errors,stats},labelKeys}}__name(runCodegenPhase,"runCodegenPhase");async function runLabelsPhase(opts){let{cwd,labelKeys,envOverride:envOverride2}=opts;if(labelKeys.length===0)return{name:"Labels",ok:!0,errors:[],stats:{labelsDeclared:0,labelsMissing:0}};let ctx;try{ctx=await getProjectContext({cwd,envOverride:envOverride2})}catch(error){return rethrowCliAuthError(error),{name:"Labels",ok:!1,errors:[infraErrorFromException("load-project-settings",error)]}}let api=new FimoApiClient(loadConfig()).withEnv(ctx.env),locale=getConfigServer(cwd).i18n?.defaultLocale??"en",pinnedBranch=(await readProjectEnv(cwd)).VITE_FIMO_BRANCH;try{let labels2;if(pinnedBranch){let branchLabels=await loadPublicTenantLabels(cwd,locale);if(branchLabels==null)return{name:"Labels",ok:!1,errors:[infraErrorFromException("labels-check",new Error(`Could not read labels from branch "${pinnedBranch}"`))]};labels2=branchLabels}else labels2=labelsToRecord(await api.listLabels(ctx.projectId,locale));let existingKeys=new Set(Object.keys(labels2)),missing=labelKeys.filter(label=>!existingKeys.has(label.key));return{name:"Labels",ok:missing.length===0,errors:missing.map(label=>authorError({rule:"labels/missing",message:`Missing label "${label.key}" for locale "${locale}". Run: fimo labels set ${JSON.stringify(label.key)} --locale ${locale} --value "...".`,file:label.file})),stats:{labelsDeclared:labelKeys.length,labelsMissing:missing.length}}}catch(error){return rethrowCliAuthError(error),{name:"Labels",ok:!1,errors:[infraErrorFromException("labels-check",error)]}}}__name(runLabelsPhase,"runLabelsPhase");function labelsToRecord(labels2){return Object.fromEntries(labels2.map(label=>[label.key,label.value??label.defaultValue??""]))}__name(labelsToRecord,"labelsToRecord");async function loadPublicTenantLabels(cwd,locale){let env=await readProjectEnv(cwd),apiUrl=env.VITE_API_URL;if(!apiUrl)return null;try{let url=new URL("/labels",apiUrl);url.searchParams.set("locale",locale);let headers=env.VITE_FIMO_BRANCH?{"X-Fimo-Branch":env.VITE_FIMO_BRANCH}:{},response=await fetchWithTimeout(url,4e3,headers);if(!response.ok)return null;let body=await response.json();return Array.isArray(body.data)?labelsToRecord(body.data):body.data?.labels??null}catch{return null}}__name(loadPublicTenantLabels,"loadPublicTenantLabels");async function fetchWithTimeout(url,timeoutMs,headers){let controller=new AbortController,timeout=setTimeout(()=>controller.abort(),timeoutMs);try{return await fetch(url,{signal:controller.signal,headers})}finally{clearTimeout(timeout)}}__name(fetchWithTimeout,"fetchWithTimeout");async function readProjectEnv(cwd){let values={...await readEnvFile(path25.join(cwd,".env")),...await readEnvFile(path25.join(cwd,".env.local"))};return process.env.VITE_API_URL&&(values.VITE_API_URL=process.env.VITE_API_URL),process.env.VITE_FIMO_BRANCH&&(values.VITE_FIMO_BRANCH=process.env.VITE_FIMO_BRANCH),values}__name(readProjectEnv,"readProjectEnv");async function readEnvFile(filePath){try{let content2=await readFile2(filePath,"utf8"),values={};for(let line of content2.split(/\r?\n/)){let trimmed=line.trim();if(!trimmed||trimmed.startsWith("#"))continue;let index2=trimmed.indexOf("=");if(index2===-1)continue;let key=trimmed.slice(0,index2).trim(),value=trimmed.slice(index2+1).trim().replace(/^['"]|['"]$/g,"");values[key]=value}return values}catch{return{}}}__name(readEnvFile,"readEnvFile");async function deployCommand(options,cmd){let cwd=path26.resolve(options.cwd??process.cwd()),fimoPath=path26.join(cwd,".fimo.settings.json");if(!fs22.existsSync(fimoPath)){ui.error({command:"deploy",code:"NOT_IN_PROJECT",message:"You are not in a Fimo project.",hint:"Run `fimo create` to start one, or cd into a linked project."});return}let local=JSON.parse(fs22.readFileSync(fimoPath,"utf-8"));if(!await isGitRepo(cwd)){ui.error({command:"deploy",code:"NOT_A_GIT_REPO",message:"Not a git repository."});return}let isPretty2=currentMode().format==="pretty";isPretty2&&mt(options.publish?"Deploying and publishing":"Deploying to preview");let envOverride2=envOverrideFromCommand(cmd),ctx=await getProjectContext({cwd,envOverride:envOverride2});if(isPretty2&&process.stderr.write(`${envSourceBanner(ctx)}
1153
+ `)}`}),process.exit(1)),ui.ok({command:"scripts validate-route-metadata",headline:`Route metadata validation passed (${result.routesChecked} route${result.routesChecked===1?"":"s"}).`,details:{routes_checked:result.routesChecked}})}__name(validateRouteMetadataCommand,"validateRouteMetadataCommand");function extractTranslationsCommand(options){let cwd=resolveCwd(options),data2={keys:extractTranslations({cwd}).keys.length};ui.result({command:"scripts extract-translations",data:data2,render:__name(()=>JSON.stringify(data2),"render")})}__name(extractTranslationsCommand,"extractTranslationsCommand");function resolveRoutesCommand(changedFiles,options){let cwd=resolveCwd(options),files=[];if(changedFiles.length===1&&changedFiles[0].startsWith("{"))try{files=JSON.parse(changedFiles[0]).files??[]}catch{files=[]}else files=changedFiles;let output=resolveRoutes(files,{cwd});ui.result({command:"resolve-routes",data:output,render:__name(()=>JSON.stringify(output),"render")})}__name(resolveRoutesCommand,"resolveRoutesCommand");function lintProject(opts){let files=opts.targetPath?[resolve6(opts.cwd,opts.targetPath)]:collectFiles2(resolve6(opts.cwd,"src")),errors=[],filesChecked=0;for(let file of files)if(/\.(tsx?|jsx?)$/.test(file))try{let code2=readFileSync8(file,"utf-8");errors.push(...lintTranslationKeys(file,code2)),filesChecked++}catch{}return{errors,filesChecked}}__name(lintProject,"lintProject");function lintCommand(targetPath,options){let cwd=resolveCwd(options),result=lintProject({cwd,targetPath});ui.result({command:"scripts lint",data:result,render:__name(()=>JSON.stringify(result),"render")}),result.errors.length>0&&process.exit(1)}__name(lintCommand,"lintCommand");function collectFiles2(root2){if(!existsSync7(root2))return[];let results=[];for(let name of readdirSync6(root2)){if(name==="node_modules"||name==="dist"||name===".git")continue;let full=join9(root2,name);statSync3(full).isDirectory()?results.push(...collectFiles2(full)):results.push(full)}return results}__name(collectFiles2,"collectFiles");async function validateCommand(options,cmd){let cwd=resolveProjectCwd(options.cwd),envOverride2=envOverrideFromCommand(cmd),report=await runValidate({cwd,envOverride:envOverride2}),warnings=[getDbLabelsRuntimeWarning(cwd)].filter(warning=>!!warning);ui.result({command:"validate",data:report,render:__name(()=>formatReportHuman(report),"render"),warnings:warnings.length>0?warnings:void 0}),report.ok||(process.exitCode=report.exitCode)}__name(validateCommand,"validateCommand");async function runValidate(opts){let{cwd,envOverride:envOverride2}=opts,checks=await runChecksPhase({cwd}),codegen,labelKeys=null;if(checks.errors.length>0)codegen=skippedPhase("Codegen","static checks failed");else{let codegenResult=await runCodegenPhase({cwd});codegen=codegenResult.phase,labelKeys=codegenResult.labelKeys}let sync;return checks.errors.length>0?sync=skippedPhase("Labels","static checks failed"):labelKeys===null?sync=skippedPhase("Labels","label key scan failed"):sync=await runLabelsPhase({cwd,labelKeys,envOverride:envOverride2}),finalizeReport({phases:{checks,codegen,sync}})}__name(runValidate,"runValidate");async function runChecksPhase(opts){let{cwd}=opts,[lintResult,routeResult,agentsResult,schemasResult]=await Promise.allSettled([Promise.resolve().then(()=>lintProject({cwd})),Promise.resolve().then(()=>runRouteMetadataCheck({cwd})),Promise.resolve().then(()=>runAgentsCheck({cwd})),Promise.resolve().then(()=>validateSchemas({cwd}))]),errors=[],stats={};if(lintResult.status==="fulfilled"){stats.filesLinted=lintResult.value.filesChecked;for(let err of lintResult.value.errors)errors.push(authorError({rule:`lint/${err.rule}`,message:err.message,file:err.file,line:err.line,column:err.column}))}else errors.push(infraErrorFromException("lint",lintResult.reason));if(routeResult.status==="fulfilled"){if(routeResult.value){stats.routesChecked=routeResult.value.routesChecked;for(let message of routeResult.value.errors)errors.push(authorError({rule:"route-metadata",message}))}}else errors.push(infraErrorFromException("route-metadata",routeResult.reason));if(agentsResult.status==="fulfilled"?(Object.assign(stats,agentsResult.value.stats),errors.push(...agentsResult.value.errors)):errors.push(infraErrorFromException("agents",agentsResult.reason)),schemasResult.status==="fulfilled"){stats.schemasChecked=schemasResult.value.schemasChecked;for(let err of schemasResult.value.errors)errors.push(authorError({rule:err.rule,message:err.message,file:err.file}))}else errors.push(infraErrorFromException("schemas",schemasResult.reason));return{name:"Checks",ok:errors.length===0,errors,stats}}__name(runChecksPhase,"runChecksPhase");async function runAgentsCheck(opts){let{cwd}=opts,report=await runValidateAgents({cwd,json:!0}),errors=[];if(report.policy&&!report.policy.ok)for(let issue of report.policy.issues)errors.push(authorError({rule:"agents/policy",message:issue.message,file:issue.pointer}));for(let bundle of report.bundles)for(let phase of["parse","capabilities","composition","semantics"])for(let issue of bundle.phases[phase].issues)errors.push(authorError({rule:`agents/${phase}`,message:`${bundle.name}: ${issue.message}`,file:issue.pointer}));return{errors,stats:{agentsChecked:report.bundles.length}}}__name(runAgentsCheck,"runAgentsCheck");function runRouteMetadataCheck(opts){return validateRouteMetadata({cwd:opts.cwd})}__name(runRouteMetadataCheck,"runRouteMetadataCheck");async function runCodegenPhase(opts){let{cwd}=opts,[schemasResult,formsResult,labelsResult]=await Promise.allSettled([Promise.resolve().then(()=>syncSchemas({cwd,verbose:!1})),Promise.resolve().then(()=>syncForms({cwd,verbose:!1})),Promise.resolve().then(()=>extractLabelKeys({cwd}))]),errors=[],stats={};schemasResult.status==="fulfilled"?(stats.schemasCreated=schemasResult.value.created,stats.schemasUpdated=schemasResult.value.updated,stats.schemasDeleted=schemasResult.value.deleted):errors.push(infraErrorFromException("sync-schemas",schemasResult.reason)),formsResult.status==="fulfilled"?(stats.formsCreated=formsResult.value.created,stats.formsUpdated=formsResult.value.updated,stats.formsDeleted=formsResult.value.deleted):errors.push(infraErrorFromException("sync-forms",formsResult.reason));let labelKeys=null;return labelsResult.status==="fulfilled"?(labelKeys=labelsResult.value,stats.labelsDeclared=labelKeys.length):errors.push(infraErrorFromException("extract-label-keys",labelsResult.reason)),{phase:{name:"Codegen",ok:errors.length===0,errors,stats},labelKeys}}__name(runCodegenPhase,"runCodegenPhase");async function runLabelsPhase(opts){let{cwd,labelKeys,envOverride:envOverride2}=opts;if(labelKeys.length===0)return{name:"Labels",ok:!0,errors:[],stats:{labelsDeclared:0,labelsMissing:0}};let ctx;try{ctx=await getProjectContext({cwd,envOverride:envOverride2})}catch(error){return rethrowCliAuthError(error),{name:"Labels",ok:!1,errors:[infraErrorFromException("load-project-settings",error)]}}let api=new FimoApiClient(loadConfig()).withEnv(ctx.env),locale=getConfigServer(cwd).i18n?.defaultLocale??"en";try{let labels2=await loadLabelsForValidation({cwd,locale,projectId:ctx.projectId,loadAuthenticatedLabels:__name(()=>api.listLabels(ctx.projectId,locale),"loadAuthenticatedLabels")}),existingKeys=new Set(Object.keys(labels2)),missing=labelKeys.filter(label=>!existingKeys.has(label.key));return{name:"Labels",ok:missing.length===0,errors:missing.map(label=>authorError({rule:"labels/missing",message:`Missing label "${label.key}" for locale "${locale}". Run: fimo labels set ${JSON.stringify(label.key)} --locale ${locale} --value "...".`,file:label.file})),stats:{labelsDeclared:labelKeys.length,labelsMissing:missing.length}}}catch(error){return rethrowCliAuthError(error),{name:"Labels",ok:!1,errors:[infraErrorFromException("labels-check",error)]}}}__name(runLabelsPhase,"runLabelsPhase");function labelsToRecord(labels2){return Object.fromEntries(labels2.map(label=>[label.key,label.value??label.defaultValue??""]))}__name(labelsToRecord,"labelsToRecord");async function loadPublicTenantLabels(cwd,locale){let env=await readProjectEnv(cwd),apiUrl=env.VITE_API_URL;if(!apiUrl)return null;try{let url=new URL("/labels",apiUrl);url.searchParams.set("locale",locale);let headers=env.VITE_FIMO_BRANCH?{"X-Fimo-Branch":env.VITE_FIMO_BRANCH}:{},response=await fetchWithTimeout(url,3e4,headers);if(!response.ok)return null;let body=await response.json();return Array.isArray(body.data)?labelsToRecord(body.data):body.data?.labels??null}catch{return null}}__name(loadPublicTenantLabels,"loadPublicTenantLabels");async function loadLabelsForValidation({cwd,locale,projectId,loadAuthenticatedLabels}){let env=await readProjectEnv(cwd);if(!env.VITE_API_URL)return labelsToRecord(await loadAuthenticatedLabels());let labels2=await loadPublicTenantLabels(cwd,locale);if(labels2==null){let source=env.VITE_FIMO_BRANCH?`branch "${env.VITE_FIMO_BRANCH}"`:`project "${projectId}"`;throw new Error(`Could not read labels from ${source}`)}return labels2}__name(loadLabelsForValidation,"loadLabelsForValidation");async function fetchWithTimeout(url,timeoutMs,headers){let controller=new AbortController,timeout=setTimeout(()=>controller.abort(),timeoutMs);try{return await fetch(url,{signal:controller.signal,headers})}finally{clearTimeout(timeout)}}__name(fetchWithTimeout,"fetchWithTimeout");async function readProjectEnv(cwd){let values={...await readEnvFile(path25.join(cwd,".env")),...await readEnvFile(path25.join(cwd,".env.local"))};return process.env.VITE_API_URL&&(values.VITE_API_URL=process.env.VITE_API_URL),process.env.VITE_FIMO_BRANCH&&(values.VITE_FIMO_BRANCH=process.env.VITE_FIMO_BRANCH),values}__name(readProjectEnv,"readProjectEnv");async function readEnvFile(filePath){try{let content2=await readFile2(filePath,"utf8"),values={};for(let line of content2.split(/\r?\n/)){let trimmed=line.trim();if(!trimmed||trimmed.startsWith("#"))continue;let index2=trimmed.indexOf("=");if(index2===-1)continue;let key=trimmed.slice(0,index2).trim(),value=trimmed.slice(index2+1).trim().replace(/^['"]|['"]$/g,"");values[key]=value}return values}catch{return{}}}__name(readEnvFile,"readEnvFile");async function deployCommand(options,cmd){let cwd=path26.resolve(options.cwd??process.cwd()),fimoPath=path26.join(cwd,".fimo.settings.json");if(!fs22.existsSync(fimoPath)){ui.error({command:"deploy",code:"NOT_IN_PROJECT",message:"You are not in a Fimo project.",hint:"Run `fimo create` to start one, or cd into a linked project."});return}let local=JSON.parse(fs22.readFileSync(fimoPath,"utf-8"));if(!await isGitRepo(cwd)){ui.error({command:"deploy",code:"NOT_A_GIT_REPO",message:"Not a git repository."});return}let isPretty2=currentMode().format==="pretty";isPretty2&&mt(options.publish?"Deploying and publishing":"Deploying to preview");let envOverride2=envOverrideFromCommand(cmd),ctx=await getProjectContext({cwd,envOverride:envOverride2});if(isPretty2&&process.stderr.write(`${envSourceBanner(ctx)}
1154
1154
  `),ctx.readOnly){ui.error({command:"deploy",code:"ENV_READONLY",message:`Refusing to deploy to read-only env '${ctx.env}'.`,hint:"Switch off the release/* tag or pass --env <writable-env>."});return}if(options.publish&&ctx.env!==DEFAULT_ENV){ui.error({command:"deploy",code:"PUBLISH_REQUIRES_DEFAULT_ENV",message:`Refusing to --publish from env '${ctx.env}'. Publish only runs from '${DEFAULT_ENV}'.`,hint:`Drop --publish to deploy a preview, or switch to '${DEFAULT_ENV}' first.`});return}await requireOperation("fimo.deploy",{projectId:ctx.projectId,env:ctx.env});let cfg=loadConfig(),api=new FimoApiClient(cfg).withEnv(ctx.env),previewEnabled=(await api.getProject(local.projectId)).previewEnabled!==!1,validateSp=spinner("Validating project"),report=await runValidate({cwd,envOverride:envOverride2});if(report.ok)validateSp.succeed("Validation passed");else if(options.force)validateSp.fail("Validation failed \u2014 continuing anyway (--force)"),ui.warn({command:"deploy",message:"Bypassing validation errors because --force was passed. The deploy may still fail later in the sandbox."});else{validateSp.fail("Validation failed"),ui.result({command:"deploy",data:{stage:"validate",report},render:__name(()=>formatReportHuman(report),"render")}),process.exitCode=report.exitCode;return}let devFimoDependency=null;try{devFimoDependency=prepareDevFimoDependencyForDeploy(cwd)}catch(error){ui.error({command:"deploy",code:"DEV_FIMO_VERSION_MISSING",message:errorMessage(error)});return}devFimoDependency&&isPretty2&&O2.info(`Deploying with fimo ${devFimoDependency.version}; restoring local file: dependency after push.`);try{if(await hasUncommittedChanges(cwd)){let message;if(options.message!==void 0)message=options.message;else{assertInteractive('Pass -m "<message>" or commit your changes manually.',"deploy"),isPretty2&&O2.warn("You have uncommitted changes. Commit them before deploying.");let answer=await Ot({message:"Commit message",placeholder:"Deploy",defaultValue:"Deploy"});if(q(answer)){ui.info({command:"deploy",message:"Cancelled."});return}message=String(answer)}let commitSp=spinner("Committing pending changes");await runGit(["add","-A"],cwd),await runGit(["commit","-m",message],cwd),commitSp.succeed("Changes committed")}let remoteSp=spinner("Refreshing git remote URL"),remote=await api.getGitRemote(local.projectId,ctx.env);await ensureFimoRemote(cwd,remote.url);let remoteBranch=remote.branch;remoteSp.succeed(`Remote ready (branch ${remoteBranch})`);let pushSp=spinner(`Pushing to fimo/${remoteBranch}`);try{await runGit(["push","fimo",`HEAD:${remoteBranch}`],cwd),pushSp.succeed("Pushed to Fimo")}catch(error){pushSp.fail("Push failed");let failure=classifyPushError(error);emitPushFailure(failure,remoteBranch);return}}finally{devFimoDependency?.restore()}let stepLabel="",stepStartedAt=0,stepSp=null,finalizeStep=__name((status="ok",failMessage)=>{if(!stepSp)return;let elapsed=((Date.now()-stepStartedAt)/1e3).toFixed(1);status==="ok"?stepSp.succeed(`${stepLabel} (${elapsed}s)`):stepSp.fail(failMessage??`${stepLabel} failed`),stepSp=null},"finalizeStep");try{await api.sandboxPull(local.projectId,({message})=>{finalizeStep("ok"),stepLabel=message,stepStartedAt=Date.now(),stepSp=spinner(message)},ctx.env,{noRestart:options.restart===!1}),finalizeStep("ok")}catch(error){rethrowCliAuthError(error,()=>finalizeStep("fail","Authentication required")),finalizeStep("fail",`Sandbox sync failed: ${errorMessage(error)}`),ui.error({command:"deploy",code:"SANDBOX_SYNC_FAILED",message:`Sandbox sync failed: ${errorMessage(error)}`});return}let previewUrl=null;if(previewEnabled)try{previewUrl=(await api.getPreviewUrl(local.projectId,ctx.env)).url}catch(error){isPretty2&&O2.warn(`Could not fetch preview URL: ${errorMessage(error)}`)}if(!options.publish){ui.result({command:"deploy",data:{project_id:local.projectId,env:ctx.env,preview_enabled:previewEnabled,preview_url:previewUrl,published:!1},render:__name(()=>{let lines=[];return previewUrl&&lines.push(`Preview: ${previewUrl}`),lines.push(previewEnabled?"Deployed to preview. Run `fimo deploy --publish` or `fimo publish` to publish a new version.":"Changes synced. Preview is disabled for this project."),lines.join(`
1155
1155
  `)},"render")});return}let publishSp=spinner("Publishing (this can take a few minutes)");try{await api.publishProject(local.projectId)}catch(error){rethrowCliAuthError(error,()=>publishSp.fail("Authentication required")),publishSp.fail(),ui.error({command:"deploy",code:"PUBLISH_FAILED",message:`Publish failed: ${errorMessage(error)}`});return}publishSp.succeed("Published");let appUrl=null;try{appUrl=(await api.getProject(local.projectId)).appUrl}catch(error){isPretty2&&O2.warn(`Could not fetch published URL: ${errorMessage(error)}`)}ui.result({command:"deploy",data:{project_id:local.projectId,env:ctx.env,preview_enabled:previewEnabled,preview_url:previewUrl,published:!0,app_url:appUrl},render:__name(()=>{let lines=[];return previewUrl&&lines.push(`Preview: ${previewUrl}`),appUrl?(lines.push(`Live: ${appUrl}`),lines.push("Your site is live.")):lines.push("Publish completed. Check Fimo for the live URL."),lines.join(`
1156
1156
  `)},"render")})}__name(deployCommand,"deployCommand");function emitPushFailure(failure,env){switch(failure.kind){case"non-fast-forward":ui.error({command:"deploy",code:"PUSH_NON_FAST_FORWARD",message:`The remote \`fimo/${env}\` has commits your local branch does not.`,hint:`Fix it manually, then re-run \`fimo deploy\`:
@@ -1,4 +1,19 @@
1
1
  import type { CollectionContentClient, ContentClientConfig } from './types.js';
2
+ /**
3
+ * React Query's structural sharing (`replaceEqualDeep`) rebuilds plain objects
4
+ * and arrays key-by-key from `Object.keys`, which silently drops the
5
+ * non-enumerable, symbol-keyed source tag `wrapField` puts on `json` values.
6
+ * A `json` field would then lose its `data-fimo-source` on the first refetch
7
+ * that changes it, making it unselectable in every editing surface. Every other
8
+ * wrapped type is a class instance, which structural sharing returns untouched.
9
+ *
10
+ * Turning it off costs nothing here: `wrapEntry` allocates fresh wrappers on
11
+ * every fetch, so a wrapped entry never compared equal to its predecessor and
12
+ * sharing was already returning the new object.
13
+ */
14
+ export declare const SOURCE_TRACKED_QUERY_OPTIONS: {
15
+ readonly structuralSharing: false;
16
+ };
2
17
  export declare function createContentClient<T extends {
3
18
  id: string;
4
19
  }, TInput extends object>(config: ContentClientConfig): CollectionContentClient<T, TInput>;
@@ -2,6 +2,19 @@ import { useInfiniteQuery, useMutation, useQuery, useQueryClient, useSuspenseQue
2
2
  import { parseCollectionResponse } from '../parseCollectionResponse.js';
3
3
  import { apiBase, buildReadPath, fimoHeaders, interpretSchema, readCollectionLocale, resolveClientHandle, splitWritePayload, validateReferences, withMutationLocale, wrapEntry, } from './internal.js';
4
4
  import { useQueryLocale } from './useQueryLocale.js';
5
+ /**
6
+ * React Query's structural sharing (`replaceEqualDeep`) rebuilds plain objects
7
+ * and arrays key-by-key from `Object.keys`, which silently drops the
8
+ * non-enumerable, symbol-keyed source tag `wrapField` puts on `json` values.
9
+ * A `json` field would then lose its `data-fimo-source` on the first refetch
10
+ * that changes it, making it unselectable in every editing surface. Every other
11
+ * wrapped type is a class instance, which structural sharing returns untouched.
12
+ *
13
+ * Turning it off costs nothing here: `wrapEntry` allocates fresh wrappers on
14
+ * every fetch, so a wrapped entry never compared equal to its predecessor and
15
+ * sharing was already returning the new object.
16
+ */
17
+ export const SOURCE_TRACKED_QUERY_OPTIONS = { structuralSharing: false };
5
18
  export function createContentClient(config) {
6
19
  const references = config.references ?? {};
7
20
  const info = interpretSchema(config.schema);
@@ -147,6 +160,7 @@ export function createContentClient(config) {
147
160
  return useQuery({
148
161
  queryKey: qk.byId(id, localizedOptions),
149
162
  queryFn: () => getById(id, localizedOptions),
163
+ ...SOURCE_TRACKED_QUERY_OPTIONS,
150
164
  });
151
165
  }
152
166
  function useGetBySlug(slug, options = {}) {
@@ -155,6 +169,7 @@ export function createContentClient(config) {
155
169
  return useQuery({
156
170
  queryKey: qk.bySlug(slug, localizedOptions),
157
171
  queryFn: () => getBySlug(slug, localizedOptions),
172
+ ...SOURCE_TRACKED_QUERY_OPTIONS,
158
173
  enabled: !!slug,
159
174
  });
160
175
  }
@@ -164,6 +179,7 @@ export function createContentClient(config) {
164
179
  return useQuery({
165
180
  queryKey: qk.byField(fieldName, value, localizedOptions),
166
181
  queryFn: () => getByField(fieldName, value, localizedOptions),
182
+ ...SOURCE_TRACKED_QUERY_OPTIONS,
167
183
  enabled: !!value,
168
184
  });
169
185
  }
@@ -173,6 +189,7 @@ export function createContentClient(config) {
173
189
  return useQuery({
174
190
  queryKey: qk.list(localizedParams),
175
191
  queryFn: () => list(localizedParams),
192
+ ...SOURCE_TRACKED_QUERY_OPTIONS,
176
193
  select: (page) => page.items,
177
194
  });
178
195
  }
@@ -182,6 +199,7 @@ export function createContentClient(config) {
182
199
  return useQuery({
183
200
  queryKey: qk.list(localizedParams),
184
201
  queryFn: () => list(localizedParams),
202
+ ...SOURCE_TRACKED_QUERY_OPTIONS,
185
203
  });
186
204
  }
187
205
  function useInfiniteList(params = {}) {
@@ -190,6 +208,7 @@ export function createContentClient(config) {
190
208
  const query = useInfiniteQuery({
191
209
  queryKey: qk.infiniteList(localizedParams),
192
210
  queryFn: ({ pageParam }) => list({ ...localizedParams, offset: pageParam }),
211
+ ...SOURCE_TRACKED_QUERY_OPTIONS,
193
212
  initialPageParam: 0,
194
213
  getNextPageParam: (lastPage) => lastPage.pageInfo.nextOffset ?? undefined,
195
214
  });
@@ -208,6 +227,7 @@ export function createContentClient(config) {
208
227
  return useSuspenseQuery({
209
228
  queryKey: qk.byId(id, localizedOptions),
210
229
  queryFn: () => getById(id, localizedOptions),
230
+ ...SOURCE_TRACKED_QUERY_OPTIONS,
211
231
  });
212
232
  }
213
233
  function useSuspenseGetBySlug(slug, options = {}) {
@@ -216,6 +236,7 @@ export function createContentClient(config) {
216
236
  return useSuspenseQuery({
217
237
  queryKey: qk.bySlug(slug, localizedOptions),
218
238
  queryFn: () => getBySlug(slug, localizedOptions),
239
+ ...SOURCE_TRACKED_QUERY_OPTIONS,
219
240
  });
220
241
  }
221
242
  function useSuspenseGetByField(fieldName, value, options = {}) {
@@ -224,6 +245,7 @@ export function createContentClient(config) {
224
245
  return useSuspenseQuery({
225
246
  queryKey: qk.byField(fieldName, value, localizedOptions),
226
247
  queryFn: () => getByField(fieldName, value, localizedOptions),
248
+ ...SOURCE_TRACKED_QUERY_OPTIONS,
227
249
  });
228
250
  }
229
251
  function useSuspenseGet(params = {}) {
@@ -232,6 +254,7 @@ export function createContentClient(config) {
232
254
  return useSuspenseQuery({
233
255
  queryKey: qk.list(localizedParams),
234
256
  queryFn: () => list(localizedParams),
257
+ ...SOURCE_TRACKED_QUERY_OPTIONS,
235
258
  select: (page) => page.items,
236
259
  });
237
260
  }
@@ -1,4 +1,5 @@
1
1
  import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
2
+ import { SOURCE_TRACKED_QUERY_OPTIONS } from './createContentClient.js';
2
3
  import { apiBase, buildReadPath, fimoHeaders, interpretSchema, readSingletonLocale, referenceTargetUids, resolveClientHandle, splitWritePayload, validateReferences, withMutationLocale, wrapEntry, } from './internal.js';
3
4
  import { useQueryLocale } from './useQueryLocale.js';
4
5
  export function createSingletonClient(config) {
@@ -78,6 +79,7 @@ export function createSingletonClient(config) {
78
79
  return useQuery({
79
80
  queryKey: qk.document(localizedOptions),
80
81
  queryFn: () => get(localizedOptions),
82
+ ...SOURCE_TRACKED_QUERY_OPTIONS,
81
83
  });
82
84
  }
83
85
  function useSuspenseGet(options = {}) {
@@ -86,6 +88,7 @@ export function createSingletonClient(config) {
86
88
  return useSuspenseQuery({
87
89
  queryKey: qk.document(localizedOptions),
88
90
  queryFn: () => get(localizedOptions),
91
+ ...SOURCE_TRACKED_QUERY_OPTIONS,
89
92
  });
90
93
  }
91
94
  function useUpdate() {
@@ -1,7 +1,7 @@
1
1
  import { pascalCase } from 'change-case';
2
2
  // @ts-ignore - virtual module provided by fimoConfigPlugin in fimo/vite
3
3
  import fimoConfig from 'virtual:fimo-config';
4
- import { FimoBoolean, FimoMedia, FimoRichText, FimoString } from '../../primitives/lib/primitives.js';
4
+ import { FIMO_SOURCE, FimoBoolean, FimoMedia, FimoRichText, FimoString } from '../../primitives/lib/primitives.js';
5
5
  export function interpretSchema(schema) {
6
6
  if (!schema || typeof schema.uid !== 'string' || schema.uid.length === 0) {
7
7
  throw new Error('Content schema is missing a "uid".');
@@ -176,15 +176,59 @@ function wrapReference(value, uid, resolve) {
176
176
  }
177
177
  return target.wrapWithSource(value);
178
178
  }
179
+ /**
180
+ * Blocks values have no wrapper class of their own — the array structure stays
181
+ * plain JSON (matching the API/codegen shape) and only the scalar leaves inside
182
+ * each item are wrapped. `_key`/`_type` are structural metadata, never fields.
183
+ */
184
+ function blockItemFields(field, item) {
185
+ const schema = field.schema;
186
+ if (!schema) {
187
+ return undefined;
188
+ }
189
+ if ('oneOf' in schema) {
190
+ return schema.oneOf.find((variant) => variant.uid === item._type)?.fields;
191
+ }
192
+ return schema.fields;
193
+ }
194
+ function wrapBlockItem(field, item, source, resolve) {
195
+ if (item == null || typeof item !== 'object' || Array.isArray(item)) {
196
+ return item;
197
+ }
198
+ const record = item;
199
+ const fields = blockItemFields(field, record);
200
+ if (!fields) {
201
+ return item;
202
+ }
203
+ const wrapped = { ...record };
204
+ for (const [key, childField] of Object.entries(fields)) {
205
+ wrapped[key] = wrapField(childField, record[key], `${source}.${key}`, resolve);
206
+ }
207
+ return wrapped;
208
+ }
209
+ /**
210
+ * A JSON field's declared shape is a plain object/array — tagging it in place
211
+ * (rather than boxing it, like the scalar wrappers below) keeps that shape
212
+ * intact so consumers that read it as ordinary JSON see no difference.
213
+ */
214
+ function tagWithSource(value, source) {
215
+ Object.defineProperty(value, FIMO_SOURCE, { value: source, enumerable: false, configurable: true });
216
+ return value;
217
+ }
179
218
  function wrapField(field, value, source, resolve) {
180
219
  switch (field.type) {
181
220
  case 'string':
182
221
  case 'text':
183
- return value != null ? new FimoString(value, source) : value;
222
+ case 'date':
223
+ return value != null ? new FimoString(String(value), source) : value;
184
224
  case 'richtext':
185
225
  return value != null ? new FimoRichText(value, source) : value;
186
226
  case 'boolean':
187
227
  return value != null ? new FimoBoolean(value, source) : value;
228
+ case 'json':
229
+ return value != null && typeof value === 'object'
230
+ ? tagWithSource(Array.isArray(value) ? [...value] : { ...value }, source)
231
+ : value;
188
232
  case 'media':
189
233
  if (field.multiple) {
190
234
  return (value?.map((m, i) => new FimoMedia(m, `${source}[${i}]`)) ?? value);
@@ -207,6 +251,23 @@ function wrapField(field, value, source, resolve) {
207
251
  }
208
252
  return wrapReference(value, uid, resolve);
209
253
  }
254
+ case 'blocks':
255
+ return Array.isArray(value)
256
+ ? value.map((item, i) => wrapBlockItem(field, item, `${source}[${i}]`, resolve))
257
+ : value;
258
+ // 'number' deliberately falls through here — this is a decision, not a
259
+ // gap. React renders a boxed String directly (`<span>{new FimoString(...)}
260
+ // </span>` works, because React special-cases String objects), but it
261
+ // throws "Objects are not valid as a React child" on a boxed Number. A
262
+ // FimoNumber wrapper — whether a plain class or `extends Number` — would
263
+ // break `{post.views}`, which is exactly how a raw number field renders
264
+ // in every Fimo site today. `extends Number` would recover `.toFixed()`/
265
+ // `.toPrecision()`/`.toLocaleString()` and make `JSON.stringify` emit `7`
266
+ // instead of `{"value":7}`, but it does NOT fix JSX rendering, and
267
+ // `typeof`/`===`/`Number.isInteger` stay broken either way (boxed
268
+ // primitives never satisfy those). Revisiting this needs a `<Number>`
269
+ // component in `fimo/ui` (none exists yet — every other wrapped type has
270
+ // one) plus a migration path for sites already rendering numbers raw.
210
271
  default:
211
272
  return value;
212
273
  }
@@ -6,6 +6,19 @@ interface RecordData {
6
6
  visible: boolean;
7
7
  }
8
8
  export type GroupedDataSources = Record<string, RecordData[]>;
9
+ /**
10
+ * Parses a `data-fimo-source` attribute into its schema, identity, and field
11
+ * parts. The field itself may contain dots (e.g. a blocks leaf such as
12
+ * "body[0].title"), so only the first two segments are fixed positions.
13
+ */
14
+ export declare function parseDataSource(source: string): {
15
+ schemaName: string;
16
+ identity: {
17
+ id: string;
18
+ locale?: string;
19
+ };
20
+ fieldName: string;
21
+ } | null;
9
22
  /**
10
23
  * Finds all visible elements with a `data-fimo-source` attribute and groups them
11
24
  * by schema and ID into a structured object.
@@ -14,6 +14,24 @@ function parseRecordIdentity(token) {
14
14
  return null;
15
15
  }
16
16
  }
17
+ /**
18
+ * Parses a `data-fimo-source` attribute into its schema, identity, and field
19
+ * parts. The field itself may contain dots (e.g. a blocks leaf such as
20
+ * "body[0].title"), so only the first two segments are fixed positions.
21
+ */
22
+ export function parseDataSource(source) {
23
+ const parts = source.split('.');
24
+ if (parts.length < 3) {
25
+ return null;
26
+ }
27
+ const [schemaName, identityToken] = parts;
28
+ const fieldName = parts.slice(2).join('.');
29
+ const identity = parseRecordIdentity(identityToken);
30
+ if (!identity) {
31
+ return null;
32
+ }
33
+ return { schemaName, identity, fieldName };
34
+ }
17
35
  /**
18
36
  * Checks if an element is genuinely visible to the user within the viewport.
19
37
  * @param el The element to check.
@@ -51,37 +69,33 @@ export function retrieveVisibleDataSources() {
51
69
  if (!source) {
52
70
  return;
53
71
  }
54
- const parts = source.split('.');
55
- // Expects the format "SchemaName.id.fieldName"
56
- if (parts.length === 3) {
57
- const [schemaName, identityToken, fieldName] = parts;
58
- const identity = parseRecordIdentity(identityToken);
59
- if (!identity) {
60
- return;
61
- }
62
- const { id, locale } = identity;
63
- const recordKey = JSON.stringify([id, locale ?? null]);
64
- // Skip __translations
65
- if (schemaName === '__translations') {
66
- return;
67
- }
68
- // Ensure the schema key exists
69
- if (!tempGroupedData[schemaName]) {
70
- tempGroupedData[schemaName] = {};
71
- }
72
- // Ensure the record (by ID) exists for this schema
73
- if (!tempGroupedData[schemaName][recordKey]) {
74
- tempGroupedData[schemaName][recordKey] = {
75
- id: id,
76
- ...(locale ? { locale } : {}),
77
- data: {},
78
- visible: isElementVisible(el),
79
- };
80
- }
81
- // Add the field and its text content to the record
82
- if (el.textContent) {
83
- tempGroupedData[schemaName][recordKey].data[fieldName] = el.textContent.trim();
84
- }
72
+ const parsed = parseDataSource(source);
73
+ if (!parsed) {
74
+ return;
75
+ }
76
+ const { schemaName, identity, fieldName } = parsed;
77
+ const { id, locale } = identity;
78
+ const recordKey = JSON.stringify([id, locale ?? null]);
79
+ // Skip __translations
80
+ if (schemaName === '__translations') {
81
+ return;
82
+ }
83
+ // Ensure the schema key exists
84
+ if (!tempGroupedData[schemaName]) {
85
+ tempGroupedData[schemaName] = {};
86
+ }
87
+ // Ensure the record (by ID) exists for this schema
88
+ if (!tempGroupedData[schemaName][recordKey]) {
89
+ tempGroupedData[schemaName][recordKey] = {
90
+ id: id,
91
+ ...(locale ? { locale } : {}),
92
+ data: {},
93
+ visible: isElementVisible(el),
94
+ };
95
+ }
96
+ // Add the field and its text content to the record
97
+ if (el.textContent) {
98
+ tempGroupedData[schemaName][recordKey].data[fieldName] = el.textContent.trim();
85
99
  }
86
100
  });
87
101
  // Convert the temporary object into the final desired array structure.
@@ -0,0 +1,25 @@
1
+ import { type ComponentPropsWithoutRef, type ElementType, type ReactNode } from 'react';
2
+ type JsonProps<T extends ElementType = 'span'> = {
3
+ value: unknown;
4
+ as?: T;
5
+ children?: (value: unknown) => ReactNode;
6
+ } & Omit<ComponentPropsWithoutRef<T>, 'value' | 'as' | 'children'>;
7
+ /**
8
+ * Json component for rendering JSON field values with source tracking.
9
+ *
10
+ * Use children render function to customize how the value is displayed.
11
+ * Pass `as="pre"` (with a `white-space: pre-wrap` class) for formatted output.
12
+ *
13
+ * @example
14
+ * // Default (single-line JSON text)
15
+ * <Json value={product.metadata} />
16
+ *
17
+ * // Pretty-printed
18
+ * <Json value={product.metadata} as="pre" className="whitespace-pre-wrap" />
19
+ *
20
+ * // With render function for custom display
21
+ * <Json value={product.metadata}>{(val) => val.tags?.join(', ')}</Json>
22
+ */
23
+ export declare function Json<T extends ElementType = 'span'>({ value, as, children, ...props }: JsonProps<T>): import("react/jsx-runtime").JSX.Element | null;
24
+ export {};
25
+ //# sourceMappingURL=Json.d.ts.map
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { getFimoSource, useFimoLiveValue } from '../lib/index.js';
3
+ /**
4
+ * Json component for rendering JSON field values with source tracking.
5
+ *
6
+ * Use children render function to customize how the value is displayed.
7
+ * Pass `as="pre"` (with a `white-space: pre-wrap` class) for formatted output.
8
+ *
9
+ * @example
10
+ * // Default (single-line JSON text)
11
+ * <Json value={product.metadata} />
12
+ *
13
+ * // Pretty-printed
14
+ * <Json value={product.metadata} as="pre" className="whitespace-pre-wrap" />
15
+ *
16
+ * // With render function for custom display
17
+ * <Json value={product.metadata}>{(val) => val.tags?.join(', ')}</Json>
18
+ */
19
+ export function Json({ value, as, children, ...props }) {
20
+ const Component = as || 'span';
21
+ const { value: liveValue, ref } = useFimoLiveValue(value);
22
+ // Defensive: return null if no value
23
+ if (value === null || value === undefined) {
24
+ return null;
25
+ }
26
+ const source = getFimoSource(value);
27
+ const content = children ? children(liveValue) : JSON.stringify(liveValue);
28
+ return (_jsx(Component, { ref: ref, ...props, "data-fimo-source": source, children: content }));
29
+ }
@@ -4,6 +4,7 @@ export { StaticImage } from './components/StaticImage.js';
4
4
  export { Video } from './components/Video.js';
5
5
  export { Date } from './components/Date.js';
6
6
  export { Boolean } from './components/Boolean.js';
7
+ export { Json } from './components/Json.js';
7
8
  export { RichText } from './components/RichText.js';
8
9
  export { useLabels, useTranslations } from './translations.js';
9
10
  export { FimoString, FimoDate, FimoMedia, FimoBoolean, FimoRichText, type FimoPolymorphicReference, type FimoReference, type FimoSource, type FimoPart, type FimoRichTextContent, type FimoRichTextNodeProps, type FimoRichTextMarkProps, type FimoRichTextComponents, FIMO_SOURCE, FIMO_PARTS, isFimoString, isFimoDate, hasFimoSource, getFimoSource, getFimoParts, fimo, useFimoLiveValue, } from './lib/index.js';
@@ -7,6 +7,7 @@ export { StaticImage } from './components/StaticImage.js';
7
7
  export { Video } from './components/Video.js';
8
8
  export { Date } from './components/Date.js';
9
9
  export { Boolean } from './components/Boolean.js';
10
+ export { Json } from './components/Json.js';
10
11
  export { RichText } from './components/RichText.js';
11
12
  // Labels
12
13
  export { useLabels, useTranslations } from './translations.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fimo",
3
- "version": "0.10.1-staging.4",
3
+ "version": "0.11.0-staging.1",
4
4
  "description": "Fimo CLI - create, deploy, and manage Fimo projects",
5
5
  "bin": {
6
6
  "fimo": "dist/cli/index.js"
@@ -68,7 +68,7 @@
68
68
  "eta": "^3.5.0",
69
69
  "oxc-parser": "^0.118.0",
70
70
  "react-error-boundary": "^6.0.0",
71
- "zod": "4.1.8"
71
+ "zod": "4.4.3"
72
72
  },
73
73
  "devDependencies": {
74
74
  "@clack/core": "^1.2.0",
@@ -82,7 +82,7 @@
82
82
  "@types/react": "^19.2.14",
83
83
  "@types/react-dom": "^19.2.3",
84
84
  "agent-install": "^0.0.5",
85
- "better-auth": "1.6.23",
85
+ "better-auth": "1.6.25",
86
86
  "cheerio": "^1.0.0",
87
87
  "commander": "^13.1.0",
88
88
  "esbuild": "^0.25.10",
@@ -94,7 +94,7 @@
94
94
  "typescript": "5.9.2",
95
95
  "vite": "^8.0.13",
96
96
  "vitest": "^4.1.6",
97
- "wrangler": "^4.83.0",
97
+ "wrangler": "4.114.0",
98
98
  "yaml": "2.8.2"
99
99
  },
100
100
  "peerDependencies": {
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@base-ui/react": "^1.3.0",
15
- "@hookform/resolvers": "^5.2.2",
15
+ "@hookform/resolvers": "^5.5.7",
16
16
  "@react-router/node": "^8.2.0",
17
17
  "@tailwindcss/typography": "^0.5.19",
18
18
  "@tanstack/react-query": "^5.80.6",
@@ -21,25 +21,25 @@
21
21
  "cmdk": "^1.1.1",
22
22
  "date-fns": "^4.1.0",
23
23
  "embla-carousel-react": "^8.6.0",
24
- "fimo": "0.10.1-staging.4",
24
+ "fimo": "0.11.0-staging.1",
25
25
  "input-otp": "^1.4.2",
26
26
  "isbot": "^5",
27
27
  "lucide-react": "^0.577.0",
28
- "motion": "^12.36.0",
28
+ "motion": "^12.42.2",
29
29
  "next-themes": "^0.4.6",
30
- "radix-ui": "^1.4.3",
30
+ "radix-ui": "^1.6.7",
31
31
  "react": "^19.2.7",
32
32
  "react-day-picker": "^9.14.0",
33
33
  "react-dom": "^19.2.7",
34
34
  "react-error-boundary": "^6.0.0",
35
- "react-hook-form": "^7.72.1",
35
+ "react-hook-form": "^7.83.0",
36
36
  "react-resizable-panels": "^4.9.0",
37
37
  "react-router": "^8.2.0",
38
38
  "recharts": "^3.8.0",
39
39
  "sonner": "^2.0.7",
40
40
  "tailwind-merge": "^3.2.0",
41
41
  "vaul": "^1.1.2",
42
- "zod": "^4.3.6"
42
+ "zod": "^4.4.3"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@react-router/dev": "^8.2.0",