@aiwg/cli 2026.7.19 → 2026.7.21

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 (88) hide show
  1. package/README.md +397 -385
  2. package/dist/src/api/index.d.ts +1 -0
  3. package/dist/src/api/index.js +1 -0
  4. package/dist/src/artifacts/browser-export.js +7 -0
  5. package/dist/src/artifacts/citation-parser.js +96 -35
  6. package/dist/src/artifacts/cli.js +59 -5
  7. package/dist/src/artifacts/discover-facets.js +15 -0
  8. package/dist/src/artifacts/discovery-eval.js +290 -0
  9. package/dist/src/artifacts/fortemi-core-query-adapter.js +1 -1
  10. package/dist/src/artifacts/fortemi-shard-export.js +1 -1
  11. package/dist/src/artifacts/index-builder.js +54 -17
  12. package/dist/src/artifacts/query-engine.js +10 -6
  13. package/dist/src/artifacts/state-transfer.js +27 -0
  14. package/dist/src/artifacts/stats.js +8 -0
  15. package/dist/src/cli/cli-extension-loader.js +73 -0
  16. package/dist/src/cli/handlers/help.js +2 -1
  17. package/dist/src/cli/handlers/index.js +6 -2
  18. package/dist/src/cli/handlers/resource-versions.js +247 -0
  19. package/dist/src/cli/handlers/sessions.js +966 -0
  20. package/dist/src/cli/handlers/skill-lint.js +49 -45
  21. package/dist/src/cli/handlers/subcommands.js +55 -3
  22. package/dist/src/cli/handlers/use.js +154 -59
  23. package/dist/src/cli/handlers/utilities.js +49 -34
  24. package/dist/src/cli/skill-usage.js +146 -24
  25. package/dist/src/config/cli.js +13 -9
  26. package/dist/src/config/project-artifacts-runtime.mjs +68 -0
  27. package/dist/src/config/project-artifacts.js +1 -68
  28. package/dist/src/extensions/commands/definitions.js +65 -2
  29. package/dist/src/extensions/manifest.js +29 -0
  30. package/dist/src/extensions/project-local-discovery.js +86 -2
  31. package/dist/src/extensions/project-local-remove.js +52 -56
  32. package/dist/src/extensions/shadow-resolver.js +3 -1
  33. package/dist/src/plugins/standalone-packager.js +143 -0
  34. package/dist/src/resources/cache-cleanup.js +67 -0
  35. package/dist/src/resources/doctor.js +107 -0
  36. package/dist/src/resources/lockfile.js +125 -0
  37. package/dist/src/resources/resolver.js +133 -0
  38. package/dist/src/resources/web-release.d.ts +8 -0
  39. package/dist/src/resources/web-release.js +159 -1
  40. package/dist/src/sessions/adapters/claude.js +357 -0
  41. package/dist/src/sessions/adapters/codex.js +521 -0
  42. package/dist/src/sessions/adapters/copilot.js +226 -0
  43. package/dist/src/sessions/adapters/cursor.js +372 -0
  44. package/dist/src/sessions/adapters/factory.js +345 -0
  45. package/dist/src/sessions/adapters/generic.js +225 -0
  46. package/dist/src/sessions/adapters/hermes.js +341 -0
  47. package/dist/src/sessions/adapters/openclaw.js +381 -0
  48. package/dist/src/sessions/adapters/opencode.js +454 -0
  49. package/dist/src/sessions/adapters/openhuman.js +315 -0
  50. package/dist/src/sessions/adapters/warp.js +160 -0
  51. package/dist/src/sessions/adapters/windsurf.js +212 -0
  52. package/dist/src/sessions/candidates.js +210 -0
  53. package/dist/src/sessions/contracts.js +310 -0
  54. package/dist/src/sessions/discovery.js +51 -0
  55. package/dist/src/sessions/fixtures.js +12 -0
  56. package/dist/src/sessions/importer.js +315 -0
  57. package/dist/src/sessions/index.js +25 -0
  58. package/dist/src/sessions/knowledge-shard.js +61 -0
  59. package/dist/src/sessions/optional-backends.js +238 -0
  60. package/dist/src/sessions/policy.js +192 -0
  61. package/dist/src/sessions/ports.js +2 -0
  62. package/dist/src/sessions/promotion.js +367 -0
  63. package/dist/src/sessions/readers.js +176 -0
  64. package/dist/src/sessions/repository.js +1551 -0
  65. package/dist/src/skills/adapters/agent-skills.js +59 -0
  66. package/dist/src/skills/adapters/local.js +19 -1
  67. package/dist/src/skills/agent-skills.js +249 -0
  68. package/dist/src/skills/cli.js +463 -7
  69. package/dist/src/skills/deployer.js +554 -0
  70. package/dist/src/skills/doctor.js +105 -0
  71. package/dist/src/skills/exporter.js +382 -0
  72. package/dist/src/skills/importer.js +921 -0
  73. package/dist/src/skills/registry.js +19 -0
  74. package/dist/src/skills/validator.js +323 -0
  75. package/dist/src/smiths/context-pipeline/aiwg-md.js +5 -1
  76. package/dist/src/smiths/context-pipeline/claude-hook.js +21 -1
  77. package/dist/src/smiths/context-pipeline/finalization.js +5 -3
  78. package/dist/src/smiths/context-pipeline/generator.js +4 -1
  79. package/dist/src/smiths/context-pipeline/parallelism-section.js +34 -1
  80. package/dist/src/smiths/context-pipeline/workspace-context.js +15 -3
  81. package/dist/src/smiths/mcpsmith/example.js +3 -1
  82. package/dist/src/smiths/mcpsmith/generator.js +3 -1
  83. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +2 -1
  84. package/dist/src/storage/cli.js +3 -2
  85. package/dist/src/storage/subsystem-cli.js +7 -2
  86. package/dist/src/update/notifier.mjs +1 -1
  87. package/dist/src/update/service.mjs +123 -0
  88. package/package.json +3 -2
