@nseng-ai/harness-artifacts 0.1.2
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/package.json +32 -0
- package/src/api.ts +190 -0
- package/src/artifact-catalog.ts +65 -0
- package/src/diagnostic-sort.ts +14 -0
- package/src/filesystem.ts +181 -0
- package/src/first-party-catalog.ts +44 -0
- package/src/first-party-skill-provisioning.ts +162 -0
- package/src/fs-state.ts +22 -0
- package/src/harness-paths.ts +217 -0
- package/src/index.ts +1 -0
- package/src/module-artifact-declaration.ts +273 -0
- package/src/module-artifact-discovery.ts +416 -0
- package/src/ns/command.ts +32 -0
- package/src/ns/commands/install.ts +31 -0
- package/src/ns/commands/list.ts +23 -0
- package/src/ns/commands/path.ts +26 -0
- package/src/ns/commands/update.ts +28 -0
- package/src/ns/preinstalled-catalog.ts +26 -0
- package/src/ns/repo-local-ns-extension.ts +21 -0
- package/src/ns/skills-install.ts +141 -0
- package/src/ns/skills-list.ts +43 -0
- package/src/ns/skills-path.ts +53 -0
- package/src/ns/skills-shared.ts +65 -0
- package/src/ns/update.ts +99 -0
- package/src/ns-toml.ts +195 -0
- package/src/provision-apply.ts +350 -0
- package/src/provision-plan.ts +389 -0
- package/src/reconcile.ts +583 -0
- package/src/skill-frontmatter.ts +151 -0
- package/src/skill-mirror-conventions.ts +128 -0
- package/src/skills-lockfile.ts +107 -0
- package/src/sort.ts +3 -0
package/src/fs-state.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem inspection states shared by harness-artifact convention checks.
|
|
3
|
+
*
|
|
4
|
+
* Moved from `@nseng-ai/areg` (`AregPathState` / `AregTextFileState`) so the
|
|
5
|
+
* harness-artifact subsystem and areg speak one vocabulary for "what is at
|
|
6
|
+
* this path".
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type PathState =
|
|
10
|
+
| { type: "missing" }
|
|
11
|
+
| { type: "file" }
|
|
12
|
+
| { type: "directory" }
|
|
13
|
+
| { type: "symlink"; target: string }
|
|
14
|
+
| { type: "other" };
|
|
15
|
+
|
|
16
|
+
export type TextFileState =
|
|
17
|
+
| { type: "missing" }
|
|
18
|
+
| { type: "file"; text: string }
|
|
19
|
+
| { type: "directory" }
|
|
20
|
+
| { type: "symlink"; target: string }
|
|
21
|
+
| { type: "other" }
|
|
22
|
+
| { type: "unreadable"; message: string };
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
import { resultErr, resultOk, type Result } from "@nseng-ai/foundation/result";
|
|
4
|
+
|
|
5
|
+
import type { HarnessArtifactKind } from "./artifact-catalog.ts";
|
|
6
|
+
|
|
7
|
+
export const HARNESS_SCOPES = ["project", "user"] as const;
|
|
8
|
+
|
|
9
|
+
export type HarnessScope = (typeof HARNESS_SCOPES)[number];
|
|
10
|
+
|
|
11
|
+
export interface HarnessPathEnvironment {
|
|
12
|
+
readonly CLAUDE_CONFIG_DIR?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface HarnessPathContext {
|
|
16
|
+
projectRoot: string;
|
|
17
|
+
homeDir?: string;
|
|
18
|
+
env?: HarnessPathEnvironment;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface HarnessSpecData {
|
|
22
|
+
id: string;
|
|
23
|
+
aliases: readonly string[];
|
|
24
|
+
skillRoots: HarnessScopedPathSpec;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface HarnessScopedPathSpec {
|
|
28
|
+
project: HarnessBasePathSpec;
|
|
29
|
+
user: HarnessBasePathSpec;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type HarnessBasePathSpec =
|
|
33
|
+
| { type: "project"; relativePath: string }
|
|
34
|
+
| { type: "home"; relativePath: string }
|
|
35
|
+
| {
|
|
36
|
+
type: "env-or-home";
|
|
37
|
+
envName: keyof HarnessPathEnvironment;
|
|
38
|
+
homeRelativePath: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export interface ResolvedHarnessSkillRoot {
|
|
42
|
+
harness: HarnessId;
|
|
43
|
+
rootPath: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ResolvedHarnessArtifactPath {
|
|
47
|
+
harness: HarnessId;
|
|
48
|
+
scope: HarnessScope;
|
|
49
|
+
kind: "skill";
|
|
50
|
+
rootPath: string;
|
|
51
|
+
artifactPath: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type HarnessPathErrorInfo =
|
|
55
|
+
| { code: "unknown_harness"; message: string; details: { input: string } }
|
|
56
|
+
| {
|
|
57
|
+
code: "unsupported_artifact_kind";
|
|
58
|
+
message: string;
|
|
59
|
+
details: { harness: HarnessId; kind: HarnessArtifactKind };
|
|
60
|
+
}
|
|
61
|
+
| {
|
|
62
|
+
code: "missing_home_directory";
|
|
63
|
+
message: string;
|
|
64
|
+
details: { harness: HarnessId; scope: "user" };
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const HARNESS_SPECS = [
|
|
68
|
+
{
|
|
69
|
+
id: "claude-code",
|
|
70
|
+
aliases: ["claude"],
|
|
71
|
+
skillRoots: {
|
|
72
|
+
project: { type: "project", relativePath: ".claude/skills" },
|
|
73
|
+
user: {
|
|
74
|
+
type: "env-or-home",
|
|
75
|
+
envName: "CLAUDE_CONFIG_DIR",
|
|
76
|
+
homeRelativePath: ".claude/skills",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "codex",
|
|
82
|
+
aliases: [],
|
|
83
|
+
skillRoots: {
|
|
84
|
+
project: { type: "project", relativePath: ".agents/skills" },
|
|
85
|
+
user: { type: "home", relativePath: ".agents/skills" },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "pi",
|
|
90
|
+
aliases: ["pi-dev"],
|
|
91
|
+
skillRoots: {
|
|
92
|
+
project: { type: "project", relativePath: ".pi/skills" },
|
|
93
|
+
user: { type: "home", relativePath: ".pi/agent/skills" },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
] as const satisfies readonly HarnessSpecData[];
|
|
97
|
+
|
|
98
|
+
export type HarnessId = (typeof HARNESS_SPECS)[number]["id"];
|
|
99
|
+
export type HarnessSpec = (typeof HARNESS_SPECS)[number];
|
|
100
|
+
|
|
101
|
+
const HARNESS_SPEC_BY_ID: ReadonlyMap<HarnessId, HarnessSpec> = new Map(
|
|
102
|
+
HARNESS_SPECS.map((spec) => [spec.id, spec]),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
export const ALL_HARNESS_IDS: readonly HarnessId[] = HARNESS_SPECS.map((spec) => spec.id);
|
|
106
|
+
|
|
107
|
+
export function normalizeHarnessId(input: string): Result<HarnessId, HarnessPathErrorInfo> {
|
|
108
|
+
const normalized = input.trim().toLowerCase();
|
|
109
|
+
for (const spec of HARNESS_SPECS) {
|
|
110
|
+
const aliases: readonly string[] = spec.aliases;
|
|
111
|
+
if (spec.id === normalized || aliases.includes(normalized)) return resultOk(spec.id);
|
|
112
|
+
}
|
|
113
|
+
return resultErr({
|
|
114
|
+
code: "unknown_harness",
|
|
115
|
+
message: `Unknown harness ${JSON.stringify(input)}. Supported harnesses: ${ALL_HARNESS_IDS.join(", ")}.`,
|
|
116
|
+
details: { input },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function resolveHarnessSpec(input: string): Result<HarnessSpec, HarnessPathErrorInfo> {
|
|
121
|
+
const harness = normalizeHarnessId(input);
|
|
122
|
+
if (!harness.ok) return harness;
|
|
123
|
+
const spec = HARNESS_SPEC_BY_ID.get(harness.value);
|
|
124
|
+
if (spec === undefined) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Harness spec missing for normalized harness ${JSON.stringify(harness.value)}.`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return resultOk(spec);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function resolveHarnessSkillRoot(input: {
|
|
133
|
+
harness: string;
|
|
134
|
+
scope: HarnessScope;
|
|
135
|
+
context: HarnessPathContext;
|
|
136
|
+
}): Result<ResolvedHarnessSkillRoot, HarnessPathErrorInfo> {
|
|
137
|
+
const spec = resolveHarnessSpec(input.harness);
|
|
138
|
+
if (!spec.ok) return spec;
|
|
139
|
+
const basePathSpec = spec.value.skillRoots[input.scope];
|
|
140
|
+
if (input.scope === "user" && needsHomeDirectory(basePathSpec, input.context)) {
|
|
141
|
+
return resultErr({
|
|
142
|
+
code: "missing_home_directory",
|
|
143
|
+
message: `${spec.value.id} user-scope provisioning requires a home directory. Set HOME or pass NsExtensionApi.homeDir.`,
|
|
144
|
+
details: { harness: spec.value.id, scope: "user" },
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return resultOk({
|
|
148
|
+
harness: spec.value.id,
|
|
149
|
+
rootPath: resolveBasePath(basePathSpec, input.context),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function resolveHarnessArtifactPath(input: {
|
|
154
|
+
harness: string;
|
|
155
|
+
scope: HarnessScope;
|
|
156
|
+
kind: HarnessArtifactKind;
|
|
157
|
+
artifactName: string;
|
|
158
|
+
context: HarnessPathContext;
|
|
159
|
+
}): Result<ResolvedHarnessArtifactPath, HarnessPathErrorInfo> {
|
|
160
|
+
const root = resolveHarnessSkillRoot(input);
|
|
161
|
+
if (!root.ok) return root;
|
|
162
|
+
if (input.kind !== "skill") {
|
|
163
|
+
return resultErr({
|
|
164
|
+
code: "unsupported_artifact_kind",
|
|
165
|
+
message: `${root.value.harness} does not support provisioning ${input.kind} artifacts in the steelthread path table yet.`,
|
|
166
|
+
details: { harness: root.value.harness, kind: input.kind },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return resultOk({
|
|
170
|
+
harness: root.value.harness,
|
|
171
|
+
scope: input.scope,
|
|
172
|
+
kind: "skill",
|
|
173
|
+
rootPath: root.value.rootPath,
|
|
174
|
+
artifactPath: join(root.value.rootPath, input.artifactName),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function needsHomeDirectory(spec: HarnessBasePathSpec, context: HarnessPathContext): boolean {
|
|
179
|
+
return spec.type !== "project" && resolvedUserScopeBasePath(spec, context) === undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function resolveBasePath(spec: HarnessBasePathSpec, context: HarnessPathContext): string {
|
|
183
|
+
switch (spec.type) {
|
|
184
|
+
case "project":
|
|
185
|
+
return join(context.projectRoot, spec.relativePath);
|
|
186
|
+
case "home":
|
|
187
|
+
case "env-or-home": {
|
|
188
|
+
const basePath = resolvedUserScopeBasePath(spec, context);
|
|
189
|
+
if (basePath === undefined) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
"Harness user-scope path resolution reached home fallback without a home directory.",
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
return basePath;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function resolvedUserScopeBasePath(
|
|
200
|
+
spec: Exclude<HarnessBasePathSpec, { type: "project" }>,
|
|
201
|
+
context: HarnessPathContext,
|
|
202
|
+
): string | undefined {
|
|
203
|
+
if (spec.type === "env-or-home") {
|
|
204
|
+
const configured = context.env?.[spec.envName];
|
|
205
|
+
if (isPresent(configured)) return join(configured, "skills");
|
|
206
|
+
return joinPresent(context.homeDir, spec.homeRelativePath);
|
|
207
|
+
}
|
|
208
|
+
return joinPresent(context.homeDir, spec.relativePath);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function joinPresent(basePath: string | undefined, relativePath: string): string | undefined {
|
|
212
|
+
return isPresent(basePath) ? join(basePath, relativePath) : undefined;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function isPresent(value: string | undefined): value is string {
|
|
216
|
+
return value !== undefined && value.trim() !== "";
|
|
217
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./api.ts";
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { formatErrorMessage, formatZodIssue } from "@nseng-ai/foundation/primitives";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import type { SkillHarnessArtifactEntry } from "./artifact-catalog.ts";
|
|
5
|
+
import { sortDiagnosticsByKey } from "./diagnostic-sort.ts";
|
|
6
|
+
|
|
7
|
+
export const MODULE_ARTIFACT_DECLARATION_DIAGNOSTIC_CODES = [
|
|
8
|
+
"module_artifact_package_json_invalid",
|
|
9
|
+
"module_artifact_package_name_invalid",
|
|
10
|
+
"module_artifact_declarations_not_array",
|
|
11
|
+
"module_artifact_declaration_invalid",
|
|
12
|
+
"module_artifact_kind_unsupported",
|
|
13
|
+
"module_artifact_name_invalid",
|
|
14
|
+
"module_artifact_path_invalid",
|
|
15
|
+
"module_artifact_duplicate_name",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export type ModuleArtifactDeclarationDiagnosticCode =
|
|
19
|
+
(typeof MODULE_ARTIFACT_DECLARATION_DIAGNOSTIC_CODES)[number];
|
|
20
|
+
|
|
21
|
+
export interface ModuleArtifactDeclarationDiagnostic {
|
|
22
|
+
code: ModuleArtifactDeclarationDiagnosticCode;
|
|
23
|
+
message: string;
|
|
24
|
+
path?: string;
|
|
25
|
+
artifactName?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ParseModuleArtifactDeclarationResult =
|
|
29
|
+
| {
|
|
30
|
+
ok: true;
|
|
31
|
+
packageName: string;
|
|
32
|
+
version: string;
|
|
33
|
+
artifacts: readonly SkillHarnessArtifactEntry[];
|
|
34
|
+
diagnostics: readonly ModuleArtifactDeclarationDiagnostic[];
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
ok: false;
|
|
38
|
+
diagnostics: readonly ModuleArtifactDeclarationDiagnostic[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
interface RawSkillDeclaration {
|
|
42
|
+
kind: "skill";
|
|
43
|
+
name: unknown;
|
|
44
|
+
path: unknown;
|
|
45
|
+
description?: unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const packageJsonSchema = z.looseObject({
|
|
49
|
+
name: z.string(),
|
|
50
|
+
version: z.string().optional(),
|
|
51
|
+
ns: z.looseObject({ harnessArtifacts: z.unknown().optional() }).optional(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const declarationObjectSchema = z.looseObject({
|
|
55
|
+
kind: z.string(),
|
|
56
|
+
name: z.unknown().optional(),
|
|
57
|
+
path: z.unknown().optional(),
|
|
58
|
+
description: z.unknown().optional(),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export function parseModuleArtifactDeclaration(
|
|
62
|
+
packageJsonText: string,
|
|
63
|
+
): ParseModuleArtifactDeclarationResult {
|
|
64
|
+
let data: unknown;
|
|
65
|
+
try {
|
|
66
|
+
data = JSON.parse(packageJsonText);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
diagnostics: [
|
|
71
|
+
{
|
|
72
|
+
code: "module_artifact_package_json_invalid",
|
|
73
|
+
message: `Package manifest is not valid JSON: ${formatErrorMessage(error)}`,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const packageJson = packageJsonSchema.safeParse(data);
|
|
80
|
+
if (!packageJson.success || packageJson.data.name.trim() === "") {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
diagnostics: [
|
|
84
|
+
{
|
|
85
|
+
code: "module_artifact_package_name_invalid",
|
|
86
|
+
message: packageJson.success
|
|
87
|
+
? "Package manifest name must be a non-empty string."
|
|
88
|
+
: `Package manifest name must be a string: ${formatZodIssue(
|
|
89
|
+
packageJson.error.issues[0],
|
|
90
|
+
{
|
|
91
|
+
rootPath: "$",
|
|
92
|
+
pathPrefix: "$.",
|
|
93
|
+
fallback: "invalid package manifest",
|
|
94
|
+
},
|
|
95
|
+
)}`,
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const packageName = packageJson.data.name;
|
|
102
|
+
const version = packageJson.data.version ?? "unversioned";
|
|
103
|
+
const declarations = packageJson.data.ns?.harnessArtifacts;
|
|
104
|
+
if (declarations === undefined) return emptyCatalog(packageName, version);
|
|
105
|
+
if (!Array.isArray(declarations)) {
|
|
106
|
+
return emptyCatalog(packageName, version, [
|
|
107
|
+
{
|
|
108
|
+
code: "module_artifact_declarations_not_array",
|
|
109
|
+
message: "Package manifest ns.harnessArtifacts must be an array.",
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return parseDeclarations({
|
|
115
|
+
packageName,
|
|
116
|
+
version,
|
|
117
|
+
declarations,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function emptyCatalog(
|
|
122
|
+
packageName: string,
|
|
123
|
+
version: string,
|
|
124
|
+
diagnostics: readonly ModuleArtifactDeclarationDiagnostic[] = [],
|
|
125
|
+
): ParseModuleArtifactDeclarationResult {
|
|
126
|
+
return {
|
|
127
|
+
ok: true,
|
|
128
|
+
packageName,
|
|
129
|
+
version,
|
|
130
|
+
artifacts: [],
|
|
131
|
+
diagnostics,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseDeclarations(options: {
|
|
136
|
+
packageName: string;
|
|
137
|
+
version: string;
|
|
138
|
+
declarations: readonly unknown[];
|
|
139
|
+
}): ParseModuleArtifactDeclarationResult {
|
|
140
|
+
const diagnostics: ModuleArtifactDeclarationDiagnostic[] = [];
|
|
141
|
+
const artifacts: SkillHarnessArtifactEntry[] = [];
|
|
142
|
+
const duplicateNames = duplicateDeclarationNames(options.declarations);
|
|
143
|
+
for (const declaration of options.declarations) {
|
|
144
|
+
const parsed = declarationObjectSchema.safeParse(declaration);
|
|
145
|
+
if (!parsed.success) {
|
|
146
|
+
diagnostics.push({
|
|
147
|
+
code: "module_artifact_declaration_invalid",
|
|
148
|
+
message: "Harness artifact declarations must be objects with a string kind.",
|
|
149
|
+
});
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (parsed.data.kind !== "skill") {
|
|
154
|
+
diagnostics.push({
|
|
155
|
+
code: "module_artifact_kind_unsupported",
|
|
156
|
+
message: `Harness artifact kind ${JSON.stringify(parsed.data.kind)} is not supported for provisioning in this slice.`,
|
|
157
|
+
});
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const skillDeclaration: RawSkillDeclaration = {
|
|
162
|
+
kind: "skill",
|
|
163
|
+
name: parsed.data.name,
|
|
164
|
+
path: parsed.data.path,
|
|
165
|
+
...(parsed.data.description === undefined ? {} : { description: parsed.data.description }),
|
|
166
|
+
};
|
|
167
|
+
const artifact = parseSkillDeclaration({
|
|
168
|
+
packageName: options.packageName,
|
|
169
|
+
declaration: skillDeclaration,
|
|
170
|
+
duplicateNames,
|
|
171
|
+
});
|
|
172
|
+
if (artifact.ok) {
|
|
173
|
+
artifacts.push(artifact.artifact);
|
|
174
|
+
} else {
|
|
175
|
+
diagnostics.push(...artifact.diagnostics);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
ok: true,
|
|
181
|
+
packageName: options.packageName,
|
|
182
|
+
version: options.version,
|
|
183
|
+
artifacts: [...artifacts].sort((left, right) => left.id.localeCompare(right.id)),
|
|
184
|
+
diagnostics: sortDeclarationDiagnostics(diagnostics),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function parseSkillDeclaration(options: {
|
|
189
|
+
packageName: string;
|
|
190
|
+
declaration: RawSkillDeclaration;
|
|
191
|
+
duplicateNames: ReadonlySet<string>;
|
|
192
|
+
}):
|
|
193
|
+
| { ok: true; artifact: SkillHarnessArtifactEntry }
|
|
194
|
+
| { ok: false; diagnostics: readonly ModuleArtifactDeclarationDiagnostic[] } {
|
|
195
|
+
const diagnostics: ModuleArtifactDeclarationDiagnostic[] = [];
|
|
196
|
+
const name = readNonEmptyString(options.declaration.name);
|
|
197
|
+
if (name === undefined) {
|
|
198
|
+
diagnostics.push({
|
|
199
|
+
code: "module_artifact_name_invalid",
|
|
200
|
+
message: "Skill harness artifact declaration name must be a non-empty string.",
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
const relativePath = readNonEmptyString(options.declaration.path);
|
|
204
|
+
if (relativePath === undefined || !isValidModuleArtifactRelativePath(relativePath)) {
|
|
205
|
+
diagnostics.push({
|
|
206
|
+
code: "module_artifact_path_invalid",
|
|
207
|
+
message:
|
|
208
|
+
"Skill harness artifact declaration path must be a relative POSIX path inside the package.",
|
|
209
|
+
...(relativePath === undefined ? {} : { path: relativePath }),
|
|
210
|
+
...(name === undefined ? {} : { artifactName: name }),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
if (name !== undefined && options.duplicateNames.has(name)) {
|
|
214
|
+
diagnostics.push({
|
|
215
|
+
code: "module_artifact_duplicate_name",
|
|
216
|
+
message: `Skill harness artifact declaration name ${JSON.stringify(name)} appears more than once in this package.`,
|
|
217
|
+
artifactName: name,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (diagnostics.length > 0 || name === undefined || relativePath === undefined) {
|
|
221
|
+
return { ok: false, diagnostics };
|
|
222
|
+
}
|
|
223
|
+
const description = readNonEmptyString(options.declaration.description);
|
|
224
|
+
return {
|
|
225
|
+
ok: true,
|
|
226
|
+
artifact: {
|
|
227
|
+
kind: "skill",
|
|
228
|
+
id: `${options.packageName}:${name}`,
|
|
229
|
+
name,
|
|
230
|
+
description: description ?? `Skill declared by ${options.packageName}.`,
|
|
231
|
+
skillName: name,
|
|
232
|
+
source: {
|
|
233
|
+
type: "npm-module",
|
|
234
|
+
packageName: options.packageName,
|
|
235
|
+
relativePath,
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function isValidModuleArtifactRelativePath(relativePath: string): boolean {
|
|
242
|
+
if (relativePath === "" || relativePath.startsWith("/") || relativePath.includes("\\")) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
const segments = relativePath.split("/");
|
|
246
|
+
return segments.every((segment) => segment !== "" && segment !== "." && segment !== "..");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function duplicateDeclarationNames(declarations: readonly unknown[]): ReadonlySet<string> {
|
|
250
|
+
const names: string[] = [];
|
|
251
|
+
for (const declaration of declarations) {
|
|
252
|
+
const parsed = declarationObjectSchema.safeParse(declaration);
|
|
253
|
+
if (!parsed.success || parsed.data.kind !== "skill") continue;
|
|
254
|
+
const name = readNonEmptyString(parsed.data.name);
|
|
255
|
+
if (name !== undefined) names.push(name);
|
|
256
|
+
}
|
|
257
|
+
return new Set(names.filter((name, index) => names.indexOf(name) !== index));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function readNonEmptyString(value: unknown): string | undefined {
|
|
261
|
+
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function sortDeclarationDiagnostics(
|
|
265
|
+
diagnostics: readonly ModuleArtifactDeclarationDiagnostic[],
|
|
266
|
+
): readonly ModuleArtifactDeclarationDiagnostic[] {
|
|
267
|
+
return sortDiagnosticsByKey(diagnostics, (diagnostic) => [
|
|
268
|
+
diagnostic.artifactName,
|
|
269
|
+
diagnostic.path,
|
|
270
|
+
diagnostic.code,
|
|
271
|
+
diagnostic.message,
|
|
272
|
+
]);
|
|
273
|
+
}
|