heyspex 0.2.4 → 0.2.6
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/.claude-plugin/marketplace.json +1 -1
- package/bin/hs.mjs +281 -30
- package/dev-factory/lib/dispatch-guard.mjs +75 -0
- package/dev-factory/lib/runner-path.mjs +51 -0
- package/dev-factory/spexcrawl/spexcrawl-cli.mjs +734 -0
- package/dist/cli/hs-connect.mjs +20 -1
- package/dist/cli/hs-defer.mjs +3802 -3744
- package/dist/cli/hs-events.mjs +1296 -0
- package/dist/cli/hs-export-events.mjs +1252 -0
- package/dist/cli/hs-handoff.mjs +2576 -0
- package/dist/cli/hs-index-rebuild.mjs +209 -36
- package/dist/cli/hs-index-reconcile.mjs +212 -39
- package/dist/cli/hs-init.mjs +20 -1
- package/dist/cli/hs-install.mjs +20 -1
- package/dist/cli/hs-join.mjs +20 -1
- package/dist/cli/hs-next-launch.mjs +2698 -0
- package/dist/cli/hs-pin.mjs +1336 -0
- package/dist/cli/hs-prereq.mjs +1380 -0
- package/dist/cli/hs-revalidate.mjs +3257 -0
- package/dist/cli/hs-spawn.mjs +12377 -0
- package/dist/cli/hs-test.mjs +1046 -0
- package/dist/cli/hs-trash.mjs +362 -0
- package/dist/cli/hs-trust.mjs +197 -0
- package/dist/cli/hs-vault-pull.mjs +214 -41
- package/dist/cli/hs-vault-push.mjs +212 -39
- package/dist/cli-templates/harness-commands/claude/hs-log.md +1 -1
- package/dist/cli-templates/harness-commands/claude/hs-res.md +71 -63
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -23
- package/dist/cli-templates/harness-commands/cursor/hs-log.md +1 -1
- package/dist/cli-templates/harness-commands/cursor/hs-res.md +66 -58
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +18 -18
- package/docs/copilot-plugin-install.md +2 -2
- package/package.json +9 -5
- package/plugins/heyspex/.claude-plugin/plugin.json +1 -1
- package/plugins/heyspex/.cursor-plugin/plugin.json +1 -1
- package/scripts/claude-hooks/wip-checkpoint.mjs +17 -2
- package/scripts/claude-statusline/statusline-command.mjs +287 -27
- package/scripts/hs-companion.mjs +8 -0
- package/scripts/install-claude-code-hooks.mjs +104 -3
- package/scripts/install-copilot-hooks.mjs +21 -1
- package/scripts/install-cursor-hooks.mjs +4 -3
- package/scripts/lib/capture-execution-guard.mjs +221 -0
- package/scripts/lib/customer-kit.mjs +48 -0
- package/scripts/lib/hs-companion-settings.mjs +25 -0
- package/scripts/record-grounding.mjs +38 -0
- package/vendor/heyspex-mcp/http-transport.mjs +1 -1
- package/vendor/heyspex-mcp/test-helpers.mjs +37 -23
- package/bin/postinstall.mjs +0 -88
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { createRequire as __hsCreateRequire } from 'node:module';
|
|
2
|
+
const require = __hsCreateRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// spex-console/bin/hs-trash.ts
|
|
5
|
+
import { existsSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import process2 from "node:process";
|
|
8
|
+
|
|
9
|
+
// spex-console/src/lib/cli/trash.ts
|
|
10
|
+
var TRASH_EXIT = { OK: 0, USAGE: 2, RUNTIME: 1, NOT_FOUND: 10 };
|
|
11
|
+
var TRASH_SUBCOMMANDS = ["list", "restore", "empty"];
|
|
12
|
+
var DEFAULT_RETENTION_DAYS = 30;
|
|
13
|
+
function restorePathFor(trashRelPath) {
|
|
14
|
+
const normalized = trashRelPath.split("\\").join("/");
|
|
15
|
+
const slashIdx = normalized.lastIndexOf("/");
|
|
16
|
+
const dir = slashIdx === -1 ? "" : normalized.slice(0, slashIdx);
|
|
17
|
+
const name = slashIdx === -1 ? normalized : normalized.slice(slashIdx + 1);
|
|
18
|
+
const parentDir = dir.replace(/(^|\/)\.trash$/, "");
|
|
19
|
+
const basename = name.replace(TRASH_STAMP_RE, "");
|
|
20
|
+
return parentDir ? `${parentDir}/${basename}` : basename;
|
|
21
|
+
}
|
|
22
|
+
var TRASH_STAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(?:-\d{3})?Z-/;
|
|
23
|
+
function trashedAtFrom(name) {
|
|
24
|
+
const m = TRASH_STAMP_RE.exec(name);
|
|
25
|
+
if (!m) return null;
|
|
26
|
+
const stamp = m[0].slice(0, -1);
|
|
27
|
+
const iso = stamp.replace(
|
|
28
|
+
/T(\d{2})-(\d{2})-(\d{2})(?:-(\d{3}))?Z$/,
|
|
29
|
+
(_all, h, min, sec, ms) => `T${h}:${min}:${sec}${ms ? `.${ms}` : ""}Z`
|
|
30
|
+
);
|
|
31
|
+
const parsed = new Date(iso);
|
|
32
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
33
|
+
}
|
|
34
|
+
function ageDaysFrom(trashedAt, now) {
|
|
35
|
+
if (!trashedAt) return null;
|
|
36
|
+
return Math.max(0, Math.floor((now.getTime() - trashedAt.getTime()) / 864e5));
|
|
37
|
+
}
|
|
38
|
+
function parseTrashArgs(argv) {
|
|
39
|
+
if (argv.includes("--help") || argv.includes("-h")) return { help: true };
|
|
40
|
+
let json = false;
|
|
41
|
+
let yes = false;
|
|
42
|
+
let dryRun = false;
|
|
43
|
+
let olderThanDays;
|
|
44
|
+
const positionals = [];
|
|
45
|
+
for (let i = 0; i < argv.length; i++) {
|
|
46
|
+
const arg = argv[i];
|
|
47
|
+
if (arg === "--json") json = true;
|
|
48
|
+
else if (arg === "--yes" || arg === "-y") yes = true;
|
|
49
|
+
else if (arg === "--dry-run") dryRun = true;
|
|
50
|
+
else if (arg === "--older-than" || arg.startsWith("--older-than=")) {
|
|
51
|
+
const v = arg.startsWith("--older-than=") ? arg.slice("--older-than=".length) : argv[++i];
|
|
52
|
+
if (v === void 0) {
|
|
53
|
+
return {
|
|
54
|
+
error: { message: "--older-than requires a number of days", code: TRASH_EXIT.USAGE }
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const days = Number(v);
|
|
58
|
+
if (!Number.isFinite(days) || days < 0) {
|
|
59
|
+
return {
|
|
60
|
+
error: {
|
|
61
|
+
message: `--older-than must be a number of days (got "${v}")`,
|
|
62
|
+
code: TRASH_EXIT.USAGE
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
olderThanDays = days;
|
|
67
|
+
} else if (arg.startsWith("-")) {
|
|
68
|
+
return { error: { message: `unknown flag: ${arg}`, code: TRASH_EXIT.USAGE } };
|
|
69
|
+
} else positionals.push(arg);
|
|
70
|
+
}
|
|
71
|
+
const sub = positionals[0] ?? "list";
|
|
72
|
+
if (!TRASH_SUBCOMMANDS.includes(sub)) {
|
|
73
|
+
return {
|
|
74
|
+
error: {
|
|
75
|
+
message: `unknown subcommand "${sub}" (expected: ${TRASH_SUBCOMMANDS.join(" | ")})`,
|
|
76
|
+
code: TRASH_EXIT.USAGE
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const target = positionals[1];
|
|
81
|
+
if (positionals.length > 2) {
|
|
82
|
+
return { error: { message: `unexpected argument: ${positionals[2]}`, code: TRASH_EXIT.USAGE } };
|
|
83
|
+
}
|
|
84
|
+
if (sub === "restore" && !target) {
|
|
85
|
+
return {
|
|
86
|
+
error: {
|
|
87
|
+
message: "restore needs a file (e.g. hs trash restore <name>)",
|
|
88
|
+
code: TRASH_EXIT.USAGE
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (sub !== "restore" && target) {
|
|
93
|
+
return { error: { message: `${sub} takes no file argument`, code: TRASH_EXIT.USAGE } };
|
|
94
|
+
}
|
|
95
|
+
if (olderThanDays !== void 0 && sub !== "empty") {
|
|
96
|
+
return { error: { message: "--older-than only applies to empty", code: TRASH_EXIT.USAGE } };
|
|
97
|
+
}
|
|
98
|
+
return { args: { subcommand: sub, target, olderThanDays, yes, dryRun, json } };
|
|
99
|
+
}
|
|
100
|
+
function selectForEmpty(entries, olderThanDays) {
|
|
101
|
+
if (olderThanDays === void 0) return [...entries];
|
|
102
|
+
return entries.filter((e) => e.ageDays !== null && e.ageDays >= olderThanDays);
|
|
103
|
+
}
|
|
104
|
+
function matchEntries(entries, target) {
|
|
105
|
+
const needle = target.trim().toLowerCase();
|
|
106
|
+
if (!needle) return [];
|
|
107
|
+
const exact = entries.filter(
|
|
108
|
+
(e) => e.name.toLowerCase() === needle || e.relPath.toLowerCase() === needle
|
|
109
|
+
);
|
|
110
|
+
if (exact.length) return exact;
|
|
111
|
+
return entries.filter(
|
|
112
|
+
(e) => e.name.toLowerCase().includes(needle) || e.restoreRelPath.toLowerCase().includes(needle)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
function humanSize(bytes) {
|
|
116
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
117
|
+
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}K`;
|
|
118
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
|
|
119
|
+
}
|
|
120
|
+
function humanAge(entry) {
|
|
121
|
+
if (entry.ageDays === null) return "age unknown";
|
|
122
|
+
if (entry.ageDays === 0) return "today";
|
|
123
|
+
if (entry.ageDays === 1) return "1 day ago";
|
|
124
|
+
return `${entry.ageDays} days ago`;
|
|
125
|
+
}
|
|
126
|
+
function formatTrashList(entries) {
|
|
127
|
+
if (!entries.length) return "Trash is empty.";
|
|
128
|
+
const total = entries.reduce((sum, e) => sum + e.sizeBytes, 0);
|
|
129
|
+
const wName = Math.max(...entries.map((e) => e.restoreRelPath.length));
|
|
130
|
+
const lines = entries.map(
|
|
131
|
+
(e) => ` ${e.restoreRelPath.padEnd(wName)} ${humanAge(e).padEnd(14)} ${humanSize(e.sizeBytes)}`
|
|
132
|
+
);
|
|
133
|
+
const oldest = entries.reduce(
|
|
134
|
+
(max, e) => e.ageDays !== null && e.ageDays > max ? e.ageDays : max,
|
|
135
|
+
0
|
|
136
|
+
);
|
|
137
|
+
return [
|
|
138
|
+
`${entries.length} item(s) in the trash \xB7 ${humanSize(total)} \xB7 oldest ${oldest} days`,
|
|
139
|
+
...lines,
|
|
140
|
+
"",
|
|
141
|
+
// Preview leads. The first version of this footer offered two ways to
|
|
142
|
+
// destroy and never named --dry-run, so the one safe option was missing
|
|
143
|
+
// from the exact place someone looks for it.
|
|
144
|
+
" see what a clear takes: hs trash empty --dry-run",
|
|
145
|
+
" put one back: hs trash restore <name>",
|
|
146
|
+
` clear the old: hs trash empty --older-than ${DEFAULT_RETENTION_DAYS} --yes`,
|
|
147
|
+
" clear it all: hs trash empty --yes"
|
|
148
|
+
].join("\n");
|
|
149
|
+
}
|
|
150
|
+
var TRASH_HELP = [
|
|
151
|
+
"hs trash \u2014 the bin every spec delete soft-deletes into",
|
|
152
|
+
"",
|
|
153
|
+
"Usage:",
|
|
154
|
+
" hs trash [list] [--json] Show what is in the bin, and how old",
|
|
155
|
+
" hs trash restore <name> Put one file back where it came from",
|
|
156
|
+
" hs trash empty [--older-than <days>] [--yes] [--dry-run]",
|
|
157
|
+
"",
|
|
158
|
+
"Deleting a spec never unlinks it \u2014 it moves to `<folder>/.trash/` with a",
|
|
159
|
+
"timestamp on the name, per the destructive-action checklist. That part has",
|
|
160
|
+
"always worked. What was missing was any way to SEE the result, so every spec",
|
|
161
|
+
"ever deleted is still on disk. This is the bin those files were going into.",
|
|
162
|
+
"",
|
|
163
|
+
"`empty` is confirm-first: with no --yes it prints what would go and removes",
|
|
164
|
+
"nothing. `--older-than <days>` limits it to entries past that age, which is",
|
|
165
|
+
"the form worth running on a schedule. An entry whose timestamp cannot be read",
|
|
166
|
+
"has an unknown age and is never swept by an age-filtered empty \u2014 it only goes",
|
|
167
|
+
"on a full `hs trash empty --yes`.",
|
|
168
|
+
"",
|
|
169
|
+
"`restore` puts a file back at its original path with the timestamp stripped,",
|
|
170
|
+
"and refuses if something already lives there rather than overwriting it.",
|
|
171
|
+
"",
|
|
172
|
+
"Nothing auto-empties today. Recommended: run",
|
|
173
|
+
` hs trash empty --older-than ${DEFAULT_RETENTION_DAYS} --yes`,
|
|
174
|
+
"on a schedule \u2014 the same retention shape `scripts/backup-coordinator-state.mjs`",
|
|
175
|
+
"already uses for coordinator-state backups.",
|
|
176
|
+
"",
|
|
177
|
+
"This is the VAULT bin only. Handoff lanes have no trash \u2014 a lane is",
|
|
178
|
+
"coordination that self-purges on a TTL, and `hs handoff delete` hands the row",
|
|
179
|
+
"back as its own undo. Coordinator-state backups purge on their own schedule.",
|
|
180
|
+
"",
|
|
181
|
+
"Flags:",
|
|
182
|
+
" --older-than <days> Only entries at least this old (empty only).",
|
|
183
|
+
" --yes, -y Confirm an empty. Without it, empty prints and refuses.",
|
|
184
|
+
" --dry-run Print what would go, touch nothing.",
|
|
185
|
+
" --json Machine-readable output.",
|
|
186
|
+
" --help, -h Show this help."
|
|
187
|
+
].join("\n");
|
|
188
|
+
|
|
189
|
+
// spex-console/bin/run-entry.ts
|
|
190
|
+
import process from "node:process";
|
|
191
|
+
var BinExit = class extends Error {
|
|
192
|
+
code;
|
|
193
|
+
constructor(code) {
|
|
194
|
+
super(`exit ${code}`);
|
|
195
|
+
this.name = "BinExit";
|
|
196
|
+
this.code = code;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
function runEntry(name, main2, options = {}) {
|
|
200
|
+
const { fallbackCode = 1, unhandledLabel = "unhandled" } = options;
|
|
201
|
+
void (async () => {
|
|
202
|
+
try {
|
|
203
|
+
await main2();
|
|
204
|
+
} catch (err) {
|
|
205
|
+
if (err instanceof BinExit) {
|
|
206
|
+
process.exitCode = err.code;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
210
|
+
process.stderr.write(`${name}: ${unhandledLabel}: ${message}
|
|
211
|
+
`);
|
|
212
|
+
process.exitCode = fallbackCode;
|
|
213
|
+
}
|
|
214
|
+
})();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// spex-console/bin/hs-trash.ts
|
|
218
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".next"]);
|
|
219
|
+
function scanTrash(root, now) {
|
|
220
|
+
const found = [];
|
|
221
|
+
function walk(dir, inTrash) {
|
|
222
|
+
let dirents;
|
|
223
|
+
try {
|
|
224
|
+
dirents = readdirSync(dir, { withFileTypes: true });
|
|
225
|
+
} catch {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
for (const dirent of dirents) {
|
|
229
|
+
const full = path.join(dir, dirent.name);
|
|
230
|
+
if (dirent.isDirectory()) {
|
|
231
|
+
if (SKIP_DIRS.has(dirent.name)) continue;
|
|
232
|
+
walk(full, inTrash || dirent.name === ".trash");
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (!inTrash || !dirent.isFile()) continue;
|
|
236
|
+
const relPath = path.relative(root, full).split("\\").join("/");
|
|
237
|
+
const trashedAt = trashedAtFrom(dirent.name);
|
|
238
|
+
let sizeBytes = 0;
|
|
239
|
+
try {
|
|
240
|
+
sizeBytes = statSync(full).size;
|
|
241
|
+
} catch {
|
|
242
|
+
}
|
|
243
|
+
found.push({
|
|
244
|
+
relPath,
|
|
245
|
+
name: dirent.name,
|
|
246
|
+
restoreRelPath: restorePathFor(relPath),
|
|
247
|
+
trashedAt,
|
|
248
|
+
ageDays: ageDaysFrom(trashedAt, now),
|
|
249
|
+
sizeBytes
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
walk(root, false);
|
|
254
|
+
return found.sort((a, b) => (b.ageDays ?? -1) - (a.ageDays ?? -1));
|
|
255
|
+
}
|
|
256
|
+
function main() {
|
|
257
|
+
const raw = process2.argv.slice(2);
|
|
258
|
+
const argv = raw[0] === "trash" ? raw.slice(1) : raw;
|
|
259
|
+
const parsed = parseTrashArgs(argv);
|
|
260
|
+
if (parsed.help) {
|
|
261
|
+
process2.stdout.write(`${TRASH_HELP}
|
|
262
|
+
`);
|
|
263
|
+
process2.exitCode = TRASH_EXIT.OK;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (parsed.error) {
|
|
267
|
+
process2.stderr.write(`hs trash: ${parsed.error.message}
|
|
268
|
+
`);
|
|
269
|
+
process2.exitCode = parsed.error.code;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const args = parsed.args;
|
|
273
|
+
const vaultRoot = process2.env.HS_VAULT_ROOT;
|
|
274
|
+
if (!vaultRoot) {
|
|
275
|
+
process2.stderr.write(
|
|
276
|
+
"hs trash: HS_VAULT_ROOT is not set (the dispatcher must resolve and pass it).\n"
|
|
277
|
+
);
|
|
278
|
+
process2.exitCode = TRASH_EXIT.USAGE;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const now = /* @__PURE__ */ new Date();
|
|
282
|
+
const entries = scanTrash(vaultRoot, now);
|
|
283
|
+
if (args.subcommand === "list") {
|
|
284
|
+
process2.stdout.write(`${args.json ? JSON.stringify({ entries }) : formatTrashList(entries)}
|
|
285
|
+
`);
|
|
286
|
+
process2.exitCode = TRASH_EXIT.OK;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (args.subcommand === "restore") {
|
|
290
|
+
const matches = matchEntries(entries, args.target);
|
|
291
|
+
if (matches.length === 0) {
|
|
292
|
+
process2.stderr.write(`hs trash: nothing in the trash matches "${args.target}".
|
|
293
|
+
`);
|
|
294
|
+
process2.exitCode = TRASH_EXIT.NOT_FOUND;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (matches.length > 1) {
|
|
298
|
+
process2.stderr.write(
|
|
299
|
+
`hs trash: "${args.target}" matches ${matches.length} entries:
|
|
300
|
+
` + matches.map((m) => ` ${m.name}`).join("\n") + "\n Name one exactly.\n"
|
|
301
|
+
);
|
|
302
|
+
process2.exitCode = TRASH_EXIT.USAGE;
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const entry = matches[0];
|
|
306
|
+
const from = path.join(vaultRoot, entry.relPath);
|
|
307
|
+
const to = path.join(vaultRoot, entry.restoreRelPath);
|
|
308
|
+
if (existsSync(to)) {
|
|
309
|
+
process2.stderr.write(
|
|
310
|
+
`hs trash: ${entry.restoreRelPath} already exists \u2014 restoring would overwrite it.
|
|
311
|
+
`
|
|
312
|
+
);
|
|
313
|
+
process2.exitCode = TRASH_EXIT.USAGE;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
renameSync(from, to);
|
|
317
|
+
process2.stdout.write(
|
|
318
|
+
`${args.json ? JSON.stringify({ ok: true, restored: entry.restoreRelPath, from: entry.relPath }) : `Restored ${entry.restoreRelPath}
|
|
319
|
+
from ${entry.relPath}`}
|
|
320
|
+
`
|
|
321
|
+
);
|
|
322
|
+
process2.exitCode = TRASH_EXIT.OK;
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const doomed = selectForEmpty(entries, args.olderThanDays);
|
|
326
|
+
const scope = args.olderThanDays === void 0 ? "everything in the trash" : `entries at least ${args.olderThanDays} days old`;
|
|
327
|
+
if (doomed.length === 0) {
|
|
328
|
+
process2.stdout.write(`Nothing to empty \u2014 no ${scope}.
|
|
329
|
+
`);
|
|
330
|
+
process2.exitCode = TRASH_EXIT.OK;
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const totalBytes = doomed.reduce((sum, e) => sum + e.sizeBytes, 0);
|
|
334
|
+
const listing = doomed.map((e) => ` ${e.restoreRelPath} (${e.ageDays ?? "?"}d)`).join("\n");
|
|
335
|
+
if (!args.yes || args.dryRun) {
|
|
336
|
+
process2.stdout.write(
|
|
337
|
+
`${args.dryRun ? "[dry-run] would permanently delete" : "Would permanently delete"} ${doomed.length} item(s) \u2014 ${scope}:
|
|
338
|
+
${listing}
|
|
339
|
+
This is permanent. They were soft-deleted once already, so there is no
|
|
340
|
+
second bin behind this one.
|
|
341
|
+
` + (args.dryRun ? "" : ` do it with: hs trash empty${args.olderThanDays !== void 0 ? ` --older-than ${args.olderThanDays}` : ""} --yes
|
|
342
|
+
`)
|
|
343
|
+
);
|
|
344
|
+
process2.exitCode = args.dryRun ? TRASH_EXIT.OK : TRASH_EXIT.USAGE;
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
let removed = 0;
|
|
348
|
+
for (const entry of doomed) {
|
|
349
|
+
try {
|
|
350
|
+
rmSync(path.join(vaultRoot, entry.relPath));
|
|
351
|
+
removed++;
|
|
352
|
+
} catch {
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
process2.stdout.write(
|
|
356
|
+
`${args.json ? JSON.stringify({ ok: true, removed, requested: doomed.length, bytes: totalBytes }) : `Emptied ${removed} of ${doomed.length} item(s) \u2014 ${scope}.
|
|
357
|
+
Run \`hs trash empty --older-than ${DEFAULT_RETENTION_DAYS} --yes\` on a schedule to keep it from piling up.`}
|
|
358
|
+
`
|
|
359
|
+
);
|
|
360
|
+
process2.exitCode = TRASH_EXIT.OK;
|
|
361
|
+
}
|
|
362
|
+
runEntry("hs trash", main, { fallbackCode: TRASH_EXIT.RUNTIME });
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { createRequire as __hsCreateRequire } from 'node:module';
|
|
2
|
+
const require = __hsCreateRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// spex-console/bin/hs-trust.ts
|
|
5
|
+
import process3 from "node:process";
|
|
6
|
+
|
|
7
|
+
// spex-console/src/lib/cli/trust-cli.ts
|
|
8
|
+
import path3 from "node:path";
|
|
9
|
+
|
|
10
|
+
// spex-console/src/lib/cli/folder-trust.ts
|
|
11
|
+
import { realpathSync } from "node:fs";
|
|
12
|
+
import path2 from "node:path";
|
|
13
|
+
|
|
14
|
+
// scripts/lib/heyspex-home.mjs
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
function heyspexHome(env = process.env) {
|
|
17
|
+
const override = env?.HEYSPEX_HOME;
|
|
18
|
+
if (typeof override === "string" && override.trim().length > 0) {
|
|
19
|
+
return override.trim();
|
|
20
|
+
}
|
|
21
|
+
return homedir();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// vendor/heyspex-mcp/folder-trust-store.mjs
|
|
25
|
+
import fs from "node:fs";
|
|
26
|
+
import os from "node:os";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
var TRUST_STORE_REL = path.join(".heyspex", "trusted-folders.json");
|
|
29
|
+
function trustStorePath(home = heyspexHome()) {
|
|
30
|
+
return path.join(home, TRUST_STORE_REL);
|
|
31
|
+
}
|
|
32
|
+
function readTrustStore(filePath = trustStorePath()) {
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
35
|
+
const parsed = JSON.parse(raw);
|
|
36
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
37
|
+
} catch {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function writeTrustStore(store, filePath = trustStorePath()) {
|
|
42
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
43
|
+
fs.writeFileSync(filePath, `${JSON.stringify(store, null, 2)}
|
|
44
|
+
`, "utf8");
|
|
45
|
+
}
|
|
46
|
+
function listTrustedFolders(store) {
|
|
47
|
+
return Object.entries(store).map(([key, entry]) => ({
|
|
48
|
+
path: key,
|
|
49
|
+
trusted: entry.trusted,
|
|
50
|
+
decidedAt: entry.decidedAt
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// spex-console/src/lib/cli/folder-trust.ts
|
|
55
|
+
function trustStorePath2(home) {
|
|
56
|
+
return trustStorePath(home);
|
|
57
|
+
}
|
|
58
|
+
function trustKeyForCwd(cwd, realpathFn = realpathSync) {
|
|
59
|
+
try {
|
|
60
|
+
return realpathFn(cwd);
|
|
61
|
+
} catch {
|
|
62
|
+
return path2.resolve(cwd);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function revokeFolderTrust({
|
|
66
|
+
cwd,
|
|
67
|
+
store,
|
|
68
|
+
realpathFn
|
|
69
|
+
}) {
|
|
70
|
+
if (cwd === void 0 || cwd === null) return {};
|
|
71
|
+
const key = trustKeyForCwd(cwd, realpathFn);
|
|
72
|
+
const next = { ...store };
|
|
73
|
+
delete next[key];
|
|
74
|
+
return next;
|
|
75
|
+
}
|
|
76
|
+
function readTrustStore2(filePath = trustStorePath2()) {
|
|
77
|
+
return readTrustStore(filePath);
|
|
78
|
+
}
|
|
79
|
+
function writeTrustStore2(store, filePath = trustStorePath2()) {
|
|
80
|
+
writeTrustStore(store, filePath);
|
|
81
|
+
}
|
|
82
|
+
function listTrustedFolders2(store) {
|
|
83
|
+
return listTrustedFolders(store);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// spex-console/src/lib/cli/trust-cli.ts
|
|
87
|
+
var EXIT = {
|
|
88
|
+
/** Success. */
|
|
89
|
+
OK: 0,
|
|
90
|
+
/** Usage error — an unknown subcommand/argument. */
|
|
91
|
+
USAGE: 1
|
|
92
|
+
};
|
|
93
|
+
var TRUST_HELP = [
|
|
94
|
+
"hs trust \u2014 list / revoke spexcode's stored folder-trust decisions",
|
|
95
|
+
"",
|
|
96
|
+
" hs trust list List every stored decision: path, trusted yes/no, decidedAt",
|
|
97
|
+
" hs trust revoke [path] Clear one folder's decision (default: cwd) \u2014 the next",
|
|
98
|
+
" `spexcode` launch there asks again",
|
|
99
|
+
" hs trust --help Show this help",
|
|
100
|
+
"",
|
|
101
|
+
"Store: ~/.heyspex/trusted-folders.json \u2014 machine-local, never part of the vault."
|
|
102
|
+
].join("\n");
|
|
103
|
+
function runTrustCommand(argv, options = {}) {
|
|
104
|
+
if (argv.includes("--help") || argv.includes("-h") || argv.length === 0) {
|
|
105
|
+
return { code: EXIT.OK, stdout: `${TRUST_HELP}
|
|
106
|
+
`, stderr: "" };
|
|
107
|
+
}
|
|
108
|
+
const cwd = options.cwd ?? process.cwd();
|
|
109
|
+
const storePath = options.storePath ?? trustStorePath2();
|
|
110
|
+
const [sub, ...rest] = argv;
|
|
111
|
+
if (sub === "list") {
|
|
112
|
+
if (rest.length > 0) {
|
|
113
|
+
return {
|
|
114
|
+
code: EXIT.USAGE,
|
|
115
|
+
stdout: "",
|
|
116
|
+
stderr: `hs trust list: unknown argument: ${rest[0]}
|
|
117
|
+
`
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const rows = listTrustedFolders2(readTrustStore2(storePath));
|
|
121
|
+
const header = `Trusted-folder store: ${storePath}
|
|
122
|
+
`;
|
|
123
|
+
if (rows.length === 0) {
|
|
124
|
+
return { code: EXIT.OK, stdout: `${header}(no stored trust decisions)
|
|
125
|
+
`, stderr: "" };
|
|
126
|
+
}
|
|
127
|
+
const body = rows.slice().sort((a, b) => a.path.localeCompare(b.path)).map((r) => ` ${r.trusted ? "trusted " : "not trusted"} ${r.decidedAt} ${r.path}`).join("\n");
|
|
128
|
+
return { code: EXIT.OK, stdout: `${header}${body}
|
|
129
|
+
`, stderr: "" };
|
|
130
|
+
}
|
|
131
|
+
if (sub === "revoke") {
|
|
132
|
+
if (rest.length > 1) {
|
|
133
|
+
return {
|
|
134
|
+
code: EXIT.USAGE,
|
|
135
|
+
stdout: "",
|
|
136
|
+
stderr: `hs trust revoke: unknown argument: ${rest[1]}
|
|
137
|
+
`
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const target = rest[0] ? path3.resolve(cwd, rest[0]) : cwd;
|
|
141
|
+
const next = revokeFolderTrust({ cwd: target, store: readTrustStore2(storePath) });
|
|
142
|
+
writeTrustStore2(next, storePath);
|
|
143
|
+
return {
|
|
144
|
+
code: EXIT.OK,
|
|
145
|
+
stdout: `hs trust: trust decision cleared for "${target}" (${storePath}). The next launch there will ask again.
|
|
146
|
+
`,
|
|
147
|
+
stderr: ""
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
code: EXIT.USAGE,
|
|
152
|
+
stdout: "",
|
|
153
|
+
stderr: `hs trust: unknown subcommand "${sub}" (try "hs trust --help")
|
|
154
|
+
`
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// spex-console/bin/run-entry.ts
|
|
159
|
+
import process2 from "node:process";
|
|
160
|
+
var BinExit = class extends Error {
|
|
161
|
+
code;
|
|
162
|
+
constructor(code) {
|
|
163
|
+
super(`exit ${code}`);
|
|
164
|
+
this.name = "BinExit";
|
|
165
|
+
this.code = code;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
function runEntry(name, main2, options = {}) {
|
|
169
|
+
const { fallbackCode = 1, unhandledLabel = "unhandled" } = options;
|
|
170
|
+
void (async () => {
|
|
171
|
+
try {
|
|
172
|
+
await main2();
|
|
173
|
+
} catch (err) {
|
|
174
|
+
if (err instanceof BinExit) {
|
|
175
|
+
process2.exitCode = err.code;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
179
|
+
process2.stderr.write(`${name}: ${unhandledLabel}: ${message}
|
|
180
|
+
`);
|
|
181
|
+
process2.exitCode = fallbackCode;
|
|
182
|
+
}
|
|
183
|
+
})();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// spex-console/bin/hs-trust.ts
|
|
187
|
+
function main() {
|
|
188
|
+
const invokedCwd = process3.env.HS_INVOKED_CWD?.trim() || process3.cwd();
|
|
189
|
+
const result = runTrustCommand(process3.argv.slice(3), {
|
|
190
|
+
cwd: invokedCwd,
|
|
191
|
+
storePath: process3.env.HS_TRUST_STORE_PATH || void 0
|
|
192
|
+
});
|
|
193
|
+
if (result.stdout) process3.stdout.write(result.stdout);
|
|
194
|
+
if (result.stderr) process3.stderr.write(result.stderr);
|
|
195
|
+
process3.exitCode = result.code;
|
|
196
|
+
}
|
|
197
|
+
runEntry("hs trust", main, { fallbackCode: EXIT.USAGE });
|