javi-forge 1.38.5 → 1.39.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/assets/claude-hooks/javi-forge-windows-secure-object.ps1 +29 -6
- package/assets/claude-hooks/manifest.json +1 -1
- package/assets/preparation-worker.c +180 -0
- package/dist/cli/dispatch/doctor.d.ts +3 -0
- package/dist/cli/dispatch/doctor.js +27 -0
- package/dist/cli/dispatch/simple-renderers.d.ts +1 -1
- package/dist/cli/dispatch/simple-renderers.js +3 -3
- package/dist/cli/help.d.ts +5 -1
- package/dist/cli/help.js +2 -0
- package/dist/cli/main.js +26 -5
- package/dist/commands/doctor.d.ts +7 -2
- package/dist/commands/doctor.js +32 -13
- package/dist/commands/plugin.d.ts +3 -1
- package/dist/commands/plugin.js +20 -8
- package/dist/lib/__fixtures__/fake-secure-fs.js +6 -3
- package/dist/lib/agent-skills.d.ts +2 -1
- package/dist/lib/agent-skills.js +36 -22
- package/dist/lib/plugin-replacement.d.ts +28 -0
- package/dist/lib/plugin-replacement.js +170 -0
- package/dist/lib/plugin.d.ts +2 -1
- package/dist/lib/plugin.js +26 -21
- package/dist/lib/preparation-authorization.d.ts +31 -0
- package/dist/lib/preparation-authorization.js +134 -0
- package/dist/lib/preparation-capability.d.ts +64 -0
- package/dist/lib/preparation-capability.js +106 -0
- package/dist/lib/preparation-executor.d.ts +49 -0
- package/dist/lib/preparation-executor.js +427 -0
- package/dist/lib/preparation-stager.d.ts +25 -0
- package/dist/lib/preparation-stager.js +207 -0
- package/dist/lib/preparation-status-ok.d.ts +36 -0
- package/dist/lib/preparation-status-ok.js +161 -0
- package/dist/lib/secure-fs-posix.js +7 -2
- package/dist/lib/secure-fs-transaction.d.ts +12 -2
- package/dist/lib/secure-fs-transaction.js +62 -14
- package/dist/lib/secure-fs-windows.js +9 -1
- package/dist/ui/Doctor.d.ts +7 -1
- package/dist/ui/Doctor.js +3 -24
- package/dist/ui/Plugin.js +31 -1
- package/package.json +1 -1
|
@@ -92,6 +92,7 @@ namespace JaviForge
|
|
|
92
92
|
private const uint WRITE_OWNER_A = 0x00080000;
|
|
93
93
|
private const uint DELETE_A = 0x00010000;
|
|
94
94
|
private const uint FILE_READ_ATTRIBUTES = 0x0080;
|
|
95
|
+
private const uint DIRECTORY_FLUSH_ACCESS = GENERIC_WRITE | READ_CONTROL | FILE_READ_ATTRIBUTES;
|
|
95
96
|
|
|
96
97
|
private const uint FILE_SHARE_READ = 0x1;
|
|
97
98
|
private const uint FILE_SHARE_WRITE = 0x2;
|
|
@@ -866,12 +867,34 @@ namespace JaviForge
|
|
|
866
867
|
// No opaque re-check here (unlike unlink/rmdir): both endpoints are our
|
|
867
868
|
// own just-created nodes under the held parent-chain lock, so no on-path
|
|
868
869
|
// node can be swapped while the chain handle is held.
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
870
|
+
int flushErr;
|
|
871
|
+
IntPtr flushHandle = OpenNoFollow(dir.Path, DIRECTORY_FLUSH_ACCESS,
|
|
872
|
+
FILE_SHARE_READ | FILE_SHARE_WRITE, out flushErr);
|
|
873
|
+
if (flushHandle == INVALID_HANDLE)
|
|
874
|
+
return OpResult.Fail(R_CHAIN, "rename flush-open failed " + flushErr + " " + dir.Path);
|
|
875
|
+
try
|
|
876
|
+
{
|
|
877
|
+
uint attr; string opaque; bool zeroId;
|
|
878
|
+
if (!ReadInfo(flushHandle, out attr, out opaque, out zeroId))
|
|
879
|
+
return OpResult.Fail(R_CHAIN, "rename flush-open info failed " + dir.Path);
|
|
880
|
+
if ((attr & FILE_ATTRIBUTE_REPARSE_POINT) != 0)
|
|
881
|
+
return OpResult.Fail(R_CHAIN, "rename flush-open reparse point " + dir.Path);
|
|
882
|
+
if ((attr & FILE_ATTRIBUTE_DIRECTORY) == 0)
|
|
883
|
+
return OpResult.Fail(R_CHAIN, "rename flush-open not a directory " + dir.Path);
|
|
884
|
+
if (zeroId)
|
|
885
|
+
return OpResult.Fail(R_CHAIN, "rename flush-open unresolvable identity " + dir.Path);
|
|
886
|
+
if (!String.Equals(opaque, dir.Opaque, StringComparison.OrdinalIgnoreCase))
|
|
887
|
+
return OpResult.Fail(R_CHAIN, "rename flush-open identity changed " + dir.Path);
|
|
888
|
+
if (!MoveFileExW(fromFull, toFull,
|
|
889
|
+
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
|
|
890
|
+
return OpResult.Fail(R_CHAIN, "rename failed " + Marshal.GetLastWin32Error()
|
|
891
|
+
+ " " + fromFull);
|
|
892
|
+
if (!FlushFileBuffers(flushHandle))
|
|
893
|
+
return OpResult.Fail(R_CHAIN, "rename flush failed " + Marshal.GetLastWin32Error()
|
|
894
|
+
+ " " + dir.Path);
|
|
895
|
+
return OpResult.Good();
|
|
896
|
+
}
|
|
897
|
+
finally { CloseHandle(flushHandle); }
|
|
875
898
|
}
|
|
876
899
|
catch (Exception ex) { return OpResult.Fail(R_CHAIN, "rename exception " + ex.Message); }
|
|
877
900
|
}
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"installerHelpers": {
|
|
31
31
|
"windowsSecureObject": {
|
|
32
32
|
"name": "javi-forge-windows-secure-object.ps1",
|
|
33
|
-
"sha256": "
|
|
33
|
+
"sha256": "4ee446d4e540adbea88c343df540efdcad1a5b26d31481ac5ae7057d8375cd11"
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/* Fixed Linux x86-64 data-only preparation worker. Build statically; never run
|
|
2
|
+
* generated helpers. Operator approves the exact binary AND this source digest.
|
|
3
|
+
* JSON checks are artifact validation, not helper behavior or domain-schema tests. */
|
|
4
|
+
#define _GNU_SOURCE
|
|
5
|
+
#include <errno.h>
|
|
6
|
+
#include <fcntl.h>
|
|
7
|
+
#include <linux/audit.h>
|
|
8
|
+
#include <linux/filter.h>
|
|
9
|
+
#include <linux/seccomp.h>
|
|
10
|
+
#include <stddef.h>
|
|
11
|
+
#include <stdint.h>
|
|
12
|
+
#include <stdio.h>
|
|
13
|
+
#include <stdlib.h>
|
|
14
|
+
#include <string.h>
|
|
15
|
+
#include <sys/prctl.h>
|
|
16
|
+
#include <sys/socket.h>
|
|
17
|
+
#include <sys/stat.h>
|
|
18
|
+
#include <sys/syscall.h>
|
|
19
|
+
#include <time.h>
|
|
20
|
+
#include <unistd.h>
|
|
21
|
+
|
|
22
|
+
#define LIMIT 1048576
|
|
23
|
+
#define DIR_FLAGS (O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC)
|
|
24
|
+
#define FILE_FLAGS (O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC)
|
|
25
|
+
static const char *names[6] = {"app-request.json", "minimal.py", "test_minimal.py", "dispatch.py", "run_gateway.py", "preparation-result.json"};
|
|
26
|
+
static unsigned char *data[6];
|
|
27
|
+
static size_t sizes[6];
|
|
28
|
+
extern char **environ;
|
|
29
|
+
|
|
30
|
+
#define ALLOW(n) BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_##n, 0, 1), BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW)
|
|
31
|
+
static int restrict_syscalls(void) {
|
|
32
|
+
struct sock_filter rules[] = {
|
|
33
|
+
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch)),
|
|
34
|
+
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
|
|
35
|
+
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
|
|
36
|
+
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
|
|
37
|
+
ALLOW(read), ALLOW(write), ALLOW(close), ALLOW(fstat), ALLOW(newfstatat),
|
|
38
|
+
ALLOW(fsync), ALLOW(fchmod), ALLOW(mkdirat), ALLOW(unlinkat),
|
|
39
|
+
ALLOW(brk), ALLOW(mmap), ALLOW(munmap), ALLOW(mprotect), ALLOW(futex),
|
|
40
|
+
ALLOW(clock_gettime), ALLOW(alarm), ALLOW(rt_sigaction), ALLOW(rt_sigprocmask),
|
|
41
|
+
ALLOW(rt_sigreturn), ALLOW(exit), ALLOW(exit_group), ALLOW(getpid),
|
|
42
|
+
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_openat, 1, 0),
|
|
43
|
+
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
|
|
44
|
+
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[2])),
|
|
45
|
+
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, DIR_FLAGS, 2, 0),
|
|
46
|
+
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, FILE_FLAGS, 1, 0),
|
|
47
|
+
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
|
|
48
|
+
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW)
|
|
49
|
+
};
|
|
50
|
+
struct sock_fprog program = {(unsigned short)(sizeof(rules) / sizeof(rules[0])), rules};
|
|
51
|
+
return prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) || prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &program);
|
|
52
|
+
}
|
|
53
|
+
static int isolation_probes(int parent) {
|
|
54
|
+
errno = 0;
|
|
55
|
+
if (syscall(__NR_socket, AF_INET, SOCK_STREAM, 0) != -1 || errno != EPERM) return 0;
|
|
56
|
+
errno = 0;
|
|
57
|
+
if (openat(parent, "credential-probe", O_RDONLY) != -1 || errno != EPERM) return 0;
|
|
58
|
+
errno = 0;
|
|
59
|
+
if (syscall(__NR_clone, 0, 0, 0, 0, 0) != -1 || errno != EPERM) return 0;
|
|
60
|
+
errno = 0;
|
|
61
|
+
if (syscall(__NR_execve, "/worker", 0, 0) != -1 || errno != EPERM) return 0;
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
struct json { const unsigned char *p, *end; };
|
|
66
|
+
static void ws(struct json *j) { while (j->p < j->end && (*j->p == ' ' || *j->p == '\n' || *j->p == '\r' || *j->p == '\t')) j->p++; }
|
|
67
|
+
static int hex(unsigned char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); }
|
|
68
|
+
static int string(struct json *j) {
|
|
69
|
+
if (j->p == j->end || *j->p++ != '"') return 0;
|
|
70
|
+
while (j->p < j->end) {
|
|
71
|
+
unsigned char c = *j->p++;
|
|
72
|
+
if (c == '"') return 1;
|
|
73
|
+
if (c < 32) return 0;
|
|
74
|
+
if (c == '\\') {
|
|
75
|
+
if (j->p == j->end) return 0;
|
|
76
|
+
c = *j->p++;
|
|
77
|
+
if (c == 'u') {
|
|
78
|
+
for (int i = 0; i < 4; i++) if (j->p == j->end || !hex(*j->p++)) return 0;
|
|
79
|
+
} else if (!strchr("\"\\/bfnrt", c)) return 0;
|
|
80
|
+
} else if (c >= 128) {
|
|
81
|
+
unsigned int value; int count;
|
|
82
|
+
if (c >= 0xc2 && c <= 0xdf) { value = c & 31; count = 1; }
|
|
83
|
+
else if (c >= 0xe0 && c <= 0xef) { value = c & 15; count = 2; }
|
|
84
|
+
else if (c >= 0xf0 && c <= 0xf4) { value = c & 7; count = 3; }
|
|
85
|
+
else return 0;
|
|
86
|
+
int remaining = count;
|
|
87
|
+
while (remaining--) { if (j->p == j->end || (*j->p & 0xc0) != 0x80) return 0; value = (value << 6) | (*j->p++ & 63); }
|
|
88
|
+
if ((count == 2 && value < 0x800) || (count == 3 && value < 0x10000) || value > 0x10ffff || (value >= 0xd800 && value <= 0xdfff)) return 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
static int value(struct json *j, int depth) {
|
|
94
|
+
ws(j); if (depth > 64 || j->p == j->end) return 0;
|
|
95
|
+
unsigned char c = *j->p;
|
|
96
|
+
if (c == '"') return string(j);
|
|
97
|
+
if (c == '{' || c == '[') {
|
|
98
|
+
unsigned char stop = c == '{' ? '}' : ']'; j->p++; ws(j);
|
|
99
|
+
if (j->p < j->end && *j->p == stop) { j->p++; return 1; }
|
|
100
|
+
for (;;) {
|
|
101
|
+
if (c == '{') { if (!string(j)) return 0; ws(j); if (j->p == j->end || *j->p++ != ':') return 0; }
|
|
102
|
+
if (!value(j, depth + 1)) return 0;
|
|
103
|
+
ws(j); if (j->p == j->end) return 0;
|
|
104
|
+
unsigned char next = *j->p++; if (next == stop) return 1;
|
|
105
|
+
if (next != ',') return 0;
|
|
106
|
+
ws(j);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const char *literal = c == 't' ? "true" : c == 'f' ? "false" : c == 'n' ? "null" : NULL;
|
|
110
|
+
if (literal) { size_t n = strlen(literal); if ((size_t)(j->end - j->p) < n || memcmp(j->p, literal, n)) return 0; j->p += n; return 1; }
|
|
111
|
+
if (*j->p == '-') j->p++;
|
|
112
|
+
if (j->p == j->end) return 0;
|
|
113
|
+
if (*j->p == '0') j->p++;
|
|
114
|
+
else { if (*j->p < '1' || *j->p > '9') return 0; do { j->p++; } while (j->p < j->end && *j->p >= '0' && *j->p <= '9'); }
|
|
115
|
+
if (j->p < j->end && *j->p == '.') { j->p++; const unsigned char *start = j->p; while (j->p < j->end && *j->p >= '0' && *j->p <= '9') j->p++; if (j->p == start) return 0; }
|
|
116
|
+
if (j->p < j->end && (*j->p == 'e' || *j->p == 'E')) { j->p++; if (j->p < j->end && (*j->p == '+' || *j->p == '-')) j->p++; const unsigned char *start = j->p; while (j->p < j->end && *j->p >= '0' && *j->p <= '9') j->p++; if (j->p == start) return 0; }
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
static int object(const unsigned char *bytes, size_t n) { struct json j = {bytes, bytes + n}; ws(&j); if (j.p == j.end || *j.p != '{' || !value(&j, 0)) return 0; ws(&j); return j.p == j.end; }
|
|
120
|
+
static int write_all(int fd, const unsigned char *bytes, size_t length) {
|
|
121
|
+
while (length) { ssize_t n = write(fd, bytes, length); if (n <= 0) return 0; bytes += n; length -= (size_t)n; } return 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
int main(int argc, char **argv) {
|
|
125
|
+
(void)argv;
|
|
126
|
+
alarm(30); umask(077); setvbuf(stdout, NULL, _IONBF, 0);
|
|
127
|
+
if (argc != 1) return 11;
|
|
128
|
+
if (environ && *environ && (environ[1] || strcmp(environ[0], "PWD=/"))) return 12;
|
|
129
|
+
if (clearenv()) return 13;
|
|
130
|
+
struct timespec started; if (clock_gettime(CLOCK_MONOTONIC, &started)) return 10;
|
|
131
|
+
int parent = open("/out", DIR_FLAGS); struct stat parent_stat, existing;
|
|
132
|
+
if (parent < 0 || fstat(parent, &parent_stat) || (parent_stat.st_mode & 0777) != 0700) return 10;
|
|
133
|
+
if (fstatat(parent, "attempt-3", &existing, AT_SYMLINK_NOFOLLOW) == 0 || errno != ENOENT) return 10;
|
|
134
|
+
unsigned long long dev, ino; FILE *identity = fopen("/identity", "r");
|
|
135
|
+
if (!identity || fscanf(identity, "%llu %llu", &dev, &ino) != 2 || dev != (unsigned long long)parent_stat.st_dev || ino != (unsigned long long)parent_stat.st_ino) return 10;
|
|
136
|
+
fclose(identity);
|
|
137
|
+
size_t total = 0;
|
|
138
|
+
for (int i = 0; i < 6; i++) {
|
|
139
|
+
char filename[128]; snprintf(filename, sizeof(filename), "/payload/%s", names[i]);
|
|
140
|
+
int fd = open(filename, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); struct stat st;
|
|
141
|
+
if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size < 0 || st.st_size > LIMIT) return 10;
|
|
142
|
+
sizes[i] = (size_t)st.st_size; total += sizes[i]; if (total > LIMIT) return 10;
|
|
143
|
+
data[i] = malloc(sizes[i] + 1); if (!data[i]) return 10;
|
|
144
|
+
size_t offset = 0;
|
|
145
|
+
while (offset < sizes[i]) { ssize_t n = read(fd, data[i] + offset, sizes[i] - offset); if (n <= 0) return 10; offset += (size_t)n; }
|
|
146
|
+
close(fd); data[i][sizes[i]] = 0;
|
|
147
|
+
}
|
|
148
|
+
if (restrict_syscalls() || !isolation_probes(parent)) return 10;
|
|
149
|
+
#ifdef PREPARATION_FIXTURE_NOT_READY
|
|
150
|
+
for (;;) { /* Test-only readiness stall, never a production build. */ }
|
|
151
|
+
#endif
|
|
152
|
+
puts("READY");
|
|
153
|
+
char go; if (read(STDIN_FILENO, &go, 1) != 1 || go != 'G') return 10;
|
|
154
|
+
alarm(10);
|
|
155
|
+
#ifdef PREPARATION_FIXTURE_STALL
|
|
156
|
+
for (;;) { /* Test-only compiled variant, approved only by fixture keys. */ }
|
|
157
|
+
#endif
|
|
158
|
+
if (!object(data[0], sizes[0]) || !object(data[5], sizes[5])) return 20;
|
|
159
|
+
for (int i = 1; i < 5; i++) if (!sizes[i] || memchr(data[i], 0, sizes[i])) return 20;
|
|
160
|
+
puts("VALID");
|
|
161
|
+
struct timespec now; if (clock_gettime(CLOCK_MONOTONIC, &now)) return 20;
|
|
162
|
+
long elapsed = now.tv_sec - started.tv_sec; if (elapsed >= 30) return 20;
|
|
163
|
+
alarm((unsigned int)(30 - elapsed));
|
|
164
|
+
if (mkdirat(parent, "attempt-3", 0700)) return 30;
|
|
165
|
+
int dir = openat(parent, "attempt-3", DIR_FLAGS); struct stat directory_stat;
|
|
166
|
+
if (dir < 0 || fstat(dir, &directory_stat)) return 30;
|
|
167
|
+
printf("DIR %llu %llu\n", (unsigned long long)directory_stat.st_dev, (unsigned long long)directory_stat.st_ino);
|
|
168
|
+
for (int i = 0; i < 6; i++) {
|
|
169
|
+
int fd = openat(dir, names[i], FILE_FLAGS, 0600); struct stat st;
|
|
170
|
+
if (fd < 0 || fstat(fd, &st)) return 30;
|
|
171
|
+
printf("FILE %d %llu %llu\n", i, (unsigned long long)st.st_dev, (unsigned long long)st.st_ino);
|
|
172
|
+
if (fchmod(fd, 0600) || !write_all(fd, data[i], sizes[i]) || fsync(fd)) return 30;
|
|
173
|
+
close(fd);
|
|
174
|
+
#ifdef PREPARATION_FIXTURE_PARTIAL
|
|
175
|
+
if (i == 0) return 30;
|
|
176
|
+
#endif
|
|
177
|
+
}
|
|
178
|
+
if (fsync(dir) || fsync(parent)) return 30;
|
|
179
|
+
close(dir); close(parent); puts("DONE"); return 0;
|
|
180
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createElement, useEffect, useState } from "react";
|
|
2
|
+
import { runDoctor } from "../../commands/doctor.js";
|
|
3
|
+
import Doctor from "../../ui/Doctor.js";
|
|
4
|
+
export default function DoctorController({ dryRun = false, refreshContext = false, }) {
|
|
5
|
+
const [result, setResult] = useState(null);
|
|
6
|
+
const [error, setError] = useState(null);
|
|
7
|
+
const [loading, setLoading] = useState(true);
|
|
8
|
+
const runCheck = () => {
|
|
9
|
+
setLoading(true);
|
|
10
|
+
setResult(null);
|
|
11
|
+
setError(null);
|
|
12
|
+
void runDoctor(undefined, { dryRun, refreshContext })
|
|
13
|
+
.then((nextResult) => {
|
|
14
|
+
setResult(nextResult);
|
|
15
|
+
setLoading(false);
|
|
16
|
+
})
|
|
17
|
+
.catch((nextError) => {
|
|
18
|
+
setError(String(nextError));
|
|
19
|
+
setLoading(false);
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
runCheck();
|
|
24
|
+
}, [dryRun, refreshContext]);
|
|
25
|
+
return createElement(Doctor, { loading, result, error, onRerun: runCheck });
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -15,7 +15,7 @@ export interface RendererDeps {
|
|
|
15
15
|
error?: (message: string) => void;
|
|
16
16
|
setExitCode?: (code: number) => void;
|
|
17
17
|
}
|
|
18
|
-
export declare function handleDoctor(
|
|
18
|
+
export declare function handleDoctor(cli: CLI, ctx: RendererCtx, deps?: RendererDeps): void;
|
|
19
19
|
export declare function handleAnalyze(cli: CLI, ctx: RendererCtx): void;
|
|
20
20
|
export declare function handleLlmsTxt(cli: CLI, ctx: RendererCtx): void;
|
|
21
21
|
export declare function handlePlugin(cli: CLI, ctx: RendererCtx): void;
|
|
@@ -13,11 +13,11 @@ import { resolvePlatformSupport } from "../../lib/platform-support.js";
|
|
|
13
13
|
import AnalyzeUI from "../../ui/AnalyzeUI.js";
|
|
14
14
|
import App from "../../ui/App.js";
|
|
15
15
|
import { CIProvider as CIContextProvider } from "../../ui/CIContext.js";
|
|
16
|
-
import Doctor from "../../ui/Doctor.js";
|
|
17
16
|
import LlmsTxt from "../../ui/LlmsTxt.js";
|
|
18
17
|
import Plugin from "../../ui/Plugin.js";
|
|
19
18
|
import { VALID_CI, VALID_MEMORY, VALID_STACKS } from "../validators.js";
|
|
20
|
-
|
|
19
|
+
import DoctorController from "./doctor.js";
|
|
20
|
+
export function handleDoctor(cli, ctx, deps = {}) {
|
|
21
21
|
const platformSupport = resolvePlatformSupport(deps.platform ?? process.platform);
|
|
22
22
|
if (platformSupport) {
|
|
23
23
|
(deps.error ?? console.error)(`${platformSupport.refusalCode}: ${platformSupport.guidance}`);
|
|
@@ -28,7 +28,7 @@ export function handleDoctor(_cli, ctx, deps = {}) {
|
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
30
|
(deps.render ?? render)(React.createElement(CIContextProvider, { isCI: ctx.isCI },
|
|
31
|
-
React.createElement(
|
|
31
|
+
React.createElement(DoctorController, { dryRun: cli.flags.dryRun === true, refreshContext: cli.flags.refreshContext === true })), { stdin: ctx.inkStdin });
|
|
32
32
|
}
|
|
33
33
|
export function handleAnalyze(cli, ctx) {
|
|
34
34
|
render(React.createElement(CIContextProvider, { isCI: ctx.isCI },
|
package/dist/cli/help.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Help banner shown by meow when `--help` is passed or invalid args are supplied.
|
|
9
9
|
* Multi-line template literal — preserve exact formatting (whitespace is significant).
|
|
10
10
|
*/
|
|
11
|
-
export declare const HELP_TEXT = "\n Usage\n $ javi-forge [command] [options]\n\n Commands\n init Bootstrap a new project (default)\n ci Run CI simulation (lint + compile + test + security + ghagga)\n ci validate Validate .javi-forge/ci.yaml without running anything\n ci init Install git hooks that call javi-forge ci\n tdd init Enable the TDD pre-commit section + install managed hooks\n tdd pipeline Enable the TDD pre-push section (--mode strict|warn)\n hooks run Run a git hook's composed sections (pre-commit | pre-push)\n analyze Run repoforge skills analysis\n doctor Show health report\n workflow show Render a workflow graph as ASCII (--template <name> or file path)\n workflow validate Validate project state against a workflow graph\n workflow list List available workflows and built-in templates\n plugin add Install a plugin from GitHub (org/repo)\n plugin remove Remove an installed plugin\n plugin list List installed plugins\n plugin search Search the plugin registry\n plugin validate Validate a local plugin directory\n plugin sync Auto-detect and wire installed plugins\n plugin export Export plugin to Agent Skills spec format (skills.json)\n plugin export --codex: Export plugin to Codex-compatible TOML subagent files\n plugin export-skills Generate aggregated skills.json from all installed plugins\n plugin export-skills global Generate global skills.json from all globally installed plugins\n plugin import Import an Agent Skills spec package as a javi-forge plugin\n skills doctor Show skills health report (add --deep for conflict detection)\n skills budget Show token cost of loaded skills (add -b N for custom budget)\n skills score Score a skill on quality dimensions (completeness, clarity, testability, token-efficiency)\n skills benchmark Benchmark a skill with structural quality checks\n skills auto Auto-detect project stack and suggest/install matching AI skills\n skills auto-install Alias for skills auto\n skill publish Package a skill directory for marketplace distribution (generates plugin.json)\n security baseline Create security baseline from current audit findings\n security check Check for regressions against baseline (exits non-zero if found)\n security update Re-snapshot baseline (acknowledge current vulns)\n security allowlist Add all current findings to the allowlist (suppress in future checks)\n llms-txt Generate AI-friendly llms.txt for current project\n\n Options\n --dry-run Preview changes without writing files\n --stack Project stack (node, python, go, rust, java-gradle, java-maven, elixir)\n --ci CI provider (github, gitlab, woodpecker)\n --memory Memory module (engram, obsidian-brain, memory-simple, none)\n --project-name Project name (skips name prompt)\n --ghagga Enable GHAGGA review system\n --mock Enable mock-first mode (no real API keys needed)\n --local-ai Include local AI dev stack (Ollama + Docker Compose)\n --batch Non-interactive mode (auto-proceed, no keyboard input)\n --deep Enable deep analysis (conflict + duplicate detection)\n --budget, -b Token budget limit for skills (default: 8000)\n --skills-dir Custom skills directory path\n --author Author name for skill publish\n --repo Repository URL for skill publish\n --version Show version\n --help Show this help\n\n CI options (javi-forge ci)\n --quick Lint + compile only (fast, for pre-commit)\n --shell Open interactive shell in CI container\n --detect Show detected stack and exit\n --config PATH Load ordered CI runners from a versioned config file\n (default discovery: .javi-forge/ci.yaml)\n --stack STACK Force a single explicit stack (single-stack repos only \u2014\n insufficient for hybrid repos; use --config instead)\n --no-docker Run commands natively (no Docker)\n --no-ci-ghagga Skip GHAGGA review\n --no-security Skip Semgrep security scan\n --timeout N Per-step timeout in seconds (default: 600)\n\n CI hooks (javi-forge ci init)\n Install git hooks that call javi-forge ci.\n No files copied \u2014 hooks reference the global CLI.\n Existing hooks javi-forge did not write are refused, never clobbered.\n --force Overwrite a foreign or locally modified hook. The previous\n content is copied to a .bak sibling first; if that backup\n cannot be written, the hook is left untouched. Symlinked\n hook paths are refused even with --force.\n\n SkillGuard install gate (plugin add / plugin import / skills auto)\n Every install is scanned before anything is written. Refusals are\n fail-closed and name the offending files:\n - SKILL.md files that block (critical threats) are refused \u2014 always.\n - Unscannable files (binary, oversized, unreadable) are refused unless\n --force is given.\n - Symlinks anywhere in the tree and SKILL.md files outside the declared\n set are manifest-integrity refusals \u2014 they are refused even with --force.\n - Empty or missing skills.json `skills` array on import is refused.\n A refused install/auto-install exits non-zero (exit 1) so scripts and CI\n can tell a refusal apart from success; clean installs \u2014 including\n --force-lifted unscannable ones \u2014 exit 0.\n\n Examples\n $ javi-forge\n $ javi-forge init --dry-run\n $ javi-forge init --stack node --ci github\n $ javi-forge ci\n $ javi-forge ci init\n $ javi-forge ci init --force\n $ javi-forge plugin add org/repo\n $ javi-forge plugin add org/repo --force\n $ javi-forge tdd init\n $ javi-forge ci --quick\n $ javi-forge ci --no-ci-ghagga --no-security\n $ javi-forge ci --no-docker\n $ javi-forge ci --shell\n $ javi-forge ci --config .javi-forge/ci.yaml\n $ javi-forge ci validate\n $ javi-forge ci --help\n $ javi-forge analyze\n $ javi-forge doctor\n $ javi-forge plugin add mapbox/agent-skills\n $ javi-forge plugin list\n";
|
|
11
|
+
export declare const HELP_TEXT = "\n Usage\n $ javi-forge [command] [options]\n\n Commands\n init Bootstrap a new project (default)\n ci Run CI simulation (lint + compile + test + security + ghagga)\n ci validate Validate .javi-forge/ci.yaml without running anything\n ci init Install git hooks that call javi-forge ci\n tdd init Enable the TDD pre-commit section + install managed hooks\n tdd pipeline Enable the TDD pre-push section (--mode strict|warn)\n hooks run Run a git hook's composed sections (pre-commit | pre-push)\n analyze Run repoforge skills analysis\n doctor Show health report\n workflow show Render a workflow graph as ASCII (--template <name> or file path)\n workflow validate Validate project state against a workflow graph\n workflow list List available workflows and built-in templates\n plugin add Install a plugin from GitHub (org/repo)\n plugin remove Remove an installed plugin\n plugin list List installed plugins\n plugin search Search the plugin registry\n plugin validate Validate a local plugin directory\n plugin sync Auto-detect and wire installed plugins\n plugin export Export plugin to Agent Skills spec format (skills.json)\n plugin export --codex: Export plugin to Codex-compatible TOML subagent files\n plugin export-skills Generate aggregated skills.json from all installed plugins\n plugin export-skills global Generate global skills.json from all globally installed plugins\n plugin import Import an Agent Skills spec package as a javi-forge plugin\n skills doctor Show skills health report (add --deep for conflict detection)\n skills budget Show token cost of loaded skills (add -b N for custom budget)\n skills score Score a skill on quality dimensions (completeness, clarity, testability, token-efficiency)\n skills benchmark Benchmark a skill with structural quality checks\n skills auto Auto-detect project stack and suggest/install matching AI skills\n skills auto-install Alias for skills auto\n skill publish Package a skill directory for marketplace distribution (generates plugin.json)\n security baseline Create security baseline from current audit findings\n security check Check for regressions against baseline (exits non-zero if found)\n security update Re-snapshot baseline (acknowledge current vulns)\n security allowlist Add all current findings to the allowlist (suppress in future checks)\n llms-txt Generate AI-friendly llms.txt for current project\n\n Options\n --dry-run Preview changes without writing files\n --stack Project stack (node, python, go, rust, java-gradle, java-maven, elixir)\n --ci CI provider (github, gitlab, woodpecker)\n --memory Memory module (engram, obsidian-brain, memory-simple, none)\n --project-name Project name (skips name prompt)\n --ghagga Enable GHAGGA review system\n --mock Enable mock-first mode (no real API keys needed)\n --local-ai Include local AI dev stack (Ollama + Docker Compose)\n --batch Non-interactive mode (auto-proceed, no keyboard input)\n --refresh-context Refresh .context/ during doctor (writes INDEX.md, summary.md, manifest timestamp)\n --deep Enable deep analysis (conflict + duplicate detection)\n --budget, -b Token budget limit for skills (default: 8000)\n --skills-dir Custom skills directory path\n --author Author name for skill publish\n --repo Repository URL for skill publish\n --version Show version\n --help Show this help\n\n CI options (javi-forge ci)\n --quick Lint + compile only (fast, for pre-commit)\n --shell Open interactive shell in CI container\n --detect Show detected stack and exit\n --config PATH Load ordered CI runners from a versioned config file\n (default discovery: .javi-forge/ci.yaml)\n --stack STACK Force a single explicit stack (single-stack repos only \u2014\n insufficient for hybrid repos; use --config instead)\n --no-docker Run commands natively (no Docker)\n --no-ci-ghagga Skip GHAGGA review\n --no-security Skip Semgrep security scan\n --timeout N Per-step timeout in seconds (default: 600)\n\n CI hooks (javi-forge ci init)\n Install git hooks that call javi-forge ci.\n No files copied \u2014 hooks reference the global CLI.\n Existing hooks javi-forge did not write are refused, never clobbered.\n --force Overwrite a foreign or locally modified hook. The previous\n content is copied to a .bak sibling first; if that backup\n cannot be written, the hook is left untouched. Symlinked\n hook paths are refused even with --force.\n\n SkillGuard install gate (plugin add / plugin import / skills auto)\n Every install is scanned before anything is written. Refusals are\n fail-closed and name the offending files:\n - SKILL.md files that block (critical threats) are refused \u2014 always.\n - Unscannable files (binary, oversized, unreadable) are refused unless\n --force is given.\n - Symlinks anywhere in the tree and SKILL.md files outside the declared\n set are manifest-integrity refusals \u2014 they are refused even with --force.\n - Empty or missing skills.json `skills` array on import is refused.\n A refused install/auto-install exits non-zero (exit 1) so scripts and CI\n can tell a refusal apart from success; clean installs \u2014 including\n --force-lifted unscannable ones \u2014 exit 0.\n\n Examples\n $ javi-forge\n $ javi-forge init --dry-run\n $ javi-forge init --stack node --ci github\n $ javi-forge ci\n $ javi-forge ci init\n $ javi-forge ci init --force\n $ javi-forge plugin add org/repo\n $ javi-forge plugin add org/repo --force\n $ javi-forge tdd init\n $ javi-forge ci --quick\n $ javi-forge ci --no-ci-ghagga --no-security\n $ javi-forge ci --no-docker\n $ javi-forge ci --shell\n $ javi-forge ci --config .javi-forge/ci.yaml\n $ javi-forge ci validate\n $ javi-forge ci --help\n $ javi-forge analyze\n $ javi-forge doctor\n $ javi-forge plugin add mapbox/agent-skills\n $ javi-forge plugin list\n";
|
|
12
12
|
/**
|
|
13
13
|
* Per-command help for `ci`, shown by `javi-forge ci --help` (or when `ci` is
|
|
14
14
|
* given an unknown subcommand). Kept consistent with the global HELP_TEXT
|
|
@@ -30,6 +30,10 @@ export declare const FLAGS_SCHEMA: {
|
|
|
30
30
|
readonly type: "boolean";
|
|
31
31
|
readonly default: false;
|
|
32
32
|
};
|
|
33
|
+
readonly refreshContext: {
|
|
34
|
+
readonly type: "boolean";
|
|
35
|
+
readonly default: false;
|
|
36
|
+
};
|
|
33
37
|
readonly stack: {
|
|
34
38
|
readonly type: "string";
|
|
35
39
|
readonly default: "";
|
package/dist/cli/help.js
CHANGED
|
@@ -59,6 +59,7 @@ export const HELP_TEXT = `
|
|
|
59
59
|
--mock Enable mock-first mode (no real API keys needed)
|
|
60
60
|
--local-ai Include local AI dev stack (Ollama + Docker Compose)
|
|
61
61
|
--batch Non-interactive mode (auto-proceed, no keyboard input)
|
|
62
|
+
--refresh-context Refresh .context/ during doctor (writes INDEX.md, summary.md, manifest timestamp)
|
|
62
63
|
--deep Enable deep analysis (conflict + duplicate detection)
|
|
63
64
|
--budget, -b Token budget limit for skills (default: 8000)
|
|
64
65
|
--skills-dir Custom skills directory path
|
|
@@ -214,6 +215,7 @@ export const FLAGS_SCHEMA = {
|
|
|
214
215
|
// `ci --help` can show ci-specific usage instead of the global banner).
|
|
215
216
|
help: { type: "boolean", shortFlag: "h", default: false },
|
|
216
217
|
dryRun: { type: "boolean", default: false },
|
|
218
|
+
refreshContext: { type: "boolean", default: false },
|
|
217
219
|
stack: { type: "string", default: "" },
|
|
218
220
|
ci: { type: "string", default: "" },
|
|
219
221
|
memory: { type: "string", default: "" },
|
package/dist/cli/main.js
CHANGED
|
@@ -10,11 +10,21 @@ import { handleTdd } from "./dispatch/tdd.js";
|
|
|
10
10
|
import { handleWorkflow } from "./dispatch/workflow.js";
|
|
11
11
|
import { FLAGS_SCHEMA, HELP_TEXT } from "./help.js";
|
|
12
12
|
import { createInkStdin, detectCI, setupUpdateNotifier } from "./runtime.js";
|
|
13
|
+
const KNOWN_COMMANDS = new Set([
|
|
14
|
+
"init",
|
|
15
|
+
"tdd",
|
|
16
|
+
"ci",
|
|
17
|
+
"hooks",
|
|
18
|
+
"doctor",
|
|
19
|
+
"analyze",
|
|
20
|
+
"workflow",
|
|
21
|
+
"llms-txt",
|
|
22
|
+
"plugin",
|
|
23
|
+
"skills",
|
|
24
|
+
"skill",
|
|
25
|
+
"security",
|
|
26
|
+
]);
|
|
13
27
|
export async function runCli() {
|
|
14
|
-
// Check for updates in background (non-blocking, cached 24h)
|
|
15
|
-
const _require = createRequire(import.meta.url);
|
|
16
|
-
const pkg = _require("../../package.json");
|
|
17
|
-
setupUpdateNotifier(pkg);
|
|
18
28
|
const cli = meow(HELP_TEXT, {
|
|
19
29
|
importMeta: import.meta,
|
|
20
30
|
flags: FLAGS_SCHEMA,
|
|
@@ -23,12 +33,23 @@ export async function runCli() {
|
|
|
23
33
|
autoHelp: false,
|
|
24
34
|
});
|
|
25
35
|
const subcommand = cli.input[0] ?? "init";
|
|
36
|
+
if (!KNOWN_COMMANDS.has(subcommand)) {
|
|
37
|
+
console.error(`Unknown command "${subcommand}". Run javi-forge --help for usage.`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
26
40
|
// Global --help: every command except `ci` and `hooks` shows the global banner
|
|
27
41
|
// here. Those two own their per-command help inside their handlers.
|
|
28
42
|
if (cli.flags.help && subcommand !== "ci" && subcommand !== "hooks") {
|
|
29
43
|
console.log(HELP_TEXT);
|
|
30
44
|
process.exit(0);
|
|
31
45
|
}
|
|
46
|
+
// Check for updates in background (non-blocking, cached 24h). Diagnostics and
|
|
47
|
+
// dry-runs must not create notifier cache/state before doing their actual work.
|
|
48
|
+
if (subcommand !== "doctor" && !cli.flags.dryRun) {
|
|
49
|
+
const _require = createRequire(import.meta.url);
|
|
50
|
+
const pkg = _require("../../package.json");
|
|
51
|
+
setupUpdateNotifier(pkg);
|
|
52
|
+
}
|
|
32
53
|
const isCI = detectCI(cli.flags);
|
|
33
54
|
const inkStdin = createInkStdin();
|
|
34
55
|
switch (subcommand) {
|
|
@@ -76,7 +97,7 @@ export async function runCli() {
|
|
|
76
97
|
await handleSecurity(cli);
|
|
77
98
|
break;
|
|
78
99
|
}
|
|
79
|
-
|
|
100
|
+
case "init": {
|
|
80
101
|
handleInitDefault(cli, { inkStdin, isCI });
|
|
81
102
|
break;
|
|
82
103
|
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import fs from "fs-extra";
|
|
2
2
|
import { detectStack } from "../lib/common.js";
|
|
3
|
-
import { refreshContextDir } from "../lib/context.js";
|
|
3
|
+
import { detectDependenciesDetailed, refreshContextDir } from "../lib/context.js";
|
|
4
4
|
import { execFileAsync } from "../lib/exec.js";
|
|
5
5
|
import { listInstalledPlugins } from "../lib/plugin.js";
|
|
6
6
|
import type { DoctorResult } from "../types/index.js";
|
|
7
7
|
export type CheckStatus = "ok" | "fail" | "skip";
|
|
8
8
|
type DoctorFilesystem = Pick<typeof fs, "pathExists" | "readJson" | "readdir">;
|
|
9
|
-
export interface
|
|
9
|
+
export interface DoctorOptions {
|
|
10
|
+
dryRun?: boolean;
|
|
11
|
+
refreshContext?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface DoctorDeps extends DoctorOptions {
|
|
10
14
|
platform?: string;
|
|
11
15
|
cwd?: () => string;
|
|
12
16
|
filesystem?: DoctorFilesystem;
|
|
@@ -14,6 +18,7 @@ export interface DoctorDeps {
|
|
|
14
18
|
stackDetector?: typeof detectStack;
|
|
15
19
|
pluginLister?: typeof listInstalledPlugins;
|
|
16
20
|
contextRefresher?: typeof refreshContextDir;
|
|
21
|
+
dependencyDetector?: typeof detectDependenciesDetailed;
|
|
17
22
|
}
|
|
18
23
|
interface RemoteInfo {
|
|
19
24
|
host: "github" | "gitlab" | "other";
|
package/dist/commands/doctor.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import fs from "fs-extra";
|
|
3
3
|
import { FORGE_ROOT, MODULES_DIR, PLUGINS_DIR, TEMPLATES_DIR, } from "../constants.js";
|
|
4
4
|
import { detectStack } from "../lib/common.js";
|
|
5
|
-
import { refreshContextDir } from "../lib/context.js";
|
|
5
|
+
import { detectDependenciesDetailed, refreshContextDir, } from "../lib/context.js";
|
|
6
6
|
import { execFileAsync } from "../lib/exec.js";
|
|
7
7
|
import { resolvePlatformSupport } from "../lib/platform-support.js";
|
|
8
8
|
import { listInstalledPlugins } from "../lib/plugin.js";
|
|
@@ -162,6 +162,7 @@ export async function runDoctor(projectDir, deps = {}) {
|
|
|
162
162
|
const stackDetector = deps.stackDetector ?? detectStack;
|
|
163
163
|
const pluginLister = deps.pluginLister ?? listInstalledPlugins;
|
|
164
164
|
const contextRefresher = deps.contextRefresher ?? refreshContextDir;
|
|
165
|
+
const dependencyDetector = deps.dependencyDetector ?? detectDependenciesDetailed;
|
|
165
166
|
const sections = [];
|
|
166
167
|
// ── 1. System Tools ────────────────────────────────────────────────────────
|
|
167
168
|
const toolChecks = [];
|
|
@@ -328,19 +329,37 @@ export async function runDoctor(projectDir, deps = {}) {
|
|
|
328
329
|
});
|
|
329
330
|
}
|
|
330
331
|
sections.push({ title: "Plugins", checks: pluginChecks });
|
|
331
|
-
// ── 7. Context Directory
|
|
332
|
+
// ── 7. Context Directory (refresh is explicit) ──────────────────────────────
|
|
332
333
|
const contextChecks = [];
|
|
333
334
|
try {
|
|
334
|
-
|
|
335
|
-
|
|
335
|
+
if (deps.refreshContext && !deps.dryRun) {
|
|
336
|
+
const result = await contextRefresher(cwd);
|
|
336
337
|
contextChecks.push({
|
|
337
338
|
label: ".context/ refresh",
|
|
338
|
-
status: "ok",
|
|
339
|
-
detail:
|
|
339
|
+
status: result ? "ok" : "skip",
|
|
340
|
+
detail: result
|
|
341
|
+
? "INDEX.md + summary.md updated"
|
|
342
|
+
: "no .context/ or no manifest found",
|
|
340
343
|
});
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
+
for (const warning of result?.warnings ?? []) {
|
|
345
|
+
contextChecks.push({
|
|
346
|
+
label: "dependency manifest",
|
|
347
|
+
status: "fail",
|
|
348
|
+
detail: warning,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else if (manifest &&
|
|
353
|
+
(await filesystem.pathExists(path.join(cwd, ".context")))) {
|
|
354
|
+
contextChecks.push({
|
|
355
|
+
label: ".context/",
|
|
356
|
+
status: deps.refreshContext ? "skip" : "ok",
|
|
357
|
+
detail: deps.refreshContext
|
|
358
|
+
? "dry-run: would refresh INDEX.md + summary.md and manifest timestamp"
|
|
359
|
+
: "present; not refreshed (use --refresh-context to update)",
|
|
360
|
+
});
|
|
361
|
+
const { warnings } = await dependencyDetector(cwd, manifest.stack);
|
|
362
|
+
for (const warning of warnings) {
|
|
344
363
|
contextChecks.push({
|
|
345
364
|
label: "dependency manifest",
|
|
346
365
|
status: "fail",
|
|
@@ -350,17 +369,17 @@ export async function runDoctor(projectDir, deps = {}) {
|
|
|
350
369
|
}
|
|
351
370
|
else {
|
|
352
371
|
contextChecks.push({
|
|
353
|
-
label: ".context/
|
|
372
|
+
label: ".context/",
|
|
354
373
|
status: "skip",
|
|
355
374
|
detail: "no .context/ or no manifest found",
|
|
356
375
|
});
|
|
357
376
|
}
|
|
358
377
|
}
|
|
359
|
-
catch (
|
|
378
|
+
catch (error) {
|
|
360
379
|
contextChecks.push({
|
|
361
|
-
label: ".context/
|
|
380
|
+
label: ".context/",
|
|
362
381
|
status: "fail",
|
|
363
|
-
detail: `
|
|
382
|
+
detail: `context check failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
364
383
|
});
|
|
365
384
|
}
|
|
366
385
|
sections.push({ title: "Context Directory", checks: contextChecks });
|
|
@@ -17,7 +17,9 @@ export declare function runPluginList(onStep: StepCallback): Promise<void>;
|
|
|
17
17
|
/**
|
|
18
18
|
* Search the remote plugin registry.
|
|
19
19
|
*/
|
|
20
|
-
export declare function runPluginSearch(query: string | undefined, onStep: StepCallback
|
|
20
|
+
export declare function runPluginSearch(query: string | undefined, onStep: StepCallback, options?: {
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
}): Promise<void>;
|
|
21
23
|
/**
|
|
22
24
|
* Validate a local plugin directory.
|
|
23
25
|
*/
|
package/dist/commands/plugin.js
CHANGED
|
@@ -4,6 +4,22 @@ import { installPlugin, listInstalledPlugins, removePlugin, searchRegistry, sync
|
|
|
4
4
|
function report(onStep, id, label, status, detail) {
|
|
5
5
|
onStep({ id, label, status, detail });
|
|
6
6
|
}
|
|
7
|
+
function successDetail(action, dryRun, result) {
|
|
8
|
+
const verb = action === "install" ? "install" : "import";
|
|
9
|
+
const past = action === "install" ? "installed" : "imported";
|
|
10
|
+
const lines = [
|
|
11
|
+
dryRun ? `dry-run: would ${verb} ${result.name}` : `${past} ${result.name}`,
|
|
12
|
+
];
|
|
13
|
+
if (result.warning)
|
|
14
|
+
lines.push(`warning: ${result.warning}`);
|
|
15
|
+
if (result.recoveryPaths?.length) {
|
|
16
|
+
lines.push("manual recovery paths:");
|
|
17
|
+
for (const recoveryPath of result.recoveryPaths) {
|
|
18
|
+
lines.push(` - ${recoveryPath}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return lines.join("\n");
|
|
22
|
+
}
|
|
7
23
|
/**
|
|
8
24
|
* Add (install) a plugin from a GitHub source.
|
|
9
25
|
*/
|
|
@@ -12,9 +28,7 @@ export async function runPluginAdd(source, dryRun, onStep, options = {}) {
|
|
|
12
28
|
report(onStep, stepId, `Install plugin: ${source}`, "running");
|
|
13
29
|
const result = await installPlugin(source, { dryRun, force: options.force });
|
|
14
30
|
if (result.success) {
|
|
15
|
-
report(onStep, stepId, `Install plugin: ${source}`, "done", dryRun
|
|
16
|
-
? `dry-run: would install ${result.name}`
|
|
17
|
-
: `installed ${result.name}`);
|
|
31
|
+
report(onStep, stepId, `Install plugin: ${source}`, "done", successDetail("install", dryRun, result));
|
|
18
32
|
}
|
|
19
33
|
else {
|
|
20
34
|
report(onStep, stepId, `Install plugin: ${source}`, "error", result.error);
|
|
@@ -58,10 +72,10 @@ export async function runPluginList(onStep) {
|
|
|
58
72
|
/**
|
|
59
73
|
* Search the remote plugin registry.
|
|
60
74
|
*/
|
|
61
|
-
export async function runPluginSearch(query, onStep) {
|
|
75
|
+
export async function runPluginSearch(query, onStep, options = {}) {
|
|
62
76
|
const stepId = "plugin-search";
|
|
63
77
|
report(onStep, stepId, `Search plugins${query ? `: ${query}` : ""}`, "running");
|
|
64
|
-
const results = await searchRegistry(query);
|
|
78
|
+
const results = await searchRegistry(query, options);
|
|
65
79
|
if (results.status === "cancelled") {
|
|
66
80
|
report(onStep, stepId, `Search plugins${query ? `: ${query}` : ""}`, "error", "registry search cancelled");
|
|
67
81
|
}
|
|
@@ -163,9 +177,7 @@ export async function runPluginImport(sourceDir, dryRun, onStep, options = {}) {
|
|
|
163
177
|
force: options.force,
|
|
164
178
|
});
|
|
165
179
|
if (result.success) {
|
|
166
|
-
report(onStep, stepId, `Import agent-skills package: ${sourceDir}`, "done", dryRun
|
|
167
|
-
? `dry-run: would import ${result.name}`
|
|
168
|
-
: `imported ${result.name}`);
|
|
180
|
+
report(onStep, stepId, `Import agent-skills package: ${sourceDir}`, "done", successDetail("import", dryRun, result));
|
|
169
181
|
}
|
|
170
182
|
else {
|
|
171
183
|
report(onStep, stepId, `Import agent-skills package: ${sourceDir}`, "error", result.error);
|
|
@@ -177,16 +177,19 @@ export function makeFakeSecureFs() {
|
|
|
177
177
|
},
|
|
178
178
|
async renameInDir(dir, from, to) {
|
|
179
179
|
if (fake.faults.renameRefuse?.(to))
|
|
180
|
-
return unsafe(`rename ${to}`);
|
|
180
|
+
return { ...unsafe(`rename ${to}`), mutation: "not-applied" };
|
|
181
181
|
const fromP = path.join(dir.path, from);
|
|
182
182
|
const toP = path.join(dir.path, to);
|
|
183
183
|
const file = files.get(fromP);
|
|
184
184
|
if (!file)
|
|
185
|
-
return
|
|
185
|
+
return {
|
|
186
|
+
...unsafe(`rename enoent ${fromP}`),
|
|
187
|
+
mutation: "not-applied",
|
|
188
|
+
};
|
|
186
189
|
files.set(toP, file);
|
|
187
190
|
files.delete(fromP);
|
|
188
191
|
inos.delete(toP); // fresh identity for the renamed-in target
|
|
189
|
-
return ok
|
|
192
|
+
return { ok: true, mutation: "applied" };
|
|
190
193
|
},
|
|
191
194
|
async unlinkIfIdentity(dir, name, _held) {
|
|
192
195
|
const full = path.join(dir.path, name);
|