@harness-engineering/cli 1.11.0 → 1.13.0
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/dist/agents/skills/claude-code/harness-autopilot/SKILL.md +57 -9
- package/dist/agents/skills/claude-code/harness-brainstorming/SKILL.md +1 -1
- package/dist/agents/skills/claude-code/harness-code-review/SKILL.md +19 -2
- package/dist/agents/skills/claude-code/harness-execution/SKILL.md +39 -12
- package/dist/agents/skills/claude-code/harness-planning/SKILL.md +28 -11
- package/dist/agents/skills/claude-code/harness-roadmap/SKILL.md +34 -0
- package/dist/agents/skills/claude-code/harness-verification/SKILL.md +42 -0
- package/dist/agents/skills/gemini-cli/harness-autopilot/SKILL.md +57 -9
- package/dist/agents/skills/gemini-cli/harness-brainstorming/SKILL.md +1 -1
- package/dist/agents/skills/gemini-cli/harness-code-review/SKILL.md +19 -2
- package/dist/agents/skills/gemini-cli/harness-execution/SKILL.md +39 -12
- package/dist/agents/skills/gemini-cli/harness-planning/SKILL.md +28 -11
- package/dist/agents/skills/gemini-cli/harness-roadmap/SKILL.md +34 -0
- package/dist/agents/skills/gemini-cli/harness-verification/SKILL.md +42 -0
- package/dist/{agents-md-ZFV6RR5J.js → agents-md-P2RHSUV7.js} +1 -1
- package/dist/{architecture-EXNUMH5R.js → architecture-ESOOE26S.js} +2 -2
- package/dist/bin/harness-mcp.js +10 -10
- package/dist/bin/harness.js +12 -12
- package/dist/{check-phase-gate-VZFOY2PO.js → check-phase-gate-S2MZKLFQ.js} +2 -2
- package/dist/{chunk-GSIVNYVJ.js → chunk-2VU4MFM3.js} +4 -4
- package/dist/{chunk-2NCIKJES.js → chunk-3KOLLWWE.js} +1 -1
- package/dist/{chunk-X3MN5UQJ.js → chunk-5VY23YK3.js} +1 -1
- package/dist/{chunk-I6JZYEGT.js → chunk-7KQSUZVG.js} +96 -50
- package/dist/{chunk-PA2XHK75.js → chunk-7PZWR4LI.js} +3 -3
- package/dist/{chunk-2YSQOUHO.js → chunk-KELT6K6M.js} +662 -283
- package/dist/{chunk-WUJTCNOU.js → chunk-LD3DKUK5.js} +1 -1
- package/dist/{chunk-Z75JC6I2.js → chunk-MACVXDZK.js} +2 -2
- package/dist/{chunk-NC6PXVWT.js → chunk-MI5XJQDY.js} +3 -3
- package/dist/{chunk-WJZDO6OY.js → chunk-PSNN4LWX.js} +2 -2
- package/dist/{chunk-ZWC3MN5E.js → chunk-RZSUJBZZ.js} +765 -203
- package/dist/{chunk-TI4TGEX6.js → chunk-WPPDRIJL.js} +1 -1
- package/dist/{ci-workflow-K5RCRNYR.js → ci-workflow-4NYBUG6R.js} +1 -1
- package/dist/{dist-JVZ2MKBC.js → dist-WF4C7A4A.js} +27 -1
- package/dist/{docs-PWCUVYWU.js → docs-BPYCN2DR.js} +2 -2
- package/dist/{engine-6XUP6GAK.js → engine-LXLIWQQ3.js} +1 -1
- package/dist/{entropy-4I6JEYAC.js → entropy-4VDVV5CR.js} +2 -2
- package/dist/{feedback-TNIW534S.js → feedback-63QB5RCA.js} +1 -1
- package/dist/{generate-agent-definitions-MWKEA5NU.js → generate-agent-definitions-QABOJG56.js} +1 -1
- package/dist/index.d.ts +80 -43
- package/dist/index.js +17 -13
- package/dist/{loader-4FIPIFII.js → loader-Z2IT7QX3.js} +1 -1
- package/dist/{mcp-MOKLYNZL.js → mcp-KQHEL5IF.js} +10 -10
- package/dist/{performance-BTOJCPXU.js → performance-26BH47O4.js} +2 -2
- package/dist/{review-pipeline-3YTW3463.js → review-pipeline-GHR3WFBI.js} +1 -1
- package/dist/{runtime-GO7K2PJE.js → runtime-PDWD7UIK.js} +1 -1
- package/dist/{security-4P2GGFF6.js → security-UQFUZXEN.js} +1 -1
- package/dist/{validate-JN44D2Q7.js → validate-N7QJOKFZ.js} +2 -2
- package/dist/{validate-cross-check-DB7RIFFF.js → validate-cross-check-EDQ5QGTM.js} +1 -1
- package/package.json +4 -4
|
@@ -5,9 +5,10 @@ import {
|
|
|
5
5
|
OutputFormatter,
|
|
6
6
|
OutputMode,
|
|
7
7
|
createCheckPhaseGateCommand,
|
|
8
|
+
findConfigFile,
|
|
8
9
|
findFiles,
|
|
9
10
|
resolveConfig
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-3KOLLWWE.js";
|
|
11
12
|
import {
|
|
12
13
|
createGenerateAgentDefinitionsCommand,
|
|
13
14
|
generateAgentDefinitions
|
|
@@ -49,7 +50,7 @@ import {
|
|
|
49
50
|
generateSlashCommands,
|
|
50
51
|
handleGetImpact,
|
|
51
52
|
handleOrphanDeletion
|
|
52
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-7KQSUZVG.js";
|
|
53
54
|
import {
|
|
54
55
|
VALID_PLATFORMS
|
|
55
56
|
} from "./chunk-ZOAWBDWU.js";
|
|
@@ -79,11 +80,13 @@ import {
|
|
|
79
80
|
ArchConfigSchema,
|
|
80
81
|
BaselineManager,
|
|
81
82
|
BlueprintGenerator,
|
|
83
|
+
BundleSchema,
|
|
82
84
|
CriticalPathResolver,
|
|
83
85
|
EntropyAnalyzer,
|
|
84
86
|
ProjectScanner,
|
|
85
87
|
SecurityScanner,
|
|
86
88
|
TypeScriptParser,
|
|
89
|
+
addProvenance,
|
|
87
90
|
appendLearning,
|
|
88
91
|
applyFixes,
|
|
89
92
|
archiveStream,
|
|
@@ -91,6 +94,7 @@ import {
|
|
|
91
94
|
checkDocCoverage,
|
|
92
95
|
createFixes,
|
|
93
96
|
createStream,
|
|
97
|
+
deepMergeConstraints,
|
|
94
98
|
defineLayer,
|
|
95
99
|
detectCircularDepsInFiles,
|
|
96
100
|
detectDeadCode,
|
|
@@ -104,6 +108,10 @@ import {
|
|
|
104
108
|
parseDiff,
|
|
105
109
|
parseManifest,
|
|
106
110
|
parseSecurityConfig,
|
|
111
|
+
pruneLearnings,
|
|
112
|
+
readLockfile,
|
|
113
|
+
removeContributions,
|
|
114
|
+
removeProvenance,
|
|
107
115
|
requestPeerReview,
|
|
108
116
|
resolveStreamPath,
|
|
109
117
|
runAll,
|
|
@@ -113,15 +121,16 @@ import {
|
|
|
113
121
|
validateAgentsMap,
|
|
114
122
|
validateDependencies,
|
|
115
123
|
validateKnowledgeMap,
|
|
116
|
-
writeConfig
|
|
117
|
-
|
|
124
|
+
writeConfig,
|
|
125
|
+
writeLockfile
|
|
126
|
+
} from "./chunk-KELT6K6M.js";
|
|
118
127
|
import {
|
|
119
128
|
Err,
|
|
120
129
|
Ok
|
|
121
130
|
} from "./chunk-MHBMTPW7.js";
|
|
122
131
|
|
|
123
132
|
// src/index.ts
|
|
124
|
-
import { Command as
|
|
133
|
+
import { Command as Command55 } from "commander";
|
|
125
134
|
|
|
126
135
|
// src/commands/validate.ts
|
|
127
136
|
import { Command } from "commander";
|
|
@@ -200,7 +209,7 @@ function createValidateCommand() {
|
|
|
200
209
|
process.exit(result.error.exitCode);
|
|
201
210
|
}
|
|
202
211
|
if (opts.crossCheck) {
|
|
203
|
-
const { runCrossCheck: runCrossCheck2 } = await import("./validate-cross-check-
|
|
212
|
+
const { runCrossCheck: runCrossCheck2 } = await import("./validate-cross-check-EDQ5QGTM.js");
|
|
204
213
|
const cwd = process.cwd();
|
|
205
214
|
const specsDir = path.join(cwd, "docs", "specs");
|
|
206
215
|
const plansDir = path.join(cwd, "docs", "plans");
|
|
@@ -468,10 +477,10 @@ async function runCheckSecurity(cwd, options) {
|
|
|
468
477
|
const projectRoot = path4.resolve(cwd);
|
|
469
478
|
let configData = {};
|
|
470
479
|
try {
|
|
471
|
-
const
|
|
480
|
+
const fs23 = await import("fs");
|
|
472
481
|
const configPath = path4.join(projectRoot, "harness.config.json");
|
|
473
|
-
if (
|
|
474
|
-
const raw =
|
|
482
|
+
if (fs23.existsSync(configPath)) {
|
|
483
|
+
const raw = fs23.readFileSync(configPath, "utf-8");
|
|
475
484
|
const parsed = JSON.parse(raw);
|
|
476
485
|
configData = parsed.security ?? {};
|
|
477
486
|
}
|
|
@@ -558,7 +567,7 @@ function createPerfCommand() {
|
|
|
558
567
|
perf.command("bench [glob]").description("Run benchmarks via vitest bench").action(async (glob, _opts, cmd) => {
|
|
559
568
|
const globalOpts = cmd.optsWithGlobals();
|
|
560
569
|
const cwd = process.cwd();
|
|
561
|
-
const { BenchmarkRunner } = await import("./dist-
|
|
570
|
+
const { BenchmarkRunner } = await import("./dist-WF4C7A4A.js");
|
|
562
571
|
const runner = new BenchmarkRunner();
|
|
563
572
|
const benchFiles = runner.discover(cwd, glob);
|
|
564
573
|
if (benchFiles.length === 0) {
|
|
@@ -627,7 +636,7 @@ Results (${result.results.length} benchmarks):`);
|
|
|
627
636
|
baselines.command("update").description("Update baselines from latest benchmark run").action(async (_opts, cmd) => {
|
|
628
637
|
const globalOpts = cmd.optsWithGlobals();
|
|
629
638
|
const cwd = process.cwd();
|
|
630
|
-
const { BenchmarkRunner } = await import("./dist-
|
|
639
|
+
const { BenchmarkRunner } = await import("./dist-WF4C7A4A.js");
|
|
631
640
|
const runner = new BenchmarkRunner();
|
|
632
641
|
const manager = new BaselineManager(cwd);
|
|
633
642
|
logger.info("Running benchmarks to update baselines...");
|
|
@@ -655,7 +664,7 @@ Results (${result.results.length} benchmarks):`);
|
|
|
655
664
|
perf.command("report").description("Full performance report with metrics, trends, and hotspots").action(async (_opts, cmd) => {
|
|
656
665
|
const globalOpts = cmd.optsWithGlobals();
|
|
657
666
|
const cwd = process.cwd();
|
|
658
|
-
const { EntropyAnalyzer: EntropyAnalyzer2 } = await import("./dist-
|
|
667
|
+
const { EntropyAnalyzer: EntropyAnalyzer2 } = await import("./dist-WF4C7A4A.js");
|
|
659
668
|
const analyzer = new EntropyAnalyzer2({
|
|
660
669
|
rootDir: path5.resolve(cwd),
|
|
661
670
|
analyze: { complexity: true, coupling: true }
|
|
@@ -1905,7 +1914,7 @@ function sortedStringify(obj) {
|
|
|
1905
1914
|
2
|
|
1906
1915
|
);
|
|
1907
1916
|
}
|
|
1908
|
-
function
|
|
1917
|
+
function readLockfile2(filePath) {
|
|
1909
1918
|
if (!fs5.existsSync(filePath)) {
|
|
1910
1919
|
return createEmptyLockfile();
|
|
1911
1920
|
}
|
|
@@ -1925,7 +1934,7 @@ function readLockfile(filePath) {
|
|
|
1925
1934
|
}
|
|
1926
1935
|
return parsed;
|
|
1927
1936
|
}
|
|
1928
|
-
function
|
|
1937
|
+
function writeLockfile2(filePath, lockfile) {
|
|
1929
1938
|
const dir = path14.dirname(filePath);
|
|
1930
1939
|
fs5.mkdirSync(dir, { recursive: true });
|
|
1931
1940
|
fs5.writeFileSync(filePath, sortedStringify(lockfile) + "\n", "utf-8");
|
|
@@ -1996,8 +2005,18 @@ function collectSkills(opts) {
|
|
|
1996
2005
|
const globalDir = resolveGlobalSkillsDir();
|
|
1997
2006
|
const skillsDir = path15.dirname(globalDir);
|
|
1998
2007
|
const communityBase = path15.join(skillsDir, "community");
|
|
2008
|
+
const communityPlatformDir = path15.join(communityBase, "claude-code");
|
|
1999
2009
|
const lockfilePath = path15.join(communityBase, "skills-lock.json");
|
|
2000
|
-
const lockfile =
|
|
2010
|
+
const lockfile = readLockfile2(lockfilePath);
|
|
2011
|
+
const communitySkills = scanDirectory(communityPlatformDir, "community");
|
|
2012
|
+
for (const skill of communitySkills) {
|
|
2013
|
+
const pkgName = `@harness-skills/${skill.name}`;
|
|
2014
|
+
const lockEntry = lockfile.skills[pkgName];
|
|
2015
|
+
if (lockEntry) {
|
|
2016
|
+
skill.version = lockEntry.version;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
addUnique(communitySkills);
|
|
2001
2020
|
for (const [pkgName, entry] of Object.entries(lockfile.skills)) {
|
|
2002
2021
|
const shortName = pkgName.replace("@harness-skills/", "");
|
|
2003
2022
|
if (!seen.has(shortName)) {
|
|
@@ -2393,6 +2412,9 @@ function createInfoCommand() {
|
|
|
2393
2412
|
import { Command as Command25 } from "commander";
|
|
2394
2413
|
|
|
2395
2414
|
// src/registry/npm-client.ts
|
|
2415
|
+
import * as fs10 from "fs";
|
|
2416
|
+
import * as path19 from "path";
|
|
2417
|
+
import * as os2 from "os";
|
|
2396
2418
|
var NPM_REGISTRY = "https://registry.npmjs.org";
|
|
2397
2419
|
var FETCH_TIMEOUT_MS = 3e4;
|
|
2398
2420
|
var HARNESS_SKILLS_SCOPE = "@harness-skills/";
|
|
@@ -2411,12 +2433,37 @@ function extractSkillName(packageName) {
|
|
|
2411
2433
|
}
|
|
2412
2434
|
return packageName;
|
|
2413
2435
|
}
|
|
2414
|
-
|
|
2436
|
+
function readNpmrcToken(registryUrl) {
|
|
2437
|
+
const { hostname, pathname } = new URL(registryUrl);
|
|
2438
|
+
const registryPath = `//${hostname}${pathname.replace(/\/$/, "")}/:_authToken=`;
|
|
2439
|
+
const candidates = [path19.join(process.cwd(), ".npmrc"), path19.join(os2.homedir(), ".npmrc")];
|
|
2440
|
+
for (const npmrcPath of candidates) {
|
|
2441
|
+
try {
|
|
2442
|
+
const content = fs10.readFileSync(npmrcPath, "utf-8");
|
|
2443
|
+
for (const line of content.split("\n")) {
|
|
2444
|
+
const trimmed = line.trim();
|
|
2445
|
+
if (trimmed.startsWith(registryPath)) {
|
|
2446
|
+
return trimmed.slice(registryPath.length).trim();
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
} catch {
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
return null;
|
|
2453
|
+
}
|
|
2454
|
+
async function fetchPackageMetadata(packageName, registryUrl) {
|
|
2455
|
+
const registry = registryUrl ?? NPM_REGISTRY;
|
|
2456
|
+
const headers = {};
|
|
2457
|
+
if (registryUrl) {
|
|
2458
|
+
const token = readNpmrcToken(registryUrl);
|
|
2459
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
2460
|
+
}
|
|
2415
2461
|
const encodedName = encodeURIComponent(packageName);
|
|
2416
|
-
const url = `${
|
|
2462
|
+
const url = `${registry}/${encodedName}`;
|
|
2417
2463
|
let response;
|
|
2418
2464
|
try {
|
|
2419
2465
|
response = await fetch(url, {
|
|
2466
|
+
headers,
|
|
2420
2467
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
2421
2468
|
});
|
|
2422
2469
|
} catch {
|
|
@@ -2432,11 +2479,16 @@ async function fetchPackageMetadata(packageName) {
|
|
|
2432
2479
|
}
|
|
2433
2480
|
return await response.json();
|
|
2434
2481
|
}
|
|
2435
|
-
async function downloadTarball(tarballUrl) {
|
|
2482
|
+
async function downloadTarball(tarballUrl, authToken) {
|
|
2436
2483
|
let lastError;
|
|
2484
|
+
const headers = {};
|
|
2485
|
+
if (authToken) {
|
|
2486
|
+
headers["Authorization"] = `Bearer ${authToken}`;
|
|
2487
|
+
}
|
|
2437
2488
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2438
2489
|
try {
|
|
2439
2490
|
const response = await fetch(tarballUrl, {
|
|
2491
|
+
headers,
|
|
2440
2492
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
2441
2493
|
});
|
|
2442
2494
|
if (!response.ok) {
|
|
@@ -2450,12 +2502,19 @@ async function downloadTarball(tarballUrl) {
|
|
|
2450
2502
|
}
|
|
2451
2503
|
throw new Error(`Download failed for ${tarballUrl}. Try again. (${lastError?.message})`);
|
|
2452
2504
|
}
|
|
2453
|
-
async function searchNpmRegistry(query) {
|
|
2505
|
+
async function searchNpmRegistry(query, registryUrl) {
|
|
2506
|
+
const registry = registryUrl ?? NPM_REGISTRY;
|
|
2507
|
+
const headers = {};
|
|
2508
|
+
if (registryUrl) {
|
|
2509
|
+
const token = readNpmrcToken(registryUrl);
|
|
2510
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
2511
|
+
}
|
|
2454
2512
|
const searchText = encodeURIComponent(`scope:harness-skills ${query}`);
|
|
2455
|
-
const url = `${
|
|
2513
|
+
const url = `${registry}/-/v1/search?text=${searchText}&size=20`;
|
|
2456
2514
|
let response;
|
|
2457
2515
|
try {
|
|
2458
2516
|
response = await fetch(url, {
|
|
2517
|
+
headers,
|
|
2459
2518
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
2460
2519
|
});
|
|
2461
2520
|
} catch {
|
|
@@ -2476,7 +2535,7 @@ async function searchNpmRegistry(query) {
|
|
|
2476
2535
|
|
|
2477
2536
|
// src/commands/skill/search.ts
|
|
2478
2537
|
async function runSearch(query, opts) {
|
|
2479
|
-
const results = await searchNpmRegistry(query);
|
|
2538
|
+
const results = await searchNpmRegistry(query, opts.registry);
|
|
2480
2539
|
return results.filter((r) => {
|
|
2481
2540
|
if (opts.platform && !r.keywords.includes(opts.platform)) {
|
|
2482
2541
|
return false;
|
|
@@ -2488,7 +2547,7 @@ async function runSearch(query, opts) {
|
|
|
2488
2547
|
});
|
|
2489
2548
|
}
|
|
2490
2549
|
function createSearchCommand() {
|
|
2491
|
-
return new Command25("search").description("Search for community skills on the @harness-skills registry").argument("<query>", "Search query").option("--platform <platform>", "Filter by platform (e.g., claude-code)").option("--trigger <trigger>", "Filter by trigger type (e.g., manual, automatic)").action(async (query, opts, cmd) => {
|
|
2550
|
+
return new Command25("search").description("Search for community skills on the @harness-skills registry").argument("<query>", "Search query").option("--platform <platform>", "Filter by platform (e.g., claude-code)").option("--trigger <trigger>", "Filter by trigger type (e.g., manual, automatic)").option("--registry <url>", "Use a custom npm registry URL").action(async (query, opts, cmd) => {
|
|
2492
2551
|
const globalOpts = cmd.optsWithGlobals();
|
|
2493
2552
|
try {
|
|
2494
2553
|
const results = await runSearch(query, opts);
|
|
@@ -2525,8 +2584,8 @@ Found ${results.length} skill(s):
|
|
|
2525
2584
|
|
|
2526
2585
|
// src/commands/skill/create.ts
|
|
2527
2586
|
import { Command as Command26 } from "commander";
|
|
2528
|
-
import * as
|
|
2529
|
-
import * as
|
|
2587
|
+
import * as path20 from "path";
|
|
2588
|
+
import * as fs11 from "fs";
|
|
2530
2589
|
import YAML from "yaml";
|
|
2531
2590
|
var KEBAB_CASE_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
2532
2591
|
function buildReadme(name, description) {
|
|
@@ -2610,22 +2669,22 @@ function runCreate(name, opts) {
|
|
|
2610
2669
|
if (!KEBAB_CASE_RE.test(name)) {
|
|
2611
2670
|
throw new Error(`Invalid skill name "${name}". Must be kebab-case (e.g., my-skill).`);
|
|
2612
2671
|
}
|
|
2613
|
-
const baseDir = opts.outputDir ??
|
|
2614
|
-
const skillDir =
|
|
2615
|
-
if (
|
|
2672
|
+
const baseDir = opts.outputDir ?? path20.join(process.cwd(), "agents", "skills", "claude-code");
|
|
2673
|
+
const skillDir = path20.join(baseDir, name);
|
|
2674
|
+
if (fs11.existsSync(skillDir)) {
|
|
2616
2675
|
throw new Error(`Skill directory already exists: ${skillDir}`);
|
|
2617
2676
|
}
|
|
2618
|
-
|
|
2677
|
+
fs11.mkdirSync(skillDir, { recursive: true });
|
|
2619
2678
|
const description = opts.description || `A community skill: ${name}`;
|
|
2620
2679
|
const skillYaml = buildSkillYaml(name, opts);
|
|
2621
|
-
const skillYamlPath =
|
|
2622
|
-
|
|
2680
|
+
const skillYamlPath = path20.join(skillDir, "skill.yaml");
|
|
2681
|
+
fs11.writeFileSync(skillYamlPath, YAML.stringify(skillYaml));
|
|
2623
2682
|
const skillMd = buildSkillMd(name, description);
|
|
2624
|
-
const skillMdPath =
|
|
2625
|
-
|
|
2683
|
+
const skillMdPath = path20.join(skillDir, "SKILL.md");
|
|
2684
|
+
fs11.writeFileSync(skillMdPath, skillMd);
|
|
2626
2685
|
const readme = buildReadme(name, description);
|
|
2627
|
-
const readmePath =
|
|
2628
|
-
|
|
2686
|
+
const readmePath = path20.join(skillDir, "README.md");
|
|
2687
|
+
fs11.writeFileSync(readmePath, readme);
|
|
2629
2688
|
return {
|
|
2630
2689
|
name,
|
|
2631
2690
|
directory: skillDir,
|
|
@@ -2653,7 +2712,7 @@ function createCreateCommand() {
|
|
|
2653
2712
|
logger.info(`
|
|
2654
2713
|
Next steps:`);
|
|
2655
2714
|
logger.info(
|
|
2656
|
-
` 1. Edit ${
|
|
2715
|
+
` 1. Edit ${path20.join(result.directory, "SKILL.md")} with your skill content`
|
|
2657
2716
|
);
|
|
2658
2717
|
logger.info(` 2. Run: harness skill validate ${name}`);
|
|
2659
2718
|
logger.info(` 3. Run: harness skills publish`);
|
|
@@ -2667,27 +2726,28 @@ Next steps:`);
|
|
|
2667
2726
|
|
|
2668
2727
|
// src/commands/skill/publish.ts
|
|
2669
2728
|
import { Command as Command27 } from "commander";
|
|
2670
|
-
import * as
|
|
2671
|
-
import * as
|
|
2729
|
+
import * as fs14 from "fs";
|
|
2730
|
+
import * as path23 from "path";
|
|
2672
2731
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
2673
2732
|
|
|
2674
2733
|
// src/registry/validator.ts
|
|
2675
|
-
import * as
|
|
2676
|
-
import * as
|
|
2734
|
+
import * as fs13 from "fs";
|
|
2735
|
+
import * as path22 from "path";
|
|
2677
2736
|
import { parse as parse5 } from "yaml";
|
|
2678
2737
|
import semver from "semver";
|
|
2679
2738
|
|
|
2680
2739
|
// src/registry/bundled-skills.ts
|
|
2681
|
-
import * as
|
|
2740
|
+
import * as fs12 from "fs";
|
|
2741
|
+
import * as path21 from "path";
|
|
2682
2742
|
function getBundledSkillNames(bundledSkillsDir) {
|
|
2683
|
-
if (!
|
|
2743
|
+
if (!fs12.existsSync(bundledSkillsDir)) {
|
|
2684
2744
|
return /* @__PURE__ */ new Set();
|
|
2685
2745
|
}
|
|
2686
|
-
const entries =
|
|
2746
|
+
const entries = fs12.readdirSync(bundledSkillsDir);
|
|
2687
2747
|
const names = /* @__PURE__ */ new Set();
|
|
2688
2748
|
for (const entry of entries) {
|
|
2689
2749
|
try {
|
|
2690
|
-
const stat =
|
|
2750
|
+
const stat = fs12.statSync(path21.join(bundledSkillsDir, String(entry)));
|
|
2691
2751
|
if (stat.isDirectory()) {
|
|
2692
2752
|
names.add(String(entry));
|
|
2693
2753
|
}
|
|
@@ -2698,16 +2758,16 @@ function getBundledSkillNames(bundledSkillsDir) {
|
|
|
2698
2758
|
}
|
|
2699
2759
|
|
|
2700
2760
|
// src/registry/validator.ts
|
|
2701
|
-
async function validateForPublish(skillDir) {
|
|
2761
|
+
async function validateForPublish(skillDir, registryUrl) {
|
|
2702
2762
|
const errors = [];
|
|
2703
|
-
const skillYamlPath =
|
|
2704
|
-
if (!
|
|
2763
|
+
const skillYamlPath = path22.join(skillDir, "skill.yaml");
|
|
2764
|
+
if (!fs13.existsSync(skillYamlPath)) {
|
|
2705
2765
|
errors.push("skill.yaml not found. Create one with: harness skill create <name>");
|
|
2706
2766
|
return { valid: false, errors };
|
|
2707
2767
|
}
|
|
2708
2768
|
let skillMeta;
|
|
2709
2769
|
try {
|
|
2710
|
-
const raw =
|
|
2770
|
+
const raw = fs13.readFileSync(skillYamlPath, "utf-8");
|
|
2711
2771
|
const parsed = parse5(raw);
|
|
2712
2772
|
const result = SkillMetadataSchema.safeParse(parsed);
|
|
2713
2773
|
if (!result.success) {
|
|
@@ -2729,11 +2789,11 @@ async function validateForPublish(skillDir) {
|
|
|
2729
2789
|
if (!skillMeta.triggers || skillMeta.triggers.length === 0) {
|
|
2730
2790
|
errors.push("At least one trigger is required. Add triggers to skill.yaml.");
|
|
2731
2791
|
}
|
|
2732
|
-
const skillMdPath =
|
|
2733
|
-
if (!
|
|
2792
|
+
const skillMdPath = path22.join(skillDir, "SKILL.md");
|
|
2793
|
+
if (!fs13.existsSync(skillMdPath)) {
|
|
2734
2794
|
errors.push("SKILL.md not found. Create it with content describing your skill.");
|
|
2735
2795
|
} else {
|
|
2736
|
-
const content =
|
|
2796
|
+
const content = fs13.readFileSync(skillMdPath, "utf-8");
|
|
2737
2797
|
if (!content.includes("## When to Use")) {
|
|
2738
2798
|
errors.push('SKILL.md must contain a "## When to Use" section.');
|
|
2739
2799
|
}
|
|
@@ -2750,21 +2810,25 @@ async function validateForPublish(skillDir) {
|
|
|
2750
2810
|
}
|
|
2751
2811
|
try {
|
|
2752
2812
|
const packageName = resolvePackageName(skillMeta.name);
|
|
2753
|
-
const metadata = await fetchPackageMetadata(packageName);
|
|
2813
|
+
const metadata = await fetchPackageMetadata(packageName, registryUrl);
|
|
2754
2814
|
const publishedVersion = metadata["dist-tags"]?.latest;
|
|
2755
2815
|
if (publishedVersion && !semver.gt(skillMeta.version, publishedVersion)) {
|
|
2756
2816
|
errors.push(
|
|
2757
2817
|
`Version ${skillMeta.version} must be greater than published version ${publishedVersion}. Bump the version in skill.yaml.`
|
|
2758
2818
|
);
|
|
2759
2819
|
}
|
|
2760
|
-
} catch {
|
|
2820
|
+
} catch (err) {
|
|
2821
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2822
|
+
if (!msg.includes("not found")) {
|
|
2823
|
+
errors.push(`Cannot verify version against npm registry: ${msg}`);
|
|
2824
|
+
}
|
|
2761
2825
|
}
|
|
2762
2826
|
if (skillMeta.depends_on && skillMeta.depends_on.length > 0) {
|
|
2763
2827
|
for (const dep of skillMeta.depends_on) {
|
|
2764
2828
|
if (bundledNames.has(dep)) continue;
|
|
2765
2829
|
try {
|
|
2766
2830
|
const depPkg = resolvePackageName(dep);
|
|
2767
|
-
await fetchPackageMetadata(depPkg);
|
|
2831
|
+
await fetchPackageMetadata(depPkg, registryUrl);
|
|
2768
2832
|
} catch {
|
|
2769
2833
|
errors.push(
|
|
2770
2834
|
`Dependency "${dep}" not found on npm or as a bundled skill. Publish it first or remove from depends_on.`
|
|
@@ -2801,7 +2865,7 @@ function derivePackageJson(skill) {
|
|
|
2801
2865
|
|
|
2802
2866
|
// src/commands/skill/publish.ts
|
|
2803
2867
|
async function runPublish(skillDir, opts) {
|
|
2804
|
-
const validation = await validateForPublish(skillDir);
|
|
2868
|
+
const validation = await validateForPublish(skillDir, opts.registry);
|
|
2805
2869
|
if (!validation.valid) {
|
|
2806
2870
|
const errorList = validation.errors.map((e) => ` - ${e}`).join("\n");
|
|
2807
2871
|
throw new Error(`Pre-publish validation failed:
|
|
@@ -2809,11 +2873,11 @@ ${errorList}`);
|
|
|
2809
2873
|
}
|
|
2810
2874
|
const meta = validation.skillMeta;
|
|
2811
2875
|
const pkg = derivePackageJson(meta);
|
|
2812
|
-
const pkgPath =
|
|
2813
|
-
|
|
2814
|
-
const readmePath =
|
|
2815
|
-
if (!
|
|
2816
|
-
const skillMdContent =
|
|
2876
|
+
const pkgPath = path23.join(skillDir, "package.json");
|
|
2877
|
+
fs14.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
2878
|
+
const readmePath = path23.join(skillDir, "README.md");
|
|
2879
|
+
if (!fs14.existsSync(readmePath)) {
|
|
2880
|
+
const skillMdContent = fs14.readFileSync(path23.join(skillDir, "SKILL.md"), "utf-8");
|
|
2817
2881
|
const readme = `# ${pkg.name}
|
|
2818
2882
|
|
|
2819
2883
|
${meta.description}
|
|
@@ -2827,7 +2891,7 @@ harness install ${meta.name}
|
|
|
2827
2891
|
---
|
|
2828
2892
|
|
|
2829
2893
|
${skillMdContent}`;
|
|
2830
|
-
|
|
2894
|
+
fs14.writeFileSync(readmePath, readme);
|
|
2831
2895
|
}
|
|
2832
2896
|
if (opts.dryRun) {
|
|
2833
2897
|
return {
|
|
@@ -2837,7 +2901,11 @@ ${skillMdContent}`;
|
|
|
2837
2901
|
dryRun: true
|
|
2838
2902
|
};
|
|
2839
2903
|
}
|
|
2840
|
-
|
|
2904
|
+
const publishArgs = ["publish", "--access", "public"];
|
|
2905
|
+
if (opts.registry) {
|
|
2906
|
+
publishArgs.push("--registry", opts.registry);
|
|
2907
|
+
}
|
|
2908
|
+
execFileSync3("npm", publishArgs, {
|
|
2841
2909
|
cwd: skillDir,
|
|
2842
2910
|
stdio: "pipe",
|
|
2843
2911
|
timeout: 6e4
|
|
@@ -2849,12 +2917,13 @@ ${skillMdContent}`;
|
|
|
2849
2917
|
};
|
|
2850
2918
|
}
|
|
2851
2919
|
function createPublishCommand() {
|
|
2852
|
-
return new Command27("publish").description("Validate and publish a skill to @harness-skills on npm").option("--dry-run", "Run validation and generate package.json without publishing").option("--dir <dir>", "Skill directory (default: current directory)").action(async (opts, cmd) => {
|
|
2920
|
+
return new Command27("publish").description("Validate and publish a skill to @harness-skills on npm").option("--dry-run", "Run validation and generate package.json without publishing").option("--dir <dir>", "Skill directory (default: current directory)").option("--registry <url>", "Use a custom npm registry URL").action(async (opts, cmd) => {
|
|
2853
2921
|
const globalOpts = cmd.optsWithGlobals();
|
|
2854
2922
|
const skillDir = opts.dir || process.cwd();
|
|
2855
2923
|
try {
|
|
2856
2924
|
const result = await runPublish(skillDir, {
|
|
2857
|
-
dryRun: opts.dryRun
|
|
2925
|
+
dryRun: opts.dryRun,
|
|
2926
|
+
registry: opts.registry
|
|
2858
2927
|
});
|
|
2859
2928
|
if (globalOpts.json) {
|
|
2860
2929
|
logger.raw(result);
|
|
@@ -2889,11 +2958,11 @@ import { Command as Command33 } from "commander";
|
|
|
2889
2958
|
|
|
2890
2959
|
// src/commands/state/show.ts
|
|
2891
2960
|
import { Command as Command29 } from "commander";
|
|
2892
|
-
import * as
|
|
2961
|
+
import * as path24 from "path";
|
|
2893
2962
|
function createShowCommand() {
|
|
2894
2963
|
return new Command29("show").description("Show current project state").option("--path <path>", "Project root path", ".").option("--stream <name>", "Target a specific stream").action(async (opts, cmd) => {
|
|
2895
2964
|
const globalOpts = cmd.optsWithGlobals();
|
|
2896
|
-
const projectPath =
|
|
2965
|
+
const projectPath = path24.resolve(opts.path);
|
|
2897
2966
|
const result = await loadState(projectPath, opts.stream);
|
|
2898
2967
|
if (!result.ok) {
|
|
2899
2968
|
logger.error(result.error.message);
|
|
@@ -2934,12 +3003,12 @@ Decisions: ${state.decisions.length}`);
|
|
|
2934
3003
|
|
|
2935
3004
|
// src/commands/state/reset.ts
|
|
2936
3005
|
import { Command as Command30 } from "commander";
|
|
2937
|
-
import * as
|
|
2938
|
-
import * as
|
|
3006
|
+
import * as fs15 from "fs";
|
|
3007
|
+
import * as path25 from "path";
|
|
2939
3008
|
import * as readline from "readline";
|
|
2940
3009
|
function createResetCommand() {
|
|
2941
3010
|
return new Command30("reset").description("Reset project state (deletes .harness/state.json)").option("--path <path>", "Project root path", ".").option("--stream <name>", "Target a specific stream").option("--yes", "Skip confirmation prompt").action(async (opts, _cmd) => {
|
|
2942
|
-
const projectPath =
|
|
3011
|
+
const projectPath = path25.resolve(opts.path);
|
|
2943
3012
|
let statePath;
|
|
2944
3013
|
if (opts.stream) {
|
|
2945
3014
|
const streamResult = await resolveStreamPath(projectPath, { stream: opts.stream });
|
|
@@ -2948,19 +3017,19 @@ function createResetCommand() {
|
|
|
2948
3017
|
process.exit(ExitCode.ERROR);
|
|
2949
3018
|
return;
|
|
2950
3019
|
}
|
|
2951
|
-
statePath =
|
|
3020
|
+
statePath = path25.join(streamResult.value, "state.json");
|
|
2952
3021
|
} else {
|
|
2953
|
-
statePath =
|
|
3022
|
+
statePath = path25.join(projectPath, ".harness", "state.json");
|
|
2954
3023
|
}
|
|
2955
|
-
if (!
|
|
3024
|
+
if (!fs15.existsSync(statePath)) {
|
|
2956
3025
|
logger.info("No state file found. Nothing to reset.");
|
|
2957
3026
|
process.exit(ExitCode.SUCCESS);
|
|
2958
3027
|
return;
|
|
2959
3028
|
}
|
|
2960
3029
|
if (!opts.yes) {
|
|
2961
3030
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2962
|
-
const answer = await new Promise((
|
|
2963
|
-
rl.question("Reset project state? This cannot be undone. [y/N] ",
|
|
3031
|
+
const answer = await new Promise((resolve28) => {
|
|
3032
|
+
rl.question("Reset project state? This cannot be undone. [y/N] ", resolve28);
|
|
2964
3033
|
});
|
|
2965
3034
|
rl.close();
|
|
2966
3035
|
if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
|
|
@@ -2970,7 +3039,7 @@ function createResetCommand() {
|
|
|
2970
3039
|
}
|
|
2971
3040
|
}
|
|
2972
3041
|
try {
|
|
2973
|
-
|
|
3042
|
+
fs15.unlinkSync(statePath);
|
|
2974
3043
|
logger.success("Project state reset.");
|
|
2975
3044
|
} catch (e) {
|
|
2976
3045
|
logger.error(`Failed to reset state: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -2983,10 +3052,10 @@ function createResetCommand() {
|
|
|
2983
3052
|
|
|
2984
3053
|
// src/commands/state/learn.ts
|
|
2985
3054
|
import { Command as Command31 } from "commander";
|
|
2986
|
-
import * as
|
|
3055
|
+
import * as path26 from "path";
|
|
2987
3056
|
function createLearnCommand() {
|
|
2988
3057
|
return new Command31("learn").description("Append a learning to .harness/learnings.md").argument("<message>", "The learning to record").option("--path <path>", "Project root path", ".").option("--stream <name>", "Target a specific stream").action(async (message, opts, _cmd) => {
|
|
2989
|
-
const projectPath =
|
|
3058
|
+
const projectPath = path26.resolve(opts.path);
|
|
2990
3059
|
const result = await appendLearning(projectPath, message, void 0, void 0, opts.stream);
|
|
2991
3060
|
if (!result.ok) {
|
|
2992
3061
|
logger.error(result.error.message);
|
|
@@ -3000,12 +3069,12 @@ function createLearnCommand() {
|
|
|
3000
3069
|
|
|
3001
3070
|
// src/commands/state/streams.ts
|
|
3002
3071
|
import { Command as Command32 } from "commander";
|
|
3003
|
-
import * as
|
|
3072
|
+
import * as path27 from "path";
|
|
3004
3073
|
function createStreamsCommand() {
|
|
3005
3074
|
const command = new Command32("streams").description("Manage state streams");
|
|
3006
3075
|
command.command("list").description("List all known streams").option("--path <path>", "Project root path", ".").action(async (opts, cmd) => {
|
|
3007
3076
|
const globalOpts = cmd.optsWithGlobals();
|
|
3008
|
-
const projectPath =
|
|
3077
|
+
const projectPath = path27.resolve(opts.path);
|
|
3009
3078
|
const indexResult = await loadStreamIndex(projectPath);
|
|
3010
3079
|
const result = await listStreams(projectPath);
|
|
3011
3080
|
if (!result.ok) {
|
|
@@ -3029,7 +3098,7 @@ function createStreamsCommand() {
|
|
|
3029
3098
|
process.exit(ExitCode.SUCCESS);
|
|
3030
3099
|
});
|
|
3031
3100
|
command.command("create <name>").description("Create a new stream").option("--path <path>", "Project root path", ".").option("--branch <branch>", "Associate with a git branch").action(async (name, opts) => {
|
|
3032
|
-
const projectPath =
|
|
3101
|
+
const projectPath = path27.resolve(opts.path);
|
|
3033
3102
|
const result = await createStream(projectPath, name, opts.branch);
|
|
3034
3103
|
if (!result.ok) {
|
|
3035
3104
|
logger.error(result.error.message);
|
|
@@ -3040,7 +3109,7 @@ function createStreamsCommand() {
|
|
|
3040
3109
|
process.exit(ExitCode.SUCCESS);
|
|
3041
3110
|
});
|
|
3042
3111
|
command.command("archive <name>").description("Archive a stream").option("--path <path>", "Project root path", ".").action(async (name, opts) => {
|
|
3043
|
-
const projectPath =
|
|
3112
|
+
const projectPath = path27.resolve(opts.path);
|
|
3044
3113
|
const result = await archiveStream(projectPath, name);
|
|
3045
3114
|
if (!result.ok) {
|
|
3046
3115
|
logger.error(result.error.message);
|
|
@@ -3051,7 +3120,7 @@ function createStreamsCommand() {
|
|
|
3051
3120
|
process.exit(ExitCode.SUCCESS);
|
|
3052
3121
|
});
|
|
3053
3122
|
command.command("activate <name>").description("Set the active stream").option("--path <path>", "Project root path", ".").action(async (name, opts) => {
|
|
3054
|
-
const projectPath =
|
|
3123
|
+
const projectPath = path27.resolve(opts.path);
|
|
3055
3124
|
const result = await setActiveStream(projectPath, name);
|
|
3056
3125
|
if (!result.ok) {
|
|
3057
3126
|
logger.error(result.error.message);
|
|
@@ -3163,8 +3232,8 @@ function createCheckCommand() {
|
|
|
3163
3232
|
|
|
3164
3233
|
// src/commands/ci/init.ts
|
|
3165
3234
|
import { Command as Command35 } from "commander";
|
|
3166
|
-
import * as
|
|
3167
|
-
import * as
|
|
3235
|
+
import * as fs16 from "fs";
|
|
3236
|
+
import * as path28 from "path";
|
|
3168
3237
|
var ALL_CHECKS = [
|
|
3169
3238
|
"validate",
|
|
3170
3239
|
"deps",
|
|
@@ -3265,8 +3334,8 @@ function generateCIConfig(options) {
|
|
|
3265
3334
|
});
|
|
3266
3335
|
}
|
|
3267
3336
|
function detectPlatform() {
|
|
3268
|
-
if (
|
|
3269
|
-
if (
|
|
3337
|
+
if (fs16.existsSync(".github")) return "github";
|
|
3338
|
+
if (fs16.existsSync(".gitlab-ci.yml")) return "gitlab";
|
|
3270
3339
|
return null;
|
|
3271
3340
|
}
|
|
3272
3341
|
function createInitCommand2() {
|
|
@@ -3282,12 +3351,12 @@ function createInitCommand2() {
|
|
|
3282
3351
|
process.exit(result.error.exitCode);
|
|
3283
3352
|
}
|
|
3284
3353
|
const { filename, content } = result.value;
|
|
3285
|
-
const targetPath =
|
|
3286
|
-
const dir =
|
|
3287
|
-
|
|
3288
|
-
|
|
3354
|
+
const targetPath = path28.resolve(filename);
|
|
3355
|
+
const dir = path28.dirname(targetPath);
|
|
3356
|
+
fs16.mkdirSync(dir, { recursive: true });
|
|
3357
|
+
fs16.writeFileSync(targetPath, content);
|
|
3289
3358
|
if (platform === "generic" && process.platform !== "win32") {
|
|
3290
|
-
|
|
3359
|
+
fs16.chmodSync(targetPath, "755");
|
|
3291
3360
|
}
|
|
3292
3361
|
if (globalOpts.json) {
|
|
3293
3362
|
console.log(JSON.stringify({ file: filename, platform }));
|
|
@@ -3367,10 +3436,10 @@ function prompt(question) {
|
|
|
3367
3436
|
input: process.stdin,
|
|
3368
3437
|
output: process.stdout
|
|
3369
3438
|
});
|
|
3370
|
-
return new Promise((
|
|
3439
|
+
return new Promise((resolve28) => {
|
|
3371
3440
|
rl.question(question, (answer) => {
|
|
3372
3441
|
rl.close();
|
|
3373
|
-
|
|
3442
|
+
resolve28(answer.trim().toLowerCase());
|
|
3374
3443
|
});
|
|
3375
3444
|
});
|
|
3376
3445
|
}
|
|
@@ -3508,7 +3577,7 @@ function createGenerateCommand3() {
|
|
|
3508
3577
|
|
|
3509
3578
|
// src/commands/graph/scan.ts
|
|
3510
3579
|
import { Command as Command39 } from "commander";
|
|
3511
|
-
import * as
|
|
3580
|
+
import * as path29 from "path";
|
|
3512
3581
|
async function runScan(projectPath) {
|
|
3513
3582
|
const { GraphStore, CodeIngestor, TopologicalLinker, KnowledgeIngestor, GitIngestor } = await import("./dist-M6BQODWC.js");
|
|
3514
3583
|
const store = new GraphStore();
|
|
@@ -3521,13 +3590,13 @@ async function runScan(projectPath) {
|
|
|
3521
3590
|
await new GitIngestor(store).ingest(projectPath);
|
|
3522
3591
|
} catch {
|
|
3523
3592
|
}
|
|
3524
|
-
const graphDir =
|
|
3593
|
+
const graphDir = path29.join(projectPath, ".harness", "graph");
|
|
3525
3594
|
await store.save(graphDir);
|
|
3526
3595
|
return { nodeCount: store.nodeCount, edgeCount: store.edgeCount, durationMs: Date.now() - start };
|
|
3527
3596
|
}
|
|
3528
3597
|
function createScanCommand() {
|
|
3529
3598
|
return new Command39("scan").description("Scan project and build knowledge graph").argument("[path]", "Project root path", ".").action(async (inputPath, _opts, cmd) => {
|
|
3530
|
-
const projectPath =
|
|
3599
|
+
const projectPath = path29.resolve(inputPath);
|
|
3531
3600
|
const globalOpts = cmd.optsWithGlobals();
|
|
3532
3601
|
try {
|
|
3533
3602
|
const result = await runScan(projectPath);
|
|
@@ -3547,12 +3616,12 @@ function createScanCommand() {
|
|
|
3547
3616
|
|
|
3548
3617
|
// src/commands/graph/ingest.ts
|
|
3549
3618
|
import { Command as Command40 } from "commander";
|
|
3550
|
-
import * as
|
|
3619
|
+
import * as path30 from "path";
|
|
3551
3620
|
async function loadConnectorConfig(projectPath, source) {
|
|
3552
3621
|
try {
|
|
3553
|
-
const
|
|
3554
|
-
const configPath =
|
|
3555
|
-
const config = JSON.parse(await
|
|
3622
|
+
const fs23 = await import("fs/promises");
|
|
3623
|
+
const configPath = path30.join(projectPath, "harness.config.json");
|
|
3624
|
+
const config = JSON.parse(await fs23.readFile(configPath, "utf-8"));
|
|
3556
3625
|
const connector = config.graph?.connectors?.find(
|
|
3557
3626
|
(c) => c.source === source
|
|
3558
3627
|
);
|
|
@@ -3592,7 +3661,7 @@ async function runIngest(projectPath, source, opts) {
|
|
|
3592
3661
|
JiraConnector,
|
|
3593
3662
|
SlackConnector
|
|
3594
3663
|
} = await import("./dist-M6BQODWC.js");
|
|
3595
|
-
const graphDir =
|
|
3664
|
+
const graphDir = path30.join(projectPath, ".harness", "graph");
|
|
3596
3665
|
const store = new GraphStore();
|
|
3597
3666
|
await store.load(graphDir);
|
|
3598
3667
|
if (opts?.all) {
|
|
@@ -3659,7 +3728,7 @@ function createIngestCommand() {
|
|
|
3659
3728
|
process.exit(1);
|
|
3660
3729
|
}
|
|
3661
3730
|
const globalOpts = cmd.optsWithGlobals();
|
|
3662
|
-
const projectPath =
|
|
3731
|
+
const projectPath = path30.resolve(globalOpts.config ? path30.dirname(globalOpts.config) : ".");
|
|
3663
3732
|
try {
|
|
3664
3733
|
const result = await runIngest(projectPath, opts.source ?? "", {
|
|
3665
3734
|
full: opts.full,
|
|
@@ -3682,11 +3751,11 @@ function createIngestCommand() {
|
|
|
3682
3751
|
|
|
3683
3752
|
// src/commands/graph/query.ts
|
|
3684
3753
|
import { Command as Command41 } from "commander";
|
|
3685
|
-
import * as
|
|
3754
|
+
import * as path31 from "path";
|
|
3686
3755
|
async function runQuery(projectPath, rootNodeId, opts) {
|
|
3687
3756
|
const { GraphStore, ContextQL } = await import("./dist-M6BQODWC.js");
|
|
3688
3757
|
const store = new GraphStore();
|
|
3689
|
-
const graphDir =
|
|
3758
|
+
const graphDir = path31.join(projectPath, ".harness", "graph");
|
|
3690
3759
|
const loaded = await store.load(graphDir);
|
|
3691
3760
|
if (!loaded) throw new Error("No graph found. Run `harness scan` first.");
|
|
3692
3761
|
const params = {
|
|
@@ -3702,7 +3771,7 @@ async function runQuery(projectPath, rootNodeId, opts) {
|
|
|
3702
3771
|
function createQueryCommand() {
|
|
3703
3772
|
return new Command41("query").description("Query the knowledge graph").argument("<rootNodeId>", "Starting node ID").option("--depth <n>", "Max traversal depth", "3").option("--types <types>", "Comma-separated node types to include").option("--edges <edges>", "Comma-separated edge types to include").option("--bidirectional", "Traverse both directions").action(async (rootNodeId, opts, cmd) => {
|
|
3704
3773
|
const globalOpts = cmd.optsWithGlobals();
|
|
3705
|
-
const projectPath =
|
|
3774
|
+
const projectPath = path31.resolve(globalOpts.config ? path31.dirname(globalOpts.config) : ".");
|
|
3706
3775
|
try {
|
|
3707
3776
|
const result = await runQuery(projectPath, rootNodeId, {
|
|
3708
3777
|
depth: parseInt(opts.depth),
|
|
@@ -3731,18 +3800,18 @@ function createQueryCommand() {
|
|
|
3731
3800
|
import { Command as Command42 } from "commander";
|
|
3732
3801
|
|
|
3733
3802
|
// src/commands/graph/status.ts
|
|
3734
|
-
import * as
|
|
3803
|
+
import * as path32 from "path";
|
|
3735
3804
|
async function runGraphStatus(projectPath) {
|
|
3736
3805
|
const { GraphStore } = await import("./dist-M6BQODWC.js");
|
|
3737
|
-
const graphDir =
|
|
3806
|
+
const graphDir = path32.join(projectPath, ".harness", "graph");
|
|
3738
3807
|
const store = new GraphStore();
|
|
3739
3808
|
const loaded = await store.load(graphDir);
|
|
3740
3809
|
if (!loaded) return { status: "no_graph", message: "No graph found. Run `harness scan` first." };
|
|
3741
|
-
const
|
|
3742
|
-
const metaPath =
|
|
3810
|
+
const fs23 = await import("fs/promises");
|
|
3811
|
+
const metaPath = path32.join(graphDir, "metadata.json");
|
|
3743
3812
|
let lastScan = "unknown";
|
|
3744
3813
|
try {
|
|
3745
|
-
const meta = JSON.parse(await
|
|
3814
|
+
const meta = JSON.parse(await fs23.readFile(metaPath, "utf-8"));
|
|
3746
3815
|
lastScan = meta.lastScanTimestamp;
|
|
3747
3816
|
} catch {
|
|
3748
3817
|
}
|
|
@@ -3753,8 +3822,8 @@ async function runGraphStatus(projectPath) {
|
|
|
3753
3822
|
}
|
|
3754
3823
|
let connectorSyncStatus = {};
|
|
3755
3824
|
try {
|
|
3756
|
-
const syncMetaPath =
|
|
3757
|
-
const syncMeta = JSON.parse(await
|
|
3825
|
+
const syncMetaPath = path32.join(graphDir, "sync-metadata.json");
|
|
3826
|
+
const syncMeta = JSON.parse(await fs23.readFile(syncMetaPath, "utf-8"));
|
|
3758
3827
|
for (const [name, data] of Object.entries(syncMeta.connectors ?? {})) {
|
|
3759
3828
|
connectorSyncStatus[name] = data.lastSyncTimestamp;
|
|
3760
3829
|
}
|
|
@@ -3771,10 +3840,10 @@ async function runGraphStatus(projectPath) {
|
|
|
3771
3840
|
}
|
|
3772
3841
|
|
|
3773
3842
|
// src/commands/graph/export.ts
|
|
3774
|
-
import * as
|
|
3843
|
+
import * as path33 from "path";
|
|
3775
3844
|
async function runGraphExport(projectPath, format) {
|
|
3776
3845
|
const { GraphStore } = await import("./dist-M6BQODWC.js");
|
|
3777
|
-
const graphDir =
|
|
3846
|
+
const graphDir = path33.join(projectPath, ".harness", "graph");
|
|
3778
3847
|
const store = new GraphStore();
|
|
3779
3848
|
const loaded = await store.load(graphDir);
|
|
3780
3849
|
if (!loaded) throw new Error("No graph found. Run `harness scan` first.");
|
|
@@ -3803,13 +3872,13 @@ async function runGraphExport(projectPath, format) {
|
|
|
3803
3872
|
}
|
|
3804
3873
|
|
|
3805
3874
|
// src/commands/graph/index.ts
|
|
3806
|
-
import * as
|
|
3875
|
+
import * as path34 from "path";
|
|
3807
3876
|
function createGraphCommand() {
|
|
3808
3877
|
const graph = new Command42("graph").description("Knowledge graph management");
|
|
3809
3878
|
graph.command("status").description("Show graph statistics").action(async (_opts, cmd) => {
|
|
3810
3879
|
try {
|
|
3811
3880
|
const globalOpts = cmd.optsWithGlobals();
|
|
3812
|
-
const projectPath =
|
|
3881
|
+
const projectPath = path34.resolve(globalOpts.config ? path34.dirname(globalOpts.config) : ".");
|
|
3813
3882
|
const result = await runGraphStatus(projectPath);
|
|
3814
3883
|
if (globalOpts.json) {
|
|
3815
3884
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -3836,7 +3905,7 @@ function createGraphCommand() {
|
|
|
3836
3905
|
});
|
|
3837
3906
|
graph.command("export").description("Export graph").requiredOption("--format <format>", "Output format (json, mermaid)").action(async (opts, cmd) => {
|
|
3838
3907
|
const globalOpts = cmd.optsWithGlobals();
|
|
3839
|
-
const projectPath =
|
|
3908
|
+
const projectPath = path34.resolve(globalOpts.config ? path34.dirname(globalOpts.config) : ".");
|
|
3840
3909
|
try {
|
|
3841
3910
|
const output = await runGraphExport(projectPath, opts.format);
|
|
3842
3911
|
console.log(output);
|
|
@@ -3852,7 +3921,7 @@ function createGraphCommand() {
|
|
|
3852
3921
|
import { Command as Command43 } from "commander";
|
|
3853
3922
|
function createMcpCommand() {
|
|
3854
3923
|
return new Command43("mcp").description("Start the MCP (Model Context Protocol) server on stdio").action(async () => {
|
|
3855
|
-
const { startServer: startServer2 } = await import("./mcp-
|
|
3924
|
+
const { startServer: startServer2 } = await import("./mcp-KQHEL5IF.js");
|
|
3856
3925
|
await startServer2();
|
|
3857
3926
|
});
|
|
3858
3927
|
}
|
|
@@ -3860,8 +3929,8 @@ function createMcpCommand() {
|
|
|
3860
3929
|
// src/commands/impact-preview.ts
|
|
3861
3930
|
import { Command as Command44 } from "commander";
|
|
3862
3931
|
import { execSync as execSync3 } from "child_process";
|
|
3863
|
-
import * as
|
|
3864
|
-
import * as
|
|
3932
|
+
import * as path35 from "path";
|
|
3933
|
+
import * as fs17 from "fs";
|
|
3865
3934
|
function getStagedFiles(cwd) {
|
|
3866
3935
|
try {
|
|
3867
3936
|
const output = execSync3("git diff --cached --name-only", {
|
|
@@ -3875,7 +3944,7 @@ function getStagedFiles(cwd) {
|
|
|
3875
3944
|
}
|
|
3876
3945
|
function graphExists(projectPath) {
|
|
3877
3946
|
try {
|
|
3878
|
-
return
|
|
3947
|
+
return fs17.existsSync(path35.join(projectPath, ".harness", "graph", "graph.json"));
|
|
3879
3948
|
} catch {
|
|
3880
3949
|
return false;
|
|
3881
3950
|
}
|
|
@@ -3884,7 +3953,7 @@ function extractNodeName(id) {
|
|
|
3884
3953
|
const parts = id.split(":");
|
|
3885
3954
|
if (parts.length > 1) {
|
|
3886
3955
|
const fullPath = parts.slice(1).join(":");
|
|
3887
|
-
return
|
|
3956
|
+
return path35.basename(fullPath);
|
|
3888
3957
|
}
|
|
3889
3958
|
return id;
|
|
3890
3959
|
}
|
|
@@ -4007,7 +4076,7 @@ function formatPerFile(perFileResults) {
|
|
|
4007
4076
|
return lines.join("\n");
|
|
4008
4077
|
}
|
|
4009
4078
|
async function runImpactPreview(options) {
|
|
4010
|
-
const projectPath =
|
|
4079
|
+
const projectPath = path35.resolve(options.path ?? process.cwd());
|
|
4011
4080
|
const stagedFiles = getStagedFiles(projectPath);
|
|
4012
4081
|
if (stagedFiles.length === 0) {
|
|
4013
4082
|
return "Impact Preview: no staged changes";
|
|
@@ -4229,19 +4298,19 @@ function createCheckArchCommand() {
|
|
|
4229
4298
|
|
|
4230
4299
|
// src/commands/blueprint.ts
|
|
4231
4300
|
import { Command as Command46 } from "commander";
|
|
4232
|
-
import * as
|
|
4301
|
+
import * as path36 from "path";
|
|
4233
4302
|
function createBlueprintCommand() {
|
|
4234
4303
|
return new Command46("blueprint").description("Generate a self-contained, interactive blueprint of the codebase").argument("[path]", "Path to the project root", ".").option("-o, --output <dir>", "Output directory", "docs/blueprint").action(async (projectPath, options) => {
|
|
4235
4304
|
try {
|
|
4236
|
-
const rootDir =
|
|
4237
|
-
const outputDir =
|
|
4305
|
+
const rootDir = path36.resolve(projectPath);
|
|
4306
|
+
const outputDir = path36.resolve(options.output);
|
|
4238
4307
|
logger.info(`Scanning project at ${rootDir}...`);
|
|
4239
4308
|
const scanner = new ProjectScanner(rootDir);
|
|
4240
4309
|
const data = await scanner.scan();
|
|
4241
4310
|
logger.info(`Generating blueprint to ${outputDir}...`);
|
|
4242
4311
|
const generator = new BlueprintGenerator();
|
|
4243
4312
|
await generator.generate(data, { outputDir });
|
|
4244
|
-
logger.success(`Blueprint generated successfully at ${
|
|
4313
|
+
logger.success(`Blueprint generated successfully at ${path36.join(outputDir, "index.html")}`);
|
|
4245
4314
|
} catch (error) {
|
|
4246
4315
|
logger.error(
|
|
4247
4316
|
`Failed to generate blueprint: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -4253,15 +4322,15 @@ function createBlueprintCommand() {
|
|
|
4253
4322
|
|
|
4254
4323
|
// src/commands/share.ts
|
|
4255
4324
|
import { Command as Command47 } from "commander";
|
|
4256
|
-
import * as
|
|
4257
|
-
import * as
|
|
4325
|
+
import * as fs18 from "fs";
|
|
4326
|
+
import * as path37 from "path";
|
|
4258
4327
|
import { parse as parseYaml } from "yaml";
|
|
4259
4328
|
var MANIFEST_FILENAME = "constraints.yaml";
|
|
4260
4329
|
function createShareCommand() {
|
|
4261
4330
|
return new Command47("share").description("Extract and publish a constraints bundle from constraints.yaml").argument("[path]", "Path to the project root", ".").option("-o, --output <dir>", "Output directory for the bundle", ".").action(async (projectPath, options) => {
|
|
4262
|
-
const rootDir =
|
|
4263
|
-
const manifestPath =
|
|
4264
|
-
if (!
|
|
4331
|
+
const rootDir = path37.resolve(projectPath);
|
|
4332
|
+
const manifestPath = path37.join(rootDir, MANIFEST_FILENAME);
|
|
4333
|
+
if (!fs18.existsSync(manifestPath)) {
|
|
4265
4334
|
logger.error(
|
|
4266
4335
|
`No ${MANIFEST_FILENAME} found at ${manifestPath}.
|
|
4267
4336
|
Create a constraints.yaml in your project root to define what to share.`
|
|
@@ -4270,7 +4339,7 @@ Create a constraints.yaml in your project root to define what to share.`
|
|
|
4270
4339
|
}
|
|
4271
4340
|
let parsed;
|
|
4272
4341
|
try {
|
|
4273
|
-
const raw =
|
|
4342
|
+
const raw = fs18.readFileSync(manifestPath, "utf-8");
|
|
4274
4343
|
parsed = parseYaml(raw);
|
|
4275
4344
|
} catch (err) {
|
|
4276
4345
|
logger.error(
|
|
@@ -4284,7 +4353,7 @@ Create a constraints.yaml in your project root to define what to share.`
|
|
|
4284
4353
|
process.exit(1);
|
|
4285
4354
|
}
|
|
4286
4355
|
const manifest = manifestResult.value;
|
|
4287
|
-
const configResult = resolveConfig(
|
|
4356
|
+
const configResult = resolveConfig(path37.join(rootDir, "harness.config.json"));
|
|
4288
4357
|
if (!configResult.ok) {
|
|
4289
4358
|
logger.error(configResult.error.message);
|
|
4290
4359
|
process.exit(1);
|
|
@@ -4302,12 +4371,11 @@ Create a constraints.yaml in your project root to define what to share.`
|
|
|
4302
4371
|
);
|
|
4303
4372
|
process.exit(1);
|
|
4304
4373
|
}
|
|
4305
|
-
const outputDir =
|
|
4306
|
-
const outputPath =
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
logger.error(`Failed to write bundle: ${err instanceof Error ? err.message : String(err)}`);
|
|
4374
|
+
const outputDir = path37.resolve(options.output);
|
|
4375
|
+
const outputPath = path37.join(outputDir, `${manifest.name}.harness-constraints.json`);
|
|
4376
|
+
const writeResult = await writeConfig(outputPath, bundle);
|
|
4377
|
+
if (!writeResult.ok) {
|
|
4378
|
+
logger.error(`Failed to write bundle: ${writeResult.error.message}`);
|
|
4311
4379
|
process.exit(1);
|
|
4312
4380
|
}
|
|
4313
4381
|
logger.success(`Bundle written to ${outputPath}`);
|
|
@@ -4315,25 +4383,25 @@ Create a constraints.yaml in your project root to define what to share.`
|
|
|
4315
4383
|
}
|
|
4316
4384
|
|
|
4317
4385
|
// src/commands/install.ts
|
|
4318
|
-
import * as
|
|
4319
|
-
import * as
|
|
4386
|
+
import * as fs20 from "fs";
|
|
4387
|
+
import * as path39 from "path";
|
|
4320
4388
|
import { Command as Command48 } from "commander";
|
|
4321
4389
|
import { parse as yamlParse } from "yaml";
|
|
4322
4390
|
|
|
4323
4391
|
// src/registry/tarball.ts
|
|
4324
|
-
import * as
|
|
4325
|
-
import * as
|
|
4326
|
-
import * as
|
|
4392
|
+
import * as fs19 from "fs";
|
|
4393
|
+
import * as path38 from "path";
|
|
4394
|
+
import * as os3 from "os";
|
|
4327
4395
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
4328
4396
|
function extractTarball(tarballBuffer) {
|
|
4329
|
-
const tmpDir =
|
|
4330
|
-
const tarballPath =
|
|
4397
|
+
const tmpDir = fs19.mkdtempSync(path38.join(os3.tmpdir(), "harness-skill-install-"));
|
|
4398
|
+
const tarballPath = path38.join(tmpDir, "package.tgz");
|
|
4331
4399
|
try {
|
|
4332
|
-
|
|
4400
|
+
fs19.writeFileSync(tarballPath, tarballBuffer);
|
|
4333
4401
|
execFileSync5("tar", ["-xzf", tarballPath, "-C", tmpDir], {
|
|
4334
4402
|
timeout: 3e4
|
|
4335
4403
|
});
|
|
4336
|
-
|
|
4404
|
+
fs19.unlinkSync(tarballPath);
|
|
4337
4405
|
} catch (err) {
|
|
4338
4406
|
cleanupTempDir(tmpDir);
|
|
4339
4407
|
throw new Error(
|
|
@@ -4344,37 +4412,37 @@ function extractTarball(tarballBuffer) {
|
|
|
4344
4412
|
return tmpDir;
|
|
4345
4413
|
}
|
|
4346
4414
|
function placeSkillContent(extractedPkgDir, communityBaseDir, skillName, platforms) {
|
|
4347
|
-
const files =
|
|
4415
|
+
const files = fs19.readdirSync(extractedPkgDir);
|
|
4348
4416
|
for (const platform of platforms) {
|
|
4349
|
-
const targetDir =
|
|
4350
|
-
if (
|
|
4351
|
-
|
|
4417
|
+
const targetDir = path38.join(communityBaseDir, platform, skillName);
|
|
4418
|
+
if (fs19.existsSync(targetDir)) {
|
|
4419
|
+
fs19.rmSync(targetDir, { recursive: true, force: true });
|
|
4352
4420
|
}
|
|
4353
|
-
|
|
4421
|
+
fs19.mkdirSync(targetDir, { recursive: true });
|
|
4354
4422
|
for (const file of files) {
|
|
4355
4423
|
if (file === "package.json" || file === "node_modules") continue;
|
|
4356
|
-
const srcPath =
|
|
4357
|
-
const destPath =
|
|
4358
|
-
const stat =
|
|
4424
|
+
const srcPath = path38.join(extractedPkgDir, file);
|
|
4425
|
+
const destPath = path38.join(targetDir, file);
|
|
4426
|
+
const stat = fs19.statSync(srcPath);
|
|
4359
4427
|
if (stat.isDirectory()) {
|
|
4360
|
-
|
|
4428
|
+
fs19.cpSync(srcPath, destPath, { recursive: true });
|
|
4361
4429
|
} else {
|
|
4362
|
-
|
|
4430
|
+
fs19.copyFileSync(srcPath, destPath);
|
|
4363
4431
|
}
|
|
4364
4432
|
}
|
|
4365
4433
|
}
|
|
4366
4434
|
}
|
|
4367
4435
|
function removeSkillContent(communityBaseDir, skillName, platforms) {
|
|
4368
4436
|
for (const platform of platforms) {
|
|
4369
|
-
const targetDir =
|
|
4370
|
-
if (
|
|
4371
|
-
|
|
4437
|
+
const targetDir = path38.join(communityBaseDir, platform, skillName);
|
|
4438
|
+
if (fs19.existsSync(targetDir)) {
|
|
4439
|
+
fs19.rmSync(targetDir, { recursive: true, force: true });
|
|
4372
4440
|
}
|
|
4373
4441
|
}
|
|
4374
4442
|
}
|
|
4375
4443
|
function cleanupTempDir(dirPath) {
|
|
4376
4444
|
try {
|
|
4377
|
-
|
|
4445
|
+
fs19.rmSync(dirPath, { recursive: true, force: true });
|
|
4378
4446
|
} catch {
|
|
4379
4447
|
}
|
|
4380
4448
|
}
|
|
@@ -4407,44 +4475,111 @@ function resolveVersion(metadata, versionRange) {
|
|
|
4407
4475
|
return metadata.versions[matched];
|
|
4408
4476
|
}
|
|
4409
4477
|
function findDependentsOf(lockfile, targetPackageName) {
|
|
4410
|
-
const
|
|
4411
|
-
|
|
4412
|
-
|
|
4478
|
+
const dependents = [];
|
|
4479
|
+
const targetEntry = lockfile.skills[targetPackageName];
|
|
4480
|
+
if (targetEntry?.dependencyOf) {
|
|
4481
|
+
dependents.push(targetEntry.dependencyOf);
|
|
4482
|
+
}
|
|
4483
|
+
return dependents;
|
|
4413
4484
|
}
|
|
4414
4485
|
|
|
4415
4486
|
// src/commands/install.ts
|
|
4416
4487
|
function validateSkillYaml(parsed) {
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
if (typeof obj["name"] !== "string" || typeof obj["version"] !== "string" || !Array.isArray(obj["platforms"])) {
|
|
4422
|
-
throw new Error("contains invalid skill.yaml");
|
|
4488
|
+
const result = SkillMetadataSchema.safeParse(parsed);
|
|
4489
|
+
if (!result.success) {
|
|
4490
|
+
const issues = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
4491
|
+
throw new Error(`contains invalid skill.yaml: ${issues}`);
|
|
4423
4492
|
}
|
|
4424
4493
|
return {
|
|
4425
|
-
name:
|
|
4426
|
-
version:
|
|
4427
|
-
platforms:
|
|
4428
|
-
depends_on:
|
|
4494
|
+
name: result.data.name,
|
|
4495
|
+
version: result.data.version,
|
|
4496
|
+
platforms: result.data.platforms,
|
|
4497
|
+
depends_on: result.data.depends_on ?? []
|
|
4429
4498
|
};
|
|
4430
4499
|
}
|
|
4500
|
+
async function runLocalInstall(fromPath, options) {
|
|
4501
|
+
const resolvedPath = path39.resolve(fromPath);
|
|
4502
|
+
if (!fs20.existsSync(resolvedPath)) {
|
|
4503
|
+
throw new Error(`--from path does not exist: ${resolvedPath}`);
|
|
4504
|
+
}
|
|
4505
|
+
const stat = fs20.statSync(resolvedPath);
|
|
4506
|
+
let extractDir = null;
|
|
4507
|
+
let pkgDir;
|
|
4508
|
+
if (stat.isDirectory()) {
|
|
4509
|
+
pkgDir = resolvedPath;
|
|
4510
|
+
} else if (resolvedPath.endsWith(".tgz") || resolvedPath.endsWith(".tar.gz")) {
|
|
4511
|
+
const tarballBuffer = fs20.readFileSync(resolvedPath);
|
|
4512
|
+
extractDir = extractTarball(tarballBuffer);
|
|
4513
|
+
pkgDir = path39.join(extractDir, "package");
|
|
4514
|
+
} else {
|
|
4515
|
+
throw new Error(`--from path must be a directory or .tgz file. Got: ${resolvedPath}`);
|
|
4516
|
+
}
|
|
4517
|
+
try {
|
|
4518
|
+
const skillYamlPath = path39.join(pkgDir, "skill.yaml");
|
|
4519
|
+
if (!fs20.existsSync(skillYamlPath)) {
|
|
4520
|
+
throw new Error(`No skill.yaml found at ${skillYamlPath}`);
|
|
4521
|
+
}
|
|
4522
|
+
const rawYaml = fs20.readFileSync(skillYamlPath, "utf-8");
|
|
4523
|
+
const parsed = yamlParse(rawYaml);
|
|
4524
|
+
const skillYaml = validateSkillYaml(parsed);
|
|
4525
|
+
const shortName = skillYaml.name;
|
|
4526
|
+
const globalDir = resolveGlobalSkillsDir();
|
|
4527
|
+
const skillsDir = path39.dirname(globalDir);
|
|
4528
|
+
const communityBase = path39.join(skillsDir, "community");
|
|
4529
|
+
const lockfilePath = path39.join(communityBase, "skills-lock.json");
|
|
4530
|
+
const bundledNames = getBundledSkillNames(globalDir);
|
|
4531
|
+
if (bundledNames.has(shortName)) {
|
|
4532
|
+
throw new Error(
|
|
4533
|
+
`'${shortName}' is a bundled skill and cannot be overridden by community installs.`
|
|
4534
|
+
);
|
|
4535
|
+
}
|
|
4536
|
+
placeSkillContent(pkgDir, communityBase, shortName, skillYaml.platforms);
|
|
4537
|
+
const packageName = `@harness-skills/${shortName}`;
|
|
4538
|
+
const lockfile = readLockfile2(lockfilePath);
|
|
4539
|
+
const entry = {
|
|
4540
|
+
version: skillYaml.version,
|
|
4541
|
+
resolved: `local:${resolvedPath}`,
|
|
4542
|
+
integrity: "",
|
|
4543
|
+
platforms: skillYaml.platforms,
|
|
4544
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4545
|
+
dependencyOf: options._dependencyOf ?? null
|
|
4546
|
+
};
|
|
4547
|
+
const updatedLockfile = updateLockfileEntry(lockfile, packageName, entry);
|
|
4548
|
+
writeLockfile2(lockfilePath, updatedLockfile);
|
|
4549
|
+
return {
|
|
4550
|
+
installed: true,
|
|
4551
|
+
name: packageName,
|
|
4552
|
+
version: skillYaml.version
|
|
4553
|
+
};
|
|
4554
|
+
} finally {
|
|
4555
|
+
if (extractDir) {
|
|
4556
|
+
cleanupTempDir(extractDir);
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4431
4560
|
async function runInstall(skillName, options) {
|
|
4561
|
+
if (options.from && options.registry) {
|
|
4562
|
+
throw new Error("--from and --registry cannot be used together");
|
|
4563
|
+
}
|
|
4564
|
+
if (options.from) {
|
|
4565
|
+
return runLocalInstall(options.from, options);
|
|
4566
|
+
}
|
|
4432
4567
|
const packageName = resolvePackageName(skillName);
|
|
4433
4568
|
const shortName = extractSkillName(packageName);
|
|
4434
4569
|
const globalDir = resolveGlobalSkillsDir();
|
|
4435
|
-
const skillsDir =
|
|
4436
|
-
const communityBase =
|
|
4437
|
-
const lockfilePath =
|
|
4570
|
+
const skillsDir = path39.dirname(globalDir);
|
|
4571
|
+
const communityBase = path39.join(skillsDir, "community");
|
|
4572
|
+
const lockfilePath = path39.join(communityBase, "skills-lock.json");
|
|
4438
4573
|
const bundledNames = getBundledSkillNames(globalDir);
|
|
4439
4574
|
if (bundledNames.has(shortName)) {
|
|
4440
4575
|
throw new Error(
|
|
4441
4576
|
`'${shortName}' is a bundled skill and cannot be overridden by community installs.`
|
|
4442
4577
|
);
|
|
4443
4578
|
}
|
|
4444
|
-
const metadata = await fetchPackageMetadata(packageName);
|
|
4579
|
+
const metadata = await fetchPackageMetadata(packageName, options.registry);
|
|
4445
4580
|
const versionInfo = resolveVersion(metadata, options.version);
|
|
4446
4581
|
const resolvedVersion = versionInfo.version;
|
|
4447
|
-
const lockfile =
|
|
4582
|
+
const lockfile = readLockfile2(lockfilePath);
|
|
4448
4583
|
const existingEntry = lockfile.skills[packageName];
|
|
4449
4584
|
const previousVersion = existingEntry?.version;
|
|
4450
4585
|
if (existingEntry && existingEntry.version === resolvedVersion && !options.force) {
|
|
@@ -4455,16 +4590,17 @@ async function runInstall(skillName, options) {
|
|
|
4455
4590
|
version: resolvedVersion
|
|
4456
4591
|
};
|
|
4457
4592
|
}
|
|
4458
|
-
const
|
|
4593
|
+
const authToken = options.registry ? readNpmrcToken(options.registry) ?? void 0 : void 0;
|
|
4594
|
+
const tarballBuffer = await downloadTarball(versionInfo.dist.tarball, authToken);
|
|
4459
4595
|
const extractDir = extractTarball(tarballBuffer);
|
|
4460
4596
|
let skillYaml;
|
|
4461
4597
|
try {
|
|
4462
|
-
const extractedPkgDir =
|
|
4463
|
-
const skillYamlPath =
|
|
4464
|
-
if (!
|
|
4598
|
+
const extractedPkgDir = path39.join(extractDir, "package");
|
|
4599
|
+
const skillYamlPath = path39.join(extractedPkgDir, "skill.yaml");
|
|
4600
|
+
if (!fs20.existsSync(skillYamlPath)) {
|
|
4465
4601
|
throw new Error(`contains invalid skill.yaml: file not found in package`);
|
|
4466
4602
|
}
|
|
4467
|
-
const rawYaml =
|
|
4603
|
+
const rawYaml = fs20.readFileSync(skillYamlPath, "utf-8");
|
|
4468
4604
|
const parsed = yamlParse(rawYaml);
|
|
4469
4605
|
skillYaml = validateSkillYaml(parsed);
|
|
4470
4606
|
placeSkillContent(extractedPkgDir, communityBase, shortName, skillYaml.platforms);
|
|
@@ -4479,10 +4615,10 @@ async function runInstall(skillName, options) {
|
|
|
4479
4615
|
integrity: versionInfo.dist.integrity,
|
|
4480
4616
|
platforms: skillYaml.platforms,
|
|
4481
4617
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4482
|
-
dependencyOf: null
|
|
4618
|
+
dependencyOf: options._dependencyOf ?? null
|
|
4483
4619
|
};
|
|
4484
4620
|
let updatedLockfile = updateLockfileEntry(lockfile, packageName, entry);
|
|
4485
|
-
|
|
4621
|
+
writeLockfile2(lockfilePath, updatedLockfile);
|
|
4486
4622
|
const result = {
|
|
4487
4623
|
installed: true,
|
|
4488
4624
|
name: packageName,
|
|
@@ -4494,14 +4630,17 @@ async function runInstall(skillName, options) {
|
|
|
4494
4630
|
}
|
|
4495
4631
|
const deps = skillYaml.depends_on ?? [];
|
|
4496
4632
|
for (const dep of deps) {
|
|
4497
|
-
logger.info(`Installing dependency: ${dep}`);
|
|
4498
|
-
await runInstall(dep, {
|
|
4633
|
+
logger.info(`Installing dependency: ${dep} (required by ${shortName})`);
|
|
4634
|
+
await runInstall(dep, {
|
|
4635
|
+
_dependencyOf: packageName,
|
|
4636
|
+
...options.registry !== void 0 ? { registry: options.registry } : {}
|
|
4637
|
+
});
|
|
4499
4638
|
}
|
|
4500
4639
|
return result;
|
|
4501
4640
|
}
|
|
4502
4641
|
function createInstallCommand() {
|
|
4503
4642
|
const cmd = new Command48("install");
|
|
4504
|
-
cmd.description("Install a community skill from the @harness-skills registry").argument("<skill>", "Skill name or @harness-skills/scoped package name").option("--version <range>", "Semver range or exact version to install").option("--force", "Force reinstall even if same version is already installed").action(async (skill, opts) => {
|
|
4643
|
+
cmd.description("Install a community skill from the @harness-skills registry").argument("<skill>", "Skill name or @harness-skills/scoped package name").option("--version <range>", "Semver range or exact version to install").option("--force", "Force reinstall even if same version is already installed").option("--from <path>", "Install from a local directory or .tgz file").option("--registry <url>", "Use a custom npm registry URL").action(async (skill, opts) => {
|
|
4505
4644
|
try {
|
|
4506
4645
|
const result = await runInstall(skill, opts);
|
|
4507
4646
|
if (result.skipped) {
|
|
@@ -4523,17 +4662,377 @@ function createInstallCommand() {
|
|
|
4523
4662
|
return cmd;
|
|
4524
4663
|
}
|
|
4525
4664
|
|
|
4526
|
-
// src/commands/
|
|
4527
|
-
import * as
|
|
4665
|
+
// src/commands/install-constraints.ts
|
|
4666
|
+
import * as fs21 from "fs/promises";
|
|
4667
|
+
import * as path40 from "path";
|
|
4528
4668
|
import { Command as Command49 } from "commander";
|
|
4669
|
+
import semver3 from "semver";
|
|
4670
|
+
async function runInstallConstraints(options) {
|
|
4671
|
+
const { source, configPath, lockfilePath } = options;
|
|
4672
|
+
let rawBundle;
|
|
4673
|
+
try {
|
|
4674
|
+
rawBundle = await fs21.readFile(source, "utf-8");
|
|
4675
|
+
} catch (err) {
|
|
4676
|
+
if (isNodeError(err) && err.code === "ENOENT") {
|
|
4677
|
+
return { ok: false, error: `Bundle file not found: ${source}` };
|
|
4678
|
+
}
|
|
4679
|
+
return {
|
|
4680
|
+
ok: false,
|
|
4681
|
+
error: `Failed to read bundle: ${err instanceof Error ? err.message : String(err)}`
|
|
4682
|
+
};
|
|
4683
|
+
}
|
|
4684
|
+
let parsedJson;
|
|
4685
|
+
try {
|
|
4686
|
+
parsedJson = JSON.parse(rawBundle);
|
|
4687
|
+
} catch {
|
|
4688
|
+
return { ok: false, error: `Bundle file contains invalid JSON: ${source}` };
|
|
4689
|
+
}
|
|
4690
|
+
const bundleResult = BundleSchema.safeParse(parsedJson);
|
|
4691
|
+
if (!bundleResult.success) {
|
|
4692
|
+
const issues = bundleResult.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
4693
|
+
return { ok: false, error: `Bundle schema validation failed: ${issues}` };
|
|
4694
|
+
}
|
|
4695
|
+
const bundle = bundleResult.data;
|
|
4696
|
+
if (bundle.minHarnessVersion) {
|
|
4697
|
+
const installed = semver3.valid(semver3.coerce(CLI_VERSION));
|
|
4698
|
+
const required = semver3.valid(semver3.coerce(bundle.minHarnessVersion));
|
|
4699
|
+
if (installed && required && semver3.lt(installed, required)) {
|
|
4700
|
+
return {
|
|
4701
|
+
ok: false,
|
|
4702
|
+
error: `Bundle requires harness version >= ${bundle.minHarnessVersion}, but installed version is ${CLI_VERSION}. Please upgrade.`
|
|
4703
|
+
};
|
|
4704
|
+
}
|
|
4705
|
+
}
|
|
4706
|
+
const constraintKeys = Object.keys(bundle.constraints).filter(
|
|
4707
|
+
(k) => bundle.constraints[k] !== void 0
|
|
4708
|
+
);
|
|
4709
|
+
if (constraintKeys.length === 0) {
|
|
4710
|
+
return {
|
|
4711
|
+
ok: false,
|
|
4712
|
+
error: "Bundle contains no constraints. Nothing to install."
|
|
4713
|
+
};
|
|
4714
|
+
}
|
|
4715
|
+
let localConfig;
|
|
4716
|
+
try {
|
|
4717
|
+
const raw = await fs21.readFile(configPath, "utf-8");
|
|
4718
|
+
localConfig = JSON.parse(raw);
|
|
4719
|
+
} catch (err) {
|
|
4720
|
+
return {
|
|
4721
|
+
ok: false,
|
|
4722
|
+
error: `Failed to read local config at ${configPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
4723
|
+
};
|
|
4724
|
+
}
|
|
4725
|
+
const lockfileResult = await readLockfile(lockfilePath);
|
|
4726
|
+
if (!lockfileResult.ok) {
|
|
4727
|
+
return { ok: false, error: lockfileResult.error };
|
|
4728
|
+
}
|
|
4729
|
+
const existingLockfile = lockfileResult.value ?? {
|
|
4730
|
+
version: 1,
|
|
4731
|
+
packages: {}
|
|
4732
|
+
};
|
|
4733
|
+
const existingEntry = existingLockfile.packages[bundle.name];
|
|
4734
|
+
if (existingEntry && existingEntry.version === bundle.version) {
|
|
4735
|
+
return {
|
|
4736
|
+
ok: true,
|
|
4737
|
+
value: {
|
|
4738
|
+
installed: false,
|
|
4739
|
+
packageName: bundle.name,
|
|
4740
|
+
version: bundle.version,
|
|
4741
|
+
contributionsCount: 0,
|
|
4742
|
+
conflicts: [],
|
|
4743
|
+
alreadyInstalled: true
|
|
4744
|
+
}
|
|
4745
|
+
};
|
|
4746
|
+
}
|
|
4747
|
+
if (existingEntry) {
|
|
4748
|
+
const oldContributions = existingEntry.contributions ?? {};
|
|
4749
|
+
localConfig = removeContributions(localConfig, oldContributions);
|
|
4750
|
+
}
|
|
4751
|
+
const mergeResult = deepMergeConstraints(localConfig, bundle.constraints);
|
|
4752
|
+
if (mergeResult.conflicts.length > 0) {
|
|
4753
|
+
if (options.forceLocal) {
|
|
4754
|
+
} else if (options.forcePackage) {
|
|
4755
|
+
for (const conflict of mergeResult.conflicts) {
|
|
4756
|
+
applyPackageValue(mergeResult.config, conflict);
|
|
4757
|
+
addConflictContribution(mergeResult.contributions, conflict);
|
|
4758
|
+
}
|
|
4759
|
+
} else if (!options.dryRun) {
|
|
4760
|
+
return {
|
|
4761
|
+
ok: false,
|
|
4762
|
+
error: formatConflictsError(mergeResult.conflicts)
|
|
4763
|
+
};
|
|
4764
|
+
}
|
|
4765
|
+
}
|
|
4766
|
+
if (options.dryRun) {
|
|
4767
|
+
return {
|
|
4768
|
+
ok: true,
|
|
4769
|
+
value: {
|
|
4770
|
+
installed: false,
|
|
4771
|
+
packageName: bundle.name,
|
|
4772
|
+
version: bundle.version,
|
|
4773
|
+
contributionsCount: Object.keys(mergeResult.contributions).length,
|
|
4774
|
+
conflicts: mergeResult.conflicts,
|
|
4775
|
+
dryRun: true
|
|
4776
|
+
}
|
|
4777
|
+
};
|
|
4778
|
+
}
|
|
4779
|
+
const writeResult = await writeConfig(configPath, mergeResult.config);
|
|
4780
|
+
if (!writeResult.ok) {
|
|
4781
|
+
return {
|
|
4782
|
+
ok: false,
|
|
4783
|
+
error: `Failed to write config: ${writeResult.error instanceof Error ? writeResult.error.message : String(writeResult.error)}`
|
|
4784
|
+
};
|
|
4785
|
+
}
|
|
4786
|
+
const lockfileEntry = {
|
|
4787
|
+
version: bundle.version,
|
|
4788
|
+
source,
|
|
4789
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4790
|
+
contributions: mergeResult.contributions
|
|
4791
|
+
};
|
|
4792
|
+
const updatedLockfile = addProvenance(existingLockfile, bundle.name, lockfileEntry);
|
|
4793
|
+
const lockfileWriteResult = await writeLockfile(lockfilePath, updatedLockfile);
|
|
4794
|
+
if (!lockfileWriteResult.ok) {
|
|
4795
|
+
return {
|
|
4796
|
+
ok: false,
|
|
4797
|
+
error: `Config was written but lockfile write failed: ${lockfileWriteResult.error.message}. Lockfile may be out of sync.`
|
|
4798
|
+
};
|
|
4799
|
+
}
|
|
4800
|
+
return {
|
|
4801
|
+
ok: true,
|
|
4802
|
+
value: {
|
|
4803
|
+
installed: true,
|
|
4804
|
+
packageName: bundle.name,
|
|
4805
|
+
version: bundle.version,
|
|
4806
|
+
contributionsCount: Object.keys(mergeResult.contributions).length,
|
|
4807
|
+
conflicts: mergeResult.conflicts
|
|
4808
|
+
}
|
|
4809
|
+
};
|
|
4810
|
+
}
|
|
4811
|
+
var sectionAppliers = {
|
|
4812
|
+
layers(config, key, value) {
|
|
4813
|
+
const layers = config.layers;
|
|
4814
|
+
const idx = layers.findIndex((l) => l.name === key);
|
|
4815
|
+
if (idx >= 0) layers[idx] = value;
|
|
4816
|
+
},
|
|
4817
|
+
forbiddenImports(config, key, value) {
|
|
4818
|
+
const rules = config.forbiddenImports;
|
|
4819
|
+
const idx = rules.findIndex((r) => r.from === key);
|
|
4820
|
+
if (idx >= 0) rules[idx] = value;
|
|
4821
|
+
},
|
|
4822
|
+
"architecture.thresholds"(config, key, value) {
|
|
4823
|
+
const arch = config.architecture;
|
|
4824
|
+
if (arch?.thresholds) arch.thresholds[key] = value;
|
|
4825
|
+
},
|
|
4826
|
+
"architecture.modules"(config, key, value) {
|
|
4827
|
+
const arch = config.architecture;
|
|
4828
|
+
const [modulePath, category] = key.split(":");
|
|
4829
|
+
if (arch?.modules && modulePath && category && arch.modules[modulePath]) {
|
|
4830
|
+
arch.modules[modulePath][category] = value;
|
|
4831
|
+
}
|
|
4832
|
+
},
|
|
4833
|
+
"security.rules"(config, key, value) {
|
|
4834
|
+
const security = config.security;
|
|
4835
|
+
if (security?.rules) security.rules[key] = value;
|
|
4836
|
+
}
|
|
4837
|
+
};
|
|
4838
|
+
function applyPackageValue(config, conflict) {
|
|
4839
|
+
const applier = sectionAppliers[conflict.section];
|
|
4840
|
+
if (applier) applier(config, conflict.key, conflict.packageValue);
|
|
4841
|
+
}
|
|
4842
|
+
function addConflictContribution(contributions, conflict) {
|
|
4843
|
+
const section = conflict.section;
|
|
4844
|
+
const existing = contributions[section] ?? [];
|
|
4845
|
+
existing.push(conflict.key);
|
|
4846
|
+
contributions[section] = existing;
|
|
4847
|
+
}
|
|
4848
|
+
function formatConflictsError(conflicts) {
|
|
4849
|
+
const lines = [
|
|
4850
|
+
`${conflicts.length} conflict(s) detected. Resolve with --force-local or --force-package:`,
|
|
4851
|
+
""
|
|
4852
|
+
];
|
|
4853
|
+
for (const c of conflicts) {
|
|
4854
|
+
lines.push(` [${c.section}] ${c.key}: ${c.description}`);
|
|
4855
|
+
lines.push(` Local: ${JSON.stringify(c.localValue)}`);
|
|
4856
|
+
lines.push(` Package: ${JSON.stringify(c.packageValue)}`);
|
|
4857
|
+
lines.push("");
|
|
4858
|
+
}
|
|
4859
|
+
return lines.join("\n");
|
|
4860
|
+
}
|
|
4861
|
+
function isNodeError(err) {
|
|
4862
|
+
return err instanceof Error && "code" in err;
|
|
4863
|
+
}
|
|
4864
|
+
function resolveConfigPath(opts) {
|
|
4865
|
+
if (opts.config) return path40.resolve(opts.config);
|
|
4866
|
+
const found = findConfigFile();
|
|
4867
|
+
if (!found.ok) {
|
|
4868
|
+
logger.error(found.error.message);
|
|
4869
|
+
process.exit(1);
|
|
4870
|
+
}
|
|
4871
|
+
return found.value;
|
|
4872
|
+
}
|
|
4873
|
+
function logInstallResult(val, opts) {
|
|
4874
|
+
if (val.dryRun) {
|
|
4875
|
+
logger.info(`[dry-run] Would install ${val.packageName}@${val.version}`);
|
|
4876
|
+
logger.info(`[dry-run] ${val.contributionsCount} section(s) would be added`);
|
|
4877
|
+
if (val.conflicts.length > 0) {
|
|
4878
|
+
logger.warn(`[dry-run] ${val.conflicts.length} conflict(s) detected`);
|
|
4879
|
+
for (const c of val.conflicts) {
|
|
4880
|
+
logger.warn(` [${c.section}] ${c.key}: ${c.description}`);
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4883
|
+
return;
|
|
4884
|
+
}
|
|
4885
|
+
if (val.alreadyInstalled) {
|
|
4886
|
+
logger.info(`${val.packageName}@${val.version} is already installed. No changes made.`);
|
|
4887
|
+
return;
|
|
4888
|
+
}
|
|
4889
|
+
logger.success(
|
|
4890
|
+
`Installed ${val.packageName}@${val.version} (${val.contributionsCount} section(s) merged)`
|
|
4891
|
+
);
|
|
4892
|
+
if (val.conflicts.length > 0) {
|
|
4893
|
+
logger.warn(
|
|
4894
|
+
`${val.conflicts.length} conflict(s) resolved with ${opts.forceLocal ? "--force-local" : "--force-package"}`
|
|
4895
|
+
);
|
|
4896
|
+
}
|
|
4897
|
+
}
|
|
4898
|
+
async function handleInstallConstraints(source, opts) {
|
|
4899
|
+
const configPath = resolveConfigPath(opts);
|
|
4900
|
+
const projectRoot = path40.dirname(configPath);
|
|
4901
|
+
const lockfilePath = path40.join(projectRoot, ".harness", "constraints.lock.json");
|
|
4902
|
+
const resolvedSource = path40.resolve(source);
|
|
4903
|
+
if (opts.forceLocal && opts.forcePackage) {
|
|
4904
|
+
logger.error("Cannot use both --force-local and --force-package.");
|
|
4905
|
+
process.exit(1);
|
|
4906
|
+
}
|
|
4907
|
+
const result = await runInstallConstraints({
|
|
4908
|
+
source: resolvedSource,
|
|
4909
|
+
configPath,
|
|
4910
|
+
lockfilePath,
|
|
4911
|
+
...opts.forceLocal && { forceLocal: true },
|
|
4912
|
+
...opts.forcePackage && { forcePackage: true },
|
|
4913
|
+
...opts.dryRun && { dryRun: true }
|
|
4914
|
+
});
|
|
4915
|
+
if (!result.ok) {
|
|
4916
|
+
logger.error(result.error);
|
|
4917
|
+
process.exit(1);
|
|
4918
|
+
}
|
|
4919
|
+
logInstallResult(result.value, opts);
|
|
4920
|
+
}
|
|
4921
|
+
function createInstallConstraintsCommand() {
|
|
4922
|
+
const cmd = new Command49("install-constraints");
|
|
4923
|
+
cmd.description("Install a constraints bundle into the local harness config").argument("<source>", "Path to a .harness-constraints.json bundle file").option("--force-local", "Resolve all conflicts by keeping local values").option("--force-package", "Resolve all conflicts by using package values").option("--dry-run", "Show what would change without writing files").option("-c, --config <path>", "Path to harness.config.json").action(handleInstallConstraints);
|
|
4924
|
+
return cmd;
|
|
4925
|
+
}
|
|
4926
|
+
|
|
4927
|
+
// src/commands/uninstall-constraints.ts
|
|
4928
|
+
import * as fs22 from "fs/promises";
|
|
4929
|
+
import * as path41 from "path";
|
|
4930
|
+
import { Command as Command50 } from "commander";
|
|
4931
|
+
async function runUninstallConstraints(options) {
|
|
4932
|
+
const { packageName, configPath, lockfilePath } = options;
|
|
4933
|
+
const lockfileResult = await readLockfile(lockfilePath);
|
|
4934
|
+
if (!lockfileResult.ok) {
|
|
4935
|
+
return { ok: false, error: lockfileResult.error };
|
|
4936
|
+
}
|
|
4937
|
+
if (lockfileResult.value === null) {
|
|
4938
|
+
return { ok: false, error: "No lockfile found. No constraint packages are installed." };
|
|
4939
|
+
}
|
|
4940
|
+
const lockfile = lockfileResult.value;
|
|
4941
|
+
const entry = lockfile.packages[packageName];
|
|
4942
|
+
if (!entry) {
|
|
4943
|
+
return {
|
|
4944
|
+
ok: false,
|
|
4945
|
+
error: `Package '${packageName}' is not installed.`
|
|
4946
|
+
};
|
|
4947
|
+
}
|
|
4948
|
+
let localConfig;
|
|
4949
|
+
try {
|
|
4950
|
+
const raw = await fs22.readFile(configPath, "utf-8");
|
|
4951
|
+
localConfig = JSON.parse(raw);
|
|
4952
|
+
} catch (err) {
|
|
4953
|
+
return {
|
|
4954
|
+
ok: false,
|
|
4955
|
+
error: `Failed to read local config at ${configPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
4956
|
+
};
|
|
4957
|
+
}
|
|
4958
|
+
const contributions = entry.contributions ?? {};
|
|
4959
|
+
const sectionsRemoved = Object.keys(contributions);
|
|
4960
|
+
const updatedConfig = removeContributions(localConfig, contributions);
|
|
4961
|
+
const { lockfile: updatedLockfile } = removeProvenance(lockfile, packageName);
|
|
4962
|
+
const writeResult = await writeConfig(configPath, updatedConfig);
|
|
4963
|
+
if (!writeResult.ok) {
|
|
4964
|
+
return {
|
|
4965
|
+
ok: false,
|
|
4966
|
+
error: `Failed to write config: ${writeResult.error instanceof Error ? writeResult.error.message : String(writeResult.error)}`
|
|
4967
|
+
};
|
|
4968
|
+
}
|
|
4969
|
+
const lockfileWriteResult = await writeLockfile(lockfilePath, updatedLockfile);
|
|
4970
|
+
if (!lockfileWriteResult.ok) {
|
|
4971
|
+
return {
|
|
4972
|
+
ok: false,
|
|
4973
|
+
error: `Config was written but lockfile write failed: ${lockfileWriteResult.error.message}. Lockfile may be out of sync.`
|
|
4974
|
+
};
|
|
4975
|
+
}
|
|
4976
|
+
return {
|
|
4977
|
+
ok: true,
|
|
4978
|
+
value: {
|
|
4979
|
+
removed: true,
|
|
4980
|
+
packageName,
|
|
4981
|
+
version: entry.version,
|
|
4982
|
+
sectionsRemoved
|
|
4983
|
+
}
|
|
4984
|
+
};
|
|
4985
|
+
}
|
|
4986
|
+
function createUninstallConstraintsCommand() {
|
|
4987
|
+
const cmd = new Command50("uninstall-constraints");
|
|
4988
|
+
cmd.description("Remove a previously installed constraints package").argument("<name>", "Name of the constraint package to uninstall").option("-c, --config <path>", "Path to harness.config.json").action(async (name, opts) => {
|
|
4989
|
+
let configPath;
|
|
4990
|
+
if (opts.config) {
|
|
4991
|
+
configPath = path41.resolve(opts.config);
|
|
4992
|
+
} else {
|
|
4993
|
+
const found = findConfigFile();
|
|
4994
|
+
if (!found.ok) {
|
|
4995
|
+
logger.error(found.error.message);
|
|
4996
|
+
process.exit(1);
|
|
4997
|
+
}
|
|
4998
|
+
configPath = found.value;
|
|
4999
|
+
}
|
|
5000
|
+
const projectRoot = path41.dirname(configPath);
|
|
5001
|
+
const lockfilePath = path41.join(projectRoot, ".harness", "constraints.lock.json");
|
|
5002
|
+
const result = await runUninstallConstraints({
|
|
5003
|
+
packageName: name,
|
|
5004
|
+
configPath,
|
|
5005
|
+
lockfilePath
|
|
5006
|
+
});
|
|
5007
|
+
if (!result.ok) {
|
|
5008
|
+
logger.error(result.error);
|
|
5009
|
+
process.exit(1);
|
|
5010
|
+
}
|
|
5011
|
+
const val = result.value;
|
|
5012
|
+
if (val.sectionsRemoved.length === 0) {
|
|
5013
|
+
logger.success(
|
|
5014
|
+
`Removed ${val.packageName}@${val.version} (no contributed rules to remove)`
|
|
5015
|
+
);
|
|
5016
|
+
} else {
|
|
5017
|
+
logger.success(
|
|
5018
|
+
`Removed ${val.packageName}@${val.version} (${val.sectionsRemoved.length} section(s): ${val.sectionsRemoved.join(", ")})`
|
|
5019
|
+
);
|
|
5020
|
+
}
|
|
5021
|
+
});
|
|
5022
|
+
return cmd;
|
|
5023
|
+
}
|
|
5024
|
+
|
|
5025
|
+
// src/commands/uninstall.ts
|
|
5026
|
+
import * as path42 from "path";
|
|
5027
|
+
import { Command as Command51 } from "commander";
|
|
4529
5028
|
async function runUninstall(skillName, options) {
|
|
4530
5029
|
const packageName = resolvePackageName(skillName);
|
|
4531
5030
|
const shortName = extractSkillName(packageName);
|
|
4532
5031
|
const globalDir = resolveGlobalSkillsDir();
|
|
4533
|
-
const skillsDir =
|
|
4534
|
-
const communityBase =
|
|
4535
|
-
const lockfilePath =
|
|
4536
|
-
const lockfile =
|
|
5032
|
+
const skillsDir = path42.dirname(globalDir);
|
|
5033
|
+
const communityBase = path42.join(skillsDir, "community");
|
|
5034
|
+
const lockfilePath = path42.join(communityBase, "skills-lock.json");
|
|
5035
|
+
const lockfile = readLockfile2(lockfilePath);
|
|
4537
5036
|
const entry = lockfile.skills[packageName];
|
|
4538
5037
|
if (!entry) {
|
|
4539
5038
|
throw new Error(`Skill '${shortName}' is not installed.`);
|
|
@@ -4550,7 +5049,7 @@ async function runUninstall(skillName, options) {
|
|
|
4550
5049
|
}
|
|
4551
5050
|
removeSkillContent(communityBase, shortName, entry.platforms);
|
|
4552
5051
|
const updatedLockfile = removeLockfileEntry(lockfile, packageName);
|
|
4553
|
-
|
|
5052
|
+
writeLockfile2(lockfilePath, updatedLockfile);
|
|
4554
5053
|
const result = {
|
|
4555
5054
|
removed: true,
|
|
4556
5055
|
name: packageName,
|
|
@@ -4562,7 +5061,7 @@ async function runUninstall(skillName, options) {
|
|
|
4562
5061
|
return result;
|
|
4563
5062
|
}
|
|
4564
5063
|
function createUninstallCommand() {
|
|
4565
|
-
const cmd = new
|
|
5064
|
+
const cmd = new Command51("uninstall");
|
|
4566
5065
|
cmd.description("Uninstall a community skill").argument("<skill>", "Skill name or @harness-skills/scoped package name").option("--force", "Remove even if other skills depend on this one").action(async (skill, opts) => {
|
|
4567
5066
|
try {
|
|
4568
5067
|
const result = await runUninstall(skill, opts);
|
|
@@ -4581,13 +5080,13 @@ function createUninstallCommand() {
|
|
|
4581
5080
|
}
|
|
4582
5081
|
|
|
4583
5082
|
// src/commands/orchestrator.ts
|
|
4584
|
-
import { Command as
|
|
4585
|
-
import * as
|
|
5083
|
+
import { Command as Command52 } from "commander";
|
|
5084
|
+
import * as path43 from "path";
|
|
4586
5085
|
import { Orchestrator, WorkflowLoader, launchTUI } from "@harness-engineering/orchestrator";
|
|
4587
5086
|
function createOrchestratorCommand() {
|
|
4588
|
-
const orchestrator = new
|
|
5087
|
+
const orchestrator = new Command52("orchestrator");
|
|
4589
5088
|
orchestrator.command("run").description("Run the orchestrator daemon").option("-w, --workflow <path>", "Path to WORKFLOW.md", "WORKFLOW.md").action(async (opts) => {
|
|
4590
|
-
const workflowPath =
|
|
5089
|
+
const workflowPath = path43.resolve(process.cwd(), opts.workflow);
|
|
4591
5090
|
const loader = new WorkflowLoader();
|
|
4592
5091
|
const result = await loader.loadWorkflow(workflowPath);
|
|
4593
5092
|
if (!result.ok) {
|
|
@@ -4610,9 +5109,67 @@ function createOrchestratorCommand() {
|
|
|
4610
5109
|
return orchestrator;
|
|
4611
5110
|
}
|
|
4612
5111
|
|
|
5112
|
+
// src/commands/learnings/index.ts
|
|
5113
|
+
import { Command as Command54 } from "commander";
|
|
5114
|
+
|
|
5115
|
+
// src/commands/learnings/prune.ts
|
|
5116
|
+
import { Command as Command53 } from "commander";
|
|
5117
|
+
import * as path44 from "path";
|
|
5118
|
+
async function handlePrune(opts) {
|
|
5119
|
+
const projectPath = path44.resolve(opts.path);
|
|
5120
|
+
const result = await pruneLearnings(projectPath, opts.stream);
|
|
5121
|
+
if (!result.ok) {
|
|
5122
|
+
logger.error(result.error.message);
|
|
5123
|
+
process.exit(ExitCode.ERROR);
|
|
5124
|
+
return;
|
|
5125
|
+
}
|
|
5126
|
+
const { kept, archived, patterns } = result.value;
|
|
5127
|
+
if (archived === 0 && patterns.length === 0) {
|
|
5128
|
+
logger.info(`Nothing to prune. ${kept} learnings in file, all within retention window.`);
|
|
5129
|
+
process.exit(ExitCode.SUCCESS);
|
|
5130
|
+
return;
|
|
5131
|
+
}
|
|
5132
|
+
if (patterns.length > 0) {
|
|
5133
|
+
printPatternProposals(patterns);
|
|
5134
|
+
}
|
|
5135
|
+
if (archived > 0) {
|
|
5136
|
+
logger.success(`Pruned ${archived} entries. ${kept} most recent entries retained.`);
|
|
5137
|
+
logger.info("Archived entries written to .harness/learnings-archive/");
|
|
5138
|
+
} else {
|
|
5139
|
+
logger.info(`No entries archived. ${kept} entries retained.`);
|
|
5140
|
+
}
|
|
5141
|
+
process.exit(ExitCode.SUCCESS);
|
|
5142
|
+
}
|
|
5143
|
+
function printPatternProposals(patterns) {
|
|
5144
|
+
console.log("\n--- Improvement Proposals ---\n");
|
|
5145
|
+
for (const pattern of patterns) {
|
|
5146
|
+
console.log(` [${pattern.tag}] ${pattern.count} learnings with this theme.`);
|
|
5147
|
+
console.log(` Proposal: These learnings suggest a recurring pattern in "${pattern.tag}".`);
|
|
5148
|
+
console.log(
|
|
5149
|
+
` To add to roadmap: harness mcp manage_roadmap --action add --feature "<improvement>" --status planned
|
|
5150
|
+
`
|
|
5151
|
+
);
|
|
5152
|
+
}
|
|
5153
|
+
console.log(
|
|
5154
|
+
"Review the proposals above. If any warrant a process improvement, add them to the roadmap manually or via manage_roadmap.\n"
|
|
5155
|
+
);
|
|
5156
|
+
}
|
|
5157
|
+
function createPruneCommand() {
|
|
5158
|
+
return new Command53("prune").description(
|
|
5159
|
+
"Analyze global learnings for patterns, present improvement proposals, and archive old entries"
|
|
5160
|
+
).option("--path <path>", "Project root path", ".").option("--stream <name>", "Target a specific stream").action(handlePrune);
|
|
5161
|
+
}
|
|
5162
|
+
|
|
5163
|
+
// src/commands/learnings/index.ts
|
|
5164
|
+
function createLearningsCommand() {
|
|
5165
|
+
const command = new Command54("learnings").description("Learnings management commands");
|
|
5166
|
+
command.addCommand(createPruneCommand());
|
|
5167
|
+
return command;
|
|
5168
|
+
}
|
|
5169
|
+
|
|
4613
5170
|
// src/index.ts
|
|
4614
5171
|
function createProgram() {
|
|
4615
|
-
const program = new
|
|
5172
|
+
const program = new Command55();
|
|
4616
5173
|
program.name("harness").description("CLI for Harness Engineering toolkit").version(CLI_VERSION).option("-c, --config <path>", "Path to config file").option("--json", "Output as JSON").option("--verbose", "Verbose output").option("--quiet", "Minimal output");
|
|
4617
5174
|
program.addCommand(createValidateCommand());
|
|
4618
5175
|
program.addCommand(createCheckDepsCommand());
|
|
@@ -4629,6 +5186,7 @@ function createProgram() {
|
|
|
4629
5186
|
program.addCommand(createPersonaCommand());
|
|
4630
5187
|
program.addCommand(createSkillCommand());
|
|
4631
5188
|
program.addCommand(createStateCommand());
|
|
5189
|
+
program.addCommand(createLearningsCommand());
|
|
4632
5190
|
program.addCommand(createCheckPhaseGateCommand());
|
|
4633
5191
|
program.addCommand(createCreateSkillCommand());
|
|
4634
5192
|
program.addCommand(createSetupMcpCommand());
|
|
@@ -4647,6 +5205,8 @@ function createProgram() {
|
|
|
4647
5205
|
program.addCommand(createBlueprintCommand());
|
|
4648
5206
|
program.addCommand(createShareCommand());
|
|
4649
5207
|
program.addCommand(createInstallCommand());
|
|
5208
|
+
program.addCommand(createInstallConstraintsCommand());
|
|
5209
|
+
program.addCommand(createUninstallConstraintsCommand());
|
|
4650
5210
|
program.addCommand(createUninstallCommand());
|
|
4651
5211
|
program.addCommand(createOrchestratorCommand());
|
|
4652
5212
|
return program;
|
|
@@ -4662,6 +5222,8 @@ export {
|
|
|
4662
5222
|
runImpactPreview,
|
|
4663
5223
|
runCheckArch,
|
|
4664
5224
|
runInstall,
|
|
5225
|
+
runInstallConstraints,
|
|
5226
|
+
runUninstallConstraints,
|
|
4665
5227
|
runUninstall,
|
|
4666
5228
|
createProgram
|
|
4667
5229
|
};
|