Files
dashback/src/tools/applet.js
2026-10-10 06:39:32 +02:00

89 lines
3.3 KiB
JavaScript

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* applet.js — the default `.applet.js` example files.
*
* Default applets are the executable counterpart to default SKILL files: small, real examples
* seeded into every new company's VFS so an operator can open one, read it, and adapt it instead
* of starting from a blank editor. They follow the same two rules as skills:
*
* 1. **Flat only.** The name is a bare file name at the VFS root — no path separators — so the
* whole set stays enumerable and addressable by name.
* 2. **Version-gated, never overwritten.** A seeded applet is written only when the target file
* does not exist yet. Once an operator edits one, their version is never clobbered by a
* later run — bump {@link DEFAULT_APPLETS_VERSION} and the file name to ship a new example.
*/
/** Resolved directory holding the shipped example applets. */
export const DEFAULT_APPLETS_DIR = fs.existsSync(path.resolve(__dirname, '../defaultApplets'))
? path.resolve(__dirname, '../defaultApplets')
: path.resolve(process.cwd(), 'defaultApplets');
/** Canonical extension for applet files, including the leading dot. */
export const APPLET_EXT = '.applet.js';
/**
* Bumped whenever the shipped examples change materially. Embedded in the seeded file name so a
* rewritten example arrives as a NEW file rather than overwriting one an operator may have
* customised. Keep `defaultApplets/` file names in sync with this prefix.
*/
export const DEFAULT_APPLETS_VERSION = 'v1';
/**
* Checks whether a bare file name is an applet file name.
* Rejects anything containing a path separator — applets are flat by contract, like skills.
* @param {string} name
* @returns {boolean}
*/
export function isAppletFileName(name) {
if (typeof name !== 'string') return false;
const clean = name.trim();
if (!clean) return false;
if (clean.includes('/') || clean.includes('\\')) return false;
if (!clean.toLowerCase().endsWith(APPLET_EXT)) return false;
return clean.slice(0, -APPLET_EXT.length).trim().length > 0;
}
/**
* Checks whether a VFS path points at an applet file, tolerating leading `./` or `/`.
* @param {string} p
* @returns {boolean}
*/
export function isAppletPath(p) {
if (typeof p !== 'string') return false;
return isAppletFileName(p.trim().replace(/^([a-zA-Z]:|\/)+/, '').replace(/^\.\/+/, ''));
}
/**
* Reads every shipped example applet from {@link DEFAULT_APPLETS_DIR}.
* @returns {Array<{ name: string, content: string }>}
*/
export function getDefaultApplets() {
if (!fs.existsSync(DEFAULT_APPLETS_DIR)) return [];
let dirents;
try {
dirents = fs.readdirSync(DEFAULT_APPLETS_DIR, { withFileTypes: true });
} catch {
return [];
}
const out = [];
for (const dirent of dirents) {
if (!dirent.isFile() || !isAppletFileName(dirent.name)) continue;
const absPath = path.join(DEFAULT_APPLETS_DIR, dirent.name);
try {
out.push({ name: dirent.name, content: fs.readFileSync(absPath, 'utf8') });
} catch (err) {
console.warn(`[applet] failed to read default applet "${dirent.name}":`, err.message);
}
}
out.sort((a, b) => a.name.localeCompare(b.name));
return out;
}