joycraft 0.6.10 → 0.6.12

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.
@@ -1,9 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- applyGitignoreProfile,
4
- ensureGitignoreEntry,
5
3
  getPackageVersion
6
- } from "./chunk-P45XNSGK.js";
4
+ } from "./chunk-IELXGWSH.js";
5
+ import {
6
+ PRIVATE_UNTRACK_COMMAND,
7
+ applyGitignoreProfile,
8
+ resolveGitignoreProfile,
9
+ validateGitignoreFlag
10
+ } from "./chunk-VIVJUY6J.js";
7
11
  import {
8
12
  CODEX_SKILLS,
9
13
  PI_AGENTS,
@@ -12,16 +16,14 @@ import {
12
16
  PI_SKILLS,
13
17
  SKILLS,
14
18
  TEMPLATES
15
- } from "./chunk-W6AHAE7X.js";
19
+ } from "./chunk-GM2T44P6.js";
16
20
  import {
17
- DEFAULT_GITIGNORE_PROFILE,
18
21
  LEGACY_VERSION_FILE,
19
- STATE_PATH,
20
22
  hashContent,
21
23
  readVersion,
22
24
  truncateHash,
23
25
  writeVersion
24
- } from "./chunk-PUYGEBLA.js";
26
+ } from "./chunk-TD65VH2W.js";
25
27
 
26
28
  // src/upgrade.ts
27
29
  import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync as mkdirSync2, rmSync as rmSync2, chmodSync } from "fs";
@@ -311,14 +313,12 @@ function migrateLegacyVersionFile(targetDir) {
311
313
  parsed = JSON.parse(readFileSync(legacyPath, "utf-8"));
312
314
  } catch {
313
315
  rmSync2(legacyPath, { force: true });
314
- ensureGitignoreEntry(targetDir, STATE_PATH);
315
316
  return true;
316
317
  }
317
318
  const version = typeof parsed.version === "string" ? parsed.version : getPackageVersion();
318
319
  const files = parsed.files && typeof parsed.files === "object" ? parsed.files : {};
319
320
  writeVersion(targetDir, version, files);
320
321
  rmSync2(legacyPath, { force: true });
321
- ensureGitignoreEntry(targetDir, STATE_PATH);
322
322
  return true;
323
323
  }
