create-nextblock 0.10.9 → 0.11.2

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.
Files changed (64) hide show
  1. package/package.json +1 -1
  2. package/templates/nextblock-template/app/actions/interactions.test.ts +301 -0
  3. package/templates/nextblock-template/app/actions/interactions.ts +372 -0
  4. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +4 -4
  5. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +2 -2
  6. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +56 -57
  7. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +1 -1
  8. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +837 -0
  9. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +6 -0
  10. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +4 -0
  11. package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -0
  12. package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -0
  13. package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +18 -13
  14. package/templates/nextblock-template/app/cms/interactions/InteractionsModerationClient.tsx +408 -0
  15. package/templates/nextblock-template/app/cms/interactions/page.tsx +51 -0
  16. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +4 -3
  17. package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +1 -1
  18. package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +3 -5
  19. package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +1 -1
  20. package/templates/nextblock-template/app/page.tsx +2 -2
  21. package/templates/nextblock-template/app/product/[slug]/page.tsx +2 -0
  22. package/templates/nextblock-template/components/AppShell.tsx +1 -1
  23. package/templates/nextblock-template/components/PostCommentsSection.tsx +369 -0
  24. package/templates/nextblock-template/components/ProductReviewsSection.tsx +419 -0
  25. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +2 -0
  26. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +62 -19
  27. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +19 -19
  28. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +4 -4
  29. package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +9 -8
  30. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +38 -9
  31. package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +2 -0
  32. package/templates/nextblock-template/lib/onboarding/status.ts +13 -6
  33. package/templates/nextblock-template/lib/setup/actions.ts +3 -1
  34. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +30 -0
  35. package/templates/nextblock-template/lib/updates/check-upstream.ts +44 -7
  36. package/templates/nextblock-template/lib/updates/github-device.ts +206 -0
  37. package/templates/nextblock-template/lib/updates/repo-identity.ts +11 -1
  38. package/templates/nextblock-template/package.json +2 -1
  39. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +2 -4
  40. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +1 -1
  41. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +1 -1
  42. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +1 -1
  43. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  44. package/templates/nextblock-template/lib/ai-block-generation.ts +0 -339
  45. package/templates/nextblock-template/lib/ai-client.ts +0 -247
  46. package/templates/nextblock-template/lib/ai-config.ts +0 -98
  47. package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +0 -125
  48. package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +0 -363
  49. package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +0 -405
  50. package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +0 -1228
  51. package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +0 -5
  52. package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +0 -223
  53. package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +0 -2183
  54. package/templates/nextblock-template/lib/ai-global-agent-tools.ts +0 -4807
  55. package/templates/nextblock-template/lib/ai-key-crypto.test.ts +0 -70
  56. package/templates/nextblock-template/lib/ai-key-crypto.ts +0 -132
  57. package/templates/nextblock-template/lib/ai-model-catalog.test.ts +0 -49
  58. package/templates/nextblock-template/lib/ai-model-catalog.ts +0 -41
  59. package/templates/nextblock-template/lib/ai-model-registry.test.ts +0 -231
  60. package/templates/nextblock-template/lib/ai-model-registry.ts +0 -522
  61. package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +0 -199
  62. package/templates/nextblock-template/lib/cortex-widget-registry.ts +0 -88
  63. package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +0 -237
  64. package/templates/nextblock-template/lib/cortex-widget-schema.ts +0 -393
