1
0

Preparing for an initial release

This commit is contained in:
Zef Hemel 2022-06-28 14:14:15 +02:00
parent be92bf2311
commit 16fcd91d6c
73 changed files with 3205 additions and 20734 deletions

BIN
.DS_Store vendored

Binary file not shown.

8
.idea/.gitignore generated vendored
View File

@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -1,48 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
<option name="HTML_ENFORCE_QUOTES" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

17
.idea/dataSources.xml generated
View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="data" uuid="b34630b0-d7b1-473e-9067-ffd03fa15451">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/pages/data.db</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
<libraries>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.36.0.3/sqlite-jdbc-3.36.0.3.jar</url>
</library>
</libraries>
</data-source>
</component>
</project>

8
.idea/misc.xml generated
View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SwUserDefinedSpecifications">
<option name="specTypeByUrl">
<map />
</option>
</component>
</project>

8
.idea/modules.xml generated
View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/silverbullet.iml" filepath="$PROJECT_DIR$/.idea/silverbullet.iml" />
</modules>
</component>
</project>

6
.idea/prettier.xml generated
View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myRunOnSave" value="true" />
</component>
</project>

11
.idea/silverbullet.iml generated
View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/dist" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated
View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

1
.vscode/configurationCache.log vendored Normal file
View File

@ -0,0 +1 @@
{"buildTargets":["release"],"launchTargets":[],"customConfigurationProvider":{"workspaceBrowse":{"browsePath":[],"compilerArgs":[]},"fileIndex":[]}}

6
.vscode/dryrun.log vendored Normal file
View File

@ -0,0 +1,6 @@
make --dry-run --always-make --keep-going --print-directory
make: Entering directory `/Users/zef/git/silverbullet'
make: `release' is up to date.
make: Leaving directory `/Users/zef/git/silverbullet'

241
.vscode/targets.log vendored Normal file
View File

@ -0,0 +1,241 @@
make all --print-data-base --no-builtin-variables --no-builtin-rules --question
# GNU Make 3.81
# Copyright (C) 2006 Free Software Foundation, Inc.
# This is free software; see the source for copying conditions.
# There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE.
# This program built for i386-apple-darwin11.3.0
make: *** No rule to make target `all'. Stop.
# Make data base, printed on Tue Jun 28 12:27:45 2022
# Variables
# automatic
<D = $(patsubst %/,%,$(dir $<))
# automatic
?F = $(notdir $?)
# environment
VSCODE_LOG_NATIVE = false
# environment
NVM_INC = /Users/zef/.nvm/versions/node/v16.13.2/include/node
# automatic
?D = $(patsubst %/,%,$(dir $?))
# automatic
@D = $(patsubst %/,%,$(dir $@))
# automatic
@F = $(notdir $@)
# makefile
CURDIR := /Users/zef/git/silverbullet
# makefile
SHELL = /bin/sh
# environment
VSCODE_NLS_CONFIG = {"locale":"en-us","availableLanguages":{},"_languagePackSupport":true}
# environment
_ = /usr/bin/make
# makefile (from `Makefile', line 1)
MAKEFILE_LIST := Makefile
# environment
VSCODE_VERBOSE_LOGGING = true
# environment
__CFBundleIdentifier = com.microsoft.VSCode
# environment
INFOPATH = /opt/homebrew/share/info:
# environment
VSCODE_IPC_HOOK_EXTHOST = /var/folders/s2/4nqrw2192hngtxg672qzc0nr0000gn/T/vscode-ipc-94d58bf8-97af-4a54-9bba-ba61d0cf0d30.sock
# environment
VSCODE_CWD = /
# environment
PATH = /Users/zef/.nvm/versions/node/v16.13.2/bin:/Library/Frameworks/Python.framework/Versions/2.7/bin:/Users/zef/.local/share/solana/install/active_release/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:/Users/zef/.cargo/bin:/Users/zef/.fig/bin:/Users/zef/.local/bin
# environment
LSCOLORS = Gxfxcxdxbxegedabagacad
# environment
NVM_BIN = /Users/zef/.nvm/versions/node/v16.13.2/bin
# environment
VSCODE_LOG_STACK = false
# environment
ELECTRON_RUN_AS_NODE = 1
# default
.FEATURES := target-specific order-only second-expansion else-if archives jobserver check-symlink
# environment
SSH_AUTH_SOCK = /private/tmp/com.apple.launchd.qE7FAVvbDO/Listeners
# automatic
%F = $(notdir $%)
# environment
TTY = not a tty
# environment
VSCODE_PIPE_LOGGING = true
# environment
FIG_PID = 89255
# environment
PWD = /Users/zef/git/silverbullet
# environment
HOMEBREW_CELLAR = /opt/homebrew/Cellar
# environment
ORIGINAL_XDG_CURRENT_DESKTOP = undefined
# environment
MANPATH = /Users/zef/.nvm/versions/node/v16.13.2/share/man:/opt/homebrew/share/man::
# environment
VSCODE_AMD_ENTRYPOINT = vs/workbench/api/node/extensionHostProcess
# environment
HOME = /Users/zef
# default
MAKEFILEPATH := /Applications/Xcode.app/Contents/Developer/Makefiles
# environment
VSCODE_CODE_CACHE_PATH = /Users/zef/Library/Application Support/Code/CachedData/30d9c6cd9483b2cc586687151bcbcd635f373630
# environment
LOGNAME = zef
# environment
APPLICATION_INSIGHTS_NO_DIAGNOSTIC_CHANNEL = true
# environment
NVM_CD_FLAGS = -q
# environment
ZSH = /Users/zef/.local/share/fig/plugins/ohmyzsh
# environment
VSCODE_HANDLES_UNCAUGHT_ERRORS = true
# automatic
^D = $(patsubst %/,%,$(dir $^))
# environment
XPC_FLAGS = 0x0
# default
MAKE = $(MAKE_COMMAND)
# default
MAKECMDGOALS := all
# environment
SHLVL = 1
# default
MAKE_VERSION := 3.81
# environment
USER = zef
# makefile
.DEFAULT_GOAL := release
# environment
LESS = -R
# automatic
%D = $(patsubst %/,%,$(dir $%))
# default
MAKE_COMMAND := /Applications/Xcode.app/Contents/Developer/usr/bin/make
# default
.VARIABLES :=
# environment
TMPDIR = /var/folders/s2/4nqrw2192hngtxg672qzc0nr0000gn/T/
# automatic
*F = $(notdir $*)
# environment
VSCODE_IPC_HOOK = /Users/zef/Library/Application Support/Code/1.68.1-main.sock
# makefile
MAKEFLAGS = Rrqp
# environment
MFLAGS = -Rrqp
# automatic
*D = $(patsubst %/,%,$(dir $*))
# environment
NVM_DIR = /Users/zef/.nvm
# environment
XPC_SERVICE_NAME = application.com.microsoft.VSCode.109834161.109834167
# environment
HOMEBREW_PREFIX = /opt/homebrew
# automatic
+D = $(patsubst %/,%,$(dir $+))
# automatic
+F = $(notdir $+)
# environment
HOMEBREW_REPOSITORY = /opt/homebrew
# environment
__CF_USER_TEXT_ENCODING = 0x1F5:0x0:0x0
# environment
COMMAND_MODE = unix2003
# default
MAKEFILES :=
# automatic
<F = $(notdir $<)
# environment
PAGER = less
# environment
LC_ALL = C
# automatic
^F = $(notdir $^)
# default
SUFFIXES :=
# environment
MAKELEVEL := 0
# environment
LANG = C
# environment
VSCODE_PID = 89247
# variable set hash-table stats:
# Load=76/1024=7%, Rehash=0, Collisions=1/99=1%
# Pattern-specific Variable Values
# No pattern-specific variable values.
# Directories
# . (device 16777232, inode 91684251): 20 files, no impossibilities.
# 20 files, no impossibilities in 1 directories.
# Implicit Rules
# No implicit rules.
# Files
# Not a target:
all:
# Command-line target.
# Implicit rule search has been done.
# File does not exist.
# File has not been updated.
# variable set hash-table stats:
# Load=0/32=0%, Rehash=0, Collisions=0/0=0%
# Not a target:
.SUFFIXES:
# Implicit rule search has not been done.
# Modification time never checked.
# File has not been updated.
# Not a target:
Makefile:
# Implicit rule search has been done.
# Last modified 2022-06-28 12:27:42
# File has been updated.
# Successfully updated.
# variable set hash-table stats:
# Load=0/32=0%, Rehash=0, Collisions=0/0=0%
release:
# Implicit rule search has not been done.
# Modification time never checked.
# File has not been updated.
# commands to execute (from `Makefile', line 2):
# Not a target:
.DEFAULT:
# Implicit rule search has not been done.
# Modification time never checked.
# File has not been updated.
# files hash-table stats:
# Load=5/1024=0%, Rehash=0, Collisions=0/17=0%
# VPATH Search Paths
# No `vpath' search paths.
# No general (`VPATH' variable) search path.
# # of strings in strcache: 1
# # of strcache buffers: 1
# strcache size: total = 4096 / max = 4096 / min = 4096 / avg = 4096
# strcache free: total = 4087 / max = 4087 / min = 4087 / avg = 4087
# Finished Make data base on Tue Jun 28 12:27:45 2022

8
LICENSE.md Normal file
View File

