@spader/dotllm 1.0.0 → 1.2.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/package.json +14 -18
- package/src/cli/commands/{add.ts → add.js} +89 -81
- package/src/cli/commands/cd.js +45 -0
- package/src/cli/commands/index.js +18 -0
- package/src/cli/commands/link.js +78 -0
- package/src/cli/commands/list.js +42 -0
- package/src/cli/commands/{remove.ts → remove.js} +13 -11
- package/src/cli/commands/{sync.ts → sync.js} +14 -23
- package/src/cli/commands/which.js +30 -0
- package/src/cli/index.js +30 -0
- package/src/cli/layout.js +145 -0
- package/src/cli/prompt.js +23 -0
- package/src/cli/theme.js +22 -0
- package/src/cli/{yargs.ts → yargs.js} +46 -124
- package/src/core/{add.ts → add.js} +19 -45
- package/src/core/config.js +112 -0
- package/src/core/index.js +17 -0
- package/src/core/link.js +18 -0
- package/src/core/{remove.ts → remove.js} +6 -12
- package/src/core/{sync.ts → sync.js} +39 -75
- package/src/core/{unlink.ts → unlink.js} +6 -12
- package/src/cli/commands/index.ts +0 -5
- package/src/cli/commands/link.ts +0 -54
- package/src/cli/commands/list.ts +0 -44
- package/src/cli/index.ts +0 -24
- package/src/cli/layout.ts +0 -200
- package/src/cli/theme.ts +0 -35
- package/src/core/config.ts +0 -126
- package/src/core/index.ts +0 -6
- package/src/core/link.ts +0 -16
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/cli/layout.ts
|
|
3
|
+
import { defaultTheme as theme } from "@spader/dotllm/cli/theme";
|
|
4
|
+
var ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
5
|
+
function clean(value) {
|
|
6
|
+
return value.replace(/[\t\n]/g, " ").replace(ANSI_RE, "");
|
|
7
|
+
}
|
|
8
|
+
function truncateMiddle(value, width) {
|
|
9
|
+
if (width <= 0)
|
|
10
|
+
return "";
|
|
11
|
+
if (value.length <= width)
|
|
12
|
+
return value;
|
|
13
|
+
if (width <= 3)
|
|
14
|
+
return value.slice(0, width);
|
|
15
|
+
const tail = Math.floor((width - 3) / 2);
|
|
16
|
+
const head = width - 3 - tail;
|
|
17
|
+
return `${value.slice(0, head)}...${value.slice(value.length - tail)}`;
|
|
18
|
+
}
|
|
19
|
+
function truncateStart(value, width) {
|
|
20
|
+
if (width <= 0)
|
|
21
|
+
return "";
|
|
22
|
+
if (value.length <= width)
|
|
23
|
+
return value;
|
|
24
|
+
if (width <= 3)
|
|
25
|
+
return "...".slice(0, width);
|
|
26
|
+
return `...${value.slice(value.length - (width - 3))}`;
|
|
27
|
+
}
|
|
28
|
+
function truncateEnd(value, width) {
|
|
29
|
+
if (width <= 0)
|
|
30
|
+
return "";
|
|
31
|
+
if (value.length <= width)
|
|
32
|
+
return value;
|
|
33
|
+
if (width <= 3)
|
|
34
|
+
return "...".slice(0, width);
|
|
35
|
+
return `${value.slice(0, width - 3)}...`;
|
|
36
|
+
}
|
|
37
|
+
function truncate(value, width, mode) {
|
|
38
|
+
if (mode === "start")
|
|
39
|
+
return truncateStart(value, width);
|
|
40
|
+
if (mode === "end")
|
|
41
|
+
return truncateEnd(value, width);
|
|
42
|
+
return truncateMiddle(value, width);
|
|
43
|
+
}
|
|
44
|
+
function fitWidths(natural, available, flex, noTruncate) {
|
|
45
|
+
const widths = [...natural];
|
|
46
|
+
const totalNatural = natural.reduce((sum, width) => sum + width, 0);
|
|
47
|
+
if (totalNatural <= available) {
|
|
48
|
+
return widths;
|
|
49
|
+
}
|
|
50
|
+
let fixedWidth = 0;
|
|
51
|
+
const dynamic = [];
|
|
52
|
+
for (let index = 0;index < widths.length; index++) {
|
|
53
|
+
if (noTruncate[index] || flex[index] === 0) {
|
|
54
|
+
fixedWidth += widths[index] ?? 0;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
dynamic.push({ index, weight: flex[index] ?? 1 });
|
|
58
|
+
}
|
|
59
|
+
const remaining = Math.max(0, available - fixedWidth);
|
|
60
|
+
if (dynamic.length === 0) {
|
|
61
|
+
return widths;
|
|
62
|
+
}
|
|
63
|
+
const totalWeight = dynamic.reduce((sum, item) => sum + item.weight, 0);
|
|
64
|
+
let used = 0;
|
|
65
|
+
for (const item of dynamic) {
|
|
66
|
+
const share = Math.floor(remaining * item.weight / totalWeight);
|
|
67
|
+
const width = Math.max(1, share);
|
|
68
|
+
widths[item.index] = width;
|
|
69
|
+
used += width;
|
|
70
|
+
}
|
|
71
|
+
let extra = remaining - used;
|
|
72
|
+
let cursor = 0;
|
|
73
|
+
while (extra > 0) {
|
|
74
|
+
const item = dynamic[cursor % dynamic.length];
|
|
75
|
+
widths[item.index] = (widths[item.index] ?? 0) + 1;
|
|
76
|
+
extra--;
|
|
77
|
+
cursor++;
|
|
78
|
+
}
|
|
79
|
+
return widths;
|
|
80
|
+
}
|
|
81
|
+
function table(headers, columns, options = {}) {
|
|
82
|
+
if (headers.length === 0)
|
|
83
|
+
return;
|
|
84
|
+
const count = headers.length;
|
|
85
|
+
const gap = 2;
|
|
86
|
+
const rows = columns.reduce((max, column) => Math.max(max, column.length), 0);
|
|
87
|
+
const visibleRows = Math.min(rows, options.maxRows ?? rows);
|
|
88
|
+
const natural = [];
|
|
89
|
+
for (let col = 0;col < count; col++) {
|
|
90
|
+
const headerWidth = clean(headers[col] ?? "").length;
|
|
91
|
+
let width = headerWidth;
|
|
92
|
+
for (let row = 0;row < visibleRows; row++) {
|
|
93
|
+
const value = clean(columns[col]?.[row] ?? "");
|
|
94
|
+
width = Math.max(width, value.length);
|
|
95
|
+
}
|
|
96
|
+
natural[col] = width;
|
|
97
|
+
}
|
|
98
|
+
const maxWidth = options.maxWidth ?? (process.stdout.columns == null ? 120 : process.stdout.columns);
|
|
99
|
+
const available = Math.max(0, maxWidth - gap * (count - 1) - 1);
|
|
100
|
+
const widths = fitWidths(natural, available, options.flex ?? [], options.noTruncate ?? []);
|
|
101
|
+
const header = headers.map((title, col) => truncateMiddle(clean(title), widths[col] ?? 0).padEnd(widths[col] ?? 0)).join(" ");
|
|
102
|
+
process.stdout.write(`${theme.dim(header)}
|
|
103
|
+
`);
|
|
104
|
+
for (let row = 0;row < visibleRows; row++) {
|
|
105
|
+
const cells = [];
|
|
106
|
+
for (let col = 0;col < count; col++) {
|
|
107
|
+
const width = widths[col] ?? 0;
|
|
108
|
+
const source = clean(columns[col]?.[row] ?? "");
|
|
109
|
+
const mode = options.truncate?.[col] ?? "middle";
|
|
110
|
+
const value = options.noTruncate?.[col] ? source : truncate(source, width, mode);
|
|
111
|
+
const padded = width > 0 ? value.padEnd(width) : value;
|
|
112
|
+
const formatted = options.format?.[col]?.(padded, row, col) ?? padded;
|
|
113
|
+
cells.push(formatted);
|
|
114
|
+
}
|
|
115
|
+
process.stdout.write(`${cells.join(" ")}
|
|
116
|
+
`);
|
|
117
|
+
}
|
|
118
|
+
if (rows > visibleRows) {
|
|
119
|
+
process.stdout.write(`${theme.dim("(...truncated)")}
|
|
120
|
+
`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function cols(rows, colorFns) {
|
|
124
|
+
if (rows.length === 0)
|
|
125
|
+
return;
|
|
126
|
+
const widths = rows[0].map((_, col) => {
|
|
127
|
+
let width = 0;
|
|
128
|
+
for (const row of rows) {
|
|
129
|
+
width = Math.max(width, clean(row[col] ?? "").length);
|
|
130
|
+
}
|
|
131
|
+
return width;
|
|
132
|
+
});
|
|
133
|
+
for (const row of rows) {
|
|
134
|
+
const line = row.map((value, col) => {
|
|
135
|
+
const padded = clean(value).padEnd(widths[col] ?? 0);
|
|
136
|
+
return colorFns?.[col]?.(padded) ?? padded;
|
|
137
|
+
}).join(" ");
|
|
138
|
+
process.stdout.write(`${line}
|
|
139
|
+
`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export {
|
|
143
|
+
table,
|
|
144
|
+
cols
|
|
145
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/cli/prompt.ts
|
|
3
|
+
import * as prompts from "@clack/prompts";
|
|
4
|
+
var Prompt;
|
|
5
|
+
((Prompt) => {
|
|
6
|
+
function sync(result) {
|
|
7
|
+
const parts = [];
|
|
8
|
+
if (result.linked.length > 0)
|
|
9
|
+
parts.push(`${result.linked.length} added`);
|
|
10
|
+
if (result.removed.length > 0)
|
|
11
|
+
parts.push(`${result.removed.length} removed`);
|
|
12
|
+
if (result.unchanged.length > 0)
|
|
13
|
+
parts.push(`${result.unchanged.length} unchanged`);
|
|
14
|
+
if (result.missing.length > 0)
|
|
15
|
+
parts.push(`${result.missing.length} missing`);
|
|
16
|
+
if (parts.length > 0)
|
|
17
|
+
prompts.log.step(parts.join(", "));
|
|
18
|
+
}
|
|
19
|
+
Prompt.sync = sync;
|
|
20
|
+
})(Prompt ||= {});
|
|
21
|
+
export {
|
|
22
|
+
Prompt
|
|
23
|
+
};
|
package/src/cli/theme.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/cli/theme.ts
|
|
3
|
+
function rgb(r, g, b) {
|
|
4
|
+
return (value) => `\x1B[38;2;${r};${g};${b}m${value}\x1B[39m`;
|
|
5
|
+
}
|
|
6
|
+
var gray = (value) => rgb(value, value, value);
|
|
7
|
+
var defaultTheme = {
|
|
8
|
+
primary: rgb(114, 161, 136),
|
|
9
|
+
link: rgb(114, 140, 212),
|
|
10
|
+
header: gray(128),
|
|
11
|
+
command: rgb(114, 161, 136),
|
|
12
|
+
arg: rgb(161, 212, 212),
|
|
13
|
+
option: rgb(212, 212, 161),
|
|
14
|
+
type: gray(128),
|
|
15
|
+
description: (value) => value,
|
|
16
|
+
dim: gray(128),
|
|
17
|
+
error: rgb(212, 114, 114),
|
|
18
|
+
success: rgb(114, 212, 136)
|
|
19
|
+
};
|
|
20
|
+
export {
|
|
21
|
+
defaultTheme
|
|
22
|
+
};
|
|
@@ -1,121 +1,69 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/cli/yargs.ts
|
|
1
3
|
import yargs from "yargs";
|
|
2
4
|
import { hideBin } from "yargs/helpers";
|
|
3
5
|
import pc from "picocolors";
|
|
4
6
|
import { cols } from "@spader/dotllm/cli/layout";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
alias?: string;
|
|
9
|
-
type: "string" | "number" | "boolean" | "array";
|
|
10
|
-
description: string;
|
|
11
|
-
required?: boolean;
|
|
12
|
-
default?: unknown;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export type Options = Record<string, OptionDef>;
|
|
16
|
-
|
|
17
|
-
export type PositionalDef = {
|
|
18
|
-
type: "string" | "number";
|
|
19
|
-
description: string;
|
|
20
|
-
required?: boolean;
|
|
21
|
-
default?: unknown;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export type Positionals = Record<string, PositionalDef>;
|
|
25
|
-
|
|
26
|
-
export type CommandDef = {
|
|
27
|
-
description: string;
|
|
28
|
-
summary?: string;
|
|
29
|
-
hidden?: boolean;
|
|
30
|
-
positionals?: Positionals;
|
|
31
|
-
options?: Options;
|
|
32
|
-
commands?: Record<string, CommandDef>;
|
|
33
|
-
handler?: (argv: Record<string, unknown>) => void | Promise<void>;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export type CliDef = {
|
|
37
|
-
name: string;
|
|
38
|
-
description: string;
|
|
39
|
-
options?: Options;
|
|
40
|
-
commands: Record<string, CommandDef>;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
function usage(def: CommandDef | CliDef, path: string[], t: Theme): string {
|
|
44
|
-
const parts: string[] = [];
|
|
7
|
+
import { defaultTheme } from "@spader/dotllm/cli/theme";
|
|
8
|
+
function usage(def, path, t) {
|
|
9
|
+
const parts = [];
|
|
45
10
|
const last = path.length - 1;
|
|
46
|
-
for (let i = 0;
|
|
11
|
+
for (let i = 0;i < path.length; i++) {
|
|
47
12
|
const fmt = t.command;
|
|
48
|
-
parts.push(i === last ? fmt(path[i]
|
|
13
|
+
parts.push(i === last ? fmt(path[i]) : path[i]);
|
|
49
14
|
}
|
|
50
|
-
|
|
51
15
|
if ("positionals" in def && def.positionals) {
|
|
52
16
|
for (const [k, v] of Object.entries(def.positionals)) {
|
|
53
17
|
const name = t.arg(`$${k}`);
|
|
54
18
|
parts.push(v.required ? name : `[${name}]`);
|
|
55
19
|
}
|
|
56
20
|
}
|
|
57
|
-
|
|
58
21
|
if (def.options && Object.keys(def.options).length > 0) {
|
|
59
22
|
parts.push(t.dim("[options]"));
|
|
60
23
|
}
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
"commands" in def &&
|
|
64
|
-
def.commands &&
|
|
65
|
-
Object.keys(def.commands).length > 0
|
|
66
|
-
) {
|
|
24
|
+
if ("commands" in def && def.commands && Object.keys(def.commands).length > 0) {
|
|
67
25
|
parts.push(t.arg("$command"));
|
|
68
26
|
}
|
|
69
|
-
|
|
70
27
|
return parts.join(" ");
|
|
71
28
|
}
|
|
72
|
-
|
|
73
|
-
export function help(
|
|
74
|
-
def: CommandDef | CliDef,
|
|
75
|
-
name: string,
|
|
76
|
-
path: string[] = [],
|
|
77
|
-
t: Theme = defaultTheme,
|
|
78
|
-
): void {
|
|
29
|
+
function help(def, name, path = [], t = defaultTheme) {
|
|
79
30
|
let prev = false;
|
|
80
|
-
|
|
81
31
|
if (def.description) {
|
|
82
32
|
console.log(t.description(def.description));
|
|
83
33
|
prev = true;
|
|
84
34
|
}
|
|
85
|
-
|
|
86
|
-
|
|
35
|
+
if (prev)
|
|
36
|
+
console.log("");
|
|
87
37
|
console.log(t.header("usage:"));
|
|
88
38
|
console.log(` ${usage(def, [name, ...path], t)}`);
|
|
89
39
|
prev = true;
|
|
90
|
-
|
|
91
40
|
const pos = "positionals" in def ? def.positionals : undefined;
|
|
92
41
|
if (pos && Object.keys(pos).length > 0) {
|
|
93
|
-
if (prev)
|
|
42
|
+
if (prev)
|
|
43
|
+
console.log("");
|
|
94
44
|
console.log(t.header("arguments"));
|
|
95
45
|
prev = true;
|
|
96
|
-
|
|
97
|
-
const rows: string[][] = [];
|
|
46
|
+
const rows = [];
|
|
98
47
|
for (const [k, v] of Object.entries(pos)) {
|
|
99
48
|
let desc = v.description;
|
|
100
49
|
if (v.default !== undefined)
|
|
101
50
|
desc += ` ${t.dim(`(default: ${v.default})`)}`;
|
|
102
|
-
if (v.required)
|
|
51
|
+
if (v.required)
|
|
52
|
+
desc += ` ${t.dim("(required)")}`;
|
|
103
53
|
rows.push([` ${k}`, v.type, desc]);
|
|
104
54
|
}
|
|
105
55
|
cols(rows, [t.arg, t.type, t.description]);
|
|
106
56
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
help: { alias: "h", type: "boolean", description: "Show help" },
|
|
57
|
+
const opts = {
|
|
58
|
+
...def.options ?? {},
|
|
59
|
+
help: { alias: "h", type: "boolean", description: "Show help" }
|
|
111
60
|
};
|
|
112
|
-
|
|
113
61
|
if (Object.keys(opts).length > 0) {
|
|
114
|
-
if (prev)
|
|
62
|
+
if (prev)
|
|
63
|
+
console.log("");
|
|
115
64
|
console.log(t.header("options"));
|
|
116
65
|
prev = true;
|
|
117
|
-
|
|
118
|
-
const rows: string[][] = [];
|
|
66
|
+
const rows = [];
|
|
119
67
|
for (const [k, v] of Object.entries(opts)) {
|
|
120
68
|
const short = v.alias ? `-${v.alias}, ` : " ";
|
|
121
69
|
let desc = v.description;
|
|
@@ -126,35 +74,31 @@ export function help(
|
|
|
126
74
|
}
|
|
127
75
|
cols(rows, [t.option, t.type, t.description]);
|
|
128
76
|
}
|
|
129
|
-
|
|
130
77
|
const cmds = "commands" in def ? def.commands : undefined;
|
|
131
78
|
if (cmds && Object.keys(cmds).length > 0) {
|
|
132
|
-
if (prev)
|
|
79
|
+
if (prev)
|
|
80
|
+
console.log("");
|
|
133
81
|
console.log(t.header("commands"));
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
const rowCmdFmt: ((s: string) => string)[] = [];
|
|
82
|
+
const rows = [];
|
|
83
|
+
const rowCmdFmt = [];
|
|
137
84
|
for (const [k, v] of Object.entries(cmds)) {
|
|
138
|
-
if (v.hidden)
|
|
85
|
+
if (v.hidden)
|
|
86
|
+
continue;
|
|
139
87
|
const args = v.positionals ? Object.keys(v.positionals).join(" ") : "";
|
|
140
88
|
rows.push([` ${k}`, args, v.summary ?? v.description]);
|
|
141
89
|
rowCmdFmt.push(t.command);
|
|
142
90
|
}
|
|
143
91
|
let ri = 0;
|
|
144
|
-
cols(rows, [(s) => rowCmdFmt[ri++]
|
|
92
|
+
cols(rows, [(s) => rowCmdFmt[ri++](s), t.arg, t.description]);
|
|
145
93
|
}
|
|
146
94
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return (msg: string | null): void => {
|
|
95
|
+
function fail(def, name, path = []) {
|
|
96
|
+
return (msg) => {
|
|
150
97
|
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
151
98
|
help(def, name, path);
|
|
152
99
|
process.exit(0);
|
|
153
100
|
}
|
|
154
|
-
if (
|
|
155
|
-
msg?.includes("You must specify") ||
|
|
156
|
-
msg?.includes("Not enough non-option arguments")
|
|
157
|
-
) {
|
|
101
|
+
if (msg?.includes("You must specify") || msg?.includes("Not enough non-option arguments")) {
|
|
158
102
|
help(def, name, path);
|
|
159
103
|
process.exit(1);
|
|
160
104
|
}
|
|
@@ -162,10 +106,9 @@ function fail(def: CommandDef | CliDef, name: string, path: string[] = []) {
|
|
|
162
106
|
process.exit(1);
|
|
163
107
|
};
|
|
164
108
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const args = argv as Record<string, unknown> & { _: unknown[]; help?: boolean };
|
|
109
|
+
function check(def, name, path = []) {
|
|
110
|
+
return (argv) => {
|
|
111
|
+
const args = argv;
|
|
169
112
|
if (args.help && args._.length === path.length) {
|
|
170
113
|
help(def, name, path);
|
|
171
114
|
process.exit(0);
|
|
@@ -173,24 +116,17 @@ function check(def: CommandDef | CliDef, name: string, path: string[] = []) {
|
|
|
173
116
|
return true;
|
|
174
117
|
};
|
|
175
118
|
}
|
|
176
|
-
|
|
177
|
-
function configure(
|
|
178
|
-
y: ReturnType<typeof yargs>,
|
|
179
|
-
def: CommandDef | CliDef,
|
|
180
|
-
root: string,
|
|
181
|
-
path: string[],
|
|
182
|
-
): void {
|
|
119
|
+
function configure(y, def, root, path) {
|
|
183
120
|
if ("positionals" in def && def.positionals) {
|
|
184
121
|
for (const [k, v] of Object.entries(def.positionals)) {
|
|
185
122
|
y.positional(k, {
|
|
186
123
|
type: v.type,
|
|
187
124
|
describe: v.description,
|
|
188
125
|
demandOption: v.required,
|
|
189
|
-
default: v.default
|
|
126
|
+
default: v.default
|
|
190
127
|
});
|
|
191
128
|
}
|
|
192
129
|
}
|
|
193
|
-
|
|
194
130
|
if (def.options) {
|
|
195
131
|
for (const [k, v] of Object.entries(def.options)) {
|
|
196
132
|
y.option(k, {
|
|
@@ -198,48 +134,34 @@ function configure(
|
|
|
198
134
|
type: v.type,
|
|
199
135
|
describe: v.description,
|
|
200
136
|
demandOption: v.required,
|
|
201
|
-
default: v.default
|
|
137
|
+
default: v.default
|
|
202
138
|
});
|
|
203
139
|
}
|
|
204
140
|
}
|
|
205
|
-
|
|
206
141
|
if ("commands" in def && def.commands) {
|
|
207
142
|
for (const [k, v] of Object.entries(def.commands)) {
|
|
208
143
|
command(y, k, v, root, path);
|
|
209
144
|
}
|
|
210
145
|
y.demandCommand(1, "You must specify a command");
|
|
211
146
|
}
|
|
212
|
-
|
|
213
|
-
y.help(false)
|
|
214
|
-
.option("help", { alias: "h", type: "boolean", describe: "Show help" })
|
|
215
|
-
.check(check(def, root, path))
|
|
216
|
-
.fail(fail(def, root, path));
|
|
147
|
+
y.help(false).option("help", { alias: "h", type: "boolean", describe: "Show help" }).check(check(def, root, path)).fail(fail(def, root, path));
|
|
217
148
|
}
|
|
218
|
-
|
|
219
|
-
function command(
|
|
220
|
-
y: ReturnType<typeof yargs>,
|
|
221
|
-
name: string,
|
|
222
|
-
def: CommandDef,
|
|
223
|
-
root: string,
|
|
224
|
-
path: string[],
|
|
225
|
-
): void {
|
|
149
|
+
function command(y, name, def, root, path) {
|
|
226
150
|
let cmd = name;
|
|
227
151
|
if (def.positionals) {
|
|
228
152
|
for (const [k, v] of Object.entries(def.positionals)) {
|
|
229
153
|
cmd += v.required ? ` <${k}>` : ` [${k}]`;
|
|
230
154
|
}
|
|
231
155
|
}
|
|
232
|
-
y.command(
|
|
233
|
-
cmd,
|
|
234
|
-
def.description,
|
|
235
|
-
(inner: ReturnType<typeof yargs>) => configure(inner, def, root, [...path, name]),
|
|
236
|
-
def.handler as (argv: Record<string, unknown>) => void,
|
|
237
|
-
);
|
|
156
|
+
y.command(cmd, def.description, (inner) => configure(inner, def, root, [...path, name]), def.handler);
|
|
238
157
|
}
|
|
239
|
-
|
|
240
|
-
export function build(def: CliDef): ReturnType<typeof yargs> {
|
|
158
|
+
function build(def) {
|
|
241
159
|
const y = yargs(hideBin(process.argv)).scriptName(def.name);
|
|
242
160
|
configure(y, def, def.name, []);
|
|
243
161
|
y.strict();
|
|
244
162
|
return y;
|
|
245
163
|
}
|
|
164
|
+
export {
|
|
165
|
+
help,
|
|
166
|
+
build
|
|
167
|
+
};
|
|
@@ -1,100 +1,74 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/core/add.ts
|
|
1
3
|
import fs from "fs";
|
|
2
4
|
import path from "path";
|
|
3
|
-
import { Config
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
return value.startsWith("http://") ||
|
|
7
|
-
value.startsWith("https://") ||
|
|
8
|
-
value.startsWith("git@") ||
|
|
9
|
-
value.startsWith("ssh://");
|
|
5
|
+
import { Config } from "@spader/dotllm/core/config";
|
|
6
|
+
function isUrl(value) {
|
|
7
|
+
return value.startsWith("http://") || value.startsWith("https://") || value.startsWith("git@") || value.startsWith("ssh://");
|
|
10
8
|
}
|
|
11
|
-
|
|
12
|
-
function nameFromGitRemote(dir: string): string | null {
|
|
9
|
+
function nameFromGitRemote(dir) {
|
|
13
10
|
const result = Bun.spawnSync(["git", "remote", "get-url", "origin"], {
|
|
14
11
|
cwd: dir,
|
|
15
12
|
stdout: "pipe",
|
|
16
|
-
stderr: "pipe"
|
|
13
|
+
stderr: "pipe"
|
|
17
14
|
});
|
|
18
|
-
if (result.exitCode !== 0)
|
|
15
|
+
if (result.exitCode !== 0)
|
|
16
|
+
return null;
|
|
19
17
|
const url = result.stdout.toString().trim();
|
|
20
18
|
return nameFromUrl(url);
|
|
21
19
|
}
|
|
22
|
-
|
|
23
|
-
function nameFromUrl(url: string): string {
|
|
20
|
+
function nameFromUrl(url) {
|
|
24
21
|
const base = url.split("/").pop() ?? url;
|
|
25
22
|
return base.replace(/\.git$/, "");
|
|
26
23
|
}
|
|
27
|
-
|
|
28
|
-
export type AddResult = {
|
|
29
|
-
ok: true;
|
|
30
|
-
entry: RepoEntry;
|
|
31
|
-
storePath: string;
|
|
32
|
-
} | {
|
|
33
|
-
ok: false;
|
|
34
|
-
error: string;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export async function add(uri: string, name?: string, description?: string): Promise<AddResult> {
|
|
24
|
+
async function add(uri, name, description) {
|
|
38
25
|
const desc = description ?? "";
|
|
39
|
-
|
|
40
26
|
if (isUrl(uri)) {
|
|
41
27
|
return cloneUrl(uri, name, desc);
|
|
42
28
|
}
|
|
43
|
-
|
|
44
29
|
return linkLocal(uri, name, desc);
|
|
45
30
|
}
|
|
46
|
-
|
|
47
|
-
async function cloneUrl(url: string, name: string | undefined, description: string): Promise<AddResult> {
|
|
31
|
+
async function cloneUrl(url, name, description) {
|
|
48
32
|
const resolved = name ?? nameFromUrl(url);
|
|
49
33
|
const store = Config.storeDir();
|
|
50
34
|
fs.mkdirSync(store, { recursive: true });
|
|
51
|
-
|
|
52
35
|
const target = path.join(store, resolved);
|
|
53
|
-
|
|
54
36
|
if (!fs.existsSync(target)) {
|
|
55
37
|
const proc = Bun.spawn(["git", "clone", url, target], {
|
|
56
38
|
stdout: "pipe",
|
|
57
|
-
stderr: "pipe"
|
|
39
|
+
stderr: "pipe"
|
|
58
40
|
});
|
|
59
41
|
const code = await proc.exited;
|
|
60
|
-
|
|
61
42
|
if (code !== 0) {
|
|
62
43
|
const msg = await new Response(proc.stderr).text();
|
|
63
44
|
return { ok: false, error: `git clone failed: ${msg.trim()}` };
|
|
64
45
|
}
|
|
65
46
|
}
|
|
66
|
-
|
|
67
|
-
const entry: RepoEntry = { kind: "url", name: resolved, uri: url, description };
|
|
47
|
+
const entry = { kind: "url", name: resolved, uri: url, description };
|
|
68
48
|
const global = Config.Global.read();
|
|
69
49
|
Config.Global.write(Config.Global.add(global, entry));
|
|
70
|
-
|
|
71
50
|
return { ok: true, entry, storePath: target };
|
|
72
51
|
}
|
|
73
|
-
|
|
74
|
-
function linkLocal(raw: string, name: string | undefined, description: string): AddResult {
|
|
52
|
+
function linkLocal(raw, name, description) {
|
|
75
53
|
const resolved = path.resolve(raw);
|
|
76
|
-
|
|
77
54
|
if (!fs.existsSync(resolved)) {
|
|
78
55
|
return { ok: false, error: `Path does not exist: ${resolved}` };
|
|
79
56
|
}
|
|
80
|
-
|
|
81
57
|
if (!fs.statSync(resolved).isDirectory()) {
|
|
82
58
|
return { ok: false, error: `Not a directory: ${resolved}` };
|
|
83
59
|
}
|
|
84
|
-
|
|
85
60
|
const store = Config.storeDir();
|
|
86
61
|
fs.mkdirSync(store, { recursive: true });
|
|
87
|
-
|
|
88
62
|
const finalName = name ?? nameFromGitRemote(resolved) ?? path.basename(resolved);
|
|
89
63
|
const target = path.join(store, finalName);
|
|
90
|
-
|
|
91
64
|
if (!fs.existsSync(target)) {
|
|
92
65
|
fs.symlinkSync(resolved, target, "dir");
|
|
93
66
|
}
|
|
94
|
-
|
|
95
|
-
const entry: RepoEntry = { kind: "file", name: finalName, uri: resolved, description };
|
|
67
|
+
const entry = { kind: "file", name: finalName, uri: resolved, description };
|
|
96
68
|
const global = Config.Global.read();
|
|
97
69
|
Config.Global.write(Config.Global.add(global, entry));
|
|
98
|
-
|
|
99
70
|
return { ok: true, entry, storePath: target };
|
|
100
71
|
}
|
|
72
|
+
export {
|
|
73
|
+
add
|
|
74
|
+
};
|