clawfire 0.6.19 → 0.6.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawfire",
3
- "version": "0.6.19",
3
+ "version": "0.6.20",
4
4
  "description": "AI-First Firebase app framework — Speak. Build. Deploy.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -1,11 +1,34 @@
1
1
  <!-- @title: Todos — Clawfire -->
2
2
 
3
3
  <div class="max-w-xl mx-auto">
4
- <div class="text-center mb-8">
4
+ <div class="text-center mb-6">
5
5
  <h1 class="text-3xl font-bold">
6
6
  <span class="bg-gradient-to-r from-brand-400 to-brand-600 bg-clip-text text-transparent">Todos</span>
7
7
  </h1>
8
- <p class="text-zinc-500 text-sm mt-1">Your first Clawfire app add, complete, and delete todos</p>
8
+ <p class="text-zinc-500 text-sm mt-1">Dual-mode storecompare In-Memory vs Firestore</p>
9
+ </div>
10
+
11
+ <!-- Tabs -->
12
+ <div class="flex border-b border-zinc-800 mb-6">
13
+ <button id="tab-memory" onclick="switchTab('memory')"
14
+ class="flex-1 py-3 text-sm font-semibold text-center border-b-2 transition-colors cursor-pointer">
15
+ <span class="inline-flex items-center gap-1.5">
16
+ <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2zM9 9h6v6H9V9z"/></svg>
17
+ In-Memory
18
+ </span>
19
+ </button>
20
+ <button id="tab-firestore" onclick="switchTab('firestore')"
21
+ class="flex-1 py-3 text-sm font-semibold text-center border-b-2 transition-colors cursor-pointer">
22
+ <span class="inline-flex items-center gap-1.5">
23
+ <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/></svg>
24
+ Firestore DB
25
+ </span>
26
+ </button>
27
+ </div>
28
+
29
+ <!-- Mode Badge -->
30
+ <div class="flex justify-center mb-4">
31
+ <span id="mode-badge" class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium"></span>
9
32
  </div>
10
33
 
11
34
  <!-- Input -->
@@ -44,10 +67,28 @@
44
67
  <script>
