@volter/world 2.0.0 → 2.0.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/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +13 -3
- package/dist/src/world.d.ts +11 -5
- package/dist/src/world.js +59 -18
- package/package.json +8 -3
- package/src/cli.ts +5 -3
- package/src/world.ts +54 -17
package/dist/src/cli.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export declare const HELP = "volter \u2014 run your app against a world of twins\n\nworld \u2014 the twins your app needs, running together (the world of the current directory)\n volter world init [--name <world>] [--allow-unknown] detect the app's vendors, write .volter/world.json\n volter world up [--sandbox] [--no-seed] start the twins on this branch (loads the default data the first time)\n volter world run [--verbose] -- <command...> run the app or its tests inside the world\n volter world activate eval \"$(volter world activate)\": vendor CLIs and curl in this shell reach the twins\n volter world shell a subshell with the world active\n volter world init --bare <org>/<world> --twins a,b a world with no app: a shared world for a team, or one standing in for a vendor\n volter world serve [--port <p>] [--console-port <c>] serve this world on a URL under /<org>/<world>/; prints the token and the console (its own origin, port p+1 by default)\n volter world console [<https://host/org/world>] [--port <p>] the console on this machine for a World served elsewhere (default: this world's origin); needs @volter/world-console\n volter world status world, branch, origin, unpushed changes, what is running\n volter world log [--receipts] [--json] every write the app made; --receipts adds each change's receipt\n volter world diff [--json] what changed since the default data (or the last mark)\n volter world seed load the default data\n volter world reset back to the default data\n volter world down [--purge] stop the twins (--purge forgets this branch's state)\n volter world branch [<name>] list branches, or make one from here and check it out\n volter world checkout <name> switch branches\n volter world replay <changeset> --into <branch> feed a changeset's changes into another branch's twins\n volter world clock show | set <iso> | advance <N s|m|h|d> the world's clock: set, not observed\n volter world clone <url> --token <token> [--admin-token <t>] connect this world to a remote and fetch its history (the admin token copies the whole tree at once)\n volter world fetch what the remote observed since the last fetch\n volter world origin where this world clones from and pushes to\n volter world changeset [<name>] -m \"<message>\" cut the unpushed changes into a reviewable changeset\n volter world pull [--token <token>] fetch, then move this branch onto what came in (git's pull)\n volter world push [<name>] [--token <token>] [--force] push to the remote, which deploys and answers with receipts\n volter world rebase [<changeset>] rebase this branch onto its moved base, or a changeset onto the moved mirror\n volter world verify <changeset> run the world's checks over a changeset and record the result\n volter world approve <changeset> --as <principal> sign a changeset's current hash\n volter world deploy [<changeset>] perform landed changes against each twin's root, by its policy\n\ntwin \u2014 one twin: in this world, or on its own\n volter twin <vendor> the twin's URL, root, deploy policy, whether a credential is sealed\n volter twin <vendor> root <url> [--deploy auto|gated|hold] [--scope <path>] the vendor's real account behind this twin (--none clears it)\n volter twin <vendor> credential seal a credential read from stdin beside the world; never readable back\n volter twin <vendor> refresh observe the root now\n volter twin <vendor> serve [--port <p>] [--read-only] serve the twin; your SDK talks to it at http://127.0.0.1:<p>\n volter twin <vendor> mirror [--port <p>] the twin's UI mirror\n volter twin <vendor> conformance check the twin against the vendor's spec\n\nremote \u2014 the worlds this one pushes to and fetches from, by name\n volter login <platform url> --token <personal token> sign the CLI into the hosted platform (a personal token from the console)\n volter remote add <name> <url|path|org/world> [--token <token>] name a remote; org/world resolves through the platform you logged into\n volter remote remove <name> forget it\n volter remote list every remote, with its URL and whether a token is stored\n (the hosting product's serve answers here for one release: volter remote serve)\n\n --world <dir> the world root, when the cwd is not inside it\n --branch <name> act on that branch instead of the checked-out one\n --json machine-readable output where offered\n";
|
|
2
|
+
export declare const HELP = "volter \u2014 run your app against a world of twins\n\nworld \u2014 the twins your app needs, running together (the world of the current directory)\n volter world init [--name <world>] [--allow-unknown] detect the app's vendors, write .volter/world.json\n volter world up [--sandbox] [--no-seed] start the twins on this branch (loads the default data the first time)\n volter world run [--verbose] -- <command...> run the app or its tests inside the world\n volter world activate eval \"$(volter world activate)\": vendor CLIs and curl in this shell reach the twins\n volter world shell a subshell with the world active\n volter world init --bare <org>/<world> --twins a,b a world with no app: a shared world for a team, or one standing in for a vendor\n volter world serve [--port <p>] [--console-port <c>] serve this world on a URL under /<org>/<world>/; prints the token and the console (its own origin, port p+1 by default)\n volter world console [<https://host/org/world>] [--port <p>] the console on this machine for a World served elsewhere (default: this world's origin); needs @volter/world-console\n volter world status world, branch, origin, unpushed changes, what is running\n volter world log [--receipts] [--json] every write the app made; --receipts adds each change's receipt\n volter world diff [--json] what changed since the default data (or the last mark)\n volter world seed load the default data\n volter world reset back to the default data\n volter world down [--purge] stop the twins (--purge forgets this branch's state)\n volter world branch [<name>] list branches, or make one from here and check it out\n volter world checkout <name> switch branches\n volter world replay <changeset> --into <branch> feed a changeset's changes into another branch's twins\n volter world clock show | set <iso> | advance <N s|m|h|d> | shift <N s|m|h|d> | clear the world's clock: set, not observed\n volter world clone <url> --token <token> [--admin-token <t>] connect this world to a remote and fetch its history (the admin token copies the whole tree at once)\n volter world fetch what the remote observed since the last fetch\n volter world origin where this world clones from and pushes to\n volter world changeset [<name>] -m \"<message>\" cut the unpushed changes into a reviewable changeset\n volter world pull [--token <token>] fetch, then move this branch onto what came in (git's pull)\n volter world push [<name>] [--token <token>] [--force] push to the remote, which deploys and answers with receipts\n volter world rebase [<changeset>] rebase this branch onto its moved base, or a changeset onto the moved mirror\n volter world verify <changeset> run the world's checks over a changeset and record the result\n volter world approve <changeset> --as <principal> sign a changeset's current hash\n volter world deploy [<changeset>] perform landed changes against each twin's root, by its policy\n\ntwin \u2014 one twin: in this world, or on its own\n volter twin <vendor> the twin's URL, root, deploy policy, whether a credential is sealed\n volter twin <vendor> root <url> [--deploy auto|gated|hold] [--scope <path>] the vendor's real account behind this twin (--none clears it)\n volter twin <vendor> credential seal a credential read from stdin beside the world; never readable back\n volter twin <vendor> refresh observe the root now\n volter twin <vendor> serve [--port <p>] [--read-only] serve the twin; your SDK talks to it at http://127.0.0.1:<p>\n volter twin <vendor> mirror [--port <p>] the twin's UI mirror\n volter twin <vendor> conformance check the twin against the vendor's spec\n\nremote \u2014 the worlds this one pushes to and fetches from, by name\n volter login <platform url> --token <personal token> sign the CLI into the hosted platform (a personal token from the console)\n volter remote add <name> <url|path|org/world> [--token <token>] name a remote; org/world resolves through the platform you logged into\n volter remote remove <name> forget it\n volter remote list every remote, with its URL and whether a token is stored\n (the hosting product's serve answers here for one release: volter remote serve)\n\n --world <dir> the world root, when the cwd is not inside it\n --branch <name> act on that branch instead of the checked-out one\n --json machine-readable output where offered\n";
|
package/dist/src/cli.js
CHANGED
|
@@ -32,7 +32,7 @@ world — the twins your app needs, running together (the world of the current
|
|
|
32
32
|
volter world branch [<name>] list branches, or make one from here and check it out
|
|
33
33
|
volter world checkout <name> switch branches
|
|
34
34
|
volter world replay <changeset> --into <branch> feed a changeset's changes into another branch's twins
|
|
35
|
-
volter world clock show | set <iso> | advance <N s|m|h|d> the world's clock: set, not observed
|
|
35
|
+
volter world clock show | set <iso> | advance <N s|m|h|d> | shift <N s|m|h|d> | clear the world's clock: set, not observed
|
|
36
36
|
volter world clone <url> --token <token> [--admin-token <t>] connect this world to a remote and fetch its history (the admin token copies the whole tree at once)
|
|
37
37
|
volter world fetch what the remote observed since the last fetch
|
|
38
38
|
volter world origin where this world clones from and pushes to
|
|
@@ -313,12 +313,22 @@ async function worldCommand(verb, args) {
|
|
|
313
313
|
out(world.advanceClock(arg));
|
|
314
314
|
return;
|
|
315
315
|
}
|
|
316
|
+
if (action === 'shift') {
|
|
317
|
+
if (!arg)
|
|
318
|
+
throw new Error('volter world clock shift <N s|m|h|d>');
|
|
319
|
+
out(world.shiftClock(arg));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (action === 'clear') {
|
|
323
|
+
out(world.clearClock());
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
316
326
|
if (action === undefined || action === 'show') {
|
|
317
327
|
const c = world.clock();
|
|
318
|
-
out(`${c.at}${c.frozen ? '' : ' (wall clock — not set)'}`);
|
|
328
|
+
out(`${c.at}${c.frozen ? '' : c.running ? ' (running)' : ' (wall clock — not set)'}`);
|
|
319
329
|
return;
|
|
320
330
|
}
|
|
321
|
-
throw new Error('volter world clock show | set <iso> | advance <N s|m|h|d>');
|
|
331
|
+
throw new Error('volter world clock show | set <iso> | advance <N s|m|h|d> | shift <N s|m|h|d> | clear');
|
|
322
332
|
}
|
|
323
333
|
case 'replay': {
|
|
324
334
|
const name = pos[0];
|
package/dist/src/world.d.ts
CHANGED
|
@@ -6,8 +6,6 @@ export type WorldRef = {
|
|
|
6
6
|
root?: string;
|
|
7
7
|
};
|
|
8
8
|
export type WorldMode = 'local' | 'share' | 'sealed';
|
|
9
|
-
/** One change as `log` shows it: the write, and the receipt the vendor answered with once pushed. */
|
|
10
|
-
/** A twin's branch log as the world reads it: the twin's name, the state service it records under, its control root. */
|
|
11
9
|
export declare class TwinLog {
|
|
12
10
|
readonly service: string;
|
|
13
11
|
readonly stateService: string;
|
|
@@ -164,15 +162,23 @@ export declare class World {
|
|
|
164
162
|
isMain(): boolean;
|
|
165
163
|
/** What this branch's changes are measured from, as `diff` says it: the branch point, origin, or the story. */
|
|
166
164
|
diffBase(): string;
|
|
167
|
-
/** The world's clock:
|
|
165
|
+
/** The world's clock (world-clock.cjs): its time now, and whether it is frozen at an instant, running (moved with
|
|
166
|
+
* `shiftClock`), or the wall clock (unset). */
|
|
168
167
|
clock(): {
|
|
169
168
|
at: string;
|
|
170
169
|
frozen: boolean;
|
|
170
|
+
running: boolean;
|
|
171
171
|
};
|
|
172
|
-
/** Set the world's clock to an instant; every twin stamps from it until it moves. */
|
|
172
|
+
/** Set the world's clock to an instant; every twin, and the World's application, stamps from it until it moves. */
|
|
173
173
|
setClock(iso: string): string;
|
|
174
|
-
/** Move a set clock forward: `30d`, `12h`, `5m`, `90s`. */
|
|
174
|
+
/** Move a set clock forward, frozen or running: `30d`, `12h`, `5m`, `90s`. */
|
|
175
175
|
advanceClock(by: string): string;
|
|
176
|
+
/** Move the World forward while its time keeps running (a World serving an application): from the wall clock, or a
|
|
177
|
+
* running clock further. */
|
|
178
|
+
shiftClock(by: string): string;
|
|
179
|
+
/** Return the World to the machine's time. */
|
|
180
|
+
clearClock(): string;
|
|
181
|
+
private writeClock;
|
|
176
182
|
/** Rebase this branch onto its base's current position, every twin; conflicts named per subject and field. */
|
|
177
183
|
rebaseBranch(): Array<{
|
|
178
184
|
service: string;
|
package/dist/src/world.js
CHANGED
|
@@ -4,14 +4,22 @@
|
|
|
4
4
|
// user reads. The `volter` command is one client of this class and adds nothing.
|
|
5
5
|
import { rebaseChangeset, worldBootMarker, listActions, isTwinBookkeeping, pushablePendingActions, pendingActions, applyTwinWrite, twinResources } from '@volter/world-core';
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import clockForm from '@volter/world-core/world-clock';
|
|
7
8
|
import { activateScript, approveWorldChangeset, branchWorld, clockFile, checkoutWorld, createWorldChangeset, diffWorld, downWorld, fetchFromOrigin, findWorldChangeset, initWorld, listWorldChangesets, listWorldMarks, listWorlds, markWorld, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, runWithWorldEnv, seedWorld, shellWorld, statusWorld, statusWorldChangeset, upWorld, verifyWorldChangeset, worldLedgers, worldOrigin, deployWorld, loadWorldConfig, writeWorldConfig, readServeRecord, refreshTwin, serveWorld, findInstalledPackage, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, credentialPayloadFrom, materializeRoots } from '@volter/world-runtime';
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
10
|
import { dirname, join, resolve } from 'node:path';
|
|
10
11
|
import { requireToken, storeToken } from "./credentials.js";
|
|
11
12
|
import { currentBranch, findWorldRoot, mainBranch, requireWorldRoot, setCurrentBranch, worldConfigRelative, worldEnvPath, worldSeedPath } from "./locate.js";
|
|
12
13
|
import { parentEntries, rebaseBranch } from '@volter/world-core';
|
|
13
14
|
/** One change as `log` shows it: the write, and the receipt the vendor answered with once pushed. */
|
|
14
15
|
/** A twin's branch log as the world reads it: the twin's name, the state service it records under, its control root. */
|
|
16
|
+
/** `30d`, `12h`, `5m`, `90s` in ms. */
|
|
17
|
+
function durationMs(by) {
|
|
18
|
+
const m = /^(\d+(?:\.\d+)?)(s|m|h|d)$/.exec(by.trim());
|
|
19
|
+
if (!m)
|
|
20
|
+
throw new Error(`${JSON.stringify(by)} is not <N>(s|m|h|d)`);
|
|
21
|
+
return Number(m[1]) * { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2]];
|
|
22
|
+
}
|
|
15
23
|
export class TwinLog {
|
|
16
24
|
service;
|
|
17
25
|
stateService;
|
|
@@ -248,9 +256,19 @@ export class World {
|
|
|
248
256
|
async branch(name, opts = {}) {
|
|
249
257
|
if (listWorlds(this.root).some((w) => w.name === name))
|
|
250
258
|
throw new Error(`A branch "${name}" already exists — \`volter world checkout ${name}\` switches to it`);
|
|
251
|
-
|
|
252
|
-
if
|
|
259
|
+
// this branch stops first (a managed database is copied from a stopped World, and the two would share its port),
|
|
260
|
+
// and comes back if the new one fails
|
|
261
|
+
const wasRunning = !!this.instance()?.running;
|
|
262
|
+
if (wasRunning)
|
|
253
263
|
await downWorld(this.name, this.root);
|
|
264
|
+
try {
|
|
265
|
+
await branchWorld(this.name, name, { root: this.root, envFile: worldEnvPath(this.root), ...(opts.at ? { at: opts.at } : {}) });
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
if (wasRunning)
|
|
269
|
+
await checkoutWorld(this.name, { root: this.root });
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
254
272
|
setCurrentBranch(this.root, name);
|
|
255
273
|
return new World(name, this.root);
|
|
256
274
|
}
|
|
@@ -283,31 +301,54 @@ export class World {
|
|
|
283
301
|
return existsSync(worldSeedPath(this.root)) ? 'the story' : 'the default data';
|
|
284
302
|
}
|
|
285
303
|
// ── the clock ───────────────────────────────────────────────────────────────────────────
|
|
286
|
-
/** The world's clock:
|
|
304
|
+
/** The world's clock (world-clock.cjs): its time now, and whether it is frozen at an instant, running (moved with
|
|
305
|
+
* `shiftClock`), or the wall clock (unset). */
|
|
287
306
|
clock() {
|
|
288
307
|
const file = clockFile(this.root, this.name);
|
|
289
|
-
|
|
308
|
+
if (!existsSync(file))
|
|
309
|
+
return { at: new Date().toISOString(), frozen: false, running: false };
|
|
310
|
+
const c = clockForm.parseClock(readFileSync(file, 'utf8'));
|
|
311
|
+
return { at: new Date(clockForm.clockNowMs(c, Date.now())).toISOString(), frozen: c.kind === 'frozen', running: c.kind === 'running' };
|
|
290
312
|
}
|
|
291
|
-
/** Set the world's clock to an instant; every twin stamps from it until it moves. */
|
|
313
|
+
/** Set the world's clock to an instant; every twin, and the World's application, stamps from it until it moves. */
|
|
292
314
|
setClock(iso) {
|
|
293
315
|
const parsed = Date.parse(iso);
|
|
294
316
|
if (Number.isNaN(parsed))
|
|
295
317
|
throw new Error(`${JSON.stringify(iso)} is not an ISO-8601 instant`);
|
|
296
|
-
|
|
297
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
298
|
-
writeFileSync(file, `${new Date(parsed).toISOString()}\n`);
|
|
318
|
+
this.writeClock({ kind: 'frozen', at: parsed });
|
|
299
319
|
return new Date(parsed).toISOString();
|
|
300
320
|
}
|
|
301
|
-
/** Move a set clock forward: `30d`, `12h`, `5m`, `90s`. */
|
|
321
|
+
/** Move a set clock forward, frozen or running: `30d`, `12h`, `5m`, `90s`. */
|
|
302
322
|
advanceClock(by) {
|
|
303
|
-
const
|
|
304
|
-
if (!
|
|
305
|
-
throw new Error(
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
323
|
+
const file = clockFile(this.root, this.name);
|
|
324
|
+
if (!existsSync(file))
|
|
325
|
+
throw new Error('the clock is not set — `volter world clock set <iso>` first; advancing the wall clock would freeze time as a side effect (`volter world clock shift` moves a running World)');
|
|
326
|
+
const c = clockForm.parseClock(readFileSync(file, 'utf8'));
|
|
327
|
+
this.writeClock({ ...c, at: c.at + durationMs(by) });
|
|
328
|
+
return this.clock().at;
|
|
329
|
+
}
|
|
330
|
+
/** Move the World forward while its time keeps running (a World serving an application): from the wall clock, or a
|
|
331
|
+
* running clock further. */
|
|
332
|
+
shiftClock(by) {
|
|
333
|
+
const file = clockFile(this.root, this.name);
|
|
334
|
+
const c = existsSync(file) ? clockForm.parseClock(readFileSync(file, 'utf8')) : null;
|
|
335
|
+
if (c?.kind === 'frozen')
|
|
336
|
+
throw new Error('the clock is frozen — `volter world clock advance` moves it');
|
|
337
|
+
const wall = Date.now();
|
|
338
|
+
this.writeClock(c ? { ...c, at: c.at + durationMs(by) } : { kind: 'running', at: wall + durationMs(by), since: wall });
|
|
339
|
+
return this.clock().at;
|
|
340
|
+
}
|
|
341
|
+
/** Return the World to the machine's time. */
|
|
342
|
+
clearClock() {
|
|
343
|
+
const file = clockFile(this.root, this.name);
|
|
344
|
+
if (existsSync(file))
|
|
345
|
+
rmSync(file);
|
|
346
|
+
return new Date().toISOString();
|
|
347
|
+
}
|
|
348
|
+
writeClock(c) {
|
|
349
|
+
const file = clockFile(this.root, this.name);
|
|
350
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
351
|
+
writeFileSync(file, `${clockForm.formatClock(c)}\n`);
|
|
311
352
|
}
|
|
312
353
|
/** Rebase this branch onto its base's current position, every twin; conflicts named per subject and field. */
|
|
313
354
|
rebaseBranch() { return this.repos().map((r) => ({ service: r.service, ...rebaseBranch(r.stateService, r.root) })); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Run your app against a world of twins. `World` and `Repo` — one method per verb: init, up, run, log, diff, reset, branch, checkout, clone, fetch, changeset, push — and the `volter` command, one client of them.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"volter",
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
],
|
|
14
14
|
"author": "Volter (https://github.com/volter-ai)",
|
|
15
15
|
"license": "Apache-2.0",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/volter-ai/twin.git",
|
|
19
|
+
"directory": "packages/cli"
|
|
20
|
+
},
|
|
16
21
|
"publishConfig": {
|
|
17
22
|
"access": "public"
|
|
18
23
|
},
|
|
@@ -23,8 +28,8 @@
|
|
|
23
28
|
"dist"
|
|
24
29
|
],
|
|
25
30
|
"dependencies": {
|
|
26
|
-
"@volter/world-core": "2.0.
|
|
27
|
-
"@volter/world-runtime": "2.0.
|
|
31
|
+
"@volter/world-core": "2.0.1",
|
|
32
|
+
"@volter/world-runtime": "2.0.1"
|
|
28
33
|
},
|
|
29
34
|
"bin": {
|
|
30
35
|
"volter": "dist/src/cli.js"
|
package/src/cli.ts
CHANGED
|
@@ -34,7 +34,7 @@ world — the twins your app needs, running together (the world of the current
|
|
|
34
34
|
volter world branch [<name>] list branches, or make one from here and check it out
|
|
35
35
|
volter world checkout <name> switch branches
|
|
36
36
|
volter world replay <changeset> --into <branch> feed a changeset's changes into another branch's twins
|
|
37
|
-
volter world clock show | set <iso> | advance <N s|m|h|d> the world's clock: set, not observed
|
|
37
|
+
volter world clock show | set <iso> | advance <N s|m|h|d> | shift <N s|m|h|d> | clear the world's clock: set, not observed
|
|
38
38
|
volter world clone <url> --token <token> [--admin-token <t>] connect this world to a remote and fetch its history (the admin token copies the whole tree at once)
|
|
39
39
|
volter world fetch what the remote observed since the last fetch
|
|
40
40
|
volter world origin where this world clones from and pushes to
|
|
@@ -248,8 +248,10 @@ async function worldCommand(verb: string, args: string[]): Promise<void> {
|
|
|
248
248
|
const [action, arg] = pos;
|
|
249
249
|
if (action === 'set') { if (!arg) throw new Error('volter world clock set <iso-8601>'); out(world.setClock(arg)); return; }
|
|
250
250
|
if (action === 'advance') { if (!arg) throw new Error('volter world clock advance <N s|m|h|d>'); out(world.advanceClock(arg)); return; }
|
|
251
|
-
if (action ===
|
|
252
|
-
|
|
251
|
+
if (action === 'shift') { if (!arg) throw new Error('volter world clock shift <N s|m|h|d>'); out(world.shiftClock(arg)); return; }
|
|
252
|
+
if (action === 'clear') { out(world.clearClock()); return; }
|
|
253
|
+
if (action === undefined || action === 'show') { const c = world.clock(); out(`${c.at}${c.frozen ? '' : c.running ? ' (running)' : ' (wall clock — not set)'}`); return; }
|
|
254
|
+
throw new Error('volter world clock show | set <iso> | advance <N s|m|h|d> | shift <N s|m|h|d> | clear');
|
|
253
255
|
}
|
|
254
256
|
case 'replay': {
|
|
255
257
|
const name = pos[0]; const into = value(args, '--into');
|
package/src/world.ts
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
// user reads. The `volter` command is one client of this class and adds nothing.
|
|
5
5
|
import { rebaseChangeset, worldBootMarker, type Changeset, type ChangesetApplication, type LedgerDelta, type PerformAction, type RemoteExecute, type TwinAction, type WorldMarker, type ApplyReceipt, listActions, isTwinBookkeeping, pushablePendingActions, pendingActions, applyTwinWrite, twinResources, type TwinResource } from '@volter/world-core';
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import clockForm from '@volter/world-core/world-clock';
|
|
7
8
|
import {
|
|
8
9
|
activateScript, approveWorldChangeset, branchWorld, clockFile, checkoutWorld, createWorldChangeset, diffWorld, downWorld, fetchFromOrigin, findWorldChangeset, initWorld, listWorldChangesets, listWorldMarks, listWorlds, markWorld, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, runWithWorldEnv, seedWorld, shellWorld,
|
|
9
10
|
statusWorld, statusWorldChangeset, upWorld, verifyWorldChangeset, worldLedgers, worldOrigin,
|
|
10
11
|
deployWorld, loadWorldConfig, writeWorldConfig, readServeRecord, refreshTwin, serveWorld, findInstalledPackage, type ServedWorld, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, credentialPayloadFrom,
|
|
11
12
|
type DeployTwinOutcome, type MaterializedRoot,
|
|
12
13
|
type ChangesetLocation, type InitOptions, type InitResult, type WorldInstance, materializeRoots } from '@volter/world-runtime';
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
14
15
|
import { dirname, join, resolve } from 'node:path';
|
|
15
16
|
import { requireToken, storeToken } from './credentials.ts';
|
|
16
17
|
import { currentBranch, findWorldRoot, mainBranch, requireWorldRoot, setCurrentBranch, worldConfigRelative, worldEnvPath, worldSeedPath } from './locate.ts';
|
|
@@ -21,6 +22,13 @@ export type WorldMode = 'local' | 'share' | 'sealed';
|
|
|
21
22
|
|
|
22
23
|
/** One change as `log` shows it: the write, and the receipt the vendor answered with once pushed. */
|
|
23
24
|
/** A twin's branch log as the world reads it: the twin's name, the state service it records under, its control root. */
|
|
25
|
+
/** `30d`, `12h`, `5m`, `90s` in ms. */
|
|
26
|
+
function durationMs(by: string): number {
|
|
27
|
+
const m = /^(\d+(?:\.\d+)?)(s|m|h|d)$/.exec(by.trim());
|
|
28
|
+
if (!m) throw new Error(`${JSON.stringify(by)} is not <N>(s|m|h|d)`);
|
|
29
|
+
return Number(m[1]) * { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2] as 's' | 'm' | 'h' | 'd'];
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
export class TwinLog {
|
|
25
33
|
constructor(readonly service: string, readonly stateService: string, readonly root: string) {}
|
|
26
34
|
/** this branch's own entries, bookkeeping aside */
|
|
@@ -233,8 +241,16 @@ export class World {
|
|
|
233
241
|
*/
|
|
234
242
|
async branch(name: string, opts: { at?: { instant?: string; positions?: Record<string, number>; views?: Record<string, string> } } = {}): Promise<World> {
|
|
235
243
|
if (listWorlds(this.root).some((w) => w.name === name)) throw new Error(`A branch "${name}" already exists — \`volter world checkout ${name}\` switches to it`);
|
|
236
|
-
|
|
237
|
-
if
|
|
244
|
+
// this branch stops first (a managed database is copied from a stopped World, and the two would share its port),
|
|
245
|
+
// and comes back if the new one fails
|
|
246
|
+
const wasRunning = !!this.instance()?.running;
|
|
247
|
+
if (wasRunning) await downWorld(this.name, this.root);
|
|
248
|
+
try {
|
|
249
|
+
await branchWorld(this.name, name, { root: this.root, envFile: worldEnvPath(this.root), ...(opts.at ? { at: opts.at } : {}) });
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (wasRunning) await checkoutWorld(this.name, { root: this.root });
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
238
254
|
setCurrentBranch(this.root, name);
|
|
239
255
|
return new World(name, this.root);
|
|
240
256
|
}
|
|
@@ -261,28 +277,49 @@ export class World {
|
|
|
261
277
|
}
|
|
262
278
|
|
|
263
279
|
// ── the clock ───────────────────────────────────────────────────────────────────────────
|
|
264
|
-
/** The world's clock:
|
|
265
|
-
|
|
280
|
+
/** The world's clock (world-clock.cjs): its time now, and whether it is frozen at an instant, running (moved with
|
|
281
|
+
* `shiftClock`), or the wall clock (unset). */
|
|
282
|
+
clock(): { at: string; frozen: boolean; running: boolean } {
|
|
266
283
|
const file = clockFile(this.root, this.name);
|
|
267
|
-
|
|
284
|
+
if (!existsSync(file)) return { at: new Date().toISOString(), frozen: false, running: false };
|
|
285
|
+
const c = clockForm.parseClock(readFileSync(file, 'utf8'));
|
|
286
|
+
return { at: new Date(clockForm.clockNowMs(c, Date.now())).toISOString(), frozen: c.kind === 'frozen', running: c.kind === 'running' };
|
|
268
287
|
}
|
|
269
|
-
/** Set the world's clock to an instant; every twin stamps from it until it moves. */
|
|
288
|
+
/** Set the world's clock to an instant; every twin, and the World's application, stamps from it until it moves. */
|
|
270
289
|
setClock(iso: string): string {
|
|
271
290
|
const parsed = Date.parse(iso);
|
|
272
291
|
if (Number.isNaN(parsed)) throw new Error(`${JSON.stringify(iso)} is not an ISO-8601 instant`);
|
|
273
|
-
|
|
274
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
275
|
-
writeFileSync(file, `${new Date(parsed).toISOString()}\n`);
|
|
292
|
+
this.writeClock({ kind: 'frozen', at: parsed });
|
|
276
293
|
return new Date(parsed).toISOString();
|
|
277
294
|
}
|
|
278
|
-
/** Move a set clock forward: `30d`, `12h`, `5m`, `90s`. */
|
|
295
|
+
/** Move a set clock forward, frozen or running: `30d`, `12h`, `5m`, `90s`. */
|
|
279
296
|
advanceClock(by: string): string {
|
|
280
|
-
const
|
|
281
|
-
if (!
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
297
|
+
const file = clockFile(this.root, this.name);
|
|
298
|
+
if (!existsSync(file)) throw new Error('the clock is not set — `volter world clock set <iso>` first; advancing the wall clock would freeze time as a side effect (`volter world clock shift` moves a running World)');
|
|
299
|
+
const c = clockForm.parseClock(readFileSync(file, 'utf8'));
|
|
300
|
+
this.writeClock({ ...c, at: c.at + durationMs(by) });
|
|
301
|
+
return this.clock().at;
|
|
302
|
+
}
|
|
303
|
+
/** Move the World forward while its time keeps running (a World serving an application): from the wall clock, or a
|
|
304
|
+
* running clock further. */
|
|
305
|
+
shiftClock(by: string): string {
|
|
306
|
+
const file = clockFile(this.root, this.name);
|
|
307
|
+
const c = existsSync(file) ? clockForm.parseClock(readFileSync(file, 'utf8')) : null;
|
|
308
|
+
if (c?.kind === 'frozen') throw new Error('the clock is frozen — `volter world clock advance` moves it');
|
|
309
|
+
const wall = Date.now();
|
|
310
|
+
this.writeClock(c ? { ...c, at: c.at + durationMs(by) } : { kind: 'running', at: wall + durationMs(by), since: wall });
|
|
311
|
+
return this.clock().at;
|
|
312
|
+
}
|
|
313
|
+
/** Return the World to the machine's time. */
|
|
314
|
+
clearClock(): string {
|
|
315
|
+
const file = clockFile(this.root, this.name);
|
|
316
|
+
if (existsSync(file)) rmSync(file);
|
|
317
|
+
return new Date().toISOString();
|
|
318
|
+
}
|
|
319
|
+
private writeClock(c: Parameters<typeof clockForm.formatClock>[0]): void {
|
|
320
|
+
const file = clockFile(this.root, this.name);
|
|
321
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
322
|
+
writeFileSync(file, `${clockForm.formatClock(c)}\n`);
|
|
286
323
|
}
|
|
287
324
|
/** Rebase this branch onto its base's current position, every twin; conflicts named per subject and field. */
|
|
288
325
|
rebaseBranch(): Array<{ service: string } & ReturnType<typeof rebaseBranch>> { return this.repos().map((r) => ({ service: r.service, ...rebaseBranch(r.stateService, r.root) })); }
|