@avantf/dsh-plugin-base 0.5.3 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -13
- package/lib/bootstrap.d.ts +1 -1
- package/lib/bootstrap.js +1 -1
- package/lib/client/load_more.d.ts +157 -0
- package/lib/client/load_more.d.ts.map +1 -0
- package/lib/client/load_more.js +252 -0
- package/lib/conformance.d.ts.map +1 -1
- package/lib/conformance.js +5 -0
- package/lib/errors.d.ts +3 -1
- package/lib/errors.d.ts.map +1 -1
- package/lib/fs.d.ts.map +1 -1
- package/lib/fs.js +3 -0
- package/lib/index.d.ts +10 -4
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +18 -3
- package/lib/interface.d.ts +165 -32
- package/lib/interface.d.ts.map +1 -1
- package/lib/interface.js +56 -16
- package/lib/internal.d.ts +4 -0
- package/lib/internal.d.ts.map +1 -1
- package/lib/internal.js +22 -0
- package/lib/lint.d.ts.map +1 -1
- package/lib/lint.js +39 -1
- package/lib/providers/npm.d.ts +10 -1
- package/lib/providers/npm.d.ts.map +1 -1
- package/lib/providers/npm.js +2 -2
- package/lib/providers/runtime_deps.d.ts +184 -0
- package/lib/providers/runtime_deps.d.ts.map +1 -0
- package/lib/providers/runtime_deps.js +1214 -0
- package/lib/provisioner.d.ts.map +1 -1
- package/lib/provisioner.js +4 -1
- package/lib/runtime_deps_helper.d.ts +71 -0
- package/lib/runtime_deps_helper.d.ts.map +1 -0
- package/lib/runtime_deps_helper.js +161 -0
- package/lib/session_notices.d.ts +95 -0
- package/lib/session_notices.d.ts.map +1 -0
- package/lib/session_notices.js +172 -0
- package/lib/state.d.ts +8 -1
- package/lib/state.d.ts.map +1 -1
- package/lib/state.js +20 -4
- package/lib/types.d.ts +14 -1
- package/lib/types.d.ts.map +1 -1
- package/package.json +7 -4
- package/src/client/controls.module.css +294 -0
package/lib/provisioner.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provisioner.d.ts","sourceRoot":"","sources":["../src/provisioner.ts"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EACV,SAAS,EACT,KAAK,EAeL,aAAa,EAMb,WAAW,EAEX,kBAAkB,
|
|
1
|
+
{"version":3,"file":"provisioner.d.ts","sourceRoot":"","sources":["../src/provisioner.ts"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EACV,SAAS,EACT,KAAK,EAeL,aAAa,EAMb,WAAW,EAEX,kBAAkB,EAOnB,MAAM,YAAY,CAAA;AAEnB,yDAAyD;AACzD,eAAO,MAAM,YAAY,EAAE,SAAS,MAAM,EAA6C,CAAA;AAEvF,kDAAkD;AAClD,eAAO,MAAM,mBAAmB,QAAS,CAAA;AAEzC,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,IAAI,CAAA;AAepC,UAAU,iBAAiB;IACzB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAA;IAC7B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;CACtB;AAED;;;iFAGiF;AACjF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,CAAC,WAAW,CAAC,GAAG,iBAAiB,CAKvF;AA0DD;cACc;AACd,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAy/B1E"}
|
package/lib/provisioner.js
CHANGED
|
@@ -744,7 +744,10 @@ export function createProvisioner(options) {
|
|
|
744
744
|
},
|
|
745
745
|
};
|
|
746
746
|
putRow(declared, resolved.version, state, resolved.source);
|
|
747
|
-
|
|
747
|
+
// A provider that says it only REUSED an equivalent root (another process published it while
|
|
748
|
+
// this one waited) placed nothing: the honest action is `present`, not `installed`.
|
|
749
|
+
const action = resolved.reused === true ? 'present' : 'installed';
|
|
750
|
+
return entryOf(declared, { action, source: resolved.source, started, version: resolved.version });
|
|
748
751
|
}
|
|
749
752
|
catch (error) {
|
|
750
753
|
const { code, message } = reasonOf(error);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { DependencySource, InstallAttempt } from './providers/runtime_deps.js';
|
|
2
|
+
import type { SessionNoticeRegistry, SessionNoticeSpec } from './session_notices.js';
|
|
3
|
+
import type { ProvisionItem, ProvisionReportEntry, Provisioner, Resolved } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* What a plugin declares. A superset of {@link RuntimeDepsSpec} (the provider's own input) plus the
|
|
6
|
+
* identity and the notices: the spec fields are forwarded verbatim, so nothing here decides anything
|
|
7
|
+
* about packages, registries or managers.
|
|
8
|
+
*/
|
|
9
|
+
export interface RuntimeDepsConfig {
|
|
10
|
+
/** The plugin's namespace (`mem`); the default item id is `${namespace}:runtime-deps`. */
|
|
11
|
+
readonly namespace: string;
|
|
12
|
+
/** Override the item id; must still be prefixed with `${namespace}:`. */
|
|
13
|
+
readonly itemId?: string;
|
|
14
|
+
/** Package name → dependency spec. Forwarded to {@link RuntimeDepsSpec.packages}. */
|
|
15
|
+
readonly packages: Readonly<Record<string, string>>;
|
|
16
|
+
/** The registry/cache endpoints the attempts reference. */
|
|
17
|
+
readonly sources: readonly DependencySource[];
|
|
18
|
+
/** Install attempts, tried in order. */
|
|
19
|
+
readonly attempts: readonly InstallAttempt[];
|
|
20
|
+
/** Specifiers every successful root must resolve; non-empty. */
|
|
21
|
+
readonly verify: readonly string[];
|
|
22
|
+
/** Host-provided singletons, linked never installed. */
|
|
23
|
+
readonly peers?: Readonly<Record<string, string>>;
|
|
24
|
+
/** A lockfile whose bytes seed/audit the install. */
|
|
25
|
+
readonly lockfile?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Notices to deliver once per session when this item is NOT ready. Each one's `when` selects which
|
|
28
|
+
* observed states match: `failed` → the item failed; `missing` → it is missing or was skipped;
|
|
29
|
+
* `degraded` → any of those three (the broad "the shared root is not there" condition).
|
|
30
|
+
*/
|
|
31
|
+
readonly notices?: readonly SessionNoticeSpec[];
|
|
32
|
+
}
|
|
33
|
+
/** What {@link createRuntimeDeps} hands back. */
|
|
34
|
+
export interface RuntimeDepsHandle {
|
|
35
|
+
/** The item to declare through the caller's own provisioner. */
|
|
36
|
+
readonly item: ProvisionItem;
|
|
37
|
+
/** The registry, pre-loaded with {@link RuntimeDepsConfig.notices}. */
|
|
38
|
+
readonly notices: SessionNoticeRegistry;
|
|
39
|
+
/**
|
|
40
|
+
* Ensure THIS item through the caller's provisioner and return its report entry.
|
|
41
|
+
*
|
|
42
|
+
* The item must already be declared (the helper never declares); `undefined` means the report
|
|
43
|
+
* carried no entry for it — a background item is dispatched and reports through `onSettled`
|
|
44
|
+
* instead, and `resolve()` reflects it as soon as it settles. A rejected `ensure` propagates: that
|
|
45
|
+
* is a dispatch failure, not a provisioning outcome.
|
|
46
|
+
*/
|
|
47
|
+
ensure(provisioner: Provisioner): Promise<ProvisionReportEntry | undefined>;
|
|
48
|
+
/** The ready shared root as the core reports it, or `undefined` while it is not ready. */
|
|
49
|
+
resolve(): Resolved | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Resolve `specifier` from the shared root and import its entry (dynamic `import()`).
|
|
52
|
+
*
|
|
53
|
+
* Read-only by construction: `createRequire(<root>/package.json).resolve()` plus `import()` of the
|
|
54
|
+
* resolved file — no copy, no `node_modules` write, no link. The module is imported by FILE URL, so
|
|
55
|
+
* a package that must not be resolved twice through the host's tree still gets this root's copy.
|
|
56
|
+
*
|
|
57
|
+
* Rejects with a {@link ProvisionError} (`invalid-option`) when the root is not ready or the
|
|
58
|
+
* specifier is empty, and with the underlying resolution/import error otherwise — a failed resolve
|
|
59
|
+
* is the caller's specifier meeting this root, not a provisioning failure with a code.
|
|
60
|
+
*/
|
|
61
|
+
load<T = unknown>(specifier: string): Promise<T>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Build the handle for one shared runtime-dependency root.
|
|
65
|
+
*
|
|
66
|
+
* @param config - the plugin's declaration; forwarded to the provider field by field.
|
|
67
|
+
* @throws ProvisionError (`invalid-option`) for a namespace/itemId that cannot make a namespaced item
|
|
68
|
+
* id — a caller bug at construction time, not a provisioning outcome.
|
|
69
|
+
*/
|
|
70
|
+
export declare function createRuntimeDeps(config: RuntimeDepsConfig): RuntimeDepsHandle;
|
|
71
|
+
//# sourceMappingURL=runtime_deps_helper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime_deps_helper.d.ts","sourceRoot":"","sources":["../src/runtime_deps_helper.ts"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAmB,MAAM,6BAA6B,CAAA;AACpG,OAAO,KAAK,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AACpF,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,WAAW,EAAE,QAAQ,EAAiB,MAAM,YAAY,CAAA;AAE3G;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,0FAA0F;IAC1F,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,yEAAyE;IACzE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,qFAAqF;IACrF,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACnD,2DAA2D;IAC3D,QAAQ,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAA;IAC7C,wCAAwC;IACxC,QAAQ,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,CAAA;IAC5C,gEAAgE;IAChE,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;IAClC,wDAAwD;IACxD,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACjD,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAA;CAChD;AAED,iDAAiD;AACjD,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAA;IAC5B,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAA;IACvC;;;;;;;OAOG;IACH,MAAM,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAAA;IAC3E,0FAA0F;IAC1F,OAAO,IAAI,QAAQ,GAAG,SAAS,CAAA;IAC/B;;;;;;;;;;OAUG;IACH,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;CACjD;AAiDD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,GAAG,iBAAiB,CA+E9E"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createRuntimeDeps` — the thin, library-level helper a plugin uses to DECLARE one shared
|
|
3
|
+
* runtime-dependency root and then reach it: the item, the session notices and the loader.
|
|
4
|
+
*
|
|
5
|
+
* It is sugar over two members this package already exports, never a second implementation of them:
|
|
6
|
+
*
|
|
7
|
+
* - `runtimeDepsProvider()` (interface v5) owns the content-addressed store, the install attempts,
|
|
8
|
+
* verify, peer linking and failure reporting;
|
|
9
|
+
* - `createSessionNotices()` (interface v6) owns the once-per-session notice bookkeeping.
|
|
10
|
+
*
|
|
11
|
+
* What the helper adds is the wiring in between, and nothing else:
|
|
12
|
+
*
|
|
13
|
+
* - **one item** built from the caller's spec — `kind: 'runtime-deps'`, the canonical
|
|
14
|
+
* `<home>/deps/runtime` store, this build's item schema. Base names no package;
|
|
15
|
+
* - **notices bound to the ITEM's real state**: each {@link SessionNoticeSpec.when} is mapped onto the
|
|
16
|
+
* provisioner's own `resolve(item.id)`, re-read on every `consume` — so a root that healed stops
|
|
17
|
+
* matching, exactly as the registry requires;
|
|
18
|
+
* - **`ensure()` / `resolve()`** as the caller's handle onto the provisioner it already owns (the
|
|
19
|
+
* plugin still declares the item and still calls `ensure`; the helper only narrows the answer to
|
|
20
|
+
* this item);
|
|
21
|
+
* - **`load()`**, which resolves a specifier from the shared root and imports it — it COPIES NO
|
|
22
|
+
* PACKAGE and writes nothing, including no `node_modules`.
|
|
23
|
+
*
|
|
24
|
+
* Deliberately absent: no hook registration, no configuration reading, no filesystem access at
|
|
25
|
+
* construction, no session bookkeeping of its own. Base stays a library, and the plugin stays the only
|
|
26
|
+
* DSH lifecycle owner.
|
|
27
|
+
*
|
|
28
|
+
* @module runtime_deps_helper
|
|
29
|
+
*/
|
|
30
|
+
import { createRequire } from 'node:module';
|
|
31
|
+
import { join } from 'node:path';
|
|
32
|
+
import { pathToFileURL } from 'node:url';
|
|
33
|
+
import { ProvisionError } from './errors.js';
|
|
34
|
+
import { ITEM_SCHEMA_VERSION } from './provisioner.js';
|
|
35
|
+
import { RUNTIME_DEPS_DIR, RUNTIME_DEPS_KIND } from './providers/runtime_deps.js';
|
|
36
|
+
import { createSessionNotices } from './session_notices.js';
|
|
37
|
+
/** `plugin` namespaces this build accepts in an item id: anything but `:`, `/` and whitespace. */
|
|
38
|
+
const NAMESPACE = /^[^\s:/]+$/u;
|
|
39
|
+
/** The observed states one {@link SessionNoticeSpec.when} covers. */
|
|
40
|
+
function matchesWhen(when, state) {
|
|
41
|
+
if (state === undefined)
|
|
42
|
+
return false;
|
|
43
|
+
switch (state.state) {
|
|
44
|
+
case 'failed':
|
|
45
|
+
return when === 'failed' || when === 'degraded';
|
|
46
|
+
case 'missing':
|
|
47
|
+
case 'skipped':
|
|
48
|
+
return when === 'missing' || when === 'degraded';
|
|
49
|
+
// `pending` is deliberately NOT a notice: the install/job is still in flight, and a latch taken
|
|
50
|
+
// while it runs would make a "not ready" line permanent for that session even after it succeeds.
|
|
51
|
+
case 'pending':
|
|
52
|
+
case 'ready':
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The `Resolved` shape of a ready handle.
|
|
58
|
+
*
|
|
59
|
+
* The fields come from the CORE's {@link ResourceHandle}; `entryDir` equals `dir` because the shared
|
|
60
|
+
* store publishes its root as the usable entry (the provider's own `resolvedOf` says the same), and
|
|
61
|
+
* `name` is the item id the caller declared. Nothing is invented: a handle that is not ready has no
|
|
62
|
+
* `Resolved` at all.
|
|
63
|
+
*/
|
|
64
|
+
function resolvedOf(state) {
|
|
65
|
+
if (state?.state !== 'ready')
|
|
66
|
+
return undefined;
|
|
67
|
+
const handle = state.handle;
|
|
68
|
+
return {
|
|
69
|
+
key: handle.key,
|
|
70
|
+
name: handle.id,
|
|
71
|
+
version: handle.version ?? '',
|
|
72
|
+
dir: handle.dir,
|
|
73
|
+
entryDir: handle.dir,
|
|
74
|
+
source: handle.source,
|
|
75
|
+
...(Object.keys(handle.env).length === 0 ? {} : { env: handle.env }),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** A declared list, or an empty one: a missing/malformed field is the provider's `invalid-option`. */
|
|
79
|
+
function arrayOf(value) {
|
|
80
|
+
return Array.isArray(value) ? value : [];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Build the handle for one shared runtime-dependency root.
|
|
84
|
+
*
|
|
85
|
+
* @param config - the plugin's declaration; forwarded to the provider field by field.
|
|
86
|
+
* @throws ProvisionError (`invalid-option`) for a namespace/itemId that cannot make a namespaced item
|
|
87
|
+
* id — a caller bug at construction time, not a provisioning outcome.
|
|
88
|
+
*/
|
|
89
|
+
export function createRuntimeDeps(config) {
|
|
90
|
+
const namespace = config.namespace;
|
|
91
|
+
if (typeof namespace !== 'string' || !NAMESPACE.test(namespace)) {
|
|
92
|
+
throw new ProvisionError('invalid-option', `runtime-deps 的 namespace 必须是插件名(不含 :、/ 与空白):${JSON.stringify(namespace)}`);
|
|
93
|
+
}
|
|
94
|
+
const id = config.itemId ?? `${namespace}:runtime-deps`;
|
|
95
|
+
if (typeof id !== 'string' || !id.startsWith(`${namespace}:`) || id.length <= namespace.length + 1) {
|
|
96
|
+
throw new ProvisionError('invalid-option', `runtime-deps 的 itemId 必须以 "${namespace}:" 前缀化且非空:${JSON.stringify(config.itemId)}`);
|
|
97
|
+
}
|
|
98
|
+
// COPIED, field by field, for two reasons: the caller may keep mutating its own config object (the
|
|
99
|
+
// spec is content-key material, so it must be frozen at construction), and a missing field becomes an
|
|
100
|
+
// empty one, which the PROVIDER then rejects as `invalid-option` on the item — a bad declaration
|
|
101
|
+
// degrades to a failed item instead of throwing out of the plugin's mount path.
|
|
102
|
+
const spec = {
|
|
103
|
+
packages: { ...config.packages },
|
|
104
|
+
sources: arrayOf(config.sources).map(source => ({ ...source })),
|
|
105
|
+
attempts: arrayOf(config.attempts).map(attempt => ({ ...attempt })),
|
|
106
|
+
verify: [...arrayOf(config.verify)],
|
|
107
|
+
...(config.peers === undefined ? {} : { peers: { ...config.peers } }),
|
|
108
|
+
...(config.lockfile === undefined ? {} : { lockfile: config.lockfile }),
|
|
109
|
+
};
|
|
110
|
+
const item = {
|
|
111
|
+
id,
|
|
112
|
+
kind: RUNTIME_DEPS_KIND,
|
|
113
|
+
spec,
|
|
114
|
+
// The provider's canonical shared store: every plugin declaring the same spec meets in ONE root.
|
|
115
|
+
target: { root: RUNTIME_DEPS_DIR },
|
|
116
|
+
schemaVersion: ITEM_SCHEMA_VERSION,
|
|
117
|
+
};
|
|
118
|
+
/** The provisioner handed to {@link RuntimeDepsHandle.ensure}; notices need it to observe state. */
|
|
119
|
+
let active;
|
|
120
|
+
const stateNow = () => {
|
|
121
|
+
if (active === undefined)
|
|
122
|
+
return undefined;
|
|
123
|
+
try {
|
|
124
|
+
return active.resolve(id);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// `resolve` is a map read and does not throw; a hostile/partial provisioner must not turn a
|
|
128
|
+
// prompt line into a crash either way.
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const resolveNow = () => resolvedOf(stateNow());
|
|
133
|
+
const notices = createSessionNotices();
|
|
134
|
+
for (const notice of arrayOf(config.notices)) {
|
|
135
|
+
notices.register(notice, () => matchesWhen(notice.when, stateNow()));
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
item,
|
|
139
|
+
notices,
|
|
140
|
+
async ensure(provisioner) {
|
|
141
|
+
active = provisioner;
|
|
142
|
+
const report = await provisioner.ensure({ only: [id] });
|
|
143
|
+
return report.entries.find(entry => entry.id === id);
|
|
144
|
+
},
|
|
145
|
+
resolve: resolveNow,
|
|
146
|
+
async load(specifier) {
|
|
147
|
+
const resolved = resolveNow();
|
|
148
|
+
if (resolved === undefined) {
|
|
149
|
+
throw new ProvisionError('invalid-option', `runtime-deps 句柄尚未就绪:item ${id} 没有共享根(先 ensure,并在 settle 之后再 load)`);
|
|
150
|
+
}
|
|
151
|
+
if (typeof specifier !== 'string' || specifier.trim() === '') {
|
|
152
|
+
throw new ProvisionError('invalid-option', `runtime-deps.load 需要一个非空的 specifier(item ${id})`);
|
|
153
|
+
}
|
|
154
|
+
// The root's own `package.json` is the resolution anchor: `node_modules` under it is what the
|
|
155
|
+
// provider installed, and it is the ONLY tree this import may see.
|
|
156
|
+
const entry = createRequire(join(resolved.dir, 'package.json')).resolve(specifier);
|
|
157
|
+
return (await import(pathToFileURL(entry).href));
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=runtime_deps_helper.js.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped notices — the generic, in-memory channel through which a plugin tells ONE agent
|
|
3
|
+
* session that one of its optional capabilities is not ready.
|
|
4
|
+
*
|
|
5
|
+
* The defect this exists for is a shape, not a package: a plugin provisions an optional capability
|
|
6
|
+
* (a shared runtime-dependency root, a model cache, an archive) in the BACKGROUND, mounts either way,
|
|
7
|
+
* and then has to say so in the owner's prompt — exactly once, with the caller's own wording, and
|
|
8
|
+
* without ever being able to refuse the mount or to leak a stale line after the capability healed.
|
|
9
|
+
* Every family plugin re-implemented that bookkeeping locally; this is the ONE implementation, so a
|
|
10
|
+
* fix here is a base release and not one rebuild per plugin.
|
|
11
|
+
*
|
|
12
|
+
* The registry deliberately knows nothing else:
|
|
13
|
+
*
|
|
14
|
+
* - **No wording.** {@link SessionNoticeSpec.body} is returned VERBATIM; the registry adds no prefix,
|
|
15
|
+
* no severity label and no punctuation. Base names no package, command or user-facing sentence.
|
|
16
|
+
* - **No state of its own.** `state()` is the caller's predicate and it is evaluated on EVERY
|
|
17
|
+
* {@link SessionNoticeRegistry.consume}: a healed dependency simply stops matching, without the
|
|
18
|
+
* registry having to know what "healed" means.
|
|
19
|
+
* - **No session identity.** The caller supplies the session id and decides which sessions may see a
|
|
20
|
+
* line ({@link SessionNoticeRegistry.consume}'s `ownerOnly`); the registry only remembers, per
|
|
21
|
+
* session id, which notice ids it has already delivered.
|
|
22
|
+
* - **No persistence.** Session ids live in a `Map` for the lifetime of the instance. Nothing here
|
|
23
|
+
* touches the filesystem, a database, `process.env` or any global: a notice is a prompt line, not
|
|
24
|
+
* a record.
|
|
25
|
+
* - **No throwing.** `consume` runs on the prompt path of a live agent; a caller bug or a throwing
|
|
26
|
+
* `state()` must cost one skipped line, never the turn.
|
|
27
|
+
*
|
|
28
|
+
* The registry is a LIBRARY, not a DSH plugin: it registers no hook, mounts nothing and reads no
|
|
29
|
+
* configuration. The caller owns the lifecycle (which hook, which session, which text) and the
|
|
30
|
+
* registry owns only the once-per-session bookkeeping.
|
|
31
|
+
*
|
|
32
|
+
* @module session_notices
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* One notice a caller may want shown.
|
|
36
|
+
*
|
|
37
|
+
* `id` is the notice's identity for the once-per-session latch, so it must be namespaced by the
|
|
38
|
+
* plugin's own item/plugin prefix (`<plugin>:…`): two plugins sharing a process must not be able to
|
|
39
|
+
* silently suppress each other's line by picking the same id.
|
|
40
|
+
*/
|
|
41
|
+
export interface SessionNoticeSpec {
|
|
42
|
+
/** Stable id, namespaced `<plugin>:…`; the unit the once-per-session rule applies to. */
|
|
43
|
+
readonly id: string;
|
|
44
|
+
/** How loud the line is. Ordering only — the registry renders no label for it. */
|
|
45
|
+
readonly severity: 'info' | 'warning' | 'degraded';
|
|
46
|
+
/**
|
|
47
|
+
* Which failure shape the notice describes. The caller's `state()` decides WHETHER it matches; this
|
|
48
|
+
* field is carried so the caller's own wiring (and the runtime-deps helper) can map one spec onto
|
|
49
|
+
* several observed states without inventing a second type.
|
|
50
|
+
*/
|
|
51
|
+
readonly when: 'failed' | 'missing' | 'degraded';
|
|
52
|
+
/** The line itself, self-contained (no path into this repository's docs). Returned verbatim. */
|
|
53
|
+
readonly body: string;
|
|
54
|
+
}
|
|
55
|
+
/** The registry {@link createSessionNotices} returns. */
|
|
56
|
+
export interface SessionNoticeRegistry {
|
|
57
|
+
/**
|
|
58
|
+
* Register one notice with the caller's state predicate.
|
|
59
|
+
*
|
|
60
|
+
* `state()` is called on every `consume`, so it must be cheap, total and side-effect free; a throw
|
|
61
|
+
* is a warning and skips that notice for that call (it is NOT latched, so a later consume may still
|
|
62
|
+
* deliver it). Registration is guarded the same way: a malformed spec, or a second registration of
|
|
63
|
+
* the same id, is a warning and is ignored — never a throw, because registration happens on the
|
|
64
|
+
* plugin's mount path.
|
|
65
|
+
*/
|
|
66
|
+
register(spec: SessionNoticeSpec, state: () => boolean): void;
|
|
67
|
+
/**
|
|
68
|
+
* The notices that match RIGHT NOW and have not yet been delivered to `sessionId`, merged into one
|
|
69
|
+
* array of bodies: severity first (`degraded` > `warning` > `info`), then registration order.
|
|
70
|
+
*
|
|
71
|
+
* Every returned body is latched for that session id at the moment it is returned, so a notice is
|
|
72
|
+
* emitted at most once per session id even if `state()` flips back to `true` later. `consume` never
|
|
73
|
+
* throws: a throwing `state()` is a warning and that notice is skipped.
|
|
74
|
+
*
|
|
75
|
+
* `ownerOnly: true` is the caller's claim that this call is on behalf of the session that OWNS the
|
|
76
|
+
* plugin's delivery — the family's "the caller owns owner/worker filtering" rule. Only a claimed
|
|
77
|
+
* call is served, so a worker/other session stays quiet by not claiming; the caller that does want a
|
|
78
|
+
* line in every session simply claims from every session (the latch stays per session id either
|
|
79
|
+
* way). A call without the claim returns `[]` and latches nothing.
|
|
80
|
+
*/
|
|
81
|
+
consume(sessionId: string, options?: {
|
|
82
|
+
ownerOnly?: boolean;
|
|
83
|
+
}): string[];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Build one session-notice registry.
|
|
87
|
+
*
|
|
88
|
+
* @param logger - optional warning sink. Omitted means diagnostics are dropped (the registry still
|
|
89
|
+
* never throws); a throwing sink is swallowed for the same reason.
|
|
90
|
+
* @returns the registry; every member is total.
|
|
91
|
+
*/
|
|
92
|
+
export declare function createSessionNotices(logger?: {
|
|
93
|
+
warn(message: string): void;
|
|
94
|
+
}): SessionNoticeRegistry;
|
|
95
|
+
//# sourceMappingURL=session_notices.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session_notices.d.ts","sourceRoot":"","sources":["../src/session_notices.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,yFAAyF;IACzF,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,kFAAkF;IAClF,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,CAAA;IAClD;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,UAAU,CAAA;IAChD,gGAAgG;IAChG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED,yDAAyD;AACzD,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,IAAI,EAAE,iBAAiB,EAAE,KAAK,EAAE,MAAM,OAAO,GAAG,IAAI,CAAA;IAC7D;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,MAAM,EAAE,CAAA;CACxE;AA6CD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE;IAAE,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,qBAAqB,CA+FpG"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped notices — the generic, in-memory channel through which a plugin tells ONE agent
|
|
3
|
+
* session that one of its optional capabilities is not ready.
|
|
4
|
+
*
|
|
5
|
+
* The defect this exists for is a shape, not a package: a plugin provisions an optional capability
|
|
6
|
+
* (a shared runtime-dependency root, a model cache, an archive) in the BACKGROUND, mounts either way,
|
|
7
|
+
* and then has to say so in the owner's prompt — exactly once, with the caller's own wording, and
|
|
8
|
+
* without ever being able to refuse the mount or to leak a stale line after the capability healed.
|
|
9
|
+
* Every family plugin re-implemented that bookkeeping locally; this is the ONE implementation, so a
|
|
10
|
+
* fix here is a base release and not one rebuild per plugin.
|
|
11
|
+
*
|
|
12
|
+
* The registry deliberately knows nothing else:
|
|
13
|
+
*
|
|
14
|
+
* - **No wording.** {@link SessionNoticeSpec.body} is returned VERBATIM; the registry adds no prefix,
|
|
15
|
+
* no severity label and no punctuation. Base names no package, command or user-facing sentence.
|
|
16
|
+
* - **No state of its own.** `state()` is the caller's predicate and it is evaluated on EVERY
|
|
17
|
+
* {@link SessionNoticeRegistry.consume}: a healed dependency simply stops matching, without the
|
|
18
|
+
* registry having to know what "healed" means.
|
|
19
|
+
* - **No session identity.** The caller supplies the session id and decides which sessions may see a
|
|
20
|
+
* line ({@link SessionNoticeRegistry.consume}'s `ownerOnly`); the registry only remembers, per
|
|
21
|
+
* session id, which notice ids it has already delivered.
|
|
22
|
+
* - **No persistence.** Session ids live in a `Map` for the lifetime of the instance. Nothing here
|
|
23
|
+
* touches the filesystem, a database, `process.env` or any global: a notice is a prompt line, not
|
|
24
|
+
* a record.
|
|
25
|
+
* - **No throwing.** `consume` runs on the prompt path of a live agent; a caller bug or a throwing
|
|
26
|
+
* `state()` must cost one skipped line, never the turn.
|
|
27
|
+
*
|
|
28
|
+
* The registry is a LIBRARY, not a DSH plugin: it registers no hook, mounts nothing and reads no
|
|
29
|
+
* configuration. The caller owns the lifecycle (which hook, which session, which text) and the
|
|
30
|
+
* registry owns only the once-per-session bookkeeping.
|
|
31
|
+
*
|
|
32
|
+
* @module session_notices
|
|
33
|
+
*/
|
|
34
|
+
/** The severity order of {@link SessionNoticeSpec.severity}, loudest first. */
|
|
35
|
+
const SEVERITY_RANK = {
|
|
36
|
+
degraded: 0,
|
|
37
|
+
warning: 1,
|
|
38
|
+
info: 2,
|
|
39
|
+
};
|
|
40
|
+
/** The severities this build understands (a JS caller can hand over anything). */
|
|
41
|
+
const SEVERITIES = ['info', 'warning', 'degraded'];
|
|
42
|
+
/** The conditions this build understands. */
|
|
43
|
+
const CONDITIONS = ['failed', 'missing', 'degraded'];
|
|
44
|
+
/** `<plugin>:<rest>` — a notice id must be namespaced, see {@link SessionNoticeSpec.id}. */
|
|
45
|
+
const NAMESPACED_ID = /^[^\s:]+:\S+$/u;
|
|
46
|
+
/** Why a spec cannot be registered, or `undefined` when it can. Reads every field once. */
|
|
47
|
+
function specProblem(spec, state) {
|
|
48
|
+
if (typeof spec !== 'object' || spec === null)
|
|
49
|
+
return 'spec 必须是对象';
|
|
50
|
+
const candidate = spec;
|
|
51
|
+
if (typeof candidate.id !== 'string' || !NAMESPACED_ID.test(candidate.id)) {
|
|
52
|
+
return `id 必须以 "<plugin>:" 前缀化(收到 ${JSON.stringify(candidate.id)})`;
|
|
53
|
+
}
|
|
54
|
+
if (typeof candidate.severity !== 'string' || !SEVERITIES.includes(candidate.severity)) {
|
|
55
|
+
return `severity 只能是 ${SEVERITIES.join(' / ')}(收到 ${JSON.stringify(candidate.severity)})`;
|
|
56
|
+
}
|
|
57
|
+
if (typeof candidate.when !== 'string' || !CONDITIONS.includes(candidate.when)) {
|
|
58
|
+
return `when 只能是 ${CONDITIONS.join(' / ')}(收到 ${JSON.stringify(candidate.when)})`;
|
|
59
|
+
}
|
|
60
|
+
if (typeof candidate.body !== 'string' || candidate.body.trim() === '')
|
|
61
|
+
return 'body 必须是非空字符串';
|
|
62
|
+
if (typeof state !== 'function')
|
|
63
|
+
return 'state 必须是函数';
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Build one session-notice registry.
|
|
68
|
+
*
|
|
69
|
+
* @param logger - optional warning sink. Omitted means diagnostics are dropped (the registry still
|
|
70
|
+
* never throws); a throwing sink is swallowed for the same reason.
|
|
71
|
+
* @returns the registry; every member is total.
|
|
72
|
+
*/
|
|
73
|
+
export function createSessionNotices(logger) {
|
|
74
|
+
const entries = [];
|
|
75
|
+
/** notice id → the session ids it was already delivered to (the once-per-session latch). */
|
|
76
|
+
const delivered = new Map();
|
|
77
|
+
const warn = (message) => {
|
|
78
|
+
try {
|
|
79
|
+
logger?.warn(message);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// A logging failure must never break the prompt path this registry exists to serve.
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const reasonOf = (error) => {
|
|
86
|
+
try {
|
|
87
|
+
if (error instanceof Error && typeof error.message === 'string' && error.message !== '')
|
|
88
|
+
return error.message;
|
|
89
|
+
return String(error);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return 'unknown';
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const wasDelivered = (id, sessionId) => delivered.get(id)?.has(sessionId) === true;
|
|
96
|
+
const latch = (id, sessionId) => {
|
|
97
|
+
let sessions = delivered.get(id);
|
|
98
|
+
if (sessions === undefined) {
|
|
99
|
+
sessions = new Set();
|
|
100
|
+
delivered.set(id, sessions);
|
|
101
|
+
}
|
|
102
|
+
sessions.add(sessionId);
|
|
103
|
+
};
|
|
104
|
+
return {
|
|
105
|
+
register(spec, state) {
|
|
106
|
+
try {
|
|
107
|
+
const problem = specProblem(spec, state);
|
|
108
|
+
if (problem !== undefined) {
|
|
109
|
+
warn(`session-notices: 忽略一个非法 notice — ${problem}`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (entries.some(entry => entry.id === spec.id)) {
|
|
113
|
+
// "An id is emitted at most once" only has one meaning: the first registration owns the id.
|
|
114
|
+
warn(`session-notices: notice id 重复,保留先注册的那个 — ${spec.id}`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// Snapshot the delivery fields: the registry's decisions (ordering and the returned body) must
|
|
118
|
+
// not change because the caller later mutated the object it registered. `when` is validated
|
|
119
|
+
// above but belongs to the caller's own mapping — the registry matches on `state()` alone.
|
|
120
|
+
entries.push({
|
|
121
|
+
id: spec.id,
|
|
122
|
+
severity: spec.severity,
|
|
123
|
+
body: spec.body,
|
|
124
|
+
state,
|
|
125
|
+
order: entries.length,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
warn(`session-notices: 注册 notice 失败,已忽略(${reasonOf(error)})`);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
consume(sessionId, options) {
|
|
133
|
+
try {
|
|
134
|
+
if (typeof sessionId !== 'string' || sessionId === '') {
|
|
135
|
+
warn('session-notices: consume 需要一个非空的 session id');
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
if (options?.ownerOnly !== true)
|
|
139
|
+
return [];
|
|
140
|
+
const matched = [];
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
if (wasDelivered(entry.id, sessionId))
|
|
143
|
+
continue;
|
|
144
|
+
let active;
|
|
145
|
+
try {
|
|
146
|
+
active = entry.state();
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
// Skipped, NOT latched: the predicate failed, which says nothing about the notice itself.
|
|
150
|
+
warn(`session-notices: notice ${entry.id} 的 state() 抛错,本次跳过(${reasonOf(error)})`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (active !== true)
|
|
154
|
+
continue;
|
|
155
|
+
matched.push({ id: entry.id, body: entry.body, rank: SEVERITY_RANK[entry.severity], order: entry.order });
|
|
156
|
+
}
|
|
157
|
+
matched.sort((left, right) => left.rank - right.rank || left.order - right.order);
|
|
158
|
+
const lines = [];
|
|
159
|
+
for (const item of matched) {
|
|
160
|
+
lines.push(item.body);
|
|
161
|
+
latch(item.id, sessionId);
|
|
162
|
+
}
|
|
163
|
+
return lines;
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
warn(`session-notices: consume 失败,本次不返回任何 notice(${reasonOf(error)})`);
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=session_notices.js.map
|
package/lib/state.d.ts
CHANGED
|
@@ -58,7 +58,14 @@ export type StatusRead = {
|
|
|
58
58
|
} | {
|
|
59
59
|
readonly kind: 'unreadable';
|
|
60
60
|
};
|
|
61
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Read `status.json`; a newer schema yields `too-new`.
|
|
63
|
+
*
|
|
64
|
+
* A STRUCTURALLY damaged file (rows not an array, a row that is not an object, a row without
|
|
65
|
+
* `key`/`version`) is `unreadable`, never `ok` with fewer rows: a dropped row silently drops a
|
|
66
|
+
* reference, and a reader that treats the result as the complete reference list (GC) would then
|
|
67
|
+
* delete a live root. `unreadable` is the "cannot tell" the callers already know how to handle.
|
|
68
|
+
*/
|
|
62
69
|
export declare function readStatus(fs: ProvisionFs, home: string): Promise<StatusRead>;
|
|
63
70
|
/** Merge two row sets by `key × version`; newer `updated_at` wins, `items`/`plugins` are unioned. */
|
|
64
71
|
export declare function mergeRows(current: readonly StatusRow[], incoming: readonly StatusRow[]): readonly StatusRow[];
|
package/lib/state.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAc,WAAW,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEzG,2CAA2C;AAC3C,eAAO,MAAM,qBAAqB,IAAI,CAAA;AACtC,4CAA4C;AAC5C,eAAO,MAAM,qBAAqB,IAAI,CAAA;AAYtC,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,iEAAiE;AACjE,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,CAAA;IAChC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CACzG;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,IAAI,EAAE,SAAS,SAAS,EAAE,CAAA;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,EAAE,CAAC,CAAA;AAE1D,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GAClD;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE,CAAA;AAU5D,mEAAmE;AACnE,wBAAsB,UAAU,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAcnG;AAED,+BAA+B;AAC/B,wBAAsB,WAAW,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGjH;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,SAAS,EAAE,CAAA;CAAE,GAC5D;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;CAAE,CAAA;AAEnC
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"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"}
|
package/lib/state.js
CHANGED
|
@@ -52,7 +52,14 @@ export async function writeLayout(fs, home, layout, writtenBy) {
|
|
|
52
52
|
const file = { schemaVersion: LAYOUT_SCHEMA_VERSION, layout, writtenBy };
|
|
53
53
|
await fs.atomicWrite(layoutPath(home), new TextEncoder().encode(`${JSON.stringify(file, null, 2)}\n`));
|
|
54
54
|
}
|
|
55
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Read `status.json`; a newer schema yields `too-new`.
|
|
57
|
+
*
|
|
58
|
+
* A STRUCTURALLY damaged file (rows not an array, a row that is not an object, a row without
|
|
59
|
+
* `key`/`version`) is `unreadable`, never `ok` with fewer rows: a dropped row silently drops a
|
|
60
|
+
* reference, and a reader that treats the result as the complete reference list (GC) would then
|
|
61
|
+
* delete a live root. `unreadable` is the "cannot tell" the callers already know how to handle.
|
|
62
|
+
*/
|
|
56
63
|
export async function readStatus(fs, home) {
|
|
57
64
|
const path = statusPath(home);
|
|
58
65
|
if (!(await exists(fs, path)))
|
|
@@ -63,15 +70,17 @@ export async function readStatus(fs, home) {
|
|
|
63
70
|
const schemaVersion = typeof raw['schemaVersion'] === 'number' ? raw['schemaVersion'] : 0;
|
|
64
71
|
if (schemaVersion > STATUS_SCHEMA_VERSION)
|
|
65
72
|
return { kind: 'too-new' };
|
|
66
|
-
const rows =
|
|
73
|
+
const rows = raw['rows'];
|
|
74
|
+
if (!Array.isArray(rows))
|
|
75
|
+
return { kind: 'unreadable' };
|
|
67
76
|
const parsed = [];
|
|
68
77
|
for (const row of rows) {
|
|
69
78
|
if (!isRecord(row))
|
|
70
|
-
|
|
79
|
+
return { kind: 'unreadable' };
|
|
71
80
|
const key = str(row['key']);
|
|
72
81
|
const version = typeof row['version'] === 'string' ? row['version'] : undefined;
|
|
73
82
|
if (key === undefined || version === undefined)
|
|
74
|
-
|
|
83
|
+
return { kind: 'unreadable' };
|
|
75
84
|
const lastError = isRecord(row['last_error']) ? row['last_error'] : undefined;
|
|
76
85
|
const code = lastError === undefined ? undefined : str(lastError['code']);
|
|
77
86
|
const detail = lastError === undefined ? undefined : str(lastError['detail']);
|
|
@@ -165,6 +174,13 @@ export async function persistStatus(fs, lock, home, rows, logger) {
|
|
|
165
174
|
const existing = await readStatus(fs, home);
|
|
166
175
|
if (existing.kind === 'too-new')
|
|
167
176
|
return 'too-new';
|
|
177
|
+
// A structurally damaged file is NOT an empty one: overwriting it would drop the rows of every
|
|
178
|
+
// other plugin (and with them the references GC trusts), so it is left untouched and reported
|
|
179
|
+
// the same way an unreadable/corrupt file already was.
|
|
180
|
+
if (existing.kind === 'unreadable') {
|
|
181
|
+
logger.warn(`state: ${statusPath(home)} 结构不可读,本次不覆盖(保留原文,状态面按只读处理)`);
|
|
182
|
+
return 'read-only';
|
|
183
|
+
}
|
|
168
184
|
const base = existing.kind === 'ok' ? existing.rows : [];
|
|
169
185
|
const merged = mergeRows(base, rows);
|
|
170
186
|
const file = { schemaVersion: STATUS_SCHEMA_VERSION, rows: merged };
|
package/lib/types.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import type { ProvisionCode } from './errors.js';
|
|
6
6
|
/** The kinds the framework ships providers for; these names are reserved.
|
|
7
7
|
* @stable */
|
|
8
|
-
export type BuiltinKind = 'npm-package' | 'binary-archive' | 'model-cache';
|
|
8
|
+
export type BuiltinKind = 'npm-package' | 'binary-archive' | 'model-cache' | 'runtime-deps';
|
|
9
9
|
/** A kind is a namespaced string; third-party kinds must carry a prefix.
|
|
10
10
|
* @stable */
|
|
11
11
|
export type Kind = BuiltinKind | (string & {});
|
|
@@ -260,6 +260,13 @@ export interface ProvisionFs {
|
|
|
260
260
|
readlink(path: string): Promise<string | undefined>;
|
|
261
261
|
copyFile(from: string, to: string): Promise<void>;
|
|
262
262
|
chmod(path: string, mode: number): Promise<void>;
|
|
263
|
+
/** The canonical path with every symlink resolved.
|
|
264
|
+
*
|
|
265
|
+
* Optional, and absent means "this seam cannot tell": a provider that identifies a directory
|
|
266
|
+
* physically (the same host copy must name one shared root, however it was spelled) uses it when
|
|
267
|
+
* present and falls back to the spelled path when not. It is read-only, so an fs that cannot
|
|
268
|
+
* answer costs nothing but a weaker identity — never a failed install. */
|
|
269
|
+
realpath?(path: string): Promise<string>;
|
|
263
270
|
}
|
|
264
271
|
/** Control-plane mutual exclusion; the default is `<home>/.envinit/.lock`.
|
|
265
272
|
* @stable */
|
|
@@ -330,6 +337,12 @@ export interface Resolved {
|
|
|
330
337
|
readonly source: ResourceSource;
|
|
331
338
|
/** Environment additions for the handle (a managed binary's PATH). */
|
|
332
339
|
readonly env?: Readonly<Record<string, string>>;
|
|
340
|
+
/**
|
|
341
|
+
* `true` when `install()` found an equivalent root already published (another process wrote it
|
|
342
|
+
* while this one waited) and reused it: nothing was placed by this call, so the core reports the
|
|
343
|
+
* item as `present` instead of `installed`. Omitted = the provider placed what it returned.
|
|
344
|
+
*/
|
|
345
|
+
readonly reused?: boolean;
|
|
333
346
|
}
|
|
334
347
|
/** @stable */
|
|
335
348
|
export interface ProviderContext {
|