@aiwg/cli 2026.7.19 → 2026.7.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +397 -385
  2. package/dist/src/api/index.d.ts +1 -0
  3. package/dist/src/api/index.js +1 -0
  4. package/dist/src/artifacts/browser-export.js +7 -0
  5. package/dist/src/artifacts/citation-parser.js +96 -35
  6. package/dist/src/artifacts/cli.js +59 -5
  7. package/dist/src/artifacts/discover-facets.js +15 -0
  8. package/dist/src/artifacts/discovery-eval.js +290 -0
  9. package/dist/src/artifacts/fortemi-core-query-adapter.js +1 -1
  10. package/dist/src/artifacts/fortemi-shard-export.js +1 -1
  11. package/dist/src/artifacts/index-builder.js +54 -17
  12. package/dist/src/artifacts/query-engine.js +10 -6
  13. package/dist/src/artifacts/state-transfer.js +27 -0
  14. package/dist/src/artifacts/stats.js +8 -0
  15. package/dist/src/cli/cli-extension-loader.js +73 -0
  16. package/dist/src/cli/handlers/help.js +2 -1
  17. package/dist/src/cli/handlers/index.js +6 -2
  18. package/dist/src/cli/handlers/resource-versions.js +247 -0
  19. package/dist/src/cli/handlers/sessions.js +966 -0
  20. package/dist/src/cli/handlers/skill-lint.js +49 -45
  21. package/dist/src/cli/handlers/subcommands.js +55 -3
  22. package/dist/src/cli/handlers/use.js +154 -59
  23. package/dist/src/cli/handlers/utilities.js +49 -34
  24. package/dist/src/cli/skill-usage.js +146 -24
  25. package/dist/src/config/cli.js +13 -9
  26. package/dist/src/config/project-artifacts-runtime.mjs +68 -0
  27. package/dist/src/config/project-artifacts.js +1 -68
  28. package/dist/src/extensions/commands/definitions.js +65 -2
  29. package/dist/src/extensions/manifest.js +29 -0
  30. package/dist/src/extensions/project-local-discovery.js +86 -2
  31. package/dist/src/extensions/project-local-remove.js +52 -56
  32. package/dist/src/extensions/shadow-resolver.js +3 -1
  33. package/dist/src/plugins/standalone-packager.js +143 -0
  34. package/dist/src/resources/cache-cleanup.js +67 -0
  35. package/dist/src/resources/doctor.js +107 -0
  36. package/dist/src/resources/lockfile.js +125 -0
  37. package/dist/src/resources/resolver.js +133 -0
  38. package/dist/src/resources/web-release.d.ts +8 -0
  39. package/dist/src/resources/web-release.js +159 -1
  40. package/dist/src/sessions/adapters/claude.js +357 -0
  41. package/dist/src/sessions/adapters/codex.js +521 -0
  42. package/dist/src/sessions/adapters/copilot.js +226 -0
  43. package/dist/src/sessions/adapters/cursor.js +372 -0
  44. package/dist/src/sessions/adapters/factory.js +345 -0
  45. package/dist/src/sessions/adapters/generic.js +225 -0
  46. package/dist/src/sessions/adapters/hermes.js +341 -0
  47. package/dist/src/sessions/adapters/openclaw.js +381 -0
  48. package/dist/src/sessions/adapters/opencode.js +454 -0
  49. package/dist/src/sessions/adapters/openhuman.js +315 -0
  50. package/dist/src/sessions/adapters/warp.js +160 -0
  51. package/dist/src/sessions/adapters/windsurf.js +212 -0
  52. package/dist/src/sessions/candidates.js +210 -0
  53. package/dist/src/sessions/contracts.js +310 -0
  54. package/dist/src/sessions/discovery.js +51 -0
  55. package/dist/src/sessions/fixtures.js +12 -0
  56. package/dist/src/sessions/importer.js +315 -0
  57. package/dist/src/sessions/index.js +25 -0
  58. package/dist/src/sessions/knowledge-shard.js +61 -0
  59. package/dist/src/sessions/optional-backends.js +238 -0
  60. package/dist/src/sessions/policy.js +192 -0
  61. package/dist/src/sessions/ports.js +2 -0
  62. package/dist/src/sessions/promotion.js +367 -0
  63. package/dist/src/sessions/readers.js +176 -0
  64. package/dist/src/sessions/repository.js +1551 -0
  65. package/dist/src/skills/adapters/agent-skills.js +59 -0
  66. package/dist/src/skills/adapters/local.js +19 -1
  67. package/dist/src/skills/agent-skills.js +249 -0
  68. package/dist/src/skills/cli.js +463 -7
  69. package/dist/src/skills/deployer.js +554 -0
  70. package/dist/src/skills/doctor.js +105 -0
  71. package/dist/src/skills/exporter.js +382 -0
  72. package/dist/src/skills/importer.js +921 -0
  73. package/dist/src/skills/registry.js +19 -0
  74. package/dist/src/skills/validator.js +323 -0
  75. package/dist/src/smiths/context-pipeline/aiwg-md.js +5 -1
  76. package/dist/src/smiths/context-pipeline/claude-hook.js +21 -1
  77. package/dist/src/smiths/context-pipeline/finalization.js +5 -3
  78. package/dist/src/smiths/context-pipeline/generator.js +4 -1
  79. package/dist/src/smiths/context-pipeline/parallelism-section.js +34 -1
  80. package/dist/src/smiths/context-pipeline/workspace-context.js +15 -3
  81. package/dist/src/smiths/mcpsmith/example.js +3 -1
  82. package/dist/src/smiths/mcpsmith/generator.js +3 -1
  83. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +2 -1
  84. package/dist/src/storage/cli.js +3 -2
  85. package/dist/src/storage/subsystem-cli.js +7 -2
  86. package/dist/src/update/notifier.mjs +1 -1
  87. package/dist/src/update/service.mjs +123 -0
  88. package/package.json +3 -2
