nervur 0.20.0 → 0.20.1
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/README.md +107 -20
- package/dist/being/being.d.ts +1 -0
- package/dist/being/being.js +5 -1
- package/dist/being/faculty.d.ts +1 -0
- package/dist/being/faculty.js +13 -7
- package/dist/being/index.d.ts +1 -0
- package/dist/being/index.js +1 -0
- package/dist/being/kind.d.ts +4 -0
- package/dist/being/kind.js +28 -0
- package/dist/being/types.d.ts +3 -4
- package/dist/cli/command.d.ts +13 -0
- package/dist/cli/command.js +194 -0
- package/dist/cli/harbor.d.ts +21 -0
- package/dist/cli/harbor.js +141 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +14 -0
- package/dist/contract/index.d.ts +15 -2
- package/dist/contract/index.js +16 -2
- package/dist/folder/index.d.ts +5 -4
- package/dist/folder/index.js +45 -83
- package/dist/harbor/catalogue.d.ts +25 -0
- package/dist/harbor/catalogue.js +93 -0
- package/dist/harbor/dock.d.ts +12 -2
- package/dist/harbor/dock.js +39 -9
- package/dist/harbor/harbor.d.ts +15 -6
- package/dist/harbor/harbor.js +83 -21
- package/dist/harbor/index.d.ts +4 -1
- package/dist/harbor/index.js +5 -1
- package/dist/harbor/package.d.ts +10 -0
- package/dist/harbor/package.js +115 -0
- package/dist/harbor/registry.d.ts +5 -3
- package/dist/harbor/registry.js +72 -7
- package/dist/harbor/root-line.d.ts +9 -0
- package/dist/harbor/root-line.js +36 -0
- package/dist/harbor/terrain.d.ts +2 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +5 -5
- package/dist/pointer/bodies.d.ts +9 -3
- package/dist/pointer/bodies.js +37 -18
- package/dist/pointer/index.d.ts +1 -1
- package/dist/pointer/index.js +1 -1
- package/dist/pointer/world.d.ts +5 -4
- package/dist/pointer/world.js +23 -14
- package/dist/quo/door.js +2 -2
- package/dist/quo/keys.js +1 -1
- package/dist/tcp/frame.d.ts +29 -0
- package/dist/tcp/frame.js +74 -0
- package/dist/tcp/index.d.ts +18 -0
- package/dist/tcp/index.js +164 -0
- package/dist/ward/index.d.ts +1 -0
- package/dist/ward/index.js +1 -0
- package/dist/ward/pilot.d.ts +8 -0
- package/dist/ward/pilot.js +50 -0
- package/dist/ward/stance.d.ts +2 -4
- package/dist/ward/stance.js +7 -6
- package/dist/ward/ward-being.d.ts +2 -0
- package/dist/ward/ward-being.js +7 -1
- package/dist/ward/ward.d.ts +4 -2
- package/dist/ward/ward.js +35 -5
- package/package.json +8 -1
- package/src/being/being.ts +5 -1
- package/src/being/faculty.ts +14 -7
- package/src/being/index.ts +1 -0
- package/src/being/kind.ts +30 -0
- package/src/being/types.ts +8 -6
- package/src/cli/command.ts +188 -0
- package/src/cli/harbor.ts +137 -0
- package/src/cli/main.ts +13 -0
- package/src/contract/index.ts +38 -9
- package/src/folder/index.ts +44 -86
- package/src/harbor/catalogue.ts +110 -0
- package/src/harbor/dock.ts +48 -12
- package/src/harbor/harbor.ts +104 -29
- package/src/harbor/index.ts +6 -2
- package/src/harbor/package.ts +131 -0
- package/src/harbor/registry.ts +62 -9
- package/src/harbor/root-line.ts +36 -0
- package/src/harbor/terrain.ts +3 -1
- package/src/index.ts +5 -5
- package/src/pointer/bodies.ts +34 -16
- package/src/pointer/index.ts +1 -1
- package/src/pointer/world.ts +21 -18
- package/src/quo/door.ts +2 -2
- package/src/quo/keys.ts +1 -1
- package/src/stand/main.ts +1 -0
- package/src/stand/stand.ts +46 -4
- package/src/tcp/frame.ts +78 -0
- package/src/tcp/index.ts +173 -0
- package/src/ward/index.ts +1 -0
- package/src/ward/pilot.ts +50 -0
- package/src/ward/stance.ts +8 -7
- package/src/ward/ward-being.ts +7 -1
- package/src/ward/ward.ts +37 -6
package/README.md
CHANGED
|
@@ -23,9 +23,12 @@ a disk and needs Node's file system.
|
|
|
23
23
|
```js
|
|
24
24
|
import { Being, Faculty, World } from 'nervur';
|
|
25
25
|
|
|
26
|
-
// A contract, and a faculty that fulfils it.
|
|
27
|
-
class Clock extends Faculty {
|
|
26
|
+
// A contract, and a faculty that fulfils it. Every class names its kind.
|
|
27
|
+
class Clock extends Faculty {
|
|
28
|
+
static kind = 'org.example.clock';
|
|
29
|
+
}
|
|
28
30
|
class SystemTime extends Clock {
|
|
31
|
+
static kind = 'org.example.system-time';
|
|
29
32
|
static asks = { now: {} };
|
|
30
33
|
now() {
|
|
31
34
|
return { now: Date.now() };
|
|
@@ -34,6 +37,7 @@ class SystemTime extends Clock {
|
|
|
34
37
|
|
|
35
38
|
// A being, who lends a clock by its contract.
|
|
36
39
|
class Greeter extends Being {
|
|
40
|
+
static kind = 'org.example.greeter';
|
|
37
41
|
static asks = { hello: {} };
|
|
38
42
|
async hello(args, asker) {
|
|
39
43
|
if (!this.stance.standings.get('clock')) await this.stance.lend(Clock, 'clock');
|
|
@@ -42,42 +46,125 @@ class Greeter extends Being {
|
|
|
42
46
|
}
|
|
43
47
|
}
|
|
44
48
|
|
|
49
|
+
// The module these classes are: its name, its version, its classes.
|
|
50
|
+
const greetings = { module: 'org.example.greetings', version: '1.0.0', classes: [SystemTime, Greeter] };
|
|
51
|
+
|
|
52
|
+
// A harbor whose terrain can load that module, and whose catalogue runs it.
|
|
45
53
|
const world = new World();
|
|
46
|
-
const harbor = await world.harbor('home', [
|
|
47
|
-
await harbor.root('
|
|
54
|
+
const harbor = await world.harbor('home', [greetings]);
|
|
55
|
+
await harbor.root('ask', { being: 'catalogue', method: 'add', args: { from: 'org.example.greetings' } });
|
|
56
|
+
await harbor.root('open', { key: 'clock', class: 'org.example.system-time' });
|
|
48
57
|
await harbor.root('host', { ward: 'alice' });
|
|
49
|
-
await harbor.ward('alice').root('boot', { key: 'greeter', class: '
|
|
58
|
+
await harbor.ward('alice').root('boot', { key: 'greeter', class: 'org.example.greeter' });
|
|
50
59
|
```
|
|
51
60
|
|
|
52
61
|
A second harbor in the same world takes an invitation on the greeter and
|
|
53
62
|
asks it through the world, sealed as Quo's bytes. `world.restart('home')`
|
|
54
63
|
opens the harbor again from what its memory kept.
|
|
55
64
|
|
|
65
|
+
## Kinds
|
|
66
|
+
|
|
67
|
+
A row keeps the kind of the class a being is born of, and a being lends
|
|
68
|
+
by a contract's kind. A kind is declared on the class, because a bundler
|
|
69
|
+
renames classes. It is a domain you own, reversed, then a name, as Apple
|
|
70
|
+
and Matrix name things: `com.acme.shop`. Every segment is lowercase
|
|
71
|
+
letters, digits and `-`, and there are three at least. Each class
|
|
72
|
+
declares its own: a subclass that stands, and every contract between a
|
|
73
|
+
faculty and `Faculty`. `org.nervur.` is the kit's, and `org.example.` is
|
|
74
|
+
for examples like the one above. A module's classes stand under its own
|
|
75
|
+
domain: `com.acme.shop` stands `com.acme.*` kinds. A harbor refuses a
|
|
76
|
+
module in which one kind names two classes or two contracts, or names a
|
|
77
|
+
class and a contract.
|
|
78
|
+
|
|
79
|
+
## Modules
|
|
80
|
+
|
|
81
|
+
Your classes come in modules: `{ module, version, classes }`, a module
|
|
82
|
+
named as a kind is. A harbor's catalogue is a faculty of its own, and her
|
|
83
|
+
cells name the modules the harbor runs. `add { from }` runs the module
|
|
84
|
+
the terrain's loader finds there, and `remove { module }` stops one no
|
|
85
|
+
being stands on. A restart loads every module she names and refuses to
|
|
86
|
+
stand when one is missing. A module found at a new version wakes, and
|
|
87
|
+
`modules` shows the version kept and the version running.
|
|
88
|
+
|
|
56
89
|
## The onion
|
|
57
90
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
91
|
+
A harbor is one package. Only the seed and the terrain's bodies, its
|
|
92
|
+
loader among them, stand outside it.
|
|
93
|
+
|
|
94
|
+
1. The terrain gives the harbor's seed, which opens its memory. The
|
|
95
|
+
terrain keeps that memory as sealed bytes and reads none of it.
|
|
96
|
+
2. The box ward unpacks under the seed, its ward-being the dock.
|
|
97
|
+
3. The catalogue wakes, and loads every module she names.
|
|
98
|
+
4. The other faculties are born from their rows.
|
|
99
|
+
5. The routes the dock keeps are handed to the carrier.
|
|
100
|
+
6. Every ward the dock hosts unpacks, under the seed the dock keeps for it.
|
|
101
|
+
7. In every ward, the ward-being first, then each being from her row.
|
|
63
102
|
|
|
64
|
-
Cells decide what stands. The
|
|
65
|
-
|
|
66
|
-
the root's asks write the rest: `
|
|
103
|
+
Cells decide what stands. The modules only resolve a kind a row keeps.
|
|
104
|
+
Opening a harbor on empty memory is its genesis: the dock stands the
|
|
105
|
+
catalogue, and the root's asks write the rest: `add` on the catalogue,
|
|
106
|
+
`open`, `host`, `route`, and `boot` on a ward. `host { ward, seed? }`
|
|
107
|
+
stands a ward under a seed you name, or a drawn one.
|
|
67
108
|
|
|
68
109
|
## What it holds
|
|
69
110
|
|
|
70
111
|
- **Being, Faculty.** What you extend. A faculty's contract is its class
|
|
71
112
|
and every class between it and `Faculty`.
|
|
72
|
-
- **Entropy, Clock, Custody, Memory, Carrier.** The contracts a
|
|
73
|
-
fulfils. Each has one suite, and every body that fulfils it
|
|
74
|
-
|
|
75
|
-
- **
|
|
76
|
-
|
|
113
|
+
- **Entropy, Clock, Custody, Memory, Carrier, Loader.** The contracts a
|
|
114
|
+
terrain fulfils. Each has one suite, and every body that fulfils it
|
|
115
|
+
passes it.
|
|
116
|
+
- **Harbor, Terrain, Dock, Catalogue, Registry, WardBeing.** The onion.
|
|
117
|
+
- **World, PointerTerrain** and the pointer bodies, `HeldLoader` among
|
|
118
|
+
them. A whole world with this package alone.
|
|
77
119
|
- **FolderMemory, FolderCustody**, from `nervur/folder`.
|
|
120
|
+
- **TcpCarrier, TcpListener**, from `nervur/tcp`: Quo over TCP. Route a
|
|
121
|
+
ward pk to `host:port` on the carrier, hand the listener your harbor,
|
|
122
|
+
and harbors in two processes, or a harbor and another kit, speak.
|
|
123
|
+
|
|
124
|
+
Other carriers, storage beyond a folder, key custody, and every screen
|
|
125
|
+
are `@nervur-org/dock`'s, as classes fulfilling these contracts.
|
|
126
|
+
|
|
127
|
+
## The command
|
|
128
|
+
|
|
129
|
+
`nervur` stands a harbor on this machine: its seed and sealed memory in a
|
|
130
|
+
folder, Quo over TCP as its carrier. A module is a file that exports
|
|
131
|
+
`module`, `version` and `classes`.
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
nervur init --dir ./harbor
|
|
135
|
+
nervur module add ./greetings.js --dir ./harbor
|
|
136
|
+
nervur serve --dir ./harbor --port 7000
|
|
137
|
+
nervur host alice --dir ./harbor
|
|
138
|
+
nervur boot --ward alice greeter org.example.greeter --dir ./harbor
|
|
139
|
+
nervur route <ward pk> host:port --dir ./harbor
|
|
140
|
+
nervur ask --ward alice greeter hello --dir ./harbor
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`init` makes the harbor and prints its pk and an owner invitation. `serve`
|
|
144
|
+
listens for Quo on TCP and for the root's asks on a local socket, open to
|
|
145
|
+
you alone. Every other command is one root ask, sent to the served
|
|
146
|
+
harbor, or answered from the folder when none is served. Each prints one
|
|
147
|
+
JSON line. `--dir` defaults to `$NERVUR_DIR`, then `~/.nervur`. The
|
|
148
|
+
catalogue keeps a module by its file's absolute path, and routes live in
|
|
149
|
+
the harbor like everything else, so a restart finds both. `nervur
|
|
150
|
+
modules` shows what runs, and `nervur module remove <module>` stops one.
|
|
151
|
+
|
|
152
|
+
A harbor elsewhere is piloted with the owner invitation its `init`
|
|
153
|
+
printed, from a harbor of your own:
|
|
154
|
+
|
|
155
|
+
```sh
|
|
156
|
+
nervur pilot far '<owner invitation>' host:port --dir ./mine
|
|
157
|
+
nervur host shop --via far --dir ./mine
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`pilot` boots an `org.nervur.pilot` being in your harbor that holds the
|
|
161
|
+
invitation as an ordinary Quo relation, kept in your harbor's memory. With
|
|
162
|
+
`--via far`, any command is asked of the far harbor as its owner, sealed
|
|
163
|
+
over TCP, except `own` and `disown`, which stay the far root's.
|
|
164
|
+
`nervur unboot far` lets it go.
|
|
78
165
|
|
|
79
|
-
|
|
80
|
-
|
|
166
|
+
`rootLine(harbor, request)` is the same request in code, for any other
|
|
167
|
+
front: `{ ward?, method?, args? }` in, one JSON answer out.
|
|
81
168
|
|
|
82
169
|
## License
|
|
83
170
|
|
package/dist/being/being.d.ts
CHANGED
package/dist/being/being.js
CHANGED
|
@@ -9,8 +9,12 @@ const wrote = (self, name) => {
|
|
|
9
9
|
return false;
|
|
10
10
|
};
|
|
11
11
|
// A subclass's statics replace its parent's and never merge: her blueprint
|
|
12
|
-
// is what the class in front of you declares, in its order.
|
|
12
|
+
// is what the class in front of you declares, in its order. Her kind is
|
|
13
|
+
// never inherited: see `kind.ts`.
|
|
13
14
|
export class Being {
|
|
15
|
+
// The name her row keeps for this class. Every class that stands declares
|
|
16
|
+
// its own.
|
|
17
|
+
static kind = 'org.nervur.being';
|
|
14
18
|
// Her cells' defaults, written at birth only where a key is missing.
|
|
15
19
|
static cells = {};
|
|
16
20
|
static asks = {};
|
package/dist/being/faculty.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Being } from './being.ts';
|
|
|
2
2
|
import { type Asker, type Blueprint, type JsonObject, type Reply } from './types.ts';
|
|
3
3
|
export declare class Faculty extends Being {
|
|
4
4
|
#private;
|
|
5
|
+
static readonly kind: string;
|
|
5
6
|
static contracts(C: abstract new (...args: never[]) => unknown): string[];
|
|
6
7
|
static fulfils(C: unknown): C is typeof Faculty;
|
|
7
8
|
describe(asker: Asker): Blueprint;
|
package/dist/being/faculty.js
CHANGED
|
@@ -2,23 +2,29 @@
|
|
|
2
2
|
// A faculty: a being of the box ward that fulfils a contract. A contract is
|
|
3
3
|
// a class between `Faculty` and the class that stands, usually abstract:
|
|
4
4
|
//
|
|
5
|
-
// abstract class Timer extends Faculty {
|
|
6
|
-
//
|
|
5
|
+
// abstract class Timer extends Faculty { the contract
|
|
6
|
+
// static override kind = 'com.acme.timer' }
|
|
7
|
+
// class IntervalTimer extends Timer { one class that fulfils it
|
|
8
|
+
// static override kind = 'com.acme.interval-timer' }
|
|
7
9
|
//
|
|
8
|
-
// A being lends by the contract and never learns the class.
|
|
10
|
+
// A being lends by the contract's kind and never learns the class. Every
|
|
11
|
+
// class between the faculty and `Faculty` is a contract and declares its
|
|
12
|
+
// own kind, and a faculty fulfils each of them and her own. Two asks are the
|
|
9
13
|
// faculty's own and answered to the ward-being that opened her alone:
|
|
10
14
|
// `offer`, an invitation on herself for a ward the dock vouches for, and
|
|
11
15
|
// `retract`, an offer that was not taken.
|
|
12
16
|
import { Being } from './being.js';
|
|
17
|
+
import { kindOf } from './kind.js';
|
|
13
18
|
import { WARD } from './types.js';
|
|
14
19
|
export class Faculty extends Being {
|
|
20
|
+
static kind = 'org.nervur.faculty';
|
|
15
21
|
// The contracts a class fulfils: itself and every class between it and
|
|
16
|
-
// `Faculty`, by
|
|
22
|
+
// `Faculty`, by kind. A class among them with no kind of its own throws.
|
|
17
23
|
static contracts(C) {
|
|
18
|
-
const
|
|
24
|
+
const kinds = [];
|
|
19
25
|
for (let c = C; typeof c === 'function' && c !== Faculty; c = Object.getPrototypeOf(c))
|
|
20
|
-
|
|
21
|
-
return
|
|
26
|
+
kinds.push(kindOf(c));
|
|
27
|
+
return kinds;
|
|
22
28
|
}
|
|
23
29
|
static fulfils(C) {
|
|
24
30
|
return typeof C === 'function' && C.prototype instanceof Faculty;
|
package/dist/being/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { Being, type AskSpec } from './being.ts';
|
|
2
2
|
export { canonical, digest } from './digest.ts';
|
|
3
3
|
export { Faculty } from './faculty.ts';
|
|
4
|
+
export { isKind, KIT, kindOf, ownKind } from './kind.ts';
|
|
4
5
|
export { OWNER, WARD, type Answer, type Ask, type Asker, type BeingClass, type BeingLike, type Blueprint, type Invitation, type Json, type JsonObject, type Occupants, type Reply, type Schema, type Stance, type Standing, type Standings, type Wanted } from './types.ts';
|
|
5
6
|
export { isSilence, isWord, silence, told, word, type Silence, type Word, type WordName } from './words.ts';
|
package/dist/being/index.js
CHANGED
|
@@ -3,5 +3,6 @@
|
|
|
3
3
|
export { Being } from './being.js';
|
|
4
4
|
export { canonical, digest } from './digest.js';
|
|
5
5
|
export { Faculty } from './faculty.js';
|
|
6
|
+
export { isKind, KIT, kindOf, ownKind } from './kind.js';
|
|
6
7
|
export { OWNER, WARD } from './types.js';
|
|
7
8
|
export { isSilence, isWord, silence, told, word } from './words.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// A kind: the name a row keeps for the class a being is born of, and the
|
|
3
|
+
// name a contract is lent by. It is declared on the class, never taken from
|
|
4
|
+
// the name the language gives it, because a bundler renames classes.
|
|
5
|
+
//
|
|
6
|
+
// static kind = 'com.acme.shop'
|
|
7
|
+
//
|
|
8
|
+
// A kind is a reversed domain its author owns, then a name: at least three
|
|
9
|
+
// segments joined by `.`, each lowercase letters, digits and `-`, starting
|
|
10
|
+
// with a letter or digit. `org.nervur.` is the kit's own. A class declares
|
|
11
|
+
// its own kind: one it inherits names its parent, so it is none.
|
|
12
|
+
export const KIT = 'org.nervur.';
|
|
13
|
+
const KIND = /^[a-z0-9][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*){2,}$/;
|
|
14
|
+
export const isKind = (text) => typeof text === 'string' && KIND.test(text);
|
|
15
|
+
// The kind a class declares on itself, or undefined.
|
|
16
|
+
export const ownKind = (C) => {
|
|
17
|
+
if (typeof C !== 'function' || !Object.hasOwn(C, 'kind'))
|
|
18
|
+
return undefined;
|
|
19
|
+
const kind = C.kind;
|
|
20
|
+
return isKind(kind) ? kind : undefined;
|
|
21
|
+
};
|
|
22
|
+
// The kind a class declares on itself, or a throw naming the class.
|
|
23
|
+
export const kindOf = (C) => {
|
|
24
|
+
const kind = ownKind(C);
|
|
25
|
+
if (kind === undefined)
|
|
26
|
+
throw new Error(`class ${typeof C === 'function' ? C.name : String(C)} declares no kind of its own`);
|
|
27
|
+
return kind;
|
|
28
|
+
};
|
package/dist/being/types.d.ts
CHANGED
|
@@ -48,10 +48,8 @@ export interface Stance {
|
|
|
48
48
|
readonly cells: JsonObject;
|
|
49
49
|
readonly occupants: Occupants;
|
|
50
50
|
readonly standings: Standings;
|
|
51
|
-
boot(
|
|
52
|
-
lend(contract:
|
|
53
|
-
readonly name: string;
|
|
54
|
-
}, id: string): Promise<string | null>;
|
|
51
|
+
boot(kind: string, key: string, id?: string): Promise<string | null>;
|
|
52
|
+
lend(contract: abstract new (...args: never[]) => unknown, id: string): Promise<string | null>;
|
|
55
53
|
}
|
|
56
54
|
export interface BeingLike {
|
|
57
55
|
answer(asker: Asker, method: string | undefined, args: JsonObject): Reply | Promise<Reply>;
|
|
@@ -59,4 +57,5 @@ export interface BeingLike {
|
|
|
59
57
|
}
|
|
60
58
|
export type BeingClass = (new (stance: Stance) => BeingLike) & {
|
|
61
59
|
readonly name: string;
|
|
60
|
+
readonly kind: string;
|
|
62
61
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { JsonObject } from '../being/index.ts';
|
|
2
|
+
import type { RootRequest } from '../harbor/index.ts';
|
|
3
|
+
export declare const parse: (argv: string[]) => {
|
|
4
|
+
command?: string;
|
|
5
|
+
words: string[];
|
|
6
|
+
flags: Record<string, string>;
|
|
7
|
+
};
|
|
8
|
+
export declare const requestOf: (command: string, words: string[], flags: Record<string, string>) => RootRequest;
|
|
9
|
+
export declare const main: (argv: string[], env: Record<string, string | undefined>, print: (line: string) => void) => Promise<number>;
|
|
10
|
+
export declare const pilotRequests: (name: string, invitation: JsonObject, at: string) => RootRequest[];
|
|
11
|
+
export declare const via: (pilot: string, far: RootRequest) => RootRequest;
|
|
12
|
+
export declare const farAnswer: (local: JsonObject) => JsonObject;
|
|
13
|
+
export declare const SERVING = -1;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// `nervur`: a harbor on this machine. Each command is one root request, the
|
|
3
|
+
// same the root line carries from any front, printed as one JSON line.
|
|
4
|
+
//
|
|
5
|
+
// nervur init [--dir D] the harbor made: its seed drawn,
|
|
6
|
+
// its catalogue stood, and an owner
|
|
7
|
+
// invitation for whoever made it
|
|
8
|
+
// nervur serve [--dir D] [--host H] [--port P] the harbor served: Quo over TCP,
|
|
9
|
+
// and the root line on its socket
|
|
10
|
+
// nervur module add <file> a module the catalogue runs
|
|
11
|
+
// nervur module remove <module> a module it runs no more
|
|
12
|
+
// nervur modules what the catalogue runs
|
|
13
|
+
// nervur census [--ward W] the empty ask
|
|
14
|
+
// nervur open <key> <kind> a faculty stands in the box ward
|
|
15
|
+
// nervur host <ward> [--seed S] a hosted ward stands
|
|
16
|
+
// nervur route <ward pk> <host:port | none> where a ward is reached
|
|
17
|
+
// nervur boot [--ward W] <key> <kind> a being stands
|
|
18
|
+
// nervur unboot [--ward W] <being> a being goes
|
|
19
|
+
// nervur public [--ward W] <key | none> the being a stranger reaches
|
|
20
|
+
// nervur invite [--ward W] <being> <id> an invitation on a being
|
|
21
|
+
// nervur ask [--ward W] [being] [method] [args] a being, asked as the owner
|
|
22
|
+
// nervur own [--ward W] <id> an invitation for an owner
|
|
23
|
+
// nervur disown [--ward W] <id> an owner no more
|
|
24
|
+
// nervur pilot <name> <invitation> <host:port> a far ward held as its owner, by a
|
|
25
|
+
// pilot being of the box ward
|
|
26
|
+
// nervur <command> --via <name> ... the command asked of the far ward
|
|
27
|
+
// that pilot holds, as its owner
|
|
28
|
+
//
|
|
29
|
+
// D is --dir, then $NERVUR_DIR, then ~/.nervur. A module file is named from
|
|
30
|
+
// where the command runs and kept by its absolute path. No --ward is the box
|
|
31
|
+
// ward, and a pilot holds one ward, so --via takes no --ward. The exit is 1
|
|
32
|
+
// when the answer is an error.
|
|
33
|
+
import { existsSync } from 'node:fs';
|
|
34
|
+
import { homedir } from 'node:os';
|
|
35
|
+
import { join, resolve } from 'node:path';
|
|
36
|
+
import { open, request, serve } from './harbor.js';
|
|
37
|
+
const USAGE = 'usage: nervur <init|serve|module|modules|census|open|host|route|boot|unboot|public|invite|ask|own|disown|pilot> [args] [--dir D] [--ward W] [--via V] [--seed S] [--host H] [--port P]';
|
|
38
|
+
const FLAGS = new Set(['dir', 'ward', 'via', 'seed', 'host', 'port']);
|
|
39
|
+
export const parse = (argv) => {
|
|
40
|
+
const flags = {};
|
|
41
|
+
const words = [];
|
|
42
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
43
|
+
const arg = argv[i];
|
|
44
|
+
const name = arg.startsWith('--') ? arg.slice(2) : undefined;
|
|
45
|
+
if (name === undefined)
|
|
46
|
+
words.push(arg);
|
|
47
|
+
else if (!FLAGS.has(name) || argv[i + 1] === undefined)
|
|
48
|
+
throw new Error(`--${name} is no flag with a value`);
|
|
49
|
+
else
|
|
50
|
+
flags[name] = argv[(i += 1)];
|
|
51
|
+
}
|
|
52
|
+
const [command, ...rest] = words;
|
|
53
|
+
return { ...(command === undefined ? {} : { command }), words: rest, flags };
|
|
54
|
+
};
|
|
55
|
+
const none = (word) => (word === 'none' ? null : word);
|
|
56
|
+
const json = (word) => (word === undefined ? undefined : JSON.parse(word));
|
|
57
|
+
const catalogue = (method, args = {}) => ({ method: 'ask', args: { being: 'catalogue', method, args } });
|
|
58
|
+
// The root request a command names, or a throw saying what it lacks.
|
|
59
|
+
export const requestOf = (command, words, flags) => {
|
|
60
|
+
const need = (n) => {
|
|
61
|
+
if (words.length !== n)
|
|
62
|
+
throw new Error(`${command} takes ${n} argument${n === 1 ? '' : 's'}`);
|
|
63
|
+
return words;
|
|
64
|
+
};
|
|
65
|
+
const at = flags.ward === undefined ? {} : { ward: flags.ward };
|
|
66
|
+
switch (command) {
|
|
67
|
+
case 'module': {
|
|
68
|
+
const [verb, what] = need(2);
|
|
69
|
+
if (verb === 'add')
|
|
70
|
+
return catalogue('add', { from: resolve(what) });
|
|
71
|
+
if (verb === 'remove')
|
|
72
|
+
return catalogue('remove', { module: what });
|
|
73
|
+
throw new Error('module is add or remove');
|
|
74
|
+
}
|
|
75
|
+
case 'modules':
|
|
76
|
+
return (need(0), catalogue('modules'));
|
|
77
|
+
case 'census':
|
|
78
|
+
return (need(0), at);
|
|
79
|
+
case 'open': {
|
|
80
|
+
const [key, kind] = need(2);
|
|
81
|
+
return { method: 'open', args: { key: key, class: kind } };
|
|
82
|
+
}
|
|
83
|
+
case 'host': {
|
|
84
|
+
const [ward] = need(1);
|
|
85
|
+
return { method: 'host', args: { ward: ward, ...(flags.seed === undefined ? {} : { seed: flags.seed }) } };
|
|
86
|
+
}
|
|
87
|
+
case 'route': {
|
|
88
|
+
const [ward, where] = need(2);
|
|
89
|
+
return { method: 'route', args: { ward: ward, at: none(where) } };
|
|
90
|
+
}
|
|
91
|
+
case 'boot': {
|
|
92
|
+
const [key, kind] = need(2);
|
|
93
|
+
return { ...at, method: 'boot', args: { key: key, class: kind } };
|
|
94
|
+
}
|
|
95
|
+
case 'unboot':
|
|
96
|
+
return { ...at, method: 'unboot', args: { being: need(1)[0] } };
|
|
97
|
+
case 'public':
|
|
98
|
+
return { ...at, method: 'public', args: { key: none(need(1)[0]) } };
|
|
99
|
+
case 'invite': {
|
|
100
|
+
const [being, id] = need(2);
|
|
101
|
+
return { ...at, method: 'invite', args: { being: being, id: id } };
|
|
102
|
+
}
|
|
103
|
+
case 'ask': {
|
|
104
|
+
if (words.length > 3)
|
|
105
|
+
throw new Error('ask takes at most a being, a method and its args');
|
|
106
|
+
const [being, method, args] = words;
|
|
107
|
+
return { ...at, method: 'ask', args: { ...(being === undefined ? {} : { being }), ...(method === undefined ? {} : { method }), ...(args === undefined ? {} : { args: json(args) }) } };
|
|
108
|
+
}
|
|
109
|
+
case 'own':
|
|
110
|
+
case 'disown':
|
|
111
|
+
return { ...at, method: command, args: { id: need(1)[0] } };
|
|
112
|
+
default:
|
|
113
|
+
throw new Error(USAGE);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
// An error answer at any depth the root line or an ask carries it.
|
|
117
|
+
const failed = (answer) => {
|
|
118
|
+
if (typeof answer !== 'object' || answer === null)
|
|
119
|
+
return false;
|
|
120
|
+
const a = answer;
|
|
121
|
+
return 'error' in a || failed(a.answer);
|
|
122
|
+
};
|
|
123
|
+
export const main = async (argv, env, print) => {
|
|
124
|
+
const { command, words, flags } = parse(argv);
|
|
125
|
+
if (command === undefined)
|
|
126
|
+
throw new Error(USAGE);
|
|
127
|
+
const dir = flags.dir ?? env.NERVUR_DIR ?? join(homedir(), '.nervur');
|
|
128
|
+
const out = (answer) => {
|
|
129
|
+
print(JSON.stringify(answer));
|
|
130
|
+
return failed(answer) ? 1 : 0;
|
|
131
|
+
};
|
|
132
|
+
if (command === 'init') {
|
|
133
|
+
if (words.length > 0)
|
|
134
|
+
throw new Error('init takes no argument');
|
|
135
|
+
if (existsSync(join(dir, 'seed')))
|
|
136
|
+
return out({ error: `a harbor stands at ${dir}` });
|
|
137
|
+
const { harbor, carrier } = await open(dir);
|
|
138
|
+
try {
|
|
139
|
+
const owner = await harbor.root('own', { id: 'owner' });
|
|
140
|
+
return out({ pk: harbor.box.pk, owner: owner });
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
await carrier.close();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (command === 'serve') {
|
|
147
|
+
if (words.length > 0)
|
|
148
|
+
throw new Error('serve takes no argument');
|
|
149
|
+
const served = await serve(dir, flags.host, flags.port === undefined ? undefined : Number(flags.port));
|
|
150
|
+
const stop = () => void served.close().then(() => process.exit(0));
|
|
151
|
+
process.once('SIGINT', stop);
|
|
152
|
+
process.once('SIGTERM', stop);
|
|
153
|
+
print(JSON.stringify({ pk: served.pk, at: served.at, root: served.root }));
|
|
154
|
+
return SERVING;
|
|
155
|
+
}
|
|
156
|
+
if (command === 'pilot') {
|
|
157
|
+
if (words.length !== 3)
|
|
158
|
+
throw new Error('pilot takes 3 arguments');
|
|
159
|
+
const [name, invitation, at] = words;
|
|
160
|
+
const held = JSON.parse(invitation);
|
|
161
|
+
for (const req of pilotRequests(name, held, at)) {
|
|
162
|
+
const answer = await request(dir, req);
|
|
163
|
+
if (!failed(answer))
|
|
164
|
+
continue;
|
|
165
|
+
if (req.method === 'ask')
|
|
166
|
+
await request(dir, { method: 'unboot', args: { being: name } });
|
|
167
|
+
return out(answer);
|
|
168
|
+
}
|
|
169
|
+
return out({ piloting: name, ward: held.ward });
|
|
170
|
+
}
|
|
171
|
+
if (flags.via === undefined)
|
|
172
|
+
return out(await request(dir, requestOf(command, words, flags)));
|
|
173
|
+
if (flags.ward !== undefined)
|
|
174
|
+
throw new Error('--via takes no --ward');
|
|
175
|
+
return out(farAnswer(await request(dir, via(flags.via, requestOf(command, words, flags)))));
|
|
176
|
+
};
|
|
177
|
+
// The root requests that hold a far ward: its route, the pilot, the take.
|
|
178
|
+
export const pilotRequests = (name, invitation, at) => [
|
|
179
|
+
{ method: 'route', args: { ward: invitation.ward ?? null, at } },
|
|
180
|
+
{ method: 'boot', args: { key: name, class: 'org.nervur.pilot' } },
|
|
181
|
+
{ method: 'ask', args: { being: name, method: 'hold', args: { invitation } } },
|
|
182
|
+
];
|
|
183
|
+
// A root request, asked of the far ward a pilot holds.
|
|
184
|
+
export const via = (pilot, far) => ({
|
|
185
|
+
method: 'ask',
|
|
186
|
+
args: { being: pilot, method: 'pilot', args: { ...(far.method === undefined ? {} : { method: far.method }), ...(far.args === undefined ? {} : { args: far.args }) } },
|
|
187
|
+
});
|
|
188
|
+
// The far ward-being's answer out of the pilot's, or the local error.
|
|
189
|
+
export const farAnswer = (local) => {
|
|
190
|
+
const inner = local.answer?.answer;
|
|
191
|
+
return typeof inner === 'object' && inner !== null && !Array.isArray(inner) ? inner : local;
|
|
192
|
+
};
|
|
193
|
+
// What `main` answers for a harbor it keeps serving: no exit yet.
|
|
194
|
+
export const SERVING = -1;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { JsonObject } from '../being/index.ts';
|
|
2
|
+
import { Loader, type Loaded } from '../contract/index.ts';
|
|
3
|
+
import { Harbor, type RootRequest } from '../harbor/index.ts';
|
|
4
|
+
import { TcpCarrier } from '../tcp/index.ts';
|
|
5
|
+
export declare const socketOf: (dir: string) => string;
|
|
6
|
+
export declare class ImportLoader extends Loader {
|
|
7
|
+
load(from: string): Promise<Loaded | null>;
|
|
8
|
+
}
|
|
9
|
+
export type Opened = {
|
|
10
|
+
harbor: Harbor;
|
|
11
|
+
carrier: TcpCarrier;
|
|
12
|
+
};
|
|
13
|
+
export declare const open: (dir: string) => Promise<Opened>;
|
|
14
|
+
export declare const request: (dir: string, req: RootRequest) => Promise<JsonObject>;
|
|
15
|
+
export type Served = {
|
|
16
|
+
pk: string;
|
|
17
|
+
at: string;
|
|
18
|
+
root: string;
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
export declare const serve: (dir: string, host?: string, port?: number) => Promise<Served>;
|