@klhapp/skillmux 1.12.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/CHANGELOG.md +8 -0
- package/README.md +18 -9
- package/bin/skillmux.js +89 -0
- package/docs/README.md +4 -4
- package/docs/cli.md +14 -4
- package/docs/concepts.md +2 -2
- package/docs/configuration.md +18 -3
- package/docs/deployment.md +3 -3
- package/docs/getting-started.md +32 -7
- package/docs/releasing.md +17 -2
- package/docs/sdd/cli-remote-target-parity/schema-bucket-b.yaml +297 -0
- package/docs/sdd/cli-remote-target-parity/spec-bucket-b.md +95 -0
- package/docs/sdd/cli-remote-target-parity/spec.md +111 -0
- package/docs/sdd/cli-remote-target-parity/think.md +183 -0
- package/docs/sdd/cli-surface-consistency/think.md +149 -0
- package/docs/sdd/config-authority-rerank-resilience/plan.md +338 -0
- package/docs/sdd/config-authority-rerank-resilience/review.md +124 -0
- package/docs/sdd/config-authority-rerank-resilience/spec.md +78 -0
- package/docs/sdd/feat-packaging-deployment/review.md +95 -0
- package/docs/sdd/fetch-outcome-flywheel/review.md +103 -0
- package/docs/sdd/fetch-outcome-flywheel/schema.json +347 -0
- package/docs/sdd/fetch-outcome-flywheel/spec.md +185 -0
- package/docs/sdd/npm-native-binaries/review.md +103 -0
- package/docs/sdd/npm-native-binaries/spec.md +148 -0
- package/docs/sdd/ranked-shortlist-2/pr3-calibration-removal.md +56 -0
- package/docs/sdd/refactor-builtin-target-paths/spec.md +41 -0
- package/docs/sdd/runtime-resource-hardening/review.md +86 -0
- package/docs/sdd/runtime-resource-hardening/spec.md +43 -0
- package/docs/sdd/security-hardening-phase/think.md +129 -0
- package/docs/sdd/security-observability/review.md +96 -0
- package/docs/sdd/security-observability/spec.md +55 -0
- package/docs/sdd/skill-provenance-update/review.md +152 -0
- package/docs/sdd/skill-provenance-update/schema.json +193 -0
- package/docs/sdd/skill-provenance-update/spec.md +134 -0
- package/docs/sdd/supply-chain-hardening/review.md +50 -0
- package/docs/sdd/supply-chain-hardening/spec.md +42 -0
- package/docs/sdd/target-marker-rehome/spec.md +33 -0
- package/docs/troubleshooting.md +18 -2
- package/package.json +15 -8
- package/src/adapters.ts +0 -438
- package/src/audit.ts +0 -21
- package/src/cli.ts +0 -928
- package/src/clients.ts +0 -368
- package/src/commands/audit.ts +0 -85
- package/src/commands/config.ts +0 -224
- package/src/commands/context.ts +0 -104
- package/src/commands/core.ts +0 -56
- package/src/commands/doctor.ts +0 -97
- package/src/commands/eval.ts +0 -88
- package/src/commands/init.ts +0 -672
- package/src/commands/install.ts +0 -155
- package/src/commands/local-vault.ts +0 -60
- package/src/commands/models.ts +0 -10
- package/src/commands/outdated.ts +0 -115
- package/src/commands/project.ts +0 -580
- package/src/commands/report.ts +0 -66
- package/src/commands/scan.ts +0 -71
- package/src/commands/shared.ts +0 -38
- package/src/commands/skill.ts +0 -33
- package/src/commands/sync.ts +0 -233
- package/src/commands/target.ts +0 -225
- package/src/commands/update.ts +0 -277
- package/src/completions.ts +0 -196
- package/src/concurrency-limiter.ts +0 -61
- package/src/config-service.ts +0 -398
- package/src/config-watcher.ts +0 -253
- package/src/config.ts +0 -583
- package/src/context.ts +0 -188
- package/src/db-audit.ts +0 -286
- package/src/db-index.ts +0 -238
- package/src/db.ts +0 -3
- package/src/deployment.ts +0 -39
- package/src/doctor.ts +0 -205
- package/src/eval.ts +0 -302
- package/src/global-flags.ts +0 -46
- package/src/init-agents.ts +0 -329
- package/src/init-instructions.ts +0 -192
- package/src/init.ts +0 -390
- package/src/install.ts +0 -232
- package/src/lifecycle.ts +0 -51
- package/src/logger.ts +0 -26
- package/src/manifest.ts +0 -359
- package/src/mcp-registration.ts +0 -89
- package/src/metrics.ts +0 -121
- package/src/models.ts +0 -20
- package/src/output.ts +0 -214
- package/src/project-setup.ts +0 -36
- package/src/prompts.ts +0 -124
- package/src/provenance.ts +0 -99
- package/src/rate-limiter.ts +0 -137
- package/src/readiness.ts +0 -30
- package/src/redact.ts +0 -52
- package/src/router-core.ts +0 -666
- package/src/rrf.ts +0 -31
- package/src/scan.ts +0 -300
- package/src/server.ts +0 -930
- package/src/setup.ts +0 -145
- package/src/snapshot.ts +0 -135
- package/src/stats.ts +0 -340
- package/src/sync.ts +0 -482
- package/src/toml-writer.ts +0 -51
- package/src/types.ts +0 -212
- package/src/vault.ts +0 -207
package/src/commands/scan.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { expandHome, loadConfig } from "../config";
|
|
2
|
-
import { emitSuccess, warn } from "../output";
|
|
3
|
-
import {
|
|
4
|
-
parseFailOn,
|
|
5
|
-
renderScanJson,
|
|
6
|
-
renderScanText,
|
|
7
|
-
scanExitCode,
|
|
8
|
-
scanPath,
|
|
9
|
-
type ScanSeverity,
|
|
10
|
-
} from "../scan";
|
|
11
|
-
import { isGlobalFlag } from "../global-flags";
|
|
12
|
-
|
|
13
|
-
function parseScanArgs(args: string[]): {
|
|
14
|
-
path?: string;
|
|
15
|
-
format: "text" | "json";
|
|
16
|
-
formatExplicit: boolean;
|
|
17
|
-
failOn?: ScanSeverity;
|
|
18
|
-
} {
|
|
19
|
-
let path: string | undefined;
|
|
20
|
-
let format: "text" | "json" = "text";
|
|
21
|
-
let formatExplicit = false;
|
|
22
|
-
let failOn: ScanSeverity | undefined;
|
|
23
|
-
for (let i = 0; i < args.length; i++) {
|
|
24
|
-
const option = args[i];
|
|
25
|
-
if (option === "--format") {
|
|
26
|
-
const value = args[++i];
|
|
27
|
-
if (value !== "text" && value !== "json")
|
|
28
|
-
throw new Error("--format must be text or json");
|
|
29
|
-
format = value;
|
|
30
|
-
formatExplicit = true;
|
|
31
|
-
} else if (option === "--fail-on") {
|
|
32
|
-
// scan accepts "none" for symmetry with install/update, where it is the
|
|
33
|
-
// opt-out. Here it is already the default: scan reports, and the caller
|
|
34
|
-
// opts into a non-zero exit code.
|
|
35
|
-
const parsed = parseFailOn(args[++i]);
|
|
36
|
-
failOn = parsed === "none" ? undefined : parsed;
|
|
37
|
-
} else if (isGlobalFlag(option, "--json")) {
|
|
38
|
-
// handled globally by main()'s isJson flag; recognized here so it isn't rejected
|
|
39
|
-
} else if (option?.startsWith("--")) {
|
|
40
|
-
throw new Error(`unknown scan option: ${option}`);
|
|
41
|
-
} else if (path !== undefined) {
|
|
42
|
-
throw new Error("skillmux scan accepts at most one <path> argument");
|
|
43
|
-
} else {
|
|
44
|
-
path = option;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return { path, format, formatExplicit, failOn };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function runScan(
|
|
51
|
-
args: string[],
|
|
52
|
-
options: { isJson: boolean },
|
|
53
|
-
): Promise<void> {
|
|
54
|
-
const { path, format, formatExplicit, failOn } = parseScanArgs(args);
|
|
55
|
-
if (formatExplicit) {
|
|
56
|
-
// --format predates the shared --json envelope and is the last command
|
|
57
|
-
// flag that emits JSON outside it. Kept working for existing callers;
|
|
58
|
-
// the warning goes to stderr so stdout stays machine-parseable.
|
|
59
|
-
warn("--format is deprecated and will be removed in a future 1.x release; use --json instead");
|
|
60
|
-
}
|
|
61
|
-
const rootPath = path
|
|
62
|
-
? expandHome(path)
|
|
63
|
-
: expandHome((await loadConfig()).vault_path);
|
|
64
|
-
const result = await scanPath(rootPath);
|
|
65
|
-
emitSuccess({ isJson: options.isJson }, result, () => {
|
|
66
|
-
console.log(
|
|
67
|
-
format === "json" ? renderScanJson(result) : renderScanText(result),
|
|
68
|
-
);
|
|
69
|
-
});
|
|
70
|
-
process.exitCode = scanExitCode(result.findings, failOn);
|
|
71
|
-
}
|
package/src/commands/shared.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { expandHome, loadConfig } from "../config";
|
|
2
|
-
import { parseManifest, resolveManifestPath } from "../manifest";
|
|
3
|
-
import { isInteractive } from "../output";
|
|
4
|
-
import { askQuestion, type PromptIO } from "../prompts";
|
|
5
|
-
|
|
6
|
-
export async function confirmAction(
|
|
7
|
-
prompt: string,
|
|
8
|
-
io: PromptIO = {},
|
|
9
|
-
): Promise<boolean> {
|
|
10
|
-
const answer = (await askQuestion(`${prompt} [y/N] `, io)).trim().toLowerCase();
|
|
11
|
-
return answer === "y" || answer === "yes";
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function loadManifestContext() {
|
|
15
|
-
const config = await loadConfig();
|
|
16
|
-
const vaultPath = expandHome(config.vault_path);
|
|
17
|
-
const manifestPath = resolveManifestPath(vaultPath);
|
|
18
|
-
if (!manifestPath) {
|
|
19
|
-
throw new Error(
|
|
20
|
-
`no skillmux.toml found at ${vaultPath}; run skillmux init first`,
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
const manifest = parseManifest(await Bun.file(manifestPath).text());
|
|
24
|
-
return { config, vaultPath, manifestPath, manifest };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function confirmIfNeeded(opts: {
|
|
28
|
-
confirmed: boolean;
|
|
29
|
-
isJson: boolean;
|
|
30
|
-
prompt: string;
|
|
31
|
-
nonInteractiveError: string;
|
|
32
|
-
}): Promise<boolean> {
|
|
33
|
-
if (opts.confirmed) return true;
|
|
34
|
-
if (opts.isJson || !isInteractive()) {
|
|
35
|
-
throw new Error(opts.nonInteractiveError);
|
|
36
|
-
}
|
|
37
|
-
return confirmAction(opts.prompt);
|
|
38
|
-
}
|
package/src/commands/skill.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { expandHome, loadConfig } from "../config";
|
|
4
|
-
import { unknownSubcommandError } from "../output";
|
|
5
|
-
import { vaultResolutionOrder } from "../vault";
|
|
6
|
-
|
|
7
|
-
export async function runSkill(subCommand: string, args: string[]): Promise<void> {
|
|
8
|
-
if (subCommand !== "which") throw unknownSubcommandError("skill", subCommand, ["which"]);
|
|
9
|
-
await runWhich(args);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async function runWhich(args: string[]): Promise<void> {
|
|
13
|
-
const skillId = args[0];
|
|
14
|
-
if (!skillId) {
|
|
15
|
-
throw new Error(
|
|
16
|
-
"usage: skillmux skill which <skill_id> (local vault shadow resolution; unrelated to MCP routing)",
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
const config = await loadConfig();
|
|
20
|
-
const vaultPath = expandHome(config.vault_path);
|
|
21
|
-
const localVaultPaths = config.local_vault_paths.map(expandHome);
|
|
22
|
-
const roots = vaultResolutionOrder(vaultPath, localVaultPaths).filter(
|
|
23
|
-
(root) => existsSync(join(root, skillId, "SKILL.md")),
|
|
24
|
-
);
|
|
25
|
-
if (roots.length === 0) {
|
|
26
|
-
console.log(`${skillId}: not found in vault_path or local_vault_paths`);
|
|
27
|
-
process.exitCode = 1;
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
console.log(`${skillId}: serving from ${roots[0]}`);
|
|
31
|
-
for (const shadowedRoot of roots.slice(1))
|
|
32
|
-
console.log(` shadows: ${shadowedRoot}`);
|
|
33
|
-
}
|
package/src/commands/sync.ts
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { hostname } from "node:os";
|
|
3
|
-
import { expandHome, loadConfig } from "../config";
|
|
4
|
-
import {
|
|
5
|
-
parseManifest,
|
|
6
|
-
resolveManifestPath,
|
|
7
|
-
validateManifest,
|
|
8
|
-
resolveTargetDir,
|
|
9
|
-
} from "../manifest";
|
|
10
|
-
import { emitSuccess, isInteractive, warn } from "../output";
|
|
11
|
-
import {
|
|
12
|
-
installPostMergeHook,
|
|
13
|
-
resolveProjectPinDir,
|
|
14
|
-
restoreMonolith as restoreMonolithTarget,
|
|
15
|
-
syncProjectTargets,
|
|
16
|
-
syncTarget,
|
|
17
|
-
type ProjectGroupInput,
|
|
18
|
-
} from "../sync";
|
|
19
|
-
import { confirmAction } from "./shared";
|
|
20
|
-
|
|
21
|
-
function parseSyncArgs(args: string[]): {
|
|
22
|
-
dryRun: boolean;
|
|
23
|
-
restoreMonolith: boolean;
|
|
24
|
-
installHook: boolean;
|
|
25
|
-
yes: boolean;
|
|
26
|
-
isJson: boolean;
|
|
27
|
-
} {
|
|
28
|
-
let dryRun = false;
|
|
29
|
-
let restoreMonolith = false;
|
|
30
|
-
let installHook = false;
|
|
31
|
-
let yes = false;
|
|
32
|
-
let isJson = false;
|
|
33
|
-
for (const arg of args) {
|
|
34
|
-
if (arg === "--dry-run") dryRun = true;
|
|
35
|
-
else if (arg === "--restore-monolith") restoreMonolith = true;
|
|
36
|
-
else if (arg === "--install-hook") installHook = true;
|
|
37
|
-
else if (arg === "--yes") yes = true;
|
|
38
|
-
else if (arg === "--json") isJson = true;
|
|
39
|
-
else throw new Error(`unknown sync option: ${arg}`);
|
|
40
|
-
}
|
|
41
|
-
return { dryRun, restoreMonolith, installHook, yes, isJson };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* A target directory that doesn't exist yet is about to be created by `sync`.
|
|
46
|
-
* `manifest.targets[*].dir` is vault content — readable and writable by whatever
|
|
47
|
-
* populated the vault (a shared git-backed vault pulled in, or a hand-edit) — and
|
|
48
|
-
* `sync` can run unattended via the `--install-hook` post-merge hook. Without this
|
|
49
|
-
* gate, a tampered manifest naming a brand-new path gets that directory silently
|
|
50
|
-
* created (and populated with symlinks) the next time anyone pulls. Creation for
|
|
51
|
-
* an as-yet-unseen directory therefore requires either `--yes` or an interactive
|
|
52
|
-
* confirmation; once the directory exists, later syncs never hit this path again.
|
|
53
|
-
*/
|
|
54
|
-
async function confirmNewSyncTarget(
|
|
55
|
-
label: string,
|
|
56
|
-
dir: string,
|
|
57
|
-
yes: boolean,
|
|
58
|
-
isJson: boolean,
|
|
59
|
-
): Promise<boolean> {
|
|
60
|
-
if (yes) return true;
|
|
61
|
-
if (!isInteractive()) {
|
|
62
|
-
if (!isJson) {
|
|
63
|
-
console.log(
|
|
64
|
-
`${label}: skipped — ${dir} does not exist yet; creating it requires approval. Re-run "skillmux sync --yes", or run "skillmux sync" interactively, once you've confirmed this target is expected.`,
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
return confirmAction(`${label}: create new target directory ${dir}?`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
interface SyncTargetSummary {
|
|
73
|
-
target: string;
|
|
74
|
-
status:
|
|
75
|
-
| "synced"
|
|
76
|
-
| "skipped_host_mismatch"
|
|
77
|
-
| "restored"
|
|
78
|
-
| "not_owned"
|
|
79
|
-
| "skipped_not_approved";
|
|
80
|
-
added?: string[];
|
|
81
|
-
removed?: string[];
|
|
82
|
-
skipped?: string[];
|
|
83
|
-
projects?: {
|
|
84
|
-
group: string;
|
|
85
|
-
pin_dir: string;
|
|
86
|
-
added: string[];
|
|
87
|
-
removed: string[];
|
|
88
|
-
skipped: string[];
|
|
89
|
-
}[];
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export async function runSync(args: string[]): Promise<void> {
|
|
93
|
-
const { dryRun, restoreMonolith, installHook, yes, isJson } = parseSyncArgs(args);
|
|
94
|
-
const config = await loadConfig();
|
|
95
|
-
const vaultPath = expandHome(config.vault_path);
|
|
96
|
-
const log = (line: string) => {
|
|
97
|
-
if (!isJson) console.log(line);
|
|
98
|
-
};
|
|
99
|
-
const warnLine = (line: string) => {
|
|
100
|
-
if (!isJson) warn(line);
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
let hookInstalled: boolean | undefined;
|
|
104
|
-
if (installHook) {
|
|
105
|
-
const result = installPostMergeHook(vaultPath);
|
|
106
|
-
hookInstalled = result.installed;
|
|
107
|
-
log(result.installed ? "installed post-merge hook" : "post-merge hook already installed");
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const manifestPath = resolveManifestPath(vaultPath);
|
|
111
|
-
if (!manifestPath) {
|
|
112
|
-
emitSuccess({ isJson }, { hook_installed: hookInstalled ?? null, targets: [] }, () =>
|
|
113
|
-
console.log("no skillmux.toml found at vault root — nothing to sync"),
|
|
114
|
-
);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const manifest = parseManifest(await Bun.file(manifestPath).text());
|
|
119
|
-
const localVaultPaths = config.local_vault_paths.map(expandHome);
|
|
120
|
-
const { notes } = validateManifest(manifest, vaultPath, localVaultPaths);
|
|
121
|
-
for (const note of notes) log(`note: ${note}`);
|
|
122
|
-
|
|
123
|
-
const currentHost = hostname();
|
|
124
|
-
const targetSummaries: SyncTargetSummary[] = [];
|
|
125
|
-
for (const [targetName, target] of Object.entries(manifest.targets)) {
|
|
126
|
-
if (target.host !== undefined && target.host !== currentHost) {
|
|
127
|
-
log(
|
|
128
|
-
`${targetName}: skipped (host ${target.host} does not match current host ${currentHost})`,
|
|
129
|
-
);
|
|
130
|
-
targetSummaries.push({ target: targetName, status: "skipped_host_mismatch" });
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
const targetDir = resolveTargetDir(targetName, target);
|
|
134
|
-
|
|
135
|
-
if (restoreMonolith) {
|
|
136
|
-
const result = restoreMonolithTarget(targetDir, vaultPath);
|
|
137
|
-
log(
|
|
138
|
-
result.restored
|
|
139
|
-
? `${targetName}: restored to a vault symlink`
|
|
140
|
-
: `${targetName}: not owned by skillmux, left untouched`,
|
|
141
|
-
);
|
|
142
|
-
targetSummaries.push({
|
|
143
|
-
target: targetName,
|
|
144
|
-
status: result.restored ? "restored" : "not_owned",
|
|
145
|
-
});
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (!dryRun && !existsSync(targetDir)) {
|
|
150
|
-
const approved = await confirmNewSyncTarget(targetName, targetDir, yes, isJson);
|
|
151
|
-
if (!approved) {
|
|
152
|
-
if (isInteractive()) {
|
|
153
|
-
log(`${targetName}: skipped — creating ${targetDir} was not approved`);
|
|
154
|
-
}
|
|
155
|
-
targetSummaries.push({ target: targetName, status: "skipped_not_approved" });
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const suffix = dryRun ? " (dry-run)" : "";
|
|
161
|
-
const result = syncTarget(
|
|
162
|
-
{
|
|
163
|
-
vaultPath,
|
|
164
|
-
targetDir,
|
|
165
|
-
targetName,
|
|
166
|
-
coreSkillIds: manifest.core.skills,
|
|
167
|
-
localVaultPaths,
|
|
168
|
-
},
|
|
169
|
-
{ dryRun },
|
|
170
|
-
);
|
|
171
|
-
log(`${targetName}: +${result.added.length} -${result.removed.length}${suffix}`);
|
|
172
|
-
if (result.skipped.length > 0) {
|
|
173
|
-
warnLine(`refused to sync ${result.skipped.join(", ")} — skill directory contains a symlink`);
|
|
174
|
-
}
|
|
175
|
-
const summary: SyncTargetSummary = {
|
|
176
|
-
target: targetName,
|
|
177
|
-
status: "synced",
|
|
178
|
-
added: result.added,
|
|
179
|
-
removed: result.removed,
|
|
180
|
-
skipped: result.skipped,
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
if (target.project_groups.length > 0) {
|
|
184
|
-
const allGroups = manifest.project ?? {};
|
|
185
|
-
const projectGroups: Record<string, ProjectGroupInput> = {};
|
|
186
|
-
for (const groupName of target.project_groups) {
|
|
187
|
-
const group = allGroups[groupName]!;
|
|
188
|
-
const approvedPaths: string[] = [];
|
|
189
|
-
for (const path of group.paths) {
|
|
190
|
-
// Mirror syncProjectTargets' own `if (!existsSync(path)) continue` so we
|
|
191
|
-
// never prompt for a project path it would silently skip anyway.
|
|
192
|
-
if (!existsSync(path)) continue;
|
|
193
|
-
const pinDir = resolveProjectPinDir(targetDir, path);
|
|
194
|
-
if (dryRun || existsSync(pinDir)) {
|
|
195
|
-
approvedPaths.push(path);
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
const approved = await confirmNewSyncTarget(`${targetName}/${groupName}`, pinDir, yes, isJson);
|
|
199
|
-
if (approved) approvedPaths.push(path);
|
|
200
|
-
}
|
|
201
|
-
projectGroups[groupName] = { ...group, paths: approvedPaths };
|
|
202
|
-
}
|
|
203
|
-
const projectResults = syncProjectTargets(
|
|
204
|
-
{ vaultPath, targetDir, targetName, projectGroups, localVaultPaths },
|
|
205
|
-
{ dryRun },
|
|
206
|
-
);
|
|
207
|
-
summary.projects = projectResults.map((projectResult) => ({
|
|
208
|
-
group: projectResult.group,
|
|
209
|
-
pin_dir: projectResult.pinDir,
|
|
210
|
-
added: projectResult.added,
|
|
211
|
-
removed: projectResult.removed,
|
|
212
|
-
skipped: projectResult.skipped,
|
|
213
|
-
}));
|
|
214
|
-
for (const projectResult of projectResults) {
|
|
215
|
-
log(
|
|
216
|
-
` ${projectResult.group} -> ${projectResult.pinDir}: +${projectResult.added.length} -${projectResult.removed.length}${suffix}`,
|
|
217
|
-
);
|
|
218
|
-
if (projectResult.skipped.length > 0) {
|
|
219
|
-
warnLine(
|
|
220
|
-
`refused to sync ${projectResult.skipped.join(", ")} — skill directory contains a symlink`,
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
targetSummaries.push(summary);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
emitSuccess(
|
|
229
|
-
{ isJson },
|
|
230
|
-
{ hook_installed: hookInstalled ?? null, notes, targets: targetSummaries },
|
|
231
|
-
() => {},
|
|
232
|
-
);
|
|
233
|
-
}
|
package/src/commands/target.ts
DELETED
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { hostname } from "node:os";
|
|
3
|
-
import { expandHome } from "../config";
|
|
4
|
-
import {
|
|
5
|
-
BUILT_IN_TARGET_NAMES,
|
|
6
|
-
planAgentSurfaces,
|
|
7
|
-
resolveBuiltInTarget,
|
|
8
|
-
SUPPORTED_AGENT_IDS,
|
|
9
|
-
} from "../init-agents";
|
|
10
|
-
import { planInitManifest, applyInit } from "../init";
|
|
11
|
-
import { resolveTargetDir, writeManifestAtomic } from "../manifest";
|
|
12
|
-
import { emitSuccess, unknownSubcommandError } from "../output";
|
|
13
|
-
import { applyTargetMarkerRehome, planTargetMarkerRehome, resolveProjectPinDir } from "../sync";
|
|
14
|
-
import { confirmIfNeeded, loadManifestContext } from "./shared";
|
|
15
|
-
|
|
16
|
-
export async function runTarget(
|
|
17
|
-
subCommand: string,
|
|
18
|
-
args: string[],
|
|
19
|
-
options: { isJson: boolean; dryRun: boolean },
|
|
20
|
-
): Promise<void> {
|
|
21
|
-
const { config, vaultPath, manifestPath, manifest } = await loadManifestContext();
|
|
22
|
-
|
|
23
|
-
if (subCommand === "list" || subCommand === "show") {
|
|
24
|
-
const names =
|
|
25
|
-
subCommand === "show" ? [args[0] ?? ""] : Object.keys(manifest.targets);
|
|
26
|
-
if (subCommand === "show" && !manifest.targets[names[0]!]) {
|
|
27
|
-
throw new Error(`target "${names[0]}" does not exist`);
|
|
28
|
-
}
|
|
29
|
-
const targets = names.map((name) => {
|
|
30
|
-
const target = manifest.targets[name]!;
|
|
31
|
-
const agents = SUPPORTED_AGENT_IDS.filter((agent) => {
|
|
32
|
-
const surface = planAgentSurfaces([agent]).surfaces[0];
|
|
33
|
-
return surface !== undefined && surface.path === resolveTargetDir(name, target);
|
|
34
|
-
});
|
|
35
|
-
return { name, ...target, dir: resolveTargetDir(name, target), agents };
|
|
36
|
-
});
|
|
37
|
-
emitSuccess({ isJson: options.isJson }, { targets }, () => {
|
|
38
|
-
if (targets.length === 0) {
|
|
39
|
-
console.log("no targets configured");
|
|
40
|
-
} else {
|
|
41
|
-
for (const target of targets) {
|
|
42
|
-
console.log(`${target.name}:`);
|
|
43
|
-
console.log(` dir: ${target.dir}`);
|
|
44
|
-
console.log(` host: ${target.host ?? "(global)"}`);
|
|
45
|
-
console.log(` agents: ${target.agents.join(", ") || "(custom)"}`);
|
|
46
|
-
console.log(
|
|
47
|
-
` projects: ${target.project_groups.join(", ") || "(none)"}`,
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (subCommand === "add") {
|
|
56
|
-
const name = args[0];
|
|
57
|
-
const dirIndex = args.indexOf("--dir");
|
|
58
|
-
const rawPath = dirIndex === -1 ? undefined : args[dirIndex + 1];
|
|
59
|
-
if (!name)
|
|
60
|
-
throw new Error("usage: skillmux target add <name> --dir <dir> --yes");
|
|
61
|
-
|
|
62
|
-
let path: string;
|
|
63
|
-
if (rawPath) {
|
|
64
|
-
path = expandHome(rawPath);
|
|
65
|
-
} else if (BUILT_IN_TARGET_NAMES.has(name)) {
|
|
66
|
-
path = resolveBuiltInTarget(name, {
|
|
67
|
-
codexHome: process.env.CODEX_HOME ? expandHome(process.env.CODEX_HOME) : undefined,
|
|
68
|
-
}).path;
|
|
69
|
-
} else {
|
|
70
|
-
throw new Error(
|
|
71
|
-
"usage: skillmux target add <name> --dir <dir> --yes (--dir may be omitted for built-in target names: agent-skills, claude-code, codex)",
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
if (options.dryRun) {
|
|
75
|
-
const planned = planInitManifest(vaultPath, [{ name, dir: path }], []);
|
|
76
|
-
emitSuccess(
|
|
77
|
-
{ isJson: options.isJson },
|
|
78
|
-
{ target: planned.targets[name] },
|
|
79
|
-
() => console.log(`target add: ${name} -> ${path} (dry-run)`),
|
|
80
|
-
);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
if (
|
|
84
|
-
!(await confirmIfNeeded({
|
|
85
|
-
confirmed: args.includes("--yes"),
|
|
86
|
-
isJson: options.isJson,
|
|
87
|
-
prompt: `adopt target ${name} at ${path}?`,
|
|
88
|
-
nonInteractiveError:
|
|
89
|
-
"skillmux target add requires --yes when run non-interactively",
|
|
90
|
-
}))
|
|
91
|
-
)
|
|
92
|
-
return;
|
|
93
|
-
applyInit(vaultPath, [{ name, dir: path }]);
|
|
94
|
-
emitSuccess({ isJson: options.isJson }, { name, dir: path }, () =>
|
|
95
|
-
console.log(`target "${name}" added at ${path}`),
|
|
96
|
-
);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (subCommand === "remove") {
|
|
101
|
-
const name = args[0];
|
|
102
|
-
if (!name || !manifest.targets[name]) {
|
|
103
|
-
throw new Error(
|
|
104
|
-
name
|
|
105
|
-
? `target "${name}" does not exist`
|
|
106
|
-
: "usage: skillmux target remove <name> --yes",
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
if (options.dryRun) {
|
|
110
|
-
emitSuccess(
|
|
111
|
-
{ isJson: options.isJson },
|
|
112
|
-
{ name, preserved_dir: resolveTargetDir(name, manifest.targets[name]!) },
|
|
113
|
-
() => console.log(`target remove: ${name} (files preserved, dry-run)`),
|
|
114
|
-
);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
if (
|
|
118
|
-
!(await confirmIfNeeded({
|
|
119
|
-
confirmed: args.includes("--yes"),
|
|
120
|
-
isJson: options.isJson,
|
|
121
|
-
prompt: `remove target ${name} from the manifest and preserve its files?`,
|
|
122
|
-
nonInteractiveError:
|
|
123
|
-
"skillmux target remove requires --yes when run non-interactively",
|
|
124
|
-
}))
|
|
125
|
-
)
|
|
126
|
-
return;
|
|
127
|
-
const targets = { ...manifest.targets };
|
|
128
|
-
const removedDir = resolveTargetDir(name, manifest.targets[name]!);
|
|
129
|
-
delete targets[name];
|
|
130
|
-
writeManifestAtomic(manifestPath, { ...manifest, targets });
|
|
131
|
-
emitSuccess(
|
|
132
|
-
{ isJson: options.isJson },
|
|
133
|
-
{ name, preserved_dir: removedDir },
|
|
134
|
-
() =>
|
|
135
|
-
console.log(
|
|
136
|
-
`target "${name}" removed from the manifest; files preserved at ${removedDir}`,
|
|
137
|
-
),
|
|
138
|
-
);
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (subCommand === "rehome") {
|
|
143
|
-
const name = args[0];
|
|
144
|
-
const target = name ? manifest.targets[name] : undefined;
|
|
145
|
-
if (!name || !target) {
|
|
146
|
-
throw new Error(name ? `target "${name}" does not exist` : "usage: skillmux target rehome <name> --yes");
|
|
147
|
-
}
|
|
148
|
-
if (target.host !== undefined && target.host !== hostname()) {
|
|
149
|
-
throw new Error(`target "${name}" is scoped to host ${target.host}, not ${hostname()}`);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const targetDir = resolveTargetDir(name, target);
|
|
153
|
-
if (!existsSync(targetDir)) throw new Error(`target "${name}" directory does not exist: ${targetDir}`);
|
|
154
|
-
const dirs = [targetDir];
|
|
155
|
-
for (const groupName of target.project_groups) {
|
|
156
|
-
const group = manifest.project?.[groupName];
|
|
157
|
-
if (!group) continue;
|
|
158
|
-
for (const projectPath of group.paths) {
|
|
159
|
-
if (!existsSync(projectPath)) continue;
|
|
160
|
-
const pinDir = resolveProjectPinDir(targetDir, projectPath);
|
|
161
|
-
if (existsSync(pinDir)) dirs.push(pinDir);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
const plans = dirs.map((dir) =>
|
|
165
|
-
planTargetMarkerRehome(dir, name, vaultPath, config.local_vault_paths.map(expandHome)),
|
|
166
|
-
);
|
|
167
|
-
const markerPaths = plans.map((plan) => plan.markerPath);
|
|
168
|
-
if (options.dryRun) {
|
|
169
|
-
emitSuccess(
|
|
170
|
-
{ isJson: options.isJson },
|
|
171
|
-
{ name, marker_paths: markerPaths },
|
|
172
|
-
() => console.log(`target rehome: ${name} (${markerPaths.length} markers, dry-run)`),
|
|
173
|
-
);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
if (
|
|
177
|
-
!(await confirmIfNeeded({
|
|
178
|
-
confirmed: args.includes("--yes"),
|
|
179
|
-
isJson: options.isJson,
|
|
180
|
-
prompt: `rehome ${markerPaths.length} ${name} marker(s) to ${vaultPath}?`,
|
|
181
|
-
nonInteractiveError: "skillmux target rehome requires --yes when run non-interactively",
|
|
182
|
-
}))
|
|
183
|
-
)
|
|
184
|
-
return;
|
|
185
|
-
applyTargetMarkerRehome(plans, vaultPath);
|
|
186
|
-
emitSuccess(
|
|
187
|
-
{ isJson: options.isJson },
|
|
188
|
-
{ name, marker_paths: markerPaths },
|
|
189
|
-
() => console.log(`target "${name}" rehomed ${markerPaths.length} marker(s) to ${vaultPath}`),
|
|
190
|
-
);
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (subCommand === "migrate") {
|
|
195
|
-
const builtInTargets = Object.entries(manifest.targets).filter(([name]) =>
|
|
196
|
-
BUILT_IN_TARGET_NAMES.has(name),
|
|
197
|
-
);
|
|
198
|
-
if (options.dryRun) {
|
|
199
|
-
emitSuccess(
|
|
200
|
-
{ isJson: options.isJson },
|
|
201
|
-
{ migrated_targets: builtInTargets.map(([name]) => name) },
|
|
202
|
-
() => console.log(`target migrate: ${builtInTargets.length} built-in target(s) (dry-run)`),
|
|
203
|
-
);
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
if (
|
|
207
|
-
!(await confirmIfNeeded({
|
|
208
|
-
confirmed: args.includes("--yes"),
|
|
209
|
-
isJson: options.isJson,
|
|
210
|
-
prompt: `remove redundant dir fields from ${builtInTargets.length} built-in target(s)?`,
|
|
211
|
-
nonInteractiveError: "skillmux target migrate requires --yes when run non-interactively",
|
|
212
|
-
}))
|
|
213
|
-
)
|
|
214
|
-
return;
|
|
215
|
-
writeManifestAtomic(manifestPath, manifest);
|
|
216
|
-
emitSuccess(
|
|
217
|
-
{ isJson: options.isJson },
|
|
218
|
-
{ migrated_targets: builtInTargets.map(([name]) => name) },
|
|
219
|
-
() => console.log(`target migrate: normalized ${builtInTargets.length} built-in target(s)`),
|
|
220
|
-
);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
throw unknownSubcommandError("target", subCommand, ["list", "show", "add", "remove", "rehome", "migrate"]);
|
|
225
|
-
}
|