@mevdragon/vidfarm-devcli 0.1.0 → 0.2.0

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 (63) hide show
  1. package/.env.example +11 -4
  2. package/PLATFORM_SPEC.md +142 -2
  3. package/README.md +165 -16
  4. package/SKILL.developer.md +577 -0
  5. package/dist/infra/cdk/bin/vidfarm-prod.js +59 -0
  6. package/dist/infra/cdk/lib/vidfarm-prod-stack.js +212 -0
  7. package/dist/src/account-pages.js +578 -0
  8. package/dist/src/app.js +887 -66
  9. package/dist/src/cli.js +284 -5
  10. package/dist/src/config.js +24 -4
  11. package/dist/src/db.js +427 -18
  12. package/dist/src/dev-app.js +59 -12
  13. package/dist/src/homepage.js +441 -0
  14. package/dist/src/index.js +12 -7
  15. package/dist/src/lib/crypto.js +14 -0
  16. package/dist/src/lib/template-dna.js +542 -0
  17. package/dist/src/lib/template-style-options.js +49 -0
  18. package/dist/src/registry.js +54 -7
  19. package/dist/src/runtime.js +3 -1
  20. package/dist/src/services/auth.js +69 -5
  21. package/dist/src/services/jobs.js +23 -4
  22. package/dist/src/services/providers.js +74 -12
  23. package/dist/src/services/storage.js +52 -18
  24. package/dist/src/services/template-certification.js +160 -0
  25. package/dist/src/services/template-loader.js +37 -0
  26. package/dist/src/services/template-sources.js +135 -0
  27. package/dist/src/worker.js +19 -7
  28. package/dist/templates/template_0000/src/lib/images.js +242 -0
  29. package/dist/templates/template_0000/src/remotion/Root.js +33 -0
  30. package/dist/templates/template_0000/src/sdk.js +3 -0
  31. package/dist/templates/template_0000/src/style-options.js +51 -0
  32. package/dist/templates/template_0000/src/template-dna.js +9 -0
  33. package/dist/templates/template_0000/src/template.js +1217 -0
  34. package/package.json +9 -1
  35. package/templates/template_0000/README.md +121 -0
  36. package/templates/template_0000/SKILL.md +193 -0
  37. package/templates/template_0000/assets/Abel-Regular.ttf +0 -0
  38. package/templates/template_0000/assets/DMSerifDisplay-Regular.ttf +0 -0
  39. package/templates/template_0000/assets/Montserrat[wght].ttf +0 -0
  40. package/templates/template_0000/assets/SourceCodePro[wght].ttf +0 -0
  41. package/templates/template_0000/assets/TikTokSans-SemiBold.ttf +0 -0
  42. package/templates/template_0000/assets/Yesteryear-Regular.ttf +0 -0
  43. package/templates/template_0000/composition.json +11 -0
  44. package/templates/template_0000/package-lock.json +5137 -0
  45. package/templates/template_0000/package.json +30 -0
  46. package/templates/template_0000/research/preview/.gitkeep +1 -0
  47. package/templates/template_0000/research/source_notes.md +7 -0
  48. package/templates/template_0000/scripts/create-site.mjs +27 -0
  49. package/templates/template_0000/scripts/render-cloud.mjs +72 -0
  50. package/templates/template_0000/src/lib/images.ts +284 -0
  51. package/templates/template_0000/src/remotion/Root.js +33 -0
  52. package/templates/template_0000/src/remotion/Root.tsx +75 -0
  53. package/templates/template_0000/src/remotion/index.tsx +4 -0
  54. package/templates/template_0000/src/sdk.ts +122 -0
  55. package/templates/template_0000/src/style-options.js +51 -0
  56. package/templates/template_0000/src/style-options.ts +60 -0
  57. package/templates/template_0000/src/template-dna.ts +15 -0
  58. package/templates/template_0000/src/template.ts +1747 -0
  59. package/templates/template_0000/template.config.json +26 -0
  60. package/templates/template_0000/tsconfig.json +19 -0
  61. package/dist/templates/template_0000/demo-template.js +0 -196
  62. package/dist/templates/template_0000/remotion/Root.js +0 -66
  63. /package/dist/templates/template_0000/{remotion → src/remotion}/index.js +0 -0
