javi-forge 1.39.0 → 1.39.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/claude-hooks/javi-forge-skillguard-pre-tool-use.mjs +61 -3
- package/assets/claude-hooks/manifest.json +3 -2
- package/dist/cli/dispatch/plugin.d.ts +9 -0
- package/dist/cli/dispatch/plugin.js +68 -0
- package/dist/cli/dispatch/simple-renderers.js +9 -18
- package/dist/commands/plugin.d.ts +43 -11
- package/dist/commands/plugin.js +122 -11
- package/dist/ui/Plugin.d.ts +6 -6
- package/dist/ui/Plugin.js +8 -148
- package/package.json +1 -1
|
@@ -792,12 +792,63 @@ function bashSubstitutions(command) {
|
|
|
792
792
|
}
|
|
793
793
|
return bodies;
|
|
794
794
|
}
|
|
795
|
+
// This deliberately recognizes only a direct, quoted Python stdin heredoc on
|
|
796
|
+
// the first command line. It removes the opaque body before shell inspection;
|
|
797
|
+
// it does not parse Python or permit its execution.
|
|
798
|
+
function pythonQuotedHeredoc(command) {
|
|
799
|
+
const newline = command.indexOf("\n");
|
|
800
|
+
const header = newline < 0 ? command : command.slice(0, newline);
|
|
801
|
+
const starts = [0];
|
|
802
|
+
let quote = "", escaped = false, heredocs = 0;
|
|
803
|
+
for (let index = 0; index < header.length; index++) {
|
|
804
|
+
const char = header[index];
|
|
805
|
+
if (escaped) { escaped = false; continue; }
|
|
806
|
+
if (char === "\\" && quote !== "'") { escaped = true; continue; }
|
|
807
|
+
if (quote) { if (char === quote) quote = ""; continue; }
|
|
808
|
+
if (char === "'" || char === '"' || char === "`") { quote = char; continue; }
|
|
809
|
+
if (char === "<" && header[index + 1] === "<") { heredocs++; index++; }
|
|
810
|
+
if (char === ";" || char === "|" || char === "&") starts.push(index + 1);
|
|
811
|
+
}
|
|
812
|
+
const literalPath = String.raw`(?:[A-Za-z0-9_./~-]+|'[A-Za-z0-9_./~ -]+'|"[A-Za-z0-9_./~ -]+")`;
|
|
813
|
+
const redirection = String.raw`(?:[0-9]*>>?|[0-9]*<)[ \t]*${literalPath}[ \t]*`;
|
|
814
|
+
const python = String.raw`[ \t]*(?:python3?|/(?:[A-Za-z0-9_.-]+/)*python3?)(?:[ \t]+-)?[ \t]*`;
|
|
815
|
+
const pattern = new RegExp(String.raw`^(${python}(?:${redirection})*)<<(-?)[ \t]*(['"])([A-Za-z_][A-Za-z0-9_]*)\3([ \t]*(?:${redirection})*)([;|&].*)?$`);
|
|
816
|
+
for (const start of starts) {
|
|
817
|
+
const match = pattern.exec(header.slice(start));
|
|
818
|
+
if (!match) continue;
|
|
819
|
+
// Multiple documents and tab-stripping need a shell grammar this guard
|
|
820
|
+
// intentionally does not implement, so retain fail-closed behavior.
|
|
821
|
+
if (newline < 0 || heredocs !== 1 || match[2]) fail("unlexable-command");
|
|
822
|
+
if (match[6]?.trimStart().startsWith("|")) fail("unlexable-command");
|
|
823
|
+
let offset = newline + 1;
|
|
824
|
+
while (offset <= command.length) {
|
|
825
|
+
const end = command.indexOf("\n", offset);
|
|
826
|
+
const lineEnd = end < 0 ? command.length : end;
|
|
827
|
+
if (command.slice(offset, lineEnd) === match[4]) {
|
|
828
|
+
const outerHeader = header.slice(0, start) + match[1] + match[5] + (match[6] ?? "");
|
|
829
|
+
return { command: `${outerHeader}\n${command.slice(lineEnd + 1)}`, unsupported: true };
|
|
830
|
+
}
|
|
831
|
+
if (end < 0) break;
|
|
832
|
+
offset = end + 1;
|
|
833
|
+
}
|
|
834
|
+
fail("unlexable-command");
|
|
835
|
+
}
|
|
836
|
+
return { command, unsupported: false };
|
|
837
|
+
}
|
|
795
838
|
function evaluateBash(command, cwd, config = AGENT_CONFIGS.claude, projectRoot = PROJECT_ROOT, depth = 0) {
|
|
796
839
|
if (depth > 4) return { allowed: false, ruleId: "shell.obfuscated-interpreter" };
|
|
840
|
+
let unsupported = false;
|
|
841
|
+
try { ({ command, unsupported } = pythonQuotedHeredoc(command)); } catch { return { allowed: false, ruleId: "shell.obfuscated-interpreter" }; }
|
|
797
842
|
const headerOnlyCommand = opaqueInertCatHereDoc(command, cwd, config, projectRoot);
|
|
798
843
|
if (headerOnlyCommand === null) return { allowed: false, ruleId: "shell.obfuscated-interpreter" };
|
|
799
844
|
command = headerOnlyCommand;
|
|
800
|
-
try {
|
|
845
|
+
try {
|
|
846
|
+
for (const body of bashSubstitutions(command)) {
|
|
847
|
+
const nested = evaluateBash(body, cwd, config, projectRoot, depth + 1);
|
|
848
|
+
if (nested.ruleId === "shell.unsupported-interpreter") unsupported = true;
|
|
849
|
+
else if (!nested.allowed) return nested;
|
|
850
|
+
}
|
|
851
|
+
} catch { return { allowed: false, ruleId: "shell.obfuscated-interpreter" }; }
|
|
801
852
|
if (/^\s*:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:\s*$/.test(command)) return { allowed: false, ruleId: "shell.destructive-root" };
|
|
802
853
|
let parsed;
|
|
803
854
|
try { parsed = lex(command); } catch { return { allowed: false, ruleId: "shell.obfuscated-interpreter" }; }
|
|
@@ -834,10 +885,16 @@ function evaluateBash(command, cwd, config = AGENT_CONFIGS.claude, projectRoot =
|
|
|
834
885
|
if (/^(?:powershell|pwsh)(?:\.exe)?$/i.test(executable) && tokens.some((token) => /^-(?:enc|encodedcommand)$/i.test(token))) return { allowed: false, ruleId: "shell.obfuscated-interpreter" };
|
|
835
886
|
if (/^(?:bash|sh|zsh|dash|ksh)$/.test(executable)) {
|
|
836
887
|
const flag = tokens.findIndex((token) => /^-[^-]*c[^-]*$/.test(token));
|
|
837
|
-
if (flag >= 0) {
|
|
888
|
+
if (flag >= 0) {
|
|
889
|
+
const body = tokens[flag + 1];
|
|
890
|
+
if (!body || /\$(?!\()/.test(body)) return { allowed: false, ruleId: "shell.obfuscated-interpreter" };
|
|
891
|
+
const nested = evaluateBash(body, cwd, config, projectRoot, depth + 1);
|
|
892
|
+
if (nested.ruleId === "shell.unsupported-interpreter") unsupported = true;
|
|
893
|
+
else if (!nested.allowed) return nested;
|
|
894
|
+
}
|
|
838
895
|
}
|
|
839
896
|
}
|
|
840
|
-
return { allowed: true };
|
|
897
|
+
return unsupported ? { allowed: false, ruleId: "shell.unsupported-interpreter" } : { allowed: true };
|
|
841
898
|
}
|
|
842
899
|
function evaluatePowerShell(command, cwd, config = AGENT_CONFIGS.claude, projectRoot = PROJECT_ROOT) {
|
|
843
900
|
const parsed = lex(command, true);
|
|
@@ -939,6 +996,7 @@ function diagnostic(error) {
|
|
|
939
996
|
}
|
|
940
997
|
function denialDiagnostic(toolName, decision) {
|
|
941
998
|
const tool = SUPPORTED_TOOLS.includes(toolName) ? toolName : "supported tool";
|
|
999
|
+
if (decision.ruleId === "shell.unsupported-interpreter") return "javi-forge PreToolUse denied Bash [shell.unsupported-interpreter]: quoted Python heredoc execution is unsupported";
|
|
942
1000
|
if (decision.ambiguity) return `javi-forge PreToolUse denied ${tool} [${decision.ruleId}]: ${decision.ambiguity.utility} ${decision.ambiguity.profile} ${decision.ambiguity.sink} semantics denied as ambiguous`;
|
|
943
1001
|
return `javi-forge PreToolUse denied ${tool} [${decision.ruleId}]: global guard policy denied the invocation`;
|
|
944
1002
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"name": "javi-forge-skillguard-pre-tool-use.mjs",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"policyVersion": 2,
|
|
7
|
-
"sha256": "
|
|
7
|
+
"sha256": "507a57a15f1b967103bb1eefe701a74813d5a9603fa7bcc15579a215b03621e3",
|
|
8
8
|
"historical": [
|
|
9
9
|
"78be7e6613c012280b7ad17886462ba166b63ebd031e34565d757b3a0796d7cc",
|
|
10
10
|
"5dc2a5c31131f4ac7d8657c78b950de52776aad6eaefe78ea0d764a9963c4425",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"3581862f0567cce75a58b693c9ade80d39ee7d58add11537a34a8461c47c1ed4",
|
|
13
13
|
"54a270f28b068450b79547a88ec6f2d4854514392fd5f38ed1d6174ea093d7aa",
|
|
14
14
|
"9a565cec31d9e091e3fb9420b86685f824733bc1ebe479f086b2b955aba6ef3e",
|
|
15
|
-
"59fc4224975ad64cfc85bab50ec60d9bd4948070e9d41f42ab43e6d8231c19a1"
|
|
15
|
+
"59fc4224975ad64cfc85bab50ec60d9bd4948070e9d41f42ab43e6d8231c19a1",
|
|
16
|
+
"6edfbb31ce0551b27e38ae1ffd1daf9cc4bea54f2c86687c5124132af5b8c0af"
|
|
16
17
|
]
|
|
17
18
|
},
|
|
18
19
|
"settingsEntries": {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ReactElement } from "react";
|
|
2
|
+
import { type PluginCommandRequest, type PluginCommandResult } from "../../commands/plugin.js";
|
|
3
|
+
/** Preserve an earlier CLI failure, even when this request succeeds. */
|
|
4
|
+
export declare function pluginCommandExitCode(result: PluginCommandResult, previous?: typeof process.exitCode): typeof process.exitCode;
|
|
5
|
+
/** One mounted controller is one CLI invocation, never a retry or retarget. */
|
|
6
|
+
export default function PluginController({ request, }: {
|
|
7
|
+
request: PluginCommandRequest;
|
|
8
|
+
}): ReactElement;
|
|
9
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createElement, useEffect, useRef, useState, } from "react";
|
|
2
|
+
import { PLUGIN_COMMAND_STATUS, runPluginCommand, } from "../../commands/plugin.js";
|
|
3
|
+
import Plugin from "../../ui/Plugin.js";
|
|
4
|
+
/** Preserve an earlier CLI failure, even when this request succeeds. */
|
|
5
|
+
export function pluginCommandExitCode(result, previous = undefined) {
|
|
6
|
+
if (previous !== undefined && previous !== 0 && previous !== "0")
|
|
7
|
+
return previous;
|
|
8
|
+
return result.status === PLUGIN_COMMAND_STATUS.SUCCESS ? 0 : 1;
|
|
9
|
+
}
|
|
10
|
+
function abortSearch(signal, controller) {
|
|
11
|
+
if (controller.signal.aborted)
|
|
12
|
+
return;
|
|
13
|
+
controller.abort();
|
|
14
|
+
if (signal === "SIGINT")
|
|
15
|
+
process.exitCode = 130;
|
|
16
|
+
if (signal === "SIGTERM")
|
|
17
|
+
process.exitCode = 143;
|
|
18
|
+
}
|
|
19
|
+
/** One mounted controller is one CLI invocation, never a retry or retarget. */
|
|
20
|
+
export default function PluginController({ request, }) {
|
|
21
|
+
const initialRequest = useRef({ ...request }).current;
|
|
22
|
+
const started = useRef(false);
|
|
23
|
+
const [steps, setSteps] = useState([]);
|
|
24
|
+
const [result, setResult] = useState(null);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (started.current)
|
|
27
|
+
return;
|
|
28
|
+
started.current = true;
|
|
29
|
+
const controller = initialRequest.action === "search" ? new AbortController() : undefined;
|
|
30
|
+
const sigintHandler = controller === undefined
|
|
31
|
+
? undefined
|
|
32
|
+
: () => abortSearch("SIGINT", controller);
|
|
33
|
+
const sigtermHandler = controller === undefined
|
|
34
|
+
? undefined
|
|
35
|
+
: () => abortSearch("SIGTERM", controller);
|
|
36
|
+
if (sigintHandler)
|
|
37
|
+
process.on("SIGINT", sigintHandler);
|
|
38
|
+
if (sigtermHandler)
|
|
39
|
+
process.on("SIGTERM", sigtermHandler);
|
|
40
|
+
const onStep = (step) => {
|
|
41
|
+
setSteps((previous) => {
|
|
42
|
+
const index = previous.findIndex((item) => item.id === step.id);
|
|
43
|
+
if (index < 0)
|
|
44
|
+
return [...previous, step];
|
|
45
|
+
const next = [...previous];
|
|
46
|
+
next[index] = step;
|
|
47
|
+
return next;
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
void runPluginCommand({ ...initialRequest, signal: controller?.signal }, onStep).then((outcome) => {
|
|
51
|
+
process.exitCode = pluginCommandExitCode(outcome, process.exitCode);
|
|
52
|
+
setResult(outcome);
|
|
53
|
+
});
|
|
54
|
+
return () => {
|
|
55
|
+
if (sigintHandler)
|
|
56
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
57
|
+
if (sigtermHandler)
|
|
58
|
+
process.removeListener("SIGTERM", sigtermHandler);
|
|
59
|
+
};
|
|
60
|
+
}, [initialRequest]);
|
|
61
|
+
return createElement(Plugin, {
|
|
62
|
+
action: initialRequest.action ?? "list",
|
|
63
|
+
dryRun: initialRequest.dryRun,
|
|
64
|
+
steps,
|
|
65
|
+
result,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -14,9 +14,9 @@ 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
16
|
import LlmsTxt from "../../ui/LlmsTxt.js";
|
|
17
|
-
import Plugin from "../../ui/Plugin.js";
|
|
18
17
|
import { VALID_CI, VALID_MEMORY, VALID_STACKS } from "../validators.js";
|
|
19
18
|
import DoctorController from "./doctor.js";
|
|
19
|
+
import PluginController from "./plugin.js";
|
|
20
20
|
export function handleDoctor(cli, ctx, deps = {}) {
|
|
21
21
|
const platformSupport = resolvePlatformSupport(deps.platform ?? process.platform);
|
|
22
22
|
if (platformSupport) {
|
|
@@ -39,24 +39,15 @@ export function handleLlmsTxt(cli, ctx) {
|
|
|
39
39
|
React.createElement(LlmsTxt, { projectDir: process.cwd(), dryRun: cli.flags.dryRun })), { stdin: ctx.inkStdin });
|
|
40
40
|
}
|
|
41
41
|
export function handlePlugin(cli, ctx) {
|
|
42
|
-
const pluginAction = cli.input[1];
|
|
43
|
-
const VALID_PLUGIN_ACTIONS = [
|
|
44
|
-
"add",
|
|
45
|
-
"remove",
|
|
46
|
-
"list",
|
|
47
|
-
"search",
|
|
48
|
-
"validate",
|
|
49
|
-
"sync",
|
|
50
|
-
"export",
|
|
51
|
-
"import",
|
|
52
|
-
"export-skills",
|
|
53
|
-
];
|
|
54
|
-
const action = pluginAction && VALID_PLUGIN_ACTIONS.includes(pluginAction)
|
|
55
|
-
? pluginAction
|
|
56
|
-
: "list";
|
|
57
|
-
const target = cli.input[2];
|
|
58
42
|
render(React.createElement(CIContextProvider, { isCI: ctx.isCI },
|
|
59
|
-
React.createElement(
|
|
43
|
+
React.createElement(PluginController, { request: {
|
|
44
|
+
action: cli.input[1],
|
|
45
|
+
target: cli.input[2],
|
|
46
|
+
projectDir: process.cwd(),
|
|
47
|
+
dryRun: cli.flags.dryRun,
|
|
48
|
+
codex: cli.flags.codex,
|
|
49
|
+
force: cli.flags.force,
|
|
50
|
+
} })), { stdin: ctx.inkStdin });
|
|
60
51
|
}
|
|
61
52
|
export function handleInitDefault(cli, ctx) {
|
|
62
53
|
const platformSupport = resolvePlatformSupport(process.platform);
|
|
@@ -1,55 +1,87 @@
|
|
|
1
1
|
import type { InitStep } from "../types/index.js";
|
|
2
2
|
type StepCallback = (step: InitStep) => void;
|
|
3
|
+
export declare const PLUGIN_COMMAND_STATUS: {
|
|
4
|
+
readonly SUCCESS: "success";
|
|
5
|
+
readonly FAILURE: "failure";
|
|
6
|
+
readonly REFUSED: "refused";
|
|
7
|
+
};
|
|
8
|
+
export type PluginCommandStatus = (typeof PLUGIN_COMMAND_STATUS)[keyof typeof PLUGIN_COMMAND_STATUS];
|
|
9
|
+
export declare const PLUGIN_COMMAND_ACTION: {
|
|
10
|
+
readonly ADD: "add";
|
|
11
|
+
readonly REMOVE: "remove";
|
|
12
|
+
readonly LIST: "list";
|
|
13
|
+
readonly SEARCH: "search";
|
|
14
|
+
readonly VALIDATE: "validate";
|
|
15
|
+
readonly SYNC: "sync";
|
|
16
|
+
readonly EXPORT: "export";
|
|
17
|
+
readonly IMPORT: "import";
|
|
18
|
+
readonly EXPORT_SKILLS: "export-skills";
|
|
19
|
+
};
|
|
20
|
+
export interface PluginCommandResult {
|
|
21
|
+
status: PluginCommandStatus;
|
|
22
|
+
}
|
|
23
|
+
export interface PluginCommandRequest {
|
|
24
|
+
action?: string;
|
|
25
|
+
target?: string;
|
|
26
|
+
projectDir: string;
|
|
27
|
+
dryRun: boolean;
|
|
28
|
+
codex?: boolean;
|
|
29
|
+
/** Force only unscannable sources; never override a guard block. */
|
|
30
|
+
force?: boolean;
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
}
|
|
3
33
|
/**
|
|
4
34
|
* Add (install) a plugin from a GitHub source.
|
|
5
35
|
*/
|
|
6
36
|
export declare function runPluginAdd(source: string, dryRun: boolean, onStep: StepCallback, options?: {
|
|
7
37
|
force?: boolean;
|
|
8
|
-
}): Promise<
|
|
38
|
+
}): Promise<PluginCommandResult>;
|
|
9
39
|
/**
|
|
10
40
|
* Remove an installed plugin by name.
|
|
11
41
|
*/
|
|
12
|
-
export declare function runPluginRemove(name: string, dryRun: boolean, onStep: StepCallback): Promise<
|
|
42
|
+
export declare function runPluginRemove(name: string, dryRun: boolean, onStep: StepCallback): Promise<PluginCommandResult>;
|
|
13
43
|
/**
|
|
14
44
|
* List all installed plugins.
|
|
15
45
|
*/
|
|
16
|
-
export declare function runPluginList(onStep: StepCallback): Promise<
|
|
46
|
+
export declare function runPluginList(onStep: StepCallback): Promise<PluginCommandResult>;
|
|
17
47
|
/**
|
|
18
48
|
* Search the remote plugin registry.
|
|
19
49
|
*/
|
|
20
50
|
export declare function runPluginSearch(query: string | undefined, onStep: StepCallback, options?: {
|
|
21
51
|
signal?: AbortSignal;
|
|
22
|
-
}): Promise<
|
|
52
|
+
}): Promise<PluginCommandResult>;
|
|
23
53
|
/**
|
|
24
54
|
* Validate a local plugin directory.
|
|
25
55
|
*/
|
|
26
|
-
export declare function runPluginValidate(pluginDir: string, onStep: StepCallback): Promise<
|
|
56
|
+
export declare function runPluginValidate(pluginDir: string, onStep: StepCallback): Promise<PluginCommandResult>;
|
|
27
57
|
/**
|
|
28
58
|
* Sync detected plugins into the project manifest.
|
|
29
59
|
*/
|
|
30
|
-
export declare function runPluginSync(projectDir: string, dryRun: boolean, onStep: StepCallback): Promise<
|
|
60
|
+
export declare function runPluginSync(projectDir: string, dryRun: boolean, onStep: StepCallback): Promise<PluginCommandResult>;
|
|
31
61
|
/**
|
|
32
62
|
* Export an installed plugin to Agent Skills spec format.
|
|
33
63
|
*/
|
|
34
|
-
export declare function runPluginExport(name: string, onStep: StepCallback): Promise<
|
|
64
|
+
export declare function runPluginExport(name: string, onStep: StepCallback): Promise<PluginCommandResult>;
|
|
35
65
|
/**
|
|
36
66
|
* Export an installed plugin to Codex-compatible TOML subagent files.
|
|
37
67
|
*/
|
|
38
|
-
export declare function runPluginExportCodex(name: string, onStep: StepCallback): Promise<
|
|
68
|
+
export declare function runPluginExportCodex(name: string, onStep: StepCallback): Promise<PluginCommandResult>;
|
|
39
69
|
/**
|
|
40
70
|
* Import an Agent Skills spec package and convert to javi-forge plugin format.
|
|
41
71
|
*/
|
|
42
72
|
export declare function runPluginImport(sourceDir: string, dryRun: boolean, onStep: StepCallback, options?: {
|
|
43
73
|
force?: boolean;
|
|
44
|
-
}): Promise<
|
|
74
|
+
}): Promise<PluginCommandResult>;
|
|
45
75
|
/**
|
|
46
76
|
* Generate a project-level skills.json from all installed plugins.
|
|
47
77
|
* Makes the project discoverable by `npx skills add` and 40+ AI agents.
|
|
48
78
|
*/
|
|
49
|
-
export declare function runPluginExportSkillsJson(projectDir: string, dryRun: boolean, onStep: StepCallback): Promise<
|
|
79
|
+
export declare function runPluginExportSkillsJson(projectDir: string, dryRun: boolean, onStep: StepCallback): Promise<PluginCommandResult>;
|
|
50
80
|
/**
|
|
51
81
|
* Generate a global skills.json from all globally installed plugins.
|
|
52
82
|
*/
|
|
53
|
-
export declare function runPluginExportGlobalSkillsJson(dryRun: boolean, onStep: StepCallback): Promise<
|
|
83
|
+
export declare function runPluginExportGlobalSkillsJson(dryRun: boolean, onStep: StepCallback): Promise<PluginCommandResult>;
|
|
84
|
+
/** Execute one plugin request; the CLI dispatcher owns process exit status. */
|
|
85
|
+
export declare function runPluginCommand(request: PluginCommandRequest, onStep: StepCallback): Promise<PluginCommandResult>;
|
|
54
86
|
export {};
|
|
55
87
|
//# sourceMappingURL=plugin.d.ts.map
|
package/dist/commands/plugin.js
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { exportPluginAsAgentSkills, generateGlobalSkillsJson, generateProjectSkillsJson, importAgentSkillsPackage, } from "../lib/agent-skills.js";
|
|
2
2
|
import { exportPluginAsCodexToml } from "../lib/codex-export.js";
|
|
3
3
|
import { installPlugin, listInstalledPlugins, removePlugin, searchRegistry, syncPlugins, validatePlugin, } from "../lib/plugin.js";
|
|
4
|
+
export const PLUGIN_COMMAND_STATUS = {
|
|
5
|
+
SUCCESS: "success",
|
|
6
|
+
FAILURE: "failure",
|
|
7
|
+
REFUSED: "refused",
|
|
8
|
+
};
|
|
9
|
+
export const PLUGIN_COMMAND_ACTION = {
|
|
10
|
+
ADD: "add",
|
|
11
|
+
REMOVE: "remove",
|
|
12
|
+
LIST: "list",
|
|
13
|
+
SEARCH: "search",
|
|
14
|
+
VALIDATE: "validate",
|
|
15
|
+
SYNC: "sync",
|
|
16
|
+
EXPORT: "export",
|
|
17
|
+
IMPORT: "import",
|
|
18
|
+
EXPORT_SKILLS: "export-skills",
|
|
19
|
+
};
|
|
4
20
|
function report(onStep, id, label, status, detail) {
|
|
5
21
|
onStep({ id, label, status, detail });
|
|
6
22
|
}
|
|
@@ -32,13 +48,14 @@ export async function runPluginAdd(source, dryRun, onStep, options = {}) {
|
|
|
32
48
|
}
|
|
33
49
|
else {
|
|
34
50
|
report(onStep, stepId, `Install plugin: ${source}`, "error", result.error);
|
|
35
|
-
// FU-1 (R4-002): a skillguard refusal must be distinguishable from
|
|
36
|
-
// success by scripted consumers — exit non-zero. `process.exitCode`
|
|
37
|
-
// (not `process.exit`) so the Ink tree keeps rendering/unmounting
|
|
38
|
-
// normally. Non-gate failures (validation, clone errors) keep exit 0.
|
|
39
|
-
if (result.refused)
|
|
40
|
-
process.exitCode = 1;
|
|
41
51
|
}
|
|
52
|
+
return {
|
|
53
|
+
status: result.success
|
|
54
|
+
? PLUGIN_COMMAND_STATUS.SUCCESS
|
|
55
|
+
: result.refused
|
|
56
|
+
? PLUGIN_COMMAND_STATUS.REFUSED
|
|
57
|
+
: PLUGIN_COMMAND_STATUS.FAILURE,
|
|
58
|
+
};
|
|
42
59
|
}
|
|
43
60
|
/**
|
|
44
61
|
* Remove an installed plugin by name.
|
|
@@ -53,6 +70,11 @@ export async function runPluginRemove(name, dryRun, onStep) {
|
|
|
53
70
|
else {
|
|
54
71
|
report(onStep, stepId, `Remove plugin: ${name}`, "error", result.error);
|
|
55
72
|
}
|
|
73
|
+
return {
|
|
74
|
+
status: result.success
|
|
75
|
+
? PLUGIN_COMMAND_STATUS.SUCCESS
|
|
76
|
+
: PLUGIN_COMMAND_STATUS.FAILURE,
|
|
77
|
+
};
|
|
56
78
|
}
|
|
57
79
|
/**
|
|
58
80
|
* List all installed plugins.
|
|
@@ -68,6 +90,7 @@ export async function runPluginList(onStep) {
|
|
|
68
90
|
const summary = plugins.map((p) => `${p.name}@${p.version}`).join(", ");
|
|
69
91
|
report(onStep, stepId, "List installed plugins", "done", `${plugins.length} plugins: ${summary}`);
|
|
70
92
|
}
|
|
93
|
+
return { status: PLUGIN_COMMAND_STATUS.SUCCESS };
|
|
71
94
|
}
|
|
72
95
|
/**
|
|
73
96
|
* Search the remote plugin registry.
|
|
@@ -78,9 +101,11 @@ export async function runPluginSearch(query, onStep, options = {}) {
|
|
|
78
101
|
const results = await searchRegistry(query, options);
|
|
79
102
|
if (results.status === "cancelled") {
|
|
80
103
|
report(onStep, stepId, `Search plugins${query ? `: ${query}` : ""}`, "error", "registry search cancelled");
|
|
104
|
+
return { status: PLUGIN_COMMAND_STATUS.FAILURE };
|
|
81
105
|
}
|
|
82
106
|
else if (results.status === "unavailable") {
|
|
83
107
|
report(onStep, stepId, `Search plugins${query ? `: ${query}` : ""}`, "error", "registry unavailable");
|
|
108
|
+
return { status: PLUGIN_COMMAND_STATUS.FAILURE };
|
|
84
109
|
}
|
|
85
110
|
else if (results.entries.length === 0) {
|
|
86
111
|
report(onStep, stepId, `Search plugins${query ? `: ${query}` : ""}`, "done", query ? `no plugins matching "${query}"` : "registry empty");
|
|
@@ -91,6 +116,7 @@ export async function runPluginSearch(query, onStep, options = {}) {
|
|
|
91
116
|
.join("\n ");
|
|
92
117
|
report(onStep, stepId, `Search plugins${query ? `: ${query}` : ""}`, "done", `${results.entries.length} results:\n ${summary}`);
|
|
93
118
|
}
|
|
119
|
+
return { status: PLUGIN_COMMAND_STATUS.SUCCESS };
|
|
94
120
|
}
|
|
95
121
|
/**
|
|
96
122
|
* Validate a local plugin directory.
|
|
@@ -108,6 +134,11 @@ export async function runPluginValidate(pluginDir, onStep) {
|
|
|
108
134
|
.join("\n");
|
|
109
135
|
report(onStep, stepId, `Validate plugin: ${pluginDir}`, "error", `${result.errors.length} errors:\n${msgs}`);
|
|
110
136
|
}
|
|
137
|
+
return {
|
|
138
|
+
status: result.valid
|
|
139
|
+
? PLUGIN_COMMAND_STATUS.SUCCESS
|
|
140
|
+
: PLUGIN_COMMAND_STATUS.FAILURE,
|
|
141
|
+
};
|
|
111
142
|
}
|
|
112
143
|
/**
|
|
113
144
|
* Sync detected plugins into the project manifest.
|
|
@@ -132,10 +163,12 @@ export async function runPluginSync(projectDir, dryRun, onStep) {
|
|
|
132
163
|
parts.push("no plugins detected");
|
|
133
164
|
const prefix = dryRun ? "dry-run: " : "";
|
|
134
165
|
report(onStep, stepId, "Sync plugins", "done", `${prefix}${parts.join(" | ")}`);
|
|
166
|
+
return { status: PLUGIN_COMMAND_STATUS.SUCCESS };
|
|
135
167
|
}
|
|
136
168
|
catch (e) {
|
|
137
169
|
const msg = e instanceof Error ? e.message : String(e);
|
|
138
170
|
report(onStep, stepId, "Sync plugins", "error", msg);
|
|
171
|
+
return { status: PLUGIN_COMMAND_STATUS.FAILURE };
|
|
139
172
|
}
|
|
140
173
|
}
|
|
141
174
|
/**
|
|
@@ -151,6 +184,11 @@ export async function runPluginExport(name, onStep) {
|
|
|
151
184
|
else {
|
|
152
185
|
report(onStep, stepId, `Export plugin: ${name}`, "error", result.error);
|
|
153
186
|
}
|
|
187
|
+
return {
|
|
188
|
+
status: result.success
|
|
189
|
+
? PLUGIN_COMMAND_STATUS.SUCCESS
|
|
190
|
+
: PLUGIN_COMMAND_STATUS.FAILURE,
|
|
191
|
+
};
|
|
154
192
|
}
|
|
155
193
|
/**
|
|
156
194
|
* Export an installed plugin to Codex-compatible TOML subagent files.
|
|
@@ -165,6 +203,11 @@ export async function runPluginExportCodex(name, onStep) {
|
|
|
165
203
|
else {
|
|
166
204
|
report(onStep, stepId, `Export plugin as Codex TOML: ${name}`, "error", result.error);
|
|
167
205
|
}
|
|
206
|
+
return {
|
|
207
|
+
status: result.success
|
|
208
|
+
? PLUGIN_COMMAND_STATUS.SUCCESS
|
|
209
|
+
: PLUGIN_COMMAND_STATUS.FAILURE,
|
|
210
|
+
};
|
|
168
211
|
}
|
|
169
212
|
/**
|
|
170
213
|
* Import an Agent Skills spec package and convert to javi-forge plugin format.
|
|
@@ -181,12 +224,14 @@ export async function runPluginImport(sourceDir, dryRun, onStep, options = {}) {
|
|
|
181
224
|
}
|
|
182
225
|
else {
|
|
183
226
|
report(onStep, stepId, `Import agent-skills package: ${sourceDir}`, "error", result.error);
|
|
184
|
-
// FU-1 (R4-002): same exit-code contract as runPluginAdd — a skillguard
|
|
185
|
-
// refusal (manifest-integrity or verdict) exits non-zero; plain input
|
|
186
|
-
// errors (skills.json missing/invalid) keep exit 0.
|
|
187
|
-
if (result.refused)
|
|
188
|
-
process.exitCode = 1;
|
|
189
227
|
}
|
|
228
|
+
return {
|
|
229
|
+
status: result.success
|
|
230
|
+
? PLUGIN_COMMAND_STATUS.SUCCESS
|
|
231
|
+
: result.refused
|
|
232
|
+
? PLUGIN_COMMAND_STATUS.REFUSED
|
|
233
|
+
: PLUGIN_COMMAND_STATUS.FAILURE,
|
|
234
|
+
};
|
|
190
235
|
}
|
|
191
236
|
/**
|
|
192
237
|
* Generate a project-level skills.json from all installed plugins.
|
|
@@ -203,6 +248,11 @@ export async function runPluginExportSkillsJson(projectDir, dryRun, onStep) {
|
|
|
203
248
|
else {
|
|
204
249
|
report(onStep, stepId, "Generate project skills.json", "error", result.error);
|
|
205
250
|
}
|
|
251
|
+
return {
|
|
252
|
+
status: result.success
|
|
253
|
+
? PLUGIN_COMMAND_STATUS.SUCCESS
|
|
254
|
+
: PLUGIN_COMMAND_STATUS.FAILURE,
|
|
255
|
+
};
|
|
206
256
|
}
|
|
207
257
|
/**
|
|
208
258
|
* Generate a global skills.json from all globally installed plugins.
|
|
@@ -218,5 +268,66 @@ export async function runPluginExportGlobalSkillsJson(dryRun, onStep) {
|
|
|
218
268
|
else {
|
|
219
269
|
report(onStep, stepId, "Generate global skills.json", "error", result.error);
|
|
220
270
|
}
|
|
271
|
+
return {
|
|
272
|
+
status: result.success
|
|
273
|
+
? PLUGIN_COMMAND_STATUS.SUCCESS
|
|
274
|
+
: PLUGIN_COMMAND_STATUS.FAILURE,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function requiredTargetLabel(action) {
|
|
278
|
+
if (action === PLUGIN_COMMAND_ACTION.ADD)
|
|
279
|
+
return "source";
|
|
280
|
+
if (action === PLUGIN_COMMAND_ACTION.REMOVE ||
|
|
281
|
+
action === PLUGIN_COMMAND_ACTION.EXPORT)
|
|
282
|
+
return "name";
|
|
283
|
+
if (action === PLUGIN_COMMAND_ACTION.VALIDATE ||
|
|
284
|
+
action === PLUGIN_COMMAND_ACTION.IMPORT)
|
|
285
|
+
return "path";
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
/** Execute one plugin request; the CLI dispatcher owns process exit status. */
|
|
289
|
+
export async function runPluginCommand(request, onStep) {
|
|
290
|
+
const { action = PLUGIN_COMMAND_ACTION.LIST, dryRun, projectDir, codex = false, force = false, } = request;
|
|
291
|
+
const target = request.target ?? "";
|
|
292
|
+
const requiredLabel = requiredTargetLabel(action);
|
|
293
|
+
if (requiredLabel && !target) {
|
|
294
|
+
report(onStep, "err", "Error", "error", `${requiredLabel} required: javi-forge plugin ${action} <${requiredLabel}>`);
|
|
295
|
+
return { status: PLUGIN_COMMAND_STATUS.FAILURE };
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
switch (action) {
|
|
299
|
+
case PLUGIN_COMMAND_ACTION.ADD:
|
|
300
|
+
return await runPluginAdd(target, dryRun, onStep, { force });
|
|
301
|
+
case PLUGIN_COMMAND_ACTION.REMOVE:
|
|
302
|
+
return await runPluginRemove(target, dryRun, onStep);
|
|
303
|
+
case PLUGIN_COMMAND_ACTION.LIST:
|
|
304
|
+
return await runPluginList(onStep);
|
|
305
|
+
case PLUGIN_COMMAND_ACTION.SEARCH:
|
|
306
|
+
return await runPluginSearch(request.target, onStep, {
|
|
307
|
+
signal: request.signal,
|
|
308
|
+
});
|
|
309
|
+
case PLUGIN_COMMAND_ACTION.VALIDATE:
|
|
310
|
+
return await runPluginValidate(target, onStep);
|
|
311
|
+
case PLUGIN_COMMAND_ACTION.SYNC:
|
|
312
|
+
return await runPluginSync(projectDir, dryRun, onStep);
|
|
313
|
+
case PLUGIN_COMMAND_ACTION.EXPORT:
|
|
314
|
+
return codex
|
|
315
|
+
? await runPluginExportCodex(target, onStep)
|
|
316
|
+
: await runPluginExport(target, onStep);
|
|
317
|
+
case PLUGIN_COMMAND_ACTION.IMPORT:
|
|
318
|
+
return await runPluginImport(target, dryRun, onStep, { force });
|
|
319
|
+
case PLUGIN_COMMAND_ACTION.EXPORT_SKILLS:
|
|
320
|
+
return target === "global"
|
|
321
|
+
? await runPluginExportGlobalSkillsJson(dryRun, onStep)
|
|
322
|
+
: await runPluginExportSkillsJson(request.target ?? projectDir, dryRun, onStep);
|
|
323
|
+
default:
|
|
324
|
+
report(onStep, "err", "Error", "error", `unknown plugin action: ${action}`);
|
|
325
|
+
return { status: PLUGIN_COMMAND_STATUS.FAILURE };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
report(onStep, "fatal", "Fatal error", "error", String(error));
|
|
330
|
+
return { status: PLUGIN_COMMAND_STATUS.FAILURE };
|
|
331
|
+
}
|
|
221
332
|
}
|
|
222
333
|
//# sourceMappingURL=plugin.js.map
|
package/dist/ui/Plugin.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import type { PluginCommandResult } from "../commands/plugin.js";
|
|
3
|
+
import type { InitStep } from "../types/index.js";
|
|
2
4
|
interface PluginProps {
|
|
3
|
-
action:
|
|
4
|
-
target?: string;
|
|
5
|
+
action: string;
|
|
5
6
|
dryRun: boolean;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
force?: boolean;
|
|
7
|
+
steps: readonly InitStep[];
|
|
8
|
+
result: PluginCommandResult | null;
|
|
9
9
|
}
|
|
10
|
-
export default function Plugin({ action,
|
|
10
|
+
export default function Plugin({ action, dryRun, steps, result }: PluginProps): React.JSX.Element;
|
|
11
11
|
export {};
|
|
12
12
|
//# sourceMappingURL=Plugin.d.ts.map
|
package/dist/ui/Plugin.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Box, Text } from "ink";
|
|
2
2
|
import Spinner from "ink-spinner";
|
|
3
|
-
import React
|
|
4
|
-
import { runPluginAdd, runPluginExport, runPluginExportCodex, runPluginExportGlobalSkillsJson, runPluginExportSkillsJson, runPluginImport, runPluginList, runPluginRemove, runPluginSearch, runPluginSync, runPluginValidate, } from "../commands/plugin.js";
|
|
3
|
+
import React from "react";
|
|
5
4
|
import { theme } from "./theme.js";
|
|
6
5
|
const STATUS_ICON = {
|
|
7
6
|
pending: "\u25cb",
|
|
@@ -16,150 +15,7 @@ const STATUS_COLOR = {
|
|
|
16
15
|
error: theme.error,
|
|
17
16
|
skipped: theme.muted,
|
|
18
17
|
};
|
|
19
|
-
export default function Plugin({ action,
|
|
20
|
-
const [steps, setSteps] = useState([]);
|
|
21
|
-
const [done, setDone] = useState(false);
|
|
22
|
-
const handleTerminalSignal = (signal, signalController) => {
|
|
23
|
-
if (signalController.signal.aborted)
|
|
24
|
-
return;
|
|
25
|
-
signalController.abort();
|
|
26
|
-
if (signal === "SIGINT")
|
|
27
|
-
process.exitCode = 130;
|
|
28
|
-
if (signal === "SIGTERM")
|
|
29
|
-
process.exitCode = 143;
|
|
30
|
-
};
|
|
31
|
-
const onStep = (step) => {
|
|
32
|
-
setSteps((prev) => {
|
|
33
|
-
const idx = prev.findIndex((s) => s.id === step.id);
|
|
34
|
-
if (idx >= 0) {
|
|
35
|
-
const next = [...prev];
|
|
36
|
-
next[idx] = step;
|
|
37
|
-
return next;
|
|
38
|
-
}
|
|
39
|
-
return [...prev, step];
|
|
40
|
-
});
|
|
41
|
-
};
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
const controller = action === "search" ? new AbortController() : undefined;
|
|
44
|
-
const sigintHandler = controller === undefined
|
|
45
|
-
? undefined
|
|
46
|
-
: () => handleTerminalSignal("SIGINT", controller);
|
|
47
|
-
const sigtermHandler = controller === undefined
|
|
48
|
-
? undefined
|
|
49
|
-
: () => handleTerminalSignal("SIGTERM", controller);
|
|
50
|
-
if (action === "search" && sigintHandler && sigtermHandler) {
|
|
51
|
-
process.on("SIGINT", sigintHandler);
|
|
52
|
-
process.on("SIGTERM", sigtermHandler);
|
|
53
|
-
}
|
|
54
|
-
const run = async () => {
|
|
55
|
-
try {
|
|
56
|
-
switch (action) {
|
|
57
|
-
case "add":
|
|
58
|
-
if (!target) {
|
|
59
|
-
onStep({
|
|
60
|
-
id: "err",
|
|
61
|
-
label: "Error",
|
|
62
|
-
status: "error",
|
|
63
|
-
detail: "source required: javi-forge plugin add <org/repo>",
|
|
64
|
-
});
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
await runPluginAdd(target, dryRun, onStep, { force });
|
|
68
|
-
break;
|
|
69
|
-
case "remove":
|
|
70
|
-
if (!target) {
|
|
71
|
-
onStep({
|
|
72
|
-
id: "err",
|
|
73
|
-
label: "Error",
|
|
74
|
-
status: "error",
|
|
75
|
-
detail: "name required: javi-forge plugin remove <name>",
|
|
76
|
-
});
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
await runPluginRemove(target, dryRun, onStep);
|
|
80
|
-
break;
|
|
81
|
-
case "list":
|
|
82
|
-
await runPluginList(onStep);
|
|
83
|
-
break;
|
|
84
|
-
case "search":
|
|
85
|
-
await runPluginSearch(target, onStep, {
|
|
86
|
-
signal: controller?.signal,
|
|
87
|
-
});
|
|
88
|
-
break;
|
|
89
|
-
case "validate":
|
|
90
|
-
if (!target) {
|
|
91
|
-
onStep({
|
|
92
|
-
id: "err",
|
|
93
|
-
label: "Error",
|
|
94
|
-
status: "error",
|
|
95
|
-
detail: "path required: javi-forge plugin validate <dir>",
|
|
96
|
-
});
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
await runPluginValidate(target, onStep);
|
|
100
|
-
break;
|
|
101
|
-
case "sync":
|
|
102
|
-
await runPluginSync(process.cwd(), dryRun, onStep);
|
|
103
|
-
break;
|
|
104
|
-
case "export":
|
|
105
|
-
if (!target) {
|
|
106
|
-
onStep({
|
|
107
|
-
id: "err",
|
|
108
|
-
label: "Error",
|
|
109
|
-
status: "error",
|
|
110
|
-
detail: "name required: javi-forge plugin export <name>",
|
|
111
|
-
});
|
|
112
|
-
break;
|
|
113
|
-
}
|
|
114
|
-
if (codex) {
|
|
115
|
-
await runPluginExportCodex(target, onStep);
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
await runPluginExport(target, onStep);
|
|
119
|
-
}
|
|
120
|
-
break;
|
|
121
|
-
case "import":
|
|
122
|
-
if (!target) {
|
|
123
|
-
onStep({
|
|
124
|
-
id: "err",
|
|
125
|
-
label: "Error",
|
|
126
|
-
status: "error",
|
|
127
|
-
detail: "path required: javi-forge plugin import <dir>",
|
|
128
|
-
});
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
await runPluginImport(target, dryRun, onStep, { force });
|
|
132
|
-
break;
|
|
133
|
-
case "export-skills":
|
|
134
|
-
if (target === "global") {
|
|
135
|
-
await runPluginExportGlobalSkillsJson(dryRun, onStep);
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
await runPluginExportSkillsJson(target ?? process.cwd(), dryRun, onStep);
|
|
139
|
-
}
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
catch (e) {
|
|
144
|
-
onStep({
|
|
145
|
-
id: "fatal",
|
|
146
|
-
label: "Fatal error",
|
|
147
|
-
status: "error",
|
|
148
|
-
detail: String(e),
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
setDone(true);
|
|
152
|
-
};
|
|
153
|
-
run();
|
|
154
|
-
return () => {
|
|
155
|
-
if (sigintHandler) {
|
|
156
|
-
process.removeListener("SIGINT", sigintHandler);
|
|
157
|
-
}
|
|
158
|
-
if (sigtermHandler) {
|
|
159
|
-
process.removeListener("SIGTERM", sigtermHandler);
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
}, [action, target, dryRun, force]);
|
|
18
|
+
export default function Plugin({ action, dryRun, steps, result }) {
|
|
163
19
|
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
164
20
|
React.createElement(Box, { marginBottom: 1 },
|
|
165
21
|
React.createElement(Text, { bold: true, color: theme.primary }, "javi-forge"),
|
|
@@ -180,7 +36,11 @@ export default function Plugin({ action, target, dryRun, codex = false, force =
|
|
|
180
36
|
step.detail ? (React.createElement(Text, { color: theme.muted, dimColor: true },
|
|
181
37
|
" ",
|
|
182
38
|
step.detail)) : null))))),
|
|
183
|
-
|
|
184
|
-
React.createElement(Text, { color: theme.
|
|
39
|
+
result && (React.createElement(Box, { marginTop: 1 },
|
|
40
|
+
React.createElement(Text, { color: result.status === "success" ? theme.success : theme.error }, result.status === "success"
|
|
41
|
+
? "Done."
|
|
42
|
+
: result.status === "refused"
|
|
43
|
+
? "Refused."
|
|
44
|
+
: "Failed.")))));
|
|
185
45
|
}
|
|
186
46
|
//# sourceMappingURL=Plugin.js.map
|