agentic-workflow-manager 3.0.1 → 3.2.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/dist/src/commands/add.js +84 -0
- package/dist/src/commands/agent.js +95 -0
- package/dist/src/commands/backup.js +46 -0
- package/dist/src/commands/doctor.js +80 -4
- package/dist/src/commands/export.js +48 -0
- package/dist/src/commands/hooks/claude.js +192 -0
- package/dist/src/commands/hooks/codex.js +191 -0
- package/dist/src/commands/hooks/index.js +23 -8
- package/dist/src/commands/hooks/install.js +11 -107
- package/dist/src/commands/hooks/resync.js +28 -18
- package/dist/src/commands/hooks/shared.js +94 -0
- package/dist/src/commands/hooks/status.js +13 -64
- package/dist/src/commands/hooks/uninstall.js +11 -47
- package/dist/src/commands/init.js +90 -28
- package/dist/src/commands/registry/index.js +1 -1
- package/dist/src/commands/sync.js +113 -0
- package/dist/src/commands/update.js +107 -0
- package/dist/src/core/agent-targets.js +50 -0
- package/dist/src/core/artifact-state.js +65 -0
- package/dist/src/core/atomic-file.js +72 -0
- package/dist/src/core/bundle-install.js +60 -38
- package/dist/src/core/bundles.js +21 -0
- package/dist/src/core/context/managed-block.js +190 -0
- package/dist/src/core/context/orchestrator.js +3 -1
- package/dist/src/core/context/project-constitution-inject.js +1 -1
- package/dist/src/core/context/regenerate.js +3 -3
- package/dist/src/core/context/strategies/codex-agents.js +100 -0
- package/dist/src/core/diagnostics/checks.js +16 -0
- package/dist/src/core/diagnostics/context.js +37 -8
- package/dist/src/core/diagnostics/provider-checks.js +219 -0
- package/dist/src/core/discovery.js +9 -3
- package/dist/src/core/executor.js +30 -9
- package/dist/src/core/export/index.js +51 -0
- package/dist/src/core/export/pack.js +58 -0
- package/dist/src/core/export/resolve.js +77 -0
- package/dist/src/core/export/transform.js +57 -0
- package/dist/src/core/export/types.js +2 -0
- package/dist/src/core/init/mutation-targets.js +139 -0
- package/dist/src/core/init/provider-facts.js +164 -0
- package/dist/src/core/init/steps.js +65 -9
- package/dist/src/core/install-planner.js +206 -0
- package/dist/src/core/install-transaction.js +405 -0
- package/dist/src/core/profile.js +1 -1
- package/dist/src/core/provider-artifacts.js +54 -0
- package/dist/src/core/provider-version.js +36 -0
- package/dist/src/core/reconciliation.js +61 -0
- package/dist/src/core/registries.js +10 -0
- package/dist/src/core/renderers/canonical-agent.js +27 -0
- package/dist/src/core/renderers/codex-agent.js +60 -0
- package/dist/src/core/skill-integrity.js +4 -3
- package/dist/src/index.js +99 -289
- package/dist/src/providers/index.js +143 -39
- package/dist/src/ui/provider-preflight.js +26 -0
- package/dist/src/utils/config.js +101 -9
- package/dist/tests/commands/agent.test.js +103 -0
- package/dist/tests/commands/backup.test.js +109 -0
- package/dist/tests/commands/doctor.test.js +65 -4
- package/dist/tests/commands/export.test.js +64 -0
- package/dist/tests/commands/hooks/codex.test.js +230 -0
- package/dist/tests/commands/hooks/install.test.js +20 -1
- package/dist/tests/commands/hooks/resync.test.js +53 -6
- package/dist/tests/commands/hooks/status.test.js +10 -0
- package/dist/tests/commands/hooks/uninstall.test.js +12 -0
- package/dist/tests/commands/init.test.js +195 -8
- package/dist/tests/commands/multi-agent-targeting.test.js +231 -0
- package/dist/tests/core/agent-targets.test.js +45 -0
- package/dist/tests/core/artifact-state.test.js +144 -0
- package/dist/tests/core/atomic-file.test.js +96 -0
- package/dist/tests/core/bundle-install.test.js +208 -17
- package/dist/tests/core/bundles.test.js +12 -0
- package/dist/tests/core/context/managed-block.test.js +90 -0
- package/dist/tests/core/context/orchestrator.test.js +48 -3
- package/dist/tests/core/context/regenerate.test.js +1 -1
- package/dist/tests/core/context/strategies/codex-agents.test.js +185 -0
- package/dist/tests/core/context/strategies/config-instructions.test.js +1 -1
- package/dist/tests/core/diagnostics/checks.test.js +19 -0
- package/dist/tests/core/diagnostics/context.test.js +74 -0
- package/dist/tests/core/diagnostics/provider-checks.test.js +252 -0
- package/dist/tests/core/executor.test.js +20 -0
- package/dist/tests/core/export/engine.test.js +104 -0
- package/dist/tests/core/export/pack.test.js +122 -0
- package/dist/tests/core/export/resolve.test.js +78 -0
- package/dist/tests/core/export/transform.test.js +74 -0
- package/dist/tests/core/init/mutation-targets.test.js +235 -0
- package/dist/tests/core/init/orchestrator.test.js +1 -1
- package/dist/tests/core/init/provider-facts.test.js +129 -0
- package/dist/tests/core/init/steps.test.js +106 -2
- package/dist/tests/core/install-planner.test.js +224 -0
- package/dist/tests/core/install-transaction.test.js +257 -0
- package/dist/tests/core/provider-artifacts.test.js +64 -0
- package/dist/tests/core/provider-version.test.js +53 -0
- package/dist/tests/core/reconciliation.test.js +165 -0
- package/dist/tests/core/registries-sync.test.js +4 -4
- package/dist/tests/core/renderers/canonical-agent.test.js +58 -0
- package/dist/tests/core/renderers/codex-agent.test.js +128 -0
- package/dist/tests/core/versioning.test.js +1 -1
- package/dist/tests/integration/codex-provider-isolated.test.js +206 -0
- package/dist/tests/providers/hooks-config.test.js +43 -25
- package/dist/tests/providers/index.test.js +144 -39
- package/dist/tests/structural/codex-agent-escaping-completeness.test.js +77 -0
- package/dist/tests/ui/provider-preflight.test.js +25 -0
- package/dist/tests/utils/config.test.js +178 -18
- package/package.json +1 -1
|
@@ -16,6 +16,7 @@ const profile_1 = require("../profile");
|
|
|
16
16
|
const bundles_1 = require("../bundles");
|
|
17
17
|
const skill_integrity_1 = require("../skill-integrity");
|
|
18
18
|
const paths_1 = require("../paths");
|
|
19
|
+
const provider_checks_1 = require("./provider-checks");
|
|
19
20
|
// Estado de un artefacto en <dir>/<skill>: link vivo / symlink colgante / ausente.
|
|
20
21
|
function linkState(dir, skill) {
|
|
21
22
|
const p = path_1.default.join(dir, skill);
|
|
@@ -44,12 +45,12 @@ function classifyLinks(skillNames, dir) {
|
|
|
44
45
|
}
|
|
45
46
|
function detectGitState(repoDir) {
|
|
46
47
|
try {
|
|
47
|
-
const porcelain = (0, child_process_1.
|
|
48
|
+
const porcelain = (0, child_process_1.execFileSync)('git', ['status', '--porcelain'], { cwd: repoDir, stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' })
|
|
48
49
|
.toString().trim();
|
|
49
50
|
if (porcelain.length > 0)
|
|
50
51
|
return 'dirty';
|
|
51
52
|
try {
|
|
52
|
-
const behind = (0, child_process_1.
|
|
53
|
+
const behind = (0, child_process_1.execFileSync)('git', ['rev-list', '--count', 'HEAD..@{u}'], { cwd: repoDir, stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000, encoding: 'utf8' })
|
|
53
54
|
.toString().trim();
|
|
54
55
|
if (behind !== '' && behind !== '0')
|
|
55
56
|
return 'behind';
|
|
@@ -67,8 +68,8 @@ function detectGitState(repoDir) {
|
|
|
67
68
|
function gatherContextInjection() {
|
|
68
69
|
const out = [];
|
|
69
70
|
const orch = new orchestrator_1.InjectionOrchestrator();
|
|
70
|
-
for (const agent of
|
|
71
|
-
const inj = providers_1.
|
|
71
|
+
for (const agent of providers_1.AGENT_TARGETS) {
|
|
72
|
+
const inj = (0, providers_1.providerFor)(agent).injection;
|
|
72
73
|
if (!inj || inj.type !== 'config-instructions')
|
|
73
74
|
continue;
|
|
74
75
|
if (!fs_1.default.existsSync(inj.configPath))
|
|
@@ -96,17 +97,22 @@ function gatherMachine(bundles, agent = 'claude-code') {
|
|
|
96
97
|
const first = regs[0];
|
|
97
98
|
const cachePresent = !!first && fs_1.default.existsSync(path_1.default.join(first.contentRoot, '.git'));
|
|
98
99
|
const gitState = cachePresent ? detectGitState(first.contentRoot) : undefined;
|
|
99
|
-
// hook (reutiliza computeHookStatus)
|
|
100
|
+
// hook (reutiliza computeHookStatus) — Task 9 fix: this used to be hardcoded to
|
|
101
|
+
// 'claude-code' regardless of `agent`, so `awm init --agent codex`'s own hook
|
|
102
|
+
// precondition silently read CLAUDE's hook status instead of Codex's. Whenever
|
|
103
|
+
// Claude's hook was already installed, stepHook would then wrongly conclude
|
|
104
|
+
// Codex's hook was "already present" and skip installing it (R18 regression —
|
|
105
|
+
// caught while building this task's real end-to-end init test).
|
|
100
106
|
let hookPresent = false;
|
|
101
107
|
let hookDegraded = false;
|
|
102
108
|
try {
|
|
103
|
-
const hs = (0, status_1.computeHookStatus)(
|
|
109
|
+
const hs = (0, status_1.computeHookStatus)(agent);
|
|
104
110
|
hookPresent = hs.checks.settingsEntry.ok;
|
|
105
111
|
hookDegraded = hs.overall === 'DEGRADED';
|
|
106
112
|
}
|
|
107
113
|
catch { /* sin soporte de hooks → ausente */ }
|
|
108
114
|
// devCore (bundle baseline)
|
|
109
|
-
const skillsDir = providers_1.
|
|
115
|
+
const skillsDir = (0, providers_1.providerFor)(agent).skill.global;
|
|
110
116
|
const baseline = bundles.find((b) => b.scope === 'baseline');
|
|
111
117
|
let devCorePresent = false;
|
|
112
118
|
let brokenLinks = [];
|
|
@@ -116,6 +122,26 @@ function gatherMachine(bundles, agent = 'claude-code') {
|
|
|
116
122
|
const absent = skillNames.filter((s) => !linked.includes(s) && !broken.includes(s));
|
|
117
123
|
devCorePresent = skillNames.length > 0 && (linked.length + broken.length) > 0;
|
|
118
124
|
brokenLinks = [...broken, ...absent];
|
|
125
|
+
// Agent-type artifacts are never shared across agents (R12/R13 —
|
|
126
|
+
// install-planner.ts — unlike skills, where OpenCode and Codex both
|
|
127
|
+
// resolve to ~/.agents/skills). A shared skill directory already
|
|
128
|
+
// linked by a co-owner agent must not make THIS agent's own native
|
|
129
|
+
// agent artifacts look "present" when they were never installed for
|
|
130
|
+
// it specifically — otherwise stepDevCore (init/steps.ts) would skip
|
|
131
|
+
// re-running and Codex would never get its .toml rendered on a
|
|
132
|
+
// machine where OpenCode was initialized first. Found while building
|
|
133
|
+
// the real Codex+OpenCode coexistence E2E test
|
|
134
|
+
// (tests/integration/codex-provider-isolated.test.ts).
|
|
135
|
+
const agentConfig = (0, providers_1.providerFor)(agent).agent;
|
|
136
|
+
if (agentConfig) {
|
|
137
|
+
const agentNames = (0, bundles_1.resolveBundleAgents)(baseline.name, bundles);
|
|
138
|
+
if (agentNames.length > 0) {
|
|
139
|
+
const filenames = agentNames.map((n) => agentConfig.renderer === 'codex-agent-toml' ? `${n}.toml` : n);
|
|
140
|
+
const { linked: agentLinked, broken: agentBroken } = classifyLinks(filenames, agentConfig.global);
|
|
141
|
+
const agentAbsent = filenames.filter((f) => !agentLinked.includes(f) && !agentBroken.includes(f));
|
|
142
|
+
brokenLinks = [...brokenLinks, ...agentBroken, ...agentAbsent];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
119
145
|
}
|
|
120
146
|
// ambient (deseados desde ~/.awm/config.json)
|
|
121
147
|
let wanted = [];
|
|
@@ -145,7 +171,7 @@ function gatherMachine(bundles, agent = 'claude-code') {
|
|
|
145
171
|
function gatherProject(root, bundles, agent = 'claude-code') {
|
|
146
172
|
const profile = (0, profile_1.readProfile)(root);
|
|
147
173
|
const profilePresent = fs_1.default.existsSync(path_1.default.join(root, '.awm', 'profile.json'));
|
|
148
|
-
const localSkillsDir = path_1.default.join(root, providers_1.
|
|
174
|
+
const localSkillsDir = path_1.default.join(root, (0, providers_1.providerFor)(agent).skill.local);
|
|
149
175
|
const expected = [];
|
|
150
176
|
for (const ext of profile.extensions) {
|
|
151
177
|
for (const s of (0, bundles_1.resolveBundleSkills)(ext, bundles))
|
|
@@ -172,8 +198,11 @@ function gatherContext(opts = {}) {
|
|
|
172
198
|
const bundles = opts.bundles ?? (0, bundles_1.discoverAllBundles)();
|
|
173
199
|
const agent = opts.agent ?? 'claude-code';
|
|
174
200
|
const root = (0, profile_1.findProjectRoot)(cwd);
|
|
201
|
+
const agents = opts.agents ?? [agent];
|
|
202
|
+
const scanSkills = opts.scanSkills ?? ((dir) => (0, skill_integrity_1.classifyGlobalSkills)(dir, (0, registries_1.contentRoots)()));
|
|
175
203
|
return {
|
|
176
204
|
machine: gatherMachine(bundles, agent),
|
|
177
205
|
project: root ? gatherProject(root, bundles, agent) : null,
|
|
206
|
+
providers: (0, provider_checks_1.gatherProviderChecks)(agents, scanSkills),
|
|
178
207
|
};
|
|
179
208
|
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.gatherProviderChecks = gatherProviderChecks;
|
|
7
|
+
// src/core/diagnostics/provider-checks.ts
|
|
8
|
+
//
|
|
9
|
+
// Builds the per-provider diagnostic matrix consumed by `awm doctor`
|
|
10
|
+
// (Task 9). Kept in its own module (rather than growing context.ts or
|
|
11
|
+
// checks.ts) because it has a distinct concern: turning raw provider state
|
|
12
|
+
// (binary version, skill/agent dirs, hook trust, context injection) into a
|
|
13
|
+
// small set of stable, JSON-friendly `ProviderCheck` rows — one per
|
|
14
|
+
// provider, deduplicated for physical directories shared between providers
|
|
15
|
+
// (today: OpenCode and Codex both read/write ~/.agents/skills).
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const providers_1 = require("../../providers");
|
|
19
|
+
const provider_version_1 = require("../provider-version");
|
|
20
|
+
const status_1 = require("../../commands/hooks/status");
|
|
21
|
+
const orchestrator_1 = require("../context/orchestrator");
|
|
22
|
+
const registries_1 = require("../registries");
|
|
23
|
+
function binaryVersionCheck(agent) {
|
|
24
|
+
const provider = (0, providers_1.providerFor)(agent);
|
|
25
|
+
if (!provider.versionCommand || !provider.minimumVersion) {
|
|
26
|
+
// No version gate for this agent — nothing to enforce, treat as supported.
|
|
27
|
+
return { id: 'binary.version', state: 'supported' };
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const { version } = (0, provider_version_1.assertProviderSupported)(agent);
|
|
31
|
+
return { id: 'binary.version', state: 'supported', target: version ?? undefined };
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const message = err.message;
|
|
35
|
+
const missing = /not installed|not available on PATH/i.test(message);
|
|
36
|
+
return {
|
|
37
|
+
id: 'binary.version',
|
|
38
|
+
state: missing ? 'missing' : 'unsupported',
|
|
39
|
+
detail: message,
|
|
40
|
+
remediationCode: missing ? 'install-provider-binary' : 'upgrade-provider-binary',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function skillsGlobalCheck(dir, owners, integrity) {
|
|
45
|
+
const shared = owners.length > 1;
|
|
46
|
+
const broken = integrity.repairable.length + integrity.dead.length;
|
|
47
|
+
// Broken links are checked BEFORE shared: 'shared' is a non-degrading/OK state
|
|
48
|
+
// (see checks.ts's DEGRADING_PROVIDER_STATES), so if it were set unconditionally
|
|
49
|
+
// for a shared dir it would silently mask real broken/dead symlinks — a green
|
|
50
|
+
// checkmark next to "N broken links → repair-global-skills" would contradict its
|
|
51
|
+
// own trailing text, and `overall` would never degrade despite real breakage.
|
|
52
|
+
let state;
|
|
53
|
+
if (broken > 0) {
|
|
54
|
+
state = 'broken';
|
|
55
|
+
}
|
|
56
|
+
else if (shared) {
|
|
57
|
+
state = 'shared';
|
|
58
|
+
}
|
|
59
|
+
else if (!fs_1.default.existsSync(dir)) {
|
|
60
|
+
state = 'absent';
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
state = 'healthy';
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
id: 'skills.global',
|
|
67
|
+
state,
|
|
68
|
+
target: dir,
|
|
69
|
+
owners: shared ? owners : undefined,
|
|
70
|
+
detail: broken > 0 ? `${broken} broken links` : undefined,
|
|
71
|
+
remediationCode: broken > 0 ? 'repair-global-skills' : undefined,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** R8: verify the Codex `.toml` agents this run's renderer would have produced still parse. */
|
|
75
|
+
function tomlAgentsHealthy(dir, entries) {
|
|
76
|
+
const tomls = entries.filter((e) => e.endsWith('.toml'));
|
|
77
|
+
const broken = tomls.filter((file) => {
|
|
78
|
+
try {
|
|
79
|
+
const content = fs_1.default.readFileSync(path_1.default.join(dir, file), 'utf8');
|
|
80
|
+
// renderCodexAgent (Task 4) always emits this key — its absence means the
|
|
81
|
+
// file was hand-edited or truncated, not a shape `awm` would have written.
|
|
82
|
+
return !content.includes('developer_instructions = ');
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
}).length;
|
|
88
|
+
return { broken };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Returns `null` when a check doesn't structurally apply to `agent` (e.g. Antigravity
|
|
92
|
+
* has no `agent`/`hooks`/`injection` config; OpenCode has no hooks; Claude Code's global
|
|
93
|
+
* context rides its SessionStart hook, already covered by hook.trust). `null` rows are
|
|
94
|
+
* dropped by `gatherProviderChecks` — omitted entirely, not rendered as a failure. This
|
|
95
|
+
* is deliberately distinct from `state: 'unsupported'`, which `binaryVersionCheck` still
|
|
96
|
+
* uses for a genuine failure (an installed CLI version below the required minimum) — that
|
|
97
|
+
* state correctly degrades `overall` (see checks.ts's DEGRADING_PROVIDER_STATES) and must
|
|
98
|
+
* never be reused to mean "not applicable", or a fully-healthy single-provider `awm doctor`
|
|
99
|
+
* run (e.g. claude-code-only, opencode-only, antigravity-only) would always render
|
|
100
|
+
* inapplicable rows as red ✖ and `overall` could never be 'healthy'.
|
|
101
|
+
*/
|
|
102
|
+
function agentsNativeCheck(agent) {
|
|
103
|
+
const provider = (0, providers_1.providerFor)(agent);
|
|
104
|
+
if (!provider.agent)
|
|
105
|
+
return null;
|
|
106
|
+
const dir = provider.agent.global;
|
|
107
|
+
let entries;
|
|
108
|
+
try {
|
|
109
|
+
entries = fs_1.default.readdirSync(dir);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return { id: 'agents.native', state: 'absent', target: dir };
|
|
113
|
+
}
|
|
114
|
+
if (entries.length === 0)
|
|
115
|
+
return { id: 'agents.native', state: 'absent', target: dir };
|
|
116
|
+
if (provider.agent.renderer === 'codex-agent-toml') {
|
|
117
|
+
const { broken } = tomlAgentsHealthy(dir, entries);
|
|
118
|
+
return {
|
|
119
|
+
id: 'agents.native',
|
|
120
|
+
state: broken > 0 ? 'broken' : 'healthy',
|
|
121
|
+
target: dir,
|
|
122
|
+
detail: broken > 0 ? `${broken} malformed .toml` : undefined,
|
|
123
|
+
remediationCode: broken > 0 ? 'reinstall-native-agents' : undefined,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return { id: 'agents.native', state: 'healthy', target: dir };
|
|
127
|
+
}
|
|
128
|
+
function hookTrustCheck(agent) {
|
|
129
|
+
const provider = (0, providers_1.providerFor)(agent);
|
|
130
|
+
if (!provider.hooks)
|
|
131
|
+
return null;
|
|
132
|
+
let status;
|
|
133
|
+
try {
|
|
134
|
+
status = (0, status_1.computeHookStatus)(agent);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return { id: 'hook.trust', state: 'absent', remediationCode: 'awm-init' };
|
|
138
|
+
}
|
|
139
|
+
if (status.overall === 'NOT_INSTALLED') {
|
|
140
|
+
return { id: 'hook.trust', state: 'absent', remediationCode: 'awm-init' };
|
|
141
|
+
}
|
|
142
|
+
if (status.trust) {
|
|
143
|
+
// Codex: 'pending-trust' | 'healthy' | 'stale' map directly onto ProviderCheckState.
|
|
144
|
+
return {
|
|
145
|
+
id: 'hook.trust',
|
|
146
|
+
state: status.trust,
|
|
147
|
+
remediationCode: status.trust === 'pending-trust' ? 'open-hooks-trust' : undefined,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return { id: 'hook.trust', state: status.overall === 'HEALTHY' ? 'healthy' : 'broken' };
|
|
151
|
+
}
|
|
152
|
+
/** R7: reflects the provider's global context-delivery mechanism (config-instructions /
|
|
153
|
+
* managed-agents-md). claude-code's context rides the SessionStart hook — already
|
|
154
|
+
* covered by hook.trust, so this check is OMITTED (returns null) rather than reported,
|
|
155
|
+
* to avoid double-reporting the same fact as a separate, redundant row. */
|
|
156
|
+
function contextGlobalCheck(agent) {
|
|
157
|
+
const injection = (0, providers_1.providerFor)(agent).injection;
|
|
158
|
+
if (!injection || injection.type === 'cc-settings-merge') {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
let state = 'absent';
|
|
162
|
+
try {
|
|
163
|
+
const orchestrator = new orchestrator_1.InjectionOrchestrator();
|
|
164
|
+
const result = orchestrator.contextStatus({
|
|
165
|
+
agent,
|
|
166
|
+
scope: 'global',
|
|
167
|
+
registryRoot: (0, registries_1.capabilityRoot)('skills') ?? '',
|
|
168
|
+
installMethod: 'symlink',
|
|
169
|
+
profileExtensions: [],
|
|
170
|
+
});
|
|
171
|
+
state = result === 'injected' ? 'delivered' : result === 'stale' ? 'stale' : 'absent';
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
state = 'absent';
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
id: 'context.global',
|
|
178
|
+
state,
|
|
179
|
+
remediationCode: state === 'delivered' ? undefined : 'awm-init',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Builds one `ProviderFacts` row per requested agent. Physical skill directories shared
|
|
184
|
+
* between providers (OpenCode + Codex both use `~/.agents/skills`) are scanned exactly
|
|
185
|
+
* once via `scanSkills` and every owner's `skills.global` check is marked `state: 'shared'`
|
|
186
|
+
* — mirrors the dedup principle `install-planner.ts` (Task 5) already applies to writes.
|
|
187
|
+
*
|
|
188
|
+
* Named `gatherProviderChecks` (not `gatherProviderFacts`) to avoid colliding with the
|
|
189
|
+
* unrelated `gatherProviderFacts` in `core/init/provider-facts.ts` (Task 8, baseline-hash
|
|
190
|
+
* snapshot for rollback comparison — a different shape, a different purpose). No file
|
|
191
|
+
* currently imports both, but the names are close enough to trip up a future reader/editor.
|
|
192
|
+
*/
|
|
193
|
+
function gatherProviderChecks(agents, scanSkills) {
|
|
194
|
+
const ownersByDir = new Map();
|
|
195
|
+
for (const agent of agents) {
|
|
196
|
+
const dir = (0, providers_1.providerFor)(agent).skill.global;
|
|
197
|
+
const owners = ownersByDir.get(dir) ?? [];
|
|
198
|
+
owners.push(agent);
|
|
199
|
+
ownersByDir.set(dir, owners);
|
|
200
|
+
}
|
|
201
|
+
const scansByDir = new Map();
|
|
202
|
+
for (const dir of ownersByDir.keys()) {
|
|
203
|
+
scansByDir.set(dir, scanSkills(dir));
|
|
204
|
+
}
|
|
205
|
+
return agents.map((agent) => {
|
|
206
|
+
const provider = (0, providers_1.providerFor)(agent);
|
|
207
|
+
const dir = provider.skill.global;
|
|
208
|
+
const owners = ownersByDir.get(dir) ?? [agent];
|
|
209
|
+
const integrity = scansByDir.get(dir) ?? { valid: [], repairable: [], dead: [] };
|
|
210
|
+
const checks = [
|
|
211
|
+
binaryVersionCheck(agent),
|
|
212
|
+
skillsGlobalCheck(dir, owners, integrity),
|
|
213
|
+
agentsNativeCheck(agent),
|
|
214
|
+
hookTrustCheck(agent),
|
|
215
|
+
contextGlobalCheck(agent),
|
|
216
|
+
].filter((check) => check !== null);
|
|
217
|
+
return { id: agent, label: provider.label, checks };
|
|
218
|
+
});
|
|
219
|
+
}
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.matchFrontmatterBlock = matchFrontmatterBlock;
|
|
6
7
|
exports.readArtifactDescription = readArtifactDescription;
|
|
7
8
|
exports.discoverSkills = discoverSkills;
|
|
8
9
|
exports.discoverWorkflows = discoverWorkflows;
|
|
@@ -11,13 +12,18 @@ exports.discoverAgents = discoverAgents;
|
|
|
11
12
|
const fs_1 = __importDefault(require("fs"));
|
|
12
13
|
const path_1 = __importDefault(require("path"));
|
|
13
14
|
const registries_1 = require("./registries");
|
|
15
|
+
/** Extracts the raw frontmatter text (between the --- delimiters), or null if the block is missing/malformed. CRLF-tolerant. */
|
|
16
|
+
function matchFrontmatterBlock(raw) {
|
|
17
|
+
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
18
|
+
return fmMatch ? fmMatch[1] : null;
|
|
19
|
+
}
|
|
14
20
|
function readArtifactDescription(filePath) {
|
|
15
21
|
try {
|
|
16
22
|
const raw = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
17
|
-
const
|
|
18
|
-
if (
|
|
23
|
+
const frontmatter = matchFrontmatterBlock(raw);
|
|
24
|
+
if (frontmatter === null)
|
|
19
25
|
return '';
|
|
20
|
-
const line =
|
|
26
|
+
const line = frontmatter
|
|
21
27
|
.split(/\r?\n/)
|
|
22
28
|
.find((l) => /^description\s*:/.test(l));
|
|
23
29
|
if (!line)
|
|
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.removeArtifact = removeArtifact;
|
|
7
|
+
exports.stageArtifact = stageArtifact;
|
|
8
|
+
exports.replaceArtifact = replaceArtifact;
|
|
7
9
|
exports.installArtifact = installArtifact;
|
|
8
10
|
// src/core/executor.ts
|
|
9
11
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -22,20 +24,39 @@ function removeArtifact(targetPath) {
|
|
|
22
24
|
}
|
|
23
25
|
fs_1.default.rmSync(targetPath, { recursive: true, force: true });
|
|
24
26
|
}
|
|
25
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Stages `sourcePath` next to `targetPath` (same parent directory) WITHOUT
|
|
29
|
+
* touching `targetPath` itself (R17): the live target is never removed until
|
|
30
|
+
* a stage has already succeeded. Returns the staged path, ready to be swapped
|
|
31
|
+
* in via `replaceArtifact`.
|
|
32
|
+
*/
|
|
33
|
+
function stageArtifact(sourcePath, targetPath, method) {
|
|
26
34
|
if (!fs_1.default.existsSync(sourcePath)) {
|
|
27
35
|
throw new Error(`Source path does not exist: ${sourcePath}`);
|
|
28
36
|
}
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
// Clean up existing if it exists
|
|
34
|
-
fs_1.default.rmSync(targetPath, { recursive: true, force: true });
|
|
37
|
+
const parent = path_1.default.dirname(targetPath);
|
|
38
|
+
fs_1.default.mkdirSync(parent, { recursive: true });
|
|
39
|
+
const staged = path_1.default.join(parent, `.${path_1.default.basename(targetPath)}.${process.pid}.staged`);
|
|
40
|
+
fs_1.default.rmSync(staged, { recursive: true, force: true });
|
|
35
41
|
if (method === 'symlink') {
|
|
36
|
-
fs_1.default.symlinkSync(sourcePath,
|
|
42
|
+
fs_1.default.symlinkSync(sourcePath, staged, 'dir');
|
|
37
43
|
}
|
|
38
44
|
else {
|
|
39
|
-
fs_1.default.cpSync(sourcePath,
|
|
45
|
+
fs_1.default.cpSync(sourcePath, staged, { recursive: true });
|
|
40
46
|
}
|
|
47
|
+
return staged;
|
|
48
|
+
}
|
|
49
|
+
/** Atomically swaps a staged artifact into `targetPath`, replacing whatever is there. */
|
|
50
|
+
function replaceArtifact(staged, targetPath) {
|
|
51
|
+
fs_1.default.rmSync(targetPath, { recursive: true, force: true });
|
|
52
|
+
fs_1.default.renameSync(staged, targetPath);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Convenience wrapper over `stageArtifact` + `replaceArtifact` for callers
|
|
56
|
+
* that don't need transactional multi-target coordination (see
|
|
57
|
+
* install-transaction.ts's `applyInstallPlan` for the transactional path).
|
|
58
|
+
*/
|
|
59
|
+
function installArtifact(sourcePath, targetPath, method) {
|
|
60
|
+
const staged = stageArtifact(sourcePath, targetPath, method);
|
|
61
|
+
replaceArtifact(staged, targetPath);
|
|
41
62
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.EXPORT_TARGETS = void 0;
|
|
7
|
+
exports.runExport = runExport;
|
|
8
|
+
// cli/src/core/export/index.ts
|
|
9
|
+
//
|
|
10
|
+
// Orquestación del export (issue #9): resolve → adapt (override verbatim R3,
|
|
11
|
+
// o transform mecánico R3.1) → pack. Opera 100% offline (R5.2): solo fs local.
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const registries_1 = require("../registries");
|
|
15
|
+
const resolve_1 = require("./resolve");
|
|
16
|
+
const transform_1 = require("./transform");
|
|
17
|
+
const pack_1 = require("./pack");
|
|
18
|
+
exports.EXPORT_TARGETS = ['claude-ai'];
|
|
19
|
+
function runExport(opts) {
|
|
20
|
+
const target = opts.target ?? 'claude-ai';
|
|
21
|
+
if (!exports.EXPORT_TARGETS.includes(target)) {
|
|
22
|
+
throw new Error(`Unknown export target "${target}". Valid targets: ${exports.EXPORT_TARGETS.join(', ')}.`);
|
|
23
|
+
}
|
|
24
|
+
const roots = opts.roots ?? (0, registries_1.contentRoots)();
|
|
25
|
+
const outDir = path_1.default.join(opts.out ?? path_1.default.join(process.cwd(), 'awm-export'), target);
|
|
26
|
+
const resolution = (0, resolve_1.resolveExport)(opts.name, roots);
|
|
27
|
+
fs_1.default.mkdirSync(outDir, { recursive: true });
|
|
28
|
+
const exported = [];
|
|
29
|
+
let zipAvailable = true;
|
|
30
|
+
for (const skill of resolution.skills) {
|
|
31
|
+
let adapted;
|
|
32
|
+
if (skill.overridePath) {
|
|
33
|
+
adapted = fs_1.default.readFileSync(skill.overridePath, 'utf-8'); // R3: verbatim
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
const canonical = path_1.default.join(skill.dir, 'SKILL.md');
|
|
37
|
+
const raw = fs_1.default.readFileSync(canonical, 'utf-8');
|
|
38
|
+
try {
|
|
39
|
+
adapted = (0, transform_1.claudeAiTransform)(raw, skill.name);
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
throw new Error(`${canonical}: ${e instanceof Error ? e.message : String(e)}`); // R3.4 cita el archivo
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const packed = (0, pack_1.packSkill)({ name: skill.name, adaptedSkillMd: adapted, srcDir: skill.dir, targetRoot: outDir, zip: opts.zip });
|
|
46
|
+
if (packed.zipMissing)
|
|
47
|
+
zipAvailable = false;
|
|
48
|
+
exported.push({ name: skill.name, dir: packed.dir, zip: packed.zip });
|
|
49
|
+
}
|
|
50
|
+
return { outDir, kind: resolution.kind, exported, skipped: resolution.skipped, zipAvailable };
|
|
51
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.defaultZip = void 0;
|
|
7
|
+
exports.packSkill = packSkill;
|
|
8
|
+
// cli/src/core/export/pack.ts
|
|
9
|
+
//
|
|
10
|
+
// Escritura determinística del artefacto (R4: limpia su propio subárbol antes
|
|
11
|
+
// de escribir) + zip por capas con binario del sistema (R4.1/R4.2). ZipFn es
|
|
12
|
+
// inyectable para tests.
|
|
13
|
+
const fs_1 = __importDefault(require("fs"));
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const child_process_1 = require("child_process");
|
|
16
|
+
/** Refuses symlinks anywhere in the tree — copying/zipping them could dereference
|
|
17
|
+
* into content outside the registry (info-leak) or embed a broken/unexpected
|
|
18
|
+
* link for the recipient. Exported artifacts are plain files only. */
|
|
19
|
+
function assertNoSymlinks(dir) {
|
|
20
|
+
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
21
|
+
const full = path_1.default.join(dir, entry.name);
|
|
22
|
+
if (entry.isSymbolicLink()) {
|
|
23
|
+
throw new Error(`Refusing to export "${full}": symlinks are not supported in exported artifacts (could leak file content via zip dereferencing, or resolve unexpectedly for the recipient).`);
|
|
24
|
+
}
|
|
25
|
+
if (entry.isDirectory())
|
|
26
|
+
assertNoSymlinks(full);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Capa 1: binario `zip` del sistema. ENOENT → missing (capa 2: carpeta). */
|
|
30
|
+
const defaultZip = (cwd, zipName, folderName) => {
|
|
31
|
+
const r = (0, child_process_1.spawnSync)('zip', ['-r', '-q', zipName, folderName], { cwd });
|
|
32
|
+
if (r.error && r.error.code === 'ENOENT') {
|
|
33
|
+
return { ok: false, missing: true };
|
|
34
|
+
}
|
|
35
|
+
return { ok: r.status === 0, missing: false };
|
|
36
|
+
};
|
|
37
|
+
exports.defaultZip = defaultZip;
|
|
38
|
+
function packSkill(opts) {
|
|
39
|
+
const zip = opts.zip ?? exports.defaultZip;
|
|
40
|
+
const skillOut = path_1.default.join(opts.targetRoot, opts.name);
|
|
41
|
+
const zipPath = path_1.default.join(opts.targetRoot, `${opts.name}.zip`);
|
|
42
|
+
// R4: determinismo — el re-export limpia su propio subárbol primero.
|
|
43
|
+
fs_1.default.rmSync(skillOut, { recursive: true, force: true });
|
|
44
|
+
fs_1.default.rmSync(zipPath, { force: true });
|
|
45
|
+
fs_1.default.mkdirSync(skillOut, { recursive: true });
|
|
46
|
+
fs_1.default.writeFileSync(path_1.default.join(skillOut, 'SKILL.md'), opts.adaptedSkillMd);
|
|
47
|
+
const refs = path_1.default.join(opts.srcDir, 'references');
|
|
48
|
+
if (fs_1.default.existsSync(refs)) {
|
|
49
|
+
assertNoSymlinks(refs);
|
|
50
|
+
fs_1.default.cpSync(refs, path_1.default.join(skillOut, 'references'), { recursive: true }); // R3.2 byte-idéntico
|
|
51
|
+
}
|
|
52
|
+
const zr = zip(opts.targetRoot, `${opts.name}.zip`, opts.name);
|
|
53
|
+
if (zr.missing)
|
|
54
|
+
return { dir: skillOut, zip: null, zipMissing: true };
|
|
55
|
+
if (!zr.ok)
|
|
56
|
+
throw new Error(`zip failed for skill "${opts.name}" (non-zero exit).`);
|
|
57
|
+
return { dir: skillOut, zip: zipPath, zipMissing: false };
|
|
58
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveExport = resolveExport;
|
|
7
|
+
// cli/src/core/export/resolve.ts
|
|
8
|
+
//
|
|
9
|
+
// Resolución <nombre> → skills a exportar (R1/R1.1) con gate de portabilidad
|
|
10
|
+
// (R2.x) y consistencia de override (R3.3). Lee SIEMPRE de content roots del
|
|
11
|
+
// registry instalado — nunca de ~/.claude/skills.
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const bundles_1 = require("../bundles");
|
|
15
|
+
const discovery_1 = require("../discovery");
|
|
16
|
+
const OVERRIDE_FILE = 'port.claude-ai.md';
|
|
17
|
+
/** portable: true en el frontmatter (bloque --- inicial), CRLF-tolerant como readArtifactDescription en discovery.ts. */
|
|
18
|
+
function isPortable(skillMd) {
|
|
19
|
+
const frontmatter = (0, discovery_1.matchFrontmatterBlock)(skillMd);
|
|
20
|
+
if (frontmatter === null)
|
|
21
|
+
return false;
|
|
22
|
+
return /^portable\s*:\s*true\s*$/m.test(frontmatter);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Delegates skill location to discoverSkills(roots) so the SAME collision/override
|
|
26
|
+
* contract used by bundles/workflows/agents applies here (R3): a same-named skill in
|
|
27
|
+
* two roots throws unless the later root's awm-registry.json declares it in "overrides",
|
|
28
|
+
* in which case the later root's version wins. Locating manually (first-root-wins) would
|
|
29
|
+
* silently resolve to the wrong registry's version — see Finding 1.
|
|
30
|
+
*/
|
|
31
|
+
function locate(skillName, roots) {
|
|
32
|
+
const found = (0, discovery_1.discoverSkills)(roots).find((s) => s.name === skillName);
|
|
33
|
+
if (!found)
|
|
34
|
+
return null;
|
|
35
|
+
const dir = found.path;
|
|
36
|
+
const skillFile = path_1.default.join(dir, 'SKILL.md');
|
|
37
|
+
const overridePath = fs_1.default.existsSync(path_1.default.join(dir, OVERRIDE_FILE))
|
|
38
|
+
? path_1.default.join(dir, OVERRIDE_FILE) : null;
|
|
39
|
+
return { name: skillName, dir, portable: isPortable(fs_1.default.readFileSync(skillFile, 'utf-8')), overridePath };
|
|
40
|
+
}
|
|
41
|
+
/** R3.3: un override declara intención de export; sin portable es contrato a medias. */
|
|
42
|
+
function assertOverrideConsistency(s) {
|
|
43
|
+
if (s.overridePath && !s.portable) {
|
|
44
|
+
throw new Error(`Inconsistent metadata for skill "${s.name}": ${OVERRIDE_FILE} exists but SKILL.md does not declare portable: true.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function resolveExport(requested, roots) {
|
|
48
|
+
const bundles = (0, bundles_1.discoverAllBundles)(roots);
|
|
49
|
+
if (bundles.some((b) => b.name === requested)) {
|
|
50
|
+
const skills = [];
|
|
51
|
+
const skipped = [];
|
|
52
|
+
for (const name of (0, bundles_1.resolveBundleSkills)(requested, bundles)) {
|
|
53
|
+
const s = locate(name, roots);
|
|
54
|
+
if (!s)
|
|
55
|
+
throw new Error(`Bundle "${requested}" lists skill "${name}" but no content root contains skills/${name}/SKILL.md.`);
|
|
56
|
+
assertOverrideConsistency(s);
|
|
57
|
+
if (s.portable)
|
|
58
|
+
skills.push(s);
|
|
59
|
+
else
|
|
60
|
+
skipped.push(s.name);
|
|
61
|
+
}
|
|
62
|
+
if (skills.length === 0) {
|
|
63
|
+
throw new Error(`Bundle "${requested}" has no portable skills — nothing to export. Mark skills with portable: true in their frontmatter.`);
|
|
64
|
+
}
|
|
65
|
+
return { kind: 'bundle', requested, skills, skipped: skipped.sort() };
|
|
66
|
+
}
|
|
67
|
+
const single = locate(requested, roots);
|
|
68
|
+
if (!single) {
|
|
69
|
+
const available = bundles.map((b) => b.name).join(', ') || '(none)';
|
|
70
|
+
throw new Error(`"${requested}" is neither a bundle nor a skill in any content root. Available bundles: ${available}.`);
|
|
71
|
+
}
|
|
72
|
+
assertOverrideConsistency(single);
|
|
73
|
+
if (!single.portable) {
|
|
74
|
+
throw new Error(`Skill "${requested}" is not portable (no portable: true in its frontmatter) — it likely depends on filesystem/git and would break on claude.ai.`);
|
|
75
|
+
}
|
|
76
|
+
return { kind: 'skill', requested, skills: [single], skipped: [] };
|
|
77
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFERENCE_LINE = void 0;
|
|
4
|
+
exports.claudeAiTransform = claudeAiTransform;
|
|
5
|
+
// cli/src/core/export/transform.ts
|
|
6
|
+
//
|
|
7
|
+
// Transform mecánico claude.ai (R3.1): función pura string → string.
|
|
8
|
+
// Frontmatter line-based plano (los SKILL.md del baseline usan claves de una
|
|
9
|
+
// línea) — sin parser YAML a propósito (YAGNI, cero deps).
|
|
10
|
+
const DEFERENCE_LINE = (skillName) => `In environments with AWM installed (Claude Code), defer to the registry's ${skillName} skill — this port is for environments without filesystem access.`;
|
|
11
|
+
exports.DEFERENCE_LINE = DEFERENCE_LINE;
|
|
12
|
+
function claudeAiTransform(skillMd, skillName) {
|
|
13
|
+
// \r?\n-tolerant, same rationale as readArtifactDescription in discovery.ts:
|
|
14
|
+
// SKILL.md files may be CRLF-terminated and that's still valid frontmatter.
|
|
15
|
+
const startMatch = skillMd.match(/^---\r?\n/);
|
|
16
|
+
if (!startMatch) {
|
|
17
|
+
throw new Error('missing frontmatter block (file must start with ---)');
|
|
18
|
+
}
|
|
19
|
+
const startLen = startMatch[0].length;
|
|
20
|
+
const endMatch = skillMd.slice(startLen).match(/\r?\n---\r?\n/);
|
|
21
|
+
if (!endMatch || endMatch.index === undefined) {
|
|
22
|
+
throw new Error('unterminated frontmatter block (closing --- not found)');
|
|
23
|
+
}
|
|
24
|
+
const end = startLen + endMatch.index;
|
|
25
|
+
const body = skillMd.slice(end + endMatch[0].length);
|
|
26
|
+
const fmLines = skillMd.slice(startLen, end).split(/\r?\n/)
|
|
27
|
+
.filter((l) => !/^(version|portable):/.test(l));
|
|
28
|
+
const descIdx = fmLines.findIndex((l) => /^description:/.test(l));
|
|
29
|
+
if (descIdx === -1) {
|
|
30
|
+
throw new Error('frontmatter has no description field');
|
|
31
|
+
}
|
|
32
|
+
const descLine = fmLines[descIdx];
|
|
33
|
+
const value = descLine.slice('description:'.length).trim();
|
|
34
|
+
if (value === '' || value === '>' || value === '|' || value.startsWith('>') || value.startsWith('|')) {
|
|
35
|
+
throw new Error('description must be single-line (block scalars are not supported by the export transform)');
|
|
36
|
+
}
|
|
37
|
+
const deference = (0, exports.DEFERENCE_LINE)(skillName);
|
|
38
|
+
// Quote-style detection mirrors readArtifactDescription in discovery.ts: both
|
|
39
|
+
// single- and double-quoted scalars are first-class, and we work off the
|
|
40
|
+
// trimmed value so trailing whitespace after a closing quote doesn't fool us.
|
|
41
|
+
const isDoubleQuoted = value.length >= 2 && value.startsWith('"') && value.endsWith('"');
|
|
42
|
+
const isSingleQuoted = value.length >= 2 && value.startsWith("'") && value.endsWith("'");
|
|
43
|
+
if ((value.startsWith('"') || value.startsWith("'")) && !isDoubleQuoted && !isSingleQuoted) {
|
|
44
|
+
throw new Error('description has trailing content after its closing quote (e.g. an inline comment) — not supported by the export transform; remove the comment or use a port.claude-ai.md override');
|
|
45
|
+
}
|
|
46
|
+
// YAML single-quoted scalars escape a literal ' by doubling it (''); the
|
|
47
|
+
// deference text ("...registry's..." — see DEFERENCE_LINE) contains an
|
|
48
|
+
// apostrophe, so it must be escaped before splicing into a single-quoted
|
|
49
|
+
// description or it would prematurely close the YAML string.
|
|
50
|
+
const newValue = isDoubleQuoted
|
|
51
|
+
? `${value.slice(0, -1)} ${deference}"`
|
|
52
|
+
: isSingleQuoted
|
|
53
|
+
? `${value.slice(0, -1)} ${deference.replace(/'/g, "''")}'`
|
|
54
|
+
: `${value} ${deference}`;
|
|
55
|
+
fmLines[descIdx] = `description: ${newValue}`;
|
|
56
|
+
return `---\n${fmLines.join('\n')}\n---\n${body}`;
|
|
57
|
+
}
|