create-dokio 0.1.19 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +788 -762
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,220 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import prompts3 from "prompts";
|
|
5
4
|
import kleur7 from "kleur";
|
|
6
5
|
import { createRequire } from "module";
|
|
6
|
+
import prompts3 from "prompts";
|
|
7
7
|
|
|
8
|
-
// src/
|
|
9
|
-
import { join
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
// src/prompts.ts
|
|
14
|
-
import prompts from "prompts";
|
|
8
|
+
// src/hub.ts
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { execSync } from "child_process";
|
|
11
|
+
import fse from "fs-extra";
|
|
15
12
|
import kleur from "kleur";
|
|
16
|
-
|
|
17
|
-
// src/utils.ts
|
|
18
|
-
function toKebab(str) {
|
|
19
|
-
return str.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
20
|
-
}
|
|
21
|
-
function getPageName(index, total) {
|
|
22
|
-
if (total === 1) return "page1";
|
|
23
|
-
if (index === 0) return "page1-cover";
|
|
24
|
-
if (index === total - 1) return `page${index + 1}-cta`;
|
|
25
|
-
return `page${index + 1}-content`;
|
|
26
|
-
}
|
|
27
|
-
function buildPages(pageCount) {
|
|
28
|
-
return Array.from({ length: pageCount }, (_, i) => getPageName(i, pageCount));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// src/hubs.ts
|
|
32
|
-
var HUBS = [
|
|
33
|
-
{ id: "aexp", title: "Amex Global" },
|
|
34
|
-
{ id: "amazon-devices", title: "Amazon Devices" },
|
|
35
|
-
{ id: "australian-unity", title: "Australian Unity" },
|
|
36
|
-
{ id: "belmond", title: "Belmond" },
|
|
37
|
-
{ id: "bupa-aged-care", title: "Bupa Aged Care" },
|
|
38
|
-
{ id: "bupa-healthy-cities", title: "Bupa Healthy Cities" },
|
|
39
|
-
{ id: "bupa-hs", title: "Bupa Health Services" },
|
|
40
|
-
{ id: "bupa-marketing", title: "Bupa Marketing" },
|
|
41
|
-
{ id: "bupa-retail", title: "Bupa Retail" },
|
|
42
|
-
{ id: "bupa-sam", title: "Bupa Sales And Marketing Hub" },
|
|
43
|
-
{ id: "designsystem", title: "Design System" },
|
|
44
|
-
{ id: "eabrandhub", title: "EnergyAustralia's Brand Hub" },
|
|
45
|
-
{ id: "fridas", title: "Frida's Luxe Sip n' Paint" },
|
|
46
|
-
{ id: "gwm", title: "GWM Advertising Studio" },
|
|
47
|
-
{ id: "headspace", title: "Headspace" },
|
|
48
|
-
{ id: "hellofresh", title: "HelloFresh" },
|
|
49
|
-
{ id: "ipa", title: "IPA LAM Hub" },
|
|
50
|
-
{ id: "iris-samsung", title: "Iris | Samsung" },
|
|
51
|
-
{ id: "knowledgebase", title: "KB Hub" },
|
|
52
|
-
{ id: "mi", title: "Measurable Impact" },
|
|
53
|
-
{ id: "meridianenergy", title: "Meridian Energy Hub" },
|
|
54
|
-
{ id: "nissan", title: "Nissan" },
|
|
55
|
-
{ id: "origin", title: "Origin" },
|
|
56
|
-
{ id: "origin-fugu", title: "Origin Fugu" },
|
|
57
|
-
{ id: "originloopvpp-partnerships-hub", title: "Origin Loop VPP Partnerships Hub" },
|
|
58
|
-
{ id: "poolwerx", title: "Poolwerx Loop" },
|
|
59
|
-
{ id: "sandbox", title: "Sandbox" },
|
|
60
|
-
{ id: "scimmer", title: "Scimmer" },
|
|
61
|
-
{ id: "shell-au", title: "Shell Australia Brand Templates" },
|
|
62
|
-
{ id: "vidacorp", title: "VidaDesign Hub" }
|
|
63
|
-
];
|
|
64
|
-
function hubRepoUrl(hubId) {
|
|
65
|
-
return `https://github.com/dokioco/${hubId}-templates`;
|
|
66
|
-
}
|
|
67
|
-
function hubRepoDirName(hubId) {
|
|
68
|
-
return `${hubId}-templates`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// src/prompts.ts
|
|
72
|
-
var onCancel = () => {
|
|
73
|
-
console.log(kleur.yellow("\n Cancelled.\n"));
|
|
74
|
-
process.exit(0);
|
|
75
|
-
};
|
|
76
|
-
async function runPrompts(nameArg) {
|
|
77
|
-
const base = await prompts(
|
|
78
|
-
[
|
|
79
|
-
{
|
|
80
|
-
type: "text",
|
|
81
|
-
name: "templateId",
|
|
82
|
-
message: "Template ID (e.g. HW485)",
|
|
83
|
-
validate: (v) => /^[A-Za-z0-9]+$/.test(v.trim()) || "Alphanumeric only"
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
type: nameArg ? null : "text",
|
|
87
|
-
name: "name",
|
|
88
|
-
message: "Template name (e.g. My Cool Template)",
|
|
89
|
-
initial: "My Template",
|
|
90
|
-
validate: (v) => v.trim().length > 0 || "Required"
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
type: "select",
|
|
94
|
-
name: "mode",
|
|
95
|
-
message: "Template type",
|
|
96
|
-
choices: [
|
|
97
|
-
{ title: "PDF", value: "pdf" },
|
|
98
|
-
{ title: "General (image/JPG/PNG)", value: "general" },
|
|
99
|
-
{ title: kleur.dim("Video [WIP]"), value: "video" },
|
|
100
|
-
{ title: "Email", value: "email" }
|
|
101
|
-
]
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
type: "select",
|
|
105
|
-
name: "hubId",
|
|
106
|
-
message: "Hub",
|
|
107
|
-
choices: HUBS.map((h) => ({ title: h.title, value: h.id }))
|
|
108
|
-
}
|
|
109
|
-
],
|
|
110
|
-
{ onCancel }
|
|
111
|
-
);
|
|
112
|
-
const templateId = base.templateId.trim();
|
|
113
|
-
const name = nameArg ? nameArg.trim() : base.name.trim();
|
|
114
|
-
const fullName = `${templateId}-${toKebab(name)}`;
|
|
115
|
-
const hubId = base.hubId;
|
|
116
|
-
const subdomain = hubId;
|
|
117
|
-
if (base.mode === "pdf") {
|
|
118
|
-
const pdf = await prompts(
|
|
119
|
-
[
|
|
120
|
-
{ type: "number", name: "width", message: "Page width (mm)", initial: 210, min: 1 },
|
|
121
|
-
{ type: "number", name: "height", message: "Page height (mm)", initial: 297, min: 1 },
|
|
122
|
-
{ type: "number", name: "pageCount", message: "Number of pages", initial: 1, min: 1 },
|
|
123
|
-
{
|
|
124
|
-
type: "select",
|
|
125
|
-
name: "princeVersion",
|
|
126
|
-
message: "PrinceXML version",
|
|
127
|
-
choices: [
|
|
128
|
-
{ title: "15 (flexbox support \u2014 recommended)", value: 15 },
|
|
129
|
-
{ title: "11 (legacy)", value: 11 }
|
|
130
|
-
]
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
type: "toggle",
|
|
134
|
-
name: "resizable",
|
|
135
|
-
message: "Resizable template?",
|
|
136
|
-
initial: false,
|
|
137
|
-
active: "yes",
|
|
138
|
-
inactive: "no"
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
type: "toggle",
|
|
142
|
-
name: "proofable",
|
|
143
|
-
message: "Enable proof downloads (draft_proofable)?",
|
|
144
|
-
initial: true,
|
|
145
|
-
active: "yes",
|
|
146
|
-
inactive: "no"
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
type: "toggle",
|
|
150
|
-
name: "orderable",
|
|
151
|
-
message: "Include orderable export (Print from Snap)?",
|
|
152
|
-
initial: false,
|
|
153
|
-
active: "yes",
|
|
154
|
-
inactive: "no"
|
|
155
|
-
}
|
|
156
|
-
],
|
|
157
|
-
{ onCancel }
|
|
158
|
-
);
|
|
159
|
-
return {
|
|
160
|
-
mode: "pdf",
|
|
161
|
-
templateId,
|
|
162
|
-
name,
|
|
163
|
-
fullName,
|
|
164
|
-
subdomain,
|
|
165
|
-
hubId,
|
|
166
|
-
width: pdf.width,
|
|
167
|
-
height: pdf.height,
|
|
168
|
-
pageCount: pdf.pageCount,
|
|
169
|
-
princeVersion: pdf.princeVersion,
|
|
170
|
-
resizable: pdf.resizable,
|
|
171
|
-
proofable: pdf.proofable,
|
|
172
|
-
orderable: pdf.orderable
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
if (base.mode === "general") {
|
|
176
|
-
const gen = await prompts(
|
|
177
|
-
[
|
|
178
|
-
{ type: "number", name: "width", message: "Width (px)", initial: 400, min: 1 },
|
|
179
|
-
{ type: "number", name: "height", message: "Height (px)", initial: 400, min: 1 },
|
|
180
|
-
{
|
|
181
|
-
type: "number",
|
|
182
|
-
name: "pageCount",
|
|
183
|
-
message: "Number of pages / sections",
|
|
184
|
-
initial: 1,
|
|
185
|
-
min: 1
|
|
186
|
-
},
|
|
187
|
-
{
|
|
188
|
-
type: "multiselect",
|
|
189
|
-
name: "exportFormats",
|
|
190
|
-
message: "Export formats",
|
|
191
|
-
choices: [
|
|
192
|
-
{ title: "JPG", value: "jpg", selected: true },
|
|
193
|
-
{ title: "PNG", value: "png", selected: true }
|
|
194
|
-
],
|
|
195
|
-
min: 1
|
|
196
|
-
}
|
|
197
|
-
],
|
|
198
|
-
{ onCancel }
|
|
199
|
-
);
|
|
200
|
-
return {
|
|
201
|
-
mode: "general",
|
|
202
|
-
templateId,
|
|
203
|
-
name,
|
|
204
|
-
fullName,
|
|
205
|
-
subdomain,
|
|
206
|
-
hubId,
|
|
207
|
-
width: gen.width,
|
|
208
|
-
height: gen.height,
|
|
209
|
-
pageCount: gen.pageCount,
|
|
210
|
-
exportFormats: gen.exportFormats
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
if (base.mode === "video") {
|
|
214
|
-
return { mode: "video", templateId, name, fullName, subdomain, hubId };
|
|
215
|
-
}
|
|
216
|
-
return { mode: "email", templateId, name, fullName, subdomain, hubId };
|
|
217
|
-
}
|
|
13
|
+
import prompts from "prompts";
|
|
218
14
|
|
|
219
15
|
// src/templates/shared.ts
|
|
220
16
|
function changelog(fullName) {
|
|
@@ -324,52 +120,687 @@ function mixinsScss() {
|
|
|
324
120
|
`;
|
|
325
121
|
}
|
|
326
122
|
|
|
327
|
-
// src/templates/
|
|
328
|
-
function
|
|
329
|
-
return
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
"
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
123
|
+
// src/templates/changelog.ts
|
|
124
|
+
function changelogScript() {
|
|
125
|
+
return `#!/usr/bin/env python3
|
|
126
|
+
"""Prepend a new entry into a template's CHANGELOG.md."""
|
|
127
|
+
import sys
|
|
128
|
+
import re
|
|
129
|
+
import os
|
|
130
|
+
import datetime
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def load_dotenv():
|
|
134
|
+
env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")
|
|
135
|
+
if not os.path.isfile(env_path):
|
|
136
|
+
return
|
|
137
|
+
with open(env_path) as f:
|
|
138
|
+
for line in f:
|
|
139
|
+
line = line.strip()
|
|
140
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
141
|
+
continue
|
|
142
|
+
key, _, val = line.partition("=")
|
|
143
|
+
key = key.strip()
|
|
144
|
+
val = val.strip().strip('"').strip("'")
|
|
145
|
+
if key and key not in os.environ:
|
|
146
|
+
os.environ[key] = val
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
load_dotenv()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
STATUS_LABELS = {
|
|
153
|
+
"A": "Added",
|
|
154
|
+
"M": "Modified",
|
|
155
|
+
"D": "Deleted",
|
|
156
|
+
"R": "Renamed",
|
|
157
|
+
"C": "Copied",
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def format_date(today_str):
|
|
162
|
+
d = datetime.datetime.strptime(today_str, "%Y-%m-%d")
|
|
163
|
+
day = str(int(d.strftime("%d")))
|
|
164
|
+
return d.strftime(f"%B {day}, %Y")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def generate_description(title, files, diff):
|
|
168
|
+
try:
|
|
169
|
+
import anthropic
|
|
170
|
+
except ImportError:
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
|
174
|
+
if not api_key:
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
client = anthropic.Anthropic(api_key=api_key)
|
|
179
|
+
files_str = "\\n".join(f"- {label}: {path}" for label, path in files)
|
|
180
|
+
diff_section = f"\\n\\nDiff:\\n\`\`\`\\n{diff[:4000]}\\n\`\`\`" if diff.strip() else ""
|
|
181
|
+
|
|
182
|
+
prompt = (
|
|
183
|
+
f"Commit: {title}\\n"
|
|
184
|
+
f"Files:\\n{files_str}"
|
|
185
|
+
f"{diff_section}\\n\\n"
|
|
186
|
+
f"Write one sentence describing what changed. Be direct and specific. No filler words."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
message = client.messages.create(
|
|
190
|
+
model="claude-haiku-4-5-20251001",
|
|
191
|
+
max_tokens=256,
|
|
192
|
+
messages=[{"role": "user", "content": prompt}],
|
|
193
|
+
)
|
|
194
|
+
return message.content[0].text.strip()
|
|
195
|
+
except Exception:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def parse_files(files_raw, template_prefix):
|
|
200
|
+
files = []
|
|
201
|
+
for line in files_raw.strip().splitlines():
|
|
202
|
+
line = line.strip()
|
|
203
|
+
if not line:
|
|
204
|
+
continue
|
|
205
|
+
parts = line.split("\\t")
|
|
206
|
+
if len(parts) >= 2:
|
|
207
|
+
status_key = parts[0].strip()[0].upper()
|
|
208
|
+
label = STATUS_LABELS.get(status_key, "Modified")
|
|
209
|
+
path = parts[-1].strip()
|
|
210
|
+
else:
|
|
211
|
+
label = "Modified"
|
|
212
|
+
path = parts[0].strip()
|
|
213
|
+
if template_prefix and path.startswith(template_prefix + "/"):
|
|
214
|
+
path = path[len(template_prefix):].lstrip("/")
|
|
215
|
+
files.append((label, path))
|
|
216
|
+
return files
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def build_entry_content(branch, title, description, author, today_str, files):
|
|
220
|
+
date_str = format_date(today_str)
|
|
221
|
+
lines = [
|
|
222
|
+
f"**{author}** \u2022 *{date_str}*",
|
|
223
|
+
f"**branch:** \`{branch}\`",
|
|
224
|
+
"",
|
|
225
|
+
f"**title:** {title}",
|
|
226
|
+
"",
|
|
227
|
+
f"**description:** {description}",
|
|
228
|
+
"",
|
|
229
|
+
"### Files Changed",
|
|
230
|
+
]
|
|
231
|
+
for label, path in files:
|
|
232
|
+
lines.append(f"- **{label}** \`{path}\`")
|
|
233
|
+
return "\\n".join(lines)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def update_changelog(path, branch, today, title, author, files, diff):
|
|
237
|
+
with open(path, "r") as f:
|
|
238
|
+
content = f.read()
|
|
239
|
+
|
|
240
|
+
description = generate_description(title, files, diff) or "TODO - INTEGRATE WITH AI (JAKE TASK)"
|
|
241
|
+
entry_content = build_entry_content(branch, title, description, author, today, files)
|
|
242
|
+
|
|
243
|
+
insert_match = re.search(r"^(---|## )", content, re.MULTILINE)
|
|
244
|
+
|
|
245
|
+
if insert_match:
|
|
246
|
+
insert_pos = insert_match.start()
|
|
247
|
+
if content[insert_pos:].startswith("---"):
|
|
248
|
+
new_block = f"---\\n\\n{entry_content}\\n\\n"
|
|
249
|
+
else:
|
|
250
|
+
new_block = f"---\\n\\n{entry_content}\\n\\n---\\n\\n"
|
|
251
|
+
content = content[:insert_pos] + new_block + content[insert_pos:]
|
|
252
|
+
else:
|
|
253
|
+
content = content.rstrip("\\n") + f"\\n\\n---\\n\\n{entry_content}\\n\\n---\\n"
|
|
254
|
+
|
|
255
|
+
with open(path, "w") as f:
|
|
256
|
+
f.write(content)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
if __name__ == "__main__":
|
|
260
|
+
path = sys.argv[1]
|
|
261
|
+
today = sys.argv[2]
|
|
262
|
+
branch = os.environ.get("CHANGELOG_BRANCH", "")
|
|
263
|
+
message = os.environ.get("CHANGELOG_MESSAGE", "")
|
|
264
|
+
author = os.environ.get("CHANGELOG_AUTHOR", "")
|
|
265
|
+
files_raw = os.environ.get("CHANGELOG_FILES", "")
|
|
266
|
+
template_prefix = os.environ.get("CHANGELOG_TEMPLATE_PREFIX", "")
|
|
267
|
+
diff = os.environ.get("CHANGELOG_DIFF", "")
|
|
268
|
+
|
|
269
|
+
files = parse_files(files_raw, template_prefix)
|
|
270
|
+
update_changelog(path, branch, today, message, author, files, diff)
|
|
271
|
+
`;
|
|
272
|
+
}
|
|
273
|
+
function changelogHook() {
|
|
274
|
+
return `#!/bin/bash
|
|
275
|
+
# Auto-updates CHANGELOG.md in modified template folders after each commit.
|
|
276
|
+
# Amends the commit silently to include changelog changes.
|
|
277
|
+
|
|
278
|
+
REPO_ROOT=$(git rev-parse --show-toplevel)
|
|
279
|
+
LOCK="$REPO_ROOT/.git/changelog-hook-running"
|
|
280
|
+
|
|
281
|
+
[ -f "$LOCK" ] && exit 0
|
|
282
|
+
|
|
283
|
+
CHANGED=$(git diff-tree --no-commit-id -r --name-only HEAD | grep -v "CHANGELOG\\.md$")
|
|
284
|
+
[ -z "$CHANGED" ] && exit 0
|
|
285
|
+
|
|
286
|
+
# Detect changed templates (expects templates/<template-id>/ structure)
|
|
287
|
+
TEMPLATES=$(echo "$CHANGED" | grep "^templates/" | awk -F'/' '{print $1"/"$2}' | sort -u)
|
|
288
|
+
[ -z "$TEMPLATES" ] && exit 0
|
|
289
|
+
|
|
290
|
+
COMMIT_MSG=$(git log -1 --pretty=%s)
|
|
291
|
+
AUTHOR=$(git log -1 --pretty="%an")
|
|
292
|
+
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
293
|
+
TODAY=$(date +%Y-%m-%d)
|
|
294
|
+
|
|
295
|
+
touch "$LOCK"
|
|
296
|
+
trap 'rm -f "$LOCK"' EXIT
|
|
297
|
+
|
|
298
|
+
AMENDED=0
|
|
299
|
+
while IFS= read -r TEMPLATE; do
|
|
300
|
+
[ -z "$TEMPLATE" ] && continue
|
|
301
|
+
CHANGELOG="$REPO_ROOT/$TEMPLATE/CHANGELOG.md"
|
|
302
|
+
[ ! -f "$CHANGELOG" ] && continue
|
|
303
|
+
|
|
304
|
+
TEMPLATE_FILES=$(git diff-tree --no-commit-id -r --name-status HEAD \\
|
|
305
|
+
| grep -v "CHANGELOG\\.md$" \\
|
|
306
|
+
| grep -E $'\\t'"$TEMPLATE/")
|
|
307
|
+
|
|
308
|
+
TEMPLATE_DIFF=$(git diff-tree --no-commit-id -r -p HEAD -- "$TEMPLATE/" 2>/dev/null | head -c 4000)
|
|
309
|
+
|
|
310
|
+
export CHANGELOG_BRANCH="$BRANCH"
|
|
311
|
+
export CHANGELOG_MESSAGE="$COMMIT_MSG"
|
|
312
|
+
export CHANGELOG_AUTHOR="$AUTHOR"
|
|
313
|
+
export CHANGELOG_FILES="$TEMPLATE_FILES"
|
|
314
|
+
export CHANGELOG_TEMPLATE_PREFIX="$TEMPLATE"
|
|
315
|
+
export CHANGELOG_DIFF="$TEMPLATE_DIFF"
|
|
316
|
+
python3 "$REPO_ROOT/tools/changelog/update_changelog.py" "$CHANGELOG" "$TODAY"
|
|
317
|
+
|
|
318
|
+
git add "$CHANGELOG"
|
|
319
|
+
AMENDED=1
|
|
320
|
+
done <<< "$TEMPLATES"
|
|
321
|
+
|
|
322
|
+
if [ "$AMENDED" = "1" ]; then
|
|
323
|
+
git commit --amend --no-edit
|
|
324
|
+
fi
|
|
325
|
+
`;
|
|
326
|
+
}
|
|
327
|
+
function changelogEnvExample() {
|
|
328
|
+
return `ANTHROPIC_API_KEY=sk-ant-...
|
|
329
|
+
`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/hub.ts
|
|
333
|
+
var onCancel = () => {
|
|
334
|
+
console.log(kleur.yellow("\n Cancelled.\n"));
|
|
335
|
+
process.exit(0);
|
|
336
|
+
};
|
|
337
|
+
async function runHub() {
|
|
338
|
+
console.log(kleur.bold().cyan("\n \u25C6 dokio create hub\n"));
|
|
339
|
+
const answers = await prompts(
|
|
340
|
+
[
|
|
341
|
+
{
|
|
342
|
+
type: "text",
|
|
343
|
+
name: "hubId",
|
|
344
|
+
message: "Hub ID (kebab-case, e.g. bupa-sam)",
|
|
345
|
+
validate: (v) => /^[a-z0-9-]+$/.test(v.trim()) || "Lowercase letters, numbers, hyphens only"
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
type: "text",
|
|
349
|
+
name: "hubName",
|
|
350
|
+
message: "Hub display name (e.g. Bupa Sales And Marketing Hub)",
|
|
351
|
+
validate: (v) => v.trim().length > 0 || "Required"
|
|
352
|
+
}
|
|
353
|
+
],
|
|
354
|
+
{ onCancel }
|
|
355
|
+
);
|
|
356
|
+
const hubId = answers.hubId.trim();
|
|
357
|
+
const hubName = answers.hubName.trim();
|
|
358
|
+
const dirName = `${hubId}-templates`;
|
|
359
|
+
const outDir = join(process.cwd(), dirName);
|
|
360
|
+
if (await fse.pathExists(outDir)) {
|
|
361
|
+
console.error(kleur.red(`
|
|
362
|
+
Error: "${dirName}" already exists.
|
|
363
|
+
`));
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
const files = {
|
|
367
|
+
".githooks/commit-msg": commitMsgHook(),
|
|
368
|
+
".githooks/post-commit": changelogHook(),
|
|
369
|
+
".vscode/settings.json": JSON.stringify({ "scss.validate": false, "css.validate": false }, null, 2) + "\n",
|
|
370
|
+
"templates/.gitkeep": "",
|
|
371
|
+
"tools/changelog/update_changelog.py": changelogScript(),
|
|
372
|
+
"tools/changelog/.env.example": changelogEnvExample(),
|
|
373
|
+
".gitignore": `.DS_Store
|
|
374
|
+
node_modules/
|
|
375
|
+
*.log
|
|
376
|
+
tools/changelog/.env
|
|
377
|
+
`,
|
|
378
|
+
"README.md": `# ${hubName}
|
|
379
|
+
|
|
380
|
+
Templates for ${hubName} on Dokio.
|
|
381
|
+
|
|
382
|
+
## After cloning (required, once per clone)
|
|
383
|
+
|
|
384
|
+
Git hooks (commit-message check + auto-changelog) are not active until you run:
|
|
385
|
+
|
|
386
|
+
\`\`\`
|
|
387
|
+
npx create-dokio repair
|
|
388
|
+
\`\`\`
|
|
389
|
+
|
|
390
|
+
Git cannot enable repo hooks automatically on clone, so every teammate must run this once.
|
|
391
|
+
|
|
392
|
+
## Creating a new template
|
|
393
|
+
|
|
394
|
+
Run \`create-dokio template\` from inside this repo.
|
|
395
|
+
`
|
|
396
|
+
};
|
|
397
|
+
console.log("");
|
|
398
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
399
|
+
const fullPath = join(outDir, rel);
|
|
400
|
+
await fse.ensureDir(join(fullPath, ".."));
|
|
401
|
+
await fse.writeFile(fullPath, content, "utf8");
|
|
402
|
+
if (rel === ".githooks/commit-msg" || rel === ".githooks/post-commit" || rel === "tools/changelog/update_changelog.py") await fse.chmod(fullPath, 493);
|
|
403
|
+
console.log(kleur.dim(` + ${rel}`));
|
|
404
|
+
}
|
|
405
|
+
execSync("git init", { cwd: outDir, stdio: "ignore" });
|
|
406
|
+
execSync("git config core.hooksPath .githooks", { cwd: outDir, stdio: "ignore" });
|
|
407
|
+
execSync("git add .", { cwd: outDir, stdio: "ignore" });
|
|
408
|
+
execSync('git commit -m "chore: init Dokio Hub"', { cwd: outDir, stdio: "ignore" });
|
|
409
|
+
console.log(kleur.green(`
|
|
410
|
+
\u2713 Created ${kleur.bold(dirName)}
|
|
411
|
+
`));
|
|
412
|
+
console.log(kleur.dim(` Next steps:`));
|
|
413
|
+
console.log(kleur.dim(` cd ${dirName}`));
|
|
414
|
+
console.log(kleur.dim(` Create a GitHub repo: github.com/dokioco/${dirName}`));
|
|
415
|
+
console.log(kleur.dim(` git remote add origin https://github.com/dokioco/${dirName}`));
|
|
416
|
+
console.log(kleur.dim(` git push -u origin main`));
|
|
417
|
+
console.log("");
|
|
418
|
+
console.log(kleur.dim(` Teammates \u2014 after cloning, enable git hooks once:`));
|
|
419
|
+
console.log(kleur.dim(` npx create-dokio repair`));
|
|
420
|
+
console.log("");
|
|
421
|
+
console.log(kleur.dim(` Changelog (optional \u2014 for AI descriptions):`));
|
|
422
|
+
console.log(kleur.dim(` cp tools/changelog/.env.example tools/changelog/.env`));
|
|
423
|
+
console.log(kleur.dim(` # Add your ANTHROPIC_API_KEY to tools/changelog/.env`));
|
|
424
|
+
console.log("");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/repair.ts
|
|
428
|
+
import { execSync as execSync3 } from "child_process";
|
|
429
|
+
import { basename } from "path";
|
|
430
|
+
import kleur2 from "kleur";
|
|
431
|
+
|
|
432
|
+
// src/hubSetup.ts
|
|
433
|
+
import { join as join2 } from "path";
|
|
434
|
+
import { execSync as execSync2 } from "child_process";
|
|
435
|
+
import fse2 from "fs-extra";
|
|
436
|
+
function vscodeSettings() {
|
|
437
|
+
return JSON.stringify({ "scss.validate": false, "css.validate": false }, null, 2) + "\n";
|
|
438
|
+
}
|
|
439
|
+
var REQUIRED_IGNORES = [".DS_Store", "node_modules/", "*.log", "tools/changelog/.env"];
|
|
440
|
+
var MANAGED = [
|
|
441
|
+
{ rel: ".githooks/commit-msg", content: commitMsgHook, exec: true },
|
|
442
|
+
{ rel: ".githooks/post-commit", content: changelogHook, exec: true },
|
|
443
|
+
{ rel: "tools/changelog/update_changelog.py", content: changelogScript, exec: true }
|
|
444
|
+
];
|
|
445
|
+
var ENSURE = [
|
|
446
|
+
{ rel: "templates/.gitkeep", content: () => "" },
|
|
447
|
+
{ rel: "tools/changelog/.env.example", content: changelogEnvExample },
|
|
448
|
+
{ rel: ".vscode/settings.json", content: vscodeSettings }
|
|
449
|
+
];
|
|
450
|
+
var NON_TEMPLATE_DIRS = /* @__PURE__ */ new Set(["templates", "tools", "node_modules", "dist"]);
|
|
451
|
+
async function migrateLegacyTemplates(hubDir) {
|
|
452
|
+
const moved = [];
|
|
453
|
+
const skipped = [];
|
|
454
|
+
const entries = await fse2.readdir(hubDir, { withFileTypes: true });
|
|
455
|
+
for (const entry of entries) {
|
|
456
|
+
if (!entry.isDirectory()) continue;
|
|
457
|
+
if (entry.name.startsWith(".")) continue;
|
|
458
|
+
if (NON_TEMPLATE_DIRS.has(entry.name)) continue;
|
|
459
|
+
const dest = join2(hubDir, "templates", entry.name);
|
|
460
|
+
if (await fse2.pathExists(dest)) {
|
|
461
|
+
skipped.push(`! templates/${entry.name} (already exists \u2014 left at root)`);
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
await fse2.move(join2(hubDir, entry.name), dest);
|
|
465
|
+
moved.push(`\u2192 templates/${entry.name}`);
|
|
466
|
+
}
|
|
467
|
+
return { moved, skipped };
|
|
468
|
+
}
|
|
469
|
+
async function writeFile(hubDir, file) {
|
|
470
|
+
const fullPath = join2(hubDir, file.rel);
|
|
471
|
+
await fse2.ensureDir(join2(fullPath, ".."));
|
|
472
|
+
await fse2.writeFile(fullPath, file.content(), "utf8");
|
|
473
|
+
if (file.exec) await fse2.chmod(fullPath, 493);
|
|
474
|
+
}
|
|
475
|
+
async function syncHubFiles(hubDir, opts) {
|
|
476
|
+
const written = [];
|
|
477
|
+
for (const file of MANAGED) {
|
|
478
|
+
const exists = await fse2.pathExists(join2(hubDir, file.rel));
|
|
479
|
+
if (exists && !opts.force) continue;
|
|
480
|
+
await writeFile(hubDir, file);
|
|
481
|
+
written.push(`${exists ? "~" : "+"} ${file.rel}`);
|
|
482
|
+
}
|
|
483
|
+
for (const file of ENSURE) {
|
|
484
|
+
if (await fse2.pathExists(join2(hubDir, file.rel))) continue;
|
|
485
|
+
await writeFile(hubDir, file);
|
|
486
|
+
written.push(`+ ${file.rel}`);
|
|
487
|
+
}
|
|
488
|
+
const gitignorePath = join2(hubDir, ".gitignore");
|
|
489
|
+
if (!await fse2.pathExists(gitignorePath)) {
|
|
490
|
+
await fse2.writeFile(gitignorePath, REQUIRED_IGNORES.join("\n") + "\n", "utf8");
|
|
491
|
+
written.push("+ .gitignore");
|
|
492
|
+
} else {
|
|
493
|
+
const existing = await fse2.readFile(gitignorePath, "utf8");
|
|
494
|
+
const missing = REQUIRED_IGNORES.filter((e) => !existing.includes(e));
|
|
495
|
+
if (missing.length) {
|
|
496
|
+
await fse2.appendFile(gitignorePath, missing.join("\n") + "\n");
|
|
497
|
+
written.push(`~ .gitignore (added: ${missing.join(", ")})`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return { written };
|
|
501
|
+
}
|
|
502
|
+
function setHooksPath(hubDir) {
|
|
503
|
+
execSync2("git config core.hooksPath .githooks", { cwd: hubDir, stdio: "ignore" });
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/repair.ts
|
|
507
|
+
function repoRoot() {
|
|
508
|
+
try {
|
|
509
|
+
return execSync3("git rev-parse --show-toplevel", {
|
|
510
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
511
|
+
}).toString().trim();
|
|
512
|
+
} catch {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
async function runRepair() {
|
|
517
|
+
console.log(kleur2.bold().cyan("\n \u25C6 dokio repair\n"));
|
|
518
|
+
const hubDir = repoRoot();
|
|
519
|
+
if (!hubDir) {
|
|
520
|
+
console.error(kleur2.red(" Not a git repository.\n"));
|
|
521
|
+
console.error(kleur2.dim(" Run this from inside a cloned hub repo (e.g. bupa-sam-templates/).\n"));
|
|
522
|
+
process.exit(1);
|
|
523
|
+
}
|
|
524
|
+
const { moved, skipped } = await migrateLegacyTemplates(hubDir);
|
|
525
|
+
if (moved.length) {
|
|
526
|
+
console.log(kleur2.dim(` Migrating ${moved.length} root template${moved.length === 1 ? "" : "s"} into templates/`));
|
|
527
|
+
for (const line of moved) console.log(kleur2.dim(` ${line}`));
|
|
528
|
+
}
|
|
529
|
+
for (const line of skipped) console.log(kleur2.yellow(` ${line}`));
|
|
530
|
+
const { written } = await syncHubFiles(hubDir, { force: true });
|
|
531
|
+
setHooksPath(hubDir);
|
|
532
|
+
for (const line of written) console.log(kleur2.dim(` ${line}`));
|
|
533
|
+
console.log(kleur2.dim(" \u2713 git config core.hooksPath .githooks"));
|
|
534
|
+
console.log(kleur2.green(`
|
|
535
|
+
\u2713 Repaired ${kleur2.bold(basename(hubDir))}
|
|
536
|
+
`));
|
|
537
|
+
console.log(kleur2.dim(" Git hooks are now active for this clone:"));
|
|
538
|
+
console.log(kleur2.dim(" \u2022 commit-msg \u2192 enforces Conventional Commits"));
|
|
539
|
+
console.log(kleur2.dim(" \u2022 post-commit \u2192 auto-updates template CHANGELOG.md"));
|
|
540
|
+
console.log("");
|
|
541
|
+
console.log(kleur2.dim(" For AI changelog descriptions (optional):"));
|
|
542
|
+
console.log(kleur2.dim(" cp tools/changelog/.env.example tools/changelog/.env"));
|
|
543
|
+
console.log(kleur2.dim(" # add ANTHROPIC_API_KEY to tools/changelog/.env"));
|
|
544
|
+
console.log("");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/template.ts
|
|
548
|
+
import { join as join5 } from "path";
|
|
549
|
+
import fse5 from "fs-extra";
|
|
550
|
+
import kleur6 from "kleur";
|
|
551
|
+
|
|
552
|
+
// src/prompts.ts
|
|
553
|
+
import prompts2 from "prompts";
|
|
554
|
+
import kleur3 from "kleur";
|
|
555
|
+
|
|
556
|
+
// src/utils.ts
|
|
557
|
+
function toKebab(str) {
|
|
558
|
+
return str.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
559
|
+
}
|
|
560
|
+
function getPageName(index, total) {
|
|
561
|
+
if (total === 1) return "page1";
|
|
562
|
+
if (index === 0) return "page1-cover";
|
|
563
|
+
if (index === total - 1) return `page${index + 1}-cta`;
|
|
564
|
+
return `page${index + 1}-content`;
|
|
565
|
+
}
|
|
566
|
+
function buildPages(pageCount) {
|
|
567
|
+
return Array.from({ length: pageCount }, (_, i) => getPageName(i, pageCount));
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// src/hubs.ts
|
|
571
|
+
var HUBS = [
|
|
572
|
+
{ id: "aexp", title: "Amex Global" },
|
|
573
|
+
{ id: "amazon-devices", title: "Amazon Devices" },
|
|
574
|
+
{ id: "australian-unity", title: "Australian Unity" },
|
|
575
|
+
{ id: "belmond", title: "Belmond" },
|
|
576
|
+
{ id: "bupa-aged-care", title: "Bupa Aged Care" },
|
|
577
|
+
{ id: "bupa-healthy-cities", title: "Bupa Healthy Cities" },
|
|
578
|
+
{ id: "bupa-hs", title: "Bupa Health Services" },
|
|
579
|
+
{ id: "bupa-marketing", title: "Bupa Marketing" },
|
|
580
|
+
{ id: "bupa-retail", title: "Bupa Retail" },
|
|
581
|
+
{ id: "bupa-sam", title: "Bupa Sales And Marketing Hub" },
|
|
582
|
+
{ id: "designsystem", title: "Design System" },
|
|
583
|
+
{ id: "eabrandhub", title: "EnergyAustralia's Brand Hub" },
|
|
584
|
+
{ id: "fridas", title: "Frida's Luxe Sip n' Paint" },
|
|
585
|
+
{ id: "gwm", title: "GWM Advertising Studio" },
|
|
586
|
+
{ id: "headspace", title: "Headspace" },
|
|
587
|
+
{ id: "hellofresh", title: "HelloFresh" },
|
|
588
|
+
{ id: "ipa", title: "IPA LAM Hub" },
|
|
589
|
+
{ id: "iris-samsung", title: "Iris | Samsung" },
|
|
590
|
+
{ id: "knowledgebase", title: "KB Hub" },
|
|
591
|
+
{ id: "mi", title: "Measurable Impact" },
|
|
592
|
+
{ id: "meridianenergy", title: "Meridian Energy Hub" },
|
|
593
|
+
{ id: "nissan", title: "Nissan" },
|
|
594
|
+
{ id: "origin", title: "Origin" },
|
|
595
|
+
{ id: "origin-fugu", title: "Origin Fugu" },
|
|
596
|
+
{ id: "originloopvpp-partnerships-hub", title: "Origin Loop VPP Partnerships Hub" },
|
|
597
|
+
{ id: "poolwerx", title: "Poolwerx Loop" },
|
|
598
|
+
{ id: "sandbox", title: "Sandbox" },
|
|
599
|
+
{ id: "scimmer", title: "Scimmer" },
|
|
600
|
+
{ id: "shell-au", title: "Shell Australia Brand Templates" },
|
|
601
|
+
{ id: "vidacorp", title: "VidaDesign Hub" }
|
|
602
|
+
];
|
|
603
|
+
function hubRepoUrl(hubId) {
|
|
604
|
+
return `https://github.com/dokioco/${hubId}-templates`;
|
|
605
|
+
}
|
|
606
|
+
function hubRepoDirName(hubId) {
|
|
607
|
+
return `${hubId}-templates`;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/prompts.ts
|
|
611
|
+
var onCancel2 = () => {
|
|
612
|
+
console.log(kleur3.yellow("\n Cancelled.\n"));
|
|
613
|
+
process.exit(0);
|
|
614
|
+
};
|
|
615
|
+
async function runPrompts(nameArg) {
|
|
616
|
+
const base = await prompts2(
|
|
617
|
+
[
|
|
618
|
+
{
|
|
619
|
+
type: "text",
|
|
620
|
+
name: "templateId",
|
|
621
|
+
message: "Template ID (e.g. HW485)",
|
|
622
|
+
validate: (v) => /^[A-Za-z0-9]+$/.test(v.trim()) || "Alphanumeric only"
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
type: nameArg ? null : "text",
|
|
626
|
+
name: "name",
|
|
627
|
+
message: "Template name (e.g. My Cool Template)",
|
|
628
|
+
initial: "My Template",
|
|
629
|
+
validate: (v) => v.trim().length > 0 || "Required"
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
type: "select",
|
|
633
|
+
name: "mode",
|
|
634
|
+
message: "Template type",
|
|
635
|
+
choices: [
|
|
636
|
+
{ title: "PDF", value: "pdf" },
|
|
637
|
+
{ title: "General (image/JPG/PNG)", value: "general" },
|
|
638
|
+
{ title: kleur3.dim("Video [WIP]"), value: "video" },
|
|
639
|
+
{ title: "Email", value: "email" }
|
|
640
|
+
]
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
type: "select",
|
|
644
|
+
name: "hubId",
|
|
645
|
+
message: "Hub",
|
|
646
|
+
choices: HUBS.map((h) => ({ title: h.title, value: h.id }))
|
|
647
|
+
}
|
|
648
|
+
],
|
|
649
|
+
{ onCancel: onCancel2 }
|
|
650
|
+
);
|
|
651
|
+
const templateId = base.templateId.trim();
|
|
652
|
+
const name = nameArg ? nameArg.trim() : base.name.trim();
|
|
653
|
+
const fullName = `${templateId}-${toKebab(name)}`;
|
|
654
|
+
const hubId = base.hubId;
|
|
655
|
+
const subdomain = hubId;
|
|
656
|
+
if (base.mode === "pdf") {
|
|
657
|
+
const pdf = await prompts2(
|
|
658
|
+
[
|
|
659
|
+
{ type: "number", name: "width", message: "Page width (mm)", initial: 210, min: 1 },
|
|
660
|
+
{ type: "number", name: "height", message: "Page height (mm)", initial: 297, min: 1 },
|
|
661
|
+
{ type: "number", name: "pageCount", message: "Number of pages", initial: 1, min: 1 },
|
|
662
|
+
{
|
|
663
|
+
type: "select",
|
|
664
|
+
name: "princeVersion",
|
|
665
|
+
message: "PrinceXML version",
|
|
666
|
+
choices: [
|
|
667
|
+
{ title: "15 (flexbox support \u2014 recommended)", value: 15 },
|
|
668
|
+
{ title: "11 (legacy)", value: 11 }
|
|
669
|
+
]
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
type: "toggle",
|
|
673
|
+
name: "resizable",
|
|
674
|
+
message: "Resizable template?",
|
|
675
|
+
initial: false,
|
|
676
|
+
active: "yes",
|
|
677
|
+
inactive: "no"
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
type: "toggle",
|
|
681
|
+
name: "proofable",
|
|
682
|
+
message: "Enable proof downloads (draft_proofable)?",
|
|
683
|
+
initial: true,
|
|
684
|
+
active: "yes",
|
|
685
|
+
inactive: "no"
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
type: "toggle",
|
|
689
|
+
name: "orderable",
|
|
690
|
+
message: "Include orderable export (Print from Snap)?",
|
|
691
|
+
initial: false,
|
|
692
|
+
active: "yes",
|
|
693
|
+
inactive: "no"
|
|
694
|
+
}
|
|
695
|
+
],
|
|
696
|
+
{ onCancel: onCancel2 }
|
|
697
|
+
);
|
|
698
|
+
return {
|
|
699
|
+
mode: "pdf",
|
|
700
|
+
templateId,
|
|
701
|
+
name,
|
|
702
|
+
fullName,
|
|
703
|
+
subdomain,
|
|
704
|
+
hubId,
|
|
705
|
+
width: pdf.width,
|
|
706
|
+
height: pdf.height,
|
|
707
|
+
pageCount: pdf.pageCount,
|
|
708
|
+
princeVersion: pdf.princeVersion,
|
|
709
|
+
resizable: pdf.resizable,
|
|
710
|
+
proofable: pdf.proofable,
|
|
711
|
+
orderable: pdf.orderable
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
if (base.mode === "general") {
|
|
715
|
+
const gen = await prompts2(
|
|
716
|
+
[
|
|
717
|
+
{ type: "number", name: "width", message: "Width (px)", initial: 400, min: 1 },
|
|
718
|
+
{ type: "number", name: "height", message: "Height (px)", initial: 400, min: 1 },
|
|
719
|
+
{
|
|
720
|
+
type: "number",
|
|
721
|
+
name: "pageCount",
|
|
722
|
+
message: "Number of pages / sections",
|
|
723
|
+
initial: 1,
|
|
724
|
+
min: 1
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
type: "multiselect",
|
|
728
|
+
name: "exportFormats",
|
|
729
|
+
message: "Export formats",
|
|
730
|
+
choices: [
|
|
731
|
+
{ title: "JPG", value: "jpg", selected: true },
|
|
732
|
+
{ title: "PNG", value: "png", selected: true }
|
|
733
|
+
],
|
|
734
|
+
min: 1
|
|
735
|
+
}
|
|
736
|
+
],
|
|
737
|
+
{ onCancel: onCancel2 }
|
|
738
|
+
);
|
|
739
|
+
return {
|
|
740
|
+
mode: "general",
|
|
741
|
+
templateId,
|
|
742
|
+
name,
|
|
743
|
+
fullName,
|
|
744
|
+
subdomain,
|
|
745
|
+
hubId,
|
|
746
|
+
width: gen.width,
|
|
747
|
+
height: gen.height,
|
|
748
|
+
pageCount: gen.pageCount,
|
|
749
|
+
exportFormats: gen.exportFormats
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
if (base.mode === "video") {
|
|
753
|
+
return { mode: "video", templateId, name, fullName, subdomain, hubId };
|
|
754
|
+
}
|
|
755
|
+
return { mode: "email", templateId, name, fullName, subdomain, hubId };
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// src/templates/pdf.ts
|
|
759
|
+
function pdfFiles(config, pages) {
|
|
760
|
+
return {
|
|
761
|
+
"CHANGELOG.md": changelog(`${config.templateId} - ${config.name}`),
|
|
762
|
+
"data.yaml": pdfYaml(config),
|
|
763
|
+
"index.html": pdfHtml(pages),
|
|
764
|
+
"assets/.gitkeep": gitkeep(),
|
|
765
|
+
...pdfPartials(pages),
|
|
766
|
+
"scss/style.scss.hbs": pdfStyleSass(config, pages),
|
|
767
|
+
"scss/_fonts.scss": fontsScssEmpty(),
|
|
768
|
+
"scss/_variables.scss": pdfVariables(config),
|
|
769
|
+
"scss/_mixins.scss": mixinsScss(),
|
|
770
|
+
...pdfPageScss(pages)
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function pdfYaml(config) {
|
|
774
|
+
const { templateId, name, subdomain, width, height, pageCount, princeVersion, resizable, proofable, orderable } = config;
|
|
775
|
+
const lines = [
|
|
776
|
+
`name: ${templateId} - ${name}`,
|
|
777
|
+
`mode: pdf`,
|
|
778
|
+
`prince_version: ${princeVersion}`,
|
|
779
|
+
`status: 0`,
|
|
780
|
+
`subdomain: ${subdomain}`,
|
|
781
|
+
`dimension_mode: mm`,
|
|
782
|
+
`page_count: ${pageCount}`,
|
|
783
|
+
`dimension_width: ${width}`,
|
|
784
|
+
`dimension_height: ${height}`,
|
|
785
|
+
`compositing_data: {}`
|
|
786
|
+
];
|
|
787
|
+
if (proofable) lines.push(`draft_proofable: true`);
|
|
788
|
+
if (resizable) lines.push(`resizable: true`);
|
|
789
|
+
lines.push(`export_options:`);
|
|
790
|
+
if (orderable) {
|
|
791
|
+
lines.push(
|
|
792
|
+
` orderable:`,
|
|
793
|
+
` title: Print order`,
|
|
794
|
+
` description: Order from Snap`,
|
|
795
|
+
` description_complete: Ordered from Snap`,
|
|
796
|
+
` title_download: Download for your own printer`,
|
|
797
|
+
` description_download: Download for your own printer`,
|
|
798
|
+
` description_download_complete: Downloaded for your own printer`,
|
|
799
|
+
` marks: true`,
|
|
800
|
+
` bleed: true`,
|
|
801
|
+
` supplier_choice: true`
|
|
802
|
+
);
|
|
803
|
+
}
|
|
373
804
|
lines.push(
|
|
374
805
|
` downloadables:`,
|
|
375
806
|
` hi_res:`,
|
|
@@ -802,376 +1233,109 @@ function emailHtml() {
|
|
|
802
1233
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
803
1234
|
<title>{{{subject}}}</title>
|
|
804
1235
|
<style type="text/css">{{{___assembled_css}}}</style>
|
|
805
|
-
</head>
|
|
806
|
-
<body style="margin: 0 0 0 0; padding: 0 0 0 0;" bgcolor="#ededed">
|
|
807
|
-
<table role="none" width="100%" cellpadding="0" cellspacing="0" border="0">
|
|
808
|
-
<tr>
|
|
809
|
-
<td>
|
|
810
|
-
<center>
|
|
811
|
-
<table role="none" width="600" class="full-wd" cellpadding="0" cellspacing="0" border="0">
|
|
812
|
-
<tr>
|
|
813
|
-
<td>
|
|
814
|
-
{{! TODO: Email content here }}
|
|
815
|
-
</td>
|
|
816
|
-
</tr>
|
|
817
|
-
</table>
|
|
818
|
-
</center>
|
|
819
|
-
</td>
|
|
820
|
-
</tr>
|
|
821
|
-
</table>
|
|
822
|
-
</body>
|
|
823
|
-
</html>
|
|
824
|
-
`;
|
|
825
|
-
}
|
|
826
|
-
function emailStyleSass() {
|
|
827
|
-
return `@import 'fonts';
|
|
828
|
-
@import 'variables';
|
|
829
|
-
@import 'mixins';
|
|
830
|
-
@import 'pages/page1';
|
|
831
|
-
|
|
832
|
-
// Use hex/rgb only \u2014 no CMYK in email
|
|
833
|
-
|
|
834
|
-
* {
|
|
835
|
-
box-sizing: border-box;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
body {
|
|
839
|
-
margin: 0;
|
|
840
|
-
padding: 0;
|
|
841
|
-
background-color: #f4f4f4;
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
img {
|
|
845
|
-
display: block;
|
|
846
|
-
border: 0;
|
|
847
|
-
outline: none;
|
|
848
|
-
text-decoration: none;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
@media screen and (max-width: 599px) {
|
|
852
|
-
.mobileOff {
|
|
853
|
-
width: 0px !important;
|
|
854
|
-
display: none !important;
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
`;
|
|
858
|
-
}
|
|
859
|
-
function emailVariables() {
|
|
860
|
-
return `$email-width: 600px;
|
|
861
|
-
|
|
862
|
-
// Use hex/rgb only (no CMYK)
|
|
863
|
-
$font-primary: 'Montserrat', Arial, sans-serif;
|
|
864
|
-
|
|
865
|
-
$color-primary: #000000;
|
|
866
|
-
$color-secondary: #ffffff;
|
|
867
|
-
$color-background: #f4f4f4;
|
|
868
|
-
$color-accent: #0079C8;
|
|
869
|
-
`;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
// src/files.ts
|
|
873
|
-
function buildFiles(config) {
|
|
874
|
-
if (config.mode === "pdf") return pdfFiles(config, buildPages(config.pageCount));
|
|
875
|
-
if (config.mode === "general") return generalFiles(config, buildPages(config.pageCount));
|
|
876
|
-
if (config.mode === "video") return videoFiles(config);
|
|
877
|
-
return emailFiles(config);
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// src/git.ts
|
|
881
|
-
import { join as join2, basename } from "path";
|
|
882
|
-
import { execSync as execSync2 } from "child_process";
|
|
883
|
-
import fse2 from "fs-extra";
|
|
884
|
-
import kleur2 from "kleur";
|
|
885
|
-
|
|
886
|
-
// src/hubSetup.ts
|
|
887
|
-
import { join } from "path";
|
|
888
|
-
import { execSync } from "child_process";
|
|
889
|
-
import fse from "fs-extra";
|
|
890
|
-
|
|
891
|
-
// src/templates/changelog.ts
|
|
892
|
-
function changelogScript() {
|
|
893
|
-
return `#!/usr/bin/env python3
|
|
894
|
-
"""Prepend a new entry into a template's CHANGELOG.md."""
|
|
895
|
-
import sys
|
|
896
|
-
import re
|
|
897
|
-
import os
|
|
898
|
-
import datetime
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
def load_dotenv():
|
|
902
|
-
env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")
|
|
903
|
-
if not os.path.isfile(env_path):
|
|
904
|
-
return
|
|
905
|
-
with open(env_path) as f:
|
|
906
|
-
for line in f:
|
|
907
|
-
line = line.strip()
|
|
908
|
-
if not line or line.startswith("#") or "=" not in line:
|
|
909
|
-
continue
|
|
910
|
-
key, _, val = line.partition("=")
|
|
911
|
-
key = key.strip()
|
|
912
|
-
val = val.strip().strip('"').strip("'")
|
|
913
|
-
if key and key not in os.environ:
|
|
914
|
-
os.environ[key] = val
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
load_dotenv()
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
STATUS_LABELS = {
|
|
921
|
-
"A": "Added",
|
|
922
|
-
"M": "Modified",
|
|
923
|
-
"D": "Deleted",
|
|
924
|
-
"R": "Renamed",
|
|
925
|
-
"C": "Copied",
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
def format_date(today_str):
|
|
930
|
-
d = datetime.datetime.strptime(today_str, "%Y-%m-%d")
|
|
931
|
-
day = str(int(d.strftime("%d")))
|
|
932
|
-
return d.strftime(f"%B {day}, %Y")
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
def generate_description(title, files, diff):
|
|
936
|
-
try:
|
|
937
|
-
import anthropic
|
|
938
|
-
except ImportError:
|
|
939
|
-
return None
|
|
940
|
-
|
|
941
|
-
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
|
942
|
-
if not api_key:
|
|
943
|
-
return None
|
|
944
|
-
|
|
945
|
-
try:
|
|
946
|
-
client = anthropic.Anthropic(api_key=api_key)
|
|
947
|
-
files_str = "\\n".join(f"- {label}: {path}" for label, path in files)
|
|
948
|
-
diff_section = f"\\n\\nDiff:\\n\`\`\`\\n{diff[:4000]}\\n\`\`\`" if diff.strip() else ""
|
|
949
|
-
|
|
950
|
-
prompt = (
|
|
951
|
-
f"Commit: {title}\\n"
|
|
952
|
-
f"Files:\\n{files_str}"
|
|
953
|
-
f"{diff_section}\\n\\n"
|
|
954
|
-
f"Write one sentence describing what changed. Be direct and specific. No filler words."
|
|
955
|
-
)
|
|
956
|
-
|
|
957
|
-
message = client.messages.create(
|
|
958
|
-
model="claude-haiku-4-5-20251001",
|
|
959
|
-
max_tokens=256,
|
|
960
|
-
messages=[{"role": "user", "content": prompt}],
|
|
961
|
-
)
|
|
962
|
-
return message.content[0].text.strip()
|
|
963
|
-
except Exception:
|
|
964
|
-
return None
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
def parse_files(files_raw, template_prefix):
|
|
968
|
-
files = []
|
|
969
|
-
for line in files_raw.strip().splitlines():
|
|
970
|
-
line = line.strip()
|
|
971
|
-
if not line:
|
|
972
|
-
continue
|
|
973
|
-
parts = line.split("\\t")
|
|
974
|
-
if len(parts) >= 2:
|
|
975
|
-
status_key = parts[0].strip()[0].upper()
|
|
976
|
-
label = STATUS_LABELS.get(status_key, "Modified")
|
|
977
|
-
path = parts[-1].strip()
|
|
978
|
-
else:
|
|
979
|
-
label = "Modified"
|
|
980
|
-
path = parts[0].strip()
|
|
981
|
-
if template_prefix and path.startswith(template_prefix + "/"):
|
|
982
|
-
path = path[len(template_prefix):].lstrip("/")
|
|
983
|
-
files.append((label, path))
|
|
984
|
-
return files
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
def build_entry_content(branch, title, description, author, today_str, files):
|
|
988
|
-
date_str = format_date(today_str)
|
|
989
|
-
lines = [
|
|
990
|
-
f"**{author}** \u2022 *{date_str}*",
|
|
991
|
-
f"**branch:** \`{branch}\`",
|
|
992
|
-
"",
|
|
993
|
-
f"**title:** {title}",
|
|
994
|
-
"",
|
|
995
|
-
f"**description:** {description}",
|
|
996
|
-
"",
|
|
997
|
-
"### Files Changed",
|
|
998
|
-
]
|
|
999
|
-
for label, path in files:
|
|
1000
|
-
lines.append(f"- **{label}** \`{path}\`")
|
|
1001
|
-
return "\\n".join(lines)
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
def update_changelog(path, branch, today, title, author, files, diff):
|
|
1005
|
-
with open(path, "r") as f:
|
|
1006
|
-
content = f.read()
|
|
1007
|
-
|
|
1008
|
-
description = generate_description(title, files, diff) or "TODO - INTEGRATE WITH AI (JAKE TASK)"
|
|
1009
|
-
entry_content = build_entry_content(branch, title, description, author, today, files)
|
|
1010
|
-
|
|
1011
|
-
insert_match = re.search(r"^(---|## )", content, re.MULTILINE)
|
|
1012
|
-
|
|
1013
|
-
if insert_match:
|
|
1014
|
-
insert_pos = insert_match.start()
|
|
1015
|
-
if content[insert_pos:].startswith("---"):
|
|
1016
|
-
new_block = f"---\\n\\n{entry_content}\\n\\n"
|
|
1017
|
-
else:
|
|
1018
|
-
new_block = f"---\\n\\n{entry_content}\\n\\n---\\n\\n"
|
|
1019
|
-
content = content[:insert_pos] + new_block + content[insert_pos:]
|
|
1020
|
-
else:
|
|
1021
|
-
content = content.rstrip("\\n") + f"\\n\\n---\\n\\n{entry_content}\\n\\n---\\n"
|
|
1022
|
-
|
|
1023
|
-
with open(path, "w") as f:
|
|
1024
|
-
f.write(content)
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
if __name__ == "__main__":
|
|
1028
|
-
path = sys.argv[1]
|
|
1029
|
-
today = sys.argv[2]
|
|
1030
|
-
branch = os.environ.get("CHANGELOG_BRANCH", "")
|
|
1031
|
-
message = os.environ.get("CHANGELOG_MESSAGE", "")
|
|
1032
|
-
author = os.environ.get("CHANGELOG_AUTHOR", "")
|
|
1033
|
-
files_raw = os.environ.get("CHANGELOG_FILES", "")
|
|
1034
|
-
template_prefix = os.environ.get("CHANGELOG_TEMPLATE_PREFIX", "")
|
|
1035
|
-
diff = os.environ.get("CHANGELOG_DIFF", "")
|
|
1036
|
-
|
|
1037
|
-
files = parse_files(files_raw, template_prefix)
|
|
1038
|
-
update_changelog(path, branch, today, message, author, files, diff)
|
|
1236
|
+
</head>
|
|
1237
|
+
<body style="margin: 0 0 0 0; padding: 0 0 0 0;" bgcolor="#ededed">
|
|
1238
|
+
<table role="none" width="100%" cellpadding="0" cellspacing="0" border="0">
|
|
1239
|
+
<tr>
|
|
1240
|
+
<td>
|
|
1241
|
+
<center>
|
|
1242
|
+
<table role="none" width="600" class="full-wd" cellpadding="0" cellspacing="0" border="0">
|
|
1243
|
+
<tr>
|
|
1244
|
+
<td>
|
|
1245
|
+
{{! TODO: Email content here }}
|
|
1246
|
+
</td>
|
|
1247
|
+
</tr>
|
|
1248
|
+
</table>
|
|
1249
|
+
</center>
|
|
1250
|
+
</td>
|
|
1251
|
+
</tr>
|
|
1252
|
+
</table>
|
|
1253
|
+
</body>
|
|
1254
|
+
</html>
|
|
1039
1255
|
`;
|
|
1040
1256
|
}
|
|
1041
|
-
function
|
|
1042
|
-
return
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
REPO_ROOT=$(git rev-parse --show-toplevel)
|
|
1047
|
-
LOCK="$REPO_ROOT/.git/changelog-hook-running"
|
|
1048
|
-
|
|
1049
|
-
[ -f "$LOCK" ] && exit 0
|
|
1050
|
-
|
|
1051
|
-
CHANGED=$(git diff-tree --no-commit-id -r --name-only HEAD | grep -v "CHANGELOG\\.md$")
|
|
1052
|
-
[ -z "$CHANGED" ] && exit 0
|
|
1053
|
-
|
|
1054
|
-
# Detect changed templates (expects templates/<template-id>/ structure)
|
|
1055
|
-
TEMPLATES=$(echo "$CHANGED" | grep "^templates/" | awk -F'/' '{print $1"/"$2}' | sort -u)
|
|
1056
|
-
[ -z "$TEMPLATES" ] && exit 0
|
|
1057
|
-
|
|
1058
|
-
COMMIT_MSG=$(git log -1 --pretty=%s)
|
|
1059
|
-
AUTHOR=$(git log -1 --pretty="%an")
|
|
1060
|
-
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
1061
|
-
TODAY=$(date +%Y-%m-%d)
|
|
1062
|
-
|
|
1063
|
-
touch "$LOCK"
|
|
1064
|
-
trap 'rm -f "$LOCK"' EXIT
|
|
1065
|
-
|
|
1066
|
-
AMENDED=0
|
|
1067
|
-
while IFS= read -r TEMPLATE; do
|
|
1068
|
-
[ -z "$TEMPLATE" ] && continue
|
|
1069
|
-
CHANGELOG="$REPO_ROOT/$TEMPLATE/CHANGELOG.md"
|
|
1070
|
-
[ ! -f "$CHANGELOG" ] && continue
|
|
1257
|
+
function emailStyleSass() {
|
|
1258
|
+
return `@import 'fonts';
|
|
1259
|
+
@import 'variables';
|
|
1260
|
+
@import 'mixins';
|
|
1261
|
+
@import 'pages/page1';
|
|
1071
1262
|
|
|
1072
|
-
|
|
1073
|
-
| grep -v "CHANGELOG\\.md$" \\
|
|
1074
|
-
| grep -E $'\\t'"$TEMPLATE/")
|
|
1263
|
+
// Use hex/rgb only \u2014 no CMYK in email
|
|
1075
1264
|
|
|
1076
|
-
|
|
1265
|
+
* {
|
|
1266
|
+
box-sizing: border-box;
|
|
1267
|
+
}
|
|
1077
1268
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
export CHANGELOG_DIFF="$TEMPLATE_DIFF"
|
|
1084
|
-
python3 "$REPO_ROOT/tools/changelog/update_changelog.py" "$CHANGELOG" "$TODAY"
|
|
1269
|
+
body {
|
|
1270
|
+
margin: 0;
|
|
1271
|
+
padding: 0;
|
|
1272
|
+
background-color: #f4f4f4;
|
|
1273
|
+
}
|
|
1085
1274
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1275
|
+
img {
|
|
1276
|
+
display: block;
|
|
1277
|
+
border: 0;
|
|
1278
|
+
outline: none;
|
|
1279
|
+
text-decoration: none;
|
|
1280
|
+
}
|
|
1089
1281
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1282
|
+
@media screen and (max-width: 599px) {
|
|
1283
|
+
.mobileOff {
|
|
1284
|
+
width: 0px !important;
|
|
1285
|
+
display: none !important;
|
|
1286
|
+
}
|
|
1094
1287
|
}
|
|
1095
|
-
function changelogEnvExample() {
|
|
1096
|
-
return `ANTHROPIC_API_KEY=sk-ant-...
|
|
1097
1288
|
`;
|
|
1098
1289
|
}
|
|
1290
|
+
function emailVariables() {
|
|
1291
|
+
return `$email-width: 600px;
|
|
1099
1292
|
|
|
1100
|
-
//
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
{ rel: "tools/changelog/update_changelog.py", content: changelogScript, exec: true }
|
|
1109
|
-
];
|
|
1110
|
-
var ENSURE = [
|
|
1111
|
-
{ rel: "tools/changelog/.env.example", content: changelogEnvExample },
|
|
1112
|
-
{ rel: ".vscode/settings.json", content: vscodeSettings }
|
|
1113
|
-
];
|
|
1114
|
-
async function writeFile(hubDir, file) {
|
|
1115
|
-
const fullPath = join(hubDir, file.rel);
|
|
1116
|
-
await fse.ensureDir(join(fullPath, ".."));
|
|
1117
|
-
await fse.writeFile(fullPath, file.content(), "utf8");
|
|
1118
|
-
if (file.exec) await fse.chmod(fullPath, 493);
|
|
1119
|
-
}
|
|
1120
|
-
async function syncHubFiles(hubDir, opts) {
|
|
1121
|
-
const written = [];
|
|
1122
|
-
for (const file of MANAGED) {
|
|
1123
|
-
const exists = await fse.pathExists(join(hubDir, file.rel));
|
|
1124
|
-
if (exists && !opts.force) continue;
|
|
1125
|
-
await writeFile(hubDir, file);
|
|
1126
|
-
written.push(`${exists ? "~" : "+"} ${file.rel}`);
|
|
1127
|
-
}
|
|
1128
|
-
for (const file of ENSURE) {
|
|
1129
|
-
if (await fse.pathExists(join(hubDir, file.rel))) continue;
|
|
1130
|
-
await writeFile(hubDir, file);
|
|
1131
|
-
written.push(`+ ${file.rel}`);
|
|
1132
|
-
}
|
|
1133
|
-
const gitignorePath = join(hubDir, ".gitignore");
|
|
1134
|
-
if (!await fse.pathExists(gitignorePath)) {
|
|
1135
|
-
await fse.writeFile(gitignorePath, REQUIRED_IGNORES.join("\n") + "\n", "utf8");
|
|
1136
|
-
written.push("+ .gitignore");
|
|
1137
|
-
} else {
|
|
1138
|
-
const existing = await fse.readFile(gitignorePath, "utf8");
|
|
1139
|
-
const missing = REQUIRED_IGNORES.filter((e) => !existing.includes(e));
|
|
1140
|
-
if (missing.length) {
|
|
1141
|
-
await fse.appendFile(gitignorePath, missing.join("\n") + "\n");
|
|
1142
|
-
written.push(`~ .gitignore (added: ${missing.join(", ")})`);
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
return { written };
|
|
1293
|
+
// Use hex/rgb only (no CMYK)
|
|
1294
|
+
$font-primary: 'Montserrat', Arial, sans-serif;
|
|
1295
|
+
|
|
1296
|
+
$color-primary: #000000;
|
|
1297
|
+
$color-secondary: #ffffff;
|
|
1298
|
+
$color-background: #f4f4f4;
|
|
1299
|
+
$color-accent: #0079C8;
|
|
1300
|
+
`;
|
|
1146
1301
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1302
|
+
|
|
1303
|
+
// src/files.ts
|
|
1304
|
+
function buildFiles(config) {
|
|
1305
|
+
if (config.mode === "pdf") return pdfFiles(config, buildPages(config.pageCount));
|
|
1306
|
+
if (config.mode === "general") return generalFiles(config, buildPages(config.pageCount));
|
|
1307
|
+
if (config.mode === "video") return videoFiles(config);
|
|
1308
|
+
return emailFiles(config);
|
|
1149
1309
|
}
|
|
1150
1310
|
|
|
1151
1311
|
// src/git.ts
|
|
1312
|
+
import { join as join3, basename as basename2 } from "path";
|
|
1313
|
+
import { execSync as execSync4 } from "child_process";
|
|
1314
|
+
import fse3 from "fs-extra";
|
|
1315
|
+
import kleur4 from "kleur";
|
|
1152
1316
|
async function ensureHubRepo(hubId) {
|
|
1153
1317
|
const cwd = process.cwd();
|
|
1154
1318
|
const dirName = hubRepoDirName(hubId);
|
|
1155
|
-
const alreadyInside =
|
|
1156
|
-
const hubDir = alreadyInside ? cwd :
|
|
1157
|
-
if (alreadyInside || await
|
|
1158
|
-
console.log(
|
|
1319
|
+
const alreadyInside = basename2(cwd) === dirName;
|
|
1320
|
+
const hubDir = alreadyInside ? cwd : join3(cwd, dirName);
|
|
1321
|
+
if (alreadyInside || await fse3.pathExists(hubDir)) {
|
|
1322
|
+
console.log(kleur4.dim(`
|
|
1159
1323
|
\u21BB Pulling latest ${dirName}...`));
|
|
1160
|
-
|
|
1324
|
+
execSync4("git pull", { cwd: hubDir, stdio: "ignore" });
|
|
1161
1325
|
} else {
|
|
1162
|
-
console.log(
|
|
1326
|
+
console.log(kleur4.dim(`
|
|
1163
1327
|
\u2193 Cloning ${hubRepoUrl(hubId)}...`));
|
|
1164
|
-
|
|
1328
|
+
execSync4(`git clone ${hubRepoUrl(hubId)}`, { stdio: "inherit" });
|
|
1165
1329
|
}
|
|
1166
1330
|
return hubDir;
|
|
1167
1331
|
}
|
|
1168
1332
|
async function setupHooks(hubDir) {
|
|
1169
1333
|
const { written } = await syncHubFiles(hubDir, { force: false });
|
|
1170
|
-
for (const line of written) console.log(
|
|
1171
|
-
const readmePath =
|
|
1172
|
-
if (!await
|
|
1173
|
-
const hubName =
|
|
1174
|
-
await
|
|
1334
|
+
for (const line of written) console.log(kleur4.dim(` ${line}`));
|
|
1335
|
+
const readmePath = join3(hubDir, "README.md");
|
|
1336
|
+
if (!await fse3.pathExists(readmePath)) {
|
|
1337
|
+
const hubName = basename2(hubDir);
|
|
1338
|
+
await fse3.writeFile(readmePath, `# ${hubName}
|
|
1175
1339
|
|
|
1176
1340
|
Templates for ${hubName} on Dokio.
|
|
1177
1341
|
|
|
@@ -1189,34 +1353,34 @@ Git cannot enable repo hooks automatically on clone, so every teammate must run
|
|
|
1189
1353
|
|
|
1190
1354
|
Run \`create-dokio template\` from inside this repo.
|
|
1191
1355
|
`, "utf8");
|
|
1192
|
-
console.log(
|
|
1356
|
+
console.log(kleur4.dim(` + README.md`));
|
|
1193
1357
|
}
|
|
1194
1358
|
setHooksPath(hubDir);
|
|
1195
1359
|
}
|
|
1196
1360
|
|
|
1197
1361
|
// src/scaffold.ts
|
|
1198
|
-
import { join as
|
|
1199
|
-
import
|
|
1200
|
-
import
|
|
1362
|
+
import { join as join4 } from "path";
|
|
1363
|
+
import fse4 from "fs-extra";
|
|
1364
|
+
import kleur5 from "kleur";
|
|
1201
1365
|
async function writeFiles(outDir, files, fullName) {
|
|
1202
1366
|
for (const [rel, content] of Object.entries(files)) {
|
|
1203
|
-
const fullPath =
|
|
1204
|
-
await
|
|
1205
|
-
await
|
|
1206
|
-
console.log(
|
|
1367
|
+
const fullPath = join4(outDir, rel);
|
|
1368
|
+
await fse4.ensureDir(join4(fullPath, ".."));
|
|
1369
|
+
await fse4.writeFile(fullPath, content, "utf8");
|
|
1370
|
+
console.log(kleur5.dim(` + ${fullName}/${rel}`));
|
|
1207
1371
|
}
|
|
1208
1372
|
}
|
|
1209
1373
|
|
|
1210
1374
|
// src/template.ts
|
|
1211
1375
|
async function runTemplate(nameArg) {
|
|
1212
|
-
console.log(
|
|
1376
|
+
console.log(kleur6.bold().cyan("\n \u25C6 dokio create template\n"));
|
|
1213
1377
|
const config = await runPrompts(nameArg);
|
|
1214
1378
|
const files = buildFiles(config);
|
|
1215
1379
|
const hubDir = await ensureHubRepo(config.hubId);
|
|
1216
1380
|
const hubDirName = hubRepoDirName(config.hubId);
|
|
1217
|
-
const outDir =
|
|
1218
|
-
if (await
|
|
1219
|
-
console.error(
|
|
1381
|
+
const outDir = join5(hubDir, "templates", config.fullName);
|
|
1382
|
+
if (await fse5.pathExists(outDir)) {
|
|
1383
|
+
console.error(kleur6.red(`
|
|
1220
1384
|
Error: "${config.fullName}" already exists in ${hubDirName}/templates/.
|
|
1221
1385
|
`));
|
|
1222
1386
|
process.exit(1);
|
|
@@ -1225,161 +1389,23 @@ async function runTemplate(nameArg) {
|
|
|
1225
1389
|
await writeFiles(outDir, files, config.fullName);
|
|
1226
1390
|
await setupHooks(hubDir);
|
|
1227
1391
|
const templatePath = `${hubDirName}/templates/${config.fullName}`;
|
|
1228
|
-
console.log(
|
|
1229
|
-
\u2713 Created ${
|
|
1392
|
+
console.log(kleur6.green(`
|
|
1393
|
+
\u2713 Created ${kleur6.bold(templatePath)}
|
|
1230
1394
|
`));
|
|
1231
1395
|
if (config.mode === "video") {
|
|
1232
|
-
console.log(
|
|
1233
|
-
`));
|
|
1234
|
-
}
|
|
1235
|
-
console.log(kleur4.dim(` Next steps:`));
|
|
1236
|
-
console.log(kleur4.dim(` cd ${templatePath}`));
|
|
1237
|
-
console.log(kleur4.dim(` Edit data.yaml \u2014 set your template ID`));
|
|
1238
|
-
console.log(kleur4.dim(` Add assets to assets/`));
|
|
1239
|
-
console.log("");
|
|
1240
|
-
console.log(kleur4.dim(` When ready to commit:`));
|
|
1241
|
-
console.log(kleur4.dim(` cd ../../ (back to ${hubDirName}/)`));
|
|
1242
|
-
console.log(kleur4.dim(` git add templates/${config.fullName}/`));
|
|
1243
|
-
console.log(kleur4.dim(` git commit -m "feat: add ${config.name} template"`));
|
|
1244
|
-
console.log(kleur4.dim(` git push`));
|
|
1245
|
-
console.log("");
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
// src/hub.ts
|
|
1249
|
-
import { join as join5 } from "path";
|
|
1250
|
-
import { execSync as execSync3 } from "child_process";
|
|
1251
|
-
import fse5 from "fs-extra";
|
|
1252
|
-
import kleur5 from "kleur";
|
|
1253
|
-
import prompts2 from "prompts";
|
|
1254
|
-
var onCancel2 = () => {
|
|
1255
|
-
console.log(kleur5.yellow("\n Cancelled.\n"));
|
|
1256
|
-
process.exit(0);
|
|
1257
|
-
};
|
|
1258
|
-
async function runHub() {
|
|
1259
|
-
console.log(kleur5.bold().cyan("\n \u25C6 dokio create hub\n"));
|
|
1260
|
-
const answers = await prompts2(
|
|
1261
|
-
[
|
|
1262
|
-
{
|
|
1263
|
-
type: "text",
|
|
1264
|
-
name: "hubId",
|
|
1265
|
-
message: "Hub ID (kebab-case, e.g. bupa-sam)",
|
|
1266
|
-
validate: (v) => /^[a-z0-9-]+$/.test(v.trim()) || "Lowercase letters, numbers, hyphens only"
|
|
1267
|
-
},
|
|
1268
|
-
{
|
|
1269
|
-
type: "text",
|
|
1270
|
-
name: "hubName",
|
|
1271
|
-
message: "Hub display name (e.g. Bupa Sales And Marketing Hub)",
|
|
1272
|
-
validate: (v) => v.trim().length > 0 || "Required"
|
|
1273
|
-
}
|
|
1274
|
-
],
|
|
1275
|
-
{ onCancel: onCancel2 }
|
|
1276
|
-
);
|
|
1277
|
-
const hubId = answers.hubId.trim();
|
|
1278
|
-
const hubName = answers.hubName.trim();
|
|
1279
|
-
const dirName = `${hubId}-templates`;
|
|
1280
|
-
const outDir = join5(process.cwd(), dirName);
|
|
1281
|
-
if (await fse5.pathExists(outDir)) {
|
|
1282
|
-
console.error(kleur5.red(`
|
|
1283
|
-
Error: "${dirName}" already exists.
|
|
1284
|
-
`));
|
|
1285
|
-
process.exit(1);
|
|
1286
|
-
}
|
|
1287
|
-
const files = {
|
|
1288
|
-
".githooks/commit-msg": commitMsgHook(),
|
|
1289
|
-
".githooks/post-commit": changelogHook(),
|
|
1290
|
-
".vscode/settings.json": JSON.stringify({ "scss.validate": false, "css.validate": false }, null, 2) + "\n",
|
|
1291
|
-
"templates/.gitkeep": "",
|
|
1292
|
-
"tools/changelog/update_changelog.py": changelogScript(),
|
|
1293
|
-
"tools/changelog/.env.example": changelogEnvExample(),
|
|
1294
|
-
".gitignore": `.DS_Store
|
|
1295
|
-
node_modules/
|
|
1296
|
-
*.log
|
|
1297
|
-
tools/changelog/.env
|
|
1298
|
-
`,
|
|
1299
|
-
"README.md": `# ${hubName}
|
|
1300
|
-
|
|
1301
|
-
Templates for ${hubName} on Dokio.
|
|
1302
|
-
|
|
1303
|
-
## After cloning (required, once per clone)
|
|
1304
|
-
|
|
1305
|
-
Git hooks (commit-message check + auto-changelog) are not active until you run:
|
|
1306
|
-
|
|
1307
|
-
\`\`\`
|
|
1308
|
-
npx create-dokio repair
|
|
1309
|
-
\`\`\`
|
|
1310
|
-
|
|
1311
|
-
Git cannot enable repo hooks automatically on clone, so every teammate must run this once.
|
|
1312
|
-
|
|
1313
|
-
## Creating a new template
|
|
1314
|
-
|
|
1315
|
-
Run \`create-dokio template\` from inside this repo.
|
|
1316
|
-
`
|
|
1317
|
-
};
|
|
1318
|
-
console.log("");
|
|
1319
|
-
for (const [rel, content] of Object.entries(files)) {
|
|
1320
|
-
const fullPath = join5(outDir, rel);
|
|
1321
|
-
await fse5.ensureDir(join5(fullPath, ".."));
|
|
1322
|
-
await fse5.writeFile(fullPath, content, "utf8");
|
|
1323
|
-
if (rel === ".githooks/commit-msg" || rel === ".githooks/post-commit" || rel === "tools/changelog/update_changelog.py") await fse5.chmod(fullPath, 493);
|
|
1324
|
-
console.log(kleur5.dim(` + ${rel}`));
|
|
1325
|
-
}
|
|
1326
|
-
execSync3("git init", { cwd: outDir, stdio: "ignore" });
|
|
1327
|
-
execSync3("git config core.hooksPath .githooks", { cwd: outDir, stdio: "ignore" });
|
|
1328
|
-
execSync3("git add .", { cwd: outDir, stdio: "ignore" });
|
|
1329
|
-
execSync3('git commit -m "chore: init Dokio Hub"', { cwd: outDir, stdio: "ignore" });
|
|
1330
|
-
console.log(kleur5.green(`
|
|
1331
|
-
\u2713 Created ${kleur5.bold(dirName)}
|
|
1396
|
+
console.log(kleur6.yellow(` \u26A0 video support is WIP \u2014 check data.yaml for TODOs
|
|
1332
1397
|
`));
|
|
1333
|
-
console.log(kleur5.dim(` Next steps:`));
|
|
1334
|
-
console.log(kleur5.dim(` cd ${dirName}`));
|
|
1335
|
-
console.log(kleur5.dim(` Create a GitHub repo: github.com/dokioco/${dirName}`));
|
|
1336
|
-
console.log(kleur5.dim(` git remote add origin https://github.com/dokioco/${dirName}`));
|
|
1337
|
-
console.log(kleur5.dim(` git push -u origin main`));
|
|
1338
|
-
console.log("");
|
|
1339
|
-
console.log(kleur5.dim(` Teammates \u2014 after cloning, enable git hooks once:`));
|
|
1340
|
-
console.log(kleur5.dim(` npx create-dokio repair`));
|
|
1341
|
-
console.log("");
|
|
1342
|
-
console.log(kleur5.dim(` Changelog (optional \u2014 for AI descriptions):`));
|
|
1343
|
-
console.log(kleur5.dim(` cp tools/changelog/.env.example tools/changelog/.env`));
|
|
1344
|
-
console.log(kleur5.dim(` # Add your ANTHROPIC_API_KEY to tools/changelog/.env`));
|
|
1345
|
-
console.log("");
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
// src/repair.ts
|
|
1349
|
-
import { execSync as execSync4 } from "child_process";
|
|
1350
|
-
import { basename as basename2 } from "path";
|
|
1351
|
-
import kleur6 from "kleur";
|
|
1352
|
-
function repoRoot() {
|
|
1353
|
-
try {
|
|
1354
|
-
return execSync4("git rev-parse --show-toplevel", {
|
|
1355
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1356
|
-
}).toString().trim();
|
|
1357
|
-
} catch {
|
|
1358
|
-
return null;
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
async function runRepair() {
|
|
1362
|
-
console.log(kleur6.bold().cyan("\n \u25C6 dokio repair\n"));
|
|
1363
|
-
const hubDir = repoRoot();
|
|
1364
|
-
if (!hubDir) {
|
|
1365
|
-
console.error(kleur6.red(" Not a git repository.\n"));
|
|
1366
|
-
console.error(kleur6.dim(" Run this from inside a cloned hub repo (e.g. bupa-sam-templates/).\n"));
|
|
1367
|
-
process.exit(1);
|
|
1368
1398
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
console.log(kleur6.dim(
|
|
1373
|
-
console.log(kleur6.green(`
|
|
1374
|
-
\u2713 Repaired ${kleur6.bold(basename2(hubDir))}
|
|
1375
|
-
`));
|
|
1376
|
-
console.log(kleur6.dim(" Git hooks are now active for this clone:"));
|
|
1377
|
-
console.log(kleur6.dim(" \u2022 commit-msg \u2192 enforces Conventional Commits"));
|
|
1378
|
-
console.log(kleur6.dim(" \u2022 post-commit \u2192 auto-updates template CHANGELOG.md"));
|
|
1399
|
+
console.log(kleur6.dim(` Next steps:`));
|
|
1400
|
+
console.log(kleur6.dim(` cd ${templatePath}`));
|
|
1401
|
+
console.log(kleur6.dim(` Edit data.yaml \u2014 set your template ID`));
|
|
1402
|
+
console.log(kleur6.dim(` Add assets to assets/`));
|
|
1379
1403
|
console.log("");
|
|
1380
|
-
console.log(kleur6.dim(
|
|
1381
|
-
console.log(kleur6.dim(
|
|
1382
|
-
console.log(kleur6.dim(
|
|
1404
|
+
console.log(kleur6.dim(` When ready to commit:`));
|
|
1405
|
+
console.log(kleur6.dim(` cd ../../ (back to ${hubDirName}/)`));
|
|
1406
|
+
console.log(kleur6.dim(` git add templates/${config.fullName}/`));
|
|
1407
|
+
console.log(kleur6.dim(` git commit -m "feat: add ${config.name} template"`));
|
|
1408
|
+
console.log(kleur6.dim(` git push`));
|
|
1383
1409
|
console.log("");
|
|
1384
1410
|
}
|
|
1385
1411
|
|