@aperant/framework 0.8.6 → 0.9.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.
- package/CHANGELOG.md +103 -0
- package/agents/apt-pr-review-fixer.md +9 -4
- package/dist/cli/commands/init.mjs +140 -22
- package/dist/cli/commands/init.mjs.map +1 -1
- package/dist/cli/commands/toolchain-detect.d.mts.map +1 -1
- package/dist/cli/commands/toolchain-detect.mjs +44 -0
- package/dist/cli/commands/toolchain-detect.mjs.map +1 -1
- package/dist/cli/commands/triage.d.mts.map +1 -1
- package/dist/cli/commands/triage.mjs +346 -13
- package/dist/cli/commands/triage.mjs.map +1 -1
- package/dist/cli/commands/uninstall.d.mts.map +1 -1
- package/dist/cli/commands/uninstall.mjs +11 -8
- package/dist/cli/commands/uninstall.mjs.map +1 -1
- package/dist/cli/install/manifest.d.mts +13 -0
- package/dist/cli/install/manifest.d.mts.map +1 -1
- package/dist/cli/install/manifest.mjs +5 -0
- package/dist/cli/install/manifest.mjs.map +1 -1
- package/dist/cli/install/pipeline.d.mts +15 -0
- package/dist/cli/install/pipeline.d.mts.map +1 -1
- package/dist/cli/install/pipeline.mjs +27 -0
- package/dist/cli/install/pipeline.mjs.map +1 -1
- package/dist/cli/install/runtime-detect.d.mts +26 -0
- package/dist/cli/install/runtime-detect.d.mts.map +1 -1
- package/dist/cli/install/runtime-detect.mjs +27 -0
- package/dist/cli/install/runtime-detect.mjs.map +1 -1
- package/dist/cli/install/transforms/codex.d.mts +1 -1
- package/dist/cli/install/transforms/codex.d.mts.map +1 -1
- package/dist/cli/install/transforms/codex.mjs +43 -2
- package/dist/cli/install/transforms/codex.mjs.map +1 -1
- package/dist/cli/install/version-header.d.mts.map +1 -1
- package/dist/cli/install/version-header.mjs +7 -1
- package/dist/cli/install/version-header.mjs.map +1 -1
- package/dist/cli/util/aperant-section.d.mts +12 -0
- package/dist/cli/util/aperant-section.d.mts.map +1 -1
- package/dist/cli/util/aperant-section.mjs +12 -0
- package/dist/cli/util/aperant-section.mjs.map +1 -1
- package/dist/cli/util/copy.d.mts +27 -0
- package/dist/cli/util/copy.d.mts.map +1 -1
- package/dist/cli/util/copy.mjs +49 -5
- package/dist/cli/util/copy.mjs.map +1 -1
- package/dist/cli/verify-proof/idl/index.d.mts +12 -1
- package/dist/cli/verify-proof/idl/index.d.mts.map +1 -1
- package/dist/cli/verify-proof/idl/index.mjs +50 -1
- package/dist/cli/verify-proof/idl/index.mjs.map +1 -1
- package/dist/cli/verify-proof/resolver.d.mts.map +1 -1
- package/dist/cli/verify-proof/resolver.mjs +51 -1
- package/dist/cli/verify-proof/resolver.mjs.map +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/agents/apt-pr-review-fixer.md +9 -4
- package/dist/plugin/skills/apt-pr-review/SKILL.md +52 -15
- package/dist/plugin/skills/apt-setup/SKILL.md +7 -6
- package/dist/plugin/skills/apt-triage/SKILL.md +8 -6
- package/dist/plugin/skills/apt-update/SKILL.md +1 -1
- package/package.json +1 -1
- package/skills/apt-pr-review/SKILL.md +52 -15
- package/skills/apt-setup/SKILL.md +7 -6
- package/skills/apt-triage/SKILL.md +8 -6
- package/skills/apt-update/SKILL.md +1 -1
- package/src/cli/commands/init.mjs +144 -19
- package/src/cli/commands/toolchain-detect.mjs +44 -0
- package/src/cli/commands/triage.mjs +352 -11
- package/src/cli/commands/uninstall.mjs +11 -8
- package/src/cli/install/manifest.mjs +5 -0
- package/src/cli/install/pipeline.mjs +28 -0
- package/src/cli/install/runtime-detect.mjs +27 -0
- package/src/cli/install/transforms/codex.mjs +51 -2
- package/src/cli/install/version-header.mjs +7 -1
- package/src/cli/util/aperant-section.mjs +14 -0
- package/src/cli/util/copy.mjs +53 -8
- package/src/cli/verify-proof/idl/index.mjs +49 -11
- package/src/cli/verify-proof/resolver.mjs +49 -2
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
* against installRoot `.agents` to land at `.agents/skills/<folder>/SKILL.md`).
|
|
15
15
|
* Markdown agents are NOT a Codex surface (custom-agents are TOML
|
|
16
16
|
* under `.codex/agents/`); only skills are emitted.
|
|
17
|
+
* - agent inlining: Codex has no markdown agent-spawn surface, so a skill
|
|
18
|
+
* declaring `spawns_agent: true` + `agent_name: X` would, on Claude, spawn
|
|
19
|
+
* the X agent (whose prompt lives in agents/X.md). On Codex that file is
|
|
20
|
+
* never installed and the spawn directive is stripped by the whitelist —
|
|
21
|
+
* leaving the skill referencing a non-existent agent. To keep Codex at
|
|
22
|
+
* parity, the referenced agent's body is appended inline to the skill body
|
|
23
|
+
* so the model has the full methodology even without a spawn primitive.
|
|
17
24
|
*/
|
|
18
25
|
|
|
19
26
|
import { basename, dirname } from 'node:path'
|
|
@@ -36,16 +43,33 @@ const KEEP = ['name', 'description']
|
|
|
36
43
|
* @param {import('../pipeline.mjs').TransformInput} input
|
|
37
44
|
* @returns {import('../pipeline.mjs').TransformOutput[]}
|
|
38
45
|
*/
|
|
39
|
-
export function transform({
|
|
46
|
+
export function transform({
|
|
47
|
+
canonicalContent,
|
|
48
|
+
canonicalRelPath,
|
|
49
|
+
version,
|
|
50
|
+
fileType,
|
|
51
|
+
agentContentByName = {},
|
|
52
|
+
}) {
|
|
40
53
|
const subbed = substituteVersion(canonicalContent, version)
|
|
41
54
|
const parsed = parseFrontmatter(subbed)
|
|
42
55
|
let payload
|
|
43
56
|
if (parsed.hasFrontmatter && parsed.fm) {
|
|
57
|
+
// Read the spawn directive BEFORE the whitelist strips it. On Codex
|
|
58
|
+
// there's no agent-spawn surface, so we inline the agent body instead.
|
|
59
|
+
const spawnsAgent = parsed.fm.spawns_agent === true
|
|
60
|
+
const agentName =
|
|
61
|
+
typeof parsed.fm.agent_name === 'string' && parsed.fm.agent_name.length > 0
|
|
62
|
+
? parsed.fm.agent_name
|
|
63
|
+
: null
|
|
44
64
|
// Whitelist filter: drop every frontmatter key NOT in KEEP.
|
|
45
65
|
for (const k of Object.keys(parsed.fm)) {
|
|
46
66
|
if (!KEEP.includes(k)) delete parsed.fm[k]
|
|
47
67
|
}
|
|
48
|
-
|
|
68
|
+
let body = parsed.body
|
|
69
|
+
if (spawnsAgent && agentName && agentContentByName[agentName]) {
|
|
70
|
+
body = `${body}\n\n${inlineAgentSection(agentName, agentContentByName[agentName], version)}`
|
|
71
|
+
}
|
|
72
|
+
const wrappedBody = `<codex_agent_role>\n${body}\n</codex_agent_role>\n`
|
|
49
73
|
payload = serializeFrontmatter({ ...parsed, body: wrappedBody })
|
|
50
74
|
} else {
|
|
51
75
|
payload = `<codex_agent_role>\n${subbed}\n</codex_agent_role>\n`
|
|
@@ -63,3 +87,28 @@ export function transform({ canonicalContent, canonicalRelPath, version, fileTyp
|
|
|
63
87
|
// another dead surface. Drop the agent branch entirely.
|
|
64
88
|
return []
|
|
65
89
|
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build the inlined-agent section appended to a Codex skill body. Strips the
|
|
93
|
+
* agent file's own frontmatter (Codex skills carry a single frontmatter block)
|
|
94
|
+
* and version-substitutes the body. The fenced header makes it unambiguous to
|
|
95
|
+
* the model that this is the methodology it should execute directly — there is
|
|
96
|
+
* no separate agent to spawn on Codex.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} agentName
|
|
99
|
+
* @param {string} agentContent raw canonical agent file (with frontmatter + {{APT_VERSION}})
|
|
100
|
+
* @param {string} version
|
|
101
|
+
* @returns {string}
|
|
102
|
+
*/
|
|
103
|
+
function inlineAgentSection(agentName, agentContent, version) {
|
|
104
|
+
const agentBody = parseFrontmatter(substituteVersion(agentContent, version)).body.trim()
|
|
105
|
+
return [
|
|
106
|
+
`## Inlined agent: ${agentName}`,
|
|
107
|
+
'',
|
|
108
|
+
`Codex has no separate agent-spawn surface. Where this skill says to spawn`,
|
|
109
|
+
`the \`${agentName}\` agent, execute the methodology below directly — you ARE`,
|
|
110
|
+
`that agent for this task.`,
|
|
111
|
+
'',
|
|
112
|
+
agentBody,
|
|
113
|
+
].join('\n')
|
|
114
|
+
}
|
|
@@ -99,7 +99,13 @@ export function inspectManifestFiles(manifest, installRoot, opts = {}) {
|
|
|
99
99
|
continue
|
|
100
100
|
}
|
|
101
101
|
const declared = parseVersionHeader(buf.toString('utf-8'))
|
|
102
|
-
|
|
102
|
+
// `headerless` entries are files whose runtime transform legitimately
|
|
103
|
+
// strips the apt-skill-version header (e.g. Codex whitelists frontmatter
|
|
104
|
+
// to {name, description} and rejects unknown keys). For those, the
|
|
105
|
+
// installedHash check above is the sole drift signal — requiring a header
|
|
106
|
+
// would false-positive every install. The flag is recorded at install
|
|
107
|
+
// time by the pipeline from the post-transform content.
|
|
108
|
+
if (declared === null && !entry.headerless && /\.(md|js|mjs|sh)$/.test(entry.path)) {
|
|
103
109
|
stale.push({ path: entry.path, reason: 'missing_header' })
|
|
104
110
|
continue
|
|
105
111
|
}
|
|
@@ -22,6 +22,20 @@ import { discoverSkills } from '../route/skill-discover.mjs'
|
|
|
22
22
|
|
|
23
23
|
const SECTION_START = '<!-- APT:framework-start -->'
|
|
24
24
|
const SECTION_END = '<!-- APT:framework-end -->'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Matches the full APT managed block (start marker through end marker,
|
|
28
|
+
* inclusive). Exported so callers in the same package can reference a
|
|
29
|
+
* single source rather than inlining duplicate regex literals.
|
|
30
|
+
*
|
|
31
|
+
* `APT_SECTION_RE` — no trailing newline; used for in-place replace.
|
|
32
|
+
* `APT_SECTION_STRIP_RE` — includes optional trailing newline; used for
|
|
33
|
+
* strip operations that want to remove the newline
|
|
34
|
+
* the block was preceded/followed by.
|
|
35
|
+
*/
|
|
36
|
+
export const APT_SECTION_RE = /<!-- APT:framework-start -->[\s\S]*?<!-- APT:framework-end -->/
|
|
37
|
+
export const APT_SECTION_STRIP_RE =
|
|
38
|
+
/<!-- APT:framework-start -->[\s\S]*?<!-- APT:framework-end -->\n?/
|
|
25
39
|
const SECTION_HEADING = '## Aperant Framework'
|
|
26
40
|
const APPENDIX_REL_PATH = ['templates', 'aperant-claude-md-appendix.md']
|
|
27
41
|
|
package/src/cli/util/copy.mjs
CHANGED
|
@@ -20,7 +20,21 @@ import {
|
|
|
20
20
|
writeFileSync,
|
|
21
21
|
} from 'node:fs'
|
|
22
22
|
import { join } from 'node:path'
|
|
23
|
-
import { extractAptSectionRowIds } from './aperant-section.mjs'
|
|
23
|
+
import { APT_SECTION_RE, extractAptSectionRowIds } from './aperant-section.mjs'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Return the row IDs present in `existing` that are NOT present in
|
|
27
|
+
* `aptSection`. An empty array means no foreign rows (safe to overwrite).
|
|
28
|
+
*
|
|
29
|
+
* @param {string} existing Current on-disk file contents.
|
|
30
|
+
* @param {string} aptSection Output of buildAptSection().
|
|
31
|
+
* @returns {string[]}
|
|
32
|
+
*/
|
|
33
|
+
function computeForeignRowIds(existing, aptSection) {
|
|
34
|
+
const existingIds = new Set(extractAptSectionRowIds(existing))
|
|
35
|
+
const generatedIds = new Set(extractAptSectionRowIds(aptSection))
|
|
36
|
+
return [...existingIds].filter((id) => !generatedIds.has(id))
|
|
37
|
+
}
|
|
24
38
|
|
|
25
39
|
export function copyDirRecursive(src, dest) {
|
|
26
40
|
mkdirSync(dest, { recursive: true })
|
|
@@ -35,6 +49,42 @@ export function copyDirRecursive(src, dest) {
|
|
|
35
49
|
}
|
|
36
50
|
}
|
|
37
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Preflight-only sibling of `injectInstructionFile`. Runs the same
|
|
54
|
+
* foreign-row defensive check against `filePath` but NEVER writes — used by
|
|
55
|
+
* the multi-target init flow to check every instruction target up-front so a
|
|
56
|
+
* single aborting target leaves NO file partially written (US-03 / AC3).
|
|
57
|
+
*
|
|
58
|
+
* Returns the same status vocabulary as `injectInstructionFile` minus the
|
|
59
|
+
* write side-effect: `'created'` (file absent → would be appended-to),
|
|
60
|
+
* `'updated'` (markers present, no foreign rows → would be replaced),
|
|
61
|
+
* `'aborted-foreign-rows'` (markers present with rows not in the generated
|
|
62
|
+
* set → init must abort). `force` short-circuits the foreign-row gate exactly
|
|
63
|
+
* as the writer does.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} filePath
|
|
66
|
+
* @param {Object} opts
|
|
67
|
+
* @param {string} opts.aptSection Output of buildAptSection().
|
|
68
|
+
* @param {boolean} [opts.force=false]
|
|
69
|
+
* Bypass the foreign-row check.
|
|
70
|
+
* @returns {{ status: 'created' | 'updated' | 'aborted-foreign-rows', foreignRowIds?: string[] }}
|
|
71
|
+
*/
|
|
72
|
+
export function checkInstructionFile(filePath, { aptSection, force = false }) {
|
|
73
|
+
const existing = existsSync(filePath) ? readFileSync(filePath, 'utf-8') : ''
|
|
74
|
+
|
|
75
|
+
if (existing.includes('<!-- APT:framework-start -->')) {
|
|
76
|
+
if (!force) {
|
|
77
|
+
const foreignRowIds = computeForeignRowIds(existing, aptSection)
|
|
78
|
+
if (foreignRowIds.length > 0) {
|
|
79
|
+
return { status: 'aborted-foreign-rows', foreignRowIds }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { status: 'updated' }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { status: 'created' }
|
|
86
|
+
}
|
|
87
|
+
|
|
38
88
|
/**
|
|
39
89
|
* Write the (already-generated) APT section into `filePath`. Creates the
|
|
40
90
|
* file if missing; replaces the section in-place if the markers are
|
|
@@ -60,17 +110,12 @@ export function injectInstructionFile(filePath, { aptSection, force = false }) {
|
|
|
60
110
|
|
|
61
111
|
if (existing.includes('<!-- APT:framework-start -->')) {
|
|
62
112
|
if (!force) {
|
|
63
|
-
const
|
|
64
|
-
const generatedIds = new Set(extractAptSectionRowIds(aptSection))
|
|
65
|
-
const foreignRowIds = [...existingIds].filter((id) => !generatedIds.has(id))
|
|
113
|
+
const foreignRowIds = computeForeignRowIds(existing, aptSection)
|
|
66
114
|
if (foreignRowIds.length > 0) {
|
|
67
115
|
return { status: 'aborted-foreign-rows', written: false, foreignRowIds }
|
|
68
116
|
}
|
|
69
117
|
}
|
|
70
|
-
const updated = existing.replace(
|
|
71
|
-
/<!-- APT:framework-start -->[\s\S]*?<!-- APT:framework-end -->/,
|
|
72
|
-
aptSection,
|
|
73
|
-
)
|
|
118
|
+
const updated = existing.replace(APT_SECTION_RE, aptSection)
|
|
74
119
|
writeFileSync(filePath, updated, 'utf-8')
|
|
75
120
|
return { status: 'updated', written: true }
|
|
76
121
|
}
|
|
@@ -8,16 +8,54 @@
|
|
|
8
8
|
* refactor of every callsite.
|
|
9
9
|
*
|
|
10
10
|
* If you add a verb, edit packages/framework/src/driver-sdk/idl.ts.
|
|
11
|
+
*
|
|
12
|
+
* Defense-in-depth (issue #248). `@aperant/framework/driver-sdk` resolves
|
|
13
|
+
* via the package `exports` map to `dist/driver-sdk/index.js`. A *static*
|
|
14
|
+
* `export … from '@aperant/framework/driver-sdk'` here made this barrel —
|
|
15
|
+
* and therefore the whole apt-tools CLI, since `dispatch.mjs` eagerly
|
|
16
|
+
* imports `driver-doctor.mjs` which imports this barrel — crash at
|
|
17
|
+
* module-resolution time with an opaque `ERR_MODULE_NOT_FOUND` when the
|
|
18
|
+
* dist was not built (e.g. a CI step that skipped `pnpm build`), before
|
|
19
|
+
* any command body could run. Resolving the registry via a *dynamic*
|
|
20
|
+
* `await import()` wrapped in try/catch keeps that dependency off the
|
|
21
|
+
* hard module-link path: dist present (tests, post-build CI, published
|
|
22
|
+
* tarball) → the REAL registry values are re-exported unchanged; dist
|
|
23
|
+
* absent → empty/no-op fallbacks let the CLI load (so commands that don't
|
|
24
|
+
* touch the IDL registry, like `route`, still run) instead of crashing.
|
|
11
25
|
*/
|
|
12
26
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
/** @type {typeof import('@aperant/framework/driver-sdk')} */
|
|
28
|
+
let _sdk
|
|
29
|
+
try {
|
|
30
|
+
_sdk = await import('@aperant/framework/driver-sdk')
|
|
31
|
+
} catch (err) {
|
|
32
|
+
// Only the intended "dist not built" condition degrades. A corrupt or
|
|
33
|
+
// partially-written dist (SyntaxError, broken transitive load) raises a
|
|
34
|
+
// different code and MUST surface, not be masked into an empty registry.
|
|
35
|
+
if (err?.code !== 'ERR_MODULE_NOT_FOUND') throw err
|
|
36
|
+
// dist/driver-sdk absent — degrade to empty registry so module load
|
|
37
|
+
// does not crash. Any code path that actually needs a verb registry
|
|
38
|
+
// (driver-doctor, the bundled drivers) requires a built dist anyway;
|
|
39
|
+
// commands that don't (e.g. `route`) keep working.
|
|
40
|
+
_sdk = {
|
|
41
|
+
ASSERTION_VERBS: [],
|
|
42
|
+
ASYNC_VERBS: [],
|
|
43
|
+
EVIDENCE_VERBS: [],
|
|
44
|
+
IDL_VERB_ARGS: {},
|
|
45
|
+
IDL_VERB_NAMES: [],
|
|
46
|
+
INTERACTION_VERBS: [],
|
|
47
|
+
isVerbResult: () => false,
|
|
48
|
+
LIFECYCLE_VERBS: [],
|
|
49
|
+
validateVerbArgs: () => ({ ok: true, errors: [] }),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const ASSERTION_VERBS = _sdk.ASSERTION_VERBS
|
|
54
|
+
export const ASYNC_VERBS = _sdk.ASYNC_VERBS
|
|
55
|
+
export const EVIDENCE_VERBS = _sdk.EVIDENCE_VERBS
|
|
56
|
+
export const IDL_VERB_ARGS = _sdk.IDL_VERB_ARGS
|
|
57
|
+
export const IDL_VERB_NAMES = _sdk.IDL_VERB_NAMES
|
|
58
|
+
export const INTERACTION_VERBS = _sdk.INTERACTION_VERBS
|
|
59
|
+
export const isVerbResult = _sdk.isVerbResult
|
|
60
|
+
export const LIFECYCLE_VERBS = _sdk.LIFECYCLE_VERBS
|
|
61
|
+
export const validateVerbArgs = _sdk.validateVerbArgs
|
|
@@ -30,13 +30,60 @@
|
|
|
30
30
|
* Existing `resolveDriver()` and `scoreDriver()` exports are unchanged.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
import { UnsatisfiedRequiredCapabilityError } from '@aperant/framework/driver-sdk'
|
|
34
|
-
|
|
35
33
|
/**
|
|
36
34
|
* @typedef {import('@aperant/framework/driver-sdk').DriverManifest} DriverManifest
|
|
37
35
|
* @typedef {import('./runtime-detect.mjs').RuntimeCapabilities} RuntimeCapabilities
|
|
38
36
|
*/
|
|
39
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Defense-in-depth (issue #248). The driver-sdk error class lives in
|
|
40
|
+
* `@aperant/framework/driver-sdk`, whose `exports` map resolves to
|
|
41
|
+
* `dist/driver-sdk/index.js`. An eager *static* top-level
|
|
42
|
+
* `import { UnsatisfiedRequiredCapabilityError } from '@aperant/framework/driver-sdk'`
|
|
43
|
+
* made the WHOLE apt-tools CLI crash at module-resolution time when the
|
|
44
|
+
* dist was not built (e.g. a CI step that skipped `pnpm build`): the
|
|
45
|
+
* static import is resolved at module-link, so an opaque
|
|
46
|
+
* `ERR_MODULE_NOT_FOUND` was raised before any command body — or any
|
|
47
|
+
* `--no-fail` downgrade in run-route-eval.mjs — could run.
|
|
48
|
+
*
|
|
49
|
+
* We instead resolve the class via a *dynamic* `await import()` that is
|
|
50
|
+
* wrapped in try/catch. Dynamic import is still evaluated at module
|
|
51
|
+
* load (top-level await), but its failure is now swallowable rather than
|
|
52
|
+
* a hard link-time crash:
|
|
53
|
+
* - dist present (tests, post-build CI, published tarball) → the REAL
|
|
54
|
+
* class is bound, so any thrown error stays
|
|
55
|
+
* `instanceof UnsatisfiedRequiredCapabilityError` for every consumer
|
|
56
|
+
* (including the resolver suites that import the class from the same
|
|
57
|
+
* `@aperant/framework/driver-sdk` ESM specifier);
|
|
58
|
+
* - dist absent → we fall back to a structurally identical local class
|
|
59
|
+
* (same `.name` / `.capability` / `.attempts` shape, which
|
|
60
|
+
* `detect-runtime.mjs` inspects via `.attempts`) so the CLI degrades
|
|
61
|
+
* to a clear, catchable error instead of crashing at load.
|
|
62
|
+
*
|
|
63
|
+
* `resolveDriver` stays fully synchronous — the class is already bound by
|
|
64
|
+
* the time any importer of this module finishes linking.
|
|
65
|
+
*
|
|
66
|
+
* @type {new (message: string, capability?: string, attempts?: ReadonlyArray<{driver_id: string, reason: string}>) => Error}
|
|
67
|
+
*/
|
|
68
|
+
let UnsatisfiedRequiredCapabilityError
|
|
69
|
+
try {
|
|
70
|
+
;({ UnsatisfiedRequiredCapabilityError } = await import('@aperant/framework/driver-sdk'))
|
|
71
|
+
} catch (err) {
|
|
72
|
+
// Only the intended "dist not built" condition degrades. A corrupt or
|
|
73
|
+
// partially-written dist (SyntaxError, broken transitive load) raises a
|
|
74
|
+
// different code and MUST surface, not be masked into a silent fallback.
|
|
75
|
+
if (err?.code !== 'ERR_MODULE_NOT_FOUND') throw err
|
|
76
|
+
// dist/driver-sdk absent — degrade instead of crashing the CLI.
|
|
77
|
+
UnsatisfiedRequiredCapabilityError = class UnsatisfiedRequiredCapabilityError extends Error {
|
|
78
|
+
constructor(message, capability, attempts = []) {
|
|
79
|
+
super(message)
|
|
80
|
+
this.name = 'UnsatisfiedRequiredCapabilityError'
|
|
81
|
+
this.capability = capability
|
|
82
|
+
this.attempts = attempts
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
40
87
|
const STABILITY_RANK = { ga: 3, beta: 2, experimental: 1 }
|
|
41
88
|
const LOCALITY_RANK = { bundled: 3, user: 2, registry: 1 }
|
|
42
89
|
|