@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
@@ -7,7 +7,8 @@ function escapeJsonForHtml(value) {
7
7
  export function renderDevApp(input) {
8
8
  const boot = escapeJsonForHtml({
9
9
  templates: input.templates,
10
- environment: input.environment
10
+ environment: input.environment,
11
+ apiBasePath: "/api/v1"
11
12
  });
12
13
  return `<!doctype html>
13
14
  <html lang="en">
@@ -516,14 +517,20 @@ export function renderDevApp(input) {
516
517
  const BOOT = ${boot};
517
518
  const state = {
518
519
  baseUrl: window.location.origin,
520
+ apiBasePath: BOOT.apiBasePath,
519
521
  session: loadSession(),
520
522
  templates: BOOT.templates,
521
523
  selectedTemplateId: BOOT.templates[0]?.id ?? null,
522
524
  selectedOperation: null,
523
525
  selectedJobId: null,
524
- jobs: []
526
+ jobs: [],
527
+ jobPollTimer: null
525
528
  };
526
529
 
530
+ const STANDARD_JOB_POLL_MS = 60 * 1000;
531
+ const BATCH_JOB_POLL_MS = 5 * 60 * 1000;
532
+ const BATCH_JOB_THRESHOLD = 5;
533
+
527
534
  const els = {
528
535
  email: document.getElementById("email"),
529
536
  otp: document.getElementById("otp"),
@@ -589,7 +596,7 @@ export function renderDevApp(input) {
589
596
  }
590
597
 
591
598
  async function request(path, options = {}) {
592
- const response = await fetch(state.baseUrl + path, options);
599
+ const response = await fetch(state.baseUrl + state.apiBasePath + path, options);
593
600
  const data = await response.json().catch(() => ({}));
594
601
  if (!response.ok) {
595
602
  throw new Error(data.error || JSON.stringify(data));
@@ -603,7 +610,9 @@ export function renderDevApp(input) {
603
610
  els.sessionSummary.textContent = "No active session.";
604
611
  return;
605
612
  }
606
- els.sessionSummary.textContent = "Active: " + state.session.customer.email + " " + state.session.customer.id;
613
+ const developerLabel = state.session.customer.isDeveloper ? "developer" : "standard";
614
+ els.sessionSummary.textContent =
615
+ "Active: " + state.session.customer.email + " • " + state.session.customer.id + " • " + developerLabel;
607
616
  }
608
617
 
609
618
  function getSelectedTemplate() {
@@ -611,12 +620,12 @@ export function renderDevApp(input) {
611
620
  }
612
621
 
613
622
  function getTemplateDefaults(templateId, operation) {
614
- if (templateId === "demo-template") {
623
+ if (templateId === "template_0000" || templateId === "4c7a7e1a-7f35-4f30-9f86-9c8a63c7f2db") {
615
624
  const config = {
616
625
  defaultProvider: "openai",
617
626
  textModel: "gpt-4.1-mini",
618
627
  imageModel: "gpt-image-1",
619
- renderCompositionId: "demo-template"
628
+ renderCompositionId: "template-0000"
620
629
  };
621
630
  const payloads = {
622
631
  generate: {
@@ -710,17 +719,54 @@ export function renderDevApp(input) {
710
719
  }
711
720
 
712
721
  async function refreshProviderKeys() {
713
- const data = await request("/me/provider-keys", { headers: headers() });
722
+ const data = await request("/user/me/provider-keys", { headers: headers() });
714
723
  els.providerKeysView.textContent = JSON.stringify(data.provider_keys, null, 2);
715
724
  }
716
725
 
717
726
  async function refreshJobs() {
718
- const data = await request("/me/jobs", { headers: headers() });
727
+ const data = await request("/user/me/jobs", { headers: headers() });
719
728
  state.jobs = data.jobs || [];
720
729
  renderJobs();
721
730
  }
722
731
 
732
+ function clearJobPollTimer() {
733
+ if (state.jobPollTimer) {
734
+ clearTimeout(state.jobPollTimer);
735
+ state.jobPollTimer = null;
736
+ }
737
+ }
738
+
739
+ function getSelectedJobRecord() {
740
+ return state.jobs.find((job) => (job.id || job.job_id) === state.selectedJobId) || null;
741
+ }
742
+
743
+ function getJobPollIntervalMs() {
744
+ const selectedJob = getSelectedJobRecord();
745
+ const selectedTracer = selectedJob?.tracer;
746
+ if (!selectedTracer) {
747
+ return STANDARD_JOB_POLL_MS;
748
+ }
749
+ const activeTracerJobs = state.jobs.filter((job) => {
750
+ const status = job.status;
751
+ return job.tracer === selectedTracer && (status === "queued" || status === "running");
752
+ }).length;
753
+ return activeTracerJobs >= BATCH_JOB_THRESHOLD ? BATCH_JOB_POLL_MS : STANDARD_JOB_POLL_MS;
754
+ }
755
+
756
+ function scheduleJobPoll() {
757
+ clearJobPollTimer();
758
+ state.jobPollTimer = setTimeout(async () => {
759
+ try {
760
+ await refreshJobs();
761
+ await refreshJobDetail();
762
+ } catch (error) {
763
+ els.inspector.textContent = JSON.stringify({ error: error.message }, null, 2);
764
+ }
765
+ }, getJobPollIntervalMs());
766
+ }
767
+
723
768
  async function refreshJobDetail() {
769
+ clearJobPollTimer();
724
770
  if (!state.selectedJobId || !state.selectedTemplateId) {
725
771
  return;
726
772
  }
@@ -731,7 +777,7 @@ export function renderDevApp(input) {
731
777
  renderTimeline(logs.logs || []);
732
778
  els.jobSummary.textContent = "Job " + job.job_id + " is " + job.status + " with progress " + job.progress;
733
779
  if (job.status === "queued" || job.status === "running") {
734
- setTimeout(refreshJobDetail, 1500);
780
+ scheduleJobPoll();
735
781
  }
736
782
  }
737
783
 
@@ -759,7 +805,7 @@ export function renderDevApp(input) {
759
805
  els.requestOtp.addEventListener("click", async () => {
760
806
  const email = els.email.value.trim();
761
807
  if (!email) return;
762
- await request("/auth/request-otp", {
808
+ await request("/user/request-otp", {
763
809
  method: "POST",
764
810
  headers: { "content-type": "application/json" },
765
811
  body: JSON.stringify({ email })
@@ -767,7 +813,7 @@ export function renderDevApp(input) {
767
813
  });
768
814
 
769
815
  els.verifyOtp.addEventListener("click", async () => {
770
- const data = await request("/auth/verify-otp", {
816
+ const data = await request("/user/verify-otp", {
771
817
  method: "POST",
772
818
  headers: { "content-type": "application/json" },
773
819
  body: JSON.stringify({
@@ -782,6 +828,7 @@ export function renderDevApp(input) {
782
828
  });
783
829
 
784
830
  els.clearSession.addEventListener("click", () => {
831
+ clearJobPollTimer();
785
832
  saveSession(null);
786
833
  els.providerKeysView.textContent = "[]";
787
834
  els.jobs.innerHTML = "";
@@ -789,7 +836,7 @@ export function renderDevApp(input) {
789
836
  });
790
837
 
791
838
  els.saveProviderKey.addEventListener("click", async () => {
792
- await request("/me/provider-keys", {
839
+ await request("/user/me/provider-keys", {
793
840
  method: "POST",
794
841
  headers: headers(),
795
842
  body: JSON.stringify({
@@ -0,0 +1,441 @@
1
+ function escapeHtml(value) {
2
+ return value
3
+ .replace(/&/g, "&amp;")
4
+ .replace(/</g, "&lt;")
5
+ .replace(/>/g, "&gt;")
6
+ .replace(/"/g, "&quot;");
7
+ }
8
+ function escapeJsonForHtml(value) {
9
+ return JSON.stringify(value)
10
+ .replace(/</g, "\\u003c")
11
+ .replace(/>/g, "\\u003e")
12
+ .replace(/&/g, "\\u0026");
13
+ }
14
+ function formatApprovedAt(value) {
15
+ if (!value) {
16
+ return "No release timestamp";
17
+ }
18
+ const date = new Date(value);
19
+ if (Number.isNaN(date.getTime())) {
20
+ return value;
21
+ }
22
+ return new Intl.DateTimeFormat("en-US", {
23
+ dateStyle: "medium",
24
+ timeStyle: "short"
25
+ }).format(date);
26
+ }
27
+ function isVideoPreview(url) {
28
+ return Boolean(url && /\.(mp4|webm|ogg|mov)(\?|#|$)/i.test(url));
29
+ }
30
+ export function renderHomepage(input) {
31
+ const boot = escapeJsonForHtml({
32
+ skills: Object.fromEntries(input.templates.map((template) => [template.templateId, template.skillContent]))
33
+ });
34
+ const rows = input.templates.map((template) => {
35
+ const search = `${template.templateId} ${template.title} ${template.slugId}`.toLowerCase();
36
+ const media = template.previewUrl
37
+ ? isVideoPreview(template.previewUrl)
38
+ ? `<video class="preview" src="${escapeHtml(template.previewUrl)}" preload="none" muted playsinline controls></video>`
39
+ : `<img class="preview" src="${escapeHtml(template.previewUrl)}" alt="${escapeHtml(template.title)} preview" loading="lazy" decoding="async" />`
40
+ : `<div class="preview preview-empty">No preview</div>`;
41
+ return `
42
+ <li class="row" data-search="${escapeHtml(search)}">
43
+ <div class="media">${media}</div>
44
+ <div class="details">
45
+ <div class="meta-line">
46
+ <h2>${escapeHtml(template.title)}</h2>
47
+ <button class="copy-button" type="button" data-copy-text="${escapeHtml(template.skillContent)}" data-default-label="SKILL.md">SKILL.md</button>
48
+ </div>
49
+ <dl class="meta-grid">
50
+ <div>
51
+ <dt>template_id</dt>
52
+ <dd><button class="copy-inline" type="button" data-copy-text="${escapeHtml(template.templateId)}" data-default-label="${escapeHtml(template.templateId)}"><code>${escapeHtml(template.templateId)}</code></button></dd>
53
+ </div>
54
+ <div>
55
+ <dt>slug_id</dt>
56
+ <dd><button class="copy-inline" type="button" data-copy-text="${escapeHtml(template.slugId)}" data-default-label="${escapeHtml(template.slugId)}"><code>${escapeHtml(template.slugId)}</code></button></dd>
57
+ </div>
58
+ <div>
59
+ <dt>approved</dt>
60
+ <dd>${escapeHtml(formatApprovedAt(template.approvedAt))}</dd>
61
+ </div>
62
+ </dl>
63
+ <div class="viral-block">
64
+ <span class="eyebrow">Viral DNA</span>
65
+ <p>${escapeHtml(template.viralDna)}</p>
66
+ </div>
67
+ </div>
68
+ </li>
69
+ `;
70
+ }).join("");
71
+ const accountLink = input.account.isLoggedIn
72
+ ? `<a class="account-link" href="/settings">Settings</a>`
73
+ : `<a class="account-link" href="/login">Login</a>`;
74
+ return `<!doctype html>
75
+ <html lang="en">
76
+ <head>
77
+ <meta charset="utf-8" />
78
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
79
+ <title>Approved Templates</title>
80
+ <style>
81
+ :root {
82
+ --bg: #f4f1e8;
83
+ --panel: rgba(255, 252, 246, 0.88);
84
+ --line: rgba(37, 32, 27, 0.12);
85
+ --ink: #171311;
86
+ --muted: #64584d;
87
+ --accent: #9f3d24;
88
+ --accent-soft: rgba(159, 61, 36, 0.12);
89
+ --shadow: 0 24px 70px rgba(36, 24, 15, 0.09);
90
+ }
91
+
92
+ * {
93
+ box-sizing: border-box;
94
+ }
95
+
96
+ body {
97
+ margin: 0;
98
+ min-height: 100vh;
99
+ color: var(--ink);
100
+ font-family: Georgia, "Times New Roman", serif;
101
+ background:
102
+ radial-gradient(circle at top left, rgba(159, 61, 36, 0.14), transparent 24%),
103
+ linear-gradient(180deg, #f8f5ed 0%, #f1ebde 100%);
104
+ }
105
+
106
+ body::before {
107
+ content: "";
108
+ position: fixed;
109
+ inset: 0;
110
+ pointer-events: none;
111
+ background-image:
112
+ linear-gradient(rgba(23, 19, 17, 0.035) 1px, transparent 1px),
113
+ linear-gradient(90deg, rgba(23, 19, 17, 0.035) 1px, transparent 1px);
114
+ background-size: 28px 28px;
115
+ mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.5), transparent 85%);
116
+ }
117
+
118
+ main {
119
+ width: min(1180px, calc(100vw - 32px));
120
+ margin: 0 auto;
121
+ padding: 32px 0 48px;
122
+ }
123
+
124
+ .shell {
125
+ background: var(--panel);
126
+ border: 1px solid var(--line);
127
+ box-shadow: var(--shadow);
128
+ backdrop-filter: blur(18px);
129
+ }
130
+
131
+ .topbar {
132
+ padding: 24px;
133
+ border-bottom: 1px solid var(--line);
134
+ display: grid;
135
+ gap: 16px;
136
+ }
137
+
138
+ .topline {
139
+ display: flex;
140
+ align-items: center;
141
+ justify-content: space-between;
142
+ gap: 16px;
143
+ }
144
+
145
+ .eyebrow {
146
+ display: inline-block;
147
+ letter-spacing: 0.18em;
148
+ text-transform: uppercase;
149
+ font-size: 11px;
150
+ color: var(--muted);
151
+ }
152
+
153
+ .account-link {
154
+ border: 1px solid var(--ink);
155
+ background: rgba(255, 255, 255, 0.64);
156
+ color: var(--ink);
157
+ text-decoration: none;
158
+ padding: 10px 14px;
159
+ font-size: 13px;
160
+ letter-spacing: 0.08em;
161
+ text-transform: uppercase;
162
+ }
163
+
164
+ h1 {
165
+ margin: 0;
166
+ font-weight: 400;
167
+ font-size: clamp(2rem, 5vw, 4rem);
168
+ line-height: 0.95;
169
+ }
170
+
171
+ .subcopy {
172
+ margin: 0;
173
+ max-width: 58ch;
174
+ color: var(--muted);
175
+ font-size: 17px;
176
+ line-height: 1.6;
177
+ }
178
+
179
+ .search-row {
180
+ display: grid;
181
+ grid-template-columns: minmax(0, 1fr) auto;
182
+ gap: 12px;
183
+ align-items: center;
184
+ }
185
+
186
+ input {
187
+ width: 100%;
188
+ border: 1px solid var(--line);
189
+ background: rgba(255, 255, 255, 0.72);
190
+ color: var(--ink);
191
+ padding: 14px 16px;
192
+ font: inherit;
193
+ outline: none;
194
+ }
195
+
196
+ input:focus {
197
+ border-color: var(--accent);
198
+ box-shadow: 0 0 0 4px var(--accent-soft);
199
+ }
200
+
201
+ .count {
202
+ color: var(--muted);
203
+ font-size: 13px;
204
+ letter-spacing: 0.08em;
205
+ text-transform: uppercase;
206
+ }
207
+
208
+ .list {
209
+ list-style: none;
210
+ margin: 0;
211
+ padding: 0;
212
+ }
213
+
214
+ .row {
215
+ display: grid;
216
+ grid-template-columns: 240px minmax(0, 1fr);
217
+ gap: 20px;
218
+ padding: 24px;
219
+ border-bottom: 1px solid var(--line);
220
+ }
221
+
222
+ .row[hidden] {
223
+ display: none;
224
+ }
225
+
226
+ .media {
227
+ width: 100%;
228
+ }
229
+
230
+ .preview {
231
+ display: block;
232
+ width: 100%;
233
+ aspect-ratio: 9 / 16;
234
+ object-fit: cover;
235
+ border: 1px solid var(--line);
236
+ background: rgba(23, 19, 17, 0.06);
237
+ }
238
+
239
+ .preview-empty {
240
+ display: grid;
241
+ place-items: center;
242
+ color: var(--muted);
243
+ }
244
+
245
+ .details {
246
+ min-width: 0;
247
+ display: grid;
248
+ gap: 16px;
249
+ }
250
+
251
+ .meta-line {
252
+ display: flex;
253
+ align-items: start;
254
+ justify-content: space-between;
255
+ gap: 16px;
256
+ }
257
+
258
+ h2 {
259
+ margin: 0;
260
+ font-weight: 400;
261
+ font-size: clamp(1.35rem, 2vw, 2rem);
262
+ }
263
+
264
+ .copy-button {
265
+ border: 1px solid var(--ink);
266
+ background: var(--ink);
267
+ color: #f8f5ed;
268
+ padding: 10px 14px;
269
+ font: inherit;
270
+ cursor: pointer;
271
+ white-space: nowrap;
272
+ }
273
+
274
+ .copy-button:hover {
275
+ background: var(--accent);
276
+ border-color: var(--accent);
277
+ }
278
+
279
+ .copy-button[data-state="done"] {
280
+ background: transparent;
281
+ color: var(--ink);
282
+ }
283
+
284
+ .copy-inline {
285
+ border: 0;
286
+ background: transparent;
287
+ color: inherit;
288
+ padding: 0;
289
+ font: inherit;
290
+ text-align: left;
291
+ cursor: pointer;
292
+ }
293
+
294
+ .copy-inline:hover code,
295
+ .copy-inline:focus-visible code {
296
+ text-decoration: underline;
297
+ text-underline-offset: 0.14em;
298
+ }
299
+
300
+ .meta-grid {
301
+ display: grid;
302
+ grid-template-columns: repeat(3, minmax(0, 1fr));
303
+ gap: 14px;
304
+ margin: 0;
305
+ }
306
+
307
+ .meta-grid div {
308
+ padding-top: 12px;
309
+ border-top: 1px solid var(--line);
310
+ }
311
+
312
+ dt {
313
+ margin-bottom: 8px;
314
+ color: var(--muted);
315
+ font-size: 11px;
316
+ letter-spacing: 0.18em;
317
+ text-transform: uppercase;
318
+ }
319
+
320
+ dd {
321
+ margin: 0;
322
+ line-height: 1.5;
323
+ overflow-wrap: anywhere;
324
+ }
325
+
326
+ code {
327
+ font-family: "SFMono-Regular", Menlo, Consolas, monospace;
328
+ font-size: 0.92em;
329
+ }
330
+
331
+ .viral-block p {
332
+ margin: 8px 0 0;
333
+ color: var(--ink);
334
+ line-height: 1.6;
335
+ }
336
+
337
+ .empty {
338
+ display: none;
339
+ padding: 28px 24px;
340
+ color: var(--muted);
341
+ }
342
+
343
+ .empty[data-visible="true"] {
344
+ display: block;
345
+ }
346
+
347
+ @media (max-width: 860px) {
348
+ main {
349
+ width: min(100vw - 20px, 1180px);
350
+ padding-top: 20px;
351
+ }
352
+
353
+ .search-row,
354
+ .row,
355
+ .meta-grid {
356
+ grid-template-columns: 1fr;
357
+ }
358
+
359
+ .row {
360
+ gap: 18px;
361
+ padding: 18px;
362
+ }
363
+
364
+ .meta-line {
365
+ flex-direction: column;
366
+ }
367
+ }
368
+ </style>
369
+ </head>
370
+ <body>
371
+ <main>
372
+ <section class="shell">
373
+ <header class="topbar">
374
+ <div class="topline">
375
+ <span class="eyebrow">Vidfarm</span>
376
+ ${accountLink}
377
+ </div>
378
+ <h1>Approved templates</h1>
379
+ <p class="subcopy">Simple index of approved templates with preview media, viral DNA, exact template IDs, and one-click SKILL copy.</p>
380
+ <div class="search-row">
381
+ <input id="search" type="search" placeholder="Quicksearch: template_id, title, slug_id" autocomplete="off" />
382
+ <div class="count" id="count">${input.templates.length} visible</div>
383
+ </div>
384
+ </header>
385
+ <ul class="list" id="template-list">${rows}</ul>
386
+ <div class="empty" id="empty-state">No approved templates match that search.</div>
387
+ </section>
388
+ </main>
389
+ <script type="application/json" id="homepage-boot">${boot}</script>
390
+ <script>
391
+ const boot = JSON.parse(document.getElementById("homepage-boot").textContent || "{}");
392
+ const searchInput = document.getElementById("search");
393
+ const rows = Array.from(document.querySelectorAll(".row"));
394
+ const count = document.getElementById("count");
395
+ const emptyState = document.getElementById("empty-state");
396
+
397
+ const updateFilter = () => {
398
+ const query = (searchInput.value || "").trim().toLowerCase();
399
+ let visibleCount = 0;
400
+ for (const row of rows) {
401
+ const searchText = row.getAttribute("data-search") || "";
402
+ const visible = !query || searchText.includes(query);
403
+ row.hidden = !visible;
404
+ if (visible) {
405
+ visibleCount += 1;
406
+ }
407
+ }
408
+ count.textContent = visibleCount + " visible";
409
+ emptyState.dataset.visible = visibleCount === 0 ? "true" : "false";
410
+ };
411
+
412
+ searchInput.addEventListener("input", updateFilter);
413
+
414
+ for (const button of document.querySelectorAll("[data-copy-text]")) {
415
+ button.addEventListener("click", async () => {
416
+ const text = button.getAttribute("data-copy-text") || "";
417
+ const defaultLabel = button.getAttribute("data-default-label") || "";
418
+ if (!text) {
419
+ button.textContent = "Missing";
420
+ button.dataset.state = "done";
421
+ return;
422
+ }
423
+
424
+ try {
425
+ await navigator.clipboard.writeText(text);
426
+ button.textContent = "Copied";
427
+ } catch {
428
+ button.textContent = "Clipboard blocked";
429
+ }
430
+
431
+ button.dataset.state = "done";
432
+ window.setTimeout(() => {
433
+ button.textContent = defaultLabel;
434
+ button.dataset.state = "";
435
+ }, 1600);
436
+ });
437
+ }
438
+ </script>
439
+ </body>
440
+ </html>`;
441
+ }
package/dist/src/index.js CHANGED
@@ -1,8 +1,13 @@
1
1
  import { startRuntime } from "./runtime.js";
2
- const runtime = startRuntime();
3
- for (const signal of ["SIGINT", "SIGTERM"]) {
4
- process.on(signal, () => {
5
- runtime.shutdown();
6
- process.exit(0);
7
- });
8
- }
2
+ void (async () => {
3
+ const runtime = await startRuntime();
4
+ for (const signal of ["SIGINT", "SIGTERM"]) {
5
+ process.on(signal, () => {
6
+ runtime.shutdown();
7
+ process.exit(0);
8
+ });
9
+ }
10
+ })().catch((error) => {
11
+ console.error(error instanceof Error ? error.stack ?? error.message : String(error));
12
+ process.exit(1);
13
+ });
@@ -28,3 +28,17 @@ export function decryptString(value, secret) {
28
28
  decipher.setAuthTag(tag);
29
29
  return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8");
30
30
  }
31
+ export function hashPassword(password) {
32
+ const salt = randomBytes(16).toString("hex");
33
+ const digest = scryptSync(password, salt, 32).toString("hex");
34
+ return `${salt}:${digest}`;
35
+ }
36
+ export function verifyPassword(password, storedHash) {
37
+ const [salt, digest] = storedHash.split(":");
38
+ if (!salt || !digest) {
39
+ return false;
40
+ }
41
+ const left = Buffer.from(scryptSync(password, salt, 32).toString("hex"), "hex");
42
+ const right = Buffer.from(digest, "hex");
43
+ return left.length === right.length && timingSafeEqual(left, right);
44
+ }