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.
Files changed (103) hide show
  1. package/dist/src/commands/add.js +84 -0
  2. package/dist/src/commands/agent.js +95 -0
  3. package/dist/src/commands/backup.js +46 -0
  4. package/dist/src/commands/doctor.js +80 -4
  5. package/dist/src/commands/export.js +48 -0
  6. package/dist/src/commands/hooks/claude.js +192 -0
  7. package/dist/src/commands/hooks/codex.js +191 -0
  8. package/dist/src/commands/hooks/index.js +23 -8
  9. package/dist/src/commands/hooks/install.js +11 -107
  10. package/dist/src/commands/hooks/resync.js +28 -18
  11. package/dist/src/commands/hooks/shared.js +94 -0
  12. package/dist/src/commands/hooks/status.js +13 -64
  13. package/dist/src/commands/hooks/uninstall.js +11 -47
  14. package/dist/src/commands/init.js +90 -28
  15. package/dist/src/commands/registry/index.js +1 -1
  16. package/dist/src/commands/sync.js +113 -0
  17. package/dist/src/commands/update.js +107 -0
  18. package/dist/src/core/agent-targets.js +50 -0
  19. package/dist/src/core/artifact-state.js +65 -0
  20. package/dist/src/core/atomic-file.js +72 -0
  21. package/dist/src/core/bundle-install.js +60 -38
  22. package/dist/src/core/bundles.js +21 -0
  23. package/dist/src/core/context/managed-block.js +190 -0
  24. package/dist/src/core/context/orchestrator.js +3 -1
  25. package/dist/src/core/context/project-constitution-inject.js +1 -1
  26. package/dist/src/core/context/regenerate.js +3 -3
  27. package/dist/src/core/context/strategies/codex-agents.js +100 -0
  28. package/dist/src/core/diagnostics/checks.js +16 -0
  29. package/dist/src/core/diagnostics/context.js +37 -8
  30. package/dist/src/core/diagnostics/provider-checks.js +219 -0
  31. package/dist/src/core/discovery.js +9 -3
  32. package/dist/src/core/executor.js +30 -9
  33. package/dist/src/core/export/index.js +51 -0
  34. package/dist/src/core/export/pack.js +58 -0
  35. package/dist/src/core/export/resolve.js +77 -0
  36. package/dist/src/core/export/transform.js +57 -0
  37. package/dist/src/core/export/types.js +2 -0
  38. package/dist/src/core/init/mutation-targets.js +139 -0
  39. package/dist/src/core/init/provider-facts.js +164 -0
  40. package/dist/src/core/init/steps.js +65 -9
  41. package/dist/src/core/install-planner.js +206 -0
  42. package/dist/src/core/install-transaction.js +405 -0
  43. package/dist/src/core/profile.js +1 -1
  44. package/dist/src/core/provider-artifacts.js +54 -0
  45. package/dist/src/core/provider-version.js +36 -0
  46. package/dist/src/core/reconciliation.js +61 -0
  47. package/dist/src/core/registries.js +10 -0
  48. package/dist/src/core/renderers/canonical-agent.js +27 -0
  49. package/dist/src/core/renderers/codex-agent.js +60 -0
  50. package/dist/src/core/skill-integrity.js +4 -3
  51. package/dist/src/index.js +99 -289
  52. package/dist/src/providers/index.js +143 -39
  53. package/dist/src/ui/provider-preflight.js +26 -0
  54. package/dist/src/utils/config.js +101 -9
  55. package/dist/tests/commands/agent.test.js +103 -0
  56. package/dist/tests/commands/backup.test.js +109 -0
  57. package/dist/tests/commands/doctor.test.js +65 -4
  58. package/dist/tests/commands/export.test.js +64 -0
  59. package/dist/tests/commands/hooks/codex.test.js +230 -0
  60. package/dist/tests/commands/hooks/install.test.js +20 -1
  61. package/dist/tests/commands/hooks/resync.test.js +53 -6
  62. package/dist/tests/commands/hooks/status.test.js +10 -0
  63. package/dist/tests/commands/hooks/uninstall.test.js +12 -0
  64. package/dist/tests/commands/init.test.js +195 -8
  65. package/dist/tests/commands/multi-agent-targeting.test.js +231 -0
  66. package/dist/tests/core/agent-targets.test.js +45 -0
  67. package/dist/tests/core/artifact-state.test.js +144 -0
  68. package/dist/tests/core/atomic-file.test.js +96 -0
  69. package/dist/tests/core/bundle-install.test.js +208 -17
  70. package/dist/tests/core/bundles.test.js +12 -0
  71. package/dist/tests/core/context/managed-block.test.js +90 -0
  72. package/dist/tests/core/context/orchestrator.test.js +48 -3
  73. package/dist/tests/core/context/regenerate.test.js +1 -1
  74. package/dist/tests/core/context/strategies/codex-agents.test.js +185 -0
  75. package/dist/tests/core/context/strategies/config-instructions.test.js +1 -1
  76. package/dist/tests/core/diagnostics/checks.test.js +19 -0
  77. package/dist/tests/core/diagnostics/context.test.js +74 -0
  78. package/dist/tests/core/diagnostics/provider-checks.test.js +252 -0
  79. package/dist/tests/core/executor.test.js +20 -0
  80. package/dist/tests/core/export/engine.test.js +104 -0
  81. package/dist/tests/core/export/pack.test.js +122 -0
  82. package/dist/tests/core/export/resolve.test.js +78 -0
  83. package/dist/tests/core/export/transform.test.js +74 -0
  84. package/dist/tests/core/init/mutation-targets.test.js +235 -0
  85. package/dist/tests/core/init/orchestrator.test.js +1 -1
  86. package/dist/tests/core/init/provider-facts.test.js +129 -0
  87. package/dist/tests/core/init/steps.test.js +106 -2
  88. package/dist/tests/core/install-planner.test.js +224 -0
  89. package/dist/tests/core/install-transaction.test.js +257 -0
  90. package/dist/tests/core/provider-artifacts.test.js +64 -0
  91. package/dist/tests/core/provider-version.test.js +53 -0
  92. package/dist/tests/core/reconciliation.test.js +165 -0
  93. package/dist/tests/core/registries-sync.test.js +4 -4
  94. package/dist/tests/core/renderers/canonical-agent.test.js +58 -0
  95. package/dist/tests/core/renderers/codex-agent.test.js +128 -0
  96. package/dist/tests/core/versioning.test.js +1 -1
  97. package/dist/tests/integration/codex-provider-isolated.test.js +206 -0
  98. package/dist/tests/providers/hooks-config.test.js +43 -25
  99. package/dist/tests/providers/index.test.js +144 -39
  100. package/dist/tests/structural/codex-agent-escaping-completeness.test.js +77 -0
  101. package/dist/tests/ui/provider-preflight.test.js +25 -0
  102. package/dist/tests/utils/config.test.js +178 -18
  103. 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.execSync)('git status --porcelain', { cwd: repoDir, stdio: ['ignore', 'pipe', 'ignore'] })
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.execSync)('git rev-list --count HEAD..@{u}', { cwd: repoDir, stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000 })
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 Object.keys(providers_1.PROVIDERS)) {
71
- const inj = providers_1.PROVIDERS[agent].injection;
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)('claude-code');
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.PROVIDERS[agent].skill.global;
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.PROVIDERS[agent].skill.local);
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 fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
18
- if (!fmMatch)
23
+ const frontmatter = matchFrontmatterBlock(raw);
24
+ if (frontmatter === null)
19
25
  return '';
20
- const line = fmMatch[1]
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
- function installArtifact(sourcePath, targetPath, method) {
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 parentDir = path_1.default.dirname(targetPath);
30
- if (!fs_1.default.existsSync(parentDir)) {
31
- fs_1.default.mkdirSync(parentDir, { recursive: true });
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, targetPath, 'dir');
42
+ fs_1.default.symlinkSync(sourcePath, staged, 'dir');
37
43
  }
38
44
  else {
39
- fs_1.default.cpSync(sourcePath, targetPath, { recursive: true });
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
+ }