@aiwg/cli 2026.7.19 → 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.
- package/README.md +397 -385
- package/dist/src/artifacts/cli.js +59 -5
- package/dist/src/artifacts/discover-facets.js +15 -0
- package/dist/src/artifacts/discovery-eval.js +290 -0
- package/dist/src/artifacts/fortemi-core-query-adapter.js +1 -1
- package/dist/src/artifacts/fortemi-shard-export.js +1 -1
- package/dist/src/artifacts/query-engine.js +10 -6
- package/dist/src/cli/handlers/help.js +2 -1
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/resource-versions.js +247 -0
- package/dist/src/cli/handlers/subcommands.js +55 -3
- package/dist/src/cli/handlers/use.js +15 -3
- package/dist/src/cli/handlers/utilities.js +27 -26
- package/dist/src/config/cli.js +13 -9
- package/dist/src/config/project-artifacts-runtime.mjs +68 -0
- package/dist/src/config/project-artifacts.js +1 -68
- package/dist/src/extensions/commands/definitions.js +36 -2
- package/dist/src/extensions/project-local-discovery.js +86 -2
- package/dist/src/extensions/project-local-remove.js +52 -56
- package/dist/src/extensions/shadow-resolver.js +3 -1
- package/dist/src/plugins/standalone-packager.js +143 -0
- package/dist/src/resources/cache-cleanup.js +67 -0
- package/dist/src/resources/doctor.js +107 -0
- package/dist/src/resources/lockfile.js +125 -0
- package/dist/src/resources/resolver.js +133 -0
- package/dist/src/resources/web-release.d.ts +8 -0
- package/dist/src/resources/web-release.js +159 -1
- package/dist/src/smiths/context-pipeline/aiwg-md.js +5 -1
- package/dist/src/smiths/context-pipeline/claude-hook.js +21 -1
- package/dist/src/smiths/context-pipeline/finalization.js +5 -3
- package/dist/src/smiths/context-pipeline/generator.js +4 -1
- package/dist/src/smiths/context-pipeline/parallelism-section.js +34 -1
- package/dist/src/smiths/context-pipeline/workspace-context.js +15 -3
- package/dist/src/smiths/mcpsmith/example.js +3 -1
- package/dist/src/smiths/mcpsmith/generator.js +3 -1
- package/dist/src/smiths/toolsmith/runtime-discovery.mjs +2 -1
- package/dist/src/storage/cli.js +3 -2
- package/dist/src/storage/subsystem-cli.js +7 -2
- package/dist/src/update/notifier.mjs +1 -1
- package/dist/src/update/service.mjs +123 -0
- 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
|
-
|
|
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 <
|
|
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 <
|
|
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
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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
|
|
266
|
-
const k = await classify(artifactHashes[sourceRel], abs);
|
|
255
|
+
const k = await classify(artifactHashes[sourceRel], c);
|
|
267
256
|
if (k !== 'missing') {
|
|
268
|
-
resolvedAbs =
|
|
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]
|
|
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:
|
|
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
|
-
|
|
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
|