@aiwg/cli 2026.7.18 → 2026.7.20

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 (41) hide show
  1. package/README.md +1037 -13
  2. package/dist/src/artifacts/cli.js +59 -5
  3. package/dist/src/artifacts/discover-facets.js +15 -0
  4. package/dist/src/artifacts/discovery-eval.js +290 -0
  5. package/dist/src/artifacts/fortemi-core-query-adapter.js +1 -1
  6. package/dist/src/artifacts/fortemi-shard-export.js +1 -1
  7. package/dist/src/artifacts/query-engine.js +10 -6
  8. package/dist/src/cli/handlers/help.js +2 -1
  9. package/dist/src/cli/handlers/index.js +3 -1
  10. package/dist/src/cli/handlers/resource-versions.js +247 -0
  11. package/dist/src/cli/handlers/subcommands.js +55 -3
  12. package/dist/src/cli/handlers/use.js +15 -3
  13. package/dist/src/cli/handlers/utilities.js +27 -26
  14. package/dist/src/config/cli.js +13 -9
  15. package/dist/src/config/project-artifacts-runtime.mjs +68 -0
  16. package/dist/src/config/project-artifacts.js +1 -68
  17. package/dist/src/extensions/commands/definitions.js +36 -2
  18. package/dist/src/extensions/project-local-discovery.js +86 -2
  19. package/dist/src/extensions/project-local-remove.js +52 -56
  20. package/dist/src/extensions/shadow-resolver.js +3 -1
  21. package/dist/src/plugins/standalone-packager.js +143 -0
  22. package/dist/src/resources/cache-cleanup.js +67 -0
  23. package/dist/src/resources/doctor.js +107 -0
  24. package/dist/src/resources/lockfile.js +125 -0
  25. package/dist/src/resources/resolver.js +133 -0
  26. package/dist/src/resources/web-release.d.ts +8 -0
  27. package/dist/src/resources/web-release.js +159 -1
  28. package/dist/src/smiths/context-pipeline/aiwg-md.js +5 -1
  29. package/dist/src/smiths/context-pipeline/claude-hook.js +21 -1
  30. package/dist/src/smiths/context-pipeline/finalization.js +5 -3
  31. package/dist/src/smiths/context-pipeline/generator.js +4 -1
  32. package/dist/src/smiths/context-pipeline/parallelism-section.js +34 -1
  33. package/dist/src/smiths/context-pipeline/workspace-context.js +15 -3
  34. package/dist/src/smiths/mcpsmith/example.js +3 -1
  35. package/dist/src/smiths/mcpsmith/generator.js +3 -1
  36. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +2 -1
  37. package/dist/src/storage/cli.js +3 -2
  38. package/dist/src/storage/subsystem-cli.js +7 -2
  39. package/dist/src/update/notifier.mjs +1 -1
  40. package/dist/src/update/service.mjs +123 -0
  41. package/package.json +3 -2
@@ -5,72 +5,5 @@
5
5
  * override for the project-local `.aiwg/` artifact root. Keep that contract
6
6
  * centralized so callers do not hardcode `<project>/.aiwg`.
7
7
  */
8
- import { homedir } from 'os';
9
- import { existsSync, readFileSync } from 'fs';
10
- import { isAbsolute, join, resolve } from 'path';
11
- export const DEFAULT_PROJECT_AIWG_DIR = '.aiwg';
12
- export const AIWG_ARTIFACTS_PATH_ENV = 'AIWG_ARTIFACTS_PATH';
13
- export const PROJECT_AIWG_LOCATION_FILE = '.aiwg-location';
14
- const ARTIFACT_PATH_ENV_ALIASES = [
15
- AIWG_ARTIFACTS_PATH_ENV,
16
- 'AIWG_PROJECT_ARTIFACTS_PATH',
17
- 'AIWG_PROJECT_AIWG_DIR',
18
- ];
19
- export function expandProjectArtifactPath(pathValue, projectDir) {
20
- const trimmed = pathValue.trim();
21
- if (trimmed === '~')
22
- return homedir();
23
- if (trimmed.startsWith('~/'))
24
- return resolve(homedir(), trimmed.slice(2));
25
- if (isAbsolute(trimmed))
26
- return trimmed;
27
- return resolve(projectDir, trimmed);
28
- }
29
- export function parseProjectArtifactLocation(contents) {
30
- for (const rawLine of contents.split(/\r?\n/)) {
31
- let line = rawLine.trim();
32
- if (line.length === 0 || line.startsWith('#'))
33
- continue;
34
- if (line.startsWith('export '))
35
- line = line.slice('export '.length).trim();
36
- const assignment = line.match(/^AIWG_ARTIFACTS_PATH\s*=\s*(.+)$/);
37
- if (assignment)
38
- line = assignment[1].trim();
39
- if ((line.startsWith('"') && line.endsWith('"')) ||
40
- (line.startsWith("'") && line.endsWith("'"))) {
41
- line = line.slice(1, -1);
42
- }
43
- return line.length > 0 ? line : null;
44
- }
45
- return null;
46
- }
47
- export function readProjectArtifactLocation(projectDir) {
48
- const pointerPath = resolve(projectDir, PROJECT_AIWG_LOCATION_FILE);
49
- if (!existsSync(pointerPath))
50
- return null;
51
- return parseProjectArtifactLocation(readFileSync(pointerPath, 'utf-8'));
52
- }
53
- /**
54
- * Resolve the directory that contains AIWG project artifacts.
55
- *
56
- * Defaults to `<projectDir>/.aiwg`. When `AIWG_ARTIFACTS_PATH` is set, the
57
- * override may be absolute, project-relative, or `~/`-relative. The override
58
- * intentionally points at the artifact directory itself, not its parent, so
59
- * callers can rename `.aiwg` or place it outside the checkout.
60
- */
61
- export function resolveProjectAiwgDir(projectDir, env = process.env) {
62
- for (const key of ARTIFACT_PATH_ENV_ALIASES) {
63
- const value = env[key];
64
- if (typeof value === 'string' && value.trim().length > 0) {
65
- return expandProjectArtifactPath(value, projectDir);
66
- }
67
- }
68
- const configuredLocation = readProjectArtifactLocation(projectDir);
69
- if (configuredLocation)
70
- return expandProjectArtifactPath(configuredLocation, projectDir);
71
- return resolve(projectDir, DEFAULT_PROJECT_AIWG_DIR);
72
- }
73
- export function projectAiwgPath(projectDir, ...segments) {
74
- return join(resolveProjectAiwgDir(projectDir), ...segments);
75
- }
8
+ export { AIWG_ARTIFACTS_PATH_ENV, DEFAULT_PROJECT_AIWG_DIR, PROJECT_AIWG_LOCATION_FILE, expandProjectArtifactPath, parseProjectArtifactLocation, projectAiwgPath, readProjectArtifactLocation, resolveProjectAiwgDir, } from './project-artifacts-runtime.mjs';
76
9
  //# sourceMappingURL=project-artifacts.js.map
