@nervur-org/dock 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GETTING_STARTED.md +128 -0
- package/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +37 -0
- package/api/quo-api.md +196 -0
- package/api/route.ts +227 -0
- package/beings/GLOSSARY.md +118 -0
- package/beings/WORLDS.md +567 -0
- package/beings/avatar.ts +101 -0
- package/beings/carry.ts +159 -0
- package/beings/clock.ts +552 -0
- package/beings/courier.ts +107 -0
- package/beings/desk.ts +128 -0
- package/beings/doorbell.ts +129 -0
- package/beings/envoy.ts +218 -0
- package/beings/index.ts +16 -0
- package/beings/join.ts +51 -0
- package/beings/link.ts +104 -0
- package/beings/look.ts +139 -0
- package/beings/porter.ts +43 -0
- package/beings/presence.ts +39 -0
- package/beings/quo-app-bells.md +280 -0
- package/beings/quo-dock.md +1942 -0
- package/beings/setup.ts +45 -0
- package/beings/side.ts +60 -0
- package/beings/user.ts +459 -0
- package/beings/way.ts +65 -0
- package/cli/bundles.ts +20 -0
- package/cli/client.ts +73 -0
- package/cli/daemon.ts +581 -0
- package/cli/estate/Caddyfile +33 -0
- package/cli/estate/quo.service +35 -0
- package/cli/estate/systems.quo.plist +44 -0
- package/cli/estate.ts +48 -0
- package/cli/folder.ts +27 -0
- package/cli/oauth.ts +32 -0
- package/cli/pilot.ts +38 -0
- package/cli/quo-dock.md +1942 -0
- package/cli/quo.ts +203 -0
- package/cli/stand.ts +160 -0
- package/dist/api/route.d.ts +21 -0
- package/dist/api/route.js +193 -0
- package/dist/beings/avatar.d.ts +32 -0
- package/dist/beings/avatar.js +100 -0
- package/dist/beings/carry.d.ts +12 -0
- package/dist/beings/carry.js +174 -0
- package/dist/beings/clock.d.ts +161 -0
- package/dist/beings/clock.js +528 -0
- package/dist/beings/courier.d.ts +50 -0
- package/dist/beings/courier.js +96 -0
- package/dist/beings/desk.d.ts +53 -0
- package/dist/beings/desk.js +135 -0
- package/dist/beings/doorbell.d.ts +78 -0
- package/dist/beings/doorbell.js +133 -0
- package/dist/beings/envoy.d.ts +54 -0
- package/dist/beings/envoy.js +211 -0
- package/dist/beings/index.d.ts +14 -0
- package/dist/beings/index.js +16 -0
- package/dist/beings/join.d.ts +15 -0
- package/dist/beings/join.js +42 -0
- package/dist/beings/link.d.ts +14 -0
- package/dist/beings/link.js +91 -0
- package/dist/beings/look.d.ts +30 -0
- package/dist/beings/look.js +104 -0
- package/dist/beings/porter.d.ts +14 -0
- package/dist/beings/porter.js +41 -0
- package/dist/beings/presence.d.ts +8 -0
- package/dist/beings/presence.js +21 -0
- package/dist/beings/setup.d.ts +3 -0
- package/dist/beings/setup.js +39 -0
- package/dist/beings/side.d.ts +32 -0
- package/dist/beings/side.js +37 -0
- package/dist/beings/user.d.ts +315 -0
- package/dist/beings/user.js +462 -0
- package/dist/beings/way.d.ts +19 -0
- package/dist/beings/way.js +28 -0
- package/dist/cli/bundles.d.ts +5 -0
- package/dist/cli/bundles.js +17 -0
- package/dist/cli/client.d.ts +15 -0
- package/dist/cli/client.js +58 -0
- package/dist/cli/daemon.d.ts +50 -0
- package/dist/cli/daemon.js +584 -0
- package/dist/cli/estate/Caddyfile +33 -0
- package/dist/cli/estate/quo.service +35 -0
- package/dist/cli/estate/systems.quo.plist +44 -0
- package/dist/cli/estate.d.ts +5 -0
- package/dist/cli/estate.js +50 -0
- package/dist/cli/folder.d.ts +2 -0
- package/dist/cli/folder.js +25 -0
- package/dist/cli/oauth.d.ts +2 -0
- package/dist/cli/oauth.js +32 -0
- package/dist/cli/pilot.d.ts +5 -0
- package/dist/cli/pilot.js +22 -0
- package/dist/cli/quo.d.ts +2 -0
- package/dist/cli/quo.js +201 -0
- package/dist/cli/stand.d.ts +37 -0
- package/dist/cli/stand.js +110 -0
- package/dist/dock/agents.d.ts +102 -0
- package/dist/dock/agents.js +105 -0
- package/dist/dock/apns.d.ts +27 -0
- package/dist/dock/apns.js +98 -0
- package/dist/dock/console.d.ts +6 -0
- package/dist/dock/console.js +20 -0
- package/dist/dock/dialers.d.ts +56 -0
- package/dist/dock/dialers.js +75 -0
- package/dist/dock/faculty.d.ts +24 -0
- package/dist/dock/faculty.js +142 -0
- package/dist/dock/fcm.d.ts +13 -0
- package/dist/dock/fcm.js +77 -0
- package/dist/dock/grants.d.ts +41 -0
- package/dist/dock/grants.js +99 -0
- package/dist/dock/http2.d.ts +4 -0
- package/dist/dock/http2.js +68 -0
- package/dist/dock/index.d.ts +41 -0
- package/dist/dock/index.js +113 -0
- package/dist/dock/local.d.ts +13 -0
- package/dist/dock/local.js +44 -0
- package/dist/dock/location.d.ts +19 -0
- package/dist/dock/location.js +11 -0
- package/dist/dock/notifications.d.ts +38 -0
- package/dist/dock/notifications.js +60 -0
- package/dist/dock/push.d.ts +79 -0
- package/dist/dock/push.js +132 -0
- package/dist/dock/service.d.ts +4 -0
- package/dist/dock/service.js +107 -0
- package/dist/dock/timer.d.ts +52 -0
- package/dist/dock/timer.js +149 -0
- package/dist/dock/webpush.d.ts +12 -0
- package/dist/dock/webpush.js +144 -0
- package/dist/harbor/browser.d.ts +15 -0
- package/dist/harbor/browser.js +82 -0
- package/dist/harbor/capacitor.d.ts +28 -0
- package/dist/harbor/capacitor.js +218 -0
- package/dist/harbor/disk.d.ts +27 -0
- package/dist/harbor/disk.js +249 -0
- package/dist/harbor/edge/alarm.d.ts +10 -0
- package/dist/harbor/edge/alarm.js +38 -0
- package/dist/harbor/edge/edge.d.ts +54 -0
- package/dist/harbor/edge/edge.js +370 -0
- package/dist/harbor/edge/exercise.d.ts +7 -0
- package/dist/harbor/edge/exercise.js +232 -0
- package/dist/harbor/edge/given.d.ts +26 -0
- package/dist/harbor/edge/given.js +42 -0
- package/dist/harbor/edge/platform.d.ts +39 -0
- package/dist/harbor/edge/storage.d.ts +30 -0
- package/dist/harbor/edge/storage.js +180 -0
- package/dist/harbor/edge/worker.d.ts +17 -0
- package/dist/harbor/edge/worker.js +67 -0
- package/dist/harbor/files.d.ts +15 -0
- package/dist/harbor/files.js +220 -0
- package/dist/harbor/http.d.ts +19 -0
- package/dist/harbor/http.js +177 -0
- package/dist/harbor/idb.d.ts +15 -0
- package/dist/harbor/idb.js +0 -0
- package/dist/harbor/quo.d.ts +10 -0
- package/dist/harbor/quo.js +42 -0
- package/dist/harbor/resolve.d.ts +16 -0
- package/dist/harbor/resolve.js +17 -0
- package/dist/harbor/seal.d.ts +3 -0
- package/dist/harbor/seal.js +25 -0
- package/dist/harbor/sealed.d.ts +35 -0
- package/dist/harbor/sealed.js +162 -0
- package/dist/harbor/tauri.d.ts +16 -0
- package/dist/harbor/tauri.js +59 -0
- package/dist/harbor/ward.d.ts +9 -0
- package/dist/harbor/ward.js +15 -0
- package/dist/human/app.d.ts +32 -0
- package/dist/human/app.js +527 -0
- package/dist/human/apps.d.ts +10 -0
- package/dist/human/apps.js +29 -0
- package/dist/human/dom.d.ts +2 -0
- package/dist/human/dom.js +15 -0
- package/dist/human/door.d.ts +19 -0
- package/dist/human/door.js +92 -0
- package/dist/human/guest.d.ts +3 -0
- package/dist/human/guest.js +33 -0
- package/dist/human/html.d.ts +55 -0
- package/dist/human/html.js +451 -0
- package/dist/human/local.d.ts +7 -0
- package/dist/human/local.js +10 -0
- package/dist/human/mark.d.ts +8 -0
- package/dist/human/mark.js +47 -0
- package/dist/human/move.d.ts +17 -0
- package/dist/human/move.js +90 -0
- package/dist/human/screen.d.ts +19 -0
- package/dist/human/screen.js +119 -0
- package/dist/human/style.d.ts +2 -0
- package/dist/human/style.js +60 -0
- package/dist/human/tab.bundle.txt +4775 -0
- package/dist/human/tab.d.ts +15 -0
- package/dist/human/tab.js +409 -0
- package/dist/human/tree.d.ts +50 -0
- package/dist/human/tree.js +157 -0
- package/dist/human/wake.d.ts +34 -0
- package/dist/human/wake.js +179 -0
- package/dist/human/web.d.ts +33 -0
- package/dist/human/web.js +184 -0
- package/dist/human/worker.bundle.txt +3868 -0
- package/dist/human/worker.d.ts +1 -0
- package/dist/human/worker.js +81 -0
- package/dist/human/worlds.d.ts +24 -0
- package/dist/human/worlds.js +99 -0
- package/dist/mcp/agent.d.ts +10 -0
- package/dist/mcp/agent.js +78 -0
- package/dist/mcp/allow.d.ts +20 -0
- package/dist/mcp/allow.js +88 -0
- package/dist/mcp/http.d.ts +27 -0
- package/dist/mcp/http.js +92 -0
- package/dist/mcp/oauth.d.ts +91 -0
- package/dist/mcp/oauth.js +241 -0
- package/dist/mcp/route.d.ts +15 -0
- package/dist/mcp/route.js +42 -0
- package/dist/mcp/runner.d.ts +45 -0
- package/dist/mcp/runner.js +99 -0
- package/dist/mcp/server.d.ts +11 -0
- package/dist/mcp/server.js +102 -0
- package/dock/agents.ts +121 -0
- package/dock/apns.ts +138 -0
- package/dock/console.ts +23 -0
- package/dock/dialers.ts +85 -0
- package/dock/faculty.ts +148 -0
- package/dock/fcm.ts +103 -0
- package/dock/grants.ts +111 -0
- package/dock/http2.ts +70 -0
- package/dock/index.ts +133 -0
- package/dock/local.ts +48 -0
- package/dock/location.ts +22 -0
- package/dock/notifications.ts +67 -0
- package/dock/push.ts +161 -0
- package/dock/quo-app-bells.md +280 -0
- package/dock/quo-dock.md +1942 -0
- package/dock/service.ts +100 -0
- package/dock/timer.ts +174 -0
- package/dock/webpush.ts +182 -0
- package/harbor/browser.ts +97 -0
- package/harbor/capacitor.ts +223 -0
- package/harbor/disk.ts +237 -0
- package/harbor/edge/alarm.ts +48 -0
- package/harbor/edge/edge.ts +407 -0
- package/harbor/edge/exercise.ts +241 -0
- package/harbor/edge/given.ts +106 -0
- package/harbor/edge/platform.d.ts +39 -0
- package/harbor/edge/storage.ts +179 -0
- package/harbor/edge/text.d.ts +15 -0
- package/harbor/edge/worker.ts +70 -0
- package/harbor/edge/wrangler.toml +30 -0
- package/harbor/files.ts +221 -0
- package/harbor/http.ts +188 -0
- package/harbor/idb.ts +0 -0
- package/harbor/quo-harbor.md +850 -0
- package/harbor/quo.ts +44 -0
- package/harbor/resolve.ts +31 -0
- package/harbor/seal.ts +26 -0
- package/harbor/sealed.ts +183 -0
- package/harbor/tauri.ts +79 -0
- package/harbor/ward.ts +28 -0
- package/human/app.ts +580 -0
- package/human/apps.ts +47 -0
- package/human/dom.ts +21 -0
- package/human/door.ts +113 -0
- package/human/guest.ts +34 -0
- package/human/html.ts +490 -0
- package/human/local.ts +30 -0
- package/human/mark.ts +53 -0
- package/human/move.ts +122 -0
- package/human/quo-human.md +1042 -0
- package/human/screen.ts +136 -0
- package/human/style.ts +63 -0
- package/human/tab.bundle.txt +4775 -0
- package/human/tab.ts +446 -0
- package/human/tree.ts +217 -0
- package/human/wake.ts +191 -0
- package/human/web.ts +259 -0
- package/human/worker.bundle.txt +3868 -0
- package/human/worker.ts +124 -0
- package/human/worlds.ts +127 -0
- package/mcp/agent.ts +94 -0
- package/mcp/allow.ts +131 -0
- package/mcp/http.ts +92 -0
- package/mcp/oauth.ts +270 -0
- package/mcp/quo-mcp.md +343 -0
- package/mcp/route.ts +45 -0
- package/mcp/runner.ts +136 -0
- package/mcp/server.ts +101 -0
- package/package.json +121 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# The daemon on a droplet, as one user, forever. Install with:
|
|
2
|
+
# sudo cp droplet/quo.service /etc/systemd/system/quo.service
|
|
3
|
+
# sudo systemctl enable --now quo
|
|
4
|
+
# The unit runs as the `quo` user, whose home holds the estate folder and
|
|
5
|
+
# the harbor folder, so the seed, the lease and the sockets are that user's
|
|
6
|
+
# alone. The HTTP door is loopback only; Caddy fronts it. See Caddyfile
|
|
7
|
+
# beside this file.
|
|
8
|
+
[Unit]
|
|
9
|
+
Description=quo: one harbor on this device
|
|
10
|
+
After=network.target
|
|
11
|
+
|
|
12
|
+
[Service]
|
|
13
|
+
User=quo
|
|
14
|
+
Group=quo
|
|
15
|
+
Environment=QUO_DIR=/home/quo/.quo
|
|
16
|
+
Environment=QUO_HTTP=8787
|
|
17
|
+
# CLAUDE_CODE_OAUTH_TOKEN, when an agent folder runs `claude -p`: the agent
|
|
18
|
+
# reads its credential from the device, never from cells.
|
|
19
|
+
EnvironmentFile=-/etc/quo/env
|
|
20
|
+
WorkingDirectory=/home/quo/ESTATE
|
|
21
|
+
ExecStart=/usr/bin/node /home/quo/ESTATE/node_modules/@nervur-org/dock/dist/cli/quo.js serve
|
|
22
|
+
Restart=always
|
|
23
|
+
RestartSec=2
|
|
24
|
+
KillSignal=SIGTERM
|
|
25
|
+
TimeoutStopSec=10
|
|
26
|
+
NoNewPrivileges=true
|
|
27
|
+
ProtectSystem=strict
|
|
28
|
+
# Agents run in their folders and Claude Code keeps its own state in the
|
|
29
|
+
# user's home; both must be writable for a run to happen, and a harbor
|
|
30
|
+
# with no agent has neither.
|
|
31
|
+
ReadWritePaths=/home/quo/.quo -/home/quo/agents -/home/quo/.claude -/home/quo/.claude.json /tmp
|
|
32
|
+
PrivateTmp=false
|
|
33
|
+
|
|
34
|
+
[Install]
|
|
35
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
The daemon on a Mac, as the user, forever: a launchd agent. It holds the
|
|
4
|
+
harbor at ~/.quo and the socket to every quo. route ~/.quo/dial.json
|
|
5
|
+
names, so a Mac behind NAT is reached through the droplet it dialed. The
|
|
6
|
+
daemon runs the dock's emitted `dist/cli/quo.js` from where the estate
|
|
7
|
+
installed it, never the source. Install, with node on the path:
|
|
8
|
+
|
|
9
|
+
sed "s#HOME#$HOME#g; s#NODE#$(which node)#g" mac/systems.quo.plist > ~/Library/LaunchAgents/systems.quo.plist
|
|
10
|
+
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/systems.quo.plist
|
|
11
|
+
|
|
12
|
+
Remove with `launchctl bootout gui/$(id -u)/systems.quo`; restart after a
|
|
13
|
+
wire change with `launchctl kickstart -k gui/$(id -u)/systems.quo`. The
|
|
14
|
+
log is ~/.quo/serve.log. No HTTP door: a Mac is a dialer, not a listener.
|
|
15
|
+
-->
|
|
16
|
+
<plist version="1.0">
|
|
17
|
+
<dict>
|
|
18
|
+
<key>Label</key>
|
|
19
|
+
<string>systems.quo</string>
|
|
20
|
+
<key>ProgramArguments</key>
|
|
21
|
+
<array>
|
|
22
|
+
<string>NODE</string>
|
|
23
|
+
<string>HOME/ESTATE/node_modules/@nervur-org/dock/dist/cli/quo.js</string>
|
|
24
|
+
<string>serve</string>
|
|
25
|
+
</array>
|
|
26
|
+
<key>EnvironmentVariables</key>
|
|
27
|
+
<dict>
|
|
28
|
+
<key>QUO_DIR</key>
|
|
29
|
+
<string>HOME/.quo</string>
|
|
30
|
+
</dict>
|
|
31
|
+
<key>WorkingDirectory</key>
|
|
32
|
+
<string>HOME/ESTATE</string>
|
|
33
|
+
<key>RunAtLoad</key>
|
|
34
|
+
<true/>
|
|
35
|
+
<key>KeepAlive</key>
|
|
36
|
+
<true/>
|
|
37
|
+
<key>ThrottleInterval</key>
|
|
38
|
+
<integer>2</integer>
|
|
39
|
+
<key>StandardOutPath</key>
|
|
40
|
+
<string>HOME/.quo/serve.log</string>
|
|
41
|
+
<key>StandardErrorPath</key>
|
|
42
|
+
<string>HOME/.quo/serve.log</string>
|
|
43
|
+
</dict>
|
|
44
|
+
</plist>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// `quo estate DIR --domain D`: an estate folder, written once, of the shape
|
|
3
|
+
// every estate has. One folder per harbor: `droplet/`, the quo directory
|
|
4
|
+
// that device runs minus what it mints, its routes under the domain, no
|
|
5
|
+
// agents, no schedules, no classes of its own, and that device's unit and
|
|
6
|
+
// Caddyfile; and `mac/`, the launchd agent that runs the daemon on an
|
|
7
|
+
// operator's Mac as a dialer, with the dial list it holds. A package file
|
|
8
|
+
// that depends on this dock and nothing else, and one document to fill in.
|
|
9
|
+
// An estate needs nothing the dock does not give it, and this is the dock
|
|
10
|
+
// giving it.
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
13
|
+
import { basename, join } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { DOMAIN, routesFor } from './daemon.js';
|
|
16
|
+
// This package's own version, read beside the emitted or the source tree.
|
|
17
|
+
async function version() {
|
|
18
|
+
for (const rel of ['../package.json', '../../package.json']) {
|
|
19
|
+
const at = fileURLToPath(new URL(rel, import.meta.url));
|
|
20
|
+
if (!existsSync(at))
|
|
21
|
+
continue;
|
|
22
|
+
const pkg = JSON.parse(await readFile(at, 'utf8'));
|
|
23
|
+
if (pkg.name === '@nervur-org/dock' && pkg.version)
|
|
24
|
+
return pkg.version;
|
|
25
|
+
}
|
|
26
|
+
return '0.1.0';
|
|
27
|
+
}
|
|
28
|
+
const template = (name) => readFile(fileURLToPath(new URL(`estate/${name}`, import.meta.url)), 'utf8');
|
|
29
|
+
export async function estate(dir, domain) {
|
|
30
|
+
if (!DOMAIN.test(domain))
|
|
31
|
+
throw new Error(`estate: ${domain} is not a domain`);
|
|
32
|
+
if (existsSync(join(dir, 'package.json')))
|
|
33
|
+
throw new Error(`estate: ${dir} already holds a package`);
|
|
34
|
+
const name = basename(dir);
|
|
35
|
+
const files = {
|
|
36
|
+
'package.json': JSON.stringify({ name, private: true, description: `The ${name} estate: one owner's harbors, built on the dock and nothing else.`, type: 'module', dependencies: { '@nervur-org/dock': `^${await version()}` } }, null, 2) + '\n',
|
|
37
|
+
[`${name}.md`]: `# ${name}\n\nAn estate: all of one owner's harbors. One folder per harbor, each the quo\ndirectory that device runs minus what it mints, plus that device's unit.\n\`droplet/\` is the first: its routes under \`${domain}\`, its agents, its\nclasses, its systemd unit and its Caddyfile. This folder depends on the\ndock and nothing else.\n\n## The droplet\n\nWhere it is, how it is reached, and what was done to stand it up: yours\nto write.\n`,
|
|
38
|
+
'droplet/routes.json': JSON.stringify(routesFor(domain)) + '\n',
|
|
39
|
+
'droplet/stand.ts': `// SPDX-License-Identifier: Apache-2.0\n// What the root stands up on this harbor, once, after the inits and with no\n// daemon over the folder: the beings the estate boots for its own working\n// and the relations between them, placed on real invitations. Run as\n// \`node droplet/stand.ts <dir>\`; twice is once.\nimport { stand } from '@nervur-org/dock/stand';\n\nconsole.log(await stand(process.argv[2] ?? process.env.QUO_DIR ?? '', { boot: [], wire: [] }));\n`,
|
|
40
|
+
'droplet/classes/index.ts': '// The classes this harbor holds beside the dock\'s built-in ones, exported\n// by name.\nexport {};\n',
|
|
41
|
+
'droplet/quo.service': (await template('quo.service')).replaceAll('ESTATE', name),
|
|
42
|
+
'droplet/Caddyfile': (await template('Caddyfile')).replaceAll('DOMAIN', domain),
|
|
43
|
+
'mac/systems.quo.plist': (await template('systems.quo.plist')).replaceAll('ESTATE', name),
|
|
44
|
+
};
|
|
45
|
+
for (const [rel, text] of Object.entries(files)) {
|
|
46
|
+
await mkdir(join(dir, rel, '..'), { recursive: true });
|
|
47
|
+
await writeFile(join(dir, rel), text);
|
|
48
|
+
}
|
|
49
|
+
return { dir, name, files: Object.keys(files) };
|
|
50
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The harbor folder as the web route asks for it. A route reads no file, so
|
|
3
|
+
// what a world keeps beside its wards is answered here, where the daemon
|
|
4
|
+
// already has a disk: `design/` for the face it wears and `tab/` for its own
|
|
5
|
+
// code for the tab. Nothing is held between requests, because a folder is
|
|
6
|
+
// worn, built and changed while the daemon runs and a restart is not how a
|
|
7
|
+
// world puts on a face.
|
|
8
|
+
//
|
|
9
|
+
// A world wears a design only when it holds `tokens.css`: that file is the
|
|
10
|
+
// design, and a folder with fonts and no tokens wears nothing, so nothing of
|
|
11
|
+
// it is served.
|
|
12
|
+
import { access, readFile } from 'node:fs/promises';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { TAB_CODE, TAB_SOURCE } from '../human/web.js';
|
|
15
|
+
const DESIGN = 'design';
|
|
16
|
+
export function diskFolder(dir) {
|
|
17
|
+
const there = (at) => access(at).then(() => true, () => false);
|
|
18
|
+
const tokens = join(dir, DESIGN, 'tokens.css');
|
|
19
|
+
return {
|
|
20
|
+
wearing: async () => ((await there(tokens)) ? { tokens: true, fonts: await there(join(dir, DESIGN, 'fonts.css')) } : { tokens: false, fonts: false }),
|
|
21
|
+
design: async (file) => ((await there(tokens)) ? readFile(join(dir, DESIGN, file)).then((b) => new Uint8Array(b), () => null) : null),
|
|
22
|
+
serving: async () => (await there(join(dir, TAB_CODE))) || (await there(join(dir, TAB_SOURCE))),
|
|
23
|
+
beings: () => readFile(join(dir, TAB_CODE), 'utf8').catch(() => null),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The OAuth dance as the daemon keeps it: `<dir>/oauth.json`, the route's
|
|
3
|
+
// own file and nobody else's, written whole through a temporary name so a
|
|
4
|
+
// crash mid-write leaves the last good dance behind. A route reads no file,
|
|
5
|
+
// so this is where the disk is, beside the folder and the wards.
|
|
6
|
+
//
|
|
7
|
+
// The writes are queued because the dance moves on a request and requests
|
|
8
|
+
// overlap: two saves racing on one name is how a file ends up half of each.
|
|
9
|
+
import { readFile, writeFile, rename } from 'node:fs/promises';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { emptyDance } from '../mcp/oauth.js';
|
|
12
|
+
export function danceOnDisk(dir) {
|
|
13
|
+
const file = join(dir, 'oauth.json');
|
|
14
|
+
let queue = Promise.resolve();
|
|
15
|
+
return {
|
|
16
|
+
load: async () => {
|
|
17
|
+
const read = await readFile(file, 'utf8').catch(() => null);
|
|
18
|
+
if (read === null)
|
|
19
|
+
return emptyDance();
|
|
20
|
+
try {
|
|
21
|
+
return { ...emptyDance(), ...JSON.parse(read) };
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return emptyDance(); // a dance nobody can read is a dance nobody is in the middle of
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
save: (dance) => (queue = queue.then(async () => {
|
|
28
|
+
await writeFile(file + '.tmp', JSON.stringify(dance), { mode: 0o600 });
|
|
29
|
+
await rename(file + '.tmp', file);
|
|
30
|
+
})),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
2
|
+
import type { Serving } from '../beings/side.ts';
|
|
3
|
+
import { type Subject } from '../mcp/server.ts';
|
|
4
|
+
export declare function owner(dir: string, ward: string, via?: string): Subject;
|
|
5
|
+
export declare function pilotSide(dir: string, ward: string, transport: Transport, via?: string): Promise<Serving>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ask } from './client.js';
|
|
2
|
+
import { mcpSide } from '../mcp/server.js';
|
|
3
|
+
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
4
|
+
// The owner hears objects: the socket's own failure, a silence and a word
|
|
5
|
+
// each come back as an error object named for what it was.
|
|
6
|
+
export function owner(dir, ward, via) {
|
|
7
|
+
const one = async (method, args) => {
|
|
8
|
+
const out = await ask(dir, method, args, ward, via);
|
|
9
|
+
if ('error' in out)
|
|
10
|
+
return { error: out.error };
|
|
11
|
+
const r = out.result;
|
|
12
|
+
if (isRecord(r) && r.silence === true)
|
|
13
|
+
return { error: 'silence' };
|
|
14
|
+
if (isRecord(r) && typeof r.word === 'string')
|
|
15
|
+
return { error: r.word };
|
|
16
|
+
return r;
|
|
17
|
+
};
|
|
18
|
+
return { tools: () => one(undefined, {}), call: (name, args) => one(name, args ?? {}), ears: new Set() };
|
|
19
|
+
}
|
|
20
|
+
export function pilotSide(dir, ward, transport, via) {
|
|
21
|
+
return mcpSide(owner(dir, ward, via), transport);
|
|
22
|
+
}
|
package/dist/cli/quo.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// `quo`: the one command of the estate. Thin on purpose: each subcommand
|
|
4
|
+
// picks a folder, reaches the daemon or the disk, and hands off. Logic that
|
|
5
|
+
// appears here was missing from a being or a side, and moves there.
|
|
6
|
+
//
|
|
7
|
+
// quo init [--dir D] [--ward W] [--user U] [--class C] [--default] [--show] [--domain DOM] mint a seed, boot a ward over a disk store, write both; C is the class of the ward's home being, User unless said; --default marks it the default ward; --show shows it at the web route; --domain writes the four routes under DOM
|
|
8
|
+
// quo default [--dir D] <ward> mark one ward the default, the one a caller means when naming none; the one ward hosted is the default unmarked
|
|
9
|
+
// quo show|hide [--dir D] <ward> show a ward at the web route, listed at its root and its public being's page painted for a stranger; hidden at init, and a hidden ward's page is the plain door
|
|
10
|
+
// quo domain [--dir D] <domain> write routes.json by the convention: mcp., web., quo. and api. under the domain, over https
|
|
11
|
+
// quo serve [--dir D] [--http PORT] the daemon: harbor from disk, the two sockets, the HTTP door on loopback, a socket held to every route the dialers faculty keeps
|
|
12
|
+
// quo census [--dir D] [--ward W] [--via S] the empty ask: pk and every being
|
|
13
|
+
// quo boot [--dir D] [--ward W] [--via S] <json> { key, class, public? }
|
|
14
|
+
// quo invite [--dir D] [--ward W] [--via S] <json> { being, id }; with routes.json naming web., the answer carries the link
|
|
15
|
+
// quo knock [--dir D] [--ward W] [--via S] <json> { being | { boot, key }, id, invitation, method?, args?, wanted? }
|
|
16
|
+
// quo remove [--dir D] [--ward W] [--via S] <json> { being, id }: a relation out of a being; on the ward pk, an owner
|
|
17
|
+
// quo unboot [--dir D] [--ward W] [--via S] <json> { being }: a being out of the ward, with every relation she holds
|
|
18
|
+
// quo ask [--dir D] [--ward W] <being> [method] [json] the root asking a being of its own ward, as the device
|
|
19
|
+
// quo side [--dir D] [--ward W] --as NAME an avatar over stdio for a local MCP client; NAME is the client identity
|
|
20
|
+
// quo run [--dir D] [--ward W] --as NAME --url URL --model NAME [--turns N] a human at a terminal talking to a model through an avatar: a line in, the model's final text out
|
|
21
|
+
// quo pilot [--dir D] [--ward W] [--via S] the owner pilot over stdio: census, boot, invite, knock, remove
|
|
22
|
+
// quo reach [--dir D] <pk> <url> a hint for the harbor's directory: that ward pk lives at that quo. route
|
|
23
|
+
// quo estate <dir> --domain D an estate folder of the shape every estate has: a droplet's quo directory, unit and Caddyfile, a Mac's launchd agent, on this dock
|
|
24
|
+
//
|
|
25
|
+
// D defaults to $QUO_DIR, then ~/.quo. W defaults to the default ward, and
|
|
26
|
+
// init names a new ward main when none is given. init takes
|
|
27
|
+
// --user NAME for the user being, defaulting to the device's user. run
|
|
28
|
+
// takes the model's base URL as --url or $QUO_MODEL_URL, its name as
|
|
29
|
+
// --model or $QUO_MODEL, a bearer from $QUO_MODEL_KEY, read here and never
|
|
30
|
+
// kept, and --turns as the ceiling of model calls in one turn. S is a
|
|
31
|
+
// standing the user being holds at another ward, one the root there invited
|
|
32
|
+
// as an owner: with --via, the owner asks go to that ward's door, sealed,
|
|
33
|
+
// and the answers are its. Without it they are the root's, on the pointer.
|
|
34
|
+
import { homedir, userInfo } from 'node:os';
|
|
35
|
+
import { join } from 'node:path';
|
|
36
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
37
|
+
import { isInvitation } from '@nervur-org/nervur';
|
|
38
|
+
import { DiskHarbor } from '../harbor/disk.js';
|
|
39
|
+
import { serve, readRoutes, writeRoutes } from './daemon.js';
|
|
40
|
+
import { ask, askBeing, side, reach } from './client.js';
|
|
41
|
+
import { link } from '../beings/link.js';
|
|
42
|
+
import { pilotSide } from './pilot.js';
|
|
43
|
+
import { estate } from './estate.js';
|
|
44
|
+
const OWNER_ASKS = new Set(['boot', 'public', 'invite', 'knock', 'remove', 'unboot']);
|
|
45
|
+
function parse(argv) {
|
|
46
|
+
const flags = {};
|
|
47
|
+
const rest = [];
|
|
48
|
+
for (let i = 0; i < argv.length; i++) {
|
|
49
|
+
const a = argv[i] ?? '';
|
|
50
|
+
// a flag takes the word after it, unless that word is a flag or there is
|
|
51
|
+
// none: --default stands alone
|
|
52
|
+
if (a.startsWith('--'))
|
|
53
|
+
flags[a.slice(2)] = argv[i + 1] === undefined || argv[i + 1].startsWith('--') ? '' : argv[++i];
|
|
54
|
+
else
|
|
55
|
+
rest.push(a);
|
|
56
|
+
}
|
|
57
|
+
const [cmd, json, more, third] = rest;
|
|
58
|
+
return { cmd, json, more, third, flags, dir: flags.dir ?? process.env.QUO_DIR ?? join(homedir(), '.quo'), ward: flags.ward, via: flags.via };
|
|
59
|
+
}
|
|
60
|
+
const print = (v) => console.log(JSON.stringify(v, null, 2));
|
|
61
|
+
async function main(argv) {
|
|
62
|
+
const { cmd, json, more, third, flags, dir, via } = parse(argv);
|
|
63
|
+
if (cmd === 'init') {
|
|
64
|
+
const made = await DiskHarbor.init(dir, flags.ward, flags.user ?? userInfo().username, flags.class, 'default' in flags, 'show' in flags);
|
|
65
|
+
print(flags.domain ? { ...made, routes: await writeRoutes(dir, flags.domain) } : made);
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
if (cmd === 'default') {
|
|
69
|
+
if (!json) {
|
|
70
|
+
console.error('quo default <ward>');
|
|
71
|
+
return 2;
|
|
72
|
+
}
|
|
73
|
+
await DiskHarbor.setDefault(dir, json);
|
|
74
|
+
print({ default: json });
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
if (cmd === 'show' || cmd === 'hide') {
|
|
78
|
+
if (!json) {
|
|
79
|
+
console.error(`quo ${cmd} <ward>`);
|
|
80
|
+
return 2;
|
|
81
|
+
}
|
|
82
|
+
await DiskHarbor.setShown(dir, json, cmd === 'show');
|
|
83
|
+
print({ [cmd === 'show' ? 'shown' : 'hidden']: json });
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
if (cmd === 'domain') {
|
|
87
|
+
if (!json) {
|
|
88
|
+
console.error('quo domain <domain>');
|
|
89
|
+
return 2;
|
|
90
|
+
}
|
|
91
|
+
print({ routes: await writeRoutes(dir, json) });
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
// Every command that reaches a ward is on one: the one named, or the
|
|
95
|
+
// default, read from the folder the way the daemon reads it, and only once
|
|
96
|
+
// the command is held right.
|
|
97
|
+
const wardOf = async () => flags.ward ?? (await DiskHarbor.defaultOf(dir));
|
|
98
|
+
if (cmd === 'side' || cmd === 'run') {
|
|
99
|
+
if (!flags.as) {
|
|
100
|
+
console.error(`quo ${cmd} needs --as NAME`);
|
|
101
|
+
return 2;
|
|
102
|
+
}
|
|
103
|
+
let run;
|
|
104
|
+
if (cmd === 'run') {
|
|
105
|
+
const url = flags.url ?? process.env.QUO_MODEL_URL;
|
|
106
|
+
const model = flags.model ?? process.env.QUO_MODEL;
|
|
107
|
+
if (!url || !model) {
|
|
108
|
+
console.error('quo run needs --url URL and --model NAME, or QUO_MODEL_URL and QUO_MODEL');
|
|
109
|
+
return 2;
|
|
110
|
+
}
|
|
111
|
+
const key = process.env.QUO_MODEL_KEY;
|
|
112
|
+
const turns = flags.turns !== undefined ? Number(flags.turns) : undefined;
|
|
113
|
+
if (turns !== undefined && !(Number.isInteger(turns) && turns > 0)) {
|
|
114
|
+
console.error('--turns is a positive integer');
|
|
115
|
+
return 2;
|
|
116
|
+
}
|
|
117
|
+
run = { url, model, ...(key ? { key } : {}), ...(turns !== undefined ? { turns } : {}) };
|
|
118
|
+
}
|
|
119
|
+
await side(dir, flags.as, await wardOf(), run); // stdio to the daemon, until one end closes
|
|
120
|
+
return -1;
|
|
121
|
+
}
|
|
122
|
+
if (cmd === 'pilot') {
|
|
123
|
+
const ward = await wardOf();
|
|
124
|
+
await ask(dir, undefined, {}, ward); // no daemon is a clear word now, not at the first tool call
|
|
125
|
+
const serving = await pilotSide(dir, ward, new StdioServerTransport(), via);
|
|
126
|
+
process.stdin.once('end', () => void serving.close().then(() => process.exit(0)));
|
|
127
|
+
return -1;
|
|
128
|
+
}
|
|
129
|
+
if (cmd === 'serve') {
|
|
130
|
+
const port = flags.http ?? process.env.QUO_HTTP;
|
|
131
|
+
const s = await serve(dir, port !== undefined ? { http: { port: Number(port) } } : {});
|
|
132
|
+
print({ dir: s.harbor.dir, sock: s.sock, side: s.side, http: s.http ? `http://${s.http.host}:${s.http.port}` : null, dial: s.quo.dialers.map((d) => d.url), wards: Object.fromEntries([...s.harbor.wards].map(([n, h]) => [n, h.pk])) });
|
|
133
|
+
const stop = () => void s.close().then(() => process.exit(0));
|
|
134
|
+
process.once('SIGINT', stop);
|
|
135
|
+
process.once('SIGTERM', stop);
|
|
136
|
+
return -1; // stays up
|
|
137
|
+
}
|
|
138
|
+
if (cmd === 'estate') {
|
|
139
|
+
if (!json || !flags.domain) {
|
|
140
|
+
console.error('quo estate needs <dir> --domain D');
|
|
141
|
+
return 2;
|
|
142
|
+
}
|
|
143
|
+
print(await estate(json, flags.domain));
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
// The root reaching into a being of its own ward. Two positionals and the
|
|
147
|
+
// args as JSON, because a human writes this one by hand: `quo ask clock keep
|
|
148
|
+
// '{"id":"nightly","ask":"desk.hello","cron":"0 3 * * *"}'`. With no
|
|
149
|
+
// method it is that being's describe, as the device sees her.
|
|
150
|
+
if (cmd === 'ask') {
|
|
151
|
+
if (!json) {
|
|
152
|
+
console.error('quo ask needs <being> [method] [json]');
|
|
153
|
+
return 2;
|
|
154
|
+
}
|
|
155
|
+
const out = await askBeing(dir, json, more, third ? JSON.parse(third) : {}, await wardOf());
|
|
156
|
+
print('error' in out ? { error: out.error } : out.result);
|
|
157
|
+
const failed = 'error' in out || (typeof out.result === 'object' && out.result !== null && 'error' in out.result);
|
|
158
|
+
return failed ? 1 : 0;
|
|
159
|
+
}
|
|
160
|
+
if (cmd === 'reach') {
|
|
161
|
+
if (!json || !more) {
|
|
162
|
+
console.error('quo reach needs <pk> <url>');
|
|
163
|
+
return 2;
|
|
164
|
+
}
|
|
165
|
+
const out = await reach(dir, json, more);
|
|
166
|
+
print('error' in out ? { error: out.error } : out.result);
|
|
167
|
+
return 'error' in out ? 1 : 0;
|
|
168
|
+
}
|
|
169
|
+
if (cmd === 'census' || (cmd !== undefined && OWNER_ASKS.has(cmd))) {
|
|
170
|
+
const args = json ? JSON.parse(json) : {};
|
|
171
|
+
const ward = await wardOf();
|
|
172
|
+
let out = await ask(dir, cmd === 'census' ? undefined : cmd, args, ward, via);
|
|
173
|
+
// an invitation minted here is handed on as a link when the harbor's
|
|
174
|
+
// routes name a web. origin: the world's page with the invitation in
|
|
175
|
+
// the fragment, one string to send to the person it is for
|
|
176
|
+
const minted = cmd === 'invite' && !via && 'result' in out && isInvitation(out.result) ? out.result : null;
|
|
177
|
+
const routes = minted ? await readRoutes(dir) : null;
|
|
178
|
+
// the ward answers null to an invite on an id already held, standing or
|
|
179
|
+
// occupant, and null on a terminal is nothing to read: it is a refusal
|
|
180
|
+
// here, in words, and an exit code a script can tell apart
|
|
181
|
+
if (cmd === 'invite' && 'result' in out && out.result === null)
|
|
182
|
+
out = { result: { error: 'id taken' } };
|
|
183
|
+
if (minted && routes)
|
|
184
|
+
print({ ...minted, link: link(`${routes.web}/${encodeURIComponent(ward)}`, minted) });
|
|
185
|
+
else
|
|
186
|
+
print('error' in out ? { error: out.error } : out.result);
|
|
187
|
+
// an error object the ward answered is an ordinary answer, and still a
|
|
188
|
+
// non-zero exit, so a script can tell a boot that happened from one that did not.
|
|
189
|
+
const failed = 'error' in out || (typeof out.result === 'object' && out.result !== null && 'error' in out.result);
|
|
190
|
+
return failed ? 1 : 0;
|
|
191
|
+
}
|
|
192
|
+
console.error('usage: quo init|default|show|hide|domain|serve|census|boot|public|invite|knock|remove|unboot|ask|side|run|pilot|reach|estate [--dir D] [--ward W] [--via S] [--as NAME] [--url URL --model NAME] [json]');
|
|
193
|
+
return 2;
|
|
194
|
+
}
|
|
195
|
+
main(process.argv.slice(2)).then((code) => {
|
|
196
|
+
if (code >= 0)
|
|
197
|
+
process.exit(code);
|
|
198
|
+
}, (e) => {
|
|
199
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
200
|
+
process.exit(1);
|
|
201
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { JsonObject } from '@nervur-org/nervur';
|
|
2
|
+
import type { Harbor } from '@nervur-org/nervur/harbor';
|
|
3
|
+
export type Boot = {
|
|
4
|
+
ward: string;
|
|
5
|
+
key: string;
|
|
6
|
+
class: string;
|
|
7
|
+
public?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type Wire = {
|
|
10
|
+
from: string;
|
|
11
|
+
to: string;
|
|
12
|
+
as?: string;
|
|
13
|
+
known?: string;
|
|
14
|
+
notes?: JsonObject;
|
|
15
|
+
};
|
|
16
|
+
export type Stand = {
|
|
17
|
+
boot?: Boot[];
|
|
18
|
+
wire?: Wire[];
|
|
19
|
+
};
|
|
20
|
+
type Wards = {
|
|
21
|
+
wards: Map<string, {
|
|
22
|
+
being(key: string): unknown;
|
|
23
|
+
ask(method: string, args: JsonObject): Promise<unknown>;
|
|
24
|
+
}>;
|
|
25
|
+
partitionOf(name: string): Record<string, unknown> | undefined;
|
|
26
|
+
};
|
|
27
|
+
export declare function standUp(harbor: Harbor | Wards, decl: Stand): Promise<{
|
|
28
|
+
booted: string[];
|
|
29
|
+
wired: string[];
|
|
30
|
+
already: string[];
|
|
31
|
+
}>;
|
|
32
|
+
export declare function stand(dir: string, decl: Stand): Promise<{
|
|
33
|
+
booted: string[];
|
|
34
|
+
wired: string[];
|
|
35
|
+
already: string[];
|
|
36
|
+
}>;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { DiskHarbor } from '../harbor/disk.js';
|
|
2
|
+
const KEY = /^[\w.:-]+$/;
|
|
3
|
+
const RESERVED = new Set(['knock', 'take', 'remove']);
|
|
4
|
+
const fail = (why) => {
|
|
5
|
+
throw new Error(`stand: ${why}`);
|
|
6
|
+
};
|
|
7
|
+
// `<ward>.<key>`, split at the first dot: a ward's name is a plain word and a
|
|
8
|
+
// being's key is not, so the ambiguity only ever falls one way.
|
|
9
|
+
function at(v, what) {
|
|
10
|
+
const dot = v.indexOf('.');
|
|
11
|
+
if (dot < 1)
|
|
12
|
+
return fail(`${what}: "${v}" is not "<ward>.<being>"`);
|
|
13
|
+
return { ward: v.slice(0, dot), key: v.slice(dot + 1) };
|
|
14
|
+
}
|
|
15
|
+
// A name a wire would use, read off the partition the harbor already holds:
|
|
16
|
+
// whether a being holds it as a standing and whether she holds it as an
|
|
17
|
+
// occupant. Standings and occupants are one namespace, so a name held under
|
|
18
|
+
// the other kind is a collision and never this wire.
|
|
19
|
+
function holds(h, ward, key, id) {
|
|
20
|
+
const p = h.partitionOf(ward);
|
|
21
|
+
return { standing: Object.hasOwn(p?.bind?.[key]?.standings ?? {}, id), occupant: Object.hasOwn(p?.beings?.[key]?.occupants ?? {}, id) };
|
|
22
|
+
}
|
|
23
|
+
const ward = (h, name, why) => h.wards.get(name) ?? fail(`${why}: this device hosts no ward ${name}`);
|
|
24
|
+
// Stand a booted harbor up. Everything declared and absent is placed;
|
|
25
|
+
// everything declared and present is left exactly as it is.
|
|
26
|
+
//
|
|
27
|
+
// Idempotency is read off the mint and not off anybody's cells. A being is
|
|
28
|
+
// whatever answers, `Being` or a bare object with one method, so there is no
|
|
29
|
+
// shape here to reach into: what says a wire already stands is that the far
|
|
30
|
+
// being refuses to mint that occupant a second time. `invite` answers null
|
|
31
|
+
// for exactly one reason once the reader has held the id to a word the ward
|
|
32
|
+
// does not reserve, which is that the id is taken, and an id taken is this
|
|
33
|
+
// wire, placed the last time the script ran.
|
|
34
|
+
export async function standUp(harbor, decl) {
|
|
35
|
+
const h = harbor;
|
|
36
|
+
const booted = [];
|
|
37
|
+
for (const b of decl.boot ?? []) {
|
|
38
|
+
const w = ward(h, b.ward, `boot ${b.ward}.${b.key}`);
|
|
39
|
+
if (!w.being(b.key)) {
|
|
40
|
+
const out = (await w.ask('boot', { key: b.key, class: b.class }));
|
|
41
|
+
if (out.error)
|
|
42
|
+
fail(`boot ${b.ward}.${b.key} as ${b.class}: ${out.error}`);
|
|
43
|
+
booted.push(`${b.ward}.${b.key}`);
|
|
44
|
+
}
|
|
45
|
+
// the mark is one per ward and the script is what says who holds it:
|
|
46
|
+
// taken off whoever holds it, the desk init put there, and put on her,
|
|
47
|
+
// which is the same two asks whether or not she held it already
|
|
48
|
+
if (b.public) {
|
|
49
|
+
const off = (await w.ask('public', { key: null }));
|
|
50
|
+
if (off.error)
|
|
51
|
+
fail(`public ${b.ward}.${b.key}: ${off.error}`);
|
|
52
|
+
const out = (await w.ask('public', { key: b.key }));
|
|
53
|
+
if (out.error)
|
|
54
|
+
fail(`public ${b.ward}.${b.key}: ${out.error}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const wired = [];
|
|
58
|
+
const already = [];
|
|
59
|
+
for (const one of decl.wire ?? []) {
|
|
60
|
+
const from = at(one.from, 'from'), to = at(one.to, 'to');
|
|
61
|
+
const as = one.as ?? to.key, known = one.known ?? from.key;
|
|
62
|
+
if (!KEY.test(as) || !KEY.test(known))
|
|
63
|
+
fail(`a name is a word: ${one.from} -> ${one.to}`);
|
|
64
|
+
// The three the stance keeps for itself. A ward refuses them at invite
|
|
65
|
+
// and at take, and refusing them here is what leaves `invite` answering
|
|
66
|
+
// null for one reason only, which is the whole of how this stays
|
|
67
|
+
// idempotent without reading anybody's cells.
|
|
68
|
+
if (RESERVED.has(as) || RESERVED.has(known))
|
|
69
|
+
fail(`knock, take and remove are the ward's own words: ${one.from} -> ${one.to}`);
|
|
70
|
+
const near = ward(h, from.ward, `wire ${one.from}`), far = ward(h, to.ward, `wire ${one.to}`);
|
|
71
|
+
if (!near.being(from.key))
|
|
72
|
+
fail(`wire ${one.from}: no such being`);
|
|
73
|
+
if (!far.being(to.key))
|
|
74
|
+
fail(`wire ${one.to}: no such being`);
|
|
75
|
+
// A name held under the other kind is not this wire and never will be:
|
|
76
|
+
// said now, before a heir is spent on it, rather than read as a wire
|
|
77
|
+
// already placed or refused at the knock with the far side already bound.
|
|
78
|
+
const atFar = holds(h, to.ward, to.key, known);
|
|
79
|
+
const atNear = holds(h, from.ward, from.key, as);
|
|
80
|
+
if (atFar.standing)
|
|
81
|
+
fail(`wire ${one.from} -> ${one.to}: ${to.key} already holds a standing named ${known}, and a name is one of the two`);
|
|
82
|
+
if (atNear.occupant)
|
|
83
|
+
fail(`wire ${one.from} -> ${one.to}: ${from.key} already holds an occupant named ${as}, and a name is one of the two`);
|
|
84
|
+
const inv = (await far.ask('invite', { being: to.key, id: known, ...(one.notes ? { notes: one.notes } : {}) }));
|
|
85
|
+
if (inv === null) {
|
|
86
|
+
already.push(`${one.from} -> ${one.to}`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if ('error' in inv)
|
|
90
|
+
fail(`wire ${one.from} -> ${one.to}: ${inv.error}`);
|
|
91
|
+
const took = (await near.ask('knock', { being: from.key, id: as, invitation: inv }));
|
|
92
|
+
if (took.taken !== as)
|
|
93
|
+
fail(`wire ${one.from} -> ${one.to}: ${took.error ?? `${from.key} would not take ${as}`}`);
|
|
94
|
+
wired.push(`${one.from} -> ${one.to}`);
|
|
95
|
+
}
|
|
96
|
+
return { booted, wired, already };
|
|
97
|
+
}
|
|
98
|
+
// What an estate's `stand.ts` calls: the harbor folder booted here, with no
|
|
99
|
+
// daemon over it, stood up and written back. The lease refuses a folder a
|
|
100
|
+
// daemon holds, so the recipe is stop, stand, start, like an init.
|
|
101
|
+
export async function stand(dir, decl) {
|
|
102
|
+
const h = new DiskHarbor(dir);
|
|
103
|
+
await h.boot();
|
|
104
|
+
try {
|
|
105
|
+
return await standUp(h, decl);
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
await h.close();
|
|
109
|
+
}
|
|
110
|
+
}
|