start-vibing-stacks 2.3.0 → 2.4.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/dist/detector.js CHANGED
@@ -25,10 +25,6 @@ const STACK_SIGNATURES = [
25
25
  { id: 'python', files: ['pyproject.toml'], weight: 90, reason: 'pyproject.toml detected' },
26
26
  { id: 'python', files: ['Pipfile'], weight: 85, reason: 'Pipfile detected' },
27
27
  { id: 'python', files: ['setup.py'], weight: 75, reason: 'setup.py detected' },
28
- // Rust
29
- { id: 'rust', files: ['Cargo.toml'], weight: 95, reason: 'Cargo.toml detected' },
30
- // Go
31
- { id: 'go', files: ['go.mod'], weight: 95, reason: 'go.mod detected' },
32
28
  ];
33
29
  export function detectProject(projectDir) {
34
30
  const result = {
@@ -95,8 +91,26 @@ export function detectNodeFramework(projectDir) {
95
91
  return 'nextjs';
96
92
  if (existsSync(join(projectDir, 'nuxt.config.ts')))
97
93
  return 'nuxt';
98
- if (existsSync(join(projectDir, 'astro.config.mjs')))
94
+ if (existsSync(join(projectDir, 'astro.config.mjs')) || existsSync(join(projectDir, 'astro.config.ts')))
99
95
  return 'astro';
96
+ if (existsSync(join(projectDir, 'svelte.config.js')) || existsSync(join(projectDir, 'svelte.config.ts')))
97
+ return 'svelte';
98
+ const pkgPath = join(projectDir, 'package.json');
99
+ if (existsSync(pkgPath)) {
100
+ try {
101
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
102
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
103
+ if (deps['express'])
104
+ return 'express';
105
+ if (deps['fastify'])
106
+ return 'fastify';
107
+ if (deps['hono'])
108
+ return 'hono';
109
+ }
110
+ catch {
111
+ // ignore parse errors
112
+ }
113
+ }
100
114
  return null;
101
115
  }
102
116
  export function detectPythonFramework(projectDir) {
package/dist/index.js CHANGED
@@ -6,7 +6,8 @@
6
6
  * Detects project stack, validates requirements, and configures agents.
7
7
  */
8
8
  import { existsSync, readFileSync } from 'fs';
9
- import { join, basename } from 'path';
9
+ import { join, basename, dirname, resolve } from 'path';
10
+ import { fileURLToPath } from 'url';
10
11
  import inquirer from 'inquirer';
11
12
  import chalk from 'chalk';
12
13
  import * as ui from './ui.js';
@@ -15,6 +16,10 @@ import { autoInstall, installComposer, installClaudeCode } from './installer.js'
15
16
  import { loadStackConfig, setupProject } from './setup.js';
16
17
  import { selectMcpServers, installMcpServers } from './mcp.js';
17
18
  import { scanProjectStandards } from './scanner.js';
19
+ const __cli_filename = fileURLToPath(import.meta.url);
20
+ const __cli_dirname = dirname(__cli_filename);
21
+ const CLI_ROOT = resolve(__cli_dirname, '..');
22
+ const PKG_VERSION = JSON.parse(readFileSync(join(CLI_ROOT, 'package.json'), 'utf8')).version;
18
23
  // =============================================================================
19
24
  // CLI Arguments
20
25
  // =============================================================================
@@ -28,7 +33,7 @@ const FLAGS = {
28
33
  version: args.includes('--version') || args.includes('-v'),
29
34
  };
30
35
  if (FLAGS.version) {
31
- console.log('1.0.0');
36
+ console.log(PKG_VERSION);
32
37
  process.exit(0);
33
38
  }
34
39
  if (FLAGS.help) {
@@ -158,12 +163,15 @@ async function main() {
158
163
  : stackId === 'python'
159
164
  ? detectPythonFramework(projectDir)
160
165
  : null;
166
+ const defaultFramework = detectedFramework ||
167
+ stackConfig.frameworks.find((f) => f.default)?.id ||
168
+ undefined;
161
169
  const { framework } = await inquirer.prompt([
162
170
  {
163
171
  type: 'list',
164
172
  name: 'framework',
165
173
  message: 'Select framework:',
166
- default: detectedFramework || undefined,
174
+ default: defaultFramework,
167
175
  choices: stackConfig.frameworks.map((f) => ({
168
176
  name: `${f.icon} ${f.name}`,
169
177
  value: f.id,
@@ -185,11 +193,13 @@ async function main() {
185
193
  ]);
186
194
  // ─── Step 5: Frontend ──────────────────────────────────────────────────
187
195
  const compatibleFrontends = stackConfig.frontendOptions.filter((f) => !f.frameworks || f.frameworks.includes(framework));
196
+ const defaultFrontend = compatibleFrontends.find((f) => f.default)?.id || undefined;
188
197
  const { frontend } = await inquirer.prompt([
189
198
  {
190
199
  type: 'list',
191
200
  name: 'frontend',
192
201
  message: 'Frontend included?',
202
+ default: defaultFrontend,
193
203
  choices: compatibleFrontends.map((f) => ({
194
204
  name: `${f.icon} ${f.name}`,
195
205
  value: f.id,
@@ -249,12 +259,14 @@ async function main() {
249
259
  }
250
260
  }
251
261
  // ─── Step 7: Show Summary & Confirm ────────────────────────────────────
262
+ const selectedFrontend = compatibleFrontends.find((f) => f.id === frontend);
252
263
  const config = {
253
264
  name: projectName,
254
265
  stack: stackId,
255
266
  framework,
256
267
  database,
257
268
  frontend,
269
+ frontendSkillsDir: selectedFrontend?.skillsDir,
258
270
  deploy,
259
271
  path: projectDir,
260
272
  createdAt: new Date().toISOString(),
package/dist/scanner.js CHANGED
@@ -102,6 +102,8 @@ const NPM_PACKAGES = {
102
102
  // Media
103
103
  'uploadthing': { category: 'media', name: 'UploadThing file uploads' },
104
104
  'sharp': { category: 'media', name: 'Sharp image processing' },
105
+ // Design System
106
+ 'preline': { category: 'ui', name: 'Preline UI design system' },
105
107
  };
106
108
  function readFileIfExists(path) {
107
109
  if (!existsSync(path))
@@ -424,8 +426,14 @@ function scanProjectFiles(projectDir) {
424
426
  patterns.push({ category: 'quality', name: 'Prettier config present', confidence: 100 });
425
427
  }
426
428
  // Environment files (works for both PHP and Node.js projects)
427
- const envContent = readFileIfExists(join(projectDir, '.env.example')) ||
428
- readFileIfExists(join(projectDir, '.env'));
429
+ const envFiles = ['.env.example', '.env', '.env.local', '.env.development', '.env.production'];
430
+ let envContent = null;
431
+ for (const envFile of envFiles) {
432
+ const content = readFileIfExists(join(projectDir, envFile));
433
+ if (content) {
434
+ envContent = (envContent || '') + '\n' + content;
435
+ }
436
+ }
429
437
  if (envContent) {
430
438
  if (/REDIS_HOST|REDIS_URL/i.test(envContent)) {
431
439
  patterns.push({ category: 'cache', name: 'Redis configured', confidence: 85 });
@@ -442,6 +450,19 @@ function scanProjectFiles(projectDir) {
442
450
  if (/STRIPE_SECRET|STRIPE_KEY/i.test(envContent)) {
443
451
  patterns.push({ category: 'billing', name: 'Stripe payments configured', confidence: 85 });
444
452
  }
453
+ // Security: detect NEXT_PUBLIC_ with sensitive values
454
+ const sensitivePublicEnv = envContent.match(/^NEXT_PUBLIC_\w*(SECRET|TOKEN|PRIVATE|PASSWORD|CREDENTIAL|OPENAI|API_KEY|DATABASE)\w*\s*=/gmi);
455
+ if (sensitivePublicEnv) {
456
+ for (const match of sensitivePublicEnv) {
457
+ const varName = match.split('=')[0].trim();
458
+ patterns.push({
459
+ category: 'security',
460
+ name: `EXPOSED SECRET: ${varName} is public (visible in browser)`,
461
+ confidence: 100,
462
+ detail: `${varName} uses NEXT_PUBLIC_ prefix which embeds the value in the client JS bundle. Remove NEXT_PUBLIC_ and access via Route Handler or Server Action.`,
463
+ });
464
+ }
465
+ }
445
466
  }
446
467
  if (patterns.length === 0)
447
468
  return null;
package/dist/setup.js CHANGED
@@ -7,6 +7,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSy
7
7
  import { join, dirname, resolve } from 'path';
8
8
  import { fileURLToPath } from 'url';
9
9
  import ora from 'ora';
10
+ import * as ui from './ui.js';
10
11
  const __filename = fileURLToPath(import.meta.url);
11
12
  const __dirname = dirname(__filename);
12
13
  const PACKAGE_ROOT = resolve(__dirname, '..');
@@ -89,7 +90,8 @@ export async function setupProject(projectDir, config, options = {}) {
89
90
  copyDirRecursive(stackConfigDir, join(claudeDir, 'config'), options.force);
90
91
  // 8. Copy frontend-specific skills if applicable
91
92
  if (config.frontend && config.frontend !== 'none') {
92
- const frontendDir = join(PACKAGE_ROOT, 'stacks', 'frontend', config.frontend);
93
+ const frontendId = config.frontendSkillsDir || config.frontend;
94
+ const frontendDir = join(PACKAGE_ROOT, 'stacks', 'frontend', frontendId);
93
95
  if (existsSync(frontendDir)) {
94
96
  const feSkillCount = copyDirRecursive(join(frontendDir, 'skills'), join(claudeDir, 'skills'), options.force);
95
97
  spinner.text = `Loaded ${feSkillCount} frontend skills`;
@@ -286,6 +288,20 @@ export async function setupProject(projectDir, config, options = {}) {
286
288
  settings.quality_gates = gates;
287
289
  }
288
290
  writeFileSync(join(claudeDir, 'settings.json'), JSON.stringify(settings, null, '\t'));
291
+ // 14. Detect Preline and suggest official agent skills
292
+ const projectPkgPath = join(projectDir, 'package.json');
293
+ if (existsSync(projectPkgPath)) {
294
+ try {
295
+ const projectPkg = JSON.parse(readFileSync(projectPkgPath, 'utf8'));
296
+ const allDeps = { ...projectPkg.dependencies, ...projectPkg.devDependencies };
297
+ if (allDeps['preline'] && !existsSync(join(claudeDir, 'skills', 'preline-theme-generator'))) {
298
+ ui.info('Preline UI detected — run: npx skills add htmlstreamofficial/preline');
299
+ }
300
+ }
301
+ catch {
302
+ // package.json parse error, skip
303
+ }
304
+ }
289
305
  spinner.succeed(`Setup complete: ${agentCount} agents, ${sharedSkillCount + stackSkillCount} skills, ${hookCount} hooks`);
290
306
  return true;
291
307
  }
package/dist/types.d.ts CHANGED
@@ -32,6 +32,7 @@ export interface FrameworkOption {
32
32
  icon: string;
33
33
  detectFiles?: string[];
34
34
  skills?: string[];
35
+ default?: boolean;
35
36
  extra?: Record<string, unknown>;
36
37
  }
37
38
  export interface DatabaseOption {
@@ -44,6 +45,8 @@ export interface FrontendOption {
44
45
  name: string;
45
46
  icon: string;
46
47
  frameworks?: string[];
48
+ skillsDir?: string;
49
+ default?: boolean;
47
50
  }
48
51
  export interface DeployTarget {
49
52
  id: string;
@@ -67,6 +70,7 @@ export interface ProjectConfig {
67
70
  framework: string;
68
71
  database: string;
69
72
  frontend: string;
73
+ frontendSkillsDir?: string;
70
74
  deploy: string;
71
75
  path: string;
72
76
  createdAt: string;
package/dist/ui.js CHANGED
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Start Vibing Stacks — Terminal UI
3
3
  */
4
+ import { readFileSync } from 'fs';
5
+ import { join, dirname, resolve } from 'path';
6
+ import { fileURLToPath } from 'url';
4
7
  import chalk from 'chalk';
5
- const VERSION = '2.2.0';
6
- const gradient = (text) => {
7
- const colors = [chalk.hex('#FF6B6B'), chalk.hex('#FF8E53'), chalk.hex('#FFBD2E'), chalk.hex('#48BB78'), chalk.hex('#4299E1'), chalk.hex('#9F7AEA')];
8
- return text.split('').map((c, i) => colors[i % colors.length](c)).join('');
9
- };
8
+ const __ui_filename = fileURLToPath(import.meta.url);
9
+ const __ui_dirname = dirname(__ui_filename);
10
+ const VERSION = JSON.parse(readFileSync(join(resolve(__ui_dirname, '..'), 'package.json'), 'utf8')).version;
10
11
  export const LOGO = `
11
12
  ${chalk.hex('#9F7AEA')(' ╔═══════════════════════════════════════════════╗')}
12
13
  ${chalk.hex('#9F7AEA')(' ║')} ${chalk.hex('#9F7AEA')('║')}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "start-vibing-stacks",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "AI-powered multi-stack dev workflow for Claude Code. Supports PHP, Node.js, Python and more.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -5,17 +5,39 @@
5
5
  },
6
6
  "sensitivePatterns": {
7
7
  "forbidden": [
8
- "eval(",
9
- "exec(",
10
- "system(",
11
- "passthru(",
12
- "shell_exec(",
13
8
  "password:",
14
9
  "passwordHash",
15
10
  "apiKey:",
16
11
  "secret:"
17
12
  ]
18
13
  },
14
+ "envExposure": {
15
+ "$comment": "NEXT_PUBLIC_ vars are embedded in browser JS bundle. These patterns indicate secrets being exposed to the client.",
16
+ "forbiddenPublicEnvPatterns": [
17
+ "NEXT_PUBLIC_.*SECRET",
18
+ "NEXT_PUBLIC_.*TOKEN",
19
+ "NEXT_PUBLIC_.*PRIVATE",
20
+ "NEXT_PUBLIC_.*PASSWORD",
21
+ "NEXT_PUBLIC_.*CREDENTIAL"
22
+ ],
23
+ "forbiddenPublicEnvExact": [
24
+ "NEXT_PUBLIC_OPENAI_KEY",
25
+ "NEXT_PUBLIC_OPENAI_API_KEY",
26
+ "NEXT_PUBLIC_STRIPE_SECRET",
27
+ "NEXT_PUBLIC_STRIPE_SECRET_KEY",
28
+ "NEXT_PUBLIC_DATABASE_URL",
29
+ "NEXT_PUBLIC_SUPABASE_SERVICE_KEY",
30
+ "NEXT_PUBLIC_FIREBASE_ADMIN_KEY"
31
+ ],
32
+ "safePublicEnvPatterns": [
33
+ "NEXT_PUBLIC_.*URL",
34
+ "NEXT_PUBLIC_.*ID",
35
+ "NEXT_PUBLIC_STRIPE_KEY",
36
+ "NEXT_PUBLIC_GA_",
37
+ "NEXT_PUBLIC_SENTRY_DSN"
38
+ ],
39
+ "rule": "API keys, secrets, and tokens MUST stay server-side. Use Route Handlers or Server Actions as proxy."
40
+ },
19
41
  "cookies": {
20
42
  "httpOnly": true,
21
43
  "secure": true,
@@ -10,7 +10,7 @@ Preline is a **semantic token-based design system** built on TailwindCSS. It pro
10
10
  - Theme generator for custom color schemes
11
11
  - Light + dark mode via `data-theme` + `.dark`
12
12
 
13
- ## Installation (Laravel + Inertia)
13
+ ## Installation
14
14
 
15
15
  ### Step 1: Install
16
16
 
@@ -21,7 +21,7 @@ npm install preline @tailwindcss/forms
21
21
  ### Step 2: CSS Config
22
22
 
23
23
  ```css
24
- /* resources/css/app.css */
24
+ /* src/app/globals.css (Next.js) or styles/globals.css */
25
25
  @import "tailwindcss";
26
26
 
27
27
  /* Preline — MUST be in this order */
@@ -31,39 +31,35 @@ npm install preline @tailwindcss/forms
31
31
  @import "./node_modules/preline/themes/theme.css"; /* Base theme */
32
32
  ```
33
33
 
34
- ### Step 3: Vite Config
34
+ ### Step 3: Init Preline on Route Changes (MANDATORY)
35
35
 
36
- ```js
37
- // vite.config.js
38
- import { defineConfig } from 'vite';
39
- import laravel from 'laravel-vite-plugin';
40
- import react from '@vitejs/plugin-react';
36
+ ```tsx
37
+ // src/components/PrelineInit.tsx
38
+ 'use client';
41
39
 
42
- export default defineConfig({
43
- plugins: [
44
- laravel({ input: ['resources/css/app.css', 'resources/js/app.tsx'], refresh: true }),
45
- react(),
46
- ],
47
- });
48
- ```
40
+ import { usePathname } from 'next/navigation';
41
+ import { useEffect } from 'react';
49
42
 
50
- ### Step 4: Init Preline in Inertia (MANDATORY)
43
+ export function PrelineInit() {
44
+ const pathname = usePathname();
51
45
 
52
- ```tsx
53
- // resources/js/app.tsx
54
- import { router } from '@inertiajs/react';
55
-
56
- // Re-init Preline components after every SPA navigation
57
- router.on('navigate', () => {
58
- setTimeout(() => {
59
- import('preline/preline').then(({ HSStaticMethods }) => {
60
- HSStaticMethods.autoInit();
61
- });
62
- }, 100);
63
- });
46
+ useEffect(() => {
47
+ const timer = setTimeout(() => {
48
+ import('preline/preline').then(({ HSStaticMethods }) => {
49
+ HSStaticMethods.autoInit();
50
+ });
51
+ }, 100);
52
+ return () => clearTimeout(timer);
53
+ }, [pathname]);
54
+
55
+ return null;
56
+ }
57
+
58
+ // Add to root layout:
59
+ // <PrelineInit />
64
60
  ```
65
61
 
66
- **Rule:** Without `HSStaticMethods.autoInit()`, dropdowns, modals, and accordions will NOT work after Inertia navigation.
62
+ **Rule:** Without `HSStaticMethods.autoInit()`, dropdowns, modals, and accordions will NOT work after client-side navigation.
67
63
 
68
64
  ## Templates & Components (840+ free)
69
65
 
@@ -80,11 +76,11 @@ router.on('navigate', () => {
80
76
 
81
77
  1. Browse https://preline.co/examples.html
82
78
  2. Click a block → copy the HTML/JSX
83
- 3. Adapt to React + Inertia:
84
- - Replace `<a href>` with `<Link href>` (Inertia)
79
+ 3. Adapt to React:
80
+ - Replace `<a href>` with Next.js `<Link href>` (from `next/link`)
85
81
  - Replace `class=` with `className=`
86
82
  - Add Preline `data-*` attributes for interactive components
87
- - Use `usePage().props` for dynamic data
83
+ - Pass data via props or fetch with TanStack Query / Server Components
88
84
 
89
85
  ### Example: Copy a Hero Block
90
86
 
@@ -245,7 +241,7 @@ function ThemeToggle() {
245
241
  }
246
242
  ```
247
243
 
248
- ## Using Components with React (Inertia)
244
+ ## Using Components with React
249
245
 
250
246
  ### Navbar
251
247
 
@@ -327,7 +323,7 @@ function ThemeToggle() {
327
323
 
328
324
  ```bash
329
325
  # Generate theme from config
330
- npx preline-theme-generator /tmp/config.json ./resources/css/themes/brand.css
326
+ npx preline-theme-generator /tmp/config.json ./src/styles/themes/brand.css
331
327
 
332
328
  # Config format:
333
329
  {
@@ -359,4 +355,4 @@ npx preline-theme-generator /tmp/config.json ./resources/css/themes/brand.css
359
355
  | Force HTML class changes | Theme activation via `data-theme` only |
360
356
  | Invent token names | Follow Preline's naming system |
361
357
  | `@apply` for component styles | React components with token classes |
362
- | Skip `HSStaticMethods.autoInit()` | Always re-init after Inertia navigation |
358
+ | Skip `HSStaticMethods.autoInit()` | Always re-init after client-side navigation |
@@ -5,16 +5,16 @@
5
5
  - **ReactJS >= 19** — MANDATORY
6
6
  - **TailwindCSS >= 4** — MANDATORY
7
7
 
8
- ## Translation Pattern
8
+ ## Label Constants Pattern
9
9
 
10
10
  ```tsx
11
- // ✅ Translations as CONST at the top, BEFORE hooks
11
+ // ✅ Labels as CONST at the top, BEFORE hooks
12
12
  const LABELS = {
13
- title: __('dashboard.title'),
14
- save: __('common.save'),
15
- cancel: __('common.cancel'),
16
- errorRequired: __('errors.field_required'),
17
- };
13
+ title: 'Dashboard',
14
+ save: 'Save',
15
+ cancel: 'Cancel',
16
+ errorRequired: 'This field is required',
17
+ } as const;
18
18
 
19
19
  export default function Dashboard() {
20
20
  const [data, setData] = useState(null);
@@ -22,14 +22,14 @@ export default function Dashboard() {
22
22
  return <h1>{LABELS.title}</h1>;
23
23
  }
24
24
 
25
- // ❌ NEVER call __() inside JSX (Hook violations)
26
- return <h1>{__('dashboard.title')}</h1>; // ❌
25
+ // ❌ NEVER scatter string literals across JSX
26
+ return <h1>Dashboard</h1>; // ❌ Duplicated, hard to maintain
27
27
  ```
28
28
 
29
29
  **Rules:**
30
- - Translations in `CONST` variables before state hooks
31
- - New strings add to `lang/en/*.php` and `lang/pt/*.php`
32
- - Error strings centralized in `lang/*/errors.php`
30
+ - Labels in `CONST` objects before state hooks for stable references
31
+ - For i18n projects, use `next-intl` or `i18next` same CONST pattern applies with `t()` calls
32
+ - Error strings centralized in a shared constants file
33
33
 
34
34
  ## Debug Logging
35
35
 
@@ -65,11 +65,11 @@ export default function Dashboard() {
65
65
 
66
66
  ```tsx
67
67
  // ═══════════════════════════════════════════
68
- // 1. TRANSLATIONS (before hooks)
68
+ // 1. LABELS (before hooks)
69
69
  // ═══════════════════════════════════════════
70
70
  const LABELS = {
71
- title: __('dashboard.title'),
72
- save: __('common.save'),
71
+ title: 'Dashboard',
72
+ save: 'Save',
73
73
  } as const;
74
74
 
75
75
  // ═══════════════════════════════════════════
@@ -156,7 +156,7 @@ import { cn } from '@/lib/utils';
156
156
  5. **Shared styles** — create a `styles.ts` file for cross-component constants
157
157
 
158
158
  ```tsx
159
- // resources/js/styles.ts — shared across components
159
+ // src/styles.ts — shared across components
160
160
  export const SHARED_STYLES = {
161
161
  page: 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8',
162
162
  btnPrimary: 'px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary-hover ...',
@@ -187,10 +187,10 @@ export default function Bad() {
187
187
  ## SVG Icons
188
188
 
189
189
  ```tsx
190
- // ✅ Separate files, import with ?react
191
- // resources/js/Icons/CheckIcon.svg
192
- import CheckIcon from '@/Icons/CheckIcon.svg?react';
193
- import { CheckIcon, AlertIcon } from '@/Icons';
190
+ // ✅ Separate files, import with ?react (Vite) or as components
191
+ // src/components/icons/CheckIcon.svg
192
+ import CheckIcon from '@/components/icons/CheckIcon.svg?react';
193
+ import { CheckIcon, AlertIcon } from '@/components/icons';
194
194
 
195
195
  // ❌ Inline SVG
196
196
  <svg viewBox="0 0 24 24">...</svg> // ❌ Bloats JSX