@wildo-ai/wildo-module-client 1.1.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/LICENSE +34 -0
- package/dist/esm/.builder.pid +9 -0
- package/dist/esm/__tests__/lockfile-repair.service.test.d.ts +0 -0
- package/dist/esm/__tests__/lockfile-repair.service.test.d.ts.map +0 -0
- package/dist/esm/__tests__/lockfile-repair.service.test.js.map +0 -0
- package/dist/esm/__tests__/module-adopter.service.test.d.ts +0 -0
- package/dist/esm/__tests__/module-adopter.service.test.d.ts.map +0 -0
- package/dist/esm/__tests__/module-adopter.service.test.js.map +0 -0
- package/dist/esm/__tests__/module-packager.service.test.d.ts +0 -0
- package/dist/esm/__tests__/module-packager.service.test.d.ts.map +0 -0
- package/dist/esm/__tests__/module-packager.service.test.js.map +0 -0
- package/dist/esm/__tests__/provider-materialization-hook.test.d.ts +0 -0
- package/dist/esm/__tests__/provider-materialization-hook.test.d.ts.map +0 -0
- package/dist/esm/__tests__/provider-materialization-hook.test.js.map +0 -0
- package/dist/esm/__tests__/provider-sync-runner.test.d.ts +0 -0
- package/dist/esm/__tests__/provider-sync-runner.test.d.ts.map +0 -0
- package/dist/esm/__tests__/provider-sync-runner.test.js.map +0 -0
- package/dist/esm/__tests__/registry-lockfile.service.test.d.ts +0 -0
- package/dist/esm/__tests__/registry-lockfile.service.test.d.ts.map +0 -0
- package/dist/esm/__tests__/registry-lockfile.service.test.js.map +0 -0
- package/dist/esm/__tests__/registry-server-client.test.d.ts +0 -0
- package/dist/esm/__tests__/registry-server-client.test.d.ts.map +0 -0
- package/dist/esm/__tests__/registry-server-client.test.js.map +0 -0
- package/dist/esm/index.d.ts +18 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +18 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/services/_http-client.d.ts +44 -0
- package/dist/esm/services/_http-client.d.ts.map +1 -0
- package/dist/esm/services/_http-client.js +75 -0
- package/dist/esm/services/_http-client.js.map +1 -0
- package/dist/esm/services/_zod-issue-formatting.d.ts +12 -0
- package/dist/esm/services/_zod-issue-formatting.d.ts.map +1 -0
- package/dist/esm/services/_zod-issue-formatting.js +47 -0
- package/dist/esm/services/_zod-issue-formatting.js.map +1 -0
- package/dist/esm/services/index.d.ts +10 -0
- package/dist/esm/services/index.d.ts.map +1 -0
- package/dist/esm/services/index.js +10 -0
- package/dist/esm/services/index.js.map +1 -0
- package/dist/esm/services/lockfile-repair.service.d.ts +68 -0
- package/dist/esm/services/lockfile-repair.service.d.ts.map +1 -0
- package/dist/esm/services/lockfile-repair.service.js +160 -0
- package/dist/esm/services/lockfile-repair.service.js.map +1 -0
- package/dist/esm/services/module-adopter.service.d.ts +59 -0
- package/dist/esm/services/module-adopter.service.d.ts.map +1 -0
- package/dist/esm/services/module-adopter.service.js +126 -0
- package/dist/esm/services/module-adopter.service.js.map +1 -0
- package/dist/esm/services/module-packager.service.d.ts +105 -0
- package/dist/esm/services/module-packager.service.d.ts.map +1 -0
- package/dist/esm/services/module-packager.service.js +212 -0
- package/dist/esm/services/module-packager.service.js.map +1 -0
- package/dist/esm/services/provider-materialization-hook.d.ts +32 -0
- package/dist/esm/services/provider-materialization-hook.d.ts.map +1 -0
- package/dist/esm/services/provider-materialization-hook.js +33 -0
- package/dist/esm/services/provider-materialization-hook.js.map +1 -0
- package/dist/esm/services/provider-sync-runner.d.ts +87 -0
- package/dist/esm/services/provider-sync-runner.d.ts.map +1 -0
- package/dist/esm/services/provider-sync-runner.js +207 -0
- package/dist/esm/services/provider-sync-runner.js.map +1 -0
- package/dist/esm/services/registry-companion-client.d.ts +39 -0
- package/dist/esm/services/registry-companion-client.d.ts.map +1 -0
- package/dist/esm/services/registry-companion-client.js +109 -0
- package/dist/esm/services/registry-companion-client.js.map +1 -0
- package/dist/esm/services/registry-lockfile.service.d.ts +56 -0
- package/dist/esm/services/registry-lockfile.service.d.ts.map +1 -0
- package/dist/esm/services/registry-lockfile.service.js +99 -0
- package/dist/esm/services/registry-lockfile.service.js.map +1 -0
- package/dist/esm/services/registry-server-client.d.ts +94 -0
- package/dist/esm/services/registry-server-client.d.ts.map +1 -0
- package/dist/esm/services/registry-server-client.js +225 -0
- package/dist/esm/services/registry-server-client.js.map +1 -0
- package/dist/esm/services/types.d.ts +22 -0
- package/dist/esm/services/types.d.ts.map +1 -0
- package/dist/esm/services/types.js +16 -0
- package/dist/esm/services/types.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +51 -0
- package/src/__tests__/lockfile-repair.service.test.ts +106 -0
- package/src/__tests__/module-adopter.service.test.ts +96 -0
- package/src/__tests__/module-packager.service.test.ts +258 -0
- package/src/__tests__/provider-materialization-hook.test.ts +47 -0
- package/src/__tests__/provider-sync-runner.test.ts +113 -0
- package/src/__tests__/registry-lockfile.service.test.ts +134 -0
- package/src/__tests__/registry-server-client.test.ts +312 -0
- package/src/index.ts +18 -0
- package/src/services/_http-client.ts +91 -0
- package/src/services/_zod-issue-formatting.ts +50 -0
- package/src/services/index.ts +9 -0
- package/src/services/lockfile-repair.service.ts +215 -0
- package/src/services/module-adopter.service.ts +155 -0
- package/src/services/module-packager.service.ts +251 -0
- package/src/services/provider-materialization-hook.ts +54 -0
- package/src/services/provider-sync-runner.ts +298 -0
- package/src/services/registry-companion-client.ts +163 -0
- package/src/services/registry-lockfile.service.ts +122 -0
- package/src/services/registry-server-client.ts +334 -0
- package/src/services/types.ts +22 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `runProviderMaterializationHook` — Phase 0.5 dry-run hook.
|
|
3
|
+
*
|
|
4
|
+
* Phase 2 superseded this by `runProviderSync` (the real
|
|
5
|
+
* `wildo config sync --domain config` shell-out with classified
|
|
6
|
+
* failure surfaces). This file is kept as a thin
|
|
7
|
+
* `dryRun: true` forwarder so the Phase 0.5 lockfile-smoke
|
|
8
|
+
* command and any external caller continue to work unchanged.
|
|
9
|
+
*
|
|
10
|
+
* @deprecated Phase 2+ flows MUST call `runProviderSync` directly.
|
|
11
|
+
* This forwarder will be removed once Wonder Todos's
|
|
12
|
+
* `lockfile-smoke` command is rewritten in the Phase 4 CLI
|
|
13
|
+
* read-flow series.
|
|
14
|
+
*/
|
|
15
|
+
import { runProviderSync } from './provider-sync-runner.js';
|
|
16
|
+
export async function runProviderMaterializationHook(input) {
|
|
17
|
+
const result = await runProviderSync({
|
|
18
|
+
saasRoot: input.saasRoot,
|
|
19
|
+
reason: input.reason,
|
|
20
|
+
log: input.log,
|
|
21
|
+
dryRun: true,
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
invoked: true,
|
|
25
|
+
mode: result.mode,
|
|
26
|
+
performedFsWrite: false,
|
|
27
|
+
providerJsonRelativePaths: result.providerJsonRelativePaths,
|
|
28
|
+
providerSourceRelativePath: result.providerSourceRelativePath,
|
|
29
|
+
providerLockSectionPath: result.providerLockSectionPath,
|
|
30
|
+
reason: result.reason,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=provider-materialization-hook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-materialization-hook.js","sourceRoot":"","sources":["../../../../src/services/provider-materialization-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAmBzD,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAClD,KAAuC;IAEvC,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC;QACnC,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,GAAG,EAAE,KAAK,CAAC,GAAG;QACd,MAAM,EAAE,IAAI;KACb,CAAC,CAAC;IACH,OAAO;QACL,OAAO,EAAE,IAAI;QACb,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,gBAAgB,EAAE,KAAK;QACvB,yBAAyB,EAAE,MAAM,CAAC,yBAAyB;QAC3D,0BAA0B,EAAE,MAAM,CAAC,0BAA0B;QAC7D,uBAAuB,EAAE,MAAM,CAAC,uBAAuB;QACvD,MAAM,EAAE,MAAM,CAAC,MAAM;KACtB,CAAC;AACJ,CAAC","sourcesContent":["/**\n * `runProviderMaterializationHook` — Phase 0.5 dry-run hook.\n *\n * Phase 2 superseded this by `runProviderSync` (the real\n * `wildo config sync --domain config` shell-out with classified\n * failure surfaces). This file is kept as a thin\n * `dryRun: true` forwarder so the Phase 0.5 lockfile-smoke\n * command and any external caller continue to work unchanged.\n *\n * @deprecated Phase 2+ flows MUST call `runProviderSync` directly.\n * This forwarder will be removed once Wonder Todos's\n * `lockfile-smoke` command is rewritten in the Phase 4 CLI\n * read-flow series.\n */\n\nimport { PROVIDER_LOCK_SECTION_PATH } from '@wildo-ai/platform-config-lib';\nimport { runProviderSync } from './provider-sync-runner';\nimport type { RegistryClientLogger } from './types';\n\nexport interface ProviderMaterializationHookInput {\n saasRoot: string;\n reason: 'install' | 'update' | 'remove' | 'adopt' | 'smoke';\n log?: RegistryClientLogger;\n}\n\nexport interface ProviderMaterializationHookResult {\n invoked: true;\n mode: 'dry-run' | 'shell-out';\n performedFsWrite: false;\n providerJsonRelativePaths: readonly string[];\n providerSourceRelativePath: string;\n providerLockSectionPath: typeof PROVIDER_LOCK_SECTION_PATH;\n reason: ProviderMaterializationHookInput['reason'];\n}\n\nexport async function runProviderMaterializationHook(\n input: ProviderMaterializationHookInput,\n): Promise<ProviderMaterializationHookResult> {\n const result = await runProviderSync({\n saasRoot: input.saasRoot,\n reason: input.reason,\n log: input.log,\n dryRun: true,\n });\n return {\n invoked: true,\n mode: result.mode,\n performedFsWrite: false,\n providerJsonRelativePaths: result.providerJsonRelativePaths,\n providerSourceRelativePath: result.providerSourceRelativePath,\n providerLockSectionPath: result.providerLockSectionPath,\n reason: result.reason,\n };\n}\n"]}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `runProviderSync` — Phase 2 real shell-out to
|
|
3
|
+
* `wildo config sync --domain config`.
|
|
4
|
+
*
|
|
5
|
+
* Module-registry install/update/remove/adopt flows mutate app
|
|
6
|
+
* source. When that source changes provider implementations,
|
|
7
|
+
* provider companion exports, or provider scopes, the
|
|
8
|
+
* provider-runtime delivery layer must regenerate its outputs
|
|
9
|
+
* (KD-18). The canonical command is owned by the provider
|
|
10
|
+
* workstream — module-registry MUST delegate, not fork.
|
|
11
|
+
*
|
|
12
|
+
* Phase 0.5 / 1 shipped a no-op `runProviderMaterializationHook`
|
|
13
|
+
* that recorded intent. Phase 2 replaces it with a real
|
|
14
|
+
* `child_process.spawn`-based runner that:
|
|
15
|
+
* - resolves the `wildo` CLI binary from the saas root's
|
|
16
|
+
* `node_modules/.bin/wildo` (the workspace's own version);
|
|
17
|
+
* - shells `wildo config sync --domain config` in the saas root;
|
|
18
|
+
* - times out at the operator-configurable budget;
|
|
19
|
+
* - classifies stderr-tail patterns into the
|
|
20
|
+
* `RegistryProviderSyncFailedError.failureKind` discriminator
|
|
21
|
+
* (`companion-discovery` / `runtime-import-reachability` /
|
|
22
|
+
* `invalid-scope` / `unknown`) so callers report repairable
|
|
23
|
+
* failures with first-class UX rather than generic install
|
|
24
|
+
* noise (`module-registry-analysis.md` / Provider Migration
|
|
25
|
+
* WIP Seams §1).
|
|
26
|
+
*
|
|
27
|
+
* Hard contract (KD-18): the runner NEVER writes provider runtime
|
|
28
|
+
* artifacts itself. It only invokes the provider workstream's
|
|
29
|
+
* canonical command. Tests assert no provider artifact is touched
|
|
30
|
+
* by this code path.
|
|
31
|
+
*/
|
|
32
|
+
import { PROVIDER_LOCK_SECTION_PATH } from '@wildo-ai/platform-config-lib';
|
|
33
|
+
import type { RegistryClientLogger } from './types';
|
|
34
|
+
export interface ProviderSyncRunnerInput {
|
|
35
|
+
/** Absolute path to the SaaS application root. */
|
|
36
|
+
saasRoot: string;
|
|
37
|
+
/** Reason for the call — drives diagnostic copy. */
|
|
38
|
+
reason: 'install' | 'update' | 'remove' | 'adopt' | 'smoke';
|
|
39
|
+
/** Optional logger. Defaults to no-op. */
|
|
40
|
+
log?: RegistryClientLogger;
|
|
41
|
+
/**
|
|
42
|
+
* Override the wildo CLI binary path. Defaults to
|
|
43
|
+
* `<saasRoot>/node_modules/.bin/wildo`. Phase 2 tests override
|
|
44
|
+
* this to point at a stub script.
|
|
45
|
+
*/
|
|
46
|
+
wildoBinPath?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Maximum wall-time before SIGTERM. Default 120s — provider sync
|
|
49
|
+
* can be heavy on first run (artifact generation + cleanup),
|
|
50
|
+
* but every additional second is operator-visible.
|
|
51
|
+
*/
|
|
52
|
+
timeoutMs?: number;
|
|
53
|
+
/**
|
|
54
|
+
* If true, do NOT actually shell out — log the intent and
|
|
55
|
+
* resolve as `mode: 'dry-run'`. Used by the Phase 0.5 smoke
|
|
56
|
+
* command which intentionally cannot rely on a working
|
|
57
|
+
* `wildo` binary in the smoke sandbox.
|
|
58
|
+
*/
|
|
59
|
+
dryRun?: boolean;
|
|
60
|
+
}
|
|
61
|
+
export interface ProviderSyncRunnerResult {
|
|
62
|
+
invoked: true;
|
|
63
|
+
/**
|
|
64
|
+
* `'shell-out'` when the runner spawned `wildo config sync`.
|
|
65
|
+
* `'dry-run'` when `dryRun: true` was passed (smoke flow).
|
|
66
|
+
*/
|
|
67
|
+
mode: 'shell-out' | 'dry-run';
|
|
68
|
+
/** Exit code reported by the wildo binary (0 on success). */
|
|
69
|
+
exitCode: number | null;
|
|
70
|
+
/** Wall-clock duration (ms). */
|
|
71
|
+
durationMs: number;
|
|
72
|
+
/** Reason that was passed in. */
|
|
73
|
+
reason: ProviderSyncRunnerInput['reason'];
|
|
74
|
+
/** Provider-output paths the canonical sync would refresh. */
|
|
75
|
+
providerJsonRelativePaths: readonly string[];
|
|
76
|
+
/** Source module the Node-runtime consumes. */
|
|
77
|
+
providerSourceRelativePath: string;
|
|
78
|
+
/** Provider lock section the materializer owns end-to-end. */
|
|
79
|
+
providerLockSectionPath: typeof PROVIDER_LOCK_SECTION_PATH;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Run the canonical provider-sync command. Throws
|
|
83
|
+
* `RegistryProviderSyncFailedError` (with classified `failureKind`)
|
|
84
|
+
* when the spawned process exits non-zero or fails to start.
|
|
85
|
+
*/
|
|
86
|
+
export declare function runProviderSync(input: ProviderSyncRunnerInput): Promise<ProviderSyncRunnerResult>;
|
|
87
|
+
//# sourceMappingURL=provider-sync-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-sync-runner.d.ts","sourceRoot":"","sources":["../../../../src/services/provider-sync-runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAKH,OAAO,EACL,0BAA0B,EAI3B,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAIpD,MAAM,WAAW,uBAAuB;IACtC,kDAAkD;IAClD,QAAQ,EAAE,MAAM,CAAC;IACjB,oDAAoD;IACpD,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;IAC5D,0CAA0C;IAC1C,GAAG,CAAC,EAAE,oBAAoB,CAAC;IAC3B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,IAAI,CAAC;IACd;;;OAGG;IACH,IAAI,EAAE,WAAW,GAAG,SAAS,CAAC;IAC9B,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,gCAAgC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,iCAAiC;IACjC,MAAM,EAAE,uBAAuB,CAAC,QAAQ,CAAC,CAAC;IAC1C,8DAA8D;IAC9D,yBAAyB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C,+CAA+C;IAC/C,0BAA0B,EAAE,MAAM,CAAC;IACnC,8DAA8D;IAC9D,uBAAuB,EAAE,OAAO,0BAA0B,CAAC;CAC5D;AA+ID;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,uBAAuB,GAC7B,OAAO,CAAC,wBAAwB,CAAC,CAuDnC"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `runProviderSync` — Phase 2 real shell-out to
|
|
3
|
+
* `wildo config sync --domain config`.
|
|
4
|
+
*
|
|
5
|
+
* Module-registry install/update/remove/adopt flows mutate app
|
|
6
|
+
* source. When that source changes provider implementations,
|
|
7
|
+
* provider companion exports, or provider scopes, the
|
|
8
|
+
* provider-runtime delivery layer must regenerate its outputs
|
|
9
|
+
* (KD-18). The canonical command is owned by the provider
|
|
10
|
+
* workstream — module-registry MUST delegate, not fork.
|
|
11
|
+
*
|
|
12
|
+
* Phase 0.5 / 1 shipped a no-op `runProviderMaterializationHook`
|
|
13
|
+
* that recorded intent. Phase 2 replaces it with a real
|
|
14
|
+
* `child_process.spawn`-based runner that:
|
|
15
|
+
* - resolves the `wildo` CLI binary from the saas root's
|
|
16
|
+
* `node_modules/.bin/wildo` (the workspace's own version);
|
|
17
|
+
* - shells `wildo config sync --domain config` in the saas root;
|
|
18
|
+
* - times out at the operator-configurable budget;
|
|
19
|
+
* - classifies stderr-tail patterns into the
|
|
20
|
+
* `RegistryProviderSyncFailedError.failureKind` discriminator
|
|
21
|
+
* (`companion-discovery` / `runtime-import-reachability` /
|
|
22
|
+
* `invalid-scope` / `unknown`) so callers report repairable
|
|
23
|
+
* failures with first-class UX rather than generic install
|
|
24
|
+
* noise (`module-registry-analysis.md` / Provider Migration
|
|
25
|
+
* WIP Seams §1).
|
|
26
|
+
*
|
|
27
|
+
* Hard contract (KD-18): the runner NEVER writes provider runtime
|
|
28
|
+
* artifacts itself. It only invokes the provider workstream's
|
|
29
|
+
* canonical command. Tests assert no provider artifact is touched
|
|
30
|
+
* by this code path.
|
|
31
|
+
*/
|
|
32
|
+
import { spawn } from 'node:child_process';
|
|
33
|
+
import { promises as fs } from 'node:fs';
|
|
34
|
+
import * as path from 'node:path';
|
|
35
|
+
import { PROVIDER_LOCK_SECTION_PATH, PROVIDER_RUNTIME_ARTIFACT_RELATIVE_PATHS, PROVIDER_RUNTIME_GENERATED_MODULE_RELATIVE_PATH, RegistryProviderSyncFailedError, } from '@wildo-ai/platform-config-lib';
|
|
36
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
37
|
+
/**
|
|
38
|
+
* Provider artifact paths sourced from the canonical
|
|
39
|
+
* `PROVIDER_RUNTIME_ARTIFACT_RELATIVE_PATHS` constant in
|
|
40
|
+
* `platform-config-lib`. Refactored on 2026-05-06 to import the
|
|
41
|
+
* single source of truth instead of duplicating the literal paths
|
|
42
|
+
* here — the artifacts moved from `.wildo-saas/providers/` to
|
|
43
|
+
* `.wildo-saas/generated/providers/` and any duplication would
|
|
44
|
+
* silently drift.
|
|
45
|
+
*/
|
|
46
|
+
const PROVIDER_JSON_RELATIVE_PATHS = Object.values(PROVIDER_RUNTIME_ARTIFACT_RELATIVE_PATHS);
|
|
47
|
+
const PROVIDER_SOURCE_RELATIVE_PATH = PROVIDER_RUNTIME_GENERATED_MODULE_RELATIVE_PATH;
|
|
48
|
+
/**
|
|
49
|
+
* Classify a `wildo config sync` failure based on its stderr tail.
|
|
50
|
+
* The matchers mirror the failure modes the provider workstream
|
|
51
|
+
* surfaces today; new modes get a new branch + a new
|
|
52
|
+
* discriminator value (out of `RegistryProviderSyncFailedError`).
|
|
53
|
+
*/
|
|
54
|
+
function classifyStderr(stderr) {
|
|
55
|
+
const tail = stderr.slice(-2000);
|
|
56
|
+
// Companion-export discovery failure — the provider sync walks
|
|
57
|
+
// each owner package's `./companion` export to discover provider
|
|
58
|
+
// contributions. A package whose export contract is broken
|
|
59
|
+
// surfaces a "companion export" / "DiscoveredProviderCompanionContribution"
|
|
60
|
+
// error.
|
|
61
|
+
if (/companion[- ]export|DiscoveredProviderCompanionContribution|companion-discovery/i.test(tail)) {
|
|
62
|
+
return 'companion-discovery';
|
|
63
|
+
}
|
|
64
|
+
// Runtime-import reachability — the materializer can't resolve a
|
|
65
|
+
// contributed `runtimeImport` against the owning runtime package
|
|
66
|
+
// graph. Typical messages mention `runtimeImport`, `Cannot find
|
|
67
|
+
// module`, or `MODULE_NOT_FOUND`.
|
|
68
|
+
if (/runtimeImport|MODULE_NOT_FOUND|Cannot find module|cannot resolve/i.test(tail)) {
|
|
69
|
+
return 'runtime-import-reachability';
|
|
70
|
+
}
|
|
71
|
+
// Scope validation — `defineSaaSProviders({ scopes })` rejects
|
|
72
|
+
// malformed scope inputs ahead of materialization.
|
|
73
|
+
if (/defineSaaSProviders|invalid[- ]scope|providers\.scopes/i.test(tail)) {
|
|
74
|
+
return 'invalid-scope';
|
|
75
|
+
}
|
|
76
|
+
return 'unknown';
|
|
77
|
+
}
|
|
78
|
+
async function resolveWildoBin(input) {
|
|
79
|
+
if (input.wildoBinPath)
|
|
80
|
+
return input.wildoBinPath;
|
|
81
|
+
const candidate = path.join(input.saasRoot, 'node_modules', '.bin', 'wildo');
|
|
82
|
+
try {
|
|
83
|
+
await fs.access(candidate);
|
|
84
|
+
return candidate;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Fall back to PATH-resolved `wildo`. spawn will raise ENOENT
|
|
88
|
+
// if it's not on PATH either, surfaced as
|
|
89
|
+
// `RegistryProviderSyncFailedError(failureKind: 'unknown')`.
|
|
90
|
+
return 'wildo';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Grace window between SIGTERM and SIGKILL. A well-behaved
|
|
95
|
+
* `wildo config sync` should react to SIGTERM in well under a
|
|
96
|
+
* second — the grace exists for tools that need to flush a buffer
|
|
97
|
+
* or release a file lock. After the window we escalate to SIGKILL
|
|
98
|
+
* so a hung child cannot pin the parent forever.
|
|
99
|
+
*/
|
|
100
|
+
const SIGKILL_GRACE_MS = 5_000;
|
|
101
|
+
function runWildoConfigSync(binPath, cwd, timeoutMs) {
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
const startedAt = Date.now();
|
|
104
|
+
const child = spawn(binPath, ['config', 'sync', '--domain', 'config'], {
|
|
105
|
+
cwd,
|
|
106
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
107
|
+
});
|
|
108
|
+
let stderr = '';
|
|
109
|
+
let signaled = false;
|
|
110
|
+
let killTimer;
|
|
111
|
+
child.stderr?.on('data', (b) => {
|
|
112
|
+
// Cap captured stderr so a runaway sync can't OOM the parent.
|
|
113
|
+
stderr += b.toString();
|
|
114
|
+
if (stderr.length > 16_384)
|
|
115
|
+
stderr = stderr.slice(-16_384);
|
|
116
|
+
});
|
|
117
|
+
const termTimer = setTimeout(() => {
|
|
118
|
+
// Soft-kill first; record so the caller can synthesize a
|
|
119
|
+
// diagnostic message that distinguishes a TERM-induced exit
|
|
120
|
+
// from a normal one.
|
|
121
|
+
signaled = true;
|
|
122
|
+
stderr += `\n[provider-sync] timeout ${timeoutMs}ms exceeded — sent SIGTERM.`;
|
|
123
|
+
child.kill('SIGTERM');
|
|
124
|
+
// Escalate to SIGKILL if the child doesn't react within the
|
|
125
|
+
// grace window. Without this a child stuck in a syscall (or
|
|
126
|
+
// deliberately ignoring SIGTERM) would leave the parent
|
|
127
|
+
// blocked on the spawn promise forever.
|
|
128
|
+
killTimer = setTimeout(() => {
|
|
129
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
130
|
+
stderr += `\n[provider-sync] SIGTERM grace ${SIGKILL_GRACE_MS}ms exceeded — escalating to SIGKILL.`;
|
|
131
|
+
try {
|
|
132
|
+
child.kill('SIGKILL');
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
/* swallow — best-effort escalation */
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}, SIGKILL_GRACE_MS);
|
|
139
|
+
}, timeoutMs);
|
|
140
|
+
child.on('exit', (code) => {
|
|
141
|
+
clearTimeout(termTimer);
|
|
142
|
+
if (killTimer)
|
|
143
|
+
clearTimeout(killTimer);
|
|
144
|
+
resolve({
|
|
145
|
+
exitCode: code,
|
|
146
|
+
stderr,
|
|
147
|
+
durationMs: Date.now() - startedAt,
|
|
148
|
+
signaled,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
child.on('error', (err) => {
|
|
152
|
+
clearTimeout(termTimer);
|
|
153
|
+
if (killTimer)
|
|
154
|
+
clearTimeout(killTimer);
|
|
155
|
+
resolve({
|
|
156
|
+
exitCode: null,
|
|
157
|
+
stderr: stderr + `\n[spawn error] ${err.message}`,
|
|
158
|
+
durationMs: Date.now() - startedAt,
|
|
159
|
+
signaled,
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Run the canonical provider-sync command. Throws
|
|
166
|
+
* `RegistryProviderSyncFailedError` (with classified `failureKind`)
|
|
167
|
+
* when the spawned process exits non-zero or fails to start.
|
|
168
|
+
*/
|
|
169
|
+
export async function runProviderSync(input) {
|
|
170
|
+
const log = input.log;
|
|
171
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
172
|
+
const baseResult = {
|
|
173
|
+
invoked: true,
|
|
174
|
+
reason: input.reason,
|
|
175
|
+
providerJsonRelativePaths: PROVIDER_JSON_RELATIVE_PATHS,
|
|
176
|
+
providerSourceRelativePath: PROVIDER_SOURCE_RELATIVE_PATH,
|
|
177
|
+
providerLockSectionPath: PROVIDER_LOCK_SECTION_PATH,
|
|
178
|
+
};
|
|
179
|
+
if (input.dryRun) {
|
|
180
|
+
log?.info?.(`[registry] post-${input.reason} provider sync (dry-run): would shell ` +
|
|
181
|
+
`\`wildo config sync --domain config\` in ${input.saasRoot}.`);
|
|
182
|
+
return { ...baseResult, mode: 'dry-run', exitCode: 0, durationMs: 0 };
|
|
183
|
+
}
|
|
184
|
+
const binPath = await resolveWildoBin(input);
|
|
185
|
+
log?.info?.(`[registry] post-${input.reason} provider sync: spawning ` +
|
|
186
|
+
`${binPath} config sync --domain config (cwd=${input.saasRoot}, timeout=${timeoutMs}ms)`);
|
|
187
|
+
const { exitCode, stderr, durationMs, signaled } = await runWildoConfigSync(binPath, input.saasRoot, timeoutMs);
|
|
188
|
+
if (exitCode === 0) {
|
|
189
|
+
log?.success?.(`[registry] provider sync succeeded in ${durationMs}ms.`);
|
|
190
|
+
return { ...baseResult, mode: 'shell-out', exitCode, durationMs };
|
|
191
|
+
}
|
|
192
|
+
// H5: distinguish a timeout-signaled exit from a generic non-zero
|
|
193
|
+
// exit. `classifyStderr` runs regex against the appended
|
|
194
|
+
// `[provider-sync] timeout ... SIGTERM/SIGKILL` lines but those
|
|
195
|
+
// tails match no failure-mode regex — so prior to this branch
|
|
196
|
+
// operators saw `failureKind: 'unknown'` for a hard timeout. The
|
|
197
|
+
// typed error now carries the discriminator + a hint to either
|
|
198
|
+
// raise the timeout or investigate the wedge.
|
|
199
|
+
const failureKind = signaled ? 'timeout' : classifyStderr(stderr);
|
|
200
|
+
log?.error?.(`[registry] provider sync failed (${failureKind}, exit=${exitCode}, ${durationMs}ms).`);
|
|
201
|
+
const messageSuffix = failureKind === 'timeout'
|
|
202
|
+
? `Wall-clock exceeded ${timeoutMs}ms; child was sent SIGTERM (then SIGKILL after grace). ` +
|
|
203
|
+
'Either raise `timeoutMs` for legitimately heavy syncs or investigate why the sync wedged.'
|
|
204
|
+
: 'Run `wildo config sync --domain config` manually to inspect.';
|
|
205
|
+
throw new RegistryProviderSyncFailedError(`Post-${input.reason} provider sync failed (${failureKind}). ${messageSuffix}`, failureKind, stderr.slice(-1024));
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=provider-sync-runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-sync-runner.js","sourceRoot":"","sources":["../../../../src/services/provider-sync-runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EACL,0BAA0B,EAC1B,wCAAwC,EACxC,+CAA+C,EAC/C,+BAA+B,GAChC,MAAM,+BAA+B,CAAC;AAGvC,MAAM,kBAAkB,GAAG,OAAO,CAAC;AAmDnC;;;;;;;;GAQG;AACH,MAAM,4BAA4B,GAAG,MAAM,CAAC,MAAM,CAChD,wCAAwC,CACpB,CAAC;AAEvB,MAAM,6BAA6B,GAAG,+CAA+C,CAAC;AAEtF;;;;;GAKG;AACH,SAAS,cAAc,CACrB,MAAc;IAEd,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC;IACjC,+DAA+D;IAC/D,iEAAiE;IACjE,2DAA2D;IAC3D,4EAA4E;IAC5E,SAAS;IACT,IACE,kFAAkF,CAAC,IAAI,CAAC,IAAI,CAAC,EAC7F,CAAC;QACD,OAAO,qBAAqB,CAAC;IAC/B,CAAC;IACD,iEAAiE;IACjE,iEAAiE;IACjE,gEAAgE;IAChE,kCAAkC;IAClC,IACE,mEAAmE,CAAC,IAAI,CAAC,IAAI,CAAC,EAC9E,CAAC;QACD,OAAO,6BAA6B,CAAC;IACvC,CAAC;IACD,+DAA+D;IAC/D,mDAAmD;IACnD,IACE,yDAAyD,CAAC,IAAI,CAAC,IAAI,CAAC,EACpE,CAAC;QACD,OAAO,eAAe,CAAC;IACzB,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,KAAK,UAAU,eAAe,CAAC,KAA8B;IAC3D,IAAI,KAAK,CAAC,YAAY;QAAE,OAAO,KAAK,CAAC,YAAY,CAAC;IAClD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,cAAc,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7E,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAC3B,OAAO,SAAS,CAAC;IACnB,CAAC;IAAC,MAAM,CAAC;QACP,8DAA8D;QAC9D,0CAA0C;QAC1C,6DAA6D;QAC7D,OAAO,OAAO,CAAC;IACjB,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,gBAAgB,GAAG,KAAK,CAAC;AAE/B,SAAS,kBAAkB,CACzB,OAAe,EACf,GAAW,EACX,SAAiB;IAEjB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE;YACrE,GAAG;YACH,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;SAClC,CAAC,CAAC;QACH,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,IAAI,SAAqC,CAAC;QAC1C,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAS,EAAE,EAAE;YACrC,8DAA8D;YAC9D,MAAM,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;YACvB,IAAI,MAAM,CAAC,MAAM,GAAG,MAAM;gBAAE,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC;QAC7D,CAAC,CAAC,CAAC;QACH,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE;YAChC,yDAAyD;YACzD,4DAA4D;YAC5D,qBAAqB;YACrB,QAAQ,GAAG,IAAI,CAAC;YAChB,MAAM,IAAI,6BAA6B,SAAS,6BAA6B,CAAC;YAC9E,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACtB,4DAA4D;YAC5D,4DAA4D;YAC5D,wDAAwD;YACxD,wCAAwC;YACxC,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC1B,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;oBACzD,MAAM,IAAI,mCAAmC,gBAAgB,sCAAsC,CAAC;oBACpG,IAAI,CAAC;wBACH,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;oBACxB,CAAC;oBAAC,MAAM,CAAC;wBACP,sCAAsC;oBACxC,CAAC;gBACH,CAAC;YACH,CAAC,EAAE,gBAAgB,CAAC,CAAC;QACvB,CAAC,EAAE,SAAS,CAAC,CAAC;QACd,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;YACxB,YAAY,CAAC,SAAS,CAAC,CAAC;YACxB,IAAI,SAAS;gBAAE,YAAY,CAAC,SAAS,CAAC,CAAC;YACvC,OAAO,CAAC;gBACN,QAAQ,EAAE,IAAI;gBACd,MAAM;gBACN,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;gBAClC,QAAQ;aACT,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACxB,YAAY,CAAC,SAAS,CAAC,CAAC;YACxB,IAAI,SAAS;gBAAE,YAAY,CAAC,SAAS,CAAC,CAAC;YACvC,OAAO,CAAC;gBACN,QAAQ,EAAE,IAAI;gBACd,MAAM,EAAE,MAAM,GAAG,mBAAmB,GAAG,CAAC,OAAO,EAAE;gBACjD,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;gBAClC,QAAQ;aACT,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,KAA8B;IAE9B,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC;IACtB,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,IAAI,kBAAkB,CAAC;IACxD,MAAM,UAAU,GAAG;QACjB,OAAO,EAAE,IAAa;QACtB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,yBAAyB,EAAE,4BAA4B;QACvD,0BAA0B,EAAE,6BAA6B;QACzD,uBAAuB,EAAE,0BAA0B;KACpD,CAAC;IAEF,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QACjB,GAAG,EAAE,IAAI,EAAE,CACT,mBAAmB,KAAK,CAAC,MAAM,wCAAwC;YACvE,4CAA4C,KAAK,CAAC,QAAQ,GAAG,CAC9D,CAAC;QACF,OAAO,EAAE,GAAG,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;IACxE,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,KAAK,CAAC,CAAC;IAC7C,GAAG,EAAE,IAAI,EAAE,CACT,mBAAmB,KAAK,CAAC,MAAM,2BAA2B;QAC1D,GAAG,OAAO,qCAAqC,KAAK,CAAC,QAAQ,aAAa,SAAS,KAAK,CACzF,CAAC;IACF,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,MAAM,kBAAkB,CACzE,OAAO,EACP,KAAK,CAAC,QAAQ,EACd,SAAS,CACV,CAAC;IAEF,IAAI,QAAQ,KAAK,CAAC,EAAE,CAAC;QACnB,GAAG,EAAE,OAAO,EAAE,CAAC,yCAAyC,UAAU,KAAK,CAAC,CAAC;QACzE,OAAO,EAAE,GAAG,UAAU,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;IACpE,CAAC;IAED,kEAAkE;IAClE,yDAAyD;IACzD,gEAAgE;IAChE,8DAA8D;IAC9D,iEAAiE;IACjE,+DAA+D;IAC/D,8CAA8C;IAC9C,MAAM,WAAW,GAAG,QAAQ,CAAC,CAAC,CAAE,SAAmB,CAAC,CAAC,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;IAC7E,GAAG,EAAE,KAAK,EAAE,CACV,oCAAoC,WAAW,UAAU,QAAQ,KAAK,UAAU,MAAM,CACvF,CAAC;IACF,MAAM,aAAa,GAAG,WAAW,KAAK,SAAS;QAC7C,CAAC,CAAC,uBAAuB,SAAS,yDAAyD;YACzF,2FAA2F;QAC7F,CAAC,CAAC,8DAA8D,CAAC;IACnE,MAAM,IAAI,+BAA+B,CACvC,QAAQ,KAAK,CAAC,MAAM,0BAA0B,WAAW,MAAM,aAAa,EAAE,EAC9E,WAAW,EACX,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CACpB,CAAC;AACJ,CAAC","sourcesContent":["/**\n * `runProviderSync` — Phase 2 real shell-out to\n * `wildo config sync --domain config`.\n *\n * Module-registry install/update/remove/adopt flows mutate app\n * source. When that source changes provider implementations,\n * provider companion exports, or provider scopes, the\n * provider-runtime delivery layer must regenerate its outputs\n * (KD-18). The canonical command is owned by the provider\n * workstream — module-registry MUST delegate, not fork.\n *\n * Phase 0.5 / 1 shipped a no-op `runProviderMaterializationHook`\n * that recorded intent. Phase 2 replaces it with a real\n * `child_process.spawn`-based runner that:\n * - resolves the `wildo` CLI binary from the saas root's\n * `node_modules/.bin/wildo` (the workspace's own version);\n * - shells `wildo config sync --domain config` in the saas root;\n * - times out at the operator-configurable budget;\n * - classifies stderr-tail patterns into the\n * `RegistryProviderSyncFailedError.failureKind` discriminator\n * (`companion-discovery` / `runtime-import-reachability` /\n * `invalid-scope` / `unknown`) so callers report repairable\n * failures with first-class UX rather than generic install\n * noise (`module-registry-analysis.md` / Provider Migration\n * WIP Seams §1).\n *\n * Hard contract (KD-18): the runner NEVER writes provider runtime\n * artifacts itself. It only invokes the provider workstream's\n * canonical command. Tests assert no provider artifact is touched\n * by this code path.\n */\n\nimport { spawn } from 'node:child_process';\nimport { promises as fs } from 'node:fs';\nimport * as path from 'node:path';\nimport {\n PROVIDER_LOCK_SECTION_PATH,\n PROVIDER_RUNTIME_ARTIFACT_RELATIVE_PATHS,\n PROVIDER_RUNTIME_GENERATED_MODULE_RELATIVE_PATH,\n RegistryProviderSyncFailedError,\n} from '@wildo-ai/platform-config-lib';\nimport type { RegistryClientLogger } from './types';\n\nconst DEFAULT_TIMEOUT_MS = 120_000;\n\nexport interface ProviderSyncRunnerInput {\n /** Absolute path to the SaaS application root. */\n saasRoot: string;\n /** Reason for the call — drives diagnostic copy. */\n reason: 'install' | 'update' | 'remove' | 'adopt' | 'smoke';\n /** Optional logger. Defaults to no-op. */\n log?: RegistryClientLogger;\n /**\n * Override the wildo CLI binary path. Defaults to\n * `<saasRoot>/node_modules/.bin/wildo`. Phase 2 tests override\n * this to point at a stub script.\n */\n wildoBinPath?: string;\n /**\n * Maximum wall-time before SIGTERM. Default 120s — provider sync\n * can be heavy on first run (artifact generation + cleanup),\n * but every additional second is operator-visible.\n */\n timeoutMs?: number;\n /**\n * If true, do NOT actually shell out — log the intent and\n * resolve as `mode: 'dry-run'`. Used by the Phase 0.5 smoke\n * command which intentionally cannot rely on a working\n * `wildo` binary in the smoke sandbox.\n */\n dryRun?: boolean;\n}\n\nexport interface ProviderSyncRunnerResult {\n invoked: true;\n /**\n * `'shell-out'` when the runner spawned `wildo config sync`.\n * `'dry-run'` when `dryRun: true` was passed (smoke flow).\n */\n mode: 'shell-out' | 'dry-run';\n /** Exit code reported by the wildo binary (0 on success). */\n exitCode: number | null;\n /** Wall-clock duration (ms). */\n durationMs: number;\n /** Reason that was passed in. */\n reason: ProviderSyncRunnerInput['reason'];\n /** Provider-output paths the canonical sync would refresh. */\n providerJsonRelativePaths: readonly string[];\n /** Source module the Node-runtime consumes. */\n providerSourceRelativePath: string;\n /** Provider lock section the materializer owns end-to-end. */\n providerLockSectionPath: typeof PROVIDER_LOCK_SECTION_PATH;\n}\n\n/**\n * Provider artifact paths sourced from the canonical\n * `PROVIDER_RUNTIME_ARTIFACT_RELATIVE_PATHS` constant in\n * `platform-config-lib`. Refactored on 2026-05-06 to import the\n * single source of truth instead of duplicating the literal paths\n * here — the artifacts moved from `.wildo-saas/providers/` to\n * `.wildo-saas/generated/providers/` and any duplication would\n * silently drift.\n */\nconst PROVIDER_JSON_RELATIVE_PATHS = Object.values(\n PROVIDER_RUNTIME_ARTIFACT_RELATIVE_PATHS,\n) as readonly string[];\n\nconst PROVIDER_SOURCE_RELATIVE_PATH = PROVIDER_RUNTIME_GENERATED_MODULE_RELATIVE_PATH;\n\n/**\n * Classify a `wildo config sync` failure based on its stderr tail.\n * The matchers mirror the failure modes the provider workstream\n * surfaces today; new modes get a new branch + a new\n * discriminator value (out of `RegistryProviderSyncFailedError`).\n */\nfunction classifyStderr(\n stderr: string,\n): 'companion-discovery' | 'runtime-import-reachability' | 'invalid-scope' | 'unknown' {\n const tail = stderr.slice(-2000);\n // Companion-export discovery failure — the provider sync walks\n // each owner package's `./companion` export to discover provider\n // contributions. A package whose export contract is broken\n // surfaces a \"companion export\" / \"DiscoveredProviderCompanionContribution\"\n // error.\n if (\n /companion[- ]export|DiscoveredProviderCompanionContribution|companion-discovery/i.test(tail)\n ) {\n return 'companion-discovery';\n }\n // Runtime-import reachability — the materializer can't resolve a\n // contributed `runtimeImport` against the owning runtime package\n // graph. Typical messages mention `runtimeImport`, `Cannot find\n // module`, or `MODULE_NOT_FOUND`.\n if (\n /runtimeImport|MODULE_NOT_FOUND|Cannot find module|cannot resolve/i.test(tail)\n ) {\n return 'runtime-import-reachability';\n }\n // Scope validation — `defineSaaSProviders({ scopes })` rejects\n // malformed scope inputs ahead of materialization.\n if (\n /defineSaaSProviders|invalid[- ]scope|providers\\.scopes/i.test(tail)\n ) {\n return 'invalid-scope';\n }\n return 'unknown';\n}\n\nasync function resolveWildoBin(input: ProviderSyncRunnerInput): Promise<string> {\n if (input.wildoBinPath) return input.wildoBinPath;\n const candidate = path.join(input.saasRoot, 'node_modules', '.bin', 'wildo');\n try {\n await fs.access(candidate);\n return candidate;\n } catch {\n // Fall back to PATH-resolved `wildo`. spawn will raise ENOENT\n // if it's not on PATH either, surfaced as\n // `RegistryProviderSyncFailedError(failureKind: 'unknown')`.\n return 'wildo';\n }\n}\n\n/**\n * Grace window between SIGTERM and SIGKILL. A well-behaved\n * `wildo config sync` should react to SIGTERM in well under a\n * second — the grace exists for tools that need to flush a buffer\n * or release a file lock. After the window we escalate to SIGKILL\n * so a hung child cannot pin the parent forever.\n */\nconst SIGKILL_GRACE_MS = 5_000;\n\nfunction runWildoConfigSync(\n binPath: string,\n cwd: string,\n timeoutMs: number,\n): Promise<{ exitCode: number | null; stderr: string; durationMs: number; signaled: boolean }> {\n return new Promise((resolve) => {\n const startedAt = Date.now();\n const child = spawn(binPath, ['config', 'sync', '--domain', 'config'], {\n cwd,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n let stderr = '';\n let signaled = false;\n let killTimer: NodeJS.Timeout | undefined;\n child.stderr?.on('data', (b: Buffer) => {\n // Cap captured stderr so a runaway sync can't OOM the parent.\n stderr += b.toString();\n if (stderr.length > 16_384) stderr = stderr.slice(-16_384);\n });\n const termTimer = setTimeout(() => {\n // Soft-kill first; record so the caller can synthesize a\n // diagnostic message that distinguishes a TERM-induced exit\n // from a normal one.\n signaled = true;\n stderr += `\\n[provider-sync] timeout ${timeoutMs}ms exceeded — sent SIGTERM.`;\n child.kill('SIGTERM');\n // Escalate to SIGKILL if the child doesn't react within the\n // grace window. Without this a child stuck in a syscall (or\n // deliberately ignoring SIGTERM) would leave the parent\n // blocked on the spawn promise forever.\n killTimer = setTimeout(() => {\n if (child.exitCode === null && child.signalCode === null) {\n stderr += `\\n[provider-sync] SIGTERM grace ${SIGKILL_GRACE_MS}ms exceeded — escalating to SIGKILL.`;\n try {\n child.kill('SIGKILL');\n } catch {\n /* swallow — best-effort escalation */\n }\n }\n }, SIGKILL_GRACE_MS);\n }, timeoutMs);\n child.on('exit', (code) => {\n clearTimeout(termTimer);\n if (killTimer) clearTimeout(killTimer);\n resolve({\n exitCode: code,\n stderr,\n durationMs: Date.now() - startedAt,\n signaled,\n });\n });\n child.on('error', (err) => {\n clearTimeout(termTimer);\n if (killTimer) clearTimeout(killTimer);\n resolve({\n exitCode: null,\n stderr: stderr + `\\n[spawn error] ${err.message}`,\n durationMs: Date.now() - startedAt,\n signaled,\n });\n });\n });\n}\n\n/**\n * Run the canonical provider-sync command. Throws\n * `RegistryProviderSyncFailedError` (with classified `failureKind`)\n * when the spawned process exits non-zero or fails to start.\n */\nexport async function runProviderSync(\n input: ProviderSyncRunnerInput,\n): Promise<ProviderSyncRunnerResult> {\n const log = input.log;\n const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const baseResult = {\n invoked: true as const,\n reason: input.reason,\n providerJsonRelativePaths: PROVIDER_JSON_RELATIVE_PATHS,\n providerSourceRelativePath: PROVIDER_SOURCE_RELATIVE_PATH,\n providerLockSectionPath: PROVIDER_LOCK_SECTION_PATH,\n };\n\n if (input.dryRun) {\n log?.info?.(\n `[registry] post-${input.reason} provider sync (dry-run): would shell ` +\n `\\`wildo config sync --domain config\\` in ${input.saasRoot}.`,\n );\n return { ...baseResult, mode: 'dry-run', exitCode: 0, durationMs: 0 };\n }\n\n const binPath = await resolveWildoBin(input);\n log?.info?.(\n `[registry] post-${input.reason} provider sync: spawning ` +\n `${binPath} config sync --domain config (cwd=${input.saasRoot}, timeout=${timeoutMs}ms)`,\n );\n const { exitCode, stderr, durationMs, signaled } = await runWildoConfigSync(\n binPath,\n input.saasRoot,\n timeoutMs,\n );\n\n if (exitCode === 0) {\n log?.success?.(`[registry] provider sync succeeded in ${durationMs}ms.`);\n return { ...baseResult, mode: 'shell-out', exitCode, durationMs };\n }\n\n // H5: distinguish a timeout-signaled exit from a generic non-zero\n // exit. `classifyStderr` runs regex against the appended\n // `[provider-sync] timeout ... SIGTERM/SIGKILL` lines but those\n // tails match no failure-mode regex — so prior to this branch\n // operators saw `failureKind: 'unknown'` for a hard timeout. The\n // typed error now carries the discriminator + a hint to either\n // raise the timeout or investigate the wedge.\n const failureKind = signaled ? ('timeout' as const) : classifyStderr(stderr);\n log?.error?.(\n `[registry] provider sync failed (${failureKind}, exit=${exitCode}, ${durationMs}ms).`,\n );\n const messageSuffix = failureKind === 'timeout'\n ? `Wall-clock exceeded ${timeoutMs}ms; child was sent SIGTERM (then SIGKILL after grace). ` +\n 'Either raise `timeoutMs` for legitimately heavy syncs or investigate why the sync wedged.'\n : 'Run `wildo config sync --domain config` manually to inspect.';\n throw new RegistryProviderSyncFailedError(\n `Post-${input.reason} provider sync failed (${failureKind}). ${messageSuffix}`,\n failureKind,\n stderr.slice(-1024),\n );\n}\n"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `RegistryCompanionClient` — HTTP client for the local dev
|
|
3
|
+
* companion's registry routes.
|
|
4
|
+
*
|
|
5
|
+
* Distinct from `RegistryServerClient` (which talks to the central
|
|
6
|
+
* `platform-module-registry` server). The companion client targets
|
|
7
|
+
* `/api/companion/registry/*` on `http://localhost:<companion-port>`
|
|
8
|
+
* for AST/source mutations the CLI delegates to the companion
|
|
9
|
+
* (KD-6).
|
|
10
|
+
*
|
|
11
|
+
* Phase 2 ships the four mutation routes as method calls; the
|
|
12
|
+
* companion controller (under
|
|
13
|
+
* `platform/factory/wildo-dev-companion/src/companion-api/controllers/`)
|
|
14
|
+
* implements them with the actual ts-morph + filesystem work in
|
|
15
|
+
* Phase 5+.
|
|
16
|
+
*/
|
|
17
|
+
import { type RegistryInstallRequest, type RegistryPublishRequest, type RegistryRemoveRequest, type RegistryRouteResult, type RegistryUpdateRequest } from '@wildo-ai/platform-config-lib';
|
|
18
|
+
export interface RegistryCompanionClientOptions {
|
|
19
|
+
baseUrl: string;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare const RegistryCompanionClient: {
|
|
23
|
+
/**
|
|
24
|
+
* Liveness probe against `/health` (the same endpoint the
|
|
25
|
+
* dev-supervisor monitors). Used by `wildo registry ping`.
|
|
26
|
+
*/
|
|
27
|
+
readonly ping: (options: RegistryCompanionClientOptions) => Promise<boolean>;
|
|
28
|
+
/**
|
|
29
|
+
* Ask the companion to install a module: download bytes, copy
|
|
30
|
+
* source, mutate `wildo.saas.config.ts > moduleRegistries` +
|
|
31
|
+
* the three `modules-registry.*.ts` files, refresh
|
|
32
|
+
* `sections.modules`, run provider sync.
|
|
33
|
+
*/
|
|
34
|
+
readonly install: (options: RegistryCompanionClientOptions, request: RegistryInstallRequest) => Promise<RegistryRouteResult>;
|
|
35
|
+
readonly update: (options: RegistryCompanionClientOptions, request: RegistryUpdateRequest) => Promise<RegistryRouteResult>;
|
|
36
|
+
readonly remove: (options: RegistryCompanionClientOptions, request: RegistryRemoveRequest) => Promise<RegistryRouteResult>;
|
|
37
|
+
readonly publish: (options: RegistryCompanionClientOptions, request: RegistryPublishRequest) => Promise<RegistryRouteResult>;
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=registry-companion-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry-companion-client.d.ts","sourceRoot":"","sources":["../../../../src/services/registry-companion-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAKL,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC3B,MAAM,+BAA+B,CAAC;AAMvC,MAAM,WAAW,8BAA8B;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA2CD,eAAO,MAAM,uBAAuB;IAClC;;;OAGG;6BACiB,8BAA8B,KAAG,OAAO,CAAC,OAAO,CAAC;IAcrE;;;;;OAKG;gCAEQ,8BAA8B,WAC9B,sBAAsB,KAC9B,OAAO,CAAC,mBAAmB,CAAC;+BAMpB,8BAA8B,WAC9B,qBAAqB,KAC7B,OAAO,CAAC,mBAAmB,CAAC;+BAMpB,8BAA8B,WAC9B,qBAAqB,KAC7B,OAAO,CAAC,mBAAmB,CAAC;gCAMpB,8BAA8B,WAC9B,sBAAsB,KAC9B,OAAO,CAAC,mBAAmB,CAAC;CAIvB,CAAC"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `RegistryCompanionClient` — HTTP client for the local dev
|
|
3
|
+
* companion's registry routes.
|
|
4
|
+
*
|
|
5
|
+
* Distinct from `RegistryServerClient` (which talks to the central
|
|
6
|
+
* `platform-module-registry` server). The companion client targets
|
|
7
|
+
* `/api/companion/registry/*` on `http://localhost:<companion-port>`
|
|
8
|
+
* for AST/source mutations the CLI delegates to the companion
|
|
9
|
+
* (KD-6).
|
|
10
|
+
*
|
|
11
|
+
* Phase 2 ships the four mutation routes as method calls; the
|
|
12
|
+
* companion controller (under
|
|
13
|
+
* `platform/factory/wildo-dev-companion/src/companion-api/controllers/`)
|
|
14
|
+
* implements them with the actual ts-morph + filesystem work in
|
|
15
|
+
* Phase 5+.
|
|
16
|
+
*/
|
|
17
|
+
import { REGISTRY_COMPANION_ROUTES, RegistryMalformedPayloadError, RegistryRouteResultSchema, RegistryUnexpectedStatusError, } from '@wildo-ai/platform-config-lib';
|
|
18
|
+
import { fetchWithTimeout, joinUrl } from './_http-client.js';
|
|
19
|
+
import { formatZodIssuesAsSingleLine } from './_zod-issue-formatting.js';
|
|
20
|
+
const DIAGNOSTIC_LABEL = 'Companion registry';
|
|
21
|
+
async function postJson(options, routePath, body) {
|
|
22
|
+
const url = joinUrl(options.baseUrl, routePath);
|
|
23
|
+
const res = await fetchWithTimeout(url, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: { 'content-type': 'application/json' },
|
|
26
|
+
body: JSON.stringify(body),
|
|
27
|
+
timeoutMs: options.timeoutMs,
|
|
28
|
+
diagnosticLabel: DIAGNOSTIC_LABEL,
|
|
29
|
+
});
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
// 3rd-pass M5: typed `RegistryUnexpectedStatusError` instead of
|
|
32
|
+
// bare `new Error(...)` so callers can `isRegistryError(err)` and
|
|
33
|
+
// discriminate by status code without re-parsing message text.
|
|
34
|
+
// The companion deliberately reuses the same error vocabulary
|
|
35
|
+
// as `RegistryServerClient` since the wire shape (status + URL +
|
|
36
|
+
// action) is identical even though the lane is different.
|
|
37
|
+
throw new RegistryUnexpectedStatusError(options.baseUrl, res.status, res.statusText, `companion ${routePath}`);
|
|
38
|
+
}
|
|
39
|
+
// 6th-pass M3: typed JSON-parse — a misconfigured proxy returning
|
|
40
|
+
// HTML at 2xx would otherwise throw a bare `SyntaxError` that
|
|
41
|
+
// bypasses the typed-error contract.
|
|
42
|
+
try {
|
|
43
|
+
return (await res.json());
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
throw new RegistryMalformedPayloadError(options.baseUrl, `companion ${routePath}`, `response is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export const RegistryCompanionClient = {
|
|
50
|
+
/**
|
|
51
|
+
* Liveness probe against `/health` (the same endpoint the
|
|
52
|
+
* dev-supervisor monitors). Used by `wildo registry ping`.
|
|
53
|
+
*/
|
|
54
|
+
async ping(options) {
|
|
55
|
+
const url = joinUrl(options.baseUrl, '/health');
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetchWithTimeout(url, {
|
|
58
|
+
method: 'GET',
|
|
59
|
+
timeoutMs: options.timeoutMs,
|
|
60
|
+
diagnosticLabel: DIAGNOSTIC_LABEL,
|
|
61
|
+
});
|
|
62
|
+
return res.ok;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
/**
|
|
69
|
+
* Ask the companion to install a module: download bytes, copy
|
|
70
|
+
* source, mutate `wildo.saas.config.ts > moduleRegistries` +
|
|
71
|
+
* the three `modules-registry.*.ts` files, refresh
|
|
72
|
+
* `sections.modules`, run provider sync.
|
|
73
|
+
*/
|
|
74
|
+
async install(options, request) {
|
|
75
|
+
const raw = await postJson(options, REGISTRY_COMPANION_ROUTES.install, request);
|
|
76
|
+
return parseCompanionResult(options.baseUrl, REGISTRY_COMPANION_ROUTES.install, raw);
|
|
77
|
+
},
|
|
78
|
+
async update(options, request) {
|
|
79
|
+
const raw = await postJson(options, REGISTRY_COMPANION_ROUTES.update, request);
|
|
80
|
+
return parseCompanionResult(options.baseUrl, REGISTRY_COMPANION_ROUTES.update, raw);
|
|
81
|
+
},
|
|
82
|
+
async remove(options, request) {
|
|
83
|
+
const raw = await postJson(options, REGISTRY_COMPANION_ROUTES.remove, request);
|
|
84
|
+
return parseCompanionResult(options.baseUrl, REGISTRY_COMPANION_ROUTES.remove, raw);
|
|
85
|
+
},
|
|
86
|
+
async publish(options, request) {
|
|
87
|
+
const raw = await postJson(options, REGISTRY_COMPANION_ROUTES.publish, request);
|
|
88
|
+
return parseCompanionResult(options.baseUrl, REGISTRY_COMPANION_ROUTES.publish, raw);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* M5: thin wrapper around `RegistryRouteResultSchema.parse(raw)` that
|
|
93
|
+
* converts a Zod parse failure into the typed
|
|
94
|
+
* `RegistryMalformedPayloadError` instead of letting a bare `ZodError`
|
|
95
|
+
* bubble up. Keeps the typed-handler contract aligned with
|
|
96
|
+
* `RegistryServerClient`.
|
|
97
|
+
*/
|
|
98
|
+
function parseCompanionResult(baseUrl, routePath, raw) {
|
|
99
|
+
const parsed = RegistryRouteResultSchema.safeParse(raw);
|
|
100
|
+
if (!parsed.success) {
|
|
101
|
+
// 7th-pass C-L2: format the issues array for operator
|
|
102
|
+
// readability instead of using Zod 4's JSON-stringified
|
|
103
|
+
// `error.message`.
|
|
104
|
+
const detail = formatZodIssuesAsSingleLine(parsed.error);
|
|
105
|
+
throw new RegistryMalformedPayloadError(baseUrl, `companion ${routePath}`, detail);
|
|
106
|
+
}
|
|
107
|
+
return parsed.data;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=registry-companion-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry-companion-client.js","sourceRoot":"","sources":["../../../../src/services/registry-companion-client.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `RegistryCompanionClient` — HTTP client for the local dev\n * companion's registry routes.\n *\n * Distinct from `RegistryServerClient` (which talks to the central\n * `platform-module-registry` server). The companion client targets\n * `/api/companion/registry/*` on `http://localhost:<companion-port>`\n * for AST/source mutations the CLI delegates to the companion\n * (KD-6).\n *\n * Phase 2 ships the four mutation routes as method calls; the\n * companion controller (under\n * `platform/factory/wildo-dev-companion/src/companion-api/controllers/`)\n * implements them with the actual ts-morph + filesystem work in\n * Phase 5+.\n */\n\nimport {\n REGISTRY_COMPANION_ROUTES,\n RegistryMalformedPayloadError,\n RegistryRouteResultSchema,\n RegistryUnexpectedStatusError,\n type RegistryInstallRequest,\n type RegistryPublishRequest,\n type RegistryRemoveRequest,\n type RegistryRouteResult,\n type RegistryUpdateRequest,\n} from '@wildo-ai/platform-config-lib';\nimport { fetchWithTimeout, joinUrl } from './_http-client';\nimport { formatZodIssuesAsSingleLine } from './_zod-issue-formatting';\n\nconst DIAGNOSTIC_LABEL = 'Companion registry';\n\nexport interface RegistryCompanionClientOptions {\n baseUrl: string;\n timeoutMs?: number;\n}\n\nasync function postJson<R>(\n options: RegistryCompanionClientOptions,\n routePath: string,\n body: unknown,\n): Promise<R> {\n const url = joinUrl(options.baseUrl, routePath);\n const res = await fetchWithTimeout(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n timeoutMs: options.timeoutMs,\n diagnosticLabel: DIAGNOSTIC_LABEL,\n });\n if (!res.ok) {\n // 3rd-pass M5: typed `RegistryUnexpectedStatusError` instead of\n // bare `new Error(...)` so callers can `isRegistryError(err)` and\n // discriminate by status code without re-parsing message text.\n // The companion deliberately reuses the same error vocabulary\n // as `RegistryServerClient` since the wire shape (status + URL +\n // action) is identical even though the lane is different.\n throw new RegistryUnexpectedStatusError(\n options.baseUrl,\n res.status,\n res.statusText,\n `companion ${routePath}`,\n );\n }\n // 6th-pass M3: typed JSON-parse — a misconfigured proxy returning\n // HTML at 2xx would otherwise throw a bare `SyntaxError` that\n // bypasses the typed-error contract.\n try {\n return (await res.json()) as R;\n } catch (err) {\n throw new RegistryMalformedPayloadError(\n options.baseUrl,\n `companion ${routePath}`,\n `response is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n}\n\nexport const RegistryCompanionClient = {\n /**\n * Liveness probe against `/health` (the same endpoint the\n * dev-supervisor monitors). Used by `wildo registry ping`.\n */\n async ping(options: RegistryCompanionClientOptions): Promise<boolean> {\n const url = joinUrl(options.baseUrl, '/health');\n try {\n const res = await fetchWithTimeout(url, {\n method: 'GET',\n timeoutMs: options.timeoutMs,\n diagnosticLabel: DIAGNOSTIC_LABEL,\n });\n return res.ok;\n } catch {\n return false;\n }\n },\n\n /**\n * Ask the companion to install a module: download bytes, copy\n * source, mutate `wildo.saas.config.ts > moduleRegistries` +\n * the three `modules-registry.*.ts` files, refresh\n * `sections.modules`, run provider sync.\n */\n async install(\n options: RegistryCompanionClientOptions,\n request: RegistryInstallRequest,\n ): Promise<RegistryRouteResult> {\n const raw = await postJson<unknown>(options, REGISTRY_COMPANION_ROUTES.install, request);\n return parseCompanionResult(options.baseUrl, REGISTRY_COMPANION_ROUTES.install, raw);\n },\n\n async update(\n options: RegistryCompanionClientOptions,\n request: RegistryUpdateRequest,\n ): Promise<RegistryRouteResult> {\n const raw = await postJson<unknown>(options, REGISTRY_COMPANION_ROUTES.update, request);\n return parseCompanionResult(options.baseUrl, REGISTRY_COMPANION_ROUTES.update, raw);\n },\n\n async remove(\n options: RegistryCompanionClientOptions,\n request: RegistryRemoveRequest,\n ): Promise<RegistryRouteResult> {\n const raw = await postJson<unknown>(options, REGISTRY_COMPANION_ROUTES.remove, request);\n return parseCompanionResult(options.baseUrl, REGISTRY_COMPANION_ROUTES.remove, raw);\n },\n\n async publish(\n options: RegistryCompanionClientOptions,\n request: RegistryPublishRequest,\n ): Promise<RegistryRouteResult> {\n const raw = await postJson<unknown>(options, REGISTRY_COMPANION_ROUTES.publish, request);\n return parseCompanionResult(options.baseUrl, REGISTRY_COMPANION_ROUTES.publish, raw);\n },\n} as const;\n\n/**\n * M5: thin wrapper around `RegistryRouteResultSchema.parse(raw)` that\n * converts a Zod parse failure into the typed\n * `RegistryMalformedPayloadError` instead of letting a bare `ZodError`\n * bubble up. Keeps the typed-handler contract aligned with\n * `RegistryServerClient`.\n */\nfunction parseCompanionResult(\n baseUrl: string,\n routePath: string,\n raw: unknown,\n): RegistryRouteResult {\n const parsed = RegistryRouteResultSchema.safeParse(raw);\n if (!parsed.success) {\n // 7th-pass C-L2: format the issues array for operator\n // readability instead of using Zod 4's JSON-stringified\n // `error.message`.\n const detail = formatZodIssuesAsSingleLine(parsed.error);\n throw new RegistryMalformedPayloadError(\n baseUrl,\n `companion ${routePath}`,\n detail,\n );\n }\n return parsed.data;\n}\n"]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `RegistryLockfileService` — section-scoped reader/writer for the
|
|
3
|
+
* `sections.modules` slice of `wildo-saas.lock.json` (KD-4).
|
|
4
|
+
*
|
|
5
|
+
* Phase 1 graduates the locking + atomic-write primitives into
|
|
6
|
+
* `@wildo-ai/platform-config-lib` (validated by the Phase 0.B
|
|
7
|
+
* contention spike). This service is a thin wrapper around
|
|
8
|
+
* `withLockfileSectionMutate` that pre-binds the `modules` section
|
|
9
|
+
* key + schema, so module-registry callers cannot accidentally
|
|
10
|
+
* touch sibling sections.
|
|
11
|
+
*
|
|
12
|
+
* Hard contracts (KD-4 / KD-15 / KD-18):
|
|
13
|
+
* - Mutates ONLY `sections.modules`. The shared library preserves
|
|
14
|
+
* `sections.providers`, `sections.i18n`, and every unknown
|
|
15
|
+
* `sections.*` payload verbatim — no defensive logic needed
|
|
16
|
+
* here.
|
|
17
|
+
* - Never writes `.wildo-saas/generated/providers/*.runtime-providers.generated.json`
|
|
18
|
+
* or `src/generated/wildo-provider-runtime.generated.ts` —
|
|
19
|
+
* those are produced by the provider materialization workflow.
|
|
20
|
+
* - Never mutates `sections.providers.runtimeScopes`.
|
|
21
|
+
*/
|
|
22
|
+
import { type RegistryModulesLockSection, type RegistryModulesLockEntry } from '@wildo-ai/platform-config-lib';
|
|
23
|
+
export declare const RegistryLockfileService: {
|
|
24
|
+
/**
|
|
25
|
+
* Read the current `sections.modules` payload (or an empty section
|
|
26
|
+
* when missing). Unsynchronized — callers needing read-after-write
|
|
27
|
+
* coherence should read INSIDE `mutate`.
|
|
28
|
+
*/
|
|
29
|
+
readonly read: (saasRoot: string) => Promise<RegistryModulesLockSection>;
|
|
30
|
+
/**
|
|
31
|
+
* Run `mutator` against the current `sections.modules` payload
|
|
32
|
+
* under an OS-level lock + atomic-write commit. The mutator can
|
|
33
|
+
* either mutate-in-place or return a new value; either way the
|
|
34
|
+
* result is re-validated against `RegistryModulesLockSectionSchema`
|
|
35
|
+
* before commit.
|
|
36
|
+
*/
|
|
37
|
+
readonly mutate: (saasRoot: string, mutator: (current: RegistryModulesLockSection) => RegistryModulesLockSection | void | Promise<RegistryModulesLockSection | void>) => Promise<RegistryModulesLockSection>;
|
|
38
|
+
/** Convenience helper used by Phase 5+ install flow. */
|
|
39
|
+
readonly upsertEntry: (saasRoot: string, entry: RegistryModulesLockEntry) => Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Find every other lockfile entry whose `files[]` claims any of the
|
|
42
|
+
* given paths. Used by `update`/`remove` to skip deletes that would
|
|
43
|
+
* silently pull files out from under modules that own them too
|
|
44
|
+
* (cross-module file overlap).
|
|
45
|
+
*
|
|
46
|
+
* Returns a `Map<relPath, owners[]>` where `owners` excludes
|
|
47
|
+
* `excludeModuleId`. Empty array → uniquely owned by the excluded
|
|
48
|
+
* module, safe to delete.
|
|
49
|
+
*/
|
|
50
|
+
readonly findFileOwners: (args: {
|
|
51
|
+
saasRoot: string;
|
|
52
|
+
paths: readonly string[];
|
|
53
|
+
excludeModuleId: string;
|
|
54
|
+
}) => Promise<Map<string, string[]>>;
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=registry-lockfile.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry-lockfile.service.d.ts","sourceRoot":"","sources":["../../../../src/services/registry-lockfile.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAIL,KAAK,0BAA0B,EAC/B,KAAK,wBAAwB,EAC9B,MAAM,+BAA+B,CAAC;AAevC,eAAO,MAAM,uBAAuB;IAClC;;;;OAIG;8BACkB,MAAM,KAAG,OAAO,CAAC,0BAA0B,CAAC;IAOjE;;;;;;OAMG;gCAES,MAAM,WACP,CACP,OAAO,EAAE,0BAA0B,KAEjC,0BAA0B,GAC1B,IAAI,GACJ,OAAO,CAAC,0BAA0B,GAAG,IAAI,CAAC,KAC7C,OAAO,CAAC,0BAA0B,CAAC;IAYtC,wDAAwD;qCAC5B,MAAM,SAAS,wBAAwB,KAAG,OAAO,CAAC,IAAI,CAAC;IAOnF;;;;;;;;;OASG;oCACwB;QACzB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;QACzB,eAAe,EAAE,MAAM,CAAC;KACzB,KAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CAgB1B,CAAC"}
|