@north-light/crouter 0.3.281 → 0.3.282
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/api/plugin-manifest-schema.d.ts +208 -0
- package/dist/api/plugin-manifest-schema.js +23 -0
- package/dist/cli.js +8 -2
- package/dist/clients/attach/viewer.js +533 -533
- package/dist/commands/pkg/browse/catalog.js +1 -1
- package/dist/commands/pkg/plugin-manage.d.ts +46 -1
- package/dist/commands/pkg/plugin-manage.js +170 -20
- package/dist/core/__tests__/integration/plugin-revalidate.test.d.ts +1 -0
- package/dist/core/__tests__/integration/plugin-revalidate.test.js +355 -0
- package/dist/core/command-manifests/registry.d.ts +3 -1
- package/dist/core/command-manifests/schema.d.ts +2 -75
- package/dist/core/command-manifests/schema.js +5 -0
- package/dist/core/command-plugins/compose.js +1 -1
- package/dist/core/command-plugins/revalidate.d.ts +18 -0
- package/dist/core/command-plugins/revalidate.js +152 -0
- package/dist/core/command-plugins/transport/http-fetch.d.ts +29 -4
- package/dist/core/command-plugins/transport/http-fetch.js +18 -8
- package/dist/core/command-plugins/transport/http-invoke.js +7 -0
- package/dist/core/command.d.ts +8 -0
- package/dist/core/command.js +19 -3
- package/dist/core/exclusive-lock.d.ts +12 -0
- package/dist/core/exclusive-lock.js +29 -0
- package/dist/core/installed-plugins.js +31 -0
- package/dist/core/manifest-recovery.d.ts +15 -0
- package/dist/core/manifest-recovery.js +72 -0
- package/dist/core/manifest-stale.d.ts +18 -0
- package/dist/core/manifest-stale.js +39 -0
- package/dist/core/plugin-swap-lock.d.ts +9 -0
- package/dist/core/plugin-swap-lock.js +31 -0
- package/dist/types.d.ts +7 -0
- package/package.json +2 -1
- package/runtime.lock.json +2 -2
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Dispatch with recovery from an out-of-date command description.
|
|
2
|
+
//
|
|
3
|
+
// Extracted from the cli entry so the whole behaviour — rethrow, refetch,
|
|
4
|
+
// re-parse, exactly-once — can be driven by a test through `runCli` and a real
|
|
5
|
+
// error off the wire, rather than only by launching the binary.
|
|
6
|
+
import { resolveRoot } from '../build-root.js';
|
|
7
|
+
import { runCli } from './command.js';
|
|
8
|
+
import { forceRevalidateBundlePlugin } from './command-plugins/revalidate.js';
|
|
9
|
+
import { handle } from './io.js';
|
|
10
|
+
import { staleManifestPlugin } from './manifest-stale.js';
|
|
11
|
+
/**
|
|
12
|
+
* Run `argv` against `root`, and if the backend answers `manifest_stale`,
|
|
13
|
+
* refetch that plugin's package and run the ORIGINAL argv again against a
|
|
14
|
+
* freshly resolved tree — exactly once.
|
|
15
|
+
*
|
|
16
|
+
* Re-parsing the original argv, rather than re-running the leaf that failed, is
|
|
17
|
+
* what makes a RENAMED verb recoverable: the old leaf no longer exists in the
|
|
18
|
+
* refreshed tree, and only the dispatcher can answer that against the fresh
|
|
19
|
+
* tree's own siblings.
|
|
20
|
+
*
|
|
21
|
+
* Never throws: every outcome is rendered through the dispatcher's own
|
|
22
|
+
* `handle`, so a caller can mark the end of dispatch unconditionally.
|
|
23
|
+
*/
|
|
24
|
+
export async function dispatchWithStaleRecovery(root, argv) {
|
|
25
|
+
try {
|
|
26
|
+
// The dispatcher renders every other failure itself; a `manifest_stale`
|
|
27
|
+
// error is the one it rethrows, because recovery needs this frame.
|
|
28
|
+
await runCli(root, argv);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (!(await refetchStalePlugin(error))) {
|
|
33
|
+
// Nothing newer to run against: report the backend's own complaint
|
|
34
|
+
// exactly as the dispatcher would have.
|
|
35
|
+
handle(error);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
// The refreshed tree, re-resolved: the package this invocation dispatches
|
|
40
|
+
// against is not the one it was built from. Replaying the argv is only
|
|
41
|
+
// safe because a server that sets the flag has promised it — the two
|
|
42
|
+
// preconditions are recorded on `ManifestStaleEnvelope` in
|
|
43
|
+
// `src/api/plugin-manifest-schema.ts`, and crtr cannot check either one.
|
|
44
|
+
await runCli(await resolveRoot(argv[2]), argv);
|
|
45
|
+
}
|
|
46
|
+
catch (retryError) {
|
|
47
|
+
// Exactly once. A second `manifest_stale` against a package just
|
|
48
|
+
// refetched is the server's answer, not another refresh cycle.
|
|
49
|
+
handle(retryError);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A backend that answers `manifest_stale` is saying this client invoked from an
|
|
55
|
+
* out-of-date description of its commands. Refetch that plugin's package and
|
|
56
|
+
* report whether it actually changed. An unchanged package means the operation
|
|
57
|
+
* is genuinely gone, so the server's own error, which already names what it
|
|
58
|
+
* does serve, is the honest answer and stands.
|
|
59
|
+
*/
|
|
60
|
+
async function refetchStalePlugin(error) {
|
|
61
|
+
const plugin = staleManifestPlugin(error);
|
|
62
|
+
if (plugin === undefined)
|
|
63
|
+
return false;
|
|
64
|
+
try {
|
|
65
|
+
return await forceRevalidateBundlePlugin(plugin);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// The refetch is the recovery. If it fails there is nothing newer to retry
|
|
69
|
+
// against, and the server's original complaint is what the caller needs.
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ManifestStaleEnvelope } from '../api/plugin-manifest-schema.js';
|
|
2
|
+
/** What crtr attaches to the `CrtrError` it raises for a stale answer. */
|
|
3
|
+
type StaleErrorDetails = Required<ManifestStaleEnvelope> & {
|
|
4
|
+
plugin: string;
|
|
5
|
+
};
|
|
6
|
+
/** Did the backend's own error envelope claim staleness? Reads the parsed JSON
|
|
7
|
+
* body, before it becomes a `CrtrError`. */
|
|
8
|
+
export declare function envelopeClaimsStale(envelope: Record<string, unknown>): boolean;
|
|
9
|
+
/** The details a transport adds when it converts a stale envelope into the
|
|
10
|
+
* error the dispatcher rethrows. */
|
|
11
|
+
export declare function staleErrorDetails(plugin: string): StaleErrorDetails;
|
|
12
|
+
/** Is this the one error class the dispatcher rethrows instead of rendering? */
|
|
13
|
+
export declare function isManifestStaleError(error: unknown): boolean;
|
|
14
|
+
/** The plugin to refetch, or `undefined` when this error is not a recoverable
|
|
15
|
+
* stale answer. A stale flag with no plugin name is not actionable: nothing
|
|
16
|
+
* identifies the package whose description went out of date. */
|
|
17
|
+
export declare function staleManifestPlugin(error: unknown): string | undefined;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// The one place in crtr that names the `manifest_stale` wire field.
|
|
2
|
+
//
|
|
3
|
+
// The field itself is declared in the published format subpath
|
|
4
|
+
// (`src/api/plugin-manifest-schema.ts`), which is what a plugin backend compiles
|
|
5
|
+
// against. This module binds crtr's readers and writer to that declaration, so
|
|
6
|
+
// renaming the field there fails the build here rather than silently splitting
|
|
7
|
+
// the two sides: a backend that keeps setting a field nobody reads, and a client
|
|
8
|
+
// that never recovers.
|
|
9
|
+
import { CrtrError } from './errors.js';
|
|
10
|
+
/** Bound to the published declaration: `keyof` is the field's literal type, so
|
|
11
|
+
* a rename over there makes this assignment a type error. */
|
|
12
|
+
const STALE_FIELD = 'manifest_stale';
|
|
13
|
+
/** The plugin whose package produced the out-of-date description, carried
|
|
14
|
+
* alongside the flag so the recovery frame knows what to refetch. The backend
|
|
15
|
+
* does not send this — the transport that made the call names it. */
|
|
16
|
+
const PLUGIN_FIELD = 'plugin';
|
|
17
|
+
/** Did the backend's own error envelope claim staleness? Reads the parsed JSON
|
|
18
|
+
* body, before it becomes a `CrtrError`. */
|
|
19
|
+
export function envelopeClaimsStale(envelope) {
|
|
20
|
+
return envelope[STALE_FIELD] === true;
|
|
21
|
+
}
|
|
22
|
+
/** The details a transport adds when it converts a stale envelope into the
|
|
23
|
+
* error the dispatcher rethrows. */
|
|
24
|
+
export function staleErrorDetails(plugin) {
|
|
25
|
+
return { [STALE_FIELD]: true, [PLUGIN_FIELD]: plugin };
|
|
26
|
+
}
|
|
27
|
+
/** Is this the one error class the dispatcher rethrows instead of rendering? */
|
|
28
|
+
export function isManifestStaleError(error) {
|
|
29
|
+
return error instanceof CrtrError && error.details?.[STALE_FIELD] === true;
|
|
30
|
+
}
|
|
31
|
+
/** The plugin to refetch, or `undefined` when this error is not a recoverable
|
|
32
|
+
* stale answer. A stale flag with no plugin name is not actionable: nothing
|
|
33
|
+
* identifies the package whose description went out of date. */
|
|
34
|
+
export function staleManifestPlugin(error) {
|
|
35
|
+
if (!isManifestStaleError(error))
|
|
36
|
+
return undefined;
|
|
37
|
+
const plugin = error.details?.[PLUGIN_FIELD];
|
|
38
|
+
return typeof plugin === 'string' ? plugin : undefined;
|
|
39
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Under the scope root's `tmp/`, alongside the staging and `.prev` dirs the
|
|
2
|
+
* swap itself uses, so one directory holds the whole transition. */
|
|
3
|
+
export declare function bundleSwapLockPath(scopeRootPath: string, name: string): string;
|
|
4
|
+
/** Whether ANOTHER live process currently holds this plugin's swap lock. Our
|
|
5
|
+
* own lock does not count: the swapper reads the plugin list from inside its
|
|
6
|
+
* own critical section. */
|
|
7
|
+
export declare function bundleSwapInFlightElsewhere(scopeRootPath: string, name: string): boolean;
|
|
8
|
+
/** Wait out another process's swap of this plugin, bounded. */
|
|
9
|
+
export declare function awaitBundleSwap(scopeRootPath: string, name: string): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// The one name for the lock a bundle-plugin package swap is held under, and the
|
|
2
|
+
// two questions a reader asks about it.
|
|
3
|
+
//
|
|
4
|
+
// Two modules need it and neither may own it: the swapper lives in
|
|
5
|
+
// `commands/pkg/plugin-manage.ts` (the whole command graph), the reader in
|
|
6
|
+
// `core/installed-plugins.ts` (deliberately leaf-safe: types, fs-utils,
|
|
7
|
+
// manifest). This module keeps both dependency-light — `node:path` plus the
|
|
8
|
+
// generic lock primitive, which itself imports only `node:fs` and
|
|
9
|
+
// `node:crypto`.
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { awaitExclusiveLockRelease, exclusiveLockOwnerPid } from './exclusive-lock.js';
|
|
12
|
+
/** How long a reader waits for another process's swap to finish before it
|
|
13
|
+
* reports what it can see. A swap is one rename of an already-staged, already-
|
|
14
|
+
* validated tree, so a wait this long means the holder is wedged, not slow. */
|
|
15
|
+
const READER_WAIT_MS = 2_000;
|
|
16
|
+
/** Under the scope root's `tmp/`, alongside the staging and `.prev` dirs the
|
|
17
|
+
* swap itself uses, so one directory holds the whole transition. */
|
|
18
|
+
export function bundleSwapLockPath(scopeRootPath, name) {
|
|
19
|
+
return join(scopeRootPath, 'tmp', `${name}.swap.lock`);
|
|
20
|
+
}
|
|
21
|
+
/** Whether ANOTHER live process currently holds this plugin's swap lock. Our
|
|
22
|
+
* own lock does not count: the swapper reads the plugin list from inside its
|
|
23
|
+
* own critical section. */
|
|
24
|
+
export function bundleSwapInFlightElsewhere(scopeRootPath, name) {
|
|
25
|
+
const pid = exclusiveLockOwnerPid(bundleSwapLockPath(scopeRootPath, name));
|
|
26
|
+
return pid !== null && pid !== process.pid;
|
|
27
|
+
}
|
|
28
|
+
/** Wait out another process's swap of this plugin, bounded. */
|
|
29
|
+
export function awaitBundleSwap(scopeRootPath, name) {
|
|
30
|
+
awaitExclusiveLockRelease(bundleSwapLockPath(scopeRootPath, name), READER_WAIT_MS);
|
|
31
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -386,8 +386,15 @@ export interface ScopeState {
|
|
|
386
386
|
marketplaces: Record<string, {
|
|
387
387
|
last_updated?: string;
|
|
388
388
|
}>;
|
|
389
|
+
/** Per-plugin cache bookkeeping for the bundle revalidation pass.
|
|
390
|
+
* `etag` is the validator the server returned with the archive currently
|
|
391
|
+
* unpacked on disk, replaced only on a 200. `checked_at` is written on every
|
|
392
|
+
* revalidation attempt, success or failure, so an unreachable server costs
|
|
393
|
+
* one timed-out probe per TTL rather than one per command. */
|
|
389
394
|
plugins: Record<string, {
|
|
390
395
|
last_updated?: string;
|
|
396
|
+
etag?: string;
|
|
397
|
+
checked_at?: string;
|
|
391
398
|
}>;
|
|
392
399
|
last_self_check?: string;
|
|
393
400
|
/** The name of the remote canvas target `crtr canvas use` last selected
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.282",
|
|
4
4
|
"description": "crtr — agent runtime with memory, plugins, and marketplaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
"typecheck:pi-packages": "npx tsc -p src/builtin-pi-packages/pi-crtr-extensions/tsconfig.json",
|
|
77
77
|
"test:build-output": "node scripts/build-concurrency-test.mjs",
|
|
78
78
|
"check:cli-no-canvas": "node scripts/check-cli-no-canvas.mjs",
|
|
79
|
+
"check:cli-cold-start": "node scripts/check-cli-cold-start.mjs",
|
|
79
80
|
"check:daemon-no-barrel": "node scripts/check-daemon-no-barrel.mjs",
|
|
80
81
|
"check:quote-style": "node scripts/check-quote-style.mjs",
|
|
81
82
|
"postinstall": "node scripts/postinstall.mjs",
|
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.282",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.282",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|