whale-igniter 1.3.1 → 1.3.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.
@@ -0,0 +1,437 @@
1
+ import path from "node:path";
2
+ import fs from "fs-extra";
3
+ import { resolveTarget } from "../utils/paths.js";
4
+ import { loadConfig } from "../utils/config.js";
5
+ import { upsertComponent } from "../utils/components.js";
6
+ import { generateWiki } from "../generators/wikiGenerator.js";
7
+ import { ui } from "../ui/index.js";
8
+ const DEFAULT_SECTIONS = ["hero", "features", "proof", "contact"];
9
+ const VALID_SECTIONS = new Set(DEFAULT_SECTIONS);
10
+ export async function createLandingCommand(options = {}) {
11
+ const target = resolveTarget();
12
+ const config = await loadConfig(target);
13
+ const sections = parseSections(options.sections);
14
+ if (sections.length === 0) {
15
+ console.log(ui.fail("No valid sections selected. Use hero,features,proof,contact."));
16
+ process.exitCode = 1;
17
+ return;
18
+ }
19
+ const outDir = options.outDir ?? "src";
20
+ const htmlRel = path.join(outDir, "index.html").replace(/\\/g, "/");
21
+ const cssRel = path.join(outDir, "styles.css").replace(/\\/g, "/");
22
+ const htmlAbs = path.join(target, htmlRel);
23
+ const cssAbs = path.join(target, cssRel);
24
+ const existing = [];
25
+ if (await fs.pathExists(htmlAbs))
26
+ existing.push(htmlRel);
27
+ if (await fs.pathExists(cssAbs))
28
+ existing.push(cssRel);
29
+ if (existing.length > 0 && !options.force) {
30
+ console.log(ui.fail(`${existing.map((f) => ui.path(f)).join(", ")} already exists. Use --force to overwrite.`));
31
+ process.exitCode = 1;
32
+ return;
33
+ }
34
+ if (config.stack && config.stack !== "css") {
35
+ console.log(ui.warn(`Project stack is ${ui.code(config.stack)}; generating portable HTML/CSS anyway.`));
36
+ }
37
+ await fs.ensureDir(path.dirname(htmlAbs));
38
+ const grid = config.foundations?.grid ?? 8;
39
+ const controlRadius = config.foundations?.radius?.control ?? 2;
40
+ const containerRadius = config.foundations?.radius?.container ?? 4;
41
+ const projectName = config.projectName ?? "Whale Landing";
42
+ await fs.writeFile(htmlAbs, renderHtml(projectName, sections), "utf8");
43
+ await fs.writeFile(cssAbs, renderCss({
44
+ grid,
45
+ controlRadius,
46
+ containerRadius,
47
+ includeProof: sections.includes("proof"),
48
+ includeContact: sections.includes("contact")
49
+ }), "utf8");
50
+ console.log(ui.ok(`Created ${ui.path(htmlRel)}`));
51
+ console.log(ui.ok(`Created ${ui.path(cssRel)}`));
52
+ console.log(` ${ui.kv("sections", sections.join(", "), { keyWidth: 8 })}`);
53
+ console.log(` ${ui.kv("grid", `${grid}px`, { keyWidth: 8 })}`);
54
+ console.log(` ${ui.kv("radius", `${controlRadius}px controls, ${containerRadius}px containers`, { keyWidth: 8 })}`);
55
+ if (!options.noRegister) {
56
+ for (const section of sections) {
57
+ await upsertComponent(target, {
58
+ name: sectionName(section),
59
+ description: sectionDescription(section),
60
+ category: section === "contact" ? "form" : section === "hero" ? "layout" : "surface",
61
+ files: [htmlRel, cssRel],
62
+ variants: section === "contact" ? ["default"] : undefined,
63
+ states: section === "contact" ? ["focus", "disabled"] : undefined,
64
+ tokens: ["--color-bg", "--color-surface", "--color-accent", "--space-grid"]
65
+ });
66
+ }
67
+ console.log(ui.muted(` registered ${sections.length} section(s) in intelligence/components.json`));
68
+ }
69
+ if (!options.noSync && !options.noRegister) {
70
+ await generateWiki(target);
71
+ console.log(ui.muted(`${ui.glyph.check} AI context updated`));
72
+ }
73
+ }
74
+ function parseSections(raw) {
75
+ if (!raw)
76
+ return DEFAULT_SECTIONS;
77
+ const seen = new Set();
78
+ const sections = [];
79
+ for (const part of raw.split(",")) {
80
+ const value = part.trim().toLowerCase();
81
+ if (VALID_SECTIONS.has(value) && !seen.has(value)) {
82
+ seen.add(value);
83
+ sections.push(value);
84
+ }
85
+ }
86
+ return sections;
87
+ }
88
+ function sectionName(section) {
89
+ const map = {
90
+ hero: "HeroSection",
91
+ features: "FeatureSection",
92
+ proof: "SocialProofSection",
93
+ contact: "ContactForm"
94
+ };
95
+ return map[section];
96
+ }
97
+ function sectionDescription(section) {
98
+ const map = {
99
+ hero: "Landing hero with headline, value prop and primary CTA.",
100
+ features: "Feature grid for the landing page offer.",
101
+ proof: "Social proof band with customer quotes and trust signals.",
102
+ contact: "Contact form with accessible labels and focus states."
103
+ };
104
+ return map[section];
105
+ }
106
+ function titleize(input) {
107
+ return input
108
+ .split(/[-_\s]+/)
109
+ .filter(Boolean)
110
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
111
+ .join(" ");
112
+ }
113
+ function renderHtml(projectName, sections) {
114
+ const title = titleize(projectName);
115
+ const blocks = sections.map((section) => renderSection(section, title, sections)).join("\n\n");
116
+ return `<!doctype html>
117
+ <html lang="en">
118
+ <head>
119
+ <meta charset="utf-8" />
120
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
121
+ <title>${title}</title>
122
+ <link rel="stylesheet" href="./styles.css" />
123
+ </head>
124
+ <body>
125
+ <main>
126
+ ${indent(blocks, 6)}
127
+ </main>
128
+ </body>
129
+ </html>
130
+ `;
131
+ }
132
+ function renderSection(section, title, sections) {
133
+ switch (section) {
134
+ case "hero": {
135
+ const ctaTarget = sections.includes("contact")
136
+ ? "#contact"
137
+ : sections.includes("features")
138
+ ? "#features-title"
139
+ : "#";
140
+ return `<section class="section hero" aria-labelledby="hero-title">
141
+ <div class="section__inner hero__grid">
142
+ <div class="hero__copy">
143
+ <p class="eyebrow">Launch-ready landing</p>
144
+ <h1 id="hero-title">${title} turns interest into qualified conversations.</h1>
145
+ <p class="lede">A focused page with clear benefits, trust signals and a path that is easy to scan.</p>
146
+ <a class="button" href="${ctaTarget}">Start a conversation</a>
147
+ </div>
148
+ <div class="hero__panel" aria-label="Landing page highlights">
149
+ <span>Fast setup</span>
150
+ <strong>${sections.length} sections</strong>
151
+ <span>${sections.includes("contact") ? "Contact-ready" : "Ready to extend"}</span>
152
+ </div>
153
+ </div>
154
+ </section>`;
155
+ }
156
+ case "features":
157
+ return `<section class="section" aria-labelledby="features-title">
158
+ <div class="section__inner">
159
+ <p class="eyebrow">Why it works</p>
160
+ <h2 id="features-title">Everything a simple landing needs.</h2>
161
+ <div class="feature-grid">
162
+ <article class="card">
163
+ <h3>Clear offer</h3>
164
+ <p>A direct headline and CTA keep the first screen focused.</p>
165
+ </article>
166
+ <article class="card">
167
+ <h3>Readable proof</h3>
168
+ <p>Compact testimonials and metrics make trust easy to evaluate.</p>
169
+ </article>
170
+ <article class="card">
171
+ <h3>Low-friction contact</h3>
172
+ <p>The form asks only for the details needed to continue.</p>
173
+ </article>
174
+ </div>
175
+ </div>
176
+ </section>`;
177
+ case "proof":
178
+ return `<section class="section proof" aria-labelledby="proof-title">
179
+ <div class="section__inner proof__grid">
180
+ <div>
181
+ <p class="eyebrow">Social proof</p>
182
+ <h2 id="proof-title">Built for teams that need momentum.</h2>
183
+ </div>
184
+ <blockquote>
185
+ <p>"The page gave us a cleaner way to explain the offer and capture serious leads."</p>
186
+ <cite>Marina Silva, Growth Lead</cite>
187
+ </blockquote>
188
+ </div>
189
+ </section>`;
190
+ case "contact":
191
+ return `<section class="section contact" id="contact" aria-labelledby="contact-title">
192
+ <div class="section__inner contact__grid">
193
+ <div>
194
+ <p class="eyebrow">Contact</p>
195
+ <h2 id="contact-title">Tell us what you want to launch.</h2>
196
+ <p class="lede">Share a few details and we will follow up with next steps.</p>
197
+ </div>
198
+ <form class="contact-form">
199
+ <label>
200
+ Name
201
+ <input name="name" type="text" autocomplete="name" required />
202
+ </label>
203
+ <label>
204
+ Email
205
+ <input name="email" type="email" autocomplete="email" required />
206
+ </label>
207
+ <label>
208
+ Message
209
+ <textarea name="message" rows="4" required></textarea>
210
+ </label>
211
+ <button class="button" type="submit">Send message</button>
212
+ </form>
213
+ </div>
214
+ </section>`;
215
+ }
216
+ }
217
+ function renderCss(args) {
218
+ const { grid, controlRadius, containerRadius, includeProof, includeContact } = args;
219
+ return `:root {
220
+ --space-grid: ${grid}px;
221
+ --color-bg: #f7fafc;
222
+ --color-text: #111827;
223
+ --color-muted: #4b5563;
224
+ --color-surface: #ffffff;
225
+ --color-accent: #0891b2;
226
+ --color-accent-strong: #0e7490;
227
+ --color-border: #d1d5db;
228
+ }
229
+
230
+ * {
231
+ box-sizing: border-box;
232
+ }
233
+
234
+ body {
235
+ margin: 0;
236
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
237
+ color: var(--color-text);
238
+ background: var(--color-bg);
239
+ }
240
+
241
+ a {
242
+ color: inherit;
243
+ }
244
+
245
+ .section {
246
+ padding: ${grid * 8}px ${grid * 3}px;
247
+ }
248
+
249
+ .section__inner {
250
+ width: 100%;
251
+ max-width: ${grid * 140}px;
252
+ margin: 0 auto;
253
+ }
254
+
255
+ .hero {
256
+ min-height: ${grid * 80}px;
257
+ display: grid;
258
+ align-items: center;
259
+ }
260
+
261
+ .hero__grid,
262
+ .contact__grid,
263
+ .proof__grid {
264
+ display: grid;
265
+ grid-template-columns: minmax(0, 1.2fr) minmax(${grid * 36}px, 0.8fr);
266
+ gap: ${grid * 6}px;
267
+ align-items: center;
268
+ }
269
+
270
+ .eyebrow {
271
+ margin: 0 0 ${grid * 2}px;
272
+ color: var(--color-accent-strong);
273
+ font-size: 0.875rem;
274
+ font-weight: 700;
275
+ text-transform: uppercase;
276
+ }
277
+
278
+ h1,
279
+ h2,
280
+ h3,
281
+ p {
282
+ margin-top: 0;
283
+ }
284
+
285
+ h1 {
286
+ max-width: ${grid * 88}px;
287
+ margin-bottom: ${grid * 3}px;
288
+ font-size: clamp(2.5rem, 6vw, 4.5rem);
289
+ line-height: 1;
290
+ }
291
+
292
+ h2 {
293
+ max-width: ${grid * 80}px;
294
+ margin-bottom: ${grid * 4}px;
295
+ font-size: clamp(2rem, 4vw, 3rem);
296
+ line-height: 1.1;
297
+ }
298
+
299
+ h3 {
300
+ margin-bottom: ${grid}px;
301
+ font-size: 1.125rem;
302
+ }
303
+
304
+ .lede {
305
+ max-width: ${grid * 72}px;
306
+ margin-bottom: ${grid * 4}px;
307
+ color: var(--color-muted);
308
+ font-size: 1.125rem;
309
+ line-height: 1.6;
310
+ }
311
+
312
+ .button {
313
+ display: inline-flex;
314
+ min-height: ${grid * 6}px;
315
+ align-items: center;
316
+ justify-content: center;
317
+ padding: ${grid}px ${grid * 3}px;
318
+ border: 0;
319
+ border-radius: ${controlRadius}px;
320
+ background: var(--color-accent);
321
+ color: var(--color-surface);
322
+ font: inherit;
323
+ font-weight: 700;
324
+ text-decoration: none;
325
+ cursor: pointer;
326
+ }
327
+
328
+ .button:hover {
329
+ background: var(--color-accent-strong);
330
+ }
331
+
332
+ .button:focus-visible,
333
+ input:focus-visible,
334
+ textarea:focus-visible {
335
+ outline: ${controlRadius}px solid var(--color-accent);
336
+ outline-offset: ${grid / 2}px;
337
+ }
338
+
339
+ .hero__panel,
340
+ .card,
341
+ blockquote,
342
+ .contact-form {
343
+ border: 1px solid var(--color-border);
344
+ border-radius: ${containerRadius}px;
345
+ background: var(--color-surface);
346
+ box-shadow: 0 ${grid}px ${grid * 3}px rgba(17, 24, 39, 0.08);
347
+ }
348
+
349
+ .hero__panel {
350
+ display: grid;
351
+ gap: ${grid * 2}px;
352
+ padding: ${grid * 4}px;
353
+ }
354
+
355
+ .hero__panel strong {
356
+ font-size: 2rem;
357
+ }
358
+
359
+ .feature-grid {
360
+ display: grid;
361
+ grid-template-columns: repeat(3, minmax(0, 1fr));
362
+ gap: ${grid * 3}px;
363
+ }
364
+
365
+ .card {
366
+ padding: ${grid * 3}px;
367
+ }
368
+ ${includeProof ? proofCss(grid) : ""}
369
+ ${includeContact ? contactCss(grid) : ""}
370
+ @media (max-width: ${grid * 96}px) {
371
+ .hero__grid,
372
+ .contact__grid,
373
+ .proof__grid,
374
+ .feature-grid {
375
+ grid-template-columns: 1fr;
376
+ }
377
+
378
+ .section {
379
+ padding: ${grid * 6}px ${grid * 2}px;
380
+ }
381
+ }
382
+ `;
383
+ }
384
+ function proofCss(grid) {
385
+ return `
386
+ .proof {
387
+ background: var(--color-surface);
388
+ }
389
+
390
+ blockquote {
391
+ margin: 0;
392
+ padding: ${grid * 3}px;
393
+ }
394
+
395
+ blockquote p {
396
+ margin-bottom: ${grid * 2}px;
397
+ font-size: 1.25rem;
398
+ line-height: 1.5;
399
+ }
400
+
401
+ cite {
402
+ color: var(--color-muted);
403
+ font-style: normal;
404
+ }
405
+ `;
406
+ }
407
+ function contactCss(grid) {
408
+ return `
409
+ .contact-form {
410
+ display: grid;
411
+ gap: ${grid * 2}px;
412
+ padding: ${grid * 3}px;
413
+ }
414
+
415
+ label {
416
+ display: grid;
417
+ gap: ${grid}px;
418
+ font-weight: 700;
419
+ }
420
+
421
+ input,
422
+ textarea {
423
+ width: 100%;
424
+ padding: ${grid}px ${grid * 2}px;
425
+ border: 1px solid var(--color-border);
426
+ border-radius: 2px;
427
+ font: inherit;
428
+ }
429
+ `;
430
+ }
431
+ function indent(input, spaces) {
432
+ const prefix = " ".repeat(spaces);
433
+ return input
434
+ .split("\n")
435
+ .map((line) => (line.length > 0 ? `${prefix}${line}` : line))
436
+ .join("\n");
437
+ }
@@ -46,7 +46,8 @@ export async function igniteCommand(projectName = "whale-project", options = {})
46
46
  };
