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.
Files changed (93) hide show
  1. package/README.md +107 -20
  2. package/dist/being/being.d.ts +1 -0
  3. package/dist/being/being.js +5 -1
  4. package/dist/being/faculty.d.ts +1 -0
  5. package/dist/being/faculty.js +13 -7
  6. package/dist/being/index.d.ts +1 -0
  7. package/dist/being/index.js +1 -0
  8. package/dist/being/kind.d.ts +4 -0
  9. package/dist/being/kind.js +28 -0
  10. package/dist/being/types.d.ts +3 -4
  11. package/dist/cli/command.d.ts +13 -0
  12. package/dist/cli/command.js +194 -0
  13. package/dist/cli/harbor.d.ts +21 -0
  14. package/dist/cli/harbor.js +141 -0
  15. package/dist/cli/main.d.ts +2 -0
  16. package/dist/cli/main.js +14 -0
  17. package/dist/contract/index.d.ts +15 -2
  18. package/dist/contract/index.js +16 -2
  19. package/dist/folder/index.d.ts +5 -4
  20. package/dist/folder/index.js +45 -83
  21. package/dist/harbor/catalogue.d.ts +25 -0
  22. package/dist/harbor/catalogue.js +93 -0
  23. package/dist/harbor/dock.d.ts +12 -2
  24. package/dist/harbor/dock.js +39 -9
  25. package/dist/harbor/harbor.d.ts +15 -6
  26. package/dist/harbor/harbor.js +83 -21
  27. package/dist/harbor/index.d.ts +4 -1
  28. package/dist/harbor/index.js +5 -1
  29. package/dist/harbor/package.d.ts +10 -0
  30. package/dist/harbor/package.js +115 -0
  31. package/dist/harbor/registry.d.ts +5 -3
  32. package/dist/harbor/registry.js +72 -7
  33. package/dist/harbor/root-line.d.ts +9 -0
  34. package/dist/harbor/root-line.js +36 -0
  35. package/dist/harbor/terrain.d.ts +2 -1
  36. package/dist/index.d.ts +4 -4
  37. package/dist/index.js +5 -5
  38. package/dist/pointer/bodies.d.ts +9 -3
  39. package/dist/pointer/bodies.js +37 -18
  40. package/dist/pointer/index.d.ts +1 -1
  41. package/dist/pointer/index.js +1 -1
  42. package/dist/pointer/world.d.ts +5 -4
  43. package/dist/pointer/world.js +23 -14
  44. package/dist/quo/door.js +2 -2
  45. package/dist/quo/keys.js +1 -1
  46. package/dist/tcp/frame.d.ts +29 -0
  47. package/dist/tcp/frame.js +74 -0
  48. package/dist/tcp/index.d.ts +18 -0
  49. package/dist/tcp/index.js +164 -0
  50. package/dist/ward/index.d.ts +1 -0
  51. package/dist/ward/index.js +1 -0
  52. package/dist/ward/pilot.d.ts +8 -0
  53. package/dist/ward/pilot.js +50 -0
  54. package/dist/ward/stance.d.ts +2 -4
  55. package/dist/ward/stance.js +7 -6
  56. package/dist/ward/ward-being.d.ts +2 -0
  57. package/dist/ward/ward-being.js +7 -1
  58. package/dist/ward/ward.d.ts +4 -2
  59. package/dist/ward/ward.js +35 -5
  60. package/package.json +8 -1
  61. package/src/being/being.ts +5 -1
  62. package/src/being/faculty.ts +14 -7
  63. package/src/being/index.ts +1 -0
  64. package/src/being/kind.ts +30 -0
  65. package/src/being/types.ts +8 -6
  66. package/src/cli/command.ts +188 -0
  67. package/src/cli/harbor.ts +137 -0
  68. package/src/cli/main.ts +13 -0
  69. package/src/contract/index.ts +38 -9
  70. package/src/folder/index.ts +44 -86
  71. package/src/harbor/catalogue.ts +110 -0
  72. package/src/harbor/dock.ts +48 -12
  73. package/src/harbor/harbor.ts +104 -29
  74. package/src/harbor/index.ts +6 -2
  75. package/src/harbor/package.ts +131 -0
  76. package/src/harbor/registry.ts +62 -9
  77. package/src/harbor/root-line.ts +36 -0
  78. package/src/harbor/terrain.ts +3 -1
  79. package/src/index.ts +5 -5
  80. package/src/pointer/bodies.ts +34 -16
  81. package/src/pointer/index.ts +1 -1
  82. package/src/pointer/world.ts +21 -18
  83. package/src/quo/door.ts +2 -2
  84. package/src/quo/keys.ts +1 -1
  85. package/src/stand/main.ts +1 -0
  86. package/src/stand/stand.ts +46 -4
  87. package/src/tcp/frame.ts +78 -0
  88. package/src/tcp/index.ts +173 -0
  89. package/src/ward/index.ts +1 -0
  90. package/src/ward/pilot.ts +50 -0
  91. package/src/ward/stance.ts +8 -7
  92. package/src/ward/ward-being.ts +7 -1
  93. 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', [SystemTime, Greeter]);
47
- await harbor.root('open', { key: 'clock', class: 'SystemTime' });
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: 'Greeter' });
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
- 1. The terrain gives the box ward's seed and memory.
59
- 2. The box ward unpacks, its ward-being the dock.
60
- 3. Its faculties are born from their rows.
61
- 4. Every ward the dock hosts unpacks.
62
- 5. In every ward, the ward-being first, then each being from her row.
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 classes you hand a harbor only resolve a
65
- name a row keeps. Opening a harbor on empty memory is its genesis, and
66
- the root's asks write the rest: `open`, `host`, and `boot` on a ward.
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 terrain
73
- fulfils. Each has one suite, and every body that fulfils it passes it.
74
- - **Harbor, Terrain, Registry, Dock, WardBeing.** The onion.
75
- - **World, PointerTerrain** and the pointer bodies. A whole world with
76
- this package alone.
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
- Carriers over a network, storage beyond a folder, key custody, and every
80
- screen are `@nervur-org/dock`'s, as classes fulfilling these contracts.
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
 
@@ -5,6 +5,7 @@ export type AskSpec = {
5
5
  readonly for?: (asker: Asker, notes: JsonObject | undefined) => boolean;
6
6
  };
7
7
  export declare class Being {
8
+ static readonly kind: string;
8
9
  static cells: JsonObject;
9
10
  static asks: Record<string, AskSpec>;
10
11
  readonly stance: Stance;
@@ -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 = {};
@@ -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;
@@ -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 { ... } the contract
6
- // class IntervalTimer extends Timer { ... } one class that fulfils it
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. Two asks are the
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 name.
22
+ // `Faculty`, by kind. A class among them with no kind of its own throws.
17
23
  static contracts(C) {
18
- const names = [];
24
+ const kinds = [];
19
25
  for (let c = C; typeof c === 'function' && c !== Faculty; c = Object.getPrototypeOf(c))
20
- names.push(c.name);
21
- return names;
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;
@@ -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';
@@ -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,4 @@
1
+ export declare const KIT = "org.nervur.";
2
+ export declare const isKind: (text: unknown) => text is string;
3
+ export declare const ownKind: (C: unknown) => string | undefined;
4
+ export declare const kindOf: (C: unknown) => string;
@@ -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
+ };
@@ -48,10 +48,8 @@ export interface Stance {
48
48
  readonly cells: JsonObject;
49
49
  readonly occupants: Occupants;
50
50
  readonly standings: Standings;
51
- boot(className: string, key: string, id?: string): Promise<string | null>;
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>;