@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
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
import { formatErrorMessage, formatZodIssue, optionalEntry } from "@nseng-ai/foundation/primitives";
|
|
4
|
+
import { resultErr, resultOk, type Result } from "@nseng-ai/foundation/result";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
import type { HarnessArtifactEntry } from "./artifact-catalog.ts";
|
|
8
|
+
import {
|
|
9
|
+
ALL_HARNESS_IDS,
|
|
10
|
+
HARNESS_SCOPES,
|
|
11
|
+
type HarnessPathContext,
|
|
12
|
+
type HarnessScope,
|
|
13
|
+
} from "./harness-paths.ts";
|
|
14
|
+
import {
|
|
15
|
+
fileSystemError,
|
|
16
|
+
nodeHarnessArtifactFileSystemGateway,
|
|
17
|
+
type HarnessArtifactFileSystemErrorInfo,
|
|
18
|
+
type HarnessArtifactFileSystemGateway,
|
|
19
|
+
type OptionalFileState,
|
|
20
|
+
type OptionalTextFileState,
|
|
21
|
+
} from "./filesystem.ts";
|
|
22
|
+
import {
|
|
23
|
+
buildInstallManifestData,
|
|
24
|
+
buildInstallManifestEntry,
|
|
25
|
+
buildProvisionPlan,
|
|
26
|
+
classifyProvisionDecisions,
|
|
27
|
+
contentHashForBytes,
|
|
28
|
+
installManifestKey,
|
|
29
|
+
type InstallManifestData,
|
|
30
|
+
type InstallManifestEntryData,
|
|
31
|
+
type InstallManifestFileData,
|
|
32
|
+
type InstallManifestSourceData,
|
|
33
|
+
type ProvisionDecisionErrorInfo,
|
|
34
|
+
type ProvisionDecisionSet,
|
|
35
|
+
type ProvisionPlan,
|
|
36
|
+
type ProvisionPlanErrorInfo,
|
|
37
|
+
type ProvisionSourceFile,
|
|
38
|
+
type TargetFileHashFact,
|
|
39
|
+
} from "./provision-plan.ts";
|
|
40
|
+
|
|
41
|
+
export const INSTALL_MANIFEST_FILE_NAME = ".ns-harness-artifacts-manifest.json";
|
|
42
|
+
|
|
43
|
+
export interface HarnessArtifactProvisionRequest {
|
|
44
|
+
artifact: HarnessArtifactEntry;
|
|
45
|
+
harness: string;
|
|
46
|
+
scope: HarnessScope;
|
|
47
|
+
context: HarnessPathContext;
|
|
48
|
+
sourceRoot: string;
|
|
49
|
+
sourceVersion: string;
|
|
50
|
+
fs?: HarnessArtifactFileSystemGateway;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface HarnessArtifactProvisionPreview {
|
|
54
|
+
plan: ProvisionPlan;
|
|
55
|
+
decisions: ProvisionDecisionSet;
|
|
56
|
+
manifestPath: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface PreparedHarnessArtifactProvision extends HarnessArtifactProvisionPreview {
|
|
60
|
+
manifest: InstallManifestData;
|
|
61
|
+
sourceRoot: string;
|
|
62
|
+
fs: HarnessArtifactFileSystemGateway;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ApplyPreparedProvisionOptions {
|
|
66
|
+
shouldForce: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface HarnessArtifactProvisionApplyResult extends HarnessArtifactProvisionPreview {
|
|
70
|
+
manifest: InstallManifestData;
|
|
71
|
+
writtenFiles: readonly string[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface HarnessArtifactProvisionAppliedOutcome extends HarnessArtifactProvisionApplyResult {
|
|
75
|
+
outcome: "applied";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface HarnessArtifactProvisionConflictOutcome extends HarnessArtifactProvisionPreview {
|
|
79
|
+
outcome: "conflicted";
|
|
80
|
+
conflictingFiles: readonly string[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type HarnessArtifactProvisionApplyOutcome =
|
|
84
|
+
| HarnessArtifactProvisionAppliedOutcome
|
|
85
|
+
| HarnessArtifactProvisionConflictOutcome;
|
|
86
|
+
|
|
87
|
+
export type {
|
|
88
|
+
HarnessArtifactFileSystemErrorInfo,
|
|
89
|
+
HarnessArtifactFileSystemGateway,
|
|
90
|
+
OptionalFileState,
|
|
91
|
+
OptionalTextFileState,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export type HarnessArtifactProvisionErrorInfo =
|
|
95
|
+
| ProvisionPlanErrorInfo
|
|
96
|
+
| ProvisionDecisionErrorInfo
|
|
97
|
+
| HarnessArtifactFileSystemErrorInfo
|
|
98
|
+
| {
|
|
99
|
+
code: "invalid_install_manifest";
|
|
100
|
+
message: string;
|
|
101
|
+
details: { manifestPath: string };
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const installManifestSourceSchema: z.ZodType<InstallManifestSourceData> = z.object({
|
|
105
|
+
type: z.enum(["first-party", "npm-module"]),
|
|
106
|
+
packageName: z.string(),
|
|
107
|
+
relativePath: z.string(),
|
|
108
|
+
version: z.string(),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const installManifestFileSchema: z.ZodType<InstallManifestFileData> = z.object({
|
|
112
|
+
sourcePath: z.string(),
|
|
113
|
+
targetPath: z.string(),
|
|
114
|
+
contentHash: z.string(),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const installManifestEntrySchema: z.ZodType<InstallManifestEntryData> = z.object({
|
|
118
|
+
artifactId: z.string(),
|
|
119
|
+
kind: z.literal("skill"),
|
|
120
|
+
provisionName: z.string(),
|
|
121
|
+
harness: z.enum(ALL_HARNESS_IDS),
|
|
122
|
+
scope: z.enum(HARNESS_SCOPES),
|
|
123
|
+
targetRoot: z.string(),
|
|
124
|
+
targetArtifactPath: z.string(),
|
|
125
|
+
source: installManifestSourceSchema,
|
|
126
|
+
files: z.record(z.string(), installManifestFileSchema),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const installManifestSchema: z.ZodType<InstallManifestData> = z.object({
|
|
130
|
+
version: z.literal(1),
|
|
131
|
+
artifacts: z.record(z.string(), installManifestEntrySchema),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
export { nodeHarnessArtifactFileSystemGateway };
|
|
135
|
+
|
|
136
|
+
export async function previewHarnessArtifactProvision(
|
|
137
|
+
request: HarnessArtifactProvisionRequest,
|
|
138
|
+
): Promise<Result<HarnessArtifactProvisionPreview, HarnessArtifactProvisionErrorInfo>> {
|
|
139
|
+
const prepared = await prepareProvision(request);
|
|
140
|
+
if (!prepared.ok) return prepared;
|
|
141
|
+
return resultOk(previewFromPrepared(prepared.value));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function applyHarnessArtifactProvision(
|
|
145
|
+
request: HarnessArtifactProvisionRequest,
|
|
146
|
+
): Promise<Result<HarnessArtifactProvisionApplyOutcome, HarnessArtifactProvisionErrorInfo>> {
|
|
147
|
+
const prepared = await prepareProvision(request);
|
|
148
|
+
if (!prepared.ok) return prepared;
|
|
149
|
+
return applyPreparedProvision(prepared.value, { shouldForce: false });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function prepareProvision(
|
|
153
|
+
request: HarnessArtifactProvisionRequest,
|
|
154
|
+
): Promise<Result<PreparedHarnessArtifactProvision, HarnessArtifactProvisionErrorInfo>> {
|
|
155
|
+
const fs = request.fs ?? nodeHarnessArtifactFileSystemGateway;
|
|
156
|
+
const sourceFiles = await collectSourceFiles({
|
|
157
|
+
fs,
|
|
158
|
+
sourceRoot: request.sourceRoot,
|
|
159
|
+
sourceRelativePath: request.artifact.source.relativePath,
|
|
160
|
+
});
|
|
161
|
+
if (!sourceFiles.ok) return sourceFiles;
|
|
162
|
+
|
|
163
|
+
const plan = buildProvisionPlan({
|
|
164
|
+
artifact: request.artifact,
|
|
165
|
+
harness: request.harness,
|
|
166
|
+
scope: request.scope,
|
|
167
|
+
context: request.context,
|
|
168
|
+
sourceVersion: request.sourceVersion,
|
|
169
|
+
sourceFiles: sourceFiles.value,
|
|
170
|
+
});
|
|
171
|
+
if (!plan.ok) return plan;
|
|
172
|
+
|
|
173
|
+
const manifestPath = installManifestPathForPlan(plan.value);
|
|
174
|
+
const manifest = await readInstallManifest(fs, manifestPath);
|
|
175
|
+
if (!manifest.ok) return manifest;
|
|
176
|
+
const targetFacts = await collectTargetHashFacts({ fs, plan: plan.value });
|
|
177
|
+
if (!targetFacts.ok) return targetFacts;
|
|
178
|
+
const existingManifestEntry = manifest.value.artifacts[installManifestKey(plan.value)];
|
|
179
|
+
const decisions = classifyProvisionDecisions({
|
|
180
|
+
plan: plan.value,
|
|
181
|
+
...optionalEntry("existingManifestEntry", existingManifestEntry),
|
|
182
|
+
targetFacts: targetFacts.value,
|
|
183
|
+
});
|
|
184
|
+
if (!decisions.ok) return decisions;
|
|
185
|
+
|
|
186
|
+
return resultOk({
|
|
187
|
+
plan: plan.value,
|
|
188
|
+
decisions: decisions.value,
|
|
189
|
+
manifestPath,
|
|
190
|
+
manifest: manifest.value,
|
|
191
|
+
sourceRoot: request.sourceRoot,
|
|
192
|
+
fs,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function applyPreparedProvision(
|
|
197
|
+
prepared: PreparedHarnessArtifactProvision,
|
|
198
|
+
options: ApplyPreparedProvisionOptions,
|
|
199
|
+
): Promise<Result<HarnessArtifactProvisionApplyOutcome, HarnessArtifactProvisionErrorInfo>> {
|
|
200
|
+
const conflicts = prepared.decisions.files.filter(
|
|
201
|
+
(decision) => decision.type === "locally-edited-conflict",
|
|
202
|
+
);
|
|
203
|
+
if (conflicts.length > 0 && !options.shouldForce) {
|
|
204
|
+
return resultOk({
|
|
205
|
+
outcome: "conflicted",
|
|
206
|
+
...previewFromPrepared(prepared),
|
|
207
|
+
conflictingFiles: conflicts.map((decision) => decision.file.targetPath),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const writtenFiles: string[] = [];
|
|
212
|
+
for (const decision of prepared.decisions.files) {
|
|
213
|
+
if (decision.type === "unchanged") continue;
|
|
214
|
+
const sourcePath = join(prepared.sourceRoot, decision.file.sourcePath);
|
|
215
|
+
const source = await readRequiredFile(prepared.fs, sourcePath);
|
|
216
|
+
if (!source.ok) return source;
|
|
217
|
+
const write = await prepared.fs.writeFile(decision.file.targetPath, source.value);
|
|
218
|
+
if (!write.ok) return write;
|
|
219
|
+
writtenFiles.push(decision.file.targetPath);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const manifest = updateManifest(prepared.manifest, prepared.plan);
|
|
223
|
+
const writeManifest = await prepared.fs.writeTextFile(
|
|
224
|
+
prepared.manifestPath,
|
|
225
|
+
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
226
|
+
);
|
|
227
|
+
if (!writeManifest.ok) return writeManifest;
|
|
228
|
+
|
|
229
|
+
return resultOk({
|
|
230
|
+
outcome: "applied",
|
|
231
|
+
plan: prepared.plan,
|
|
232
|
+
decisions: prepared.decisions,
|
|
233
|
+
manifestPath: prepared.manifestPath,
|
|
234
|
+
manifest,
|
|
235
|
+
writtenFiles,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function installManifestPathForPlan(plan: ProvisionPlan): string {
|
|
240
|
+
return join(plan.targetRoot, INSTALL_MANIFEST_FILE_NAME);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function readInstallManifestAtRoot(options: {
|
|
244
|
+
targetRoot: string;
|
|
245
|
+
fs?: HarnessArtifactFileSystemGateway;
|
|
246
|
+
}): Promise<Result<InstallManifestData, HarnessArtifactProvisionErrorInfo>> {
|
|
247
|
+
const fs = options.fs ?? nodeHarnessArtifactFileSystemGateway;
|
|
248
|
+
return readInstallManifest(fs, join(options.targetRoot, INSTALL_MANIFEST_FILE_NAME));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function previewFromPrepared(
|
|
252
|
+
prepared: PreparedHarnessArtifactProvision,
|
|
253
|
+
): HarnessArtifactProvisionPreview {
|
|
254
|
+
return {
|
|
255
|
+
plan: prepared.plan,
|
|
256
|
+
decisions: prepared.decisions,
|
|
257
|
+
manifestPath: prepared.manifestPath,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function collectSourceFiles(input: {
|
|
262
|
+
fs: HarnessArtifactFileSystemGateway;
|
|
263
|
+
sourceRoot: string;
|
|
264
|
+
sourceRelativePath: string;
|
|
265
|
+
}): Promise<Result<readonly ProvisionSourceFile[], HarnessArtifactProvisionErrorInfo>> {
|
|
266
|
+
const sourceDirectory = join(input.sourceRoot, input.sourceRelativePath);
|
|
267
|
+
const relativePaths = await input.fs.listFiles(sourceDirectory);
|
|
268
|
+
if (!relativePaths.ok) return relativePaths;
|
|
269
|
+
const sourceFiles: ProvisionSourceFile[] = [];
|
|
270
|
+
for (const relativePath of relativePaths.value) {
|
|
271
|
+
const sourcePath = join(sourceDirectory, relativePath);
|
|
272
|
+
const source = await readRequiredFile(input.fs, sourcePath);
|
|
273
|
+
if (!source.ok) return source;
|
|
274
|
+
sourceFiles.push({ relativePath, contentHash: contentHashForBytes(source.value) });
|
|
275
|
+
}
|
|
276
|
+
return resultOk(sourceFiles);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function readRequiredFile(
|
|
280
|
+
fs: HarnessArtifactFileSystemGateway,
|
|
281
|
+
path: string,
|
|
282
|
+
): Promise<Result<Uint8Array, HarnessArtifactProvisionErrorInfo>> {
|
|
283
|
+
const source = await fs.readOptionalFile(path);
|
|
284
|
+
if (!source.ok) return source;
|
|
285
|
+
if (source.value.type === "missing") {
|
|
286
|
+
return resultErr(fileSystemError(path, "read", new Error("Source file is missing.")));
|
|
287
|
+
}
|
|
288
|
+
return resultOk(source.value.bytes);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function collectTargetHashFacts(input: {
|
|
292
|
+
fs: HarnessArtifactFileSystemGateway;
|
|
293
|
+
plan: ProvisionPlan;
|
|
294
|
+
}): Promise<Result<readonly TargetFileHashFact[], HarnessArtifactProvisionErrorInfo>> {
|
|
295
|
+
const facts: TargetFileHashFact[] = [];
|
|
296
|
+
for (const file of input.plan.files) {
|
|
297
|
+
const target = await input.fs.readOptionalFile(file.targetPath);
|
|
298
|
+
if (!target.ok) return target;
|
|
299
|
+
if (target.value.type === "missing") {
|
|
300
|
+
facts.push({ type: "missing", targetPath: file.targetPath });
|
|
301
|
+
} else {
|
|
302
|
+
facts.push({
|
|
303
|
+
type: "file",
|
|
304
|
+
targetPath: file.targetPath,
|
|
305
|
+
contentHash: contentHashForBytes(target.value.bytes),
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return resultOk(facts);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function readInstallManifest(
|
|
313
|
+
fs: HarnessArtifactFileSystemGateway,
|
|
314
|
+
manifestPath: string,
|
|
315
|
+
): Promise<Result<InstallManifestData, HarnessArtifactProvisionErrorInfo>> {
|
|
316
|
+
const manifest = await fs.readOptionalTextFile(manifestPath);
|
|
317
|
+
if (!manifest.ok) return manifest;
|
|
318
|
+
if (manifest.value.type === "missing") return resultOk({ version: 1, artifacts: {} });
|
|
319
|
+
let data: unknown;
|
|
320
|
+
try {
|
|
321
|
+
data = JSON.parse(manifest.value.text);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
return resultErr({
|
|
324
|
+
code: "invalid_install_manifest",
|
|
325
|
+
message: `Install manifest at ${manifestPath} is not valid JSON: ${formatErrorMessage(error)}`,
|
|
326
|
+
details: { manifestPath },
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
const parsed = installManifestSchema.safeParse(data);
|
|
330
|
+
if (!parsed.success) {
|
|
331
|
+
return resultErr({
|
|
332
|
+
code: "invalid_install_manifest",
|
|
333
|
+
message: `Install manifest at ${manifestPath} is invalid: ${formatZodIssue(
|
|
334
|
+
parsed.error.issues[0],
|
|
335
|
+
{ rootPath: "$", pathPrefix: "$.", fallback: "invalid install manifest" },
|
|
336
|
+
)}`,
|
|
337
|
+
details: { manifestPath },
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return resultOk(parsed.data);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function updateManifest(manifest: InstallManifestData, plan: ProvisionPlan): InstallManifestData {
|
|
344
|
+
const nextEntry = buildInstallManifestEntry(plan);
|
|
345
|
+
const nextKey = installManifestKey(plan);
|
|
346
|
+
const entries = Object.entries(manifest.artifacts)
|
|
347
|
+
.filter(([key]) => key !== nextKey)
|
|
348
|
+
.map(([, entry]) => entry);
|
|
349
|
+
return buildInstallManifestData([...entries, nextEntry]);
|
|
350
|
+
}
|