fimo 0.2.5-experimental.1782385179125 → 0.2.5-staging.14

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.
@@ -14,7 +14,7 @@ Fimo provides locale utilities that follow the Fimo standard, but they are helpe
14
14
  "routing": {
15
15
  "strategy": "path-prefix", // only supported strategy: /es/about for Spanish; default locale can stay /about
16
16
  "prefixDefaultLocale": false, // when false, omit /en for the default locale
17
- }
17
+ },
18
18
  },
19
19
  }
20
20
  ```
@@ -69,6 +69,7 @@ Load the matching reference when the user's request fires its trigger. Recipes t
69
69
  - "What's in this project / show the project structure / describe the project / what schemas does it have / how many routes / project tree / show me what's here" → `references/describe.md`
70
70
  - "Deploy / publish / go live / push my changes" → `references/deploy.md`
71
71
  - "Get the preview URL / open the preview / what's the URL for this env / refresh the preview link / I just want to see the site / the preview URL stopped working" → `references/preview.md`
72
+ - "Schedule a command / cron / nightly publish / recurring checks / `fimo schedules`" → `references/schedules.md`
72
73
  - "Set env vars / configure API keys / add project secrets / local `.env.local` / `fimo vars` / `fimo secrets`" → `references/project-vars-and-secrets.md`
73
74
  - "Enable provider tools / integrations / API key connection for a provider / agent integration access" → `references/integrations.md`
74
75
  - "Firecrawl / scrape a public website / competitor research / agent web research" → `references/integrations.md` **+ load `references/integrations/firecrawl.md`**
@@ -14,7 +14,7 @@ Edit `fimo-config.json`. Keep one locale unless the user asks for multilingual o
14
14
  "routing": {
15
15
  "strategy": "path-prefix", // only supported strategy: /es/about for Spanish; default locale can stay /about
16
16
  "prefixDefaultLocale": false, // when false, omit /en for the default locale
17
- }
17
+ },
18
18
  },
19
19
  }
20
20
  ```
