@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.
@@ -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
+ }