create-dokio 0.1.8 → 0.1.11

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 (3) hide show
  1. package/README.md +109 -47
  2. package/dist/index.js +561 -65
  3. package/package.json +8 -3
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # create-dokio
2
2
 
3
- CLI scaffold for Dokio templates. Generates the correct file structure, `data.yaml`, HTML boilerplate, and SCSS partials for PDF, General, Email, and Video template types.
3
+ CLI scaffold for Dokio hub repos and templates. Generates correct file structure, `data.yaml`, HTML boilerplate, SCSS partials, and auto-wires changelog tooling.
4
4
 
5
5
  ## Usage
6
6
 
@@ -14,6 +14,77 @@ npx create-dokio pool-care
14
14
 
15
15
  > Using pnpm? `pnpm dlx create-dokio`
16
16
 
17
+ Run from your **repos workspace folder** — the folder where all your hub repos live (e.g. `~/Projects/dokio/`).
18
+
19
+ ---
20
+
21
+ ## Commands
22
+
23
+ ### `create-dokio hub`
24
+
25
+ Creates a new hub repository locally with full structure ready to push.
26
+
27
+ ```bash
28
+ npx create-dokio hub
29
+ ```
30
+
31
+ **Prompts:**
32
+ - Hub ID (kebab-case, e.g. `bupa-sam`)
33
+ - Hub display name (e.g. `Bupa Sales And Marketing Hub`)
34
+
35
+ **Generates:**
36
+
37
+ ```
38
+ bupa-sam-templates/
39
+ ├── .githooks/
40
+ │ ├── commit-msg # Enforces conventional commits
41
+ │ └── post-commit # Auto-updates template CHANGELOGs on commit
42
+ ├── .vscode/
43
+ │ └── settings.json # scss.validate: false, css.validate: false
44
+ ├── templates/ # All templates live here
45
+ ├── tools/
46
+ │ └── changelog/
47
+ │ ├── update_changelog.py # Changelog generator script
48
+ │ └── .env.example # ANTHROPIC_API_KEY for AI descriptions
49
+ ├── .gitignore
50
+ └── README.md
51
+ ```
52
+
53
+ **Next steps after hub creation:**
54
+
55
+ ```bash
56
+ cd bupa-sam-templates
57
+ # Create GitHub repo at github.com/dokioco/bupa-sam-templates
58
+ git remote add origin https://github.com/dokioco/bupa-sam-templates
59
+ git push -u origin main
60
+
61
+ # Optional — enable AI changelog descriptions:
62
+ cp tools/changelog/.env.example tools/changelog/.env
63
+ # Add your ANTHROPIC_API_KEY to tools/changelog/.env
64
+ ```
65
+
66
+ ---
67
+
68
+ ### `create-dokio template`
69
+
70
+ Scaffolds a new template inside an existing hub repo. Clones the hub if not found locally.
71
+
72
+ ```bash
73
+ npx create-dokio template
74
+ # or just:
75
+ npx create-dokio
76
+ ```
77
+
78
+ **Prompts:**
79
+ - Template ID (e.g. `HW485`)
80
+ - Template name (e.g. `My Cool Template`)
81
+ - Template type (PDF / General / Email / Video)
82
+ - Hub selection
83
+
84
+ If the hub repo exists locally, it pulls latest. If not, it clones it.
85
+
86
+ If the hub is missing any tooling (changelog script, hooks, `.vscode`, `.gitignore`, `README.md`), it adds them automatically.
87
+
17
88
  ---
18
89
 
19
90
  ## Template types
@@ -22,15 +93,14 @@ npx create-dokio pool-care
22
93
  PrinceXML-rendered print documents. Supports multi-page, resizable layouts, orderable exports, and proof downloads.
23
94
 
24
95
  ```
25
- dokio-pool-care/
96
+ HW485-my-cool-template/
26
97
  ├── CHANGELOG.md
27
98
  ├── data.yaml
28
99
  ├── index.html
29
100
  ├── assets/
30
101
  ├── partials/
31
102
  │ ├── page1-cover.html
32
- ├── page2-content.html
33
- │ └── page3-cta.html
103
+ └── page2-content.html
34
104
  └── scss/
35
105
  ├── style.scss.hbs
36
106
  ├── _fonts.scss
@@ -38,35 +108,24 @@ dokio-pool-care/
38
108
  ├── _variables.scss
39
109
  └── pages/
40
110
  ├── _page1.scss
41
- ├── _page2.scss
42
- └── _page3.scss
111
+ └── _page2.scss
43
112
  ```
44
113
 
45
- **Prompts:**
46
- - Page dimensions (mm)
47
- - Page count
48
- - PrinceXML version (11 or 15 — use 15 for flexbox)
49
- - Resizable template
50
- - Proof downloads (`draft_proofable`)
51
- - Orderable export (Print from Snap)
114
+ **Prompts:** page dimensions (mm), page count, PrinceXML version (11 or 15), resizable, proof downloads, orderable export.
52
115
 
53
116
  ---
54
117
 
55
118
  ### General (image)
56
119
  Chrome-rendered image output — JPG and/or PNG. Single or multi-section layouts.
57
120
 
58
- **Prompts:**
59
- - Dimensions (px)
60
- - Page/section count
61
- - Export formats (JPG, PNG, or both)
121
+ **Prompts:** dimensions (px), page/section count, export formats (JPG, PNG, or both).
62
122
 
63
123
  ---
64
124
 
65
125
  ### Email
66
126
  HTML email scaffold. Includes Outlook conditional comments, `{{{___assembled_css}}}` injection, `{{{subject}}}` title, and Gmail-safe table structure.
67
127
 
68
- **Prompts:**
69
- - Subdomain only — email structure is fixed at 600px
128
+ **Prompts:** subdomain only — email structure is fixed at 600px.
70
129
 
71
130
  ---
72
131
 
