@spader/dotllm 1.3.0 → 1.3.2
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 +1 -1
- package/src/cli/commands/add.js +2 -0
- package/src/cli/commands/completions.js +216 -0
- package/src/cli/commands/index.js +4 -2
- package/src/cli/commands/link.js +2 -0
- package/src/cli/commands/list.js +2 -0
- package/src/cli/commands/remove.js +2 -0
- package/src/cli/commands/sync.js +19 -4
- package/src/cli/commands/which.js +20 -6
- package/src/cli/index.js +3 -2
- package/src/cli/layout.js +2 -0
- package/src/cli/prompt.js +2 -0
- package/src/cli/theme.js +2 -0
- package/src/cli/yargs.js +6 -1
- package/src/core/add.js +3 -1
- package/src/core/config.js +24 -6
- package/src/core/index.js +2 -0
- package/src/core/link.js +3 -1
- package/src/core/remove.js +4 -2
- package/src/core/sync.js +81 -16
- package/src/core/unlink.js +6 -3
- package/src/cli/commands/cd.js +0 -45
package/package.json
CHANGED
package/src/cli/commands/add.js
CHANGED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
4
|
+
// src/cli/commands/completions.ts
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { Config } from "@spader/dotllm/core";
|
|
9
|
+
import { defaultTheme as t } from "@spader/dotllm/cli/theme";
|
|
10
|
+
var BASH = `_dotllm() {
|
|
11
|
+
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
12
|
+
|
|
13
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
14
|
+
COMPREPLY=( $(compgen -W "add remove list link sync which completions" -- "\${cur}") )
|
|
15
|
+
return
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
case "\${COMP_WORDS[1]}" in
|
|
19
|
+
which|link)
|
|
20
|
+
local repos
|
|
21
|
+
repos=$(dotllm completions --names 2>/dev/null)
|
|
22
|
+
COMPREPLY=( $(compgen -W "\${repos}" -- "\${cur}") )
|
|
23
|
+
;;
|
|
24
|
+
esac
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
complete -F _dotllm dotllm`;
|
|
28
|
+
var ZSH = `#compdef dotllm
|
|
29
|
+
|
|
30
|
+
_dotllm() {
|
|
31
|
+
local -a commands
|
|
32
|
+
commands=(
|
|
33
|
+
'add:Register a new repo'
|
|
34
|
+
'remove:Remove a repo from the registry'
|
|
35
|
+
'list:Show the registry'
|
|
36
|
+
'link:Link references'
|
|
37
|
+
'sync:Sync linked repos'
|
|
38
|
+
'which:Show repo store path'
|
|
39
|
+
'completions:Output shell completions'
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
_arguments -C '1:command:->cmd' '*::arg:->args'
|
|
43
|
+
|
|
44
|
+
case "$state" in
|
|
45
|
+
cmd)
|
|
46
|
+
_describe 'command' commands
|
|
47
|
+
;;
|
|
48
|
+
args)
|
|
49
|
+
case "\${words[1]}" in
|
|
50
|
+
which|link)
|
|
51
|
+
local -a repos
|
|
52
|
+
repos=(\${(f)"$(dotllm completions --names 2>/dev/null)"})
|
|
53
|
+
_describe 'repo' repos
|
|
54
|
+
;;
|
|
55
|
+
esac
|
|
56
|
+
;;
|
|
57
|
+
esac
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_dotllm`;
|
|
61
|
+
var FISH = `complete -c dotllm -f
|
|
62
|
+
complete -c dotllm -n "__fish_use_subcommand" -a add -d "Register a new repo"
|
|
63
|
+
complete -c dotllm -n "__fish_use_subcommand" -a remove -d "Remove a repo from the registry"
|
|
64
|
+
complete -c dotllm -n "__fish_use_subcommand" -a list -d "Show the registry"
|
|
65
|
+
complete -c dotllm -n "__fish_use_subcommand" -a link -d "Link references"
|
|
66
|
+
complete -c dotllm -n "__fish_use_subcommand" -a sync -d "Sync linked repos"
|
|
67
|
+
complete -c dotllm -n "__fish_use_subcommand" -a which -d "Show repo store path"
|
|
68
|
+
complete -c dotllm -n "__fish_use_subcommand" -a completions -d "Output shell completions"
|
|
69
|
+
complete -c dotllm -n "__fish_seen_subcommand_from which link" -a "(dotllm completions --names 2>/dev/null)"`;
|
|
70
|
+
var CANARY = "@dotllm_completions";
|
|
71
|
+
function completionFile(shell) {
|
|
72
|
+
return path.join(path.dirname(Config.storeDir()), `completions.${shell}`);
|
|
73
|
+
}
|
|
74
|
+
function hookSnippet(shell) {
|
|
75
|
+
const file = completionFile(shell);
|
|
76
|
+
switch (shell) {
|
|
77
|
+
case "bash":
|
|
78
|
+
return `
|
|
79
|
+
# ${CANARY}
|
|
80
|
+
# Installed by the dotllm CLI
|
|
81
|
+
[ -f "${file}" ] && source "${file}"
|
|
82
|
+
`;
|
|
83
|
+
case "zsh":
|
|
84
|
+
return `
|
|
85
|
+
# ${CANARY}
|
|
86
|
+
# Installed by the dotllm CLI
|
|
87
|
+
[ -f "${file}" ] && source "${file}"
|
|
88
|
+
`;
|
|
89
|
+
case "fish":
|
|
90
|
+
return `
|
|
91
|
+
# ${CANARY}
|
|
92
|
+
# Installed by the dotllm CLI
|
|
93
|
+
test -f "${file}"; and source "${file}"
|
|
94
|
+
`;
|
|
95
|
+
default:
|
|
96
|
+
return "";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function rcPath(shell) {
|
|
100
|
+
const home = os.homedir();
|
|
101
|
+
switch (shell) {
|
|
102
|
+
case "bash": {
|
|
103
|
+
const bashrc = path.join(home, ".bashrc");
|
|
104
|
+
if (fs.existsSync(bashrc))
|
|
105
|
+
return bashrc;
|
|
106
|
+
return path.join(home, ".bash_profile");
|
|
107
|
+
}
|
|
108
|
+
case "zsh":
|
|
109
|
+
return path.join(home, ".zshrc");
|
|
110
|
+
case "fish":
|
|
111
|
+
return path.join(home, ".config", "fish", "config.fish");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
var install = {
|
|
115
|
+
description: "Add completions to your shell rc file",
|
|
116
|
+
summary: "Install completions",
|
|
117
|
+
handler: async (argv) => {
|
|
118
|
+
if (process.platform === "win32") {
|
|
119
|
+
console.log(t.dim("Shell completions are not supported on Windows."));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const shell = detectShell();
|
|
123
|
+
const rc = rcPath(shell);
|
|
124
|
+
if (!rc) {
|
|
125
|
+
console.error(t.error(`Could not determine rc file for shell: "${shell}"`));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const script = shellScript(shell);
|
|
130
|
+
if (!script) {
|
|
131
|
+
console.error(t.error(`Unknown shell: "${shell}"`));
|
|
132
|
+
process.exit(1);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const cached = completionFile(shell);
|
|
136
|
+
fs.mkdirSync(path.dirname(cached), { recursive: true });
|
|
137
|
+
fs.writeFileSync(cached, script + `
|
|
138
|
+
`);
|
|
139
|
+
const existing = fs.existsSync(rc) ? fs.readFileSync(rc, "utf-8") : "";
|
|
140
|
+
if (existing.includes(CANARY)) {
|
|
141
|
+
console.log(t.dim(`Updated ${cached}`));
|
|
142
|
+
console.log(t.dim(`Already sourced from ${rc}`));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
fs.appendFileSync(rc, hookSnippet(shell));
|
|
146
|
+
console.log(`Installed completions in ${t.link(rc)}`);
|
|
147
|
+
console.log(t.dim(`Restart your shell or run: source ${rc}`));
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
var command = {
|
|
151
|
+
description: "Output shell completion script for bash, zsh, or fish",
|
|
152
|
+
summary: "Shell completions",
|
|
153
|
+
options: {
|
|
154
|
+
shell: {
|
|
155
|
+
alias: "s",
|
|
156
|
+
type: "string",
|
|
157
|
+
description: "Shell type: bash, zsh, or fish"
|
|
158
|
+
},
|
|
159
|
+
names: {
|
|
160
|
+
type: "boolean",
|
|
161
|
+
description: "Print repo names (used internally by completions)"
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
commands: {
|
|
165
|
+
install
|
|
166
|
+
},
|
|
167
|
+
handler: async (argv) => {
|
|
168
|
+
if (process.platform === "win32") {
|
|
169
|
+
console.log(t.dim("Shell completions are not supported on Windows."));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (argv.names) {
|
|
173
|
+
const { Config: Config2 } = await import("@spader/dotllm/core");
|
|
174
|
+
const global = Config2.Global.read();
|
|
175
|
+
for (const r of global.repos)
|
|
176
|
+
console.log(r.name);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const shell = typeof argv.shell === "string" && argv.shell.length > 0 ? argv.shell : detectShell();
|
|
180
|
+
switch (shell) {
|
|
181
|
+
case "bash":
|
|
182
|
+
console.log(BASH);
|
|
183
|
+
break;
|
|
184
|
+
case "zsh":
|
|
185
|
+
console.log(ZSH);
|
|
186
|
+
break;
|
|
187
|
+
case "fish":
|
|
188
|
+
console.log(FISH);
|
|
189
|
+
break;
|
|
190
|
+
default:
|
|
191
|
+
console.error(t.error(`Unknown shell: "${shell}". Use bash, zsh, or fish.`));
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
function shellScript(shell) {
|
|
197
|
+
switch (shell) {
|
|
198
|
+
case "bash":
|
|
199
|
+
return BASH;
|
|
200
|
+
case "zsh":
|
|
201
|
+
return ZSH;
|
|
202
|
+
case "fish":
|
|
203
|
+
return FISH;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function detectShell() {
|
|
207
|
+
const login = process.env.SHELL ?? "";
|
|
208
|
+
if (login.endsWith("/fish"))
|
|
209
|
+
return "fish";
|
|
210
|
+
if (login.endsWith("/zsh"))
|
|
211
|
+
return "zsh";
|
|
212
|
+
return "bash";
|
|
213
|
+
}
|
|
214
|
+
export {
|
|
215
|
+
command
|
|
216
|
+
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
2
4
|
// src/cli/commands/index.ts
|
|
3
5
|
import { command } from "@spader/dotllm/cli/commands/add";
|
|
4
6
|
import { command as command2 } from "@spader/dotllm/cli/commands/remove";
|
|
@@ -6,13 +8,13 @@ import { command as command3 } from "@spader/dotllm/cli/commands/list";
|
|
|
6
8
|
import { command as command4 } from "@spader/dotllm/cli/commands/link";
|
|
7
9
|
import { command as command5 } from "@spader/dotllm/cli/commands/sync";
|
|
8
10
|
import { command as command6 } from "@spader/dotllm/cli/commands/which";
|
|
9
|
-
import { command as command7 } from "@spader/dotllm/cli/commands/
|
|
11
|
+
import { command as command7 } from "@spader/dotllm/cli/commands/completions";
|
|
10
12
|
export {
|
|
11
13
|
command6 as which,
|
|
12
14
|
command5 as sync,
|
|
13
15
|
command2 as remove,
|
|
14
16
|
command3 as list,
|
|
15
17
|
command4 as link,
|
|
16
|
-
command7 as
|
|
18
|
+
command7 as completions,
|
|
17
19
|
command as add
|
|
18
20
|
};
|
package/src/cli/commands/link.js
CHANGED
package/src/cli/commands/list.js
CHANGED
package/src/cli/commands/sync.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
2
4
|
// src/cli/commands/sync.ts
|
|
3
5
|
import * as prompts from "@clack/prompts";
|
|
4
6
|
import { pull, sync } from "@spader/dotllm/core";
|
|
@@ -7,7 +9,14 @@ import { Prompt } from "@spader/dotllm/cli/prompt";
|
|
|
7
9
|
var command = {
|
|
8
10
|
description: "Re-create symlinks from .llm/dotllm.json",
|
|
9
11
|
summary: "Sync symlinks from local config",
|
|
10
|
-
|
|
12
|
+
options: {
|
|
13
|
+
force: {
|
|
14
|
+
alias: "f",
|
|
15
|
+
type: "boolean",
|
|
16
|
+
description: "Pull all repos regardless of recent access"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
handler: async (argv) => {
|
|
11
20
|
prompts.intro("dotllm sync");
|
|
12
21
|
const result = sync();
|
|
13
22
|
if (result.linked.length === 0 && result.removed.length === 0 && result.missing.length === 0 && result.unchanged.length === 0) {
|
|
@@ -20,15 +29,21 @@ var command = {
|
|
|
20
29
|
if (refs.length === 0) {
|
|
21
30
|
return;
|
|
22
31
|
}
|
|
32
|
+
const force = argv.force === true;
|
|
23
33
|
const spinner2 = prompts.spinner();
|
|
24
|
-
spinner2.start(`
|
|
25
|
-
const pulled = await pull(refs);
|
|
34
|
+
spinner2.start(`Checking ${refs.length} linked repo${refs.length === 1 ? "" : "s"}`);
|
|
35
|
+
const pulled = await pull(refs, { force });
|
|
26
36
|
if (pulled.failed.length > 0) {
|
|
27
37
|
spinner2.stop(t.error(`pull failed for ${pulled.failed.length} repo${pulled.failed.length === 1 ? "" : "s"}`));
|
|
28
38
|
process.exit(1);
|
|
29
39
|
return;
|
|
30
40
|
}
|
|
31
|
-
|
|
41
|
+
const parts = [];
|
|
42
|
+
if (pulled.pulled.length > 0)
|
|
43
|
+
parts.push(`${t.success("pulled")} ${pulled.pulled.length}`);
|
|
44
|
+
if (pulled.skipped.length > 0)
|
|
45
|
+
parts.push(t.dim(`${pulled.skipped.length} skipped`));
|
|
46
|
+
spinner2.stop(parts.length > 0 ? parts.join(" ") : t.dim("nothing to pull"));
|
|
32
47
|
}
|
|
33
48
|
};
|
|
34
49
|
export {
|
|
@@ -1,28 +1,42 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
2
4
|
// src/cli/commands/which.ts
|
|
3
5
|
import path from "path";
|
|
4
6
|
import { Config } from "@spader/dotllm/core";
|
|
5
7
|
import { defaultTheme as t } from "@spader/dotllm/cli/theme";
|
|
6
8
|
var command = {
|
|
7
|
-
description: "Print the absolute path to a repo in the store",
|
|
9
|
+
description: "Print the absolute path to a repo in the store (prefix match, shortest wins)",
|
|
8
10
|
summary: "Show repo store path",
|
|
9
11
|
positionals: {
|
|
10
12
|
name: {
|
|
11
13
|
type: "string",
|
|
12
|
-
description: "Name of the repo",
|
|
14
|
+
description: "Name (or prefix) of the repo",
|
|
13
15
|
required: true
|
|
14
16
|
}
|
|
15
17
|
},
|
|
16
18
|
handler: (argv) => {
|
|
17
19
|
const name = String(argv.name);
|
|
18
20
|
const global = Config.Global.read();
|
|
19
|
-
const
|
|
20
|
-
if (
|
|
21
|
-
console.
|
|
21
|
+
const exact = Config.Global.find(global, name);
|
|
22
|
+
if (exact) {
|
|
23
|
+
console.log(path.join(Config.storeDir(), exact.name));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const lower = name.toLowerCase();
|
|
27
|
+
const matches = global.repos.filter((r) => r.name.toLowerCase().startsWith(lower)).sort((a, b) => a.name.length - b.name.length);
|
|
28
|
+
if (matches.length === 0) {
|
|
29
|
+
console.error(t.error(`No repo matching "${name}" in registry`));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (matches.length > 1 && matches[0].name.length === matches[1].name.length) {
|
|
34
|
+
const names = matches.filter((m) => m.name.length === matches[0].name.length).map((m) => m.name);
|
|
35
|
+
console.error(t.error(`Ambiguous prefix "${name}": ${names.join(", ")}`));
|
|
22
36
|
process.exit(1);
|
|
23
37
|
return;
|
|
24
38
|
}
|
|
25
|
-
console.log(path.join(Config.storeDir(), name));
|
|
39
|
+
console.log(path.join(Config.storeDir(), matches[0].name));
|
|
26
40
|
}
|
|
27
41
|
};
|
|
28
42
|
export {
|
package/src/cli/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
+
var __require = import.meta.require;
|
|
3
4
|
|
|
4
5
|
// src/cli/index.ts
|
|
5
6
|
import { build } from "@spader/dotllm/cli/yargs";
|
|
6
|
-
import { add, remove, list, link, sync, which,
|
|
7
|
+
import { add, remove, list, link, sync, which, completions } from "@spader/dotllm/cli/commands/index";
|
|
7
8
|
var DotLlmCli;
|
|
8
9
|
((DotLlmCli) => {
|
|
9
10
|
async function run() {
|
|
@@ -20,7 +21,7 @@ var DotLlmCli;
|
|
|
20
21
|
link,
|
|
21
22
|
sync,
|
|
22
23
|
which,
|
|
23
|
-
|
|
24
|
+
completions
|
|
24
25
|
}
|
|
25
26
|
};
|
|
26
27
|
build(def).parse();
|
package/src/cli/layout.js
CHANGED
package/src/cli/prompt.js
CHANGED
package/src/cli/theme.js
CHANGED
package/src/cli/yargs.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
2
4
|
// src/cli/yargs.ts
|
|
3
5
|
import yargs from "yargs";
|
|
4
6
|
import { hideBin } from "yargs/helpers";
|
|
@@ -145,7 +147,10 @@ function configure(y, def, root, path) {
|
|
|
145
147
|
for (const [k, v] of Object.entries(def.commands)) {
|
|
146
148
|
command(y, k, v, root, path);
|
|
147
149
|
}
|
|
148
|
-
|
|
150
|
+
const hasHandler = "handler" in def && typeof def.handler === "function";
|
|
151
|
+
if (!hasHandler) {
|
|
152
|
+
y.demandCommand(1, "You must specify a command");
|
|
153
|
+
}
|
|
149
154
|
}
|
|
150
155
|
y.help(false).option("help", { alias: "h", type: "boolean", describe: "Show help" }).check(check(def, root, path)).fail(fail(def, root, path));
|
|
151
156
|
if (path.length === 0 && "version" in def && def.version) {
|
package/src/core/add.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
2
4
|
// src/core/add.ts
|
|
3
5
|
import fs from "fs";
|
|
4
6
|
import path from "path";
|
|
@@ -34,7 +36,7 @@ async function cloneUrl(url, name, description) {
|
|
|
34
36
|
fs.mkdirSync(store, { recursive: true });
|
|
35
37
|
const target = path.join(store, resolved);
|
|
36
38
|
if (!fs.existsSync(target)) {
|
|
37
|
-
const proc = Bun.spawn(["git", "clone", url, target], {
|
|
39
|
+
const proc = Bun.spawn(["git", "clone", "--depth=1", url, target], {
|
|
38
40
|
stdout: "pipe",
|
|
39
41
|
stderr: "pipe"
|
|
40
42
|
});
|
package/src/core/config.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
2
4
|
// src/core/config.ts
|
|
3
5
|
import fs from "fs";
|
|
4
6
|
import os from "os";
|
|
@@ -56,16 +58,19 @@ var Config;
|
|
|
56
58
|
}
|
|
57
59
|
Global.write = write;
|
|
58
60
|
function find(config, name) {
|
|
59
|
-
|
|
61
|
+
const lower = name.toLowerCase();
|
|
62
|
+
return config.repos.find((r) => r.name.toLowerCase() === lower);
|
|
60
63
|
}
|
|
61
64
|
Global.find = find;
|
|
62
65
|
function add(config, entry) {
|
|
63
|
-
const
|
|
66
|
+
const lower = entry.name.toLowerCase();
|
|
67
|
+
const filtered = config.repos.filter((r) => r.name.toLowerCase() !== lower);
|
|
64
68
|
return { repos: [...filtered, entry] };
|
|
65
69
|
}
|
|
66
70
|
Global.add = add;
|
|
67
71
|
function remove(config, name) {
|
|
68
|
-
|
|
72
|
+
const lower = name.toLowerCase();
|
|
73
|
+
return { repos: config.repos.filter((r) => r.name.toLowerCase() !== lower) };
|
|
69
74
|
}
|
|
70
75
|
Global.remove = remove;
|
|
71
76
|
})(Global = Config.Global ||= {});
|
|
@@ -87,16 +92,29 @@ var Config;
|
|
|
87
92
|
`);
|
|
88
93
|
}
|
|
89
94
|
Local.write = write;
|
|
95
|
+
function find(config, name) {
|
|
96
|
+
const lower = name.toLowerCase();
|
|
97
|
+
for (const [key, value] of Object.entries(config.refs)) {
|
|
98
|
+
if (key.toLowerCase() === lower)
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
Local.find = find;
|
|
90
104
|
function has(config, name) {
|
|
91
|
-
return
|
|
105
|
+
return find(config, name) !== undefined;
|
|
92
106
|
}
|
|
93
107
|
Local.has = has;
|
|
94
108
|
function add(config, repo) {
|
|
95
|
-
|
|
109
|
+
const lower = repo.name.toLowerCase();
|
|
110
|
+
const refs = Object.fromEntries(Object.entries(config.refs).filter(([key]) => key.toLowerCase() !== lower));
|
|
111
|
+
refs[repo.name] = repo;
|
|
112
|
+
return { refs };
|
|
96
113
|
}
|
|
97
114
|
Local.add = add;
|
|
98
115
|
function remove(config, name) {
|
|
99
|
-
const
|
|
116
|
+
const lower = name.toLowerCase();
|
|
117
|
+
const refs = Object.fromEntries(Object.entries(config.refs).filter(([key]) => key.toLowerCase() !== lower));
|
|
100
118
|
return { refs };
|
|
101
119
|
}
|
|
102
120
|
Local.remove = remove;
|
package/src/core/index.js
CHANGED
package/src/core/link.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
2
4
|
// src/core/link.ts
|
|
3
5
|
import { Config } from "@spader/dotllm/core/config";
|
|
4
6
|
import { sync } from "@spader/dotllm/core/sync";
|
|
@@ -8,7 +10,7 @@ function link(names) {
|
|
|
8
10
|
const repo = Config.Global.find(global, name);
|
|
9
11
|
if (!repo)
|
|
10
12
|
return null;
|
|
11
|
-
return [name, repo];
|
|
13
|
+
return [repo.name, repo];
|
|
12
14
|
}).filter((row) => row !== null);
|
|
13
15
|
Config.Local.write({ refs: Object.fromEntries(rows) });
|
|
14
16
|
return sync();
|
package/src/core/remove.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
2
4
|
// src/core/remove.ts
|
|
3
5
|
import fs from "fs";
|
|
4
6
|
import path from "path";
|
|
@@ -9,7 +11,7 @@ function remove(name) {
|
|
|
9
11
|
if (!found) {
|
|
10
12
|
return { ok: false, error: `No repo named "${name}" in registry` };
|
|
11
13
|
}
|
|
12
|
-
const target = path.join(Config.storeDir(), name);
|
|
14
|
+
const target = path.join(Config.storeDir(), found.name);
|
|
13
15
|
if (fs.existsSync(target)) {
|
|
14
16
|
const stat = fs.lstatSync(target);
|
|
15
17
|
if (stat.isSymbolicLink()) {
|
|
@@ -19,7 +21,7 @@ function remove(name) {
|
|
|
19
21
|
fs.rmSync(target, { recursive: true, force: true });
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
|
-
Config.Global.write(Config.Global.remove(global, name));
|
|
24
|
+
Config.Global.write(Config.Global.remove(global, found.name));
|
|
23
25
|
return { ok: true };
|
|
24
26
|
}
|
|
25
27
|
export {
|
package/src/core/sync.js
CHANGED
|
@@ -1,33 +1,97 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
2
4
|
// src/core/sync.ts
|
|
3
5
|
import fs from "fs";
|
|
4
6
|
import path from "path";
|
|
5
7
|
import { Config } from "@spader/dotllm/core/config";
|
|
6
|
-
|
|
8
|
+
var FRESH_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
var STALE_MS = 7 * FRESH_MS;
|
|
10
|
+
function shouldPull(storeDir, force) {
|
|
11
|
+
if (force)
|
|
12
|
+
return true;
|
|
13
|
+
const head = path.join(storeDir, ".git", "HEAD");
|
|
14
|
+
const headStat = fs.statSync(head, { throwIfNoEntry: false });
|
|
15
|
+
if (!headStat)
|
|
16
|
+
return true;
|
|
17
|
+
const age = Date.now() - headStat.mtimeMs;
|
|
18
|
+
if (age < FRESH_MS)
|
|
19
|
+
return false;
|
|
20
|
+
if (age > STALE_MS)
|
|
21
|
+
return true;
|
|
22
|
+
const dirStat = fs.statSync(storeDir, { throwIfNoEntry: false });
|
|
23
|
+
if (!dirStat)
|
|
24
|
+
return true;
|
|
25
|
+
return dirStat.atimeMs > headStat.mtimeMs;
|
|
26
|
+
}
|
|
27
|
+
async function pull(names, options = {}) {
|
|
28
|
+
const force = options.force === true;
|
|
29
|
+
const global = Config.Global.read();
|
|
7
30
|
const states = await Promise.all(names.map(async (name) => {
|
|
8
31
|
const cwd = path.join(Config.refDir(), name);
|
|
9
32
|
if (!fs.existsSync(cwd)) {
|
|
10
|
-
return { name, error: "reference directory missing" };
|
|
33
|
+
return { kind: "failed", name, error: "reference directory missing" };
|
|
34
|
+
}
|
|
35
|
+
const repo = Config.Global.find(global, name);
|
|
36
|
+
if (repo && repo.kind === "file") {
|
|
37
|
+
return { kind: "skipped", name };
|
|
38
|
+
}
|
|
39
|
+
const storeDir = path.join(Config.storeDir(), name);
|
|
40
|
+
const storeStat = fs.lstatSync(storeDir, { throwIfNoEntry: false });
|
|
41
|
+
if (!storeStat || storeStat.isSymbolicLink()) {
|
|
42
|
+
return { kind: "skipped", name };
|
|
43
|
+
}
|
|
44
|
+
if (!fs.existsSync(path.join(storeDir, ".git"))) {
|
|
45
|
+
return { kind: "failed", name, error: "store directory is not a git repo" };
|
|
46
|
+
}
|
|
47
|
+
if (!shouldPull(storeDir, force)) {
|
|
48
|
+
return { kind: "skipped", name };
|
|
49
|
+
}
|
|
50
|
+
const fetch = Bun.spawn(["git", "fetch", "--depth=1", "origin", "HEAD"], {
|
|
51
|
+
cwd,
|
|
52
|
+
stdout: "pipe",
|
|
53
|
+
stderr: "pipe"
|
|
54
|
+
});
|
|
55
|
+
const [fetchCode, fetchOut, fetchErr] = await Promise.all([
|
|
56
|
+
fetch.exited,
|
|
57
|
+
new Response(fetch.stdout).text(),
|
|
58
|
+
new Response(fetch.stderr).text()
|
|
59
|
+
]);
|
|
60
|
+
if (fetchCode !== 0) {
|
|
61
|
+
const msg = `${fetchOut}
|
|
62
|
+
${fetchErr}`.trim();
|
|
63
|
+
return { kind: "failed", name, error: msg || "git fetch failed" };
|
|
11
64
|
}
|
|
12
|
-
const
|
|
65
|
+
const reset = Bun.spawn(["git", "reset", "--hard", "FETCH_HEAD"], {
|
|
13
66
|
cwd,
|
|
14
67
|
stdout: "pipe",
|
|
15
68
|
stderr: "pipe"
|
|
16
69
|
});
|
|
17
|
-
const [
|
|
18
|
-
|
|
19
|
-
new Response(
|
|
20
|
-
new Response(
|
|
70
|
+
const [resetCode, resetOut, resetErr] = await Promise.all([
|
|
71
|
+
reset.exited,
|
|
72
|
+
new Response(reset.stdout).text(),
|
|
73
|
+
new Response(reset.stderr).text()
|
|
21
74
|
]);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return { name, error: msg || "git
|
|
75
|
+
if (resetCode !== 0) {
|
|
76
|
+
const msg = `${resetOut}
|
|
77
|
+
${resetErr}`.trim();
|
|
78
|
+
return { kind: "failed", name, error: msg || "git reset failed" };
|
|
26
79
|
}
|
|
27
|
-
return
|
|
80
|
+
return { kind: "ok" };
|
|
28
81
|
}));
|
|
29
|
-
const
|
|
30
|
-
|
|
82
|
+
const pulled = [];
|
|
83
|
+
const skipped = [];
|
|
84
|
+
const failed = [];
|
|
85
|
+
for (let i = 0;i < states.length; i++) {
|
|
86
|
+
const state = states[i];
|
|
87
|
+
if (state.kind === "ok")
|
|
88
|
+
pulled.push(names[i]);
|
|
89
|
+
if (state.kind === "skipped")
|
|
90
|
+
skipped.push(state.name);
|
|
91
|
+
if (state.kind === "failed")
|
|
92
|
+
failed.push({ name: state.name, error: state.error });
|
|
93
|
+
}
|
|
94
|
+
return { count: names.length, pulled, skipped, failed };
|
|
31
95
|
}
|
|
32
96
|
function sync() {
|
|
33
97
|
const local = Config.Local.read();
|
|
@@ -56,11 +120,12 @@ function sync() {
|
|
|
56
120
|
}
|
|
57
121
|
for (const [name, repo] of Object.entries(local.refs)) {
|
|
58
122
|
const store = path.join(Config.storeDir(), name);
|
|
59
|
-
|
|
123
|
+
const storeBroken = repo.kind === "url" && fs.existsSync(store) && !fs.existsSync(path.join(store, ".git"));
|
|
124
|
+
if (storeBroken) {
|
|
60
125
|
fs.rmSync(store, { recursive: true, force: true });
|
|
61
126
|
}
|
|
62
127
|
if (!fs.existsSync(store) && repo.kind === "url") {
|
|
63
|
-
const clone = Bun.spawnSync(["git", "clone", repo.uri, store], {
|
|
128
|
+
const clone = Bun.spawnSync(["git", "clone", "--depth=1", repo.uri, store], {
|
|
64
129
|
stdout: "pipe",
|
|
65
130
|
stderr: "pipe"
|
|
66
131
|
});
|
package/src/core/unlink.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
2
4
|
// src/core/unlink.ts
|
|
3
5
|
import fs from "fs";
|
|
4
6
|
import path from "path";
|
|
5
7
|
import { Config } from "@spader/dotllm/core/config";
|
|
6
8
|
function unlink(name) {
|
|
7
9
|
const local = Config.Local.read();
|
|
8
|
-
|
|
10
|
+
const found = Config.Local.find(local, name);
|
|
11
|
+
if (!found) {
|
|
9
12
|
return { ok: false, error: `"${name}" is not linked in local config` };
|
|
10
13
|
}
|
|
11
|
-
const target = path.join(Config.refDir(), name);
|
|
14
|
+
const target = path.join(Config.refDir(), found.name);
|
|
12
15
|
if (fs.existsSync(target)) {
|
|
13
16
|
fs.unlinkSync(target);
|
|
14
17
|
}
|
|
15
|
-
Config.Local.write(Config.Local.remove(local, name));
|
|
18
|
+
Config.Local.write(Config.Local.remove(local, found.name));
|
|
16
19
|
return { ok: true };
|
|
17
20
|
}
|
|
18
21
|
export {
|
package/src/cli/commands/cd.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
// src/cli/commands/cd.ts
|
|
3
|
-
import path from "path";
|
|
4
|
-
import fs from "fs";
|
|
5
|
-
import { Config } from "@spader/dotllm/core";
|
|
6
|
-
import { defaultTheme as t } from "@spader/dotllm/cli/theme";
|
|
7
|
-
var command = {
|
|
8
|
-
description: "Open a subshell in a repo's store directory",
|
|
9
|
-
summary: "cd into a repo",
|
|
10
|
-
positionals: {
|
|
11
|
-
name: {
|
|
12
|
-
type: "string",
|
|
13
|
-
description: "Name of the repo",
|
|
14
|
-
required: true
|
|
15
|
-
}
|
|
16
|
-
},
|
|
17
|
-
handler: async (argv) => {
|
|
18
|
-
const name = String(argv.name);
|
|
19
|
-
const global = Config.Global.read();
|
|
20
|
-
const repo = Config.Global.find(global, name);
|
|
21
|
-
if (!repo) {
|
|
22
|
-
console.error(t.error(`No repo named "${name}" in registry`));
|
|
23
|
-
process.exit(1);
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
const dir = path.join(Config.storeDir(), name);
|
|
27
|
-
if (!fs.existsSync(dir)) {
|
|
28
|
-
console.error(t.error(`Store path does not exist: ${dir}`));
|
|
29
|
-
process.exit(1);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
const shell = process.env.SHELL ?? "/bin/sh";
|
|
33
|
-
const proc = Bun.spawn([shell], {
|
|
34
|
-
cwd: dir,
|
|
35
|
-
stdin: "inherit",
|
|
36
|
-
stdout: "inherit",
|
|
37
|
-
stderr: "inherit"
|
|
38
|
-
});
|
|
39
|
-
const code = await proc.exited;
|
|
40
|
-
process.exit(code);
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
export {
|
|
44
|
-
command
|
|
45
|
-
};
|