@ccmsg/cli 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.ts +301 -198
- package/src/daemon/registry.ts +151 -128
- package/src/daemon/supervise.ts +16 -4
- package/src/instance/ccmsg-config.d.ts +10 -9
- package/src/instance/config.ts +374 -310
- package/src/instance/identity.ts +3 -3
- package/src/instance/instance.ts +63 -25
- package/src/instance/paths.ts +24 -6
- package/src/mesh/index.ts +0 -1
- package/src/mesh/mesh.ts +18 -61
- package/src/mesh/wire.ts +1 -29
- package/src/mesh/probe.ts +0 -105
package/src/daemon/registry.ts
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, isAbsolute, join, resolve } from "node:path";
|
|
3
3
|
import type { Endpoint, InstanceId, InstancePingResult } from "@ccmsg/protocol";
|
|
4
4
|
import { DEFAULT_HARNESS, type Harness, HARNESS, HARNESSES } from "../harness/index.ts";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
type
|
|
6
|
+
applied,
|
|
7
|
+
type ConfigProblem,
|
|
8
8
|
CONFIG_FILE,
|
|
9
9
|
CONFIG_NAME,
|
|
10
|
+
configOf,
|
|
11
|
+
DEFAULT_CONFIG,
|
|
12
|
+
type EndpointRow,
|
|
13
|
+
ENDPOINTS_FILE,
|
|
14
|
+
evaluate,
|
|
10
15
|
type InstanceConfig,
|
|
11
16
|
instanceFileName,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
saveClusters,
|
|
17
|
+
type InstanceSetting,
|
|
18
|
+
type Satisfied,
|
|
19
|
+
settle,
|
|
20
|
+
SUPERVISOR_FILE,
|
|
17
21
|
TYPES_FILE,
|
|
18
22
|
writeConfigTypes,
|
|
19
23
|
} from "../instance/config.ts";
|
|
20
|
-
import {
|
|
24
|
+
import { instanceIdentity } from "../instance/identity.ts";
|
|
21
25
|
import { alive, lockHolder } from "../instance/lock.ts";
|
|
22
26
|
import { type Env, type InstancePaths, resolvePaths, resolvePathsFor } from "../instance/paths.ts";
|
|
23
27
|
import { prepareSocketDir } from "../instance/socket.ts";
|
|
@@ -52,7 +56,38 @@ export function configHome(dir: string, harness: Harness = DEFAULT_HARNESS): str
|
|
|
52
56
|
* `daemon run` is. */
|
|
53
57
|
export async function harnessFor(env: Env, dir: string): Promise<Harness> {
|
|
54
58
|
const path = isAbsolute(dir) ? dir : resolve(dir);
|
|
55
|
-
|
|
59
|
+
const found = (await known(env)).instances.find((one) => one.dir === path);
|
|
60
|
+
return found?.config.harness ?? DEFAULT_HARNESS;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** What this host runs: the settings that are applied, read again from the
|
|
64
|
+
* files when they check out.
|
|
65
|
+
*
|
|
66
|
+
* Every command goes through the one path — read, check, apply — so what a
|
|
67
|
+
* command acts on is what a start would run. A config that does not check out
|
|
68
|
+
* leaves the applied one standing, which is what keeps a command about one
|
|
69
|
+
* instance working while another instance's file is being edited. */
|
|
70
|
+
export async function known(env: Env): Promise<Satisfied> {
|
|
71
|
+
const paths = resolvePaths(env);
|
|
72
|
+
const read = await evaluate(paths.configDir);
|
|
73
|
+
return read.satisfied ?? applied(paths.stateRoot) ?? EMPTY_SATISFIED;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const EMPTY_SATISFIED: Satisfied = {
|
|
77
|
+
endpoints: [],
|
|
78
|
+
supervisor: { instances: [] },
|
|
79
|
+
instances: [],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Read the files, check them, and write down what holds — the one thing a
|
|
83
|
+
* start, a reload, an `add` and a `remove` all do. */
|
|
84
|
+
export async function reload(env: Env): Promise<{
|
|
85
|
+
satisfied: Satisfied;
|
|
86
|
+
problems: readonly ConfigProblem[];
|
|
87
|
+
}> {
|
|
88
|
+
const paths = resolvePaths(env);
|
|
89
|
+
const settled = await settle(paths.configDir, paths.stateRoot);
|
|
90
|
+
return { satisfied: settled.satisfied, problems: settled.problems };
|
|
56
91
|
}
|
|
57
92
|
|
|
58
93
|
/** One row of `daemon list`: which config home, and whether anything answers
|
|
@@ -60,16 +95,13 @@ export async function harnessFor(env: Env, dir: string): Promise<Harness> {
|
|
|
60
95
|
export interface InstanceRow {
|
|
61
96
|
readonly id: InstanceId;
|
|
62
97
|
/** The label this instance is listed under: its own file's `name`, which
|
|
63
|
-
* defaults to its id. A `daemon run` on a config home
|
|
64
|
-
* none. */
|
|
98
|
+
* defaults to its id. A `daemon run` on a config home nothing states
|
|
99
|
+
* settings for has none. */
|
|
65
100
|
readonly name?: string;
|
|
66
|
-
/** Which cluster this row was read through. An instance in two clusters is
|
|
67
|
-
* one instance and one process, listed once under each of them. */
|
|
68
|
-
readonly cluster_id?: string;
|
|
69
|
-
readonly cluster_name?: string;
|
|
70
101
|
readonly dir: string;
|
|
71
|
-
/**
|
|
102
|
+
/** The address it binds, and the one its peers dial (§7.1). */
|
|
72
103
|
readonly port?: number;
|
|
104
|
+
readonly endpoint?: string;
|
|
73
105
|
readonly running: boolean;
|
|
74
106
|
readonly pid?: number;
|
|
75
107
|
}
|
|
@@ -77,6 +109,9 @@ export interface InstanceRow {
|
|
|
77
109
|
/** One row of `daemon status`: the list's row, plus what the instance itself
|
|
78
110
|
* says when there is one to ask. */
|
|
79
111
|
export interface StatusRow extends InstanceRow {
|
|
112
|
+
/** What was wrong with the files, where the applied settings are older than
|
|
113
|
+
* what is written. */
|
|
114
|
+
readonly config_problems?: readonly ConfigProblem[];
|
|
80
115
|
/** What this config home's instance is configured with, after the shared
|
|
81
116
|
* file's defaults and its own entry are merged (§8.2).
|
|
82
117
|
*
|
|
@@ -95,37 +130,27 @@ export interface StatusRow extends InstanceRow {
|
|
|
95
130
|
|
|
96
131
|
/** Everything one command needs to reach one config home. */
|
|
97
132
|
export interface Target {
|
|
98
|
-
/** The label this instance is listed under, where
|
|
133
|
+
/** The label this instance is listed under, where it is registered. */
|
|
99
134
|
readonly name?: string;
|
|
100
135
|
/** Its id, which is what its file is called. */
|
|
101
136
|
readonly id?: string;
|
|
102
|
-
/** The clusters it belongs to, as the host writes them down. */
|
|
103
|
-
readonly clusters?: readonly ClusterInfo[];
|
|
104
137
|
readonly dir: string;
|
|
105
138
|
readonly paths: InstancePaths;
|
|
106
139
|
}
|
|
107
140
|
|
|
108
|
-
export function targetFor(
|
|
109
|
-
env: Env,
|
|
110
|
-
dir: string,
|
|
111
|
-
name?: string,
|
|
112
|
-
id?: string,
|
|
113
|
-
clusters?: readonly ClusterInfo[],
|
|
114
|
-
): Target {
|
|
141
|
+
export function targetFor(env: Env, dir: string, name?: string, id?: string): Target {
|
|
115
142
|
return {
|
|
116
143
|
...(name === undefined ? {} : { name }),
|
|
117
144
|
...(id === undefined ? {} : { id }),
|
|
118
|
-
...(clusters === undefined ? {} : { clusters }),
|
|
119
145
|
dir,
|
|
120
146
|
paths: resolvePathsFor(dir, env),
|
|
121
147
|
};
|
|
122
148
|
}
|
|
123
149
|
|
|
124
|
-
/** The config homes this host
|
|
150
|
+
/** The config homes this host starts, in the order the supervisor lists them. */
|
|
125
151
|
export async function registered(env: Env): Promise<Target[]> {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
targetFor(env, entry.dir, entry.name, entry.id, entry.clusters),
|
|
152
|
+
return (await known(env)).instances.map((entry) =>
|
|
153
|
+
targetFor(env, entry.dir, entry.name, entry.id),
|
|
129
154
|
);
|
|
130
155
|
}
|
|
131
156
|
|
|
@@ -183,13 +208,6 @@ const STARTING_PRESETS = [
|
|
|
183
208
|
/** What `daemon add` takes: the config home the instance answers for, and the
|
|
184
209
|
* two settings a person would otherwise open the file to write. */
|
|
185
210
|
export interface AddOptions {
|
|
186
|
-
/** Which cluster the instance joins, by id or by name. With none said: the
|
|
187
|
-
* one cluster there is, a new one where there is none, and a refusal where
|
|
188
|
-
* there are several — the last because which management unit an instance
|
|
189
|
-
* belongs to is not something to guess at. An id nothing answers to is a
|
|
190
|
-
* cluster this host has not met yet and is made under that id, which is how
|
|
191
|
-
* a second host joins one. */
|
|
192
|
-
readonly cluster?: string;
|
|
193
211
|
readonly harness?: Harness;
|
|
194
212
|
readonly port?: number;
|
|
195
213
|
}
|
|
@@ -285,25 +303,23 @@ export async function add(env: Env, dir: string, options: AddOptions = {}): Prom
|
|
|
285
303
|
const harness = options.harness ?? harnessOf(where);
|
|
286
304
|
const home = configHome(where, harness);
|
|
287
305
|
const paths = resolvePaths(env);
|
|
288
|
-
const
|
|
289
|
-
const taken =
|
|
306
|
+
const held = await known(env);
|
|
307
|
+
const taken = held.instances.find((one) => one.dir === home);
|
|
290
308
|
if (taken !== undefined) {
|
|
291
309
|
throw new CommandError("file_exists", `${home} は既に ${taken.name} として登録されています`);
|
|
292
310
|
}
|
|
293
|
-
const cluster = clusterFor(all.clusters, options.cluster);
|
|
294
311
|
// The id the state directory already holds, or a new one written there now:
|
|
295
312
|
// a config home that was registered before keeps the id everything it issued
|
|
296
313
|
// is keyed by, and a fresh one gets its id here rather than at its first
|
|
297
314
|
// start (DR-0001 §2.1).
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
//
|
|
301
|
-
// (§7.1) and a mesh is reached over the entry: what `--port` settles is which
|
|
315
|
+
const id = instanceIdentity(targetFor(env, home).paths.instanceIdFile);
|
|
316
|
+
// Every instance listens, because an instance is an entry of the mesh (§7.1)
|
|
317
|
+
// and a mesh is reached over the entry: what `--port` settles is which
|
|
302
318
|
// address, not whether there is one.
|
|
303
319
|
const port =
|
|
304
320
|
options.port ??
|
|
305
321
|
(await freePort(
|
|
306
|
-
|
|
322
|
+
held.instances.flatMap((one) =>
|
|
307
323
|
one.config.entry === undefined ? [] : [one.config.entry.port],
|
|
308
324
|
),
|
|
309
325
|
));
|
|
@@ -315,55 +331,63 @@ export async function add(env: Env, dir: string, options: AddOptions = {}): Prom
|
|
|
315
331
|
join(paths.instancesDir, instanceFileName(id)),
|
|
316
332
|
instanceTemplate(name, home, harness, port),
|
|
317
333
|
);
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
+
// The loopback address, because that is the one this host is certainly
|
|
335
|
+
// reached at. A proxy in front of it is a deployment fact nothing here can
|
|
336
|
+
// see, so an operator who has one edits this row (§8.2).
|
|
337
|
+
saveEndpoints(paths.configDir, [
|
|
338
|
+
...readEndpointRows(paths.configDir).filter((row) => row.id !== id),
|
|
339
|
+
{ id, endpoint: `http://127.0.0.1:${String(port)}/` as EndpointRow["endpoint"] },
|
|
340
|
+
]);
|
|
341
|
+
saveSupervisor(paths.configDir, [
|
|
342
|
+
...readSupervised(paths.configDir).filter((one) => one !== id),
|
|
343
|
+
id,
|
|
344
|
+
]);
|
|
345
|
+
const settled = await reload(env);
|
|
346
|
+
const written = configOf(settled.satisfied, home);
|
|
347
|
+
if (written === undefined) {
|
|
348
|
+
throw new CommandError(
|
|
349
|
+
"internal_error",
|
|
350
|
+
`${home} を書きましたが設定が通りませんでした: ${settled.problems
|
|
351
|
+
.map((one) => `${one.file}: ${one.msg}`)
|
|
352
|
+
.join("; ")}`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
return rowOf(env, written);
|
|
334
356
|
}
|
|
335
357
|
|
|
336
|
-
/** The
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
named: string | undefined,
|
|
346
|
-
): ClusterSetting {
|
|
347
|
-
if (named !== undefined) {
|
|
348
|
-
const found = clusters.find((one) => one.id === named || one.name === named);
|
|
349
|
-
if (found !== undefined) return found;
|
|
350
|
-
if (!ID.test(named)) {
|
|
351
|
-
throw new CommandError(
|
|
352
|
-
"not_found",
|
|
353
|
-
`${named} という cluster はありません (新しく作るなら id を渡してください)`,
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
return { id: named, name: named, peers: [], instances: [] };
|
|
358
|
+
/** The mesh as the file holds it right now, for a command that is about to
|
|
359
|
+
* edit it. Read as data rather than through the checks, because a command that
|
|
360
|
+
* adds a row has to be able to fix a file that does not check out yet. */
|
|
361
|
+
function readEndpointRows(configDir: string): EndpointRow[] {
|
|
362
|
+
try {
|
|
363
|
+
const parsed = JSON.parse(readFileSync(join(configDir, ENDPOINTS_FILE), "utf8")) as unknown;
|
|
364
|
+
return Array.isArray(parsed) ? (parsed as EndpointRow[]) : [];
|
|
365
|
+
} catch {
|
|
366
|
+
return [];
|
|
357
367
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function readSupervised(configDir: string): string[] {
|
|
371
|
+
try {
|
|
372
|
+
const parsed = JSON.parse(readFileSync(join(configDir, SUPERVISOR_FILE), "utf8")) as {
|
|
373
|
+
instances?: unknown;
|
|
374
|
+
};
|
|
375
|
+
return Array.isArray(parsed.instances) ? (parsed.instances as string[]) : [];
|
|
376
|
+
} catch {
|
|
377
|
+
return [];
|
|
363
378
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function saveEndpoints(configDir: string, rows: readonly EndpointRow[]): void {
|
|
382
|
+
mkdirSync(configDir, { recursive: true });
|
|
383
|
+
writeFileSync(join(configDir, ENDPOINTS_FILE), `${JSON.stringify(rows, null, 2)}\n`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function saveSupervisor(configDir: string, ids: readonly string[]): void {
|
|
387
|
+
mkdirSync(configDir, { recursive: true });
|
|
388
|
+
writeFileSync(
|
|
389
|
+
join(configDir, SUPERVISOR_FILE),
|
|
390
|
+
`${JSON.stringify({ instances: ids }, null, 2)}\n`,
|
|
367
391
|
);
|
|
368
392
|
}
|
|
369
393
|
|
|
@@ -378,23 +402,23 @@ export async function remove(
|
|
|
378
402
|
ref: string,
|
|
379
403
|
): Promise<{ id: string; name: string; dir: string; removed: boolean }> {
|
|
380
404
|
const paths = resolvePaths(env);
|
|
381
|
-
const
|
|
382
|
-
|
|
405
|
+
const found = (await known(env)).instances.find(
|
|
406
|
+
(one) => one.id === ref || one.name === ref || one.dir === ref,
|
|
407
|
+
);
|
|
383
408
|
if (found === undefined) throw new CommandError("not_found", `${ref} は登録されていません`);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
});
|
|
393
|
-
}
|
|
409
|
+
saveSupervisor(
|
|
410
|
+
paths.configDir,
|
|
411
|
+
readSupervised(paths.configDir).filter((one) => one !== found.id),
|
|
412
|
+
);
|
|
413
|
+
saveEndpoints(
|
|
414
|
+
paths.configDir,
|
|
415
|
+
readEndpointRows(paths.configDir).filter((row) => row.id !== found.id),
|
|
416
|
+
);
|
|
394
417
|
rmSync(join(paths.instancesDir, instanceFileName(found.id)), { force: true });
|
|
395
418
|
// The state directory stays, its id with it: what the instance issued is
|
|
396
419
|
// keyed by that id, and re-adding the same config home has to answer to the
|
|
397
420
|
// same one.
|
|
421
|
+
await reload(env);
|
|
398
422
|
return { id: found.id, name: found.name, dir: found.dir, removed: true };
|
|
399
423
|
}
|
|
400
424
|
|
|
@@ -405,8 +429,8 @@ function defaultsTemplate(): string {
|
|
|
405
429
|
/** 全 instance に配る値。\`builtin\` は組み込みの既定値 (凍結済み)、\`config\` は
|
|
406
430
|
* そのコピーなので、書き換えて返す。ここに書いた値を各 instance が受け取る。 */
|
|
407
431
|
const defaults: Defaults = ({ config }) => {
|
|
408
|
-
// mesh
|
|
409
|
-
//
|
|
432
|
+
// mesh はここには書かない。誰が居てどこで届くかは endpoints.json が正で、
|
|
433
|
+
// この関数は読めるが変えられない。
|
|
410
434
|
|
|
411
435
|
// dump の名前付き選択。prefix は一族を、\`@name\` は他の選択をその場に広げる。
|
|
412
436
|
config.dump.presets = [
|
|
@@ -492,36 +516,35 @@ export function rowFor(target: Target): InstanceRow {
|
|
|
492
516
|
};
|
|
493
517
|
}
|
|
494
518
|
|
|
495
|
-
/** What `daemon list` answers:
|
|
496
|
-
*
|
|
497
|
-
* By cluster because that is the unit a person manages — which mesh, which
|
|
498
|
-
* authentication records — and an instance in two of them is in both listings,
|
|
499
|
-
* as the same id with the same process. */
|
|
519
|
+
/** What `daemon list` answers: the instances this host starts, and whether
|
|
520
|
+
* anything answers for each right now. */
|
|
500
521
|
export async function list(env: Env): Promise<InstanceRow[]> {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
cluster_id: cluster.id,
|
|
511
|
-
cluster_name: cluster.name,
|
|
512
|
-
...(found.config.entry === undefined ? {} : { port: found.config.entry.port }),
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
return rows;
|
|
522
|
+
return (await known(env)).instances.map((one) => rowOf(env, one));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function rowOf(env: Env, one: InstanceSetting): InstanceRow {
|
|
526
|
+
return {
|
|
527
|
+
...rowFor(targetFor(env, one.dir, one.name, one.id)),
|
|
528
|
+
...(one.config.entry === undefined ? {} : { port: one.config.entry.port }),
|
|
529
|
+
...(one.config.endpoint === undefined ? {} : { endpoint: one.config.endpoint }),
|
|
530
|
+
};
|
|
517
531
|
}
|
|
518
532
|
|
|
519
533
|
/** Ask one instance how it is. A config home with nothing behind it answers the
|
|
520
534
|
* list's row and nothing more: not running is a state, not a failure. */
|
|
521
535
|
export async function status(target: Target): Promise<StatusRow> {
|
|
536
|
+
const read = await evaluate(target.paths.configDir);
|
|
537
|
+
const satisfied = read.satisfied ?? applied(target.paths.stateRoot) ?? EMPTY_SATISFIED;
|
|
538
|
+
const own = configOf(satisfied, target.dir);
|
|
522
539
|
const row = {
|
|
523
540
|
...rowFor(target),
|
|
524
|
-
config:
|
|
541
|
+
...(own?.config.entry === undefined ? {} : { port: own.config.entry.port }),
|
|
542
|
+
...(own?.config.endpoint === undefined ? {} : { endpoint: own.config.endpoint }),
|
|
543
|
+
config: own?.config ?? DEFAULT_CONFIG,
|
|
544
|
+
// What a person has to be told even though the instance is running: an
|
|
545
|
+
// edit that did not check out is not applied, and the only sign of it
|
|
546
|
+
// otherwise is a setting that did not take (§8.3).
|
|
547
|
+
...(read.problems.length === 0 ? {} : { config_problems: read.problems }),
|
|
525
548
|
};
|
|
526
549
|
const conn = await connect(target.paths.socket);
|
|
527
550
|
if (conn === undefined) return row;
|
package/src/daemon/supervise.ts
CHANGED
|
@@ -8,8 +8,8 @@ import {
|
|
|
8
8
|
type Child,
|
|
9
9
|
configHome,
|
|
10
10
|
harnessFor,
|
|
11
|
+
reload,
|
|
11
12
|
prepareFor,
|
|
12
|
-
registered,
|
|
13
13
|
rowFor,
|
|
14
14
|
type SpawnInstance,
|
|
15
15
|
spawnInstance,
|
|
@@ -120,14 +120,26 @@ export class Supervisor {
|
|
|
120
120
|
this.#log = options.log ?? ((line) => process.stderr.write(`${JSON.stringify(line)}\n`));
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
/** Read
|
|
123
|
+
/** Read the edited files, check them, write down what held, and look after
|
|
124
|
+
* what it names (§8.2).
|
|
125
|
+
*
|
|
126
|
+
* This is the one thing that writes the applied settings: the children read
|
|
127
|
+
* them and write nothing, so nothing races over the file and there is one
|
|
128
|
+
* answer to "what is running". A config that does not check out leaves the
|
|
129
|
+
* applied one standing and is written to this supervisor's log — the
|
|
130
|
+
* children of a host are not something a typo in one file should take down.
|
|
124
131
|
*
|
|
125
132
|
* In `run` rather than in the constructor because the files are TypeScript
|
|
126
133
|
* and reading one is an import: a caller holding a supervisor that has not
|
|
127
|
-
* run yet is holding one that has not read the
|
|
134
|
+
* run yet is holding one that has not read the files yet, which is the same
|
|
128
135
|
* moment it was already true that nothing had been started. */
|
|
129
136
|
async #adopt(): Promise<void> {
|
|
130
|
-
|
|
137
|
+
const settled = await reload(this.#env);
|
|
138
|
+
for (const problem of settled.problems) {
|
|
139
|
+
this.#log({ event: "config refused", file: problem.file, problem: problem.msg });
|
|
140
|
+
}
|
|
141
|
+
for (const one of settled.satisfied.instances) {
|
|
142
|
+
const target = targetFor(this.#env, one.dir, one.name, one.id);
|
|
131
143
|
this.#units.set(target.dir, new Supervised(target));
|
|
132
144
|
}
|
|
133
145
|
}
|
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
/** ある config home が動かすもの。 */
|
|
8
8
|
export type Harness = "claude" | "codex";
|
|
9
9
|
|
|
10
|
+
/** mesh の 1 行。endpoint は公開 base URL で、末尾の `/` まで含める。 */
|
|
11
|
+
export interface Endpoint {
|
|
12
|
+
id: string;
|
|
13
|
+
endpoint: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
/** どこで待ち受け、誰からを受けるか。 */
|
|
11
17
|
export interface Entry {
|
|
12
18
|
host: string;
|
|
@@ -65,12 +71,12 @@ export interface Dump {
|
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
/** 1 instance 分の設定。`config_v2.ts` が返すのも、`instances/instance-<id>.ts` が
|
|
68
|
-
* `dir` を足して返すのも、これ。
|
|
69
|
-
*
|
|
70
|
-
* mesh の相手はここに書かない。この host の instance は `instances/*.ts` の
|
|
71
|
-
* port から、別 host の endpoint は `peers.json` (`ccmsg mesh add`) から入る。 */
|
|
74
|
+
* `dir` を足して返すのも、これ。 */
|
|
72
75
|
export interface Config {
|
|
73
76
|
harness: Harness;
|
|
77
|
+
/** mesh の全 instance (自分も含む)。`endpoints.json` が正で、読むのは自由だが
|
|
78
|
+
* 違う物を返したら config error。自分がどれかは自分の id の行。 */
|
|
79
|
+
endpoints: Endpoint[];
|
|
74
80
|
/** 無ければ unix socket だけで serve する。 */
|
|
75
81
|
entry?: Entry;
|
|
76
82
|
upstream: Upstream;
|
|
@@ -84,11 +90,6 @@ export interface InstanceConfig extends Config {
|
|
|
84
90
|
dir: string;
|
|
85
91
|
/** 人向けのラベル。書かなければ id。ファイル名は id なので、変えても何も動かない。 */
|
|
86
92
|
name?: string;
|
|
87
|
-
/** peer と人がこの instance に届く URL (末尾 `/`)。reverse proxy の後ろに
|
|
88
|
-
* 居る instance は、待ち受ける address と届く URL が別で、互いに導けない。
|
|
89
|
-
* mesh の一覧にはこの値が載る (= probe が確定する self、handshake の
|
|
90
|
-
* `iss` / `aud`、人に見せる URL)。書かなければ待ち受ける address になる。 */
|
|
91
|
-
endpoint?: string;
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
/** `config_v2.ts` が default export する関数。
|