@@ -0,0 +1,107 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { getResourceCacheRoot, readVerifiedRegularFile } from "./web-release.js";
4
+ import { readResourceLockfile, resourceLockfilePath } from "./lockfile.js";
5
+ const MAX_RELEASE_MANIFEST_BYTES = 4 * 1024 * 1024;
6
+ function mark(severity) {
7
+ if (severity === "ok")
8
+ return "✓";
9
+ if (severity === "info")
10
+ return "•";
11
+ if (severity === "warning")
12
+ return "⚠";
13
+ return "✗";
14
+ }
15
+ function cacheGenerationDir(cacheRoot, version, manifestSha256) {
16
+ return path.join(cacheRoot, "releases", version, manifestSha256);
17
+ }
18
+ function isRealDirectory(pathname) {
19
+ try {
20
+ const stat = fs.lstatSync(pathname);
21
+ return stat.isDirectory() && !stat.isSymbolicLink();
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ export function buildWebResourceDoctorSection(projectDir, options = {}) {
28
+ const diagnostics = [];
29
+ const cacheRoot = getResourceCacheRoot(options.cacheRoot);
30
+ let lockfile;
31
+ try {
32
+ lockfile = readResourceLockfile(projectDir);
33
+ }
34
+ catch (error) {
35
+ diagnostics.push({
36
+ severity: "error",
37
+ code: "resource-lockfile-invalid",
38
+ message: `${resourceLockfilePath(projectDir)} is invalid: ${error instanceof Error ? error.message : String(error)}`,
39
+ });
40
+ }
41
+ if (!lockfile) {
42
+ diagnostics.push({
43
+ severity: "info",
44
+ code: "resource-lockfile-missing",
45
+ message: `No ${resourceLockfilePath(projectDir)} found; web resource source mode is not pinned for this project.`,
46
+ });
47
+ }
48
+ else {
49
+ for (const [key, resource] of Object.entries(lockfile.resources)) {
50
+ diagnostics.push({
51
+ severity: "info",
52
+ code: "resource-source-mode",
53
+ message: `${key}: source=${resource.source}, selector=${resource.selector}, resolved=${resource.version}, manifest=${resource.manifestSha256}`,
54
+ });
55
+ const generationDir = cacheGenerationDir(cacheRoot, resource.version, resource.manifestSha256);
56
+ if (!isRealDirectory(generationDir)) {
57
+ diagnostics.push({
58
+ severity: "warning",
59
+ code: "resource-cache-miss",
60
+ message: `${key}: locked release ${resource.version}/${resource.manifestSha256} is not present in ${cacheRoot}; run the pinned versions command once online to warm the cache.`,
61
+ });
62
+ continue;
63
+ }
64
+ try {
65
+ readVerifiedRegularFile(path.join(generationDir, "manifest.json"), {
66
+ label: `${key} locked resource manifest`,
67
+ maxBytes: MAX_RELEASE_MANIFEST_BYTES,
68
+ expectedSha256: resource.manifestSha256,
69
+ });
70
+ readVerifiedRegularFile(path.join(generationDir, "raw", "prebuilt", "fortemi-core", "framework", "manifest.json"), {
71
+ label: `${key} locked Fortemi Core manifest`,
72
+ maxBytes: MAX_RELEASE_MANIFEST_BYTES,
73
+ expectedSize: resource.fortemiCore.manifestSize,
74
+ expectedSha256: resource.fortemiCore.manifestSha256,
75
+ });
76
+ readVerifiedRegularFile(path.join(generationDir, "raw", "prebuilt", "fortemi-core", "framework", "aiwg-fortemi-index-v2.json"), {
77
+ label: `${key} locked Fortemi Core export`,
78
+ maxBytes: resource.fortemiCore.exportSize,
79
+ expectedSize: resource.fortemiCore.exportSize,
80
+ expectedSha256: resource.fortemiCore.exportSha256,
81
+ });
82
+ diagnostics.push({
83
+ severity: "ok",
84
+ code: "resource-cache-verified",
85
+ message: `${key}: locked release cache is warm and digest-verified.`,
86
+ });
87
+ }
88
+ catch (error) {
89
+ diagnostics.push({
90
+ severity: "error",
91
+ code: "resource-cache-drift",
92
+ message: `${key}: locked release cache drift detected: ${error instanceof Error ? error.message : String(error)}`,
93
+ });
94
+ }
95
+ }
96
+ }
97
+ const lines = ["\n── Web resource cache ──"];
98
+ for (const diagnostic of diagnostics) {
99
+ lines.push(` ${mark(diagnostic.severity)} ${diagnostic.message}`);
100
+ }
101
+ return {
102
+ output: lines.join("\n"),
103
+ diagnostics,
104
+ hasFailures: diagnostics.some((diagnostic) => diagnostic.severity === "error"),
105
+ };
106
+ }
107
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1,125 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { projectAiwgPath, resolveProjectAiwgDir } from "../config/project-artifacts.js";
5
+ export const RESOURCE_LOCKFILE_SCHEMA = "aiwg.resources-lock/v1";
6
+ export const RESOURCE_LOCKFILE_NAME = "resources.lock.json";
7
+ export const DEFAULT_RESOURCE_LOCK_KEY = "framework";
8
+ function isRecord(value) {
9
+ return value !== null && typeof value === "object" && !Array.isArray(value);
10
+ }
11
+ function validateLockedResource(value, key) {
12
+ if (!isRecord(value))
13
+ throw new Error(`resource lock entry '${key}' must be an object`);
14
+ if (value.source !== "web")
15
+ throw new Error(`resource lock entry '${key}' has an unsupported source`);
16
+ for (const field of ["selector", "selectorKind", "version", "manifestUrl", "baseUrl", "manifestSha256", "lockedAt"]) {
17
+ if (typeof value[field] !== "string")
18
+ throw new Error(`resource lock entry '${key}' has an invalid ${field}`);
19
+ }
20
+ if (value.selectorKind !== "exact" &&
21
+ value.selectorKind !== "channel" &&
22
+ value.selectorKind !== "range" &&
23
+ value.selectorKind !== "digest") {
24
+ throw new Error(`resource lock entry '${key}' has an invalid selectorKind`);
25
+ }
26
+ if (typeof value.descriptorCount !== "number" || !Number.isSafeInteger(value.descriptorCount) || value.descriptorCount < 0) {
27
+ throw new Error(`resource lock entry '${key}' has an invalid descriptorCount`);
28
+ }
29
+ if (value.channelSequence !== undefined && (typeof value.channelSequence !== "number" ||
30
+ !Number.isSafeInteger(value.channelSequence) ||
31
+ value.channelSequence < 1)) {
32
+ throw new Error(`resource lock entry '${key}' has an invalid channelSequence`);
33
+ }
34
+ if (!isRecord(value.fortemiCore))
35
+ throw new Error(`resource lock entry '${key}' has invalid fortemiCore metadata`);
36
+ const fortemiCore = value.fortemiCore;
37
+ for (const field of ["manifestSha256", "exportSha256"]) {
38
+ if (typeof fortemiCore[field] !== "string") {
39
+ throw new Error(`resource lock entry '${key}' has an invalid fortemiCore.${field}`);
40
+ }
41
+ }
42
+ for (const field of ["manifestSize", "exportSize"]) {
43
+ if (typeof fortemiCore[field] !== "number" || !Number.isSafeInteger(fortemiCore[field]) || fortemiCore[field] < 0) {
44
+ throw new Error(`resource lock entry '${key}' has an invalid fortemiCore.${field}`);
45
+ }
46
+ }
47
+ return value;
48
+ }
49
+ export function resourceLockfilePath(projectDir) {
50
+ return projectAiwgPath(projectDir, RESOURCE_LOCKFILE_NAME);
51
+ }
52
+ export function releaseToLockedWebResource(release, now = new Date()) {
53
+ return {
54
+ source: "web",
55
+ selector: release.selector,
56
+ selectorKind: release.selectorKind,
57
+ version: release.version,
58
+ manifestUrl: release.manifestUrl,
59
+ baseUrl: release.baseUrl,
60
+ manifestSha256: release.manifestDigest,
61
+ ...(release.channelSequence === undefined ? {} : { channelSequence: release.channelSequence }),
62
+ fortemiCore: {
63
+ manifestSha256: release.fortemiManifestSha256,
64
+ manifestSize: release.fortemiManifestSize,
65
+ exportSha256: release.fortemiExportSha256,
66
+ exportSize: release.fortemiExportSize,
67
+ },
68
+ descriptorCount: release.descriptors.size,
69
+ lockedAt: now.toISOString(),
70
+ };
71
+ }
72
+ export function readResourceLockfile(projectDir) {
73
+ const pathname = resourceLockfilePath(projectDir);
74
+ if (!fs.existsSync(pathname))
75
+ return null;
76
+ const parsed = JSON.parse(fs.readFileSync(pathname, "utf8"));
77
+ if (!isRecord(parsed) || parsed.schemaVersion !== RESOURCE_LOCKFILE_SCHEMA) {
78
+ throw new Error(`${RESOURCE_LOCKFILE_NAME} has an unsupported schemaVersion`);
79
+ }
80
+ if (typeof parsed.generatedAt !== "string") {
81
+ throw new Error(`${RESOURCE_LOCKFILE_NAME} has an invalid generatedAt`);
82
+ }
83
+ if (!isRecord(parsed.resources)) {
84
+ throw new Error(`${RESOURCE_LOCKFILE_NAME} has an invalid resources object`);
85
+ }
86
+ const resources = {};
87
+ for (const [key, value] of Object.entries(parsed.resources)) {
88
+ resources[key] = validateLockedResource(value, key);
89
+ }
90
+ return {
91
+ schemaVersion: RESOURCE_LOCKFILE_SCHEMA,
92
+ generatedAt: parsed.generatedAt,
93
+ resources,
94
+ };
95
+ }
96
+ export function writeWebResourceLock(projectDir, release, options = {}) {
97
+ const key = options.key ?? DEFAULT_RESOURCE_LOCK_KEY;
98
+ if (!/^[a-z][a-z0-9-]{0,63}$/.test(key))
99
+ throw new Error(`Invalid resource lock key: ${key}`);
100
+ const now = options.now ?? new Date();
101
+ const existing = readResourceLockfile(projectDir);
102
+ const entry = releaseToLockedWebResource(release, now);
103
+ const lockfile = {
104
+ schemaVersion: RESOURCE_LOCKFILE_SCHEMA,
105
+ generatedAt: now.toISOString(),
106
+ resources: {
107
+ ...(existing?.resources ?? {}),
108
+ [key]: entry,
109
+ },
110
+ };
111
+ const dir = resolveProjectAiwgDir(projectDir);
112
+ fs.mkdirSync(dir, { recursive: true });
113
+ const pathname = path.join(dir, RESOURCE_LOCKFILE_NAME);
114
+ const tmpPath = `${pathname}.${randomBytes(6).toString("hex")}.tmp`;
115
+ try {
116
+ fs.writeFileSync(tmpPath, JSON.stringify(lockfile, null, 2) + "\n", { encoding: "utf8", flag: "wx" });
117
+ fs.renameSync(tmpPath, pathname);
118
+ }
119
+ catch (error) {
120
+ fs.rmSync(tmpPath, { force: true });
121
+ throw error;
122
+ }
123
+ return { path: pathname, lockfile, entry };
124
+ }
125
+ //# sourceMappingURL=lockfile.js.map
@@ -0,0 +1,133 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fetchVerifiedRawResource, resolveWebRelease, } from "./web-release.js";
4
+ function assertSafeSegment(value, label) {
5
+ if (!/^[A-Za-z0-9._-]+$/.test(value) || value === "." || value === "..") {
6
+ throw new Error(`${label} is not a safe AIWG resource segment`);
7
+ }
8
+ }
9
+ function assertSafeRelativePath(value) {
10
+ if (value.length === 0 ||
11
+ value.startsWith("/") ||
12
+ value.includes("\\") ||
13
+ value.split("/").some((part) => part === "" || part === "." || part === "..")) {
14
+ throw new Error("AIWG logical resource path is not safe");
15
+ }
16
+ for (const part of value.split("/"))
17
+ assertSafeSegment(part, "AIWG logical resource path segment");
18
+ }
19
+ export function parseAiwgResourceId(logicalId) {
20
+ let url;
21
+ try {
22
+ url = new URL(logicalId);
23
+ }
24
+ catch {
25
+ throw new Error(`Invalid AIWG logical resource ID: ${logicalId}`);
26
+ }
27
+ if (url.protocol !== "aiwg:")
28
+ throw new Error(`Unsupported AIWG logical resource protocol: ${url.protocol}`);
29
+ if (url.search || url.hash || url.username || url.password) {
30
+ throw new Error("AIWG logical resource IDs must not include auth, query, or fragment components");
31
+ }
32
+ const namespace = url.hostname;
33
+ const parts = url.pathname.split("/").filter(Boolean);
34
+ let relativePath;
35
+ if (namespace === "frameworks") {
36
+ const [name, ...rest] = parts;
37
+ if (!name || rest.length === 0)
38
+ throw new Error("Framework resource IDs require a name and path");
39
+ assertSafeSegment(name, "framework name");
40
+ const suffix = rest.join("/");
41
+ assertSafeRelativePath(suffix);
42
+ relativePath = `agentic/code/frameworks/${name}/${suffix}`;
43
+ }
44
+ else if (namespace === "addons") {
45
+ const [name, ...rest] = parts;
46
+ if (!name || rest.length === 0)
47
+ throw new Error("Addon resource IDs require a name and path");
48
+ assertSafeSegment(name, "addon name");
49
+ const suffix = rest.join("/");
50
+ assertSafeRelativePath(suffix);
51
+ relativePath = `agentic/code/addons/${name}/${suffix}`;
52
+ }
53
+ else if (namespace === "core") {
54
+ if (parts.length < 2)
55
+ throw new Error("Core resource IDs require a kind and path");
56
+ const [kind, ...rest] = parts;
57
+ assertSafeSegment(kind, "core resource kind");
58
+ const suffix = rest.join("/");
59
+ assertSafeRelativePath(suffix);
60
+ relativePath = `agentic/code/${kind}/${suffix}`;
61
+ }
62
+ else {
63
+ throw new Error(`Unsupported AIWG logical resource namespace: ${namespace}`);
64
+ }
65
+ return {
66
+ logicalId,
67
+ relativePath,
68
+ rawPath: `raw/${relativePath}`,
69
+ };
70
+ }
71
+ export function logicalIdFromFirstPartyPath(indexedPath) {
72
+ const normalized = indexedPath.replace(/\\/g, "/").replace(/^raw\//, "").replace(/^\/+/, "");
73
+ const parts = normalized.split("/");
74
+ if (parts[0] !== "agentic" || parts[1] !== "code")
75
+ return null;
76
+ if ((parts[2] === "frameworks" || parts[2] === "addons") && parts.length > 4) {
77
+ return `aiwg://${parts[2]}/${parts[3]}/${parts.slice(4).join("/")}`;
78
+ }
79
+ if (parts.length > 3)
80
+ return `aiwg://core/${parts[2]}/${parts.slice(3).join("/")}`;
81
+ return null;
82
+ }
83
+ async function readLocalResource(parsed, frameworkRoot) {
84
+ const root = path.resolve(frameworkRoot);
85
+ const pathname = path.resolve(root, parsed.relativePath);
86
+ if (!pathname.startsWith(`${root}${path.sep}`))
87
+ throw new Error("AIWG local resource resolved outside the framework root");
88
+ return fs.readFile(pathname);
89
+ }
90
+ export async function resolveAiwgResourceBytes(logicalId, options) {
91
+ const parsed = parseAiwgResourceId(logicalId);
92
+ const requestedSource = options.source ?? "local";
93
+ const diagnostics = [];
94
+ if (requestedSource === "local" || requestedSource === "auto") {
95
+ try {
96
+ const bytes = await readLocalResource(parsed, options.frameworkRoot);
97
+ diagnostics.push(`source=${requestedSource === "auto" ? "auto(local)" : "local"}`);
98
+ return {
99
+ logicalId,
100
+ source: "local",
101
+ path: path.resolve(options.frameworkRoot, parsed.relativePath),
102
+ bytes,
103
+ diagnostics,
104
+ };
105
+ }
106
+ catch (error) {
107
+ if (requestedSource === "local")
108
+ throw error;
109
+ diagnostics.push(`local-miss=${error instanceof Error ? error.message : String(error)}`);
110
+ }
111
+ }
112
+ const webRelease = options.webRelease ?? await resolveWebRelease({
113
+ ...options.webReleaseOptions,
114
+ selector: options.selector ?? "stable",
115
+ offline: options.offline,
116
+ });
117
+ const bytes = await fetchVerifiedRawResource(webRelease, parsed.rawPath, {
118
+ baseUrl: options.webReleaseOptions?.baseUrl,
119
+ fetcher: options.webReleaseOptions?.fetcher,
120
+ allowInsecureLoopbackHttp: options.webReleaseOptions?.allowInsecureLoopbackHttp,
121
+ offline: options.offline,
122
+ });
123
+ diagnostics.push(`source=${requestedSource === "auto" ? "auto(web)" : "web"}`);
124
+ return {
125
+ logicalId,
126
+ source: "web",
127
+ path: parsed.rawPath,
128
+ bytes,
129
+ webRelease,
130
+ diagnostics,
131
+ };
132
+ }
133
+ //# sourceMappingURL=resolver.js.map
@@ -7,6 +7,14 @@ export type ResourceSelector = {
7
7
  } | {
8
8
  kind: "channel";
9
9
  value: string;
10
+ } | {
11
+ kind: "range";
12
+ value: string;
13
+ normalizedRange: string;
14
+ } | {
15
+ kind: "digest";
16
+ value: string;
17
+ digest: string;
10
18
  };
11
19
  export interface ResourceFetchResponse {
12
20
  ok: boolean;
@@ -2,6 +2,7 @@ import { createHash, createPublicKey, randomUUID, verify } from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
+ import semver from "semver";
5
6
  export const DEFAULT_RESOURCE_BASE_URL = "https://releases.aiwg.io";
6
7
  export const AIWG_RELEASE_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
7
8
  MCowBQYDK2VwAyEA8BsJ2vjuHBReexz328sknfL7MKUtxynX6MGfqFVMD38=
@@ -9,6 +10,7 @@ MCowBQYDK2VwAyEA8BsJ2vjuHBReexz328sknfL7MKUtxynX6MGfqFVMD38=
9
10
  const EXACT_VERSION_PATTERN = /^(?:19|20)\d{2}\.(?:[1-9]|1[0-2])\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$/;
10
11
  const CHANNEL_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
11
12
  const SHA256_PATTERN = /^[0-9a-f]{64}$/;
13
+ const DIGEST_SELECTOR_PATTERN = /^sha256:([0-9a-f]{64})$/;
12
14
  const SIGNATURE_PATTERN = /^(?:[A-Za-z0-9+/]{4}){21}(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)$/;
13
15
  const RELEASE_MANIFEST_SCHEMAS = new Set([
14
16
  "aiwg.resource-manifest/v1",
@@ -23,6 +25,8 @@ const MAX_SIGNATURE_BYTES = 64 * 1024;
23
25
  const MAX_COMPLETION_MARKER_BYTES = 4 * 1024;
24
26
  const MAX_CHANNEL_CACHE_CANDIDATES = 128;
25
27
  const MAX_RELEASE_CACHE_CANDIDATES = 32;
28
+ const MAX_VERSION_INDEX_BYTES = 4 * 1024 * 1024;
29
+ const MAX_VERSION_INDEX_ENTRIES = 2048;
26
30
  // A cold lightweight-CLI install downloads the signed release manifest
27
31
  // (~2 MiB) and Fortemi export (~11 MiB today). Fifteen seconds proved too
28
32
  // aggressive on otherwise healthy container and remote-network paths. Keep a
@@ -85,12 +89,24 @@ export function verifySignedResourceBytes(bytes, signatureBytes, publicKeyPem =
85
89
  return digest;
86
90
  }
87
91
  export function parseResourceSelector(selector) {
92
+ if (selector.trim() !== selector || selector.length === 0 || /^v\d/.test(selector)) {
93
+ throw new Error(`Unsupported AIWG resource selector '${selector}'. This release supports exact calendar-semver versions, SemVer ranges, sha256 manifest digests, or channel names.`);
94
+ }
88
95
  if (EXACT_VERSION_PATTERN.test(selector))
89
96
  return { kind: "exact", value: selector };
97
+ const digestMatch = DIGEST_SELECTOR_PATTERN.exec(selector);
98
+ if (digestMatch)
99
+ return { kind: "digest", value: selector, digest: digestMatch[1] };
90
100
  if (CHANNEL_PATTERN.test(selector) && !/^v\d/.test(selector)) {
91
101
  return { kind: "channel", value: selector };
92
102
  }
93
- throw new Error(`Unsupported AIWG resource selector '${selector}'. This release supports exact calendar-semver versions or channel names only.`);
103
+ const normalizedRange = semver.validRange(selector, { includePrerelease: true });
104
+ const explicitRange = /[<>=~^*xX|\s]/.test(selector);
105
+ const versionLikeTokens = selector.match(/\b(?:19|20)\d{2}\.\d+\.\d+(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?\b/g) ?? [];
106
+ const calendarVersionTokens = versionLikeTokens.every((token) => EXACT_VERSION_PATTERN.test(token));
107
+ if (normalizedRange && explicitRange && calendarVersionTokens)
108
+ return { kind: "range", value: selector, normalizedRange };
109
+ throw new Error(`Unsupported AIWG resource selector '${selector}'. This release supports exact calendar-semver versions, SemVer ranges, sha256 manifest digests, or channel names.`);
94
110
  }
95
111
  function assertSafeRelativePath(value, label) {
96
112
  if (typeof value !== "string" ||
@@ -173,6 +189,43 @@ function validateChannelManifest(value, channel) {
173
189
  }
174
190
  return value;
175
191
  }
192
+ function validateVersionIndex(value) {
193
+ if (!isRecord(value) || value.schemaVersion !== "aiwg.resource-version-index/v1") {
194
+ throw new Error("resource version index has an unsupported schemaVersion");
195
+ }
196
+ if (!Array.isArray(value.versions) || value.versions.length > MAX_VERSION_INDEX_ENTRIES) {
197
+ throw new Error("resource version index has an invalid versions list");
198
+ }
199
+ const seenVersions = new Set();
200
+ const seenDigests = new Set();
201
+ const versions = [];
202
+ for (const entry of value.versions) {
203
+ if (!isRecord(entry) || typeof entry.version !== "string" || !EXACT_VERSION_PATTERN.test(entry.version)) {
204
+ throw new Error("resource version index contains an invalid version");
205
+ }
206
+ const expectedPath = `/resources/${entry.version}/manifest.json`;
207
+ if (entry.releaseManifest !== expectedPath) {
208
+ throw new Error(`resource version index entry ${entry.version} has an invalid release manifest path`);
209
+ }
210
+ if (typeof entry.releaseManifestSha256 !== "string" || !SHA256_PATTERN.test(entry.releaseManifestSha256)) {
211
+ throw new Error(`resource version index entry ${entry.version} has an invalid release manifest digest`);
212
+ }
213
+ if (seenVersions.has(entry.version))
214
+ throw new Error(`resource version index contains duplicate version ${entry.version}`);
215
+ if (seenDigests.has(entry.releaseManifestSha256)) {
216
+ throw new Error(`resource version index contains duplicate digest ${entry.releaseManifestSha256}`);
217
+ }
218
+ seenVersions.add(entry.version);
219
+ seenDigests.add(entry.releaseManifestSha256);
220
+ versions.push({
221
+ version: entry.version,
222
+ releaseManifest: entry.releaseManifest,
223
+ releaseManifestSha256: entry.releaseManifestSha256,
224
+ });
225
+ }
226
+ versions.sort((left, right) => semver.rcompare(left.version, right.version));
227
+ return { schemaVersion: "aiwg.resource-version-index/v1", versions };
228
+ }
176
229
  function validateFortemiFiles(manifestBytes, exportBytes) {
177
230
  const manifest = parseJson(manifestBytes, "Fortemi Core manifest");
178
231
  if (!isRecord(manifest) || manifest.schema_version !== "aiwg.fortemi.prebuilt.v1") {
@@ -487,6 +540,16 @@ function verifyCachedGeneration(cacheRoot, version, digest, selector, publicKeyP
487
540
  descriptors,
488
541
  };
489
542
  }
543
+ function cachedReleaseVersions(cacheRoot) {
544
+ const root = path.join(cacheRoot, "releases");
545
+ if (!fs.existsSync(root))
546
+ return [];
547
+ assertCacheDirectory(root, "cached releases directory");
548
+ return fs.readdirSync(root, { withFileTypes: true })
549
+ .filter((entry) => entry.isDirectory() && EXACT_VERSION_PATTERN.test(entry.name))
550
+ .map((entry) => entry.name)
551
+ .sort((left, right) => semver.rcompare(left, right));
552
+ }
490
553
  function cachedDigests(cacheRoot, version) {
491
554
  const root = path.join(cacheRoot, "releases", version);
492
555
  if (!fs.existsSync(root))
@@ -576,6 +639,90 @@ function readCachedChannel(cacheRoot, channel, publicKeyPem) {
576
639
  throw new Error(`cached channel ${channel} is corrupt and cannot be used offline`);
577
640
  return null;
578
641
  }
642
+ function versionIndexCacheDir(cacheRoot) {
643
+ return path.join(cacheRoot, "versions");
644
+ }
645
+ function readCachedVersionIndex(cacheRoot, publicKeyPem) {
646
+ const dir = versionIndexCacheDir(cacheRoot);
647
+ if (!fs.existsSync(dir))
648
+ return null;
649
+ assertCacheDirectory(dir, "cached resource version index directory");
650
+ const bytes = readVerifiedRegularFile(path.join(dir, "versions.json"), {
651
+ label: "cached resource version index",
652
+ maxBytes: MAX_VERSION_INDEX_BYTES,
653
+ });
654
+ const signatureBytes = readVerifiedRegularFile(path.join(dir, "versions.sig"), {
655
+ label: "cached resource version index signature",
656
+ maxBytes: MAX_SIGNATURE_BYTES,
657
+ });
658
+ verifySignedResourceBytes(bytes, signatureBytes, publicKeyPem, "cached resource version index");
659
+ return validateVersionIndex(parseJson(bytes, "cached resource version index"));
660
+ }
661
+ function cacheVersionIndex(cacheRoot, bytes, signatureBytes) {
662
+ const target = versionIndexCacheDir(cacheRoot);
663
+ const stagingRoot = path.join(cacheRoot, ".staging", "versions");
664
+ fs.mkdirSync(stagingRoot, { recursive: true });
665
+ const stage = fs.mkdtempSync(path.join(stagingRoot, "index-"));
666
+ try {
667
+ fs.writeFileSync(path.join(stage, "versions.json"), bytes, { flag: "wx" });
668
+ fs.writeFileSync(path.join(stage, "versions.sig"), signatureBytes, { flag: "wx" });
669
+ if (fs.existsSync(target))
670
+ fs.rmSync(target, { recursive: true, force: true });
671
+ installGeneration(stage, target);
672
+ }
673
+ catch (error) {
674
+ fs.rmSync(stage, { recursive: true, force: true });
675
+ throw error;
676
+ }
677
+ }
678
+ async function fetchAndCacheVersionIndex(base, fetcher, cacheRoot, publicKeyPem) {
679
+ const indexBytes = await fetchBytes(fetcher, resourceUrl(base, "resources/versions.json"), "resource version index", MAX_VERSION_INDEX_BYTES);
680
+ const signatureBytes = await fetchBytes(fetcher, resourceUrl(base, "resources/versions.sig"), "resource version index signature", MAX_SIGNATURE_BYTES);
681
+ verifySignedResourceBytes(indexBytes, signatureBytes, publicKeyPem, "resource version index");
682
+ const index = validateVersionIndex(parseJson(indexBytes, "resource version index"));
683
+ cacheVersionIndex(cacheRoot, indexBytes, signatureBytes);
684
+ return index;
685
+ }
686
+ async function resolveVersionIndex(base, fetcher, cacheRoot, publicKeyPem, offline) {
687
+ if (offline) {
688
+ const cached = readCachedVersionIndex(cacheRoot, publicKeyPem);
689
+ if (cached)
690
+ return cached;
691
+ const versions = cachedReleaseVersions(cacheRoot).flatMap((version) => cachedDigests(cacheRoot, version).map((digest) => ({
692
+ version,
693
+ releaseManifest: `/resources/${version}/manifest.json`,
694
+ releaseManifestSha256: digest,
695
+ })));
696
+ if (versions.length > 0)
697
+ return validateVersionIndex({ schemaVersion: "aiwg.resource-version-index/v1", versions });
698
+ throw new Error("AIWG resource version index is not cached; offline mode cannot resolve range or digest selectors");
699
+ }
700
+ if (!fetcher)
701
+ throw new Error("No fetch implementation is available for AIWG web resources");
702
+ try {
703
+ return await fetchAndCacheVersionIndex(base, fetcher, cacheRoot, publicKeyPem);
704
+ }
705
+ catch (error) {
706
+ const cached = readCachedVersionIndex(cacheRoot, publicKeyPem);
707
+ if (cached)
708
+ return cached;
709
+ throw error;
710
+ }
711
+ }
712
+ function selectVersionFromIndex(index, selector) {
713
+ if (selector.kind === "digest") {
714
+ const found = index.versions.find((entry) => entry.releaseManifestSha256 === selector.digest);
715
+ if (!found)
716
+ throw new Error(`AIWG resource manifest digest ${selector.value} is not present in the signed version index`);
717
+ return found;
718
+ }
719
+ if (selector.kind !== "range")
720
+ throw new Error("version index selection requires a range or digest selector");
721
+ const found = index.versions.find((entry) => semver.satisfies(entry.version, selector.normalizedRange, { includePrerelease: true }));
722
+ if (!found)
723
+ throw new Error(`No AIWG resource release satisfies selector '${selector.value}'`);
724
+ return found;
725
+ }
579
726
  function installGeneration(stageDir, targetDir) {
580
727
  fsyncTree(stageDir);
581
728
  fs.mkdirSync(path.dirname(targetDir), { recursive: true });
@@ -765,6 +912,17 @@ export async function resolveWebRelease(options = {}) {
765
912
  throw new Error("No fetch implementation is available for AIWG web resources");
766
913
  return fetchAndCacheRelease(base, fetcher, cacheRoot, selector, selector.value, publicKeyPem);
767
914
  }
915
+ if (selector.kind === "range" || selector.kind === "digest") {
916
+ const fetcher = options.fetcher ?? globalThis.fetch;
917
+ const index = await resolveVersionIndex(base, fetcher, cacheRoot, publicKeyPem, options.offline);
918
+ const selected = selectVersionFromIndex(index, selector);
919
+ if (options.offline) {
920
+ return resolveOfflineExact(cacheRoot, selector, selected.version, publicKeyPem, base, selected.releaseManifestSha256);
921
+ }
922
+ if (!fetcher)
923
+ throw new Error("No fetch implementation is available for AIWG web resources");
924
+ return fetchAndCacheRelease(base, fetcher, cacheRoot, selector, selected.version, publicKeyPem, selected.releaseManifestSha256);
925
+ }
768
926
  if (options.offline) {
769
927
  const cached = readCachedChannel(cacheRoot, selector.value, publicKeyPem);
770
928
  if (!cached)