- devdesk attach --fs-list/--fs-read/--git-status/--git-show/--asset (libexec/devdesk-files.sh), confined to the session folder. - The state hook records files Claude edits (Edit/Write/MultiEdit/ NotebookEdit) for the Changes tab's "C" badge; rename/end carry it. - Files pane: folders, CodeMirror read-only view, sanitized markdown preview in a sandboxed iframe with inlined relative images, images, Changes tab with line counts and diff against HEAD, "→ Claude" @path, resizable split. - The editor bundle ships as /usr/share/devdesk/assets/editor.js, is fetched over ttyd on first use, verified against the page's SHA-256 and cached in IndexedDB. - Fix: the state hook's field split collapsed empty fields. Closes #19 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
102 lines
3.9 KiB
JavaScript
102 lines
3.9 KiB
JavaScript
// Builds dist/index.html: one self-contained page (ttyd --index serves a single
|
|
// file and nothing else), with CSS, JS, fonts and the favicon all inlined.
|
|
import { build, transform } from 'esbuild';
|
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
import { createHash } from 'node:crypto';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
const at = (...parts) => join(root, ...parts);
|
|
const read = (...parts) => readFile(at(...parts), 'utf8');
|
|
|
|
// Version: $DEVDESK_VERSION (CI) or the top entry of debian/changelog.
|
|
async function version() {
|
|
if (process.env.DEVDESK_VERSION) return process.env.DEVDESK_VERSION;
|
|
const changelog = await read('debian', 'changelog');
|
|
return /^devdesk \(([^)]+)\)/.exec(changelog)?.[1] ?? 'dev';
|
|
}
|
|
|
|
// JetBrains Mono (OFL-1.1) from @fontsource: keep the latin + latin-ext faces
|
|
// of the given weights and inline their woff2 files.
|
|
async function fontFaces(weights) {
|
|
const pkg = at('node_modules', '@fontsource', 'jetbrains-mono');
|
|
const faces = [];
|
|
for (const weight of weights) {
|
|
const css = await readFile(join(pkg, `${weight}.css`), 'utf8');
|
|
for (const block of css.match(/\/\* jetbrains-mono-latin(-ext)?-\d+-normal \*\/\s*@font-face\s*{[^}]*}/g) ?? []) {
|
|
const file = /url\(\.\/files\/([^)]+\.woff2)\)/.exec(block)[1];
|
|
const data = (await readFile(join(pkg, 'files', file))).toString('base64');
|
|
faces.push(block.replace(/src:[^;]*;/, `src: url(data:font/woff2;base64,${data}) format('woff2');`));
|
|
}
|
|
}
|
|
if (!faces.length) throw new Error('no JetBrains Mono faces found');
|
|
return faces.join('\n');
|
|
}
|
|
|
|
// The file viewer's editor (devdesk#19): its own IIFE, served over the ttyd
|
|
// channel by `devdesk attach --asset editor.js` and cached by the browser. The
|
|
// page carries its SHA-256, which is both the integrity check and the cache key.
|
|
async function editorBundle() {
|
|
const out = await build({
|
|
entryPoints: [at('web', 'editor', 'index.js')],
|
|
bundle: true,
|
|
minify: true,
|
|
format: 'iife',
|
|
globalName: 'DevDeskEditor',
|
|
target: ['es2020'],
|
|
write: false,
|
|
legalComments: 'eof',
|
|
});
|
|
const code = out.outputFiles[0].text;
|
|
await mkdir(at('dist', 'assets'), { recursive: true });
|
|
await writeFile(at('dist', 'assets', 'editor.js'), code);
|
|
return { sha: createHash('sha256').update(code).digest('hex'), size: Buffer.byteLength(code) };
|
|
}
|
|
|
|
async function main() {
|
|
const ver = await version();
|
|
const editor = await editorBundle();
|
|
|
|
const js = await build({
|
|
entryPoints: [at('web', 'src', 'main.js')],
|
|
bundle: true,
|
|
minify: true,
|
|
format: 'iife',
|
|
target: ['es2020'],
|
|
write: false,
|
|
legalComments: 'eof',
|
|
define: {
|
|
__DEVDESK_VERSION__: JSON.stringify(ver),
|
|
__EDITOR_SHA__: JSON.stringify(editor.sha),
|
|
},
|
|
});
|
|
|
|
const css = await transform([
|
|
await read('vendor', 'web_template', 'base.css'),
|
|
await read('node_modules', '@xterm', 'xterm', 'css', 'xterm.css'),
|
|
await fontFaces([400, 700]),
|
|
await read('web', 'styles.css'),
|
|
].join('\n'), { loader: 'css', minify: true, legalComments: 'eof' });
|
|
|
|
const { FAVICON: favicon } = await import('./src/icon.js');
|
|
|
|
const script = js.outputFiles[0].text.replace(/<\/script/gi, '<\\/script');
|
|
const style = css.code.replace(/<\/style/gi, '<\\/style');
|
|
const template = await read('web', 'index.template.html');
|
|
const html = template
|
|
.replace('__FAVICON__', () => favicon)
|
|
.replace('<!--STYLES-->', () => `<style>${style}</style>`)
|
|
.replace('<!--SCRIPT-->', () => `<script>${script}</script>`);
|
|
|
|
await mkdir(at('dist'), { recursive: true });
|
|
await writeFile(at('dist', 'index.html'), html);
|
|
console.log(`dist/index.html ${(html.length / 1024).toFixed(0)} KiB (devdesk ${ver})`);
|
|
console.log(`dist/assets/editor.js ${(editor.size / 1024).toFixed(0)} KiB sha256 ${editor.sha.slice(0, 12)}`);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|