@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.
@@ -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
+ };
@@ -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 { type Theme, defaultTheme } from "@spader/dotllm/cli/theme";
6
-
7
- export type OptionDef = {
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; i < path.length; i++) {
11
+ for (let i = 0;i < path.length; i++) {
47
12
  const fmt = t.command;
48
- parts.push(i === last ? fmt(path[i]!) : 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
- if (prev) console.log("");
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) console.log("");
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) desc += ` ${t.dim("(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
- const opts: Record<string, OptionDef> = {
109
- ...(def.options ?? {}),
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) console.log("");
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) console.log("");
79
+ if (prev)
80
+ console.log("");
133
81
  console.log(t.header("commands"));
134
-
135
- const rows: string[][] = [];
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) continue;
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++]!(s), t.arg, t.description]);
92
+ cols(rows, [(s) => rowCmdFmt[ri++](s), t.arg, t.description]);
145
93
  }
146
94
  }
147
-
148
- function fail(def: CommandDef | CliDef, name: string, path: string[] = []) {
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
- function check(def: CommandDef | CliDef, name: string, path: string[] = []) {
167
- return (argv: Record<string, unknown>): boolean => {
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, type RepoEntry } from "@spader/dotllm/core/config";
4
-
5
- function isUrl(value: string): boolean {
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) return null;
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
+ };