@tryvitris/website-harness 0.1.0-beta.7 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +49 -117
  2. package/agent/graph.yaml +12 -6
  3. package/agent/refs/admin.md +11 -0
  4. package/agent/refs/analytics.md +17 -0
  5. package/agent/refs/launch.md +15 -11
  6. package/agent/refs/performance.md +1 -1
  7. package/bin/api/agent/evaluate.js +5 -5
  8. package/bin/api/agent/grade.js +5 -5
  9. package/bin/api/cli/api.js +15 -13
  10. package/bin/api/cli.js +15 -15
  11. package/bin/api/engine.js +6 -6
  12. package/bin/api/hosting.js +9 -7
  13. package/bin/api/index.js +2 -2
  14. package/bin/api/render.js +1 -1
  15. package/bin/chunks/{chunk-ZIROO2U4.js → chunk-37F7XIMH.js} +4 -3
  16. package/bin/chunks/{chunk-TTAUU3AZ.js → chunk-3OKR5AZJ.js} +1 -1
  17. package/bin/chunks/{chunk-PGFFKPK6.js → chunk-A2XMAPVW.js} +2 -2
  18. package/bin/chunks/{chunk-O55LAQ2C.js → chunk-FGSQE3DU.js} +2 -2
  19. package/bin/chunks/{chunk-VTI32UGJ.js → chunk-H3BS2EBR.js} +2 -2
  20. package/bin/chunks/{chunk-DSS2U2OM.js → chunk-KVMZSQ3D.js} +8 -2
  21. package/bin/chunks/{chunk-4MFPATAB.js → chunk-L44ZTNDM.js} +12 -5
  22. package/bin/chunks/{chunk-7V3OKLKH.js → chunk-LZC37OJZ.js} +4 -4
  23. package/bin/chunks/{chunk-5YSPWKFF.js → chunk-PYCRUMCT.js} +1 -1
  24. package/bin/chunks/{chunk-QRWGFLUU.js → chunk-R2WRQDVE.js} +2 -2
  25. package/bin/chunks/{chunk-YZWEXHT4.js → chunk-SEMH6MCV.js} +129 -21
  26. package/bin/chunks/{chunk-H7Y72HXC.js → chunk-UFTVWTEZ.js} +26 -7
  27. package/bin/chunks/{chunk-P56ZDMX7.js → chunk-WMJVLMU2.js} +16 -4
  28. package/bin/chunks/{chunk-FRABKM65.js → chunk-XY73ZLHB.js} +12 -9
  29. package/bin/chunks/{chunk-ZWFG4GPG.js → chunk-YXJAQYDE.js} +69 -53
  30. package/bin/chunks/{design-context-store-DCZOAK56.js → design-context-store-W3VTZ4HA.js} +5 -5
  31. package/bin/chunks/{packets-QOLWPRNV.js → packets-LBMDIFPO.js} +5 -5
  32. package/bin/chunks/{preparation-T2IOD5SJ.js → preparation-AV4PDQY6.js} +5 -5
  33. package/bin/chunks/{template-gallery-ET76ERZQ.js → template-gallery-H2SVYNRD.js} +1 -1
  34. package/bin/cloudflare/backend.js +144 -62
  35. package/bin/types/src/admin/connection.d.ts +3 -0
  36. package/bin/types/src/cli/admin.d.ts +21 -0
  37. package/bin/types/src/cli/api.d.ts +1 -1
  38. package/bin/types/src/cli/deploy.d.ts +2 -0
  39. package/bin/types/src/cli/doctor.d.ts +1 -0
  40. package/bin/types/src/cli/site-create.d.ts +2 -0
  41. package/bin/types/src/compile/site.d.ts +1 -1
  42. package/bin/types/src/integrations/hosting.d.ts +2 -0
  43. package/bin/vitris-art.js +5 -5
  44. package/bin/vitris-build.js +3 -3
  45. package/bin/vitris-create-site.js +9 -7
  46. package/bin/vitris-cursor-hook.js +5 -5
  47. package/bin/vitris-eval.js +7 -7
  48. package/bin/vitris-go.js +7 -7
  49. package/bin/vitris-harvest.js +6 -6
  50. package/bin/vitris-knowledge.js +6 -6
  51. package/bin/vitris-launch-preflight.js +5 -5
  52. package/bin/vitris-motion-view.js +5 -5
  53. package/bin/vitris-references.js +3 -3
  54. package/bin/vitris-refresh.js +6 -6
  55. package/bin/vitris-stock.js +1 -1
  56. package/bin/vitris-templates.js +1 -1
  57. package/bin/vitris-themes.js +5 -5
  58. package/bin/vitris-upgrade.js +6 -6
  59. package/bin/vitris-verify-browser.js +5 -5
  60. package/bin/vitris-verify-performance.js +3 -3
  61. package/bin/vitris.js +15 -15
  62. package/docs/ADMIN.md +85 -0
  63. package/docs/CLI.md +14 -6
  64. package/docs/CLOUDFLARE.md +27 -2
  65. package/docs/DISTRIBUTION.md +5 -3
  66. package/docs/ENGINE.md +6 -0
  67. package/docs/PRESERVE.md +1 -1
  68. package/docs/SECURITY.md +2 -2
  69. package/package.json +28 -23
  70. package/templates/site/AGENTS.md +4 -2
  71. package/templates/site/README.md +8 -3
  72. package/templates/site/deployment/README.md +24 -0
  73. package/templates/site/{.github/workflows/deploy.yml → deployment/github-actions.yml.example} +5 -6
  74. package/templates/site/gitignore +3 -0
  75. package/templates/site/package.json +3 -1