package/dist/src/db.js CHANGED
@@ -11,6 +11,12 @@ create table if not exists customers (
11
11
  email text not null unique,
12
12
  name text,
13
13
  default_webhook_url text,
14
+ is_developer integer not null default 0,
15
+ is_paid_plan integer not null default 0,
16
+ about text,
17
+ groupchat_url text,
18
+ flockposter_api_key text,
19
+ password_hash text,
14
20
  created_at text not null,
15
21
  updated_at text not null
16
22
  );
@@ -19,6 +25,7 @@ create table if not exists api_keys (
19
25
  id text primary key,
20
26
  customer_id text not null references customers(id) on delete cascade,
21
27
  key_hash text not null unique,
28
+ raw_value text,
22
29
  label text,
23
30
  status text not null,
24
31
  last_used_at text,
@@ -160,7 +167,87 @@ create table if not exists webhook_deliveries (
160
167
  created_at text not null,
161
168
  updated_at text not null
162
169
  );
170
+
171
+ create table if not exists user_attachments (
172
+ id text primary key,
173
+ customer_id text not null references customers(id) on delete cascade,
174
+ file_name text not null,
175
+ content_type text not null,
176
+ size_bytes integer not null,
177
+ storage_key text not null,
178
+ public_url text,
179
+ created_at text not null
180
+ );
181
+
182
+ create index if not exists idx_user_attachments_customer_created on user_attachments(customer_id, created_at desc);
183
+
184
+ create table if not exists template_sources (
185
+ id text primary key,
186
+ template_id text not null unique,
187
+ slug_id text,
188
+ repo_url text not null,
189
+ branch text not null,
190
+ template_module_path text not null,
191
+ skill_path text not null,
192
+ install_command text not null,
193
+ build_command text not null,
194
+ status text not null,
195
+ created_at text not null,
196
+ updated_at text not null
197
+ );
198
+
199
+ create table if not exists template_releases (
200
+ id text primary key,
201
+ source_id text not null references template_sources(id) on delete cascade,
202
+ template_id text not null,
203
+ branch text not null,
204
+ commit_sha text not null,
205
+ checkout_path text not null,
206
+ skill_path text not null,
207
+ module_path text not null,
208
+ status text not null,
209
+ certification_report_json text,
210
+ activated_at text,
211
+ created_at text not null,
212
+ updated_at text not null,
213
+ unique(source_id, commit_sha)
214
+ );
215
+
216
+ create index if not exists idx_template_releases_template_status on template_releases(template_id, status, created_at);
217
+ `);
218
+ const customerColumns = db.prepare(`pragma table_info(customers)`).all();
219
+ if (!customerColumns.some((column) => column.name === "is_developer")) {
220
+ db.exec(`alter table customers add column is_developer integer not null default 0;`);
221
+ }
222
+ if (!customerColumns.some((column) => column.name === "is_paid_plan")) {
223
+ db.exec(`alter table customers add column is_paid_plan integer not null default 0;`);
224
+ }
225
+ if (!customerColumns.some((column) => column.name === "password_hash")) {
226
+ db.exec(`alter table customers add column password_hash text;`);
227
+ }
228
+ if (!customerColumns.some((column) => column.name === "about")) {
229
+ db.exec(`alter table customers add column about text;`);
230
+ }
231
+ if (!customerColumns.some((column) => column.name === "groupchat_url")) {
232
+ db.exec(`alter table customers add column groupchat_url text;`);
233
+ }
234
+ if (!customerColumns.some((column) => column.name === "flockposter_api_key")) {
235
+ db.exec(`alter table customers add column flockposter_api_key text;`);
236
+ }
237
+ const apiKeyColumns = db.prepare(`pragma table_info(api_keys)`).all();
238
+ if (!apiKeyColumns.some((column) => column.name === "raw_value")) {
239
+ db.exec(`alter table api_keys add column raw_value text;`);
240
+ }
241
+ const templateSourceColumns = db.prepare(`pragma table_info(template_sources)`).all();
242
+ if (!templateSourceColumns.some((column) => column.name === "slug_id")) {
243
+ db.exec(`alter table template_sources add column slug_id text;`);
244
+ }
245
+ db.exec(`
246
+ update template_sources
247
+ set slug_id = replace(template_id, '-', '_')
248
+ where slug_id is null or trim(slug_id) = '';
163
249
  `);
250
+ db.exec(`create unique index if not exists idx_template_sources_slug_id on template_sources(slug_id);`);
164
251
  function mapJob(row) {
165
252
  return {
166
253
  id: String(row.id),
@@ -186,23 +273,85 @@ function mapJob(row) {
186
273
  completedAt: row.completed_at ? String(row.completed_at) : null
187
274
  };
188
275
  }
276
+ function mapTemplateSource(row) {
277
+ return {
278
+ id: String(row.id),
279
+ templateId: String(row.template_id),
280
+ slugId: String(row.slug_id),
281
+ repoUrl: String(row.repo_url),
282
+ branch: String(row.branch),
283
+ templateModulePath: String(row.template_module_path),
284
+ skillPath: String(row.skill_path),
285
+ installCommand: String(row.install_command),
286
+ buildCommand: String(row.build_command),
287
+ status: row.status,
288
+ createdAt: String(row.created_at),
289
+ updatedAt: String(row.updated_at)
290
+ };
291
+ }
292
+ function mapTemplateRelease(row) {
293
+ return {
294
+ id: String(row.id),
295
+ sourceId: String(row.source_id),
296
+ templateId: String(row.template_id),
297
+ branch: String(row.branch),
298
+ commitSha: String(row.commit_sha),
299
+ checkoutPath: String(row.checkout_path),
300
+ skillPath: String(row.skill_path),
301
+ modulePath: String(row.module_path),
302
+ status: row.status,
303
+ certificationReport: parseJson(row.certification_report_json, null),
304
+ activatedAt: row.activated_at ? String(row.activated_at) : null,
305
+ createdAt: String(row.created_at),
306
+ updatedAt: String(row.updated_at)
307
+ };
308
+ }
309
+ function mapUserAttachment(row) {
310
+ return {
311
+ id: String(row.id),
312
+ customerId: String(row.customer_id),
313
+ fileName: String(row.file_name),
314
+ contentType: String(row.content_type),
315
+ sizeBytes: Number(row.size_bytes),
316
+ storageKey: String(row.storage_key),
317
+ publicUrl: row.public_url ? String(row.public_url) : null,
318
+ createdAt: String(row.created_at)
319
+ };
320
+ }
189
321
  export const database = {
190
322
  raw: db,
191
323
  upsertCustomer(input) {
192
324
  const timestamp = nowIso();
325
+ const current = db.prepare(`select * from customers where id = ?`).get(input.id);
193
326
  db.prepare(`
194
- insert into customers (id, email, name, default_webhook_url, created_at, updated_at)
195
- values (@id, @email, @name, @default_webhook_url, @created_at, @updated_at)
327
+ insert into customers (
328
+ id, email, name, default_webhook_url, is_developer, is_paid_plan, about, groupchat_url, flockposter_api_key, password_hash, created_at, updated_at
329
+ )
330
+ values (
331
+ @id, @email, @name, @default_webhook_url, @is_developer, @is_paid_plan, @about, @groupchat_url, @flockposter_api_key, @password_hash, @created_at, @updated_at
332
+ )
196
333
  on conflict(id) do update set
197
334
  email = excluded.email,
198
335
  name = excluded.name,
199
336
  default_webhook_url = excluded.default_webhook_url,
337
+ is_developer = excluded.is_developer,
338
+ is_paid_plan = excluded.is_paid_plan,
339
+ about = excluded.about,
340
+ groupchat_url = excluded.groupchat_url,
341
+ flockposter_api_key = excluded.flockposter_api_key,
342
+ password_hash = excluded.password_hash,
200
343
  updated_at = excluded.updated_at
201
344
  `).run({
202
345
  id: input.id,
203
346
  email: input.email,
204
347
  name: input.name ?? null,
205
348
  default_webhook_url: input.defaultWebhookUrl ?? null,
349
+ is_developer: input.isDeveloper === undefined ? Number(current?.is_developer ?? 0) : (input.isDeveloper ? 1 : 0),
350
+ is_paid_plan: input.isPaidPlan === undefined ? Number(current?.is_paid_plan ?? 0) : (input.isPaidPlan ? 1 : 0),
351
+ about: input.about === undefined ? (current?.about ?? null) : input.about,
352
+ groupchat_url: input.groupchatUrl === undefined ? (current?.groupchat_url ?? null) : input.groupchatUrl,
353
+ flockposter_api_key: input.flockposterApiKey === undefined ? (current?.flockposter_api_key ?? null) : input.flockposterApiKey,
354
+ password_hash: input.passwordHash === undefined ? (current?.password_hash ?? null) : input.passwordHash,
206
355
  created_at: timestamp,
207
356
  updated_at: timestamp
208
357
  });
@@ -217,13 +366,48 @@ export const database = {
217
366
  id: String(row.id),
218
367
  email: String(row.email),
219
368
  name: row.name ? String(row.name) : null,
220
- defaultWebhookUrl: row.default_webhook_url ? String(row.default_webhook_url) : null
369
+ defaultWebhookUrl: row.default_webhook_url ? String(row.default_webhook_url) : null,
370
+ isDeveloper: Boolean(row.is_developer),
371
+ isPaidPlan: Boolean(row.is_paid_plan),
372
+ about: row.about ? String(row.about) : null,
373
+ groupchatUrl: row.groupchat_url ? String(row.groupchat_url) : null,
374
+ flockposterApiKey: row.flockposter_api_key ? String(row.flockposter_api_key) : null
221
375
  };
222
376
  },
223
377
  getCustomerByEmail(email) {
224
378
  const row = db.prepare(`select * from customers where lower(email) = lower(?)`).get(email);
225
379
  return row ? this.getCustomerById(String(row.id)) : null;
226
380
  },
381
+ getCustomerAuthByEmail(email) {
382
+ return db.prepare(`
383
+ select id, email, name, default_webhook_url, is_developer, is_paid_plan, password_hash
384
+ , about, groupchat_url, flockposter_api_key
385
+ from customers
386
+ where lower(email) = lower(?)
387
+ limit 1
388
+ `).get(email);
389
+ },
390
+ updateCustomerPassword(customerId, passwordHash) {
391
+ db.prepare(`
392
+ update customers
393
+ set password_hash = ?, updated_at = ?
394
+ where id = ?
395
+ `).run(passwordHash, nowIso(), customerId);
396
+ },
397
+ updateCustomerPaidPlan(customerId, isPaidPlan) {
398
+ db.prepare(`
399
+ update customers
400
+ set is_paid_plan = ?, updated_at = ?
401
+ where id = ?
402
+ `).run(isPaidPlan ? 1 : 0, nowIso(), customerId);
403
+ },
404
+ updateCustomerProfile(input) {
405
+ db.prepare(`
406
+ update customers
407
+ set about = ?, groupchat_url = ?, flockposter_api_key = ?, updated_at = ?
408
+ where id = ?
409
+ `).run(input.about, input.groupchatUrl, input.flockposterApiKey, nowIso(), input.customerId);
410
+ },
227
411
  listProviderKeys(customerId) {
228
412
  return db.prepare(`
229
413
  select id, provider, label, status, weight, last_used_at, cooldown_until, disabled_reason, created_at, updated_at
@@ -232,6 +416,17 @@ export const database = {
232
416
  order by provider asc, created_at desc
233
417
  `).all(customerId);
234
418
  },
419
+ listProviderKeysWithSecrets(customerId) {
420
+ return db.prepare(`
421
+ select id, provider, label, encrypted_secret as secret, status, weight, last_used_at, cooldown_until, disabled_reason, created_at, updated_at
422
+ from customer_provider_keys
423
+ where customer_id = ?
424
+ order by provider asc, created_at desc
425
+ `).all(customerId);
426
+ },
427
+ deleteProviderKey(customerId, keyId) {
428
+ db.prepare(`delete from customer_provider_keys where id = ? and customer_id = ?`).run(keyId, customerId);
429
+ },
235
430
  insertOtpChallenge(record) {
236
431
  db.prepare(`
237
432
  insert into otp_challenges (id, email, code_hash, expires_at, created_at)
@@ -258,20 +453,30 @@ export const database = {
258
453
  insertApiKey(record) {
259
454
  const timestamp = nowIso();
260
455
  db.prepare(`
261
- insert into api_keys (id, customer_id, key_hash, label, status, created_at, updated_at)
262
- values (@id, @customer_id, @key_hash, @label, 'active', @created_at, @updated_at)
456
+ insert into api_keys (id, customer_id, key_hash, raw_value, label, status, created_at, updated_at)
457
+ values (@id, @customer_id, @key_hash, @raw_value, @label, 'active', @created_at, @updated_at)
263
458
  `).run({
264
459
  id: record.id,
265
460
  customer_id: record.customerId,
266
461
  key_hash: record.keyHash,
462
+ raw_value: record.rawValue ?? null,
267
463
  label: record.label,
268
464
  created_at: timestamp,
269
465
  updated_at: timestamp
270
466
  });
271
467
  },
468
+ getLatestApiKeyForCustomer(customerId) {
469
+ return db.prepare(`
470
+ select id, raw_value, label, last_used_at, created_at, updated_at
471
+ from api_keys
472
+ where customer_id = ? and status = 'active'
473
+ order by case when raw_value is null or trim(raw_value) = '' then 1 else 0 end asc, created_at desc
474
+ limit 1
475
+ `).get(customerId);
476
+ },
272
477
  findApiKeyByHash(keyHash) {
273
478
  return db.prepare(`
274
- select a.*, c.email, c.name, c.default_webhook_url
479
+ select a.*, c.email, c.name, c.default_webhook_url, c.is_developer, c.is_paid_plan, c.about, c.groupchat_url, c.flockposter_api_key
275
480
  from api_keys a
276
481
  join customers c on c.id = a.customer_id
277
482
  where a.key_hash = ? and a.status = 'active'
@@ -346,6 +551,21 @@ export const database = {
346
551
  const row = db.prepare(`select * from jobs where id = ?`).get(id);
347
552
  return row ? mapJob(row) : null;
348
553
  },
554
+ countJobsByStatuses(input) {
555
+ const statuses = input.statuses.length ? input.statuses : ["queued"];
556
+ const filters = [`status in (${statuses.map(() => "?").join(", ")})`];
557
+ const values = [...statuses];
558
+ if (input.customerId) {
559
+ filters.push("customer_id = ?");
560
+ values.push(input.customerId);
561
+ }
562
+ const row = db.prepare(`
563
+ select count(*) as count
564
+ from jobs
565
+ where ${filters.join(" and ")}
566
+ `).get(...values);
567
+ return Number(row.count);
568
+ },
349
569
  listRunnableJobs(limit) {
350
570
  const rows = db.prepare(`
351
571
  select * from jobs
@@ -386,10 +606,32 @@ export const database = {
386
606
  updated_at: nowIso()
387
607
  });
388
608
  },
389
- listJobsForCustomer(customerId, templateId) {
390
- const rows = templateId
391
- ? db.prepare(`select * from jobs where customer_id = ? and template_id = ? order by created_at desc limit 100`).all(customerId, templateId)
392
- : db.prepare(`select * from jobs where customer_id = ? order by created_at desc limit 100`).all(customerId);
609
+ listJobsForCustomer(input) {
610
+ const filters = ["customer_id = ?"];
611
+ const values = [input.customerId];
612
+ if (input.templateId) {
613
+ filters.push("template_id = ?");
614
+ values.push(input.templateId);
615
+ }
616
+ if (input.tracer) {
617
+ filters.push("tracer = ?");
618
+ values.push(input.tracer);
619
+ }
620
+ if (input.startTime) {
621
+ filters.push("created_at >= ?");
622
+ values.push(input.startTime);
623
+ }
624
+ if (input.endTime) {
625
+ filters.push("created_at <= ?");
626
+ values.push(input.endTime);
627
+ }
628
+ values.push(Math.max(1, Math.min(input.limit ?? 100, 500)));
629
+ const rows = db.prepare(`
630
+ select * from jobs
631
+ where ${filters.join(" and ")}
632
+ order by created_at desc
633
+ limit ?
634
+ `).all(...values);
393
635
  return rows.map(mapJob);
394
636
  },
395
637
  addJobEvent(event) {
@@ -407,14 +649,24 @@ export const database = {
407
649
  created_at: nowIso()
408
650
  });
409
651
  },
410
- getJobEvents(jobId, since, limit = 100) {
411
- const rows = since
412
- ? db.prepare(`
413
- select * from job_events where job_id = ? and created_at > ? order by created_at asc limit ?
414
- `).all(jobId, since, limit)
415
- : db.prepare(`
416
- select * from job_events where job_id = ? order by created_at asc limit ?
417
- `).all(jobId, limit);
652
+ getJobEvents(input) {
653
+ const filters = ["job_id = ?"];
654
+ const values = [input.jobId];
655
+ if (input.startTime) {
656
+ filters.push("created_at >= ?");
657
+ values.push(input.startTime);
658
+ }
659
+ if (input.endTime) {
660
+ filters.push("created_at <= ?");
661
+ values.push(input.endTime);
662
+ }
663
+ values.push(Math.max(1, Math.min(input.limit ?? 100, 500)));
664
+ const rows = db.prepare(`
665
+ select * from job_events
666
+ where ${filters.join(" and ")}
667
+ order by created_at asc
668
+ limit ?
669
+ `).all(...values);
418
670
  return rows.map((row) => ({
419
671
  id: String(row.id),
420
672
  jobId: String(row.job_id),
@@ -584,5 +836,162 @@ export const database = {
584
836
  created_at: timestamp,
585
837
  updated_at: timestamp
586
838
  });
839
+ },
840
+ createUserAttachment(record) {
841
+ db.prepare(`
842
+ insert into user_attachments (
843
+ id, customer_id, file_name, content_type, size_bytes, storage_key, public_url, created_at
844
+ ) values (
845
+ @id, @customer_id, @file_name, @content_type, @size_bytes, @storage_key, @public_url, @created_at
846
+ )
847
+ `).run({
848
+ id: record.id,
849
+ customer_id: record.customerId,
850
+ file_name: record.fileName,
851
+ content_type: record.contentType,
852
+ size_bytes: record.sizeBytes,
853
+ storage_key: record.storageKey,
854
+ public_url: record.publicUrl ?? null,
855
+ created_at: nowIso()
856
+ });
857
+ },
858
+ listUserAttachments(customerId) {
859
+ const rows = db.prepare(`
860
+ select *
861
+ from user_attachments
862
+ where customer_id = ?
863
+ order by created_at desc
864
+ `).all(customerId);
865
+ return rows.map(mapUserAttachment);
866
+ },
867
+ getUserAttachment(customerId, attachmentId) {
868
+ const row = db.prepare(`
869
+ select *
870
+ from user_attachments
871
+ where customer_id = ? and id = ?
872
+ limit 1
873
+ `).get(customerId, attachmentId);
874
+ return row ? mapUserAttachment(row) : null;
875
+ },
876
+ deleteUserAttachment(customerId, attachmentId) {
877
+ db.prepare(`delete from user_attachments where customer_id = ? and id = ?`).run(customerId, attachmentId);
878
+ },
879
+ createTemplateSource(record) {
880
+ const timestamp = nowIso();
881
+ db.prepare(`
882
+ insert into template_sources (
883
+ id, template_id, slug_id, repo_url, branch, template_module_path, skill_path, install_command, build_command, status, created_at, updated_at
884
+ ) values (
885
+ @id, @template_id, @slug_id, @repo_url, @branch, @template_module_path, @skill_path, @install_command, @build_command, @status, @created_at, @updated_at
886
+ )
887
+ `).run({
888
+ id: record.id,
889
+ template_id: record.templateId,
890
+ slug_id: record.slugId,
891
+ repo_url: record.repoUrl,
892
+ branch: record.branch,
893
+ template_module_path: record.templateModulePath,
894
+ skill_path: record.skillPath,
895
+ install_command: record.installCommand,
896
+ build_command: record.buildCommand,
897
+ status: record.status,
898
+ created_at: timestamp,
899
+ updated_at: timestamp
900
+ });
901
+ return this.getTemplateSourceByTemplateId(record.templateId);
902
+ },
903
+ getTemplateSource(id) {
904
+ const row = db.prepare(`select * from template_sources where id = ?`).get(id);
905
+ return row ? mapTemplateSource(row) : null;
906
+ },
907
+ getTemplateSourceByTemplateId(templateId) {
908
+ const row = db.prepare(`select * from template_sources where template_id = ?`).get(templateId);
909
+ return row ? mapTemplateSource(row) : null;
910
+ },
911
+ getTemplateSourceBySlugId(slugId) {
912
+ const row = db.prepare(`select * from template_sources where slug_id = ?`).get(slugId);
913
+ return row ? mapTemplateSource(row) : null;
914
+ },
915
+ listTemplateSources() {
916
+ return db.prepare(`select * from template_sources order by template_id asc`).all().map(mapTemplateSource);
917
+ },
918
+ createTemplateRelease(record) {
919
+ const timestamp = nowIso();
920
+ db.prepare(`
921
+ insert into template_releases (
922
+ id, source_id, template_id, branch, commit_sha, checkout_path, skill_path, module_path, status,
923
+ certification_report_json, activated_at, created_at, updated_at
924
+ ) values (
925
+ @id, @source_id, @template_id, @branch, @commit_sha, @checkout_path, @skill_path, @module_path, @status,
926
+ @certification_report_json, @activated_at, @created_at, @updated_at
927
+ )
928
+ on conflict(source_id, commit_sha) do update set
929
+ checkout_path = excluded.checkout_path,
930
+ skill_path = excluded.skill_path,
931
+ module_path = excluded.module_path,
932
+ status = excluded.status,
933
+ certification_report_json = excluded.certification_report_json,
934
+ activated_at = excluded.activated_at,
935
+ updated_at = excluded.updated_at
936
+ `).run({
937
+ id: record.id,
938
+ source_id: record.sourceId,
939
+ template_id: record.templateId,
940
+ branch: record.branch,
941
+ commit_sha: record.commitSha,
942
+ checkout_path: record.checkoutPath,
943
+ skill_path: record.skillPath,
944
+ module_path: record.modulePath,
945
+ status: record.status,
946
+ certification_report_json: stringifyJson(record.certificationReport ?? null),
947
+ activated_at: record.activatedAt ?? null,
948
+ created_at: timestamp,
949
+ updated_at: timestamp
950
+ });
951
+ return this.getTemplateReleaseBySourceAndCommit(record.sourceId, record.commitSha);
952
+ },
953
+ getTemplateRelease(id) {
954
+ const row = db.prepare(`select * from template_releases where id = ?`).get(id);
955
+ return row ? mapTemplateRelease(row) : null;
956
+ },
957
+ getTemplateReleaseBySourceAndCommit(sourceId, commitSha) {
958
+ const row = db.prepare(`select * from template_releases where source_id = ? and commit_sha = ?`).get(sourceId, commitSha);
959
+ return row ? mapTemplateRelease(row) : null;
960
+ },
961
+ listTemplateReleases(templateId) {
962
+ const rows = templateId
963
+ ? db.prepare(`select * from template_releases where template_id = ? order by created_at desc`).all(templateId)
964
+ : db.prepare(`select * from template_releases order by created_at desc`).all();
965
+ return rows.map(mapTemplateRelease);
966
+ },
967
+ getActiveTemplateReleases() {
968
+ return db.prepare(`select * from template_releases where status = 'active' order by template_id asc`).all().map(mapTemplateRelease);
969
+ },
970
+ updateTemplateReleaseStatus(input) {
971
+ const current = this.getTemplateRelease(input.id);
972
+ db.prepare(`
973
+ update template_releases
974
+ set status = @status,
975
+ certification_report_json = @certification_report_json,
976
+ activated_at = @activated_at,
977
+ updated_at = @updated_at
978
+ where id = @id
979
+ `).run({
980
+ id: input.id,
981
+ status: input.status,
982
+ certification_report_json: input.certificationReport === undefined
983
+ ? stringifyJson(current?.certificationReport ?? null)
984
+ : stringifyJson(input.certificationReport),
985
+ activated_at: input.activatedAt === undefined ? current?.activatedAt ?? null : input.activatedAt,
986
+ updated_at: nowIso()
987
+ });
988
+ },
989
+ clearActiveTemplateReleases(templateId) {
990
+ db.prepare(`
991
+ update template_releases
992
+ set status = case when status = 'active' then 'certified' else status end,
993
+ updated_at = ?
994
+ where template_id = ?
995
+ `).run(nowIso(), templateId);
587
996
  }
588
997
  };