@@ -0,0 +1,46 @@
1
+ # Scheduled Commands
2
+
3
+ `fimo schedules` manages recurring commands for a project env. A schedule is scoped to the env of the current checkout by default, or to `--env <name>` when passed.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ fimo schedules create <name> --cron "0 2 * * *" --do "fimo publish"
9
+ fimo schedules create smoke --cron "*/15 * * * *" --do "pnpm test"
10
+ fimo schedules list
11
+ fimo schedules run <name>
12
+ fimo schedules delete <name>
13
+ ```
14
+
15
+ ## Runtime Model
16
+
17
+ Scheduled commands run in the project's env runtime, not on the user's machine. On each run, Fimo prepares the env, refreshes the project files and dependencies, authenticates as the schedule owner, then executes the stored command from the project root.
18
+
19
+ `--do` can be any CLI command that makes sense for the project, such as `fimo publish`, `fimo validate`, `pnpm test`, or a project script.
20
+
21
+ Cron expressions use standard five-field syntax. Schedules must not run more often than every five minutes.
22
+
23
+ ## Useful Patterns
24
+
25
+ Nightly publish:
26
+
27
+ ```bash
28
+ fimo schedules create nightly --cron "0 2 * * *" --do "fimo publish"
29
+ ```
30
+
31
+ Recurring smoke test:
32
+
33
+ ```bash
34
+ fimo schedules create smoke --cron "*/15 * * * *" --do "pnpm test"
35
+ ```
36
+
37
+ Manual run without waiting for cron:
38
+
39
+ ```bash
40
+ fimo schedules run smoke
41
+ ```
42
+
43
+ ## Notes
44
+
45
+ - `list`, `run`, and `delete` operate on the current checkout env unless `--env` is passed.
46
+ - A failed command marks the schedule `failed`, stores the error/log excerpt, and `fimo schedules run` exits non-zero.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/build/vite/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,YAAY,EAAE,MAAM,MAAM,CAAC;AAwEjD,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;;;;;;;;OAUG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAGD,wBAAgB,IAAI,CAAC,IAAI,GAAE,eAAoB,GAAG,YAAY,EAAE,CAsB/D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/build/vite/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,YAAY,EAAE,MAAM,MAAM,CAAC;AAyEjD,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;;;;;;;;OAUG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAGD,wBAAgB,IAAI,CAAC,IAAI,GAAE,eAAoB,GAAG,YAAY,EAAE,CAsB/D"}
@@ -34,6 +34,7 @@ function bundleFimoIntoSsrPlugin() {
34
34
  config: () => ({
35
35
  ssr: { noExternal: ['fimo'] },
36
36
  optimizeDeps: {
37
+ include: ['fimo/ui'],
37
38
  esbuildOptions: {
38
39
  plugins: [
39
40
  {
@@ -1 +1 @@
1
- {"version":3,"file":"translations.d.ts","sourceRoot":"","sources":["../../../../src/build/vite/plugins/translations.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAC;AAQlD,UAAU,WAAW;IACnB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACrD,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAuBD,MAAM,CAAC,OAAO,UAAU,kBAAkB,IAAI,MAAM,CA2MnD;AAqCD,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAwC3E"}
1
+ {"version":3,"file":"translations.d.ts","sourceRoot":"","sources":["../../../../src/build/vite/plugins/translations.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAC;AAQlD,UAAU,WAAW;IACnB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACrD,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAwCD,MAAM,CAAC,OAAO,UAAU,kBAAkB,IAAI,MAAM,CAmOnD;AAoED,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAwC3E"}
@@ -34,7 +34,21 @@ export default function translationsPlugin() {
34
34
  for (const mod of findVirtualTranslationModules(devServer)) {
35
35
  devServer.moduleGraph.invalidateModule(mod);
36
36
  }
37
- devServer.ws.send({ type: 'full-reload', path: '*' });
37
+ devServer.ws.send({
38
+ type: 'custom',
39
+ event: 'fimo:labels-updated',
40
+ data: labelUpdatePayload(bundle),
41
+ });
42
+ };
43
+ const emitContentUpdate = (payload) => {
44
+ if (!devServer) {
45
+ return;
46
+ }
47
+ devServer.ws.send({
48
+ type: 'custom',
49
+ event: 'fimo:content-updated',
50
+ data: payload,
51
+ });
38
52
  };
39
53
  const scheduleRefresh = () => {
40
54
  if (refreshTimer) {
@@ -161,6 +175,10 @@ export default function translationsPlugin() {
161
175
  }
162
176
  try {
163
177
  const message = JSON.parse(raw);
178
+ if (message.type === 'content:changed') {
179
+ emitContentUpdate(parseContentChangePayload(message.payload));
180
+ return;
181
+ }
164
182
  if (message.type !== 'labels:changed') {
165
183
  return;
166
184
  }
@@ -190,6 +208,27 @@ export default function translationsPlugin() {
190
208
  return sockets;
191
209
  }
192
210
  }
211
+ function parseContentChangePayload(payload) {
212
+ if (!payload || typeof payload !== 'object') {
213
+ return { changes: [] };
214
+ }
215
+ const source = payload;
216
+ const changes = Array.isArray(source.changes)
217
+ ? source.changes
218
+ .filter((change) => Boolean(change) && typeof change === 'object')
219
+ .map((change) => ({
220
+ action: typeof change.action === 'string' ? change.action : undefined,
221
+ contentType: typeof change.contentType === 'string' ? change.contentType : undefined,
222
+ documentId: typeof change.documentId === 'string' ? change.documentId : undefined,
223
+ id: typeof change.id === 'string' ? change.id : undefined,
224
+ locale: typeof change.locale === 'string' ? change.locale : undefined,
225
+ }))
226
+ : [];
227
+ return {
228
+ changes,
229
+ updatedAt: typeof source.updatedAt === 'string' ? source.updatedAt : undefined,
230
+ };
231
+ }
193
232
  function serializeBundle(bundle) {
194
233
  return JSON.stringify({
195
234
  dictionaries: bundle.dictionaries,
@@ -221,6 +260,12 @@ function findVirtualTranslationModules(server) {
221
260
  function labelSignature(bundle) {
222
261
  return Buffer.from(serializeBundle(bundle)).toString('base64');
223
262
  }
263
+ function labelUpdatePayload(bundle) {
264
+ return {
265
+ ...bundle,
266
+ signature: labelSignature(bundle),
267
+ };
268
+ }
224
269
  export async function loadLabelBundle(rootDir) {
225
270
  const config = getConfigServer(rootDir);
226
271
  const defaultLocale = getFimoDefaultLocale(config);
@@ -379,7 +424,7 @@ async function generateOneTimeToken(apiUrl) {
379
424
  throw new Error('Not signed in to Fimo.');
380
425
  }
381
426
  const response = await fetch(new URL('/api/auth/one-time-token/generate', apiUrl), {
382
- method: 'POST',
427
+ method: 'GET',
383
428
  headers: {
384
429
  Authorization: `Bearer ${bearer}`,
385
430
  },
@@ -110,7 +110,7 @@ describe('translationsPlugin', () => {
110
110
  expect(source).toContain('export const translations = {};');
111
111
  expect(source).toContain('export const defaultLocale = "es";');
112
112
  });
113
- it('invalidates the dev page when background labels arrive', async () => {
113
+ it('pushes updated labels over custom HMR without a full page reload', async () => {
114
114
  const root = createProjectRoot();
115
115
  mockLabels({ headline: 'Hola' });
116
116
  const plugin = createPlugin(root, 'serve');
@@ -121,7 +121,18 @@ describe('translationsPlugin', () => {
121
121
  }
122
122
  plugin.configureServer.call(pluginContext, server);
123
123
  await waitFor(() => server.ws.send.mock.calls.length > 0);
124
- expect(server.ws.send).toHaveBeenCalledWith({ type: 'full-reload', path: '*' });
124
+ expect(server.ws.send).toHaveBeenCalledWith({
125
+ type: 'custom',
126
+ event: 'fimo:labels-updated',
127
+ data: expect.objectContaining({
128
+ defaultLocale: 'es',
129
+ dictionaries: { es: { headline: 'Hola' } },
130
+ locales: ['es'],
131
+ signature: expect.any(String),
132
+ updatedAt: '2026-06-24T00:00:00.000Z',
133
+ }),
134
+ });
135
+ expect(server.ws.send).not.toHaveBeenCalledWith({ type: 'full-reload', path: '*' });
125
136
  const source = await loadTranslations(plugin);
126
137
  expect(source).toContain('"headline":"Hola"');
127
138
  });
@@ -141,7 +152,7 @@ describe('translationsPlugin', () => {
141
152
  }
142
153
  close() { }
143
154
  });
144
- vi.stubGlobal('fetch', vi.fn(async (input) => {
155
+ const fetchMock = vi.fn(async (input, init) => {
145
156
  const url = new URL(input.toString());
146
157
  if (url.pathname === '/labels') {
147
158
  return jsonResponse({ data: { labels: {}, updatedAt: null } });
@@ -150,10 +161,12 @@ describe('translationsPlugin', () => {
150
161
  return jsonResponse({ data: { eventsServerUrl: 'https://events.example.test' } });
151
162
  }
152
163
  if (url.pathname === '/api/auth/one-time-token/generate') {
153
- return jsonResponse({ data: { token: 'event-token' } });
164
+ expect(init?.method).toBe('GET');
165
+ return jsonResponse({ token: 'event-token' });
154
166
  }
155
167
  return { ok: false, json: async () => ({}) };
156
- }));
168
+ });
169
+ vi.stubGlobal('fetch', fetchMock);
157
170
  const plugin = createPlugin(root, 'serve');
158
171
  await buildStart(plugin);
159
172
  const server = createDevServer();
@@ -165,6 +178,62 @@ describe('translationsPlugin', () => {
165
178
  expect(socketUrls[0]).toContain(`wss://events.example.test/parties/events/${projectId}`);
