nervur 0.19.2 → 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 +156 -104
- package/dist/being/being.d.ts +8 -15
- package/dist/being/being.js +20 -67
- package/dist/being/digest.d.ts +1 -1
- package/dist/being/digest.js +7 -27
- package/dist/being/faculty.d.ts +10 -0
- package/dist/being/faculty.js +74 -0
- package/dist/being/index.d.ts +5 -5
- package/dist/being/index.js +6 -5
- package/dist/being/kind.d.ts +4 -0
- package/dist/being/kind.js +28 -0
- package/dist/being/types.d.ts +35 -63
- package/dist/being/types.js +3 -57
- package/dist/being/words.d.ts +13 -0
- package/dist/being/words.js +14 -0
- 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 +39 -0
- package/dist/contract/index.js +33 -0
- package/dist/crypto/aes.d.ts +3 -0
- package/dist/crypto/aes.js +24 -0
- package/dist/crypto/bytes.d.ts +6 -0
- package/dist/crypto/bytes.js +33 -0
- package/dist/crypto/ed25519.d.ts +3 -0
- package/dist/crypto/ed25519.js +85 -0
- package/dist/crypto/hash.d.ts +2 -0
- package/dist/crypto/hash.js +11 -0
- package/dist/crypto/index.d.ts +7 -0
- package/dist/crypto/index.js +10 -0
- package/dist/crypto/json.d.ts +8 -0
- package/dist/crypto/json.js +250 -0
- package/dist/crypto/mlkem.d.ts +12 -0
- package/dist/crypto/mlkem.js +36 -0
- package/dist/crypto/subtle.d.ts +10 -0
- package/dist/crypto/subtle.js +29 -0
- package/dist/crypto/x25519.d.ts +2 -0
- package/dist/crypto/x25519.js +21 -0
- package/dist/folder/index.d.ts +14 -0
- package/dist/folder/index.js +87 -0
- package/dist/harbor/catalogue.d.ts +25 -0
- package/dist/harbor/catalogue.js +93 -0
- package/dist/harbor/dock.d.ts +29 -0
- package/dist/harbor/dock.js +122 -0
- package/dist/harbor/harbor.d.ts +31 -0
- package/dist/harbor/harbor.js +146 -0
- package/dist/harbor/index.d.ts +7 -10
- package/dist/harbor/index.js +9 -18
- package/dist/harbor/package.d.ts +10 -0
- package/dist/harbor/package.js +115 -0
- package/dist/harbor/registry.d.ts +9 -0
- package/dist/harbor/registry.js +78 -0
- package/dist/harbor/root-line.d.ts +9 -0
- package/dist/harbor/root-line.js +36 -0
- package/dist/harbor/terrain.d.ts +9 -0
- package/dist/harbor/terrain.js +2 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/pointer/bodies.d.ts +26 -0
- package/dist/pointer/bodies.js +64 -0
- package/dist/pointer/index.d.ts +2 -0
- package/dist/pointer/index.js +4 -0
- package/dist/pointer/world.d.ts +35 -0
- package/dist/pointer/world.js +85 -0
- package/dist/quo/door.d.ts +34 -0
- package/dist/quo/door.js +172 -0
- package/dist/quo/index.d.ts +8 -0
- package/dist/quo/index.js +11 -0
- package/dist/quo/invitation.d.ts +7 -0
- package/dist/quo/invitation.js +15 -0
- package/dist/quo/keys.d.ts +40 -0
- package/dist/quo/keys.js +79 -0
- package/dist/quo/payload.d.ts +15 -0
- package/dist/quo/payload.js +55 -0
- package/dist/quo/relations.d.ts +40 -0
- package/dist/quo/relations.js +33 -0
- package/dist/quo/reply.d.ts +13 -0
- package/dist/quo/reply.js +35 -0
- package/dist/quo/seal.d.ts +42 -0
- package/dist/quo/seal.js +78 -0
- package/dist/quo/standing.d.ts +39 -0
- package/dist/quo/standing.js +90 -0
- 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/allowance.d.ts +13 -9
- package/dist/ward/allowance.js +28 -67
- package/dist/ward/cells.d.ts +6 -6
- package/dist/ward/cells.js +67 -179
- package/dist/ward/index.d.ts +6 -10
- package/dist/ward/index.js +7 -13
- package/dist/ward/partition.d.ts +44 -60
- package/dist/ward/partition.js +56 -285
- package/dist/ward/pilot.d.ts +8 -0
- package/dist/ward/pilot.js +50 -0
- package/dist/ward/stance.d.ts +27 -20
- package/dist/ward/stance.js +174 -406
- package/dist/ward/ward-being.d.ts +35 -0
- package/dist/ward/ward-being.js +82 -0
- package/dist/ward/ward.d.ts +45 -11
- package/dist/ward/ward.js +239 -357
- package/package.json +24 -31
- package/src/being/being.ts +35 -78
- package/src/being/digest.ts +7 -35
- package/src/being/faculty.ts +73 -0
- package/src/being/index.ts +6 -6
- package/src/being/kind.ts +30 -0
- package/src/being/types.ts +50 -146
- package/src/being/words.ts +31 -0
- 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 +73 -0
- package/src/crypto/aes.ts +26 -0
- package/src/crypto/bytes.ts +37 -0
- package/src/crypto/ed25519.ts +84 -0
- package/src/crypto/hash.ts +14 -0
- package/src/crypto/index.ts +10 -0
- package/src/crypto/json.ts +241 -0
- package/src/crypto/mlkem.ts +38 -0
- package/src/crypto/subtle.ts +33 -0
- package/src/crypto/x25519.ts +21 -0
- package/src/folder/index.ts +92 -0
- package/src/harbor/catalogue.ts +110 -0
- package/src/harbor/dock.ts +137 -0
- package/src/harbor/harbor.ts +180 -0
- package/src/harbor/index.ts +9 -19
- package/src/harbor/package.ts +131 -0
- package/src/harbor/registry.ts +73 -0
- package/src/harbor/root-line.ts +36 -0
- package/src/harbor/terrain.ts +15 -0
- package/src/index.ts +20 -0
- package/src/pointer/bodies.ts +65 -0
- package/src/pointer/index.ts +4 -0
- package/src/pointer/world.ts +94 -0
- package/src/quo/door.ts +178 -0
- package/src/quo/index.ts +11 -0
- package/src/quo/invitation.ts +15 -0
- package/src/quo/keys.ts +91 -0
- package/src/quo/payload.ts +61 -0
- package/src/quo/relations.ts +62 -0
- package/src/quo/reply.ts +38 -0
- package/src/quo/seal.ts +101 -0
- package/src/quo/standing.ts +111 -0
- package/src/stand/main.ts +20 -0
- package/src/stand/stand.ts +220 -0
- package/src/tcp/frame.ts +78 -0
- package/src/tcp/index.ts +173 -0
- package/src/ward/allowance.ts +37 -75
- package/src/ward/cells.ts +63 -176
- package/src/ward/index.ts +7 -17
- package/src/ward/partition.ts +86 -326
- package/src/ward/pilot.ts +50 -0
- package/src/ward/stance.ts +186 -420
- package/src/ward/ward-being.ts +103 -0
- package/src/ward/ward.ts +256 -359
- package/dist/being/lent.d.ts +0 -32
- package/dist/being/lent.js +0 -72
- package/dist/being/silence.d.ts +0 -12
- package/dist/being/silence.js +0 -41
- package/dist/conformance/assert.d.ts +0 -11
- package/dist/conformance/assert.js +0 -106
- package/dist/conformance/beings.d.ts +0 -199
- package/dist/conformance/beings.js +0 -188
- package/dist/conformance/estate.d.ts +0 -5
- package/dist/conformance/estate.js +0 -388
- package/dist/conformance/index.d.ts +0 -80
- package/dist/conformance/index.js +0 -819
- package/dist/conformance/reach.d.ts +0 -10
- package/dist/conformance/reach.js +0 -72
- package/dist/conformance/store.d.ts +0 -5
- package/dist/conformance/store.js +0 -113
- package/dist/harbor/box.d.ts +0 -121
- package/dist/harbor/box.js +0 -121
- package/dist/harbor/core.d.ts +0 -55
- package/dist/harbor/core.js +0 -662
- package/dist/harbor/dial.d.ts +0 -9
- package/dist/harbor/dial.js +0 -81
- package/dist/harbor/memory.d.ts +0 -29
- package/dist/harbor/memory.js +0 -104
- package/dist/harbor/reach.d.ts +0 -36
- package/dist/harbor/reach.js +0 -199
- package/dist/harbor/store.d.ts +0 -35
- package/dist/harbor/store.js +0 -62
- package/dist/vector/cases.d.ts +0 -42
- package/dist/vector/cases.js +0 -223
- package/dist/vector/index.d.ts +0 -6
- package/dist/vector/index.js +0 -8
- package/dist/vector/stand.d.ts +0 -9
- package/dist/vector/stand.js +0 -77
- package/dist/vector/world.d.ts +0 -144
- package/dist/vector/world.js +0 -209
- package/dist/ward/arithmetic.d.ts +0 -31
- package/dist/ward/arithmetic.js +0 -249
- package/dist/ward/door.d.ts +0 -18
- package/dist/ward/door.js +0 -186
- package/dist/ward/ground.d.ts +0 -29
- package/dist/ward/ground.js +0 -56
- package/dist/ward/heirs.d.ts +0 -13
- package/dist/ward/heirs.js +0 -117
- package/dist/ward/json.d.ts +0 -2
- package/dist/ward/json.js +0 -163
- package/dist/ward/owner.d.ts +0 -13
- package/dist/ward/owner.js +0 -220
- package/dist/ward/seal.d.ts +0 -52
- package/dist/ward/seal.js +0 -150
- package/src/being/lent.ts +0 -72
- package/src/being/silence.ts +0 -46
- package/src/conformance/assert.ts +0 -100
- package/src/conformance/beings.ts +0 -188
- package/src/conformance/estate.ts +0 -412
- package/src/conformance/index.ts +0 -965
- package/src/conformance/reach.ts +0 -83
- package/src/conformance/store.ts +0 -125
- package/src/harbor/box.ts +0 -131
- package/src/harbor/core.ts +0 -699
- package/src/harbor/dial.ts +0 -112
- package/src/harbor/memory.ts +0 -123
- package/src/harbor/reach.ts +0 -221
- package/src/harbor/store.ts +0 -91
- package/src/vector/cases.ts +0 -257
- package/src/vector/index.ts +0 -11
- package/src/vector/stand.ts +0 -76
- package/src/vector/world.ts +0 -232
- package/src/ward/arithmetic.ts +0 -251
- package/src/ward/door.ts +0 -186
- package/src/ward/ground.ts +0 -142
- package/src/ward/heirs.ts +0 -116
- package/src/ward/json.ts +0 -144
- package/src/ward/owner.ts +0 -214
- package/src/ward/seal.ts +0 -178
package/src/being/types.ts
CHANGED
|
@@ -1,170 +1,74 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
import type {
|
|
2
|
+
// Everything a being author holds, as types.
|
|
3
|
+
import type { Json } from '../crypto/index.ts';
|
|
4
|
+
import type { Invitation } from '../quo/index.ts';
|
|
5
|
+
import type { Silence, Word } from './words.ts';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
export type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
|
|
7
|
+
export type { Invitation, Json };
|
|
8
8
|
export type JsonObject = { [key: string]: Json };
|
|
9
9
|
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// The ids the ward speaks under, and no being may mint. An asker id is hers:
|
|
14
|
-
// she chooses the strings, and the ward has no business in her namespace. But
|
|
15
|
-
// the ward must sometimes stand at her door itself — its describe asks every
|
|
16
|
-
// being under OWNER — and an id it shares with one of her occupants is two
|
|
17
|
-
// different parties wearing one name. She could not tell them apart, and the
|
|
18
|
-
// ward would shape its describe through an occupant's gate.
|
|
19
|
-
//
|
|
20
|
-
// So the ward's words are shouted, and refused at the mint. A word can only
|
|
21
|
-
// be reserved before anyone has used it: a partition already holding an
|
|
22
|
-
// occupant of that name could never have it taken back. PUBLIC guards nothing
|
|
23
|
-
// today — a public asker is {} and carries no id at all — and that is exactly
|
|
24
|
-
// why it is claimed now, while claiming it is free.
|
|
10
|
+
// Who is at the door: the id she filed the occupant under, `{}` for nobody,
|
|
11
|
+
// and `{ id: OWNER }` for whoever holds the ward's unsealed ask.
|
|
12
|
+
export type Asker = { readonly id: string } | { readonly id?: undefined };
|
|
25
13
|
export const OWNER = 'OWNER';
|
|
26
|
-
export const PUBLIC = 'PUBLIC';
|
|
27
|
-
export const RESERVED_IDS: readonly string[] = [OWNER, PUBLIC];
|
|
28
14
|
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
export
|
|
15
|
+
// The ward-being's key in her ward, and so the id every being she makes
|
|
16
|
+
// knows her by.
|
|
17
|
+
export const WARD = 'ward';
|
|
32
18
|
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// an invitation to a public being carries no heir and no secret, and a key
|
|
37
|
-
// present and undefined is not the same object once JSON has been through it.
|
|
38
|
-
export const invitationArgs = (inv: Invitation): JsonObject => ({
|
|
39
|
-
ward: inv.ward,
|
|
40
|
-
...(inv.heir !== undefined ? { heir: inv.heir } : {}),
|
|
41
|
-
...(inv.secret !== undefined ? { secret: inv.secret } : {}),
|
|
42
|
-
});
|
|
19
|
+
// What comes back from an ask, and what a being answers.
|
|
20
|
+
export type Answer = Json | Silence | Word;
|
|
21
|
+
export type Reply = Json | Silence | undefined;
|
|
43
22
|
|
|
44
|
-
|
|
45
|
-
// the reading of it is too, and it is one reading: a form that
|
|
46
|
-
// asks a guest for one, a lent being handed one to wake her maker by, and a
|
|
47
|
-
// shell that finds one in a link all ask the same question, and a kinder
|
|
48
|
-
// answer in one of them is a value that fails at a door instead of at the
|
|
49
|
-
// edge it came in by. A ward pk is a hundred and twenty-eight lowercase hex,
|
|
50
|
-
// and a heir comes with its secret or neither comes: an invitation to a
|
|
51
|
-
// public being carries no heir, and a heir with no secret opens nothing.
|
|
52
|
-
export const isInvitation = (v: unknown): v is Invitation => {
|
|
53
|
-
if (v === null || typeof v !== 'object' || Array.isArray(v)) return false;
|
|
54
|
-
const o = v as Record<string, unknown>;
|
|
55
|
-
if (typeof o.ward !== 'string' || !/^[0-9a-f]{128}$/.test(o.ward)) return false;
|
|
56
|
-
const heir = typeof o.heir === 'string',
|
|
57
|
-
secret = typeof o.secret === 'string';
|
|
58
|
-
return (heir && secret) || (!heir && !secret && !('heir' in o) && !('secret' in o));
|
|
59
|
-
};
|
|
23
|
+
export type Wanted = { readonly time?: number };
|
|
60
24
|
|
|
61
|
-
// A blueprint
|
|
25
|
+
// A blueprint: the asks a being can be asked, as an MCP tool list, and notes.
|
|
62
26
|
export type Schema = JsonObject;
|
|
63
|
-
export type Ask = { name: string; description?: string; input: Schema
|
|
27
|
+
export type Ask = { name: string; description?: string; input: Schema };
|
|
64
28
|
export type Blueprint = { asks: Ask[]; notes: Json };
|
|
65
29
|
|
|
66
|
-
|
|
67
|
-
// the far side of a door is somebody else's code, so nothing may be written
|
|
68
|
-
// into a standing's record as a blueprint without being read as one first: a
|
|
69
|
-
// side walks `asks` by name, and a list of anything else, or no list at all,
|
|
70
|
-
// is a side that breaks on a far ward's answer. The asks are checked to the
|
|
71
|
-
// depth a side reads them, a name and an input each, and no further: what a
|
|
72
|
-
// far being puts beside those is hers.
|
|
73
|
-
export const isBlueprint = (v: unknown): v is Blueprint => {
|
|
74
|
-
if (v === null || typeof v !== 'object' || Array.isArray(v)) return false;
|
|
75
|
-
const { asks, notes } = v as { asks?: unknown; notes?: unknown };
|
|
76
|
-
if (!Array.isArray(asks) || notes === undefined) return false;
|
|
77
|
-
return asks.every((a) => a !== null && typeof a === 'object' && !Array.isArray(a) && typeof (a as Ask).name === 'string' && (a as Ask).input !== null && typeof (a as Ask).input === 'object' && !Array.isArray((a as Ask).input));
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export type StandingRecord = { id: string; digest: string | null; blueprint: Blueprint | null; seen: string | null };
|
|
81
|
-
export type OccupantRecord = { id: string; notes: JsonObject };
|
|
82
|
-
export type Cells = {
|
|
83
|
-
standings: Record<string, StandingRecord>;
|
|
84
|
-
occupants: Record<string, OccupantRecord>;
|
|
85
|
-
[hers: string]: Json;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export type Silence = typeof silence;
|
|
89
|
-
|
|
90
|
-
// The ward's words: what her ward says when no object came back and it knows
|
|
91
|
-
// why. A door says the first five to a key it has bound, and rides them on
|
|
92
|
-
// the wire as `{ quo: word }`; her own ward says the last four to her and
|
|
93
|
-
// they never leave the ward. Each is one frozen object under one symbol key,
|
|
94
|
-
// so no being makes one by accident and none crosses an edge as a value. A
|
|
95
|
-
// being who reaches for the symbol on purpose makes an object her ward reads
|
|
96
|
-
// as her having thrown, which is what a being who returns nonsense gets: the
|
|
97
|
-
// key is a guard against collision, never against her.
|
|
98
|
-
export type DoorWord = 'removed' | 'absent' | 'unannounced' | 'repeated' | 'threw';
|
|
99
|
-
export type WardWord = 'unreached' | 'late' | 'invitation' | 'dropped';
|
|
100
|
-
export type WordName = DoorWord | WardWord;
|
|
101
|
-
export const WORD_KEY: unique symbol = Symbol.for('quo.word');
|
|
102
|
-
export type Word<W extends WordName = WordName> = { readonly [K in typeof WORD_KEY]: W };
|
|
103
|
-
export type Unreached = Word<'unreached'>;
|
|
104
|
-
|
|
105
|
-
// What comes back from an ask or a knock.
|
|
106
|
-
export type Answer = Json | Silence | Word;
|
|
107
|
-
// What a being answers.
|
|
108
|
-
export type Reply = Json | Silence;
|
|
109
|
-
|
|
110
|
-
// What a being may ask for when she wants to say so herself: the time this
|
|
111
|
-
// one ask may spend. Optional, and so is saying anything at all: an ask that
|
|
112
|
-
// says nothing gets her ward's default, which is the ordinary way to ask.
|
|
113
|
-
export type Wanted = { time?: number };
|
|
114
|
-
|
|
115
|
-
export type Standing = {
|
|
30
|
+
export interface Standing {
|
|
116
31
|
readonly id: string;
|
|
117
32
|
ask(method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
|
|
118
|
-
}
|
|
33
|
+
}
|
|
119
34
|
|
|
120
|
-
|
|
121
|
-
|
|
35
|
+
// Her side of the relations she holds.
|
|
36
|
+
export interface Standings {
|
|
37
|
+
// Knocks with the empty ask and holds the relation under `id`: the id, or
|
|
38
|
+
// null where the id is taken or reserved, or no object came back.
|
|
122
39
|
take(id: string, invitation: Invitation): Promise<string | null>;
|
|
123
|
-
|
|
124
|
-
|
|
40
|
+
get(id: string): Standing | undefined;
|
|
41
|
+
ids(): string[];
|
|
42
|
+
remove(id: string): boolean;
|
|
43
|
+
}
|
|
125
44
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// written on the occupant for her gate to read, and hers alone after that.
|
|
45
|
+
// The door's side: the ids she invited.
|
|
46
|
+
export interface Occupants {
|
|
129
47
|
invite(id: string, notes?: JsonObject): Promise<Invitation | null>;
|
|
48
|
+
notes(id: string): JsonObject | undefined;
|
|
49
|
+
ids(): string[];
|
|
50
|
+
remove(id: string): boolean;
|
|
51
|
+
}
|
|
130
52
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
export type Stance = {
|
|
136
|
-
readonly cells: Cells;
|
|
53
|
+
// What every being is handed at birth.
|
|
54
|
+
export interface Stance {
|
|
55
|
+
readonly key: string;
|
|
56
|
+
readonly cells: JsonObject;
|
|
137
57
|
readonly occupants: Occupants;
|
|
138
58
|
readonly standings: Standings;
|
|
139
|
-
// A
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// holds afterwards is an ordinary standing that counts, rotates, can be
|
|
148
|
-
// removed and hears `removed`.
|
|
149
|
-
//
|
|
150
|
-
// The ground is the booting harbor's, so this reaches the box she is
|
|
151
|
-
// running on and no other. A standing she wakes up holding names a being on
|
|
152
|
-
// the box she was on when it was made, and a migration is a restart she
|
|
153
|
-
// cannot tell from any other, so a being who wants the box she is on asks
|
|
154
|
-
// again at every birth.
|
|
155
|
-
lend(name: string, id: string): Promise<string | null>;
|
|
156
|
-
// A new being of her ward, by class name, under a key she chooses. The key
|
|
157
|
-
// back, or null: the key is taken, the harbor holds no such class, or the
|
|
158
|
-
// class threw at birth. A being may make; only the owner reaches into
|
|
159
|
-
// another. Naming an id asks for a way back too: the being made holds an
|
|
160
|
-
// occupant for her maker under the maker's own key, and the maker holds
|
|
161
|
-
// the standing under that id. A relation that could not be made is a boot
|
|
162
|
-
// that made nobody, and the being made goes with it.
|
|
163
|
-
boot(className: string, key: string, id?: string): Promise<string | null>;
|
|
164
|
-
};
|
|
59
|
+
// A new being of her ward, by kind, under a key she chooses. With an id,
|
|
60
|
+
// the new being invites her under her key, and she takes it under id.
|
|
61
|
+
boot(kind: string, key: string, id?: string): Promise<string | null>;
|
|
62
|
+
// A standing under `id` at the faculty this harbor opened for a contract:
|
|
63
|
+
// the id, or null where none is open, the contract declares no kind of
|
|
64
|
+
// its own, or the relation was refused.
|
|
65
|
+
lend(contract: abstract new (...args: never[]) => unknown, id: string): Promise<string | null>;
|
|
66
|
+
}
|
|
165
67
|
|
|
166
|
-
//
|
|
68
|
+
// Anything with `answer` is a being.
|
|
167
69
|
export interface BeingLike {
|
|
168
|
-
answer(asker: Asker, method
|
|
70
|
+
answer(asker: Asker, method: string | undefined, args: JsonObject): Reply | Promise<Reply>;
|
|
71
|
+
describe?(asker: Asker): Blueprint;
|
|
169
72
|
}
|
|
170
|
-
|
|
73
|
+
// A class that stands. Its kind is its own, as `kind.ts` says.
|
|
74
|
+
export type BeingClass = (new (stance: Stance) => BeingLike) & { readonly name: string; readonly kind: string };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// What a being is told when no object came back. Silence is one symbol.
|
|
3
|
+
// A word is one frozen object per name, so a being never makes one by
|
|
4
|
+
// accident, and two copies of the package in one bundle still agree.
|
|
5
|
+
//
|
|
6
|
+
// removed, unannounced, repeated a far door's words, as Quo says them
|
|
7
|
+
// unreached nothing came back: asking again is safe
|
|
8
|
+
// late the allowance ran out
|
|
9
|
+
// dropped the standing was removed while asking
|
|
10
|
+
// invitation what was handed is no invitation
|
|
11
|
+
import type { Word as DoorWord } from '../quo/index.ts';
|
|
12
|
+
|
|
13
|
+
export const silence: unique symbol = Symbol.for('nervur.silence');
|
|
14
|
+
export type Silence = typeof silence;
|
|
15
|
+
export const isSilence = (x: unknown): x is Silence => x === silence;
|
|
16
|
+
|
|
17
|
+
export type WordName = DoorWord | 'unreached' | 'late' | 'dropped' | 'invitation';
|
|
18
|
+
const KEY: unique symbol = Symbol.for('nervur.word');
|
|
19
|
+
export type Word<W extends WordName = WordName> = { readonly [KEY]: W };
|
|
20
|
+
|
|
21
|
+
const made = new Map<WordName, Word>();
|
|
22
|
+
export const word = <W extends WordName>(name: W): Word<W> => {
|
|
23
|
+
let w = made.get(name);
|
|
24
|
+
if (!w) made.set(name, (w = Object.freeze({ [KEY]: name })));
|
|
25
|
+
return w as Word<W>;
|
|
26
|
+
};
|
|
27
|
+
export const isWord = (x: unknown): x is Word => typeof x === 'object' && x !== null && typeof (x as Record<symbol, unknown>)[KEY] === 'string';
|
|
28
|
+
|
|
29
|
+
// A word by its name, and anything else as it is, so one comparison says
|
|
30
|
+
// which answer came back.
|
|
31
|
+
export const told = (x: unknown): unknown => (isWord(x) ? x[KEY] : x);
|
|
@@ -0,0 +1,188 @@
|
|
|
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 type { JsonObject } from '../being/index.ts';
|
|
37
|
+
import type { RootRequest } from '../harbor/index.ts';
|
|
38
|
+
import { open, request, serve } from './harbor.ts';
|
|
39
|
+
|
|
40
|
+
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]';
|
|
41
|
+
const FLAGS = new Set(['dir', 'ward', 'via', 'seed', 'host', 'port']);
|
|
42
|
+
|
|
43
|
+
export const parse = (argv: string[]): { command?: string; words: string[]; flags: Record<string, string> } => {
|
|
44
|
+
const flags: Record<string, string> = {};
|
|
45
|
+
const words: string[] = [];
|
|
46
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
47
|
+
const arg = argv[i]!;
|
|
48
|
+
const name = arg.startsWith('--') ? arg.slice(2) : undefined;
|
|
49
|
+
if (name === undefined) words.push(arg);
|
|
50
|
+
else if (!FLAGS.has(name) || argv[i + 1] === undefined) throw new Error(`--${name} is no flag with a value`);
|
|
51
|
+
else flags[name] = argv[(i += 1)]!;
|
|
52
|
+
}
|
|
53
|
+
const [command, ...rest] = words;
|
|
54
|
+
return { ...(command === undefined ? {} : { command }), words: rest, flags };
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const none = (word: string): string | null => (word === 'none' ? null : word);
|
|
58
|
+
const json = (word: string | undefined): JsonObject | undefined => (word === undefined ? undefined : (JSON.parse(word) as JsonObject));
|
|
59
|
+
const catalogue = (method: string, args: JsonObject = {}): RootRequest => ({ method: 'ask', args: { being: 'catalogue', method, args } });
|
|
60
|
+
|
|
61
|
+
// The root request a command names, or a throw saying what it lacks.
|
|
62
|
+
export const requestOf = (command: string, words: string[], flags: Record<string, string>): RootRequest => {
|
|
63
|
+
const need = (n: number): string[] => {
|
|
64
|
+
if (words.length !== n) throw new Error(`${command} takes ${n} argument${n === 1 ? '' : 's'}`);
|
|
65
|
+
return words;
|
|
66
|
+
};
|
|
67
|
+
const at = flags.ward === undefined ? {} : { ward: flags.ward };
|
|
68
|
+
switch (command) {
|
|
69
|
+
case 'module': {
|
|
70
|
+
const [verb, what] = need(2);
|
|
71
|
+
if (verb === 'add') return catalogue('add', { from: resolve(what!) });
|
|
72
|
+
if (verb === 'remove') 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) throw new Error('ask takes at most a being, a method and its args');
|
|
105
|
+
const [being, method, args] = words;
|
|
106
|
+
return { ...at, method: 'ask', args: { ...(being === undefined ? {} : { being }), ...(method === undefined ? {} : { method }), ...(args === undefined ? {} : { args: json(args)! }) } };
|
|
107
|
+
}
|
|
108
|
+
case 'own':
|
|
109
|
+
case 'disown':
|
|
110
|
+
return { ...at, method: command, args: { id: need(1)[0]! } };
|
|
111
|
+
default:
|
|
112
|
+
throw new Error(USAGE);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// An error answer at any depth the root line or an ask carries it.
|
|
117
|
+
const failed = (answer: unknown): boolean => {
|
|
118
|
+
if (typeof answer !== 'object' || answer === null) return false;
|
|
119
|
+
const a = answer as JsonObject;
|
|
120
|
+
return 'error' in a || failed(a.answer);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const main = async (argv: string[], env: Record<string, string | undefined>, print: (line: string) => void): Promise<number> => {
|
|
124
|
+
const { command, words, flags } = parse(argv);
|
|
125
|
+
if (command === undefined) throw new Error(USAGE);
|
|
126
|
+
const dir = flags.dir ?? env.NERVUR_DIR ?? join(homedir(), '.nervur');
|
|
127
|
+
const out = (answer: JsonObject): number => {
|
|
128
|
+
print(JSON.stringify(answer));
|
|
129
|
+
return failed(answer) ? 1 : 0;
|
|
130
|
+
};
|
|
131
|
+
if (command === 'init') {
|
|
132
|
+
if (words.length > 0) throw new Error('init takes no argument');
|
|
133
|
+
if (existsSync(join(dir, 'seed'))) return out({ error: `a harbor stands at ${dir}` });
|
|
134
|
+
const { harbor, carrier } = await open(dir);
|
|
135
|
+
try {
|
|
136
|
+
const owner = await harbor.root('own', { id: 'owner' });
|
|
137
|
+
return out({ pk: harbor.box.pk, owner: owner as JsonObject });
|
|
138
|
+
} finally {
|
|
139
|
+
await carrier.close();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (command === 'serve') {
|
|
143
|
+
if (words.length > 0) throw new Error('serve takes no argument');
|
|
144
|
+
const served = await serve(dir, flags.host, flags.port === undefined ? undefined : Number(flags.port));
|
|
145
|
+
const stop = (): void => void served.close().then(() => process.exit(0));
|
|
146
|
+
process.once('SIGINT', stop);
|
|
147
|
+
process.once('SIGTERM', stop);
|
|
148
|
+
print(JSON.stringify({ pk: served.pk, at: served.at, root: served.root }));
|
|
149
|
+
return SERVING;
|
|
150
|
+
}
|
|
151
|
+
if (command === 'pilot') {
|
|
152
|
+
if (words.length !== 3) throw new Error('pilot takes 3 arguments');
|
|
153
|
+
const [name, invitation, at] = words as [string, string, string];
|
|
154
|
+
const held = JSON.parse(invitation) as JsonObject;
|
|
155
|
+
for (const req of pilotRequests(name, held, at)) {
|
|
156
|
+
const answer = await request(dir, req);
|
|
157
|
+
if (!failed(answer)) continue;
|
|
158
|
+
if (req.method === 'ask') await request(dir, { method: 'unboot', args: { being: name } });
|
|
159
|
+
return out(answer);
|
|
160
|
+
}
|
|
161
|
+
return out({ piloting: name, ward: held.ward! });
|
|
162
|
+
}
|
|
163
|
+
if (flags.via === undefined) return out(await request(dir, requestOf(command, words, flags)));
|
|
164
|
+
if (flags.ward !== undefined) throw new Error('--via takes no --ward');
|
|
165
|
+
return out(farAnswer(await request(dir, via(flags.via, requestOf(command, words, flags)))));
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// The root requests that hold a far ward: its route, the pilot, the take.
|
|
169
|
+
export const pilotRequests = (name: string, invitation: JsonObject, at: string): RootRequest[] => [
|
|
170
|
+
{ method: 'route', args: { ward: invitation.ward ?? null, at } },
|
|
171
|
+
{ method: 'boot', args: { key: name, class: 'org.nervur.pilot' } },
|
|
172
|
+
{ method: 'ask', args: { being: name, method: 'hold', args: { invitation } } },
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
// A root request, asked of the far ward a pilot holds.
|
|
176
|
+
export const via = (pilot: string, far: RootRequest): RootRequest => ({
|
|
177
|
+
method: 'ask',
|
|
178
|
+
args: { being: pilot, method: 'pilot', args: { ...(far.method === undefined ? {} : { method: far.method }), ...(far.args === undefined ? {} : { args: far.args }) } },
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// The far ward-being's answer out of the pilot's, or the local error.
|
|
182
|
+
export const farAnswer = (local: JsonObject): JsonObject => {
|
|
183
|
+
const inner = (local.answer as JsonObject | undefined)?.answer;
|
|
184
|
+
return typeof inner === 'object' && inner !== null && !Array.isArray(inner) ? inner : local;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// What `main` answers for a harbor it keeps serving: no exit yet.
|
|
188
|
+
export const SERVING = -1;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// A harbor on this machine, as the `nervur` command stands it: its seed and
|
|
3
|
+
// sealed memory in a folder, its code imported from the files its
|
|
4
|
+
// catalogue names, Quo over TCP as its carrier, and the root line on a
|
|
5
|
+
// local socket beside them.
|
|
6
|
+
//
|
|
7
|
+
// <dir>/seed the harbor's seed
|
|
8
|
+
// <dir>/<place>/ its sealed memory
|
|
9
|
+
// <dir>/root.sock the root line while the harbor is served, open to
|
|
10
|
+
// this user alone, or in the temporary folder for a
|
|
11
|
+
// folder too deep for a socket path
|
|
12
|
+
//
|
|
13
|
+
// A root request goes to a served harbor over its socket. With none served,
|
|
14
|
+
// the command unpacks the harbor from its folder, asks, and closes it, so
|
|
15
|
+
// one process at a time writes the folder.
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
17
|
+
import { chmod, rm } from 'node:fs/promises';
|
|
18
|
+
import { connect, createServer, type Server } from 'node:net';
|
|
19
|
+
import { tmpdir } from 'node:os';
|
|
20
|
+
import { join, resolve } from 'node:path';
|
|
21
|
+
import { pathToFileURL } from 'node:url';
|
|
22
|
+
import type { JsonObject } from '../being/index.ts';
|
|
23
|
+
import { Loader, type Loaded } from '../contract/index.ts';
|
|
24
|
+
import { FolderCustody, FolderMemory } from '../folder/index.ts';
|
|
25
|
+
import { Harbor, rootLine, type RootRequest } from '../harbor/index.ts';
|
|
26
|
+
import { CryptoEntropy, PointerTerrain } from '../pointer/index.ts';
|
|
27
|
+
import { TcpCarrier, TcpListener } from '../tcp/index.ts';
|
|
28
|
+
|
|
29
|
+
// A socket path has a hundred bytes or so on every system, so a folder too
|
|
30
|
+
// deep for one puts its socket in this user's temporary folder, under a
|
|
31
|
+
// name drawn from the folder's own path.
|
|
32
|
+
const SOCKET_PATH = 100;
|
|
33
|
+
export const socketOf = (dir: string): string => {
|
|
34
|
+
const beside = join(resolve(dir), 'root.sock');
|
|
35
|
+
if (Buffer.byteLength(beside) <= SOCKET_PATH) return beside;
|
|
36
|
+
return join(tmpdir(), `nervur-${createHash('sha256').update(resolve(dir)).digest('hex').slice(0, 32)}.sock`);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Code from a file: a module that exports `module`, `version` and
|
|
40
|
+
// `classes`, found again by its absolute path. A file that cannot be
|
|
41
|
+
// imported, or exports no module, is none.
|
|
42
|
+
export class ImportLoader extends Loader {
|
|
43
|
+
async load(from: string): Promise<Loaded | null> {
|
|
44
|
+
const path = resolve(from);
|
|
45
|
+
let exported: { module?: unknown; version?: unknown; classes?: unknown };
|
|
46
|
+
try {
|
|
47
|
+
exported = (await import(pathToFileURL(path).href)) as typeof exported;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const { module, version, classes } = exported;
|
|
52
|
+
if (typeof module !== 'string' || typeof version !== 'string' || !Array.isArray(classes)) return null;
|
|
53
|
+
return { module, version, classes, from: path };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type Opened = { harbor: Harbor; carrier: TcpCarrier };
|
|
58
|
+
|
|
59
|
+
export const open = async (dir: string): Promise<Opened> => {
|
|
60
|
+
const entropy = new CryptoEntropy();
|
|
61
|
+
const carrier = new TcpCarrier();
|
|
62
|
+
const harbor = await Harbor.open(new PointerTerrain({ entropy, carrier, loader: new ImportLoader(), custody: new FolderCustody(dir, entropy), memory: new FolderMemory(dir) }));
|
|
63
|
+
return { harbor, carrier };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// One root request over a served harbor's socket, or undefined where none
|
|
67
|
+
// is served.
|
|
68
|
+
const overSocket = (dir: string, request: RootRequest): Promise<JsonObject | undefined> =>
|
|
69
|
+
new Promise((done, fail) => {
|
|
70
|
+
const socket = connect(socketOf(dir));
|
|
71
|
+
let text = '';
|
|
72
|
+
socket.once('connect', () => socket.write(`${JSON.stringify(request)}\n`));
|
|
73
|
+
socket.on('data', (chunk: Buffer) => {
|
|
74
|
+
text += chunk.toString('utf8');
|
|
75
|
+
const end = text.indexOf('\n');
|
|
76
|
+
if (end < 0) return;
|
|
77
|
+
socket.end();
|
|
78
|
+
done(JSON.parse(text.slice(0, end)) as JsonObject);
|
|
79
|
+
});
|
|
80
|
+
socket.once('error', (e: NodeJS.ErrnoException) => (e.code === 'ENOENT' || e.code === 'ECONNREFUSED' ? done(undefined) : fail(e)));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export const request = async (dir: string, req: RootRequest): Promise<JsonObject> => {
|
|
84
|
+
const served = await overSocket(dir, req);
|
|
85
|
+
if (served !== undefined) return served;
|
|
86
|
+
const { harbor, carrier } = await open(dir);
|
|
87
|
+
try {
|
|
88
|
+
return await rootLine(harbor, req);
|
|
89
|
+
} finally {
|
|
90
|
+
await carrier.close();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export type Served = { pk: string; at: string; root: string; close(): Promise<void> };
|
|
95
|
+
|
|
96
|
+
// The harbor served: Quo over TCP on host and port, the root line on the
|
|
97
|
+
// socket. A socket file nobody answers on is a stale one, and is replaced.
|
|
98
|
+
export const serve = async (dir: string, host?: string, port?: number): Promise<Served> => {
|
|
99
|
+
if ((await overSocket(dir, {})) !== undefined) throw new Error(`a harbor is already served from ${dir}`);
|
|
100
|
+
const { harbor, carrier } = await open(dir);
|
|
101
|
+
const listener = await TcpListener.listen(harbor, host, port);
|
|
102
|
+
const root = socketOf(dir);
|
|
103
|
+
await rm(root, { force: true });
|
|
104
|
+
const server: Server = createServer((socket) => {
|
|
105
|
+
let text = '';
|
|
106
|
+
socket.on('data', (chunk: Buffer) => {
|
|
107
|
+
text += chunk.toString('utf8');
|
|
108
|
+
for (let end = text.indexOf('\n'); end >= 0; end = text.indexOf('\n')) {
|
|
109
|
+
const line = text.slice(0, end);
|
|
110
|
+
text = text.slice(end + 1);
|
|
111
|
+
let value: unknown;
|
|
112
|
+
try {
|
|
113
|
+
value = JSON.parse(line);
|
|
114
|
+
} catch {
|
|
115
|
+
value = undefined;
|
|
116
|
+
}
|
|
117
|
+
void rootLine(harbor, value)
|
|
118
|
+
.catch(() => ({ error: 'the harbor threw' }))
|
|
119
|
+
.then((answer) => socket.write(`${JSON.stringify(answer)}\n`));
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
socket.on('error', () => socket.destroy());
|
|
123
|
+
});
|
|
124
|
+
await new Promise<void>((done) => server.listen(root, done));
|
|
125
|
+
await chmod(root, 0o600);
|
|
126
|
+
return {
|
|
127
|
+
pk: harbor.box.pk,
|
|
128
|
+
at: listener.at,
|
|
129
|
+
root,
|
|
130
|
+
close: async () => {
|
|
131
|
+
await new Promise((done) => server.close(done));
|
|
132
|
+
await rm(root, { force: true });
|
|
133
|
+
await listener.close();
|
|
134
|
+
await carrier.close();
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
};
|
package/src/cli/main.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// The `nervur` program: `command.ts` over this process's arguments. A
|
|
4
|
+
// command that cannot run exits 2 and says why on stderr.
|
|
5
|
+
import { main, SERVING } from './command.ts';
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const code = await main(process.argv.slice(2), process.env, (line) => process.stdout.write(`${line}\n`));
|
|
9
|
+
if (code !== SERVING) process.exitCode = code;
|
|
10
|
+
} catch (e) {
|
|
11
|
+
process.stderr.write(`nervur: ${(e as Error).message}\n`);
|
|
12
|
+
process.exitCode = 2;
|
|
13
|
+
}
|