324
324
  function countLines(content) {
@@ -402,6 +402,7 @@ function runForcedMigration(projectDir) {
402
402
  }
403
403
  async function upgrade(dir, opts) {
404
404
  const targetDir = resolve(dir);
405
+ if (opts.gitignore !== void 0) validateGitignoreFlag(opts.gitignore);
405
406
  const cliCheck = await checkCliVersion();
406
407
  if (cliCheck.stale) {
407
408
  const pkgVersion2 = getPackageVersion();
@@ -427,8 +428,18 @@ async function upgrade(dir, opts) {
427
428
  const managedFiles = getManagedFiles();
428
429
  const installed = readVersion(targetDir);
429
430
  const installedHashes = installed?.files ?? {};
430
- const gitignoreProfile = installed?.gitignoreProfile ?? DEFAULT_GITIGNORE_PROFILE;
431
+ const resolvedProfile = await resolveGitignoreProfile({
432
+ flag: opts.gitignore,
433
+ persisted: installed?.gitignoreProfile,
434
+ interactive: process.stdin.isTTY === true && !opts.yes,
435
+ promptIntro: "\nJoycraft can now control how much of the harness is tracked in git."
436
+ });
437
+ const gitignoreProfile = resolvedProfile.profile;
431
438
  applyGitignoreProfile(targetDir, gitignoreProfile);
439
+ if (gitignoreProfile === "private" && installed?.gitignoreProfile !== "private") {
440
+ console.log("Gitignore profile: private. If harness files were already committed, untrack them with:");
441
+ console.log(` ${PRIVATE_UNTRACK_COMMAND}`);
442
+ }
432
443
  const changes = [];
433
444
  let upToDate = 0;
434
445
  for (const [relPath, newContent] of Object.entries(managedFiles)) {
@@ -452,6 +463,9 @@ async function upgrade(dir, opts) {
452
463
  }
453
464
  }
454
465
  if (changes.length === 0) {
466
+ if (resolvedProfile.decided && installed && installed.gitignoreProfile !== gitignoreProfile) {
467
+ writeVersion(targetDir, installed.version, installedHashes, gitignoreProfile);
468
+ }
455
469
  console.log("Already up to date.");
456
470
  return;
457
471
  }
@@ -499,7 +513,7 @@ async function upgrade(dir, opts) {
499
513
  newHashes[relPath] = hashContent(current);
500
514
  }
501
515
  }
502
- writeVersion(targetDir, pkgVersion, newHashes, gitignoreProfile);
516
+ writeVersion(targetDir, pkgVersion, newHashes, resolvedProfile.decided ? gitignoreProfile : void 0);
503
517
  const parts = [];
504
518
  if (updated > 0) parts.push(`Updated ${updated}`);
505
519
  if (skipped > 0) parts.push(`skipped ${skipped} (customized)`);
@@ -511,4 +525,4 @@ Upgrade complete: ${parts.join(", ")}.`);
511
525
  export {
512
526
  upgrade
513
527
  };
514
- //# sourceMappingURL=upgrade-7E5ZGYHP.js.map
528
+ //# sourceMappingURL=upgrade-33NLD24D.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/upgrade.ts","../src/migration.ts"],"sourcesContent":["import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync, chmodSync } from 'node:fs';\nimport { join, dirname, resolve } from 'node:path';\nimport { createInterface } from 'node:readline';\nimport { readVersion, writeVersion, hashContent, truncateHash, LEGACY_VERSION_FILE } from './version.js';\nimport { applyGitignoreProfile, resolveGitignoreProfile, validateGitignoreFlag, PRIVATE_UNTRACK_COMMAND } from './gitignore.js';\nimport { SKILLS, TEMPLATES, CODEX_SKILLS, PI_SKILLS, PI_SCRIPTS, PI_EXTENSIONS, PI_AGENTS } from './bundled-files.js';\nimport { getPackageVersion } from './package-version.js';\nimport { planMigration, applyMigration, type MigrationPlan } from './migration.js';\n\nfunction isStaleVersion(current: string, latest: string): boolean {\n const currentParts = current.split('.').map(Number);\n const latestParts = latest.split('.').map(Number);\n const len = Math.max(currentParts.length, latestParts.length);\n for (let i = 0; i < len; i++) {\n const c = currentParts[i] ?? 0;\n const l = latestParts[i] ?? 0;\n if (c < l) return true;\n if (c > l) return false;\n }\n return false;\n}\n\nasync function checkCliVersion(): Promise<{ stale: boolean; latest?: string }> {\n try {\n const pkgVersion = getPackageVersion();\n const res = await fetch('https://registry.npmjs.org/joycraft/latest', {\n signal: AbortSignal.timeout(3000)\n });\n if (!res.ok) return { stale: false };\n const data = (await res.json()) as { version: string };\n if (isStaleVersion(pkgVersion, data.version)) {\n return { stale: true, latest: data.version };\n }\n } catch {\n // Silent fallback on network errors or missing version\n }\n return { stale: false };\n}\n\nexport interface UpgradeOptions {\n yes: boolean;\n /** Raw --gitignore value from the CLI, if provided. Validated in upgrade(). */\n gitignore?: string;\n}\n\ninterface FileChange {\n relativePath: string;\n absolutePath: string;\n newContent: string;\n kind: 'new' | 'updated' | 'customized';\n}\n\nfunction getManagedFiles(): Record<string, string> {\n const files: Record<string, string> = {};\n for (const [name, content] of Object.entries(SKILLS)) {\n const skillName = name.replace(/\\.md$/, '');\n files[join('.claude', 'skills', skillName, 'SKILL.md')] = content;\n }\n for (const [name, content] of Object.entries(TEMPLATES)) {\n files[join('docs', 'templates', name)] = content;\n }\n for (const [name, content] of Object.entries(CODEX_SKILLS)) {\n const skillName = name.replace(/\\.md$/, '');\n files[join('.agents', 'skills', skillName, 'SKILL.md')] = content;\n }\n for (const [name, content] of Object.entries(PI_SKILLS)) {\n const skillName = name.replace(/\\.md$/, '');\n files[join('.pi', 'skills', skillName, 'SKILL.md')] = content;\n }\n for (const [name, content] of Object.entries(PI_SCRIPTS)) {\n files[join('.pi', 'scripts', 'joycraft', name)] = content;\n }\n for (const [name, content] of Object.entries(PI_EXTENSIONS)) {\n files[join('.pi', 'extensions', name)] = content;\n }\n for (const [name, content] of Object.entries(PI_AGENTS)) {\n files[join('.pi', 'agents', name)] = content;\n }\n return files;\n}\n\n// Deprecated skill names from previous versions of Joycraft.\n// These get removed during upgrade to prevent stale slash commands.\nconst DEPRECATED_SKILL_DIRS = [\n // Pre-rebrand names\n 'joysmith', // pre-rebrand main skill\n 'joysmith-assess', // merged into joycraft-tune\n 'joysmith-upgrade', // merged into joycraft-tune\n // Pre-namespace names (bare names without joycraft- prefix)\n 'tune', // now /joycraft-tune\n 'tune-assess', // merged into joycraft-tune\n 'tune-upgrade', // merged into joycraft-tune\n 'joy', // merged into joycraft-tune\n 'interview', // now /joycraft-interview\n 'new-feature', // now /joycraft-new-feature\n 'decompose', // now /joycraft-decompose\n 'session-end', // now /joycraft-session-end\n];\n\n// Flat .md files from the pre-directory skill format\nconst DEPRECATED_SKILL_FILES = [\n 'tune.md',\n 'joy.md',\n 'joysmith.md',\n 'joysmith-assess.md',\n 'joysmith-upgrade.md',\n 'tune-assess.md',\n 'tune-upgrade.md',\n 'interview.md',\n 'new-feature.md',\n 'decompose.md',\n 'session-end.md',\n];\n\nfunction cleanupDeprecatedSkills(targetDir: string): number {\n const skillsDir = join(targetDir, '.claude', 'skills');\n if (!existsSync(skillsDir)) return 0;\n\n let removed = 0;\n\n // Remove deprecated directories\n for (const name of DEPRECATED_SKILL_DIRS) {\n const dir = join(skillsDir, name);\n if (existsSync(dir)) {\n rmSync(dir, { recursive: true, force: true });\n removed++;\n }\n }\n\n // Remove flat .md files from pre-directory format\n for (const name of DEPRECATED_SKILL_FILES) {\n const file = join(skillsDir, name);\n if (existsSync(file)) {\n rmSync(file);\n removed++;\n }\n }\n\n return removed;\n}\n\n/**\n * Self-heal projects inited by an older Joycraft that wrote state to the repo\n * root (`.joycraft-version`). Reads the legacy file, re-writes it to the hidden\n * `.claude/.joycraft/state.json` location, and deletes the root file. No-op\n * when no legacy root file exists.\n *\n * Gitignore handling is deliberately NOT done here: the profile isn't resolved\n * yet at this point in upgrade(), and applyGitignoreProfile later in the same\n * run covers the state entry (shared) or the whole .claude/ tree (private) —\n * writing the state entry here would leave a dead line under `private`.\n *\n * Runs BEFORE the managed-file diff so the recorded-original hashes are\n * available at the new location for the same run's 3-way comparison. The hidden\n * state's own write truncates the (possibly full-length legacy) hashes, so the\n * comparison stays consistent — see the truncateHash() call in the diff loop.\n */\nfunction migrateLegacyVersionFile(targetDir: string): boolean {\n const legacyPath = join(targetDir, LEGACY_VERSION_FILE);\n if (!existsSync(legacyPath)) return false;\n\n let parsed: { version?: unknown; files?: unknown };\n try {\n parsed = JSON.parse(readFileSync(legacyPath, 'utf-8'));\n } catch {\n // Corrupt legacy file: treat as no usable baseline. Remove it so the root\n // stops being polluted; upgrade then proceeds with no recorded-original\n // (every changed file becomes \"customized\" — safe, never silently wrong).\n rmSync(legacyPath, { force: true });\n return true;\n }\n\n const version = typeof parsed.version === 'string' ? parsed.version : getPackageVersion();\n const files =\n parsed.files && typeof parsed.files === 'object'\n ? (parsed.files as Record<string, string>)\n : {};\n\n // writeVersion targets the new hidden path and truncates the hashes. A\n // gitignoreProfile already persisted in the hidden state is preserved\n // (writeVersion keeps it when the argument is omitted).\n writeVersion(targetDir, version, files);\n rmSync(legacyPath, { force: true });\n return true;\n}\n\nfunction countLines(content: string): number {\n return content.split('\\n').length;\n}\n\nfunction ensureScriptExecutable(absolutePath: string): void {\n // Joycraft shell scripts in .pi/scripts/joycraft/ must be executable.\n // README.md is the only file in that directory that should stay 644.\n if (absolutePath.includes('.pi/scripts/joycraft/') && !absolutePath.endsWith('README.md')) {\n try {\n chmodSync(absolutePath, 0o755);\n } catch {\n // non-fatal — permissions may be restricted\n }\n }\n}\n\nasync function askUser(question: string): Promise<boolean> {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n return new Promise((resolve) => {\n rl.question(`${question} [y/N] `, (answer) => {\n rl.close();\n resolve(answer.trim().toLowerCase() === 'y');\n });\n });\n}\n\nfunction printMigrationSummary(plan: MigrationPlan, projectDir: string): void {\n console.log('');\n console.log('Joycraft is migrating your docs/ to the new per-feature layout:');\n console.log('');\n\n const relTo = (p: string) => (p.startsWith(projectDir) ? p.slice(projectDir.length + 1) : p);\n\n // Group per-feature moves by feature folder for readability.\n // Bugfix-area moves are listed separately below.\n const featureMoves = plan.moves.filter(m => m.kind !== 'bugfix-dir');\n const bugfixMoves = plan.moves.filter(m => m.kind === 'bugfix-dir');\n\n const bySlug = new Map<string, typeof plan.moves>();\n for (const move of featureMoves) {\n const parts = relTo(move.to).split(/[\\\\/]/);\n // docs/features/<slug>/...\n const slug = parts.length >= 3 ? parts[2] : '(root)';\n if (!bySlug.has(slug)) bySlug.set(slug, []);\n bySlug.get(slug)!.push(move);\n }\n for (const [slug, moves] of bySlug) {\n console.log(` ${slug}/`);\n for (const move of moves) {\n console.log(` ${relTo(move.from)} → ${relTo(move.to)}`);\n }\n }\n\n if (bugfixMoves.length > 0) {\n console.log('');\n console.log(' Migrating bugfix areas:');\n for (const move of bugfixMoves) {\n console.log(` ${relTo(move.from)} → ${relTo(move.to)}`);\n }\n }\n console.log('');\n}\n\nfunction printMigrationBanner(): void {\n console.log('');\n console.log('Migration complete. See the README section \"Migration: Flat → Per-Feature Layout\"');\n console.log('for context on what changed and why. If your project is a git repo, run');\n console.log('`git status` to inspect the moves before committing.');\n console.log('');\n}\n\nfunction runForcedMigration(projectDir: string): void {\n const plan = planMigration(projectDir);\n if (plan.moves.length === 0 && plan.orphans.specsDirs.length === 0) {\n return; // No flat layout — silent no-op.\n }\n\n printMigrationSummary(plan, projectDir);\n const result = applyMigration(plan);\n\n // Abort threshold: if more than 50% of attempted moves failed, bail loudly.\n const attempted = result.applied + result.errors.length;\n if (attempted > 0 && result.errors.length / attempted > 0.5) {\n console.error('Migration failed for more than half of attempted moves. Aborting upgrade.');\n for (const e of result.errors) {\n console.error(` ${e.move.from} → ${e.move.to}: ${e.error}`);\n }\n throw new Error('Migration aborted: too many failures');\n }\n\n if (result.errors.length > 0) {\n console.log('Some moves had errors but upgrade will continue:');\n for (const e of result.errors) {\n console.log(` warn: ${e.move.from} → ${e.move.to}: ${e.error}`);\n }\n }\n\n printMigrationBanner();\n}\n\nexport async function upgrade(dir: string, opts: UpgradeOptions): Promise<void> {\n const targetDir = resolve(dir);\n\n // Validate the --gitignore flag before any side effects (network check,\n // legacy migration, docs migration) so a typo'd value changes nothing.\n if (opts.gitignore !== undefined) validateGitignoreFlag(opts.gitignore);\n\n // Guard: if the CLI itself is out of date, warn and bail before comparing\n // project files against stale bundled content.\n const cliCheck = await checkCliVersion();\n if (cliCheck.stale) {\n const pkgVersion = getPackageVersion();\n console.log(`Joycraft CLI is out of date (you have ${pkgVersion}, latest is ${cliCheck.latest}).`);\n console.log('Update with: npm install -g joycraft');\n console.log('Then re-run: npx joycraft upgrade');\n return;\n }\n\n // Check if project was initialized. A project is \"initialized\" if it has the\n // hidden state, OR a known skill, OR a legacy root state file (pre-relocation).\n const hasLegacyState = existsSync(join(targetDir, LEGACY_VERSION_FILE));\n const hasSkill = existsSync(join(targetDir, '.claude', 'skills', 'joycraft-tune', 'SKILL.md'))\n || existsSync(join(targetDir, '.claude', 'skills', 'tune', 'SKILL.md'))\n || existsSync(join(targetDir, '.claude', 'skills', 'joy', 'SKILL.md'))\n || existsSync(join(targetDir, '.claude', 'skills', 'joysmith', 'SKILL.md'));\n\n if (!readVersion(targetDir) && !hasLegacyState && !hasSkill) {\n console.log('This project has not been initialized with Joycraft.');\n console.log('Run `npx joycraft init` first.');\n return;\n }\n\n // Relocate any legacy repo-root .joycraft-version → hidden state, BEFORE the\n // diff loop so the migrated recorded-original is used on this same run. No-op\n // when no legacy root file exists.\n migrateLegacyVersionFile(targetDir);\n\n // Clean up deprecated skill directories/files from older versions\n const deprecatedRemoved = cleanupDeprecatedSkills(targetDir);\n if (deprecatedRemoved > 0) {\n console.log(`Removed ${deprecatedRemoved} deprecated skill(s) from previous Joycraft versions.`);\n }\n\n // Forced migration: flat docs/{briefs,research,designs,specs/<feature>}/\n // → docs/features/<slug>/{brief,research,design,specs/}/\n // Runs before the managed-file diff loop so any new managed files end up\n // correctly placed in an already-migrated tree.\n runForcedMigration(targetDir);\n\n // Get current package version\n const pkgVersion = getPackageVersion();\n\n // If version matches exactly, check if any file content actually changed.\n // Re-read state AFTER migration so a just-migrated project's recorded-original\n // hashes (now at the hidden path) feed the comparison below.\n const managedFiles = getManagedFiles();\n const installed = readVersion(targetDir);\n const installedHashes = installed?.files ?? {};\n\n // Resolve the project's gitignore profile.\n // - --gitignore flag: explicit choice — the non-interactive way to set or\n // switch the profile on an existing project.\n // - Already chosen (init, or a prior upgrade): honor it silently.\n // - Never chosen (pre-feature project) + interactive: ask once, then persist\n // so this prompt never recurs. --yes suppresses the prompt — it promises\n // a fully unattended run.\n // - Never chosen + non-interactive: fall back to shared for this run only;\n // decided=false means it is never persisted, so the project stays\n // undecided and will be asked next time someone runs upgrade in a TTY.\n const resolvedProfile = await resolveGitignoreProfile({\n flag: opts.gitignore,\n persisted: installed?.gitignoreProfile,\n interactive: process.stdin.isTTY === true && !opts.yes,\n promptIntro: '\\nJoycraft can now control how much of the harness is tracked in git.',\n });\n const gitignoreProfile = resolvedProfile.profile;\n applyGitignoreProfile(targetDir, gitignoreProfile);\n if (gitignoreProfile === 'private' && installed?.gitignoreProfile !== 'private') {\n console.log('Gitignore profile: private. If harness files were already committed, untrack them with:');\n console.log(` ${PRIVATE_UNTRACK_COMMAND}`);\n }\n\n const changes: FileChange[] = [];\n let upToDate = 0;\n\n for (const [relPath, newContent] of Object.entries(managedFiles)) {\n const absPath = join(targetDir, relPath);\n const newHash = hashContent(newContent);\n\n if (!existsSync(absPath)) {\n // File doesn't exist locally — new file\n changes.push({ relativePath: relPath, absolutePath: absPath, newContent, kind: 'new' });\n continue;\n }\n\n const currentContent = readFileSync(absPath, 'utf-8');\n const currentHash = hashContent(currentContent);\n\n if (currentHash === newHash) {\n // Already matches the latest version\n upToDate++;\n continue;\n }\n\n // installedHashes are stored truncated; truncate the fresh hash to match.\n const originalHash = installedHashes[relPath];\n\n if (originalHash && truncateHash(currentHash) === originalHash) {\n // User hasn't modified the file — safe to auto-update\n changes.push({ relativePath: relPath, absolutePath: absPath, newContent, kind: 'updated' });\n } else {\n // User has customized this file (or no original hash recorded)\n changes.push({ relativePath: relPath, absolutePath: absPath, newContent, kind: 'customized' });\n }\n }\n\n if (changes.length === 0) {\n // Persist a freshly-decided profile (prompt answer or --gitignore switch)\n // even when no files changed, so the decision sticks. Never persist the\n // non-interactive fallback — an undecided project must stay undecided.\n if (resolvedProfile.decided && installed && installed.gitignoreProfile !== gitignoreProfile) {\n writeVersion(targetDir, installed.version, installedHashes, gitignoreProfile);\n }\n console.log('Already up to date.');\n return;\n }\n\n // Process changes\n let updated = 0;\n let skipped = 0;\n let added = 0;\n\n for (const change of changes) {\n if (change.kind === 'new') {\n // New Joycraft files are always auto-added — no prompt needed\n mkdirSync(dirname(change.absolutePath), { recursive: true });\n writeFileSync(change.absolutePath, change.newContent, 'utf-8');\n ensureScriptExecutable(change.absolutePath);\n added++;\n console.log(` + ${change.relativePath}`);\n } else if (change.kind === 'updated') {\n // Safe to auto-update — user hasn't touched the file\n writeFileSync(change.absolutePath, change.newContent, 'utf-8');\n ensureScriptExecutable(change.absolutePath);\n updated++;\n } else if (change.kind === 'customized') {\n const currentContent = readFileSync(change.absolutePath, 'utf-8');\n const currentLines = countLines(currentContent);\n const newLines = countLines(change.newContent);\n const diff = newLines - currentLines;\n const diffLabel = diff > 0 ? `+${diff} lines` : diff < 0 ? `${diff} lines` : 'same length';\n const label = `Customized: ${change.relativePath} (local: ${currentLines} lines, latest: ${newLines} lines, ${diffLabel})`;\n\n if (opts.yes) {\n writeFileSync(change.absolutePath, change.newContent, 'utf-8');\n updated++;\n } else {\n const accept = await askUser(`${label} — overwrite with latest?`);\n if (accept) {\n writeFileSync(change.absolutePath, change.newContent, 'utf-8');\n ensureScriptExecutable(change.absolutePath);\n updated++;\n } else {\n skipped++;\n }\n }\n }\n }\n\n // Write new version file with updated hashes\n const newHashes: Record<string, string> = {};\n for (const [relPath, content] of Object.entries(managedFiles)) {\n const absPath = join(targetDir, relPath);\n if (existsSync(absPath)) {\n const current = readFileSync(absPath, 'utf-8');\n newHashes[relPath] = hashContent(current);\n }\n }\n // Record the profile only when this run actually decided it; writeVersion\n // preserves an already-persisted profile when the argument is omitted, and an\n // undecided project stays undecided (so the one-time prompt can still fire).\n writeVersion(targetDir, pkgVersion, newHashes, resolvedProfile.decided ? gitignoreProfile : undefined);\n\n // Print summary\n const parts: string[] = [];\n if (updated > 0) parts.push(`Updated ${updated}`);\n if (skipped > 0) parts.push(`skipped ${skipped} (customized)`);\n if (added > 0) parts.push(`added ${added} new`);\n if (upToDate > 0) parts.push(`${upToDate} already up to date`);\n console.log(`\\nUpgrade complete: ${parts.join(', ')}.`);\n}\n\n","// Migration module — moves flat docs/{briefs,research,designs,specs/<feature>} layouts\n// into per-feature folders at docs/features/<slug>/{brief,research,design,specs/}.\n// Plan-then-apply split so callers can render a summary before mutating the filesystem.\n\nimport {\n cpSync,\n existsSync,\n mkdirSync,\n readdirSync,\n renameSync,\n rmSync,\n statSync,\n} from 'node:fs';\nimport { join } from 'node:path';\n\nexport type MoveKind = 'brief' | 'research' | 'design' | 'specs-dir' | 'bugfix-dir';\n\nexport interface Move {\n from: string;\n to: string;\n kind: MoveKind;\n}\n\nexport interface MigrationPlan {\n moves: Move[];\n slugs: string[];\n orphans: { specsDirs: string[] };\n skipped?: Move[];\n}\n\nexport interface MigrationResult {\n applied: number;\n skipped: number;\n errors: Array<{ move: Move; error: string }>;\n}\n\nconst DATE_PREFIX_RE = /^\\d{4}-\\d{2}-\\d{2}-(.+)$/;\n\nfunction deriveSlug(filename: string): string {\n return filename.replace(/\\.md$/, '');\n}\n\nfunction slugWithoutDate(slug: string): string | null {\n const m = slug.match(DATE_PREFIX_RE);\n return m ? m[1] : null;\n}\n\nfunction listMdFiles(dir: string): string[] {\n if (!existsSync(dir)) return [];\n try {\n return readdirSync(dir).filter(f => f.endsWith('.md'));\n } catch {\n return [];\n }\n}\n\nfunction listSubdirs(dir: string): string[] {\n if (!existsSync(dir)) return [];\n try {\n return readdirSync(dir).filter(name => {\n try {\n return statSync(join(dir, name)).isDirectory();\n } catch {\n return false;\n }\n });\n } catch {\n return [];\n }\n}\n\nexport function planMigration(projectDir: string): MigrationPlan {\n const briefsDir = join(projectDir, 'docs', 'briefs');\n const researchDir = join(projectDir, 'docs', 'research');\n const designsDir = join(projectDir, 'docs', 'designs');\n const specsDir = join(projectDir, 'docs', 'specs');\n\n const moves: Move[] = [];\n const skipped: Move[] = [];\n const slugSet = new Set<string>();\n\n // Briefs\n for (const file of listMdFiles(briefsDir)) {\n const slug = deriveSlug(file);\n slugSet.add(slug);\n const move: Move = {\n from: join(briefsDir, file),\n to: join(projectDir, 'docs', 'features', slug, 'brief.md'),\n kind: 'brief',\n };\n if (existsSync(move.to)) skipped.push(move);\n else moves.push(move);\n }\n\n // Research\n for (const file of listMdFiles(researchDir)) {\n const slug = deriveSlug(file);\n slugSet.add(slug);\n const move: Move = {\n from: join(researchDir, file),\n to: join(projectDir, 'docs', 'features', slug, 'research.md'),\n kind: 'research',\n };\n if (existsSync(move.to)) skipped.push(move);\n else moves.push(move);\n }\n\n // Designs\n for (const file of listMdFiles(designsDir)) {\n const slug = deriveSlug(file);\n slugSet.add(slug);\n const move: Move = {\n from: join(designsDir, file),\n to: join(projectDir, 'docs', 'features', slug, 'design.md'),\n kind: 'design',\n };\n if (existsSync(move.to)) skipped.push(move);\n else moves.push(move);\n }\n\n // Spec dirs — match by exact slug or date-stripped slug\n const briefSlugs = Array.from(slugSet);\n const orphanSpecsDirs: string[] = [];\n\n for (const subdir of listSubdirs(specsDir)) {\n let matchedSlug: string | null = null;\n\n if (briefSlugs.includes(subdir)) {\n matchedSlug = subdir;\n } else {\n // Look for a brief slug whose date-stripped form matches this subdir\n for (const slug of briefSlugs) {\n if (slugWithoutDate(slug) === subdir) {\n matchedSlug = slug;\n break;\n }\n }\n }\n\n if (matchedSlug) {\n const move: Move = {\n from: join(specsDir, subdir),\n to: join(projectDir, 'docs', 'features', matchedSlug, 'specs'),\n kind: 'specs-dir',\n };\n if (existsSync(move.to)) skipped.push(move);\n else moves.push(move);\n } else {\n // No matching brief slug → this is an area-level (bugfix) spec dir.\n // Reclassify it as a forced move into docs/bugfixes/<area>/.\n orphanSpecsDirs.push(subdir);\n const move: Move = {\n from: join(specsDir, subdir),\n to: join(projectDir, 'docs', 'bugfixes', subdir),\n kind: 'bugfix-dir',\n };\n if (existsSync(move.to)) skipped.push(move);\n else moves.push(move);\n }\n }\n\n const plan: MigrationPlan = {\n moves,\n slugs: Array.from(slugSet),\n orphans: { specsDirs: orphanSpecsDirs },\n };\n if (skipped.length > 0) plan.skipped = skipped;\n return plan;\n}\n\nfunction moveFsItem(from: string, to: string): void {\n mkdirSync(join(to, '..'), { recursive: true });\n try {\n renameSync(from, to);\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === 'EXDEV') {\n cpSync(from, to, { recursive: true });\n rmSync(from, { recursive: true, force: true });\n } else {\n throw err;\n }\n }\n}\n\nexport function applyMigration(plan: MigrationPlan): MigrationResult {\n let applied = 0;\n let skipped = (plan.skipped?.length) ?? 0;\n const errors: Array<{ move: Move; error: string }> = [];\n\n for (const move of plan.moves) {\n if (existsSync(move.to)) {\n skipped++;\n continue;\n }\n if (!existsSync(move.from)) {\n errors.push({ move, error: `Source missing: ${move.from}` });\n continue;\n }\n try {\n moveFsItem(move.from, move.to);\n applied++;\n } catch (err) {\n errors.push({ move, error: (err as Error).message });\n }\n }\n\n return { applied, skipped, errors };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,cAAAA,aAAY,cAAc,eAAe,aAAAC,YAAW,UAAAC,SAAqB,iBAAiB;AACnG,SAAS,QAAAC,OAAM,SAAS,eAAe;AACvC,SAAS,uBAAuB;;;ACEhC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,YAAY;AAuBrB,IAAM,iBAAiB;AAEvB,SAAS,WAAW,UAA0B;AAC5C,SAAO,SAAS,QAAQ,SAAS,EAAE;AACrC;AAEA,SAAS,gBAAgB,MAA6B;AACpD,QAAM,IAAI,KAAK,MAAM,cAAc;AACnC,SAAO,IAAI,EAAE,CAAC,IAAI;AACpB;AAEA,SAAS,YAAY,KAAuB;AAC1C,MAAI,CAAC,WAAW,GAAG,EAAG,QAAO,CAAC;AAC9B,MAAI;AACF,WAAO,YAAY,GAAG,EAAE,OAAO,OAAK,EAAE,SAAS,KAAK,CAAC;AAAA,EACvD,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEA,SAAS,YAAY,KAAuB;AAC1C,MAAI,CAAC,WAAW,GAAG,EAAG,QAAO,CAAC;AAC9B,MAAI;AACF,WAAO,YAAY,GAAG,EAAE,OAAO,UAAQ;AACrC,UAAI;AACF,eAAO,SAAS,KAAK,KAAK,IAAI,CAAC,EAAE,YAAY;AAAA,MAC/C,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF,CAAC;AAAA,EACH,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEO,SAAS,cAAc,YAAmC;AAC/D,QAAM,YAAY,KAAK,YAAY,QAAQ,QAAQ;AACnD,QAAM,cAAc,KAAK,YAAY,QAAQ,UAAU;AACvD,QAAM,aAAa,KAAK,YAAY,QAAQ,SAAS;AACrD,QAAM,WAAW,KAAK,YAAY,QAAQ,OAAO;AAEjD,QAAM,QAAgB,CAAC;AACvB,QAAM,UAAkB,CAAC;AACzB,QAAM,UAAU,oBAAI,IAAY;AAGhC,aAAW,QAAQ,YAAY,SAAS,GAAG;AACzC,UAAM,OAAO,WAAW,IAAI;AAC5B,YAAQ,IAAI,IAAI;AAChB,UAAM,OAAa;AAAA,MACjB,MAAM,KAAK,WAAW,IAAI;AAAA,MAC1B,IAAI,KAAK,YAAY,QAAQ,YAAY,MAAM,UAAU;AAAA,MACzD,MAAM;AAAA,IACR;AACA,QAAI,WAAW,KAAK,EAAE,EAAG,SAAQ,KAAK,IAAI;AAAA,QACrC,OAAM,KAAK,IAAI;AAAA,EACtB;AAGA,aAAW,QAAQ,YAAY,WAAW,GAAG;AAC3C,UAAM,OAAO,WAAW,IAAI;AAC5B,YAAQ,IAAI,IAAI;AAChB,UAAM,OAAa;AAAA,MACjB,MAAM,KAAK,aAAa,IAAI;AAAA,MAC5B,IAAI,KAAK,YAAY,QAAQ,YAAY,MAAM,aAAa;AAAA,MAC5D,MAAM;AAAA,IACR;AACA,QAAI,WAAW,KAAK,EAAE,EAAG,SAAQ,KAAK,IAAI;AAAA,QACrC,OAAM,KAAK,IAAI;AAAA,EACtB;AAGA,aAAW,QAAQ,YAAY,UAAU,GAAG;AAC1C,UAAM,OAAO,WAAW,IAAI;AAC5B,YAAQ,IAAI,IAAI;AAChB,UAAM,OAAa;AAAA,MACjB,MAAM,KAAK,YAAY,IAAI;AAAA,MAC3B,IAAI,KAAK,YAAY,QAAQ,YAAY,MAAM,WAAW;AAAA,MAC1D,MAAM;AAAA,IACR;AACA,QAAI,WAAW,KAAK,EAAE,EAAG,SAAQ,KAAK,IAAI;AAAA,QACrC,OAAM,KAAK,IAAI;AAAA,EACtB;AAGA,QAAM,aAAa,MAAM,KAAK,OAAO;AACrC,QAAM,kBAA4B,CAAC;AAEnC,aAAW,UAAU,YAAY,QAAQ,GAAG;AAC1C,QAAI,cAA6B;AAEjC,QAAI,WAAW,SAAS,MAAM,GAAG;AAC/B,oBAAc;AAAA,IAChB,OAAO;AAEL,iBAAW,QAAQ,YAAY;AAC7B,YAAI,gBAAgB,IAAI,MAAM,QAAQ;AACpC,wBAAc;AACd;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,QAAI,aAAa;AACf,YAAM,OAAa;AAAA,QACjB,MAAM,KAAK,UAAU,MAAM;AAAA,QAC3B,IAAI,KAAK,YAAY,QAAQ,YAAY,aAAa,OAAO;AAAA,QAC7D,MAAM;AAAA,MACR;AACA,UAAI,WAAW,KAAK,EAAE,EAAG,SAAQ,KAAK,IAAI;AAAA,UACrC,OAAM,KAAK,IAAI;AAAA,IACtB,OAAO;AAGL,sBAAgB,KAAK,MAAM;AAC3B,YAAM,OAAa;AAAA,QACjB,MAAM,KAAK,UAAU,MAAM;AAAA,QAC3B,IAAI,KAAK,YAAY,QAAQ,YAAY,MAAM;AAAA,QAC/C,MAAM;AAAA,MACR;AACA,UAAI,WAAW,KAAK,EAAE,EAAG,SAAQ,KAAK,IAAI;AAAA,UACrC,OAAM,KAAK,IAAI;AAAA,IACtB;AAAA,EACF;AAEA,QAAM,OAAsB;AAAA,IAC1B;AAAA,IACA,OAAO,MAAM,KAAK,OAAO;AAAA,IACzB,SAAS,EAAE,WAAW,gBAAgB;AAAA,EACxC;AACA,MAAI,QAAQ,SAAS,EAAG,MAAK,UAAU;AACvC,SAAO;AACT;AAEA,SAAS,WAAW,MAAc,IAAkB;AAClD,YAAU,KAAK,IAAI,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC7C,MAAI;AACF,eAAW,MAAM,EAAE;AAAA,EACrB,SAAS,KAAK;AACZ,UAAM,OAAQ,IAA8B;AAC5C,QAAI,SAAS,SAAS;AACpB,aAAO,MAAM,IAAI,EAAE,WAAW,KAAK,CAAC;AACpC,aAAO,MAAM,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,IAC/C,OAAO;AACL,YAAM;AAAA,IACR;AAAA,EACF;AACF;AAEO,SAAS,eAAe,MAAsC;AACnE,MAAI,UAAU;AACd,MAAI,UAAW,KAAK,SAAS,UAAW;AACxC,QAAM,SAA+C,CAAC;AAEtD,aAAW,QAAQ,KAAK,OAAO;AAC7B,QAAI,WAAW,KAAK,EAAE,GAAG;AACvB;AACA;AAAA,IACF;AACA,QAAI,CAAC,WAAW,KAAK,IAAI,GAAG;AAC1B,aAAO,KAAK,EAAE,MAAM,OAAO,mBAAmB,KAAK,IAAI,GAAG,CAAC;AAC3D;AAAA,IACF;AACA,QAAI;AACF,iBAAW,KAAK,MAAM,KAAK,EAAE;AAC7B;AAAA,IACF,SAAS,KAAK;AACZ,aAAO,KAAK,EAAE,MAAM,OAAQ,IAAc,QAAQ,CAAC;AAAA,IACrD;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,SAAS,OAAO;AACpC;;;ADvMA,SAAS,eAAe,SAAiB,QAAyB;AAChE,QAAM,eAAe,QAAQ,MAAM,GAAG,EAAE,IAAI,MAAM;AAClD,QAAM,cAAc,OAAO,MAAM,GAAG,EAAE,IAAI,MAAM;AAChD,QAAM,MAAM,KAAK,IAAI,aAAa,QAAQ,YAAY,MAAM;AAC5D,WAAS,IAAI,GAAG,IAAI,KAAK,KAAK;AAC5B,UAAM,IAAI,aAAa,CAAC,KAAK;AAC7B,UAAM,IAAI,YAAY,CAAC,KAAK;AAC5B,QAAI,IAAI,EAAG,QAAO;AAClB,QAAI,IAAI,EAAG,QAAO;AAAA,EACpB;AACA,SAAO;AACT;AAEA,eAAe,kBAAgE;AAC7E,MAAI;AACF,UAAM,aAAa,kBAAkB;AACrC,UAAM,MAAM,MAAM,MAAM,8CAA8C;AAAA,MACpE,QAAQ,YAAY,QAAQ,GAAI;AAAA,IAClC,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,QAAO,EAAE,OAAO,MAAM;AACnC,UAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,QAAI,eAAe,YAAY,KAAK,OAAO,GAAG;AAC5C,aAAO,EAAE,OAAO,MAAM,QAAQ,KAAK,QAAQ;AAAA,IAC7C;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO,EAAE,OAAO,MAAM;AACxB;AAeA,SAAS,kBAA0C;AACjD,QAAM,QAAgC,CAAC;AACvC,aAAW,CAAC,MAAM,OAAO,KAAK,OAAO,QAAQ,MAAM,GAAG;AACpD,UAAM,YAAY,KAAK,QAAQ,SAAS,EAAE;AAC1C,UAAMC,MAAK,WAAW,UAAU,WAAW,UAAU,CAAC,IAAI;AAAA,EAC5D;AACA,aAAW,CAAC,MAAM,OAAO,KAAK,OAAO,QAAQ,SAAS,GAAG;AACvD,UAAMA,MAAK,QAAQ,aAAa,IAAI,CAAC,IAAI;AAAA,EAC3C;AACA,aAAW,CAAC,MAAM,OAAO,KAAK,OAAO,QAAQ,YAAY,GAAG;AAC1D,UAAM,YAAY,KAAK,QAAQ,SAAS,EAAE;AAC1C,UAAMA,MAAK,WAAW,UAAU,WAAW,UAAU,CAAC,IAAI;AAAA,EAC5D;AACA,aAAW,CAAC,MAAM,OAAO,KAAK,OAAO,QAAQ,SAAS,GAAG;AACvD,UAAM,YAAY,KAAK,QAAQ,SAAS,EAAE;AAC1C,UAAMA,MAAK,OAAO,UAAU,WAAW,UAAU,CAAC,IAAI;AAAA,EACxD;AACA,aAAW,CAAC,MAAM,OAAO,KAAK,OAAO,QAAQ,UAAU,GAAG;AACxD,UAAMA,MAAK,OAAO,WAAW,YAAY,IAAI,CAAC,IAAI;AAAA,EACpD;AACA,aAAW,CAAC,MAAM,OAAO,KAAK,OAAO,QAAQ,aAAa,GAAG;AAC3D,UAAMA,MAAK,OAAO,cAAc,IAAI,CAAC,IAAI;AAAA,EAC3C;AACA,aAAW,CAAC,MAAM,OAAO,KAAK,OAAO,QAAQ,SAAS,GAAG;AACvD,UAAMA,MAAK,OAAO,UAAU,IAAI,CAAC,IAAI;AAAA,EACvC;AACA,SAAO;AACT;AAIA,IAAM,wBAAwB;AAAA;AAAA,EAE5B;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA;AAAA,EAEA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AACF;AAGA,IAAM,yBAAyB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,SAAS,wBAAwB,WAA2B;AAC1D,QAAM,YAAYA,MAAK,WAAW,WAAW,QAAQ;AACrD,MAAI,CAACC,YAAW,SAAS,EAAG,QAAO;AAEnC,MAAI,UAAU;AAGd,aAAW,QAAQ,uBAAuB;AACxC,UAAM,MAAMD,MAAK,WAAW,IAAI;AAChC,QAAIC,YAAW,GAAG,GAAG;AACnB,MAAAC,QAAO,KAAK,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAC5C;AAAA,IACF;AAAA,EACF;AAGA,aAAW,QAAQ,wBAAwB;AACzC,UAAM,OAAOF,MAAK,WAAW,IAAI;AACjC,QAAIC,YAAW,IAAI,GAAG;AACpB,MAAAC,QAAO,IAAI;AACX;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAkBA,SAAS,yBAAyB,WAA4B;AAC5D,QAAM,aAAaF,MAAK,WAAW,mBAAmB;AACtD,MAAI,CAACC,YAAW,UAAU,EAAG,QAAO;AAEpC,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,aAAa,YAAY,OAAO,CAAC;AAAA,EACvD,QAAQ;AAIN,IAAAC,QAAO,YAAY,EAAE,OAAO,KAAK,CAAC;AAClC,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,OAAO,OAAO,YAAY,WAAW,OAAO,UAAU,kBAAkB;AACxF,QAAM,QACJ,OAAO,SAAS,OAAO,OAAO,UAAU,WACnC,OAAO,QACR,CAAC;AAKP,eAAa,WAAW,SAAS,KAAK;AACtC,EAAAA,QAAO,YAAY,EAAE,OAAO,KAAK,CAAC;AAClC,SAAO;AACT;AAEA,SAAS,WAAW,SAAyB;AAC3C,SAAO,QAAQ,MAAM,IAAI,EAAE;AAC7B;AAEA,SAAS,uBAAuB,cAA4B;AAG1D,MAAI,aAAa,SAAS,uBAAuB,KAAK,CAAC,aAAa,SAAS,WAAW,GAAG;AACzF,QAAI;AACF,gBAAU,cAAc,GAAK;AAAA,IAC/B,QAAQ;AAAA,IAER;AAAA,EACF;AACF;AAEA,eAAe,QAAQ,UAAoC;AACzD,QAAM,KAAK,gBAAgB,EAAE,OAAO,QAAQ,OAAO,QAAQ,QAAQ,OAAO,CAAC;AAC3E,SAAO,IAAI,QAAQ,CAACC,aAAY;AAC9B,OAAG,SAAS,GAAG,QAAQ,WAAW,CAAC,WAAW;AAC5C,SAAG,MAAM;AACT,MAAAA,SAAQ,OAAO,KAAK,EAAE,YAAY,MAAM,GAAG;AAAA,IAC7C,CAAC;AAAA,EACH,CAAC;AACH;AAEA,SAAS,sBAAsB,MAAqB,YAA0B;AAC5E,UAAQ,IAAI,EAAE;AACd,UAAQ,IAAI,iEAAiE;AAC7E,UAAQ,IAAI,EAAE;AAEd,QAAM,QAAQ,CAAC,MAAe,EAAE,WAAW,UAAU,IAAI,EAAE,MAAM,WAAW,SAAS,CAAC,IAAI;AAI1F,QAAM,eAAe,KAAK,MAAM,OAAO,OAAK,EAAE,SAAS,YAAY;AACnE,QAAM,cAAc,KAAK,MAAM,OAAO,OAAK,EAAE,SAAS,YAAY;AAElE,QAAM,SAAS,oBAAI,IAA+B;AAClD,aAAW,QAAQ,cAAc;AAC/B,UAAM,QAAQ,MAAM,KAAK,EAAE,EAAE,MAAM,OAAO;AAE1C,UAAM,OAAO,MAAM,UAAU,IAAI,MAAM,CAAC,IAAI;AAC5C,QAAI,CAAC,OAAO,IAAI,IAAI,EAAG,QAAO,IAAI,MAAM,CAAC,CAAC;AAC1C,WAAO,IAAI,IAAI,EAAG,KAAK,IAAI;AAAA,EAC7B;AACA,aAAW,CAAC,MAAM,KAAK,KAAK,QAAQ;AAClC,YAAQ,IAAI,KAAK,IAAI,GAAG;AACxB,eAAW,QAAQ,OAAO;AACxB,cAAQ,IAAI,OAAO,MAAM,KAAK,IAAI,CAAC,WAAM,MAAM,KAAK,EAAE,CAAC,EAAE;AAAA,IAC3D;AAAA,EACF;AAEA,MAAI,YAAY,SAAS,GAAG;AAC1B,YAAQ,IAAI,EAAE;AACd,YAAQ,IAAI,2BAA2B;AACvC,eAAW,QAAQ,aAAa;AAC9B,cAAQ,IAAI,OAAO,MAAM,KAAK,IAAI,CAAC,WAAM,MAAM,KAAK,EAAE,CAAC,EAAE;AAAA,IAC3D;AAAA,EACF;AACA,UAAQ,IAAI,EAAE;AAChB;AAEA,SAAS,uBAA6B;AACpC,UAAQ,IAAI,EAAE;AACd,UAAQ,IAAI,wFAAmF;AAC/F,UAAQ,IAAI,yEAAyE;AACrF,UAAQ,IAAI,sDAAsD;AAClE,UAAQ,IAAI,EAAE;AAChB;AAEA,SAAS,mBAAmB,YAA0B;AACpD,QAAM,OAAO,cAAc,UAAU;AACrC,MAAI,KAAK,MAAM,WAAW,KAAK,KAAK,QAAQ,UAAU,WAAW,GAAG;AAClE;AAAA,EACF;AAEA,wBAAsB,MAAM,UAAU;AACtC,QAAM,SAAS,eAAe,IAAI;AAGlC,QAAM,YAAY,OAAO,UAAU,OAAO,OAAO;AACjD,MAAI,YAAY,KAAK,OAAO,OAAO,SAAS,YAAY,KAAK;AAC3D,YAAQ,MAAM,2EAA2E;AACzF,eAAW,KAAK,OAAO,QAAQ;AAC7B,cAAQ,MAAM,KAAK,EAAE,KAAK,IAAI,WAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE;AAAA,IAC7D;AACA,UAAM,IAAI,MAAM,sCAAsC;AAAA,EACxD;AAEA,MAAI,OAAO,OAAO,SAAS,GAAG;AAC5B,YAAQ,IAAI,kDAAkD;AAC9D,eAAW,KAAK,OAAO,QAAQ;AAC7B,cAAQ,IAAI,WAAW,EAAE,KAAK,IAAI,WAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE;AAAA,IACjE;AAAA,EACF;AAEA,uBAAqB;AACvB;AAEA,eAAsB,QAAQ,KAAa,MAAqC;AAC9E,QAAM,YAAY,QAAQ,GAAG;AAI7B,MAAI,KAAK,cAAc,OAAW,uBAAsB,KAAK,SAAS;AAItE,QAAM,WAAW,MAAM,gBAAgB;AACvC,MAAI,SAAS,OAAO;AAClB,UAAMC,cAAa,kBAAkB;AACrC,YAAQ,IAAI,yCAAyCA,WAAU,eAAe,SAAS,MAAM,IAAI;AACjG,YAAQ,IAAI,sCAAsC;AAClD,YAAQ,IAAI,mCAAmC;AAC/C;AAAA,EACF;AAIA,QAAM,iBAAiBH,YAAWD,MAAK,WAAW,mBAAmB,CAAC;AACtE,QAAM,WAAWC,YAAWD,MAAK,WAAW,WAAW,UAAU,iBAAiB,UAAU,CAAC,KACxFC,YAAWD,MAAK,WAAW,WAAW,UAAU,QAAQ,UAAU,CAAC,KACnEC,YAAWD,MAAK,WAAW,WAAW,UAAU,OAAO,UAAU,CAAC,KAClEC,YAAWD,MAAK,WAAW,WAAW,UAAU,YAAY,UAAU,CAAC;AAE5E,MAAI,CAAC,YAAY,SAAS,KAAK,CAAC,kBAAkB,CAAC,UAAU;AAC3D,YAAQ,IAAI,sDAAsD;AAClE,YAAQ,IAAI,gCAAgC;AAC5C;AAAA,EACF;AAKA,2BAAyB,SAAS;AAGlC,QAAM,oBAAoB,wBAAwB,SAAS;AAC3D,MAAI,oBAAoB,GAAG;AACzB,YAAQ,IAAI,WAAW,iBAAiB,uDAAuD;AAAA,EACjG;AAMA,qBAAmB,SAAS;AAG5B,QAAM,aAAa,kBAAkB;AAKrC,QAAM,eAAe,gBAAgB;AACrC,QAAM,YAAY,YAAY,SAAS;AACvC,QAAM,kBAAkB,WAAW,SAAS,CAAC;AAY7C,QAAM,kBAAkB,MAAM,wBAAwB;AAAA,IACpD,MAAM,KAAK;AAAA,IACX,WAAW,WAAW;AAAA,IACtB,aAAa,QAAQ,MAAM,UAAU,QAAQ,CAAC,KAAK;AAAA,IACnD,aAAa;AAAA,EACf,CAAC;AACD,QAAM,mBAAmB,gBAAgB;AACzC,wBAAsB,WAAW,gBAAgB;AACjD,MAAI,qBAAqB,aAAa,WAAW,qBAAqB,WAAW;AAC/E,YAAQ,IAAI,yFAAyF;AACrG,YAAQ,IAAI,KAAK,uBAAuB,EAAE;AAAA,EAC5C;AAEA,QAAM,UAAwB,CAAC;AAC/B,MAAI,WAAW;AAEf,aAAW,CAAC,SAAS,UAAU,KAAK,OAAO,QAAQ,YAAY,GAAG;AAChE,UAAM,UAAUA,MAAK,WAAW,OAAO;AACvC,UAAM,UAAU,YAAY,UAAU;AAEtC,QAAI,CAACC,YAAW,OAAO,GAAG;AAExB,cAAQ,KAAK,EAAE,cAAc,SAAS,cAAc,SAAS,YAAY,MAAM,MAAM,CAAC;AACtF;AAAA,IACF;AAEA,UAAM,iBAAiB,aAAa,SAAS,OAAO;AACpD,UAAM,cAAc,YAAY,cAAc;AAE9C,QAAI,gBAAgB,SAAS;AAE3B;AACA;AAAA,IACF;AAGA,UAAM,eAAe,gBAAgB,OAAO;AAE5C,QAAI,gBAAgB,aAAa,WAAW,MAAM,cAAc;AAE9D,cAAQ,KAAK,EAAE,cAAc,SAAS,cAAc,SAAS,YAAY,MAAM,UAAU,CAAC;AAAA,IAC5F,OAAO;AAEL,cAAQ,KAAK,EAAE,cAAc,SAAS,cAAc,SAAS,YAAY,MAAM,aAAa,CAAC;AAAA,IAC/F;AAAA,EACF;AAEA,MAAI,QAAQ,WAAW,GAAG;AAIxB,QAAI,gBAAgB,WAAW,aAAa,UAAU,qBAAqB,kBAAkB;AAC3F,mBAAa,WAAW,UAAU,SAAS,iBAAiB,gBAAgB;AAAA,IAC9E;AACA,YAAQ,IAAI,qBAAqB;AACjC;AAAA,EACF;AAGA,MAAI,UAAU;AACd,MAAI,UAAU;AACd,MAAI,QAAQ;AAEZ,aAAW,UAAU,SAAS;AAC5B,QAAI,OAAO,SAAS,OAAO;AAEzB,MAAAI,WAAU,QAAQ,OAAO,YAAY,GAAG,EAAE,WAAW,KAAK,CAAC;AAC3D,oBAAc,OAAO,cAAc,OAAO,YAAY,OAAO;AAC7D,6BAAuB,OAAO,YAAY;AAC1C;AACA,cAAQ,IAAI,OAAO,OAAO,YAAY,EAAE;AAAA,IAC1C,WAAW,OAAO,SAAS,WAAW;AAEpC,oBAAc,OAAO,cAAc,OAAO,YAAY,OAAO;AAC7D,6BAAuB,OAAO,YAAY;AAC1C;AAAA,IACF,WAAW,OAAO,SAAS,cAAc;AACvC,YAAM,iBAAiB,aAAa,OAAO,cAAc,OAAO;AAChE,YAAM,eAAe,WAAW,cAAc;AAC9C,YAAM,WAAW,WAAW,OAAO,UAAU;AAC7C,YAAM,OAAO,WAAW;AACxB,YAAM,YAAY,OAAO,IAAI,IAAI,IAAI,WAAW,OAAO,IAAI,GAAG,IAAI,WAAW;AAC7E,YAAM,QAAQ,eAAe,OAAO,YAAY,YAAY,YAAY,mBAAmB,QAAQ,WAAW,SAAS;AAEvH,UAAI,KAAK,KAAK;AACZ,sBAAc,OAAO,cAAc,OAAO,YAAY,OAAO;AAC7D;AAAA,MACF,OAAO;AACL,cAAM,SAAS,MAAM,QAAQ,GAAG,KAAK,gCAA2B;AAChE,YAAI,QAAQ;AACV,wBAAc,OAAO,cAAc,OAAO,YAAY,OAAO;AAC7D,iCAAuB,OAAO,YAAY;AAC1C;AAAA,QACF,OAAO;AACL;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,QAAM,YAAoC,CAAC;AAC3C,aAAW,CAAC,SAAS,OAAO,KAAK,OAAO,QAAQ,YAAY,GAAG;AAC7D,UAAM,UAAUL,MAAK,WAAW,OAAO;AACvC,QAAIC,YAAW,OAAO,GAAG;AACvB,YAAM,UAAU,aAAa,SAAS,OAAO;AAC7C,gBAAU,OAAO,IAAI,YAAY,OAAO;AAAA,IAC1C;AAAA,EACF;AAIA,eAAa,WAAW,YAAY,WAAW,gBAAgB,UAAU,mBAAmB,MAAS;AAGrG,QAAM,QAAkB,CAAC;AACzB,MAAI,UAAU,EAAG,OAAM,KAAK,WAAW,OAAO,EAAE;AAChD,MAAI,UAAU,EAAG,OAAM,KAAK,WAAW,OAAO,eAAe;AAC7D,MAAI,QAAQ,EAAG,OAAM,KAAK,SAAS,KAAK,MAAM;AAC9C,MAAI,WAAW,EAAG,OAAM,KAAK,GAAG,QAAQ,qBAAqB;AAC7D,UAAQ,IAAI;AAAA,oBAAuB,MAAM,KAAK,IAAI,CAAC,GAAG;AACxD;","names":["existsSync","mkdirSync","rmSync","join","join","existsSync","rmSync","resolve","pkgVersion","mkdirSync"]}
@@ -9,7 +9,7 @@ import {
9
9
  readVersion,
10
10
  truncateHash,
11
11
  writeVersion
12
- } from "./chunk-PUYGEBLA.js";
12
+ } from "./chunk-TD65VH2W.js";
13
13
  export {
14
14
  DEFAULT_GITIGNORE_PROFILE,
15
15
  LEGACY_VERSION_FILE,
@@ -21,4 +21,4 @@ export {
21
21
  truncateHash,
22
22
  writeVersion
23
23
  };
24
- //# sourceMappingURL=version-TG3D7QLM.js.map
24
+ //# sourceMappingURL=version-2FGZETKD.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "joycraft",
3
- "version": "0.6.10",
3
+ "version": "0.6.12",
4
4
  "description": "CLI + Claude Code plugin that scaffolds and upgrades AI development harnesses",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,55 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- STATE_PATH
4
- } from "./chunk-PUYGEBLA.js";
5
-
6
- // src/gitignore.ts
7
- import { existsSync, readFileSync, writeFileSync } from "fs";
8
- import { join } from "path";
9
- var PRIVATE_PROFILE_IGNORES = [".claude/", ".agents/", ".pi/"];
10
- function ensureGitignoreEntry(targetDir, line) {
11
- const gitignorePath = join(targetDir, ".gitignore");
12
- if (!existsSync(gitignorePath)) {
13
- writeFileSync(gitignorePath, line + "\n", "utf-8");
14
- return true;
15
- }
16
- const current = readFileSync(gitignorePath, "utf-8");
17
- const already = current.split("\n").some((l) => l.trim() === line.trim());
18
- if (already) return false;
19
- const sep = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
20
- writeFileSync(gitignorePath, current + sep + line + "\n", "utf-8");
21
- return true;
22
- }
23
- function applyGitignoreProfile(targetDir, profile) {
24
- const added = [];
25
- if (profile === "private") {
26
- for (const entry of PRIVATE_PROFILE_IGNORES) {
27
- if (ensureGitignoreEntry(targetDir, entry)) added.push(entry);
28
- }
29
- return added;
30
- }
31
- if (ensureGitignoreEntry(targetDir, STATE_PATH)) added.push(STATE_PATH);
32
- return added;
33
- }
34
-
35
- // src/package-version.ts
36
- import { readFileSync as readFileSync2 } from "fs";
37
- import { fileURLToPath } from "url";
38
- import { dirname, join as join2 } from "path";
39
- var __dirname = dirname(fileURLToPath(import.meta.url));
40
- function getPackageVersion() {
41
- const pkgPath = join2(__dirname, "..", "package.json");
42
- const raw = readFileSync2(pkgPath, "utf-8");
43
- const pkg = JSON.parse(raw);
44
- if (typeof pkg.version !== "string" || pkg.version.length === 0) {
45
- throw new Error(`Joycraft package.json at ${pkgPath} is missing a version field`);
46
- }
47
- return pkg.version;
48
- }
49
-
50
- export {
51
- ensureGitignoreEntry,
52
- applyGitignoreProfile,
53
- getPackageVersion
54
- };
55
- //# sourceMappingURL=chunk-P45XNSGK.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/gitignore.ts","../src/package-version.ts"],"sourcesContent":["import { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { STATE_PATH, type GitignoreProfile } from './version.js';\n\n/**\n * The harness directories the `private` profile gitignores. Tracking only\n * CLAUDE.md, AGENTS.md, and docs/ means everything under these three dirs\n * stays local. `.claude/` already covers the hidden state file.\n */\nexport const PRIVATE_PROFILE_IGNORES = ['.claude/', '.agents/', '.pi/'];\n\n/**\n * Append-only, create-if-absent, idempotent .gitignore writer.\n *\n * Mirrors the \"append over modify when touching user files\" principle: it never\n * rewrites, reorders, or removes existing lines — it only adds `line` if no\n * existing line matches it exactly (after trimming). Returns true if it wrote.\n *\n * Used by both `init` (to gitignore the relocated state file) and `upgrade`'s\n * legacy-migration step (so migrated projects also stop committing the state).\n */\nexport function ensureGitignoreEntry(targetDir: string, line: string): boolean {\n const gitignorePath = join(targetDir, '.gitignore');\n\n if (!existsSync(gitignorePath)) {\n writeFileSync(gitignorePath, line + '\\n', 'utf-8');\n return true;\n }\n\n const current = readFileSync(gitignorePath, 'utf-8');\n const already = current.split('\\n').some((l) => l.trim() === line.trim());\n if (already) return false;\n\n // Append on its own line, tolerating a file that may or may not end in \\n.\n const sep = current.length > 0 && !current.endsWith('\\n') ? '\\n' : '';\n writeFileSync(gitignorePath, current + sep + line + '\\n', 'utf-8');\n return true;\n}\n\n/**\n * Apply a gitignore profile's entries to the project's .gitignore.\n *\n * - `shared` — ignore only the hidden upgrade-state file (current default).\n * - `private` — ignore the whole .claude/, .agents/, .pi/ trees. Since\n * .claude/ already covers the state file, the per-line writer skips the\n * redundant state entry to avoid a dead line.\n *\n * Append-only and idempotent (each line goes through ensureGitignoreEntry), so\n * re-running init/upgrade never duplicates entries. Returns the list of lines\n * actually added this call (empty when everything was already present).\n */\nexport function applyGitignoreProfile(targetDir: string, profile: GitignoreProfile): string[] {\n const added: string[] = [];\n\n if (profile === 'private') {\n for (const entry of PRIVATE_PROFILE_IGNORES) {\n if (ensureGitignoreEntry(targetDir, entry)) added.push(entry);\n }\n return added;\n }\n\n // `shared`: only the hidden state file, matching long-standing behavior.\n if (ensureGitignoreEntry(targetDir, STATE_PATH)) added.push(STATE_PATH);\n return added;\n}\n","import { readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { dirname, join } from 'node:path';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nexport function getPackageVersion(): string {\n const pkgPath = join(__dirname, '..', 'package.json');\n const raw = readFileSync(pkgPath, 'utf-8');\n const pkg = JSON.parse(raw) as { version?: unknown };\n if (typeof pkg.version !== 'string' || pkg.version.length === 0) {\n throw new Error(`Joycraft package.json at ${pkgPath} is missing a version field`);\n }\n return pkg.version;\n}\n"],"mappings":";;;;;;AAAA,SAAS,YAAY,cAAc,qBAAqB;AACxD,SAAS,YAAY;AAQd,IAAM,0BAA0B,CAAC,YAAY,YAAY,MAAM;AAY/D,SAAS,qBAAqB,WAAmB,MAAuB;AAC7E,QAAM,gBAAgB,KAAK,WAAW,YAAY;AAElD,MAAI,CAAC,WAAW,aAAa,GAAG;AAC9B,kBAAc,eAAe,OAAO,MAAM,OAAO;AACjD,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,aAAa,eAAe,OAAO;AACnD,QAAM,UAAU,QAAQ,MAAM,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,MAAM,KAAK,KAAK,CAAC;AACxE,MAAI,QAAS,QAAO;AAGpB,QAAM,MAAM,QAAQ,SAAS,KAAK,CAAC,QAAQ,SAAS,IAAI,IAAI,OAAO;AACnE,gBAAc,eAAe,UAAU,MAAM,OAAO,MAAM,OAAO;AACjE,SAAO;AACT;AAcO,SAAS,sBAAsB,WAAmB,SAAqC;AAC5F,QAAM,QAAkB,CAAC;AAEzB,MAAI,YAAY,WAAW;AACzB,eAAW,SAAS,yBAAyB;AAC3C,UAAI,qBAAqB,WAAW,KAAK,EAAG,OAAM,KAAK,KAAK;AAAA,IAC9D;AACA,WAAO;AAAA,EACT;AAGA,MAAI,qBAAqB,WAAW,UAAU,EAAG,OAAM,KAAK,UAAU;AACtE,SAAO;AACT;;;AChEA,SAAS,gBAAAA,qBAAoB;AAC7B,SAAS,qBAAqB;AAC9B,SAAS,SAAS,QAAAC,aAAY;AAE9B,IAAM,YAAY,QAAQ,cAAc,YAAY,GAAG,CAAC;AAEjD,SAAS,oBAA4B;AAC1C,QAAM,UAAUA,MAAK,WAAW,MAAM,cAAc;AACpD,QAAM,MAAMD,cAAa,SAAS,OAAO;AACzC,QAAM,MAAM,KAAK,MAAM,GAAG;AAC1B,MAAI,OAAO,IAAI,YAAY,YAAY,IAAI,QAAQ,WAAW,GAAG;AAC/D,UAAM,IAAI,MAAM,4BAA4B,OAAO,6BAA6B;AAAA,EAClF;AACA,SAAO,IAAI;AACb;","names":["readFileSync","join"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/version.ts"],"sourcesContent":["import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';\nimport { join, dirname } from 'node:path';\nimport { createHash } from 'node:crypto';\n\n/**\n * Project-relative path to Joycraft's upgrade-state file.\n *\n * Hidden inside `.claude/` — the dir `init` always creates for every harness —\n * directly analogous to npm's own hidden lockfile at\n * `node_modules/.package-lock.json`. Never at the repo root, never committed\n * (init/upgrade gitignore it). The old root location was `.joycraft-version`\n * (see LEGACY_VERSION_FILE); `upgrade` migrates it on first run.\n */\nexport const STATE_PATH = join('.claude', '.joycraft', 'state.json');\n\n/** The pre-relocation root path. Kept only so `upgrade` can migrate it. */\nexport const LEGACY_VERSION_FILE = '.joycraft-version';\n\n/**\n * Length we truncate stored hashes to. Full SHA-256 is 64 hex chars; 16 hex\n * (64 bits) is ample to detect customization across ~100 managed files and\n * shrinks the state ~4×. Compare truncated-on-both-sides (see upgrade.ts).\n */\nconst HASH_LENGTH = 16;\n\n/**\n * How much of the Joycraft harness is tracked in git.\n * - `shared` — commit skills/agents/pi so teammates get the workflow (default).\n * - `private` — gitignore .claude/, .agents/, .pi/; track only CLAUDE.md,\n * AGENTS.md, and docs/.\n */\nexport type GitignoreProfile = 'shared' | 'private';\n\nexport const DEFAULT_GITIGNORE_PROFILE: GitignoreProfile = 'shared';\n\n/** Narrow an arbitrary value to a GitignoreProfile, or null if unrecognized. */\nexport function parseGitignoreProfile(value: unknown): GitignoreProfile | null {\n return value === 'shared' || value === 'private' ? value : null;\n}\n\nexport interface VersionInfo {\n version: string;\n files: Record<string, string>;\n /**\n * The gitignore profile chosen at init/upgrade. Absent on state written by\n * Joycraft versions before this field existed — treat absent as `shared`.\n */\n gitignoreProfile?: GitignoreProfile;\n}\n\nexport function hashContent(content: string): string {\n return createHash('sha256').update(content).digest('hex');\n}\n\n/** Truncate a (full) content hash to the stored length. Idempotent for already-short input. */\nexport function truncateHash(hash: string): string {\n return hash.slice(0, HASH_LENGTH);\n}\n\nexport function readVersion(dir: string): VersionInfo | null {\n const filePath = join(dir, STATE_PATH);\n if (!existsSync(filePath)) return null;\n try {\n const raw = readFileSync(filePath, 'utf-8');\n const parsed = JSON.parse(raw);\n if (typeof parsed.version === 'string' && typeof parsed.files === 'object') {\n // Sanitize the profile: ignore unknown/legacy values rather than\n // returning them (absent or bogus → undefined, callers default to shared).\n const profile = parseGitignoreProfile(parsed.gitignoreProfile);\n return {\n version: parsed.version,\n files: parsed.files,\n ...(profile ? { gitignoreProfile: profile } : {}),\n };\n }\n return null;\n } catch {\n return null;\n }\n}\n\nexport function writeVersion(\n dir: string,\n version: string,\n files: Record<string, string>,\n gitignoreProfile?: GitignoreProfile\n): void {\n const filePath = join(dir, STATE_PATH);\n // Store truncated hashes — single source of truth for the on-disk shape.\n const truncated: Record<string, string> = {};\n for (const [path, hash] of Object.entries(files)) {\n truncated[path] = truncateHash(hash);\n }\n const data: VersionInfo = {\n version,\n files: truncated,\n ...(gitignoreProfile ? { gitignoreProfile } : {}),\n };\n mkdirSync(dirname(filePath), { recursive: true });\n writeFileSync(filePath, JSON.stringify(data, null, 2) + '\\n', 'utf-8');\n}\n\n/**\n * Detect the current Joycraft harness level for a project directory.\n * Returns 5 if Level 5 artifacts (autofix workflow + External Validation) are present, 4 otherwise.\n */\nexport function getLevel(dir: string): number {\n const hasAutofix = existsSync(join(dir, '.github', 'workflows', 'autofix.yml'));\n if (!hasAutofix) return 4;\n const claudeMdPath = join(dir, 'CLAUDE.md');\n if (!existsSync(claudeMdPath)) return 4;\n const content = readFileSync(claudeMdPath, 'utf-8');\n return content.includes('## External Validation') ? 5 : 4;\n}\n"],"mappings":";;;AAAA,SAAS,cAAc,eAAe,YAAY,iBAAiB;AACnE,SAAS,MAAM,eAAe;AAC9B,SAAS,kBAAkB;AAWpB,IAAM,aAAa,KAAK,WAAW,aAAa,YAAY;AAG5D,IAAM,sBAAsB;AAOnC,IAAM,cAAc;AAUb,IAAM,4BAA8C;AAGpD,SAAS,sBAAsB,OAAyC;AAC7E,SAAO,UAAU,YAAY,UAAU,YAAY,QAAQ;AAC7D;AAYO,SAAS,YAAY,SAAyB;AACnD,SAAO,WAAW,QAAQ,EAAE,OAAO,OAAO,EAAE,OAAO,KAAK;AAC1D;AAGO,SAAS,aAAa,MAAsB;AACjD,SAAO,KAAK,MAAM,GAAG,WAAW;AAClC;AAEO,SAAS,YAAY,KAAiC;AAC3D,QAAM,WAAW,KAAK,KAAK,UAAU;AACrC,MAAI,CAAC,WAAW,QAAQ,EAAG,QAAO;AAClC,MAAI;AACF,UAAM,MAAM,aAAa,UAAU,OAAO;AAC1C,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAI,OAAO,OAAO,YAAY,YAAY,OAAO,OAAO,UAAU,UAAU;AAG1E,YAAM,UAAU,sBAAsB,OAAO,gBAAgB;AAC7D,aAAO;AAAA,QACL,SAAS,OAAO;AAAA,QAChB,OAAO,OAAO;AAAA,QACd,GAAI,UAAU,EAAE,kBAAkB,QAAQ,IAAI,CAAC;AAAA,MACjD;AAAA,IACF;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,aACd,KACA,SACA,OACA,kBACM;AACN,QAAM,WAAW,KAAK,KAAK,UAAU;AAErC,QAAM,YAAoC,CAAC;AAC3C,aAAW,CAAC,MAAM,IAAI,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,cAAU,IAAI,IAAI,aAAa,IAAI;AAAA,EACrC;AACA,QAAM,OAAoB;AAAA,IACxB;AAAA,IACA,OAAO;AAAA,IACP,GAAI,mBAAmB,EAAE,iBAAiB,IAAI,CAAC;AAAA,EACjD;AACA,YAAU,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AAChD,gBAAc,UAAU,KAAK,UAAU,MAAM,MAAM,CAAC,IAAI,MAAM,OAAO;AACvE;AAMO,SAAS,SAAS,KAAqB;AAC5C,QAAM,aAAa,WAAW,KAAK,KAAK,WAAW,aAAa,aAAa,CAAC;AAC9E,MAAI,CAAC,WAAY,QAAO;AACxB,QAAM,eAAe,KAAK,KAAK,WAAW;AAC1C,MAAI,CAAC,WAAW,YAAY,EAAG,QAAO;AACtC,QAAM,UAAU,aAAa,cAAc,OAAO;AAClD,SAAO,QAAQ,SAAS,wBAAwB,IAAI,IAAI;AAC1D;","names":[]}