166
179
  expect(socketUrls[0]).toContain('authToken=event-token');
167
180
  });
181
+ it('forwards content changes over custom HMR without a full page reload', async () => {
182
+ const root = createProjectRoot();
183
+ const projectId = '11111111-1111-4111-8111-111111111111';
184
+ writeFileSync(join(root, '.fimo.settings.json'), JSON.stringify({ projectId, apiUrl: 'https://api.example.test' }));
185
+ process.env.FIMO_API_TOKEN = 'test-api-token';
186
+ const sockets = [];
187
+ vi.stubGlobal('WebSocket', class {
188
+ onopen = null;
189
+ onmessage = null;
190
+ onclose = null;
191
+ onerror = null;
192
+ constructor(_url) {
193
+ sockets.push(this);
194
+ }
195
+ close() { }
196
+ });
197
+ vi.stubGlobal('fetch', vi.fn(async (input) => {
198
+ const url = new URL(input.toString());
199
+ if (url.pathname === '/labels') {
200
+ return jsonResponse({ data: { labels: {}, updatedAt: null } });
201
+ }
202
+ if (url.pathname === '/runtime-config') {
203
+ return jsonResponse({ data: { eventsServerUrl: 'https://events.example.test' } });
204
+ }
205
+ if (url.pathname === '/api/auth/one-time-token/generate') {
206
+ return jsonResponse({ data: { token: 'event-token' } });
207
+ }
208
+ return { ok: false, json: async () => ({}) };
209
+ }));
210
+ const plugin = createPlugin(root, 'serve');
211
+ await buildStart(plugin);
212
+ const server = createDevServer();
213
+ if (typeof plugin.configureServer !== 'function') {
214
+ throw new Error('Expected configureServer hook');
215
+ }
216
+ plugin.configureServer.call(pluginContext, server);
217
+ await waitFor(() => sockets.length > 0);
218
+ sockets[0].onmessage?.({
219
+ data: JSON.stringify({
220
+ type: 'content:changed',
221
+ payload: {
222
+ changes: [{ action: 'update', contentType: 'LiveNote', id: 'entry-1', locale: 'en' }],
223
+ updatedAt: '2026-06-24T00:00:00.000Z',
224
+ },
225
+ }),
226
+ });
227
+ expect(server.ws.send).toHaveBeenCalledWith({
228
+ type: 'custom',
229
+ event: 'fimo:content-updated',
230
+ data: {
231
+ changes: [{ action: 'update', contentType: 'LiveNote', id: 'entry-1', locale: 'en' }],
232
+ updatedAt: '2026-06-24T00:00:00.000Z',
233
+ },
234
+ });
235
+ expect(server.ws.send).not.toHaveBeenCalledWith({ type: 'full-reload', path: '*' });
236
+ });
168
237
  it('still waits for labels during production builds', async () => {
169
238
  const root = createProjectRoot();
170
239
  const fetchMock = mockLabels({ headline: 'Hola' });
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "bundled": true,
3
3
  "bundler": "esbuild",
4
- "bundledAt": "2026-06-25T10:59:49.127Z",
5
- "cliVersion": "0.2.5-experimental.1782385179125",
4
+ "bundledAt": "2026-06-26T17:21:55.057Z",
5
+ "cliVersion": "0.2.5-staging.14",
6
6
  "external": [
7
7
  "oxc-parser",
8
8
  "fsevents"