@aiwg/cli 2026.8.0 → 2026.8.2
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/README.md +33 -0
- package/agentic/code/providers/capability-matrix.yaml +511 -0
- package/agentic/code/providers/model-capabilities.v1.json +120 -0
- package/agentic/code/providers/model-catalog.v1.json +96 -0
- package/agentic/code/providers/model-policy-evaluations.v1.json +50 -0
- package/agentic/code/providers/premium-model-allowlist.v1.json +36 -0
- package/bin/aiwg.mjs +14 -10
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/cli.js +2 -0
- package/dist/src/artifacts/types.js +4 -0
- package/dist/src/auth/client.js +209 -0
- package/dist/src/auth/config.js +38 -0
- package/dist/src/auth/credential-store.js +141 -0
- package/dist/src/auth/resource-credentials.js +25 -0
- package/dist/src/auth/types.js +2 -0
- package/dist/src/channel/manager.mjs +5 -5
- package/dist/src/cli/handlers/auth.js +125 -0
- package/dist/src/cli/handlers/help.js +1 -0
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/install.js +42 -4
- package/dist/src/cli/handlers/marketplace.js +375 -122
- package/dist/src/cli/handlers/resource-versions.js +2 -0
- package/dist/src/cli/handlers/sessions.js +23 -5
- package/dist/src/cli/handlers/subcommands.js +10 -1
- package/dist/src/cli/handlers/use.js +342 -43
- package/dist/src/config/gitignore.js +1 -0
- package/dist/src/extensions/commands/definitions.js +19 -0
- package/dist/src/marketplace/exchange.js +602 -0
- package/dist/src/marketplace/provenance-types.js +19 -0
- package/dist/src/marketplace/provenance.js +834 -0
- package/dist/src/memory/canonical-context.js +342 -0
- package/dist/src/memory/context-pack.js +282 -0
- package/dist/src/memory/index.js +4 -0
- package/dist/src/memory/intake.js +118 -0
- package/dist/src/packages/adapters/git.js +79 -29
- package/dist/src/packages/package-discovery.js +81 -0
- package/dist/src/packages/package-registry.js +2 -0
- package/dist/src/packages/registry.js +119 -20
- package/dist/src/resources/resolver.js +1 -0
- package/dist/src/resources/web-release.d.ts +3 -1
- package/dist/src/resources/web-release.js +14 -6
- package/dist/src/serve/agentic-sandbox-fleet-client.js +213 -0
- package/dist/src/serve/fleet-mission-conductor.js +293 -0
- package/dist/src/sessions/index.js +1 -0
- package/dist/src/sessions/output-registration.js +338 -0
- package/dist/src/sessions/promotion.js +73 -2
- package/dist/src/sessions/repository.js +2 -1
- package/dist/src/update/notifier.mjs +13 -2
- package/package.json +8 -1
- package/tools/_resolve-impl.mjs +74 -0
- package/tools/agents/deploy-agents.mjs +962 -0
- package/tools/agents/providers/base.mjs +2954 -0
- package/tools/agents/providers/claude.mjs +711 -0
- package/tools/agents/providers/codex.mjs +699 -0
- package/tools/agents/providers/copilot.mjs +659 -0
- package/tools/agents/providers/cursor.mjs +714 -0
- package/tools/agents/providers/factory.mjs +1130 -0
- package/tools/agents/providers/hermes.mjs +663 -0
- package/tools/agents/providers/hook-capabilities.mjs +85 -0
- package/tools/agents/providers/model-role.mjs +56 -0
- package/tools/agents/providers/openclaw-translator.mjs +348 -0
- package/tools/agents/providers/openclaw.mjs +680 -0
- package/tools/agents/providers/opencode.mjs +675 -0
- package/tools/agents/providers/openhuman.mjs +292 -0
- package/tools/agents/providers/warp.mjs +413 -0
- package/tools/agents/providers/windsurf.mjs +748 -0
- package/tools/commands/deploy-prompts-codex.mjs +336 -0
- package/tools/plugin/package-plugins.mjs +1013 -0
- package/tools/skills/deploy-skills-codex.mjs +571 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Deploy Skills to Codex
|
|
4
|
+
*
|
|
5
|
+
* Transforms AIWG skills to Codex format. The codex provider
|
|
6
|
+
* (tools/agents/providers/codex.mjs) always invokes this with an explicit
|
|
7
|
+
* `--target <project>/.agents/skills` — the cross-provider canonical path
|
|
8
|
+
* codex-rs scans (codex-rs/core-skills/src/loader.rs).
|
|
9
|
+
*
|
|
10
|
+
* The `~/.codex/skills/` default below is the LEGACY home-dir path, deprecated
|
|
11
|
+
* after the #766 regression fix: writing both .agents/skills and
|
|
12
|
+
* ~/.codex/skills made codex list every kernel skill twice. It is retained
|
|
13
|
+
* only as a standalone-invocation fallback; normal `aiwg use` deploys pass
|
|
14
|
+
* --target and never write the legacy location.
|
|
15
|
+
*
|
|
16
|
+
* Codex Skill Format:
|
|
17
|
+
* - Location: <target>/<skill-name>/SKILL.md
|
|
18
|
+
* - YAML frontmatter: name (≤100 chars), description (≤500 chars)
|
|
19
|
+
* - Body: Instructions (kept on disk, not injected into context)
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* node tools/skills/deploy-skills-codex.mjs [options]
|
|
23
|
+
*
|
|
24
|
+
* Options:
|
|
25
|
+
* --source <path> Source directory (defaults to repo root)
|
|
26
|
+
* --target <path> Target directory (defaults to ~/.codex/skills — LEGACY;
|
|
27
|
+
* orchestrator passes <project>/.agents/skills)
|
|
28
|
+
* --mode <type> Deployment mode: addons, sdlc, marketing, media-curator, research, or all (default)
|
|
29
|
+
* --dry-run Show what would be deployed without writing
|
|
30
|
+
* --force Overwrite existing files
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import realFs from 'fs';
|
|
34
|
+
import { createRequire } from 'module';
|
|
35
|
+
const _require = createRequire(import.meta.url);
|
|
36
|
+
let fs;
|
|
37
|
+
try { const gfs = _require('graceful-fs'); gfs.gracefulify(realFs); fs = realFs; } catch { fs = realFs; }
|
|
38
|
+
import path from 'path';
|
|
39
|
+
import os from 'os';
|
|
40
|
+
import { getFrameworksForMode, normalizeDeploymentMode, skillMatchesProvider, isKernelSkill } from '../agents/providers/base.mjs';
|
|
41
|
+
|
|
42
|
+
const CODEX_SKILLS_DIR = path.join(os.homedir(), '.codex', 'skills');
|
|
43
|
+
const MAX_NAME_LENGTH = 100;
|
|
44
|
+
const MAX_DESCRIPTION_LENGTH = 500;
|
|
45
|
+
const LEGACY_RENAMED_SKILLS = new Set(['aiwg-mcp']);
|
|
46
|
+
|
|
47
|
+
function parseArgs() {
|
|
48
|
+
const args = process.argv.slice(2);
|
|
49
|
+
const cfg = {
|
|
50
|
+
source: null,
|
|
51
|
+
target: CODEX_SKILLS_DIR,
|
|
52
|
+
mode: 'all',
|
|
53
|
+
dryRun: false,
|
|
54
|
+
force: false,
|
|
55
|
+
copyStandardSkills: false,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < args.length; i++) {
|
|
59
|
+
const a = args[i];
|
|
60
|
+
if (a === '--source' && args[i + 1]) cfg.source = path.resolve(args[++i]);
|
|
61
|
+
else if (a === '--target' && args[i + 1]) cfg.target = path.resolve(args[++i]);
|
|
62
|
+
else if (a === '--mode' && args[i + 1]) cfg.mode = String(args[++i]).toLowerCase();
|
|
63
|
+
else if (a === '--dry-run') cfg.dryRun = true;
|
|
64
|
+
else if (a === '--force') cfg.force = true;
|
|
65
|
+
else if (a === '--copy-all' || a === '--copy-standard-skills') cfg.copyStandardSkills = true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
cfg.mode = normalizeDeploymentMode(cfg.mode);
|
|
69
|
+
return cfg;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function ensureDir(d) {
|
|
73
|
+
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function stripWrappingQuotes(value) {
|
|
77
|
+
const trimmed = String(value ?? '').trim();
|
|
78
|
+
if (
|
|
79
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
80
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
81
|
+
) {
|
|
82
|
+
return trimmed.slice(1, -1);
|
|
83
|
+
}
|
|
84
|
+
return trimmed;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function yamlDoubleQuoted(value) {
|
|
88
|
+
return String(value ?? '')
|
|
89
|
+
.replace(/\\/g, '\\\\')
|
|
90
|
+
.replace(/"/g, '\\"')
|
|
91
|
+
.replace(/\r?\n/g, ' ')
|
|
92
|
+
.trim();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function codexDisplayName(skillName) {
|
|
96
|
+
const acronyms = new Map([
|
|
97
|
+
['aiwg', 'AIWG'],
|
|
98
|
+
['dfir', 'DFIR'],
|
|
99
|
+
['sdlc', 'SDLC'],
|
|
100
|
+
['mcp', 'MCP'],
|
|
101
|
+
['pr', 'PR'],
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
return String(skillName)
|
|
105
|
+
.split('-')
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
.map((part) => acronyms.get(part.toLowerCase()) || part.charAt(0).toUpperCase() + part.slice(1))
|
|
108
|
+
.join(' ');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function codexOpenAiMetadata(skill) {
|
|
112
|
+
return `interface:
|
|
113
|
+
display_name: "${yamlDoubleQuoted(codexDisplayName(skill.name))}"
|
|
114
|
+
short_description: "${yamlDoubleQuoted(skill.description)}"
|
|
115
|
+
`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveSkillModelPolicy(frontmatter) {
|
|
119
|
+
const block = frontmatter.match(/^commandHint:\s*\n((?:[ \t]+[^\n]*\n?)*)/m)?.[1] || '';
|
|
120
|
+
const hint = {};
|
|
121
|
+
for (const line of block.split('\n')) {
|
|
122
|
+
const match = line.trim().match(/^(model|modelRole|modelTier|modelEffort|modelRationale):\s*(.+)$/);
|
|
123
|
+
if (match) hint[match[1]] = stripWrappingQuotes(match[2]);
|
|
124
|
+
}
|
|
125
|
+
const legacy = String(hint.model || '').toLowerCase();
|
|
126
|
+
const role = hint.modelRole || (legacy === 'opus' ? 'reasoning'
|
|
127
|
+
: legacy === 'haiku' ? 'efficiency' : legacy ? 'coding' : null);
|
|
128
|
+
if (!role) return null;
|
|
129
|
+
return {
|
|
130
|
+
role,
|
|
131
|
+
tier: hint.modelTier || (role === 'reasoning' ? 'premium'
|
|
132
|
+
: role === 'efficiency' ? 'economy' : 'standard'),
|
|
133
|
+
effort: hint.modelEffort,
|
|
134
|
+
rationale: hint.modelRationale,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Find skill directories containing SKILL.md
|
|
140
|
+
*/
|
|
141
|
+
function findSkillDirs(baseDir) {
|
|
142
|
+
if (!fs.existsSync(baseDir)) return [];
|
|
143
|
+
|
|
144
|
+
const skillDirs = [];
|
|
145
|
+
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
146
|
+
|
|
147
|
+
for (const entry of entries) {
|
|
148
|
+
if (entry.isDirectory()) {
|
|
149
|
+
const skillPath = path.join(baseDir, entry.name, 'SKILL.md');
|
|
150
|
+
if (fs.existsSync(skillPath)) {
|
|
151
|
+
skillDirs.push(path.join(baseDir, entry.name));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return skillDirs;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Parse AIWG SKILL.md - handles both frontmatter and non-frontmatter formats
|
|
161
|
+
*/
|
|
162
|
+
function parseSkillContent(content, skillName) {
|
|
163
|
+
// Try YAML frontmatter format first
|
|
164
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
165
|
+
if (fmMatch) {
|
|
166
|
+
const [, frontmatter, body] = fmMatch;
|
|
167
|
+
const metadata = {};
|
|
168
|
+
|
|
169
|
+
// Parse YAML-like frontmatter
|
|
170
|
+
for (const line of frontmatter.split('\n')) {
|
|
171
|
+
const colonIdx = line.indexOf(':');
|
|
172
|
+
if (colonIdx > 0) {
|
|
173
|
+
const key = line.slice(0, colonIdx).trim();
|
|
174
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
175
|
+
metadata[key] = stripWrappingQuotes(value);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { metadata, body, modelPolicy: resolveSkillModelPolicy(frontmatter) };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Fallback: Parse non-frontmatter format (# skill-name header)
|
|
183
|
+
const lines = content.split('\n');
|
|
184
|
+
let name = skillName;
|
|
185
|
+
let description = '';
|
|
186
|
+
let bodyStartIdx = 0;
|
|
187
|
+
|
|
188
|
+
// Look for # header as name
|
|
189
|
+
for (let i = 0; i < lines.length; i++) {
|
|
190
|
+
const line = lines[i].trim();
|
|
191
|
+
if (line.startsWith('# ')) {
|
|
192
|
+
name = line.slice(2).trim();
|
|
193
|
+
bodyStartIdx = i + 1;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Look for first paragraph as description (skip empty lines)
|
|
199
|
+
for (let i = bodyStartIdx; i < lines.length; i++) {
|
|
200
|
+
const line = lines[i].trim();
|
|
201
|
+
if (
|
|
202
|
+
line &&
|
|
203
|
+
!line.endsWith(':') &&
|
|
204
|
+
!line.startsWith('#') &&
|
|
205
|
+
!line.startsWith('-') &&
|
|
206
|
+
!line.startsWith('|')
|
|
207
|
+
) {
|
|
208
|
+
description = line;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// If description is too short, try next paragraph
|
|
214
|
+
if (description.length < 20) {
|
|
215
|
+
for (let i = bodyStartIdx; i < lines.length; i++) {
|
|
216
|
+
const line = lines[i].trim();
|
|
217
|
+
if (line.startsWith('## ') && line.toLowerCase().includes('purpose')) {
|
|
218
|
+
// Look for content after ## Purpose
|
|
219
|
+
for (let j = i + 1; j < lines.length && j < i + 10; j++) {
|
|
220
|
+
const purposeLine = lines[j].trim();
|
|
221
|
+
if (purposeLine && !purposeLine.startsWith('#') && !purposeLine.startsWith('-')) {
|
|
222
|
+
description = purposeLine;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
metadata: { name, description },
|
|
233
|
+
body: content
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Transform AIWG skill to Codex format
|
|
239
|
+
*/
|
|
240
|
+
function transformToCodexSkill(skillDir) {
|
|
241
|
+
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
242
|
+
const skillName = path.basename(skillDir);
|
|
243
|
+
const content = fs.readFileSync(skillPath, 'utf8');
|
|
244
|
+
|
|
245
|
+
// Platform filtering: skip skills with explicit restrictions that exclude codex.
|
|
246
|
+
// Skills using platforms: [all] (the standard token) always pass this check.
|
|
247
|
+
if (!skillMatchesProvider(content, 'codex')) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const parsed = parseSkillContent(content, skillName);
|
|
252
|
+
|
|
253
|
+
if (!parsed) {
|
|
254
|
+
console.warn(`Warning: Could not parse ${skillPath}`);
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const { metadata, body, modelPolicy } = parsed;
|
|
259
|
+
|
|
260
|
+
// Validate and truncate
|
|
261
|
+
const name = (metadata.name || path.basename(skillDir)).slice(0, MAX_NAME_LENGTH);
|
|
262
|
+
let description = metadata.description || '';
|
|
263
|
+
|
|
264
|
+
// Codex REQUIRES a non-empty description — it rejects SKILL.md files that
|
|
265
|
+
// lack one. Fail loudly rather than silently writing `description: ""`
|
|
266
|
+
// (the exact regression this guard defends against).
|
|
267
|
+
if (!description || !String(description).trim()) {
|
|
268
|
+
console.error(
|
|
269
|
+
`ERROR: Skill '${name}' has empty/missing description in source ${skillPath}`
|
|
270
|
+
);
|
|
271
|
+
console.error(
|
|
272
|
+
` Codex rejects SKILL.md files without a description field.`
|
|
273
|
+
);
|
|
274
|
+
console.error(
|
|
275
|
+
` Fix the source file: add a non-empty 'description:' to the frontmatter.`
|
|
276
|
+
);
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Truncate description to 500 chars, ending at word boundary
|
|
281
|
+
if (description.length > MAX_DESCRIPTION_LENGTH) {
|
|
282
|
+
description = description.slice(0, MAX_DESCRIPTION_LENGTH - 3);
|
|
283
|
+
const lastSpace = description.lastIndexOf(' ');
|
|
284
|
+
if (lastSpace > MAX_DESCRIPTION_LENGTH - 50) {
|
|
285
|
+
description = description.slice(0, lastSpace);
|
|
286
|
+
}
|
|
287
|
+
description += '...';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Final guard: never emit `description: ""` under any circumstance.
|
|
291
|
+
const quotedDescription = yamlDoubleQuoted(description);
|
|
292
|
+
if (!quotedDescription || !quotedDescription.trim()) {
|
|
293
|
+
console.error(
|
|
294
|
+
`ERROR: Skill '${name}' description collapsed to empty after normalization (source: ${skillPath})`
|
|
295
|
+
);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Build Codex skill format — include platforms: [codex] so deployed skills are self-describing
|
|
300
|
+
const codexContent = `---
|
|
301
|
+
name: "${yamlDoubleQuoted(name)}"
|
|
302
|
+
description: "${quotedDescription}"
|
|
303
|
+
platforms: [codex]
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
${modelPolicy
|
|
307
|
+
? `<!-- aiwg:model-policy role=${modelPolicy.role} tier=${modelPolicy.tier}${modelPolicy.effort ? ` effort=${modelPolicy.effort}` : ''} outcome=unsupported${modelPolicy.rationale ? ` rationale=${yamlDoubleQuoted(modelPolicy.rationale)}` : ''} -->\n\n`
|
|
308
|
+
: ''}${body.trim()}
|
|
309
|
+
`;
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
name,
|
|
313
|
+
description,
|
|
314
|
+
content: codexContent,
|
|
315
|
+
metadataContent: codexOpenAiMetadata({ name, description }),
|
|
316
|
+
sourcePath: skillPath,
|
|
317
|
+
modelPolicy,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Deploy skill to Codex skills directory
|
|
323
|
+
*/
|
|
324
|
+
function deploySkill(skill, targetDir, opts) {
|
|
325
|
+
const { force = false, dryRun = false } = opts;
|
|
326
|
+
const skillDir = path.join(targetDir, skill.name);
|
|
327
|
+
const destPath = path.join(skillDir, 'SKILL.md');
|
|
328
|
+
const metadataPath = path.join(skillDir, 'agents', 'openai.yaml');
|
|
329
|
+
|
|
330
|
+
// Check if skill already exists
|
|
331
|
+
if (fs.existsSync(destPath)) {
|
|
332
|
+
const existingContent = fs.readFileSync(destPath, 'utf8');
|
|
333
|
+
const existingMetadata = fs.existsSync(metadataPath)
|
|
334
|
+
? fs.readFileSync(metadataPath, 'utf8')
|
|
335
|
+
: null;
|
|
336
|
+
if (existingContent === skill.content && existingMetadata === skill.metadataContent && !force) {
|
|
337
|
+
console.log(` skip (unchanged): ${skill.name}`);
|
|
338
|
+
return { action: 'skip', reason: 'unchanged' };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (dryRun) {
|
|
343
|
+
console.log(` [dry-run] deploy: ${skill.name}`);
|
|
344
|
+
if (skill.modelPolicy) {
|
|
345
|
+
console.log(
|
|
346
|
+
` model policy: unsupported (${skill.modelPolicy.role}/${skill.modelPolicy.tier}); no native field emitted`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
return { action: 'deploy', reason: 'dry-run' };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Create skill directory and write SKILL.md
|
|
353
|
+
ensureDir(skillDir);
|
|
354
|
+
fs.writeFileSync(destPath, skill.content, 'utf8');
|
|
355
|
+
ensureDir(path.dirname(metadataPath));
|
|
356
|
+
fs.writeFileSync(metadataPath, skill.metadataContent, 'utf8');
|
|
357
|
+
// Drop a marker file so future deploys can identify AIWG-managed
|
|
358
|
+
// skills regardless of frontmatter format (Codex strips `namespace:`
|
|
359
|
+
// during transform, so the SKILL.md alone isn't a reliable signal).
|
|
360
|
+
// Cleanup keys off this presence.
|
|
361
|
+
fs.writeFileSync(path.join(skillDir, '.aiwg-managed'), 'aiwg\n', 'utf8');
|
|
362
|
+
console.log(` deployed: ${skill.name}`);
|
|
363
|
+
|
|
364
|
+
return { action: 'deploy', reason: 'success' };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Get skill directories based on mode
|
|
369
|
+
*/
|
|
370
|
+
function getSkillDirectories(srcRoot, mode) {
|
|
371
|
+
const dirs = [];
|
|
372
|
+
|
|
373
|
+
const directSkillsDir = path.join(srcRoot, 'skills');
|
|
374
|
+
if (fs.existsSync(directSkillsDir)) {
|
|
375
|
+
dirs.push({ dir: directSkillsDir, label: path.basename(srcRoot) });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Addon skills
|
|
379
|
+
if (mode === 'addons' || mode === 'all') {
|
|
380
|
+
const addonsRoot = path.join(srcRoot, 'agentic', 'code', 'addons');
|
|
381
|
+
if (fs.existsSync(addonsRoot)) {
|
|
382
|
+
const addonDirs = fs.readdirSync(addonsRoot, { withFileTypes: true })
|
|
383
|
+
.filter(e => e.isDirectory())
|
|
384
|
+
.map(e => path.join(addonsRoot, e.name, 'skills'));
|
|
385
|
+
|
|
386
|
+
for (const addonSkillsDir of addonDirs) {
|
|
387
|
+
if (fs.existsSync(addonSkillsDir)) {
|
|
388
|
+
dirs.push({ dir: addonSkillsDir, label: path.basename(path.dirname(addonSkillsDir)) });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Framework skills discovered from framework manifests/directory structure.
|
|
395
|
+
const frameworks = getFrameworksForMode(srcRoot, mode);
|
|
396
|
+
for (const framework of frameworks) {
|
|
397
|
+
if (framework.components.skills.exists) {
|
|
398
|
+
dirs.push({ dir: framework.components.skills.path, label: framework.id });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return dirs;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function isFullAiwgSourceRoot(srcRoot) {
|
|
406
|
+
return fs.existsSync(path.join(srcRoot, 'agentic', 'code', 'frameworks')) ||
|
|
407
|
+
fs.existsSync(path.join(srcRoot, 'agentic', 'code', 'addons')) ||
|
|
408
|
+
fs.existsSync(path.join(srcRoot, 'agentic', 'code', 'extensions'));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
(async function main() {
|
|
412
|
+
const cfg = parseArgs();
|
|
413
|
+
const { source, target, mode, dryRun, force } = cfg;
|
|
414
|
+
|
|
415
|
+
// Resolve source directory
|
|
416
|
+
const scriptDir = path.dirname(new URL(import.meta.url).pathname);
|
|
417
|
+
const repoRoot = path.resolve(scriptDir, '..', '..');
|
|
418
|
+
const srcRoot = source || repoRoot;
|
|
419
|
+
const fullAiwgSourceRoot = isFullAiwgSourceRoot(srcRoot);
|
|
420
|
+
|
|
421
|
+
console.log(`Deploying skills to Codex`);
|
|
422
|
+
console.log(` Source: ${srcRoot}`);
|
|
423
|
+
console.log(` Target: ${target}`);
|
|
424
|
+
console.log(` Mode: ${mode}`);
|
|
425
|
+
if (dryRun) console.log(` [DRY RUN]`);
|
|
426
|
+
console.log();
|
|
427
|
+
|
|
428
|
+
// Create target directory
|
|
429
|
+
if (!dryRun) {
|
|
430
|
+
ensureDir(target);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Get skill directories based on mode
|
|
434
|
+
const skillDirs = getSkillDirectories(srcRoot, mode);
|
|
435
|
+
let totalDeployed = 0;
|
|
436
|
+
let totalSkipped = 0;
|
|
437
|
+
|
|
438
|
+
// Honor #1217 kernel-pivot default: deploy only kernel skills unless
|
|
439
|
+
// the operator opts in via `--copy-all` (or `--copy-standard-skills`).
|
|
440
|
+
// Codex normally deploys to the project `.agents/skills/` target, so the
|
|
441
|
+
// kernel/standard split is enforced at filter time rather than via separate
|
|
442
|
+
// destination directories. The standalone legacy default remains supported.
|
|
443
|
+
const copyStandardSkills = cfg.copyStandardSkills === true;
|
|
444
|
+
|
|
445
|
+
// Track every AIWG-managed source skill name so we can scope post-deploy
|
|
446
|
+
// cleanup to skills AIWG ships — never delete user-authored or
|
|
447
|
+
// third-party skills sitting alongside.
|
|
448
|
+
//
|
|
449
|
+
// Two name spaces matter for cleanup: source basename (`addons/foo/skills/<name>/`)
|
|
450
|
+
// AND deployed name (the `name:` frontmatter field, which Codex uses as
|
|
451
|
+
// the target directory). Sample: source `archive-acquisition` deploys
|
|
452
|
+
// as `Archive Acquisition`. Track both so cleanup catches each form.
|
|
453
|
+
const allManagedNames = new Set();
|
|
454
|
+
const desiredNames = new Set();
|
|
455
|
+
|
|
456
|
+
// Pre-pass: walk every framework/addon skill directory in the source tree
|
|
457
|
+
// so full-root cleanup can remove stale AIWG skills. Component-scoped
|
|
458
|
+
// cleanup is limited below to names owned by that component, preserving
|
|
459
|
+
// user-authored skills alongside the generated set.
|
|
460
|
+
for (const { dir } of getSkillDirectories(srcRoot, 'all')) {
|
|
461
|
+
const allSkills = findSkillDirs(dir);
|
|
462
|
+
for (const s of allSkills) {
|
|
463
|
+
allManagedNames.add(path.basename(s));
|
|
464
|
+
// Also record the frontmatter `name:` since Codex uses that as the
|
|
465
|
+
// target dir. Best-effort — ignore parse errors.
|
|
466
|
+
try {
|
|
467
|
+
const content = fs.readFileSync(path.join(s, 'SKILL.md'), 'utf8');
|
|
468
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
469
|
+
if (fmMatch) {
|
|
470
|
+
const nameMatch = fmMatch[1].match(/^\s*name:\s*(.+?)\s*$/m);
|
|
471
|
+
if (nameMatch) {
|
|
472
|
+
allManagedNames.add(stripWrappingQuotes(nameMatch[1]));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
} catch { /* ignore */ }
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
for (const { dir, label } of skillDirs) {
|
|
480
|
+
const found = findSkillDirs(dir);
|
|
481
|
+
if (found.length === 0) continue;
|
|
482
|
+
|
|
483
|
+
for (const s of found) allManagedNames.add(path.basename(s));
|
|
484
|
+
|
|
485
|
+
const skills = copyStandardSkills
|
|
486
|
+
? found
|
|
487
|
+
: found.filter(s => isKernelSkill(s));
|
|
488
|
+
if (skills.length === 0) continue;
|
|
489
|
+
|
|
490
|
+
for (const s of skills) desiredNames.add(path.basename(s));
|
|
491
|
+
|
|
492
|
+
console.log(`\n${label} (${skills.length} skills):`);
|
|
493
|
+
|
|
494
|
+
for (const skillDir of skills) {
|
|
495
|
+
const skill = transformToCodexSkill(skillDir);
|
|
496
|
+
if (!skill) {
|
|
497
|
+
// transformToCodexSkill already logged the specific reason (parse
|
|
498
|
+
// error, missing description, platform mismatch, etc.).
|
|
499
|
+
console.log(` skip: ${path.basename(skillDir)} (see error above)`);
|
|
500
|
+
totalSkipped++;
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const result = deploySkill(skill, target, { force, dryRun });
|
|
505
|
+
if (result.action === 'deploy') totalDeployed++;
|
|
506
|
+
else totalSkipped++;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Post-deploy cleanup: remove AIWG-managed skills that are stale for the
|
|
511
|
+
// current source scope.
|
|
512
|
+
//
|
|
513
|
+
// Full AIWG-root deploys may prune any `.aiwg-managed` skill because they
|
|
514
|
+
// computed a complete desired set. Component-scoped deploys (`aiwg use all`
|
|
515
|
+
// later iterates each addon/extension after the full-root pass) must only
|
|
516
|
+
// prune names owned by that component. Otherwise an addon with no kernel
|
|
517
|
+
// skills can delete the kernel skills installed by the preceding full-root
|
|
518
|
+
// Codex pass, leaving `.agents/skills/` empty while `aiwg use` still exits 0.
|
|
519
|
+
let totalPruned = 0;
|
|
520
|
+
if (fs.existsSync(target)) {
|
|
521
|
+
const targetEntries = fs.readdirSync(target, { withFileTypes: true });
|
|
522
|
+
for (const entry of targetEntries) {
|
|
523
|
+
if (!entry.isDirectory()) continue;
|
|
524
|
+
const name = entry.name;
|
|
525
|
+
if (desiredNames.has(name)) continue;
|
|
526
|
+
|
|
527
|
+
// Known renamed AIWG skills may predate both the current source name and
|
|
528
|
+
// the .aiwg-managed marker. Treat only the exact historical names as
|
|
529
|
+
// managed so malformed legacy frontmatter cannot survive an upgrade.
|
|
530
|
+
let isAiwgManaged = allManagedNames.has(name) || LEGACY_RENAMED_SKILLS.has(name);
|
|
531
|
+
if (!isAiwgManaged && fullAiwgSourceRoot) {
|
|
532
|
+
// Check for the .aiwg-managed marker file (preferred — survives
|
|
533
|
+
// frontmatter transforms) or fall back to namespace check.
|
|
534
|
+
const markerFile = path.join(target, name, '.aiwg-managed');
|
|
535
|
+
if (fs.existsSync(markerFile)) {
|
|
536
|
+
isAiwgManaged = true;
|
|
537
|
+
} else {
|
|
538
|
+
const skillFile = path.join(target, name, 'SKILL.md');
|
|
539
|
+
if (fs.existsSync(skillFile)) {
|
|
540
|
+
try {
|
|
541
|
+
const content = fs.readFileSync(skillFile, 'utf8');
|
|
542
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
543
|
+
if (fmMatch) {
|
|
544
|
+
const fm = fmMatch[1];
|
|
545
|
+
if (/^\s*namespace:\s*["']?aiwg["']?\s*$/m.test(fm)) {
|
|
546
|
+
isAiwgManaged = true;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
} catch { /* ignore unreadable; leave alone */ }
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (!isAiwgManaged) continue;
|
|
554
|
+
|
|
555
|
+
const full = path.join(target, name);
|
|
556
|
+
if (dryRun) {
|
|
557
|
+
console.log(` [dry-run] would prune stale skill: ${name}`);
|
|
558
|
+
} else {
|
|
559
|
+
fs.rmSync(full, { recursive: true, force: true });
|
|
560
|
+
}
|
|
561
|
+
totalPruned++;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const prunedNote = totalPruned > 0 ? `, ${totalPruned} pruned` : '';
|
|
566
|
+
console.log(`\nSummary: ${totalDeployed} deployed, ${totalSkipped} skipped${prunedNote}`);
|
|
567
|
+
|
|
568
|
+
if (!dryRun && totalDeployed > 0) {
|
|
569
|
+
console.log(`\nRestart Codex to load new skills.`);
|
|
570
|
+
}
|
|
571
|
+
})();
|