fimo 0.10.0 → 0.10.1-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.
@@ -5,7 +5,7 @@ description: >-
5
5
  Load when the user wants to create a new Fimo project, build content
6
6
  (schemas, entries, forms, media, labels/locales), deploy a site, manage
7
7
  custom domains, configure outbound webhooks and event-driven integrations,
8
- check site traffic/analytics, invite collaborators, inspect AI credits,
8
+ check site traffic/analytics, inspect or restore project history, invite collaborators, inspect AI credits,
9
9
  report a failing or slow Fimo API, rejected documented argument,
10
10
  undocumented workaround, missing capability that blocks work, forces a
11
11
  fragile or heavily manual workaround, or would materially accelerate a
@@ -62,6 +62,7 @@ npx fimo@latest clone # interactively choose an existin
62
62
  cd my-site # then load skills on demand: npx fimo@latest skills get fimo-code
63
63
  pnpm dev # run the app locally (use the project's PM: pnpm/npm run/yarn/bun) — your iteration loop
64
64
  npx fimo@latest credits # inspect the owning org's AI credit balance (read-only)
65
+ npx fimo@latest history list # inspect this environment's restore points
65
66
  npx fimo@latest validate # run all checks + codegen; fails on missing DB labels
66
67
  npx fimo@latest deploy # ON-DEMAND: hosted preview / shareable link (runs validate internally)
67
68
  npx fimo@latest deploy --publish # ON-DEMAND: publish live (only when the developer says "ship it")
@@ -92,6 +93,7 @@ Load the matching reference when the user's request fires its trigger. Recipes t
92
93
  - "Enable provider tools / integrations / API key connection for a provider / agent integration access" → `references/integrations.md`
93
94
  - "Firecrawl / scrape a public website / competitor research / agent web research" → `references/integrations.md` **+ load `references/integrations/firecrawl.md`**
94
95
  - "Check credits / credit balance / can I keep using AI / billing credits / running low / how many credits are left" → `references/credits.md`
96
+ - "List history / create a restore point / recover content and code / restore a previous version / snapshot usage or retention" → `references/history.md`
95
97
  - "Create a branch / feature environment / new env / switch envs / merge envs / diff two envs / env status / what changed on this branch / sync from parent / cut a release / tag a version / go to prod" → `references/branches.md`
96
98
  - "List my projects / clone an existing project locally / check out a project / delete this project / unpublish / rename the project / manage projects across the org" → `references/projects.md`
97
99
  - "Migrate an existing Webflow / Framer / Squarespace / Wix / Carrd site from only a published URL" → load the `fimo-migration` skill and start with `fimo migrate <url> [dir]` to prepare the agent-ready migration draft
@@ -0,0 +1,40 @@
1
+ # Project History
2
+
3
+ > **Run via `npx fimo@latest <command>`** — always the latest CLI, no install needed.
4
+
5
+ History is scoped to the selected project environment. A restore point records the environment's CMS content and the matching Git commit so code and content can be recovered together.
6
+
7
+ ```bash
8
+ fimo history list
9
+ fimo history show <version-id>
10
+ fimo history create "Before content migration"
11
+ fimo history restore <version-id>
12
+ ```
13
+
14
+ Use the global `--env <name>` option to target a different environment. Use `--project <id>` when there is no local `.fimo.settings.json`.
15
+
16
+ ## Pagination and usage
17
+
18
+ `fimo history list` returns 20 restore points by default. Use `--limit <1-100>` to change the page size. When another page exists, the output includes the exact next command and its opaque cursor:
19
+
20
+ ```bash
21
+ fimo history list --limit 20 --cursor <cursor>
22
+ ```
23
+
24
+ The list also reports compressed snapshot usage and the current retention period. Restore points expire after 14 days on Free and pay-as-you-go plans, or 90 days on Premium. The expiry is fixed when each point is created.
25
+
26
+ ## Restore safety
27
+
28
+ Before replacing code and content, Fimo creates an automatic `restore-safety` point from the environment's current state. Restoring that point is the fast recovery path when the selected result is not what the user expected.
29
+
30
+ Pretty mode asks for confirmation. Agents and other non-interactive callers must pass both `--json` and `--yes`:
31
+
32
+ ```bash
33
+ fimo --json history restore <version-id> --yes
34
+ ```
35
+
36
+ If another history create, update, delete, or restore is already running, the command fails with `HISTORY_BUSY`. Retry after the active history operation finishes. Do not loop without a delay.
37
+
38
+ If content or Git restore does not finish cleanly, the command fails with `RESTORE_INCOMPLETE` and names the automatic safety point. Stop active editing, inspect the environment, and retry the restore. Collaborative-editor resync continues in the background after code and content are ready.
39
+
40
+ Normal CMS edits, Git pushes, and unsaved collaborative-editor changes do not take the history-operation lock. Avoid starting a restore while someone is actively editing the same environment.
@@ -1,48 +1,71 @@
1
1
  # Scheduled Commands
