create-dokio 0.1.18 → 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.
Files changed (2) hide show
  1. package/dist/index.js +788 -762
  2. 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/template.ts
9
- import { join as join4 } from "path";
10
- import fse4 from "fs-extra";
11
- import kleur4 from "kleur";
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/pdf.ts
328
- function pdfFiles(config, pages) {
329
- return {
330
- "CHANGELOG.md": changelog(`${config.templateId} - ${config.name}`),
331
- "data.yaml": pdfYaml(config),
332
- "index.html": pdfHtml(pages),
333
- "assets/.gitkeep": gitkeep(),
334
- ...pdfPartials(pages),
335
- "scss/style.scss.hbs": pdfStyleSass(config, pages),
336
- "scss/_fonts.scss": fontsScssEmpty(),
337
- "scss/_variables.scss": pdfVariables(config),
338
- "scss/_mixins.scss": mixinsScss(),
339
- ...pdfPageScss(pages)
340
- };
341
- }
342
- function pdfYaml(config) {
343
- const { templateId, name, subdomain, width, height, pageCount, princeVersion, resizable, proofable, orderable } = config;
344
- const lines = [
345
- `name: ${templateId} - ${name}`,
346
- `mode: pdf`,
347
- `prince_version: ${princeVersion}`,
348
- `status: 0`,
349
- `subdomain: ${subdomain}`,
350
- `dimension_mode: mm`,
351
- `page_count: ${pageCount}`,
352
- `dimension_width: ${width}`,
353
- `dimension_height: ${height}`,
354
- `compositing_data: {}`
355
- ];
356
- if (proofable) lines.push(`draft_proofable: true`);
357
- if (resizable) lines.push(`resizable: true`);
358
- lines.push(`export_options:`);
359
- if (orderable) {
360
- lines.push(
361
- ` orderable:`,
362
- ` title: Print order`,
363
- ` description: Order from Snap`,
364
- ` description_complete: Ordered from Snap`,
365
- ` title_download: Download for your own printer`,
366
- ` description_download: Download for your own printer`,
367
- ` description_download_complete: Downloaded for your own printer`,
368
- ` marks: true`,
369
- ` bleed: true`,
370
- ` supplier_choice: true`
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 changelogHook() {
1042
- return `#!/bin/bash
1043
- # Auto-updates CHANGELOG.md in modified template folders after each commit.
1044
- # Amends the commit silently to include changelog changes.
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
- TEMPLATE_FILES=$(git diff-tree --no-commit-id -r --name-status HEAD \\
1073
- | grep -v "CHANGELOG\\.md$" \\
1074
- | grep -E $'\\t'"$TEMPLATE/")
1263
+ // Use hex/rgb only \u2014 no CMYK in email
1075
1264
 
1076
- TEMPLATE_DIFF=$(git diff-tree --no-commit-id -r -p HEAD -- "$TEMPLATE/" 2>/dev/null | head -c 4000)
1265
+ * {
1266
+ box-sizing: border-box;
1267
+ }
1077
1268
 
1078
- export CHANGELOG_BRANCH="$BRANCH"
1079
- export CHANGELOG_MESSAGE="$COMMIT_MSG"
1080
- export CHANGELOG_AUTHOR="$AUTHOR"
1081
- export CHANGELOG_FILES="$TEMPLATE_FILES"
1082
- export CHANGELOG_TEMPLATE_PREFIX="$TEMPLATE"
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
- git add "$CHANGELOG"
1087
- AMENDED=1
1088
- done <<< "$TEMPLATES"
1275
+ img {
1276
+ display: block;
1277
+ border: 0;
1278
+ outline: none;
1279
+ text-decoration: none;
1280
+ }
1089
1281
 
1090
- if [ "$AMENDED" = "1" ]; then
1091
- git commit --amend --no-edit
1092
- fi
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
- // src/hubSetup.ts
1101
- function vscodeSettings() {
1102
- return JSON.stringify({ "scss.validate": false, "css.validate": false }, null, 2) + "\n";
1103
- }
1104
- var REQUIRED_IGNORES = [".DS_Store", "node_modules/", "*.log", "tools/changelog/.env"];
1105
- var MANAGED = [
1106
- { rel: ".githooks/commit-msg", content: commitMsgHook, exec: true },
1107
- { rel: ".githooks/post-commit", content: changelogHook, exec: true },
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
- function setHooksPath(hubDir) {
1148
- execSync("git config core.hooksPath .githooks", { cwd: hubDir, stdio: "ignore" });
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 = basename(cwd) === dirName;
1156
- const hubDir = alreadyInside ? cwd : join2(cwd, dirName);
1157
- if (alreadyInside || await fse2.pathExists(hubDir)) {
1158
- console.log(kleur2.dim(`
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
- execSync2("git pull", { cwd: hubDir, stdio: "ignore" });
1324
+ execSync4("git pull", { cwd: hubDir, stdio: "ignore" });
1161
1325
  } else {
1162
- console.log(kleur2.dim(`
1326
+ console.log(kleur4.dim(`
1163
1327
  \u2193 Cloning ${hubRepoUrl(hubId)}...`));
1164
- execSync2(`git clone ${hubRepoUrl(hubId)}`, { stdio: "inherit" });
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(kleur2.dim(` ${line}`));
1171
- const readmePath = join2(hubDir, "README.md");
1172
- if (!await fse2.pathExists(readmePath)) {
1173
- const hubName = basename(hubDir);
1174
- await fse2.writeFile(readmePath, `# ${hubName}
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(kleur2.dim(` + README.md`));
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 join3 } from "path";
1199
- import fse3 from "fs-extra";
1200
- import kleur3 from "kleur";
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 = join3(outDir, rel);
1204
- await fse3.ensureDir(join3(fullPath, ".."));
1205
- await fse3.writeFile(fullPath, content, "utf8");
1206
- console.log(kleur3.dim(` + ${fullName}/${rel}`));
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(kleur4.bold().cyan("\n \u25C6 dokio create template\n"));
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 = join4(hubDir, "templates", config.fullName);
1218
- if (await fse4.pathExists(outDir)) {
1219
- console.error(kleur4.red(`
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(kleur4.green(`
1229
- \u2713 Created ${kleur4.bold(templatePath)}
1392
+ console.log(kleur6.green(`
1393
+ \u2713 Created ${kleur6.bold(templatePath)}
1230
1394
  `));
1231
1395
  if (config.mode === "video") {
1232
- console.log(kleur4.yellow(` \u26A0 video support is WIP \u2014 check data.yaml for TODOs
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
- const { written } = await syncHubFiles(hubDir, { force: true });
1370
- setHooksPath(hubDir);
1371
- for (const line of written) console.log(kleur6.dim(` ${line}`));
1372
- console.log(kleur6.dim(" \u2713 git config core.hooksPath .githooks"));
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(" For AI changelog descriptions (optional):"));
1381
- console.log(kleur6.dim(" cp tools/changelog/.env.example tools/changelog/.env"));
1382
- console.log(kleur6.dim(" # add ANTHROPIC_API_KEY to tools/changelog/.env"));
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