@@ -1272,7 +1272,7 @@ export const discoverCommand = {
1272
1272
  ],
1273
1273
  commandHint: {
1274
1274
  template: 'utility',
1275
- argumentHint: '"<phrase>" [--limit N] [--type skill,agent,...] [--json] [--resource-source local|web|auto] [--aiwg-version <exact-or-channel>] [--offline]',
1275
+ argumentHint: '"<phrase>" [--limit N] [--type skill,agent,...] [--json] [--resource-source local|web|auto] [--aiwg-version <version|range|digest|channel>] [--offline]',
1276
1276
  allowedTools: ['Read'],
1277
1277
  },
1278
1278
  },
@@ -1309,11 +1309,44 @@ export const showCommand = {
1309
1309
  ],
1310
1310
  commandHint: {
1311
1311
  template: 'utility',
1312
- argumentHint: '<type> <name> [--json] [--first] [--resource-source local|web|auto] [--aiwg-version <exact-or-channel>] [--offline] # type: skill | agent | command | rule',
1312
+ argumentHint: '<type> <name> [--json] [--first] [--resource-source local|web|auto] [--aiwg-version <version|range|digest|channel>] [--offline] # type: skill | agent | command | rule',
1313
1313
  allowedTools: ['Read'],
1314
1314
  },
1315
1315
  },
1316
1316
  };
1317
+ export const versionsCommand = {
1318
+ id: 'versions',
1319
+ type: 'skill',
1320
+ name: 'Resource Versions',
1321
+ description: 'Browse and resolve signed AIWG web resource releases',
1322
+ version: '1.0.0',
1323
+ capabilities: ['cli', 'resources', 'versions', 'release', 'integrity', 'cache'],
1324
+ keywords: ['versions', 'resource versions', 'aiwg version', 'release manifest', 'web resources', 'stable channel'],
1325
+ category: 'index',
1326
+ platforms: {
1327
+ claude: 'full',
1328
+ generic: 'full',
1329
+ },
1330
+ deployment: {
1331
+ pathTemplate: '.{platform}/commands/{id}.md',
1332
+ core: true,
1333
+ },
1334
+ metadata: {
1335
+ type: 'skill',
1336
+ triggerPhrases: [
1337
+ 'aiwg versions',
1338
+ 'resource versions',
1339
+ 'resolve aiwg version',
1340
+ 'show release manifest',
1341
+ 'list AIWG resource channels',
1342
+ ],
1343
+ commandHint: {
1344
+ template: 'utility',
1345
+ argumentHint: 'list|resolve|show [version|range|digest|channel] [--json] [--pretty] [--offline] [--channels stable,latest]',
1346
+ allowedTools: ['Read', 'Bash'],
1347
+ },
1348
+ },
1349
+ };
1317
1350
  // Features: list, inspect (and eventually install) AIWG's optional
1318
1351
  // runtime features — embeddings, sqlite, pty, webserver. (#1219)
1319
1352
  export const featuresCommand = {
@@ -3379,6 +3412,7 @@ export const commandDefinitions = [
3379
3412
  researchQueryCommand,
3380
3413
  discoverCommand,
3381
3414
  showCommand,
3415
+ versionsCommand,
3382
3416
  // Optional Features (1)
3383
3417
  featuresCommand,
3384
3418
  // Reproducibility (4)
@@ -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