2
2
 
3
- > **Run via `npx fimo@latest <command>`** — always the latest CLI, no install needed.
3
+ > **Run via `npx fimo@latest <command>`** - always the latest CLI, no install needed.
4
4
 
5
- `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.
5
+ `fimo schedules` manages recurring commands for one project environment. It uses the current checkout environment by default, or the environment passed with `--env <name>`.
6
+
7
+ The command starts in that environment's sandbox. Commands can still target another environment explicitly, for example with `fimo schemas list --env preview`.
6
8
 
7
9
  ## Commands
8
10
 
9
11
  ```bash
10
- fimo schedules create <name> --cron "0 2 * * *" --do "fimo publish"
11
- fimo schedules create smoke --cron "*/15 * * * *" --do "pnpm test"
12
+ fimo schedules create <name> --cron "0 2 * * *" --do "fimo validate"
13
+ fimo schedules create <name> --cron "0 2 * * *" --do "pnpm sync" --secret CMS_TOKEN --secret SEARCH_KEY
12
14
  fimo schedules list
13
- fimo schedules run <name>
14
- fimo schedules delete <name>
15
+ fimo schedules show <schedule>
16
+ fimo schedules update <schedule> --cron "0 3 * * *" --secret CMS_TOKEN
17
+ fimo schedules update <schedule> --clear-secrets
18
+ fimo schedules pause <schedule>
19
+ fimo schedules resume <schedule>
20
+ fimo schedules run <schedule>
21
+ fimo schedules run <schedule> --no-wait
22
+ fimo schedules runs <schedule> --limit 25 --status failed
23
+ fimo schedules runs <schedule> --all
24
+ fimo schedules runs show <run-id>
25
+ fimo schedules delete <schedule>
15
26
  ```
16
27
 
17
- ## Runtime Model
28
+ `<schedule>` accepts a schedule ID or an exact schedule name. Name-based commands resolve the name in the active environment and then use the stable schedule ID.
29
+
30
+ ## Runtime model
31
+
32
+ Scheduled commands run in the project's environment runtime, not on the user's machine. Fimo prepares the environment and executes the stored command from the project root.
18
33
 
19
- 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.
34
+ `--do` can be any command that works with schedule runtime permissions, such as `fimo validate`, `pnpm test`, or a project script. The runtime can edit project content and publish or unpublish the project, but it cannot manage project access, secrets, schedules, or other project-level settings.
20
35
 
21
- `--do` can be any CLI command that makes sense for the project, such as `fimo publish`, `fimo validate`, `pnpm test`, or a project script.
36
+ Runs use at-least-once delivery. If Fimo loses a completion response, the same run may execute again. Side-effecting commands should use `FIMO_SCHEDULE_RUN_ID` as their idempotency key.
22
37
 
23
38
  Cron expressions use standard five-field syntax. Schedules must not run more often than every five minutes.
24
39
 
25
- ## Useful Patterns
40
+ ## Project secrets
26
41
 
27
- Nightly publish:
42
+ Repeat `--secret KEY` to expose selected project secrets to a scheduled command. Creating a schedule without `--secret` exposes no project secrets. Updating with one or more `--secret` flags replaces the selected key names. Use `--clear-secrets` to remove every selected key; it cannot be combined with `--secret`.
28
43
 
29
- ```bash
30
- fimo schedules create nightly --cron "0 2 * * *" --do "fimo publish"
31
- ```
44
+ Schedule and run output shows secret key names and snapshot state only. It never returns secret values.
32
45
 
33
- Recurring smoke test:
46
+ ## Runs
34
47
 
35
- ```bash
36
- fimo schedules create smoke --cron "*/15 * * * *" --do "pnpm test"
37
- ```
48
+ `fimo schedules run` queues a manual run, prints its run ID, and waits for the run to finish. Use `--no-wait` to return as soon as it is queued.
49
+
50
+ Only one run for a schedule can be active at a time. If a run is already queued or running, the command returns `run_in_progress` with the active run ID.
38
51
 
39
- Manual run without waiting for cron:
52
+ Run history is cursor-paginated:
40
53
 
41
54
  ```bash