@@ -1,405 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import {
3
- executeDatabaseActionPlan,
4
- executeDatabaseMutation,
5
- executeDescribeDatabaseSchema,
6
- executeReadDatabaseRecords,
7
- } from './ai-global-agent-db-tools';
8
-
9
- type MockRow = Record<string, any>;
10
- type MockDatabase = Record<string, MockRow[]>;
11
-
12
- class MockQuery {
13
- private filters: Array<{ column: string; operator: string; value: unknown }> = [];
14
- private limitCount: number | null = null;
15
- private operation: 'delete' | 'insert' | 'select' | 'update' | 'upsert' = 'select';
16
- private orderBy: { ascending: boolean; column: string } | null = null;
17
- private payload: MockRow | MockRow[] | null = null;
18
- private rangeBounds: { from: number; to: number } | null = null;
19
- private selectedColumns: string[] | null = null;
20
-
21
- constructor(
22
- private readonly database: MockDatabase,
23
- private readonly table: string
24
- ) {}
25
-
26
- select(columns?: string) {
27
- if (columns) {
28
- this.selectedColumns = columns.split(',').map((column) => column.trim());
29
- }
30
- return this;
31
- }
32
-
33
- eq(column: string, value: unknown) {
34
- this.filters.push({ column, operator: 'eq', value });
35
- return this;
36
- }
37
-
38
- neq(column: string, value: unknown) {
39
- this.filters.push({ column, operator: 'neq', value });
40
- return this;
41
- }
42
-
43
- gt(column: string, value: unknown) {
44
- this.filters.push({ column, operator: 'gt', value });
45
- return this;
46
- }
47
-
48
- gte(column: string, value: unknown) {
49
- this.filters.push({ column, operator: 'gte', value });
50
- return this;
51
- }
52
-
53
- lt(column: string, value: unknown) {
54
- this.filters.push({ column, operator: 'lt', value });
55
- return this;
56
- }
57
-
58
- lte(column: string, value: unknown) {
59
- this.filters.push({ column, operator: 'lte', value });
60
- return this;
61
- }
62
-
63
- ilike(column: string, value: unknown) {
64
- this.filters.push({ column, operator: 'ilike', value });
65
- return this;
66
- }
67
-
68
- in(column: string, value: unknown) {
69
- this.filters.push({ column, operator: 'in', value });
70
- return this;
71
- }
72
-
73
- is(column: string, value: unknown) {
74
- this.filters.push({ column, operator: 'is', value });
75
- return this;
76
- }
77
-
78
- limit(count: number) {
79
- this.limitCount = count;
80
- return this;
81
- }
82
-
83
- range(from: number, to: number) {
84
- this.rangeBounds = { from, to };
85
- return this;
86
- }
87
-
88
- order(column: string, options?: { ascending?: boolean }) {
89
- this.orderBy = { ascending: options?.ascending ?? true, column };
90
- return this;
91
- }
92
-
93
- delete() {
94
- this.operation = 'delete';
95
- return this;
96
- }
97
-
98
- insert(payload: MockRow | MockRow[]) {
99
- this.operation = 'insert';
100
- this.payload = payload;
101
- return this;
102
- }
103
-
104
- update(payload: MockRow) {
105
- this.operation = 'update';
106
- this.payload = payload;
107
- return this;
108
- }
109
-
110
- upsert(payload: MockRow | MockRow[]) {
111
- this.operation = 'upsert';
112
- this.payload = payload;
113
- return this;
114
- }
115
-
116
- then<TResult1 = any, TResult2 = never>(
117
- onfulfilled?: ((value: any) => TResult1 | PromiseLike<TResult1>) | null,
118
- onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
119
- ) {
120
- return this.execute().then(onfulfilled, onrejected);
121
- }
122
-
123
- private matchesFilters(row: MockRow) {
124
- return this.filters.every((filter) => {
125
- const value = row[filter.column];
126
-
127
- switch (filter.operator) {
128
- case 'eq':
129
- return value === filter.value;
130
- case 'neq':
131
- return value !== filter.value;
132
- case 'gt':
133
- return value > filter.value;
134
- case 'gte':
135
- return value >= filter.value;
136
- case 'lt':
137
- return value < filter.value;
138
- case 'lte':
139
- return value <= filter.value;
140
- case 'ilike': {
141
- const pattern = String(filter.value).replace(/%/g, '').toLowerCase();
142
- return String(value).toLowerCase().includes(pattern);
143
- }
144
- case 'in':
145
- return Array.isArray(filter.value) && filter.value.includes(value);
146
- case 'is':
147
- return value === filter.value;
148
- default:
149
- return true;
150
- }
151
- });
152
- }
153
-
154
- private project(row: MockRow) {
155
- if (!this.selectedColumns) {
156
- return row;
157
- }
158
-
159
- return Object.fromEntries(this.selectedColumns.map((column) => [column, row[column]]));
160
- }
161
-
162
- private async execute() {
163
- if (!(this.table in this.database)) {
164
- throw new Error(`Unexpected mock table: ${this.table}`);
165
- }
166
-
167
- if (this.operation === 'insert') {
168
- const rows = (Array.isArray(this.payload) ? this.payload : [this.payload]).filter(Boolean) as MockRow[];
169
- const inserted = rows.map((row) => ({ ...row }));
170
- this.database[this.table].push(...inserted);
171
- return { data: inserted.map((row) => this.project(row)), error: null };
172
- }
173
-
174
- if (this.operation === 'upsert') {
175
- const rows = (Array.isArray(this.payload) ? this.payload : [this.payload]).filter(Boolean) as MockRow[];
176
-
177
- for (const row of rows) {
178
- const keyColumn = 'id' in row ? 'id' : 'key' in row ? 'key' : Object.keys(row)[0];
179
- const existingIndex = this.database[this.table].findIndex((current) => current[keyColumn] === row[keyColumn]);
180
-
181
- if (existingIndex >= 0) {
182
- this.database[this.table][existingIndex] = { ...this.database[this.table][existingIndex], ...row };
183
- } else {
184
- this.database[this.table].push({ ...row });
185
- }
186
- }
187
-
188
- return { data: rows.map((row) => this.project(row)), error: null };
189
- }
190
-
191
- if (this.operation === 'update') {
192
- const payload = (Array.isArray(this.payload) ? this.payload[0] : this.payload) || {};
193
- const updated: MockRow[] = [];
194
-
195
- this.database[this.table] = this.database[this.table].map((row) => {
196
- if (!this.matchesFilters(row)) {
197
- return row;
198
- }
199
-
200
- const nextRow = { ...row, ...payload };
201
- updated.push(nextRow);
202
- return nextRow;
203
- });
204
-
205
- return { data: updated.map((row) => this.project(row)), error: null };
206
- }
207
-
208
- if (this.operation === 'delete') {
209
- const removed = this.database[this.table].filter((row) => this.matchesFilters(row));
210
- this.database[this.table] = this.database[this.table].filter((row) => !this.matchesFilters(row));
211
- return { data: removed.map((row) => this.project(row)), error: null };
212
- }
213
-
214
- let rows = this.database[this.table].filter((row) => this.matchesFilters(row));
215
-
216
- if (this.orderBy) {
217
- const { ascending, column } = this.orderBy;
218
- rows = [...rows].sort((a, b) => {
219
- if (a[column] === b[column]) return 0;
220
- return (a[column] > b[column] ? 1 : -1) * (ascending ? 1 : -1);
221
- });
222
- }
223
-
224
- if (this.rangeBounds) {
225
- rows = rows.slice(this.rangeBounds.from, this.rangeBounds.to + 1);
226
- }
227
-
228
- if (this.limitCount !== null) {
229
- rows = rows.slice(0, this.limitCount);
230
- }
231
-
232
- return { data: rows.map((row) => this.project(row)), error: null };
233
- }
234
- }
235
-
236
- function createMockSupabase(overrides?: Partial<MockDatabase>) {
237
- const database: MockDatabase = {
238
- cortex_ai_db_mutation_audit: [],
239
- pages: [
240
- { id: 1, language_id: 1, slug: 'home', status: 'draft', title: 'Home' },
241
- { id: 2, language_id: 1, slug: 'about', status: 'draft', title: 'About' },
242
- ],
243
- profiles: [{ id: 'user_1', full_name: 'Admin', role: 'ADMIN' }],
244
- site_settings: [
245
- { key: 'site_name', value: { en: 'NextBlock' } },
246
- { key: 'cortex_ai_openrouter_api_key', value: 'sk-secret' },
247
- ],
248
- translations: [{ key: 'hello', translations: { en: 'Hello' } }],
249
- user_addresses: [{ id: 'addr_1', user_id: 'user_1', city: 'Montreal' }],
250
- ...overrides,
251
- };
252
-
253
- return {
254
- database,
255
- supabase: {
256
- from: (table: string) => new MockQuery(database, table),
257
- },
258
- };
259
- }
260
-
261
- describe('Cortex AI generic database tools', () => {
262
- it('describes allowed schema and marks PII tables read-only', async () => {
263
- const result = await executeDescribeDatabaseSchema({});
264
-
265
- expect(result.success).toBe(true);
266
- expect(result.tables.some((table) => table.table === 'profiles' && table.readOnly)).toBe(true);
267
- expect(result.tables.some((table) => table.table === 'user_addresses' && table.readOnly)).toBe(true);
268
- expect(result.tables.some((table) => table.table === 'auth.users')).toBe(false);
269
- });
270
-
271
- it('reads profiles and user addresses but refuses to mutate them', async () => {
272
- const { supabase } = createMockSupabase();
273
-
274
- const profiles = await executeReadDatabaseRecords(
275
- { table: 'profiles', columns: ['id', 'full_name', 'role'] },
276
- { supabase }
277
- );
278
- const addresses = await executeReadDatabaseRecords(
279
- { table: 'user_addresses', columns: ['id', 'city'] },
280
- { supabase }
281
- );
282
-
283
- expect(profiles.rows).toEqual([{ id: 'user_1', full_name: 'Admin', role: 'ADMIN' }]);
284
- expect(addresses.rows).toEqual([{ id: 'addr_1', city: 'Montreal' }]);
285
- await expect(
286
- executeDatabaseMutation(
287
- {
288
- filters: [{ column: 'id', operator: 'eq', value: 'user_1' }],
289
- operation: 'update',
290
- table: 'profiles',
291
- values: { full_name: 'Changed' },
292
- },
293
- { supabase }
294
- )
295
- ).rejects.toThrow('read-only');
296
- });
297
-
298
- it('blocks auth tables, secret-like fields, and protected Cortex site setting reads', async () => {
299
- const { supabase } = createMockSupabase();
300
-
301
- await expect(executeReadDatabaseRecords({ table: 'auth.users' }, { supabase })).rejects.toThrow(
302
- 'auth schema'
303
- );
304
- await expect(
305
- executeDatabaseMutation(
306
- {
307
- operation: 'insert',
308
- rows: [{ key: 'demo', api_key: 'secret' }],
309
- table: 'site_settings',
310
- },
311
- { supabase }
312
- )
313
- ).rejects.toThrow('Protected field');
314
-
315
- const settings = await executeReadDatabaseRecords({ table: 'site_settings' }, { supabase });
316
-
317
- expect(settings.rows).toEqual([{ key: 'site_name', value: { en: 'NextBlock' } }]);
318
- });
319
-
320
- it('previews and confirms a mutation before changing rows and writing an audit record', async () => {
321
- const { database, supabase } = createMockSupabase();
322
- const input = {
323
- filters: [{ column: 'id', operator: 'eq' as const, value: 1 }],
324
- operation: 'update' as const,
325
- summary: 'Publish the home page.',
326
- table: 'pages',
327
- values: { status: 'published' },
328
- };
329
-
330
- const preview = await executeDatabaseMutation(input, { actorUserId: 'user_1', supabase });
331
-
332
- expect(preview.requiresConfirmation).toBe(true);
333
- expect(database.pages[0].status).toBe('draft');
334
-
335
- const result = await executeDatabaseMutation(input, {
336
- actorUserId: 'user_1',
337
- latestUserMessage: preview.confirmationPhrase,
338
- supabase,
339
- });
340
-
341
- expect(result.success).toBe(true);
342
- expect(result.mutationExecuted).toBe(true);
343
- expect(database.pages[0].status).toBe('published');
344
- expect(database.cortex_ai_db_mutation_audit).toHaveLength(1);
345
- expect(database.cortex_ai_db_mutation_audit[0].status).toBe('success');
346
- });
347
-
348
- it('requires a fresh confirmation when update targets change', async () => {
349
- const { database, supabase } = createMockSupabase();
350
- const input = {
351
- filters: [{ column: 'status', operator: 'eq' as const, value: 'draft' }],
352
- operation: 'update' as const,
353
- table: 'pages',
354
- values: { status: 'published' },
355
- };
356
- const preview = await executeDatabaseMutation(input, { actorUserId: 'user_1', supabase });
357
-
358
- database.pages.pop();
359
- const staleResult = await executeDatabaseMutation(input, {
360
- actorUserId: 'user_1',
361
- latestUserMessage: preview.confirmationPhrase,
362
- supabase,
363
- });
364
-
365
- expect(staleResult.requiresConfirmation).toBe(true);
366
- expect(staleResult.message).toMatch(/target changed/i);
367
- expect(database.pages[0].status).toBe('draft');
368
- });
369
-
370
- it('confirms a multi-action database plan and writes one combined audit row', async () => {
371
- const { database, supabase } = createMockSupabase();
372
- const input = {
373
- actions: [
374
- {
375
- filters: [{ column: 'id', operator: 'eq' as const, value: 1 }],
376
- operation: 'update' as const,
377
- table: 'pages',
378
- values: { status: 'published' },
379
- },
380
- {
381
- operation: 'insert' as const,
382
- rows: [{ key: 'goodbye', translations: { en: 'Goodbye' } }],
383
- table: 'translations',
384
- },
385
- ],
386
- summary: 'Publish home and add a translation.',
387
- };
388
-
389
- const preview = await executeDatabaseActionPlan(input, { actorUserId: 'user_1', supabase });
390
- expect(preview.requiresConfirmation).toBe(true);
391
-
392
- const result = await executeDatabaseActionPlan(input, {
393
- actorUserId: 'user_1',
394
- latestUserMessage: preview.confirmationPhrase,
395
- supabase,
396
- });
397
-
398
- expect(result.success).toBe(true);
399
- expect(result.mutationExecuted).toBe(true);
400
- expect(database.pages[0].status).toBe('published');
401
- expect(database.translations.some((row) => row.key === 'goodbye')).toBe(true);
402
- expect(database.cortex_ai_db_mutation_audit).toHaveLength(1);
403
- expect(database.cortex_ai_db_mutation_audit[0].tool_name).toBe('execute_database_action_plan');
404
- });
405
- });