@@ -6,7 +6,7 @@ import {
6
6
  selectStageKnowledge,
7
7
  signalsFromSite,
8
8
  siteWidgets
9
- } from "./chunk-FRABKM65.js";
9
+ } from "./chunk-XY73ZLHB.js";
10
10
  import {
11
11
  coverageReport,
12
12
  loadKnowledgeV2
@@ -6,10 +6,10 @@ import {
6
6
  planKitUpgrade,
7
7
  recoverKitUpgrade,
8
8
  rollbackKitUpgrade
9
- } from "./chunk-FRABKM65.js";
9
+ } from "./chunk-XY73ZLHB.js";
10
10
  import {
11
11
  findHarnessRoot
12
- } from "./chunk-VTI32UGJ.js";
12
+ } from "./chunk-H3BS2EBR.js";
13
13
  import {
14
14
  json,
15
15
  save
@@ -7,7 +7,7 @@ import {
7
7
  sourceRevision,
8
8
  verifyRefine,
9
9
  withMutationLock
10
- } from "./chunk-FRABKM65.js";
10
+ } from "./chunk-XY73ZLHB.js";
11
11
  import {
12
12
  assertRequestId,
13
13
  parseChangePlan,
@@ -17,7 +17,7 @@ import {
17
17
  import {
18
18
  buildVerified,
19
19
  findHarnessRoot
20
- } from "./chunk-VTI32UGJ.js";
20
+ } from "./chunk-H3BS2EBR.js";
21
21
  import {
22
22
  files,
23
23
  fingerprint,
@@ -2,10 +2,10 @@ import {
2
2
  buildSite,
3
3
  connectForms,
4
4
  loadSiteDir
5
- } from "./chunk-4MFPATAB.js";
5
+ } from "./chunk-L44ZTNDM.js";
6
6
  import {
7
7
  leadsJs
8
- } from "./chunk-ZIROO2U4.js";
8
+ } from "./chunk-37F7XIMH.js";
9
9
  import {
10
10
  acquirePathLock,
11
11
  files,
@@ -37,6 +37,12 @@ const $=id=>document.getElementById(id),allThemes=m=>[...m.themes,...m.archiveTh
37
37
  let brandIndex=0,selected=null,selectedRelease=null,pending=null,opened=null,lastFocus=null,galleryY=0,detailFrame=null;
38
38
  let example='story',viewport='desktop',compareExample='story',compareViewport='desktop',comparePair=[],compareTab=0;
39
39
  const cards=new Map(),frames=new Set(),positions=new Map(),demoMemory=new Map();let compareFrames=[];
40
+ // Native lazy images can occupy every HTTP/1 connection while the server renders
41
+ // thumbnails. Reserve connections for the live preview document, fonts and images.
42
+ const thumbnailJobs=new Map();let thumbnailActive=0;
43
+ function pumpThumbnails(){for(const [img,job] of thumbnailJobs){if(!img.isConnected){thumbnailObserver.unobserve(img);job.cancel?.();thumbnailJobs.delete(img);continue;}if(thumbnailActive>=2||!job.visible||job.started)continue;job.started=true;thumbnailActive++;const finish=()=>{thumbnailObserver.unobserve(img);img.removeEventListener('load',finish);img.removeEventListener('error',finish);job.cancel=null;thumbnailActive--;thumbnailJobs.delete(img);pumpThumbnails();};job.cancel=finish;img.addEventListener('load',finish);img.addEventListener('error',finish);img.loading='eager';img.src=job.src;}}
44
+ const thumbnailObserver=new IntersectionObserver(entries=>{for(const entry of entries){const job=thumbnailJobs.get(entry.target);if(job)job.visible=entry.isIntersecting;}pumpThumbnails();},{rootMargin:'300px 0px'});
45
+ function thumbnailSource(img,src){if(img.dataset.thumbnailSource===src)return;const prior=thumbnailJobs.get(img);if(prior){thumbnailJobs.delete(img);prior.cancel?.();img.removeAttribute('src');}img.dataset.thumbnailSource=src;if(src.startsWith('data:')){thumbnailObserver.unobserve(img);img.src=src;return;}thumbnailJobs.set(img,{src,visible:false,started:false});thumbnailObserver.unobserve(img);thumbnailObserver.observe(img);}
40
46
  const model=()=>models[brandIndex],draftKey=()=> 'vitris-theme-draft:'+model().brand.siteId+':'+model().brandHash,browseKey=()=> 'vitris-theme-browse:v3:'+model().themes[0].release+':'+model().brand.siteId;
41
47
  const findTheme=(id,release)=>allThemes(model()).find(t=>t.id===id&&t.release===release),byKey=k=>allThemes(model()).find(t=>key(t)===k);
42
48
  const defaults=()=>({collection:'All directions',families:[],identities:[],industries:[],media:[],tasks:[],query:'',group:'none',sort:'curated',archive:'current',shortlist:[],compare:[],palette:'brand',viewport:innerWidth<768?'mobile':'desktop'});
@@ -68,7 +74,7 @@ function makeCard(t,archive){
68
74
  const c={element,theme:t,host,progress,choose:chooseButton,save,check,state:null,archive,role:pageKey(t.pages[0]),thumbs:[]};let active=false;
69
75
  c.deactivate=(focus=true)=>{active=false;dispose(c.state);c.state=null;host.replaceChildren();host.hidden=true;top.hidden=true;if(focus){c.lastButton?.focus({preventScroll:true});if(Number.isFinite(c.galleryY))scrollTo({top:c.galleryY,behavior:'instant'});}};c.isActive=()=>active;
70
76
  c.show=(role,focus)=>{for(const other of cards.values())if(other!==c&&other.state)other.deactivate(false);if(!active)c.galleryY=scrollY;c.role=role;c.lastButton=focus;active=true;host.hidden=false;top.hidden=false;dispose(c.state);c.state=mount(host,t,role,browse.viewport);c.syncThumbs();host.scrollIntoView({block:'center',behavior:'instant'});host.focus({preventScroll:true});};
71
- c.syncThumbs=()=>{for(const item of c.thumbs){const r=resourceFor(t,item.role,browse.viewport);item.tile.dataset.visible=String(item.role===c.role);item.img.alt=t.name+' · '+item.label+' · '+(browse.palette==='brand'?model().brand.name:'Original palette');if(r.thumbnail){item.img.src=r.thumbnail;item.img.hidden=false;item.fallback.hidden=true;}else{item.img.hidden=true;item.fallback.hidden=false;}}mobileSelect.value=c.role;c.full.href=resourceFor(t,pageKey(t.pages[0]),browse.viewport).full;c.full.textContent=t.pageCount>5?'Explore all '+t.pageCount+' pages':archive?'Open preserved preview ↗':'Open full site ↗';if(!session){c.full.onclick=e=>{e.preventDefault();openDetail(t,c.full,pageKey(t.pages[0]));};}};
77
+ c.syncThumbs=()=>{for(const item of c.thumbs){const r=resourceFor(t,item.role,browse.viewport);item.tile.dataset.visible=String(item.role===c.role);item.img.alt=t.name+' · '+item.label+' · '+(browse.palette==='brand'?model().brand.name:'Original palette');if(r.thumbnail){thumbnailSource(item.img,r.thumbnail);item.img.hidden=false;item.fallback.hidden=true;}else{item.img.hidden=true;item.fallback.hidden=false;}}mobileSelect.value=c.role;c.full.href=resourceFor(t,pageKey(t.pages[0]),browse.viewport).full;c.full.textContent=t.pageCount>5?'Explore all '+t.pageCount+' pages':archive?'Open preserved preview ↗':'Open full site ↗';if(!session){c.full.onclick=e=>{e.preventDefault();openDetail(t,c.full,pageKey(t.pages[0]));};}};
72
78
  for(const page of t.pages.filter(p=>p.featured!==false).slice(0,t.pageCount>5?5:99)){const tile=document.createElement('section');tile.className='page-tile';tile.dataset.role=pageKey(page);const imageButton=button('',()=>openDetail(t,imageButton));imageButton.className='thumbnail-button';imageButton.setAttribute('aria-label','Explore '+t.name+' '+page.label);imageButton.onclick=()=>{openDetail(t,imageButton,pageKey(page));};const img=document.createElement('img');img.loading='lazy';img.decoding='async';img.width=480;img.height=300;const fallback=document.createElement('span');fallback.className='thumb-fallback';fallback.textContent='Open the live page';img.onerror=()=>{img.hidden=true;fallback.hidden=false;fallback.textContent='Live page available';};imageButton.append(img,fallback);const caption=document.createElement('div');caption.className='tile-caption';const text=document.createElement('strong');text.textContent=page.label;const scroll=button('Scroll preview',()=>c.show(pageKey(page),scroll));scroll.setAttribute('aria-label','Scroll '+t.name+' '+page.label);caption.append(text,scroll);tile.append(imageButton,caption);tiles.append(tile);c.thumbs.push({tile,img,fallback,role:pageKey(page),label:page.label});const option=document.createElement('option');option.value=pageKey(page);option.textContent=page.label;mobileSelect.append(option);}
73
79
  for(const page of t.pages){if([...mobileSelect.options].some(o=>o.value===pageKey(page)))continue;const option=document.createElement('option');option.value=pageKey(page);option.textContent=page.label;mobileSelect.append(option);}const full=document.createElement('a');full.className='full-site-tile';full.target='_blank';full.rel='noopener';full.textContent=archive?'Open preserved preview ↗':'Open full site ↗';c.full=full;tiles.append(full);mobileSelect.onchange=()=>{c.role=mobileSelect.value;if(c.state)c.show(c.role,mobileSelect);else c.syncThumbs();};overview.append(mobileSelect,tiles);element.append(meta,overview,top,host);c.syncThumbs();exit.onclick=()=>c.deactivate();
74
80
  host.onkeydown=e=>{const map={ArrowDown:80,ArrowUp:-80,PageDown:Math.max(250,(c.state?.viewHeight||600)*.85),PageUp:-Math.max(250,(c.state?.viewHeight||600)*.85),Home:0,End:Number.MAX_SAFE_INTEGER};if(e.key==='Escape'){e.preventDefault();c.deactivate();}else if(e.key in map){e.preventDefault();scrollFrame(c.state,e.key==='Home'||e.key==='End'?map[e.key]:(c.state?.y||0)+map[e.key]);}};
@@ -82,7 +88,7 @@ function syncControls(){$('allentry').setAttribute('aria-pressed',String(browse.
82
88
  document.addEventListener('wheel',e=>{if(e.defaultPrevented||e.ctrlKey||e.metaKey||Math.abs(e.deltaX)>Math.abs(e.deltaY))return;for(const s of frames){if(!s.host.isConnected||!s.ready||!s.host.getClientRects().length)continue;const r=s.host.getBoundingClientRect();if(e.clientX<r.left||e.clientX>=r.right||e.clientY<r.top||e.clientY>=r.bottom)continue;const delta=e.deltaY*(e.deltaMode===1?16:e.deltaMode===2?s.viewHeight:1);if((delta>0&&s.y<s.height-s.viewHeight-1)||(delta<0&&s.y>1)){e.preventDefault();scrollFrame(s,s.y+delta);}break;}},{capture:true,passive:false});
83
89
  function applyFilter(){const result=browseResults(baseThemes(),browse);for(const c of cards.values())visibleObserver.unobserve(c.host);const fragment=document.createDocumentFragment();for(const g of result.groups){const section=document.createElement('section');section.className='group';if(g.name){const h=document.createElement('h2');h.textContent=g.name+' · '+g.themes.length;section.append(h);}const grid=document.createElement('div');grid.className='grid';grid.setAttribute('role','list');for(const t of g.themes)grid.append(cards.get(key(t)).element);section.append(grid);fragment.append(section);}$('grid').replaceChildren(fragment);const visible=new Set(result.visible.map(key));for(const [k,c] of cards)if(visible.has(k))visibleObserver.observe(c.host);else if(c.state){dispose(c.state);c.state=null;}$('count').textContent=result.visible.length+' of '+baseThemes().length+' directions'+(browse.query?' for “'+browse.query+'”':'');$('empty').hidden=!!result.visible.length;syncControls();syncFacetCounts();syncChips();syncShort();persist();}
84
90
  function syncChips(){$('chips').replaceChildren();const chips=[];if(browse.collection!=='All directions')chips.push(['Collection: '+browse.collection,()=>{browse.collection='All directions';applyFilter();}]);for(const k of ['industries','families','identities','media','tasks'])for(const value of browse[k])chips.push([value,()=>{browse[k]=browse[k].filter(x=>x!==value);applyFilter();}]);if(browse.query)chips.push(['Search: '+browse.query,()=>{browse.query='';applyFilter();}]);for(const [name,remove] of chips){const b=button(name+' ×',remove);b.setAttribute('aria-label','Remove '+name);$('chips').append(b);}if(!chips.length)$('chips').hidden=true;else $('chips').hidden=false;$('empty').querySelector('p').textContent='Current filters: '+(chips.map(c=>c[0]).join(' · ')||'none')+'. Try a broader phrase or remove a filter.';}
85
- function facetUI(){const specs=[['industries','Industry',vocabulary.industries||[]],['families','Family',Object.keys(vocabulary.families)],['identities','Identity',vocabulary.identities],['media','Images',['none','light','photo-led']],['tasks','Customer task',['call','quote','appointment','understand work','inspection','estimate']]];$('facets').replaceChildren();const advanced=document.createElement('details'),summary=document.createElement('summary');summary.textContent='More filters';advanced.append(summary);for(const [facet,name,values] of specs){const field=document.createElement('fieldset'),legend=document.createElement('legend');legend.textContent=name;field.append(legend);if(facet==='identities'){const explanation=document.createElement('p');explanation.className='family-description';explanation.textContent='How you want your business to feel.';field.append(explanation);}for(const value of values){const label=document.createElement('label'),input=document.createElement('input'),count=document.createElement('small');input.type='checkbox';input.value=value;input.dataset.facet=facet;input.onchange=()=>{browse[facet]=input.checked?[...browse[facet],value]:browse[facet].filter(x=>x!==value);applyFilter();};const display={none:'No photos needed',light:'A few images','photo-led':'Photography leads',call:'Phone enquiry',quote:'Request a quote',appointment:'Appointment / class','understand work':'Understand the work'};count.textContent=baseThemes().filter(t=>{if(facet==='families')return t.browse.family===value;if(facet==='media')return t.browse.media===value;if(facet==='industries')return (t.browse.industries||[t.browse.industry]).includes(value);return (t.browse[facet]||[]).includes(value);}).length;label.append(input,display[value]||value,count);field.append(label);if(facet==='families'){const d=document.createElement('p');d.className='family-description';d.textContent=vocabulary.families[value];field.append(d);if(browse.archive==='current'){const strip=document.createElement('div');strip.className='family-samples';strip.setAttribute('aria-label',value+' examples');for(const t of model().themes.filter(t=>t.browse.family===value&&vocabulary.samples[t.id]).slice(0,3)){const img=document.createElement('img');img.src=vocabulary.samples[t.id];img.alt=t.name+' original palette opening';img.title=t.name;strip.append(img);}field.append(strip);}}}if(['media','tasks'].includes(facet))advanced.append(field);else $('facets').append(field);}$('facets').append(advanced);}
91
+ function facetUI(){const specs=[['industries','Industry',vocabulary.industries||[]],['families','Family',Object.keys(vocabulary.families)],['identities','Identity',vocabulary.identities],['media','Images',['none','light','photo-led']],['tasks','Customer task',['call','quote','appointment','understand work','inspection','estimate']]];$('facets').replaceChildren();const advanced=document.createElement('details'),summary=document.createElement('summary');summary.textContent='More filters';advanced.append(summary);for(const [facet,name,values] of specs){const field=document.createElement('fieldset'),legend=document.createElement('legend');legend.textContent=name;field.append(legend);if(facet==='identities'){const explanation=document.createElement('p');explanation.className='family-description';explanation.textContent='How you want your business to feel.';field.append(explanation);}for(const value of values){const label=document.createElement('label'),input=document.createElement('input'),count=document.createElement('small');input.type='checkbox';input.value=value;input.dataset.facet=facet;input.onchange=()=>{browse[facet]=input.checked?[...browse[facet],value]:browse[facet].filter(x=>x!==value);applyFilter();};const display={none:'No photos needed',light:'A few images','photo-led':'Photography leads',call:'Phone enquiry',quote:'Request a quote',appointment:'Appointment / class','understand work':'Understand the work'};count.textContent=baseThemes().filter(t=>{if(facet==='families')return t.browse.family===value;if(facet==='media')return t.browse.media===value;if(facet==='industries')return (t.browse.industries||[t.browse.industry]).includes(value);return (t.browse[facet]||[]).includes(value);}).length;label.append(input,display[value]||value,count);field.append(label);if(facet==='families'){const d=document.createElement('p');d.className='family-description';d.textContent=vocabulary.families[value];field.append(d);if(browse.archive==='current'){const strip=document.createElement('div');strip.className='family-samples';strip.setAttribute('aria-label',value+' examples');for(const t of model().themes.filter(t=>t.browse.family===value&&vocabulary.samples[t.id]).slice(0,3)){const img=document.createElement('img');thumbnailSource(img,vocabulary.samples[t.id]);img.alt=t.name+' original palette opening';img.title=t.name;strip.append(img);}field.append(strip);}}}if(['media','tasks'].includes(facet))advanced.append(field);else $('facets').append(field);}$('facets').append(advanced);}
86
92
  function notice(text){$('browsenotice').textContent=text;setTimeout(()=>{$('browsenotice').textContent='';},5000);}
87
93
  function syncFacetCounts(){for(const input of $('facets').querySelectorAll('input[data-facet]')){const facet=input.dataset.facet;const count=browseResults(baseThemes(),{...browse,[facet]:[input.value]}).visible.length;input.parentElement.querySelector('small').textContent=count;}}
88
94
  function toggleShort(t){const k=key(t);if(browse.shortlist.includes(k)){browse.shortlist=browse.shortlist.filter(x=>x!==k);browse.compare=browse.compare.filter(x=>x!==k);}else if(browse.shortlist.length<6)browse.shortlist.push(k);else notice('Your shortlist holds six directions. Remove one to add another.');syncShort();persist();}
@@ -12,7 +12,7 @@ import {
12
12
  siteCss,
13
13
  siteIslands,
14
14
  sitemapXml
15
- } from "./chunk-ZIROO2U4.js";
15
+ } from "./chunk-37F7XIMH.js";
16
16
  import {
17
17
  mergeTheme,
18
18
  themeCss
@@ -258,6 +258,7 @@ async function buildSite(input) {
258
258
  const js = siteIslands([page], input.catalog);
259
259
  pageAssets[page.path] = { css: asset(siteCss([page], input.catalog), "css"), js: js ? asset(js, "js") : null };
260
260
  }
261
+ if (backendOrigin) pageAssets[COOKIE_COMPLIANCE_PATH] = { css: asset(siteCss([], input.catalog), "css"), js: null };
261
262
  const files = compileSite({
262
263
  site,
263
264
  pages: input.pages,
@@ -267,11 +268,14 @@ async function buildSite(input) {
267
268
  publicOrigin: input.publicOrigin,
268
269
  pageAssets,
269
270
  webAnalyticsToken: input.webAnalyticsToken
270
- });
271
+ }, compiled);
271
272
  const written = [];
272
273
  await mkdir(input.outDir, { recursive: true });
273
274
  await mkdir(join(input.outDir, "assets/immutable"), { recursive: true });
274
- for (const [path, body] of routeAssets) await writeFile(join(input.outDir, path.slice(1)), body, "utf8");
275
+ for (const [path, body] of routeAssets) {
276
+ await writeFile(join(input.outDir, path.slice(1)), body, "utf8");
277
+ written.push(path.slice(1));
278
+ }
275
279
  for (const file of files) {
276
280
  const rel = file.path === "/" ? "index.html" : `${file.path.replace(/^\//, "").replace(/\/$/, "")}/index.html`;
277
281
  const dest = join(input.outDir, rel);
@@ -281,7 +285,10 @@ async function buildSite(input) {
281
285
  }
282
286
  await writeFile(join(input.outDir, "site.css"), siteCss(compiled, input.catalog), "utf8");
283
287
  await writeFile(join(input.outDir, "theme.css"), themeCss(theme), "utf8");
284
- if (backendOrigin) await writeFile(join(input.outDir, "beacon.js"), beaconJs, "utf8");
288
+ if (backendOrigin) {
289
+ await writeFile(join(input.outDir, "beacon.js"), beaconJs, "utf8");
290
+ written.push("beacon.js");
291
+ }
285
292
  await writeFile(join(input.outDir, "robots.txt"), robotsTxt(input.publicOrigin), "utf8");
286
293
  await writeFile(
287
294
  join(input.outDir, "sitemap.xml"),
@@ -300,7 +307,7 @@ async function buildSite(input) {
300
307
  written.push(motionAssetHref.slice(1));
301
308
  }
302
309
  if (islands) written.push("islands.js");
303
- written.push("site.css", "theme.css", "beacon.js", "robots.txt", "sitemap.xml");
310
+ written.push("site.css", "theme.css", "robots.txt", "sitemap.xml");
304
311
  return written;
305
312
  }
306
313
  async function fetchLiveContent(kernelOrigin, slug, token) {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  capturePreservedSite,
3
3
  verifyPreservedSite
4
- } from "./chunk-QRWGFLUU.js";
4
+ } from "./chunk-R2WRQDVE.js";
5
5
  import {
6
6
  AGENT_RUNTIME_HISTORY_ENTRIES
7
7
  } from "./chunk-34H54KEY.js";
@@ -68,7 +68,7 @@ import {
68
68
  verifyAllowedWrites,
69
69
  withMutationLock,
70
70
  writeVisualProbe
71
- } from "./chunk-FRABKM65.js";
71
+ } from "./chunk-XY73ZLHB.js";
72
72
  import {
73
73
  QUESTION_VERSION,
74
74
  questionSemanticsHash
@@ -99,7 +99,7 @@ import {
99
99
  findHarnessRoot,
100
100
  findKernelRoot,
101
101
  isPreserveSite
102
- } from "./chunk-VTI32UGJ.js";
102
+ } from "./chunk-H3BS2EBR.js";
103
103
  import {
104
104
  STUDY_CAPTURE_HEIGHT,
105
105
  STUDY_CAPTURE_VIEWPORTS,
@@ -253,7 +253,7 @@ function scaffoldNestedSite(opts) {
253
253
  for (const [key, value] of Object.entries(pkg.scripts)) pkg.scripts[key] = value.replaceAll("__SLUG__", opts.slug);
254
254
  writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}
255
255
  `);
256
- for (const file2 of ["site.json", "README.md", "AGENTS.md"]) {
256
+ for (const file2 of ["site.json", "README.md", "AGENTS.md", "deployment/github-actions.yml.example"]) {
257
257
  const p = join2(dest, file2);
258
258
  if (!existsSync2(p)) continue;
259
259
  if (file2 === "site.json") {
@@ -13,7 +13,7 @@ import {
13
13
  formatExclusion,
14
14
  isErrorPage,
15
15
  isPlaywrightLaunchFailure
16
- } from "./chunk-FRABKM65.js";
16
+ } from "./chunk-XY73ZLHB.js";
17
17
  import {
18
18
  json,
19
19
  save
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-XL2DRAYI.js";
4
4
  import {
5
5
  sourceRevision
6
- } from "./chunk-FRABKM65.js";
6
+ } from "./chunk-XY73ZLHB.js";
7
7
  import {
8
8
  previewServer
9
9
  } from "./chunk-WEFDVXFX.js";
@@ -22,7 +22,7 @@ import {
22
22
  providerConsoleWarning,
23
23
  readOnlyEmbedProvider,
24
24
  unresolvedCaptureBlockers
25
- } from "./chunk-VTI32UGJ.js";
25
+ } from "./chunk-H3BS2EBR.js";
26
26
  import {
27
27
  save,
28
28
  within
@@ -11,48 +11,48 @@ import {
11
11
  runDoctor,
12
12
  shareSite,
13
13
  synthesizeSite
14
- } from "./chunk-ZWFG4GPG.js";
14
+ } from "./chunk-YXJAQYDE.js";
15
15
  import {
16
16
  verifySite
17
- } from "./chunk-P56ZDMX7.js";
17
+ } from "./chunk-WMJVLMU2.js";
18
18
  import {
19
19
  upgradeSite
20
- } from "./chunk-PGFFKPK6.js";
20
+ } from "./chunk-A2XMAPVW.js";
21
21
  import {
22
22
  createSite
23
- } from "./chunk-H7Y72HXC.js";
23
+ } from "./chunk-UFTVWTEZ.js";
24
24
  import {
25
25
  explainStage
26
- } from "./chunk-TTAUU3AZ.js";
26
+ } from "./chunk-3OKR5AZJ.js";
27
27
  import {
28
28
  runSite,
29
29
  statusSite,
30
30
  workflowArgvFromFlags
31
- } from "./chunk-7V3OKLKH.js";
31
+ } from "./chunk-LZC37OJZ.js";
32
32
  import {
33
33
  EXIT_CODES
34
34
  } from "./chunk-JCRGE6AL.js";
35
35
  import {
36
36
  siteInputsFromFlags
37
- } from "./chunk-FRABKM65.js";
37
+ } from "./chunk-XY73ZLHB.js";
38
38
  import {
39
39
  buildVerified,
40
40
  findHarnessRoot
41
- } from "./chunk-VTI32UGJ.js";
41
+ } from "./chunk-H3BS2EBR.js";
42
42
  import {
43
43
  approvedOrigin,
44
44
  inspectForms,
45
45
  safeId,
46
46
  validateFormConfig
47
- } from "./chunk-4MFPATAB.js";
47
+ } from "./chunk-L44ZTNDM.js";
48
48
  import {
49
49
  files
50
50
  } from "./chunk-T6PE4KN4.js";
51
51
 
52
52
  // src/cli/index.ts
53
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
54
- import { resolve as resolve2 } from "node:path";
55
- import { spawnSync } from "node:child_process";
53
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
54
+ import { resolve as resolve3 } from "node:path";
55
+ import { spawnSync as spawnSync2 } from "node:child_process";
56
56
 
57
57
  // src/cli/events.ts
58
58
  var EventStream = class {
@@ -270,6 +270,106 @@ async function formsCommand(command, opts) {
270
270
  }
271
271
  }
272
272
 
273
+ // src/cli/admin.ts
274
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
275
+ import { resolve as resolve2 } from "node:path";
276
+ import { createRequire } from "node:module";
277
+ import { spawnSync } from "node:child_process";
278
+ import { parseManifest } from "@tryvitris/website-admin-contracts";
279
+
280
+ // src/admin/connection.ts
281
+ async function readConnection(manifest, token, resource, test = false, transport = fetch) {
282
+ if (!manifest.backend || !/^[a-zA-Z0-9_-]{32,200}$/.test(token)) throw Error("A dedicated VITRIS_ADMIN_API_TOKEN and backend connection are required");
283
+ const url = new URL(`/api/admin/v1/${encodeURIComponent(manifest.backend.tenantId)}/${resource}`, manifest.backend.origin);
284
+ if (test) url.searchParams.set("mode", "test");
285
+ const response = await transport(url, { redirect: "manual", signal: AbortSignal.timeout(12e3), headers: { authorization: `Bearer ${token}`, "x-vitris-installation": manifest.installationId, "x-vitris-site-origin": manifest.site.origin } });
286
+ if (!response.ok) throw Error(`Admin v1 ${resource} rejected (${response.status}); connection is not verified`);
287
+ const reader = response.body?.getReader();
288
+ if (!reader) throw Error("Empty backend response");
289
+ const chunks = [];
290
+ let size = 0;
291
+ for (; ; ) {
292
+ const { done, value } = await reader.read();
293
+ if (done) break;
294
+ size += value.length;
295
+ if (size > 4e6) {
296
+ await reader.cancel();
297
+ throw Error("Backend response too large");
298
+ }
299
+ chunks.push(value);
300
+ }
301
+ const bytes = new Uint8Array(size);
302
+ let offset = 0;
303
+ for (const chunk of chunks) {
304
+ bytes.set(chunk, offset);
305
+ offset += chunk.length;
306
+ }
307
+ const data = JSON.parse(new TextDecoder().decode(bytes));
308
+ if (data?.schemaVersion !== 1 || data.installationId !== manifest.installationId || data.tenantId !== manifest.backend.tenantId || data.siteOrigin !== manifest.site.origin) throw Error("Backend contract or installation identity mismatch");
309
+ if (resource === "status") {
310
+ if (!Array.isArray(data.scopes)) throw Error("Invalid backend scopes");
311
+ } else {
312
+ if (data.source !== "connected" || !Array.isArray(data.rows) || data.rows.length > 200) throw Error("Invalid backend records");
313
+ if (resource === "leads" && data.rows.some((r) => ["id", "name", "email", "message", "created_at", "status"].some((k) => typeof r?.[k] !== "string"))) throw Error("Invalid lead response");
314
+ if (resource === "analytics" && data.rows.some((r) => typeof r?.day !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(r.day) || ["views", "visitors", "enquiries"].some((k) => typeof r[k] !== "number" || !Number.isFinite(r[k]) || r[k] < 0))) throw Error("Invalid analytics response");
315
+ }
316
+ return data;
317
+ }
318
+
319
+ // src/cli/admin.ts
320
+ function mergeAdminGrant(existing, addition) {
321
+ const object = (v) => !!v && typeof v === "object" && !Array.isArray(v);
322
+ if (!object(existing) || !object(addition) || Object.keys(addition).length !== 1) throw Error("Expected complete grant map and one installation grant");
323
+ for (const [id, raw] of Object.entries({ ...existing, ...addition })) {
324
+ const g = raw;
325
+ if (!/^[a-z][a-z0-9-]{0,50}$/.test(id) || !object(g) || typeof g.tenantId !== "string" || !/^[a-zA-Z0-9_-]{1,100}$/.test(g.tenantId) || typeof g.tokenHash !== "string" || !/^[a-f0-9]{64}$/.test(g.tokenHash) || typeof g.siteOrigin !== "string" || new URL(g.siteOrigin).origin !== g.siteOrigin || !g.siteOrigin.startsWith("https://") || !Array.isArray(g.scopes) || g.scopes.some((s) => !["leads:read", "analytics:read", "leads:write"].includes(s)) || Object.keys(g).some((k) => !["tenantId", "siteOrigin", "tokenHash", "scopes"].includes(k))) throw Error("Invalid hashed grant; raw credentials are forbidden");
326
+ }
327
+ for (const id of Object.keys(addition)) if (Object.hasOwn(existing, id) && JSON.stringify(existing[id]) !== JSON.stringify(addition[id])) throw Error("Grant already exists with different authority; rotate explicitly");
328
+ return { ...existing, ...addition };
329
+ }
330
+ async function adminCommand(command, options) {
331
+ try {
332
+ const site = resolve2(options.site || ".");
333
+ if (command === "grant") {
334
+ if (!options.dir || !options.grants || !options.out || options.dryRun) throw Error("admin grant requires --dir, --grants COMPLETE_MAP_FILE and --out NEW_IGNORED_FILE");
335
+ const addition = JSON.parse(readFileSync2(resolve2(options.dir, ".vitris/backend-grant.json"), "utf8"));
336
+ const manifest2 = parseManifest(JSON.parse(readFileSync2(resolve2(options.dir, "admin.config.json"), "utf8")));
337
+ const grant = addition[manifest2.installationId];
338
+ if (!grant || grant.tenantId !== manifest2.backend?.tenantId || grant.siteOrigin !== manifest2.site.origin) throw Error("Grant and installation identity differ");
339
+ const merged = mergeAdminGrant(JSON.parse(readFileSync2(resolve2(options.grants), "utf8")), addition);
340
+ writeFileSync2(resolve2(options.out), JSON.stringify(merged, null, 2) + "\n", { flag: "wx", mode: 384 });
341
+ return { ok: true, exitCode: 0, summary: "Merged hashed grants into the new local file. No backend secret changed; review and install explicitly through secret stdin." };
342
+ }
343
+ if (command === "setup") {
344
+ if (!options.dir || !options.domain || !options.tenant || !!options.plan === !!options.apply) throw Error("admin setup requires --dir, --domain, --tenant and exactly one of --plan or --apply");
345
+ if (options.dryRun) throw Error("Use --plan for local setup planning; --dry-run does not write a setup plan");
346
+ const args = ["setup", "--site", site, "--dir", resolve2(options.dir), "--domain", options.domain, "--tenant", options.tenant, options.apply ? "--apply" : "--plan"];
347
+ if (options.website) args.push("--website", options.website);
348
+ if (options.backendOrigin) args.push("--backend-origin", options.backendOrigin);
349
+ if (options.leadWrite) args.push("--lead-write");
350
+ const entry = createRequire(import.meta.url).resolve("@tryvitris/website-admin-harness/cli");
351
+ const output = spawnSync(process.execPath, [entry, ...args], { encoding: "utf8", windowsHide: true, maxBuffer: 1024 * 1024 });
352
+ if (output.status !== 0) throw Error(output.error?.message || output.stderr.trim() || "Admin setup failed");
353
+ return { ok: true, exitCode: 0, summary: output.stdout.trim() };
354
+ }
355
+ if (!["status", "verify"].includes(command)) throw Error("Use vitris admin setup|status|verify|grant");
356
+ const launch = JSON.parse(readFileSync2(resolve2(site, "launch.json"), "utf8"));
357
+ if (command === "status") return { ok: true, exitCode: 0, summary: launch.admin ? "Local admin metadata exists. Backend, authentication and deployment are unverified; run admin verify with --dir." : "No admin connection. Read docs/ADMIN.md; scaffolding alone does not enable collection.", data: { connection: launch.admin || null, verified: false } };
358
+ if (!options.dir) throw Error("admin verify requires --dir ADMIN_DIRECTORY");
359
+ const manifest = parseManifest(JSON.parse(readFileSync2(resolve2(options.dir, "admin.config.json"), "utf8")));
360
+ if (!launch.admin || manifest.installationId !== launch.admin.installationId || manifest.site.id !== launch.admin.siteId || manifest.adminOrigin !== launch.admin.url || manifest.site.origin !== launch.admin.siteOrigin || manifest.backend?.tenantId !== launch.admin.tenantId) throw Error("Website and admin installation identities differ");
361
+ const token = process.env.VITRIS_ADMIN_API_TOKEN || "";
362
+ const status = await readConnection(manifest, token, "status");
363
+ if (!["leads:read", "analytics:read"].every((scope) => status.scopes.includes(scope))) throw Error("Default read scopes are missing");
364
+ const leads = await readConnection(manifest, token, "leads");
365
+ const tests = await readConnection(manifest, token, "leads", true);
366
+ const analytics = await readConnection(manifest, token, "analytics");
367
+ return { ok: true, exitCode: 0, summary: "Admin v1 data connection verified. Authentication, collection and deployment require separate acceptance checks.", data: { verified: true, productionLeads: leads.rows.length, testSubmissions: tests.rows.length, analyticsDays: analytics.rows.length, scopes: status.scopes } };
368
+ } catch (error) {
369
+ return { ok: false, exitCode: EXIT_CODES.PRECONDITION_FAILED, error: error instanceof Error ? error.message : String(error) };
370
+ }
371
+ }
372
+
273
373
  // src/cli/index.ts
274
374
  function parseArgs(args) {
275
375
  const command = [];
@@ -309,9 +409,9 @@ function parseArgs(args) {
309
409
  }
310
410
  function resolvePrecedence(flags, siteRoot) {
311
411
  const merged = {};
312
- if (siteRoot && existsSync2(resolve2(siteRoot, "launch.json"))) {
412
+ if (siteRoot && existsSync2(resolve3(siteRoot, "launch.json"))) {
313
413
  try {
314
- const launch = JSON.parse(readFileSync2(resolve2(siteRoot, "launch.json"), "utf8"));
414
+ const launch = JSON.parse(readFileSync3(resolve3(siteRoot, "launch.json"), "utf8"));
315
415
  if (typeof launch.ownerEmail === "string") merged.owner = launch.ownerEmail;
316
416
  if (typeof launch.slug === "string") merged.slug = launch.slug;
317
417
  if (typeof launch.name === "string") merged.name = launch.name;
@@ -320,9 +420,9 @@ function resolvePrecedence(flags, siteRoot) {
320
420
  }
321
421
  if (flags.config && typeof flags.config === "string") {
322
422
  try {
323
- const configPath = resolve2(flags.config);
423
+ const configPath = resolve3(flags.config);
324
424
  if (existsSync2(configPath)) {
325
- const cfg = JSON.parse(readFileSync2(configPath, "utf8"));
425
+ const cfg = JSON.parse(readFileSync3(configPath, "utf8"));
326
426
  for (const [k, v] of Object.entries(cfg)) {
327
427
  if (typeof v === "string" || typeof v === "boolean") merged[k] = v;
328
428
  }
@@ -354,7 +454,7 @@ async function runCli(argv) {
354
454
  }
355
455
  const stream = new EventStream(outputMode);
356
456
  if (flags.version || flags.v) {
357
- stream.log(`@tryvitris/website-harness v${JSON.parse(readFileSync2(resolve2(findHarnessRoot(), "package.json"), "utf8")).version}`);
457
+ stream.log(`@tryvitris/website-harness v${JSON.parse(readFileSync3(resolve3(findHarnessRoot(), "package.json"), "utf8")).version}`);
358
458
  return EXIT_CODES.SUCCESS;
359
459
  }
360
460
  if (flags.help || flags.h || command.length === 0) {
@@ -363,6 +463,7 @@ async function runCli(argv) {
363
463
  Usage: vitris <command> [options]
364
464
 
365
465
  Commands:
466
+ admin setup|status|verify Separate business admin; explicit cloud rollout
366
467
  forms inspect|setup|status Inventory, provision, and check contact forms
367
468
  doctor Check environment, dependencies, and site health
368
469
  site create --dir DIR --slug SLUG Scaffold a new customer site repo (--from URL or --blank)
@@ -379,6 +480,7 @@ Commands:
379
480
  verify Verify compilation and evaluate all quality gates
380
481
  preview [--port PORT] Serve local verified dist preview
381
482
  deploy [--provider cloudflare] Deploy site to Cloudflare Pages with smoke verification
483
+ --quick Build and deploy, skipping review and quality gates
382
484
  cloudflare [doctor|whoami|login] Cloudflare credentials and permissions diagnostics
383
485
  share [--run ID] Start local preview or design comparison review share
384
486
  knowledge explain --stage STAGE Show the knowledge that stage would load. No model call.
@@ -390,6 +492,7 @@ Options:
390
492
  --template-release RELEASE Optional template revision
391
493
  --content TEXT Additional business content or instructions
392
494
  --content-file FILE UTF-8 content file (instead of --content)
495
+ --admin New-site opt-in: prepare sibling admin next steps
393
496
  --backend-origin URL Backend origin for a new site
394
497
  --site DIR Site directory path (default: current directory)
395
498
  --json Output single structured JSON result at completion
@@ -412,7 +515,10 @@ Options:
412
515
  const secondary = command[1];
413
516
  let result;
414
517
  try {
415
- if (primary === "forms") {
518
+ if (primary === "admin") {
519
+ const value = (key) => typeof opts[key] === "string" ? opts[key] : void 0;
520
+ result = await adminCommand(secondary || "status", { site: siteRoot, dir: value("dir"), domain: value("domain"), website: value("website"), tenant: value("tenant"), backendOrigin: value("backend-origin"), plan: !!opts.plan, apply: !!opts.apply, leadWrite: !!opts["lead-write"], grants: value("grants"), out: value("out"), dryRun });
521
+ } else if (primary === "forms") {
416
522
  const list = (name) => typeof opts[name] === "string" ? opts[name].split(",").map((s) => s.trim()).filter(Boolean) : void 0;
417
523
  result = await formsCommand(secondary || "status", { site: siteRoot, tenant: typeof opts.tenant === "string" ? opts.tenant : void 0, backendOrigin: typeof opts["backend-origin"] === "string" ? opts["backend-origin"] : void 0, origins: list("origins"), recipients: list("recipients"), mode: typeof opts.mode === "string" ? opts.mode : void 0, apply: !!opts.apply, testKeys: !!opts["test-keys"], cloudflare: !!opts.cloudflare, worker: typeof opts.worker === "string" ? opts.worker : void 0, config: typeof opts["forms-config"] === "string" ? opts["forms-config"] : void 0, dryRun });
418
524
  } else if (primary === "doctor") {
@@ -424,6 +530,7 @@ Options:
424
530
  } else if (primary === "site" && secondary === "create") {
425
531
  result = await createSite({
426
532
  ...siteInputsFromFlags(opts),
533
+ admin: !!opts.admin,
427
534
  dir: typeof opts.dir === "string" ? opts.dir : "",
428
535
  slug: typeof opts.slug === "string" ? opts.slug : "",
429
536
  name: typeof opts.name === "string" ? opts.name : void 0,
@@ -548,6 +655,7 @@ Options:
548
655
  provider: "cloudflare",
549
656
  environment: opts.environment === "production" ? "production" : "preview",
550
657
  branch: typeof opts.branch === "string" ? opts.branch : void 0,
658
+ quick: flags.quick === true,
551
659
  dryRun,
552
660
  signal: abortController.signal,
553
661
  onLog: (line) => stream.log(line)
@@ -557,14 +665,14 @@ Options:
557
665
  if (sub === "doctor") {
558
666
  result = await runDoctor({ provider: "cloudflare", operation: "pages" });
559
667
  } else if (sub === "whoami") {
560
- const probe = spawnSync(process.platform === "win32" ? "npx.cmd" : "npx", ["wrangler", "whoami"], { encoding: "utf8" });
668
+ const probe = spawnSync2(process.platform === "win32" ? "npx.cmd" : "npx", ["wrangler", "whoami"], { encoding: "utf8" });
561
669
  result = {
562
670
  ok: probe.status === 0,
563
671
  exitCode: probe.status === 0 ? EXIT_CODES.SUCCESS : EXIT_CODES.CONFIG_OR_CLI_ERROR,
564
672
  summary: probe.stdout || probe.stderr
565
673
  };
566
674
  } else if (sub === "login") {
567
- const probe = spawnSync(process.platform === "win32" ? "npx.cmd" : "npx", ["wrangler", "login"], { stdio: "inherit" });
675
+ const probe = spawnSync2(process.platform === "win32" ? "npx.cmd" : "npx", ["wrangler", "login"], { stdio: "inherit" });
568
676
  result = {
569
677
  ok: probe.status === 0,
570
678
  exitCode: probe.status === 0 ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERIC_ERROR,
@@ -578,7 +686,7 @@ Options:
578
686
  };
579
687
  }
580
688
  } else if (primary === "knowledge" && secondary === "explain") {
581
- const explained = explainStage(resolve2(typeof opts.site === "string" ? opts.site : "."), typeof opts.stage === "string" ? opts.stage : "direction");
689
+ const explained = explainStage(resolve3(typeof opts.site === "string" ? opts.site : "."), typeof opts.stage === "string" ? opts.stage : "direction");
582
690
  result = { ok: true, exitCode: EXIT_CODES.SUCCESS, summary: explained.text, data: explained };
583
691
  } else if (primary === "share") {
584
692
  result = await shareSite({
@@ -7,19 +7,19 @@ import {
7
7
  import {
8
8
  gatherBlankOnboard,
9
9
  persistOnboard
10
- } from "./chunk-5YSPWKFF.js";
10
+ } from "./chunk-PYCRUMCT.js";
11
11
  import {
12
12
  persistSiteInputs,
13
13
  recordManagedKit,
14
14
  resolveSiteInputsLive
15
- } from "./chunk-FRABKM65.js";
15
+ } from "./chunk-XY73ZLHB.js";
16
16
  import {
17
17
  initialState,
18
18
  parseGraphYaml
19
19
  } from "./chunk-BBV7PMFC.js";
20
20
  import {
21
21
  findHarnessRoot
22
- } from "./chunk-VTI32UGJ.js";
22
+ } from "./chunk-H3BS2EBR.js";
23
23
  import {
24
24
  save
25
25
  } from "./chunk-T6PE4KN4.js";
@@ -103,6 +103,7 @@ async function createSite(opts) {
103
103
  pkg.scripts.go = "node ./node_modules/@tryvitris/website-harness/bin/vitris.js run --site .";
104
104
  pkg.scripts.eval = "node ./node_modules/@tryvitris/website-harness/bin/vitris.js verify --site .";
105
105
  pkg.scripts.deploy = "node ./node_modules/@tryvitris/website-harness/bin/vitris.js deploy --site . --environment preview";
106
+ pkg.scripts["deploy:quick"] = "node ./node_modules/@tryvitris/website-harness/bin/vitris.js deploy --site . --environment preview --quick";
106
107
  pkg.scripts["verify:browser"] = pkg.scripts.eval;
107
108
  }
108
109
  for (const [key, value] of Object.entries(pkg.scripts)) pkg.scripts[key] = value.replaceAll("__SLUG__", slug);
@@ -150,7 +151,9 @@ Preserved custom website from ${fromArg}.
150
151
  2. npx playwright install chromium
151
152
  3. npm run go
152
153
  4. npm run preview
153
- 5. npm run deploy (temporary Cloudflare preview)
154
+ 5. npm run deploy (build and verify locally, then upload a temporary Cloudflare preview)
155
+
156
+ Local builds and direct uploads are the default. GitHub stores source; no deployment workflow runs automatically. Keep dist/ and release bundles out of Git.
154
157
 
155
158
  Custom source: preserved/. Verification: agent/reviews/preserve.json. See AGENTS.md and the installed harness docs/PRESERVE.md. Run vitris forms inspect and vitris forms setup to register and connect contact forms; backendOrigin alone does not enable submission or notifications. See docs/CONTACT-FORMS.md.
156
159
  `);
@@ -160,10 +163,14 @@ Run npm run go to capture and verify the existing client-facing site. Keep its a
160
163
 
161
164
  Repair source in preserved/ when agent/reviews/preserve.json reports a failure; run npm run go again. Never change reference screenshots or hashes to make a check pass. Do not overwrite source by recapturing. onboarding/preserve.json records the source and captured routes. Contact forms require explicit registration and public launch.json forms bindings. backendOrigin alone does not enable delivery. Default previews show a notice without submitting. See docs/CONTACT-FORMS.md.
162
165
 
163
- Use npm run deploy for a temporary Cloudflare preview after verification. Production and customer domains are outside this mode. Scaffold a later redesign separately from the same source URL.
166
+ Use npm run deploy to build and verify locally and directly upload a temporary Cloudflare preview. Keep dist/ and release bundles out of Git. Hosted automation is opt-in; deployment/github-actions.yml.example is inactive by default. Production and customer domains are outside this mode. Scaffold a later redesign separately from the same source URL.
164
167
  `);
165
168
  }
166
- const workflow = join(dest, ".github", "workflows", "deploy.yml");
169
+ if (mode === "preserve") {
170
+ const instructions = join(dest, "AGENTS.md");
171
+ writeFileSync(instructions, readFileSync(instructions, "utf8") + "\nFor a requested sibling business admin, read agent/refs/admin.md and the installed harness docs/ADMIN.md. Use vitris admin setup --plan then reviewed --apply. Preserve current forms and backend identity; cloud provisioning, authentication and deployment remain separate.\n");
172
+ }
173
+ const workflow = join(dest, "deployment", "github-actions.yml.example");
167
174
  if (existsSync(workflow)) {
168
175
  if (mode === "preserve") {
169
176
  writeFileSync(workflow, `name: Preserve preview
@@ -197,6 +204,18 @@ jobs:
197
204
  copyMarkdown: gathered.copyMarkdown
198
205
  });
199
206
  persistSiteInputs(dest, inputs);
207
+ if (opts.admin) {
208
+ writeFileSync(join(dest, "ADMIN-SETUP.md"), `# Optional business admin
209
+
210
+ Read node_modules/@tryvitris/website-harness/docs/ADMIN.md and agent/refs/admin.md.
211
+
212
+ Prepare a separate sibling installation after confirming the exact website origin, existing tenant ID and backend origin:
213
+
214
+ vitris admin setup --site . --dir ../${slug}-admin --domain admin.CUSTOMER_DOMAIN --website https://www.CUSTOMER_DOMAIN --tenant EXISTING_TENANT_ID --backend-origin https://BACKEND --plan
215
+
216
+ Review artifacts/admin/setup-plan.json, then repeat with --apply and a dedicated VITRIS_ADMIN_API_TOKEN in the operator environment. This opt-in has created instructions only. No backend, grant, account, collection, authentication, DNS or cloud resources have been changed.
217
+ `);
218
+ }
200
219
  const graphPath = join(dest, "agent", "graph.yaml");
201
220
  if (existsSync(graphPath)) {
202
221
  const graph = parseGraphYaml(readFileSync(graphPath, "utf8"));
@@ -209,7 +228,7 @@ jobs:
209
228
  return {
210
229
  ok: true,
211
230
  exitCode: EXIT_CODES.SUCCESS,
212
- summary: `Created site ${slug} at ${dest}`,
231
+ summary: `Created site ${slug} at ${dest}${opts.admin ? "; optional admin next steps: ADMIN-SETUP.md" : ""}`,
213
232
  data: { dest, slug, name }
214
233
  };
215
234
  }