42
- fimo schedules run smoke
55
+ fimo schedules runs nightly --limit 50
56
+ fimo schedules runs nightly --cursor <cursor>
57
+ fimo schedules runs nightly --status succeeded
58
+ fimo schedules runs nightly --all
43
59
  ```
44
60
 
61
+ Run detail includes status, timing, trigger, exit/error information, retained output, output retention metadata, and secret snapshot metadata. Output is capped and redacted before it is returned.
62
+
63
+ ## JSON
64
+
65
+ All schedule commands support the CLI's global `--json` mode. List and run-history responses preserve the API page shape, including `items`, `nextCursor`, `usage`, and `policy`.
66
+
45
67
  ## Notes
46
68
 
47
- - `list`, `run`, and `delete` operate on the current checkout env unless `--env` is passed.
48
- - A failed command marks the schedule `failed`, stores the error/log excerpt, and `fimo schedules run` exits non-zero.
69
+ - Every operation is scoped to the active environment unless `--env` is passed.
70
+ - Pausing stops future cron runs. It does not delete history.
71
+ - A failed or skipped manual run exits non-zero with its run ID and an exact `runs show` command for inspecting detail.
@@ -6,8 +6,8 @@ description: >-
6
6
  Triggers: "in Fimo Studio", "in the dashboard", "in the web app",
7
7
  "how do I X in the UI", or screenshots of the dashboard. Topics
8
8
  covered: dashboard content and CMS, publishing from the dashboard,
9
- dashboard SEO/domain settings, outbound webhooks, visual editing, version
10
- history UI, dashboard sharing/billing, and dashboard chat. Never describe
9
+ dashboard SEO/domain settings, schedules and run history, outbound webhooks,
10
+ visual editing, version history UI, dashboard sharing/billing, and dashboard chat. Never describe
11
11
  UI locations, buttons, or panels from memory — always read the relevant
12
12
  reference file first.
13
13
  ---
@@ -37,6 +37,7 @@ Read the relevant reference file when the user's question matches a topic.
37
37
  | AI Chat | references/ai-chat.md | "What can the AI do?", "How do I give context?" |
38
38
  | Code Editor | references/code-editor.md | "How do I edit code?", "Where are my files?" |
39
39
  | SEO Settings | references/seo-settings.md | "How do I set up SEO?", "Favicon, title, share image" |
40
+ | Schedules | references/schedules.md | "Recurring commands", "Run history", "Run now" |
40
41
  | Webhooks | references/webhooks.md | "React to forms", "Send events to another service" |
41
42
  | Preview & Responsive | references/preview-and-responsive.md | "How does it look on mobile?", "How do I test sizes?" |
42
43
  | Visual Editing | references/visual-editing.md | "Can I edit visually?", "How do I change colors?" |
@@ -55,6 +56,7 @@ Read the relevant reference file when the user's question matches a topic.
55
56
  | Adding images | Media Library, AI Chat, Edit Mode |
56
57
  | Content/CMS | AI Chat, Media Library |
57
58
  | Webhooks | Project secrets, Forms, Publishing, Agents |
59
+ | Schedules | Project secrets, Environments, Agents |
58
60
  | Visual changes | Edit Mode (quick tweaks), AI Chat (bigger changes), Code Editor (full control) |
59
61
  | Something went wrong | Version History, AI Chat |
60
62
  | Working with others | Sharing, Collaboration |
@@ -0,0 +1,43 @@
1
+ # Schedules
2
+
3
+ Schedules run recurring project commands in one environment. Use the Studio surface to review configuration, pause or resume automatic runs, start work now, and inspect what happened afterward.
4
+
5
+ ## Open schedules
6
+
7
+ 1. Open the project and choose **Settings**.
8
+ 2. Select **Schedules**.
9
+ 3. Choose the environment whose schedules you want to manage.
10
+
11
+ The list shows each schedule's name, cron, command, enabled state, and latest status. Usage and retention policy are shown for the selected environment.
12
+
13
+ ## Create or edit a schedule
14
+
15
+ Choose **New schedule** to set:
16
+
17
+ - A human-readable name
18
+ - A standard five-field cron expression
19
+ - The command to run from the project root
20
+ - The project secret keys the command is allowed to use
21
+
22
+ A schedule with no selected secret keys receives no project secrets. Studio displays key names only and never reveals secret values.
23
+
24
+ Open a schedule to change its configuration. Use **Pause** to prevent future cron runs without deleting its history, and **Resume** to make the cron active again.
25
+
26
+ ## Run now
27
+
28
+ Choose **Run now** from a schedule to queue a manual run. Only one run can be active for a schedule at a time. If one is already queued or running, Studio links to the active run instead of starting a duplicate.
29
+
30
+ Runs use at-least-once delivery. If Fimo loses a completion response, the same run may execute again. Side-effecting commands should use `FIMO_SCHEDULE_RUN_ID` as their idempotency key.
31
+
32
+ ## Inspect history
33
+
34
+ Open the **Runs** tab on a schedule to browse cursor-paginated history. Filter by queued, running, succeeded, failed, or skipped status.
35
+
36
+ Select a run to see:
37
+
38
+ - Status, trigger, actor, and queued/start/finish times
39
+ - Exit code, stable error code, and error message
40
+ - Redacted captured output, its size, truncation state, and expiry
41
+ - Secret snapshot state and key metadata
42
+
43
+ Secret snapshot metadata helps explain whether a selected key was resolved, missing, deleted, recreated, or rotated after the run. It never contains a secret value or hash.
@@ -1,25 +1,23 @@
1
1
  ## Where to Find Version History
2
2
 
3
- Click your **project name** in the top of the left sidebar to open the project dropdown menu, then select **History**. This opens the version history panel showing all your saved versions.
3
+ Click your **project name** in the top bar to open the project dropdown menu, then select **History**. This opens the version history panel showing all your saved versions.
4
4
 
5
5
  ## How Versions Are Created
6
6
 
7
- Versions are created automatically as you work — you don't create them by hand. Whenever the AI makes meaningful changes to your project, it captures a snapshot you can browse, rename, and restore. There is no "create version" button in the panel; the history list only shows the snapshots that already exist.
7
+ Versions are created automatically as you work — you don't create them by hand. Whenever the AI makes meaningful changes to your project, it captures a restore point for the current environment that you can browse, rename, and restore. A restore point includes the project code and CMS data, including schemas, entries, translations, and forms. There is no "create version" button in the panel; the history list only shows the restore points that already exist.
8
8
 
9
9
  ## Browsing Versions
10
10
 
11
11
  The version list shows each version's name and timestamp (the relative time, with the full date on hover).
12
12
 
13
- - The most recent version shows a **"Current"** badge.
14
-
15
13
  ## Restoring a Previous Version
16
14
 
17
15
  1. Find the version you want to restore.
18
16
  2. Click the **three-dot menu** next to the version and select **Restore**.
19
- 3. A confirmation dialog appears warning that this will replace your current project.
17
+ 3. A confirmation dialog explains that this will replace the current environment and save its current state as a safety point first.
20
18
  4. Click **Restore** to confirm.
21
19
 
22
- Your project reverts to that version.
20
+ The current environment's code and CMS data revert to that restore point. Uploaded assets do not change. If you need to undo the restore, restore the automatically created safety point.
23
21
 
24
22
  ## Editing Version Details
25
23
 
@@ -28,12 +26,14 @@ Find the version you want to update, click the **three-dot menu**, select **Edit
28
26
  ## Key Concepts
29
27
 
30
28
  - **Automatic versions** -- The AI creates these whenever it makes significant changes to your project. You do not need to do anything for these to appear.
31
- - **Rename, don't create** -- You can't add a version yourself, but you can rename any existing version (via the three-dot menu → **Edit**) to make it easier to find later.
32
- - **Restoring replaces everything** -- When you restore a version, it replaces all files in your project, not just the last change. This is a full rollback.
29
+ - **Studio is browse and restore** -- Studio does not create restore points manually. Developers can use `fimo history create "Name"` from the CLI.
30
+ - **Environment-scoped history** -- Each environment has its own restore points. Restoring one environment does not change another.
31
+ - **Safe restore** -- Fimo records the current code and CMS data as a safety point before replacing them.
32
+ - **Assets stay in place** -- Restoring changes code and CMS data, but does not remove or roll back uploaded assets.
33
33
 
34
34
  ## Quick Restore from Chat
35
35
 
36
- Each AI message in the chat that made changes to your project shows an **undo button** (↩ icon) on the right side. Click it and a **"Restore version?"** prompt appears; confirm it to restore your project to the state it was in at that message. This is the fastest way to undo a recent change without opening the full version history panel.
36
+ Each AI message in the chat that made changes to your project shows an **undo button** (↩ icon) on the right side. Click it and a **"Restore version?"** prompt appears; confirm it to restore the current environment to the state it was in at that message. Fimo saves a safety point first, so CMS work completed after that message remains recoverable. This is the fastest way to undo a recent change without opening the full version history panel.
37
37
 
38
38
  The undo button is disabled on the most recent change (you can't restore the version you're already on) and is only available to members with permission to manage history.
39
39
 
@@ -7,15 +7,24 @@ import { getFimoDefaultLocale, getFimoLocales } from '../../../runtime/i18n/loca
7
7
  import { getConfigServer } from '../../../runtime/shared/fimo-config.server.js';
8
8
  const VIRTUAL_ID = 'virtual:translations';
9
9
  const RESOLVED_ID = '\0' + VIRTUAL_ID;
10
+ const EVENT_RECONNECT_DELAY_MS = 2000;
11
+ const FALLBACK_POLL_INTERVAL_MS = 30_000;
10
12
  export default function translationsPlugin() {
11
13
  let rootDir = process.cwd();
12
- let bundle = { dictionaries: { en: {} }, defaultLocale: 'en', locales: ['en'], updatedAt: null };
14
+ let bundle = {
15
+ dictionaries: { en: {} },
16
+ defaultLocale: 'en',
17
+ locales: ['en'],
18
+ updatedAt: null,
19
+ };
13
20
  let command = 'build';
14
21
  let devServer = null;
15
22
  let eventSockets = [];
16
23
  let reconnectTimer;
17
24
  let refreshTimer;
18
25
  let pollTimer;
26
+ let eventConnectionAttempt = 0;
27
+ let closed = false;
19
28
  const refreshLabels = async () => {
20
29
  bundle = await loadLabelBundle(rootDir);
21
30
  return bundle;
@@ -51,13 +60,91 @@ export default function translationsPlugin() {
51
60
  });
52
61
  };
53
62
  const scheduleRefresh = () => {
63
+ if (closed) {
64
+ return;
65
+ }
54
66
  if (refreshTimer) {
55
67
  clearTimeout(refreshTimer);
56
68
  }
57
69
  refreshTimer = setTimeout(() => {
70
+ refreshTimer = undefined;
71
+ if (closed) {
72
+ return;
73
+ }
58
74
  void refreshLabelsAndInvalidateIfChanged().catch(() => undefined);
59
75
  }, 150);
60
76
  };
77
+ const startFallbackPolling = () => {
78
+ if (closed || pollTimer) {
79
+ return;
80
+ }
81
+ pollTimer = setInterval(() => {
82
+ void refreshLabelsAndInvalidateIfChanged().catch(() => undefined);
83
+ }, FALLBACK_POLL_INTERVAL_MS);
84
+ };
85
+ const stopFallbackPolling = () => {
86
+ if (!pollTimer) {
87
+ return;
88
+ }
89
+ clearInterval(pollTimer);
90
+ pollTimer = undefined;
91
+ };
92
+ const closeEventSockets = () => {
93
+ closeSockets(eventSockets.splice(0));
94
+ };
95
+ const scheduleReconnect = (delayMs = EVENT_RECONNECT_DELAY_MS) => {
96
+ startFallbackPolling();
97
+ if (closed || reconnectTimer) {
98
+ return;
99
+ }
100
+ reconnectTimer = setTimeout(() => {
101
+ reconnectTimer = undefined;
102
+ closeEventSockets();
103
+ void connectEvents();
104
+ }, delayMs);
105
+ };
106
+ const connectEvents = async () => {
107
+ if (closed) {
108
+ return;
109
+ }
110
+ const attempt = ++eventConnectionAttempt;
111
+ const sockets = [];
112
+ eventSockets = sockets;
113
+ try {
114
+ await connectLabelEvents(rootDir, scheduleRefresh, sockets, {
115
+ isActive: () => !closed && attempt === eventConnectionAttempt,
116
+ onConnected: () => {
117
+ if (closed || attempt !== eventConnectionAttempt) {
118
+ return;
119
+ }
120
+ if (reconnectTimer) {
121
+ clearTimeout(reconnectTimer);
122
+ reconnectTimer = undefined;
123
+ }
124
+ stopFallbackPolling();
125
+ },
126
+ onDisconnected: () => {
127
+ if (closed || attempt !== eventConnectionAttempt) {
128
+ return;
129
+ }
130
+ scheduleReconnect();
131
+ },
132
+ });
133
+ if (closed || attempt !== eventConnectionAttempt) {
134
+ closeSockets(sockets.splice(0));
135
+ return;
136
+ }
137
+ if (sockets.length === 0) {
138
+ scheduleReconnect(FALLBACK_POLL_INTERVAL_MS);
139
+ }
140
+ }
141
+ catch {
142
+ if (!closed && attempt === eventConnectionAttempt) {
143
+ closeEventSockets();
144
+ scheduleReconnect(FALLBACK_POLL_INTERVAL_MS);
145
+ }
146
+ }
147
+ };
61
148
  return {
62
149
  name: 'fimo-virtual-translations',
63
150
  enforce: 'pre',
@@ -76,16 +163,11 @@ export default function translationsPlugin() {
76
163
  },
77
164
  configureServer(server) {
78
165
  devServer = server;
166
+ closed = false;
79
167
  server.watcher.add([join(rootDir, 'fimo-config.json'), join(rootDir, '.env'), join(rootDir, '.env.local')]);
80
168
  void refreshLabelsAndInvalidateIfChanged().catch(() => undefined);
81
- connectLabelEvents(rootDir, scheduleRefresh)
82
- .then((sockets) => {
83
- eventSockets = sockets;
84
- })
85
- .catch(() => undefined);
86
- pollTimer = setInterval(() => {
87
- void refreshLabelsAndInvalidateIfChanged().catch(() => undefined);
88
- }, 2000);
169
+ startFallbackPolling();
170
+ void connectEvents();
89
171
  },
90
172
  resolveId(id) {
91
173
  if (id === VIRTUAL_ID) {
@@ -128,84 +210,119 @@ export default function translationsPlugin() {
128
210
  return [];
129
211
  },
130
212
  closeBundle() {
131
- for (const socket of eventSockets) {
132
- socket.close();
133
- }
134
- eventSockets = [];
213
+ closed = true;
214
+ eventConnectionAttempt += 1;
215
+ closeEventSockets();
216
+ devServer = null;
135
217
  if (reconnectTimer) {
136
218
  clearTimeout(reconnectTimer);
219
+ reconnectTimer = undefined;
137
220
  }
138
221
  if (refreshTimer) {
139
222
  clearTimeout(refreshTimer);
223
+ refreshTimer = undefined;
140
224
  }
141
- if (pollTimer) {
142
- clearInterval(pollTimer);
143
- }
225
+ stopFallbackPolling();
144
226
  },
145
227
  };
146
- async function connectLabelEvents(root, onLabelsChanged) {
228
+ async function connectLabelEvents(root, onLabelsChanged, sockets, callbacks) {
147
229
  const env = await readProjectEnv(root);
230
+ if (!callbacks.isActive()) {
231
+ return;
232
+ }
148
233
  const apiUrl = env.VITE_API_URL;
149
234
  if (!apiUrl) {
150
- return [];
235
+ return;
151
236
  }
152
237
  const runtimeConfig = await loadRuntimeConfig(apiUrl);
238
+ if (!callbacks.isActive()) {
239
+ return;
240
+ }
153
241
  const eventsUrl = runtimeConfig.eventsServerUrl ?? undefined;
154
242
  if (!eventsUrl) {
155
- return [];
243
+ return;
156
244
  }
157
245
  const settings = await readProjectSettings(root);
246
+ if (!callbacks.isActive()) {
247
+ return;
248
+ }
158
249
  if (!settings.projectId) {
159
- return [];
250
+ return;
160
251
  }
161
252
  const WebSocketCtor = globalThis.WebSocket;
162
253
  if (!WebSocketCtor) {
163
- return [];
254
+ return;
164
255
  }
165
- const sockets = [];
166
- for (const room of deriveEventsRooms(settings.projectId, apiUrl)) {
167
- const token = await generateOneTimeToken(settings.apiUrl ?? process.env.FIMO_API_URL ?? 'http://localhost:3000');
168
- const wsUrl = new URL(`/parties/events/${room}`, eventsUrl.replace(/^http/, 'ws'));
169
- wsUrl.searchParams.set('authToken', token);
170
- const socket = new WebSocketCtor(wsUrl.toString());
171
- socket.onmessage = (event) => {
172
- const raw = typeof event.data === 'string' ? event.data : '';
173
- if (!raw) {
256
+ const rooms = deriveEventsRooms(settings.projectId, apiUrl);
257
+ const openSockets = new Set();
258
+ try {
259
+ for (const room of rooms) {
260
+ const token = await generateOneTimeToken(settings.apiUrl ?? process.env.FIMO_API_URL ?? 'http://localhost:3000');
261
+ if (!callbacks.isActive()) {
262
+ closeSockets(sockets.splice(0));
174
263
  return;
175
264
  }
176
- try {
177
- const message = JSON.parse(raw);
178
- if (message.type === 'content:changed') {
179
- emitContentUpdate(parseContentChangePayload(message.payload));
265
+ const wsUrl = new URL(`/parties/events/${room}`, eventsUrl.replace(/^http/, 'ws'));
266
+ wsUrl.searchParams.set('authToken', token);
267
+ const socket = new WebSocketCtor(wsUrl.toString());
268
+ socket.onopen = () => {
269
+ if (!callbacks.isActive()) {
180
270
  return;
181
271
  }
182
- if (message.type !== 'labels:changed') {
272
+ openSockets.add(socket);
273
+ if (openSockets.size === rooms.length) {
274
+ callbacks.onConnected();
275
+ }
276
+ };
277
+ socket.onmessage = (event) => {
278
+ if (!callbacks.isActive()) {
183
279
  return;
184
280
  }
185
- const changedLocales = message.payload?.changes
186
- ?.map((change) => change.locale)
187
- .filter((locale) => typeof locale === 'string') ?? [];
188
- if (changedLocales.length === 0 || changedLocales.some((locale) => bundle.locales.includes(locale))) {
189
- onLabelsChanged();
281
+ const raw = typeof event.data === 'string' ? event.data : '';
282
+ if (!raw) {
283
+ return;
190
284
  }
191
- }
192
- catch {
193
- return;
194
- }
195
- };
196
- socket.onclose = () => {
197
- reconnectTimer = setTimeout(() => {
198
- void connectLabelEvents(root, onLabelsChanged)
199
- .then((nextSockets) => {
200
- eventSockets = nextSockets;
201
- })
202
- .catch(() => undefined);
203
- }, 2000);
204
- };
205
- socket.onerror = () => undefined;
206
- sockets.push(socket);
285
+ try {
286
+ const message = JSON.parse(raw);
287
+ if (message.type === 'content:changed') {
288
+ emitContentUpdate(parseContentChangePayload(message.payload));
289
+ return;
290
+ }
291
+ if (message.type !== 'labels:changed') {
292
+ return;
293
+ }
294
+ const changedLocales = message.payload?.changes
295
+ ?.map((change) => change.locale)
296
+ .filter((locale) => typeof locale === 'string') ?? [];
297
+ if (changedLocales.length === 0 || changedLocales.some((locale) => bundle.locales.includes(locale))) {
298
+ onLabelsChanged();
299
+ }
300
+ }
301
+ catch {
302
+ return;
303
+ }
304
+ };
305
+ socket.onclose = () => {
306
+ openSockets.delete(socket);
307
+ callbacks.onDisconnected();
308
+ };
309
+ socket.onerror = callbacks.onDisconnected;
310
+ sockets.push(socket);
311
+ }
207
312
  }
208
- return sockets;
313
+ catch (error) {
314
+ closeSockets(sockets.splice(0));
315
+ throw error;
316
+ }
317
+ }
318
+ }
319
+ function closeSockets(sockets) {
320
+ for (const socket of sockets) {
321
+ socket.onopen = null;
322
+ socket.onmessage = null;
323
+ socket.onclose = null;
324
+ socket.onerror = null;
325
+ socket.close();
209
326
  }
210
327
  }
211
328
  function parseContentChangePayload(payload) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "bundled": true,
3
3
  "bundler": "esbuild",
4
- "bundledAt": "2026-07-27T11:53:13.436Z",
4
+ "bundledAt": "2026-07-27T14:30:59.819Z",
5
5
  "cliVersion": "0.10.0",
6
6
  "external": [
7
7
  "oxc-parser",