@avantf/dsh-plugin-base 0.5.2 → 0.6.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.
Files changed (118) hide show
  1. package/README.md +133 -20
  2. package/{dist → lib}/bootstrap.d.ts +1 -1
  3. package/{dist → lib}/bootstrap.d.ts.map +1 -1
  4. package/{dist → lib}/bootstrap.js +45 -8
  5. package/{dist → lib}/compat.d.ts +10 -0
  6. package/{dist → lib}/compat.d.ts.map +1 -1
  7. package/{dist → lib}/compat.js +30 -7
  8. package/{dist → lib}/conformance.d.ts.map +1 -1
  9. package/{dist → lib}/conformance.js +5 -0
  10. package/{dist → lib}/errors.d.ts +3 -1
  11. package/lib/errors.d.ts.map +1 -0
  12. package/{dist → lib}/fs.d.ts.map +1 -1
  13. package/{dist → lib}/fs.js +3 -0
  14. package/{dist → lib}/index.d.ts +10 -4
  15. package/lib/index.d.ts.map +1 -0
  16. package/{dist → lib}/index.js +18 -3
  17. package/{dist → lib}/interface.d.ts +165 -32
  18. package/lib/interface.d.ts.map +1 -0
  19. package/{dist → lib}/interface.js +56 -16
  20. package/{dist → lib}/internal.d.ts +2 -0
  21. package/{dist → lib}/internal.d.ts.map +1 -1
  22. package/{dist → lib}/internal.js +3 -0
  23. package/{dist → lib}/lint.d.ts.map +1 -1
  24. package/{dist → lib}/lint.js +42 -2
  25. package/{dist → lib}/providers/model.d.ts.map +1 -1
  26. package/{dist → lib}/providers/model.js +100 -17
  27. package/{dist → lib}/providers/npm.d.ts +10 -1
  28. package/lib/providers/npm.d.ts.map +1 -0
  29. package/{dist → lib}/providers/npm.js +6 -2
  30. package/lib/providers/runtime_deps.d.ts +184 -0
  31. package/lib/providers/runtime_deps.d.ts.map +1 -0
  32. package/lib/providers/runtime_deps.js +1214 -0
  33. package/{dist → lib}/provisioner.d.ts.map +1 -1
  34. package/{dist → lib}/provisioner.js +55 -33
  35. package/lib/runtime_deps_helper.d.ts +71 -0
  36. package/lib/runtime_deps_helper.d.ts.map +1 -0
  37. package/lib/runtime_deps_helper.js +161 -0
  38. package/lib/session_notices.d.ts +95 -0
  39. package/lib/session_notices.d.ts.map +1 -0
  40. package/lib/session_notices.js +172 -0
  41. package/{dist → lib}/state.d.ts +8 -1
  42. package/{dist → lib}/state.d.ts.map +1 -1
  43. package/{dist → lib}/state.js +20 -4
  44. package/{dist → lib}/types.d.ts +14 -1
  45. package/lib/types.d.ts.map +1 -0
  46. package/package.json +37 -33
  47. package/dist/cli.d.ts +0 -4
  48. package/dist/cli.d.ts.map +0 -1
  49. package/dist/cli.js +0 -37
  50. package/dist/errors.d.ts.map +0 -1
  51. package/dist/index.d.ts.map +0 -1
  52. package/dist/interface.d.ts.map +0 -1
  53. package/dist/providers/npm.d.ts.map +0 -1
  54. package/dist/types.d.ts.map +0 -1
  55. /package/{dist → lib}/artifact.d.ts +0 -0
  56. /package/{dist → lib}/artifact.d.ts.map +0 -0
  57. /package/{dist → lib}/artifact.js +0 -0
  58. /package/{dist → lib}/conformance.d.ts +0 -0
  59. /package/{dist → lib}/errors.js +0 -0
  60. /package/{dist → lib}/fs.d.ts +0 -0
  61. /package/{dist → lib}/integrity.d.ts +0 -0
  62. /package/{dist → lib}/integrity.d.ts.map +0 -0
  63. /package/{dist → lib}/integrity.js +0 -0
  64. /package/{dist → lib}/interface_gate.d.ts +0 -0
  65. /package/{dist → lib}/interface_gate.d.ts.map +0 -0
  66. /package/{dist → lib}/interface_gate.js +0 -0
  67. /package/{dist → lib}/kit/family.d.ts +0 -0
  68. /package/{dist → lib}/kit/family.d.ts.map +0 -0
  69. /package/{dist → lib}/kit/family.js +0 -0
  70. /package/{dist → lib}/kit/index.d.ts +0 -0
  71. /package/{dist → lib}/kit/index.d.ts.map +0 -0
  72. /package/{dist → lib}/kit/index.js +0 -0
  73. /package/{dist → lib}/kit/logger.d.ts +0 -0
  74. /package/{dist → lib}/kit/logger.d.ts.map +0 -0
  75. /package/{dist → lib}/kit/logger.js +0 -0
  76. /package/{dist → lib}/kit/prompt_files.d.ts +0 -0
  77. /package/{dist → lib}/kit/prompt_files.d.ts.map +0 -0
  78. /package/{dist → lib}/kit/prompt_files.js +0 -0
  79. /package/{dist → lib}/kit/typert.d.ts +0 -0
  80. /package/{dist → lib}/kit/typert.d.ts.map +0 -0
  81. /package/{dist → lib}/kit/typert.js +0 -0
  82. /package/{dist → lib}/kit/wellformed.d.ts +0 -0
  83. /package/{dist → lib}/kit/wellformed.d.ts.map +0 -0
  84. /package/{dist → lib}/kit/wellformed.js +0 -0
  85. /package/{dist → lib}/layout.d.ts +0 -0
  86. /package/{dist → lib}/layout.d.ts.map +0 -0
  87. /package/{dist → lib}/layout.js +0 -0
  88. /package/{dist → lib}/lint.d.ts +0 -0
  89. /package/{dist → lib}/lock.d.ts +0 -0
  90. /package/{dist → lib}/lock.d.ts.map +0 -0
  91. /package/{dist → lib}/lock.js +0 -0
  92. /package/{dist → lib}/manifest.d.ts +0 -0
  93. /package/{dist → lib}/manifest.d.ts.map +0 -0
  94. /package/{dist → lib}/manifest.js +0 -0
  95. /package/{dist → lib}/net.d.ts +0 -0
  96. /package/{dist → lib}/net.d.ts.map +0 -0
  97. /package/{dist → lib}/net.js +0 -0
  98. /package/{dist → lib}/package-name.d.ts +0 -0
  99. /package/{dist → lib}/package-name.d.ts.map +0 -0
  100. /package/{dist → lib}/package-name.js +0 -0
  101. /package/{dist → lib}/preset.d.ts +0 -0
  102. /package/{dist → lib}/preset.d.ts.map +0 -0
  103. /package/{dist → lib}/preset.js +0 -0
  104. /package/{dist → lib}/providers/archive.d.ts +0 -0
  105. /package/{dist → lib}/providers/archive.d.ts.map +0 -0
  106. /package/{dist → lib}/providers/archive.js +0 -0
  107. /package/{dist → lib}/providers/model.d.ts +0 -0
  108. /package/{dist → lib}/provisioner.d.ts +0 -0
  109. /package/{dist → lib}/semver.d.ts +0 -0
  110. /package/{dist → lib}/semver.d.ts.map +0 -0
  111. /package/{dist → lib}/semver.js +0 -0
  112. /package/{dist → lib}/tar.d.ts +0 -0
  113. /package/{dist → lib}/tar.d.ts.map +0 -0
  114. /package/{dist → lib}/tar.js +0 -0
  115. /package/{dist → lib}/types.js +0 -0
  116. /package/{dist → lib}/zip.d.ts +0 -0
  117. /package/{dist → lib}/zip.d.ts.map +0 -0
  118. /package/{dist → lib}/zip.js +0 -0
