@office-kit/pptx-dev 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -3
- package/dist/cli.mjs +310 -5
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +1 -0
- package/dist/worker.mjs +2 -1
- package/dist/worker.mjs.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ npm run dev
|
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
Open the local URL printed by the server. Save a slide file, `theme.ts` or `deck.tsx` to rebuild. The viewer
|
|
20
|
-
has a vertical thumbnail strip
|
|
20
|
+
has a vertical thumbnail strip, a large slide canvas and an AI chat panel on the right. Click a thumbnail or use
|
|
21
21
|
arrow keys, Page Up/Down, Home/End to navigate. Fit/zoom and Present (Escape to
|
|
22
22
|
exit) are viewing controls; the canvas has no editing, dragging or resize handles.
|
|
23
23
|
Changes are made only in TSX, including when an AI agent edits the presentation.
|
|
@@ -43,7 +43,50 @@ The generated `CLAUDE.md` explains authoring conventions. VSCode picks up the
|
|
|
43
43
|
included `tsconfig.json` for completion and diagnostics. Start the **Preview
|
|
44
44
|
presentation** task, then use **Simple Browser: Show** with the printed URL and
|
|
45
45
|
move it to a side editor group. No custom editor extension is needed for this
|
|
46
|
-
workflow.
|
|
46
|
+
workflow. Clicking a slide supplies chat context; opening its exact TSX location is not implemented.
|
|
47
|
+
|
|
48
|
+
## Chat in the preview
|
|
49
|
+
|
|
50
|
+
Choose **Claude Code** or **Codex** in the right panel and describe the change.
|
|
51
|
+
Install the selected CLI (`claude` or `codex`) on your PATH and sign in through
|
|
52
|
+
that CLI once before using chat. The dev server runs it from the deck entry's
|
|
53
|
+
directory, using its existing account and model configuration. No API key is
|
|
54
|
+
stored in the browser. Requests use the selected provider and its normal usage
|
|
55
|
+
limits. Sending a message authorizes edits to the local project.
|
|
56
|
+
|
|
57
|
+
Each request includes the focused slide's **1-based number, text, slide count,
|
|
58
|
+
preview revision, deck entry and source dependency paths**, plus any build error.
|
|
59
|
+
The focus is captured at send time, so navigating during a response does not
|
|
60
|
+
change it. A stale preview is rejected with a retry message. Dependency paths are
|
|
61
|
+
source candidates, not an exact slide-to-file mapping; the agent locates the
|
|
62
|
+
relevant TSX using the entry, slide order and text. This also works for single-file
|
|
63
|
+
decks, generated slide lists and imported presentations.
|
|
64
|
+
|
|
65
|
+
“This slide” directs a focused patch; requests mentioning other slides or the
|
|
66
|
+
whole deck can edit other files or shared styling. Saving uses the same automatic
|
|
67
|
+
preview rebuild as manual editing. Both CLIs run non-interactively:
|
|
68
|
+
|
|
69
|
+
- Claude Code uses print mode with JSON events and `acceptEdits`, exposing only
|
|
70
|
+
`Read`, `Edit`, `Write`, `Glob` and `Grep`. Shell execution is not exposed.
|
|
71
|
+
- Codex uses `exec --json` with the `workspace-write` sandbox. Sandbox bypass is
|
|
72
|
+
never enabled. Operations requiring interactive approval are not supported.
|
|
73
|
+
|
|
74
|
+
Progress, replies and failures appear in the panel. **Stop** terminates the running
|
|
75
|
+
agent; edits already saved remain. Only one edit runs at a time across browser
|
|
76
|
+
tabs. **New chat** clears conversational context without reverting files. Recent
|
|
77
|
+
12 messages (bounded text) are passed to each new CLI invocation; these are new
|
|
78
|
+
CLI sessions, not a continuation of an existing terminal conversation. The server
|
|
79
|
+
keeps up to 100 messages in memory, shared by its tabs, until it exits. Reloading
|
|
80
|
+
restores that conversation. Restarting the server clears it.
|
|
81
|
+
|
|
82
|
+
Use **Chat** in the header to hide/show the panel, and Ctrl/Cmd+Enter to send.
|
|
83
|
+
On narrow screens the panel moves below the preview. Presentation mode hides it.
|
|
84
|
+
Chat requests require same-origin JSON as well as the server's loopback Host
|
|
85
|
+
check. Authentication and CLI installation still happen in a terminal; normal
|
|
86
|
+
slide editing and review can then stay in the preview.
|
|
87
|
+
|
|
88
|
+
Protocol references: [Codex non-interactive mode](https://developers.openai.com/codex/noninteractive)
|
|
89
|
+
and [Claude Code programmatic use](https://code.claude.com/docs/en/headless).
|
|
47
90
|
|
|
48
91
|
## Edit only what changed
|
|
49
92
|
|
|
@@ -86,7 +129,7 @@ preview server binds to `127.0.0.1` and rejects unexpected Host headers.
|
|
|
86
129
|
|
|
87
130
|
Programmatic exports: `buildDeck`, `exportDeck`, `initProject`, `inspectTemplate`.
|
|
88
131
|
`buildDeck` returns PPTX bytes, SVG slides, aspect ratio, module dependencies and
|
|
89
|
-
core validation diagnostics. Browser APIs and DOM globals are not available in
|
|
132
|
+
slide text and core validation diagnostics. Browser APIs and DOM globals are not available in
|
|
90
133
|
TSX evaluation.
|
|
91
134
|
|
|
92
135
|
## Developing in this monorepo
|
package/dist/cli.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { a as createDeckBuilder, i as initProject, n as exportDeck, r as inspect
|
|
|
3
3
|
import { dirname, resolve, sep } from "node:path";
|
|
4
4
|
import { watch } from "node:fs";
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
6
7
|
//#region src/page.ts
|
|
7
8
|
const page = `<!doctype html>
|
|
8
9
|
<html lang="en">
|
|
@@ -17,7 +18,7 @@ button:hover:not(:disabled),.download:hover{background:#f0f3f9}button:disabled{o
|
|
|
17
18
|
header{display:flex;align-items:center;gap:16px;padding:0 20px;background:#fff;border-bottom:1px solid #d4d9e2;min-width:0}
|
|
18
19
|
.brand{font-weight:700;font-size:17px;white-space:nowrap}.badge{font-size:11px;color:#616b7c;background:#f1f3f7;border-radius:4px;padding:3px 6px}
|
|
19
20
|
#status{flex:1;color:#667085;min-width:0}#present{background:#293c73;color:#fff;border-color:#293c73}
|
|
20
|
-
.workspace{display:grid;grid-template-columns:
|
|
21
|
+
.workspace{display:grid;grid-template-columns:190px minmax(0,1fr) 340px;min-height:0}
|
|
21
22
|
.filmstrip{background:#f7f8fa;border-right:1px solid #d4d9e2;overflow:auto;overscroll-behavior:contain;padding:16px 12px}
|
|
22
23
|
.filmstrip h2{margin:0 0 12px 26px;text-transform:uppercase;letter-spacing:.1em;font-size:10px;color:#7c8596;font-weight:600}
|
|
23
24
|
#thumbnails{display:flex;flex-direction:column;gap:12px;margin:0;padding:0;list-style:none}
|
|
@@ -36,18 +37,33 @@ footer{display:flex;align-items:center;gap:14px;padding:0 16px;background:#fff;b
|
|
|
36
37
|
#count{min-width:90px}footer .hint{flex:1;color:#737d8e}footer button{padding:3px 10px}footer select{padding:3px 8px}
|
|
37
38
|
#presentation-controls{display:none}
|
|
38
39
|
body.presenting{grid-template-rows:minmax(0,1fr);background:#111}
|
|
39
|
-
.presenting header,.presenting footer,.presenting .filmstrip{display:none}
|
|
40
|
+
.presenting header,.presenting footer,.presenting .filmstrip,.presenting #chat{display:none}
|
|
40
41
|
.presenting .workspace{grid-template-columns:minmax(0,1fr)}.presenting #stage{padding:0;background:#111}.presenting #slide{box-shadow:none}
|
|
41
42
|
.presenting #presentation-controls{display:flex;position:fixed;bottom:16px;left:50%;transform:translateX(-50%);align-items:center;gap:12px;background:#202735e8;color:white;padding:6px;border-radius:8px;opacity:0;transition:opacity .15s}
|
|
42
43
|
.presenting #presentation-controls:hover,.presenting #presentation-controls:focus-within{opacity:1}
|
|
43
44
|
#presentation-controls button{background:transparent;color:white;border-color:#5d6575}
|
|
44
45
|
@media(max-width:700px){.workspace{grid-template-columns:140px minmax(0,1fr)}.filmstrip{padding:12px 5px}header{padding:0 12px;gap:10px}.badge,footer .hint{display:none}#stage{padding:16px}footer{gap:8px}#status{font-size:11px}.download{padding:7px 8px}}
|
|
46
|
+
#chat{min-height:0;display:flex;flex-direction:column;background:#fff;border-left:1px solid #d4d9e2}
|
|
47
|
+
.chat-heading{display:flex;align-items:center;gap:8px;padding:16px;border-bottom:1px solid #e6e9ef}.chat-heading strong{flex:1}.chat-heading button{padding:5px 8px}
|
|
48
|
+
#messages{flex:1;overflow:auto;padding:16px;display:flex;flex-direction:column;gap:16px;overscroll-behavior:contain}
|
|
49
|
+
.chat-message{white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.6}.chat-message.user{background:#eef2ff;border-radius:8px;padding:10px 12px}.chat-message small{display:block;color:#697386;font-size:11px;margin-bottom:4px}.chat-intro{color:#737d8e;line-height:1.7}
|
|
50
|
+
#chat-form{padding:14px;border-top:1px solid #e6e9ef}#chat-context{font-size:11px;color:#4967dd;margin-bottom:8px}#chat-input{font:inherit;resize:vertical;min-height:90px;max-height:200px;width:100%;padding:10px;border:1px solid #d4d9e2;border-radius:8px} .chat-actions{display:flex;gap:8px;margin-top:8px;align-items:center}.chat-actions select{min-width:0;flex:1}#chat-send{background:#293c73;color:white}#chat-status{padding:0 14px 12px;color:#667085;font-size:11px;white-space:pre-wrap;max-height:90px;overflow:auto}
|
|
51
|
+
body.chat-hidden .workspace{grid-template-columns:190px minmax(0,1fr)}body.chat-hidden #chat{display:none}
|
|
52
|
+
body.presenting .workspace{grid-template-columns:minmax(0,1fr)}
|
|
53
|
+
@media(max-width:1000px){.workspace{grid-template-columns:120px minmax(0,1fr) 300px}.filmstrip{padding:12px 5px}#stage{padding:16px}}
|
|
54
|
+
@media(max-width:700px){.workspace{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(160px,1fr) minmax(200px,1fr)}.filmstrip{display:none}#chat{border-top:1px solid #d4d9e2}.chat-heading{padding:8px 12px}#chat-form{padding:8px 12px}#chat-input{min-height:50px}body.chat-hidden .workspace,.presenting .workspace{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr)}.brand{font-size:14px}header{gap:6px}.download{font-size:11px}}
|
|
45
55
|
@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}
|
|
46
56
|
</style>
|
|
47
|
-
<header><span class="brand">Office Kit</span><span class="badge">Preview</span><span id="status" role="status">Building…</span><button id="present" disabled>Present</button><a class="download" href="/deck.pptx">Download PPTX</a></header>
|
|
57
|
+
<header><span class="brand">Office Kit</span><span class="badge">Preview</span><span id="status" role="status">Building…</span><button id="toggle-chat" aria-expanded="true" aria-controls="chat">Chat</button><button id="present" disabled>Present</button><a class="download" href="/deck.pptx">Download PPTX</a></header>
|
|
48
58
|
<div class="workspace">
|
|
49
59
|
<nav class="filmstrip" aria-label="Slides"><h2>Slides</h2><ol id="thumbnails"></ol></nav>
|
|
50
60
|
<main aria-label="Slide viewer"><pre id="error" role="alert" hidden></pre><div id="stage" tabindex="-1"><div id="empty">Waiting for slides…</div><div id="slide" hidden></div></div></main>
|
|
61
|
+
<aside id="chat" aria-label="Slide chat">
|
|
62
|
+
<div class="chat-heading"><strong>Edit with AI</strong><button id="chat-reset">New chat</button></div>
|
|
63
|
+
<div id="messages" role="log" aria-label="Conversation"><p class="chat-intro">Describe what you want to change. The current slide is included automatically. You can also ask about other slides or the whole deck.<br><br>Uses your locally installed Claude Code or Codex. Sign in through its CLI first. Sending a message allows it to edit this project.</p></div>
|
|
64
|
+
<form id="chat-form"><div id="chat-context">No slide selected</div><label for="chat-input" class="chat-intro">What would you like to change?</label><textarea id="chat-input" maxlength="16000" placeholder="Make this headline shorter…" required></textarea><div class="chat-actions"><select id="chat-provider" aria-label="AI provider"><option value="claude">Claude Code</option><option value="codex">Codex</option></select><button id="chat-stop" type="button" hidden>Stop</button><button id="chat-send" type="submit">Send</button></div></form>
|
|
65
|
+
<div id="chat-status" role="status">Ready</div>
|
|
66
|
+
</aside>
|
|
51
67
|
</div>
|
|
52
68
|
<footer><span id="count" aria-live="polite">No slides</span><span class="hint">Changes appear automatically · Text can be selected and copied</span><button id="prev" aria-label="Previous slide" disabled>‹</button><button id="next" aria-label="Next slide" disabled>›</button><label for="zoom">Zoom</label><select id="zoom"><option value="fit">Fit</option><option value="0.5">50%</option><option value="0.75">75%</option><option value="1">100%</option><option value="1.25">125%</option><option value="1.5">150%</option><option value="2">200%</option></select></footer>
|
|
53
69
|
<div id="presentation-controls"><button id="present-prev" aria-label="Previous slide">‹</button><span id="present-count"></span><button id="present-next" aria-label="Next slide">›</button><button id="exit-present">Exit · Esc</button></div>
|
|
@@ -74,6 +90,7 @@ function resize(){
|
|
|
74
90
|
function selectSlide(next,focusThumbnail=false,reveal=true){
|
|
75
91
|
index=Math.max(0,Math.min(next,state.slides.length-1));
|
|
76
92
|
const count=state.slides.length?'Slide '+(index+1)+' of '+state.slides.length:'No slides';
|
|
93
|
+
byId('chat-context').textContent=state.slides.length?'Context: '+count:'Context: whole project';
|
|
77
94
|
byId('count').textContent=count;byId('present-count').textContent=count;
|
|
78
95
|
for(const id of ['prev','present-prev'])byId(id).disabled=index===0;
|
|
79
96
|
for(const id of ['next','present-next'])byId(id).disabled=index>=state.slides.length-1;
|
|
@@ -164,9 +181,279 @@ async function refresh(){
|
|
|
164
181
|
try{const response=await fetch('/state'+(state.revision===undefined?'':'?since='+state.revision));if(!response.ok)throw new Error('Preview unavailable');const updated=await response.json();if(id===refreshId){if(updated.changes){updated.slides=state.slides.slice(0,updated.count);updated.slides.length=updated.count;for(const [position,svg] of Object.entries(updated.changes))updated.slides[Number(position)]=svg;}update(updated);}}
|
|
165
182
|
catch{if(id===refreshId)byId('status').textContent='Reconnecting…';}
|
|
166
183
|
}
|
|
167
|
-
|
|
184
|
+
let chatRequest=0,chatBusy=false,lastMessages='';
|
|
185
|
+
function renderChat(chat){
|
|
186
|
+
chatBusy=chat.running;
|
|
187
|
+
byId('chat-send').disabled=chat.running;
|
|
188
|
+
byId('chat-reset').disabled=chat.running;
|
|
189
|
+
byId('chat-provider').disabled=chat.running;
|
|
190
|
+
byId('chat-stop').hidden=!chat.running;
|
|
191
|
+
byId('chat-status').textContent=chat.status;
|
|
192
|
+
if(chat.messages.length)byId('chat-provider').value=chat.provider;
|
|
193
|
+
const serialized=JSON.stringify(chat.messages);
|
|
194
|
+
if(serialized===lastMessages)return;
|
|
195
|
+
lastMessages=serialized;
|
|
196
|
+
const log=byId('messages'),atBottom=log.scrollHeight-log.scrollTop-log.clientHeight<60;
|
|
197
|
+
log.replaceChildren();
|
|
198
|
+
if(!chat.messages.length){const intro=document.createElement('p');intro.className='chat-intro';intro.textContent='Ask for a focused slide edit or a change across the deck. Uses your locally installed CLI and its login. Sending allows edits to this project.';log.append(intro);}
|
|
199
|
+
for(const message of chat.messages){
|
|
200
|
+
const item=document.createElement('div'),label=document.createElement('small'),body=document.createElement('div');
|
|
201
|
+
item.className='chat-message '+message.role;
|
|
202
|
+
label.textContent=message.role==='user'?'You · '+(message.context?.slide?'Slide '+message.context.slide:'Whole project'):'Assistant';
|
|
203
|
+
body.textContent=message.text||(chat.running?'Working…':'No response');item.append(label,body);log.append(item);
|
|
204
|
+
}
|
|
205
|
+
if(atBottom)log.scrollTop=log.scrollHeight;
|
|
206
|
+
}
|
|
207
|
+
async function refreshChat(){
|
|
208
|
+
const id=++chatRequest;
|
|
209
|
+
try{const response=await fetch('/chat');if(!response.ok)throw new Error('Chat unavailable');const chat=await response.json();if(id===chatRequest)renderChat(chat);}
|
|
210
|
+
catch(error){if(id===chatRequest)byId('chat-status').textContent=error.message;}
|
|
211
|
+
}
|
|
212
|
+
async function chatAction(path,payload){
|
|
213
|
+
++chatRequest;
|
|
214
|
+
const response=await fetch(path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
|
|
215
|
+
const result=await response.json();if(!response.ok)throw new Error(result.error||'Chat request failed');
|
|
216
|
+
await refreshChat();
|
|
217
|
+
}
|
|
218
|
+
byId('chat-form').onsubmit=async event=>{
|
|
219
|
+
event.preventDefault();if(chatBusy)return;
|
|
220
|
+
const input=byId('chat-input'),message=input.value.trim();if(!message)return;
|
|
221
|
+
chatBusy=true;byId('chat-send').disabled=true;
|
|
222
|
+
try{await chatAction('/chat',{message,provider:byId('chat-provider').value,slide:state.slides.length?index:null,revision:state.revision??0});if(input.value.trim()===message)input.value='';}
|
|
223
|
+
catch(error){byId('chat-status').textContent=error.message;chatBusy=false;byId('chat-send').disabled=false;}
|
|
224
|
+
};
|
|
225
|
+
byId('chat-input').onkeydown=event=>{if(event.key==='Enter'&&(event.metaKey||event.ctrlKey)&&!event.isComposing){event.preventDefault();byId('chat-form').requestSubmit();}};
|
|
226
|
+
for(const action of ['stop','reset'])byId('chat-'+action).onclick=()=>chatAction('/chat/'+action,{}).catch(error=>{byId('chat-status').textContent=error.message;});
|
|
227
|
+
byId('toggle-chat').onclick=()=>{const hidden=document.body.classList.toggle('chat-hidden');byId('toggle-chat').setAttribute('aria-expanded',String(!hidden));resize();};
|
|
228
|
+
void refreshChat();
|
|
229
|
+
const events=new EventSource('/events');events.onmessage=event=>{if(event.data==='chat'){void refreshChat();return;}if(event.data==='ready'){delete state.revision;void refreshChat();}void refresh();};events.onerror=()=>{byId('status').textContent='Reconnecting…'};refresh();
|
|
168
230
|
<\/script></html>`;
|
|
169
231
|
//#endregion
|
|
232
|
+
//#region src/chat.ts
|
|
233
|
+
/** One shared conversation and one editing process per dev server. */
|
|
234
|
+
function createChat(entry, notify) {
|
|
235
|
+
let messages = [];
|
|
236
|
+
let provider = "claude";
|
|
237
|
+
let child;
|
|
238
|
+
let status = "Ready";
|
|
239
|
+
let stopping = false;
|
|
240
|
+
let killTimer;
|
|
241
|
+
let completion = Promise.resolve();
|
|
242
|
+
const snapshot = () => ({
|
|
243
|
+
messages,
|
|
244
|
+
provider,
|
|
245
|
+
running: !!child,
|
|
246
|
+
status
|
|
247
|
+
});
|
|
248
|
+
function stop() {
|
|
249
|
+
if (!child || stopping) return;
|
|
250
|
+
stopping = true;
|
|
251
|
+
status = "Stopping…";
|
|
252
|
+
const processToStop = child;
|
|
253
|
+
const kill = (signal) => {
|
|
254
|
+
try {
|
|
255
|
+
if (process.platform !== "win32" && processToStop.pid) process.kill(-processToStop.pid, signal);
|
|
256
|
+
else processToStop.kill(signal);
|
|
257
|
+
} catch {}
|
|
258
|
+
};
|
|
259
|
+
kill("SIGTERM");
|
|
260
|
+
killTimer = setTimeout(() => kill("SIGKILL"), 2e3);
|
|
261
|
+
notify();
|
|
262
|
+
}
|
|
263
|
+
function start(text, selected, context) {
|
|
264
|
+
provider = selected;
|
|
265
|
+
const user = {
|
|
266
|
+
role: "user",
|
|
267
|
+
text,
|
|
268
|
+
context
|
|
269
|
+
};
|
|
270
|
+
const history = messages.slice(-12).map((message) => ({
|
|
271
|
+
...message,
|
|
272
|
+
text: message.text.slice(-12e3)
|
|
273
|
+
}));
|
|
274
|
+
const prompt = `You are editing a local PowerPoint TSX project in ${dirname(resolve(entry))}.
|
|
275
|
+
Read the project's authoring instructions. Make focused patches to the TSX source, preserving unrelated work.
|
|
276
|
+
The preview rebuilds automatically on save; do not start another dev server.
|
|
277
|
+
The attached focus is context, NOT a restriction: use it for references such as "this slide". For other slides or deck-wide requests, find the relevant source or shared theme instead. Do not rewrite the whole deck for a local edit.
|
|
278
|
+
Slide numbers are 1-based and refer to the last successful preview at send time. Source may have changed since then; verify it before editing. Files are dependency candidates, not an exact slide-to-source mapping.
|
|
279
|
+
Respond in the user's language, briefly describing edits or answering their question. If permissions prevent an action, explain that instead of claiming success.
|
|
280
|
+
Recent conversation (JSON): ${JSON.stringify(history)}
|
|
281
|
+
Current request and preview context (JSON): ${JSON.stringify(user)}`;
|
|
282
|
+
messages.push(user);
|
|
283
|
+
const answer = {
|
|
284
|
+
role: "assistant",
|
|
285
|
+
text: ""
|
|
286
|
+
};
|
|
287
|
+
messages.push(answer);
|
|
288
|
+
if (messages.length > 100) messages = messages.slice(-100);
|
|
289
|
+
status = "Working…";
|
|
290
|
+
stopping = false;
|
|
291
|
+
child = spawn(selected, selected === "codex" ? [
|
|
292
|
+
"exec",
|
|
293
|
+
"--json",
|
|
294
|
+
"--sandbox",
|
|
295
|
+
"workspace-write",
|
|
296
|
+
"--skip-git-repo-check",
|
|
297
|
+
"-"
|
|
298
|
+
] : [
|
|
299
|
+
"--print",
|
|
300
|
+
"--verbose",
|
|
301
|
+
"--output-format",
|
|
302
|
+
"stream-json",
|
|
303
|
+
"--permission-mode",
|
|
304
|
+
"acceptEdits",
|
|
305
|
+
"--tools",
|
|
306
|
+
"Read,Edit,Write,Glob,Grep"
|
|
307
|
+
], {
|
|
308
|
+
cwd: dirname(resolve(entry)),
|
|
309
|
+
stdio: [
|
|
310
|
+
"pipe",
|
|
311
|
+
"pipe",
|
|
312
|
+
"pipe"
|
|
313
|
+
],
|
|
314
|
+
detached: process.platform !== "win32",
|
|
315
|
+
shell: false
|
|
316
|
+
});
|
|
317
|
+
const running = child;
|
|
318
|
+
let buffer = "", stderr = "", failure = "", sawResult = false;
|
|
319
|
+
function append(value) {
|
|
320
|
+
answer.text = (answer.text + (answer.text ? "\n\n" : "") + value).slice(-1e5);
|
|
321
|
+
notify();
|
|
322
|
+
}
|
|
323
|
+
function consume(line) {
|
|
324
|
+
if (!line.trim()) return;
|
|
325
|
+
try {
|
|
326
|
+
const event = JSON.parse(line);
|
|
327
|
+
if (selected === "codex") {
|
|
328
|
+
if (event.type === "item.completed" && event.item?.type === "agent_message") append(event.item.text ?? "");
|
|
329
|
+
if (event.type === "item.started") {
|
|
330
|
+
status = "Editing / inspecting…";
|
|
331
|
+
notify();
|
|
332
|
+
}
|
|
333
|
+
if (event.type === "turn.completed") sawResult = true;
|
|
334
|
+
if (event.type === "turn.failed" || event.type === "error") failure = event.error?.message ?? event.message ?? "Agent failed";
|
|
335
|
+
} else {
|
|
336
|
+
if (event.type === "assistant") for (const block of event.message?.content ?? []) {
|
|
337
|
+
if (block.type === "text") append(block.text);
|
|
338
|
+
if (block.type === "tool_use") {
|
|
339
|
+
status = "Using " + block.name + "…";
|
|
340
|
+
notify();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (event.type === "result") {
|
|
344
|
+
sawResult = true;
|
|
345
|
+
if (event.is_error) failure = event.result || event.errors?.join("\n") || "Agent failed";
|
|
346
|
+
else if (!answer.text && event.result) append(event.result);
|
|
347
|
+
if (event.permission_denials?.length) failure = "Some actions were denied by CLI permissions. " + (event.result ?? "");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
} catch {
|
|
351
|
+
failure = "Unexpected CLI output. Update your CLI and try again.";
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
running.stdout.setEncoding("utf8");
|
|
355
|
+
running.stdout.on("data", (chunk) => {
|
|
356
|
+
buffer += chunk;
|
|
357
|
+
let end;
|
|
358
|
+
while ((end = buffer.indexOf("\n")) !== -1) {
|
|
359
|
+
consume(buffer.slice(0, end));
|
|
360
|
+
buffer = buffer.slice(end + 1);
|
|
361
|
+
}
|
|
362
|
+
if (buffer.length > 2e6) {
|
|
363
|
+
failure = "CLI output exceeded the limit.";
|
|
364
|
+
stop();
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
running.stderr.setEncoding("utf8");
|
|
368
|
+
running.stderr.on("data", (chunk) => {
|
|
369
|
+
stderr = (stderr + chunk).slice(-8e3);
|
|
370
|
+
});
|
|
371
|
+
running.stdin.on("error", () => {});
|
|
372
|
+
running.stdin.end(prompt);
|
|
373
|
+
completion = new Promise((done) => {
|
|
374
|
+
running.on("error", (error) => {
|
|
375
|
+
failure = `Could not run ${selected}. Install it and log in from your terminal first. ${error.message}`;
|
|
376
|
+
});
|
|
377
|
+
running.on("close", (code) => {
|
|
378
|
+
if (buffer.trim()) consume(buffer);
|
|
379
|
+
clearTimeout(killTimer);
|
|
380
|
+
status = stopping ? "Stopped. Saved edits remain in the preview." : failure || (code !== 0 ? stderr || `${selected} exited with code ${code}` : !sawResult ? "CLI exited without a completed result." : "Done");
|
|
381
|
+
if (status !== "Done") append(status);
|
|
382
|
+
child = void 0;
|
|
383
|
+
notify();
|
|
384
|
+
done();
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
notify();
|
|
388
|
+
}
|
|
389
|
+
async function handle(request, response, focus) {
|
|
390
|
+
const json = (code, value) => {
|
|
391
|
+
response.writeHead(code, { "Content-Type": "application/json" });
|
|
392
|
+
response.end(JSON.stringify(value));
|
|
393
|
+
};
|
|
394
|
+
if (request.method === "GET" && request.url === "/chat") {
|
|
395
|
+
json(200, snapshot());
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (request.method !== "POST") {
|
|
399
|
+
json(405, { error: "Method not allowed" });
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (request.headers.origin !== `http://${request.headers.host}` || request.headers["content-type"]?.split(";")[0] !== "application/json") {
|
|
403
|
+
json(403, { error: "Same-origin JSON required" });
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
let body = "";
|
|
408
|
+
request.setEncoding("utf8");
|
|
409
|
+
for await (const chunk of request) {
|
|
410
|
+
body += chunk;
|
|
411
|
+
if (Buffer.byteLength(body) > 32e3) {
|
|
412
|
+
json(413, { error: "Message too large" });
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const value = JSON.parse(body);
|
|
417
|
+
if (request.url === "/chat/stop") {
|
|
418
|
+
stop();
|
|
419
|
+
json(200, snapshot());
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (child) {
|
|
423
|
+
json(409, { error: "An edit is already running" });
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (request.url === "/chat/reset") {
|
|
427
|
+
messages = [];
|
|
428
|
+
status = "Ready";
|
|
429
|
+
notify();
|
|
430
|
+
json(200, snapshot());
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (request.url !== "/chat") {
|
|
434
|
+
json(404, { error: "Not found" });
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (typeof value.message !== "string" || !value.message.trim() || value.message.length > 16e3 || !["claude", "codex"].includes(value.provider) || !(value.slide === null || Number.isInteger(value.slide) && value.slide >= 0) || !Number.isInteger(value.revision)) {
|
|
438
|
+
json(400, { error: "Invalid chat request" });
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const context = focus(value.slide, value.revision);
|
|
442
|
+
start(value.message.trim(), value.provider, context);
|
|
443
|
+
json(202, snapshot());
|
|
444
|
+
} catch (cause) {
|
|
445
|
+
json(400, { error: cause instanceof Error ? cause.message : "Invalid request" });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
handle,
|
|
450
|
+
async close() {
|
|
451
|
+
stop();
|
|
452
|
+
await completion;
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
//#endregion
|
|
170
457
|
//#region src/server.ts
|
|
171
458
|
async function serveDeck(entry, port = 4173) {
|
|
172
459
|
let latest;
|
|
@@ -178,6 +465,9 @@ async function serveDeck(entry, port = 4173) {
|
|
|
178
465
|
let pending = false;
|
|
179
466
|
let closed = false;
|
|
180
467
|
const clients = /* @__PURE__ */ new Set();
|
|
468
|
+
const chat = createChat(entry, () => {
|
|
469
|
+
for (const client of clients) client.write("data: chat\n\n");
|
|
470
|
+
});
|
|
181
471
|
const server = createServer((request, response) => {
|
|
182
472
|
const host = request.headers.host;
|
|
183
473
|
if (host !== `127.0.0.1:${actualPort}` && host !== `localhost:${actualPort}`) {
|
|
@@ -185,7 +475,21 @@ async function serveDeck(entry, port = 4173) {
|
|
|
185
475
|
return;
|
|
186
476
|
}
|
|
187
477
|
response.setHeader("Cache-Control", "no-store");
|
|
188
|
-
if (request.url === "/
|
|
478
|
+
if (request.url === "/chat" || request.url?.startsWith("/chat/")) chat.handle(request, response, (index, viewedRevision) => {
|
|
479
|
+
if (viewedRevision !== revision) throw new Error("Preview changed. Review the current slide and send again.");
|
|
480
|
+
if (index !== null && (!latest || index >= latest.slides.length)) throw new Error("Slide no longer exists.");
|
|
481
|
+
return {
|
|
482
|
+
slide: index === null ? null : index + 1,
|
|
483
|
+
count: latest?.slides.length ?? 0,
|
|
484
|
+
revision,
|
|
485
|
+
text: index === null ? "" : (latest?.slideTexts[index] ?? "").slice(0, 12e3),
|
|
486
|
+
entry: resolve(entry),
|
|
487
|
+
files: latest?.dependencies ?? [resolve(entry)],
|
|
488
|
+
buildError: error
|
|
489
|
+
};
|
|
490
|
+
});
|
|
491
|
+
else if (request.method !== "GET") response.writeHead(405).end();
|
|
492
|
+
else if (request.url === "/events") {
|
|
189
493
|
response.writeHead(200, { "Content-Type": "text/event-stream" });
|
|
190
494
|
response.write("data: ready\n\n");
|
|
191
495
|
clients.add(response);
|
|
@@ -289,6 +593,7 @@ async function serveDeck(entry, port = 4173) {
|
|
|
289
593
|
closed = true;
|
|
290
594
|
clearTimeout(timer);
|
|
291
595
|
watcher.close();
|
|
596
|
+
await chat.close();
|
|
292
597
|
await builder.close();
|
|
293
598
|
for (const client of clients) client.end();
|
|
294
599
|
await new Promise((done, reject) => server.close((cause) => cause ? reject(cause) : done()));
|
package/dist/cli.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/page.ts","../src/server.ts","../src/cli.ts"],"sourcesContent":["export const page = `<!doctype html>\n<html lang=\"en\">\n<meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width\">\n<title>Office Kit — PowerPoint preview</title>\n<style>\n*{box-sizing:border-box}\nbody{margin:0;height:100dvh;overflow:hidden;background:#e9ecf1;color:#202735;font:13px system-ui;display:grid;grid-template-rows:60px minmax(0,1fr) 42px}\nbutton,a,select{font:inherit;color:inherit}button,select,.download{border:1px solid #d4d9e2;border-radius:6px;background:#fff;padding:7px 12px;text-decoration:none;cursor:pointer}\nbutton:hover:not(:disabled),.download:hover{background:#f0f3f9}button:disabled{opacity:.4;cursor:default}\n:focus-visible{outline:2px solid #4967dd;outline-offset:3px}\nheader{display:flex;align-items:center;gap:16px;padding:0 20px;background:#fff;border-bottom:1px solid #d4d9e2;min-width:0}\n.brand{font-weight:700;font-size:17px;white-space:nowrap}.badge{font-size:11px;color:#616b7c;background:#f1f3f7;border-radius:4px;padding:3px 6px}\n#status{flex:1;color:#667085;min-width:0}#present{background:#293c73;color:#fff;border-color:#293c73}\n.workspace{display:grid;grid-template-columns:224px minmax(0,1fr);min-height:0}\n.filmstrip{background:#f7f8fa;border-right:1px solid #d4d9e2;overflow:auto;overscroll-behavior:contain;padding:16px 12px}\n.filmstrip h2{margin:0 0 12px 26px;text-transform:uppercase;letter-spacing:.1em;font-size:10px;color:#7c8596;font-weight:600}\n#thumbnails{display:flex;flex-direction:column;gap:12px;margin:0;padding:0;list-style:none}\n.thumbnail{display:flex;align-items:flex-start;gap:8px;width:100%;padding:4px 3px;border:0;background:transparent;border-radius:5px;text-align:left}\n.slide-number{width:18px;flex:none;text-align:right;font-size:11px;color:#737d8e;padding-top:5px}\n.thumbnail img{display:block;min-width:0;width:calc(100% - 26px);background:white;box-shadow:0 1px 4px #19212d18;border:2px solid transparent;border-radius:3px;aspect-ratio:var(--slide-ratio,16/9);object-fit:contain}\n.thumbnail[aria-current=\"true\"]{background:#e7edff}.thumbnail[aria-current=\"true\"] img{border-color:#4967dd}.thumbnail[aria-current=\"true\"] .slide-number{color:#3654c1;font-weight:700}\nmain{min-width:0;min-height:0;display:flex;flex-direction:column}\n#error{flex:none;max-height:30%;overflow:auto;background:#fff0ef;color:#922e25;margin:0;padding:16px 20px;white-space:pre-wrap;border-bottom:1px solid #e8bbb7}\n#error[hidden]{display:none}\n#stage{flex:1;min-height:0;overflow:auto;display:flex;padding:32px;overscroll-behavior:contain}\n#slide{position:relative;flex:none;margin:auto;background:white;box-shadow:0 3px 24px #19212d20;overflow:hidden}\n#slide{user-select:text;-webkit-user-select:text}\n#empty{margin:auto;color:#737d8e}\nfooter{display:flex;align-items:center;gap:14px;padding:0 16px;background:#fff;border-top:1px solid #d4d9e2;font-size:12px}\n#count{min-width:90px}footer .hint{flex:1;color:#737d8e}footer button{padding:3px 10px}footer select{padding:3px 8px}\n#presentation-controls{display:none}\nbody.presenting{grid-template-rows:minmax(0,1fr);background:#111}\n.presenting header,.presenting footer,.presenting .filmstrip{display:none}\n.presenting .workspace{grid-template-columns:minmax(0,1fr)}.presenting #stage{padding:0;background:#111}.presenting #slide{box-shadow:none}\n.presenting #presentation-controls{display:flex;position:fixed;bottom:16px;left:50%;transform:translateX(-50%);align-items:center;gap:12px;background:#202735e8;color:white;padding:6px;border-radius:8px;opacity:0;transition:opacity .15s}\n.presenting #presentation-controls:hover,.presenting #presentation-controls:focus-within{opacity:1}\n#presentation-controls button{background:transparent;color:white;border-color:#5d6575}\n@media(max-width:700px){.workspace{grid-template-columns:140px minmax(0,1fr)}.filmstrip{padding:12px 5px}header{padding:0 12px;gap:10px}.badge,footer .hint{display:none}#stage{padding:16px}footer{gap:8px}#status{font-size:11px}.download{padding:7px 8px}}\n@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}\n</style>\n<header><span class=\"brand\">Office Kit</span><span class=\"badge\">Preview</span><span id=\"status\" role=\"status\">Building…</span><button id=\"present\" disabled>Present</button><a class=\"download\" href=\"/deck.pptx\">Download PPTX</a></header>\n<div class=\"workspace\">\n<nav class=\"filmstrip\" aria-label=\"Slides\"><h2>Slides</h2><ol id=\"thumbnails\"></ol></nav>\n<main aria-label=\"Slide viewer\"><pre id=\"error\" role=\"alert\" hidden></pre><div id=\"stage\" tabindex=\"-1\"><div id=\"empty\">Waiting for slides…</div><div id=\"slide\" hidden></div></div></main>\n</div>\n<footer><span id=\"count\" aria-live=\"polite\">No slides</span><span class=\"hint\">Changes appear automatically · Text can be selected and copied</span><button id=\"prev\" aria-label=\"Previous slide\" disabled>‹</button><button id=\"next\" aria-label=\"Next slide\" disabled>›</button><label for=\"zoom\">Zoom</label><select id=\"zoom\"><option value=\"fit\">Fit</option><option value=\"0.5\">50%</option><option value=\"0.75\">75%</option><option value=\"1\">100%</option><option value=\"1.25\">125%</option><option value=\"1.5\">150%</option><option value=\"2\">200%</option></select></footer>\n<div id=\"presentation-controls\"><button id=\"present-prev\" aria-label=\"Previous slide\">‹</button><span id=\"present-count\"></span><button id=\"present-next\" aria-label=\"Next slide\">›</button><button id=\"exit-present\">Exit · Esc</button></div>\n<script>\nlet state={slides:[],error:null,aspectRatio:16/9},index=0,urls=[],presenting=false;\nlet displayedSvg;\nconst byId=id=>document.getElementById(id);\nconst stage=byId('stage'),slide=byId('slide'),thumbnails=byId('thumbnails');\n// The slide lives in a shadow root rather than a sandboxed iframe so its text\n// (XHTML inside <foreignObject>) can be selected and copied while the deck's\n// SVG stays out of the viewer's own DOM and CSS. Keyboard events still reach the\n// document, so arrow-key navigation keeps working after clicking into a slide.\nconst canvas=slide.attachShadow({mode:'open'});\nfunction resize(){\n if(!state.slides.length)return;\n const style=getComputedStyle(stage);\n const width=Math.max(1,stage.clientWidth-parseFloat(style.paddingLeft)-parseFloat(style.paddingRight));\n const height=Math.max(1,stage.clientHeight-parseFloat(style.paddingTop)-parseFloat(style.paddingBottom));\n const ratio=state.aspectRatio;\n const zoom=byId('zoom').value;\n const slideWidth=presenting||zoom==='fit'?Math.min(width,height*ratio):1280*Number(zoom);\n slide.style.width=slideWidth+'px';slide.style.height=slideWidth/ratio+'px';\n}\nfunction selectSlide(next,focusThumbnail=false,reveal=true){\n index=Math.max(0,Math.min(next,state.slides.length-1));\n const count=state.slides.length?'Slide '+(index+1)+' of '+state.slides.length:'No slides';\n byId('count').textContent=count;byId('present-count').textContent=count;\n for(const id of ['prev','present-prev'])byId(id).disabled=index===0;\n for(const id of ['next','present-next'])byId(id).disabled=index>=state.slides.length-1;\n byId('present').disabled=!state.slides.length;\n byId('zoom').disabled=!state.slides.length;\n slide.hidden=!state.slides.length;byId('empty').hidden=!!state.slides.length;\n const svg=state.slides[index];\n if(svg!==displayedSvg){\n displayedSvg=svg;\n canvas.innerHTML=svg?'<style>svg{display:block;width:100%;height:100%}</style>'+svg:'';\n }\n slide.setAttribute('aria-label','Slide '+(index+1));\n for(const [position,item] of Array.from(thumbnails.children).entries()){\n const button=item.firstElementChild,selected=position===index;\n button.setAttribute('aria-current',String(selected));button.tabIndex=selected?0:-1;\n if(selected){if(reveal)button.scrollIntoView({block:'nearest'});if(focusThumbnail)button.focus({preventScroll:true});}\n }\n resize();\n}\nfunction update(updated){\n const focusedThumbnail=thumbnails.contains(document.activeElement);\n const previous=state;\n state=updated;\n byId('status').textContent=state.error?'Build failed · showing last successful output':state.building?'Updating…':state.slides.length+' slides · Live';\n byId('error').textContent=state.error||'';byId('error').hidden=!state.error;\n document.documentElement.style.setProperty('--slide-ratio',String(state.aspectRatio));\n for(let i=state.slides.length;i<urls.length;i++){\n URL.revokeObjectURL(urls[i]);thumbnails.lastElementChild.remove();\n }\n urls.length=state.slides.length;\n state.slides.forEach((svg,i)=>{\n if(svg===previous.slides[i])return;\n const oldUrl=urls[i];\n urls[i]=URL.createObjectURL(new Blob([svg],{type:'image/svg+xml'}));\n let item=thumbnails.children[i];\n if(!item){\n item=document.createElement('li');\n const button=document.createElement('button'),number=document.createElement('span'),image=document.createElement('img');\n button.className='thumbnail';button.setAttribute('aria-label','Slide '+(i+1));button.onclick=()=>selectSlide(i,true);\n number.className='slide-number';number.textContent=String(i+1);\n image.alt='';image.draggable=false;image.loading='lazy';\n button.append(number,image);item.append(button);thumbnails.append(item);\n }\n item.querySelector('img').src=urls[i];\n if(oldUrl)URL.revokeObjectURL(oldUrl);\n });\n selectSlide(index,focusedThumbnail,false);\n if(!state.slides.length&&presenting)void exitPresentation();\n}\nfunction setPresenting(value){\n presenting=value;document.body.classList.toggle('presenting',value);resize();\n if(value)stage.focus();else{\n byId('present').focus();\n thumbnails.children[index]?.firstElementChild.scrollIntoView({block:'nearest'});\n }\n}\nasync function exitPresentation(){\n setPresenting(false);\n if(document.fullscreenElement)await document.exitFullscreen();\n}\nbyId('present').onclick=async()=>{\n setPresenting(true);\n try{await document.documentElement.requestFullscreen();}\n catch{byId('exit-present').textContent='Exit view · Esc';}\n};\nbyId('exit-present').onclick=exitPresentation;\ndocument.addEventListener('fullscreenchange',()=>{if(!document.fullscreenElement&&presenting)setPresenting(false);});\nfor(const id of ['prev','present-prev'])byId(id).onclick=()=>selectSlide(index-1);\nfor(const id of ['next','present-next'])byId(id).onclick=()=>selectSlide(index+1);\nbyId('zoom').onchange=resize;\nstage.onclick=()=>{if(presenting&&!getSelection().toString())selectSlide(index+1);};\ndocument.addEventListener('keydown',event=>{\n if(event.key==='Escape'&&presenting){event.preventDefault();void exitPresentation();return;}\n if(event.altKey||event.ctrlKey||event.metaKey||event.target.closest('select,input,textarea,[contenteditable]'))return;\n let next=index;\n if(['ArrowLeft','ArrowUp','PageUp'].includes(event.key))next--;\n else if(['ArrowRight','ArrowDown','PageDown'].includes(event.key))next++;\n else if(event.key==='Home')next=0;\n else if(event.key==='End')next=state.slides.length-1;\n else if(event.key===' '&&presenting&&!event.target.closest('button,a'))next+=event.shiftKey?-1:1;\n else return;\n event.preventDefault();selectSlide(next,thumbnails.contains(document.activeElement));\n});\nnew ResizeObserver(resize).observe(stage);\nlet refreshId=0;\nasync function refresh(){\n const id=++refreshId;\n try{const response=await fetch('/state'+(state.revision===undefined?'':'?since='+state.revision));if(!response.ok)throw new Error('Preview unavailable');const updated=await response.json();if(id===refreshId){if(updated.changes){updated.slides=state.slides.slice(0,updated.count);updated.slides.length=updated.count;for(const [position,svg] of Object.entries(updated.changes))updated.slides[Number(position)]=svg;}update(updated);}}\n catch{if(id===refreshId)byId('status').textContent='Reconnecting…';}\n}\nconst events=new EventSource('/events');events.onmessage=event=>{if(event.data==='ready')delete state.revision;void refresh();};events.onerror=()=>{byId('status').textContent='Reconnecting…'};refresh();\n</script></html>`;\n","import { createServer, type ServerResponse } from 'node:http';\nimport { watch } from 'node:fs';\nimport { dirname, resolve, sep } from 'node:path';\nimport type { BuildResult } from './build.ts';\nimport { createDeckBuilder } from './build-runner.ts';\nimport { page } from './page.ts';\n\nexport async function serveDeck(entry: string, port = 4173) {\n let latest: BuildResult | undefined;\n let error: string | null = null;\n let building = false;\n let revision = 0;\n let generation = 0;\n let patch: Record<number, string> = {};\n let pending = false;\n let closed = false;\n const clients = new Set<ServerResponse>();\n const server = createServer((request, response) => {\n const host = request.headers.host;\n if (host !== `127.0.0.1:${actualPort}` && host !== `localhost:${actualPort}`) {\n response.writeHead(403).end();\n return;\n }\n response.setHeader('Cache-Control', 'no-store');\n if (request.url === '/events') {\n response.writeHead(200, { 'Content-Type': 'text/event-stream' });\n response.write('data: ready\\n\\n');\n clients.add(response);\n request.on('close', () => clients.delete(response));\n } else if (request.url === '/state' || request.url?.startsWith('/state?')) {\n const since = new URL(request.url, 'http://localhost').searchParams.get('since');\n const incremental =\n since !== null && (since === String(revision) || since === String(revision - 1));\n response.writeHead(200, { 'Content-Type': 'application/json' });\n response.end(\n JSON.stringify({\n revision,\n building,\n ...(incremental\n ? {\n changes: since === String(revision) ? {} : patch,\n count: latest?.slides.length ?? 0,\n }\n : { slides: latest?.slides ?? [] }),\n aspectRatio: latest?.aspectRatio ?? 16 / 9,\n error,\n diagnostics: latest?.diagnostics ?? [],\n }),\n );\n } else if (request.url === '/deck.pptx' && latest) {\n response.writeHead(200, {\n 'Content-Type': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n 'Content-Disposition': 'attachment; filename=\"deck.pptx\"',\n });\n response.end(latest.bytes);\n } else if (request.url === '/') {\n response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\n response.end(page);\n } else {\n response.writeHead(404).end();\n }\n });\n async function rebuild() {\n if (closed) return;\n if (building) {\n pending = true;\n return;\n }\n building = true;\n const started = generation;\n for (const client of clients) client.write('data: building\\n\\n');\n try {\n const result = await builder.build();\n if (started === generation && !closed) {\n patch = {};\n result.slides.forEach((svg, index) => {\n if (svg !== latest?.slides[index]) patch[index] = svg;\n });\n latest = result;\n revision++;\n error = null;\n }\n } catch (cause) {\n if (started === generation)\n error = cause instanceof Error ? (cause.stack ?? cause.message) : String(cause);\n } finally {\n building = false;\n for (const client of clients) client.write('data: updated\\n\\n');\n if (pending && !timer) {\n pending = false;\n void rebuild();\n }\n }\n }\n let timer: ReturnType<typeof setTimeout> | undefined;\n const root = dirname(resolve(entry));\n const watcher = watch(root, { recursive: true }, (_, filename) => {\n if (\n !filename ||\n filename\n .split(sep)\n .some((part) => ['node_modules', '.git', 'dist', '.office-kit'].includes(part))\n )\n return;\n if (!/\\.([cm]?[jt]sx?|json|pptx|png|jpe?g|gif|bmp|tiff?|emf|wmf|svg)$/i.test(filename)) return;\n generation++;\n void builder.cancel();\n clearTimeout(timer);\n timer = setTimeout(() => {\n timer = undefined;\n pending = false;\n void rebuild();\n }, 30);\n });\n const builder = createDeckBuilder(entry);\n let actualPort = port;\n try {\n await new Promise<void>((resolveListen, reject) => {\n server.once('error', reject);\n server.listen(port, '127.0.0.1', () => {\n server.off('error', reject);\n resolveListen();\n });\n });\n } catch (cause) {\n watcher.close();\n await builder.close();\n throw cause;\n }\n const address = server.address();\n if (address && typeof address !== 'string') actualPort = address.port;\n await rebuild();\n return {\n url: `http://127.0.0.1:${actualPort}`,\n async close() {\n closed = true;\n clearTimeout(timer);\n watcher.close();\n await builder.close();\n for (const client of clients) client.end();\n await new Promise<void>((done, reject) =>\n server.close((cause) => (cause ? reject(cause) : done())),\n );\n },\n };\n}\n","#!/usr/bin/env node\nimport { resolve } from 'node:path';\nimport { exportDeck } from './index.ts';\nimport { serveDeck } from './server.ts';\nimport { initProject } from './init.ts';\nimport { inspectTemplate } from './inspect.ts';\n\nconst usage = `Usage: office-pptx init <new-directory>\n office-pptx dev <deck.tsx> [--port 4173]\n office-pptx build <deck.tsx> [--out deck.pptx]\n office-pptx inspect <template.pptx>`;\nconst [command, entry, ...args] = process.argv.slice(2);\ntry {\n if (command === '--help' || command === '-h') {\n console.log(usage);\n } else {\n if (!entry) throw new Error(usage);\n const option = command === 'build' ? '--out' : command === 'dev' ? '--port' : undefined;\n if (args.length && (args.length !== 2 || !option || args[0] !== option || !args[1])) {\n throw new Error(`Unexpected arguments: ${args.join(' ')}\\n${usage}`);\n }\n if (command === 'init') {\n const directory = await initProject(entry);\n console.log(`Created ${directory}\\nRun npm install, then npm run dev inside that directory.`);\n } else if (command === 'inspect') {\n console.log(JSON.stringify(await inspectTemplate(entry), null, 2));\n } else if (command === 'build') {\n const output = resolve(args[1] ?? 'deck.pptx');\n const result = await exportDeck(entry, output);\n console.log(`Wrote ${output} (${result.slides.length} slides)`);\n } else if (command === 'dev') {\n const port = args[1] === undefined ? 4173 : Number(args[1]);\n if (!Number.isInteger(port) || port < 0 || port > 65535)\n throw new Error('Port must be an integer between 0 and 65535.');\n const server = await serveDeck(entry, port);\n console.log(`Preview: ${server.url}`);\n const stop = () => {\n void server.close().then(() => process.exit(0));\n };\n process.once('SIGINT', stop);\n process.once('SIGTERM', stop);\n } else {\n throw new Error(usage);\n }\n }\n} catch (cause) {\n console.error(cause instanceof Error ? cause.message : cause);\n process.exitCode = 1;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/page.ts","../src/chat.ts","../src/server.ts","../src/cli.ts"],"sourcesContent":["export const page = `<!doctype html>\n<html lang=\"en\">\n<meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width\">\n<title>Office Kit — PowerPoint preview</title>\n<style>\n*{box-sizing:border-box}\nbody{margin:0;height:100dvh;overflow:hidden;background:#e9ecf1;color:#202735;font:13px system-ui;display:grid;grid-template-rows:60px minmax(0,1fr) 42px}\nbutton,a,select{font:inherit;color:inherit}button,select,.download{border:1px solid #d4d9e2;border-radius:6px;background:#fff;padding:7px 12px;text-decoration:none;cursor:pointer}\nbutton:hover:not(:disabled),.download:hover{background:#f0f3f9}button:disabled{opacity:.4;cursor:default}\n:focus-visible{outline:2px solid #4967dd;outline-offset:3px}\nheader{display:flex;align-items:center;gap:16px;padding:0 20px;background:#fff;border-bottom:1px solid #d4d9e2;min-width:0}\n.brand{font-weight:700;font-size:17px;white-space:nowrap}.badge{font-size:11px;color:#616b7c;background:#f1f3f7;border-radius:4px;padding:3px 6px}\n#status{flex:1;color:#667085;min-width:0}#present{background:#293c73;color:#fff;border-color:#293c73}\n.workspace{display:grid;grid-template-columns:190px minmax(0,1fr) 340px;min-height:0}\n.filmstrip{background:#f7f8fa;border-right:1px solid #d4d9e2;overflow:auto;overscroll-behavior:contain;padding:16px 12px}\n.filmstrip h2{margin:0 0 12px 26px;text-transform:uppercase;letter-spacing:.1em;font-size:10px;color:#7c8596;font-weight:600}\n#thumbnails{display:flex;flex-direction:column;gap:12px;margin:0;padding:0;list-style:none}\n.thumbnail{display:flex;align-items:flex-start;gap:8px;width:100%;padding:4px 3px;border:0;background:transparent;border-radius:5px;text-align:left}\n.slide-number{width:18px;flex:none;text-align:right;font-size:11px;color:#737d8e;padding-top:5px}\n.thumbnail img{display:block;min-width:0;width:calc(100% - 26px);background:white;box-shadow:0 1px 4px #19212d18;border:2px solid transparent;border-radius:3px;aspect-ratio:var(--slide-ratio,16/9);object-fit:contain}\n.thumbnail[aria-current=\"true\"]{background:#e7edff}.thumbnail[aria-current=\"true\"] img{border-color:#4967dd}.thumbnail[aria-current=\"true\"] .slide-number{color:#3654c1;font-weight:700}\nmain{min-width:0;min-height:0;display:flex;flex-direction:column}\n#error{flex:none;max-height:30%;overflow:auto;background:#fff0ef;color:#922e25;margin:0;padding:16px 20px;white-space:pre-wrap;border-bottom:1px solid #e8bbb7}\n#error[hidden]{display:none}\n#stage{flex:1;min-height:0;overflow:auto;display:flex;padding:32px;overscroll-behavior:contain}\n#slide{position:relative;flex:none;margin:auto;background:white;box-shadow:0 3px 24px #19212d20;overflow:hidden}\n#slide{user-select:text;-webkit-user-select:text}\n#empty{margin:auto;color:#737d8e}\nfooter{display:flex;align-items:center;gap:14px;padding:0 16px;background:#fff;border-top:1px solid #d4d9e2;font-size:12px}\n#count{min-width:90px}footer .hint{flex:1;color:#737d8e}footer button{padding:3px 10px}footer select{padding:3px 8px}\n#presentation-controls{display:none}\nbody.presenting{grid-template-rows:minmax(0,1fr);background:#111}\n.presenting header,.presenting footer,.presenting .filmstrip,.presenting #chat{display:none}\n.presenting .workspace{grid-template-columns:minmax(0,1fr)}.presenting #stage{padding:0;background:#111}.presenting #slide{box-shadow:none}\n.presenting #presentation-controls{display:flex;position:fixed;bottom:16px;left:50%;transform:translateX(-50%);align-items:center;gap:12px;background:#202735e8;color:white;padding:6px;border-radius:8px;opacity:0;transition:opacity .15s}\n.presenting #presentation-controls:hover,.presenting #presentation-controls:focus-within{opacity:1}\n#presentation-controls button{background:transparent;color:white;border-color:#5d6575}\n@media(max-width:700px){.workspace{grid-template-columns:140px minmax(0,1fr)}.filmstrip{padding:12px 5px}header{padding:0 12px;gap:10px}.badge,footer .hint{display:none}#stage{padding:16px}footer{gap:8px}#status{font-size:11px}.download{padding:7px 8px}}\n#chat{min-height:0;display:flex;flex-direction:column;background:#fff;border-left:1px solid #d4d9e2}\n.chat-heading{display:flex;align-items:center;gap:8px;padding:16px;border-bottom:1px solid #e6e9ef}.chat-heading strong{flex:1}.chat-heading button{padding:5px 8px}\n#messages{flex:1;overflow:auto;padding:16px;display:flex;flex-direction:column;gap:16px;overscroll-behavior:contain}\n.chat-message{white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.6}.chat-message.user{background:#eef2ff;border-radius:8px;padding:10px 12px}.chat-message small{display:block;color:#697386;font-size:11px;margin-bottom:4px}.chat-intro{color:#737d8e;line-height:1.7}\n#chat-form{padding:14px;border-top:1px solid #e6e9ef}#chat-context{font-size:11px;color:#4967dd;margin-bottom:8px}#chat-input{font:inherit;resize:vertical;min-height:90px;max-height:200px;width:100%;padding:10px;border:1px solid #d4d9e2;border-radius:8px} .chat-actions{display:flex;gap:8px;margin-top:8px;align-items:center}.chat-actions select{min-width:0;flex:1}#chat-send{background:#293c73;color:white}#chat-status{padding:0 14px 12px;color:#667085;font-size:11px;white-space:pre-wrap;max-height:90px;overflow:auto}\nbody.chat-hidden .workspace{grid-template-columns:190px minmax(0,1fr)}body.chat-hidden #chat{display:none}\nbody.presenting .workspace{grid-template-columns:minmax(0,1fr)}\n@media(max-width:1000px){.workspace{grid-template-columns:120px minmax(0,1fr) 300px}.filmstrip{padding:12px 5px}#stage{padding:16px}}\n@media(max-width:700px){.workspace{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(160px,1fr) minmax(200px,1fr)}.filmstrip{display:none}#chat{border-top:1px solid #d4d9e2}.chat-heading{padding:8px 12px}#chat-form{padding:8px 12px}#chat-input{min-height:50px}body.chat-hidden .workspace,.presenting .workspace{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr)}.brand{font-size:14px}header{gap:6px}.download{font-size:11px}}\n@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}\n</style>\n<header><span class=\"brand\">Office Kit</span><span class=\"badge\">Preview</span><span id=\"status\" role=\"status\">Building…</span><button id=\"toggle-chat\" aria-expanded=\"true\" aria-controls=\"chat\">Chat</button><button id=\"present\" disabled>Present</button><a class=\"download\" href=\"/deck.pptx\">Download PPTX</a></header>\n<div class=\"workspace\">\n<nav class=\"filmstrip\" aria-label=\"Slides\"><h2>Slides</h2><ol id=\"thumbnails\"></ol></nav>\n<main aria-label=\"Slide viewer\"><pre id=\"error\" role=\"alert\" hidden></pre><div id=\"stage\" tabindex=\"-1\"><div id=\"empty\">Waiting for slides…</div><div id=\"slide\" hidden></div></div></main>\n<aside id=\"chat\" aria-label=\"Slide chat\">\n<div class=\"chat-heading\"><strong>Edit with AI</strong><button id=\"chat-reset\">New chat</button></div>\n<div id=\"messages\" role=\"log\" aria-label=\"Conversation\"><p class=\"chat-intro\">Describe what you want to change. The current slide is included automatically. You can also ask about other slides or the whole deck.<br><br>Uses your locally installed Claude Code or Codex. Sign in through its CLI first. Sending a message allows it to edit this project.</p></div>\n<form id=\"chat-form\"><div id=\"chat-context\">No slide selected</div><label for=\"chat-input\" class=\"chat-intro\">What would you like to change?</label><textarea id=\"chat-input\" maxlength=\"16000\" placeholder=\"Make this headline shorter…\" required></textarea><div class=\"chat-actions\"><select id=\"chat-provider\" aria-label=\"AI provider\"><option value=\"claude\">Claude Code</option><option value=\"codex\">Codex</option></select><button id=\"chat-stop\" type=\"button\" hidden>Stop</button><button id=\"chat-send\" type=\"submit\">Send</button></div></form>\n<div id=\"chat-status\" role=\"status\">Ready</div>\n</aside>\n</div>\n<footer><span id=\"count\" aria-live=\"polite\">No slides</span><span class=\"hint\">Changes appear automatically · Text can be selected and copied</span><button id=\"prev\" aria-label=\"Previous slide\" disabled>‹</button><button id=\"next\" aria-label=\"Next slide\" disabled>›</button><label for=\"zoom\">Zoom</label><select id=\"zoom\"><option value=\"fit\">Fit</option><option value=\"0.5\">50%</option><option value=\"0.75\">75%</option><option value=\"1\">100%</option><option value=\"1.25\">125%</option><option value=\"1.5\">150%</option><option value=\"2\">200%</option></select></footer>\n<div id=\"presentation-controls\"><button id=\"present-prev\" aria-label=\"Previous slide\">‹</button><span id=\"present-count\"></span><button id=\"present-next\" aria-label=\"Next slide\">›</button><button id=\"exit-present\">Exit · Esc</button></div>\n<script>\nlet state={slides:[],error:null,aspectRatio:16/9},index=0,urls=[],presenting=false;\nlet displayedSvg;\nconst byId=id=>document.getElementById(id);\nconst stage=byId('stage'),slide=byId('slide'),thumbnails=byId('thumbnails');\n// The slide lives in a shadow root rather than a sandboxed iframe so its text\n// (XHTML inside <foreignObject>) can be selected and copied while the deck's\n// SVG stays out of the viewer's own DOM and CSS. Keyboard events still reach the\n// document, so arrow-key navigation keeps working after clicking into a slide.\nconst canvas=slide.attachShadow({mode:'open'});\nfunction resize(){\n if(!state.slides.length)return;\n const style=getComputedStyle(stage);\n const width=Math.max(1,stage.clientWidth-parseFloat(style.paddingLeft)-parseFloat(style.paddingRight));\n const height=Math.max(1,stage.clientHeight-parseFloat(style.paddingTop)-parseFloat(style.paddingBottom));\n const ratio=state.aspectRatio;\n const zoom=byId('zoom').value;\n const slideWidth=presenting||zoom==='fit'?Math.min(width,height*ratio):1280*Number(zoom);\n slide.style.width=slideWidth+'px';slide.style.height=slideWidth/ratio+'px';\n}\nfunction selectSlide(next,focusThumbnail=false,reveal=true){\n index=Math.max(0,Math.min(next,state.slides.length-1));\n const count=state.slides.length?'Slide '+(index+1)+' of '+state.slides.length:'No slides';\n byId('chat-context').textContent=state.slides.length?'Context: '+count:'Context: whole project';\n byId('count').textContent=count;byId('present-count').textContent=count;\n for(const id of ['prev','present-prev'])byId(id).disabled=index===0;\n for(const id of ['next','present-next'])byId(id).disabled=index>=state.slides.length-1;\n byId('present').disabled=!state.slides.length;\n byId('zoom').disabled=!state.slides.length;\n slide.hidden=!state.slides.length;byId('empty').hidden=!!state.slides.length;\n const svg=state.slides[index];\n if(svg!==displayedSvg){\n displayedSvg=svg;\n canvas.innerHTML=svg?'<style>svg{display:block;width:100%;height:100%}</style>'+svg:'';\n }\n slide.setAttribute('aria-label','Slide '+(index+1));\n for(const [position,item] of Array.from(thumbnails.children).entries()){\n const button=item.firstElementChild,selected=position===index;\n button.setAttribute('aria-current',String(selected));button.tabIndex=selected?0:-1;\n if(selected){if(reveal)button.scrollIntoView({block:'nearest'});if(focusThumbnail)button.focus({preventScroll:true});}\n }\n resize();\n}\nfunction update(updated){\n const focusedThumbnail=thumbnails.contains(document.activeElement);\n const previous=state;\n state=updated;\n byId('status').textContent=state.error?'Build failed · showing last successful output':state.building?'Updating…':state.slides.length+' slides · Live';\n byId('error').textContent=state.error||'';byId('error').hidden=!state.error;\n document.documentElement.style.setProperty('--slide-ratio',String(state.aspectRatio));\n for(let i=state.slides.length;i<urls.length;i++){\n URL.revokeObjectURL(urls[i]);thumbnails.lastElementChild.remove();\n }\n urls.length=state.slides.length;\n state.slides.forEach((svg,i)=>{\n if(svg===previous.slides[i])return;\n const oldUrl=urls[i];\n urls[i]=URL.createObjectURL(new Blob([svg],{type:'image/svg+xml'}));\n let item=thumbnails.children[i];\n if(!item){\n item=document.createElement('li');\n const button=document.createElement('button'),number=document.createElement('span'),image=document.createElement('img');\n button.className='thumbnail';button.setAttribute('aria-label','Slide '+(i+1));button.onclick=()=>selectSlide(i,true);\n number.className='slide-number';number.textContent=String(i+1);\n image.alt='';image.draggable=false;image.loading='lazy';\n button.append(number,image);item.append(button);thumbnails.append(item);\n }\n item.querySelector('img').src=urls[i];\n if(oldUrl)URL.revokeObjectURL(oldUrl);\n });\n selectSlide(index,focusedThumbnail,false);\n if(!state.slides.length&&presenting)void exitPresentation();\n}\nfunction setPresenting(value){\n presenting=value;document.body.classList.toggle('presenting',value);resize();\n if(value)stage.focus();else{\n byId('present').focus();\n thumbnails.children[index]?.firstElementChild.scrollIntoView({block:'nearest'});\n }\n}\nasync function exitPresentation(){\n setPresenting(false);\n if(document.fullscreenElement)await document.exitFullscreen();\n}\nbyId('present').onclick=async()=>{\n setPresenting(true);\n try{await document.documentElement.requestFullscreen();}\n catch{byId('exit-present').textContent='Exit view · Esc';}\n};\nbyId('exit-present').onclick=exitPresentation;\ndocument.addEventListener('fullscreenchange',()=>{if(!document.fullscreenElement&&presenting)setPresenting(false);});\nfor(const id of ['prev','present-prev'])byId(id).onclick=()=>selectSlide(index-1);\nfor(const id of ['next','present-next'])byId(id).onclick=()=>selectSlide(index+1);\nbyId('zoom').onchange=resize;\nstage.onclick=()=>{if(presenting&&!getSelection().toString())selectSlide(index+1);};\ndocument.addEventListener('keydown',event=>{\n if(event.key==='Escape'&&presenting){event.preventDefault();void exitPresentation();return;}\n if(event.altKey||event.ctrlKey||event.metaKey||event.target.closest('select,input,textarea,[contenteditable]'))return;\n let next=index;\n if(['ArrowLeft','ArrowUp','PageUp'].includes(event.key))next--;\n else if(['ArrowRight','ArrowDown','PageDown'].includes(event.key))next++;\n else if(event.key==='Home')next=0;\n else if(event.key==='End')next=state.slides.length-1;\n else if(event.key===' '&&presenting&&!event.target.closest('button,a'))next+=event.shiftKey?-1:1;\n else return;\n event.preventDefault();selectSlide(next,thumbnails.contains(document.activeElement));\n});\nnew ResizeObserver(resize).observe(stage);\nlet refreshId=0;\nasync function refresh(){\n const id=++refreshId;\n try{const response=await fetch('/state'+(state.revision===undefined?'':'?since='+state.revision));if(!response.ok)throw new Error('Preview unavailable');const updated=await response.json();if(id===refreshId){if(updated.changes){updated.slides=state.slides.slice(0,updated.count);updated.slides.length=updated.count;for(const [position,svg] of Object.entries(updated.changes))updated.slides[Number(position)]=svg;}update(updated);}}\n catch{if(id===refreshId)byId('status').textContent='Reconnecting…';}\n}\nlet chatRequest=0,chatBusy=false,lastMessages='';\nfunction renderChat(chat){\n chatBusy=chat.running;\n byId('chat-send').disabled=chat.running;\n byId('chat-reset').disabled=chat.running;\n byId('chat-provider').disabled=chat.running;\n byId('chat-stop').hidden=!chat.running;\n byId('chat-status').textContent=chat.status;\n if(chat.messages.length)byId('chat-provider').value=chat.provider;\n const serialized=JSON.stringify(chat.messages);\n if(serialized===lastMessages)return;\n lastMessages=serialized;\n const log=byId('messages'),atBottom=log.scrollHeight-log.scrollTop-log.clientHeight<60;\n log.replaceChildren();\n if(!chat.messages.length){const intro=document.createElement('p');intro.className='chat-intro';intro.textContent='Ask for a focused slide edit or a change across the deck. Uses your locally installed CLI and its login. Sending allows edits to this project.';log.append(intro);}\n for(const message of chat.messages){\n const item=document.createElement('div'),label=document.createElement('small'),body=document.createElement('div');\n item.className='chat-message '+message.role;\n label.textContent=message.role==='user'?'You · '+(message.context?.slide?'Slide '+message.context.slide:'Whole project'):'Assistant';\n body.textContent=message.text||(chat.running?'Working…':'No response');item.append(label,body);log.append(item);\n }\n if(atBottom)log.scrollTop=log.scrollHeight;\n}\nasync function refreshChat(){\n const id=++chatRequest;\n try{const response=await fetch('/chat');if(!response.ok)throw new Error('Chat unavailable');const chat=await response.json();if(id===chatRequest)renderChat(chat);}\n catch(error){if(id===chatRequest)byId('chat-status').textContent=error.message;}\n}\nasync function chatAction(path,payload){\n ++chatRequest;\n const response=await fetch(path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});\n const result=await response.json();if(!response.ok)throw new Error(result.error||'Chat request failed');\n await refreshChat();\n}\nbyId('chat-form').onsubmit=async event=>{\n event.preventDefault();if(chatBusy)return;\n const input=byId('chat-input'),message=input.value.trim();if(!message)return;\n chatBusy=true;byId('chat-send').disabled=true;\n try{await chatAction('/chat',{message,provider:byId('chat-provider').value,slide:state.slides.length?index:null,revision:state.revision??0});if(input.value.trim()===message)input.value='';}\n catch(error){byId('chat-status').textContent=error.message;chatBusy=false;byId('chat-send').disabled=false;}\n};\nbyId('chat-input').onkeydown=event=>{if(event.key==='Enter'&&(event.metaKey||event.ctrlKey)&&!event.isComposing){event.preventDefault();byId('chat-form').requestSubmit();}};\nfor(const action of ['stop','reset'])byId('chat-'+action).onclick=()=>chatAction('/chat/'+action,{}).catch(error=>{byId('chat-status').textContent=error.message;});\nbyId('toggle-chat').onclick=()=>{const hidden=document.body.classList.toggle('chat-hidden');byId('toggle-chat').setAttribute('aria-expanded',String(!hidden));resize();};\nvoid refreshChat();\nconst events=new EventSource('/events');events.onmessage=event=>{if(event.data==='chat'){void refreshChat();return;}if(event.data==='ready'){delete state.revision;void refreshChat();}void refresh();};events.onerror=()=>{byId('status').textContent='Reconnecting…'};refresh();\n</script></html>`;\n","import { spawn, type ChildProcess } from 'node:child_process';\nimport { dirname, resolve } from 'node:path';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\n\nexport type Provider = 'claude' | 'codex';\nexport interface FocusContext {\n slide: number | null;\n count: number;\n revision: number;\n text: string;\n entry: string;\n files: string[];\n buildError: string | null;\n}\ninterface Message {\n role: 'user' | 'assistant';\n text: string;\n context?: FocusContext;\n}\n\n/** One shared conversation and one editing process per dev server. */\nexport function createChat(entry: string, notify: () => void) {\n let messages: Message[] = [];\n let provider: Provider = 'claude';\n let child: ChildProcess | undefined;\n let status = 'Ready';\n let stopping = false;\n let killTimer: ReturnType<typeof setTimeout> | undefined;\n let completion: Promise<void> = Promise.resolve();\n const snapshot = () => ({ messages, provider, running: !!child, status });\n function stop() {\n if (!child || stopping) return;\n stopping = true;\n status = 'Stopping…';\n const processToStop = child;\n const kill = (signal: NodeJS.Signals) => {\n try {\n if (process.platform !== 'win32' && processToStop.pid)\n process.kill(-processToStop.pid, signal);\n else processToStop.kill(signal);\n } catch {\n /* Already exited. */\n }\n };\n kill('SIGTERM');\n killTimer = setTimeout(() => kill('SIGKILL'), 2000);\n notify();\n }\n function start(text: string, selected: Provider, context: FocusContext) {\n provider = selected;\n const user: Message = { role: 'user', text, context };\n // Keep recent conversational intent, including each turn's original focus.\n const history = messages.slice(-12).map((message) => ({\n ...message,\n text: message.text.slice(-12000),\n }));\n const prompt = `You are editing a local PowerPoint TSX project in ${dirname(resolve(entry))}.\nRead the project's authoring instructions. Make focused patches to the TSX source, preserving unrelated work.\nThe preview rebuilds automatically on save; do not start another dev server.\nThe attached focus is context, NOT a restriction: use it for references such as \"this slide\". For other slides or deck-wide requests, find the relevant source or shared theme instead. Do not rewrite the whole deck for a local edit.\nSlide numbers are 1-based and refer to the last successful preview at send time. Source may have changed since then; verify it before editing. Files are dependency candidates, not an exact slide-to-source mapping.\nRespond in the user's language, briefly describing edits or answering their question. If permissions prevent an action, explain that instead of claiming success.\nRecent conversation (JSON): ${JSON.stringify(history)}\nCurrent request and preview context (JSON): ${JSON.stringify(user)}`;\n messages.push(user);\n const answer: Message = { role: 'assistant', text: '' };\n messages.push(answer);\n if (messages.length > 100) messages = messages.slice(-100);\n status = 'Working…';\n stopping = false;\n const args =\n selected === 'codex'\n ? ['exec', '--json', '--sandbox', 'workspace-write', '--skip-git-repo-check', '-']\n : [\n '--print',\n '--verbose',\n '--output-format',\n 'stream-json',\n '--permission-mode',\n 'acceptEdits',\n '--tools',\n 'Read,Edit,Write,Glob,Grep',\n ];\n child = spawn(selected, args, {\n cwd: dirname(resolve(entry)),\n stdio: ['pipe', 'pipe', 'pipe'],\n detached: process.platform !== 'win32',\n shell: false,\n });\n const running = child;\n let buffer = '',\n stderr = '',\n failure = '',\n sawResult = false;\n function append(value: string) {\n answer.text = (answer.text + (answer.text ? '\\n\\n' : '') + value).slice(-100000);\n notify();\n }\n function consume(line: string) {\n if (!line.trim()) return;\n try {\n const event = JSON.parse(line);\n if (selected === 'codex') {\n if (event.type === 'item.completed' && event.item?.type === 'agent_message')\n append(event.item.text ?? '');\n if (event.type === 'item.started') {\n status = 'Editing / inspecting…';\n notify();\n }\n if (event.type === 'turn.completed') sawResult = true;\n if (event.type === 'turn.failed' || event.type === 'error')\n failure = event.error?.message ?? event.message ?? 'Agent failed';\n } else {\n if (event.type === 'assistant') {\n for (const block of event.message?.content ?? []) {\n if (block.type === 'text') append(block.text);\n if (block.type === 'tool_use') {\n status = 'Using ' + block.name + '…';\n notify();\n }\n }\n }\n if (event.type === 'result') {\n sawResult = true;\n if (event.is_error)\n failure = event.result || event.errors?.join('\\n') || 'Agent failed';\n else if (!answer.text && event.result) append(event.result);\n if (event.permission_denials?.length)\n failure = 'Some actions were denied by CLI permissions. ' + (event.result ?? '');\n }\n }\n } catch {\n failure = 'Unexpected CLI output. Update your CLI and try again.';\n }\n }\n running.stdout!.setEncoding('utf8');\n running.stdout!.on('data', (chunk: string) => {\n buffer += chunk;\n let end: number;\n while ((end = buffer.indexOf('\\n')) !== -1) {\n consume(buffer.slice(0, end));\n buffer = buffer.slice(end + 1);\n }\n if (buffer.length > 2_000_000) {\n failure = 'CLI output exceeded the limit.';\n stop();\n }\n });\n running.stderr!.setEncoding('utf8');\n running.stderr!.on('data', (chunk: string) => {\n stderr = (stderr + chunk).slice(-8000);\n });\n running.stdin!.on('error', () => {\n /* Early process exit is handled below. */\n });\n running.stdin!.end(prompt);\n completion = new Promise<void>((done) => {\n running.on('error', (error) => {\n failure = `Could not run ${selected}. Install it and log in from your terminal first. ${error.message}`;\n });\n running.on('close', (code) => {\n if (buffer.trim()) consume(buffer);\n clearTimeout(killTimer);\n status = stopping\n ? 'Stopped. Saved edits remain in the preview.'\n : failure ||\n (code !== 0\n ? stderr || `${selected} exited with code ${code}`\n : !sawResult\n ? 'CLI exited without a completed result.'\n : 'Done');\n if (status !== 'Done') append(status);\n child = undefined;\n notify();\n done();\n });\n });\n notify();\n }\n async function handle(\n request: IncomingMessage,\n response: ServerResponse,\n focus: (index: number | null, revision: number) => FocusContext,\n ) {\n const json = (code: number, value: unknown) => {\n response.writeHead(code, { 'Content-Type': 'application/json' });\n response.end(JSON.stringify(value));\n };\n if (request.method === 'GET' && request.url === '/chat') {\n json(200, snapshot());\n return;\n }\n if (request.method !== 'POST') {\n json(405, { error: 'Method not allowed' });\n return;\n }\n // Same-origin JSON prevents other sites from invoking local editing tools.\n if (\n request.headers.origin !== `http://${request.headers.host}` ||\n request.headers['content-type']?.split(';')[0] !== 'application/json'\n ) {\n json(403, { error: 'Same-origin JSON required' });\n return;\n }\n try {\n let body = '';\n request.setEncoding('utf8');\n for await (const chunk of request) {\n body += chunk;\n if (Buffer.byteLength(body) > 32000) {\n json(413, { error: 'Message too large' });\n return;\n }\n }\n const value = JSON.parse(body);\n if (request.url === '/chat/stop') {\n stop();\n json(200, snapshot());\n return;\n }\n if (child) {\n json(409, { error: 'An edit is already running' });\n return;\n }\n if (request.url === '/chat/reset') {\n messages = [];\n status = 'Ready';\n notify();\n json(200, snapshot());\n return;\n }\n if (request.url !== '/chat') {\n json(404, { error: 'Not found' });\n return;\n }\n if (\n typeof value.message !== 'string' ||\n !value.message.trim() ||\n value.message.length > 16000 ||\n !['claude', 'codex'].includes(value.provider) ||\n !(value.slide === null || (Number.isInteger(value.slide) && value.slide >= 0)) ||\n !Number.isInteger(value.revision)\n ) {\n json(400, { error: 'Invalid chat request' });\n return;\n }\n const context = focus(value.slide, value.revision);\n start(value.message.trim(), value.provider, context);\n json(202, snapshot());\n } catch (cause) {\n json(400, { error: cause instanceof Error ? cause.message : 'Invalid request' });\n }\n }\n return {\n handle,\n async close() {\n stop();\n await completion;\n },\n };\n}\n","import { createServer, type ServerResponse } from 'node:http';\nimport { watch } from 'node:fs';\nimport { dirname, resolve, sep } from 'node:path';\nimport type { BuildResult } from './build.ts';\nimport { createDeckBuilder } from './build-runner.ts';\nimport { page } from './page.ts';\nimport { createChat } from './chat.ts';\n\nexport async function serveDeck(entry: string, port = 4173) {\n let latest: BuildResult | undefined;\n let error: string | null = null;\n let building = false;\n let revision = 0;\n let generation = 0;\n let patch: Record<number, string> = {};\n let pending = false;\n let closed = false;\n const clients = new Set<ServerResponse>();\n const chat = createChat(entry, () => {\n for (const client of clients) client.write('data: chat\\n\\n');\n });\n const server = createServer((request, response) => {\n const host = request.headers.host;\n if (host !== `127.0.0.1:${actualPort}` && host !== `localhost:${actualPort}`) {\n response.writeHead(403).end();\n return;\n }\n response.setHeader('Cache-Control', 'no-store');\n if (request.url === '/chat' || request.url?.startsWith('/chat/')) {\n void chat.handle(request, response, (index, viewedRevision) => {\n if (viewedRevision !== revision)\n throw new Error('Preview changed. Review the current slide and send again.');\n if (index !== null && (!latest || index >= latest.slides.length))\n throw new Error('Slide no longer exists.');\n return {\n slide: index === null ? null : index + 1,\n count: latest?.slides.length ?? 0,\n revision,\n text: index === null ? '' : (latest?.slideTexts[index] ?? '').slice(0, 12000),\n entry: resolve(entry),\n files: latest?.dependencies ?? [resolve(entry)],\n buildError: error,\n };\n });\n } else if (request.method !== 'GET') {\n response.writeHead(405).end();\n } else if (request.url === '/events') {\n response.writeHead(200, { 'Content-Type': 'text/event-stream' });\n response.write('data: ready\\n\\n');\n clients.add(response);\n request.on('close', () => clients.delete(response));\n } else if (request.url === '/state' || request.url?.startsWith('/state?')) {\n const since = new URL(request.url, 'http://localhost').searchParams.get('since');\n const incremental =\n since !== null && (since === String(revision) || since === String(revision - 1));\n response.writeHead(200, { 'Content-Type': 'application/json' });\n response.end(\n JSON.stringify({\n revision,\n building,\n ...(incremental\n ? {\n changes: since === String(revision) ? {} : patch,\n count: latest?.slides.length ?? 0,\n }\n : { slides: latest?.slides ?? [] }),\n aspectRatio: latest?.aspectRatio ?? 16 / 9,\n error,\n diagnostics: latest?.diagnostics ?? [],\n }),\n );\n } else if (request.url === '/deck.pptx' && latest) {\n response.writeHead(200, {\n 'Content-Type': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n 'Content-Disposition': 'attachment; filename=\"deck.pptx\"',\n });\n response.end(latest.bytes);\n } else if (request.url === '/') {\n response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\n response.end(page);\n } else {\n response.writeHead(404).end();\n }\n });\n async function rebuild() {\n if (closed) return;\n if (building) {\n pending = true;\n return;\n }\n building = true;\n const started = generation;\n for (const client of clients) client.write('data: building\\n\\n');\n try {\n const result = await builder.build();\n if (started === generation && !closed) {\n patch = {};\n result.slides.forEach((svg, index) => {\n if (svg !== latest?.slides[index]) patch[index] = svg;\n });\n latest = result;\n revision++;\n error = null;\n }\n } catch (cause) {\n if (started === generation)\n error = cause instanceof Error ? (cause.stack ?? cause.message) : String(cause);\n } finally {\n building = false;\n for (const client of clients) client.write('data: updated\\n\\n');\n if (pending && !timer) {\n pending = false;\n void rebuild();\n }\n }\n }\n let timer: ReturnType<typeof setTimeout> | undefined;\n const root = dirname(resolve(entry));\n const watcher = watch(root, { recursive: true }, (_, filename) => {\n if (\n !filename ||\n filename\n .split(sep)\n .some((part) => ['node_modules', '.git', 'dist', '.office-kit'].includes(part))\n )\n return;\n if (!/\\.([cm]?[jt]sx?|json|pptx|png|jpe?g|gif|bmp|tiff?|emf|wmf|svg)$/i.test(filename)) return;\n generation++;\n void builder.cancel();\n clearTimeout(timer);\n timer = setTimeout(() => {\n timer = undefined;\n pending = false;\n void rebuild();\n }, 30);\n });\n const builder = createDeckBuilder(entry);\n let actualPort = port;\n try {\n await new Promise<void>((resolveListen, reject) => {\n server.once('error', reject);\n server.listen(port, '127.0.0.1', () => {\n server.off('error', reject);\n resolveListen();\n });\n });\n } catch (cause) {\n watcher.close();\n await builder.close();\n throw cause;\n }\n const address = server.address();\n if (address && typeof address !== 'string') actualPort = address.port;\n await rebuild();\n return {\n url: `http://127.0.0.1:${actualPort}`,\n async close() {\n closed = true;\n clearTimeout(timer);\n watcher.close();\n await chat.close();\n await builder.close();\n for (const client of clients) client.end();\n await new Promise<void>((done, reject) =>\n server.close((cause) => (cause ? reject(cause) : done())),\n );\n },\n };\n}\n","#!/usr/bin/env node\nimport { resolve } from 'node:path';\nimport { exportDeck } from './index.ts';\nimport { serveDeck } from './server.ts';\nimport { initProject } from './init.ts';\nimport { inspectTemplate } from './inspect.ts';\n\nconst usage = `Usage: office-pptx init <new-directory>\n office-pptx dev <deck.tsx> [--port 4173]\n office-pptx build <deck.tsx> [--out deck.pptx]\n office-pptx inspect <template.pptx>`;\nconst [command, entry, ...args] = process.argv.slice(2);\ntry {\n if (command === '--help' || command === '-h') {\n console.log(usage);\n } else {\n if (!entry) throw new Error(usage);\n const option = command === 'build' ? '--out' : command === 'dev' ? '--port' : undefined;\n if (args.length && (args.length !== 2 || !option || args[0] !== option || !args[1])) {\n throw new Error(`Unexpected arguments: ${args.join(' ')}\\n${usage}`);\n }\n if (command === 'init') {\n const directory = await initProject(entry);\n console.log(`Created ${directory}\\nRun npm install, then npm run dev inside that directory.`);\n } else if (command === 'inspect') {\n console.log(JSON.stringify(await inspectTemplate(entry), null, 2));\n } else if (command === 'build') {\n const output = resolve(args[1] ?? 'deck.pptx');\n const result = await exportDeck(entry, output);\n console.log(`Wrote ${output} (${result.slides.length} slides)`);\n } else if (command === 'dev') {\n const port = args[1] === undefined ? 4173 : Number(args[1]);\n if (!Number.isInteger(port) || port < 0 || port > 65535)\n throw new Error('Port must be an integer between 0 and 65535.');\n const server = await serveDeck(entry, port);\n console.log(`Preview: ${server.url}`);\n const stop = () => {\n void server.close().then(() => process.exit(0));\n };\n process.once('SIGINT', stop);\n process.once('SIGTERM', stop);\n } else {\n throw new Error(usage);\n }\n }\n} catch (cause) {\n console.error(cause instanceof Error ? cause.message : cause);\n process.exitCode = 1;\n}\n"],"mappings":"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"}
|
package/dist/index.d.mts
CHANGED
package/dist/worker.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { parentPort, workerData } from "node:worker_threads";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
|
-
import { getSlideSize, getSlides, loadPresentation, savePresentation, validatePresentation } from "@office-kit/pptx";
|
|
4
|
+
import { getSlideSize, getSlideText, getSlides, loadPresentation, savePresentation, validatePresentation } from "@office-kit/pptx";
|
|
5
5
|
import { once } from "node:events";
|
|
6
6
|
import { build, transform } from "esbuild";
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -66,6 +66,7 @@ async function buildDeck(entry, directory) {
|
|
|
66
66
|
aspectRatio: size ? size.width / size.height : 16 / 9,
|
|
67
67
|
slides: getSlides(saved).map((slide) => renderSlideToSvg(saved, slide)),
|
|
68
68
|
dependencies: Object.keys(result.metafile.inputs).map((path) => resolve(path)),
|
|
69
|
+
slideTexts: getSlides(saved).map((slide) => getSlideText(slide)),
|
|
69
70
|
diagnostics
|
|
70
71
|
};
|
|
71
72
|
}
|
package/dist/worker.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.mjs","names":[],"sources":["../src/build.ts","../src/worker.ts"],"sourcesContent":["import { build, transform } from 'esbuild';\nimport { readFile } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\nimport { compile, type Node } from '@office-kit/pptx-dsl';\nimport {\n getSlideSize,\n getSlides,\n loadPresentation,\n savePresentation,\n validatePresentation,\n} from '@office-kit/pptx';\nimport { renderSlideToSvg } from '@office-kit/pptx-preview';\n\nconst resolvePackage = (name: string) => fileURLToPath(import.meta.resolve(name));\nexport interface BuildResult {\n bytes: Uint8Array;\n slides: string[];\n aspectRatio: number;\n dependencies: string[];\n diagnostics: ReturnType<typeof validatePresentation>;\n}\n/** Evaluates trusted local TSX. This is code execution, not a sandbox. */\nexport async function buildDeck(entry: string, directory: string): Promise<BuildResult> {\n const output = join(directory, 'deck.mjs');\n const result = await build({\n entryPoints: [resolve(entry)],\n outfile: output,\n bundle: true,\n platform: 'node',\n format: 'esm',\n target: 'node22',\n jsx: 'automatic',\n jsxImportSource: '@office-kit/pptx-dsl',\n sourcemap: 'inline',\n metafile: true,\n logLevel: 'silent',\n plugins: [\n {\n name: 'source-location',\n setup(builder) {\n // A bundled module lives in a temporary directory. Keep source-relative\n // asset URLs pointing at each original module, including imported files.\n builder.onLoad({ filter: /\\.[cm]?[jt]sx?$/ }, async ({ path }) => {\n const source = await readFile(path, 'utf8');\n const loader = path.endsWith('.tsx')\n ? 'tsx'\n : path.endsWith('.ts') || path.endsWith('.mts') || path.endsWith('.cts')\n ? 'ts'\n : path.endsWith('.jsx')\n ? 'jsx'\n : 'js';\n const transformed = await transform(source, {\n loader,\n sourcefile: path,\n sourcemap: 'inline',\n jsx: 'automatic',\n jsxDev: true,\n jsxImportSource: '@office-kit/pptx-dsl',\n define: { 'import.meta.url': JSON.stringify(pathToFileURL(path).href) },\n });\n return { contents: transformed.code, loader: 'js' };\n });\n },\n },\n {\n name: 'shared-office-kit',\n setup(builder) {\n // Core uses symbol-backed handles; all consumers must share its instance.\n builder.onResolve({ filter: /^@office-kit\\/pptx(?:-dsl)?(?:\\/.*)?$/ }, ({ path }) => ({\n path: resolvePackage(path),\n external: true,\n }));\n },\n },\n ],\n });\n const module: { default?: Node } = await import(pathToFileURL(output).href);\n if (!module.default) throw new Error('The TSX file must default-export a Presentation.');\n const presentation = await compile(module.default);\n const diagnostics = validatePresentation(presentation);\n const errors = diagnostics.filter((issue) => issue.severity === 'error');\n if (errors.length) throw new Error(`Invalid presentation: ${JSON.stringify(errors)}`);\n const bytes = await savePresentation(presentation);\n // Preview serialized output too, so persistence defects are visible during authoring.\n const saved = await loadPresentation(bytes);\n const size = getSlideSize(saved);\n return {\n bytes,\n aspectRatio: size ? size.width / size.height : 16 / 9,\n slides: getSlides(saved).map((slide) => renderSlideToSvg(saved, slide)),\n dependencies: Object.keys(result.metafile.inputs).map((path) => resolve(path)),\n diagnostics,\n };\n}\n","import { once } from 'node:events';\nimport { parentPort, workerData } from 'node:worker_threads';\nimport { buildDeck } from './build.ts';\n\nif (!parentPort) throw new Error('The build worker must run in a worker thread.');\nconst { entry, directory } = workerData as { entry: string; directory: string };\nawait once(parentPort, 'message');\nparentPort.postMessage(await buildDeck(entry, directory));\n"],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"worker.mjs","names":[],"sources":["../src/build.ts","../src/worker.ts"],"sourcesContent":["import { build, transform } from 'esbuild';\nimport { readFile } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\nimport { compile, type Node } from '@office-kit/pptx-dsl';\nimport {\n getSlideSize,\n getSlides,\n getSlideText,\n loadPresentation,\n savePresentation,\n validatePresentation,\n} from '@office-kit/pptx';\nimport { renderSlideToSvg } from '@office-kit/pptx-preview';\n\nconst resolvePackage = (name: string) => fileURLToPath(import.meta.resolve(name));\nexport interface BuildResult {\n bytes: Uint8Array;\n slides: string[];\n slideTexts: string[];\n aspectRatio: number;\n dependencies: string[];\n diagnostics: ReturnType<typeof validatePresentation>;\n}\n/** Evaluates trusted local TSX. This is code execution, not a sandbox. */\nexport async function buildDeck(entry: string, directory: string): Promise<BuildResult> {\n const output = join(directory, 'deck.mjs');\n const result = await build({\n entryPoints: [resolve(entry)],\n outfile: output,\n bundle: true,\n platform: 'node',\n format: 'esm',\n target: 'node22',\n jsx: 'automatic',\n jsxImportSource: '@office-kit/pptx-dsl',\n sourcemap: 'inline',\n metafile: true,\n logLevel: 'silent',\n plugins: [\n {\n name: 'source-location',\n setup(builder) {\n // A bundled module lives in a temporary directory. Keep source-relative\n // asset URLs pointing at each original module, including imported files.\n builder.onLoad({ filter: /\\.[cm]?[jt]sx?$/ }, async ({ path }) => {\n const source = await readFile(path, 'utf8');\n const loader = path.endsWith('.tsx')\n ? 'tsx'\n : path.endsWith('.ts') || path.endsWith('.mts') || path.endsWith('.cts')\n ? 'ts'\n : path.endsWith('.jsx')\n ? 'jsx'\n : 'js';\n const transformed = await transform(source, {\n loader,\n sourcefile: path,\n sourcemap: 'inline',\n jsx: 'automatic',\n jsxDev: true,\n jsxImportSource: '@office-kit/pptx-dsl',\n define: { 'import.meta.url': JSON.stringify(pathToFileURL(path).href) },\n });\n return { contents: transformed.code, loader: 'js' };\n });\n },\n },\n {\n name: 'shared-office-kit',\n setup(builder) {\n // Core uses symbol-backed handles; all consumers must share its instance.\n builder.onResolve({ filter: /^@office-kit\\/pptx(?:-dsl)?(?:\\/.*)?$/ }, ({ path }) => ({\n path: resolvePackage(path),\n external: true,\n }));\n },\n },\n ],\n });\n const module: { default?: Node } = await import(pathToFileURL(output).href);\n if (!module.default) throw new Error('The TSX file must default-export a Presentation.');\n const presentation = await compile(module.default);\n const diagnostics = validatePresentation(presentation);\n const errors = diagnostics.filter((issue) => issue.severity === 'error');\n if (errors.length) throw new Error(`Invalid presentation: ${JSON.stringify(errors)}`);\n const bytes = await savePresentation(presentation);\n // Preview serialized output too, so persistence defects are visible during authoring.\n const saved = await loadPresentation(bytes);\n const size = getSlideSize(saved);\n return {\n bytes,\n aspectRatio: size ? size.width / size.height : 16 / 9,\n slides: getSlides(saved).map((slide) => renderSlideToSvg(saved, slide)),\n dependencies: Object.keys(result.metafile.inputs).map((path) => resolve(path)),\n slideTexts: getSlides(saved).map((slide) => getSlideText(slide)),\n diagnostics,\n };\n}\n","import { once } from 'node:events';\nimport { parentPort, workerData } from 'node:worker_threads';\nimport { buildDeck } from './build.ts';\n\nif (!parentPort) throw new Error('The build worker must run in a worker thread.');\nconst { entry, directory } = workerData as { entry: string; directory: string };\nawait once(parentPort, 'message');\nparentPort.postMessage(await buildDeck(entry, directory));\n"],"mappings":";;;;;;;;;;AAeA,MAAM,kBAAkB,SAAiB,cAAc,OAAO,KAAK,QAAQ,IAAI,CAAC;;AAUhF,eAAsB,UAAU,OAAe,WAAyC;CACtF,MAAM,SAAS,KAAK,WAAW,UAAU;CACzC,MAAM,SAAS,MAAM,MAAM;EACzB,aAAa,CAAC,QAAQ,KAAK,CAAC;EAC5B,SAAS;EACT,QAAQ;EACR,UAAU;EACV,QAAQ;EACR,QAAQ;EACR,KAAK;EACL,iBAAiB;EACjB,WAAW;EACX,UAAU;EACV,UAAU;EACV,SAAS,CACP;GACE,MAAM;GACN,MAAM,SAAS;IAGb,QAAQ,OAAO,EAAE,QAAQ,kBAAkB,GAAG,OAAO,EAAE,WAAW;KAkBhE,OAAO;MAAE,WAAU,MATO,UAAU,MARf,SAAS,MAAM,MAAM,GAQE;OAC1C,QARa,KAAK,SAAS,MAAM,IAC/B,QACA,KAAK,SAAS,KAAK,KAAK,KAAK,SAAS,MAAM,KAAK,KAAK,SAAS,MAAM,IACnE,OACA,KAAK,SAAS,MAAM,IAClB,QACA;OAGN,YAAY;OACZ,WAAW;OACX,KAAK;OACL,QAAQ;OACR,iBAAiB;OACjB,QAAQ,EAAE,mBAAmB,KAAK,UAAU,cAAc,IAAI,CAAC,CAAC,IAAI,EAAE;MACxE,CAAC,EAAA,CAC8B;MAAM,QAAQ;KAAK;IACpD,CAAC;GACH;EACF,GACA;GACE,MAAM;GACN,MAAM,SAAS;IAEb,QAAQ,UAAU,EAAE,QAAQ,wCAAwC,IAAI,EAAE,YAAY;KACpF,MAAM,eAAe,IAAI;KACzB,UAAU;IACZ,EAAE;GACJ;EACF,CACF;CACF,CAAC;CACD,MAAM,SAA6B,MAAM,OAAO,cAAc,MAAM,CAAC,CAAC;CACtE,IAAI,CAAC,OAAO,SAAS,MAAM,IAAI,MAAM,kDAAkD;CACvF,MAAM,eAAe,MAAM,QAAQ,OAAO,OAAO;CACjD,MAAM,cAAc,qBAAqB,YAAY;CACrD,MAAM,SAAS,YAAY,QAAQ,UAAU,MAAM,aAAa,OAAO;CACvE,IAAI,OAAO,QAAQ,MAAM,IAAI,MAAM,yBAAyB,KAAK,UAAU,MAAM,GAAG;CACpF,MAAM,QAAQ,MAAM,iBAAiB,YAAY;CAEjD,MAAM,QAAQ,MAAM,iBAAiB,KAAK;CAC1C,MAAM,OAAO,aAAa,KAAK;CAC/B,OAAO;EACL;EACA,aAAa,OAAO,KAAK,QAAQ,KAAK,SAAS,KAAK;EACpD,QAAQ,UAAU,KAAK,CAAC,CAAC,KAAK,UAAU,iBAAiB,OAAO,KAAK,CAAC;EACtE,cAAc,OAAO,KAAK,OAAO,SAAS,MAAM,CAAC,CAAC,KAAK,SAAS,QAAQ,IAAI,CAAC;EAC7E,YAAY,UAAU,KAAK,CAAC,CAAC,KAAK,UAAU,aAAa,KAAK,CAAC;EAC/D;CACF;AACF;;;AC7FA,IAAI,CAAC,YAAY,MAAM,IAAI,MAAM,+CAA+C;AAChF,MAAM,EAAE,OAAO,cAAc;AAC7B,MAAM,KAAK,YAAY,SAAS;AAChC,WAAW,YAAY,MAAM,UAAU,OAAO,SAAS,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@office-kit/pptx-dev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Build and preview PowerPoint TSX presentations locally",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"esbuild": "^0.28.1",
|
|
31
31
|
"@office-kit/pptx": "^0.18.2",
|
|
32
|
-
"@office-kit/pptx-
|
|
33
|
-
"@office-kit/pptx-
|
|
32
|
+
"@office-kit/pptx-dsl": "^0.5.0",
|
|
33
|
+
"@office-kit/pptx-preview": "^0.9.8"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^24.13.2",
|