From eff0277be0f1480dfca9f02cb23efbd63be94b5c Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Sat, 26 Nov 2022 14:15:38 +0100 Subject: [PATCH] Space System refactor --- cmd/invokeFunction.ts | 33 +++++ cmd/publish.ts | 5 +- cmd/server.ts | 3 + server/http_server.ts | 271 ++++++++------------------------------ server/space_system.ts | 185 ++++++++++++++++++++++++++ server/syscalls/system.ts | 5 +- silverbullet.ts | 11 ++ 7 files changed, 290 insertions(+), 223 deletions(-) create mode 100644 cmd/invokeFunction.ts create mode 100644 server/space_system.ts diff --git a/cmd/invokeFunction.ts b/cmd/invokeFunction.ts new file mode 100644 index 0000000..8c18230 --- /dev/null +++ b/cmd/invokeFunction.ts @@ -0,0 +1,33 @@ +import { SpaceSystem } from "../server/space_system.ts"; + +import assetBundle from "../dist/asset_bundle.json" assert { type: "json" }; +import { path } from "../plugos/deps.ts"; +import { AssetBundle, AssetJson } from "../plugos/asset_bundle/bundle.ts"; + +export async function invokeFunction( + options: any, + pagesPath: string, + functionName: string, + ...args: string[] +) { + console.log("Going to invoke funciton", functionName, "with args", args); + const spaceSystem = new SpaceSystem( + new AssetBundle(assetBundle as AssetJson), + pagesPath, + path.join(pagesPath, options.db), + ); + + await spaceSystem.start(); + + const [plugName, funcName] = functionName.split("."); + + const plug = spaceSystem.system.loadedPlugs.get(plugName); + + if (!plug) { + console.error("Plug not found", plugName); + Deno.exit(1); + } + + await plug.invoke(funcName, args); + Deno.exit(0); +} diff --git a/cmd/publish.ts b/cmd/publish.ts index 121d58c..1369c03 100755 --- a/cmd/publish.ts +++ b/cmd/publish.ts @@ -14,7 +14,7 @@ import { storeSyscalls, } from "../plugos/syscalls/store.deno.ts"; import { System } from "../plugos/system.ts"; -import { Manifest, SilverBulletHooks } from "../common/manifest.ts"; +import { SilverBulletHooks } from "../common/manifest.ts"; import { loadMarkdownExtensions } from "../common/markdown_ext.ts"; import buildMarkdown from "../common/parser.ts"; import { DiskSpacePrimitives } from "../common/spaces/disk_space_primitives.ts"; @@ -29,15 +29,12 @@ import { } from "../server/syscalls/index.ts"; import spaceSyscalls from "../server/syscalls/space.ts"; -import { Command } from "https://deno.land/x/cliffy@v0.25.2/command/command.ts"; - import assetBundle from "../dist/asset_bundle.json" assert { type: "json" }; import { AssetBundle, AssetJson } from "../plugos/asset_bundle/bundle.ts"; import { path } from "../server/deps.ts"; import { AsyncSQLite } from "../plugos/sqlite/async_sqlite.ts"; import { AssetBundlePlugSpacePrimitives } from "../common/spaces/asset_bundle_space_primitives.ts"; import assetSyscalls from "../plugos/syscalls/asset.ts"; -import { faBullseye } from "https://esm.sh/v96/@fortawesome/free-solid-svg-icons@6.2.0/index.d.ts"; export async function publishCommand(options: { index: boolean; diff --git a/cmd/server.ts b/cmd/server.ts index 2e04b5e..a8c9af0 100644 --- a/cmd/server.ts +++ b/cmd/server.ts @@ -12,11 +12,14 @@ export function serveCommand(options: any, folder: string) { port, "serving pages from", pagesPath, + "with db file", + options.db, ); const httpServer = new HttpServer({ port: port, pagesPath: pagesPath, + dbPath: path.join(pagesPath, options.db), assetBundle: new AssetBundle(assetBundle as AssetJson), password: options.password, }); diff --git a/server/http_server.ts b/server/http_server.ts index d982505..909ffa2 100644 --- a/server/http_server.ts +++ b/server/http_server.ts @@ -1,164 +1,41 @@ import { Application, path, Router } from "./deps.ts"; -import { Manifest, SilverBulletHooks } from "../common/manifest.ts"; -import { loadMarkdownExtensions } from "../common/markdown_ext.ts"; -import buildMarkdown from "../common/parser.ts"; -import { DiskSpacePrimitives } from "../common/spaces/disk_space_primitives.ts"; -import { EventedSpacePrimitives } from "../common/spaces/evented_space_primitives.ts"; -import { Space } from "../common/spaces/space.ts"; +import { Manifest } from "../common/manifest.ts"; import { SpacePrimitives } from "../common/spaces/space_primitives.ts"; -import { markdownSyscalls } from "../common/syscalls/markdown.ts"; -import { parseYamlSettings } from "../common/util.ts"; -import { createSandbox } from "../plugos/environments/deno_sandbox.ts"; import { EndpointHook } from "../plugos/hooks/endpoint.ts"; -import { EventHook } from "../plugos/hooks/event.ts"; -import { DenoCronHook } from "../plugos/hooks/cron.deno.ts"; -import { esbuildSyscalls } from "../plugos/syscalls/esbuild.ts"; -import { eventSyscalls } from "../plugos/syscalls/event.ts"; -import fileSystemSyscalls from "../plugos/syscalls/fs.deno.ts"; -import { - ensureFTSTable, - fullTextSearchSyscalls, -} from "../plugos/syscalls/fulltext.sqlite.ts"; -import sandboxSyscalls from "../plugos/syscalls/sandbox.ts"; -import shellSyscalls from "../plugos/syscalls/shell.deno.ts"; -import { - ensureTable as ensureStoreTable, - storeSyscalls, -} from "../plugos/syscalls/store.deno.ts"; -import { SysCallMapping, System } from "../plugos/system.ts"; -import { PageNamespaceHook } from "./hooks/page_namespace.ts"; -import { PlugSpacePrimitives } from "./hooks/plug_space_primitives.ts"; -import { - ensureTable as ensureIndexTable, - pageIndexSyscalls, -} from "./syscalls/index.ts"; -import spaceSyscalls from "./syscalls/space.ts"; -import { systemSyscalls } from "./syscalls/system.ts"; -import { AssetBundlePlugSpacePrimitives } from "../common/spaces/asset_bundle_space_primitives.ts"; -import assetSyscalls from "../plugos/syscalls/asset.ts"; import { AssetBundle } from "../plugos/asset_bundle/bundle.ts"; -import { AsyncSQLite } from "../plugos/sqlite/async_sqlite.ts"; -import { FileMetaSpacePrimitives } from "../common/spaces/file_meta_space_primitives.ts"; +import { SpaceSystem } from "./space_system.ts"; +import { parseYamlSettings } from "../common/util.ts"; export type ServerOptions = { port: number; pagesPath: string; + dbPath: string; assetBundle: AssetBundle; password?: string; }; -const indexRequiredKey = "$spaceIndexed"; const staticLastModified = new Date().toUTCString(); export class HttpServer { app: Application; - system: System; - private space: Space; - private eventHook: EventHook; - private db: AsyncSQLite; + systemBoot: SpaceSystem; private port: number; password?: string; settings: { [key: string]: any } = {}; - spacePrimitives: SpacePrimitives; abortController?: AbortController; - globalModules: Manifest; - assetBundle: AssetBundle; - indexSyscalls: SysCallMapping; constructor(options: ServerOptions) { this.port = options.port; this.app = new Application(); //{ serverConstructor: FlashServer }); - this.assetBundle = options.assetBundle; this.password = options.password; - - this.globalModules = JSON.parse( - this.assetBundle.readTextFileSync(`web/global.plug.json`), + this.systemBoot = new SpaceSystem( + options.assetBundle, + options.pagesPath, + options.dbPath, ); - // Set up the PlugOS System - this.system = new System("server"); - - // Instantiate the event bus hook - this.eventHook = new EventHook(); - this.system.addHook(this.eventHook); - - // And the page namespace hook - const namespaceHook = new PageNamespaceHook(); - this.system.addHook(namespaceHook); - - // The database used for persistence (SQLite) - this.db = new AsyncSQLite(path.join(options.pagesPath, "data.db")); - this.db.init().catch((e) => { - console.error("Error initializing database", e); - }); - - this.indexSyscalls = pageIndexSyscalls(this.db); - - // The space - try { - this.spacePrimitives = new FileMetaSpacePrimitives( - new AssetBundlePlugSpacePrimitives( - new EventedSpacePrimitives( - new PlugSpacePrimitives( - new DiskSpacePrimitives(options.pagesPath), - namespaceHook, - "server", - ), - this.eventHook, - ), - this.assetBundle, - ), - this.indexSyscalls, - ); - this.space = new Space(this.spacePrimitives); - } catch (e: any) { - if (e instanceof Deno.errors.NotFound) { - console.error("Pages folder", options.pagesPath, "not found"); - } else { - console.error(e.message); - } - Deno.exit(1); - } - - // The cron hook - this.system.addHook(new DenoCronHook()); - - // Register syscalls available on the server side - this.system.registerSyscalls( - [], - this.indexSyscalls, - storeSyscalls(this.db, "store"), - fullTextSearchSyscalls(this.db, "fts"), - spaceSyscalls(this.space), - eventSyscalls(this.eventHook), - markdownSyscalls(buildMarkdown([])), - esbuildSyscalls([this.globalModules]), - systemSyscalls(this, this.system), - sandboxSyscalls(this.system), - assetSyscalls(this.system), - // jwtSyscalls(), - ); - // Danger zone - this.system.registerSyscalls(["shell"], shellSyscalls(options.pagesPath)); - this.system.registerSyscalls(["fs"], fileSystemSyscalls("/")); - - // Register the HTTP endpoint hook (with "/_/"" prefix, hardcoded for now) - this.system.addHook(new EndpointHook(this.app, "/_")); - - this.system.on({ - sandboxInitialized: async (sandbox) => { - for ( - const [modName, code] of Object.entries( - this.globalModules.dependencies!, - ) - ) { - await sandbox.loadDependency(modName, code as string); - } - }, - }); - // Second, for loading plug JSON files with absolute or relative (from CWD) paths - this.eventHook.addLocalListener( + this.systemBoot.eventHook.addLocalListener( "get-plug:file", async (plugPath: string): Promise => { const resolvedPath = path.resolve(plugPath); @@ -175,57 +52,17 @@ export class HttpServer { // Rescan disk every 5s to detect any out-of-process file changes setInterval(() => { - this.space.updatePageList().catch(console.error); + this.systemBoot.space.updatePageList().catch(console.error); }, 5000); - } - rebuildMdExtensions() { - this.system.registerSyscalls( - [], - markdownSyscalls(buildMarkdown(loadMarkdownExtensions(this.system))), - ); - } - - async reloadPlugs() { - await this.space.updatePageList(); - - const allPlugs = await this.space.listPlugs(); - - console.log("Loading plugs", allPlugs); - await Promise.all((await this.space.listPlugs()).map(async (plugName) => { - const { data } = await this.space.readAttachment(plugName, "string"); - await this.system.load(JSON.parse(data as string), createSandbox); - })); - this.rebuildMdExtensions(); - - const corePlug = this.system.loadedPlugs.get("core"); - if (!corePlug) { - console.error("Something went very wrong, 'core' plug not found"); - return; - } - - // Do we need to reindex this space? - if ( - !(await this.system.localSyscall("core", "store.get", [indexRequiredKey])) - ) { - console.log("Now reindexing space..."); - await corePlug.invoke("reindexSpace", []); - await this.system.localSyscall("core", "store.set", [ - indexRequiredKey, - true, - ]); - } + // Register the HTTP endpoint hook (with "/_/"" prefix, hardcoded for now) + this.systemBoot.system.addHook(new EndpointHook(this.app, "/_")); } async start() { - await ensureIndexTable(this.db); - await ensureStoreTable(this.db, "store"); - await ensureFTSTable(this.db, "fts"); + await this.systemBoot.start(); + await this.systemBoot.ensureSpaceIndex(); await this.ensureAndLoadSettings(); - - // Load plugs - this.reloadPlugs().catch(console.error); - // Serve static files (javascript, css, html) this.app.use(async ({ request, response }, next) => { if (request.url.pathname === "/") { @@ -234,7 +71,7 @@ export class HttpServer { return; } response.headers.set("Content-type", "text/html"); - response.body = this.assetBundle.readTextFileSync( + response.body = this.systemBoot.assetBundle.readTextFileSync( "web/index.html", ); response.headers.set("Last-Modified", staticLastModified); @@ -243,7 +80,7 @@ export class HttpServer { try { const assetName = `web${request.url.pathname}`; if ( - this.assetBundle.has(assetName) && + this.systemBoot.assetBundle.has(assetName) && request.headers.get("If-Modified-Since") === staticLastModified ) { response.status = 304; @@ -252,9 +89,9 @@ export class HttpServer { response.status = 200; response.headers.set( "Content-type", - this.assetBundle.getMimeType(assetName), + this.systemBoot.assetBundle.getMimeType(assetName), ); - const data = this.assetBundle.readFileSync( + const data = this.systemBoot.assetBundle.readFileSync( assetName, ); response.headers.set("Cache-Control", "no-cache"); @@ -270,7 +107,7 @@ export class HttpServer { }); // Pages API - const fsRouter = this.buildFsRouter(this.spacePrimitives); + const fsRouter = this.buildFsRouter(this.systemBoot.spacePrimitives); this.app.use(fsRouter.routes()); this.app.use(fsRouter.allowedMethods()); @@ -282,7 +119,7 @@ export class HttpServer { // Fallback, serve index.html this.app.use((ctx) => { ctx.response.headers.set("Content-type", "text/html"); - ctx.response.body = this.assetBundle.readTextFileSync( + ctx.response.body = this.systemBoot.assetBundle.readTextFileSync( "web/index.html", ); }); @@ -296,7 +133,34 @@ export class HttpServer { console.log( `Silver Bullet is now running: http://localhost:${this.port}`, ); - console.log("--------------"); + } + + async ensureAndLoadSettings() { + const space = this.systemBoot.space; + try { + await space.getPageMeta("SETTINGS"); + } catch { + await space.writePage( + "SETTINGS", + this.systemBoot.assetBundle.readTextFileSync("SETTINGS_template.md"), + true, + ); + } + + const { text: settingsText } = await space.readPage("SETTINGS"); + const settings = parseYamlSettings(settingsText); + if (!settings.indexPage) { + settings.indexPage = "index"; + } + + try { + await space.getPageMeta(settings.indexPage); + } catch { + await space.writePage( + settings.indexPage, + `Welcome to your new space!`, + ); + } } private addPasswordAuth(r: Router) { @@ -413,6 +277,7 @@ export class HttpServer { private buildPlugRouter(): Router { const plugRouter = new Router(); this.addPasswordAuth(plugRouter); + const system = this.systemBoot.system; plugRouter.post( "/:plug/syscall/:name", @@ -421,14 +286,14 @@ export class HttpServer { const plugName = ctx.params.plug; const args = await ctx.request.body().value; console.log("Got args", args, "for", name, "in", plugName); - const plug = this.system.loadedPlugs.get(plugName); + const plug = system.loadedPlugs.get(plugName); if (!plug) { ctx.response.status = 404; ctx.response.body = `Plug ${plugName} not found`; return; } try { - const result = await this.system.syscallWithContext( + const result = await system.syscallWithContext( { plug }, name, args, @@ -450,7 +315,7 @@ export class HttpServer { const name = ctx.params.name; const plugName = ctx.params.plug; const args = await ctx.request.body().value; - const plug = this.system.loadedPlugs.get(plugName); + const plug = system.loadedPlugs.get(plugName); if (!plug) { ctx.response.status = 404; ctx.response.body = `Plug ${plugName} not found`; @@ -471,37 +336,11 @@ export class HttpServer { return new Router().use("/plug", plugRouter.routes()); } - async ensureAndLoadSettings() { - try { - await this.space.getPageMeta("SETTINGS"); - } catch { - await this.space.writePage( - "SETTINGS", - this.assetBundle.readTextFileSync("SETTINGS_template.md"), - true, - ); - } - - const { text: settingsText } = await this.space.readPage("SETTINGS"); - this.settings = parseYamlSettings(settingsText); - if (!this.settings.indexPage) { - this.settings.indexPage = "index"; - } - - try { - await this.space.getPageMeta(this.settings.indexPage); - } catch { - await this.space.writePage( - this.settings.indexPage, - `Welcome to your new space!`, - ); - } - } - async stop() { + const system = this.systemBoot.system; if (this.abortController) { console.log("Stopping"); - await this.system.unloadAll(); + await system.unloadAll(); console.log("Stopped plugs"); this.abortController.abort(); console.log("stopped server"); diff --git a/server/space_system.ts b/server/space_system.ts new file mode 100644 index 0000000..1cd946c --- /dev/null +++ b/server/space_system.ts @@ -0,0 +1,185 @@ +import { SilverBulletHooks } from "../common/manifest.ts"; +import { loadMarkdownExtensions } from "../common/markdown_ext.ts"; +import buildMarkdown from "../common/parser.ts"; +import { DiskSpacePrimitives } from "../common/spaces/disk_space_primitives.ts"; +import { EventedSpacePrimitives } from "../common/spaces/evented_space_primitives.ts"; +import { Space } from "../common/spaces/space.ts"; +import { SpacePrimitives } from "../common/spaces/space_primitives.ts"; +import { markdownSyscalls } from "../common/syscalls/markdown.ts"; +import { createSandbox } from "../plugos/environments/deno_sandbox.ts"; +import { EventHook } from "../plugos/hooks/event.ts"; +import { DenoCronHook } from "../plugos/hooks/cron.deno.ts"; +import { esbuildSyscalls } from "../plugos/syscalls/esbuild.ts"; +import { eventSyscalls } from "../plugos/syscalls/event.ts"; +import fileSystemSyscalls from "../plugos/syscalls/fs.deno.ts"; +import { + ensureFTSTable, + fullTextSearchSyscalls, +} from "../plugos/syscalls/fulltext.sqlite.ts"; +import sandboxSyscalls from "../plugos/syscalls/sandbox.ts"; +import shellSyscalls from "../plugos/syscalls/shell.deno.ts"; +import { + ensureTable as ensureStoreTable, + storeSyscalls, +} from "../plugos/syscalls/store.deno.ts"; +import { System } from "../plugos/system.ts"; +import { PageNamespaceHook } from "./hooks/page_namespace.ts"; +import { PlugSpacePrimitives } from "./hooks/plug_space_primitives.ts"; +import { + ensureTable as ensureIndexTable, + pageIndexSyscalls, +} from "./syscalls/index.ts"; +import spaceSyscalls from "./syscalls/space.ts"; +import { systemSyscalls } from "./syscalls/system.ts"; +import { AssetBundlePlugSpacePrimitives } from "../common/spaces/asset_bundle_space_primitives.ts"; +import assetSyscalls from "../plugos/syscalls/asset.ts"; +import { AssetBundle } from "../plugos/asset_bundle/bundle.ts"; +import { AsyncSQLite } from "../plugos/sqlite/async_sqlite.ts"; +import { FileMetaSpacePrimitives } from "../common/spaces/file_meta_space_primitives.ts"; +export const indexRequiredKey = "$spaceIndexed"; + +// A composition of a PlugOS system attached to a Space for server-side use +export class SpaceSystem { + public system: System; + public space: Space; + public eventHook: EventHook; + public spacePrimitives: SpacePrimitives; + + private db: AsyncSQLite; + + constructor( + readonly assetBundle: AssetBundle, + pagesPath: string, + databasePath: string, + ) { + const globalModules = JSON.parse( + assetBundle.readTextFileSync(`web/global.plug.json`), + ); + + // Set up the PlugOS System + this.system = new System("server"); + + // Instantiate the event bus hook + this.eventHook = new EventHook(); + this.system.addHook(this.eventHook); + + // And the page namespace hook + const namespaceHook = new PageNamespaceHook(); + this.system.addHook(namespaceHook); + + // The database used for persistence (SQLite) + this.db = new AsyncSQLite(databasePath); + this.db.init().catch((e) => { + console.error("Error initializing database", e); + }); + + const indexSyscalls = pageIndexSyscalls(this.db); + // The space + try { + this.spacePrimitives = new FileMetaSpacePrimitives( + new AssetBundlePlugSpacePrimitives( + new EventedSpacePrimitives( + new PlugSpacePrimitives( + new DiskSpacePrimitives(pagesPath), + namespaceHook, + "server", + ), + this.eventHook, + ), + assetBundle, + ), + indexSyscalls, + ); + this.space = new Space(this.spacePrimitives); + } catch (e: any) { + if (e instanceof Deno.errors.NotFound) { + console.error("Pages folder", pagesPath, "not found"); + } else { + console.error(e.message); + } + Deno.exit(1); + } + + // The cron hook + this.system.addHook(new DenoCronHook()); + + // Register syscalls available on the server side + this.system.registerSyscalls( + [], + indexSyscalls, + storeSyscalls(this.db, "store"), + fullTextSearchSyscalls(this.db, "fts"), + spaceSyscalls(this.space), + eventSyscalls(this.eventHook), + markdownSyscalls(buildMarkdown([])), + esbuildSyscalls([globalModules]), + systemSyscalls(this.loadPlugsFromSpace.bind(this), this.system), + sandboxSyscalls(this.system), + assetSyscalls(this.system), + ); + + // Danger zone, these syscalls require requesting specific permissions + this.system.registerSyscalls(["shell"], shellSyscalls(pagesPath)); + this.system.registerSyscalls(["fs"], fileSystemSyscalls("/")); + + this.system.on({ + sandboxInitialized: async (sandbox) => { + for ( + const [modName, code] of Object.entries( + globalModules.dependencies!, + ) + ) { + await sandbox.loadDependency(modName, code as string); + } + }, + }); + } + + // Loads all plugs under "_plug/" in the space + async loadPlugsFromSpace() { + await this.space.updatePageList(); + + const allPlugs = await this.space.listPlugs(); + + console.log("Going to load", allPlugs.length, "plugs..."); + await Promise.all(allPlugs.map(async (plugName) => { + const { data } = await this.space.readAttachment(plugName, "string"); + await this.system.load(JSON.parse(data as string), createSandbox); + })); + + // Re-register the markdown syscall with new markdown extensions + this.system.registerSyscalls( + [], + markdownSyscalls(buildMarkdown(loadMarkdownExtensions(this.system))), + ); + } + + // Checks if the space has been indexed, and if not, does so + async ensureSpaceIndex(forceReindex = false) { + const corePlug = this.system.loadedPlugs.get("core"); + if (!corePlug) { + console.error("Something went very wrong, 'core' plug not found"); + return; + } + + // Do we need to reindex this space? + if ( + forceReindex || + !(await this.system.localSyscall("core", "store.get", [indexRequiredKey])) + ) { + console.log("Now reindexing space..."); + await corePlug.invoke("reindexSpace", []); + await this.system.localSyscall("core", "store.set", [ + indexRequiredKey, + true, + ]); + } + } + + async start() { + await ensureIndexTable(this.db); + await ensureStoreTable(this.db, "store"); + await ensureFTSTable(this.db, "fts"); + await this.loadPlugsFromSpace(); + } +} diff --git a/server/syscalls/system.ts b/server/syscalls/system.ts index 39f4a7f..710f395 100644 --- a/server/syscalls/system.ts +++ b/server/syscalls/system.ts @@ -1,9 +1,8 @@ import { Plug } from "../../plugos/plug.ts"; import { SysCallMapping, System } from "../../plugos/system.ts"; -import type { HttpServer } from "../http_server.ts"; export function systemSyscalls( - httpServer: HttpServer, + plugReloader: () => Promise, system: System, ): SysCallMapping { return { @@ -30,7 +29,7 @@ export function systemSyscalls( return plug.invoke(name, args); }, "system.reloadPlugs": () => { - return httpServer.reloadPlugs(); + return plugReloader(); }, }; } diff --git a/silverbullet.ts b/silverbullet.ts index 19b26e8..0992e24 100755 --- a/silverbullet.ts +++ b/silverbullet.ts @@ -8,6 +8,7 @@ import { fixCommand } from "./cmd/fix.ts"; import { serveCommand } from "./cmd/server.ts"; import { plugCompileCommand } from "./cmd/plug_compile.ts"; import { publishCommand } from "./cmd/publish.ts"; +import { invokeFunction } from "./cmd/invokeFunction.ts"; await new Command() .name("silverbullet") @@ -20,6 +21,9 @@ await new Command() // Main command .arguments("") .option("-p, --port ", "Port to listen on") + .option("--db ", "Filename for the database", { + default: "data.db", + }) .option("--password ", "Password for basic authentication") .action(serveCommand) // fix @@ -43,6 +47,13 @@ await new Command() ) .option("--importmap ", "Path to import map file to use") .action(plugCompileCommand) + // invokeFunction + .command("invokeFunction", "Invoke a specific plug function from the CLI") + .arguments(" [...arguments:string]") + .option("--db ", "Filename for the database", { + default: "data.db", + }) + .action(invokeFunction) // publish .command("publish") .description("Publish a SilverBullet site")