45
68
  (function() {
46
69
  const API = '/api/todos';
47
- let todos = [];
70
+ let currentMode = 'memory';
71
+ let memoryTodos = [];
72
+ let firestoreTodos = [];
73
+ let memoryLoaded = false;
74
+ let firestoreLoaded = false;
75
+ let firestoreError = null;
48
76
 
49
- async function api(path, body = {}) {
50
- const res = await fetch(`${API}/${path}`, {
77
+ function getTodos() {
78
+ return currentMode === 'memory' ? memoryTodos : firestoreTodos;
79
+ }
80
+ function setTodos(list) {
81
+ if (currentMode === 'memory') { memoryTodos = list; memoryLoaded = true; }
82
+ else { firestoreTodos = list; firestoreLoaded = true; }
83
+ }
84
+ function isLoaded() {
85
+ return currentMode === 'memory' ? memoryLoaded : firestoreLoaded;
86
+ }
87
+
88
+ async function api(path, body) {
89
+ if (!body) body = {};
90
+ body.mode = currentMode;
91
+ const res = await fetch(API + '/' + path, {
51
92
  method: 'POST',
52
93
  headers: { 'Content-Type': 'application/json' },
53
94
  body: JSON.stringify(body),
@@ -62,29 +103,91 @@
62
103
  return json.data;
63
104
  }
64
105
 
106
+ function updateTabs() {
107
+ var memTab = document.getElementById('tab-memory');
108
+ var fsTab = document.getElementById('tab-firestore');
109
+ var badge = document.getElementById('mode-badge');
110
+ if (!memTab || !fsTab) return;
111
+
112
+ if (currentMode === 'memory') {
113
+ memTab.className = 'flex-1 py-3 text-sm font-semibold text-center border-b-2 border-brand-500 text-brand-400 transition-colors cursor-pointer';
114
+ fsTab.className = 'flex-1 py-3 text-sm font-semibold text-center border-b-2 border-transparent text-zinc-500 hover:text-zinc-300 transition-colors cursor-pointer';
115
+ badge.className = 'inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-brand-500/10 text-brand-400 border border-brand-500/20';
116
+ badge.innerHTML = '<svg width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2zM9 9h6v6H9V9z"/></svg> In-Memory — resets on cold start';
117
+ } else {
118
+ fsTab.className = 'flex-1 py-3 text-sm font-semibold text-center border-b-2 border-amber-500 text-amber-400 transition-colors cursor-pointer';
119
+ memTab.className = 'flex-1 py-3 text-sm font-semibold text-center border-b-2 border-transparent text-zinc-500 hover:text-zinc-300 transition-colors cursor-pointer';
120
+ badge.className = 'inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-amber-500/10 text-amber-400 border border-amber-500/20';
121
+ badge.innerHTML = '<svg width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/></svg> Firestore DB — persists across deploys';
122
+ }
123
+ }
124
+
125
+ window.switchTab = function(mode) {
126
+ if (mode === currentMode) return;
127
+ currentMode = mode;
128
+ firestoreError = null;
129
+ updateTabs();
130
+ if (isLoaded()) {
131
+ render();
132
+ } else {
133
+ var list = document.getElementById('todo-list');
134
+ if (list) list.innerHTML = '<div class="text-center py-12 text-zinc-500 text-sm">Loading todos...</div>';
135
+ loadTodos();
136
+ }
137
+ };
138
+
65
139
  async function loadTodos() {
66
140
  try {
67
141
  const data = await api('list');
68
- todos = data.todos;
142
+ setTodos(data.todos);
143
+ if (currentMode === 'firestore') firestoreError = null;
69
144
  render();
70
145
  } catch (e) {
71
- var list = document.getElementById('todo-list');
72
- if (list) {
73
- var isApiUnavailable = e.message.includes('not available') || e.message.includes('non-JSON');
74
- if (isApiUnavailable) {
75
- list.innerHTML = '<div class="text-center py-12">'
76
- + '<svg class="mx-auto mb-3 text-zinc-600" width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z"/></svg>'
77
- + '<p class="text-zinc-400 text-sm font-medium mb-1">API Not Available</p>'
78
- + '<p class="text-zinc-600 text-xs">Cloud Functions need to be deployed for the API to work.</p>'
79
- + '<p class="text-zinc-600 text-xs mt-1">Run <code class="text-brand-400 bg-zinc-800 px-1.5 py-0.5 rounded">/clawfire-deploy</code> in Claude Code.</p>'
80
- + '</div>';
81
- } else {
82
- showError('Failed to load todos: ' + e.message);
146
+ if (currentMode === 'firestore' && (e.message.includes('Firestore') || e.message.includes('not configured') || e.message.includes('not available'))) {
147
+ firestoreError = e.message;
148
+ renderFirestoreError();
149
+ } else {
150
+ var list = document.getElementById('todo-list');
151
+ if (list) {
152
+ var isApiUnavailable = e.message.includes('not available') || e.message.includes('non-JSON');
153
+ if (isApiUnavailable) {
154
+ list.innerHTML = '<div class="text-center py-12">'
155
+ + '<svg class="mx-auto mb-3 text-zinc-600" width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z"/></svg>'
156
+ + '<p class="text-zinc-400 text-sm font-medium mb-1">API Not Available</p>'
157
+ + '<p class="text-zinc-600 text-xs">Cloud Functions need to be deployed for the API to work.</p>'
158
+ + '<p class="text-zinc-600 text-xs mt-1">Run <code class="text-brand-400 bg-zinc-800 px-1.5 py-0.5 rounded">/clawfire-deploy</code> in Claude Code.</p>'
159
+ + '</div>';
160
+ } else {
161
+ showError('Failed to load todos: ' + e.message);
162
+ }
83
163
  }
84
164
  }
85
165
  }
86
166
  }
87
167
 
168
+ function renderFirestoreError() {
169
+ var list = document.getElementById('todo-list');
170
+ var stats = document.getElementById('stats');
171
+ if (stats) stats.classList.add('hidden');
172
+ if (!list) return;
173
+ list.innerHTML = '<div class="text-center py-12">'
174
+ + '<div class="mx-auto mb-4 w-12 h-12 rounded-full bg-amber-500/10 flex items-center justify-center">'
175
+ + '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" class="text-amber-400"><path d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/></svg>'
176
+ + '</div>'
177
+ + '<p class="text-amber-400 text-sm font-medium mb-2">Firestore Not Available</p>'
178
+ + '<p class="text-zinc-500 text-xs max-w-xs mx-auto mb-3">Firebase config is not set or Firestore is not enabled for this project.</p>'
179
+ + '<div class="bg-zinc-900 border border-zinc-800 rounded-lg px-4 py-3 text-left max-w-xs mx-auto">'
180
+ + '<p class="text-zinc-400 text-xs font-medium mb-1">To enable Firestore:</p>'
181
+ + '<ol class="text-zinc-500 text-xs space-y-1 list-decimal list-inside">'
182
+ + '<li>Run <code class="text-brand-400 bg-zinc-800 px-1 py-0.5 rounded">/clawfire-init</code></li>'
183
+ + '<li>Select or create a Firebase project</li>'
184
+ + '<li>Deploy with <code class="text-brand-400 bg-zinc-800 px-1 py-0.5 rounded">/clawfire-deploy</code></li>'
185
+ + '</ol>'
186
+ + '</div>'
187
+ + '<p class="text-zinc-600 text-xs mt-3">Meanwhile, use the <button onclick="switchTab(\'memory\')" class="text-brand-400 hover:underline cursor-pointer">In-Memory</button> tab!</p>'
188
+ + '</div>';
189
+ }
190
+
88
191
  window.addTodo = async function() {
89
192
  const input = document.getElementById('todo-input');
90
193
  const title = input.value.trim();
@@ -92,8 +195,10 @@
92
195
  const btn = document.getElementById('add-btn');
93
196
  btn.disabled = true;
94
197
  try {
95
- const data = await api('create', { title });
198
+ const data = await api('create', { title: title });
199
+ var todos = getTodos();
96
200
  todos.unshift(data.todo);
201
+ setTodos(todos);
97
202
  input.value = '';
98
203
  render();
99
204
  } catch (e) {
@@ -105,13 +210,15 @@
105
210
  };
106
211
 
107
212
  window.toggleTodo = async function(id) {
108
- const todo = todos.find(t => t.id === id);
213
+ var todos = getTodos();
214
+ const todo = todos.find(function(t) { return t.id === id; });
109
215
  if (!todo) return;
110
216
  try {
111
- const data = await api('update', { id, completed: !todo.completed });
217
+ const data = await api('update', { id: id, completed: !todo.completed });
112
218
  if (data.todo) {
113
- const idx = todos.findIndex(t => t.id === id);
219
+ const idx = todos.findIndex(function(t) { return t.id === id; });
114
220
  if (idx !== -1) todos[idx] = data.todo;
221
+ setTodos(todos);
115
222
  render();
116
223
  }
117
224
  } catch (e) {
@@ -121,8 +228,9 @@
121
228
 
122
229
  window.deleteTodo = async function(id) {
123
230
  try {
124
- await api('delete', { id });
125
- todos = todos.filter(t => t.id !== id);
231
+ await api('delete', { id: id });
232
+ var todos = getTodos().filter(function(t) { return t.id !== id; });
233
+ setTodos(todos);
126
234
  render();
127
235
  } catch (e) {
128
236
  showError('Failed to delete todo: ' + e.message);
@@ -130,34 +238,44 @@
130
238
  };
131
239
 
132
240
  function render() {
241
+ var todos = getTodos();
133
242
  const list = document.getElementById('todo-list');
134
243
  const stats = document.getElementById('stats');
135
244
  if (!list) return;
136
245
 
246
+ if (currentMode === 'firestore' && firestoreError) {
247
+ renderFirestoreError();
248
+ return;
249
+ }
250
+
137
251
  if (todos.length === 0) {
138
252
  if (stats) stats.classList.add('hidden');
139
- list.innerHTML = '<div class="text-center py-12 text-zinc-600"><svg class="mx-auto mb-3" width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2M9 5h6"/></svg><p class="text-sm">No todos yet. Add one above!</p></div>';
253
+ var modeLabel = currentMode === 'memory' ? 'in-memory' : 'Firestore';
254
+ list.innerHTML = '<div class="text-center py-12 text-zinc-600">'
255
+ + '<svg class="mx-auto mb-3" width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2M9 5h6"/></svg>'
256
+ + '<p class="text-sm">No ' + modeLabel + ' todos yet. Add one above!</p>'
257
+ + '</div>';
140
258
  return;
141
259
  }
142
260
 
143
- const done = todos.filter(t => t.completed).length;
261
+ const done = todos.filter(function(t) { return t.completed; }).length;
144
262
  if (stats) {
145
263
  stats.classList.remove('hidden');
146
264
  document.getElementById('total-count').textContent = todos.length;
147
265
  document.getElementById('done-count').textContent = done;
148
266
  }
149
267
 
150
- list.innerHTML = todos.map(todo => `
151
- <div class="group flex items-center gap-3 px-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl hover:border-zinc-700 transition-colors ${todo.completed ? 'opacity-50' : ''}">
152
- <div onclick="toggleTodo('${todo.id}')" class="w-5.5 h-5.5 border-2 ${todo.completed ? 'bg-brand-500 border-brand-500' : 'border-zinc-700 hover:border-brand-500'} rounded-md cursor-pointer flex items-center justify-center flex-shrink-0 transition-colors">
153
- ${todo.completed ? '<svg width="14" height="14" fill="none" stroke="#fff" stroke-width="2.5" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg>' : ''}
154
- </div>
155
- <span class="flex-1 text-sm ${todo.completed ? 'line-through text-zinc-500' : ''}">${escapeHtml(todo.title)}</span>
156
- <button onclick="deleteTodo('${todo.id}')" class="opacity-0 group-hover:opacity-100 p-1 rounded-md text-zinc-600 hover:text-red-400 hover:bg-red-400/10 transition-all" title="Delete">
157
- <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12"/></svg>
158
- </button>
159
- </div>
160
- `).join('');
268
+ list.innerHTML = todos.map(function(todo) {
269
+ return '<div class="group flex items-center gap-3 px-4 py-3 bg-zinc-900 border border-zinc-800 rounded-xl hover:border-zinc-700 transition-colors ' + (todo.completed ? 'opacity-50' : '') + '">'
270
+ + '<div onclick="toggleTodo(\'' + todo.id + '\')" class="w-5.5 h-5.5 border-2 ' + (todo.completed ? 'bg-brand-500 border-brand-500' : 'border-zinc-700 hover:border-brand-500') + ' rounded-md cursor-pointer flex items-center justify-center flex-shrink-0 transition-colors">'
271
+ + (todo.completed ? '<svg width="14" height="14" fill="none" stroke="#fff" stroke-width="2.5" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg>' : '')
272
+ + '</div>'
273
+ + '<span class="flex-1 text-sm ' + (todo.completed ? 'line-through text-zinc-500' : '') + '">' + escapeHtml(todo.title) + '</span>'
274
+ + '<button onclick="deleteTodo(\'' + todo.id + '\')" class="opacity-0 group-hover:opacity-100 p-1 rounded-md text-zinc-600 hover:text-red-400 hover:bg-red-400/10 transition-all" title="Delete">'
275
+ + '<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12"/></svg>'
276
+ + '</button>'
277
+ + '</div>';
278
+ }).join('');
161
279
  }
162
280
 
163
281
  function escapeHtml(text) {
@@ -171,20 +289,20 @@
171
289
  if (!toast) return;
172
290
  toast.textContent = msg;
173
291
  toast.classList.remove('hidden');
174
- setTimeout(() => toast.classList.add('hidden'), 3000);
292
+ setTimeout(function() { toast.classList.add('hidden'); }, 3000);
175
293
  }
176
294
 
177
295
  function init() {
178
296
  const input = document.getElementById('todo-input');
179
297
  if (input) {
180
- input.addEventListener('keydown', (e) => {
298
+ input.addEventListener('keydown', function(e) {
181
299
  if (e.key === 'Enter') window.addTodo();
182
300
  });
183
301
  }
302
+ updateTabs();
184
303
  loadTodos();
185
304
  }
186
305
 
187
- // Initialize — SPA router re-executes scripts on navigation, so no need for clawfire:navigate listener
188
306
  init();
189
307
  })();
190
308
  </script>
@@ -11,6 +11,7 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "clawfire": "latest",
14
+ "firebase": "^11.0.0",
14
15
  "firebase-admin": "^13.0.0",
15
16
  "firebase-functions": "^6.0.0"
16
17
  },
@@ -1,8 +1,9 @@
1
1
  import { defineAPI, z } from "clawfire";
2
- import { todoStore } from "../../store.js";
2
+ import { getStore } from "../../store.js";
3
3
 
4
4
  export default defineAPI({
5
5
  input: z.object({
6
+ mode: z.enum(["memory", "firestore"]).optional().default("memory"),
6
7
  title: z.string().min(1).max(200),
7
8
  }),
8
9
  output: z.object({
@@ -19,7 +20,8 @@ export default defineAPI({
19
20
  tags: ["todos"],
20
21
  },
21
22
  handler: async (input) => {
22
- const todo = await todoStore.create(input.title);
23
+ const store = getStore(input.mode);
24
+ const todo = await store.create(input.title);
23
25
  return { todo };
24
26
  },
25
27
  });
@@ -1,8 +1,9 @@
1
1
  import { defineAPI, z } from "clawfire";
2
- import { todoStore } from "../../store.js";
2
+ import { getStore } from "../../store.js";
3
3
 
4
4
  export default defineAPI({
5
5
  input: z.object({
6
+ mode: z.enum(["memory", "firestore"]).optional().default("memory"),
6
7
  id: z.string(),
7
8
  }),
8
9
  output: z.object({
@@ -14,7 +15,8 @@ export default defineAPI({
14
15
  tags: ["todos"],
15
16
  },
16
17
  handler: async (input) => {
17
- const success = await todoStore.delete(input.id);
18
+ const store = getStore(input.mode);
19
+ const success = await store.delete(input.id);
18
20
  return { success };
19
21
  },
20
22
  });
@@ -1,8 +1,10 @@
1
1
  import { defineAPI, z } from "clawfire";
2
- import { todoStore } from "../../store.js";
2
+ import { getStore } from "../../store.js";
3
3
 
4
4
  export default defineAPI({
5
- input: z.object({}),
5
+ input: z.object({
6
+ mode: z.enum(["memory", "firestore"]).optional().default("memory"),
7
+ }),
6
8
  output: z.object({
7
9
  todos: z.array(
8
10
  z.object({
@@ -19,8 +21,9 @@ export default defineAPI({
19
21
  auth: "public",
20
22
  tags: ["todos"],
21
23
  },
22
- handler: async () => {
23
- const todos = await todoStore.list();
24
+ handler: async (input) => {
25
+ const store = getStore(input.mode);
26
+ const todos = await store.list();
24
27
  return { todos, count: todos.length };
25
28
  },
26
29
  });
@@ -1,8 +1,9 @@
1
1
  import { defineAPI, z } from "clawfire";
2
- import { todoStore } from "../../store.js";
2
+ import { getStore } from "../../store.js";
3
3
 
4
4
  export default defineAPI({
5
5
  input: z.object({
6
+ mode: z.enum(["memory", "firestore"]).optional().default("memory"),
6
7
  id: z.string(),
7
8
  title: z.string().min(1).max(200).optional(),
8
9
  completed: z.boolean().optional(),
@@ -23,7 +24,8 @@ export default defineAPI({
23
24
  tags: ["todos"],
24
25
  },
25
26
  handler: async (input) => {
26
- const todo = await todoStore.update(input.id, {
27
+ const store = getStore(input.mode);
28
+ const todo = await store.update(input.id, {
27
29
  title: input.title,
28
30
  completed: input.completed,
29
31
  });
@@ -1,9 +1,12 @@
1
1
  /**
2
2
  * Todo Store — Dual Mode (In-Memory + Firestore)
3
3
  *
4
- * Mode is auto-detected from process.env.CLAWFIRE_FIREBASE_CONFIG.
5
- * If the env var contains valid Firebase config, Firestore mode is used.
6
- * Otherwise, falls back to in-memory mode (no setup needed).
4
+ * Two explicit stores are available:
5
+ * - memoryTodoStore: Pure in-memory Map, always works
6
+ * - firestoreTodoStore: Client Firebase SDK via CLAWFIRE_FIREBASE_CONFIG
7
+ *
8
+ * Use getStore("memory" | "firestore") to select at runtime.
9
+ * Legacy todoStore is kept for backward compatibility (auto-detect).
7
10
  */
8
11
 
9
12
  export interface Todo {
@@ -13,26 +16,76 @@ export interface Todo {
13
16
  createdAt: string;
14
17
  }
15
18
 
19
+ export interface TodoStore {
20
+ list(): Promise<Todo[]>;
21
+ create(title: string): Promise<Todo>;
22
+ update(
23
+ id: string,
24
+ data: Partial<Pick<Todo, "title" | "completed">>,
25
+ ): Promise<Todo | null>;
26
+ delete(id: string): Promise<boolean>;
27
+ }
28
+
16
29
  // ─── In-Memory Store ──────────────────────────────────────────
17
- const memoryStore = new Map<string, Todo>();
30
+ const memoryMap = new Map<string, Todo>();
18
31
  let idCounter = 0;
19
32
 
20
- // ─── Firestore State (lazy-initialized) ──────────────────────
21
- let initialized = false;
22
- let useFirestore = false;
23
- let firestoreDb: any = null;
24
- let firestoreMethods: any = null;
33
+ export const memoryTodoStore: TodoStore = {
34
+ async list() {
35
+ return Array.from(memoryMap.values()).sort(
36
+ (a, b) =>
37
+ new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
38
+ );
39
+ },
40
+
41
+ async create(title) {
42
+ const todo: Todo = {
43
+ id: `todo_${++idCounter}_${Date.now()}`,
44
+ title,
45
+ completed: false,
46
+ createdAt: new Date().toISOString(),
47
+ };
48
+ memoryMap.set(todo.id, todo);
49
+ return todo;
50
+ },
51
+
52
+ async update(id, data) {
53
+ const todo = memoryMap.get(id);
54
+ if (!todo) return null;
55
+ if (data.title !== undefined) todo.title = data.title;
56
+ if (data.completed !== undefined) todo.completed = data.completed;
57
+ memoryMap.set(id, todo);
58
+ return todo;
59
+ },
60
+
61
+ async delete(id) {
62
+ return memoryMap.delete(id);
63
+ },
64
+ };
65
+
66
+ // ─── Firestore Store (lazy-initialized) ──────────────────────
67
+ let fsInitialized = false;
68
+ let fsAvailable = false;
69
+ let fsDb: any = null;
70
+ let fsMethods: any = null;
71
+ let fsError: string | null = null;
25
72
 
26
- async function ensureInit(): Promise<void> {
27
- if (initialized) return;
28
- initialized = true;
73
+ async function ensureFirestore(): Promise<void> {
74
+ if (fsInitialized) return;
75
+ fsInitialized = true;
29
76
 
30
77
  const configJson = process.env.CLAWFIRE_FIREBASE_CONFIG;
31
- if (!configJson) return;
78
+ if (!configJson) {
79
+ fsError = "Firestore not configured: CLAWFIRE_FIREBASE_CONFIG env var is not set.";
80
+ return;
81
+ }
32
82
 
33
83
  try {
34
84
  const config = JSON.parse(configJson);
35
- if (!config.apiKey || config.apiKey.startsWith("YOUR_")) return;
85
+ if (!config.apiKey || config.apiKey.startsWith("YOUR_")) {
86
+ fsError = "Firestore not configured: Firebase API key is missing or placeholder.";
87
+ return;
88
+ }
36
89
 
37
90
  const firebaseApp = await import("firebase/app");
38
91
  const firebaseFirestore = await import("firebase/firestore");
@@ -42,89 +95,117 @@ async function ensureInit(): Promise<void> {
42
95
  ? firebaseApp.initializeApp(config)
43
96
  : firebaseApp.getApps()[0];
44
97
 
45
- firestoreDb = firebaseFirestore.getFirestore(app);
46
- firestoreMethods = firebaseFirestore;
47
- useFirestore = true;
48
- console.log(" [store] Firestore mode enabled");
98
+ fsDb = firebaseFirestore.getFirestore(app);
99
+ fsMethods = firebaseFirestore;
100
+ fsAvailable = true;
101
+ console.log(" [store] Firestore mode available");
49
102
  } catch (err) {
50
- console.warn(" [store] Firestore init failed, using in-memory:", err);
103
+ fsError = `Firestore init failed: ${err instanceof Error ? err.message : String(err)}`;
104
+ console.warn(" [store]", fsError);
51
105
  }
52
106
  }
53
107
 
54
- // ─── Exported Store (async methods) ──────────────────────────
55
- export const todoStore = {
56
- async list(): Promise<Todo[]> {
57
- await ensureInit();
58
- if (useFirestore) {
59
- const { collection, getDocs, query, orderBy } = firestoreMethods;
60
- const q = query(
61
- collection(firestoreDb, "todos"),
62
- orderBy("createdAt", "desc"),
63
- );
64
- const snap = await getDocs(q);
65
- return snap.docs.map((d: any) => ({ id: d.id, ...d.data() }) as Todo);
66
- }
67
- return Array.from(memoryStore.values()).sort(
68
- (a, b) =>
69
- new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
108
+ function requireFirestore() {
109
+ if (!fsAvailable) {
110
+ throw new Error(fsError || "Firestore is not available.");
111
+ }
112
+ }
113
+
114
+ export const firestoreTodoStore: TodoStore = {
115
+ async list() {
116
+ await ensureFirestore();
117
+ requireFirestore();
118
+ const { collection, getDocs, query, orderBy } = fsMethods;
119
+ const q = query(
120
+ collection(fsDb, "todos"),
121
+ orderBy("createdAt", "desc"),
70
122
  );
123
+ const snap = await getDocs(q);
124
+ return snap.docs.map((d: any) => ({ id: d.id, ...d.data() }) as Todo);
71
125
  },
72
126
 
73
- async create(title: string): Promise<Todo> {
74
- await ensureInit();
127
+ async create(title) {
128
+ await ensureFirestore();
129
+ requireFirestore();
130
+ const { collection, addDoc } = fsMethods;
75
131
  const todo: Todo = {
76
132
  id: "",
77
133
  title,
78
134
  completed: false,
79
135
  createdAt: new Date().toISOString(),
80
136
  };
81
- if (useFirestore) {
82
- const { collection, addDoc } = firestoreMethods;
83
- const ref = await addDoc(collection(firestoreDb, "todos"), {
84
- title: todo.title,
85
- completed: todo.completed,
86
- createdAt: todo.createdAt,
87
- });
88
- todo.id = ref.id;
89
- } else {
90
- todo.id = `todo_${++idCounter}_${Date.now()}`;
91
- memoryStore.set(todo.id, todo);
92
- }
137
+ const ref = await addDoc(collection(fsDb, "todos"), {
138
+ title: todo.title,
139
+ completed: todo.completed,
140
+ createdAt: todo.createdAt,
141
+ });
142
+ todo.id = ref.id;
93
143
  return todo;
94
144
  },
95
145
 
96
- async update(
97
- id: string,
98
- data: Partial<Pick<Todo, "title" | "completed">>,
99
- ): Promise<Todo | null> {
100
- await ensureInit();
101
- if (useFirestore) {
102
- const { doc, updateDoc, getDoc } = firestoreMethods;
103
- const ref = doc(firestoreDb, "todos", id);
104
- const updateData: Record<string, any> = {};
105
- if (data.title !== undefined) updateData.title = data.title;
106
- if (data.completed !== undefined) updateData.completed = data.completed;
107
- await updateDoc(ref, updateData);
108
- const snap = await getDoc(ref);
109
- return snap.exists()
110
- ? ({ id: snap.id, ...snap.data() } as Todo)
111
- : null;
112
- }
113
- const todo = memoryStore.get(id);
114
- if (!todo) return null;
115
- if (data.title !== undefined) todo.title = data.title;
116
- if (data.completed !== undefined) todo.completed = data.completed;
117
- memoryStore.set(id, todo);
118
- return todo;
146
+ async update(id, data) {
147
+ await ensureFirestore();
148
+ requireFirestore();
149
+ const { doc, updateDoc, getDoc } = fsMethods;
150
+ const ref = doc(fsDb, "todos", id);
151
+ const updateData: Record<string, any> = {};
152
+ if (data.title !== undefined) updateData.title = data.title;
153
+ if (data.completed !== undefined) updateData.completed = data.completed;
154
+ await updateDoc(ref, updateData);
155
+ const snap = await getDoc(ref);
156
+ return snap.exists()
157
+ ? ({ id: snap.id, ...snap.data() } as Todo)
158
+ : null;
119
159
  },
120
160
 
121
- async delete(id: string): Promise<boolean> {
122
- await ensureInit();
123
- if (useFirestore) {
124
- const { doc, deleteDoc } = firestoreMethods;
125
- await deleteDoc(doc(firestoreDb, "todos", id));
126
- return true;
127
- }
128
- return memoryStore.delete(id);
161
+ async delete(id) {
162
+ await ensureFirestore();
163
+ requireFirestore();
164
+ const { doc, deleteDoc } = fsMethods;
165
+ await deleteDoc(doc(fsDb, "todos", id));
166
+ return true;
167
+ },
168
+ };
169
+
170
+ // ─── Store Selector ──────────────────────────────────────────
171
+ export function getStore(mode?: string): TodoStore {
172
+ return mode === "firestore" ? firestoreTodoStore : memoryTodoStore;
173
+ }
174
+
175
+ // ─── Legacy Export (auto-detect, backward compatible) ────────
176
+ let legacyInitialized = false;
177
+ let legacyUseFirestore = false;
178
+
179
+ async function ensureLegacyInit(): Promise<void> {
180
+ if (legacyInitialized) return;
181
+ legacyInitialized = true;
182
+ await ensureFirestore();
183
+ legacyUseFirestore = fsAvailable;
184
+ }
185
+
186
+ export const todoStore: TodoStore = {
187
+ async list() {
188
+ await ensureLegacyInit();
189
+ return legacyUseFirestore
190
+ ? firestoreTodoStore.list()
191
+ : memoryTodoStore.list();
192
+ },
193
+ async create(title) {
194
+ await ensureLegacyInit();
195
+ return legacyUseFirestore
196
+ ? firestoreTodoStore.create(title)
197
+ : memoryTodoStore.create(title);
198
+ },
199
+ async update(id, data) {
200
+ await ensureLegacyInit();
201
+ return legacyUseFirestore
202
+ ? firestoreTodoStore.update(id, data)
203
+ : memoryTodoStore.update(id, data);
204
+ },
205
+ async delete(id) {
206
+ await ensureLegacyInit();
207
+ return legacyUseFirestore
208
+ ? firestoreTodoStore.delete(id)
209
+ : memoryTodoStore.delete(id);
129
210
  },
130
211
  };
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ES2022",
4
- "module": "CommonJS",
5
- "moduleResolution": "node",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
6
  "esModuleInterop": true,
7
7
  "strict": true,
8
8
  "skipLibCheck": true,