@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.
- package/README.md +397 -385
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/browser-export.js +7 -0
- package/dist/src/artifacts/citation-parser.js +96 -35
- 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/index-builder.js +54 -17
- package/dist/src/artifacts/query-engine.js +10 -6
- package/dist/src/artifacts/state-transfer.js +27 -0
- package/dist/src/artifacts/stats.js +8 -0
- package/dist/src/cli/cli-extension-loader.js +73 -0
- package/dist/src/cli/handlers/help.js +2 -1
- package/dist/src/cli/handlers/index.js +6 -2
- package/dist/src/cli/handlers/resource-versions.js +247 -0
- package/dist/src/cli/handlers/sessions.js +966 -0
- package/dist/src/cli/handlers/skill-lint.js +49 -45
- package/dist/src/cli/handlers/subcommands.js +55 -3
- package/dist/src/cli/handlers/use.js +154 -59
- package/dist/src/cli/handlers/utilities.js +49 -34
- package/dist/src/cli/skill-usage.js +146 -24
- 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 +65 -2
- package/dist/src/extensions/manifest.js +29 -0
- 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/sessions/adapters/claude.js +357 -0
- package/dist/src/sessions/adapters/codex.js +521 -0
- package/dist/src/sessions/adapters/copilot.js +226 -0
- package/dist/src/sessions/adapters/cursor.js +372 -0
- package/dist/src/sessions/adapters/factory.js +345 -0
- package/dist/src/sessions/adapters/generic.js +225 -0
- package/dist/src/sessions/adapters/hermes.js +341 -0
- package/dist/src/sessions/adapters/openclaw.js +381 -0
- package/dist/src/sessions/adapters/opencode.js +454 -0
- package/dist/src/sessions/adapters/openhuman.js +315 -0
- package/dist/src/sessions/adapters/warp.js +160 -0
- package/dist/src/sessions/adapters/windsurf.js +212 -0
- package/dist/src/sessions/candidates.js +210 -0
- package/dist/src/sessions/contracts.js +310 -0
- package/dist/src/sessions/discovery.js +51 -0
- package/dist/src/sessions/fixtures.js +12 -0
- package/dist/src/sessions/importer.js +315 -0
- package/dist/src/sessions/index.js +25 -0
- package/dist/src/sessions/knowledge-shard.js +61 -0
- package/dist/src/sessions/optional-backends.js +238 -0
- package/dist/src/sessions/policy.js +192 -0
- package/dist/src/sessions/ports.js +2 -0
- package/dist/src/sessions/promotion.js +367 -0
- package/dist/src/sessions/readers.js +176 -0
- package/dist/src/sessions/repository.js +1551 -0
- package/dist/src/skills/adapters/agent-skills.js +59 -0
- package/dist/src/skills/adapters/local.js +19 -1
- package/dist/src/skills/agent-skills.js +249 -0
- package/dist/src/skills/cli.js +463 -7
- package/dist/src/skills/deployer.js +554 -0
- package/dist/src/skills/doctor.js +105 -0
- package/dist/src/skills/exporter.js +382 -0
- package/dist/src/skills/importer.js +921 -0
- package/dist/src/skills/registry.js +19 -0
- package/dist/src/skills/validator.js +323 -0
- 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
|
@@ -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 <
|
|
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 <
|
|
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
|
-
*
|
|
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
|