base-themes 0.1.2 → 0.1.3

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 (262) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/CODE_OF_CONDUCT.md +22 -0
  3. package/CONTRIBUTING.md +98 -0
  4. package/LICENSE +21 -0
  5. package/README.md +316 -3
  6. package/RELEASE.md +80 -0
  7. package/SECURITY.md +49 -0
  8. package/bin/base-themes.mjs +143 -0
  9. package/dist/base-themes.css +1 -1
  10. package/dist/base-themes.js +857 -302
  11. package/dist/llms-full.txt +288 -0
  12. package/dist/llms.txt +79 -0
  13. package/dist/types/blocks/AuthCard.d.ts +2 -0
  14. package/dist/types/blocks/CommandPaletteBlock.d.ts +2 -0
  15. package/dist/types/blocks/DashboardShell.d.ts +2 -0
  16. package/dist/types/blocks/DataTableBlock.d.ts +2 -0
  17. package/dist/types/blocks/PricingPanel.d.ts +2 -0
  18. package/dist/types/blocks/SettingsForm.d.ts +2 -0
  19. package/dist/types/blocks/TeamActivityFeed.d.ts +2 -0
  20. package/dist/types/blocks/ThemeShowcaseCard.d.ts +2 -0
  21. package/dist/types/blocks/index.d.ts +8 -0
  22. package/dist/types/components/ui/Input.d.ts +3 -0
  23. package/dist/types/components/ui/index.d.ts +1 -1
  24. package/dist/types/lib.d.ts +1 -0
  25. package/docs/adoption-dashboard.md +149 -0
  26. package/docs/analytics-setup.md +145 -0
  27. package/docs/community-gallery-proposal.md +64 -0
  28. package/docs/community-proof-telemetry.md +47 -0
  29. package/docs/contributor-issue-seeds.md +240 -0
  30. package/docs/registry-access-telemetry.md +87 -0
  31. package/docs/release-announcement-kit.md +229 -0
  32. package/docs/search-console-setup.md +111 -0
  33. package/docs/theme-token-contract.md +113 -0
  34. package/examples/dashboard/README.md +24 -0
  35. package/examples/dashboard/index.html +12 -0
  36. package/examples/dashboard/package-lock.json +1212 -0
  37. package/examples/dashboard/package.json +24 -0
  38. package/examples/dashboard/src/App.tsx +115 -0
  39. package/examples/dashboard/src/main.tsx +11 -0
  40. package/examples/dashboard/src/styles.css +129 -0
  41. package/examples/dashboard/src/vite-env.d.ts +4 -0
  42. package/examples/dashboard/tsconfig.app.json +23 -0
  43. package/examples/dashboard/tsconfig.json +7 -0
  44. package/examples/dashboard/tsconfig.node.json +15 -0
  45. package/examples/dashboard/vite.config.ts +6 -0
  46. package/examples/next/README.md +29 -0
  47. package/examples/next/app/base-themes-demo.tsx +70 -0
  48. package/examples/next/app/layout.tsx +16 -0
  49. package/examples/next/app/page.tsx +9 -0
  50. package/examples/next/app/styles.css +106 -0
  51. package/examples/next/next-env.d.ts +6 -0
  52. package/examples/next/next.config.ts +5 -0
  53. package/examples/next/package-lock.json +1199 -0
  54. package/examples/next/package.json +27 -0
  55. package/examples/next/tsconfig.json +36 -0
  56. package/examples/registry-copy/README.md +73 -0
  57. package/examples/registry-copy/plan-copy.mjs +130 -0
  58. package/examples/theme-customization/README.md +26 -0
  59. package/examples/theme-customization/index.html +12 -0
  60. package/examples/theme-customization/package-lock.json +1212 -0
  61. package/examples/theme-customization/package.json +24 -0
  62. package/examples/theme-customization/src/App.tsx +138 -0
  63. package/examples/theme-customization/src/main.tsx +11 -0
  64. package/examples/theme-customization/src/styles.css +138 -0
  65. package/examples/theme-customization/src/vite-env.d.ts +4 -0
  66. package/examples/theme-customization/tsconfig.app.json +23 -0
  67. package/examples/theme-customization/tsconfig.json +7 -0
  68. package/examples/theme-customization/tsconfig.node.json +15 -0
  69. package/examples/theme-customization/vite.config.ts +6 -0
  70. package/examples/vite/README.md +32 -0
  71. package/examples/vite/index.html +12 -0
  72. package/examples/vite/package-lock.json +1200 -0
  73. package/examples/vite/package.json +24 -0
  74. package/examples/vite/src/App.tsx +101 -0
  75. package/examples/vite/src/main.tsx +11 -0
  76. package/examples/vite/src/styles.css +125 -0
  77. package/examples/vite/src/vite-env.d.ts +4 -0
  78. package/examples/vite/tsconfig.app.json +23 -0
  79. package/examples/vite/tsconfig.json +7 -0
  80. package/examples/vite/tsconfig.node.json +15 -0
  81. package/examples/vite/vite.config.ts +6 -0
  82. package/llms-full.txt +288 -0
  83. package/llms.txt +79 -0
  84. package/package.json +157 -14
  85. package/registry/items/accordion.json +101 -0
  86. package/registry/items/alert-dialog.json +107 -0
  87. package/registry/items/autocomplete.json +106 -0
  88. package/registry/items/avatar.json +101 -0
  89. package/registry/items/block-auth-card.json +105 -0
  90. package/registry/items/block-command-palette.json +99 -0
  91. package/registry/items/block-dashboard-shell.json +101 -0
  92. package/registry/items/block-data-table.json +99 -0
  93. package/registry/items/block-pricing-panel.json +99 -0
  94. package/registry/items/block-settings-form.json +107 -0
  95. package/registry/items/block-team-activity-feed.json +99 -0
  96. package/registry/items/block-theme-showcase-card.json +99 -0
  97. package/registry/items/button.json +102 -0
  98. package/registry/items/checkbox-group.json +106 -0
  99. package/registry/items/checkbox.json +102 -0
  100. package/registry/items/collapsible.json +101 -0
  101. package/registry/items/combobox.json +101 -0
  102. package/registry/items/context-menu.json +106 -0
  103. package/registry/items/csp-provider.json +96 -0
  104. package/registry/items/dialog.json +102 -0
  105. package/registry/items/direction-provider.json +101 -0
  106. package/registry/items/drawer.json +101 -0
  107. package/registry/items/field.json +101 -0
  108. package/registry/items/fieldset.json +101 -0
  109. package/registry/items/form.json +101 -0
  110. package/registry/items/input.json +102 -0
  111. package/registry/items/menu.json +101 -0
  112. package/registry/items/menubar.json +106 -0
  113. package/registry/items/meter.json +101 -0
  114. package/registry/items/navigation-menu.json +101 -0
  115. package/registry/items/number-field.json +101 -0
  116. package/registry/items/otp-field.json +101 -0
  117. package/registry/items/popover.json +102 -0
  118. package/registry/items/preview-card.json +101 -0
  119. package/registry/items/progress.json +101 -0
  120. package/registry/items/radio-group.json +102 -0
  121. package/registry/items/radio.json +101 -0
  122. package/registry/items/scroll-area.json +101 -0
  123. package/registry/items/select.json +102 -0
  124. package/registry/items/separator.json +101 -0
  125. package/registry/items/slider.json +102 -0
  126. package/registry/items/switch.json +102 -0
  127. package/registry/items/tabs.json +101 -0
  128. package/registry/items/theme-bauhaus.json +107 -0
  129. package/registry/items/theme-bento.json +107 -0
  130. package/registry/items/theme-calm.json +107 -0
  131. package/registry/items/theme-cyberpunk.json +108 -0
  132. package/registry/items/theme-data-dense.json +107 -0
  133. package/registry/items/theme-editorial.json +107 -0
  134. package/registry/items/theme-enterprise.json +108 -0
  135. package/registry/items/theme-fluent.json +107 -0
  136. package/registry/items/theme-glass.json +107 -0
  137. package/registry/items/theme-linear.json +107 -0
  138. package/registry/items/theme-luxury.json +107 -0
  139. package/registry/items/theme-material.json +107 -0
  140. package/registry/items/theme-minimal.json +107 -0
  141. package/registry/items/theme-mono.json +107 -0
  142. package/registry/items/theme-neo-brutalism.json +107 -0
  143. package/registry/items/theme-playful.json +107 -0
  144. package/registry/items/theme-retro.json +107 -0
  145. package/registry/items/theme-shadcn.json +107 -0
  146. package/registry/items/theme-soft-ui.json +107 -0
  147. package/registry/items/theme-terminal.json +107 -0
  148. package/registry/items/toast.json +106 -0
  149. package/registry/items/toggle-group.json +101 -0
  150. package/registry/items/toggle.json +101 -0
  151. package/registry/items/toolbar.json +101 -0
  152. package/registry/items/tooltip.json +102 -0
  153. package/registry/registry.json +564 -49
  154. package/registry/shadcn-registry.json +415 -0
  155. package/research/telemetry-fixtures/analytics-events.jsonl +9 -0
  156. package/research/telemetry-fixtures/bundle-report.json +44 -0
  157. package/research/telemetry-fixtures/community-proof.csv +5 -0
  158. package/research/telemetry-fixtures/registry-access.jsonl +10 -0
  159. package/research/telemetry-fixtures/search-console-export.csv +4 -0
  160. package/scripts/registry-plan.mjs +434 -0
  161. package/scripts/render-launch-actions.mjs +405 -0
  162. package/scripts/render-launch-status.mjs +373 -0
  163. package/scripts/render-release-announcement.mjs +329 -0
  164. package/scripts/verify-launch-readiness.mjs +415 -0
  165. package/scripts/verify-telemetry-fixtures.mjs +85 -0
  166. package/scripts/verify-telemetry-report.mjs +89 -0
  167. package/skills/base-themes/SKILL.md +151 -47
  168. package/src/blocks/AuthCard.tsx +29 -0
  169. package/src/blocks/Blocks.css +182 -0
  170. package/src/blocks/CommandPaletteBlock.tsx +32 -0
  171. package/src/blocks/DashboardShell.tsx +36 -0
  172. package/src/blocks/DataTableBlock.tsx +44 -0
  173. package/src/blocks/PricingPanel.tsx +28 -0
  174. package/src/blocks/SettingsForm.tsx +37 -0
  175. package/src/blocks/TeamActivityFeed.tsx +38 -0
  176. package/src/blocks/ThemeShowcaseCard.tsx +32 -0
  177. package/src/blocks/index.ts +8 -0
  178. package/src/components/ui/Accordion.css +42 -0
  179. package/src/components/ui/Accordion.tsx +41 -0
  180. package/src/components/ui/AlertDialog.css +40 -0
  181. package/src/components/ui/AlertDialog.tsx +52 -0
  182. package/src/components/ui/Autocomplete.css +3 -0
  183. package/src/components/ui/Autocomplete.tsx +50 -0
  184. package/src/components/ui/Avatar.css +45 -0
  185. package/src/components/ui/Avatar.tsx +36 -0
  186. package/src/components/ui/Button.css +79 -0
  187. package/src/components/ui/Button.tsx +20 -0
  188. package/src/components/ui/Checkbox.css +37 -0
  189. package/src/components/ui/Checkbox.tsx +32 -0
  190. package/src/components/ui/CheckboxGroup.tsx +21 -0
  191. package/src/components/ui/Collapsible.css +34 -0
  192. package/src/components/ui/Collapsible.tsx +29 -0
  193. package/src/components/ui/Combobox.css +75 -0
  194. package/src/components/ui/Combobox.tsx +53 -0
  195. package/src/components/ui/ContextMenu.css +9 -0
  196. package/src/components/ui/ContextMenu.tsx +47 -0
  197. package/src/components/ui/CspProvider.tsx +10 -0
  198. package/src/components/ui/Dialog.css +41 -0
  199. package/src/components/ui/Dialog.tsx +45 -0
  200. package/src/components/ui/DirectionProvider.tsx +17 -0
  201. package/src/components/ui/Drawer.css +77 -0
  202. package/src/components/ui/Drawer.tsx +56 -0
  203. package/src/components/ui/Field.css +19 -0
  204. package/src/components/ui/Field.tsx +24 -0
  205. package/src/components/ui/Fieldset.css +16 -0
  206. package/src/components/ui/Fieldset.tsx +19 -0
  207. package/src/components/ui/Form.css +5 -0
  208. package/src/components/ui/Form.tsx +12 -0
  209. package/src/components/ui/Input.css +50 -0
  210. package/src/components/ui/Input.tsx +62 -0
  211. package/src/components/ui/Menu.css +59 -0
  212. package/src/components/ui/Menu.tsx +50 -0
  213. package/src/components/ui/Menubar.css +26 -0
  214. package/src/components/ui/Menubar.tsx +42 -0
  215. package/src/components/ui/Meter.css +45 -0
  216. package/src/components/ui/Meter.tsx +37 -0
  217. package/src/components/ui/NavigationMenu.css +103 -0
  218. package/src/components/ui/NavigationMenu.tsx +64 -0
  219. package/src/components/ui/NumberField.css +38 -0
  220. package/src/components/ui/NumberField.tsx +28 -0
  221. package/src/components/ui/OtpField.css +28 -0
  222. package/src/components/ui/OtpField.tsx +24 -0
  223. package/src/components/ui/Popover.css +25 -0
  224. package/src/components/ui/Popover.tsx +37 -0
  225. package/src/components/ui/PreviewCard.css +33 -0
  226. package/src/components/ui/PreviewCard.tsx +43 -0
  227. package/src/components/ui/Progress.css +33 -0
  228. package/src/components/ui/Progress.tsx +28 -0
  229. package/src/components/ui/Radio.tsx +22 -0
  230. package/src/components/ui/RadioGroup.css +42 -0
  231. package/src/components/ui/RadioGroup.tsx +29 -0
  232. package/src/components/ui/ScrollArea.css +42 -0
  233. package/src/components/ui/ScrollArea.tsx +22 -0
  234. package/src/components/ui/Select.css +86 -0
  235. package/src/components/ui/Select.tsx +39 -0
  236. package/src/components/ui/Separator.css +14 -0
  237. package/src/components/ui/Separator.tsx +12 -0
  238. package/src/components/ui/Slider.css +39 -0
  239. package/src/components/ui/Slider.tsx +21 -0
  240. package/src/components/ui/Switch.css +45 -0
  241. package/src/components/ui/Switch.tsx +29 -0
  242. package/src/components/ui/Tabs.css +72 -0
  243. package/src/components/ui/Tabs.tsx +44 -0
  244. package/src/components/ui/Toast.css +75 -0
  245. package/src/components/ui/Toast.tsx +48 -0
  246. package/src/components/ui/Toggle.tsx +12 -0
  247. package/src/components/ui/ToggleGroup.css +35 -0
  248. package/src/components/ui/ToggleGroup.tsx +30 -0
  249. package/src/components/ui/Toolbar.css +60 -0
  250. package/src/components/ui/Toolbar.tsx +36 -0
  251. package/src/components/ui/Tooltip.css +14 -0
  252. package/src/components/ui/Tooltip.tsx +31 -0
  253. package/src/components/ui/index.ts +83 -0
  254. package/src/components/ui/useDirection.ts +1 -0
  255. package/src/components/ui/useToastManager.ts +11 -0
  256. package/src/docs/blockMeta.json +66 -0
  257. package/src/docs/componentMeta.json +322 -0
  258. package/src/docs/staticPageMeta.json +143 -0
  259. package/src/docs/themeMeta.json +22 -0
  260. package/src/styles/tokenContract.json +61 -0
  261. package/workers/analytics-receiver.mjs +170 -0
  262. package/wrangler.analytics.jsonc +12 -0