@@ -907,6 +907,34 @@ export const sessionCommand = {
907
907
  },
908
908
  },
909
909
  };
910
+ // Session Catalog Command (#1903)
911
+ export const sessionsCommand = {
912
+ id: 'sessions',
913
+ type: 'skill',
914
+ name: 'Sessions',
915
+ description: 'Manage the normalized session catalog with versioned JSON, deterministic pagination, previews, and health checks',
916
+ version: '1.0.0',
917
+ capabilities: ['cli', 'session-catalog', 'session-import', 'session-lifecycle', 'doctor'],
918
+ keywords: ['sessions', 'catalog', 'import', 'source', 'tag', 'reindex', 'delete'],
919
+ category: 'project',
920
+ platforms: {
921
+ claude: 'full',
922
+ generic: 'full',
923
+ },
924
+ deployment: {
925
+ pathTemplate: '.{platform}/commands/{id}.md',
926
+ core: true,
927
+ },
928
+ metadata: {
929
+ type: 'skill',
930
+ triggerPhrases: ['list sessions', 'import sessions', 'session catalog', 'sessions doctor'],
931
+ commandHint: {
932
+ template: 'utility',
933
+ argumentHint: '<sources|import|list|show|tag|relocate|reindex|delete|doctor> [--json] [--dry-run]',
934
+ allowedTools: ['Bash'],
935
+ },
936
+ },
937
+ };
910
938
  // Sandbox Management Commands (#917)
911
939
  export const sandboxCommand = {
912
940
  id: 'sandbox',
@@ -1272,7 +1300,7 @@ export const discoverCommand = {
1272
1300
  ],
1273
1301
  commandHint: {
1274
1302
  template: 'utility',
1275
- argumentHint: '"<phrase>" [--limit N] [--type skill,agent,...] [--json] [--resource-source local|web|auto] [--aiwg-version <exact-or-channel>] [--offline]',
1303
+ argumentHint: '"<phrase>" [--limit N] [--type skill,agent,...] [--json] [--resource-source local|web|auto] [--aiwg-version <version|range|digest|channel>] [--offline]',
1276
1304
  allowedTools: ['Read'],
1277
1305
  },
1278
1306
  },
@@ -1309,11 +1337,44 @@ export const showCommand = {
1309
1337
  ],
1310
1338
  commandHint: {
1311
1339
  template: 'utility',
1312
- argumentHint: '<type> <name> [--json] [--first] [--resource-source local|web|auto] [--aiwg-version <exact-or-channel>] [--offline] # type: skill | agent | command | rule',
1340
+ argumentHint: '<type> <name> [--json] [--first] [--resource-source local|web|auto] [--aiwg-version <version|range|digest|channel>] [--offline] # type: skill | agent | command | rule',
1313
1341
  allowedTools: ['Read'],
1314
1342
  },
1315
1343
  },
1316
1344
  };
1345
+ export const versionsCommand = {
1346
+ id: 'versions',
1347
+ type: 'skill',
1348
+ name: 'Resource Versions',
1349
+ description: 'Browse and resolve signed AIWG web resource releases',
1350
+ version: '1.0.0',
1351
+ capabilities: ['cli', 'resources', 'versions', 'release', 'integrity', 'cache'],
1352
+ keywords: ['versions', 'resource versions', 'aiwg version', 'release manifest', 'web resources', 'stable channel'],
1353
+ category: 'index',
1354
+ platforms: {
1355
+ claude: 'full',
1356
+ generic: 'full',
1357
+ },
1358
+ deployment: {
1359
+ pathTemplate: '.{platform}/commands/{id}.md',
1360
+ core: true,
1361
+ },
1362
+ metadata: {
1363
+ type: 'skill',
1364
+ triggerPhrases: [
1365
+ 'aiwg versions',
1366
+ 'resource versions',
1367
+ 'resolve aiwg version',
1368
+ 'show release manifest',
1369
+ 'list AIWG resource channels',
1370
+ ],
1371
+ commandHint: {
1372
+ template: 'utility',
1373
+ argumentHint: 'list|resolve|show [version|range|digest|channel] [--json] [--pretty] [--offline] [--channels stable,latest]',
1374
+ allowedTools: ['Read', 'Bash'],
1375
+ },
1376
+ },
1377
+ };
1317
1378
  // Features: list, inspect (and eventually install) AIWG's optional