47
47
  }
48
48
  // ---- Step 1: scaffold workspace -------------------------------------------
49
- const target = await initCommand(projectName, { config, silent: true });
49
+ const resolvedProjectName = config.projectName ?? projectName;
50
+ const target = await initCommand(resolvedProjectName, { config, silent: true });
50
51
  const targetRel = path.relative(process.cwd(), target) || ".";
51
52
  console.log(ui.ok(`Workspace created at ${ui.path(targetRel)}`));
52
53
  // ---- Step 1b: record free-text project intent as first decision ------------
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ import { adoptCommand } from "./commands/adopt.js";
14
14
  import { adoptReviewCommand, adoptStatusCommand } from "./commands/adoptReview.js";
15
15
  import { insightsCommand, insightsDriftReviewCommand } from "./commands/insights.js";
16
16
  import { createComponentCommand } from "./commands/createComponent.js";
17
+ import { createLandingCommand } from "./commands/createLanding.js";
17
18
  import { seleneDescribeCommand, seleneAuditCommand, seleneSuggestCommand, seleneApplyCommand, seleneStatusCommand, seleneCacheClearCommand } from "./commands/selene.js";
18
19
  import { mcpServeCommand, mcpConfigCommand } from "./commands/mcp.js";
19
20
  import { watchCommand } from "./commands/watch.js";
@@ -172,6 +173,15 @@ create
172
173
  .option("--no-register", "Don't add this component to intelligence/components.json")
173
174
  .option("--no-sync", "Don't regenerate CLAUDE.md / wiki after creating")
174
175
  .action((name, opts) => createComponentCommand(name, opts));
176
+ create
177
+ .command("landing")
178
+ .description("Generate a static HTML/CSS landing page using current foundations.")
179
+ .option("--sections <list>", "Comma-separated sections (hero,features,proof,contact)")
180
+ .option("--out-dir <path>", "Output directory relative to project root (default: src)")
181
+ .option("--force", "Overwrite generated files if they already exist")
182
+ .option("--no-register", "Don't add generated sections to intelligence/components.json")
183
+ .option("--no-sync", "Don't regenerate CLAUDE.md / wiki after creating")
184
+ .action((opts) => createLandingCommand(opts));
175
185
  const adopt = program
176
186
  .command("adopt [target]")
177
187
  .description("Scan an existing project and propose components, foundations and decisions.")
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const PACKAGE_VERSION = "1.3.1";
1
+ export const PACKAGE_VERSION = "1.3.3";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whale-igniter",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
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",