@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.
- package/.env.example +11 -4
- package/PLATFORM_SPEC.md +142 -2
- package/README.md +165 -16
- package/SKILL.developer.md +577 -0
- package/dist/infra/cdk/bin/vidfarm-prod.js +59 -0
- package/dist/infra/cdk/lib/vidfarm-prod-stack.js +212 -0
- package/dist/src/account-pages.js +578 -0
- package/dist/src/app.js +887 -66
- package/dist/src/cli.js +284 -5
- package/dist/src/config.js +24 -4
- package/dist/src/db.js +427 -18
- package/dist/src/dev-app.js +59 -12
- package/dist/src/homepage.js +441 -0
- package/dist/src/index.js +12 -7
- package/dist/src/lib/crypto.js +14 -0
- package/dist/src/lib/template-dna.js +542 -0
- package/dist/src/lib/template-style-options.js +49 -0
- package/dist/src/registry.js +54 -7
- package/dist/src/runtime.js +3 -1
- package/dist/src/services/auth.js +69 -5
- package/dist/src/services/jobs.js +23 -4
- package/dist/src/services/providers.js +74 -12
- package/dist/src/services/storage.js +52 -18
- package/dist/src/services/template-certification.js +160 -0
- package/dist/src/services/template-loader.js +37 -0
- package/dist/src/services/template-sources.js +135 -0
- package/dist/src/worker.js +19 -7
- package/dist/templates/template_0000/src/lib/images.js +242 -0
- package/dist/templates/template_0000/src/remotion/Root.js +33 -0
- package/dist/templates/template_0000/src/sdk.js +3 -0
- package/dist/templates/template_0000/src/style-options.js +51 -0
- package/dist/templates/template_0000/src/template-dna.js +9 -0
- package/dist/templates/template_0000/src/template.js +1217 -0
- package/package.json +9 -1
- package/templates/template_0000/README.md +121 -0
- package/templates/template_0000/SKILL.md +193 -0
- package/templates/template_0000/assets/Abel-Regular.ttf +0 -0
- package/templates/template_0000/assets/DMSerifDisplay-Regular.ttf +0 -0
- package/templates/template_0000/assets/Montserrat[wght].ttf +0 -0
- package/templates/template_0000/assets/SourceCodePro[wght].ttf +0 -0
- package/templates/template_0000/assets/TikTokSans-SemiBold.ttf +0 -0
- package/templates/template_0000/assets/Yesteryear-Regular.ttf +0 -0
- package/templates/template_0000/composition.json +11 -0
- package/templates/template_0000/package-lock.json +5137 -0
- package/templates/template_0000/package.json +30 -0
- package/templates/template_0000/research/preview/.gitkeep +1 -0
- package/templates/template_0000/research/source_notes.md +7 -0
- package/templates/template_0000/scripts/create-site.mjs +27 -0
- package/templates/template_0000/scripts/render-cloud.mjs +72 -0
- package/templates/template_0000/src/lib/images.ts +284 -0
- package/templates/template_0000/src/remotion/Root.js +33 -0
- package/templates/template_0000/src/remotion/Root.tsx +75 -0
- package/templates/template_0000/src/remotion/index.tsx +4 -0
- package/templates/template_0000/src/sdk.ts +122 -0
- package/templates/template_0000/src/style-options.js +51 -0
- package/templates/template_0000/src/style-options.ts +60 -0
- package/templates/template_0000/src/template-dna.ts +15 -0
- package/templates/template_0000/src/template.ts +1747 -0
- package/templates/template_0000/template.config.json +26 -0
- package/templates/template_0000/tsconfig.json +19 -0
- package/dist/templates/template_0000/demo-template.js +0 -196
- package/dist/templates/template_0000/remotion/Root.js +0 -66
- /package/dist/templates/template_0000/{remotion → src/remotion}/index.js +0 -0
package/dist/src/dev-app.js
CHANGED
|
@@ -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
|
-
|
|
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 === "
|
|
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: "
|
|
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
|
-
|
|
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("/
|
|
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("/
|
|
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, "&")
|
|
4
|
+
.replace(/</g, "<")
|
|
5
|
+
.replace(/>/g, ">")
|
|
6
|
+
.replace(/"/g, """);
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
+
});
|
package/dist/src/lib/crypto.js
CHANGED
|
@@ -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
|
+
}
|