@@ -0,0 +1,405 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+
5
+ function readJsonScript(scriptPath, args = []) {
6
+ return JSON.parse(execFileSync('node', [scriptPath, ...args, '--json'], { encoding: 'utf8' }))
7
+ }
8
+
9
+ function shellQuote(value) {
10
+ return `'${value.replaceAll("'", `'\\''`)}'`
11
+ }
12
+
13
+ function ghIssueCreateCommand(issue) {
14
+ return [
15
+ 'gh issue create',
16
+ '--repo markbang/base-themes',
17
+ `--title ${shellQuote(issue.title)}`,
18
+ `--label ${shellQuote(issue.labels.join(','))}`,
19
+ `--body ${shellQuote(issue.body)}`,
20
+ ].join(' ')
21
+ }
22
+
23
+ function buildStarAction(announcement) {
24
+ return {
25
+ signalId: 'github-stars',
26
+ title: 'Earn repo stars from users who tried the package',
27
+ objective: 'Reach at least 10 GitHub stars by asking for a star only after install, doctor, or docs exploration.',
28
+ actions: [
29
+ 'Publish the GitHub Release draft and keep the install guide as the primary attributed link.',
30
+ 'Share the dashboard block route on X / Bluesky with one command and one explicit star ask.',
31
+ 'Post the Base UI-first workflow question to Hacker News or Reddit and ask users to star after trying it.',
32
+ 'Submit the CLI workflow to one product or devtool directory and ask users to try doctor before starring.',
33
+ ],
34
+ links: [
35
+ announcement.links.repo,
36
+ ...announcement.channelChecklist.map((item) => item.primaryLink),
37
+ ],
38
+ measure: 'Re-run npm run telemetry:collect and npm run launch:status at T+1 day and T+7 days.',
39
+ }
40
+ }
41
+
42
+ function buildExternalIssueAction(announcement) {
43
+ return {
44
+ signalId: 'external-human-issue-or-pr',
45
+ title: 'Convert trial users into one external issue or PR',
46
+ objective: 'Get at least one non-maintainer, non-bot issue or PR by making the first contribution path obvious.',
47
+ actions: [
48
+ 'Publish two recommended good-first issues before the first announcement wave.',
49
+ 'Link the recommended issues from the GitHub Release body or first release comment.',
50
+ 'Ask users who hit a blocker to open the smallest feature request, bug report, Show and tell Discussion, or gallery submission.',
51
+ 'Route contributors to comment on a good-first issue before opening a PR.',
52
+ ],
53
+ links: [
54
+ announcement.links.goodFirstIssues,
55
+ announcement.links.featureRequest,
56
+ announcement.links.showAndTell,
57
+ announcement.links.gallerySubmission,
58
+ ...announcement.recommendedGoodFirstIssues.map((issue) => issue.url),
59
+ ],
60
+ commands: announcement.recommendedGoodFirstIssues.map(ghIssueCreateCommand),
61
+ recommendedGoodFirstIssues: announcement.recommendedGoodFirstIssues.map((issue) => ({
62
+ title: issue.title,
63
+ url: issue.url,
64
+ })),
65
+ measure: 'Re-run npm run telemetry:collect after issues are published and after each announcement wave.',
66
+ }
67
+ }
68
+
69
+ function buildForkAction(announcement) {
70
+ return {
71
+ signalId: 'forks',
72
+ title: 'Turn customization interest into at least one fork',
73
+ objective: 'Get one public fork from a user adapting a theme, block, docs example, or registry workflow.',
74
+ actions: [
75
+ 'Ask customization-minded users to fork the repo instead of only copying snippets.',
76
+ 'Have them run the Fork-to-first-change workflow before opening a Discussion, issue, or PR.',
77
+ 'Point them at theme customization or registry-copy commands so the fork has a visible first result.',
78
+ ],
79
+ links: [announcement.links.fork, announcement.links.showAndTell, announcement.links.gallerySubmission],
80
+ commands: [
81
+ 'npm run example:theme-customization:build',
82
+ 'npm run example:registry-copy -- plan button select block:dashboard-shell theme:enterprise --json',
83
+ ],
84
+ measure: 'Re-run npm run telemetry:collect and confirm the forks signal in npm run launch:status.',
85
+ }
86
+ }
87
+
88
+ function buildDownloadAction(announcement) {
89
+ return {
90
+ signalId: 'npm-weekly-downloads',
91
+ title: 'Improve install conversion',
92
+ objective: 'Grow npm weekly downloads through clearer install, CLI, docs, and registry entry points.',
93
+ actions: [
94
+ 'Keep the install command first in release copy and README.',
95
+ 'Use the CLI docs and doctor command as the lowest-friction trial path.',
96
+ 'Share registry and block routes with attributed links so conversion is measurable.',
97
+ ],
98
+ links: [announcement.links.docs, announcement.links.cli, announcement.links.registry],
99
+ commands: ['npm install base-themes @base-ui/react react react-dom', 'npx base-themes doctor .'],
100
+ measure: 'Compare npm weekly download slope in the next telemetry report.',
101
+ }
102
+ }
103
+
104
+ function actionForSignal(signal, announcement) {
105
+ if (signal.id === 'github-stars') return buildStarAction(announcement)
106
+ if (signal.id === 'external-human-issue-or-pr') return buildExternalIssueAction(announcement)
107
+ if (signal.id === 'forks') return buildForkAction(announcement)
108
+ if (signal.id === 'npm-weekly-downloads') return buildDownloadAction(announcement)
109
+ return {
110
+ signalId: signal.id,
111
+ title: signal.label,
112
+ objective: signal.nextAction,
113
+ actions: [signal.nextAction],
114
+ links: [announcement.links.docs],
115
+ measure: 'Re-run npm run telemetry:collect and npm run launch:status.',
116
+ }
117
+ }
118
+
119
+ function targetSignalsForChannel(channel) {
120
+ if (channel === 'GitHub Release') return ['github-stars', 'external-human-issue-or-pr', 'forks']
121
+ if (channel === 'X / Bluesky') return ['github-stars', 'forks']
122
+ if (channel === 'Hacker News / Reddit') return ['github-stars', 'external-human-issue-or-pr', 'npm-weekly-downloads']
123
+ if (channel === 'Product / devtool directories') return ['github-stars', 'npm-weekly-downloads']
124
+ return ['github-stars']
125
+ }
126
+
127
+ function buildPromotionWave(announcement) {
128
+ const shareAssetsById = new Map((announcement.shareAssets ?? []).map((asset) => [asset.id, asset]))
129
+ const copyByChannel = {
130
+ 'GitHub Release': announcement.githubRelease,
131
+ 'X / Bluesky': announcement.social,
132
+ 'Hacker News / Reddit': announcement.forum,
133
+ 'Product / devtool directories': announcement.directory,
134
+ }
135
+
136
+ return (announcement.channelChecklist ?? []).map((item) => ({
137
+ channel: item.channel,
138
+ targetSignals: targetSignalsForChannel(item.channel),
139
+ action: item.action,
140
+ copy: copyByChannel[item.channel] ?? '',
141
+ primaryLink: item.primaryLink,
142
+ shareAssetIds: item.shareAssetIds ?? [],
143
+ shareAssets: (item.shareAssetIds ?? []).map((id) => shareAssetsById.get(id)).filter(Boolean),
144
+ recommendedIssueUrls: item.recommendedIssueUrls ?? [],
145
+ measure: item.measure,
146
+ }))
147
+ }
148
+
149
+ function buildCampaignChecklist(announcement, promotionWave) {
150
+ return [
151
+ {
152
+ phase: 'Before promotion',
153
+ task: 'Verify the release, telemetry, channel copy, issue URLs, and adoption gate before posting externally.',
154
+ evidence: 'Passing launch readiness output and a current live launch status.',
155
+ commands: ['npm run launch:check', 'npm run launch:status -- --live', 'npm run launch:actions -- --live'],
156
+ links: [announcement.links.docs, announcement.links.repo],
157
+ recordFields: ['Launch status output or file:', 'Verified by:', 'Verified at:'],
158
+ },
159
+ {
160
+ phase: 'Before promotion',
161
+ task: 'Publish two or three good-first issues so interested users have an immediate external contribution path.',
162
+ evidence: 'Public issue URLs copied into the GitHub Release body or first release comment.',
163
+ commands: announcement.recommendedGoodFirstIssues.map(ghIssueCreateCommand),
164
+ links: announcement.recommendedGoodFirstIssues.map((issue) => issue.url),
165
+ recordFields: ['Published issue URLs:', 'Release body or comment URL:', 'Published at:'],
166
+ },
167
+ ...promotionWave.map((item) => ({
168
+ phase: 'Promotion wave',
169
+ task: `Publish ${item.channel} copy with its attributed primary link and listed share assets.`,
170
+ evidence: `Saved ${item.channel} post URL plus observed signals for ${item.targetSignals.join(', ')}.`,
171
+ channel: item.channel,
172
+ targetSignals: item.targetSignals,
173
+ shareAssetIds: item.shareAssetIds,
174
+ links: [item.primaryLink],
175
+ recordFields: ['Post URL:', 'Posted at:', 'Observed response:', 'Follow-up owner:'],
176
+ })),
177
+ {
178
+ phase: 'T+1 day measurement',
179
+ task: 'Collect early telemetry and compare stars, forks, external issues or PRs, docs visits, and npm slope.',
180
+ evidence: 'Saved telemetry report and launch status after the first 24 hours.',
181
+ commands: ['npm run telemetry:collect', 'npm run telemetry:check', 'npm run launch:status'],
182
+ recordFields: ['Telemetry report path:', 'Launch status score:', 'Decision notes:'],
183
+ },
184
+ {
185
+ phase: 'T+7 day measurement',
186
+ task: 'Compare channel performance and decide whether to repeat, refine, or pivot the announcement wave.',
187
+ evidence: 'Launch status plus notes on which channel produced public GitHub or npm movement.',
188
+ commands: ['npm run telemetry:collect', 'npm run launch:status', 'npm run launch:actions'],
189
+ recordFields: ['Telemetry report path:', 'Launch status score:', 'Channel decision:'],
190
+ },
191
+ {
192
+ phase: 'T+30 day measurement',
193
+ task: 'Decide whether the strategy is externally validated or needs another product/docs/community iteration.',
194
+ evidence: 'Adoption dashboard updated from public telemetry and supporting analytics exports.',
195
+ commands: ['npm run telemetry:collect', 'npm run launch:status', 'npm run launch:actions'],
196
+ recordFields: ['Telemetry report path:', 'Launch status score:', 'Strategy decision:'],
197
+ },
198
+ ]
199
+ }
200
+
201
+ function buildPayload(status, announcement) {
202
+ const missingSignals = status.missingSignals ?? []
203
+ const promotionWave = buildPromotionWave(announcement)
204
+ return {
205
+ generatedAt: new Date().toISOString(),
206
+ telemetryReport: status.telemetryReport,
207
+ score: status.score,
208
+ completionThreshold: status.completionThreshold,
209
+ signalCount: status.signalCount,
210
+ externallyValidated: status.externallyValidated,
211
+ publicTelemetryComplete: status.publicTelemetryComplete,
212
+ telemetryErrors: status.telemetryErrors ?? [],
213
+ previousTelemetryReport: status.previousTelemetryReport,
214
+ signalTrends: status.signalTrends ?? [],
215
+ conclusion: status.conclusion,
216
+ actions: missingSignals.map((signal) => actionForSignal(signal, announcement)),
217
+ shareAssets: announcement.shareAssets ?? [],
218
+ channelChecklist: announcement.channelChecklist,
219
+ promotionWave,
220
+ campaignChecklist: buildCampaignChecklist(announcement, promotionWave),
221
+ }
222
+ }
223
+
224
+ function indentBlock(value) {
225
+ return value.split('\n').map((line) => ` ${line}`).join('\n')
226
+ }
227
+
228
+ function argValue(name, fallback) {
229
+ const index = process.argv.indexOf(name)
230
+ return index >= 0 ? process.argv[index + 1] : fallback
231
+ }
232
+
233
+ function escapeRegExp(value) {
234
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
235
+ }
236
+
237
+ const protectedRecordFieldLabels = [
238
+ 'Launch status output or file',
239
+ 'Verified by',
240
+ 'Verified at',
241
+ 'Published issue URLs',
242
+ 'Release body or comment URL',
243
+ 'Published at',
244
+ 'Post URL',
245
+ 'Posted at',
246
+ 'Observed response',
247
+ 'Follow-up owner',
248
+ 'Telemetry report path',
249
+ 'Launch status score',
250
+ 'Decision notes',
251
+ 'Channel decision',
252
+ 'Strategy decision',
253
+ ]
254
+
255
+ function filledRecordFields(markdown) {
256
+ return protectedRecordFieldLabels.filter((label) => {
257
+ const pattern = new RegExp(`^\\s*-\\s+${escapeRegExp(label)}:[^\\S\\r\\n]+\\S`, 'm')
258
+ return pattern.test(markdown)
259
+ })
260
+ }
261
+
262
+ function assertSafeCampaignOverwrite(markdownPath) {
263
+ if (process.argv.includes('--force') || !existsSync(markdownPath)) return
264
+
265
+ const existingMarkdown = readFileSync(markdownPath, 'utf8')
266
+ const filledFields = filledRecordFields(existingMarkdown)
267
+ if (filledFields.length === 0) return
268
+
269
+ console.error(`Refusing to overwrite ${markdownPath} because it contains filled campaign record fields: ${filledFields.join(', ')}`)
270
+ console.error('Move the filled evidence to a separate ledger or rerun with --force if overwriting it is intentional.')
271
+ process.exit(1)
272
+ }
273
+
274
+ function assertCompleteCampaignPayload(payload) {
275
+ if (process.argv.includes('--allow-incomplete')) return
276
+ if (payload.publicTelemetryComplete && payload.telemetryErrors.length === 0) return
277
+
278
+ console.error('Refusing to write launch campaign files because telemetry is incomplete.')
279
+ for (const error of payload.telemetryErrors) console.error(`- ${error}`)
280
+ console.error('Rerun after telemetry succeeds, or pass --allow-incomplete to intentionally write a diagnostic campaign pack.')
281
+ process.exit(1)
282
+ }
283
+
284
+ function renderText(payload) {
285
+ const actionBlocks = payload.actions.length
286
+ ? payload.actions.map((action) => {
287
+ const commands = action.commands?.length
288
+ ? `\nCommands:\n${action.commands.map((command) => `- ${command}`).join('\n')}`
289
+ : ''
290
+ const recommended = action.recommendedGoodFirstIssues?.length
291
+ ? `\nRecommended good-first issues:\n${action.recommendedGoodFirstIssues.map((issue) => `- ${issue.title}: ${issue.url}`).join('\n')}`
292
+ : ''
293
+ const recommendedUrls = new Set(action.recommendedGoodFirstIssues?.map((issue) => issue.url) ?? [])
294
+ const links = action.links.filter((link) => !recommendedUrls.has(link))
295
+
296
+ return `## ${action.title}
297
+
298
+ Signal: ${action.signalId}
299
+ Objective: ${action.objective}
300
+
301
+ Actions:
302
+ ${action.actions.map((item) => `- ${item}`).join('\n')}
303
+
304
+ Links:
305
+ ${links.map((link) => `- ${link}`).join('\n')}${commands}${recommended}
306
+
307
+ Measure: ${action.measure}`
308
+ }).join('\n\n')
309
+ : 'No missing public adoption signals. Keep measuring slope after promotion.'
310
+ const channelChecklist = payload.channelChecklist?.length
311
+ ? payload.channelChecklist.map((item) => `- ${item.channel}: ${item.action} Assets: ${(item.shareAssetIds ?? []).join(', ')} Measure: ${item.measure} Link: ${item.primaryLink}`).join('\n')
312
+ : '- None'
313
+ const promotionWave = payload.promotionWave?.length
314
+ ? payload.promotionWave.map((item) => `## ${item.channel}
315
+
316
+ Target signals: ${item.targetSignals.join(', ')}
317
+ Primary link: ${item.primaryLink}
318
+ Share assets: ${item.shareAssetIds.join(', ')}
319
+ Action: ${item.action}
320
+ Measure: ${item.measure}
321
+
322
+ Copy:
323
+ ${indentBlock(item.copy)}`).join('\n\n')
324
+ : '- None'
325
+ const campaignChecklist = payload.campaignChecklist?.length
326
+ ? payload.campaignChecklist.map((item) => {
327
+ const commands = item.commands?.length ? `\n Commands:\n${item.commands.map((command) => indentBlock(`- ${command}`)).join('\n')}` : ''
328
+ const links = item.links?.length ? `\n Links:\n${item.links.map((link) => indentBlock(`- ${link}`)).join('\n')}` : ''
329
+ const assets = item.shareAssetIds?.length ? `\n Assets: ${item.shareAssetIds.join(', ')}` : ''
330
+ const targets = item.targetSignals?.length ? `\n Target signals: ${item.targetSignals.join(', ')}` : ''
331
+ const recordFields = item.recordFields?.length ? `\n Record:\n${item.recordFields.map((field) => indentBlock(`- ${field}`)).join('\n')}` : ''
332
+ return `- [ ] ${item.phase}: ${item.task}\n Evidence: ${item.evidence}${targets}${assets}${links}${commands}${recordFields}`
333
+ }).join('\n')
334
+ : '- None'
335
+ const signalTrends = payload.signalTrends?.length
336
+ ? payload.signalTrends.map((signal) => {
337
+ const delta = typeof signal.delta === 'number' ? ` Delta: ${signal.delta >= 0 ? '+' : ''}${signal.delta}.` : ''
338
+ return `- ${signal.label}: ${signal.current ?? 'n/a'} (previous ${signal.previous ?? 'n/a'}).${delta}`
339
+ }).join('\n')
340
+ : '- No previous telemetry report is available for slope comparison.'
341
+
342
+ return `# Launch Actions
343
+
344
+ Telemetry report: ${payload.telemetryReport}
345
+ Public adoption score: ${payload.score ?? 'n/a'}
346
+ Completion gate: ${payload.completionThreshold}/${payload.signalCount} public signals
347
+ Externally validated: ${payload.externallyValidated ? 'yes' : 'no'}
348
+ Public telemetry complete: ${payload.publicTelemetryComplete ? 'yes' : 'no'}
349
+ Previous telemetry report: ${payload.previousTelemetryReport ?? 'none'}
350
+
351
+ ${actionBlocks}
352
+
353
+ Signal trend:
354
+ ${signalTrends}
355
+
356
+ Share assets:
357
+ ${payload.shareAssets.length ? payload.shareAssets.map((asset) => `- ${asset.title}: ${asset.url} Image: ${asset.imageUrl}`).join('\n') : '- None'}
358
+
359
+ Channel checklist:
360
+ ${channelChecklist}
361
+
362
+ Promotion wave:
363
+ ${promotionWave}
364
+
365
+ Campaign checklist:
366
+ ${campaignChecklist}
367
+
368
+ Telemetry errors:
369
+ ${payload.telemetryErrors.length ? payload.telemetryErrors.map((error) => `- ${error}`).join('\n') : '- None'}
370
+
371
+ Conclusion: ${payload.conclusion}
372
+ `
373
+ }
374
+
375
+ function writeLaunchActions(payload, text) {
376
+ const outputDir = argValue('--output', 'research')
377
+ const basename = `launch-actions-${payload.generatedAt.slice(0, 10)}`
378
+ const jsonPath = join(outputDir, `${basename}.json`)
379
+ const markdownPath = join(outputDir, `${basename}.md`)
380
+ const written = { jsonPath, markdownPath }
381
+
382
+ assertCompleteCampaignPayload(payload)
383
+ assertSafeCampaignOverwrite(markdownPath)
384
+
385
+ mkdirSync(outputDir, { recursive: true })
386
+ writeFileSync(jsonPath, `${JSON.stringify({ ...payload, written }, null, 2)}\n`)
387
+ writeFileSync(markdownPath, text)
388
+
389
+ return written
390
+ }
391
+
392
+ const status = readJsonScript('scripts/render-launch-status.mjs', process.argv.includes('--live') ? ['--live'] : [])
393
+ const announcement = readJsonScript('scripts/render-release-announcement.mjs')
394
+ const payload = buildPayload(status, announcement)
395
+ const text = renderText(payload)
396
+ const written = process.argv.includes('--write') ? writeLaunchActions(payload, text) : undefined
397
+
398
+ if (process.argv.includes('--json')) {
399
+ console.log(JSON.stringify(written ? { ...payload, written } : payload, null, 2))
400
+ } else {
401
+ console.log(text)
402
+ if (written) {
403
+ console.log(`Wrote launch actions: ${written.markdownPath} and ${written.jsonPath}`)
404
+ }
405
+ }