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; }