@klhapp/skillmux 1.0.0 → 1.0.1
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/CHANGELOG.md +9 -0
- package/package.json +1 -1
- package/src/calibrate.ts +7 -59
- package/src/cli.ts +33 -815
- package/src/commands/config.ts +202 -0
- package/src/commands/core.ts +52 -0
- package/src/commands/project.ts +412 -0
- package/src/commands/shared.ts +45 -0
- package/src/commands/target.ts +110 -0
- package/src/config-mutation.ts +65 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { expandHome } from "../config";
|
|
2
|
+
import { planClientSurfaces, SUPPORTED_CLIENT_IDS } from "../init-clients";
|
|
3
|
+
import { planInitManifest, applyInit } from "../init";
|
|
4
|
+
import { writeManifestAtomic } from "../manifest";
|
|
5
|
+
import { emitSuccess } from "../output";
|
|
6
|
+
import { confirmIfNeeded, loadManifestContext } from "./shared";
|
|
7
|
+
export async function runTarget(
|
|
8
|
+
subCommand: string,
|
|
9
|
+
args: string[],
|
|
10
|
+
options: { isJson: boolean; dryRun: boolean },
|
|
11
|
+
): Promise<void> {
|
|
12
|
+
const { vaultPath, manifestPath, manifest } = await loadManifestContext();
|
|
13
|
+
|
|
14
|
+
if (subCommand === "list" || subCommand === "show") {
|
|
15
|
+
const names =
|
|
16
|
+
subCommand === "show" ? [args[0] ?? ""] : Object.keys(manifest.targets);
|
|
17
|
+
if (subCommand === "show" && !manifest.targets[names[0]!]) {
|
|
18
|
+
throw new Error(`target "${names[0]}" does not exist`);
|
|
19
|
+
}
|
|
20
|
+
const targets = names.map((name) => {
|
|
21
|
+
const target = manifest.targets[name]!;
|
|
22
|
+
const clients = SUPPORTED_CLIENT_IDS.filter((client) => {
|
|
23
|
+
const surface = planClientSurfaces([client]).surfaces[0];
|
|
24
|
+
return surface !== undefined && surface.path === expandHome(target.dir);
|
|
25
|
+
});
|
|
26
|
+
return { name, ...target, clients };
|
|
27
|
+
});
|
|
28
|
+
emitSuccess({ isJson: options.isJson }, { targets }, () => {
|
|
29
|
+
if (targets.length === 0) {
|
|
30
|
+
console.log("no targets configured");
|
|
31
|
+
} else {
|
|
32
|
+
for (const target of targets) {
|
|
33
|
+
console.log(`${target.name}:`);
|
|
34
|
+
console.log(` dir: ${target.dir}`);
|
|
35
|
+
console.log(` host: ${target.host ?? "(global)"}`);
|
|
36
|
+
console.log(` clients: ${target.clients.join(", ") || "(custom)"}`);
|
|
37
|
+
console.log(
|
|
38
|
+
` projects: ${target.project_groups.join(", ") || "(none)"}`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (subCommand === "add") {
|
|
47
|
+
const name = args[0];
|
|
48
|
+
const dirIndex = args.indexOf("--dir");
|
|
49
|
+
const rawPath = dirIndex === -1 ? undefined : args[dirIndex + 1];
|
|
50
|
+
if (!name || !rawPath)
|
|
51
|
+
throw new Error("usage: skillmux target add <name> --dir <dir> --yes");
|
|
52
|
+
const path = expandHome(rawPath);
|
|
53
|
+
if (options.dryRun) {
|
|
54
|
+
const planned = planInitManifest(vaultPath, [{ name, dir: path }], []);
|
|
55
|
+
emitSuccess(
|
|
56
|
+
{ isJson: options.isJson },
|
|
57
|
+
{ target: planned.targets[name] },
|
|
58
|
+
() => console.log(`target add: ${name} -> ${path} (dry-run)`),
|
|
59
|
+
);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (
|
|
63
|
+
!(await confirmIfNeeded({
|
|
64
|
+
confirmed: args.includes("--yes"),
|
|
65
|
+
isJson: options.isJson,
|
|
66
|
+
prompt: `Adopt target ${name} at ${path}?`,
|
|
67
|
+
nonInteractiveError:
|
|
68
|
+
"skillmux target add requires --yes when run non-interactively",
|
|
69
|
+
}))
|
|
70
|
+
)
|
|
71
|
+
return;
|
|
72
|
+
applyInit(vaultPath, [{ name, dir: path }]);
|
|
73
|
+
console.log(`target "${name}" added at ${path}`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (subCommand === "remove") {
|
|
78
|
+
const name = args[0];
|
|
79
|
+
if (!name || !manifest.targets[name]) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
name
|
|
82
|
+
? `target "${name}" does not exist`
|
|
83
|
+
: "usage: skillmux target remove <name> --yes",
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (options.dryRun) {
|
|
87
|
+
console.log(`target remove: ${name} (files preserved, dry-run)`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (
|
|
91
|
+
!(await confirmIfNeeded({
|
|
92
|
+
confirmed: args.includes("--yes"),
|
|
93
|
+
isJson: options.isJson,
|
|
94
|
+
prompt: `Remove target ${name} from the manifest and preserve its files?`,
|
|
95
|
+
nonInteractiveError:
|
|
96
|
+
"skillmux target remove requires --yes when run non-interactively",
|
|
97
|
+
}))
|
|
98
|
+
)
|
|
99
|
+
return;
|
|
100
|
+
const targets = { ...manifest.targets };
|
|
101
|
+
delete targets[name];
|
|
102
|
+
writeManifestAtomic(manifestPath, { ...manifest, targets });
|
|
103
|
+
console.log(
|
|
104
|
+
`target "${name}" removed from the manifest; files preserved at ${manifest.targets[name]!.dir}`,
|
|
105
|
+
);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
throw new Error("usage: skillmux target <list|show|add|remove>");
|
|
110
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { writeFileSync, renameSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
export interface ThresholdsPatchOptions {
|
|
4
|
+
matchScore: number;
|
|
5
|
+
matchMargin: number;
|
|
6
|
+
candidateFloor: number;
|
|
7
|
+
runId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Remove a TOML section header and all lines until the next section header
|
|
12
|
+
* (or end of file). Matches exact header string at start of line.
|
|
13
|
+
*/
|
|
14
|
+
export function removeSectionBlock(source: string, header: string): string {
|
|
15
|
+
const lines = source.split("\n");
|
|
16
|
+
const out: string[] = [];
|
|
17
|
+
let skipping = false;
|
|
18
|
+
for (const line of lines) {
|
|
19
|
+
const trimmed = line.trimEnd();
|
|
20
|
+
if (trimmed === header || trimmed.startsWith(header + " ")) {
|
|
21
|
+
skipping = true;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (skipping && line.trimStart().startsWith("[")) {
|
|
25
|
+
skipping = false;
|
|
26
|
+
}
|
|
27
|
+
if (!skipping) out.push(line);
|
|
28
|
+
}
|
|
29
|
+
return out.join("\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Surgically patch a TOML string to set [inference.thresholds] values and
|
|
34
|
+
* [inference.calibration] run_id while preserving unrelated sections and comments.
|
|
35
|
+
*/
|
|
36
|
+
export function patchToml(
|
|
37
|
+
source: string,
|
|
38
|
+
opts: ThresholdsPatchOptions,
|
|
39
|
+
): string {
|
|
40
|
+
const thresholdsBlock = `[inference.thresholds]\nmatch_score = ${opts.matchScore}\nmatch_margin = ${opts.matchMargin}\ncandidate_floor = ${opts.candidateFloor}\n`;
|
|
41
|
+
const calibrationBlock = `[inference.calibration]\nrun_id = "${opts.runId}"\n`;
|
|
42
|
+
|
|
43
|
+
// Remove any existing [inference.thresholds] and [inference.calibration] sections
|
|
44
|
+
let result = removeSectionBlock(source, "[inference.thresholds]");
|
|
45
|
+
result = removeSectionBlock(result, "[inference.calibration]");
|
|
46
|
+
|
|
47
|
+
// Append both sections cleanly
|
|
48
|
+
result = result.trimEnd() + "\n\n" + thresholdsBlock + "\n" + calibrationBlock;
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Atomically write patched TOML to disk via temp file write and renameSync.
|
|
54
|
+
*/
|
|
55
|
+
export async function patchTomlFile(
|
|
56
|
+
tomlPath: string,
|
|
57
|
+
opts: ThresholdsPatchOptions,
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
const existing = await Bun.file(tomlPath).text();
|
|
60
|
+
const patched = patchToml(existing, opts);
|
|
61
|
+
|
|
62
|
+
const tmpPath = `${tomlPath}.${process.pid}.tmp`;
|
|
63
|
+
writeFileSync(tmpPath, patched);
|
|
64
|
+
renameSync(tmpPath, tomlPath);
|
|
65
|
+
}
|