@@ -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,EAMnB,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,CAk+B1E"}
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"}
@@ -200,38 +200,48 @@ export function createProvisioner(options) {
200
200
  const listeners = new Set();
201
201
  /** Layout read-only state, decided once per process. */
202
202
  let readOnly;
203
- let homeChecked = false;
203
+ /**
204
+ * The in-flight (then settled) home verdict. Every caller awaits the SAME promise, so a
205
+ * concurrent `declare`/`ensure` cannot slip past the gate while `readLayout` is still deciding:
206
+ * the old boolean was set BEFORE the await, so a second caller returned early with `readOnly`
207
+ * still unset and `ensureOne` then wrote into a too-new/mismatched home. The promise is never
208
+ * re-created — the verdict is once-per-process, and a rejection is part of the verdict: the body
209
+ * already folds ordinary IO failures into a `layout/unreadable` read-only code, so a promise
210
+ * that still rejects is not something a silent retry is expected to fix, and later callers
211
+ * observe the same failure rather than racing the home again.
212
+ */
213
+ let homePromise;
204
214
  const pendingDeclared = [];
205
215
  /** Decide the home's layout once. */
206
- const checkHome = async () => {
207
- if (homeChecked)
208
- return;
209
- homeChecked = true;
210
- if (unsupported !== undefined && !unsupportedWarned) {
211
- unsupportedWarned = true;
212
- logger.warn(`unsupported-envinit: 本副本 ${FRAMEWORK_VERSION} 不满足插件声明的区间 "${String(options.envinitRange)}";跳过该清单、降级挂载`);
213
- }
214
- try {
215
- const result = await readLayout(fs, home, layout);
216
- if (result.kind === 'none') {
217
- await writeLayout(fs, home, layout, 'dsh-plugin-base');
216
+ const checkHome = () => {
217
+ homePromise ??= (async () => {
218
+ if (unsupported !== undefined && !unsupportedWarned) {
219
+ unsupportedWarned = true;
220
+ logger.warn(`unsupported-envinit: 本副本 ${FRAMEWORK_VERSION} 不满足插件声明的区间 "${String(options.envinitRange)}";跳过该清单、降级挂载`);
218
221
  }
219
- else if (result.kind === 'too-new') {
220
- readOnly = 'layout/too-new';
221
- logger.warn(`layout/too-new: ${home} 由更新的布局写入;本次只读不写`);
222
+ try {
223
+ const result = await readLayout(fs, home, layout);
224
+ if (result.kind === 'none') {
225
+ await writeLayout(fs, home, layout, 'dsh-plugin-base');
226
+ }
227
+ else if (result.kind === 'too-new') {
228
+ readOnly = 'layout/too-new';
229
+ logger.warn(`layout/too-new: ${home} 由更新的布局写入;本次只读不写`);
230
+ }
231
+ else if (result.kind === 'mismatch') {
232
+ readOnly = 'layout/mismatch';
233
+ logger.warn(`layout/mismatch: ${home} 的布局是 "${result.file.layout}",本次是 "${layout}";本次只读不写`);
234
+ }
222
235
  }
223
- else if (result.kind === 'mismatch') {
224
- readOnly = 'layout/mismatch';
225
- logger.warn(`layout/mismatch: ${home} 的布局是 "${result.file.layout}",本次是 "${layout}";本次只读不写`);
236
+ catch (error) {
237
+ // A read failure keeps the read-only CONSEQUENCE but gets its own code: every item's report
238
+ // would otherwise say `layout/too-new` — "a NEWER layout wrote this, leave it alone" — about a
239
+ // home whose layout nobody managed to read at all.
240
+ logger.warn(`layout: 无法读写 .layout.json(${reasonOf(error).message});按只读继续`);
241
+ readOnly = 'layout/unreadable';
226
242
  }
227
- }
228
- catch (error) {
229
- // A read failure keeps the read-only CONSEQUENCE but gets its own code: every item's report
230
- // would otherwise say `layout/too-new` — "a NEWER layout wrote this, leave it alone" — about a
231
- // home whose layout nobody managed to read at all.
232
- logger.warn(`layout: 无法读写 .layout.json(${reasonOf(error).message});按只读继续`);
233
- readOnly = 'layout/unreadable';
234
- }
243
+ })();
244
+ return homePromise;
235
245
  };
236
246
  /**
237
247
  * The failure a persisted row should carry: an explicit `lastError` if something ever sets one,
@@ -687,6 +697,8 @@ export function createProvisioner(options) {
687
697
  return settle(declared, started, 'skipped', 'policy/offline');
688
698
  const staging = stagingDir(home, item.id, (stagingSequence += 1));
689
699
  let published;
700
+ /** Set only when the core's own `publish()` placed the directory — see the catch below. */
701
+ let corePublished;
690
702
  const installCtx = {
691
703
  ...ctx,
692
704
  stage: async () => {
@@ -696,6 +708,7 @@ export function createProvisioner(options) {
696
708
  publish: async (dir, meta) => {
697
709
  const resolved = await publish(declared, dir, meta);
698
710
  published = resolved;
711
+ corePublished = resolved;
699
712
  return resolved;
700
713
  },
701
714
  ...(onProgress === undefined
@@ -731,18 +744,27 @@ export function createProvisioner(options) {
731
744
  },
732
745
  };
733
746
  putRow(declared, resolved.version, state, resolved.source);
734
- return entryOf(declared, { action: 'installed', source: resolved.source, started, version: resolved.version });
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 });
735
751
  }
736
752
  catch (error) {
737
753
  const { code, message } = reasonOf(error);
738
- // A published-but-unusable directory is quarantined, never deleted.
739
- if (published !== undefined) {
754
+ // A published-but-unusable directory is quarantined, never deleted — but ONLY a directory the
755
+ // core itself published through `ctx.publish()`. A provider that bypasses `publish()`
756
+ // (`model-cache`, whose `dir` is the shared `snapshots/<sha>` that `refs/<revision>` and every
757
+ // sibling item of the same repo+sha point at) manages its own storage: renaming that directory
758
+ // here would dangle the ref and take healthy sibling items down with the failed one. Such a
759
+ // provider withdraws its own completion marker on verify failure instead (see
760
+ // `providers/model.ts`), which keeps the next `probe` honest without touching shared state.
761
+ if (corePublished !== undefined) {
740
762
  try {
741
- await quarantineLocked(published.dir);
742
- logger.warn(`quarantined ${published.dir}: ${message}`);
763
+ await quarantineLocked(corePublished.dir);
764
+ logger.warn(`quarantined ${corePublished.dir}: ${message}`);
743
765
  }
744
766
  catch (renameError) {
745
- logger.warn(`could not quarantine ${published.dir}: ${reasonOf(renameError).message}`);
767
+ logger.warn(`could not quarantine ${corePublished.dir}: ${reasonOf(renameError).message}`);
746
768
  }
747
769
  }
748
770
  const state = { state: 'failed', code, detail: message };
@@ -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