skvlt 0.9.9

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,77 @@
1
+ import {
2
+ defaultManifestOptionPath,
3
+ defaultGlobalLockFilePath,
4
+ } from "../paths/defaults";
5
+
6
+ export type BackupOptions = {
7
+ outputPath: string;
8
+ lockFilePath: string;
9
+ lockFileProvided: boolean;
10
+ projectScope: boolean;
11
+ dryRun: boolean;
12
+ help: boolean;
13
+ };
14
+
15
+ /**
16
+ * Parses backup CLI flags into a validated runtime options object.
17
+ */
18
+ export function parseBackupArgs(argv: string[]): BackupOptions {
19
+ const options: BackupOptions = {
20
+ outputPath: defaultManifestOptionPath,
21
+ lockFilePath: defaultGlobalLockFilePath,
22
+ lockFileProvided: false,
23
+ projectScope: false,
24
+ dryRun: false,
25
+ help: false,
26
+ };
27
+
28
+ for (let index = 0; index < argv.length; index += 1) {
29
+ const arg = argv[index];
30
+
31
+ switch (arg) {
32
+ case "--output": {
33
+ const value = argv[index + 1];
34
+ if (!value || value.startsWith("-")) {
35
+ throw new Error("--output requires a path");
36
+ }
37
+ options.outputPath = value;
38
+ index += 1;
39
+ break;
40
+ }
41
+ case "--lock-file": {
42
+ const value = argv[index + 1];
43
+ if (!value || value.startsWith("-")) {
44
+ throw new Error("--lock-file requires a path");
45
+ }
46
+ options.lockFilePath = value;
47
+ options.lockFileProvided = true;
48
+ index += 1;
49
+ break;
50
+ }
51
+ case "--project-scope":
52
+ options.projectScope = true;
53
+ break;
54
+ case "--dry-run":
55
+ options.dryRun = true;
56
+ break;
57
+ case "--help":
58
+ case "-h":
59
+ options.help = true;
60
+ break;
61
+ default:
62
+ throw new Error(`Unknown argument: ${arg}`);
63
+ }
64
+ }
65
+
66
+ if (options.help) {
67
+ return options;
68
+ }
69
+
70
+ if (options.projectScope && !options.lockFileProvided) {
71
+ throw new Error(
72
+ "--project-scope requires --lock-file because no project lock file could be auto-discovered in this workspace.",
73
+ );
74
+ }
75
+
76
+ return options;
77
+ }
@@ -0,0 +1,30 @@
1
+ export type CompletionOptions = {
2
+ shell?: string;
3
+ help: boolean;
4
+ };
5
+
6
+ /**
7
+ * Parses completion arguments while treating the first positional token as the shell.
8
+ */
9
+ export function parseCompletionArgs(argv: string[]): CompletionOptions {
10
+ const options: CompletionOptions = {
11
+ shell: undefined,
12
+ help: false,
13
+ };
14
+
15
+ for (const arg of argv) {
16
+ if (arg === "--help" || arg === "-h") {
17
+ options.help = true;
18
+ continue;
19
+ }
20
+
21
+ if (!options.shell) {
22
+ options.shell = arg;
23
+ continue;
24
+ }
25
+
26
+ throw new Error(`Unknown argument: ${arg}`);
27
+ }
28
+
29
+ return options;
30
+ }
@@ -0,0 +1,40 @@
1
+ import { defaultManifestOptionPath } from "../paths/defaults";
2
+
3
+ export type DoctorOptions = {
4
+ manifestPath: string;
5
+ help: boolean;
6
+ };
7
+
8
+ /**
9
+ * Parses doctor CLI flags into a manifest-aware diagnostics request.
10
+ */
11
+ export function parseDoctorArgs(argv: string[]): DoctorOptions {
12
+ const options: DoctorOptions = {
13
+ manifestPath: defaultManifestOptionPath,
14
+ help: false,
15
+ };
16
+
17
+ for (let index = 0; index < argv.length; index += 1) {
18
+ const arg = argv[index];
19
+
20
+ switch (arg) {
21
+ case "--manifest": {
22
+ const value = argv[index + 1];
23
+ if (!value || value.startsWith("-")) {
24
+ throw new Error("--manifest requires a path");
25
+ }
26
+ options.manifestPath = value;
27
+ index += 1;
28
+ break;
29
+ }
30
+ case "--help":
31
+ case "-h":
32
+ options.help = true;
33
+ break;
34
+ default:
35
+ throw new Error(`Unknown argument: ${arg}`);
36
+ }
37
+ }
38
+
39
+ return options;
40
+ }
@@ -0,0 +1,119 @@
1
+ import { defaultManifestOptionPath } from "../paths/defaults";
2
+
3
+ export type RestoreOptions = {
4
+ manifestPath: string;
5
+ onlySources: string[];
6
+ agents: string[];
7
+ projectScope: boolean;
8
+ reinstallAll: boolean;
9
+ installAll: boolean;
10
+ copy: boolean;
11
+ continueOnError: boolean;
12
+ concurrency?: number;
13
+ dryRun: boolean;
14
+ help: boolean;
15
+ };
16
+
17
+ function parsePositiveIntegerOption(value: string, optionName: string): number {
18
+ if (!/^\d+$/.test(value)) {
19
+ throw new Error(`${optionName} requires a positive integer`);
20
+ }
21
+
22
+ const parsed = Number(value);
23
+ if (!Number.isSafeInteger(parsed) || parsed < 1) {
24
+ throw new Error(`${optionName} requires a positive integer`);
25
+ }
26
+
27
+ return parsed;
28
+ }
29
+
30
+ /**
31
+ * Parses restore CLI flags and keeps repeatable options in call order.
32
+ */
33
+ export function parseRestoreArgs(argv: string[]): RestoreOptions {
34
+ const options: RestoreOptions = {
35
+ manifestPath: defaultManifestOptionPath,
36
+ onlySources: [],
37
+ agents: [],
38
+ projectScope: false,
39
+ reinstallAll: false,
40
+ installAll: false,
41
+ copy: false,
42
+ continueOnError: false,
43
+ concurrency: undefined,
44
+ dryRun: false,
45
+ help: false,
46
+ };
47
+
48
+ for (let index = 0; index < argv.length; index += 1) {
49
+ const arg = argv[index];
50
+
51
+ switch (arg) {
52
+ case "--manifest": {
53
+ const value = argv[index + 1];
54
+ if (!value || value.startsWith("-")) {
55
+ throw new Error("--manifest requires a path");
56
+ }
57
+ options.manifestPath = value;
58
+ index += 1;
59
+ break;
60
+ }
61
+ case "--only-source": {
62
+ const value = argv[index + 1];
63
+ if (!value || value.startsWith("-")) {
64
+ throw new Error("--only-source requires a value");
65
+ }
66
+ options.onlySources.push(value);
67
+ index += 1;
68
+ break;
69
+ }
70
+ case "--agent": {
71
+ const value = argv[index + 1];
72
+ if (!value || value.startsWith("-")) {
73
+ throw new Error("--agent requires a value");
74
+ }
75
+ options.agents.push(value);
76
+ index += 1;
77
+ break;
78
+ }
79
+ case "--project-scope":
80
+ options.projectScope = true;
81
+ break;
82
+ case "--reinstall-all":
83
+ options.reinstallAll = true;
84
+ break;
85
+ case "--all":
86
+ options.installAll = true;
87
+ break;
88
+ case "--copy":
89
+ options.copy = true;
90
+ break;
91
+ case "--continue-on-error":
92
+ options.continueOnError = true;
93
+ break;
94
+ case "--concurrency": {
95
+ const value = argv[index + 1];
96
+ if (!value || value.startsWith("-")) {
97
+ throw new Error("--concurrency requires a value");
98
+ }
99
+ options.concurrency = parsePositiveIntegerOption(
100
+ value,
101
+ "--concurrency",
102
+ );
103
+ index += 1;
104
+ break;
105
+ }
106
+ case "--dry-run":
107
+ options.dryRun = true;
108
+ break;
109
+ case "--help":
110
+ case "-h":
111
+ options.help = true;
112
+ break;
113
+ default:
114
+ throw new Error(`Unknown argument: ${arg}`);
115
+ }
116
+ }
117
+
118
+ return options;
119
+ }
@@ -0,0 +1,136 @@
1
+ const reset = "\x1b[0m";
2
+ const boldCode = "\x1b[1m";
3
+ const dimCode = "\x1b[2m";
4
+ const redCode = "\x1b[31m";
5
+ const whiteCode = "\x1b[37m";
6
+ const cyanCode = "\x1b[36m";
7
+ const yellowCode = "\x1b[33m";
8
+ const bgRedCode = "\x1b[41m";
9
+ const dimGrayCode = "\x1b[38;5;102m";
10
+ const textGrayCode = "\x1b[38;5;145m";
11
+ const grayCode = "\x1b[38;5;245m";
12
+ const grayDarkCode = "\x1b[38;5;238m";
13
+
14
+ function wrap(text: string, ...codes: string[]) {
15
+ return `${codes.join("")}${text}${reset}`;
16
+ }
17
+
18
+ export function stripAnsi(text: string): string {
19
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
20
+ }
21
+
22
+ export function bold(text: string): string {
23
+ return wrap(text, boldCode);
24
+ }
25
+
26
+ export function dim(text: string): string {
27
+ return wrap(text, dimCode);
28
+ }
29
+
30
+ export function red(text: string): string {
31
+ return wrap(text, redCode);
32
+ }
33
+
34
+ export function white(text: string): string {
35
+ return wrap(text, whiteCode);
36
+ }
37
+
38
+ export function cyan(text: string): string {
39
+ return wrap(text, cyanCode);
40
+ }
41
+
42
+ export function yellow(text: string): string {
43
+ return wrap(text, yellowCode);
44
+ }
45
+
46
+ export function dimGray(text: string): string {
47
+ return wrap(text, dimGrayCode);
48
+ }
49
+
50
+ export function textGray(text: string): string {
51
+ return wrap(text, textGrayCode);
52
+ }
53
+
54
+ export function gray(text: string): string {
55
+ return wrap(text, grayCode);
56
+ }
57
+
58
+ export function grayDark(text: string): string {
59
+ return wrap(text, grayDarkCode);
60
+ }
61
+
62
+ export function errorBadge(text: string): string {
63
+ return wrap(` ${text} `, bgRedCode, whiteCode, boldCode);
64
+ }
65
+
66
+ export function helpHeading(label: string): string {
67
+ return bold(`${label}:`);
68
+ }
69
+
70
+ export function helpExample(command: string, note?: string): string {
71
+ const suffix = note ? ` ${dimGray(`# ${note}`)}` : "";
72
+ return ` ${dimGray("$")} ${command}${suffix}`;
73
+ }
74
+
75
+ export function helpFooter(text: string): string {
76
+ return `\n${gray(`Explore the open-source repo at ${text}`)}\n\n`;
77
+ }
78
+
79
+ export function page(text: string): string {
80
+ const content = text.replace(/^\n+|\n+$/g, "");
81
+ return `\n${content}\n\n`;
82
+ }
83
+
84
+ export function commandErrorPage(
85
+ message: string,
86
+ usage: string,
87
+ example: string,
88
+ ): string {
89
+ return page(
90
+ [
91
+ `${errorBadge("ERROR")} ${red(message)}`,
92
+ "",
93
+ dim(" Usage:"),
94
+ ` ${usage}`,
95
+ "",
96
+ dim(" Example:"),
97
+ ` ${example}`,
98
+ ].join("\n"),
99
+ );
100
+ }
101
+
102
+ export function infoLine(text: string): string {
103
+ return text;
104
+ }
105
+
106
+ export function progressLine(
107
+ completed: number,
108
+ total: number,
109
+ label: string = "skills",
110
+ ): string {
111
+ const width = 10;
112
+ const safeTotal = Math.max(total, 1);
113
+ const safeCompleted = Math.min(Math.max(completed, 0), total);
114
+ const filled = Math.floor((safeCompleted / safeTotal) * width);
115
+ const bar = `[${"#".repeat(filled)}${"-".repeat(width - filled)}]`;
116
+ return `${grayDark("Progress:")} ${textGray(bar)} ${dimGray(`${safeCompleted}/${total} ${label}`)}`;
117
+ }
118
+
119
+ export function summaryLine(text: string): string {
120
+ return grayDark(text);
121
+ }
122
+
123
+ export function statusTag(status: "ok" | "warn" | "fail"): string {
124
+ switch (status) {
125
+ case "ok":
126
+ return grayDark("[ok]");
127
+ case "warn":
128
+ return gray("[warn]");
129
+ case "fail":
130
+ return red("[fail]");
131
+ }
132
+ }
133
+
134
+ export function errorMessage(text: string): string {
135
+ return red(text);
136
+ }
@@ -0,0 +1,102 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import {
3
+ defaultGlobalLockFilePath,
4
+ defaultGlobalSkillsPath,
5
+ } from "../paths/defaults";
6
+
7
+ export type GlobalSkillStateDiff = {
8
+ missingFromLock: string[];
9
+ missingFromDirectory: string[];
10
+ };
11
+
12
+ /**
13
+ * Reads the set of skill names tracked by the global lock file.
14
+ */
15
+ export async function readTrackedGlobalSkillNames(
16
+ lockFilePath: string = defaultGlobalLockFilePath,
17
+ ): Promise<Set<string>> {
18
+ if (!existsSync(lockFilePath)) {
19
+ return new Set();
20
+ }
21
+
22
+ const lock = JSON.parse(readFileSync(lockFilePath, "utf8")) as {
23
+ skills?: Record<string, unknown>;
24
+ };
25
+
26
+ return new Set(Object.keys(lock.skills ?? {}).sort());
27
+ }
28
+
29
+ /**
30
+ * Reads the set of globally installed skill directories from disk.
31
+ */
32
+ export async function readInstalledGlobalSkillNames(
33
+ skillsPath: string = defaultGlobalSkillsPath,
34
+ ): Promise<Set<string>> {
35
+ if (!existsSync(skillsPath)) {
36
+ return new Set();
37
+ }
38
+
39
+ return new Set(
40
+ readdirSync(skillsPath, { withFileTypes: true })
41
+ .filter((entry) => entry.isDirectory())
42
+ .map((entry) => entry.name)
43
+ .sort(),
44
+ );
45
+ }
46
+
47
+ /**
48
+ * Compares tracked and installed global skills in both directions.
49
+ */
50
+ export function diffGlobalSkillState(
51
+ trackedSkillNames: Set<string>,
52
+ installedSkillNames: Set<string>,
53
+ ): GlobalSkillStateDiff {
54
+ // Compare both directions so callers can distinguish "installed but
55
+ // untracked" from "tracked but missing on disk".
56
+ return {
57
+ missingFromLock: Array.from(installedSkillNames)
58
+ .filter((skillName) => !trackedSkillNames.has(skillName))
59
+ .sort((left, right) => left.localeCompare(right)),
60
+ missingFromDirectory: Array.from(trackedSkillNames)
61
+ .filter((skillName) => !installedSkillNames.has(skillName))
62
+ .sort((left, right) => left.localeCompare(right)),
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Converts a global skill mismatch into short, user-facing diagnostics.
68
+ */
69
+ export function formatGlobalSkillStateMismatch(
70
+ globalSkillStateDiff: GlobalSkillStateDiff,
71
+ ): string[] {
72
+ const mismatchLines: string[] = [];
73
+
74
+ // Compare both directions so restore cannot silently succeed with a stale lock.
75
+ if (globalSkillStateDiff.missingFromLock.length > 0) {
76
+ const preview = globalSkillStateDiff.missingFromLock
77
+ .slice(0, 10)
78
+ .join(", ");
79
+ const suffix =
80
+ globalSkillStateDiff.missingFromLock.length > 10 ? ", ..." : "";
81
+ mismatchLines.push(
82
+ `Global skills state mismatch: ${globalSkillStateDiff.missingFromLock.length} installed skill(s) missing from .skill-lock.json.`,
83
+ );
84
+ mismatchLines.push(`Missing tracked entries include: ${preview}${suffix}`);
85
+ }
86
+
87
+ if (globalSkillStateDiff.missingFromDirectory.length > 0) {
88
+ const preview = globalSkillStateDiff.missingFromDirectory
89
+ .slice(0, 10)
90
+ .join(", ");
91
+ const suffix =
92
+ globalSkillStateDiff.missingFromDirectory.length > 10 ? ", ..." : "";
93
+ mismatchLines.push(
94
+ `Global skills state mismatch: ${globalSkillStateDiff.missingFromDirectory.length} tracked skill(s) missing from the skills directory.`,
95
+ );
96
+ mismatchLines.push(
97
+ `Missing installed entries include: ${preview}${suffix}`,
98
+ );
99
+ }
100
+
101
+ return mismatchLines;
102
+ }
@@ -0,0 +1,74 @@
1
+ import { availableParallelism } from "node:os";
2
+
3
+ export type ResolveInstallConcurrencyOptions = {
4
+ configuredConcurrency?: number;
5
+ taskCount: number;
6
+ availableParallelism?: number;
7
+ };
8
+
9
+ const maxAutoConcurrency = 8;
10
+
11
+ function clamp(value: number, min: number, max: number): number {
12
+ return Math.min(Math.max(value, min), max);
13
+ }
14
+
15
+ function normalizeTaskCount(taskCount: number): number {
16
+ return Number.isFinite(taskCount) && taskCount > 0
17
+ ? Math.trunc(taskCount)
18
+ : 1;
19
+ }
20
+
21
+ function detectAvailableParallelism(): number {
22
+ try {
23
+ const detected = availableParallelism();
24
+ return Number.isFinite(detected) && detected > 0 ? detected : 1;
25
+ } catch {
26
+ return 1;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Parses a positive integer CLI flag that controls task concurrency.
32
+ */
33
+ export function parsePositiveIntegerOption(
34
+ value: string,
35
+ optionName: string,
36
+ ): number {
37
+ if (!/^\d+$/.test(value)) {
38
+ throw new Error(`${optionName} requires a positive integer`);
39
+ }
40
+
41
+ const parsed = Number(value);
42
+ if (!Number.isSafeInteger(parsed) || parsed < 1) {
43
+ throw new Error(`${optionName} requires a positive integer`);
44
+ }
45
+
46
+ return parsed;
47
+ }
48
+
49
+ /**
50
+ * Chooses a safe install concurrency from user input, task count, and device capacity.
51
+ */
52
+ export function resolveInstallConcurrency(
53
+ options: ResolveInstallConcurrencyOptions,
54
+ ): number {
55
+ const taskCount = normalizeTaskCount(options.taskCount);
56
+ const maxConcurrency = Math.min(taskCount, maxAutoConcurrency);
57
+
58
+ if (
59
+ typeof options.configuredConcurrency === "number" &&
60
+ Number.isFinite(options.configuredConcurrency)
61
+ ) {
62
+ return clamp(Math.trunc(options.configuredConcurrency), 1, taskCount);
63
+ }
64
+
65
+ const detectedParallelism =
66
+ typeof options.availableParallelism === "number" &&
67
+ Number.isFinite(options.availableParallelism) &&
68
+ options.availableParallelism > 0
69
+ ? options.availableParallelism
70
+ : detectAvailableParallelism();
71
+ const suggestedConcurrency = Math.ceil(detectedParallelism / 2);
72
+
73
+ return clamp(suggestedConcurrency, 1, maxConcurrency);
74
+ }
@@ -0,0 +1,72 @@
1
+ export type ConcurrentFailure = {
2
+ index: number;
3
+ error: unknown;
4
+ };
5
+
6
+ export type RunWithConcurrencyOptions = {
7
+ continueOnError: boolean;
8
+ };
9
+
10
+ function clamp(value: number, min: number, max: number): number {
11
+ return Math.min(Math.max(value, min), max);
12
+ }
13
+
14
+ function normalizeConcurrency(concurrency: number, taskCount: number): number {
15
+ const truncatedConcurrency = Math.trunc(concurrency);
16
+
17
+ if (!Number.isFinite(truncatedConcurrency)) {
18
+ return 1;
19
+ }
20
+
21
+ return clamp(truncatedConcurrency, 1, taskCount);
22
+ }
23
+
24
+ /**
25
+ * Executes async work items with a bounded worker pool and optional fail-fast behavior.
26
+ */
27
+ export async function runWithConcurrency<T>(
28
+ items: T[],
29
+ concurrency: number,
30
+ worker: (item: T, index: number) => Promise<void>,
31
+ options: RunWithConcurrencyOptions,
32
+ ): Promise<ConcurrentFailure[]> {
33
+ if (items.length === 0) {
34
+ return [];
35
+ }
36
+
37
+ const failures: ConcurrentFailure[] = [];
38
+ let nextIndex = 0;
39
+ let firstError: unknown = null;
40
+ const workerCount = normalizeConcurrency(concurrency, items.length);
41
+
42
+ async function runWorker(): Promise<void> {
43
+ while (true) {
44
+ if (!options.continueOnError && firstError !== null) {
45
+ return;
46
+ }
47
+
48
+ const currentIndex = nextIndex;
49
+ if (currentIndex >= items.length) {
50
+ return;
51
+ }
52
+ nextIndex += 1;
53
+
54
+ try {
55
+ await worker(items[currentIndex] as T, currentIndex);
56
+ } catch (error: unknown) {
57
+ failures.push({ index: currentIndex, error });
58
+ if (!options.continueOnError && firstError === null) {
59
+ firstError = error;
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ await Promise.all(Array.from({ length: workerCount }, () => runWorker()));
66
+
67
+ if (!options.continueOnError && firstError !== null) {
68
+ throw firstError;
69
+ }
70
+
71
+ return failures;
72
+ }
@@ -0,0 +1,53 @@
1
+ export type SkillsAddCommandOptions = {
2
+ agents: string[];
3
+ copy: boolean;
4
+ installAll: boolean;
5
+ projectScope: boolean;
6
+ };
7
+
8
+ function encodeShellArg(value: string): string {
9
+ if (/^[A-Za-z0-9_./:-]+$/.test(value)) {
10
+ return value;
11
+ }
12
+
13
+ return JSON.stringify(value);
14
+ }
15
+
16
+ /**
17
+ * Builds a `skills add` invocation that matches the selected restore mode.
18
+ */
19
+ export function buildSkillsAddCommand(
20
+ source: string,
21
+ skills: string[],
22
+ options: SkillsAddCommandOptions,
23
+ ): string[] {
24
+ const command = ["skills", "add", source];
25
+ if (!options.projectScope) {
26
+ command.push("-g");
27
+ }
28
+
29
+ command.push("-y");
30
+
31
+ for (const agent of options.agents) {
32
+ command.push("--agent", agent);
33
+ }
34
+
35
+ if (options.copy) {
36
+ command.push("--copy");
37
+ }
38
+
39
+ if (options.installAll) {
40
+ command.push("--all");
41
+ return command;
42
+ }
43
+
44
+ command.push("--skill", ...skills);
45
+ return command;
46
+ }
47
+
48
+ /**
49
+ * Formats a command array into a shell-safe preview string for logs and dry-runs.
50
+ */
51
+ export function formatCommandPreview(command: string[]): string {
52
+ return command.map(encodeShellArg).join(" ");
53
+ }