whale-igniter 1.5.4 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +19 -0
- package/dist/commands/createLanding.js +38 -11
- package/dist/version.js +1 -1
- package/docs/ROADMAP.md +9 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Whale Igniter are documented here.
|
|
4
4
|
|
|
5
|
+
## 1.6.0 - 2026-07-14
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added GitHub Actions CI for builds, tests, production dependency audits, security scanning, package validation, and committed `dist/` freshness.
|
|
10
|
+
- Added npm Trusted Publishing through GitHub Actions OIDC, restricted to version tags in the protected `npm` environment and published with provenance.
|
|
11
|
+
- Added migration coverage for Markdown-backed decisions and refinements, including legacy JSON compatibility, duplicate handling, and malformed legacy stores.
|
|
12
|
+
- Added launch metadata to generated landings: description, Open Graph and Twitter cards, canonical/social image support, and a themed inline favicon.
|
|
13
|
+
- Added configurable contact form actions plus native required, email, and minimum-length validation.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Updated the release workflow to Node.js 24 and current GitHub Actions, without long-lived npm publishing tokens or release-build dependency caches.
|
|
18
|
+
- Rebuilt and reconciled committed `dist/` output with `src/`; CI now rejects stale or untracked build artifacts.
|
|
19
|
+
- Removed internal planning documents from the public repository and documented the verified release process.
|
|
20
|
+
|
|
21
|
+
### Release history note
|
|
22
|
+
|
|
23
|
+
- An older source commit was titled `Release 1.6.0`, but no `v1.6.0` tag, GitHub Release, or npm version was created from it. Version `1.6.0` in this entry is the first real published release with that number.
|
|
24
|
+
|
|
5
25
|
## 1.5.4 - 2026-07-08
|
|
6
26
|
|
|
7
27
|
### Security
|
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
6
|
<a href="https://www.npmjs.com/package/whale-igniter"><img alt="npm" src="https://img.shields.io/npm/v/whale-igniter?color=%230034D3&labelColor=%23F2ECE1&style=flat-square"></a>
|
|
7
|
+
<a href="https://github.com/luisviol/whale-igniter/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/luisviol/whale-igniter/ci.yml?branch=main&label=CI&style=flat-square"></a>
|
|
7
8
|
<a href="./LICENSE"><img alt="MIT" src="https://img.shields.io/badge/license-MIT-%230034D3?labelColor=%23F2ECE1&style=flat-square"></a>
|
|
8
9
|
<img alt="node" src="https://img.shields.io/badge/node-%E2%89%A520-%230034D3?labelColor=%23F2ECE1&style=flat-square">
|
|
9
10
|
<img alt="local first" src="https://img.shields.io/badge/local--first-agent%20memory-%230034D3?labelColor=%23F2ECE1&style=flat-square">
|
|
@@ -224,6 +225,24 @@ Whale Igniter has no telemetry or analytics.
|
|
|
224
225
|
|
|
225
226
|
---
|
|
226
227
|
|
|
228
|
+
## Development and releases
|
|
229
|
+
|
|
230
|
+
Start from a clean checkout and use the locked dependency graph:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
npm ci
|
|
234
|
+
npm run build
|
|
235
|
+
npm test
|
|
236
|
+
npm audit --omit=dev --audit-level=high
|
|
237
|
+
npm run security:scan
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`dist/` is versioned because the CLI and MCP smoke tests execute it directly. Every source change that affects compiled output must include the matching `dist/` change. CI rebuilds the package and fails if `dist/` is modified or if the build creates an untracked distribution file.
|
|
241
|
+
|
|
242
|
+
npm releases are published only by `.github/workflows/publish.yml`, triggered from a GitHub Release whose tag matches `v<package.json version>`. The GitHub `npm` environment must provide the `NPM_TOKEN` secret. The workflow installs from `package-lock.json`, audits production dependencies, runs `prepublishOnly`, and publishes with npm provenance. Do not run `npm publish` from a development checkout.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
227
246
|
## Credits
|
|
228
247
|
|
|
229
248
|
Brand assets reference JetBrains Mono (SIL Open Font License 1.1) and Special Elite (Apache License 2.0). No font binaries are shipped.
|
|
@@ -63,7 +63,7 @@ export async function createLandingCommand(options = {}) {
|
|
|
63
63
|
await fs.ensureDir(path.dirname(contentAbs));
|
|
64
64
|
await fs.writeJson(contentAbs, content, { spaces: 2 });
|
|
65
65
|
}
|
|
66
|
-
await fs.writeFile(htmlAbs, renderHtml(content, sections), "utf8");
|
|
66
|
+
await fs.writeFile(htmlAbs, renderHtml(content, sections, theme), "utf8");
|
|
67
67
|
await fs.writeFile(cssAbs, renderCss(theme, {
|
|
68
68
|
includeProof: sections.includes("proof"),
|
|
69
69
|
includeContact: sections.includes("contact")
|
|
@@ -300,7 +300,7 @@ function validateLandingContent(value) {
|
|
|
300
300
|
if (!isRecord(value))
|
|
301
301
|
return { ok: false, error: "expected a JSON object" };
|
|
302
302
|
const input = value;
|
|
303
|
-
for (const key of ["brand", "eyebrow", "title", "subtitle"]) {
|
|
303
|
+
for (const key of ["brand", "eyebrow", "title", "subtitle", "description", "canonicalUrl", "socialImage"]) {
|
|
304
304
|
if (input[key] !== undefined && typeof input[key] !== "string") {
|
|
305
305
|
return { ok: false, error: `${key} must be a string` };
|
|
306
306
|
}
|
|
@@ -354,6 +354,7 @@ function defaultLandingContent(title, sections) {
|
|
|
354
354
|
eyebrow: "Launch-ready landing",
|
|
355
355
|
title: `${title} turns interest into qualified conversations.`,
|
|
356
356
|
subtitle: "A focused page with clear benefits, trust signals and a path that is easy to scan.",
|
|
357
|
+
description: `${title} helps visitors understand the offer, evaluate proof, and take the next step.`,
|
|
357
358
|
cta: {
|
|
358
359
|
label: "Start a conversation",
|
|
359
360
|
href: ctaTarget
|
|
@@ -382,7 +383,8 @@ function defaultLandingContent(title, sections) {
|
|
|
382
383
|
eyebrow: "Contact",
|
|
383
384
|
title: "Tell us what you want to launch.",
|
|
384
385
|
body: "Share a few details and we will follow up with next steps.",
|
|
385
|
-
submitLabel: "Send message"
|
|
386
|
+
submitLabel: "Send message",
|
|
387
|
+
action: "#"
|
|
386
388
|
}
|
|
387
389
|
};
|
|
388
390
|
}
|
|
@@ -392,6 +394,7 @@ function sampleLandingContent() {
|
|
|
392
394
|
eyebrow: "Nutrition planning",
|
|
393
395
|
title: "Meal planning for athletes who train hard.",
|
|
394
396
|
subtitle: "Build training-aware menus, grocery lists, and coach-ready exports without rebuilding spreadsheets.",
|
|
397
|
+
description: "Plan athlete meals, grocery lists, and coach-ready exports around each training block.",
|
|
395
398
|
cta: {
|
|
396
399
|
label: "Request early access",
|
|
397
400
|
href: "#contact"
|
|
@@ -427,11 +430,12 @@ function sampleLandingContent() {
|
|
|
427
430
|
function mergeLandingContent(base, next) {
|
|
428
431
|
return {
|
|
429
432
|
...base,
|
|
430
|
-
...definedPick(next, ["brand", "eyebrow", "title", "subtitle"]),
|
|
433
|
+
...definedPick(next, ["brand", "eyebrow", "title", "subtitle", "description", "canonicalUrl", "socialImage"]),
|
|
434
|
+
description: next.description ?? next.subtitle ?? base.description,
|
|
431
435
|
cta: { ...base.cta, ...definedPick(next.cta ?? {}, ["label", "href"]) },
|
|
432
436
|
features: next.features ?? base.features,
|
|
433
437
|
proof: { ...base.proof, ...definedPick(next.proof ?? {}, ["eyebrow", "title", "quote", "byline"]) },
|
|
434
|
-
contact: { ...base.contact, ...definedPick(next.contact ?? {}, ["eyebrow", "title", "body", "submitLabel"]) }
|
|
438
|
+
contact: { ...base.contact, ...definedPick(next.contact ?? {}, ["eyebrow", "title", "body", "submitLabel", "action"]) }
|
|
435
439
|
};
|
|
436
440
|
}
|
|
437
441
|
function definedPick(source, keys) {
|
|
@@ -442,7 +446,7 @@ function definedPick(source, keys) {
|
|
|
442
446
|
}
|
|
443
447
|
return out;
|
|
444
448
|
}
|
|
445
|
-
function renderHtml(content, sections) {
|
|
449
|
+
function renderHtml(content, sections, theme) {
|
|
446
450
|
const header = sections.includes("header") ? `${indent(renderSection("header", content, sections), 4)}\n` : "";
|
|
447
451
|
const footer = sections.includes("footer") ? `\n${indent(renderSection("footer", content, sections), 4)}` : "";
|
|
448
452
|
const mainBlocks = sections
|
|
@@ -455,6 +459,14 @@ function renderHtml(content, sections) {
|
|
|
455
459
|
<meta charset="utf-8" />
|
|
456
460
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
457
461
|
<title>${escapeHtml(content.brand)}</title>
|
|
462
|
+
<meta name="description" content="${escapeAttr(content.description)}" />
|
|
463
|
+
<meta property="og:type" content="website" />
|
|
464
|
+
<meta property="og:title" content="${escapeAttr(content.title)}" />
|
|
465
|
+
<meta property="og:description" content="${escapeAttr(content.description)}" />
|
|
466
|
+
<meta name="twitter:card" content="${content.socialImage ? "summary_large_image" : "summary"}" />
|
|
467
|
+
<meta name="twitter:title" content="${escapeAttr(content.title)}" />
|
|
468
|
+
<meta name="twitter:description" content="${escapeAttr(content.description)}" />
|
|
469
|
+
${content.canonicalUrl ? ` <link rel="canonical" href="${escapeAttr(content.canonicalUrl)}" />\n <meta property="og:url" content="${escapeAttr(content.canonicalUrl)}" />\n` : ""}${content.socialImage ? ` <meta property="og:image" content="${escapeAttr(content.socialImage)}" />\n <meta name="twitter:image" content="${escapeAttr(content.socialImage)}" />\n` : ""} <link rel="icon" href="${renderFaviconData(theme)}" type="image/svg+xml" />
|
|
458
470
|
<link rel="stylesheet" href="./styles.css" />
|
|
459
471
|
</head>
|
|
460
472
|
<body>
|
|
@@ -527,18 +539,19 @@ ${indent(content.features.map(renderFeature).join("\n"), 6)}
|
|
|
527
539
|
<h2 id="contact-title">${escapeHtml(content.contact.title)}</h2>
|
|
528
540
|
<p class="lede">${escapeHtml(content.contact.body)}</p>
|
|
529
541
|
</div>
|
|
530
|
-
<form class="contact-form">
|
|
542
|
+
<form class="contact-form" action="${escapeAttr(content.contact.action)}" method="post">
|
|
543
|
+
<p class="form-help" id="contact-form-help">All fields are required.</p>
|
|
531
544
|
<label>
|
|
532
545
|
Name
|
|
533
|
-
<input name="name" type="text" autocomplete="name" required />
|
|
546
|
+
<input name="name" type="text" autocomplete="name" placeholder="Ada Lovelace" aria-describedby="contact-form-help" required />
|
|
534
547
|
</label>
|
|
535
548
|
<label>
|
|
536
549
|
Email
|
|
537
|
-
<input name="email" type="email" autocomplete="email" required />
|
|
550
|
+
<input name="email" type="email" autocomplete="email" placeholder="ada@example.com" aria-describedby="contact-form-help" required />
|
|
538
551
|
</label>
|
|
539
552
|
<label>
|
|
540
553
|
Message
|
|
541
|
-
<textarea name="message" rows="4" required></textarea>
|
|
554
|
+
<textarea name="message" rows="4" minlength="10" placeholder="Tell us what you want to launch." aria-describedby="contact-form-help" required></textarea>
|
|
542
555
|
</label>
|
|
543
556
|
<button class="button" type="submit">${escapeHtml(content.contact.submitLabel)}</button>
|
|
544
557
|
</form>
|
|
@@ -583,6 +596,10 @@ function escapeHtml(value) {
|
|
|
583
596
|
function escapeAttr(value) {
|
|
584
597
|
return escapeHtml(value).replace(/'/g, "'");
|
|
585
598
|
}
|
|
599
|
+
function renderFaviconData(theme) {
|
|
600
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12" fill="${theme.identity.accent}"/><path d="M14 18l9 28 9-18 9 18 9-28" fill="none" stroke="${theme.identity.onAccent}" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
601
|
+
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
602
|
+
}
|
|
586
603
|
function printLandingHints(args) {
|
|
587
604
|
console.log();
|
|
588
605
|
console.log(ui.section("Next edits"));
|
|
@@ -594,7 +611,7 @@ function printLandingHints(args) {
|
|
|
594
611
|
`${ui.kv("copy", contentHint, { keyWidth: 8 })}`,
|
|
595
612
|
`${ui.kv("design", "try --theme atlas | harbor | aurora", { keyWidth: 8 })}`,
|
|
596
613
|
`${ui.kv("scope", `trim sections with --sections ${args.sections.join(",")}`, { keyWidth: 8 })}`,
|
|
597
|
-
`${ui.kv("
|
|
614
|
+
`${ui.kv("launch", "set contact.action, canonicalUrl, and a 1200x630 socialImage", { keyWidth: 8 })}`
|
|
598
615
|
];
|
|
599
616
|
console.log(ui.indent(lines.join("\n")));
|
|
600
617
|
}
|
|
@@ -750,6 +767,10 @@ blockquote {
|
|
|
750
767
|
}
|
|
751
768
|
|
|
752
769
|
.eyebrow {
|
|
770
|
+
display: inline-flex;
|
|
771
|
+
width: fit-content;
|
|
772
|
+
max-width: 100%;
|
|
773
|
+
align-self: flex-start;
|
|
753
774
|
margin: 0 0 calc(var(--space-grid) * 2);
|
|
754
775
|
color: var(--color-accent-strong);
|
|
755
776
|
font-size: 0.875rem;
|
|
@@ -982,6 +1003,12 @@ label {
|
|
|
982
1003
|
font-weight: 700;
|
|
983
1004
|
}
|
|
984
1005
|
|
|
1006
|
+
.form-help {
|
|
1007
|
+
margin: 0;
|
|
1008
|
+
color: var(--color-muted);
|
|
1009
|
+
font-size: 0.875rem;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
985
1012
|
input,
|
|
986
1013
|
textarea {
|
|
987
1014
|
width: 100%;
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = "1.
|
|
1
|
+
export const PACKAGE_VERSION = "1.6.0";
|
package/docs/ROADMAP.md
CHANGED
|
@@ -184,23 +184,24 @@ These are listed explicitly to defend focus over time.
|
|
|
184
184
|
|
|
185
185
|
---
|
|
186
186
|
|
|
187
|
-
## Operational milestones
|
|
187
|
+
## Operational milestones
|
|
188
188
|
|
|
189
189
|
These are tasks that depend on the project owner, not on the codebase:
|
|
190
190
|
|
|
191
|
-
- **
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
191
|
+
- **npm releases.** Publishing is performed by the GitHub Release workflow,
|
|
192
|
+
with a matching `v<version>` tag, the protected `npm` environment, and
|
|
193
|
+
provenance. Local `npm publish` is not part of the release process.
|
|
194
|
+
- **Public repo.** Keep CI, release tags, changelog, package version, source,
|
|
195
|
+
and committed `dist/` synchronized.
|
|
195
196
|
- **Demo video.** Record the 5-minute "adopt a real repo and watch
|
|
196
197
|
Claude Code work with full context" walkthrough referenced in the
|
|
197
198
|
done criteria.
|
|
198
199
|
- **Field test.** Adopt the tool in three real projects (own or
|
|
199
200
|
partner teams) and feed observations into v1.1.
|
|
200
201
|
|
|
201
|
-
|
|
202
|
-
not
|
|
202
|
+
Release readiness is decided from the CI run attached to the release commit,
|
|
203
|
+
not from a development checkout.
|
|
203
204
|
|
|
204
205
|
---
|
|
205
206
|
|
|
206
|
-
_Last updated: v1.
|
|
207
|
+
_Last updated: release hardening after v1.5.4._
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whale-igniter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "CLI-first operational intelligence. Bootstraps and adopts AI-readable project context so agents like Claude Code, Codex and Cursor understand your design system from the first commit. Includes an MCP server, a file watcher, deterministic insights, and an opt-in AI bridge (Selene).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|