@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/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nseng-ai/harness-artifacts",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"src"
|
|
7
|
+
],
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=24.12.0",
|
|
10
|
+
"pnpm": ">=11.8.0"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./src/index.ts",
|
|
17
|
+
"./api": "./src/api.ts",
|
|
18
|
+
"./ns/commands/install": "./src/ns/commands/install.ts",
|
|
19
|
+
"./ns/commands/list": "./src/ns/commands/list.ts",
|
|
20
|
+
"./ns/commands/path": "./src/ns/commands/path.ts",
|
|
21
|
+
"./ns/commands/update": "./src/ns/commands/update.ts",
|
|
22
|
+
"./ns/preinstalled-catalog": "./src/ns/preinstalled-catalog.ts"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@nseng-ai/capability-kit": "0.1.2",
|
|
26
|
+
"@nseng-ai/clinkr": "0.1.2",
|
|
27
|
+
"@nseng-ai/foundation": "0.1.2",
|
|
28
|
+
"@nseng-ai/kernel": "0.1.2",
|
|
29
|
+
"smol-toml": "^1.6.1",
|
|
30
|
+
"zod": "^4.4.3"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability API for `@nseng-ai/harness-artifacts` — the shared home for
|
|
3
|
+
* harness-artifact conventions pushed down from `@nseng-ai/areg` (see the
|
|
4
|
+
* `skill-management-subsystem` Objective). Cross-package consumers import
|
|
5
|
+
* from this door only.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
artifactProvisionName,
|
|
10
|
+
HARNESS_ARTIFACT_KINDS,
|
|
11
|
+
type AgentHarnessArtifactEntry,
|
|
12
|
+
type ExtensionBundleHarnessArtifactEntry,
|
|
13
|
+
type FirstPartyHarnessArtifactCatalog,
|
|
14
|
+
type FirstPartyHarnessArtifactSource,
|
|
15
|
+
type HarnessArtifactEntry,
|
|
16
|
+
type HarnessArtifactEntryBase,
|
|
17
|
+
type HarnessArtifactKind,
|
|
18
|
+
type HarnessArtifactSource,
|
|
19
|
+
type NpmModuleHarnessArtifactSource,
|
|
20
|
+
type SkillHarnessArtifactEntry,
|
|
21
|
+
} from "./artifact-catalog.ts";
|
|
22
|
+
export {
|
|
23
|
+
findFirstPartySkillArtifact,
|
|
24
|
+
listFirstPartySkillArtifacts,
|
|
25
|
+
NS_FIRST_PARTY_HARNESS_ARTIFACT_CATALOG,
|
|
26
|
+
type FirstPartySkillHarnessArtifact,
|
|
27
|
+
} from "./first-party-catalog.ts";
|
|
28
|
+
export {
|
|
29
|
+
nodeHarnessArtifactFileSystemGateway,
|
|
30
|
+
type HarnessArtifactFileSystemErrorInfo,
|
|
31
|
+
type HarnessArtifactFileSystemGateway,
|
|
32
|
+
type HarnessArtifactModuleDiscoveryGateway,
|
|
33
|
+
type ModuleDiscoveryDirectoryEntry,
|
|
34
|
+
type ModuleDiscoveryDirectoryState,
|
|
35
|
+
type ModuleDiscoveryPathState,
|
|
36
|
+
type OptionalFileState,
|
|
37
|
+
type OptionalTextFileState,
|
|
38
|
+
} from "./filesystem.ts";
|
|
39
|
+
export type { PathState, TextFileState } from "./fs-state.ts";
|
|
40
|
+
export {
|
|
41
|
+
FIRST_PARTY_SKILL_CATALOG_SOURCE_UNAVAILABLE_MESSAGE,
|
|
42
|
+
FIRST_PARTY_SKILL_CATALOG_SOURCE_VERSION,
|
|
43
|
+
firstPartySkillProvisionPathContext,
|
|
44
|
+
provisionFirstPartySkill,
|
|
45
|
+
resolveFirstPartyCatalogSourceRoot,
|
|
46
|
+
type ProvisionFirstPartySkillOutcome,
|
|
47
|
+
type ProvisionFirstPartySkillRequest,
|
|
48
|
+
} from "./first-party-skill-provisioning.ts";
|
|
49
|
+
export {
|
|
50
|
+
ALL_HARNESS_IDS,
|
|
51
|
+
HARNESS_SCOPES,
|
|
52
|
+
HARNESS_SPECS,
|
|
53
|
+
normalizeHarnessId,
|
|
54
|
+
resolveHarnessArtifactPath,
|
|
55
|
+
resolveHarnessSkillRoot,
|
|
56
|
+
resolveHarnessSpec,
|
|
57
|
+
type HarnessBasePathSpec,
|
|
58
|
+
type HarnessId,
|
|
59
|
+
type HarnessPathContext,
|
|
60
|
+
type HarnessPathEnvironment,
|
|
61
|
+
type HarnessPathErrorInfo,
|
|
62
|
+
type HarnessScope,
|
|
63
|
+
type HarnessScopedPathSpec,
|
|
64
|
+
type HarnessSpec,
|
|
65
|
+
type ResolvedHarnessArtifactPath,
|
|
66
|
+
type ResolvedHarnessSkillRoot,
|
|
67
|
+
} from "./harness-paths.ts";
|
|
68
|
+
export {
|
|
69
|
+
normalizeHarnessSelection,
|
|
70
|
+
parseNsTomlHarnesses,
|
|
71
|
+
planNsTomlHarnessesWrite,
|
|
72
|
+
renderNsTomlHarnesses,
|
|
73
|
+
type NsTomlChange,
|
|
74
|
+
type NsTomlErrorCode,
|
|
75
|
+
type NsTomlErrorInfo,
|
|
76
|
+
type NsTomlHarnessesParseResult,
|
|
77
|
+
type NsTomlWritePlanResult,
|
|
78
|
+
} from "./ns-toml.ts";
|
|
79
|
+
export {
|
|
80
|
+
planHarnessArtifactReconcile,
|
|
81
|
+
reconcileReportSchema,
|
|
82
|
+
runHarnessArtifactReconcile,
|
|
83
|
+
type DesiredHarnessArtifact,
|
|
84
|
+
type HarnessManifestSnapshot,
|
|
85
|
+
type HarnessSelectionState,
|
|
86
|
+
type OrphanedManifestEntry,
|
|
87
|
+
type ReconcileArtifactOutcome,
|
|
88
|
+
type ReconcileErrorInfo,
|
|
89
|
+
type ReconcilePair,
|
|
90
|
+
type ReconcileReport,
|
|
91
|
+
type SkippedArtifactCollision,
|
|
92
|
+
type RunHarnessArtifactReconcileRequest,
|
|
93
|
+
} from "./reconcile.ts";
|
|
94
|
+
export {
|
|
95
|
+
discoverExtensionModuleHarnessArtifacts,
|
|
96
|
+
moduleArtifactDiscoveryDiagnosticSchema,
|
|
97
|
+
type DiscoverExtensionModuleHarnessArtifactsRequest,
|
|
98
|
+
type DiscoverExtensionModuleHarnessArtifactsResult,
|
|
99
|
+
MODULE_ARTIFACT_DISCOVERY_DIAGNOSTIC_CODES,
|
|
100
|
+
type ModuleArtifactDiscoveryDiagnostic,
|
|
101
|
+
type ModuleArtifactDiscoveryDiagnosticCode,
|
|
102
|
+
type ResolvedNpmModuleHarnessArtifactCatalog,
|
|
103
|
+
} from "./module-artifact-discovery.ts";
|
|
104
|
+
export {
|
|
105
|
+
isValidModuleArtifactRelativePath,
|
|
106
|
+
MODULE_ARTIFACT_DECLARATION_DIAGNOSTIC_CODES,
|
|
107
|
+
parseModuleArtifactDeclaration,
|
|
108
|
+
type ModuleArtifactDeclarationDiagnostic,
|
|
109
|
+
type ModuleArtifactDeclarationDiagnosticCode,
|
|
110
|
+
type ParseModuleArtifactDeclarationResult,
|
|
111
|
+
} from "./module-artifact-declaration.ts";
|
|
112
|
+
export {
|
|
113
|
+
applyHarnessArtifactProvision,
|
|
114
|
+
applyPreparedProvision,
|
|
115
|
+
INSTALL_MANIFEST_FILE_NAME,
|
|
116
|
+
installManifestPathForPlan,
|
|
117
|
+
prepareProvision,
|
|
118
|
+
previewHarnessArtifactProvision,
|
|
119
|
+
readInstallManifestAtRoot,
|
|
120
|
+
type ApplyPreparedProvisionOptions,
|
|
121
|
+
type HarnessArtifactProvisionAppliedOutcome,
|
|
122
|
+
type HarnessArtifactProvisionApplyOutcome,
|
|
123
|
+
type HarnessArtifactProvisionApplyResult,
|
|
124
|
+
type HarnessArtifactProvisionConflictOutcome,
|
|
125
|
+
type HarnessArtifactProvisionErrorInfo,
|
|
126
|
+
type HarnessArtifactProvisionPreview,
|
|
127
|
+
type HarnessArtifactProvisionRequest,
|
|
128
|
+
type PreparedHarnessArtifactProvision,
|
|
129
|
+
} from "./provision-apply.ts";
|
|
130
|
+
export {
|
|
131
|
+
buildInstallManifestData,
|
|
132
|
+
buildInstallManifestEntry,
|
|
133
|
+
buildProvisionPlan,
|
|
134
|
+
classifyProvisionDecisions,
|
|
135
|
+
contentHashForBytes,
|
|
136
|
+
contentHashForText,
|
|
137
|
+
installManifestKey,
|
|
138
|
+
provisionFileDecisionSchema,
|
|
139
|
+
provisionPlanFileSchema,
|
|
140
|
+
type BuildProvisionPlanInput,
|
|
141
|
+
type InstallManifestData,
|
|
142
|
+
type InstallManifestEntryData,
|
|
143
|
+
type InstallManifestFileData,
|
|
144
|
+
type InstallManifestSourceData,
|
|
145
|
+
type ProvisionDecisionErrorInfo,
|
|
146
|
+
type ProvisionDecisionSet,
|
|
147
|
+
type ProvisionFileDecision,
|
|
148
|
+
type ProvisionFileDecisionType,
|
|
149
|
+
type ProvisionPlan,
|
|
150
|
+
type ProvisionPlanErrorInfo,
|
|
151
|
+
type ProvisionPlanFile,
|
|
152
|
+
type ProvisionSourceFile,
|
|
153
|
+
type ProvisionSourceProvenance,
|
|
154
|
+
type ProvisionableHarnessArtifactEntry,
|
|
155
|
+
type TargetFileHashFact,
|
|
156
|
+
} from "./provision-plan.ts";
|
|
157
|
+
export {
|
|
158
|
+
parseSkillFrontmatterBlock,
|
|
159
|
+
parseSkillFrontmatterTopLevelLine,
|
|
160
|
+
isSkillFrontmatterTopLevelKey,
|
|
161
|
+
transformSkillFrontmatter,
|
|
162
|
+
type SkillFrontmatterData,
|
|
163
|
+
type SkillFrontmatterParseResult,
|
|
164
|
+
type SkillFrontmatterTopLevelLineParseResult,
|
|
165
|
+
} from "./skill-frontmatter.ts";
|
|
166
|
+
export {
|
|
167
|
+
agentsSkillMirrorRelativePath,
|
|
168
|
+
claudeSkillMirrorRelativePath,
|
|
169
|
+
classifySkillMirrorSymlinkState,
|
|
170
|
+
expectedAgentsSkillSymlinkTarget,
|
|
171
|
+
expectedClaudeSkillSymlinkTarget,
|
|
172
|
+
expectedMirrorTarget,
|
|
173
|
+
isAgentsSkillMirror,
|
|
174
|
+
isClaudeSkillMirror,
|
|
175
|
+
isSkillMirrorRelativePath,
|
|
176
|
+
parseSkillMirrorRelativePath,
|
|
177
|
+
type SkillMirrorKind,
|
|
178
|
+
type SkillMirrorRelativePathInfo,
|
|
179
|
+
} from "./skill-mirror-conventions.ts";
|
|
180
|
+
export {
|
|
181
|
+
parseInspectedLockfile,
|
|
182
|
+
parseLockfileData,
|
|
183
|
+
parseLockfileText,
|
|
184
|
+
SOURCE_TYPES,
|
|
185
|
+
type LockfileSkill,
|
|
186
|
+
type LockfileSkillData,
|
|
187
|
+
type SkillsLockfile,
|
|
188
|
+
type SkillsLockfileData,
|
|
189
|
+
type SourceType,
|
|
190
|
+
} from "./skills-lockfile.ts";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const HARNESS_ARTIFACT_KINDS = ["skill", "agent", "extension-bundle"] as const;
|
|
2
|
+
|
|
3
|
+
export type HarnessArtifactKind = (typeof HARNESS_ARTIFACT_KINDS)[number];
|
|
4
|
+
|
|
5
|
+
export interface HarnessArtifactEntryBase {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SkillHarnessArtifactEntry extends HarnessArtifactEntryBase {
|
|
12
|
+
kind: "skill";
|
|
13
|
+
skillName: string;
|
|
14
|
+
source: HarnessArtifactSource;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AgentHarnessArtifactEntry extends HarnessArtifactEntryBase {
|
|
18
|
+
kind: "agent";
|
|
19
|
+
agentName: string;
|
|
20
|
+
source: FirstPartyHarnessArtifactSource;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ExtensionBundleHarnessArtifactEntry extends HarnessArtifactEntryBase {
|
|
24
|
+
kind: "extension-bundle";
|
|
25
|
+
bundleName: string;
|
|
26
|
+
source: FirstPartyHarnessArtifactSource;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type HarnessArtifactEntry =
|
|
30
|
+
| SkillHarnessArtifactEntry
|
|
31
|
+
| AgentHarnessArtifactEntry
|
|
32
|
+
| ExtensionBundleHarnessArtifactEntry;
|
|
33
|
+
|
|
34
|
+
export interface FirstPartyHarnessArtifactSource {
|
|
35
|
+
type: "first-party";
|
|
36
|
+
packageName: string;
|
|
37
|
+
relativePath: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface NpmModuleHarnessArtifactSource {
|
|
41
|
+
type: "npm-module";
|
|
42
|
+
packageName: string;
|
|
43
|
+
relativePath: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type HarnessArtifactSource =
|
|
47
|
+
| FirstPartyHarnessArtifactSource
|
|
48
|
+
| NpmModuleHarnessArtifactSource;
|
|
49
|
+
|
|
50
|
+
export interface FirstPartyHarnessArtifactCatalog {
|
|
51
|
+
type: "first-party-catalog";
|
|
52
|
+
catalogId: string;
|
|
53
|
+
artifacts: readonly HarnessArtifactEntry[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function artifactProvisionName(artifact: HarnessArtifactEntry): string {
|
|
57
|
+
switch (artifact.kind) {
|
|
58
|
+
case "skill":
|
|
59
|
+
return artifact.skillName;
|
|
60
|
+
case "agent":
|
|
61
|
+
return artifact.agentName;
|
|
62
|
+
case "extension-bundle":
|
|
63
|
+
return artifact.bundleName;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function sortDiagnosticsByKey<TDiagnostic>(
|
|
2
|
+
diagnostics: readonly TDiagnostic[],
|
|
3
|
+
keyFields: (diagnostic: TDiagnostic) => readonly (string | undefined)[],
|
|
4
|
+
): readonly TDiagnostic[] {
|
|
5
|
+
return [...diagnostics].sort((left, right) =>
|
|
6
|
+
joinedDiagnosticSortKey(keyFields(left)).localeCompare(
|
|
7
|
+
joinedDiagnosticSortKey(keyFields(right)),
|
|
8
|
+
),
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function joinedDiagnosticSortKey(fields: readonly (string | undefined)[]): string {
|
|
13
|
+
return fields.map((field) => field ?? "").join("\0");
|
|
14
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { errorCodeFromUnknown, formatErrorMessage } from "@nseng-ai/foundation/primitives";
|
|
5
|
+
import { resultErr, resultOk, type Result } from "@nseng-ai/foundation/result";
|
|
6
|
+
|
|
7
|
+
import { sortStrings } from "./sort.ts";
|
|
8
|
+
|
|
9
|
+
export interface HarnessArtifactFileSystemErrorInfo {
|
|
10
|
+
code: "filesystem_error";
|
|
11
|
+
message: string;
|
|
12
|
+
details: { path: string; operation: "stat" | "list" | "read" | "write" };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type OptionalFileState = { type: "missing" } | { type: "file"; bytes: Uint8Array };
|
|
16
|
+
export type OptionalTextFileState = { type: "missing" } | { type: "file"; text: string };
|
|
17
|
+
|
|
18
|
+
export type ModuleDiscoveryDirectoryState =
|
|
19
|
+
| { type: "missing" }
|
|
20
|
+
| { type: "file" }
|
|
21
|
+
| { type: "directory"; entries: readonly ModuleDiscoveryDirectoryEntry[] };
|
|
22
|
+
|
|
23
|
+
export interface ModuleDiscoveryDirectoryEntry {
|
|
24
|
+
name: string;
|
|
25
|
+
type: "directory" | "file" | "other";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ModuleDiscoveryPathState =
|
|
29
|
+
| { type: "missing" }
|
|
30
|
+
| { type: "file" }
|
|
31
|
+
| { type: "directory" }
|
|
32
|
+
| { type: "other" };
|
|
33
|
+
|
|
34
|
+
export interface HarnessArtifactFileSystemGateway {
|
|
35
|
+
listFiles(
|
|
36
|
+
rootPath: string,
|
|
37
|
+
): Promise<Result<readonly string[], HarnessArtifactFileSystemErrorInfo>>;
|
|
38
|
+
readOptionalFile(
|
|
39
|
+
path: string,
|
|
40
|
+
): Promise<Result<OptionalFileState, HarnessArtifactFileSystemErrorInfo>>;
|
|
41
|
+
writeFile(
|
|
42
|
+
path: string,
|
|
43
|
+
bytes: Uint8Array,
|
|
44
|
+
): Promise<Result<void, HarnessArtifactFileSystemErrorInfo>>;
|
|
45
|
+
readOptionalTextFile(
|
|
46
|
+
path: string,
|
|
47
|
+
): Promise<Result<OptionalTextFileState, HarnessArtifactFileSystemErrorInfo>>;
|
|
48
|
+
writeTextFile(
|
|
49
|
+
path: string,
|
|
50
|
+
text: string,
|
|
51
|
+
): Promise<Result<void, HarnessArtifactFileSystemErrorInfo>>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface HarnessArtifactModuleDiscoveryGateway {
|
|
55
|
+
readDirectory(
|
|
56
|
+
path: string,
|
|
57
|
+
): Promise<Result<ModuleDiscoveryDirectoryState, HarnessArtifactFileSystemErrorInfo>>;
|
|
58
|
+
readOptionalTextFile(
|
|
59
|
+
path: string,
|
|
60
|
+
): Promise<Result<OptionalTextFileState, HarnessArtifactFileSystemErrorInfo>>;
|
|
61
|
+
pathState(
|
|
62
|
+
path: string,
|
|
63
|
+
): Promise<Result<ModuleDiscoveryPathState, HarnessArtifactFileSystemErrorInfo>>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const nodeHarnessArtifactFileSystemGateway: HarnessArtifactFileSystemGateway &
|
|
67
|
+
HarnessArtifactModuleDiscoveryGateway = {
|
|
68
|
+
async listFiles(rootPath) {
|
|
69
|
+
try {
|
|
70
|
+
return resultOk(await listFiles(rootPath));
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return resultErr(fileSystemError(rootPath, "list", error));
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
async readOptionalFile(path) {
|
|
76
|
+
return withMissingAsOk({
|
|
77
|
+
path,
|
|
78
|
+
operation: "read",
|
|
79
|
+
run: async () => ({ type: "file", bytes: await readFile(path) }),
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
async writeFile(path, bytes) {
|
|
83
|
+
try {
|
|
84
|
+
await mkdir(dirname(path), { recursive: true });
|
|
85
|
+
await writeFile(path, bytes);
|
|
86
|
+
return resultOk(undefined);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
return resultErr(fileSystemError(path, "write", error));
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
async readOptionalTextFile(path) {
|
|
92
|
+
const state = await nodeHarnessArtifactFileSystemGateway.readOptionalFile(path);
|
|
93
|
+
if (!state.ok) return state;
|
|
94
|
+
if (state.value.type === "missing") return resultOk({ type: "missing" });
|
|
95
|
+
return resultOk({ type: "file", text: new TextDecoder().decode(state.value.bytes) });
|
|
96
|
+
},
|
|
97
|
+
async writeTextFile(path, text) {
|
|
98
|
+
return nodeHarnessArtifactFileSystemGateway.writeFile(path, new TextEncoder().encode(text));
|
|
99
|
+
},
|
|
100
|
+
async readDirectory(path) {
|
|
101
|
+
return withMissingAsOk({
|
|
102
|
+
path,
|
|
103
|
+
operation: "list",
|
|
104
|
+
async run() {
|
|
105
|
+
const pathStat = await stat(path);
|
|
106
|
+
if (pathStat.isFile()) return { type: "file" } as const;
|
|
107
|
+
if (!pathStat.isDirectory()) return { type: "file" } as const;
|
|
108
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
109
|
+
return {
|
|
110
|
+
type: "directory",
|
|
111
|
+
entries: entries.map((entry) => ({
|
|
112
|
+
name: entry.name,
|
|
113
|
+
type: entry.isDirectory() ? "directory" : entry.isFile() ? "file" : "other",
|
|
114
|
+
})),
|
|
115
|
+
} as const;
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
async pathState(path) {
|
|
120
|
+
return withMissingAsOk({
|
|
121
|
+
path,
|
|
122
|
+
operation: "stat",
|
|
123
|
+
async run() {
|
|
124
|
+
const pathStat = await stat(path);
|
|
125
|
+
if (pathStat.isDirectory()) return { type: "directory" } as const;
|
|
126
|
+
if (pathStat.isFile()) return { type: "file" } as const;
|
|
127
|
+
return { type: "other" } as const;
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
async function withMissingAsOk<T>(options: {
|
|
134
|
+
path: string;
|
|
135
|
+
operation: HarnessArtifactFileSystemErrorInfo["details"]["operation"];
|
|
136
|
+
run: () => Promise<T>;
|
|
137
|
+
}): Promise<Result<T | { type: "missing" }, HarnessArtifactFileSystemErrorInfo>> {
|
|
138
|
+
try {
|
|
139
|
+
return resultOk(await options.run());
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (isNodeErrorCode(error, "ENOENT")) return resultOk({ type: "missing" });
|
|
142
|
+
return resultErr(fileSystemError(options.path, options.operation, error));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function fileSystemError(
|
|
147
|
+
path: string,
|
|
148
|
+
operation: HarnessArtifactFileSystemErrorInfo["details"]["operation"],
|
|
149
|
+
error: unknown,
|
|
150
|
+
): HarnessArtifactFileSystemErrorInfo {
|
|
151
|
+
return {
|
|
152
|
+
code: "filesystem_error",
|
|
153
|
+
message: `Filesystem ${operation} failed for ${path}: ${formatErrorMessage(error)}`,
|
|
154
|
+
details: { path, operation },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function isNodeErrorCode(error: unknown, code: string): boolean {
|
|
159
|
+
return errorCodeFromUnknown(error) === code;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function listFiles(rootPath: string): Promise<readonly string[]> {
|
|
163
|
+
return sortStrings(await walkFiles(rootPath, ""));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function walkFiles(rootPath: string, relativePath: string): Promise<readonly string[]> {
|
|
167
|
+
const directory = relativePath === "" ? rootPath : join(rootPath, relativePath);
|
|
168
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
169
|
+
const output: string[] = [];
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
const entryRelativePath = relativePath === "" ? entry.name : join(relativePath, entry.name);
|
|
172
|
+
if (entry.isDirectory()) {
|
|
173
|
+
output.push(...(await walkFiles(rootPath, entryRelativePath)));
|
|
174
|
+
} else if (entry.isFile()) {
|
|
175
|
+
output.push(entryRelativePath);
|
|
176
|
+
} else {
|
|
177
|
+
throw new Error(`Unsupported non-file source path: ${join(rootPath, entryRelativePath)}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return output;
|
|
181
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
artifactProvisionName,
|
|
3
|
+
type FirstPartyHarnessArtifactCatalog,
|
|
4
|
+
type SkillHarnessArtifactEntry,
|
|
5
|
+
} from "./artifact-catalog.ts";
|
|
6
|
+
|
|
7
|
+
export const NS_FIRST_PARTY_HARNESS_ARTIFACT_CATALOG = {
|
|
8
|
+
type: "first-party-catalog",
|
|
9
|
+
catalogId: "ns-first-party",
|
|
10
|
+
artifacts: [
|
|
11
|
+
{
|
|
12
|
+
kind: "skill",
|
|
13
|
+
id: "objective-skill",
|
|
14
|
+
name: "Objective skill",
|
|
15
|
+
description: "Objective workflow grounding for ns Objective records.",
|
|
16
|
+
skillName: "objective",
|
|
17
|
+
source: {
|
|
18
|
+
type: "first-party",
|
|
19
|
+
packageName: "@nseng-ai/ns",
|
|
20
|
+
relativePath: "skills/objective",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
} as const satisfies FirstPartyHarnessArtifactCatalog;
|
|
25
|
+
|
|
26
|
+
export type FirstPartySkillHarnessArtifact = Extract<
|
|
27
|
+
(typeof NS_FIRST_PARTY_HARNESS_ARTIFACT_CATALOG)["artifacts"][number],
|
|
28
|
+
SkillHarnessArtifactEntry
|
|
29
|
+
>;
|
|
30
|
+
|
|
31
|
+
export function listFirstPartySkillArtifacts(): readonly FirstPartySkillHarnessArtifact[] {
|
|
32
|
+
return NS_FIRST_PARTY_HARNESS_ARTIFACT_CATALOG.artifacts.filter(
|
|
33
|
+
(artifact) => artifact.kind === "skill",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function findFirstPartySkillArtifact(
|
|
38
|
+
skill: string,
|
|
39
|
+
): FirstPartySkillHarnessArtifact | undefined {
|
|
40
|
+
const normalized = skill.trim();
|
|
41
|
+
return listFirstPartySkillArtifacts().find(
|
|
42
|
+
(artifact) => artifact.id === normalized || artifactProvisionName(artifact) === normalized,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { optionalEntry } from "@nseng-ai/foundation/primitives";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
findFirstPartySkillArtifact,
|
|
9
|
+
listFirstPartySkillArtifacts,
|
|
10
|
+
} from "./first-party-catalog.ts";
|
|
11
|
+
import type { HarnessPathContext, HarnessPathEnvironment, HarnessScope } from "./harness-paths.ts";
|
|
12
|
+
import {
|
|
13
|
+
applyPreparedProvision,
|
|
14
|
+
prepareProvision,
|
|
15
|
+
type HarnessArtifactFileSystemGateway,
|
|
16
|
+
type HarnessArtifactProvisionErrorInfo,
|
|
17
|
+
} from "./provision-apply.ts";
|
|
18
|
+
import type { ProvisionDecisionSet, ProvisionPlan } from "./provision-plan.ts";
|
|
19
|
+
|
|
20
|
+
export const FIRST_PARTY_SKILL_CATALOG_SOURCE_VERSION = "static-catalog-v1";
|
|
21
|
+
export const FIRST_PARTY_SKILL_CATALOG_SOURCE_UNAVAILABLE_MESSAGE =
|
|
22
|
+
"Could not locate the first-party ns skill catalog source root for provisioning.";
|
|
23
|
+
|
|
24
|
+
export function resolveFirstPartyCatalogSourceRoot(): string | undefined {
|
|
25
|
+
let current = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const sentinelPath = firstPartyCatalogSentinelPath();
|
|
27
|
+
if (sentinelPath === undefined) return undefined;
|
|
28
|
+
for (let index = 0; index < 12; index += 1) {
|
|
29
|
+
if (existsSync(join(current, sentinelPath))) return current;
|
|
30
|
+
const parent = dirname(current);
|
|
31
|
+
if (parent === current) break;
|
|
32
|
+
current = parent;
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ProvisionFirstPartySkillRequest {
|
|
38
|
+
skill: string;
|
|
39
|
+
harness: string;
|
|
40
|
+
scope: HarnessScope;
|
|
41
|
+
projectRoot: string;
|
|
42
|
+
homeDir?: string;
|
|
43
|
+
env: Record<string, string | undefined>;
|
|
44
|
+
isDryRun: boolean;
|
|
45
|
+
shouldForce: boolean;
|
|
46
|
+
/** Test seam: overrides the resolved first-party catalog source root. */
|
|
47
|
+
sourceRoot?: string;
|
|
48
|
+
/** Test seam: overrides the first-party catalog source version. */
|
|
49
|
+
sourceVersion?: string;
|
|
50
|
+
fs?: HarnessArtifactFileSystemGateway;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type ProvisionFirstPartySkillOutcome =
|
|
54
|
+
| {
|
|
55
|
+
type: "provisioned";
|
|
56
|
+
mode: "dry-run" | "applied";
|
|
57
|
+
plan: ProvisionPlan;
|
|
58
|
+
decisions: ProvisionDecisionSet;
|
|
59
|
+
manifestPath: string;
|
|
60
|
+
writtenFiles: readonly string[];
|
|
61
|
+
}
|
|
62
|
+
| {
|
|
63
|
+
type: "conflicted";
|
|
64
|
+
plan: ProvisionPlan;
|
|
65
|
+
decisions: ProvisionDecisionSet;
|
|
66
|
+
manifestPath: string;
|
|
67
|
+
conflictingFiles: readonly string[];
|
|
68
|
+
}
|
|
69
|
+
| { type: "unknown-skill"; skill: string }
|
|
70
|
+
| { type: "catalog-source-unavailable"; message: string }
|
|
71
|
+
| { type: "error"; error: HarnessArtifactProvisionErrorInfo };
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Deep first-party skill provisioning operation: resolves the catalog source
|
|
75
|
+
* root, looks up the skill, prepares the provision once, and either previews
|
|
76
|
+
* (dry run) or applies it. Thin adapters (`ns skills install`, ns-init's
|
|
77
|
+
* `RealSkillMaterializer`) map these outcomes onto their own surfaces.
|
|
78
|
+
*/
|
|
79
|
+
export async function provisionFirstPartySkill(
|
|
80
|
+
request: ProvisionFirstPartySkillRequest,
|
|
81
|
+
): Promise<ProvisionFirstPartySkillOutcome> {
|
|
82
|
+
const sourceRoot = request.sourceRoot ?? resolveFirstPartyCatalogSourceRoot();
|
|
83
|
+
if (sourceRoot === undefined) {
|
|
84
|
+
return {
|
|
85
|
+
type: "catalog-source-unavailable",
|
|
86
|
+
message: FIRST_PARTY_SKILL_CATALOG_SOURCE_UNAVAILABLE_MESSAGE,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const artifact = findFirstPartySkillArtifact(request.skill);
|
|
90
|
+
if (artifact === undefined) return { type: "unknown-skill", skill: request.skill };
|
|
91
|
+
const prepared = await prepareProvision({
|
|
92
|
+
artifact,
|
|
93
|
+
harness: request.harness,
|
|
94
|
+
scope: request.scope,
|
|
95
|
+
context: firstPartySkillProvisionPathContext({
|
|
96
|
+
projectRoot: request.projectRoot,
|
|
97
|
+
...optionalEntry("homeDir", request.homeDir),
|
|
98
|
+
env: request.env,
|
|
99
|
+
}),
|
|
100
|
+
sourceRoot,
|
|
101
|
+
sourceVersion: request.sourceVersion ?? FIRST_PARTY_SKILL_CATALOG_SOURCE_VERSION,
|
|
102
|
+
...optionalEntry("fs", request.fs),
|
|
103
|
+
});
|
|
104
|
+
if (!prepared.ok) return { type: "error", error: prepared.error };
|
|
105
|
+
if (request.isDryRun) {
|
|
106
|
+
return {
|
|
107
|
+
type: "provisioned",
|
|
108
|
+
mode: "dry-run",
|
|
109
|
+
plan: prepared.value.plan,
|
|
110
|
+
decisions: prepared.value.decisions,
|
|
111
|
+
manifestPath: prepared.value.manifestPath,
|
|
112
|
+
writtenFiles: [],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const applied = await applyPreparedProvision(prepared.value, {
|
|
116
|
+
shouldForce: request.shouldForce,
|
|
117
|
+
});
|
|
118
|
+
if (!applied.ok) return { type: "error", error: applied.error };
|
|
119
|
+
if (applied.value.outcome === "conflicted") {
|
|
120
|
+
return {
|
|
121
|
+
type: "conflicted",
|
|
122
|
+
plan: applied.value.plan,
|
|
123
|
+
decisions: applied.value.decisions,
|
|
124
|
+
manifestPath: applied.value.manifestPath,
|
|
125
|
+
conflictingFiles: applied.value.conflictingFiles,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
type: "provisioned",
|
|
130
|
+
mode: "applied",
|
|
131
|
+
plan: applied.value.plan,
|
|
132
|
+
decisions: applied.value.decisions,
|
|
133
|
+
manifestPath: applied.value.manifestPath,
|
|
134
|
+
writtenFiles: applied.value.writtenFiles,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function firstPartySkillProvisionPathContext(input: {
|
|
139
|
+
projectRoot: string;
|
|
140
|
+
homeDir?: string;
|
|
141
|
+
env: Record<string, string | undefined>;
|
|
142
|
+
}): HarnessPathContext {
|
|
143
|
+
return {
|
|
144
|
+
projectRoot: input.projectRoot,
|
|
145
|
+
...optionalEntry("homeDir", input.homeDir),
|
|
146
|
+
...optionalEntry("env", harnessPathEnvironment(input.env)),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function firstPartyCatalogSentinelPath(): string | undefined {
|
|
151
|
+
const artifact = listFirstPartySkillArtifacts()[0];
|
|
152
|
+
if (artifact === undefined) return undefined;
|
|
153
|
+
return join(artifact.source.relativePath, "SKILL.md");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function harnessPathEnvironment(
|
|
157
|
+
env: Record<string, string | undefined>,
|
|
158
|
+
): HarnessPathEnvironment | undefined {
|
|
159
|
+
const claudeConfigDir = env.CLAUDE_CONFIG_DIR;
|
|
160
|
+
if (claudeConfigDir === undefined) return undefined;
|
|
161
|
+
return { CLAUDE_CONFIG_DIR: claudeConfigDir };
|
|
162
|
+
}
|