@ -0,0 +1,8 @@
Copyright 2022, Zef Hemel
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,31 +1,66 @@
# Silver Bullet # Silver Bullet
Silver Bullet (SB) is a highly extensible, open source **personal knowledge playground**. At its core its effectively a Markdown-based writing/note taking application that stores your _pages_ (notes) as plain markdown files in a folder referred to as a _space_. Pages can be cross-linked using the `[[link to other page]]` syntax. This makes it a simple tool for [Personal Knowledge Management](https://en.wikipedia.org/wiki/Personal_knowledge_management). However, once you leverage its various extensions (called _plugs_) it can feel more like a _knowledge playground_, allowing you to annotate, combine and query your accumulated knowledge in creative ways specific to you.
Mono repo using npm workspaces. So what is it SB _really_? That is hard to answer. It can do tons of stuff, and Im constantly finding new use cases. Its like… a silver bullet.
To install, after clone: Heres how I use it today (but this has grown significantly over time):
* Basic note taking, e.g. during meetings, about books I read, blogs I read, podcasts I listen to, movies I watch.
* Getting a quick glance of the work people in my team are doing pulling data from our 1:1s, recent activity on Github (such as recent pull requests) and other sources.
* Writing:
* [My blog](https://zef.plus) is published via SBs [Ghost](https://ghost.org) plugin.
* An internal newsletter that I write is written in SB.
* Performance reviews for my team (I work as a people manager) are written and managed using SB (for which I extensively use SBs meta data features and query that data in various ways).
* A custom SB plugin aggregates data from our OpsGenie account every week, and publishes it to our mattermost instance.
* It powers part of my smart home: I wired HomeBridge webhooks up to custom HTTP endpoints exposed by my custom smart home SB plug.
More documentation can be found in the [docs space](https://github.com/zefhemel/silverbullet/tree/main/docs)
## Features
* **Free and open source**
* **Minimalistic** UI with [What You See is What You Mean](https://en.wikipedia.org/wiki/WYSIWYM) Markdown editing.
* **Future proof**: stores all notes in a regular folder with markdown files, no proprietary file formats. While SB uses a SQLite database for indexes, this database can be wiped and rebuilt based on your pages at any time. Your Markdown files are the single source of truth.
* **Run anywhere**: run it on your local machine, or install it on a server. You access it via your web browser (desktop or mobile), or install it as a PWA (giving it its own window frame and dock/launcher/dock icon).
* **Keyboard oriented:** you can fully operate SB via the keyboard.
* **Extensible** through plugs.
## Stack
* Written in [TypeScript](https://www.typescriptlang.org/)
* Built on the excellent [CodeMirror 6](https://codemirror.net/) editor component
* Front-end (beside CodeMirror) is built using React.js
* [ParcelJS](https://parceljs.org/) is used to build both the front-end and back-end
* Backend runs on node.js using express
## Development
This Silver Bullet repo is a monorepo using npm's "workspaces" feature.
Requirements: node 16+ and npm 8+ as well as C/C++ compilers (for compiling SQLite, on debian/ubuntu style systems you get these via the `build-essential` package)
To run, after clone:
```shell ```shell
# The path for pages, hardcoded for `npm run server`
mkdir -p pages
# Install dependencies # Install dependencies
npm install npm install
# Run initial build (web app, server, etc.) # Run initial build (web app, server, etc.)
npm run build npm run build
# Again, to install the CLIs just built # Again, to install the CLIs just built (plugos-bundler, silverbullet)
npm install npm install
# Build plugs (ctrl-c after done, it's watching) # Build built-in plugs
npm run plugs npm run build-plugs
# Launch server # Launch server
npm run server npm run server -- <PATH-TO-YOUR-SPACE>
``` ```
Open at http://localhost:3000 This `<PATH-TO-YOUR-SPACE>` can be any folder with markdown files, upon first boot SB will ensure there is an `index.md` file (root page) and `PLUGS.md` file (with default list of plugs to load). SB will also create a SQLite `data.db` file with various data caches and indices (you can delete this file at any time and use the `Space: Reindex` command to reindex everything).
Open SB at http://localhost:3000 If you're using a browser supporting PWAs, you can install this page as a PWA. This also works on iOS (use the "Add to homescreen" option in the share menu).
General development workflow: General development workflow:
Run these in separate terminals
```shell ```shell
# Run these in separate terminals # Runs ParcelJS in watch mode, rebuilding the server and webapp continuously on change
npm run watch npm run watch
# Runs the silverbullet server
npm run server npm run server
# Builds (and watches for changes) all builtin plugs (in packages/plugs)
npm run plugs npm run plugs
``` ```

89
desktop/.gitignore vendored
View File

@ -1,89 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
.DS_Store
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Webpack
.webpack/
# Electron-Forge
out/

19905
desktop/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,103 +0,0 @@
{
"name": "desktop",
"productName": "Silver Bullet",
"version": "1.0.0",
"description": "My Electron application description",
"main": "dist/index.js",
"scripts": {
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish",
"lint": "echo \"No linting configured\""
},
"keywords": [],
"author": {
"name": "Zef Hemel",
"email": "zef@zef.me"
},
"targets": {
"desktop": {
"source": [
"src/index.ts",
"src/preload.ts"
],
"outputFormat": "commonjs",
"isLibrary": true,
"context": "electron-main",
"includeNodeModules": [
"@plugos/plugos",
"@silverbulletmd/common",
"@silverbulletmd/web",
"@silverbulletmd/server"
]
}
},
"license": "MIT",
"config": {
"forge": {
"packagerConfig": {},
"makers": [
{
"name": "@electron-forge/maker-squirrel",
"config": {
"name": "desktop"
}
},
{
"name": "@electron-forge/maker-zip",
"platforms": [
"darwin"
]
},
{
"name": "@electron-forge/maker-deb",
"config": {}
},
{
"name": "@electron-forge/maker-rpm",
"config": {}
}
]
}
},
"dependencies": {
"electron": "^18.0.3",
"electron-squirrel-startup": "^1.0.0",
"@codemirror/lang-javascript": "^0.19.7",
"@codemirror/lang-markdown": "^0.19.6",
"@codemirror/language": "^0.19.0",
"@codemirror/legacy-modes": "^0.19.1",
"@codemirror/stream-parser": "^0.19.9",
"@jest/globals": "^27.5.1",
"@lezer/markdown": "^0.15.0",
"better-sqlite3": "^7.5.0",
"body-parser": "^1.19.2",
"buffer": "^6.0.3",
"cors": "^2.8.5",
"dexie": "^3.2.1",
"events": "^3.3.0",
"express": "^4.17.3",
"fake-indexeddb": "^3.1.7",
"jest": "^27.5.1",
"jsonwebtoken": "^8.5.1",
"knex": "^1.0.4",
"node-cron": "^3.0.0",
"node-fetch": "2",
"node-watch": "^0.7.3",
"nodemon": "^2.0.15",
"vm2": "^3.9.9",
"ws": "^8.5.0",
"yaml": "^1.10.2",
"yargs": "^17.3.1"
},
"devDependencies": {
"@electron-forge/cli": "^6.0.0-beta.63",
"@electron-forge/maker-deb": "^6.0.0-beta.63",
"@electron-forge/maker-rpm": "^6.0.0-beta.63",
"@electron-forge/maker-squirrel": "^6.0.0-beta.63",
"@electron-forge/maker-zip": "^6.0.0-beta.63",
"electron": "18.0.3",
"electron-rebuild": "^3.2.7"
}
}

View File

@ -1,7 +0,0 @@
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
Arial, sans-serif;
margin: auto;
max-width: 38rem;
padding: 2rem;
}

View File

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello World!</title>
<link rel="stylesheet" href="index.css" />
</head>
<body>
<h1>💖 Hello World!</h1>
<p>Welcome to your Electron application.</p>
</body>
</html>

View File

@ -1,116 +0,0 @@
import { app, BrowserWindow, dialog, Menu } from "electron";
const path = require("path");
import { ExpressServer } from "@silverbulletmd/server/express_server";
import * as fs from "fs";
let mainWindow: BrowserWindow | undefined;
const mainMenuTemplate: Electron.MenuItemConstructorOptions[] = [
{
label: "File",
submenu: [
{
label: "Switch Folder",
click: async () => {
let result = await dialog.showOpenDialog(mainWindow!, {
properties: ["openDirectory"],
});
if (result.canceled) {
return;
}
openFolder(result.filePaths[0]).catch(console.error);
},
},
{
label: "Exit",
click: () => {
app.quit();
},
},
],
},
{ label: "Edit" },
{ label: "Tools", submenu: [{ label: "Sup" }] },
];
if (process.platform === "darwin") {
const name = "Fire Sale";
mainMenuTemplate.unshift({ label: name });
}
const mainMenu = Menu.buildFromTemplate(mainMenuTemplate);
Menu.setApplicationMenu(mainMenu);
const port = 3002;
const distDir = path.resolve(
`${__dirname}/../../packages/silverbullet-web/dist`
);
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require("electron-squirrel-startup")) {
// eslint-disable-line global-require
app.quit();
}
let currentFolder = app.getPath("userData");
fs.mkdirSync(currentFolder, { recursive: true });
let server: ExpressServer | undefined;
async function openFolder(path: string) {
console.log("Opening folder", path);
if (server) {
await server.stop();
}
currentFolder = path;
console.log("Starting new server");
server = new ExpressServer(port, path, distDir);
await server.start();
console.log("Reloading page");
mainWindow!.reload();
}
async function createWindow() {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
},
});
await openFolder(currentFolder);
// and load the index.html of the app.
mainWindow.loadURL(`http://localhost:${port}`);
// Open the DevTools.
mainWindow.webContents.openDevTools();
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on("ready", async () => {
await createWindow();
});
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.

View File

@ -1,3 +0,0 @@
const { contextBridge } = require("electron");
contextBridge.exposeInMainWorld("desktop", true);

3
docs/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
data.db
_plug
_trash

14
docs/PLUGS.md Normal file
View File

@ -0,0 +1,14 @@
This file lists all plugs that SilverBullet will load. Run the `Plugs: Update` command to update and reload this list of plugs.
```yaml
- builtin:core
- builtin:emoji
- builtin:ghost
- builtin:git
- builtin:github
- builtin:markdown
- builtin:mattermost
- builtin:plugmd
- builtin:query
- builtin:tasks
```

55
docs/index.md Normal file
View File

@ -0,0 +1,55 @@
# Silver Bullet
Silver Bullet (SB) is a highly extensible, open source **personal knowledge playground**. At its core its a Markdown-based writing/note taking application that stores _pages_ (notes) as plain markdown files in a folder referred to as a _space_. Pages can be cross-linked using the `[[link to other page]]` syntax. This makes it a simple tool for [Personal Knowledge Management](https://en.wikipedia.org/wiki/Personal_knowledge_management). However, once you leverage its various extensions (called _plugs_) it can feel more like a _knowledge playground_, allowing you to annotate, combine and query your accumulated knowledge in creative ways, specific to you.
So what is it SB _really_? That is hard to answer. It can do a ton of stuff out of the box, and Im constantly finding new use cases. Its like... a silver bullet!
Heres how I use it today (but this has grown significantly over time):
* Basic note taking, e.g. meeting notes, notes on books I read, blogs I read, podcasts I listen to, movies I watch.
* Getting a quick glance at the work people in my team are doing by pulling data from our 1:1 notes, recent activity on Github (such as recent pull requests) and other sources.
* Writing:
* [My blog](https://zef.plus) is published via SBs [Ghost](https://ghost.org) plugin.
* An internal newsletter that I write is written in SB.
* Performance reviews for my team (I work as a people manager) are written and managed using SB (for which I extensively use SBs meta data features and query that data in various ways).
* A custom SB plugin aggregates data from our OpsGenie account every week, and publishes it to our [Mattermost](https://mattermost.com/) instance.
* It powers part of my smart home: I wired HomeBridge webhooks up to custom HTTP endpoints exposed by my custom smart home SB plug.
Thats a pretty crazy wide range of use cases!
I know, right?
**Disclaimer:** Silver Bullet is under heavy development and significant changes under the hood happen constantly. Its also low on automated tests and documentation. All this will improve over time. Ill do better, I promise.
[[🤯 Features]]
[[💡 Inspiration]]
[[🔌 Plugs]]
[[🔨 Development]]
## Installing and running Silver Bullet
To run a release version, you need to have a recent version of npm (8+) and node.js (16+) installed as well as some basic build infrastructure (make, cpp). Silver Bullet has only been tested on MacOS and Linux thus far.
To install and run, create a folder for your pages (can be empty or an existing folder with `.md` files) and run:
npx @silverbullet/server <path-to-folder>
Optionally you can use the `--port` argument to specify a HTTP port (defaults to `3000`) and you can pass a `--password` flag to require a password to access. Note this is a rather weak security mechanism, so its recommended to add additional layers of security on top of this if you run this on a public server somewhere (at least add TLS). Personally I run it on a tiny Linux VM on my server at home, and use a VPN (Tailscale) to access it from outside my home.
## Roadmap
More details on the [[🗺️ Roadmap]] page.
<!-- #query task render "template/tasks" -->
* [ ] [[🗺️ Roadmap@34]] Persistent recent commands (saved between sessions)
* [ ] [[🗺️ Roadmap@92]] Add ==marker== syntax
* [ ] [[🗺️ Roadmap@120]] Two finger tap gesture to bring up command palette
* [ ] [[🗺️ Roadmap@177]] Change indent level command
* [ ] [[🗺️ Roadmap@212]] Keyboard shortcuts for specific notes (e.g. `index` note)
* [ ] [[🗺️ Roadmap@276]] RevealJS slides plug
* [ ] [[🗺️ Roadmap@303]] Pinned notes and actions?
* [ ] [[🗺️ Roadmap@335]] Template for deadline, with 📅 emoji and perhaps defaulting to today?
* [ ] [[🗺️ Roadmap@411]] Use webauthn https://www.npmjs.com/package/webauthn
* [ ] [[🗺️ Roadmap@469]] Proper sign up and login
* [ ] [[🗺️ Roadmap@500]] Data store pagination API
* [ ] [[🗺️ Roadmap@532]] Hashtag plug:
* [ ] [[🗺️ Roadmap@656]] Extract `MarkdownEditor` component.
* [ ] [[🗺️ Roadmap@725]] PUT page with `If-Last-Modified-Before` type header. Rejects if not matching. Client creates a revision, navigates to it.
* [ ] [[🗺️ Roadmap@858]] Put retries exponential back off
<!-- /query -->

3
docs/template/tasks.md vendored Normal file
View File

@ -0,0 +1,3 @@
{{#each .}}
* [{{#if done}}x{{else}} {{/if}}] [[{{page}}@{{pos}}]] {{name}}
{{/each}}

3
docs/💡 Inspiration.md Normal file
View File

@ -0,0 +1,3 @@
Inspiration for Silver Bullet comes primarily from [Obsidian](https://obsidian.md/) and its various plugs (the work-in-progress plugs around querying and tasks are inspired by Obsidians tasks and dataview plugins), but also [Roam Research](https://roamresearch.com/) was an inspiration.
Why start something new? Neither of these tools are open source, and they make some different choices, specifically on how extensibility is implemented — more on the differences some time in the future.

21
docs/🔌 Plugs.md Normal file
View File

@ -0,0 +1,21 @@
Silver Bullet at its core is bare bones in terms of functionality, most of its power it gains from **plugs**.
Plugs are an extension mechanism (implemented using a library called `plugos` that runs plug code on the server in a sandboxed v8 node.js process, and in the browser using web workers). Plugs can hook into SB in various ways: plugs can extend the Markdown parser and its syntax, define new commands and keybindings, respond to various events triggered either on the server or client side, as well as run recurring and background tasks. Plugs can even define their own extension mechanisms through custom events. Each plug runs in its own sandboxed environment and communicates with SB via _syscalls_ that expose a vast range of functionality. Plugs can be loaded, unloaded and updated without having to restart SB itself.
Examples of functionality implemented as plugs:
* _Core functionality_ such as:
* Navigation between pages by clicking or hitting `Cmd/Ctrl-Enter`
* Page auto complete when using the `[[page link]]` syntax
* Indexing of cross-page links and automatically updating all references to them when a page is renamed
* Text editing commands such as bold (`Cmd/Ctrl-b`) and italics (`Cmd/Ctrl-i`) or quote or itemize entire sections.
* Full text indexing and search
* Slash commands such as `/today`, `/tomorrow` and `/meta` (to insert page meta data)
* Emoji auto complete using the `:emoji:` syntax
* An embedded query language that can be used to query various sets of indexed entities, such as:
* Tasks using the Markdown task syntax
* Page backlinks
* Page in your space and its meta data
* Data objects embedded in your pages
* Git integration
* Github integration

38
docs/🔨 Development.md Normal file
View File

@ -0,0 +1,38 @@
## Stack
Silver Bullet is written in [TypeScript](https://www.typescriptlang.org/) and built on top of the excellent [CodeMirror 6](https://codemirror.net/) editor component. Additional UI is built using React.js. [ParcelJS](https://parceljs.org/) is used to build both the front-end and back-end bundles. The server backend runs as a HTTP server on node.js using express.
## Development
This [Silver Bullet repo](https://github.com/zefhemel/silverbullet) is a monorepo using npm's "workspaces" feature.
Requirements: node 16+ and npm 8+ as well as C/C++ compilers (for compiling SQLite, on debian/ubuntu style systems you get these via the `build-essential` package)
To run, after clone:
```shell
# Install dependencies
npm install
# Run initial build (web app, server, etc.)
npm run build
# Again, to install the CLIs just built (plugos-bundler, silverbullet)
npm install
# Build built-in plugs
npm run build-plugs
# Launch server
npm run server -- <PATH-TO-YOUR-SPACE>
```
This `<PATH-TO-YOUR-SPACE>` can be any folder with markdown files, upon first boot SB will ensure there is an `index.md` file (root page) and `PLUGS.md` file (with default list of plugs to load). SB will also create a SQLite `data.db` file with various data caches and indices (you can delete this file at any time and use the `Space: Reindex` command to reindex everything).
Open SB at http://localhost:3000 If you're using a browser supporting PWAs, you can install this page as a PWA. This also works on iOS (use the "Add to homescreen" option in the share menu).
General development workflow:
Run these in separate terminals
```shell
# Runs ParcelJS in watch mode, rebuilding the server and webapp continuously on change
npm run watch
# Runs the silverbullet server
npm run server
# Builds (and watches for changes) all builtin plugs (in packages/plugs)
npm run plugs
```

22
docs/🗺️ Roadmap.md Normal file
View File

@ -0,0 +1,22 @@
Some things I want to work on:
* [ ] Persistent recent commands (saved between sessions)
* [ ] Add ==marker== syntax
* [ ] Two finger tap gesture to bring up command palette
* [ ] Change indent level command
* [ ] Keyboard shortcuts for specific notes (e.g. `index` note)
* [ ] RevealJS slides plug
* [ ] Pinned notes and actions?
* [ ] Template for deadline, with 📅 emoji and perhaps defaulting to today?
* [ ] Use webauthn https://www.npmjs.com/package/webauthn
* [ ] Proper sign up and login
* [ ] Data store pagination API
* [ ] Hashtag plug:
* Higlighting
* Page indexing/item indexing
* Tag completion
* Query providers: ht-page ht-item
* [ ] Extract `MarkdownEditor` component.
* REST API safeguards:
* [ ] PUT page with `If-Last-Modified-Before` type header. Rejects if not matching. Client creates a revision, navigates to it.
* [ ] Put retries exponential back off

10
docs/🤯 Features.md Normal file
View File

@ -0,0 +1,10 @@
* **Free and open source** (MIT licensed)
* **Distraction free** UI with [What You See is What You Mean](https://en.wikipedia.org/wiki/WYSIWYM) Markdown editing.
* **Future proof**: stores all notes in a regular folder with markdown files, no proprietary file formats. While SB uses a SQLite database for indexes, this database can be wiped and rebuilt based on your pages at any time. Your Markdown files are the single source of truth.
* **Run anywhere**: run it on your local machine, or install it on a server. You access it via your web browser (desktop or mobile), or install it as a PWA (giving it its own window frame and dock/launcher/dock icon).
* **Keyboard oriented:** you can fully operate SB via the keyboard (on laptop/desktop machines as well as iPads with a keyboard):
* Switch between pages using `Cmd-k` (Mac) or `Ctrl-k` (Linux/Windows)
* Open the command palette using `Cmd-/` (Mac) or `Ctrl-/` (Linux/Windows)
* Use slash commands by entering a `/` in your notes text
* Various plugs add additional keyboard shortcuts and auto completions like when entering `[[page links]]` and typing emoji via the `:lemon:` syntax.
* **Extensible** through plugs (see below)

1
index.md Normal file
View File

@ -0,0 +1 @@
Welcome to your new space!

2380
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,6 @@
{ {
"name": "silverbulletmd", "name": "silverbulletmd",
"private": true, "private": true,
"version": "0.0.1",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"watch": "rm -rf .parcel-cache && parcel watch --no-hmr packages/{web,server,plugos,plugs} desktop", "watch": "rm -rf .parcel-cache && parcel watch --no-hmr packages/{web,server,plugos,plugs} desktop",
@ -9,8 +8,10 @@
"nuke": "rm -rf node_modules && npm run clean", "nuke": "rm -rf node_modules && npm run clean",
"build": "parcel build packages/{web,server,plugos}", "build": "parcel build packages/{web,server,plugos}",
"plugs": "cd packages/plugs && npm run watch", "plugs": "cd packages/plugs && npm run watch",
"server": "nodemon -w packages/server/dist --exec 'silverbullet pages'", "build-plugs": "cd packages/plugs && npm run build",
"server": "nodemon -w packages/server/dist --exec silverbullet",
"test": "jest packages/*/dist/test", "test": "jest packages/*/dist/test",
"clean-build": "npm run clean && npm run build && npm i && npm run build-plugs",
"publish-all": "npm publish --access public --ws" "publish-all": "npm publish --access public --ws"
}, },
"devDependencies": { "devDependencies": {

View File

@ -4,7 +4,7 @@
"name": "Zef Hemel", "name": "Zef Hemel",
"email": "zef@zef.me" "email": "zef@zef.me"
}, },
"version": "0.0.2", "version": "0.0.7",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "6.0.1", "@codemirror/autocomplete": "6.0.1",
@ -16,8 +16,12 @@
"@codemirror/search": "6.0.0", "@codemirror/search": "6.0.0",
"@codemirror/state": "6.0.0", "@codemirror/state": "6.0.0",
"@codemirror/view": "6.0.0", "@codemirror/view": "6.0.0",
"@lezer/common": "1.0.0",
"@lezer/highlight": "1.0.0", "@lezer/highlight": "1.0.0",
"@lezer/markdown": "1.0.0", "@lezer/markdown": "1.0.0",
"@lezer/common": "1.0.0" "@silverbulletmd/common": "^0.0.7",
"@silverbulletmd/plugs": "^0.0.7",
"@silverbulletmd/web": "^0.0.7",
"server": "^1.0.37"
} }
} }

View File

@ -107,7 +107,7 @@ export class DiskSpacePrimitives implements SpacePrimitives {
perm: "rw", perm: "rw",
}; };
} catch (e) { } catch (e) {
console.error("Error while getting page meta", pageName, e); // console.error("Error while getting page meta", pageName, e);
throw Error(`Could not get meta for ${pageName}`); throw Error(`Could not get meta for ${pageName}`);
} }
} }

View File

@ -18,17 +18,17 @@ test("Test store", async () => {
async function conflictResolver(pageMeta1: PageMeta, pageMeta2: PageMeta) {} async function conflictResolver(pageMeta1: PageMeta, pageMeta2: PageMeta) {}
// Write one page to primary // Write one page to primary
await primary.writePage("start", "Hello"); await primary.writePage("index", "Hello");
expect((await secondary.listPages()).size).toBe(0); expect((await secondary.listPages()).size).toBe(0);
await syncPages(conflictResolver); await syncPages(conflictResolver);
expect((await secondary.listPages()).size).toBe(1); expect((await secondary.listPages()).size).toBe(1);
expect((await secondary.readPage("start")).text).toBe("Hello"); expect((await secondary.readPage("index")).text).toBe("Hello");
// Should be a no-op // Should be a no-op
expect(await syncPages()).toBe(0); expect(await syncPages()).toBe(0);
// Now let's make a change on the secondary // Now let's make a change on the secondary
await secondary.writePage("start", "Hello!!"); await secondary.writePage("index", "Hello!!");
await secondary.writePage("test", "Test page"); await secondary.writePage("test", "Test page");
// And sync it // And sync it
@ -37,13 +37,13 @@ test("Test store", async () => {
expect(primary.listPages().size).toBe(2); expect(primary.listPages().size).toBe(2);
expect(secondary.listPages().size).toBe(2); expect(secondary.listPages().size).toBe(2);
expect((await primary.readPage("start")).text).toBe("Hello!!"); expect((await primary.readPage("index")).text).toBe("Hello!!");
// Let's make some random edits on both ends // Let's make some random edits on both ends
await primary.writePage("start", "1"); await primary.writePage("index", "1");
await primary.writePage("start2", "2"); await primary.writePage("index2", "2");
await secondary.writePage("start3", "3"); await secondary.writePage("index3", "3");
await secondary.writePage("start4", "4"); await secondary.writePage("index4", "4");
await syncPages(); await syncPages();
expect((await primary.listPages()).size).toBe(5); expect((await primary.listPages()).size).toBe(5);
@ -53,8 +53,8 @@ test("Test store", async () => {
console.log("Deleting pages"); console.log("Deleting pages");
// Delete some pages // Delete some pages
await primary.deletePage("start"); await primary.deletePage("index");
await primary.deletePage("start3"); await primary.deletePage("index3");
console.log("Pages", await primary.listPages()); console.log("Pages", await primary.listPages());
console.log("Trash", await primary.listTrash()); console.log("Trash", await primary.listTrash());
@ -67,8 +67,8 @@ test("Test store", async () => {
// No-op // No-op
expect(await syncPages()).toBe(0); expect(await syncPages()).toBe(0);
await secondary.deletePage("start4"); await secondary.deletePage("index4");
await primary.deletePage("start2"); await primary.deletePage("index2");
await syncPages(); await syncPages();
@ -79,15 +79,15 @@ test("Test store", async () => {
// No-op // No-op
expect(await syncPages()).toBe(0); expect(await syncPages()).toBe(0);
await secondary.writePage("start", "I'm back"); await secondary.writePage("index", "I'm back");
await syncPages(); await syncPages();
expect((await primary.readPage("start")).text).toBe("I'm back"); expect((await primary.readPage("index")).text).toBe("I'm back");
// Cause a conflict // Cause a conflict
await primary.writePage("start", "Hello 1"); await primary.writePage("index", "Hello 1");
await secondary.writePage("start", "Hello 2"); await secondary.writePage("index", "Hello 2");
await syncPages(SpaceSync.primaryConflictResolver(primary, secondary)); await syncPages(SpaceSync.primaryConflictResolver(primary, secondary));
@ -95,10 +95,10 @@ test("Test store", async () => {
await syncPages(); await syncPages();
// Verify that primary won // Verify that primary won
expect((await primary.readPage("start")).text).toBe("Hello 1"); expect((await primary.readPage("index")).text).toBe("Hello 1");
expect((await secondary.readPage("start")).text).toBe("Hello 1"); expect((await secondary.readPage("index")).text).toBe("Hello 1");
// test + start + start.conflicting copy // test + index + index.conflicting copy
expect((await primary.listPages()).size).toBe(3); expect((await primary.listPages()).size).toBe(3);
expect((await secondary.listPages()).size).toBe(3); expect((await secondary.listPages()).size).toBe(3);

View File

@ -4,6 +4,12 @@
"name": "Zef Hemel", "name": "Zef Hemel",
"email": "zef@zef.me" "email": "zef@zef.me"
}, },
"version": "0.0.2", "version": "0.0.7",
"license": "MIT" "license": "MIT",
"dependencies": {
"@silverbulletmd/common": "^0.0.7",
"@silverbulletmd/plugs": "^0.0.7",
"@silverbulletmd/web": "^0.0.7",
"server": "^1.0.37"
}
} }

View File

@ -4,6 +4,12 @@
"name": "Zef Hemel", "name": "Zef Hemel",
"email": "zef@zef.me" "email": "zef@zef.me"
}, },
"version": "0.0.2", "version": "0.0.7",
"license": "MIT" "license": "MIT",
"dependencies": {
"@silverbulletmd/common": "^0.0.7",
"@silverbulletmd/plugs": "^0.0.7",
"@silverbulletmd/web": "^0.0.7",
"server": "^1.0.37"
}
} }

View File

@ -4,7 +4,7 @@
"name": "Zef Hemel", "name": "Zef Hemel",
"email": "zef@zef.me" "email": "zef@zef.me"
}, },
"version": "0.0.2", "version": "0.0.7",
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"plugos-bundle": "./dist/plugos/plugos-bundle.js", "plugos-bundle": "./dist/plugos/plugos-bundle.js",
@ -40,6 +40,9 @@
}, },
"dependencies": { "dependencies": {
"@jest/globals": "^27.5.1", "@jest/globals": "^27.5.1",
"@silverbulletmd/common": "^0.0.7",
"@silverbulletmd/plugs": "^0.0.7",
"@silverbulletmd/web": "^0.0.7",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/jsonwebtoken": "^8.5.8", "@types/jsonwebtoken": "^8.5.8",
@ -56,12 +59,13 @@
"node-cron": "^3.0.0", "node-cron": "^3.0.0",
"node-fetch": "2", "node-fetch": "2",
"node-watch": "^0.7.3", "node-watch": "^0.7.3",
"server": "^1.0.37",
"supertest": "^6.2.2", "supertest": "^6.2.2",
"typescript": "^4.6.2",
"vm2": "^3.9.9", "vm2": "^3.9.9",
"ws": "^8.5.0", "ws": "^8.5.0",
"yaml": "^1.10.2", "yaml": "^1.10.2",
"yargs": "^17.3.1", "yargs": "^17.3.1"
"typescript": "^4.6.2"
}, },
"devDependencies": { "devDependencies": {
"@lezer/lr": "1.0.0", "@lezer/lr": "1.0.0",

View File

@ -279,7 +279,6 @@ function $11a7e2bff790f35a$export$195ba6d62321b933(message, defaultValue = "") {
} }
const $c3893eec0c49ec96$var$dateMatchRegex = /(\d{4}\-\d{2}\-\d{2})/g;
function $c3893eec0c49ec96$export$5dc1410f87262ed6(d) { function $c3893eec0c49ec96$export$5dc1410f87262ed6(d) {
return d.toISOString().split("T")[0]; return d.toISOString().split("T")[0];
} }

File diff suppressed because one or more lines are too long

View File

@ -35,6 +35,13 @@ functions:
path: "./page.ts:parseIndexTextRepublish" path: "./page.ts:parseIndexTextRepublish"
events: events:
- page:index_text - page:index_text
reindexSpaceCommand:
path: "./page.ts:reindexCommand"
command:
name: "Space: Reindex"
reindexSpace:
path: "./page.ts:reindexSpace"
env: server
indexLinks: indexLinks:
path: "./page.ts:indexLinks" path: "./page.ts:indexLinks"
events: events:
@ -43,25 +50,10 @@ functions:
path: ./page.ts:linkQueryProvider path: ./page.ts:linkQueryProvider
events: events:
- query:link - query:link
# indexItems:
# path: "./item.ts:indexItems"
# events:
# - page:index
# itemQueryProvider:
# path: ./item.ts:queryProvider
# events:
# - query:item
deletePage: deletePage:
path: "./page.ts:deletePage" path: "./page.ts:deletePage"
command: command:
name: "Page: Delete" name: "Page: Delete"
reindexSpaceCommand:
path: "./page.ts:reindexCommand"
command:
name: "Space: Reindex"
reindexSpace:
path: "./page.ts:reindexSpace"
env: server
renamePage: renamePage:
path: "./page.ts:renamePage" path: "./page.ts:renamePage"
command: command:
@ -82,38 +74,39 @@ functions:
path: "./navigate.ts:clickNavigate" path: "./navigate.ts:clickNavigate"
events: events:
- page:click - page:click
insertToday:
path: "./dates.ts:insertToday" # Full text search
slashCommand: searchIndex:
name: today path: ./search.ts:index
insertTomorrow:
path: "./dates.ts:insertTomorrow"
slashCommand:
name: tomorrow
parseServerCommand:
path: ./debug.ts:parseServerPageCommand
command:
name: "Debug: Parse Document on Server"
parsePage:
path: ./debug.ts:parsePage
parseCommand:
path: ./debug.ts:parsePageCommand
command:
name: "Debug: Parse Document"
showLogsCommand:
path: ./debug.ts:showLogsCommand
command:
name: "Debug: Show Logs"
key: "Ctrl-Alt-l"
mac: "Cmd-Alt-l"
events: events:
- log:reload - page:index
hideBhsCommand: searchUnindex:
path: ./debug.ts:hideBhsCommand path: "./search.ts:unindex"
env: server
events:
- page:deleted
searchQueryProvider:
path: ./search.ts:queryProvider
events:
- query:full-text
searchCommand:
path: ./search.ts:searchCommand
command: command:
name: "UI: Hide BHS" name: "Search Space"
key: "Ctrl-Alt-b" key: Ctrl-Shift-f
mac: "Cmd-Alt-b" mac: Cmd-Shift-f
readPageSearch:
path: ./search.ts:readPageSearch
pageNamespace:
pattern: "@search/.+"
operation: readPage
getPageMetaSearch:
path: ./search.ts:getPageMetaSearch
pageNamespace:
pattern: "@search/.+"
operation: getPageMeta
# Template commands
insertPageMeta: insertPageMeta:
path: "./page.ts:insertPageMeta" path: "./page.ts:insertPageMeta"
slashCommand: slashCommand:
@ -132,7 +125,16 @@ functions:
path: ./template.ts:instantiateTemplateCommand path: ./template.ts:instantiateTemplateCommand
command: command:
name: "Template: Instantiate for Page" name: "Template: Instantiate for Page"
insertToday:
path: "./dates.ts:insertToday"
slashCommand:
name: today
insertTomorrow:
path: "./dates.ts:insertTomorrow"
slashCommand:
name: tomorrow
# Text editing commands
quoteSelection: quoteSelection:
path: ./text.ts:quoteSelection path: ./text.ts:quoteSelection
command: command:
@ -159,3 +161,40 @@ functions:
name: "Text: Italic" name: "Text: Italic"
key: "Ctrl-i" key: "Ctrl-i"
mac: "Cmd-i" mac: "Cmd-i"
# Plug manager
updatePlugsCommand:
path: ./plugmanager.ts:updatePlugsCommand
command:
name: "Plugs: Update"
key: "Ctrl-Shift-p"
mac: "Cmd-Shift-p"
updatePlugs:
path: ./plugmanager.ts:updatePlugs
env: server
# Debug commands
parseServerCommand:
path: ./debug.ts:parseServerPageCommand
command:
name: "Debug: Parse Document on Server"
parsePage:
path: ./debug.ts:parsePage
parseCommand:
path: ./debug.ts:parsePageCommand
command:
name: "Debug: Parse Document"
showLogsCommand:
path: ./debug.ts:showLogsCommand
command:
name: "Debug: Show Logs"
key: "Ctrl-Alt-l"
mac: "Cmd-Alt-l"
events:
- log:reload
hideBhsCommand:
path: ./debug.ts:hideBhsCommand
command:
name: "UI: Hide BHS"
key: "Ctrl-Alt-b"
mac: "Cmd-Alt-b"

View File

@ -1,7 +1,5 @@
import { insertAtCursor } from "@silverbulletmd/plugos-silverbullet-syscall/editor"; import { insertAtCursor } from "@silverbulletmd/plugos-silverbullet-syscall/editor";
const dateMatchRegex = /(\d{4}\-\d{2}\-\d{2})/g;
export function niceDate(d: Date): string { export function niceDate(d: Date): string {
return d.toISOString().split("T")[0]; return d.toISOString().split("T")[0];
} }

View File

@ -1,5 +1,4 @@
import { getLogs } from "@plugos/plugos-syscall/sandbox"; import { getLogs } from "@plugos/plugos-syscall/sandbox";
import { LogEntry } from "@plugos/plugos/sandbox";
import { import {
getText, getText,
hideBhs, hideBhs,

View File

@ -1,38 +0,0 @@
import {
getCursor,
getText,
insertAtPos,
replaceRange,
} from "@silverbulletmd/plugos-silverbullet-syscall/editor";
export async function toggleH1() {
await togglePrefix("# ");
}
export async function toggleH2() {
await togglePrefix("## ");
}
function lookBack(s: string, pos: number, backString: string): boolean {
return s.substring(pos - backString.length, pos) === backString;
}
async function togglePrefix(prefix: string) {
let text = await getText();
let pos = await getCursor();
if (text[pos] === "\n") {
pos--;
}
while (pos > 0 && text[pos] !== "\n") {
if (lookBack(text, pos, prefix)) {
// Already has this prefix, let's flip it
await replaceRange(pos - prefix.length, pos, "");
return;
}
pos--;
}
if (pos) {
pos++;
}
await insertAtPos(prefix, pos);
}

View File

@ -8,8 +8,6 @@ import {
import { parseMarkdown } from "@silverbulletmd/plugos-silverbullet-syscall/markdown"; import { parseMarkdown } from "@silverbulletmd/plugos-silverbullet-syscall/markdown";
import { nodeAtPos, ParseTree } from "@silverbulletmd/common/tree"; import { nodeAtPos, ParseTree } from "@silverbulletmd/common/tree";
const materializedQueryPrefix = /<!--\s*#query\s+/;
async function actionClickOrActionEnter(mdTree: ParseTree | null) { async function actionClickOrActionEnter(mdTree: ParseTree | null) {
if (!mdTree) { if (!mdTree) {
return; return;

View File

@ -35,9 +35,7 @@ import {
replaceNodesMatching, replaceNodesMatching,
} from "@silverbulletmd/common/tree"; } from "@silverbulletmd/common/tree";
import { applyQuery, QueryProviderEvent } from "../query/engine"; import { applyQuery, QueryProviderEvent } from "../query/engine";
import { PageMeta } from "@silverbulletmd/common/types";
import { extractMeta } from "../query/data"; import { extractMeta } from "../query/data";
import { jsonToMDTable } from "../query/util";
// Key space: // Key space:
// pl:toPage:pos => pageName // pl:toPage:pos => pageName
@ -101,8 +99,8 @@ export async function linkQueryProvider({
export async function deletePage() { export async function deletePage() {
let pageName = await getCurrentPage(); let pageName = await getCurrentPage();
console.log("Navigating to start page"); console.log("Navigating to index page");
await navigate("start"); await navigate("index");
console.log("Deleting page from space"); console.log("Deleting page from space");
await deletePageSyscall(pageName); await deletePageSyscall(pageName);
} }

View File

@ -0,0 +1,69 @@
import { dispatch } from "@plugos/plugos-syscall/event";
import { findNodeOfType } from "@silverbulletmd/common/tree";
import { flashNotification } from "@silverbulletmd/plugos-silverbullet-syscall/editor";
import { parseMarkdown } from "@silverbulletmd/plugos-silverbullet-syscall/markdown";
import {
deletePage,
listPages,
readPage,
writePage,
} from "@silverbulletmd/plugos-silverbullet-syscall/space";
import {
invokeFunction,
reloadPlugs,
} from "@silverbulletmd/plugos-silverbullet-syscall/system";
import YAML from "yaml";
async function listPlugs(): Promise<string[]> {
let unfilteredPages = await listPages(true);
return unfilteredPages
.filter((p) => p.name.startsWith("_plug/"))
.map((p) => p.name.substring("_plug/".length));
}
export async function updatePlugsCommand() {
flashNotification("Updating plugs...");
await invokeFunction("server", "updatePlugs");
flashNotification("And... done!");
await reloadPlugs();
}
export async function updatePlugs() {
let { text: plugPageText } = await readPage("PLUGS");
let tree = await parseMarkdown(plugPageText);
let codeTextNode = findNodeOfType(tree, "CodeText");
if (!codeTextNode) {
console.error("Could not find yaml block in PLUGS");
return;
}
let plugYaml = codeTextNode.children![0].text;
let plugList = YAML.parse(plugYaml!);
console.log("Plug YAML", plugList);
let allPlugNames: string[] = [];
for (let plugUri of plugList) {
let [protocol, ...rest] = plugUri.split(":");
let manifests = await dispatch(`get-plug:${protocol}`, rest.join(":"));
if (manifests.length === 0) {
console.error("Could not resolve plug", plugUri);
}
// console.log("Got manifests", plugUri, protocol, manifests);
let manifest = manifests[0];
allPlugNames.push(manifest.name);
// console.log("Writing", `_plug/${manifest.name}`);
await writePage(
`_plug/${manifest.name}`,
JSON.stringify(manifest, null, 2)
);
}
// And delete extra ones
for (let existingPlug of await listPlugs()) {
if (!allPlugNames.includes(existingPlug)) {
console.log("Removing plug", existingPlug);
await deletePage(`_plug/${existingPlug}`);
}
}
await reloadPlugs();
}

View File

@ -1,4 +1,8 @@
import { fullTextIndex, fullTextSearch } from "@plugos/plugos-syscall/fulltext"; import {
fullTextDelete,
fullTextIndex,
fullTextSearch,
} from "@plugos/plugos-syscall/fulltext";
import { renderToText } from "@silverbulletmd/common/tree"; import { renderToText } from "@silverbulletmd/common/tree";
import { PageMeta } from "@silverbulletmd/common/types"; import { PageMeta } from "@silverbulletmd/common/types";
import { queryPrefix } from "@silverbulletmd/plugos-silverbullet-syscall"; import { queryPrefix } from "@silverbulletmd/plugos-silverbullet-syscall";
@ -16,6 +20,10 @@ export async function index(data: IndexTreeEvent) {
await fullTextIndex(data.name, cleanText); await fullTextIndex(data.name, cleanText);
} }
export async function unindex(pageName: string) {
await fullTextDelete(pageName);
}
export async function queryProvider({ export async function queryProvider({
query, query,
}: QueryProviderEvent): Promise<any[]> { }: QueryProviderEvent): Promise<any[]> {

View File

@ -1,12 +0,0 @@
import {
EndpointRequest,
EndpointResponse,
} from "@plugos/plugos/hooks/endpoint";
export function endpointTest(req: EndpointRequest): EndpointResponse {
console.log("I'm running on the server!", req);
return {
status: 200,
body: "Hello world!",
};
}

View File

@ -1,9 +0,0 @@
function countWords(str: string): number {
const matches = str.match(/[\w\d\'-]+/gi);
return matches ? matches.length : 0;
}
function readingTime(wordCount: number): number {
// 225 is average word reading speed for adults
return Math.ceil(wordCount / 225);
}

View File

@ -2,8 +2,6 @@
import emojis from "./emoji.json"; import emojis from "./emoji.json";
import { matchBefore } from "@silverbulletmd/plugos-silverbullet-syscall/editor"; import { matchBefore } from "@silverbulletmd/plugos-silverbullet-syscall/editor";
const emojiMatcher = /\(([^\)]+)\)\s+(.+)$/;
export async function emojiCompleter() { export async function emojiCompleter() {
let prefix = await matchBefore(":[\\w]+"); let prefix = await matchBefore(":[\\w]+");
if (!prefix) { if (!prefix) {

View File

@ -1,12 +1,5 @@
name: ghost name: ghost
functions: functions:
downloadAllPostsCommand:
path: "./ghost.ts:downloadAllPostsCommand"
command:
name: "Ghost: Download Posts"
downloadAllPosts:
path: "./ghost.ts:downloadAllPosts"
env: server
publishCommand: publishCommand:
path: "./ghost.ts:publishCommand" path: "./ghost.ts:publishCommand"
command: command:

View File

@ -1,7 +1,4 @@
import { import { readPage } from "@silverbulletmd/plugos-silverbullet-syscall/space";
readPage,
writePage,
} from "@silverbulletmd/plugos-silverbullet-syscall/space";
import { invokeFunction } from "@silverbulletmd/plugos-silverbullet-syscall/system"; import { invokeFunction } from "@silverbulletmd/plugos-silverbullet-syscall/system";
import { import {
getCurrentPage, getCurrentPage,
@ -195,21 +192,6 @@ async function getConfig(): Promise<GhostConfig> {
return pageMeta as GhostConfig; return pageMeta as GhostConfig;
} }
export async function downloadAllPostsCommand() {
await invokeFunction("server", "downloadAllPosts");
}
export async function downloadAllPosts() {
let config = await getConfig();
let admin = new GhostAdmin(config.url, config.adminKey);
await admin.init();
let allPosts = await admin.listMarkdownPosts();
for (let post of allPosts) {
let text = mobileDocToMarkdown(post.mobiledoc);
text = `# ${post.title}\n${text}`;
await writePage(`${config.postPrefix}/${post.slug}`, text);
}
}
export async function publishCommand() { export async function publishCommand() {
await invokeFunction( await invokeFunction(
"server", "server",

View File

@ -7,7 +7,6 @@ export async function togglePreview() {
await clientStore.set("enableMarkdownPreview", !currentValue); await clientStore.set("enableMarkdownPreview", !currentValue);
if (!currentValue) { if (!currentValue) {
await invokeFunction("client", "preview"); await invokeFunction("client", "preview");
// updateMarkdownPreview();
} else { } else {
await hideMarkdownPreview(); await hideMarkdownPreview();
} }

View File

@ -1,12 +1,12 @@
{ {
"name": "@silverbulletmd/plugs", "name": "@silverbulletmd/plugs",
"version": "0.0.2", "version": "0.0.7",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@silverbulletmd/plugs", "name": "@silverbulletmd/plugs",
"version": "0.0.2", "version": "0.0.7",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jest/globals": "^27.5.1", "@jest/globals": "^27.5.1",

View File

@ -4,7 +4,7 @@
"name": "Zef Hemel", "name": "Zef Hemel",
"email": "zef@zef.me" "email": "zef@zef.me"
}, },
"version": "0.0.2", "version": "0.0.7",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"generate": "lezer-generator query/query.grammar -o query/parse-query.js", "generate": "lezer-generator query/query.grammar -o query/parse-query.js",
@ -34,13 +34,17 @@
"@jest/globals": "^27.5.1", "@jest/globals": "^27.5.1",
"@lezer/generator": "1.0.0", "@lezer/generator": "1.0.0",
"@lezer/lr": "1.0.0", "@lezer/lr": "1.0.0",
"@mattermost/client": "^6.7.0-0", "@mattermost/client": "^7.0.0",
"@mattermost/types": "^6.7.0-0", "@mattermost/types": "^7.0.0",
"@silverbulletmd/common": "^0.0.7",
"@silverbulletmd/plugs": "^0.0.7",
"@silverbulletmd/web": "^0.0.7",
"@types/yaml": "^1.9.7", "@types/yaml": "^1.9.7",
"handlebars": "^4.7.7",
"markdown-it": "^12.3.2", "markdown-it": "^12.3.2",
"markdown-it-task-lists": "^2.1.1", "markdown-it-task-lists": "^2.1.1",
"yaml": "^2.0.0", "server": "^1.0.37",
"handlebars": "^4.7.7" "yaml": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/markdown-it": "^12.2.3" "@types/markdown-it": "^12.2.3"

View File

@ -1,33 +0,0 @@
name: plugmanager
functions:
updatePlugsCommand:
path: ./plugmanager.ts:updatePlugsCommand
command:
name: "Plugs: Update"
key: "Ctrl-Shift-p"
mac: "Cmd-Shift-p"
updatePlugs:
path: ./plugmanager.ts:updatePlugs
env: server
check:
path: "./plugmanager.ts:checkCommand"
command:
name: "Plug: Check"
mac: "Cmd-Alt-c"
key: "Ctrl-Alt-c"
compile:
path: "./plugmanager.ts:compileCommand"
command:
name: "Plug: Compile"
mac: "Cmd-Shift-c"
key: "Ctrl-Shift-c"
compileJS:
path: "./plugmanager.ts:compileJS"
env: server
compileModule:
path: "./plugmanager.ts:compileModule"
env: server
getPlugPlugMd:
path: "./plugmanager.ts:getPlugPlugMd"
events:
- get-plug:plugmd

View File

@ -0,0 +1,24 @@
name: plugmd
functions:
check:
path: "./plugmd.ts:checkCommand"
command:
name: "Plug: Check"
mac: "Cmd-Alt-c"
key: "Ctrl-Alt-c"
compile:
path: "./plugmd.ts:compileCommand"
command:
name: "Plug: Compile"
mac: "Cmd-Shift-c"
key: "Ctrl-Shift-c"
compileJS:
path: "./plugmd.ts:compileJS"
env: server
compileModule:
path: "./plugmd.ts:compileModule"
env: server
getPlugPlugMd:
path: "./plugmd.ts:getPlugPlugMd"
events:
- get-plug:plugmd

View File

@ -1,19 +1,14 @@
import { dispatch } from "@plugos/plugos-syscall/event";
import { import {
addParentPointers,
collectNodesOfType, collectNodesOfType,
findNodeOfType, findNodeOfType,
} from "@silverbulletmd/common/tree"; } from "@silverbulletmd/common/tree";
import { import {
flashNotification,
getText, getText,
hideBhs, hideBhs,
showBhs, showBhs,
} from "@silverbulletmd/plugos-silverbullet-syscall/editor"; } from "@silverbulletmd/plugos-silverbullet-syscall/editor";
import { parseMarkdown } from "@silverbulletmd/plugos-silverbullet-syscall/markdown"; import { parseMarkdown } from "@silverbulletmd/plugos-silverbullet-syscall/markdown";
import { import {
deletePage,
listPages,
readPage, readPage,
writePage, writePage,
} from "@silverbulletmd/plugos-silverbullet-syscall/space"; } from "@silverbulletmd/plugos-silverbullet-syscall/space";
@ -23,8 +18,6 @@ import {
} from "@silverbulletmd/plugos-silverbullet-syscall/system"; } from "@silverbulletmd/plugos-silverbullet-syscall/system";
import YAML from "yaml"; import YAML from "yaml";
import { extractMeta } from "../query/data";
import type { Manifest } from "@silverbulletmd/common/manifest"; import type { Manifest } from "@silverbulletmd/common/manifest";
export async function compileCommand() { export async function compileCommand() {
@ -131,64 +124,6 @@ export async function compileModule(moduleName: string): Promise<string> {
return self.syscall("esbuild.compileModule", moduleName); return self.syscall("esbuild.compileModule", moduleName);
} }
async function listPlugs(): Promise<string[]> {
let unfilteredPages = await listPages(true);
return unfilteredPages
.filter((p) => p.name.startsWith("_plug/"))
.map((p) => p.name.substring("_plug/".length));
}
export async function listCommand() {
console.log(await listPlugs());
}
export async function updatePlugsCommand() {
flashNotification("Updating plugs...");
await invokeFunction("server", "updatePlugs");
flashNotification("And... done!");
await reloadPlugs();
}
export async function updatePlugs() {
let { text: plugPageText } = await readPage("PLUGS");
let tree = await parseMarkdown(plugPageText);
let codeTextNode = findNodeOfType(tree, "CodeText");
if (!codeTextNode) {
console.error("Could not find yaml block in PLUGS");
return;
}
let plugYaml = codeTextNode.children![0].text;
let plugList = YAML.parse(plugYaml!);
console.log("Plug YAML", plugList);
let allPlugNames: string[] = [];
for (let plugUri of plugList) {
let [protocol, ...rest] = plugUri.split(":");
let manifests = await dispatch(`get-plug:${protocol}`, rest.join(":"));
if (manifests.length === 0) {
console.error("Could not resolve plug", plugUri);
}
// console.log("Got manifests", plugUri, protocol, manifests);
let manifest = manifests[0];
allPlugNames.push(manifest.name);
// console.log("Writing", `_plug/${manifest.name}`);
await writePage(
`_plug/${manifest.name}`,
JSON.stringify(manifest, null, 2)
);
}
// And delete extra ones
for (let existingPlug of await listPlugs()) {
if (!allPlugNames.includes(existingPlug)) {
console.log("Removing plug", existingPlug);
await deletePage(`_plug/${existingPlug}`);
}
}
await reloadPlugs();
}
export async function getPlugPlugMd(pageName: string): Promise<Manifest> { export async function getPlugPlugMd(pageName: string): Promise<Manifest> {
let { text } = await readPage(pageName); let { text } = await readPage(pageName);
console.log("Compiling", pageName); console.log("Compiling", pageName);

View File

@ -1,7 +1,4 @@
name: query name: query
# dependencies:
# yaml: "yaml@2"
# "@lezer/lr": "@lezer/lr@0.15.4"
functions: functions:
updateMaterializedQueriesOnPage: updateMaterializedQueriesOnPage:
path: ./materialized_queries.ts:updateMaterializedQueriesOnPage path: ./materialized_queries.ts:updateMaterializedQueriesOnPage

View File

@ -1,26 +0,0 @@
name: search
functions:
index:
path: ./search.ts:index
events:
- page:index
queryProvider:
path: ./search.ts:queryProvider
events:
- query:full-text
searchCommand:
path: ./search.ts:searchCommand
command:
name: "Search Space"
key: Ctrl-Shift-f
mac: Cmd-Shift-f
readPageSearch:
path: ./search.ts:readPageSearch
pageNamespace:
pattern: "@search/.+"
operation: readPage
getPageMetaSearch:
path: ./search.ts:getPageMetaSearch
pageNamespace:
pattern: "@search/.+"
operation: getPageMeta

View File

@ -41,7 +41,7 @@ functions:
taskPostponeCommand: taskPostponeCommand:
path: ./task.ts:postponeCommand path: ./task.ts:postponeCommand
command: command:
name: "Task: Postpone by 1 day" name: "Task: Postpone"
key: Alt-+ key: Alt-+
contexts: contexts:
- DeadlineDate - DeadlineDate

View File

@ -57,7 +57,7 @@ export type ServerOptions = {
pagesPath: string; pagesPath: string;
distDir: string; distDir: string;
builtinPlugDir: string; builtinPlugDir: string;
token?: string; password?: string;
}; };
export class ExpressServer { export class ExpressServer {
app: Express; app: Express;
@ -69,21 +69,27 @@ export class ExpressServer {
private port: number; private port: number;
private server?: Server; private server?: Server;
builtinPlugDir: string; builtinPlugDir: string;
token?: string; password?: string;
constructor(options: ServerOptions) { constructor(options: ServerOptions) {
this.port = options.port; this.port = options.port;
this.app = express(); this.app = express();
this.builtinPlugDir = options.builtinPlugDir; this.builtinPlugDir = options.builtinPlugDir;
this.distDir = options.distDir; this.distDir = options.distDir;
this.system = new System<SilverBulletHooks>("server"); this.password = options.password;
this.token = options.token;
// Setup system // Set up the PlugOS System
this.system = new System<SilverBulletHooks>("server");
// Instantiate the event bus hook
this.eventHook = new EventHook(); this.eventHook = new EventHook();
this.system.addHook(this.eventHook); this.system.addHook(this.eventHook);
// And the page namespace hook
let namespaceHook = new PageNamespaceHook(); let namespaceHook = new PageNamespaceHook();
this.system.addHook(namespaceHook); this.system.addHook(namespaceHook);
// The space
this.space = new Space( this.space = new Space(
new EventedSpacePrimitives( new EventedSpacePrimitives(
new PlugSpacePrimitives( new PlugSpacePrimitives(
@ -94,6 +100,8 @@ export class ExpressServer {
), ),
true true
); );
// The database used for persistence (SQLite)
this.db = knex({ this.db = knex({
client: "better-sqlite3", client: "better-sqlite3",
connection: { connection: {
@ -102,9 +110,11 @@ export class ExpressServer {
useNullAsDefault: true, useNullAsDefault: true,
}); });
this.system.registerSyscalls(["shell"], shellSyscalls(options.pagesPath)); // The cron hook
this.system.addHook(new NodeCronHook()); this.system.addHook(new NodeCronHook());
// Register syscalls available on the server sid
this.system.registerSyscalls(["shell"], shellSyscalls(options.pagesPath));
this.system.registerSyscalls( this.system.registerSyscalls(
[], [],
pageIndexSyscalls(this.db), pageIndexSyscalls(this.db),
@ -117,10 +127,13 @@ export class ExpressServer {
sandboxSyscalls(this.system), sandboxSyscalls(this.system),
jwtSyscalls() jwtSyscalls()
); );
// Register the HTTP endpoint hook (with "/_/<plug-name>"" prefix, hardcoded for now)
this.system.addHook(new EndpointHook(this.app, "/_")); this.system.addHook(new EndpointHook(this.app, "/_"));
this.system.on({ this.system.on({
plugLoaded: (plug) => { plugLoaded: (plug) => {
// Automatically inject some modules into each plug
safeRun(async () => { safeRun(async () => {
for (let [modName, code] of Object.entries( for (let [modName, code] of Object.entries(
globalModules.dependencies globalModules.dependencies
@ -131,6 +144,8 @@ export class ExpressServer {
}, },
}); });
// Hook into some "get-plug:" to allow loading plugs from disk (security of this TBD)
// First, for builtins (loaded from the packages/plugs/ folder)
this.eventHook.addLocalListener( this.eventHook.addLocalListener(
"get-plug:builtin", "get-plug:builtin",
async (plugName: string): Promise<Manifest> => { async (plugName: string): Promise<Manifest> => {
@ -149,6 +164,7 @@ export class ExpressServer {
} }
); );
// Second, for loading plug JSON files with absolute or relative (from CWD) paths
this.eventHook.addLocalListener( this.eventHook.addLocalListener(
"get-plug:file", "get-plug:file",
async (plugPath: string): Promise<Manifest> => { async (plugPath: string): Promise<Manifest> => {
@ -169,9 +185,12 @@ export class ExpressServer {
} }
); );
// Rescan disk every 5s to detect any out-of-process file changes
setInterval(() => { setInterval(() => {
this.space.updatePageList().catch(console.error); this.space.updatePageList().catch(console.error);
}, 5000); }, 5000);
// Load plugs
this.reloadPlugs().catch(console.error); this.reloadPlugs().catch(console.error);
} }
@ -182,6 +201,7 @@ export class ExpressServer {
); );
} }
// In case of a new space with no `PLUGS` file, generate a default one based on all built-in plugs
private async bootstrapBuiltinPlugs() { private async bootstrapBuiltinPlugs() {
let allPlugFiles = await readdir(this.builtinPlugDir); let allPlugFiles = await readdir(this.builtinPlugDir);
let pluginNames = []; let pluginNames = [];
@ -225,9 +245,10 @@ export class ExpressServer {
} }
async start() { async start() {
const tokenMiddleware: (req: any, res: any, next: any) => void = this.token const passwordMiddleware: (req: any, res: any, next: any) => void = this
.password
? (req, res, next) => { ? (req, res, next) => {
if (req.headers.authorization === `Bearer ${this.token}`) { if (req.headers.authorization === `Bearer ${this.password}`) {
next(); next();
} else { } else {
res.status(401).send("Unauthorized"); res.status(401).send("Unauthorized");
@ -239,6 +260,7 @@ export class ExpressServer {
await ensureTable(this.db); await ensureTable(this.db);
await ensureFTSTable(this.db, "fts"); await ensureFTSTable(this.db, "fts");
await this.ensureIndexPage();
console.log("Setting up router"); console.log("Setting up router");
let auth = new Authenticator(this.db); let auth = new Authenticator(this.db);
@ -328,7 +350,7 @@ export class ExpressServer {
this.app.use( this.app.use(
"/fs", "/fs",
tokenMiddleware, passwordMiddleware,
cors({ cors({
methods: "GET,HEAD,PUT,OPTIONS,POST,DELETE", methods: "GET,HEAD,PUT,OPTIONS,POST,DELETE",
preflightContinue: true, preflightContinue: true,
@ -393,7 +415,7 @@ export class ExpressServer {
this.app.use( this.app.use(
"/plug", "/plug",
tokenMiddleware, passwordMiddleware,
cors({ cors({
methods: "GET,HEAD,PUT,OPTIONS,POST,DELETE", methods: "GET,HEAD,PUT,OPTIONS,POST,DELETE",
preflightContinue: true, preflightContinue: true,
@ -412,6 +434,14 @@ export class ExpressServer {
}); });
} }
async ensureIndexPage() {
try {
await this.space.getPageMeta("index");
} catch (e) {
await this.space.writePage("index", `Welcome to your new space!`);
}
}
async stop() { async stop() {
if (this.server) { if (this.server) {
console.log("Stopping"); console.log("Stopping");

View File

@ -4,7 +4,7 @@
"name": "Zef Hemel", "name": "Zef Hemel",
"email": "zef@zef.me" "email": "zef@zef.me"
}, },
"version": "0.0.2", "version": "0.0.7",
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"silverbullet": "./dist/server/server.js" "silverbullet": "./dist/server/server.js"
@ -41,11 +41,15 @@
"@codemirror/legacy-modes": "6.0.0", "@codemirror/legacy-modes": "6.0.0",
"@jest/globals": "^27.5.1", "@jest/globals": "^27.5.1",
"@lezer/markdown": "1.0.0", "@lezer/markdown": "1.0.0",
"@silverbulletmd/common": "^0.0.7",
"@silverbulletmd/plugs": "^0.0.7",
"@silverbulletmd/web": "^0.0.7",
"better-sqlite3": "^7.5.0", "better-sqlite3": "^7.5.0",
"body-parser": "^1.19.2", "body-parser": "^1.19.2",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"dexie": "^3.2.1", "dexie": "^3.2.1",
"esbuild": "^0.14.27",
"events": "^3.3.0", "events": "^3.3.0",
"express": "^4.17.3", "express": "^4.17.3",
"fake-indexeddb": "^3.1.7", "fake-indexeddb": "^3.1.7",
@ -56,6 +60,7 @@
"node-fetch": "2", "node-fetch": "2",
"node-watch": "^0.7.3", "node-watch": "^0.7.3",
"nodemon": "^2.0.15", "nodemon": "^2.0.15",
"server": "^1.0.37",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"vm2": "^3.9.9", "vm2": "^3.9.9",
"ws": "^8.5.0", "ws": "^8.5.0",

View File

@ -11,14 +11,14 @@ let args = yargs(hideBin(process.argv))
type: "number", type: "number",
default: 3000, default: 3000,
}) })
.option("token", { .option("password", {
type: "string", type: "string",
}) })
.parse(); .parse();
if (!args._.length) { if (!args._.length) {
console.error( console.error(
"Usage: silverbullet [--port 3000] [--token mysecrettoken] <path-to-pages>" "Usage: silverbullet [--port 3000] [--password mysecretpassword] <path-to-pages>"
); );
process.exit(1); process.exit(1);
} }
@ -40,7 +40,7 @@ const expressServer = new ExpressServer({
pagesPath: pagesPath, pagesPath: pagesPath,
distDir: webappDistDir, distDir: webappDistDir,
builtinPlugDir: plugDistDir, builtinPlugDir: plugDistDir,
token: args.token, password: args.password,
}); });
expressServer.start().catch((e) => { expressServer.start().catch((e) => {
console.error(e); console.error(e);

View File

@ -6,22 +6,23 @@ import { HttpSpacePrimitives } from "@silverbulletmd/common/spaces/http_space_pr
safeRun(async () => { safeRun(async () => {
// let localSpace = new Space(new IndexedDBSpacePrimitives("pages"), true); // let localSpace = new Space(new IndexedDBSpacePrimitives("pages"), true);
// localSpace.watch(); // localSpace.watch();
let token: string | undefined = localStorage.getItem("token") || undefined; let password: string | undefined =
localStorage.getItem("password") || undefined;
let httpPrimitives = new HttpSpacePrimitives("", token); let httpPrimitives = new HttpSpacePrimitives("", password);
while (true) { while (true) {
try { try {
await httpPrimitives.getPageMeta("start"); await httpPrimitives.getPageMeta("index");
break; break;
} catch (e: any) { } catch (e: any) {
if (e.message === "Unauthorized") { if (e.message === "Unauthorized") {
token = prompt("Token: ") || undefined; password = prompt("Password: ") || undefined;
if (!token) { if (!password) {
alert("Sorry, that's it then"); alert("Sorry, need a password");
return; return;
} }
localStorage.setItem("token", token!); localStorage.setItem("password", password!);
httpPrimitives = new HttpSpacePrimitives("", token); httpPrimitives = new HttpSpacePrimitives("", password);
} }
} }
} }

60
packages/web/commands.ts Normal file
View File

@ -0,0 +1,60 @@
import { EditorSelection, StateCommand, Transaction } from "@codemirror/state";
import { Text } from "@codemirror/state";
export function insertMarker(marker: string): StateCommand {
return ({ state, dispatch }) => {
const changes = state.changeByRange((range) => {
const isBoldBefore =
state.sliceDoc(range.from - marker.length, range.from) === marker;
const isBoldAfter =
state.sliceDoc(range.to, range.to + marker.length) === marker;
const changes = [];
changes.push(
isBoldBefore
? {
from: range.from - marker.length,
to: range.from,
insert: Text.of([""]),
}
: {
from: range.from,
insert: Text.of([marker]),
}
);
changes.push(
isBoldAfter
? {
from: range.to,
to: range.to + marker.length,
insert: Text.of([""]),
}
: {
from: range.to,
insert: Text.of([marker]),
}
);
const extendBefore = isBoldBefore ? -marker.length : marker.length;
const extendAfter = isBoldAfter ? -marker.length : marker.length;
return {
changes,
range: EditorSelection.range(
range.from + extendBefore,
range.to + extendAfter
),
};
});
dispatch(
state.update(changes, {
scrollIntoView: true,
annotations: Transaction.userEvent.of("input"),
})
);
return true;
};
}

View File

@ -221,7 +221,7 @@ export class Editor {
}); });
if (this.pageNavigator.getCurrentPage() === "") { if (this.pageNavigator.getCurrentPage() === "") {
await this.pageNavigator.navigate("start"); await this.pageNavigator.navigate("index");
} }
await this.reloadPlugs(); await this.reloadPlugs();
} }

View File

@ -4,7 +4,7 @@
"name": "Zef Hemel", "name": "Zef Hemel",
"email": "zef@zef.me" "email": "zef@zef.me"
}, },
"version": "0.0.2", "version": "0.0.7",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"watch": "rm -rf .parcel-cache && parcel watch", "watch": "rm -rf .parcel-cache && parcel watch",
@ -41,12 +41,16 @@
"@jest/globals": "^27.5.1", "@jest/globals": "^27.5.1",
"@lezer/highlight": "1.0.0", "@lezer/highlight": "1.0.0",
"@lezer/markdown": "1.0.0", "@lezer/markdown": "1.0.0",
"@silverbulletmd/common": "^0.0.7",
"@silverbulletmd/plugs": "^0.0.7",
"@silverbulletmd/web": "^0.0.7",
"fake-indexeddb": "^3.1.7", "fake-indexeddb": "^3.1.7",
"fuzzysort": "^1.9.0", "fuzzysort": "^1.9.0",
"jest": "^27.5.1", "jest": "^27.5.1",
"knex": "^1.0.4", "knex": "^1.0.4",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2" "react-dom": "^17.0.2",
"server": "^1.0.37"
}, },
"devDependencies": { "devDependencies": {
"@parcel/packager-raw-url": "2.5.0", "@parcel/packager-raw-url": "2.5.0",

View File

@ -21,9 +21,9 @@ export function spaceSyscalls(editor: Editor): SysCallMapping {
return await editor.space.writePage(name, text); return await editor.space.writePage(name, text);
}, },
"space.deletePage": async (ctx, name: string) => { "space.deletePage": async (ctx, name: string) => {
// If we're deleting the current page, navigate to the start page // If we're deleting the current page, navigate to the index page
if (editor.currentPage === name) { if (editor.currentPage === name) {
await editor.navigate("start"); await editor.navigate("index");
} }
// Remove page from open pages in editor // Remove page from open pages in editor
editor.openPages.delete(name); editor.openPages.delete(name);

7
scripts/release.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash -e
VERSION=$1
npm version --ws $VERSION || true
npm install --ws server --save @silverbulletmd/web@$VERSION @silverbulletmd/plugs@$VERSION @silverbulletmd/common@$VERSION
npm run clean-build
npm run publish-all