@konvert7/klint 0.38.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/README.md +49 -166
- package/bin/klint.js +47 -0
- package/klint.schema.json +1 -1
- package/klint.schema.yaml +1 -1
- package/package.json +15 -25
- package/cli/command.ts +0 -189
- package/cli/commands.ts +0 -90
- package/cli/help.ts +0 -21
- package/cli/install-skill.ts +0 -260
- package/cli/output.ts +0 -178
- package/cli/paths.ts +0 -20
- package/cli/rust-config.ts +0 -145
- package/cli/rust-engine.ts +0 -323
- package/cli/stats.ts +0 -92
- package/cli.ts +0 -214
- package/core/arch-comments.ts +0 -125
- package/core/arch.ts +0 -470
- package/core/ast.ts +0 -63
- package/core/config.schema.ts +0 -413
- package/core/fixer.ts +0 -47
- package/core/native-binary.ts +0 -123
- package/core/paths.ts +0 -9
- package/core/rule-helpers.ts +0 -76
- package/core/runner.ts +0 -196
- package/core/schema-version.ts +0 -46
- package/core/skill.ts +0 -164
- package/core/stats.ts +0 -160
- package/core/types.ts +0 -146
- package/plugins/index.ts +0 -6
- package/plugins/sonar.ts +0 -27
- package/rules/index.ts +0 -26
- package/rules/no-async-predicate.ts +0 -51
- package/rules/no-consecutive-array-push.ts +0 -61
- package/rules/no-date-equality.ts +0 -35
- package/rules/no-floating-promise.ts +0 -37
- package/rules/no-misused-promises.ts +0 -59
- package/rules/no-nested-template-literals.ts +0 -47
- package/rules/no-object-in-template.ts +0 -95
- package/rules/no-optional-chain-on-non-nullable.ts +0 -47
- package/rules/no-single-char-class.ts +0 -113
- package/rules/no-string-match.ts +0 -63
- package/rules/no-sync-in-async.ts +0 -43
- package/rules/no-unguarded-json-parse.ts +0 -35
- package/rules/prefer-at.ts +0 -46
- package/rules/prefer-nullish-coalescing-assign.ts +0 -111
- package/rules/prefer-string-raw-regexp.ts +0 -93
- package/rules/prefer-string-raw.ts +0 -39
- package/rules/prefer-string-replaceall.ts +0 -67
- package/skill/klint-rules/SKILL.md +0 -264
- package/tools/generate-homebrew-formula.ts +0 -64
- package/tools/prepare-native-release.ts +0 -166
- package/tools/prepare-nuget-release.ts +0 -62
- package/tools/prepare-python-release.ts +0 -76
- package/tools/semantic-release-crates.js +0 -96
- package/tools/semantic-release-native-npm.js +0 -121
- package/tools/semantic-release-root-npm.js +0 -87
package/cli/command.ts
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
interface OptionSpec {
|
|
2
|
-
description: string;
|
|
3
|
-
value?: string;
|
|
4
|
-
short?: string;
|
|
5
|
-
aliases?: string[];
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
type OptionSpecs = Record<string, OptionSpec>;
|
|
9
|
-
export type OptionValues = Record<string, string | true | undefined>;
|
|
10
|
-
|
|
11
|
-
interface Leaf {
|
|
12
|
-
kind: "leaf";
|
|
13
|
-
summary: string;
|
|
14
|
-
options: OptionSpecs;
|
|
15
|
-
details?: () => string[];
|
|
16
|
-
run(values: OptionValues): unknown;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface Group {
|
|
20
|
-
kind: "group";
|
|
21
|
-
summary: string;
|
|
22
|
-
commands: Record<string, Leaf>;
|
|
23
|
-
aliases?: Record<string, string>;
|
|
24
|
-
defaultCommand?: Leaf;
|
|
25
|
-
details?: () => string[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export type Command = Leaf | Group;
|
|
29
|
-
|
|
30
|
-
const HELP_FLAGS = new Set(["-h", "--help"]);
|
|
31
|
-
const HELP_WORD = "help";
|
|
32
|
-
const HELP_OPTION: OptionSpec = { short: "h", description: "Show this help" };
|
|
33
|
-
|
|
34
|
-
interface ParsedOptions {
|
|
35
|
-
values: OptionValues;
|
|
36
|
-
positionals: string[];
|
|
37
|
-
help: boolean;
|
|
38
|
-
error?: string;
|
|
39
|
-
rest: string[];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function findOption(
|
|
43
|
-
options: OptionSpecs,
|
|
44
|
-
token: string
|
|
45
|
-
): [string, OptionSpec] | undefined {
|
|
46
|
-
return Object.entries(options).find(
|
|
47
|
-
([name, spec]) =>
|
|
48
|
-
token === `--${name}` ||
|
|
49
|
-
(spec.short !== undefined && token === `-${spec.short}`) ||
|
|
50
|
-
(spec.aliases?.includes(token) ?? false)
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function parseOptions(
|
|
55
|
-
args: string[],
|
|
56
|
-
options: OptionSpecs,
|
|
57
|
-
stopAtPositional: boolean
|
|
58
|
-
): ParsedOptions {
|
|
59
|
-
const parsed: ParsedOptions = { values: {}, positionals: [], help: false, rest: [] };
|
|
60
|
-
for (let i = 0; i < args.length; i++) {
|
|
61
|
-
const token = args[i];
|
|
62
|
-
const match = findOption(options, token);
|
|
63
|
-
if (HELP_FLAGS.has(token)) {
|
|
64
|
-
parsed.help = true;
|
|
65
|
-
} else if (match?.[1].value !== undefined) {
|
|
66
|
-
const [name, spec] = match;
|
|
67
|
-
const value = args[i + 1];
|
|
68
|
-
if (value === undefined) parsed.error ??= `option '--${name}' needs ${spec.value}`;
|
|
69
|
-
else parsed.values[name] = value;
|
|
70
|
-
i++;
|
|
71
|
-
} else if (match) {
|
|
72
|
-
parsed.values[match[0]] = true;
|
|
73
|
-
} else if (token.startsWith("-")) {
|
|
74
|
-
parsed.error ??= `unknown option '${token}'`;
|
|
75
|
-
} else if (stopAtPositional) {
|
|
76
|
-
parsed.positionals.push(token);
|
|
77
|
-
parsed.rest = args.slice(i + 1);
|
|
78
|
-
return parsed;
|
|
79
|
-
} else {
|
|
80
|
-
parsed.positionals.push(token);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return parsed;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export async function runCommand(
|
|
87
|
-
command: Command,
|
|
88
|
-
args: string[],
|
|
89
|
-
path: string[]
|
|
90
|
-
): Promise<number> {
|
|
91
|
-
return command.kind === "group"
|
|
92
|
-
? runGroup(command, args, path)
|
|
93
|
-
: runLeaf(command, args, helpText(command, path));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async function runGroup(group: Group, args: string[], path: string[]): Promise<number> {
|
|
97
|
-
const help = helpText(group, path);
|
|
98
|
-
const parsed = parseOptions(args, group.defaultCommand?.options ?? {}, true);
|
|
99
|
-
const [name] = parsed.positionals;
|
|
100
|
-
if (parsed.help || name === HELP_WORD || group.aliases?.[name] === HELP_WORD) {
|
|
101
|
-
return showHelp(help);
|
|
102
|
-
}
|
|
103
|
-
if (name !== undefined) {
|
|
104
|
-
const sub = group.commands[group.aliases?.[name] ?? name];
|
|
105
|
-
if (!sub) return usageFailure(`unknown command '${name}'`, help);
|
|
106
|
-
if (parsed.error) return usageFailure(parsed.error, help);
|
|
107
|
-
return runCommand(sub, parsed.rest, [...path, name]);
|
|
108
|
-
}
|
|
109
|
-
if (!group.defaultCommand) return showHelp(help);
|
|
110
|
-
return runLeaf(group.defaultCommand, args, help);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function runLeaf(leaf: Leaf, args: string[], help: string): Promise<number> {
|
|
114
|
-
const parsed = parseOptions(args, leaf.options, false);
|
|
115
|
-
if (parsed.help || parsed.positionals.includes(HELP_WORD)) return showHelp(help);
|
|
116
|
-
if (parsed.error) return usageFailure(parsed.error, help);
|
|
117
|
-
const [extra] = parsed.positionals;
|
|
118
|
-
if (extra !== undefined) return usageFailure(`unexpected argument '${extra}'`, help);
|
|
119
|
-
const result = await leaf.run(parsed.values);
|
|
120
|
-
return typeof result === "number" ? result : 0;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function showHelp(help: string): number {
|
|
124
|
-
process.stdout.write(`${help}\n`);
|
|
125
|
-
return 0;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function usageFailure(message: string, help: string): number {
|
|
129
|
-
process.stderr.write(`error: ${message}\n\n${help}\n`);
|
|
130
|
-
return 1;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function helpText(command: Command, path: string[]): string {
|
|
134
|
-
return command.kind === "group" ? groupHelp(command, path) : leafHelp(command, path);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function optionLabel(name: string, spec: OptionSpec): string {
|
|
138
|
-
const short = spec.short ? `-${spec.short}, ` : "";
|
|
139
|
-
const value = spec.value ? ` ${spec.value}` : "";
|
|
140
|
-
return `${short}--${name}${value}`;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function optionRows(options: OptionSpecs): string[] {
|
|
144
|
-
return table(
|
|
145
|
-
Object.entries({ ...options, help: HELP_OPTION }).map(([name, spec]) => [
|
|
146
|
-
optionLabel(name, spec),
|
|
147
|
-
spec.description,
|
|
148
|
-
])
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function leafHelp(leaf: Leaf, path: string[]): string {
|
|
153
|
-
return [
|
|
154
|
-
`Usage: ${path.join(" ")} [options]`,
|
|
155
|
-
"",
|
|
156
|
-
` ${leaf.summary}`,
|
|
157
|
-
"",
|
|
158
|
-
"Options:",
|
|
159
|
-
...optionRows(leaf.options),
|
|
160
|
-
...detailLines(leaf.details),
|
|
161
|
-
].join("\n");
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function groupHelp(group: Group, path: string[]): string {
|
|
165
|
-
const fallback = group.defaultCommand;
|
|
166
|
-
const synopsis = fallback ? `[<command>] [options]` : "<command>";
|
|
167
|
-
return [
|
|
168
|
-
`Usage: ${path.join(" ")} ${synopsis}`,
|
|
169
|
-
"",
|
|
170
|
-
` ${group.summary}`,
|
|
171
|
-
"",
|
|
172
|
-
"Commands:",
|
|
173
|
-
...table(Object.entries(group.commands).map(([name, sub]) => [name, sub.summary])),
|
|
174
|
-
...(fallback ? ["", "Options:", ...optionRows(fallback.options)] : []),
|
|
175
|
-
"",
|
|
176
|
-
...(fallback ? [`With no command, ${fallback.summary.toLowerCase()}.`] : []),
|
|
177
|
-
`Run '${path.join(" ")} <command> --help' for its options.`,
|
|
178
|
-
...detailLines(group.details),
|
|
179
|
-
].join("\n");
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function detailLines(details: (() => string[]) | undefined): string[] {
|
|
183
|
-
return details ? ["", ...details()] : [];
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function table(rows: string[][]): string[] {
|
|
187
|
-
const width = Math.max(...rows.map(([label]) => label.length));
|
|
188
|
-
return rows.map(([label, text]) => ` ${label.padEnd(width)} ${text}`);
|
|
189
|
-
}
|
package/cli/commands.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { AGENT_SKILL_DIRS } from "../core/skill";
|
|
2
|
-
import type { Group, OptionValues } from "./command";
|
|
3
|
-
import { builtInRuleLines } from "./help";
|
|
4
|
-
|
|
5
|
-
interface CommandHandlers {
|
|
6
|
-
lint(values: OptionValues): unknown;
|
|
7
|
-
stats(values: OptionValues): unknown;
|
|
8
|
-
installSkill(values: OptionValues): unknown;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function klintCommand(handlers: CommandHandlers): Group {
|
|
12
|
-
return {
|
|
13
|
-
kind: "group",
|
|
14
|
-
summary:
|
|
15
|
-
"Architecture-as-Code linter: layer boundaries, import rules and forbidden patterns",
|
|
16
|
-
aliases: { h: "help" },
|
|
17
|
-
defaultCommand: {
|
|
18
|
-
kind: "leaf",
|
|
19
|
-
summary: "Lints the project",
|
|
20
|
-
options: {
|
|
21
|
-
config: {
|
|
22
|
-
value: "<dir>",
|
|
23
|
-
description:
|
|
24
|
-
"Directory containing klint.yaml or klint.config.json (default: cwd)",
|
|
25
|
-
},
|
|
26
|
-
rules: {
|
|
27
|
-
value: "<file>",
|
|
28
|
-
description:
|
|
29
|
-
"Custom rules file (default: <configDir>/klint.rules.ts if present)",
|
|
30
|
-
},
|
|
31
|
-
engine: {
|
|
32
|
-
value: "<name>",
|
|
33
|
-
description:
|
|
34
|
-
"Engine to use: ts (default), rust, compare, or auto (experimental)",
|
|
35
|
-
},
|
|
36
|
-
fix: { description: "Apply auto-fixes for fixable violations in place" },
|
|
37
|
-
json: { description: "Emit structured JSON to stdout (for agent and CI use)" },
|
|
38
|
-
debug: {
|
|
39
|
-
aliases: ["-debug"],
|
|
40
|
-
description: "Print file resolution and rule progress to stderr",
|
|
41
|
-
},
|
|
42
|
-
hook: { description: "Count this run as an agent hook run in klint stats" },
|
|
43
|
-
version: { short: "V", description: "Print the installed klint version" },
|
|
44
|
-
},
|
|
45
|
-
run: handlers.lint,
|
|
46
|
-
},
|
|
47
|
-
commands: {
|
|
48
|
-
stats: {
|
|
49
|
-
kind: "leaf",
|
|
50
|
-
summary:
|
|
51
|
-
"Print runs, violations reported and errors prevented, in total and per rule",
|
|
52
|
-
options: {
|
|
53
|
-
config: {
|
|
54
|
-
value: "<dir>",
|
|
55
|
-
description: "Directory holding .klint/stats.json (default: cwd)",
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
details: () => ["Set KLINT_NO_STATS=1 to stop recording."],
|
|
59
|
-
run: handlers.stats,
|
|
60
|
-
},
|
|
61
|
-
"install-skill": {
|
|
62
|
-
kind: "leaf",
|
|
63
|
-
summary: "Install the rule-authoring skill into agent config directories",
|
|
64
|
-
options: {
|
|
65
|
-
agents: {
|
|
66
|
-
value: "<list>",
|
|
67
|
-
description: `Comma-separated: ${Object.keys(AGENT_SKILL_DIRS).join(",")} (default: all)`,
|
|
68
|
-
},
|
|
69
|
-
symlink: {
|
|
70
|
-
aliases: ["--shared"],
|
|
71
|
-
description:
|
|
72
|
-
"Keep one skill in .agents/skills and symlink the others to it (default)",
|
|
73
|
-
},
|
|
74
|
-
copy: { description: "Give every agent directory its own copy" },
|
|
75
|
-
force: { description: "Replace a skill directory klint did not install" },
|
|
76
|
-
},
|
|
77
|
-
details: () => [
|
|
78
|
-
"Asks which agents and how to store the skill when run on a terminal without",
|
|
79
|
-
"--agents, --symlink or --copy.",
|
|
80
|
-
"",
|
|
81
|
-
"Installed skills record the version and hash they came from. klint warns with",
|
|
82
|
-
"klint/skill-stale once the shipped skill moves on, and with",
|
|
83
|
-
"klint/skill-legacy-link for old symlinks that point into node_modules.",
|
|
84
|
-
],
|
|
85
|
-
run: handlers.installSkill,
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
details: builtInRuleLines,
|
|
89
|
-
};
|
|
90
|
-
}
|
package/cli/help.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { BUILT_IN_PLUGINS } from "../plugins/index";
|
|
2
|
-
import { BUILT_IN_RULES } from "../rules/index";
|
|
3
|
-
|
|
4
|
-
export function builtInRuleLines(): string[] {
|
|
5
|
-
const pluginRules = new Set(
|
|
6
|
-
Object.values(BUILT_IN_PLUGINS).flatMap((p) => Object.keys(p.rules))
|
|
7
|
-
);
|
|
8
|
-
const standaloneRules = Object.keys(BUILT_IN_RULES).filter((r) => !pluginRules.has(r));
|
|
9
|
-
const pluginEntries = Object.entries(BUILT_IN_PLUGINS);
|
|
10
|
-
|
|
11
|
-
return [
|
|
12
|
-
`Built-in rules (${standaloneRules.length}):`,
|
|
13
|
-
...standaloneRules.map((r) => ` ${r}`),
|
|
14
|
-
"",
|
|
15
|
-
`Plugins (${pluginEntries.length}):`,
|
|
16
|
-
...pluginEntries.flatMap(([name, plugin]) => [
|
|
17
|
-
` ${name}`,
|
|
18
|
-
...Object.keys(plugin.rules).map((r) => ` ${r}`),
|
|
19
|
-
]),
|
|
20
|
-
];
|
|
21
|
-
}
|
package/cli/install-skill.ts
DELETED
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { cp, lstat, mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
4
|
-
import * as clack from "@clack/prompts";
|
|
5
|
-
import {
|
|
6
|
-
AGENT_SKILL_DIRS,
|
|
7
|
-
type AgentKey,
|
|
8
|
-
agentSkillDirs,
|
|
9
|
-
CANONICAL_SKILL_DIR,
|
|
10
|
-
RECEIPT_FILE_NAME,
|
|
11
|
-
SKILL_DIR_NAME,
|
|
12
|
-
SKILL_FILE_NAME,
|
|
13
|
-
type SkillReceipt,
|
|
14
|
-
skillHash,
|
|
15
|
-
} from "../core/skill";
|
|
16
|
-
import type { OptionValues } from "./command";
|
|
17
|
-
import { installedVersion, PACKAGE_ROOT } from "./paths";
|
|
18
|
-
|
|
19
|
-
const AGENT_TARGETS = [
|
|
20
|
-
{ value: "claude", label: "Claude Code" },
|
|
21
|
-
{ value: "opencode", label: "opencode" },
|
|
22
|
-
{ value: "cursor", label: "Cursor" },
|
|
23
|
-
{ value: "codex", label: "Codex" },
|
|
24
|
-
{ value: "copilot", label: "GitHub Copilot" },
|
|
25
|
-
] as const satisfies ReadonlyArray<{ value: AgentKey; label: string }>;
|
|
26
|
-
|
|
27
|
-
interface InstallPlan {
|
|
28
|
-
agents: AgentKey[];
|
|
29
|
-
shared: boolean;
|
|
30
|
-
force: boolean;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export async function installSkill(values: OptionValues): Promise<void> {
|
|
34
|
-
const skillSrc = join(PACKAGE_ROOT, "skill", SKILL_DIR_NAME);
|
|
35
|
-
if (!existsSync(skillSrc)) {
|
|
36
|
-
process.stderr.write(`klint: skill source not found at ${skillSrc}\n`);
|
|
37
|
-
process.exit(1);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const plan = await resolvePlan(values);
|
|
41
|
-
const dirs = agentSkillDirs(plan.agents);
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const report = plan.shared
|
|
45
|
-
? await installShared({ dirs, force: plan.force, skillSrc })
|
|
46
|
-
: await installCopies({ dirs, force: plan.force, skillSrc });
|
|
47
|
-
for (const line of report) process.stdout.write(`klint: ${line}\n`);
|
|
48
|
-
} catch (error) {
|
|
49
|
-
process.stderr.write(`${(error as Error).message}\n`);
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (process.stdin.isTTY) {
|
|
54
|
-
clack.outro("Done.");
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function installShared({
|
|
59
|
-
dirs,
|
|
60
|
-
force,
|
|
61
|
-
skillSrc,
|
|
62
|
-
}: {
|
|
63
|
-
dirs: string[];
|
|
64
|
-
force: boolean;
|
|
65
|
-
skillSrc: string;
|
|
66
|
-
}): Promise<string[]> {
|
|
67
|
-
const hub = join(CANONICAL_SKILL_DIR, SKILL_DIR_NAME);
|
|
68
|
-
const report = [await installCopy(hub, skillSrc, force)];
|
|
69
|
-
|
|
70
|
-
for (const dir of dirs) {
|
|
71
|
-
if (dir === CANONICAL_SKILL_DIR) continue;
|
|
72
|
-
report.push(await linkToHub(join(dir, SKILL_DIR_NAME), hub, skillSrc, force));
|
|
73
|
-
}
|
|
74
|
-
return report;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function installCopies({
|
|
78
|
-
dirs,
|
|
79
|
-
force,
|
|
80
|
-
skillSrc,
|
|
81
|
-
}: {
|
|
82
|
-
dirs: string[];
|
|
83
|
-
force: boolean;
|
|
84
|
-
skillSrc: string;
|
|
85
|
-
}): Promise<string[]> {
|
|
86
|
-
const report: string[] = [];
|
|
87
|
-
for (const dir of dirs) {
|
|
88
|
-
report.push(await installCopy(join(dir, SKILL_DIR_NAME), skillSrc, force));
|
|
89
|
-
}
|
|
90
|
-
return report;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async function installCopy(
|
|
94
|
-
target: string,
|
|
95
|
-
skillSrc: string,
|
|
96
|
-
force: boolean
|
|
97
|
-
): Promise<string> {
|
|
98
|
-
const dest = resolve(process.cwd(), target);
|
|
99
|
-
await prepareDestination(dest, target, force);
|
|
100
|
-
await cp(skillSrc, dest, { recursive: true });
|
|
101
|
-
await writeReceipt(dest, skillSrc);
|
|
102
|
-
return `installed ${target}`;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function linkToHub(
|
|
106
|
-
target: string,
|
|
107
|
-
hub: string,
|
|
108
|
-
skillSrc: string,
|
|
109
|
-
force: boolean
|
|
110
|
-
): Promise<string> {
|
|
111
|
-
const dest = resolve(process.cwd(), target);
|
|
112
|
-
await prepareDestination(dest, target, force);
|
|
113
|
-
|
|
114
|
-
const absoluteHub = resolve(process.cwd(), hub);
|
|
115
|
-
const linkPath = relative(dirname(dest), absoluteHub);
|
|
116
|
-
if (await linkResolves(linkPath, dest, "dir")) return `linked ${target} -> ${linkPath}`;
|
|
117
|
-
if (await linkResolves(absoluteHub, dest, "junction"))
|
|
118
|
-
return `linked ${target} -> ${hub}`;
|
|
119
|
-
|
|
120
|
-
await cp(skillSrc, dest, { recursive: true });
|
|
121
|
-
await writeReceipt(dest, skillSrc);
|
|
122
|
-
return `installed ${target} (symlinks unavailable here)`;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function linkResolves(
|
|
126
|
-
linkTarget: string,
|
|
127
|
-
dest: string,
|
|
128
|
-
type: "dir" | "junction"
|
|
129
|
-
): Promise<boolean> {
|
|
130
|
-
try {
|
|
131
|
-
await symlink(linkTarget, dest, type);
|
|
132
|
-
if (existsSync(join(dest, SKILL_FILE_NAME))) return true;
|
|
133
|
-
} catch {
|
|
134
|
-
/* try the next strategy */
|
|
135
|
-
}
|
|
136
|
-
await rm(dest, { force: true, recursive: true });
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function prepareDestination(
|
|
141
|
-
dest: string,
|
|
142
|
-
target: string,
|
|
143
|
-
force: boolean
|
|
144
|
-
): Promise<void> {
|
|
145
|
-
await mkdir(dirname(dest), { recursive: true });
|
|
146
|
-
|
|
147
|
-
const existing = await lstat(dest).catch(() => undefined);
|
|
148
|
-
if (existing === undefined) return;
|
|
149
|
-
|
|
150
|
-
if (!existing.isDirectory() || existsSync(join(dest, RECEIPT_FILE_NAME)) || force) {
|
|
151
|
-
await rm(dest, { force: true, recursive: true });
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
throw new Error(
|
|
156
|
-
`klint: ${target} exists and was not installed by klint — pass --force to replace it`
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async function writeReceipt(dest: string, skillSrc: string): Promise<void> {
|
|
161
|
-
const receipt: SkillReceipt = {
|
|
162
|
-
version: installedVersion(),
|
|
163
|
-
sha256: skillHash(await readFile(join(skillSrc, SKILL_FILE_NAME))),
|
|
164
|
-
};
|
|
165
|
-
await writeFile(join(dest, RECEIPT_FILE_NAME), `${JSON.stringify(receipt, null, 2)}\n`);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
async function resolvePlan(values: OptionValues): Promise<InstallPlan> {
|
|
169
|
-
const flags = parseFlags(values);
|
|
170
|
-
const decided = flags.agents !== undefined || flags.shared !== undefined;
|
|
171
|
-
|
|
172
|
-
if (!process.stdin.isTTY || decided) {
|
|
173
|
-
return {
|
|
174
|
-
agents: flags.agents ?? everyAgent(),
|
|
175
|
-
shared: flags.shared ?? true,
|
|
176
|
-
force: flags.force,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
clack.intro("klint install-skill");
|
|
181
|
-
return {
|
|
182
|
-
agents: await promptForAgents(),
|
|
183
|
-
shared: await promptForShared(),
|
|
184
|
-
force: flags.force,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function parseFlags(values: OptionValues): {
|
|
189
|
-
agents?: AgentKey[];
|
|
190
|
-
shared?: boolean;
|
|
191
|
-
force: boolean;
|
|
192
|
-
} {
|
|
193
|
-
return {
|
|
194
|
-
agents: typeof values.agents === "string" ? parseAgents(values.agents) : undefined,
|
|
195
|
-
shared: sharedChoice(values),
|
|
196
|
-
force: values.force === true,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function sharedChoice(values: OptionValues): boolean | undefined {
|
|
201
|
-
if (values.copy) return false;
|
|
202
|
-
return values.symlink ? true : undefined;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function parseAgents(value: string): AgentKey[] {
|
|
206
|
-
return value
|
|
207
|
-
.split(",")
|
|
208
|
-
.map((agent) => agent.trim())
|
|
209
|
-
.filter((agent) => agent.length > 0)
|
|
210
|
-
.map((agent) => {
|
|
211
|
-
if (!(agent in AGENT_SKILL_DIRS)) {
|
|
212
|
-
process.stderr.write(
|
|
213
|
-
`klint: unknown agent "${agent}" (expected one of: ${everyAgent().join(", ")})\n`
|
|
214
|
-
);
|
|
215
|
-
process.exit(1);
|
|
216
|
-
}
|
|
217
|
-
return agent as AgentKey;
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function everyAgent(): AgentKey[] {
|
|
222
|
-
return AGENT_TARGETS.map((target) => target.value);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
async function promptForAgents(): Promise<AgentKey[]> {
|
|
226
|
-
const agents = await clack.multiselect<AgentKey>({
|
|
227
|
-
message: "Which agents should the skill be installed for?",
|
|
228
|
-
options: AGENT_TARGETS.map((a) => ({ value: a.value, label: a.label })),
|
|
229
|
-
initialValues: everyAgent(),
|
|
230
|
-
});
|
|
231
|
-
if (clack.isCancel(agents)) {
|
|
232
|
-
clack.cancel("Cancelled.");
|
|
233
|
-
process.exit(0);
|
|
234
|
-
}
|
|
235
|
-
return agents as AgentKey[];
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
async function promptForShared(): Promise<boolean> {
|
|
239
|
-
const mode = await clack.select<"shared" | "copy">({
|
|
240
|
-
message: "How should the skill be stored?",
|
|
241
|
-
initialValue: "shared",
|
|
242
|
-
options: [
|
|
243
|
-
{
|
|
244
|
-
value: "shared",
|
|
245
|
-
label: "Shared (recommended)",
|
|
246
|
-
hint: `one skill in ${CANONICAL_SKILL_DIR}, other agents symlink to it`,
|
|
247
|
-
},
|
|
248
|
-
{
|
|
249
|
-
value: "copy",
|
|
250
|
-
label: "Separate copies",
|
|
251
|
-
hint: "an independent copy in every agent directory",
|
|
252
|
-
},
|
|
253
|
-
],
|
|
254
|
-
});
|
|
255
|
-
if (clack.isCancel(mode)) {
|
|
256
|
-
clack.cancel("Cancelled.");
|
|
257
|
-
process.exit(0);
|
|
258
|
-
}
|
|
259
|
-
return mode === "shared";
|
|
260
|
-
}
|