1318
1379
  // runtime features — embeddings, sqlite, pty, webserver. (#1219)
1319
1380
  export const featuresCommand = {
@@ -3379,6 +3440,7 @@ export const commandDefinitions = [
3379
3440
  researchQueryCommand,
3380
3441
  discoverCommand,
3381
3442
  showCommand,
3443
+ versionsCommand,
3382
3444
  // Optional Features (1)
3383
3445
  featuresCommand,
3384
3446
  // Reproducibility (4)
@@ -3424,6 +3486,7 @@ export const commandDefinitions = [
3424
3486
  feedbackCommand,
3425
3487
  // Session (#884)
3426
3488
  sessionCommand,
3489
+ sessionsCommand,
3427
3490
  ];
3428
3491
  // ============================================
3429
3492
  // Helper Functions
@@ -39,6 +39,7 @@ export const ProjectLocalTypeSchema = z.enum([
39
39
  * underscore + hyphen, optional trailing slash.
40
40
  */
41
41
  const safeRelativePath = z.string().regex(/^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*\/?$/, 'must be a relative path (alphanumeric + _-, no leading slash, no ..)');
42
+ const safeModuleFile = z.string().regex(/^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*\.mjs$/, 'must be a relative .mjs path (alphanumeric + _-, no leading slash, no ..)');
42
43
  // Single-char ids are allowed; multi-char ids must end with alphanumeric
43
44
  // (no trailing hyphen). This pattern: `[a-z0-9]([a-z0-9-]*[a-z0-9])?`
44
45
  const bundleNamePattern = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
@@ -129,6 +130,31 @@ export const ProviderConfigSchema = z.object({
129
130
  aliases: z.array(z.string().min(1).max(64)).max(20).optional(),
130
131
  capabilities: ProviderCapabilityOverridesSchema.optional(),
131
132
  }).strict();
133
+ export const CliCommandsSchema = z.object({
134
+ namespace: z.string()
135
+ .min(1)
136
+ .max(64)
137
+ .regex(bundleNamePattern, 'kebab-case alphanumeric, no leading/trailing hyphen'),
138
+ description: z.string().min(1).max(512),
139
+ entry: safeRelativePath.optional(),
140
+ subcommands: z.record(z.string()
141
+ .min(1)
142
+ .max(64)
143
+ .regex(bundleNamePattern, 'kebab-case alphanumeric, no leading/trailing hyphen'), z.object({
144
+ file: safeModuleFile,
145
+ description: z.string().min(1).max(512),
146
+ hook_event: z.enum([
147
+ 'Stop',
148
+ 'SessionStart',
149
+ 'SessionEnd',
150
+ 'PreToolUse',
151
+ 'PostToolUse',
152
+ 'FeatureComplete',
153
+ ]).optional(),
154
+ }).strict()).refine((commands) => Object.keys(commands).length > 0, {
155
+ message: 'at least one CLI subcommand is required',
156
+ }),
157
+ }).strict();
132
158
  // ============================================
133
159
  // Top-level BundleManifestSchema
134
160
  // ============================================
@@ -162,6 +188,9 @@ export const BundleManifestSchema = z.object({
162
188
  // Optional patterns shared with existing extension validation
163
189
  deprecation: DeprecationSchema.optional(),
164
190
  memory: MemoryFootprintSchema.optional(),
191
+ // Expandable CLI namespace contributed by an addon-shaped bundle.
192
+ // The same block is accepted by bundled addon manifests.
193
+ cli_commands: CliCommandsSchema.optional(),
165
194
  })
166
195
  .strict()
167
196
  .refine((m) => {
@@ -9,8 +9,8 @@
9
9
  * @architecture .aiwg/architecture/adr-aiwg-directory-layout.md (#1039)
10
10
  * @architecture .aiwg/architecture/design-manifest-schema.md (#1044)
11
11
  */
12
- import { readFile, readdir, lstat, stat, access } from 'fs/promises';
13
- import { join } from 'path';
12
+ import { readFile, readdir, lstat, stat, access, realpath } from 'fs/promises';
13
+ import { join, relative, resolve, sep } from 'path';
14
14
  import { BundleManifestSchema, zodErrorToValidationErrors, MANIFEST_MAX_BYTES, MAX_BUNDLES_PER_PROJECT, } from './manifest.js';
15
15
  import { ensureTrailingSlash, projectLocalDisplayPath, PROJECT_LOCAL_DIR_TO_TYPE, PROJECT_LOCAL_SCAN_DIRS, resolveProjectLocalSearchRoots, } from './project-local-paths.js';
16
16
  /**
@@ -246,12 +246,96 @@ export async function loadAndValidateManifest(manifestPath, expectedType, projec
246
246
  }
247
247
  const bundlePath = manifestPath.slice(0, -'/manifest.json'.length);
248
248
  const localPath = ensureTrailingSlash(projectLocalDisplayPath(projectDir, bundlePath, searchRoot));
249
+ let artifactPath = bundlePath;
250
+ if (manifest.type === 'plugin') {
251
+ // BundleManifestSchema's discriminator refinement guarantees this for a
252
+ // plugin, but Zod refinements do not narrow the inferred TypeScript type.
253
+ const pluginConfig = manifest.pluginConfig;
254
+ const configuredPayload = resolve(bundlePath, pluginConfig.payloadPath);
255
+ const lexicalRelative = relative(bundlePath, configuredPayload);
256
+ if (lexicalRelative === ''
257
+ || lexicalRelative === '..'
258
+ || lexicalRelative.startsWith(`..${sep}`)) {
259
+ return {
260
+ errors: [{
261
+ path: manifestPath,
262
+ field: 'pluginConfig.payloadPath',
263
+ expected: 'a child directory inside the plugin wrapper',
264
+ actual: pluginConfig.payloadPath,
265
+ hint: 'Plugin payloads must not resolve to the wrapper root or escape it',
266
+ severity: 'error',
267
+ }],
268
+ };
269
+ }
270
+ let wrapperReal;
271
+ let payloadReal;
272
+ try {
273
+ const payloadStat = await lstat(configuredPayload);
274
+ if (payloadStat.isSymbolicLink() || !payloadStat.isDirectory()) {
275
+ return {
276
+ errors: [{
277
+ path: configuredPayload,
278
+ field: 'pluginConfig.payloadPath',
279
+ expected: 'a regular payload directory',
280
+ actual: payloadStat.isSymbolicLink() ? 'symlink' : 'non-directory',
281
+ hint: 'Use a real directory below the plugin wrapper',
282
+ severity: 'error',
283
+ }],
284
+ };
285
+ }
286
+ [wrapperReal, payloadReal] = await Promise.all([
287
+ realpath(bundlePath),
288
+ realpath(configuredPayload),
289
+ ]);
290
+ }
291
+ catch (err) {
292
+ return {
293
+ errors: [{
294
+ path: configuredPayload,
295
+ field: 'pluginConfig.payloadPath',
296
+ expected: 'an existing readable payload directory',
297
+ actual: err.message,
298
+ hint: 'Create the configured payload directory and its manifest.json',
299
+ severity: 'error',
300
+ }],
301
+ };
302
+ }
303
+ const realRelative = relative(wrapperReal, payloadReal);
304
+ if (realRelative === ''
305
+ || realRelative === '..'
306
+ || realRelative.startsWith(`..${sep}`)) {
307
+ return {
308
+ errors: [{
309
+ path: configuredPayload,
310
+ field: 'pluginConfig.payloadPath',
311
+ expected: 'a non-symlinked child directory inside the plugin wrapper',
312
+ actual: payloadReal,
313
+ hint: 'Move the payload below the wrapper and remove symlink indirection',
314
+ severity: 'error',
315
+ }],
316
+ };
317
+ }
318
+ const payloadManifestPath = join(configuredPayload, 'manifest.json');
319
+ const payloadResult = await loadAndValidateManifest(payloadManifestPath, pluginConfig.payloadType, projectDir, searchRoot);
320
+ if (!payloadResult.bundle) {
321
+ return {
322
+ errors: payloadResult.errors.map(error => ({
323
+ ...error,
324
+ hint: error.field === 'type'
325
+ ? `Payload manifest type must match pluginConfig.payloadType: "${pluginConfig.payloadType}"`
326
+ : error.hint,
327
+ })),
328
+ };
329
+ }
330
+ artifactPath = payloadResult.bundle.bundlePath;
331
+ }
249
332
  return {
250
333
  bundle: {
251
334
  id: manifest.id,
252
335
  type: manifest.type,
253
336
  manifest,
254
337
  bundlePath,
338
+ artifactPath,
255
339
  localPath,
256
340
  manifestPath: projectLocalDisplayPath(projectDir, manifestPath, searchRoot),
257
341
  },
@@ -12,11 +12,11 @@
12
12
  * @design @.aiwg/architecture/design-aiwg-remove-revert.md
13
13
  * @implements #1037
14
14
  */
15
- import { lstat, readdir, stat, unlink } from 'fs/promises';
16
- import { resolve, join, relative } from 'path';
17
- import { homedir } from 'os';
15
+ import { lstat, readdir, stat, unlink, rmdir } from 'fs/promises';
16
+ import { resolve, join, relative, dirname, isAbsolute } from 'path';
18
17
  import { appendProjectLocalActivity } from './project-local-activity.js';
19
18
  import { sha256OfFileNormalized } from './managed-marker.js';
19
+ import { getProviderArtifactPathStrings } from '../providers/provider-definitions.js';
20
20
  /**
21
21
  * Hash an artifact file with the managed-marker line stripped.
22
22
  *
@@ -93,54 +93,32 @@ export async function hashBundleArtifacts(bundleAbsPath) {
93
93
  }
94
94
  return out;
95
95
  }
96
- /**
97
- * Provider-specific deploy-path conventions for the artifact directories
98
- * we currently emit. Keyed by provider, value is the prefix relative to
99
- * the project (or HOME for HOME-deploying providers — those are out of
100
- * scope for revert in this iteration; they are silently skipped with a
101
- * message).
102
- */
103
- // Per PUW-026 (#1127): home-deploying providers get absolute prefixes so
104
- // `resolve(projectDir, prefix)` correctly produces the home-rooted path.
105
- // Previously these were `null`, which silently skipped lifecycle ops
106
- // against home-deployed project-local bundles (OpenClaw, Hermes).
107
- const PROVIDER_PREFIX = {
108
- claude: '.claude',
109
- cursor: '.cursor',
110
- factory: '.factory',
111
- opencode: '.opencode',
112
- windsurf: '.windsurf',
113
- warp: '.warp',
114
- codex: '.codex',
115
- copilot: '.github', // copilot uses .github/agents, .github/instructions, .github/skills
116
- openclaw: resolve(homedir(), '.openclaw'),
117
- hermes: resolve(homedir(), '.hermes'),
118
- };
119
- /**
120
- * Translate a source-relative artifact path to the provider's deploy path,
121
- * relative to the project (or HOME) root. Returns null when the provider
122
- * is HOME-deploying (revert from HOME is out of scope for this iteration).
123
- *
124
- * For most providers this is a 1:1 mapping (rules/x.md → .{p}/rules/x.md).
125
- */
126
- function deployedPathFor(provider, sourceRel) {
127
- const prefix = PROVIDER_PREFIX[provider];
128
- if (!prefix)
129
- return null;
130
- // Some providers rename rules to .mdc (cursor); revert checks both.
131
- return `${prefix}/${sourceRel}`;
132
- }
133
- /** Try multiple plausible deployed paths (e.g., .md vs .mdc for rules). */
134
- function candidateDeployedPaths(provider, sourceRel) {
135
- const paths = [];
136
- const main = deployedPathFor(provider, sourceRel);
137
- if (main)
138
- paths.push(main);
139
- // Cursor rule rename
140
- if (provider === 'cursor' && sourceRel.startsWith('rules/') && sourceRel.endsWith('.md')) {
141
- paths.push(`.cursor/${sourceRel.slice(0, -3)}.mdc`);
96
+ /** Resolve a recorded source-relative artifact through the canonical provider
97
+ * definition rather than assuming every artifact lives directly under the
98
+ * provider prefix (#1869). Returned paths are absolute for both project- and
99
+ * home-deploying providers. */
100
+ export function candidateDeployedPaths(projectDir, provider, sourceRel) {
101
+ const separator = sourceRel.indexOf('/');
102
+ if (separator < 1)
103
+ return [];
104
+ const artifactType = sourceRel.slice(0, separator);
105
+ if (!['agents', 'commands', 'skills', 'rules'].includes(artifactType))
106
+ return [];
107
+ const paths = getProviderArtifactPathStrings(provider);
108
+ const root = paths?.[artifactType];
109
+ if (!root)
110
+ return [];
111
+ const tail = sourceRel.slice(separator + 1);
112
+ const absoluteRoot = isAbsolute(root) ? root : resolve(projectDir, root);
113
+ const candidates = [join(absoluteRoot, tail)];
114
+ // Provider adapters may translate source extensions.
115
+ if (provider === 'cursor' && artifactType === 'rules' && tail.endsWith('.md')) {
116
+ candidates.push(join(absoluteRoot, `${tail.slice(0, -3)}.mdc`));
117
+ }
118
+ if (provider === 'codex' && artifactType === 'agents' && tail.endsWith('.md')) {
119
+ candidates.push(join(absoluteRoot, `${tail.slice(0, -3)}.toml`));
142
120
  }
143
- return paths;
121
+ return [...new Set(candidates)];
144
122
  }
145
123
  async function tryUnlink(absPath) {
146
124
  try {
@@ -156,6 +134,18 @@ async function tryUnlink(absPath) {
156
134
  throw err;
157
135
  }
158
136
  }
137
+ async function cleanupManagedSkillDirectory(absPath) {
138
+ if (!absPath.endsWith('/SKILL.md'))
139
+ return;
140
+ const skillDir = dirname(absPath);
141
+ await tryUnlink(join(skillDir, '.aiwg-managed'));
142
+ try {
143
+ await rmdir(skillDir);
144
+ }
145
+ catch {
146
+ // Preserve directories containing any operator or provider-created files.
147
+ }
148
+ }
159
149
  async function classify(expectedHash, absPath) {
160
150
  if (!expectedHash)
161
151
  return 'unhashed';
@@ -258,28 +248,30 @@ export async function removeProjectLocalBundle(config, projectDir, bundleId, opt
258
248
  }
259
249
  continue;
260
250
  }
261
- const candidates = candidateDeployedPaths(provider, sourceRel);
251
+ const candidates = candidateDeployedPaths(projectDir, provider, sourceRel);
262
252
  let resolvedAbs = null;
263
253
  let detectedCase = 'missing';
264
254
  for (const c of candidates) {
265
- const abs = resolve(projectDir, c);
266
- const k = await classify(artifactHashes[sourceRel], abs);
255
+ const k = await classify(artifactHashes[sourceRel], c);
267
256
  if (k !== 'missing') {
268
- resolvedAbs = abs;
257
+ resolvedAbs = c;
269
258
  detectedCase = k;
270
259
  break;
271
260
  }
272
261
  }
273
262
  if (!resolvedAbs) {
274
263
  // Pick the first candidate just so the outcome carries a path
275
- const fallback = candidates[0] ? resolve(projectDir, candidates[0]) : '(unknown)';
264
+ const fallback = candidates[0] ?? '(unknown)';
265
+ providerHadSkip = true;
276
266
  outcomes.push({
277
267
  provider,
278
268
  artifactPath: sourceRel,
279
269
  deployedAbsPath: fallback,
280
270
  case: 'missing',
281
271
  reverted: false,
282
- message: 'already absent',
272
+ message: candidates.length === 0
273
+ ? 'provider artifact path unavailable — registry preserved'
274
+ : 'recorded artifact not found at canonical provider path — registry preserved for retry',
283
275
  });
284
276
  continue;
285
277
  }
@@ -295,6 +287,8 @@ export async function removeProjectLocalBundle(config, projectDir, bundleId, opt
295
287
  outcomes.push({ provider, artifactPath: sourceRel, deployedAbsPath: resolvedAbs, case: 'permission', reverted: false, message: 'permission denied' });
296
288
  }
297
289
  else {
290
+ if (r.deleted)
291
+ await cleanupManagedSkillDirectory(resolvedAbs);
298
292
  outcomes.push({ provider, artifactPath: sourceRel, deployedAbsPath: resolvedAbs, case: 'pristine', reverted: r.deleted, message: r.deleted ? 'reverted' : 'already absent' });
299
293
  }
300
294
  }
@@ -325,6 +319,8 @@ export async function removeProjectLocalBundle(config, projectDir, bundleId, opt
325
319
  outcomes.push({ provider, artifactPath: sourceRel, deployedAbsPath: resolvedAbs, case: 'permission', reverted: false, message: 'permission denied' });
326
320
  }
327
321
  else {
322
+ if (r.deleted)
323
+ await cleanupManagedSkillDirectory(resolvedAbs);
328
324
  outcomes.push({ provider, artifactPath: sourceRel, deployedAbsPath: resolvedAbs, case: detectedCase, reverted: r.deleted, message: r.deleted ? 'reverted (mutation overridden)' : 'already absent' });
329
325
  }
330
326
  }
@@ -115,7 +115,9 @@ export async function resolveShadows(bundles, upstream, options = {}) {
115
115
  // Enumerate every artifact in every bundle once.
116
116
  const enumerated = [];
117
117
  for (const bundle of bundles) {
118
- const arts = await enumerateBundleArtifacts(bundle.bundlePath);
118
+ // Preserve compatibility for programmatic callers that constructed the
119
+ // pre-#1868 ProjectLocalBundle shape; discovery always supplies artifactPath.
120
+ const arts = await enumerateBundleArtifacts(bundle.artifactPath ?? bundle.bundlePath);
119
121
  enumerated.push({ bundle, arts });
120
122
  for (const art of arts) {
121
123
  const key = `${art.type}:${art.id}`;
@@ -0,0 +1,143 @@
1
+ // @ts-nocheck
2
+ import { spawnSync } from 'node:child_process';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ const NAME = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
7
+ const PROVIDERS = new Set(['claude', 'codex']);
8
+ function inside(parent, candidate) {
9
+ const rel = path.relative(path.resolve(parent), path.resolve(candidate));
10
+ return rel === '' || (rel.length > 0 && !rel.startsWith('..') && !path.isAbsolute(rel));
11
+ }
12
+ async function validateManifest(wrapperRoot, expectedName, projectRoot) {
13
+ const manifestPath = path.join(wrapperRoot, 'manifest.json');
14
+ const { loadAndValidateManifest } = await import('../extensions/project-local-discovery.js');
15
+ const validation = await loadAndValidateManifest(manifestPath, 'plugin', projectRoot);
16
+ if (!validation.bundle) {
17
+ const details = validation.errors.map(error => {
18
+ const hint = error.hint ? `; ${error.hint}` : '';
19
+ return `${error.field}: expected ${error.expected}, got ${String(error.actual)}${hint}`;
20
+ }).join('\n - ');
21
+ throw new Error(`Standalone plugin manifest validation failed:\n - ${details}`);
22
+ }
23
+ const manifest = validation.bundle.manifest;
24
+ if (manifest.id !== expectedName)
25
+ throw new Error(`Wrapper manifest id '${manifest.id ?? '<missing>'}' does not match requested plugin '${expectedName}'`);
26
+ return { manifest, payloadRoot: validation.bundle.artifactPath };
27
+ }
28
+ function copyTree(source, destination) {
29
+ fs.mkdirSync(destination, { recursive: true });
30
+ for (const entry of fs.readdirSync(source, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
31
+ const from = path.join(source, entry.name);
32
+ const to = path.join(destination, entry.name);
33
+ if (entry.isSymbolicLink())
34
+ throw new Error(`Refusing symbolic link in plugin source: ${from}`);
35
+ if (entry.isDirectory())
36
+ copyTree(from, to);
37
+ else if (entry.isFile())
38
+ fs.copyFileSync(from, to);
39
+ }
40
+ }
41
+ function writeProviderMetadata(root, provider, manifest) {
42
+ const version = manifest.version ?? '0.0.0';
43
+ const description = manifest.description ?? manifest.name ?? manifest.id;
44
+ if (provider === 'claude') {
45
+ const dir = path.join(root, '.claude-plugin');
46
+ fs.mkdirSync(dir, { recursive: true });
47
+ fs.writeFileSync(path.join(dir, 'plugin.json'), `${JSON.stringify({
48
+ name: manifest.id,
49
+ version,
50
+ description,
51
+ }, null, 2)}\n`);
52
+ }
53
+ else {
54
+ const dir = path.join(root, '.codex-plugin');
55
+ fs.mkdirSync(dir, { recursive: true });
56
+ fs.writeFileSync(path.join(dir, 'plugin.json'), `${JSON.stringify({
57
+ name: manifest.id,
58
+ version,
59
+ description,
60
+ }, null, 2)}\n`);
61
+ fs.writeFileSync(path.join(root, 'marketplace.json'), `${JSON.stringify({
62
+ name: manifest.id,
63
+ plugins: [{ name: manifest.id, source: { source: 'local', path: '.' } }],
64
+ }, null, 2)}\n`);
65
+ }
66
+ }
67
+ function createDeterministicArchive(sourceDir, archivePath) {
68
+ const result = spawnSync('tar', [
69
+ '--sort=name',
70
+ '--mtime=@0',
71
+ '--owner=0',
72
+ '--group=0',
73
+ '--numeric-owner',
74
+ '-czf',
75
+ archivePath,
76
+ '-C',
77
+ path.dirname(sourceDir),
78
+ path.basename(sourceDir),
79
+ ], { encoding: 'utf8' });
80
+ if (result.status !== 0)
81
+ throw new Error(`Could not create deterministic archive: ${result.stderr || 'tar failed'}`);
82
+ }
83
+ export function resolveStandalonePluginSource({ cwd, name, source }) {
84
+ const projectRoot = path.resolve(cwd);
85
+ const candidate = path.resolve(projectRoot, source ?? path.join('.aiwg', 'plugins', name));
86
+ if (!inside(projectRoot, candidate))
87
+ throw new Error('Plugin --source must stay inside the current project');
88
+ if (!fs.existsSync(candidate) || !fs.statSync(candidate).isDirectory())
89
+ return null;
90
+ const real = fs.realpathSync(candidate);
91
+ if (!inside(fs.realpathSync(projectRoot), real))
92
+ throw new Error('Plugin --source resolves outside the current project through a symlink');
93
+ return real;
94
+ }
95
+ export async function packageStandalonePlugin(options) {
96
+ const { cwd, name, source, output, dryRun = false, clean = false } = options;
97
+ if (!NAME.test(name))
98
+ throw new Error(`Plugin name '${name}' must be kebab-case`);
99
+ const sourceRoot = resolveStandalonePluginSource({ cwd, name, source });
100
+ if (!sourceRoot)
101
+ return null;
102
+ const { manifest, payloadRoot } = await validateManifest(sourceRoot, name, path.resolve(cwd));
103
+ const providers = options.provider === 'all'
104
+ ? ['claude', 'codex']
105
+ : [options.provider ?? 'claude'];
106
+ for (const provider of providers) {
107
+ if (!PROVIDERS.has(provider)) {
108
+ throw new Error(`Standalone plugin provider '${provider}' is not supported; use claude, codex, or all`);
109
+ }
110
+ const support = manifest.platforms?.[provider];
111
+ if (support === undefined || support === false || support === 'none') {
112
+ throw new Error(`Wrapper manifest does not declare compatible '${provider}' platform support`);
113
+ }
114
+ }
115
+ const outputRoot = path.resolve(cwd, output ?? path.join('dist', 'plugins'));
116
+ const version = manifest.version ?? '0.0.0';
117
+ const plans = providers.map(provider => ({
118
+ provider,
119
+ archivePath: path.join(outputRoot, `${name}-${version}-${provider}.tar.gz`),
120
+ }));
121
+ if (dryRun)
122
+ return { sourceRoot, payloadRoot, plans, dryRun: true };
123
+ fs.mkdirSync(outputRoot, { recursive: true });
124
+ for (const plan of plans) {
125
+ if (fs.existsSync(plan.archivePath) && !clean) {
126
+ throw new Error(`Package output already exists: ${plan.archivePath}; use --clean or choose another --output`);
127
+ }
128
+ if (clean)
129
+ fs.rmSync(plan.archivePath, { force: true });
130
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'aiwg-plugin-package-'));
131
+ try {
132
+ const staged = path.join(tempRoot, name);
133
+ copyTree(sourceRoot, staged);
134
+ writeProviderMetadata(staged, plan.provider, manifest);
135
+ createDeterministicArchive(staged, plan.archivePath);
136
+ }
137
+ finally {
138
+ fs.rmSync(tempRoot, { recursive: true, force: true });
139
+ }
140
+ }
141
+ return { sourceRoot, payloadRoot, plans, dryRun: false };
142
+ }
143
+ //# sourceMappingURL=standalone-packager.js.map
@@ -0,0 +1,67 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { getResourceCacheRoot } from "./web-release.js";
4
+ import { readResourceLockfile } from "./lockfile.js";
5
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/;
6
+ function lockedReleaseKeys(projectDir) {
7
+ const lockfile = readResourceLockfile(projectDir);
8
+ const locked = new Set();
9
+ for (const resource of Object.values(lockfile?.resources ?? {})) {
10
+ locked.add(`${resource.version}/${resource.manifestSha256}`);
11
+ }
12
+ return locked;
13
+ }
14
+ function releaseGenerations(cacheRoot) {
15
+ const releasesRoot = path.join(cacheRoot, "releases");
16
+ const skipped = [];
17
+ const entries = [];
18
+ if (!fs.existsSync(releasesRoot))
19
+ return { entries, skipped };
20
+ for (const versionEntry of fs.readdirSync(releasesRoot, { withFileTypes: true })) {
21
+ const versionPath = path.join(releasesRoot, versionEntry.name);
22
+ if (!versionEntry.isDirectory()) {
23
+ skipped.push({ path: versionPath, reason: "not a directory" });
24
+ continue;
25
+ }
26
+ for (const digestEntry of fs.readdirSync(versionPath, { withFileTypes: true })) {
27
+ const generationPath = path.join(versionPath, digestEntry.name);
28
+ if (!digestEntry.isDirectory()) {
29
+ skipped.push({ path: generationPath, reason: "not a directory" });
30
+ continue;
31
+ }
32
+ if (!SHA256_PATTERN.test(digestEntry.name)) {
33
+ skipped.push({ path: generationPath, reason: "not a release digest generation" });
34
+ continue;
35
+ }
36
+ entries.push({
37
+ version: versionEntry.name,
38
+ manifestSha256: digestEntry.name,
39
+ path: generationPath,
40
+ });
41
+ }
42
+ }
43
+ entries.sort((left, right) => left.version.localeCompare(right.version) || left.manifestSha256.localeCompare(right.manifestSha256));
44
+ return { entries, skipped };
45
+ }
46
+ export function cleanWebResourceCache(projectDir, options = {}) {
47
+ const cacheRoot = getResourceCacheRoot(options.cacheRoot);
48
+ const dryRun = options.dryRun === true;
49
+ const force = options.force === true;
50
+ const lockedKeys = lockedReleaseKeys(projectDir);
51
+ const { entries, skipped } = releaseGenerations(cacheRoot);
52
+ const locked = entries.filter((entry) => lockedKeys.has(`${entry.version}/${entry.manifestSha256}`));
53
+ const preserved = [];
54
+ const removed = [];
55
+ for (const entry of entries) {
56
+ const isLocked = lockedKeys.has(`${entry.version}/${entry.manifestSha256}`);
57
+ if (isLocked && !force) {
58
+ preserved.push(entry);
59
+ continue;
60
+ }
61
+ removed.push(entry);
62
+ if (!dryRun)
63
+ fs.rmSync(entry.path, { recursive: true, force: true });
64
+ }
65
+ return { cacheRoot, dryRun, force, locked, removed, preserved, skipped };
66
+ }
67
+ //# sourceMappingURL=cache-cleanup.js.map