@spader/spall-cli 1.0.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/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/scripts/postinstall.js +38 -0
- package/dist/scripts/postinstall.js.map +10 -0
- package/dist/shared/display.d.ts +47 -0
- package/dist/shared/display.d.ts.map +1 -0
- package/dist/shared/help.d.ts +5 -0
- package/dist/shared/help.d.ts.map +1 -0
- package/dist/shared/index.d.ts +14 -0
- package/dist/shared/index.d.ts.map +1 -0
- package/dist/shared/layout.d.ts +16 -0
- package/dist/shared/layout.d.ts.map +1 -0
- package/dist/shared/list.d.ts +25 -0
- package/dist/shared/list.d.ts.map +1 -0
- package/dist/shared/progress.d.ts +4 -0
- package/dist/shared/progress.d.ts.map +1 -0
- package/dist/shared/search.d.ts +27 -0
- package/dist/shared/search.d.ts.map +1 -0
- package/dist/shared/status.d.ts +26 -0
- package/dist/shared/status.d.ts.map +1 -0
- package/dist/shared/theme.d.ts +20 -0
- package/dist/shared/theme.d.ts.map +1 -0
- package/dist/shared/tree.d.ts +37 -0
- package/dist/shared/tree.d.ts.map +1 -0
- package/dist/shared/vsearch.d.ts +28 -0
- package/dist/shared/vsearch.d.ts.map +1 -0
- package/dist/shared/workspace.d.ts +28 -0
- package/dist/shared/workspace.d.ts.map +1 -0
- package/dist/shared/yargs.d.ts +34 -0
- package/dist/shared/yargs.d.ts.map +1 -0
- package/dist/spall/commands/add.d.ts +20 -0
- package/dist/spall/commands/add.d.ts.map +1 -0
- package/dist/spall/commands/commit.d.ts +3 -0
- package/dist/spall/commands/commit.d.ts.map +1 -0
- package/dist/spall/commands/corpus/create.d.ts +3 -0
- package/dist/spall/commands/corpus/create.d.ts.map +1 -0
- package/dist/spall/commands/corpus/delete.d.ts +3 -0
- package/dist/spall/commands/corpus/delete.d.ts.map +1 -0
- package/dist/spall/commands/corpus/index.d.ts +3 -0
- package/dist/spall/commands/corpus/index.d.ts.map +1 -0
- package/dist/spall/commands/get.d.ts +3 -0
- package/dist/spall/commands/get.d.ts.map +1 -0
- package/dist/spall/commands/hook.d.ts +3 -0
- package/dist/spall/commands/hook.d.ts.map +1 -0
- package/dist/spall/commands/index.d.ts +15 -0
- package/dist/spall/commands/index.d.ts.map +1 -0
- package/dist/spall/commands/integrate/index.d.ts +8 -0
- package/dist/spall/commands/integrate/index.d.ts.map +1 -0
- package/dist/spall/commands/integrate/opencode.d.ts +3 -0
- package/dist/spall/commands/integrate/opencode.d.ts.map +1 -0
- package/dist/spall/commands/integrate/shell.d.ts +6 -0
- package/dist/spall/commands/integrate/shell.d.ts.map +1 -0
- package/dist/spall/commands/list.d.ts +3 -0
- package/dist/spall/commands/list.d.ts.map +1 -0
- package/dist/spall/commands/review/comments.d.ts +3 -0
- package/dist/spall/commands/review/comments.d.ts.map +1 -0
- package/dist/spall/commands/review/create.d.ts +3 -0
- package/dist/spall/commands/review/create.d.ts.map +1 -0
- package/dist/spall/commands/review/index.d.ts +3 -0
- package/dist/spall/commands/review/index.d.ts.map +1 -0
- package/dist/spall/commands/review/latest.d.ts +3 -0
- package/dist/spall/commands/review/latest.d.ts.map +1 -0
- package/dist/spall/commands/review/list.d.ts +3 -0
- package/dist/spall/commands/review/list.d.ts.map +1 -0
- package/dist/spall/commands/review/patches.d.ts +3 -0
- package/dist/spall/commands/review/patches.d.ts.map +1 -0
- package/dist/spall/commands/search.d.ts +3 -0
- package/dist/spall/commands/search.d.ts.map +1 -0
- package/dist/spall/commands/serve.d.ts +3 -0
- package/dist/spall/commands/serve.d.ts.map +1 -0
- package/dist/spall/commands/status.d.ts +3 -0
- package/dist/spall/commands/status.d.ts.map +1 -0
- package/dist/spall/commands/sync.d.ts +3 -0
- package/dist/spall/commands/sync.d.ts.map +1 -0
- package/dist/spall/commands/tui.d.ts +3 -0
- package/dist/spall/commands/tui.d.ts.map +1 -0
- package/dist/spall/commands/vsearch.d.ts +3 -0
- package/dist/spall/commands/vsearch.d.ts.map +1 -0
- package/dist/spall/commands/workspace/add.d.ts +3 -0
- package/dist/spall/commands/workspace/add.d.ts.map +1 -0
- package/dist/spall/commands/workspace/edit.d.ts +3 -0
- package/dist/spall/commands/workspace/edit.d.ts.map +1 -0
- package/dist/spall/commands/workspace/index.d.ts +3 -0
- package/dist/spall/commands/workspace/index.d.ts.map +1 -0
- package/dist/spall/commands/workspace/init.d.ts +3 -0
- package/dist/spall/commands/workspace/init.d.ts.map +1 -0
- package/dist/spall/commands/workspace/remove.d.ts +3 -0
- package/dist/spall/commands/workspace/remove.d.ts.map +1 -0
- package/dist/spall/e2e.preload.d.ts +2 -0
- package/dist/spall/e2e.preload.d.ts.map +1 -0
- package/dist/spall/index.d.ts +3 -0
- package/dist/spall/index.d.ts.map +1 -0
- package/dist/spallm/commands/add.d.ts +3 -0
- package/dist/spallm/commands/add.d.ts.map +1 -0
- package/dist/spallm/commands/fetch.d.ts +3 -0
- package/dist/spallm/commands/fetch.d.ts.map +1 -0
- package/dist/spallm/commands/get.d.ts +3 -0
- package/dist/spallm/commands/get.d.ts.map +1 -0
- package/dist/spallm/commands/list.d.ts +3 -0
- package/dist/spallm/commands/list.d.ts.map +1 -0
- package/dist/spallm/commands/prime.d.ts +3 -0
- package/dist/spallm/commands/prime.d.ts.map +1 -0
- package/dist/spallm/commands/query.d.ts +3 -0
- package/dist/spallm/commands/query.d.ts.map +1 -0
- package/dist/spallm/commands/review.d.ts +3 -0
- package/dist/spallm/commands/review.d.ts.map +1 -0
- package/dist/spallm/commands/search.d.ts +3 -0
- package/dist/spallm/commands/search.d.ts.map +1 -0
- package/dist/spallm/commands/status.d.ts +3 -0
- package/dist/spallm/commands/status.d.ts.map +1 -0
- package/dist/spallm/commands/vsearch.d.ts +3 -0
- package/dist/spallm/commands/vsearch.d.ts.map +1 -0
- package/dist/spallm/index.d.ts +3 -0
- package/dist/spallm/index.d.ts.map +1 -0
- package/dist/src/index.js +1648 -0
- package/dist/src/index.js.map +32 -0
- package/dist/src/shared/index.js +1164 -0
- package/dist/src/shared/index.js.map +21 -0
- package/dist/src/spall/index.js +1648 -0
- package/dist/src/spall/index.js.map +32 -0
- package/dist/src/spallm/index.js +294 -0
- package/dist/src/spallm/index.js.map +17 -0
- package/package.json +72 -0
|
@@ -0,0 +1,1648 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
var __require = import.meta.require;
|
|
4
|
+
|
|
5
|
+
// src/spall/index.ts
|
|
6
|
+
import { build, setActiveCli } from "@spader/spall-cli/shared";
|
|
7
|
+
|
|
8
|
+
// src/spall/commands/corpus/create.ts
|
|
9
|
+
import pc from "picocolors";
|
|
10
|
+
import consola from "consola";
|
|
11
|
+
import { Client } from "@spader/spall-sdk/client";
|
|
12
|
+
var create = {
|
|
13
|
+
description: "Create a new corpus",
|
|
14
|
+
positionals: {
|
|
15
|
+
name: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "Corpus name",
|
|
18
|
+
required: true
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
handler: async (argv) => {
|
|
22
|
+
const client = await Client.connect();
|
|
23
|
+
const result = await client.corpus.create({
|
|
24
|
+
name: argv.name
|
|
25
|
+
}).then(Client.unwrap);
|
|
26
|
+
consola.success(`Corpus ${pc.cyanBright(result.name)} (id: ${result.id})`);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// src/spall/commands/corpus/delete.ts
|
|
31
|
+
import pc2 from "picocolors";
|
|
32
|
+
import consola2 from "consola";
|
|
33
|
+
import { Client as Client2 } from "@spader/spall-sdk";
|
|
34
|
+
var remove = {
|
|
35
|
+
description: "Delete a corpus by ID",
|
|
36
|
+
positionals: {
|
|
37
|
+
id: {
|
|
38
|
+
type: "number",
|
|
39
|
+
description: "Corpus ID"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
handler: async (argv) => {
|
|
43
|
+
if (argv.id === undefined || isNaN(Number(argv.id))) {
|
|
44
|
+
consola2.error("Missing required argument: id");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const client = await Client2.connect();
|
|
48
|
+
const result = await client.corpus.delete({ id: String(argv.id) });
|
|
49
|
+
if (result.error) {
|
|
50
|
+
consola2.error(result.error.message);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
consola2.success(`Deleted corpus ${pc2.cyanBright(argv.id)}`);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// src/spall/commands/corpus/index.ts
|
|
58
|
+
var corpus = {
|
|
59
|
+
description: "Manage corpora",
|
|
60
|
+
commands: { create, delete: remove }
|
|
61
|
+
};
|
|
62
|
+
// src/spall/commands/workspace/init.ts
|
|
63
|
+
import { existsSync } from "fs";
|
|
64
|
+
import { basename } from "path";
|
|
65
|
+
import * as prompts from "@clack/prompts";
|
|
66
|
+
import consola3 from "consola";
|
|
67
|
+
import { WorkspaceConfig } from "@spader/spall-core";
|
|
68
|
+
import { Client as Client3 } from "@spader/spall-sdk/client";
|
|
69
|
+
var init = {
|
|
70
|
+
description: "Initialize a workspace in this directory",
|
|
71
|
+
options: {
|
|
72
|
+
path: {
|
|
73
|
+
alias: "p",
|
|
74
|
+
type: "string",
|
|
75
|
+
description: "Directory to initialize (defaults to git root or cwd)"
|
|
76
|
+
},
|
|
77
|
+
force: {
|
|
78
|
+
alias: "f",
|
|
79
|
+
type: "boolean",
|
|
80
|
+
description: "Overwrite existing .spall/spall.json",
|
|
81
|
+
default: false
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
handler: async (argv) => {
|
|
85
|
+
const cwd = process.cwd();
|
|
86
|
+
const root = argv.path ?? cwd;
|
|
87
|
+
const configPath = WorkspaceConfig.path(root);
|
|
88
|
+
prompts.intro("Workspace init");
|
|
89
|
+
if (existsSync(configPath) && !argv.force) {
|
|
90
|
+
const overwrite = await prompts.confirm({
|
|
91
|
+
message: "Workspace config already exists. Overwrite?",
|
|
92
|
+
initialValue: false
|
|
93
|
+
});
|
|
94
|
+
if (prompts.isCancel(overwrite) || !overwrite) {
|
|
95
|
+
prompts.outro("Done");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const defaultName = basename(root) || "default";
|
|
100
|
+
const workspaceName = await prompts.text({
|
|
101
|
+
message: "Workspace name",
|
|
102
|
+
initialValue: defaultName,
|
|
103
|
+
validate: (s) => s && s.trim().length > 0 ? undefined : "Required"
|
|
104
|
+
});
|
|
105
|
+
if (prompts.isCancel(workspaceName)) {
|
|
106
|
+
prompts.outro("Done");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const createCorpus = await prompts.confirm({
|
|
110
|
+
message: "Create a corpus for this repo?",
|
|
111
|
+
initialValue: true
|
|
112
|
+
});
|
|
113
|
+
if (prompts.isCancel(createCorpus)) {
|
|
114
|
+
prompts.outro("Done");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
let corpusName = null;
|
|
118
|
+
if (createCorpus) {
|
|
119
|
+
const name = await prompts.text({
|
|
120
|
+
message: "Corpus name",
|
|
121
|
+
initialValue: defaultName,
|
|
122
|
+
validate: (s) => s && s.trim().length > 0 ? undefined : "Required"
|
|
123
|
+
});
|
|
124
|
+
if (prompts.isCancel(name)) {
|
|
125
|
+
prompts.outro("Done");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
corpusName = String(name).trim();
|
|
129
|
+
}
|
|
130
|
+
const spinner2 = prompts.spinner();
|
|
131
|
+
spinner2.start("Creating workspace");
|
|
132
|
+
try {
|
|
133
|
+
const client = await Client3.connect();
|
|
134
|
+
const ws = await client.workspace.create({ name: String(workspaceName).trim() }).then(Client3.unwrap);
|
|
135
|
+
if (corpusName) {
|
|
136
|
+
spinner2.message("Creating corpus");
|
|
137
|
+
await client.corpus.create({ name: corpusName }).then(Client3.unwrap);
|
|
138
|
+
}
|
|
139
|
+
spinner2.message("Loading corpora");
|
|
140
|
+
const corpora = await client.corpus.list().then(Client3.unwrap);
|
|
141
|
+
const options = corpora.map((c) => {
|
|
142
|
+
const name = c.name;
|
|
143
|
+
const noteCount = typeof c.noteCount === "number" ? c.noteCount : null;
|
|
144
|
+
return {
|
|
145
|
+
label: name,
|
|
146
|
+
value: name,
|
|
147
|
+
hint: noteCount == null ? undefined : `${noteCount} notes`
|
|
148
|
+
};
|
|
149
|
+
}).sort((a, b) => a.label.localeCompare(b.label));
|
|
150
|
+
const defaults = new Set(["default"]);
|
|
151
|
+
if (corpusName)
|
|
152
|
+
defaults.add(corpusName);
|
|
153
|
+
spinner2.stop("Workspace created");
|
|
154
|
+
const picked = await prompts.autocompleteMultiselect({
|
|
155
|
+
message: "Select corpora to include in read scope (type to filter)",
|
|
156
|
+
options,
|
|
157
|
+
placeholder: "Type to filter...",
|
|
158
|
+
maxItems: 12,
|
|
159
|
+
initialValues: Array.from(defaults),
|
|
160
|
+
required: true
|
|
161
|
+
});
|
|
162
|
+
if (prompts.isCancel(picked)) {
|
|
163
|
+
prompts.outro("Done");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (!(Array.isArray(picked) && picked.every((x) => typeof x === "string"))) {
|
|
167
|
+
throw new Error("Unexpected autocompleteMultiselect result");
|
|
168
|
+
}
|
|
169
|
+
const read = picked;
|
|
170
|
+
const write = await prompts.select({
|
|
171
|
+
message: "Select default corpus for writes",
|
|
172
|
+
options: read.map((name) => ({ label: name, value: name })),
|
|
173
|
+
initialValue: (corpusName && read.includes(corpusName) ? corpusName : undefined) ?? (read.includes("default") ? "default" : read[0])
|
|
174
|
+
});
|
|
175
|
+
if (prompts.isCancel(write)) {
|
|
176
|
+
prompts.outro("Done");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const next = {
|
|
180
|
+
workspace: { name: ws.name, id: ws.id },
|
|
181
|
+
scope: { read, write: String(write) }
|
|
182
|
+
};
|
|
183
|
+
WorkspaceConfig.write(root, next);
|
|
184
|
+
prompts.outro(`Wrote ${configPath}`);
|
|
185
|
+
} catch (e) {
|
|
186
|
+
spinner2.stop("Failed");
|
|
187
|
+
consola3.error(e?.message ?? String(e));
|
|
188
|
+
prompts.outro("Done");
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// src/spall/commands/workspace/add.ts
|
|
195
|
+
import consola4 from "consola";
|
|
196
|
+
import { WorkspaceConfig as WorkspaceConfig2 } from "@spader/spall-core";
|
|
197
|
+
import { Client as Client4 } from "@spader/spall-sdk/client";
|
|
198
|
+
var add = {
|
|
199
|
+
description: "Add a corpus to the workspace read scope",
|
|
200
|
+
positionals: {
|
|
201
|
+
corpus: {
|
|
202
|
+
type: "string",
|
|
203
|
+
description: "Corpus name",
|
|
204
|
+
required: true
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
handler: async (argv) => {
|
|
208
|
+
const located = WorkspaceConfig2.locate(process.cwd());
|
|
209
|
+
if (!located) {
|
|
210
|
+
consola4.error("No workspace config found. Run `spall workspace init`.");
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
const name = String(argv.corpus ?? "").trim();
|
|
214
|
+
if (!name) {
|
|
215
|
+
consola4.error("Missing required argument: corpus");
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
const client = await Client4.connect();
|
|
219
|
+
await client.corpus.create({ name }).then(Client4.unwrap);
|
|
220
|
+
const cfg = WorkspaceConfig2.load(located.root);
|
|
221
|
+
const read = cfg.scope.read.includes(name) ? cfg.scope.read : [...cfg.scope.read, name];
|
|
222
|
+
WorkspaceConfig2.patch(located.root, { scope: { read } });
|
|
223
|
+
consola4.success(`Included corpus: ${name}`);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// src/spall/commands/workspace/remove.ts
|
|
228
|
+
import consola5 from "consola";
|
|
229
|
+
import { WorkspaceConfig as WorkspaceConfig3 } from "@spader/spall-core";
|
|
230
|
+
var remove2 = {
|
|
231
|
+
description: "Remove a corpus from the workspace read scope",
|
|
232
|
+
positionals: {
|
|
233
|
+
corpus: {
|
|
234
|
+
type: "string",
|
|
235
|
+
description: "Corpus name",
|
|
236
|
+
required: true
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
handler: async (argv) => {
|
|
240
|
+
const located = WorkspaceConfig3.locate(process.cwd());
|
|
241
|
+
if (!located) {
|
|
242
|
+
consola5.error("No workspace config found. Run `spall workspace init`.");
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
const name = String(argv.corpus ?? "").trim();
|
|
246
|
+
if (!name) {
|
|
247
|
+
consola5.error("Missing required argument: corpus");
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
const cfg = WorkspaceConfig3.load(located.root);
|
|
251
|
+
const read = cfg.scope.read.filter((c) => c !== name);
|
|
252
|
+
const write = cfg.scope.write === name ? read[0] ?? "default" : cfg.scope.write;
|
|
253
|
+
WorkspaceConfig3.patch(located.root, { scope: { read, write } });
|
|
254
|
+
consola5.success(`Removed corpus: ${name}`);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// src/spall/commands/workspace/edit.ts
|
|
259
|
+
import * as prompts2 from "@clack/prompts";
|
|
260
|
+
import consola6 from "consola";
|
|
261
|
+
import { WorkspaceConfig as WorkspaceConfig4 } from "@spader/spall-core";
|
|
262
|
+
import { Client as Client5 } from "@spader/spall-sdk/client";
|
|
263
|
+
var edit = {
|
|
264
|
+
description: "Interactively edit the workspace",
|
|
265
|
+
handler: async () => {
|
|
266
|
+
const located = WorkspaceConfig4.locate(process.cwd());
|
|
267
|
+
if (!located) {
|
|
268
|
+
consola6.error("No workspace config found. Run `spall workspace init`.");
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
const cfg = WorkspaceConfig4.load(located.root);
|
|
272
|
+
const client = await Client5.connect();
|
|
273
|
+
const corpora = await client.corpus.list().then(Client5.unwrap);
|
|
274
|
+
const options = corpora.map((c) => {
|
|
275
|
+
const name = c.name;
|
|
276
|
+
const noteCount = typeof c.noteCount === "number" ? c.noteCount : null;
|
|
277
|
+
return {
|
|
278
|
+
label: name,
|
|
279
|
+
value: name,
|
|
280
|
+
hint: noteCount == null ? undefined : `${noteCount} notes`
|
|
281
|
+
};
|
|
282
|
+
}).sort((a, b) => a.label.localeCompare(b.label));
|
|
283
|
+
prompts2.intro("Workspace scope");
|
|
284
|
+
const picked = await prompts2.autocompleteMultiselect({
|
|
285
|
+
message: "Select corpora to include in read scope (type to filter)",
|
|
286
|
+
options,
|
|
287
|
+
placeholder: "Type to filter...",
|
|
288
|
+
maxItems: 12,
|
|
289
|
+
initialValues: cfg.scope.read.filter((c) => options.some((o) => o.value === c)),
|
|
290
|
+
required: true
|
|
291
|
+
});
|
|
292
|
+
if (prompts2.isCancel(picked)) {
|
|
293
|
+
prompts2.outro("Done");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (!(Array.isArray(picked) && picked.every((x) => typeof x === "string"))) {
|
|
297
|
+
throw new Error("Unexpected autocompleteMultiselect result");
|
|
298
|
+
}
|
|
299
|
+
const read = picked;
|
|
300
|
+
const write = await prompts2.select({
|
|
301
|
+
message: "Select default corpus for writes",
|
|
302
|
+
options: read.map((name) => ({ label: name, value: name })),
|
|
303
|
+
initialValue: read.includes(cfg.scope.write) ? cfg.scope.write : read[0]
|
|
304
|
+
});
|
|
305
|
+
if (prompts2.isCancel(write)) {
|
|
306
|
+
prompts2.outro("Done");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
WorkspaceConfig4.patch(located.root, {
|
|
310
|
+
scope: { read, write: String(write) }
|
|
311
|
+
});
|
|
312
|
+
prompts2.outro("Updated workspace scope");
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// src/spall/commands/workspace/index.ts
|
|
317
|
+
import { defaultTheme as theme } from "@spader/spall-cli/shared";
|
|
318
|
+
var workspace = {
|
|
319
|
+
summary: "Manage the current workspace",
|
|
320
|
+
description: `A workspace allows you to define which corpora are included in ${theme.guide("searches")}, per directory. Separately, it provides a context for spall to automatically learn note weights based on access patterns.
|
|
321
|
+
|
|
322
|
+
It's very useful to ingest documentation with spall, but you don't want TypeScript documentation polluting ${theme.guide("search")} results for a C project. Workspaces solve this by allowing you to restrict which corpora are included in ${theme.guide("searches")} within a directory.`,
|
|
323
|
+
commands: { init, add, remove: remove2, edit }
|
|
324
|
+
};
|
|
325
|
+
// src/spall/commands/add.ts
|
|
326
|
+
import * as prompts3 from "@clack/prompts";
|
|
327
|
+
import consola7 from "consola";
|
|
328
|
+
import { Client as Client6 } from "@spader/spall-sdk/client";
|
|
329
|
+
import { WorkspaceConfig as WorkspaceConfig5 } from "@spader/spall-core";
|
|
330
|
+
import {
|
|
331
|
+
defaultTheme as theme2,
|
|
332
|
+
createModelProgressHandler,
|
|
333
|
+
formatStreamError
|
|
334
|
+
} from "@spader/spall-cli/shared";
|
|
335
|
+
function splitPath(input) {
|
|
336
|
+
const normalized = input.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
|
337
|
+
if (normalized.length === 0)
|
|
338
|
+
return { dir: "", name: "" };
|
|
339
|
+
const i = normalized.lastIndexOf("/");
|
|
340
|
+
if (i < 0)
|
|
341
|
+
return { dir: "", name: normalized };
|
|
342
|
+
return {
|
|
343
|
+
dir: normalized.slice(0, i),
|
|
344
|
+
name: normalized.slice(i + 1)
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function joinPath(dir, name) {
|
|
348
|
+
const d = dir.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
|
349
|
+
const n = name.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
|
350
|
+
return d.length > 0 ? `${d}/${n}` : n;
|
|
351
|
+
}
|
|
352
|
+
function collectDirectories(paths) {
|
|
353
|
+
const dirs = new Set;
|
|
354
|
+
dirs.add("");
|
|
355
|
+
for (const path of paths) {
|
|
356
|
+
const parts = path.split("/");
|
|
357
|
+
if (parts.length <= 1)
|
|
358
|
+
continue;
|
|
359
|
+
let prefix = "";
|
|
360
|
+
for (let i = 0;i < parts.length - 1; i++) {
|
|
361
|
+
const part = parts[i];
|
|
362
|
+
prefix = prefix ? `${prefix}/${part}` : part;
|
|
363
|
+
dirs.add(prefix);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return Array.from(dirs).sort((a, b) => a.localeCompare(b));
|
|
367
|
+
}
|
|
368
|
+
var NEW_DIRECTORY = "__new_directory__";
|
|
369
|
+
function keepServerAlive(client, signal) {
|
|
370
|
+
(async () => {
|
|
371
|
+
try {
|
|
372
|
+
const { stream } = await client.events({ signal });
|
|
373
|
+
for await (const _event of stream) {}
|
|
374
|
+
} catch {}
|
|
375
|
+
})();
|
|
376
|
+
}
|
|
377
|
+
async function submitNote(input) {
|
|
378
|
+
const corpus2 = await input.client.corpus.get({ name: input.corpusName }).catch(() => {
|
|
379
|
+
consola7.error(`Failed to find corpus: ${theme2.primary(String(input.corpusName))}`);
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}).then(Client6.unwrap);
|
|
382
|
+
const handleProgress = createModelProgressHandler();
|
|
383
|
+
const handleStreamError = (event) => {
|
|
384
|
+
if (event?.tag !== "error")
|
|
385
|
+
return false;
|
|
386
|
+
const formatted = formatStreamError(event.error, input.path);
|
|
387
|
+
const raw = (() => {
|
|
388
|
+
try {
|
|
389
|
+
return JSON.stringify(event.error);
|
|
390
|
+
} catch {
|
|
391
|
+
return String(event.error);
|
|
392
|
+
}
|
|
393
|
+
})();
|
|
394
|
+
consola7.error(formatted);
|
|
395
|
+
consola7.error(`Raw SSE error payload: ${raw}`);
|
|
396
|
+
process.exit(1);
|
|
397
|
+
};
|
|
398
|
+
if (input.update) {
|
|
399
|
+
const existing = await input.client.note.get({ id: corpus2.id.toString(), path: input.path }).then(Client6.unwrap).catch(() => null);
|
|
400
|
+
if (!existing) {
|
|
401
|
+
consola7.error(`Note not found: ${theme2.primary(input.path)}`);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
const { stream: stream2 } = await input.client.sse.note.update({
|
|
405
|
+
id: existing.id.toString(),
|
|
406
|
+
content: input.content,
|
|
407
|
+
dupe: true
|
|
408
|
+
});
|
|
409
|
+
for await (const event of stream2) {
|
|
410
|
+
handleProgress(event);
|
|
411
|
+
handleStreamError(event);
|
|
412
|
+
if (event.tag === "note.updated") {
|
|
413
|
+
return { action: "updated", info: event.info };
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
throw new Error("Note stream ended without result");
|
|
417
|
+
}
|
|
418
|
+
const { stream } = await input.client.sse.note.add({
|
|
419
|
+
path: input.path,
|
|
420
|
+
content: input.content,
|
|
421
|
+
corpus: corpus2.id,
|
|
422
|
+
dupe: true
|
|
423
|
+
});
|
|
424
|
+
for await (const event of stream) {
|
|
425
|
+
handleProgress(event);
|
|
426
|
+
handleStreamError(event);
|
|
427
|
+
if (event.tag === "note.created") {
|
|
428
|
+
return { action: "added", info: event.info };
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
throw new Error("Note stream ended without result");
|
|
432
|
+
}
|
|
433
|
+
var add2 = {
|
|
434
|
+
description: "Add a note to a corpus",
|
|
435
|
+
positionals: {
|
|
436
|
+
path: {
|
|
437
|
+
type: "string",
|
|
438
|
+
description: "Path/name for the note",
|
|
439
|
+
required: false
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
options: {
|
|
443
|
+
text: {
|
|
444
|
+
alias: "t",
|
|
445
|
+
type: "string",
|
|
446
|
+
description: "Note content"
|
|
447
|
+
},
|
|
448
|
+
corpus: {
|
|
449
|
+
alias: "c",
|
|
450
|
+
type: "string",
|
|
451
|
+
description: "Corpus name"
|
|
452
|
+
},
|
|
453
|
+
update: {
|
|
454
|
+
alias: "u",
|
|
455
|
+
type: "boolean",
|
|
456
|
+
description: "Update if note exists (upsert)"
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
handler: async (argv) => {
|
|
460
|
+
const hasPathArg = typeof argv.path === "string" && argv.path.length > 0;
|
|
461
|
+
const hasTextArg = typeof argv.text === "string";
|
|
462
|
+
const interactive = !hasPathArg || !hasTextArg;
|
|
463
|
+
const defaultCorpusName = WorkspaceConfig5.load(process.cwd()).scope.write;
|
|
464
|
+
let corpusName = typeof argv.corpus === "string" ? String(argv.corpus) : defaultCorpusName;
|
|
465
|
+
let path = hasPathArg ? String(argv.path) : "";
|
|
466
|
+
let content = hasTextArg ? String(argv.text) : "";
|
|
467
|
+
const client = await Client6.connect();
|
|
468
|
+
const keepAlive = interactive ? new AbortController : null;
|
|
469
|
+
if (keepAlive) {
|
|
470
|
+
keepServerAlive(client, keepAlive.signal);
|
|
471
|
+
}
|
|
472
|
+
try {
|
|
473
|
+
if (interactive) {
|
|
474
|
+
prompts3.intro("spall add");
|
|
475
|
+
if (typeof argv.corpus !== "string") {
|
|
476
|
+
const corpora = await client.corpus.list().then(Client6.unwrap);
|
|
477
|
+
const corpusOptions = corpora.map((c) => {
|
|
478
|
+
const name = String(c.name);
|
|
479
|
+
const noteCount = typeof c.noteCount === "number" ? c.noteCount : undefined;
|
|
480
|
+
return {
|
|
481
|
+
label: name,
|
|
482
|
+
value: name,
|
|
483
|
+
hint: noteCount === undefined ? undefined : `${noteCount} ${noteCount === 1 ? "note" : "notes"}`
|
|
484
|
+
};
|
|
485
|
+
}).sort((a, b) => a.label.localeCompare(b.label));
|
|
486
|
+
const pickedCorpus = await prompts3.autocomplete({
|
|
487
|
+
message: "Select corpus",
|
|
488
|
+
options: corpusOptions,
|
|
489
|
+
placeholder: "Type to filter...",
|
|
490
|
+
maxItems: 12,
|
|
491
|
+
initialValue: corpusName
|
|
492
|
+
});
|
|
493
|
+
if (prompts3.isCancel(pickedCorpus)) {
|
|
494
|
+
prompts3.outro("Cancelled");
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
corpusName = String(pickedCorpus);
|
|
498
|
+
}
|
|
499
|
+
const corpusForPath = await client.corpus.get({ name: corpusName }).catch(() => {
|
|
500
|
+
consola7.error(`Failed to find corpus: ${theme2.primary(corpusName)}`);
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}).then(Client6.unwrap);
|
|
503
|
+
const existing = await client.note.list({ id: String(corpusForPath.id) }).then(Client6.unwrap).catch(() => []);
|
|
504
|
+
const existingPaths = existing.map((item) => String(item.path)).sort((a, b) => a.localeCompare(b));
|
|
505
|
+
const initial = hasPathArg ? splitPath(path) : { dir: "", name: "" };
|
|
506
|
+
const directories = collectDirectories(existingPaths);
|
|
507
|
+
const dirOptions = [
|
|
508
|
+
{
|
|
509
|
+
label: "(new directory)",
|
|
510
|
+
value: NEW_DIRECTORY,
|
|
511
|
+
hint: "type custom path"
|
|
512
|
+
},
|
|
513
|
+
...directories.map((dir2) => ({
|
|
514
|
+
label: dir2.length === 0 ? "(root)" : dir2,
|
|
515
|
+
value: dir2
|
|
516
|
+
}))
|
|
517
|
+
];
|
|
518
|
+
const pickedDir = await prompts3.autocomplete({
|
|
519
|
+
message: "Select directory",
|
|
520
|
+
options: dirOptions,
|
|
521
|
+
placeholder: "Type to filter...",
|
|
522
|
+
maxItems: 16,
|
|
523
|
+
initialValue: directories.includes(initial.dir) ? initial.dir : NEW_DIRECTORY
|
|
524
|
+
});
|
|
525
|
+
if (prompts3.isCancel(pickedDir)) {
|
|
526
|
+
prompts3.outro("Cancelled");
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
let dir = String(pickedDir);
|
|
530
|
+
if (dir === NEW_DIRECTORY) {
|
|
531
|
+
const enteredDir = await prompts3.text({
|
|
532
|
+
message: "Directory",
|
|
533
|
+
placeholder: "(root)",
|
|
534
|
+
initialValue: initial.dir,
|
|
535
|
+
validate: (s) => {
|
|
536
|
+
const value = String(s ?? "");
|
|
537
|
+
if (value.startsWith("/") || value.endsWith("/")) {
|
|
538
|
+
return "Do not start or end with '/'";
|
|
539
|
+
}
|
|
540
|
+
if (value.includes("\\"))
|
|
541
|
+
return "Use '/' as path separator";
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
if (prompts3.isCancel(enteredDir)) {
|
|
546
|
+
prompts3.outro("Cancelled");
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
dir = String(enteredDir);
|
|
550
|
+
}
|
|
551
|
+
const docName = await prompts3.text({
|
|
552
|
+
message: "Document name",
|
|
553
|
+
placeholder: "e.g. overview.md",
|
|
554
|
+
initialValue: initial.name,
|
|
555
|
+
validate: (s) => {
|
|
556
|
+
const name = String(s ?? "");
|
|
557
|
+
if (name.length === 0)
|
|
558
|
+
return "Document name is required";
|
|
559
|
+
if (name.includes("/") || name.includes("\\")) {
|
|
560
|
+
return "Use directory picker for folders";
|
|
561
|
+
}
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
if (prompts3.isCancel(docName)) {
|
|
566
|
+
prompts3.outro("Cancelled");
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
path = joinPath(dir, String(docName));
|
|
570
|
+
if (!hasTextArg) {
|
|
571
|
+
const entered = await prompts3.text({
|
|
572
|
+
message: "Note content",
|
|
573
|
+
placeholder: "Write note content",
|
|
574
|
+
validate: (s) => (s ?? "").length > 0 ? undefined : "Content is required"
|
|
575
|
+
});
|
|
576
|
+
if (prompts3.isCancel(entered)) {
|
|
577
|
+
prompts3.outro("Cancelled");
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
content = String(entered);
|
|
581
|
+
}
|
|
582
|
+
const action = argv.update ? "Update" : "Add";
|
|
583
|
+
const confirmed = await prompts3.confirm({
|
|
584
|
+
message: `${action} ${theme2.primary(path)} in corpus ${theme2.primary(corpusName)}?`,
|
|
585
|
+
initialValue: true
|
|
586
|
+
});
|
|
587
|
+
if (prompts3.isCancel(confirmed) || !confirmed) {
|
|
588
|
+
prompts3.outro("Cancelled");
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (path.length === 0) {
|
|
593
|
+
consola7.error("Path is required");
|
|
594
|
+
process.exit(1);
|
|
595
|
+
}
|
|
596
|
+
if (content.length === 0) {
|
|
597
|
+
consola7.error(`Note content cannot be empty: ${theme2.primary(path)}`);
|
|
598
|
+
process.exit(1);
|
|
599
|
+
}
|
|
600
|
+
let result;
|
|
601
|
+
try {
|
|
602
|
+
result = await submitNote({
|
|
603
|
+
client,
|
|
604
|
+
corpusName,
|
|
605
|
+
path,
|
|
606
|
+
content,
|
|
607
|
+
update: argv.update
|
|
608
|
+
});
|
|
609
|
+
} catch (error) {
|
|
610
|
+
const msg = error?.message ?? String(error);
|
|
611
|
+
const stack = error?.stack ? `
|
|
612
|
+
${String(error.stack)}` : "";
|
|
613
|
+
consola7.error(`add submit failed: ${msg}${stack}`);
|
|
614
|
+
throw error;
|
|
615
|
+
}
|
|
616
|
+
if (result.action === "updated") {
|
|
617
|
+
consola7.success(`Updated note ${theme2.primary(result.info.path)} (id: ${result.info.id}, corpus: ${result.info.corpus})`);
|
|
618
|
+
} else {
|
|
619
|
+
consola7.success(`Added note ${theme2.primary(result.info.path)} (id: ${result.info.id}, corpus: ${result.info.corpus})`);
|
|
620
|
+
}
|
|
621
|
+
if (interactive) {
|
|
622
|
+
prompts3.outro("Done");
|
|
623
|
+
}
|
|
624
|
+
} finally {
|
|
625
|
+
keepAlive?.abort();
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
// src/spall/commands/commit.ts
|
|
630
|
+
import consola8 from "consola";
|
|
631
|
+
import { Client as Client7 } from "@spader/spall-sdk/client";
|
|
632
|
+
import { defaultTheme as theme3 } from "@spader/spall-cli/shared";
|
|
633
|
+
var commit = {
|
|
634
|
+
description: "Update note weights based on queries since last commit",
|
|
635
|
+
handler: async () => {
|
|
636
|
+
const client = await Client7.connect();
|
|
637
|
+
const res = await client.commit.run({ body: {} }).then(Client7.unwrap);
|
|
638
|
+
const moved = Number(res?.moved ?? 0);
|
|
639
|
+
if (moved === 0) {
|
|
640
|
+
consola8.info("No staged events to commit");
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
consola8.success(`Committed ${theme3.primary(String(moved))} event(s)`);
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
// src/spall/commands/hook.ts
|
|
647
|
+
import { hooks, supportedShells } from "@spader/spall-integration";
|
|
648
|
+
var hook = {
|
|
649
|
+
description: "Print shell hook for completion support",
|
|
650
|
+
hidden: true,
|
|
651
|
+
positionals: {
|
|
652
|
+
shell: {
|
|
653
|
+
type: "string",
|
|
654
|
+
description: `Shell to generate hook for (${supportedShells.join(", ")})`,
|
|
655
|
+
required: true
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
handler: async (argv) => {
|
|
659
|
+
const shell = argv.shell;
|
|
660
|
+
if (!supportedShells.includes(shell)) {
|
|
661
|
+
console.error(`Unknown shell: ${shell}. Supported: ${supportedShells.join(", ")}`);
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
664
|
+
process.stdout.write(hooks[shell]);
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
// src/spall/commands/integrate/index.ts
|
|
668
|
+
import * as prompts6 from "@clack/prompts";
|
|
669
|
+
|
|
670
|
+
// src/spall/commands/integrate/shell.ts
|
|
671
|
+
import { existsSync as existsSync2, readFileSync, appendFileSync } from "fs";
|
|
672
|
+
import { homedir, platform } from "os";
|
|
673
|
+
import { join } from "path";
|
|
674
|
+
import * as prompts4 from "@clack/prompts";
|
|
675
|
+
import { snippets } from "@spader/spall-integration";
|
|
676
|
+
import { defaultTheme as theme4 } from "@spader/spall-cli/shared";
|
|
677
|
+
var bash = {
|
|
678
|
+
label: "bash",
|
|
679
|
+
hint: "path completions, cli completions",
|
|
680
|
+
handler: () => shellIntegration("bash")
|
|
681
|
+
};
|
|
682
|
+
var zsh = {
|
|
683
|
+
label: "zsh",
|
|
684
|
+
hint: "path completions, cli completions",
|
|
685
|
+
handler: () => shellIntegration("zsh")
|
|
686
|
+
};
|
|
687
|
+
var SPALL_CANARY = "@spall_canary";
|
|
688
|
+
var configs = {
|
|
689
|
+
bash: {
|
|
690
|
+
defaultRc: join(homedir(), platform() === "darwin" ? ".bash_profile" : ".bashrc")
|
|
691
|
+
},
|
|
692
|
+
zsh: {
|
|
693
|
+
defaultRc: join(homedir(), ".zshrc")
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
async function shellIntegration(shell) {
|
|
697
|
+
const cfg = configs[shell];
|
|
698
|
+
const snippet = snippets[shell];
|
|
699
|
+
const rcFile = await prompts4.text({
|
|
700
|
+
message: "Where should the hook be installed?",
|
|
701
|
+
initialValue: cfg.defaultRc,
|
|
702
|
+
validate: (s) => s && s.trim() ? undefined : "Required"
|
|
703
|
+
});
|
|
704
|
+
if (prompts4.isCancel(rcFile)) {
|
|
705
|
+
prompts4.cancel("Cancelled");
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const rc = String(rcFile).trim();
|
|
709
|
+
if (existsSync2(rc)) {
|
|
710
|
+
const contents = readFileSync(rc, "utf-8");
|
|
711
|
+
if (contents.includes(SPALL_CANARY)) {
|
|
712
|
+
prompts4.outro(`Hook is already installed in ${theme4.primary(rc)}`);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
appendFileSync(rc, `
|
|
717
|
+
` + snippet);
|
|
718
|
+
prompts4.log.info(`Added hook to ${theme4.primary(rc)}`);
|
|
719
|
+
prompts4.note(`Run ${theme4.code(`source ${rc}`)} or restart your shell to activate
|
|
720
|
+
|
|
721
|
+
Get completions for available commands:
|
|
722
|
+
> ${theme4.code("spall")} <tab>
|
|
723
|
+
|
|
724
|
+
Get completions for paths:
|
|
725
|
+
> ${theme4.code("spall list ai-gateway/tutorials/")} <tab>
|
|
726
|
+
ai-gateway/tutorials/create-first-aig-workers.mdx
|
|
727
|
+
ai-gateway/tutorials/index.mdx
|
|
728
|
+
ai-gateway/tutorials/deploy-aig-worker.mdx`);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// src/spall/commands/integrate/opencode.ts
|
|
732
|
+
import * as prompts5 from "@clack/prompts";
|
|
733
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
734
|
+
import { homedir as homedir2 } from "os";
|
|
735
|
+
import { dirname, join as join2, resolve } from "path";
|
|
736
|
+
import { defaultTheme as theme5 } from "@spader/spall-cli/shared";
|
|
737
|
+
var HELLO_PLUGIN_PKG = "@spader/spall-plugin-opencode";
|
|
738
|
+
function configCandidates(dir) {
|
|
739
|
+
return [join2(dir, "opencode.jsonc"), join2(dir, "opencode.json")];
|
|
740
|
+
}
|
|
741
|
+
function pickExistingConfigPath(dir) {
|
|
742
|
+
for (const p of configCandidates(dir)) {
|
|
743
|
+
if (existsSync3(p))
|
|
744
|
+
return p;
|
|
745
|
+
}
|
|
746
|
+
return null;
|
|
747
|
+
}
|
|
748
|
+
function findOpencodeDir(startDir) {
|
|
749
|
+
let dir = resolve(startDir);
|
|
750
|
+
while (true) {
|
|
751
|
+
const candidate = join2(dir, ".opencode");
|
|
752
|
+
if (existsSync3(candidate))
|
|
753
|
+
return candidate;
|
|
754
|
+
const parent = dirname(dir);
|
|
755
|
+
if (parent === dir)
|
|
756
|
+
return null;
|
|
757
|
+
dir = parent;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
var opencode = {
|
|
761
|
+
label: "opencode",
|
|
762
|
+
hint: "install an OpenCode plugin",
|
|
763
|
+
handler: async () => {
|
|
764
|
+
const scope = await prompts5.select({
|
|
765
|
+
message: "Install for",
|
|
766
|
+
options: [
|
|
767
|
+
{
|
|
768
|
+
value: "project",
|
|
769
|
+
label: "This project",
|
|
770
|
+
hint: "uses .opencode/plugins"
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
value: "global",
|
|
774
|
+
label: "Global",
|
|
775
|
+
hint: "uses ~/.config/opencode/plugins"
|
|
776
|
+
}
|
|
777
|
+
]
|
|
778
|
+
});
|
|
779
|
+
if (prompts5.isCancel(scope)) {
|
|
780
|
+
prompts5.cancel("Cancelled");
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
let configDir;
|
|
784
|
+
if (scope === "global") {
|
|
785
|
+
configDir = join2(homedir2(), ".config", "opencode");
|
|
786
|
+
} else {
|
|
787
|
+
const opencodeDir = findOpencodeDir(process.cwd());
|
|
788
|
+
if (!opencodeDir) {
|
|
789
|
+
prompts5.log.error(`No ${theme5.primary(".opencode")} directory found walking up from ${theme5.primary(process.cwd())}`);
|
|
790
|
+
prompts5.note(`Create one (or run OpenCode in this repo once), then re-run ${theme5.code("spall integrate opencode")}.`);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
configDir = opencodeDir;
|
|
794
|
+
}
|
|
795
|
+
const existingConfig = pickExistingConfigPath(configDir);
|
|
796
|
+
const configPath = existingConfig ?? join2(configDir, "opencode.jsonc");
|
|
797
|
+
if (!existsSync3(configPath)) {
|
|
798
|
+
const ok = await prompts5.confirm({
|
|
799
|
+
message: `Create ${theme5.primary(configPath)}?`,
|
|
800
|
+
initialValue: true
|
|
801
|
+
});
|
|
802
|
+
if (prompts5.isCancel(ok) || !ok) {
|
|
803
|
+
prompts5.cancel("Cancelled");
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
807
|
+
const initial = configPath.endsWith(".jsonc") ? `{
|
|
808
|
+
"$schema": "https://opencode.ai/config.json",
|
|
809
|
+
"plugin": ["${HELLO_PLUGIN_PKG}"]
|
|
810
|
+
}
|
|
811
|
+
` : JSON.stringify({
|
|
812
|
+
$schema: "https://opencode.ai/config.json",
|
|
813
|
+
plugin: [HELLO_PLUGIN_PKG]
|
|
814
|
+
}, null, 2) + `
|
|
815
|
+
`;
|
|
816
|
+
writeFileSync(configPath, initial, "utf-8");
|
|
817
|
+
prompts5.outro(`Wrote ${theme5.primary(configPath)}`);
|
|
818
|
+
prompts5.note("Restart OpenCode to load the plugin.");
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const raw = readFileSync2(configPath, "utf-8");
|
|
822
|
+
if (raw.includes(HELLO_PLUGIN_PKG)) {
|
|
823
|
+
prompts5.outro(`Already configured in ${theme5.primary(configPath)}`);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (configPath.endsWith(".jsonc")) {
|
|
827
|
+
prompts5.log.warn(`Detected JSONC config at ${theme5.primary(configPath)}. Automatic editing is disabled to preserve formatting/comments.`);
|
|
828
|
+
prompts5.note(`Please add ${theme5.code(`"${HELLO_PLUGIN_PKG}"`)} to the ${theme5.code("plugin")} array in ${theme5.primary(configPath)}.`);
|
|
829
|
+
prompts5.note("Restart OpenCode to load the plugin.");
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
let config;
|
|
833
|
+
try {
|
|
834
|
+
config = JSON.parse(raw);
|
|
835
|
+
} catch {
|
|
836
|
+
prompts5.log.error(`Failed to parse ${theme5.primary(configPath)} as JSON`);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
if (config == null || typeof config !== "object" || Array.isArray(config)) {
|
|
840
|
+
prompts5.log.error(`Unexpected config shape in ${theme5.primary(configPath)}`);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
const existing = config.plugin;
|
|
844
|
+
if (existing === undefined) {
|
|
845
|
+
config.plugin = [];
|
|
846
|
+
} else if (!Array.isArray(existing) || existing.some((x) => typeof x !== "string")) {
|
|
847
|
+
prompts5.log.error(`Expected ${theme5.primary("plugin")} to be an array of strings in ${theme5.primary(configPath)}`);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (!config.plugin.includes(HELLO_PLUGIN_PKG)) {
|
|
851
|
+
config.plugin.push(HELLO_PLUGIN_PKG);
|
|
852
|
+
}
|
|
853
|
+
if (!config.$schema) {
|
|
854
|
+
config.$schema = "https://opencode.ai/config.json";
|
|
855
|
+
}
|
|
856
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + `
|
|
857
|
+
`, "utf-8");
|
|
858
|
+
prompts5.outro(`Updated ${theme5.primary(configPath)}`);
|
|
859
|
+
prompts5.note("Restart OpenCode to load the plugin.");
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
// src/spall/commands/integrate/index.ts
|
|
864
|
+
var integrations = {
|
|
865
|
+
bash,
|
|
866
|
+
zsh,
|
|
867
|
+
opencode
|
|
868
|
+
};
|
|
869
|
+
var integrate = {
|
|
870
|
+
description: "Set up integrations with third party tools",
|
|
871
|
+
handler: async () => {
|
|
872
|
+
prompts6.intro("Integrations");
|
|
873
|
+
const choice = await prompts6.select({
|
|
874
|
+
message: "Select a tool",
|
|
875
|
+
options: Object.entries(integrations).map(([key, cfg]) => ({
|
|
876
|
+
value: key,
|
|
877
|
+
hint: cfg.hint,
|
|
878
|
+
label: cfg.label
|
|
879
|
+
}))
|
|
880
|
+
});
|
|
881
|
+
if (prompts6.isCancel(choice)) {
|
|
882
|
+
prompts6.cancel("Cancelled");
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
await integrations[choice].handler();
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
// src/spall/commands/serve.ts
|
|
889
|
+
import { Config } from "@spader/spall-core/config";
|
|
890
|
+
var serve = {
|
|
891
|
+
description: "Start the spall server",
|
|
892
|
+
options: {
|
|
893
|
+
kill: {
|
|
894
|
+
type: "boolean",
|
|
895
|
+
description: "Stop the running server (best-effort) and remove the lock file"
|
|
896
|
+
},
|
|
897
|
+
daemon: {
|
|
898
|
+
alias: "d",
|
|
899
|
+
type: "boolean",
|
|
900
|
+
description: "Do not stop after last client disconnects"
|
|
901
|
+
},
|
|
902
|
+
timeout: {
|
|
903
|
+
alias: "t",
|
|
904
|
+
type: "number",
|
|
905
|
+
description: "Seconds to wait after last client disconnects",
|
|
906
|
+
default: Config.get().server.idleTimeout
|
|
907
|
+
},
|
|
908
|
+
force: {
|
|
909
|
+
alias: "f",
|
|
910
|
+
type: "boolean",
|
|
911
|
+
description: "Kill existing server if running"
|
|
912
|
+
}
|
|
913
|
+
},
|
|
914
|
+
handler: async (argv) => {
|
|
915
|
+
if (argv.kill) {
|
|
916
|
+
const { Lock, checkHealth, isProcessAlive } = await import("@spader/spall-sdk/server");
|
|
917
|
+
const lock = Lock.read();
|
|
918
|
+
try {
|
|
919
|
+
let shutdownOk = false;
|
|
920
|
+
if (lock?.port != null) {
|
|
921
|
+
const controller = new AbortController;
|
|
922
|
+
const timeout = setTimeout(() => controller.abort(), 500);
|
|
923
|
+
try {
|
|
924
|
+
const res = await fetch(`http://127.0.0.1:${lock.port}/shutdown`, {
|
|
925
|
+
method: "POST",
|
|
926
|
+
signal: controller.signal
|
|
927
|
+
});
|
|
928
|
+
shutdownOk = res.ok;
|
|
929
|
+
} catch {} finally {
|
|
930
|
+
clearTimeout(timeout);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (shutdownOk) {
|
|
934
|
+
const start = Date.now();
|
|
935
|
+
while (Date.now() - start < 1000) {
|
|
936
|
+
if (lock?.pid != null && !isProcessAlive(lock.pid)) {
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
if (lock?.port != null && !await checkHealth(lock.port)) {
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
await Bun.sleep(50);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (lock?.pid != null) {
|
|
946
|
+
try {
|
|
947
|
+
process.kill(lock.pid, "SIGTERM");
|
|
948
|
+
} catch {}
|
|
949
|
+
}
|
|
950
|
+
} finally {
|
|
951
|
+
Lock.remove();
|
|
952
|
+
}
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
const { Server } = await import("@spader/spall-sdk/server");
|
|
956
|
+
const { port, stopped } = await Server.start({
|
|
957
|
+
persist: argv.daemon,
|
|
958
|
+
idleTimeoutMs: argv.timeout * 1000,
|
|
959
|
+
force: argv.force
|
|
960
|
+
});
|
|
961
|
+
await stopped;
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
// src/spall/commands/get.ts
|
|
965
|
+
import { Client as Client8 } from "@spader/spall-sdk/client";
|
|
966
|
+
import {
|
|
967
|
+
createQuery,
|
|
968
|
+
displayResults
|
|
969
|
+
} from "@spader/spall-cli/shared";
|
|
970
|
+
var get = {
|
|
971
|
+
description: "Get note(s) by path or glob",
|
|
972
|
+
positionals: {
|
|
973
|
+
path: {
|
|
974
|
+
type: "string",
|
|
975
|
+
description: "Path or glob to notes",
|
|
976
|
+
required: true
|
|
977
|
+
}
|
|
978
|
+
},
|
|
979
|
+
options: {
|
|
980
|
+
corpus: {
|
|
981
|
+
alias: "c",
|
|
982
|
+
type: "string",
|
|
983
|
+
description: "Corpus name"
|
|
984
|
+
},
|
|
985
|
+
max: {
|
|
986
|
+
alias: "n",
|
|
987
|
+
type: "number",
|
|
988
|
+
description: "Maximum number of notes to return"
|
|
989
|
+
},
|
|
990
|
+
output: {
|
|
991
|
+
alias: "o",
|
|
992
|
+
type: "string",
|
|
993
|
+
description: "Output format: list, tree, table, json"
|
|
994
|
+
},
|
|
995
|
+
all: {
|
|
996
|
+
alias: "a",
|
|
997
|
+
type: "boolean",
|
|
998
|
+
description: "Print all results without limiting output"
|
|
999
|
+
}
|
|
1000
|
+
},
|
|
1001
|
+
handler: async (argv) => {
|
|
1002
|
+
const client = await Client8.connect();
|
|
1003
|
+
const { query } = await createQuery({
|
|
1004
|
+
client,
|
|
1005
|
+
corpus: argv.corpus,
|
|
1006
|
+
tracked: false
|
|
1007
|
+
});
|
|
1008
|
+
const output = argv.output ?? (argv.path === "*" ? "tree" : "list");
|
|
1009
|
+
const showAll = argv.all === true;
|
|
1010
|
+
const notes = [];
|
|
1011
|
+
let cursor = undefined;
|
|
1012
|
+
const termRows = process.stdout.rows ?? 24;
|
|
1013
|
+
const displayRows = showAll || output === "json" ? Infinity : Math.max(1, termRows - (output === "table" ? 4 : 3));
|
|
1014
|
+
const fetchLimit = Math.min(argv.max ?? Infinity, displayRows + 1);
|
|
1015
|
+
while (notes.length < fetchLimit) {
|
|
1016
|
+
const page = await client.query.notes({
|
|
1017
|
+
id: String(query.id),
|
|
1018
|
+
path: argv.path,
|
|
1019
|
+
limit: Math.min(100, fetchLimit - notes.length),
|
|
1020
|
+
after: cursor
|
|
1021
|
+
}).then(Client8.unwrap);
|
|
1022
|
+
notes.push(...page.notes);
|
|
1023
|
+
if (!page.nextCursor)
|
|
1024
|
+
break;
|
|
1025
|
+
cursor = page.nextCursor;
|
|
1026
|
+
}
|
|
1027
|
+
displayResults(notes, {
|
|
1028
|
+
output,
|
|
1029
|
+
showAll,
|
|
1030
|
+
empty: "(no notes matching pattern)",
|
|
1031
|
+
path: (n) => n.path,
|
|
1032
|
+
id: (n) => String(n.id),
|
|
1033
|
+
preview: (n) => n.content
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
// src/spall/commands/list.ts
|
|
1038
|
+
import {
|
|
1039
|
+
defaultTheme,
|
|
1040
|
+
List,
|
|
1041
|
+
noteDirEntries,
|
|
1042
|
+
noteTreeEntries
|
|
1043
|
+
} from "@spader/spall-cli/shared";
|
|
1044
|
+
var list = {
|
|
1045
|
+
description: "List note paths as a tree",
|
|
1046
|
+
positionals: List.positionals,
|
|
1047
|
+
options: {
|
|
1048
|
+
...List.options,
|
|
1049
|
+
completion: {
|
|
1050
|
+
type: "boolean",
|
|
1051
|
+
description: "Output bare paths for shell completion"
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
handler: async (argv) => {
|
|
1055
|
+
const theme6 = defaultTheme;
|
|
1056
|
+
const rawInput = String(argv.path ?? "*");
|
|
1057
|
+
const isCompletion = Boolean(argv.completion);
|
|
1058
|
+
const { notes, located, includeNames } = await List.run({
|
|
1059
|
+
path: rawInput,
|
|
1060
|
+
corpus: argv.corpus,
|
|
1061
|
+
tracked: false,
|
|
1062
|
+
completion: isCompletion
|
|
1063
|
+
});
|
|
1064
|
+
if (isCompletion) {
|
|
1065
|
+
printCompletions(notes.map((n) => n.path), rawInput);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
if (notes.length === 0) {
|
|
1069
|
+
console.log("(no notes found)");
|
|
1070
|
+
if (!located && includeNames.length === 1 && includeNames[0] === "default") {
|
|
1071
|
+
console.log(`hint: no workspace found, only searched default corpus. run ${theme6.code("spall status")} to check workspace scope, or ${theme6.code("spall workspace init")} to create a workspace)`);
|
|
1072
|
+
}
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
const showAll = Boolean(argv.all);
|
|
1076
|
+
const maxDepth = showAll ? List.parseDepth(argv.depth ?? "max") : List.parseDepth(argv.depth);
|
|
1077
|
+
const entries = showAll ? noteTreeEntries(notes, maxDepth) : noteDirEntries(notes, maxDepth);
|
|
1078
|
+
for (const e of entries) {
|
|
1079
|
+
const indent = " ".repeat(e.depth);
|
|
1080
|
+
if (e.type === "dir") {
|
|
1081
|
+
const suffix = typeof e.noteCount === "number" ? theme6.dim(` (${e.noteCount} note${e.noteCount === 1 ? "" : "s"})`) : "";
|
|
1082
|
+
console.log(`${indent}${e.name}${suffix}`);
|
|
1083
|
+
} else {
|
|
1084
|
+
console.log(`${theme6.dim(indent)}${e.name}${theme6.dim(` (id: ${e.id})`)}`);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
function printCompletions(paths, rawInput) {
|
|
1090
|
+
const lastSlash = rawInput.lastIndexOf("/");
|
|
1091
|
+
const prefix = lastSlash >= 0 ? rawInput.slice(0, lastSlash + 1) : "";
|
|
1092
|
+
const seen = new Set;
|
|
1093
|
+
for (const p of paths) {
|
|
1094
|
+
if (prefix && !p.startsWith(prefix))
|
|
1095
|
+
continue;
|
|
1096
|
+
const rest = p.slice(prefix.length);
|
|
1097
|
+
const slash = rest.indexOf("/");
|
|
1098
|
+
if (slash >= 0) {
|
|
1099
|
+
const dir = prefix + rest.slice(0, slash + 1);
|
|
1100
|
+
if (!seen.has(dir)) {
|
|
1101
|
+
seen.add(dir);
|
|
1102
|
+
console.log(dir);
|
|
1103
|
+
}
|
|
1104
|
+
} else {
|
|
1105
|
+
if (!seen.has(p)) {
|
|
1106
|
+
seen.add(p);
|
|
1107
|
+
console.log(p);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
// src/spall/commands/search.ts
|
|
1113
|
+
import consola9 from "consola";
|
|
1114
|
+
import {
|
|
1115
|
+
defaultTheme as theme6,
|
|
1116
|
+
displayResults as displayResults2,
|
|
1117
|
+
highlightSnippet,
|
|
1118
|
+
Search
|
|
1119
|
+
} from "@spader/spall-cli/shared";
|
|
1120
|
+
var search = {
|
|
1121
|
+
description: `Full text keyword ${theme6.search()} against note content`,
|
|
1122
|
+
positionals: Search.positionals,
|
|
1123
|
+
options: {
|
|
1124
|
+
output: {
|
|
1125
|
+
alias: "o",
|
|
1126
|
+
type: "string",
|
|
1127
|
+
description: "Output format (table, json, tree, list)",
|
|
1128
|
+
default: "table"
|
|
1129
|
+
},
|
|
1130
|
+
...Search.options,
|
|
1131
|
+
mode: {
|
|
1132
|
+
alias: "m",
|
|
1133
|
+
type: "string",
|
|
1134
|
+
description: "Query mode (plain, fts)",
|
|
1135
|
+
default: "plain"
|
|
1136
|
+
}
|
|
1137
|
+
},
|
|
1138
|
+
handler: async (argv) => {
|
|
1139
|
+
const mode = String(argv.mode ?? "plain");
|
|
1140
|
+
if (mode !== "plain" && mode !== "fts") {
|
|
1141
|
+
consola9.error(`Invalid mode: ${theme6.primary(String(argv.mode))}`);
|
|
1142
|
+
consola9.info(`Use ${theme6.option("--mode")} plain | fts`);
|
|
1143
|
+
process.exit(1);
|
|
1144
|
+
}
|
|
1145
|
+
const out = String(argv.output ?? "table");
|
|
1146
|
+
const { results } = await Search.run({
|
|
1147
|
+
query: argv.query,
|
|
1148
|
+
corpus: argv.corpus,
|
|
1149
|
+
path: argv.path,
|
|
1150
|
+
limit: argv.limit,
|
|
1151
|
+
tracked: false,
|
|
1152
|
+
mode
|
|
1153
|
+
});
|
|
1154
|
+
displayResults2(results, {
|
|
1155
|
+
output: out,
|
|
1156
|
+
empty: "(no matches)",
|
|
1157
|
+
path: (r) => r.path,
|
|
1158
|
+
id: (r) => String(r.id),
|
|
1159
|
+
preview: (r) => r.snippet,
|
|
1160
|
+
previewFormat: highlightSnippet,
|
|
1161
|
+
extraColumns: [
|
|
1162
|
+
{
|
|
1163
|
+
header: "score",
|
|
1164
|
+
value: (r) => r.score.toFixed(3),
|
|
1165
|
+
flex: 0,
|
|
1166
|
+
noTruncate: true,
|
|
1167
|
+
format: (s) => theme6.code(s)
|
|
1168
|
+
}
|
|
1169
|
+
]
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
// src/spall/commands/sync.ts
|
|
1174
|
+
import pc3 from "picocolors";
|
|
1175
|
+
import consola10 from "consola";
|
|
1176
|
+
import { existsSync as existsSync4, statSync } from "fs";
|
|
1177
|
+
import { basename as basename2, resolve as resolve2 } from "path";
|
|
1178
|
+
import * as prompts7 from "@clack/prompts";
|
|
1179
|
+
var {Glob } = globalThis.Bun;
|
|
1180
|
+
import { Client as Client9 } from "@spader/spall-sdk/client";
|
|
1181
|
+
import {
|
|
1182
|
+
defaultTheme as theme7,
|
|
1183
|
+
createModelProgressHandler as createModelProgressHandler2
|
|
1184
|
+
} from "@spader/spall-cli/shared";
|
|
1185
|
+
function keepServerAlive2(client, signal) {
|
|
1186
|
+
(async () => {
|
|
1187
|
+
try {
|
|
1188
|
+
const { stream } = await client.events({ signal });
|
|
1189
|
+
for await (const _ev of stream) {}
|
|
1190
|
+
} catch {}
|
|
1191
|
+
})();
|
|
1192
|
+
}
|
|
1193
|
+
function canonicalize(path) {
|
|
1194
|
+
let p = path.replace(/\\/g, "/");
|
|
1195
|
+
p = p.replace(/\/+$/, "");
|
|
1196
|
+
p = p.replace(/^\.\//, "");
|
|
1197
|
+
p = p.replace(/^\//, "");
|
|
1198
|
+
p = p.replace(/\/+/g, "/");
|
|
1199
|
+
if (p === ".")
|
|
1200
|
+
return "";
|
|
1201
|
+
return p;
|
|
1202
|
+
}
|
|
1203
|
+
async function countFiles(dir, globPattern) {
|
|
1204
|
+
const glob = new Glob(globPattern);
|
|
1205
|
+
let count = 0;
|
|
1206
|
+
let first = null;
|
|
1207
|
+
for await (const file of glob.scan({ cwd: dir, absolute: false })) {
|
|
1208
|
+
count++;
|
|
1209
|
+
if (!first)
|
|
1210
|
+
first = file;
|
|
1211
|
+
}
|
|
1212
|
+
return { count, first };
|
|
1213
|
+
}
|
|
1214
|
+
async function extensionCounts(dir) {
|
|
1215
|
+
const glob = new Glob("**/*");
|
|
1216
|
+
const counts = new Map;
|
|
1217
|
+
for await (const file of glob.scan({ cwd: dir, absolute: false })) {
|
|
1218
|
+
const base = file.split("/").pop() ?? file;
|
|
1219
|
+
const dot = base.lastIndexOf(".");
|
|
1220
|
+
if (dot <= 0 || dot === base.length - 1)
|
|
1221
|
+
continue;
|
|
1222
|
+
const ext = base.slice(dot + 1);
|
|
1223
|
+
counts.set(ext, (counts.get(ext) ?? 0) + 1);
|
|
1224
|
+
}
|
|
1225
|
+
return counts;
|
|
1226
|
+
}
|
|
1227
|
+
function pickDefaultMask(counts) {
|
|
1228
|
+
let bestExt = null;
|
|
1229
|
+
let bestCount = 0;
|
|
1230
|
+
for (const [ext, count] of counts) {
|
|
1231
|
+
if (count > bestCount) {
|
|
1232
|
+
bestExt = ext;
|
|
1233
|
+
bestCount = count;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
return bestExt ? `*.${bestExt}` : "*";
|
|
1237
|
+
}
|
|
1238
|
+
var sync = {
|
|
1239
|
+
description: "Sync a directory under a path in a corpus",
|
|
1240
|
+
positionals: {
|
|
1241
|
+
dir: {
|
|
1242
|
+
type: "string",
|
|
1243
|
+
description: "Directory to scan, recursively",
|
|
1244
|
+
required: true
|
|
1245
|
+
}
|
|
1246
|
+
},
|
|
1247
|
+
options: {
|
|
1248
|
+
glob: {
|
|
1249
|
+
alias: "g",
|
|
1250
|
+
type: "string",
|
|
1251
|
+
description: "Glob pattern to match (default: **/*.md)"
|
|
1252
|
+
},
|
|
1253
|
+
mask: {
|
|
1254
|
+
alias: "m",
|
|
1255
|
+
type: "string",
|
|
1256
|
+
description: "File mask to match (default: *.md)"
|
|
1257
|
+
},
|
|
1258
|
+
corpus: {
|
|
1259
|
+
alias: "c",
|
|
1260
|
+
type: "string",
|
|
1261
|
+
description: "Corpus name",
|
|
1262
|
+
default: "default"
|
|
1263
|
+
},
|
|
1264
|
+
path: {
|
|
1265
|
+
alias: "p",
|
|
1266
|
+
type: "string",
|
|
1267
|
+
description: "Destination path prefix in corpus"
|
|
1268
|
+
},
|
|
1269
|
+
interactive: {
|
|
1270
|
+
type: "boolean",
|
|
1271
|
+
description: "Enable interactive prompts (use --no-interactive to disable)",
|
|
1272
|
+
default: true
|
|
1273
|
+
}
|
|
1274
|
+
},
|
|
1275
|
+
handler: async (argv) => {
|
|
1276
|
+
let onSigint = null;
|
|
1277
|
+
const controller = new AbortController;
|
|
1278
|
+
try {
|
|
1279
|
+
const inputDir = argv.dir;
|
|
1280
|
+
const dir = resolve2(inputDir);
|
|
1281
|
+
if (!existsSync4(dir) || !statSync(dir).isDirectory()) {
|
|
1282
|
+
consola10.error(`Not a directory: ${theme7.primary(inputDir)}`);
|
|
1283
|
+
process.exit(1);
|
|
1284
|
+
}
|
|
1285
|
+
let interrupted = false;
|
|
1286
|
+
onSigint = () => {
|
|
1287
|
+
if (interrupted)
|
|
1288
|
+
process.exit(130);
|
|
1289
|
+
interrupted = true;
|
|
1290
|
+
controller.abort();
|
|
1291
|
+
process.exit(130);
|
|
1292
|
+
};
|
|
1293
|
+
process.on("SIGINT", onSigint);
|
|
1294
|
+
const client = await Client9.connect(controller.signal);
|
|
1295
|
+
keepServerAlive2(client, controller.signal);
|
|
1296
|
+
const interactive = argv.interactive !== false && process.stdin.isTTY && process.stdout.isTTY;
|
|
1297
|
+
const corpora = await client.corpus.list().then(Client9.unwrap);
|
|
1298
|
+
const corpusOptions = corpora.map((c) => {
|
|
1299
|
+
const name = c.name;
|
|
1300
|
+
const noteCount = typeof c.noteCount === "number" ? c.noteCount : null;
|
|
1301
|
+
return {
|
|
1302
|
+
label: name,
|
|
1303
|
+
value: name,
|
|
1304
|
+
hint: noteCount == null ? undefined : `${noteCount} notes`
|
|
1305
|
+
};
|
|
1306
|
+
}).sort((a, b) => a.label.localeCompare(b.label));
|
|
1307
|
+
const CREATE = "__create_corpus__";
|
|
1308
|
+
const corpusPickerOptions = [
|
|
1309
|
+
...corpusOptions,
|
|
1310
|
+
{ label: "Create new corpus...", value: CREATE, hint: "new" }
|
|
1311
|
+
];
|
|
1312
|
+
const defaultCorpusName = String(argv.corpus ?? "default");
|
|
1313
|
+
const initialCorpus = corpusOptions.some((o) => o.value === defaultCorpusName) ? defaultCorpusName : corpusOptions[0]?.value;
|
|
1314
|
+
const cliPath = typeof argv.path === "string" ? argv.path : "";
|
|
1315
|
+
const cliGlob = typeof argv.glob === "string" ? argv.glob : undefined;
|
|
1316
|
+
const cliMask = typeof argv.mask === "string" ? argv.mask : undefined;
|
|
1317
|
+
let corpusName = initialCorpus ?? "default";
|
|
1318
|
+
const defaultDestPrefix = cliPath || basename2(inputDir) || inputDir;
|
|
1319
|
+
let destPrefix = defaultDestPrefix;
|
|
1320
|
+
let mask = cliMask ?? (cliGlob ? cliGlob.replace(/^\*\*\//, "") : "");
|
|
1321
|
+
if (!mask) {
|
|
1322
|
+
const counts = await extensionCounts(dir);
|
|
1323
|
+
mask = pickDefaultMask(counts);
|
|
1324
|
+
}
|
|
1325
|
+
if (interactive) {
|
|
1326
|
+
prompts7.intro(`${theme7.dim("spall")} ${theme7.primary("sync")}`);
|
|
1327
|
+
const pickedCorpus = await prompts7.autocomplete({
|
|
1328
|
+
message: "Select a corpus",
|
|
1329
|
+
options: corpusPickerOptions,
|
|
1330
|
+
placeholder: "Type to filter...",
|
|
1331
|
+
maxItems: 12,
|
|
1332
|
+
initialValue: corpusName
|
|
1333
|
+
});
|
|
1334
|
+
if (prompts7.isCancel(pickedCorpus)) {
|
|
1335
|
+
controller.abort();
|
|
1336
|
+
prompts7.outro("Cancelled");
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
if (String(pickedCorpus) === CREATE) {
|
|
1340
|
+
const name = await prompts7.text({
|
|
1341
|
+
message: "New corpus name",
|
|
1342
|
+
initialValue: basename2(inputDir) || "docs",
|
|
1343
|
+
validate: (s) => s && s.trim().length > 0 ? undefined : "Required"
|
|
1344
|
+
});
|
|
1345
|
+
if (prompts7.isCancel(name)) {
|
|
1346
|
+
controller.abort();
|
|
1347
|
+
prompts7.outro("Cancelled");
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
corpusName = String(name).trim();
|
|
1351
|
+
await client.corpus.create({ name: corpusName }).then(Client9.unwrap);
|
|
1352
|
+
} else {
|
|
1353
|
+
corpusName = String(pickedCorpus);
|
|
1354
|
+
}
|
|
1355
|
+
const pickedPath = await prompts7.text({
|
|
1356
|
+
message: "Destination path prefix",
|
|
1357
|
+
placeholder: "(e.g. ai-gateway)",
|
|
1358
|
+
initialValue: destPrefix
|
|
1359
|
+
});
|
|
1360
|
+
if (prompts7.isCancel(pickedPath)) {
|
|
1361
|
+
controller.abort();
|
|
1362
|
+
prompts7.outro("Cancelled");
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
destPrefix = String(pickedPath ?? "");
|
|
1366
|
+
const pickedMask = await prompts7.text({
|
|
1367
|
+
message: "File mask",
|
|
1368
|
+
placeholder: "(e.g. *.mdx)",
|
|
1369
|
+
initialValue: mask
|
|
1370
|
+
});
|
|
1371
|
+
if (prompts7.isCancel(pickedMask)) {
|
|
1372
|
+
controller.abort();
|
|
1373
|
+
prompts7.outro("Cancelled");
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
mask = String(pickedMask ?? "");
|
|
1377
|
+
}
|
|
1378
|
+
const corpus2 = await client.corpus.get({ name: corpusName }).catch(() => {
|
|
1379
|
+
consola10.error(`Corpus not found: ${theme7.command(String(corpusName))}`);
|
|
1380
|
+
process.exit(1);
|
|
1381
|
+
}).then(Client9.unwrap);
|
|
1382
|
+
const globPattern = cliGlob ?? `**/${mask || "*.md"}`;
|
|
1383
|
+
const { count: fileCount, first: firstFile } = await countFiles(dir, globPattern);
|
|
1384
|
+
if (interactive) {
|
|
1385
|
+
const prefixLabel = canonicalize(destPrefix) || "(root)";
|
|
1386
|
+
const msg = `Syncing ${theme7.code(String(fileCount))} files under ${theme7.dim(prefixLabel)}${theme7.dim("/")} in corpus ${theme7.primary(corpusName)}`;
|
|
1387
|
+
const ok = await prompts7.confirm({ message: msg, initialValue: true });
|
|
1388
|
+
if (prompts7.isCancel(ok) || ok === false) {
|
|
1389
|
+
controller.abort();
|
|
1390
|
+
prompts7.outro("Cancelled");
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
const handleModelEvent = (() => {
|
|
1395
|
+
const base = createModelProgressHandler2();
|
|
1396
|
+
return (event) => {
|
|
1397
|
+
switch (event?.tag) {
|
|
1398
|
+
case "model.load":
|
|
1399
|
+
prompts7.log.info(`Loading model ${theme7.primary(event.info.name)}`);
|
|
1400
|
+
return;
|
|
1401
|
+
case "model.download":
|
|
1402
|
+
prompts7.log.info(`Downloading model ${theme7.primary(event.info.name)}`);
|
|
1403
|
+
return;
|
|
1404
|
+
case "model.downloaded":
|
|
1405
|
+
prompts7.log.success(`Downloaded ${theme7.primary(event.info.name)}`);
|
|
1406
|
+
return;
|
|
1407
|
+
case "model.progress":
|
|
1408
|
+
return;
|
|
1409
|
+
default:
|
|
1410
|
+
return base(event);
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
})();
|
|
1414
|
+
let scanTotal = 0;
|
|
1415
|
+
let scanProcessed = 0;
|
|
1416
|
+
let scanProgress = null;
|
|
1417
|
+
let embedTotalBytes = 0;
|
|
1418
|
+
let embedTotalFiles = 0;
|
|
1419
|
+
let embedStartTime = 0;
|
|
1420
|
+
let embedBytesProcessed = 0;
|
|
1421
|
+
let embedProgress = null;
|
|
1422
|
+
let ftsSpinner = null;
|
|
1423
|
+
let ftsActive = false;
|
|
1424
|
+
const { stream } = await client.sse.note.sync({
|
|
1425
|
+
directory: dir,
|
|
1426
|
+
glob: globPattern,
|
|
1427
|
+
corpus: corpus2.id,
|
|
1428
|
+
path: destPrefix
|
|
1429
|
+
}, { signal: controller.signal });
|
|
1430
|
+
for await (const event of stream) {
|
|
1431
|
+
if (event?.tag === "error") {
|
|
1432
|
+
const e = event.error;
|
|
1433
|
+
const msg = e?.message ?? String(e ?? "unknown error");
|
|
1434
|
+
const code = e?.code ? `${e.code}: ` : "";
|
|
1435
|
+
consola10.error(code + msg);
|
|
1436
|
+
process.exit(1);
|
|
1437
|
+
}
|
|
1438
|
+
handleModelEvent(event);
|
|
1439
|
+
switch (event?.tag) {
|
|
1440
|
+
case "scan.start":
|
|
1441
|
+
scanTotal = event.numFiles;
|
|
1442
|
+
scanProgress = prompts7.progress({
|
|
1443
|
+
max: Math.max(1, scanTotal),
|
|
1444
|
+
indicator: "timer"
|
|
1445
|
+
});
|
|
1446
|
+
scanProgress.start(`Scanning ${theme7.primary(dir)} (${theme7.primary(String(scanTotal))} files)`);
|
|
1447
|
+
break;
|
|
1448
|
+
case "scan.progress": {
|
|
1449
|
+
scanProcessed++;
|
|
1450
|
+
scanProgress?.advance(1, `Scanning ${scanProcessed}/${scanTotal}`);
|
|
1451
|
+
break;
|
|
1452
|
+
}
|
|
1453
|
+
case "scan.done":
|
|
1454
|
+
scanProgress?.stop(`Scan done (added: ${event.added}, modified: ${event.modified}, removed: ${event.removed}, ok: ${event.ok})`);
|
|
1455
|
+
scanProgress = null;
|
|
1456
|
+
break;
|
|
1457
|
+
case "embed.start":
|
|
1458
|
+
embedTotalFiles = event.numFiles;
|
|
1459
|
+
embedTotalBytes = event.numBytes;
|
|
1460
|
+
embedStartTime = performance.now();
|
|
1461
|
+
embedBytesProcessed = 0;
|
|
1462
|
+
const sizeStr = embedTotalBytes >= 1024 * 1024 ? `${(embedTotalBytes / (1024 * 1024)).toFixed(1)} MB` : embedTotalBytes >= 1024 ? `${(embedTotalBytes / 1024).toFixed(1)} KB` : `${embedTotalBytes} B`;
|
|
1463
|
+
embedProgress = prompts7.progress({
|
|
1464
|
+
max: Math.max(1, embedTotalBytes),
|
|
1465
|
+
indicator: "timer"
|
|
1466
|
+
});
|
|
1467
|
+
embedProgress.start(`Embedding ${event.numChunks} chunks from ${event.numFiles} files ${pc3.dim(`(${sizeStr})`)}`);
|
|
1468
|
+
break;
|
|
1469
|
+
case "embed.progress": {
|
|
1470
|
+
const delta = Math.max(0, event.numBytesProcessed - embedBytesProcessed);
|
|
1471
|
+
embedBytesProcessed = event.numBytesProcessed;
|
|
1472
|
+
const percent = embedTotalBytes ? event.numBytesProcessed / embedTotalBytes * 100 : 0;
|
|
1473
|
+
const percentStr = percent.toFixed(0).padStart(3);
|
|
1474
|
+
const elapsed = (performance.now() - embedStartTime) / 1000;
|
|
1475
|
+
const bps = elapsed > 0 ? event.numBytesProcessed / elapsed : 0;
|
|
1476
|
+
const bpsStr = bps >= 1024 * 1024 ? `${(bps / (1024 * 1024)).toFixed(1)} MB/s` : bps >= 1024 ? `${(bps / 1024).toFixed(1)} KB/s` : `${bps.toFixed(0)} B/s`;
|
|
1477
|
+
embedProgress?.advance(delta, `${pc3.bold(percentStr + "%")} ${event.numFilesProcessed}/${embedTotalFiles} ${pc3.dim(`(${bpsStr})`)}`);
|
|
1478
|
+
break;
|
|
1479
|
+
}
|
|
1480
|
+
case "embed.cancel": {
|
|
1481
|
+
embedProgress?.stop(`CANCELLED ${event.numFilesProcessed}/${embedTotalFiles}`);
|
|
1482
|
+
embedProgress = null;
|
|
1483
|
+
prompts7.log.warn("Index cancelled");
|
|
1484
|
+
break;
|
|
1485
|
+
}
|
|
1486
|
+
case "embed.done":
|
|
1487
|
+
embedProgress?.stop("Index complete");
|
|
1488
|
+
embedProgress = null;
|
|
1489
|
+
break;
|
|
1490
|
+
case "fts.start":
|
|
1491
|
+
ftsActive = true;
|
|
1492
|
+
ftsSpinner = prompts7.spinner({ indicator: "timer" });
|
|
1493
|
+
ftsSpinner.start("Indexing text (FTS)");
|
|
1494
|
+
break;
|
|
1495
|
+
case "fts.done":
|
|
1496
|
+
if (ftsActive) {
|
|
1497
|
+
ftsActive = false;
|
|
1498
|
+
ftsSpinner?.stop("FTS index updated");
|
|
1499
|
+
ftsSpinner = null;
|
|
1500
|
+
}
|
|
1501
|
+
break;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
if (onSigint)
|
|
1505
|
+
process.off("SIGINT", onSigint);
|
|
1506
|
+
} catch (e) {
|
|
1507
|
+
if (onSigint)
|
|
1508
|
+
process.off("SIGINT", onSigint);
|
|
1509
|
+
const msg = e?.message ?? String(e ?? "Unknown error");
|
|
1510
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
1511
|
+
prompts7.log.error(msg);
|
|
1512
|
+
prompts7.outro("Done");
|
|
1513
|
+
} else {
|
|
1514
|
+
consola10.error(msg);
|
|
1515
|
+
}
|
|
1516
|
+
process.exit(1);
|
|
1517
|
+
} finally {
|
|
1518
|
+
controller.abort();
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
// src/spall/commands/tui.ts
|
|
1523
|
+
import { db } from "@spader/spall-tui/store";
|
|
1524
|
+
var tui = {
|
|
1525
|
+
description: "Launch the interactive TUI",
|
|
1526
|
+
handler: async () => {
|
|
1527
|
+
db.init();
|
|
1528
|
+
await import("@opentui/solid/preload");
|
|
1529
|
+
const { tui: tui2 } = await import("@spader/spall-tui/app");
|
|
1530
|
+
await tui2({ repoPath: process.cwd() });
|
|
1531
|
+
}
|
|
1532
|
+
};
|
|
1533
|
+
// src/spall/commands/vsearch.ts
|
|
1534
|
+
import { displayResults as displayResults3, Vsearch } from "@spader/spall-cli/shared";
|
|
1535
|
+
function rgb(r, g, b) {
|
|
1536
|
+
return (s) => `\x1B[38;2;${r};${g};${b}m${s}\x1B[39m`;
|
|
1537
|
+
}
|
|
1538
|
+
function hsvToRgb(h, s, v) {
|
|
1539
|
+
const c = v * s;
|
|
1540
|
+
const x = c * (1 - Math.abs(h / 60 % 2 - 1));
|
|
1541
|
+
const m = v - c;
|
|
1542
|
+
let r = 0, g = 0, b = 0;
|
|
1543
|
+
if (h < 60)
|
|
1544
|
+
[r, g, b] = [c, x, 0];
|
|
1545
|
+
else if (h < 120)
|
|
1546
|
+
[r, g, b] = [x, c, 0];
|
|
1547
|
+
else if (h < 180)
|
|
1548
|
+
[r, g, b] = [0, c, x];
|
|
1549
|
+
else if (h < 240)
|
|
1550
|
+
[r, g, b] = [0, x, c];
|
|
1551
|
+
else if (h < 300)
|
|
1552
|
+
[r, g, b] = [x, 0, c];
|
|
1553
|
+
else
|
|
1554
|
+
[r, g, b] = [c, 0, x];
|
|
1555
|
+
return [
|
|
1556
|
+
Math.round((r + m) * 255),
|
|
1557
|
+
Math.round((g + m) * 255),
|
|
1558
|
+
Math.round((b + m) * 255)
|
|
1559
|
+
];
|
|
1560
|
+
}
|
|
1561
|
+
function heatColor(score) {
|
|
1562
|
+
const shifted = (score - 0.5) / 0.35;
|
|
1563
|
+
const t = Math.max(0, Math.min(1, shifted));
|
|
1564
|
+
const sat = t * t;
|
|
1565
|
+
const [r, g, b] = hsvToRgb(140, sat, 0.85);
|
|
1566
|
+
return rgb(r, g, b);
|
|
1567
|
+
}
|
|
1568
|
+
var vsearch = {
|
|
1569
|
+
summary: Vsearch.summary,
|
|
1570
|
+
description: Vsearch.description("spall"),
|
|
1571
|
+
positionals: Vsearch.positionals,
|
|
1572
|
+
options: {
|
|
1573
|
+
...Vsearch.options,
|
|
1574
|
+
output: {
|
|
1575
|
+
alias: "o",
|
|
1576
|
+
type: "string",
|
|
1577
|
+
description: "Output format: table | json | tree | list",
|
|
1578
|
+
default: "table"
|
|
1579
|
+
}
|
|
1580
|
+
},
|
|
1581
|
+
handler: async (argv) => {
|
|
1582
|
+
const out = String(argv.output ?? "table");
|
|
1583
|
+
const { results } = await Vsearch.run({
|
|
1584
|
+
query: argv.query,
|
|
1585
|
+
corpus: argv.corpus,
|
|
1586
|
+
path: argv.path,
|
|
1587
|
+
limit: argv.limit,
|
|
1588
|
+
tracked: false
|
|
1589
|
+
});
|
|
1590
|
+
const scoreMap = new Map(results.map((r, i) => [i, r.score]));
|
|
1591
|
+
displayResults3(results, {
|
|
1592
|
+
output: out,
|
|
1593
|
+
empty: "(no matches)",
|
|
1594
|
+
path: (r) => r.path,
|
|
1595
|
+
id: (r) => String(r.id),
|
|
1596
|
+
preview: (r) => Vsearch.collapseWhitespace(r.chunk),
|
|
1597
|
+
extraColumns: [
|
|
1598
|
+
{
|
|
1599
|
+
header: "score",
|
|
1600
|
+
value: (r) => r.score.toFixed(3),
|
|
1601
|
+
flex: 0,
|
|
1602
|
+
noTruncate: true,
|
|
1603
|
+
format: (s, row) => heatColor(scoreMap.get(row) ?? 0)(s)
|
|
1604
|
+
}
|
|
1605
|
+
]
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
// src/spall/commands/status.ts
|
|
1610
|
+
import consola11 from "consola";
|
|
1611
|
+
import { Status } from "@spader/spall-cli/shared";
|
|
1612
|
+
var status = {
|
|
1613
|
+
summary: Status.summary,
|
|
1614
|
+
description: Status.description,
|
|
1615
|
+
handler: async () => {
|
|
1616
|
+
const result = await Status.run();
|
|
1617
|
+
if ("error" in result) {
|
|
1618
|
+
consola11.error("Failed to list corpora:", result.error);
|
|
1619
|
+
process.exit(1);
|
|
1620
|
+
}
|
|
1621
|
+
Status.print(result, { highlightWorkspace: true });
|
|
1622
|
+
}
|
|
1623
|
+
};
|
|
1624
|
+
// src/spall/index.ts
|
|
1625
|
+
var cliDef = {
|
|
1626
|
+
name: "spall",
|
|
1627
|
+
description: "Fast, local, searchable memory for LLMs and humans",
|
|
1628
|
+
commands: {
|
|
1629
|
+
workspace,
|
|
1630
|
+
add: add2,
|
|
1631
|
+
get,
|
|
1632
|
+
list,
|
|
1633
|
+
status,
|
|
1634
|
+
search,
|
|
1635
|
+
vsearch,
|
|
1636
|
+
sync,
|
|
1637
|
+
commit,
|
|
1638
|
+
integrate,
|
|
1639
|
+
corpus,
|
|
1640
|
+
serve,
|
|
1641
|
+
tui,
|
|
1642
|
+
hook
|
|
1643
|
+
}
|
|
1644
|
+
};
|
|
1645
|
+
setActiveCli(cliDef);
|
|
1646
|
+
build(cliDef).parse();
|
|
1647
|
+
|
|
1648
|
+
//# debugId=851406C8532EA36164756E2164756E21
|