@@ -75,36 +134,34 @@ HTML email scaffold. Includes Outlook conditional comments, `{{{___assembled_css
75
134
 
76
135
  ---
77
136
 
78
- ## What gets generated
79
-
80
- | File | Description |
81
- |---|---|
82
- | `data.yaml` | Dokio template config — set your template ID and subdomain before upload |
83
- | `index.html` | Entry HTML with Handlebars partial includes |
84
- | `partials/*.html` | Per-page/section HTML fragments |
85
- | `scss/style.scss.hbs` | Main SCSS entry — supports Handlebars syntax |
86
- | `scss/_fonts.scss` | Empty with commented `@font-face` example |
87
- | `scss/_variables.scss` | Dimension, colour, and typography variables |
88
- | `scss/_mixins.scss` | `absolute()`, `flex()`, `truncate()` mixins |
89
- | `scss/pages/_pageN.scss` | Per-page style partials |
90
- | `assets/` | Drop fonts, images, and local assets here |
91
- | `CHANGELOG.md` | Template-level changelog |
137
+ ## Changelog generator
138
+
139
+ Each template has its own `CHANGELOG.md`. After every commit that touches a template folder, the `post-commit` hook automatically:
140
+
141
+ 1. Detects which templates changed
142
+ 2. Logs the commit to each affected template's `CHANGELOG.md` (author, date, branch, title, files with status labels)
143
+ 3. Amends the commit silently to include the changelog update
144
+
145
+ **Description generation:**
146
+ - If `tools/changelog/.env` exists with a valid `ANTHROPIC_API_KEY` Claude Haiku generates a description
147
+ - If no key → falls back to `TODO - INTEGRATE WITH AI (JAKE TASK)`
92
148
 
93
149
  ---
94
150
 
95
- ## After scaffolding
151
+ ## After scaffolding a template
96
152
 
97
153
  ```bash
98
- cd dokio-<name>
99
-
100
- # 1. Set your template ID in data.yaml
101
- # name: TEMPLATEID - dokio-<name> ← replace TEMPLATEID
154
+ cd bupa-sam-templates/templates/HW485-my-cool-template
102
155
 
103
- # 2. Change subdomain from 'test' before uploading
104
- # subdomain: your-hub-subdomain
156
+ # 1. Check data.yaml name and subdomain are auto-set
157
+ # 2. Add assets to assets/
158
+ # 3. Edit partials/
105
159
 
106
- # 3. Add fonts/images to assets/
107
- # Declare fonts in scss/_fonts.scss
160
+ # When ready to commit (from hub root):
161
+ cd ../../
162
+ git add templates/HW485-my-cool-template/
163
+ git commit -m "feat: add My Cool Template"
164
+ git push
108
165
  ```
109
166
 
110
167
  ---
@@ -112,23 +169,28 @@ cd dokio-<name>
112
169
  ## Development
113
170
 
114
171
  ```bash
115
- git clone <repo>
172
+ git clone https://github.com/dokioco/create-dokio
173
+ cd create-dokio
116
174
  pnpm install
117
- pnpm dev # watch mode
118
- pnpm build # compile to dist/
175
+ pnpm build # compile to dist/
176
+ pnpm link --global
119
177
  ```
120
178
 
121
179
  ### Commit format
122
180
 
123
- This repo uses [Conventional Commits](https://www.conventionalcommits.org/):
181
+ Enforced by commitlint on every commit:
124
182
 
125
183
  ```
126
184
  feat: add resizable PDF option
127
185
  fix: correct page count in data.yaml
128
186
  chore: update dependencies
187
+ docs: update README
129
188
  ```
130
189
 
131
- `CHANGELOG.md` updates automatically on `git push`.
190
+ Breaking changes:
191
+ ```
192
+ feat!: redesign template API
193
+ ```
132
194
 
133
195
  ---
134
196
 
package/dist/index.js CHANGED
@@ -1,7 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import kleur3 from "kleur";
4
+ import prompts3 from "prompts";
5
+ import kleur6 from "kleur";
6
+
7
+ // src/template.ts
8
+ import { join as join3 } from "path";
9
+ import fse3 from "fs-extra";
10
+ import kleur4 from "kleur";
5
11
 
6
12
  // src/prompts.ts
7
13
  import prompts from "prompts";
@@ -21,6 +27,46 @@ function buildPages(pageCount) {
21
27
  return Array.from({ length: pageCount }, (_, i) => getPageName(i, pageCount));
22
28
  }
23
29
 
30
+ // src/hubs.ts
31
+ var HUBS = [
32
+ { id: "meridianenergy", title: "Meridian Energy Hub" },
33
+ { id: "aexp", title: "Amex Global" },
34
+ { id: "ipa", title: "IPA LAM Hub" },
35
+ { id: "bupa-sam", title: "Bupa Sales And Marketing Hub" },
36
+ { id: "gwm", title: "GWM Advertising Studio" },
37
+ { id: "knowledgebase", title: "KB Hub" },
38
+ { id: "vidacorp", title: "VidaDesign Hub" },
39
+ { id: "mi", title: "Measurable Impact" },
40
+ { id: "bupa-hs", title: "Bupa Health Services" },
41
+ { id: "originloopvpp-partnerships-hub", title: "Origin Loop VPP Partnerships Hub" },
42
+ { id: "amazon-devices", title: "Amazon Devices" },
43
+ { id: "belmond", title: "Belmond" },
44
+ { id: "origin", title: "Origin" },
45
+ { id: "shell-au", title: "Shell Australia Brand Templates" },
46
+ { id: "nissan", title: "Nissan" },
47
+ { id: "bupa-agedcare", title: "Bupa Aged Care" },
48
+ { id: "australian-unity", title: "Australian Unity" },
49
+ { id: "poolwerx", title: "Poolwerx Loop" },
50
+ { id: "scimmer", title: "Scimmer" },
51
+ { id: "sandbox", title: "Sandbox" },
52
+ { id: "bupa-healthy-cities", title: "Bupa Healthy Cities" },
53
+ { id: "eabrandhub", title: "EnergyAustralia's Brand Hub" },
54
+ { id: "origin-fugu", title: "Origin Fugu" },
55
+ { id: "fridas", title: "Frida's Luxe Sip n' Paint" },
56
+ { id: "headspace", title: "Headspace" },
57
+ { id: "iris-samsung", title: "Iris | Samsung" },
58
+ { id: "bupa-marketing", title: "Bupa Marketing" },
59
+ { id: "bupa-retail", title: "Bupa Retail" },
60
+ { id: "designsystem", title: "Design System" },
61
+ { id: "hellofresh", title: "HelloFresh" }
62
+ ];
63
+ function hubRepoUrl(hubId) {
64
+ return `https://github.com/dokioco/${hubId}-templates`;
65
+ }
66
+ function hubRepoDirName(hubId) {
67
+ return `${hubId}-templates`;
68
+ }
69
+
24
70
  // src/prompts.ts
25
71
  var onCancel = () => {
26
72
  console.log(kleur.yellow("\n Cancelled.\n"));
@@ -29,12 +75,18 @@ var onCancel = () => {
29
75
  async function runPrompts(nameArg) {
30
76
  const base = await prompts(
31
77
  [
78
+ {
79
+ type: "text",
80
+ name: "templateId",
81
+ message: "Template ID (e.g. HW485)",
82
+ validate: (v) => /^[A-Za-z0-9]+$/.test(v.trim()) || "Alphanumeric only"
83
+ },
32
84
  {
33
85
  type: nameArg ? null : "text",
34
86
  name: "name",
35
- message: 'Template name (kebab-case, no "dokio-" prefix)',
36
- initial: "my-template",
37
- validate: (v) => /^[a-z0-9-]+$/.test(v) || "Lowercase letters, numbers, hyphens only"
87
+ message: "Template name (e.g. My Cool Template)",
88
+ initial: "My Template",
89
+ validate: (v) => v.trim().length > 0 || "Required"
38
90
  },
39
91
  {
40
92
  type: "select",
@@ -48,17 +100,19 @@ async function runPrompts(nameArg) {
48
100
  ]
49
101
  },
50
102
  {
51
- type: "text",
52
- name: "subdomain",
53
- message: "Subdomain",
54
- initial: "test"
103
+ type: "select",
104
+ name: "hubId",
105
+ message: "Hub",
106
+ choices: HUBS.map((h) => ({ title: h.title, value: h.id }))
55
107
  }
56
108
  ],
57
109
  { onCancel }
58
110
  );
59
- const name = nameArg ? toKebab(nameArg) : base.name;
60
- const fullName = `dokio-${name}`;
61
- const subdomain = base.subdomain;
111
+ const templateId = base.templateId.trim();
112
+ const name = nameArg ? nameArg.trim() : base.name.trim();
113
+ const fullName = `${templateId}-${toKebab(name)}`;
114
+ const hubId = base.hubId;
115
+ const subdomain = hubId;
62
116
  if (base.mode === "pdf") {
63
117
  const pdf = await prompts(
64
118
  [
@@ -103,9 +157,11 @@ async function runPrompts(nameArg) {
103
157
  );
104
158
  return {
105
159
  mode: "pdf",
160
+ templateId,
106
161
  name,
107
162
  fullName,
108
163
  subdomain,
164
+ hubId,
109
165
  width: pdf.width,
110
166
  height: pdf.height,
111
167
  pageCount: pdf.pageCount,
@@ -142,9 +198,11 @@ async function runPrompts(nameArg) {
142
198
  );
143
199
  return {
144
200
  mode: "general",
201
+ templateId,
145
202
  name,
146
203
  fullName,
147
204
  subdomain,
205
+ hubId,
148
206
  width: gen.width,
149
207
  height: gen.height,
150
208
  pageCount: gen.pageCount,
@@ -152,16 +210,11 @@ async function runPrompts(nameArg) {
152
210
  };
153
211
  }
154
212
  if (base.mode === "video") {
155
- return { mode: "video", name, fullName, subdomain };
213
+ return { mode: "video", templateId, name, fullName, subdomain, hubId };
156
214
  }
157
- return { mode: "email", name, fullName, subdomain };
215
+ return { mode: "email", templateId, name, fullName, subdomain, hubId };
158
216
  }
159
217
 
160
- // src/scaffold.ts
161
- import { join } from "path";
162
- import fse from "fs-extra";
163
- import kleur2 from "kleur";
164
-
165
218
  // src/templates/shared.ts
166
219
  function changelog(fullName) {
167
220
  return `# Changelog
@@ -216,6 +269,29 @@ function fontsScssEmpty() {
216
269
  // }
217
270
  `;
218
271
  }
272
+ function commitMsgHook() {
273
+ return `#!/usr/bin/env sh
274
+
275
+ commit_msg=$(cat "$1")
276
+ first_line=$(printf '%s' "$commit_msg" | head -n 1)
277
+ pattern='^(feat|fix|chore|docs|style|refactor|test|perf|ci|build|revert)(\\(.+\\))?: .+'
278
+
279
+ if ! printf '%s' "$first_line" | grep -qE "$pattern"; then
280
+ printf '\\n \u2717 Invalid commit message.\\n\\n'
281
+ printf ' Must follow Conventional Commits:\\n\\n'
282
+ printf ' feat: add new feature\\n'
283
+ printf ' fix: resolve a bug\\n'
284
+ printf ' chore: maintenance task\\n'
285
+ printf ' docs: update documentation\\n'
286
+ printf ' style: formatting only\\n'
287
+ printf ' refactor: code change, no feature/fix\\n'
288
+ printf ' test: add or update tests\\n'
289
+ printf ' perf: performance improvement\\n\\n'
290
+ printf ' Your message: "%s"\\n\\n' "$first_line"
291
+ exit 1
292
+ fi
293
+ `;
294
+ }
219
295
  function mixinsScss() {
220
296
  return `@mixin absolute($top: auto, $right: auto, $bottom: auto, $left: auto) {
221
297
  position: absolute;
@@ -250,7 +326,7 @@ function mixinsScss() {
250
326
  // src/templates/pdf.ts
251
327
  function pdfFiles(config, pages) {
252
328
  return {
253
- "CHANGELOG.md": changelog(config.fullName),
329
+ "CHANGELOG.md": changelog(`${config.templateId} - ${config.name}`),
254
330
  "data.yaml": pdfYaml(config),
255
331
  "index.html": pdfHtml(pages),
256
332
  "assets/.gitkeep": gitkeep(),
@@ -263,9 +339,9 @@ function pdfFiles(config, pages) {
263
339
  };
264
340
  }
265
341
  function pdfYaml(config) {
266
- const { fullName, subdomain, width, height, pageCount, princeVersion, resizable, proofable, orderable } = config;
342
+ const { templateId, name, subdomain, width, height, pageCount, princeVersion, resizable, proofable, orderable } = config;
267
343
  const lines = [
268
- `name: TEMPLATEID - ${fullName}`,
344
+ `name: ${templateId} - ${name}`,
269
345
  `mode: pdf`,
270
346
  `prince_version: ${princeVersion}`,
271
347
  `status: 0`,
@@ -443,7 +519,7 @@ function pdfPageScss(pages) {
443
519
  // src/templates/general.ts
444
520
  function generalFiles(config, pages) {
445
521
  return {
446
- "CHANGELOG.md": changelog(config.fullName),
522
+ "CHANGELOG.md": changelog(`${config.templateId} - ${config.name}`),
447
523
  "data.yaml": generalYaml(config),
448
524
  "index.html": generalHtml(pages),
449
525
  "assets/.gitkeep": gitkeep(),
@@ -456,9 +532,9 @@ function generalFiles(config, pages) {
456
532
  };
457
533
  }
458
534
  function generalYaml(config) {
459
- const { fullName, subdomain, width, height, pageCount, exportFormats } = config;
535
+ const { templateId, name, subdomain, width, height, pageCount, exportFormats } = config;
460
536
  const lines = [
461
- `name: TEMPLATEID - ${fullName}`,
537
+ `name: ${templateId} - ${name}`,
462
538
  `mode: general`,
463
539
  `subdomain: ${subdomain}`,
464
540
  `dimension_mode: px`,
@@ -589,7 +665,7 @@ function generalPageScss(pages) {
589
665
  // src/templates/video.ts
590
666
  function videoFiles(config) {
591
667
  return {
592
- "CHANGELOG.md": changelog(config.fullName),
668
+ "CHANGELOG.md": changelog(`${config.templateId} - ${config.name}`),
593
669
  "data.yaml": videoYaml(config),
594
670
  "index.html": videoHtml(),
595
671
  "assets/.gitkeep": gitkeep(),
@@ -605,7 +681,7 @@ function videoFiles(config) {
605
681
  }
606
682
  function videoYaml(config) {
607
683
  return `# TODO: Video template support is WIP
608
- name: TEMPLATEID - ${config.fullName}
684
+ name: ${config.templateId} - ${config.name}
609
685
  mode: video
610
686
  subdomain: ${config.subdomain}
611
687
  duration: '30'
@@ -674,7 +750,7 @@ $color-accent: #0079C8;
674
750
  // src/templates/email.ts
675
751
  function emailFiles(config) {
676
752
  return {
677
- "CHANGELOG.md": changelog(config.fullName),
753
+ "CHANGELOG.md": changelog(`${config.templateId} - ${config.name}`),
678
754
  "data.yaml": emailYaml(config),
679
755
  "index.html": emailHtml(),
680
756
  "assets/.gitkeep": gitkeep(),
@@ -689,7 +765,7 @@ function emailFiles(config) {
689
765
  };
690
766
  }
691
767
  function emailYaml(config) {
692
- return `name: TEMPLATEID - ${config.fullName}
768
+ return `name: ${config.templateId} - ${config.name}
693
769
  mode: email
694
770
  status: 0
695
771
  subdomain: ${config.subdomain}
@@ -792,55 +868,475 @@ $color-accent: #0079C8;
792
868
  `;
793
869
  }
794
870
 
871
+ // src/files.ts
872
+ function buildFiles(config) {
873
+ if (config.mode === "pdf") return pdfFiles(config, buildPages(config.pageCount));
874
+ if (config.mode === "general") return generalFiles(config, buildPages(config.pageCount));
875
+ if (config.mode === "video") return videoFiles(config);
876
+ return emailFiles(config);
877
+ }
878
+
879
+ // src/git.ts
880
+ import { join, basename } from "path";
881
+ import { execSync } from "child_process";
882
+ import fse from "fs-extra";
883
+ import kleur2 from "kleur";
884
+
885
+ // src/templates/changelog.ts
886
+ function changelogScript() {
887
+ return `#!/usr/bin/env python3
888
+ """Prepend a new entry into a template's CHANGELOG.md."""
889
+ import sys
890
+ import re
891
+ import os
892
+ import datetime
893
+
894
+
895
+ def load_dotenv():
896
+ env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")
897
+ if not os.path.isfile(env_path):
898
+ return
899
+ with open(env_path) as f:
900
+ for line in f:
901
+ line = line.strip()
902
+ if not line or line.startswith("#") or "=" not in line:
903
+ continue
904
+ key, _, val = line.partition("=")
905
+ key = key.strip()
906
+ val = val.strip().strip('"').strip("'")
907
+ if key and key not in os.environ:
908
+ os.environ[key] = val
909
+
910
+
911
+ load_dotenv()
912
+
913
+
914
+ STATUS_LABELS = {
915
+ "A": "Added",
916
+ "M": "Modified",
917
+ "D": "Deleted",
918
+ "R": "Renamed",
919
+ "C": "Copied",
920
+ }
921
+
922
+
923
+ def format_date(today_str):
924
+ d = datetime.datetime.strptime(today_str, "%Y-%m-%d")
925
+ day = str(int(d.strftime("%d")))
926
+ return d.strftime(f"%B {day}, %Y")
927
+
928
+
929
+ def generate_description(title, files, diff):
930
+ try:
931
+ import anthropic
932
+ except ImportError:
933
+ return None
934
+
935
+ api_key = os.environ.get("ANTHROPIC_API_KEY")
936
+ if not api_key:
937
+ return None
938
+
939
+ try:
940
+ client = anthropic.Anthropic(api_key=api_key)
941
+ files_str = "\\n".join(f"- {label}: {path}" for label, path in files)
942
+ diff_section = f"\\n\\nDiff:\\n\`\`\`\\n{diff[:4000]}\\n\`\`\`" if diff.strip() else ""
943
+
944
+ prompt = (
945
+ f"You are writing a changelog description for a Dokio template.\\n\\n"
946
+ f"Commit title: {title}\\n"
947
+ f"Files changed:\\n{files_str}"
948
+ f"{diff_section}\\n\\n"
949
+ f"Write a clear, concise description (2-4 sentences) explaining what changed and why. "
950
+ f"Focus on impact and purpose. Be specific but brief. No markdown headers or bullet points."
951
+ )
952
+
953
+ message = client.messages.create(
954
+ model="claude-haiku-4-5-20251001",
955
+ max_tokens=256,
956
+ messages=[{"role": "user", "content": prompt}],
957
+ )
958
+ return message.content[0].text.strip()
959
+ except Exception:
960
+ return None
961
+
962
+
963
+ def parse_files(files_raw, template_prefix):
964
+ files = []
965
+ for line in files_raw.strip().splitlines():
966
+ line = line.strip()
967
+ if not line:
968
+ continue
969
+ parts = line.split("\\t")
970
+ if len(parts) >= 2:
971
+ status_key = parts[0].strip()[0].upper()
972
+ label = STATUS_LABELS.get(status_key, "Modified")
973
+ path = parts[-1].strip()
974
+ else:
975
+ label = "Modified"
976
+ path = parts[0].strip()
977
+ if template_prefix and path.startswith(template_prefix + "/"):
978
+ path = path[len(template_prefix):].lstrip("/")
979
+ files.append((label, path))
980
+ return files
981
+
982
+
983
+ def build_entry_content(branch, title, description, author, today_str, files):
984
+ date_str = format_date(today_str)
985
+ lines = [
986
+ f"**{author}** \u2022 *{date_str}*",
987
+ f"**branch:** \`{branch}\`",
988
+ "",
989
+ f"**title:** {title}",
990
+ "",
991
+ f"**description:** {description}",
992
+ "",
993
+ "### Files Changed",
994
+ ]
995
+ for label, path in files:
996
+ lines.append(f"- **{label}** \`{path}\`")
997
+ return "\\n".join(lines)
998
+
999
+
1000
+ def update_changelog(path, branch, today, title, author, files, diff):
1001
+ with open(path, "r") as f:
1002
+ content = f.read()
1003
+
1004
+ description = generate_description(title, files, diff) or "TODO - INTEGRATE WITH AI (JAKE TASK)"
1005
+ entry_content = build_entry_content(branch, title, description, author, today, files)
1006
+
1007
+ insert_match = re.search(r"^(---|## )", content, re.MULTILINE)
1008
+
1009
+ if insert_match:
1010
+ insert_pos = insert_match.start()
1011
+ if content[insert_pos:].startswith("---"):
1012
+ new_block = f"---\\n\\n{entry_content}\\n\\n"
1013
+ else:
1014
+ new_block = f"---\\n\\n{entry_content}\\n\\n---\\n\\n"
1015
+ content = content[:insert_pos] + new_block + content[insert_pos:]
1016
+ else:
1017
+ content = content.rstrip("\\n") + f"\\n\\n---\\n\\n{entry_content}\\n\\n---\\n"
1018
+
1019
+ with open(path, "w") as f:
1020
+ f.write(content)
1021
+
1022
+
1023
+ if __name__ == "__main__":
1024
+ path = sys.argv[1]
1025
+ today = sys.argv[2]
1026
+ branch = os.environ.get("CHANGELOG_BRANCH", "")
1027
+ message = os.environ.get("CHANGELOG_MESSAGE", "")
1028
+ author = os.environ.get("CHANGELOG_AUTHOR", "")
1029
+ files_raw = os.environ.get("CHANGELOG_FILES", "")
1030
+ template_prefix = os.environ.get("CHANGELOG_TEMPLATE_PREFIX", "")
1031
+ diff = os.environ.get("CHANGELOG_DIFF", "")
1032
+
1033
+ files = parse_files(files_raw, template_prefix)
1034
+ update_changelog(path, branch, today, message, author, files, diff)
1035
+ `;
1036
+ }
1037
+ function changelogHook() {
1038
+ return `#!/bin/bash
1039
+ # Auto-updates CHANGELOG.md in modified template folders after each commit.
1040
+ # Amends the commit silently to include changelog changes.
1041
+
1042
+ REPO_ROOT=$(git rev-parse --show-toplevel)
1043
+ LOCK="$REPO_ROOT/.git/changelog-hook-running"
1044
+
1045
+ [ -f "$LOCK" ] && exit 0
1046
+
1047
+ CHANGED=$(git diff-tree --no-commit-id -r --name-only HEAD | grep -v "CHANGELOG\\.md$")
1048
+ [ -z "$CHANGED" ] && exit 0
1049
+
1050
+ # Detect changed templates (expects templates/<template-id>/ structure)
1051
+ TEMPLATES=$(echo "$CHANGED" | grep "^templates/" | awk -F'/' '{print $1"/"$2}' | sort -u)
1052
+ [ -z "$TEMPLATES" ] && exit 0
1053
+
1054
+ COMMIT_MSG=$(git log -1 --pretty=%s)
1055
+ AUTHOR=$(git log -1 --pretty="%an")
1056
+ BRANCH=$(git rev-parse --abbrev-ref HEAD)
1057
+ TODAY=$(date +%Y-%m-%d)
1058
+
1059
+ touch "$LOCK"
1060
+ trap 'rm -f "$LOCK"' EXIT
1061
+
1062
+ AMENDED=0
1063
+ while IFS= read -r TEMPLATE; do
1064
+ [ -z "$TEMPLATE" ] && continue
1065
+ CHANGELOG="$REPO_ROOT/$TEMPLATE/CHANGELOG.md"
1066
+ [ ! -f "$CHANGELOG" ] && continue
1067
+
1068
+ TEMPLATE_FILES=$(git diff-tree --no-commit-id -r --name-status HEAD \\
1069
+ | grep -v "CHANGELOG\\.md$" \\
1070
+ | grep -E $'\\t'"$TEMPLATE/")
1071
+
1072
+ TEMPLATE_DIFF=$(git diff-tree --no-commit-id -r -p HEAD -- "$TEMPLATE/" 2>/dev/null | head -c 4000)
1073
+
1074
+ export CHANGELOG_BRANCH="$BRANCH"
1075
+ export CHANGELOG_MESSAGE="$COMMIT_MSG"
1076
+ export CHANGELOG_AUTHOR="$AUTHOR"
1077
+ export CHANGELOG_FILES="$TEMPLATE_FILES"
1078
+ export CHANGELOG_TEMPLATE_PREFIX="$TEMPLATE"
1079
+ export CHANGELOG_DIFF="$TEMPLATE_DIFF"
1080
+ python3 "$REPO_ROOT/tools/changelog/update_changelog.py" "$CHANGELOG" "$TODAY"
1081
+
1082
+ git add "$CHANGELOG"
1083
+ AMENDED=1
1084
+ done <<< "$TEMPLATES"
1085
+
1086
+ if [ "$AMENDED" = "1" ]; then
1087
+ git commit --amend --no-edit
1088
+ fi
1089
+ `;
1090
+ }
1091
+ function changelogEnvExample() {
1092
+ return `ANTHROPIC_API_KEY=sk-ant-...
1093
+ `;
1094
+ }
1095
+
1096
+ // src/git.ts
1097
+ async function ensureHubRepo(hubId) {
1098
+ const cwd = process.cwd();
1099
+ const dirName = hubRepoDirName(hubId);
1100
+ const alreadyInside = basename(cwd) === dirName;
1101
+ const hubDir = alreadyInside ? cwd : join(cwd, dirName);
1102
+ if (alreadyInside || await fse.pathExists(hubDir)) {
1103
+ console.log(kleur2.dim(`
1104
+ \u21BB Pulling latest ${dirName}...`));
1105
+ execSync("git pull", { cwd: hubDir, stdio: "ignore" });
1106
+ } else {
1107
+ console.log(kleur2.dim(`
1108
+ \u2193 Cloning ${hubRepoUrl(hubId)}...`));
1109
+ execSync(`git clone ${hubRepoUrl(hubId)}`, { stdio: "inherit" });
1110
+ }
1111
+ return hubDir;
1112
+ }
1113
+ async function setupHooks(hubDir) {
1114
+ const commitMsgPath = join(hubDir, ".githooks", "commit-msg");
1115
+ if (!await fse.pathExists(commitMsgPath)) {
1116
+ await fse.ensureDir(join(commitMsgPath, ".."));
1117
+ await fse.writeFile(commitMsgPath, commitMsgHook(), "utf8");
1118
+ await fse.chmod(commitMsgPath, 493);
1119
+ console.log(kleur2.dim(` + .githooks/commit-msg`));
1120
+ }
1121
+ const postCommitPath = join(hubDir, ".githooks", "post-commit");
1122
+ const changelogScriptPath = join(hubDir, "tools", "changelog", "update_changelog.py");
1123
+ if (!await fse.pathExists(changelogScriptPath)) {
1124
+ await fse.ensureDir(join(changelogScriptPath, ".."));
1125
+ await fse.writeFile(changelogScriptPath, changelogScript(), "utf8");
1126
+ await fse.chmod(changelogScriptPath, 493);
1127
+ console.log(kleur2.dim(` + tools/changelog/update_changelog.py`));
1128
+ const envExamplePath = join(hubDir, "tools", "changelog", ".env.example");
1129
+ await fse.writeFile(envExamplePath, changelogEnvExample(), "utf8");
1130
+ console.log(kleur2.dim(` + tools/changelog/.env.example`));
1131
+ if (!await fse.pathExists(postCommitPath)) {
1132
+ await fse.writeFile(postCommitPath, changelogHook(), "utf8");
1133
+ await fse.chmod(postCommitPath, 493);
1134
+ console.log(kleur2.dim(` + .githooks/post-commit`));
1135
+ }
1136
+ }
1137
+ const vscodePath = join(hubDir, ".vscode", "settings.json");
1138
+ if (!await fse.pathExists(vscodePath)) {
1139
+ await fse.ensureDir(join(vscodePath, ".."));
1140
+ await fse.writeFile(vscodePath, JSON.stringify({ "scss.validate": false, "css.validate": false }, null, 2) + "\n", "utf8");
1141
+ console.log(kleur2.dim(` + .vscode/settings.json`));
1142
+ }
1143
+ const gitignorePath = join(hubDir, ".gitignore");
1144
+ const requiredIgnores = [".DS_Store", "node_modules/", "*.log", "tools/changelog/.env"];
1145
+ if (!await fse.pathExists(gitignorePath)) {
1146
+ await fse.writeFile(gitignorePath, requiredIgnores.join("\n") + "\n", "utf8");
1147
+ console.log(kleur2.dim(` + .gitignore`));
1148
+ } else {
1149
+ const existing = await fse.readFile(gitignorePath, "utf8");
1150
+ const missing = requiredIgnores.filter((e) => !existing.includes(e));
1151
+ if (missing.length) {
1152
+ await fse.appendFile(gitignorePath, missing.join("\n") + "\n");
1153
+ console.log(kleur2.dim(` ~ .gitignore (added: ${missing.join(", ")})`));
1154
+ }
1155
+ }
1156
+ const readmePath = join(hubDir, "README.md");
1157
+ if (!await fse.pathExists(readmePath)) {
1158
+ const hubName = basename(hubDir);
1159
+ await fse.writeFile(readmePath, `# ${hubName}
1160
+
1161
+ Templates for ${hubName} on Dokio.
1162
+
1163
+ ## Creating a new template
1164
+
1165
+ Run \`create-dokio template\` from inside this repo.
1166
+ `, "utf8");
1167
+ console.log(kleur2.dim(` + README.md`));
1168
+ }
1169
+ execSync("git config core.hooksPath .githooks", { cwd: hubDir, stdio: "ignore" });
1170
+ }
1171
+
795
1172
  // src/scaffold.ts
796
- async function scaffold(config) {
797
- const outDir = join(process.cwd(), config.fullName);
798
- if (await fse.pathExists(outDir)) {
799
- console.error(kleur2.red(`
800
- Error: directory "${config.fullName}" already exists.
1173
+ import { join as join2 } from "path";
1174
+ import fse2 from "fs-extra";
1175
+ import kleur3 from "kleur";
1176
+ async function writeFiles(outDir, files, fullName) {
1177
+ for (const [rel, content] of Object.entries(files)) {
1178
+ const fullPath = join2(outDir, rel);
1179
+ await fse2.ensureDir(join2(fullPath, ".."));
1180
+ await fse2.writeFile(fullPath, content, "utf8");
1181
+ console.log(kleur3.dim(` + ${fullName}/${rel}`));
1182
+ }
1183
+ }
1184
+
1185
+ // src/template.ts
1186
+ async function runTemplate(nameArg) {
1187
+ console.log(kleur4.bold().cyan("\n \u25C6 dokio create template\n"));
1188
+ const config = await runPrompts(nameArg);
1189
+ const files = buildFiles(config);
1190
+ const hubDir = await ensureHubRepo(config.hubId);
1191
+ const hubDirName = hubRepoDirName(config.hubId);
1192
+ const outDir = join3(hubDir, "templates", config.fullName);
1193
+ if (await fse3.pathExists(outDir)) {
1194
+ console.error(kleur4.red(`
1195
+ Error: "${config.fullName}" already exists in ${hubDirName}/templates/.
801
1196
  `));
802
1197
  process.exit(1);
803
1198
  }
804
- let files;
805
- if (config.mode === "pdf") {
806
- files = pdfFiles(config, buildPages(config.pageCount));
807
- } else if (config.mode === "general") {
808
- files = generalFiles(config, buildPages(config.pageCount));
809
- } else if (config.mode === "video") {
810
- files = videoFiles(config);
811
- } else {
812
- files = emailFiles(config);
1199
+ console.log("");
1200
+ await writeFiles(outDir, files, config.fullName);
1201
+ await setupHooks(hubDir);
1202
+ const templatePath = `${hubDirName}/templates/${config.fullName}`;
1203
+ console.log(kleur4.green(`
1204
+ \u2713 Created ${kleur4.bold(templatePath)}
1205
+ `));
1206
+ if (config.mode === "video") {
1207
+ console.log(kleur4.yellow(` \u26A0 video support is WIP \u2014 check data.yaml for TODOs
1208
+ `));
813
1209
  }
1210
+ console.log(kleur4.dim(` Next steps:`));
1211
+ console.log(kleur4.dim(` cd ${templatePath}`));
1212
+ console.log(kleur4.dim(` Edit data.yaml \u2014 set your template ID`));
1213
+ console.log(kleur4.dim(` Add assets to assets/`));
1214
+ console.log("");
1215
+ console.log(kleur4.dim(` When ready to commit:`));
1216
+ console.log(kleur4.dim(` cd ../../ (back to ${hubDirName}/)`));
1217
+ console.log(kleur4.dim(` git add templates/${config.fullName}/`));
1218
+ console.log(kleur4.dim(` git commit -m "feat: add ${config.name} template"`));
1219
+ console.log(kleur4.dim(` git push`));
1220
+ console.log("");
1221
+ }
1222
+
1223
+ // src/hub.ts
1224
+ import { join as join4 } from "path";
1225
+ import { execSync as execSync2 } from "child_process";
1226
+ import fse4 from "fs-extra";
1227
+ import kleur5 from "kleur";
1228
+ import prompts2 from "prompts";
1229
+ var onCancel2 = () => {
1230
+ console.log(kleur5.yellow("\n Cancelled.\n"));
1231
+ process.exit(0);
1232
+ };
1233
+ async function runHub() {
1234
+ console.log(kleur5.bold().cyan("\n \u25C6 dokio create hub\n"));
1235
+ const answers = await prompts2(
1236
+ [
1237
+ {
1238
+ type: "text",
1239
+ name: "hubId",
1240
+ message: "Hub ID (kebab-case, e.g. bupa-sam)",
1241
+ validate: (v) => /^[a-z0-9-]+$/.test(v.trim()) || "Lowercase letters, numbers, hyphens only"
1242
+ },
1243
+ {
1244
+ type: "text",
1245
+ name: "hubName",
1246
+ message: "Hub display name (e.g. Bupa Sales And Marketing Hub)",
1247
+ validate: (v) => v.trim().length > 0 || "Required"
1248
+ }
1249
+ ],
1250
+ { onCancel: onCancel2 }
1251
+ );
1252
+ const hubId = answers.hubId.trim();
1253
+ const hubName = answers.hubName.trim();
1254
+ const dirName = `${hubId}-templates`;
1255
+ const outDir = join4(process.cwd(), dirName);
1256
+ if (await fse4.pathExists(outDir)) {
1257
+ console.error(kleur5.red(`
1258
+ Error: "${dirName}" already exists.
1259
+ `));
1260
+ process.exit(1);
1261
+ }
1262
+ const files = {
1263
+ ".githooks/commit-msg": commitMsgHook(),
1264
+ ".githooks/post-commit": changelogHook(),
1265
+ ".vscode/settings.json": JSON.stringify({ "scss.validate": false, "css.validate": false }, null, 2) + "\n",
1266
+ "templates/.gitkeep": "",
1267
+ "tools/changelog/update_changelog.py": changelogScript(),
1268
+ "tools/changelog/.env.example": changelogEnvExample(),
1269
+ ".gitignore": `.DS_Store
1270
+ node_modules/
1271
+ *.log
1272
+ tools/changelog/.env
1273
+ `,
1274
+ "README.md": `# ${hubName}
1275
+
1276
+ Templates for ${hubName} on Dokio.
1277
+
1278
+ ## Creating a new template
1279
+
1280
+ Run \`create-dokio template\` from inside this repo.
1281
+ `
1282
+ };
814
1283
  console.log("");
815
1284
  for (const [rel, content] of Object.entries(files)) {
816
- const fullPath = join(outDir, rel);
817
- await fse.ensureDir(join(fullPath, ".."));
818
- await fse.writeFile(fullPath, content, "utf8");
819
- console.log(kleur2.dim(` + ${rel}`));
1285
+ const fullPath = join4(outDir, rel);
1286
+ await fse4.ensureDir(join4(fullPath, ".."));
1287
+ await fse4.writeFile(fullPath, content, "utf8");
1288
+ if (rel === ".githooks/commit-msg" || rel === ".githooks/post-commit" || rel === "tools/changelog/update_changelog.py") await fse4.chmod(fullPath, 493);
1289
+ console.log(kleur5.dim(` + ${rel}`));
820
1290
  }
1291
+ execSync2("git init", { cwd: outDir, stdio: "ignore" });
1292
+ execSync2("git config core.hooksPath .githooks", { cwd: outDir, stdio: "ignore" });
1293
+ execSync2("git add .", { cwd: outDir, stdio: "ignore" });
1294
+ execSync2('git commit -m "chore: init Dokio Hub"', { cwd: outDir, stdio: "ignore" });
1295
+ console.log(kleur5.green(`
1296
+ \u2713 Created ${kleur5.bold(dirName)}
1297
+ `));
1298
+ console.log(kleur5.dim(` Next steps:`));
1299
+ console.log(kleur5.dim(` cd ${dirName}`));
1300
+ console.log(kleur5.dim(` Create a GitHub repo: github.com/dokioco/${dirName}`));
1301
+ console.log(kleur5.dim(` git remote add origin https://github.com/dokioco/${dirName}`));
1302
+ console.log(kleur5.dim(` git push -u origin main`));
1303
+ console.log("");
1304
+ console.log(kleur5.dim(` Changelog (optional \u2014 for AI descriptions):`));
1305
+ console.log(kleur5.dim(` cp tools/changelog/.env.example tools/changelog/.env`));
1306
+ console.log(kleur5.dim(` # Add your ANTHROPIC_API_KEY to tools/changelog/.env`));
1307
+ console.log("");
821
1308
  }
822
1309
 
823
1310
  // src/index.ts
824
- async function run(nameArg) {
825
- console.log(kleur3.bold().cyan("\n \u25C6 dokio create\n"));
826
- const config = await runPrompts(nameArg);
827
- await scaffold(config);
828
- const isWip = config.mode === "video";
829
- console.log(kleur3.green(`
830
- \u2713 Created ${kleur3.bold(config.fullName)}
831
- `));
832
- if (isWip) {
833
- console.log(
834
- kleur3.yellow(` \u26A0 ${config.mode} support is WIP \u2014 check data.yaml for TODOs
835
- `)
836
- );
1311
+ async function main(argv) {
1312
+ const subcommand = argv[0];
1313
+ if (subcommand === "template") {
1314
+ return runTemplate(argv[1]);
837
1315
  }
838
- console.log(kleur3.dim(` Next steps:`));
839
- console.log(kleur3.dim(` cd ${config.fullName}`));
840
- console.log(kleur3.dim(` Edit data.yaml \u2014 set your template ID and subdomain`));
841
- console.log(kleur3.dim(` Add assets to assets/`));
842
- console.log("");
1316
+ if (subcommand === "hub") {
1317
+ return runHub();
1318
+ }
1319
+ console.log(kleur6.bold().cyan("\n \u25C6 dokio create\n"));
1320
+ const { action } = await prompts3(
1321
+ {
1322
+ type: "select",
1323
+ name: "action",
1324
+ message: "What are you creating?",
1325
+ choices: [
1326
+ { title: "Template", value: "template", description: "Scaffold a new template inside a hub repo" },
1327
+ { title: "Hub", value: "hub", description: "Create a new hub repository" }
1328
+ ]
1329
+ },
1330
+ {
1331
+ onCancel: () => {
1332
+ console.log(kleur6.yellow("\n Cancelled.\n"));
1333
+ process.exit(0);
1334
+ }
1335
+ }
1336
+ );
1337
+ if (action === "hub") return runHub();
1338
+ return runTemplate();
843
1339
  }
844
1340
 
845
1341
  // bin/index.ts
846
- run(process.argv[2]);
1342
+ main(process.argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dokio",
3
- "version": "0.1.8",
3
+ "version": "0.1.11",
4
4
  "description": "CLI scaffold for Dokio templates",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,14 +21,17 @@
21
21
  "@commitlint/cli": "^19.0.0",
22
22
  "@commitlint/config-conventional": "^19.0.0",
23
23
  "@commitlint/types": "^19.0.0",
24
+ "@eslint/js": "^10.0.1",
24
25
  "@types/fs-extra": "^11.0.4",
25
26
  "@types/node": "^20.0.0",
26
27
  "@types/prompts": "^2.4.9",
27
28
  "changelogen": "^0.6.2",
29
+ "eslint": "^10.4.1",
28
30
  "husky": "^9.0.0",
29
31
  "tsup": "^8.0.0",
30
32
  "tsx": "^4.22.3",
31
- "typescript": "^5.4.0"
33
+ "typescript": "^5.4.0",
34
+ "typescript-eslint": "^8.60.0"
32
35
  },
33
36
  "tsup": {
34
37
  "entry": {
@@ -52,6 +55,8 @@
52
55
  "scripts": {
53
56
  "build": "tsup",
54
57
  "dev": "tsup --watch",
55
- "changelog": "changelogen --bump && tsx scripts/changelog.ts"
58
+ "lint": "eslint",
59
+ "changelog": "changelogen --bump && tsx scripts/changelog.ts",
60
+ "release": "changelogen --bump --output CHANGELOG.md && git add package.json CHANGELOG.md && git commit -m \"chore(release): v$(node -p 'require(\"./package.json\").version')\" && git push -u origin HEAD"
56
61
  }
57
62
  }