"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // If the importer is in node compatibility mode or this is not an ESM
  // file that has been converted to a CommonJS file using a Babel-
  // compatible transform (i.e. "__esModule" has not been set), then set
  // "default" to the CommonJS "module.exports" for node compatibility.
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

// src/server/deck-serve.ts
var deck_serve_exports = {};
__export(deck_serve_exports, {
  broadcastToClients: () => broadcastToClients,
  consumeRenderError: () => consumeRenderError,
  createFeedbackWaiter: () => createFeedbackWaiter,
  resolveFeedback: () => resolveFeedback,
  runServeCli: () => runServeCli,
  startDeckServer: () => startDeckServer
});
module.exports = __toCommonJS(deck_serve_exports);
var import_node_http = __toESM(require("node:http"));
var import_node_fs3 = __toESM(require("node:fs"));
var import_node_path3 = __toESM(require("node:path"));
var import_ws = require("ws");

// src/server/launch-kit-paths.ts
var LAUNCHSECURE_DIR = ".launchsecure";

// src/server/deck-lockfile.ts
var import_node_child_process = require("node:child_process");
var import_node_fs = require("node:fs");
var import_node_os = require("node:os");
var import_node_path = require("node:path");
function lockDir(projectRoot) {
  if (projectRoot) {
    return (0, import_node_path.join)(projectRoot, LAUNCHSECURE_DIR);
  }
  return (0, import_node_path.join)((0, import_node_os.homedir)(), LAUNCHSECURE_DIR);
}
function lockPath(projectRoot) {
  return (0, import_node_path.join)(lockDir(projectRoot), "launch-deck.lock");
}
var _activeProjectRoot;
function readLock(projectRoot) {
  const root = projectRoot ?? _activeProjectRoot;
  const p = lockPath(root);
  if (!(0, import_node_fs.existsSync)(p)) return null;
  try {
    const data = JSON.parse((0, import_node_fs.readFileSync)(p, "utf-8"));
    if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
    return data;
  } catch {
    return null;
  }
}
function isPidAlive(pid) {
  try {
    process.kill(pid, 0);
    return true;
  } catch {
    return false;
  }
}
function getListenerPid(port) {
  try {
    const out = (0, import_node_child_process.execFileSync)("lsof", ["-nP", "-iTCP:" + port, "-sTCP:LISTEN", "-t"], {
      encoding: "utf-8",
      stdio: ["ignore", "pipe", "ignore"],
      timeout: 500
    }).trim();
    if (!out) return null;
    const pid = parseInt(out.split("\n")[0], 10);
    return Number.isFinite(pid) ? pid : null;
  } catch {
    return null;
  }
}
function getLiveLock(projectRoot) {
  const root = projectRoot ?? _activeProjectRoot;
  const lock = readLock(root);
  if (!lock) return null;
  const listenerPid = getListenerPid(lock.port);
  const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
  if (!live) {
    try {
      (0, import_node_fs.unlinkSync)(lockPath(root));
    } catch {
    }
    return null;
  }
  return lock;
}
function writeLock(data, projectRoot) {
  const root = projectRoot ?? _activeProjectRoot;
  (0, import_node_fs.mkdirSync)(lockDir(root), { recursive: true });
  (0, import_node_fs.writeFileSync)(lockPath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
  if (root) _activeProjectRoot = root;
}
function clearLock(projectRoot) {
  const root = projectRoot ?? _activeProjectRoot;
  try {
    (0, import_node_fs.unlinkSync)(lockPath(root));
  } catch {
  }
}

// src/server/deck-config.ts
var import_node_fs2 = require("node:fs");
var import_node_path2 = require("node:path");
var CONFIG_FILENAME = ".launchdeck.json";
function loadDeckConfig(rootDir) {
  const configPath = (0, import_node_path2.join)(rootDir, CONFIG_FILENAME);
  if (!(0, import_node_fs2.existsSync)(configPath)) return {};
  try {
    return JSON.parse((0, import_node_fs2.readFileSync)(configPath, "utf-8"));
  } catch {
    return {};
  }
}
function saveDeckConfig(rootDir, config) {
  const configPath = (0, import_node_path2.join)(rootDir, CONFIG_FILENAME);
  (0, import_node_fs2.writeFileSync)(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
}

// src/server/blast-radius-render.ts
function generateBlastRadiusHtml(manifest, baseUrl) {
  const data = JSON.stringify(manifest);
  const dlBase = baseUrl ? baseUrl.replace("/deck-files/", "/deck-download/") : "";
  return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blast Radius \u2014 ${escHtml(manifest.title)}</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#08090e;color:#e2e8f0;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','Segoe UI',system-ui,sans-serif;overflow:hidden;height:100vh;width:100vw}
.header{position:fixed;top:0;left:0;right:0;z-index:20;padding:16px 24px 12px;background:linear-gradient(180deg,rgba(8,9,14,.98) 60%,rgba(8,9,14,0) 100%);pointer-events:none}
.header>*{pointer-events:auto}
.header h1{font-size:10px;font-weight:600;color:#64748b;letter-spacing:1.2px;text-transform:uppercase}
.header h2{font-size:18px;font-weight:700;color:#f1f5f9;margin-top:4px}
.header h2 span{color:#b91c1c}
.stats{display:flex;gap:24px;margin-top:10px;flex-wrap:wrap;align-items:center}
.stat-val{font-size:22px;font-weight:700;line-height:1}
.stat-label{font-size:9px;color:#475569;text-transform:uppercase;letter-spacing:.6px;margin-top:2px}
.stat-sep{width:1px;height:28px;background:#334155}
.filter-panel{position:fixed;top:16px;right:24px;z-index:20;display:flex;flex-direction:column;gap:3px;background:rgba(8,9,14,.92);padding:14px;border-radius:12px;backdrop-filter:blur(12px);border:1px solid rgba(100,116,139,.12);min-width:160px}
.fp-section{font-size:8px;color:#475569;text-transform:uppercase;letter-spacing:1.2px;margin-top:10px;padding:0 6px}
.fp-section:first-child{margin-top:0}
.fp-btn{display:flex;align-items:center;gap:10px;font-size:12px;color:#64748b;background:none;border:1px solid transparent;border-radius:6px;padding:7px 10px;cursor:pointer;transition:all .15s;width:100%;text-align:left}
.fp-btn:hover{background:rgba(51,65,85,.5);color:#cbd5e1}
.fp-btn.active{background:rgba(99,102,241,.12);color:#e2e8f0;border-color:rgba(99,102,241,.2)}
.fp-toggle{display:flex;align-items:center;justify-content:space-between;font-size:12px;color:#64748b;background:rgba(30,41,59,.5);border:1px solid rgba(100,116,139,.15);border-radius:6px;padding:7px 10px;cursor:pointer;transition:all .15s;width:100%;text-align:left}
.fp-toggle:hover{background:rgba(51,65,85,.6);color:#cbd5e1}
.fp-toggle .toggle-dot{width:32px;height:16px;border-radius:8px;background:#334155;position:relative;transition:all .2s;flex-shrink:0}
.fp-toggle .toggle-dot::after{content:'';position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:#64748b;transition:all .2s}
.fp-toggle.on .toggle-dot{background:rgba(99,102,241,.4)}
.fp-toggle.on .toggle-dot::after{left:18px;background:#818cf8}
.fp-toggle.on{color:#e2e8f0}
.fp-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
.fp-ring{width:10px;height:10px;border-radius:50%;border:2px solid;flex-shrink:0;background:transparent}
.fp-icon{width:16px;height:16px;flex-shrink:0;display:flex;align-items:center;justify-content:center}
.fp-icon svg{width:16px;height:16px}
.fp-edge-line{width:16px;height:2px;border-radius:1px;flex-shrink:0}
.detail-panel{position:fixed;right:24px;top:50%;transform:translateY(-50%);z-index:100;display:none;width:340px;max-height:70vh;overflow-y:auto;background:rgba(15,23,42,.97);border:1px solid rgba(100,116,139,.2);border-radius:12px;padding:0;backdrop-filter:blur(16px);box-shadow:0 16px 48px rgba(0,0,0,.7)}
.dp-header{padding:16px 18px 12px;border-bottom:1px solid rgba(100,116,139,.1)}
.dp-name{font-weight:700;font-size:16px}
.dp-badges{display:flex;gap:5px;margin-top:8px;flex-wrap:wrap}
.dp-badge{font-size:10px;padding:3px 9px;border-radius:4px;font-weight:600;letter-spacing:.3px}
.dp-body{padding:14px 18px 16px}
.dp-section{margin-top:12px}
.dp-section:first-child{margin-top:0}
.dp-section-label{font-size:9px;color:#475569;text-transform:uppercase;letter-spacing:1px;margin-bottom:4px;font-weight:600}
.dp-path{color:#64748b;font-size:11px;word-break:break-all;font-family:'SF Mono','Fira Code',monospace;background:rgba(100,116,139,.08);padding:6px 10px;border-radius:6px}
.dp-reason{color:#cbd5e1;font-size:12px;line-height:1.6}
.dp-connections{list-style:none;padding:0}
.dp-connections li{font-size:11px;color:#94a3b8;padding:3px 0;display:flex;align-items:center;gap:6px}
.dp-close{position:absolute;top:12px;right:12px;background:none;border:none;color:#64748b;cursor:pointer;font-size:18px;line-height:1;padding:4px}
.dp-close:hover{color:#e2e8f0}
.controls{display:none}
.ring-label{font-size:8px;fill:#475569;text-anchor:start;font-weight:600;letter-spacing:1.5px;text-transform:uppercase}
.edge-tooltip{position:fixed;z-index:200;pointer-events:none;background:rgba(15,23,42,.95);border:1px solid rgba(100,116,139,.25);border-radius:8px;padding:8px 12px;backdrop-filter:blur(8px);font-size:11px;line-height:1.5;max-width:260px;display:none}
.edge-tooltip .et-action{font-weight:700;font-size:10px;letter-spacing:.5px;text-transform:uppercase}
.edge-tooltip .et-nodes{color:#94a3b8}
.edge-tooltip .et-label{color:#cbd5e1;margin-top:2px;font-style:italic}
</style>
</head>
<body>
<div class="header">
  <h1>${escHtml(manifest.subtitle || (manifest.mode === "structural" ? "Structural Blast Radius" : "Feature Blast Radius"))}</h1>
  <h2><span>${escHtml(manifest.title)}</span></h2>
  <div class="stats" id="stats"></div>
</div>
<div class="filter-panel" id="filterPanel"></div>
<div class="controls" id="controls"></div>
<div class="detail-panel" id="detail">
  <button class="dp-close" id="dpClose">&times;</button>
  <div class="dp-header">
    <div class="dp-name" id="dpName"></div>
    <div class="dp-badges" id="dpBadges"></div>
  </div>
  <div class="dp-body">
    <div class="dp-section" id="dpPathSection">
      <div class="dp-section-label">File</div>
      <div class="dp-path" id="dpPath"></div>
    </div>
    <div class="dp-section" id="dpReasonSection">
      <div class="dp-section-label">Why is this affected?</div>
      <div class="dp-reason" id="dpReason"></div>
    </div>
    <div class="dp-section" id="dpAcceptanceSection">
      <div class="dp-section-label">Acceptance Criteria</div>
      <ul class="dp-connections" id="dpAcceptance"></ul>
    </div>
    <div class="dp-section" id="dpConnSection">
      <div class="dp-section-label">Connected to</div>
      <ul class="dp-connections" id="dpConn"></ul>
    </div>
  </div>
</div>
<div class="edge-tooltip" id="edgeTooltip"></div>
<svg id="graph"></svg>
<script>
var M = ${data};
var DL_BASE = '${dlBase}';

var layerMap = {};
M.layers.forEach(function(l) { layerMap[l.id] = l; });
var ringMap = {};
M.rings.forEach(function(r) { ringMap[r.id] = r; });
var centerColor = '#b91c1c';

// \u2500\u2500 Edge Action Styles \u2500\u2500
var edgeActionStyles = {
  create:   {color:'#22c55e', dash:'',    opacity:0.55, width:2},
  modify:   {color:'#f59e0b', dash:'7,4', opacity:0.55, width:2},
  remove:   {color:'#ef4444', dash:'4,4', opacity:0.5,  width:1.8},
  existing: {color:'#1e293b', dash:'',    opacity:0.25, width:1}
};
var defaultEdgeStyle = edgeActionStyles.existing;
function edgeStyle(d){return edgeActionStyles[d.action]||defaultEdgeStyle;}
var hasActionEdges = M.edges.some(function(e){return e.action && e.action!=='existing';});

// \u2500\u2500 Stats \u2500\u2500
var statsEl = document.getElementById('stats');
M.rings.forEach(function(r) {
  var count = M.nodes.filter(function(n){return n.ring===r.id}).length;
  var d = document.createElement('div');
  d.innerHTML = '<div class="stat-val" style="color:'+r.color+'">'+count+'</div><div class="stat-label">'+r.name+'</div>';
  statsEl.appendChild(d);
});
(function(){var d=document.createElement('div');d.innerHTML='<div class="stat-val" style="color:#e2e8f0">'+M.layers.length+'</div><div class="stat-label">Layers</div>';statsEl.appendChild(d);})();

// Edge action stats (only when action edges exist)
if(hasActionEdges){
  var sep=document.createElement('div');sep.className='stat-sep';statsEl.appendChild(sep);
  ['create','modify','remove'].forEach(function(a){
    var count=M.edges.filter(function(e){return e.action===a;}).length;
    if(count===0)return;
    var s=edgeActionStyles[a];
    var d=document.createElement('div');
    d.innerHTML='<div class="stat-val" style="color:'+s.color+'">'+count+'</div><div class="stat-label">'+a.toUpperCase()+' edges</div>';
    statsEl.appendChild(d);
  });
}

// \u2500\u2500 Filter Panel (left vertical) \u2500\u2500
var fpEl = document.getElementById('filterPanel');
var fpHtml = '';
fpHtml += '<div class="fp-section">Toggles</div>';
fpHtml += '<button class="fp-toggle on" id="btnLabels">Aa Labels<div class="toggle-dot"></div></button>';
fpHtml += '<button class="fp-toggle on" id="btnRings">Ring colors<div class="toggle-dot"></div></button>';
fpHtml += '<div class="fp-section" style="margin-top:4px"></div>';
fpHtml += '<button class="fp-btn" id="btnReset">\\u21ba Reset zoom</button>';
fpHtml += '<div class="fp-section">Ring</div>';
fpHtml += '<button class="fp-btn active" id="rAll" data-ring="all"><div class="fp-dot" style="background:#94a3b8"></div> All</button>';
M.rings.forEach(function(r){fpHtml+='<button class="fp-btn" id="r_'+r.id+'" data-ring="'+r.id+'"><div class="fp-ring" style="border-color:'+r.color+'"></div> '+r.name+'</button>';});
fpHtml += '<div class="fp-section">Layer</div>';
fpHtml += '<button class="fp-btn active" id="lAll" data-layer="all"><div class="fp-dot" style="background:#94a3b8"></div> All</button>';
M.layers.forEach(function(l){fpHtml+='<button class="fp-btn" id="l_'+l.id+'" data-layer="'+l.id+'"><div class="fp-icon" id="fpi_'+l.id+'" style="color:'+l.color+'"></div> '+l.name+'</button>';});

// Edge Action filter (only when action edges exist)
if(hasActionEdges){
  fpHtml += '<div class="fp-section">Edge Action</div>';
  fpHtml += '<button class="fp-btn active" id="eAll" data-edge="all"><div class="fp-dot" style="background:#94a3b8"></div> All</button>';
  ['create','modify','remove','existing'].forEach(function(a){
    var count=M.edges.filter(function(e){return(e.action||'existing')===a;}).length;
    if(count===0)return;
    var s=edgeActionStyles[a];
    var dashStyle=s.dash?'background:repeating-linear-gradient(90deg,'+s.color+' 0,'+s.color+' 4px,transparent 4px,transparent 7px)':'background:'+s.color;
    fpHtml+='<button class="fp-btn" id="e_'+a+'" data-edge="'+a+'"><div class="fp-edge-line" style="'+dashStyle+'"></div> '+a.charAt(0).toUpperCase()+a.slice(1)+' <span style="color:#475569;font-size:10px">('+count+')</span></button>';
  });
}

fpEl.innerHTML = fpHtml;

var ringsOn = true;

// Attach filter panel handlers via event delegation
fpEl.addEventListener('click', function(e) {
  var btn = e.target.closest('.fp-btn,.fp-toggle');
  if (!btn) return;
  if (btn.id === 'btnReset') { svg.transition().duration(400).call(zoomBehavior.transform, d3.zoomIdentity); return; }
  if (btn.id === 'btnLabels') { labelsOn=!labelsOn; labelEls.transition().duration(200).attr('opacity',labelsOn?1:0); btn.classList.toggle('on',labelsOn); return; }
  if (btn.id === 'btnRings') {
    ringsOn=!ringsOn;
    btn.classList.toggle('on',ringsOn);
    // Hide/show node circles (not center)
    nodeEls.select('.node-bg')
      .transition().duration(300)
      .attr('stroke',function(d){
        if(d.hop===0) return centerColor;
        return ringsOn?(ringMap[d.ring]?ringMap[d.ring].color:'#666'):'transparent';
      })
      .attr('fill',function(d){
        if(d.hop===0) return '#1f0a0a';
        return ringsOn?'rgba(8,9,14,.85)':'transparent';
      })
      .attr('stroke-width',function(d){if(d.hop===0) return 3; return ringsOn?3:0;});
    // Concentric ring circles: colored when on, grey when off
    g.selectAll('.ring-circle').transition().duration(300)
      .attr('stroke',function(){return ringsOn?d3.select(this).attr('data-color'):'#334155';})
      .attr('stroke-opacity',ringsOn?0.3:0.15);
    // Hide/show edges \u2014 restore action-based opacity when turning back on
    edgePaths.transition().duration(300).attr('opacity',function(d){return ringsOn?edgeStyle(d).opacity:0;});
    // Scale icons (not center)
    nodeEls.each(function(d){
      if(d.hop===0) return;
      var fo=d3.select(this).select('foreignObject');
      if(fo.empty()) return;
      var r=nR(d);
      var s=ringsOn?Math.round(r*1.1):Math.round(r*2.5);
      fo.attr('x',-s/2).attr('y',-s/2).attr('width',s).attr('height',s);
      fo.select('div').style('width',s+'px').style('height',s+'px');
      var icon=fo.select('svg');
      if(!icon.empty()){
        icon.attr('width',s).attr('height',s)
          .style('width',s+'px').style('height',s+'px')
          .style('color','#cbd5e1')
          .style('fill',ringsOn?'rgba(203,213,225,0.15)':'rgba(203,213,225,0.3)')
          .style('stroke-width',ringsOn?'1.5':'1');
      }
    });
    return;
  }
  if (btn.dataset && btn.dataset.ring !== undefined) { filterRing(btn.dataset.ring); return; }
  if (btn.dataset && btn.dataset.layer !== undefined) { filterLayer(btn.dataset.layer); return; }
  if (btn.dataset && btn.dataset.edge !== undefined) { filterEdgeAction(btn.dataset.edge); return; }
});

// \u2500\u2500 Layout \u2500\u2500
var W = window.innerWidth, H = window.innerHeight;
var cx = W/2, cy = H/2 + 25;
var R = Math.min(W, H);
var ringIds = M.rings.map(function(r){return r.id});
var ringRadii = {};
ringIds.forEach(function(id, i){ ringRadii[id] = R * (0.14 + i * 0.13); });

var centerNode = {id:'__center__',name:M.center.name,layer:M.layers[0]?M.layers[0].id:'ui',ring:'__center__',path:'',reason:M.center.description,type:'feature',hop:0};
var allNodes = [centerNode].concat(M.nodes.map(function(n){var copy={};for(var k in n)copy[k]=n[k];copy.hop=ringIds.indexOf(n.ring)+1;return copy;}));

centerNode.fx = cx; centerNode.fy = cy;
ringIds.forEach(function(ringId){
  var ring = allNodes.filter(function(n){return n.ring===ringId});
  var layerOrder = {};
  M.layers.forEach(function(l,i){layerOrder[l.id]=i;});
  ring.sort(function(a,b){return(layerOrder[a.layer]||99)-(layerOrder[b.layer]||99);});
  var r = ringRadii[ringId];
  ring.forEach(function(n,i){var angle=-Math.PI/2+(i/ring.length)*Math.PI*2;n.fx=cx+Math.cos(angle)*r;n.fy=cy+Math.sin(angle)*r;});
});

// \u2500\u2500 D3 Render \u2500\u2500
var svg = d3.select('#graph').attr('width',W).attr('height',H);
var g = svg.append('g');
var zoomBehavior = d3.zoom().scaleExtent([0.3,5])
  .filter(function(event){
    // Don't zoom on clicks \u2014 only on wheel, drag, pinch
    if (event.type === 'mousedown' || event.type === 'touchstart') return true;
    if (event.type === 'wheel') return true;
    if (event.type === 'dblclick') return true;
    return true;
  })
  .on('zoom',function(e){g.attr('transform',e.transform);});
svg.call(zoomBehavior);

ringIds.forEach(function(id){
  var r = ringRadii[id];
  g.append('circle').attr('class','ring-circle').attr('data-color',ringMap[id].color).attr('cx',cx).attr('cy',cy).attr('r',r).attr('fill','none').attr('stroke',ringMap[id].color).attr('stroke-opacity',0.3).attr('stroke-width',1);
  var angle = -Math.PI*0.18;
  g.append('text').attr('class','ring-label').attr('x',cx+Math.cos(angle)*r+10).attr('y',cy+Math.sin(angle)*r).attr('fill',ringMap[id].color).attr('fill-opacity',1).text(ringMap[id].name.toUpperCase());
});

function nR(d){if(d.hop===0)return 30;return Math.max(18-d.hop*3,12);}
function isRemoveNode(d){return d.ring==='remove';}

var edgeGroup = g.append('g');
var edgePaths = edgeGroup.selectAll('path').data(M.edges).join('path')
  .attr('fill','none')
  .attr('stroke',function(d){return edgeStyle(d).color;})
  .attr('stroke-width',function(d){return edgeStyle(d).width;})
  .attr('opacity',function(d){return edgeStyle(d).opacity;})
  .attr('stroke-dasharray',function(d){return edgeStyle(d).dash;})
  .attr('d',function(d){
    var src=allNodes.find(function(n){return n.id===(d.source==='center'?'__center__':d.source)});
    var tgt=allNodes.find(function(n){return n.id===d.target});
    if(!src||!tgt)return'';
    var dx=tgt.fx-src.fx,dy=tgt.fy-src.fy,dist=Math.sqrt(dx*dx+dy*dy)||1;
    var ux=dx/dist,uy=dy/dist;
    return'M'+(src.fx+ux*nR(src))+','+(src.fy+uy*nR(src))+' A'+(dist*1.1)+','+(dist*1.1)+' 0 0,1 '+(tgt.fx-ux*nR(tgt))+','+(tgt.fy-uy*nR(tgt));
  });

// Edge hover tooltip
var edgeTooltipEl = document.getElementById('edgeTooltip');
edgePaths.on('mouseenter',function(event,d){
  if(selectedNodeId)return;
  var s=edgeStyle(d);
  var srcId=d.source==='center'?'__center__':d.source;
  var srcNode=allNodes.find(function(n){return n.id===srcId});
  var tgtNode=allNodes.find(function(n){return n.id===d.target});
  if(!srcNode||!tgtNode)return;
  var action=(d.action||'existing').toUpperCase();
  var html='<div class="et-action" style="color:'+s.color+'">'+action+'</div>';
  html+='<div class="et-nodes">'+srcNode.name+' \\u2192 '+tgtNode.name+'</div>';
  if(d.label)html+='<div class="et-label">'+d.label+'</div>';
  edgeTooltipEl.innerHTML=html;
  edgeTooltipEl.style.display='block';
  edgeTooltipEl.style.left=(event.clientX+12)+'px';
  edgeTooltipEl.style.top=(event.clientY-10)+'px';
  // Highlight this edge
  d3.select(this).attr('opacity',Math.min(s.opacity+0.3,1)).attr('stroke-width',s.width+1);
}).on('mousemove',function(event){
  edgeTooltipEl.style.left=(event.clientX+12)+'px';
  edgeTooltipEl.style.top=(event.clientY-10)+'px';
}).on('mouseleave',function(event,d){
  edgeTooltipEl.style.display='none';
  var s=edgeStyle(d);
  d3.select(this).attr('opacity',s.opacity).attr('stroke-width',s.width);
});
// Make edges easier to hover
edgePaths.style('pointer-events','stroke').attr('cursor','pointer');

var nodeGroup = g.append('g');
var nodeEls = nodeGroup.selectAll('.node').data(allNodes).join('g')
  .attr('class','node').attr('transform',function(d){return'translate('+d.fx+','+d.fy+')';}).style('cursor','pointer');

nodeEls.append('circle').attr('class','node-glow').attr('r',function(d){return nR(d)+12})
  .attr('fill',function(d){return d.hop===0?centerColor:(ringMap[d.ring]?ringMap[d.ring].color:'#666')}).attr('opacity',0);

nodeEls.append('circle').attr('class','node-bg').attr('r',function(d){return nR(d)})
  .attr('fill',function(d){if(d.hop===0)return'#1f0a0a';if(isRemoveNode(d))return'rgba(127,29,29,.6)';return'rgba(8,9,14,.85)';})
  .attr('stroke',function(d){return d.hop===0?centerColor:(ringMap[d.ring]?ringMap[d.ring].color:'#666')})
  .attr('stroke-width',function(d){return d.hop===0?3:3});

nodeEls.each(function(d){
  var el=d3.select(this),r=nR(d);
  if(d.hop===0){var cs=Math.round(r*1.3);var cfo=el.append('foreignObject').attr('x',-cs/2).attr('y',-cs/2).attr('width',cs).attr('height',cs).attr('style','overflow:visible;pointer-events:none;');cfo.append('xhtml:div').attr('style','width:'+cs+'px;height:'+cs+'px;display:flex;align-items:center;justify-content:center;').html('<i data-lucide="radiation" style="width:'+cs+'px;height:'+cs+'px;color:#fca5a5;fill:rgba(252,165,165,0.25);stroke-width:1.5;"></i>');return;}
  var layer=layerMap[d.layer];if(!layer)return;
  var s=Math.round(r*1.1);
  var fo=el.append('foreignObject').attr('x',-s/2).attr('y',-s/2).attr('width',s).attr('height',s).attr('style','overflow:visible;pointer-events:none;');
  var iconColor=isRemoveNode(d)?'#fca5a5':'#cbd5e1';
  var iconFill=isRemoveNode(d)?'rgba(252,165,165,0.15)':'rgba(203,213,225,0.15)';
  fo.append('xhtml:div').attr('style','width:'+s+'px;height:'+s+'px;display:flex;align-items:center;justify-content:center;')
    .html('<i data-lucide="'+layer.icon+'" style="width:'+s+'px;height:'+s+'px;color:'+iconColor+';fill:'+iconFill+';stroke-width:1.5;"></i>');
});

var pulseEl=nodeEls.filter(function(d){return d.hop===0}).append('circle').attr('r',30).attr('fill','none').attr('stroke',centerColor).attr('stroke-width',1.5).attr('opacity',0.4);
(function pulse(){pulseEl.attr('r',30).attr('opacity',0.4).transition().duration(2200).ease(d3.easeCubicOut).attr('r',55).attr('opacity',0).on('end',pulse);})();

var labelsOn = true;
var labelEls = nodeEls.append('text').attr('text-anchor','middle').attr('y',function(d){return nR(d)+15})
  .attr('font-size',function(d){return d.hop<=1?11:10}).attr('fill',function(d){if(d.hop===0)return centerColor;if(isRemoveNode(d))return'#f87171';return'#94a3b8';}).attr('font-weight',function(d){return d.hop===0?700:500})
  .attr('text-decoration',function(d){return isRemoveNode(d)?'line-through':'none';})
  .text(function(d){var m=d.hop>=2?16:20;return d.name.length>m?d.name.slice(0,m-1)+'\\u2026':d.name;});

lucide.createIcons();
M.layers.forEach(function(l){var el=document.getElementById('fpi_'+l.id);if(el){el.innerHTML='<i data-lucide="'+l.icon+'" style="width:12px;height:12px;color:#cbd5e1;fill:rgba(203,213,225,0.15);stroke-width:2;"></i>';}});
lucide.createIcons();

// \u2500\u2500 State \u2500\u2500
var activeRing = 'all', activeLayer = 'all', activeEdgeAction = 'all', selectedNodeId = null;

function isNodeVisible(d) {
  if(d.hop===0)return true;
  return(activeRing==='all'||d.ring===activeRing)&&(activeLayer==='all'||d.layer===activeLayer);
}
function isEdgeActionVisible(e) {
  if(activeEdgeAction==='all')return true;
  return(e.action||'existing')===activeEdgeAction;
}
function getRelated(nid) {
  var s = {};s[nid]=true;
  M.edges.forEach(function(e){var src=e.source==='center'?'__center__':e.source;if(src===nid||e.target===nid){s[src]=true;s[e.target]=true;}});
  return s;
}
function getConnections(nid) {
  var conns=[];
  M.edges.forEach(function(e){
    var src=e.source==='center'?'__center__':e.source;
    if(src===nid){var t=allNodes.find(function(n){return n.id===e.target});if(t)conns.push({dir:'to',node:t,action:e.action,label:e.label});}
    if(e.target===nid){var s=allNodes.find(function(n){return n.id===src});if(s)conns.push({dir:'from',node:s,action:e.action,label:e.label});}
  });
  return conns;
}

// \u2500\u2500 Hover: highlight edges only (no panel) \u2500\u2500
nodeEls.on('mouseenter',function(event,d){
  if(selectedNodeId)return;
  var related=d.hop===0?allNodes.reduce(function(o,n){o[n.id]=true;return o;},{}):getRelated(d.id);
  nodeEls.transition().duration(150).style('opacity',function(n){
    if(!isNodeVisible(n))return 0.06;
    return related[n.id]?1:0.12;
  });
  edgePaths.transition().duration(150)
    .attr('opacity',function(e){var s=e.source==='center'?'__center__':e.source;var sN=allNodes.find(function(n){return n.id===s});var tN=allNodes.find(function(n){return n.id===e.target});if(sN&&!isNodeVisible(sN)&&sN.hop!==0)return 0.02;if(tN&&!isNodeVisible(tN)&&tN.hop!==0)return 0.02;var isRelated=related[s]&&related[e.target];if(!isRelated)return 0.03;return Math.min(edgeStyle(e).opacity+0.25,0.85);})
    .attr('stroke',function(e){var s=e.source==='center'?'__center__':e.source;var isRelated=related[s]&&related[e.target];return isRelated?edgeStyle(e).color:edgeStyle(e).color;})
    .attr('stroke-width',function(e){var s=e.source==='center'?'__center__':e.source;var isRelated=related[s]&&related[e.target];return isRelated?edgeStyle(e).width+0.5:edgeStyle(e).width;});
  d3.select(this).select('.node-glow').transition().duration(150).attr('opacity',0.15);
});
nodeEls.on('mouseleave',function(){
  if(selectedNodeId)return;
  applyFilters();
  edgePaths.transition().duration(200)
    .attr('stroke',function(d){return edgeStyle(d).color;})
    .attr('stroke-width',function(d){return edgeStyle(d).width;});
  d3.select(this).select('.node-glow').transition().duration(200).attr('opacity',0);
});

// \u2500\u2500 Click: use pointerdown/pointerup with delta check to avoid zoom interference \u2500\u2500
var ptrDownPos = null;
nodeEls.on('pointerdown',function(event){
  ptrDownPos = {x:event.clientX, y:event.clientY};
});
nodeEls.on('pointerup',function(event,d){
  if(!ptrDownPos) return;
  var dx=event.clientX-ptrDownPos.x, dy=event.clientY-ptrDownPos.y;
  ptrDownPos = null;
  if(Math.sqrt(dx*dx+dy*dy) > 5) return; // was a drag, not a click

  event.stopPropagation();
  selectedNodeId = d.id;

  var related = d.hop===0 ? allNodes.reduce(function(o,n){o[n.id]=true;return o;},{}) : getRelated(d.id);
  nodeEls.transition().duration(200).style('opacity',function(n){
    if(!isNodeVisible(n))return 0.06;
    return related[n.id]?1:0.08;
  });
  edgePaths.transition().duration(200)
    .attr('opacity',function(e){var s=e.source==='center'?'__center__':e.source;var isRelated=related[s]&&related[e.target];return isRelated?Math.min(edgeStyle(e).opacity+0.3,0.9):0.03;})
    .attr('stroke',function(e){var s=e.source==='center'?'__center__':e.source;var isRelated=related[s]&&related[e.target];return isRelated?edgeStyle(e).color:edgeStyle(e).color;})
    .attr('stroke-width',function(e){var s=e.source==='center'?'__center__':e.source;var isRelated=related[s]&&related[e.target];return isRelated?edgeStyle(e).width+1:edgeStyle(e).width;});
  nodeEls.select('.node-glow').attr('opacity',0);
  d3.select(this).select('.node-glow').transition().duration(150).attr('opacity',0.25);

  // Fill detail panel
  var panel=document.getElementById('detail');
  var nameEl=document.getElementById('dpName');
  nameEl.textContent=d.name;
  nameEl.style.color=d.hop===0?centerColor:(ringMap[d.ring]?ringMap[d.ring].color:'#e2e8f0');

  var layer=layerMap[d.layer],ring=ringMap[d.ring];
  document.getElementById('dpBadges').innerHTML=
    (ring?'<span class="dp-badge" style="background:'+ring.color+'22;color:'+ring.color+'">'+ring.name+'</span>':'')+
    (d.hop===0?'<span class="dp-badge" style="background:'+centerColor+'22;color:'+centerColor+'">Center</span>':'')+
    (layer?'<span class="dp-badge" style="background:'+layer.color+'22;color:#93c5fd;border:1px solid '+layer.color+'44">'+layer.name+'</span>':'')+
    (d.type?'<span class="dp-badge" style="background:rgba(100,116,139,.12);color:#94a3b8">'+d.type+'</span>':'');

  if(d.path){document.getElementById('dpPathSection').style.display='';document.getElementById('dpPath').textContent=d.path;}
  else{document.getElementById('dpPathSection').style.display='none';}

  if(d.reason){document.getElementById('dpReasonSection').style.display='';document.getElementById('dpReason').textContent=d.reason;}
  else{document.getElementById('dpReasonSection').style.display='none';}

  var accSection=document.getElementById('dpAcceptanceSection');
  var accEl=document.getElementById('dpAcceptance');
  if(d.acceptance&&d.acceptance.length>0){
    accSection.style.display='';
    accEl.innerHTML=d.acceptance.map(function(a){return '<li style="color:#10b981;font-size:11px;">\\u2713 '+a+'</li>';}).join('');
  } else {accSection.style.display='none';}

  var conns=getConnections(d.id);
  var connSection=document.getElementById('dpConnSection');
  var connEl=document.getElementById('dpConn');
  if(conns.length>0){
    connSection.style.display='';
    connEl.innerHTML=conns.map(function(c){
      var col=c.node.hop===0?centerColor:(ringMap[c.node.ring]?ringMap[c.node.ring].color:'#666');
      var arrow=c.dir==='to'?'\\u2192':'\\u2190';
      var actionBadge='';
      if(c.action&&c.action!=='existing'){
        var ac=edgeActionStyles[c.action];
        actionBadge=' <span style="font-size:9px;padding:1px 5px;border-radius:3px;background:'+ac.color+'22;color:'+ac.color+';font-weight:600;letter-spacing:.3px">'+c.action.toUpperCase()+'</span>';
      }
      var labelText=c.label?' <span style="color:#64748b;font-style:italic">'+c.label+'</span>':'';
      return '<li><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:'+col+'"></span> '+arrow+' '+c.node.name+actionBadge+labelText+'</li>';
    }).join('');
  } else {connSection.style.display='none';}

  panel.style.display='block';
});

// Close detail on SVG background click
svg.on('pointerup',function(event){
  if(!ptrDownPos){closeDetail();}
  ptrDownPos=null;
});

document.getElementById('dpClose').addEventListener('click',function(){closeDetail();});

function closeDetail(){
  selectedNodeId=null;
  document.getElementById('detail').style.display='none';
  applyFilters();
  edgePaths.transition().duration(200)
    .attr('stroke',function(d){return edgeStyle(d).color;})
    .attr('stroke-width',function(d){return edgeStyle(d).width;});
  nodeEls.select('.node-glow').transition().duration(200).attr('opacity',0);
}

// \u2500\u2500 Filters \u2500\u2500
function applyFilters(){
  nodeEls.transition().duration(300).style('opacity',function(d){
    if(d.hop===0)return 1;
    return isNodeVisible(d)?1:0.06;
  });
  edgePaths.transition().duration(300)
    .attr('opacity',function(e){
      if(!isEdgeActionVisible(e))return 0.02;
      if(activeRing==='all'&&activeLayer==='all')return edgeStyle(e).opacity;
      var srcId=e.source==='center'?'__center__':e.source;
      var s=allNodes.find(function(n){return n.id===srcId}),t=allNodes.find(function(n){return n.id===e.target});
      if(!s||!t)return 0.02;
      return((s.hop===0||isNodeVisible(s))&&(t.hop===0||isNodeVisible(t)))?edgeStyle(e).opacity:0.02;
    })
    .attr('stroke',function(e){return edgeStyle(e).color;})
    .attr('stroke-width',function(e){return edgeStyle(e).width;});
}

function filterRing(id){
  activeRing=id;closeDetail();
  document.querySelectorAll('[data-ring]').forEach(function(b){b.classList.remove('active');});
  var el=document.getElementById(id==='all'?'rAll':'r_'+id);if(el)el.classList.add('active');
  applyFilters();
}
function filterLayer(id){
  activeLayer=id;closeDetail();
  document.querySelectorAll('[data-layer]').forEach(function(b){b.classList.remove('active');});
  var el=document.getElementById(id==='all'?'lAll':'l_'+id);if(el)el.classList.add('active');
  applyFilters();
}
function filterEdgeAction(id){
  activeEdgeAction=id;closeDetail();
  document.querySelectorAll('[data-edge]').forEach(function(b){b.classList.remove('active');});
  var el=document.getElementById(id==='all'?'eAll':'e_'+id);if(el)el.classList.add('active');
  applyFilters();
}
</script>
</body>
</html>`;
}
function generateBlastRadiusReport(manifest) {
  const layerMap = {};
  for (const l of manifest.layers) layerMap[l.id] = l.name;
  let md = `# Blast Radius: ${manifest.title}
`;
  md += `Mode: ${manifest.mode} | Layers: ${manifest.layers.length} | Total nodes: ${manifest.nodes.length}

`;
  md += `## Center
`;
  md += `**${manifest.center.name}** \u2014 ${manifest.center.description}

`;
  for (const r of manifest.rings) {
    const nodes = manifest.nodes.filter((n) => n.ring === r.id);
    if (nodes.length === 0) continue;
    md += `## ${r.name} (${nodes.length})

`;
    md += `| Node | Layer | Type | Path | Why |
`;
    md += `|------|-------|------|------|-----|
`;
    for (const n of nodes) {
      const ln = layerMap[n.layer] || n.layer;
      const path2 = (n.path || "-").replace(/\|/g, "/");
      const reason = (n.reason || "-").replace(/\|/g, "/");
      md += `| ${n.name} | ${ln} | ${n.type || "-"} | ${path2} | ${reason} |
`;
    }
    md += `
`;
  }
  md += `## Edges

`;
  for (const e of manifest.edges) {
    const action = e.action ? ` [${e.action.toUpperCase()}]` : "";
    const label = e.label ? ` \u2014 ${e.label}` : "";
    md += `- ${e.source} \u2192 ${e.target}${action}${label}
`;
  }
  return md;
}
function escHtml(s) {
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}

// src/server/contract-generator.ts
function generateContract(manifest) {
  const layerMap = {};
  for (const l of manifest.layers) layerMap[l.id] = l.name;
  const safeRingIds = manifest.rings.filter((r) => /safe|unchanged|no.?change/i.test(r.name) || /safe/i.test(r.id)).map((r) => r.id);
  const actionableNodes = manifest.nodes.filter((n) => !safeRingIds.includes(n.ring));
  const safeNodes = manifest.nodes.filter((n) => safeRingIds.includes(n.ring));
  const createRingIds = manifest.rings.filter((r) => /create|new|build/i.test(r.name) || /create/i.test(r.id)).map((r) => r.id);
  const items = actionableNodes.map((n, i) => ({
    index: i + 1,
    id: n.id,
    name: n.name,
    action: createRingIds.includes(n.ring) ? "CREATE" : "MODIFY",
    layer: layerMap[n.layer] || n.layer,
    layerId: n.layer,
    type: n.type || "unknown",
    file: n.path || null,
    spec: n.reason || "No specification provided",
    acceptance: n.acceptance || [],
    dependsOn: manifest.edges.filter((e) => e.target === n.id && e.source !== "center").map((e) => e.source),
    blocks: manifest.edges.filter((e) => e.source === n.id).map((e) => e.target),
    status: "pending",
    disposition: null
  }));
  const verify = safeNodes.map((n) => ({
    id: n.id,
    name: n.name,
    layer: layerMap[n.layer] || n.layer,
    type: n.type || "unknown",
    note: "Classified as unchanged \u2014 verify after implementation"
  }));
  const layerIds = [...new Set(actionableNodes.map((n) => n.layer))];
  return {
    title: manifest.title,
    description: manifest.center.description,
    mode: manifest.mode,
    generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
    summary: {
      createCount: items.filter((i) => i.action === "CREATE").length,
      modifyCount: items.filter((i) => i.action === "MODIFY").length,
      totalItems: items.length,
      verifyCount: verify.length,
      layers: layerIds.map((id) => layerMap[id] || id)
    },
    items,
    verify,
    edges: manifest.edges
  };
}
function contractToMarkdown(contract) {
  let md = `# Contract: ${contract.title}
`;
  md += `> ${contract.description}

`;
  md += `Generated: ${contract.generatedAt}

`;
  md += `## Summary
`;
  md += `- **Create:** ${contract.summary.createCount}
`;
  md += `- **Modify:** ${contract.summary.modifyCount}
`;
  md += `- **Total items:** ${contract.summary.totalItems}
`;
  md += `- **Verify unchanged:** ${contract.summary.verifyCount}
`;
  md += `- **Layers:** ${contract.summary.layers.join(", ")}

`;
  const byLayer = /* @__PURE__ */ new Map();
  for (const item of contract.items) {
    const group = byLayer.get(item.layer) || [];
    group.push(item);
    byLayer.set(item.layer, group);
  }
  md += `## Contract Items

`;
  for (const [layer, items] of byLayer) {
    md += `### ${layer}

`;
    md += `| # | Action | Name | Type | Spec | Status |
`;
    md += `|---|--------|------|------|------|--------|
`;
    for (const item of items) {
      const spec = item.spec.replace(/\|/g, "/").replace(/\n/g, " ");
      md += `| ${item.index} | ${item.action} | ${item.name} | ${item.type} | ${spec} | [ ] ${item.status} |
`;
    }
    md += `
`;
    const withAcceptance = items.filter((i) => i.acceptance.length > 0);
    if (withAcceptance.length > 0) {
      md += `#### Acceptance Criteria

`;
      for (const item of withAcceptance) {
        md += `**${item.index}. ${item.name}**
`;
        for (const a of item.acceptance) {
          md += `- [ ] ${a}
`;
        }
        md += `
`;
      }
    }
  }
  md += `## Dependencies

`;
  for (const item of contract.items) {
    if (item.dependsOn.length > 0) {
      md += `- **${item.name}** depends on: ${item.dependsOn.join(", ")}
`;
    }
  }
  md += `
`;
  if (contract.verify.length > 0) {
    md += `## Verify Unchanged

`;
    md += `These items were classified as "no change." Scan for misclassifications after implementation.

`;
    md += `| Name | Layer | Type | Status |
`;
    md += `|------|-------|------|--------|
`;
    for (const v of contract.verify) {
      md += `| ${v.name} | ${v.layer} | ${v.type} | [ ] verified |
`;
    }
    md += `
`;
  }
  md += `## Disposition Legend

`;
  md += `- \`pending\` \u2014 not started
`;
  md += `- \`done\` \u2014 implemented and verified against acceptance criteria
`;
  md += `- \`deferred\` \u2014 intentionally postponed (must include reason)
`;
  md += `- \`cut\` \u2014 removed from scope (must include reason)
`;
  md += `- \`superseded\` \u2014 replaced by different approach (must reference replacement)
`;
  return md;
}

// src/server/rich-html-render.ts
var WIRED_CDN = "https://unpkg.com/wired-elements?module";
var REVEAL_VER = "5.1.0";
var REVEAL_CSS = `https://cdn.jsdelivr.net/npm/reveal.js@${REVEAL_VER}/dist/reveal.css`;
var REVEAL_THEME = (t) => `https://cdn.jsdelivr.net/npm/reveal.js@${REVEAL_VER}/dist/theme/${t === "dark" ? "black" : "white"}.css`;
var REVEAL_JS = `https://cdn.jsdelivr.net/npm/reveal.js@${REVEAL_VER}/dist/reveal.js`;
var REVEAL_MD = `https://cdn.jsdelivr.net/npm/reveal.js@${REVEAL_VER}/plugin/markdown/markdown.js`;
var REVEAL_HL = `https://cdn.jsdelivr.net/npm/reveal.js@${REVEAL_VER}/plugin/highlight/highlight.js`;
var REVEAL_NOTES = `https://cdn.jsdelivr.net/npm/reveal.js@${REVEAL_VER}/plugin/notes/notes.js`;
function renderRichHtml(opts) {
  const theme = opts.theme ?? "light";
  const title = escapeHtml(opts.title ?? "LaunchDeck");
  if (opts.framework === "wired") {
    return renderWired(opts.content, theme, title);
  }
  if (opts.framework === "reveal") {
    return renderReveal(opts.content, theme, title);
  }
  return opts.content;
}
function renderWiredBody(body) {
  return `<style>
  :host {
    display: block;
    padding: 24px;
    font-family: 'Gloria Hallelujah', 'DM Sans', system-ui, sans-serif;
    color: var(--page-fg, currentColor);
  }
  *, *::before, *::after { box-sizing: border-box; }
  h1, h2, h3, h4 { font-family: 'Gloria Hallelujah', cursive; margin: 0.5em 0; }
  a { color: var(--page-accent, #7c3aed); }
  .row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0; }
  .col { display: flex; flex-direction: column; gap: 12px; align-items: stretch; }
  .card { padding: 16px; }
  wired-button, wired-card, wired-checkbox, wired-radio, wired-toggle,
  wired-input, wired-textarea, wired-search-input, wired-combo, wired-item,
  wired-listbox, wired-link, wired-tabs, wired-tab, wired-fab,
  wired-icon-button, wired-divider, wired-progress, wired-slider,
  wired-calendar, wired-dialog {
    color: inherit;
    background: transparent;
  }
</style>
${body}`;
}
function themeResolverScript(initial) {
  return `<script>
(function () {
  function applyTheme(t) {
    if (t !== 'light' && t !== 'dark') return;
    var prev = document.documentElement.dataset.theme;
    document.documentElement.dataset.theme = t;
    if (prev !== t) {
      document.dispatchEvent(new CustomEvent('deck-theme', { detail: t }));
    }
  }
  // 1) initial preference: URL > prefers-color-scheme (standalone) > baked-in default
  var url = new URL(window.location.href);
  var fromUrl = url.searchParams.get('theme');
  if (fromUrl === 'light' || fromUrl === 'dark') {
    applyTheme(fromUrl);
  } else if (window.parent === window) {
    var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
    applyTheme(prefersDark ? 'dark' : 'light');
  } else {
    applyTheme(${JSON.stringify(initial)});
  }
  // 2) live updates from parent deck
  window.addEventListener('message', function (e) {
    if (e.data && e.data.type === 'deck-theme') applyTheme(e.data.theme);
  });
  // 3) tell parent we are ready so it can push the current theme back
  if (window.parent !== window) {
    window.parent.postMessage({ type: 'deck-iframe-ready' }, '*');
  }
})();
</script>`;
}
function renderWired(body, initialTheme, title) {
  const LIGHT = { bg: "#ffffff", fg: "#18181b", accent: "#7c3aed", cardBg: "#ffffff" };
  const DARK = { bg: "#0e0e10", fg: "#e4e4e7", accent: "#a78bfa", cardBg: "#1a1a1d" };
  const wiredVarsBlock = (vars) => `
    --wired-card-background-fill: ${vars.cardBg};
    --wired-button-color: ${vars.fg};
    --wired-combo-popup-bg: ${vars.cardBg};
    --wired-progress-color: ${vars.accent};
    --wired-progress-label-background: ${vars.cardBg};
    --wired-progress-label-color: ${vars.fg};
  `;
  return `<!DOCTYPE html>
<html lang="en" data-theme="${initialTheme}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${title}</title>
<script type="module" src="${WIRED_CDN}"></script>
<link href="https://fonts.googleapis.com/css2?family=Gloria+Hallelujah&family=DM+Sans:wght@400;600;700&display=swap" rel="stylesheet">
<style>
  *, *::before, *::after { box-sizing: border-box; }
  html, body {
    margin: 0;
    min-height: 100%;
    font-family: 'Gloria Hallelujah', 'DM Sans', system-ui, sans-serif;
    background: var(--page-bg);
    color: var(--page-fg);
    transition: background 0.15s, color 0.15s;
  }
  body { padding: 24px; }
  h1, h2, h3, h4 { font-family: 'Gloria Hallelujah', cursive; margin: 0.5em 0; color: var(--page-fg); }
  a { color: var(--page-accent); }
  .row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0; }
  .col { display: flex; flex-direction: column; gap: 12px; align-items: stretch; }
  .card { padding: 16px; }

  /* Light theme */
  :root[data-theme="light"] {
    --page-bg: ${LIGHT.bg};
    --page-fg: ${LIGHT.fg};
    --page-accent: ${LIGHT.accent};
    ${wiredVarsBlock(LIGHT)}
  }

  /* Dark theme */
  :root[data-theme="dark"] {
    --page-bg: ${DARK.bg};
    --page-fg: ${DARK.fg};
    --page-accent: ${DARK.accent};
    ${wiredVarsBlock(DARK)}
  }

  /* Hosts: text color inherits via component "color: inherit" rules.
     Transparent background lets the page bg show through where the
     rough.js rectangle leaves space. */
  wired-button, wired-card, wired-checkbox, wired-radio, wired-toggle,
  wired-input, wired-textarea, wired-search-input, wired-combo, wired-item,
  wired-listbox, wired-link, wired-tabs, wired-tab, wired-fab,
  wired-icon-button, wired-divider, wired-progress, wired-slider,
  wired-calendar, wired-dialog {
    color: var(--page-fg);
    background: transparent;
  }
</style>
</head>
<body>
${body}
${themeResolverScript(initialTheme)}
<script type="module">
  // wired-input / wired-textarea / wired-search-input render their native
  // <input>/<textarea> INSIDE shadow DOM. Outer CSS cannot reach those, and
  // the native control has appearance:auto which the UA repaints over any
  // "background: transparent" rule. We must set inline styles directly on
  // the inner element with !important.
  //
  // We register one applier per theme. Re-runs when theme changes or new
  // nodes are added so the page reacts live to the deck sun/moon toggle.
  const FG = { light: ${JSON.stringify(LIGHT.fg)}, dark: ${JSON.stringify(DARK.fg)} };
  const TAGS = ['wired-input', 'wired-textarea', 'wired-search-input'];
  const DARK_PROPS = [
    'appearance', '-webkit-appearance',
    'background', 'background-color',
    'color', '-webkit-text-fill-color', 'caret-color',
    'border', 'outline',
  ];

  function applyDark(node) {
    const s = node.style;
    s.setProperty('appearance', 'none', 'important');
    s.setProperty('-webkit-appearance', 'none', 'important');
    s.setProperty('background', 'transparent', 'important');
    s.setProperty('background-color', 'transparent', 'important');
    s.setProperty('color', FG.dark, 'important');
    s.setProperty('-webkit-text-fill-color', FG.dark, 'important');
    s.setProperty('caret-color', FG.dark, 'important');
    s.setProperty('border', 'none', 'important');
    s.setProperty('outline', 'none', 'important');
  }

  function applyLight(node) {
    DARK_PROPS.forEach((p) => node.style.removeProperty(p));
  }

  function placeholderStyle(host, theme) {
    const existing = host.shadowRoot.querySelector('style[data-deck-theme]');
    if (existing) existing.remove();
    if (theme !== 'dark') return;
    const style = document.createElement('style');
    style.dataset.deckTheme = '1';
    style.textContent =
      '::placeholder{color:' + FG.dark + '99 !important;' +
      '-webkit-text-fill-color:' + FG.dark + '99 !important;}';
    host.shadowRoot.appendChild(style);
  }

  function sweepOnce() {
    const theme = document.documentElement.dataset.theme;
    for (const tag of TAGS) {
      for (const host of document.querySelectorAll(tag)) {
        if (!host.shadowRoot) continue;
        const inner = host.shadowRoot.querySelector('input, textarea');
        if (!inner) continue;
        if (theme === 'dark') applyDark(inner);
        else applyLight(inner);
        placeholderStyle(host, theme);
      }
    }
  }

  // Initial + late-upgrade catch (CDN script loads async).
  sweepOnce();
  let elapsed = 0;
  function tick() {
    sweepOnce();
    elapsed += 100;
    if (elapsed < 3000) setTimeout(tick, 100);
  }
  setTimeout(tick, 100);

  // Re-sweep on theme change.
  document.addEventListener('deck-theme', sweepOnce);
  // Re-sweep when new nodes are added.
  new MutationObserver(sweepOnce).observe(document.body, { subtree: true, childList: true });

  // whenDefined fires when each tag is registered \u2014 allSettled-style, never hangs.
  TAGS.forEach((tag) => {
    customElements.whenDefined(tag).then(sweepOnce).catch(() => {});
  });
</script>
</body>
</html>`;
}
function renderReveal(content, initialTheme, title) {
  const slides = content.includes("<section") ? content : content.split(/\n\s*---\s*\n/).map((md) => `<section data-markdown><textarea data-template>
${md.trim()}
</textarea></section>`).join("\n");
  return `<!DOCTYPE html>
<html lang="en" data-theme="${initialTheme}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${title}</title>
<link rel="stylesheet" href="${REVEAL_CSS}">
<link id="reveal-theme-light" rel="stylesheet" href="${REVEAL_THEME("light")}" ${initialTheme === "dark" ? "disabled" : ""}>
<link id="reveal-theme-dark"  rel="stylesheet" href="${REVEAL_THEME("dark")}"  ${initialTheme === "dark" ? "" : "disabled"}>
<style>
  html, body { margin: 0; height: 100%; }
  .reveal .slides { text-align: left; }
  .reveal .slides section { padding: 0 32px; }
</style>
</head>
<body>
<div class="reveal">
  <div class="slides">
${slides}
  </div>
</div>
<script src="${REVEAL_JS}"></script>
<script src="${REVEAL_MD}"></script>
<script src="${REVEAL_HL}"></script>
<script src="${REVEAL_NOTES}"></script>
<script>
  Reveal.initialize({
    hash: true,
    slideNumber: 'c/t',
    controls: true,
    progress: true,
    transition: 'slide',
    plugins: [RevealMarkdown, RevealHighlight, RevealNotes],
  });
  // React to theme changes: flip which stylesheet is enabled.
  function syncRevealTheme() {
    var dark = document.documentElement.dataset.theme === 'dark';
    var lightLink = document.getElementById('reveal-theme-light');
    var darkLink  = document.getElementById('reveal-theme-dark');
    if (lightLink) lightLink.disabled = dark;
    if (darkLink)  darkLink.disabled  = !dark;
  }
  document.addEventListener('deck-theme', syncRevealTheme);
  syncRevealTheme();
</script>
${themeResolverScript(initialTheme)}
</body>
</html>`;
}
function escapeHtml(s) {
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function slugify(label) {
  return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "block";
}

// src/server/deck-serve.ts
var DEFAULT_PORT = 52829;
var MAX_PORT_SCAN = 3;
var MIME_TYPES = {
  ".html": "text/html; charset=utf-8",
  ".js": "application/javascript; charset=utf-8",
  ".css": "text/css; charset=utf-8",
  ".json": "application/json; charset=utf-8",
  ".png": "image/png",
  ".svg": "image/svg+xml",
  ".ico": "image/x-icon",
  ".woff": "font/woff",
  ".woff2": "font/woff2"
};
function sessionDir(cwd, session) {
  return import_node_path3.default.join(cwd, LAUNCHSECURE_DIR, "deck-files", session);
}
function sessionJsonPath(cwd, session) {
  return import_node_path3.default.join(sessionDir(cwd, session), "session.json");
}
function readPersistedSession(cwd, session) {
  try {
    const raw = import_node_fs3.default.readFileSync(sessionJsonPath(cwd, session), "utf-8");
    return JSON.parse(raw);
  } catch {
    return null;
  }
}
function writePersistedSession(cwd, session, payload) {
  const dir = sessionDir(cwd, session);
  import_node_fs3.default.mkdirSync(dir, { recursive: true });
  const existing = readPersistedSession(cwd, session);
  const now = (/* @__PURE__ */ new Date()).toISOString();
  const next = {
    id: session,
    mode: payload.mode,
    prompt: payload.prompt,
    blocks: payload.blocks,
    version: existing ? existing.version + 1 : 1,
    createdAt: existing?.createdAt ?? now,
    updatedAt: now
  };
  import_node_fs3.default.writeFileSync(sessionJsonPath(cwd, session), JSON.stringify(next, null, 2));
  return next;
}
function listPersistedSessions(cwd) {
  const base = import_node_path3.default.join(cwd, LAUNCHSECURE_DIR, "deck-files");
  if (!import_node_fs3.default.existsSync(base)) return [];
  const entries = import_node_fs3.default.readdirSync(base, { withFileTypes: true });
  const sessions = [];
  for (const entry of entries) {
    if (!entry.isDirectory()) continue;
    const persisted = readPersistedSession(cwd, entry.name);
    if (!persisted) continue;
    sessions.push({
      id: persisted.id,
      mode: persisted.mode,
      blockCount: Array.isArray(persisted.blocks) ? persisted.blocks.length : 0,
      version: persisted.version,
      createdAt: persisted.createdAt,
      updatedAt: persisted.updatedAt
    });
  }
  sessions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
  return sessions;
}
function deletePersistedSession(cwd, session) {
  const base = import_node_path3.default.join(cwd, LAUNCHSECURE_DIR, "deck-files");
  if (session === "all") {
    if (import_node_fs3.default.existsSync(base)) import_node_fs3.default.rmSync(base, { recursive: true, force: true });
    return;
  }
  const dir = sessionDir(cwd, session);
  if (import_node_fs3.default.existsSync(dir)) import_node_fs3.default.rmSync(dir, { recursive: true, force: true });
}
var pendingFeedback = /* @__PURE__ */ new Map();
var lastRenderError = null;
function consumeRenderError() {
  const err = lastRenderError;
  lastRenderError = null;
  return err;
}
function createFeedbackWaiter(session) {
  const existing = pendingFeedback.get(session);
  if (existing) {
    existing.reject(new Error("Superseded by new await_feedback call"));
  }
  let resolve;
  let reject;
  const promise = new Promise((res, rej) => {
    resolve = res;
    reject = rej;
  });
  pendingFeedback.set(session, { resolve, reject });
  return promise;
}
function resolveFeedback(session, response) {
  const waiter = pendingFeedback.get(session);
  if (!waiter) return false;
  pendingFeedback.delete(session);
  waiter.resolve(response);
  return true;
}
var wss = null;
function broadcastToClients(message) {
  if (!wss) return;
  const data = JSON.stringify(message);
  for (const client of wss.clients) {
    if (client.readyState === import_ws.WebSocket.OPEN) {
      client.send(data);
    }
  }
}
function serveStatic(res, filePath) {
  if (!import_node_fs3.default.existsSync(filePath) || !import_node_fs3.default.statSync(filePath).isFile()) return false;
  const ext = import_node_path3.default.extname(filePath).toLowerCase();
  const mime = MIME_TYPES[ext] ?? "application/octet-stream";
  res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
  import_node_fs3.default.createReadStream(filePath).pipe(res);
  return true;
}
function resolveClientDir() {
  const sibling = import_node_path3.default.join(__dirname, "..", "deck-client");
  if (import_node_fs3.default.existsSync(import_node_path3.default.join(sibling, "assets"))) return sibling;
  const devDist = import_node_path3.default.join(__dirname, "..", "..", "dist", "deck-client");
  if (import_node_fs3.default.existsSync(import_node_path3.default.join(devDist, "assets"))) return devDist;
  return sibling;
}
function serveIndex(res, clientDir) {
  const indexPath = import_node_path3.default.join(clientDir, "index.html");
  if (!import_node_fs3.default.existsSync(indexPath)) {
    res.writeHead(500, { "Content-Type": "text/plain" });
    res.end(`LaunchDeck client bundle not found at ${clientDir}. Run 'npm run build:client'.`);
    return;
  }
  serveStatic(res, indexPath);
}
function readBody(req) {
  return new Promise((resolve) => {
    let body = "";
    req.on("data", (chunk) => {
      body += chunk.toString();
    });
    req.on("end", () => resolve(body));
  });
}
function jsonResponse(res, status, data) {
  res.writeHead(status, { "Content-Type": "application/json" });
  res.end(JSON.stringify(data));
}
function tryListen(server, port) {
  return new Promise((resolve, reject) => {
    const onError = (err) => {
      server.off("listening", onListening);
      reject(err);
    };
    const onListening = () => {
      server.off("error", onError);
      resolve(port);
    };
    server.once("error", onError);
    server.once("listening", onListening);
    server.listen(port, "127.0.0.1");
  });
}
async function bindWithFallback(server, startPort) {
  let lastErr = null;
  for (let i = 0; i < MAX_PORT_SCAN; i++) {
    const port = startPort + i;
    try {
      return await tryListen(server, port);
    } catch (err) {
      const code = err.code;
      if (code === "EADDRINUSE") {
        lastErr = err;
        continue;
      }
      throw err;
    }
  }
  throw lastErr ?? new Error("Failed to bind any port");
}
async function startDeckServer(opts = {}) {
  const cwd = opts.cwd ?? process.cwd();
  const existing = getLiveLock(cwd);
  if (existing) {
    if (!opts.quiet) {
      process.stderr.write(`[launch-deck] already running (pid ${existing.pid}) at ${existing.url}
`);
    }
    return { port: existing.port, url: existing.url };
  }
  const clientDir = opts.clientDir ?? resolveClientDir();
  const server = import_node_http.default.createServer(async (req, res) => {
    try {
      const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
      res.setHeader("Access-Control-Allow-Origin", "*");
      res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
      res.setHeader("Access-Control-Allow-Headers", "Content-Type");
      if (req.method === "OPTIONS") {
        res.writeHead(204);
        res.end();
        return;
      }
      if (req.method === "GET" && url2.pathname === "/api/health") {
        jsonResponse(res, 200, { ok: true, tool: "launch-deck" });
        return;
      }
      if (req.method === "GET" && url2.pathname === "/api/deck/config") {
        const cfg = loadDeckConfig(cwd);
        jsonResponse(res, 200, { config: cfg });
        return;
      }
      if (req.method === "POST" && url2.pathname === "/api/deck/config") {
        try {
          const body = JSON.parse(await readBody(req));
          const existing2 = loadDeckConfig(cwd);
          const merged = { ...existing2, ...body };
          saveDeckConfig(cwd, merged);
          jsonResponse(res, 200, { ok: true });
        } catch (err) {
          jsonResponse(res, 400, { ok: false, error: String(err) });
        }
        return;
      }
      if (req.method === "POST" && url2.pathname === "/api/deck") {
        const body = JSON.parse(await readBody(req));
        for (const block of body.blocks) {
          if (block.type === "blast-radius" && block.manifest) {
            try {
              const sessionEncoded = encodeURIComponent(body.session);
              const baseUrl = `/deck-files/${sessionEncoded}`;
              const html = generateBlastRadiusHtml(block.manifest, baseUrl);
              const report = generateBlastRadiusReport(block.manifest);
              const dir = import_node_path3.default.join(cwd, LAUNCHSECURE_DIR, "deck-files", body.session);
              import_node_fs3.default.mkdirSync(dir, { recursive: true });
              import_node_fs3.default.writeFileSync(import_node_path3.default.join(dir, "blast-radius.html"), html);
              import_node_fs3.default.writeFileSync(import_node_path3.default.join(dir, "blast-radius-report.md"), report);
              import_node_fs3.default.writeFileSync(import_node_path3.default.join(dir, "blast-radius-manifest.json"), JSON.stringify(block.manifest, null, 2));
              const contractData = generateContract(block.manifest);
              const contractMd = contractToMarkdown(contractData);
              import_node_fs3.default.writeFileSync(import_node_path3.default.join(dir, "contract.json"), JSON.stringify(contractData, null, 2));
              import_node_fs3.default.writeFileSync(import_node_path3.default.join(dir, "contract.md"), contractMd);
              block.type = "iframe";
              block.src = `${baseUrl}/blast-radius.html`;
              delete block.manifest;
            } catch (err) {
              console.error("Failed to generate blast radius HTML:", err);
            }
          }
          if (block.type === "rich-html" && block.content) {
            const framework = block.framework ?? "raw";
            if (framework === "wired") {
              try {
                block.content = renderWiredBody(block.content);
              } catch (err) {
                console.error("Failed to render wired body:", err);
              }
            } else {
              try {
                const html = renderRichHtml({
                  framework,
                  content: block.content,
                  theme: block.theme,
                  title: block.label
                });
                const sessionEncoded = encodeURIComponent(body.session);
                const baseUrl = `/deck-files/${sessionEncoded}`;
                const filename = `${slugify(block.label)}.html`;
                const dir = import_node_path3.default.join(cwd, LAUNCHSECURE_DIR, "deck-files", body.session);
                import_node_fs3.default.mkdirSync(dir, { recursive: true });
                import_node_fs3.default.writeFileSync(import_node_path3.default.join(dir, filename), html);
                block.type = "iframe";
                block.src = `${baseUrl}/${filename}`;
                delete block.content;
                delete block.framework;
                delete block.theme;
              } catch (err) {
                console.error("Failed to generate rich-html:", err);
              }
            }
          }
        }
        const persisted = writePersistedSession(cwd, body.session, {
          mode: body.mode,
          prompt: body.prompt,
          blocks: body.blocks
        });
        broadcastToClients({
          type: "session",
          session: body.session,
          mode: body.mode,
          blocks: body.blocks,
          prompt: body.prompt,
          version: persisted.version
        });
        jsonResponse(res, 200, { ok: true, version: persisted.version });
        return;
      }
      if (req.method === "GET" && url2.pathname === "/api/sessions") {
        jsonResponse(res, 200, { sessions: listPersistedSessions(cwd) });
        return;
      }
      const sessionMatch = url2.pathname.match(/^\/api\/sessions\/([^/]+)$/);
      if (sessionMatch) {
        const id = decodeURIComponent(sessionMatch[1]);
        if (req.method === "GET") {
          const persisted = readPersistedSession(cwd, id);
          if (!persisted) {
            jsonResponse(res, 404, { error: "not found" });
            return;
          }
          jsonResponse(res, 200, persisted);
          return;
        }
        if (req.method === "DELETE") {
          broadcastToClients({ type: "clear_session", session: id });
          resolveFeedback(id, { comment: "", closed: true });
          deletePersistedSession(cwd, id);
          jsonResponse(res, 200, { ok: true });
          return;
        }
      }
      if (req.method === "POST" && url2.pathname === "/api/await-feedback") {
        const { session } = JSON.parse(await readBody(req));
        try {
          const feedback = await createFeedbackWaiter(session);
          jsonResponse(res, 200, feedback);
        } catch (err) {
          jsonResponse(res, 500, { error: String(err) });
        }
        return;
      }
      if (req.method === "POST" && url2.pathname === "/api/feedback") {
        try {
          const payload = JSON.parse(await readBody(req));
          const sessionId = payload.context?.sessionId;
          if (!sessionId) {
            jsonResponse(res, 400, { ok: false, error: "context.sessionId is required" });
            return;
          }
          const verdictMap = {
            bug: "blocker",
            idea: "iterate",
            ux: "note",
            a11y: "approve"
          };
          const domEle = payload.plugins?.domEle;
          const domSS = payload.plugins?.domSS;
          const picks = Array.isArray(domEle?.pins) ? domEle.pins.filter((p) => p && typeof p.selector === "string").map((p) => ({
            selector: p.selector,
            ...p.note ? { note: p.note } : {},
            ...p.boundingRect ? { rect: p.boundingRect } : {}
          })) : [];
          const response = {
            comment: payload.description ?? "",
            selections: payload.context?.selections,
            verdict: payload.severity ? verdictMap[payload.severity] : void 0,
            picks: picks.length > 0 ? picks : void 0,
            screenshot: typeof domSS?.dataUrl === "string" ? domSS.dataUrl : void 0
          };
          const resolved = resolveFeedback(sessionId, response);
          if (!resolved) {
            jsonResponse(res, 409, { ok: false, error: `no pending await_feedback for session "${sessionId}"` });
            return;
          }
          jsonResponse(res, 200, { ok: true });
        } catch (err) {
          jsonResponse(res, 400, { ok: false, error: String(err) });
        }
        return;
      }
      if (req.method === "POST" && url2.pathname === "/api/clear-session") {
        const { session } = JSON.parse(await readBody(req));
        broadcastToClients({
          type: session === "all" ? "clear_all" : "clear_session",
          session
        });
        if (session === "all") {
          for (const [id, waiter] of pendingFeedback) {
            waiter.resolve({ comment: "", closed: true });
            pendingFeedback.delete(id);
          }
        } else {
          resolveFeedback(session, { comment: "", closed: true });
        }
        try {
          deletePersistedSession(cwd, session);
        } catch {
        }
        jsonResponse(res, 200, { ok: true });
        return;
      }
      if (req.method === "GET" && url2.pathname === "/api/render-errors") {
        const err = consumeRenderError();
        jsonResponse(res, 200, err ?? null);
        return;
      }
      if (req.method === "GET" && url2.pathname.startsWith("/deck-files/")) {
        const relative = decodeURIComponent(url2.pathname.slice("/deck-files/".length));
        if (relative.includes("..") || relative.startsWith("/")) {
          res.writeHead(403);
          res.end("Forbidden");
          return;
        }
        const filePath = import_node_path3.default.join(cwd, LAUNCHSECURE_DIR, "deck-files", relative);
        if (serveStatic(res, filePath)) return;
        res.writeHead(404);
        res.end("Not found");
        return;
      }
      if (req.method === "GET" && url2.pathname.startsWith("/deck-download/")) {
        const relative = decodeURIComponent(url2.pathname.slice("/deck-download/".length));
        if (relative.includes("..") || relative.startsWith("/")) {
          res.writeHead(403);
          res.end("Forbidden");
          return;
        }
        const filePath = import_node_path3.default.join(cwd, LAUNCHSECURE_DIR, "deck-files", relative);
        if (!import_node_fs3.default.existsSync(filePath) || !import_node_fs3.default.statSync(filePath).isFile()) {
          res.writeHead(404);
          res.end("Not found");
          return;
        }
        const ext = import_node_path3.default.extname(filePath).toLowerCase();
        const mime = MIME_TYPES[ext] ?? "application/octet-stream";
        const filename = import_node_path3.default.basename(filePath);
        res.writeHead(200, {
          "Content-Type": mime,
          "Content-Disposition": `attachment; filename="${filename}"`,
          "Cache-Control": "no-cache"
        });
        import_node_fs3.default.createReadStream(filePath).pipe(res);
        return;
      }
      if (url2.pathname !== "/") {
        const staticPath = import_node_path3.default.join(clientDir, url2.pathname);
        if (serveStatic(res, staticPath)) return;
      }
      serveIndex(res, clientDir);
    } catch (err) {
      jsonResponse(res, 500, { error: String(err) });
    }
  });
  wss = new import_ws.WebSocketServer({ server });
  wss.on("connection", (ws) => {
    if (!opts.quiet) {
      process.stderr.write("[launch-deck] browser connected\n");
    }
    ws.on("message", (raw) => {
      try {
        const msg = JSON.parse(String(raw));
        if (msg.type === "feedback" && msg.session) {
          resolveFeedback(msg.session, {
            comment: msg.comment ?? "",
            selections: msg.selections
          });
        }
        if (msg.type === "session_closed" && msg.session) {
          resolveFeedback(msg.session, {
            comment: "",
            closed: true
          });
        }
        if (msg.type === "render_error") {
          lastRenderError = {
            error: msg.error ?? "Unknown render error",
            source: msg.source ?? ""
          };
        }
      } catch {
      }
    });
    ws.on("close", () => {
      if (!opts.quiet) {
        process.stderr.write("[launch-deck] browser disconnected\n");
      }
      const hasClients = [...wss.clients].some((c) => c.readyState === import_ws.WebSocket.OPEN);
      if (!hasClients && pendingFeedback.size > 0) {
        for (const [id, waiter] of pendingFeedback) {
          waiter.reject(new Error("Browser disconnected \u2014 no clients available"));
          pendingFeedback.delete(id);
        }
      }
    });
  });
  const config = loadDeckConfig(cwd);
  const startPort = opts.port ?? config.port ?? DEFAULT_PORT;
  const port = await bindWithFallback(server, startPort);
  const url = `http://localhost:${port}`;
  writeLock({
    pid: process.pid,
    port,
    cwd,
    url,
    startedAt: (/* @__PURE__ */ new Date()).toISOString()
  }, cwd);
  const cleanup = () => {
    for (const [id, waiter] of pendingFeedback) {
      waiter.reject(new Error("LaunchDeck server shutting down"));
      pendingFeedback.delete(id);
    }
    clearLock(cwd);
    server.close();
  };
  process.once("SIGINT", () => {
    cleanup();
    process.exit(0);
  });
  process.once("SIGTERM", () => {
    cleanup();
    process.exit(0);
  });
  process.once("exit", cleanup);
  if (!opts.quiet) {
    process.stderr.write(`[launch-deck] serving ${url}
`);
  }
  return { port, url };
}
function runServeCli(argv) {
  let port;
  for (let i = 0; i < argv.length; i++) {
    if (argv[i] === "--port" && argv[i + 1]) {
      port = parseInt(argv[++i], 10);
    } else if (argv[i].startsWith("--port=")) {
      port = parseInt(argv[i].slice("--port=".length), 10);
    }
  }
  startDeckServer({ port }).catch((err) => {
    process.stderr.write(`[launch-deck] failed to start: ${err}
`);
    process.exit(1);
  });
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  broadcastToClients,
  consumeRenderError,
  createFeedbackWaiter,
  resolveFeedback,
  runServeCli,
  startDeckServer
});
