javi-forge 1.37.1 → 1.38.1
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/README.md +5 -5
- package/assets/claude-hooks/javi-forge-skillguard-pre-tool-use.mjs +13 -11
- package/assets/claude-hooks/manifest.json +35 -1
- package/ci-local/README.md +3 -6
- package/ci-local/ci-local.ps1 +36 -10
- package/ci-local/ci-local.sh +26 -9
- package/ci-local/install.ps1 +36 -14
- package/ci-local/install.sh +27 -10
- package/ci-local/lib/common.sh +3 -7
- package/ci-local/uninstall.ps1 +50 -0
- package/ci-local/uninstall.sh +38 -3
- package/dist/cli/bootstrap.d.ts +12 -0
- package/dist/cli/bootstrap.js +12 -0
- package/dist/cli/dispatch/simple-renderers.d.ts +8 -1
- package/dist/cli/dispatch/simple-renderers.js +11 -2
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +85 -0
- package/dist/commands/claude-hooks.d.ts +1 -0
- package/dist/commands/claude-hooks.js +12 -3
- package/dist/commands/codex-hooks.d.ts +1 -0
- package/dist/commands/codex-hooks.js +12 -3
- package/dist/commands/doctor.d.ts +16 -1
- package/dist/commands/doctor.js +54 -35
- package/dist/commands/init.d.ts +4 -1
- package/dist/commands/init.js +2 -2
- package/dist/index.js +4 -81
- package/dist/lib/claude-hook-manager.d.ts +11 -6
- package/dist/lib/claude-hook-manager.js +28 -21
- package/dist/lib/codex-hook-manager.d.ts +10 -4
- package/dist/lib/codex-hook-manager.js +44 -13
- package/dist/lib/platform-support.d.ts +18 -14
- package/dist/lib/platform-support.js +19 -17
- package/dist/lib/secure-fs-posix.d.ts +8 -13
- package/dist/lib/secure-fs-posix.js +11 -52
- package/dist/lib/secure-fs-transaction.d.ts +2 -3
- package/dist/lib/secure-fs-transaction.js +4 -2
- package/dist/types/index.d.ts +7 -2
- package/dist/ui/Doctor.d.ts +2 -0
- package/dist/ui/Doctor.js +13 -0
- package/lib/common.sh +3 -7
- package/package.json +10 -4
package/ci-local/uninstall.sh
CHANGED
|
@@ -13,10 +13,40 @@
|
|
|
13
13
|
# ./uninstall.sh --purge # also rm -rf ci-local/ + lib/common.sh
|
|
14
14
|
# =============================================================================
|
|
15
15
|
|
|
16
|
-
set -e
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
CI_LOCAL_ENTRYPOINT_SOURCE="${BASH_SOURCE[0]}"
|
|
18
|
+
if [[ "$CI_LOCAL_ENTRYPOINT_SOURCE" == /* ]]; then
|
|
19
|
+
CI_LOCAL_ENTRYPOINT_PATH="$CI_LOCAL_ENTRYPOINT_SOURCE"
|
|
20
|
+
else
|
|
21
|
+
CI_LOCAL_ENTRYPOINT_PATH="$PWD/$CI_LOCAL_ENTRYPOINT_SOURCE"
|
|
22
|
+
fi
|
|
23
|
+
CI_LOCAL_ENTRYPOINT_DIR="${CI_LOCAL_ENTRYPOINT_PATH%/*}"
|
|
24
|
+
|
|
25
|
+
function ci_local_normalize_platform() {
|
|
26
|
+
local platform="${1:?platform required}"
|
|
27
|
+
|
|
28
|
+
case "$platform" in
|
|
29
|
+
MINGW*_NT-*|MSYS*|CYGWIN*) printf '%s\n' 'Windows' ;;
|
|
30
|
+
*) printf '%s\n' "$platform" ;;
|
|
31
|
+
esac
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ci_local_main() {
|
|
35
|
+
local platform
|
|
36
|
+
platform="$(ci_local_normalize_platform "${1:?platform required}")"
|
|
37
|
+
shift
|
|
38
|
+
|
|
39
|
+
case "$platform" in
|
|
40
|
+
Linux|Windows) ci_local_startup_body "$@" ;;
|
|
41
|
+
*) printf '%s\n' 'unsupported-platform: javi-forge supports Linux and Windows only.'; return 1 ;;
|
|
42
|
+
esac
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function ci_local_startup_body() {
|
|
46
|
+
set -e
|
|
47
|
+
|
|
48
|
+
SCRIPT_DIR="$CI_LOCAL_ENTRYPOINT_DIR"
|
|
49
|
+
PROJECT_DIR="${SCRIPT_DIR%/*}"
|
|
20
50
|
|
|
21
51
|
# Source colors if available; degrade gracefully if not.
|
|
22
52
|
if [ -f "$SCRIPT_DIR/../lib/common.sh" ]; then
|
|
@@ -93,3 +123,8 @@ fi
|
|
|
93
123
|
echo -e ""
|
|
94
124
|
echo -e "${GREEN}Uninstall complete!${NC}"
|
|
95
125
|
echo -e ""
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
129
|
+
ci_local_main "$(/usr/bin/uname -s)" "$@"
|
|
130
|
+
fi
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type BootstrapResult = {
|
|
2
|
+
state: "supported";
|
|
3
|
+
exitCode: 0;
|
|
4
|
+
} | {
|
|
5
|
+
state: "unsupported-platform";
|
|
6
|
+
exitCode: 1;
|
|
7
|
+
};
|
|
8
|
+
export interface SupportedCli {
|
|
9
|
+
runCli: () => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export declare function bootstrapCli(platform: string, loadSupportedCli: () => Promise<SupportedCli>, refuse: (message: string) => void): Promise<BootstrapResult>;
|
|
12
|
+
//# sourceMappingURL=bootstrap.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { resolvePlatformSupport } from "../lib/platform-support.js";
|
|
2
|
+
export async function bootstrapCli(platform, loadSupportedCli, refuse) {
|
|
3
|
+
const refusal = resolvePlatformSupport(platform);
|
|
4
|
+
if (refusal) {
|
|
5
|
+
refuse(`${refusal.refusalCode}: ${refusal.guidance}`);
|
|
6
|
+
return { state: "unsupported-platform", exitCode: 1 };
|
|
7
|
+
}
|
|
8
|
+
const supportedCli = await loadSupportedCli();
|
|
9
|
+
await supportedCli.runCli();
|
|
10
|
+
return { state: "supported", exitCode: 0 };
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=bootstrap.js.map
|
|
@@ -7,8 +7,15 @@
|
|
|
7
7
|
* Ink + React are already eagerly imported at the entrypoint, so importing the
|
|
8
8
|
* concrete UI components at the top of this file does NOT worsen cold-start.
|
|
9
9
|
*/
|
|
10
|
+
import { render } from "ink";
|
|
10
11
|
import type { CLI, RendererCtx } from "./types.js";
|
|
11
|
-
export
|
|
12
|
+
export interface RendererDeps {
|
|
13
|
+
platform?: string;
|
|
14
|
+
render?: typeof render;
|
|
15
|
+
error?: (message: string) => void;
|
|
16
|
+
setExitCode?: (code: number) => void;
|
|
17
|
+
}
|
|
18
|
+
export declare function handleDoctor(_cli: CLI, ctx: RendererCtx, deps?: RendererDeps): void;
|
|
12
19
|
export declare function handleAnalyze(cli: CLI, ctx: RendererCtx): void;
|
|
13
20
|
export declare function handleLlmsTxt(cli: CLI, ctx: RendererCtx): void;
|
|
14
21
|
export declare function handlePlugin(cli: CLI, ctx: RendererCtx): void;
|
|
@@ -17,8 +17,17 @@ import Doctor from "../../ui/Doctor.js";
|
|
|
17
17
|
import LlmsTxt from "../../ui/LlmsTxt.js";
|
|
18
18
|
import Plugin from "../../ui/Plugin.js";
|
|
19
19
|
import { VALID_CI, VALID_MEMORY, VALID_STACKS } from "../validators.js";
|
|
20
|
-
export function handleDoctor(_cli, ctx) {
|
|
21
|
-
|
|
20
|
+
export function handleDoctor(_cli, ctx, deps = {}) {
|
|
21
|
+
const platformSupport = resolvePlatformSupport(deps.platform ?? process.platform);
|
|
22
|
+
if (platformSupport) {
|
|
23
|
+
(deps.error ?? console.error)(`${platformSupport.refusalCode}: ${platformSupport.guidance}`);
|
|
24
|
+
(deps.setExitCode ??
|
|
25
|
+
((code) => {
|
|
26
|
+
process.exitCode = code;
|
|
27
|
+
}))(1);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
(deps.render ?? render)(React.createElement(CIContextProvider, { isCI: ctx.isCI },
|
|
22
31
|
React.createElement(Doctor, null)), { stdin: ctx.inkStdin });
|
|
23
32
|
}
|
|
24
33
|
export function handleAnalyze(cli, ctx) {
|
package/dist/cli/main.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import meow from "meow";
|
|
3
|
+
import { handleCi } from "./dispatch/ci.js";
|
|
4
|
+
import { handleHooks } from "./dispatch/hooks.js";
|
|
5
|
+
import { handleSecurity } from "./dispatch/security.js";
|
|
6
|
+
import { handleAnalyze, handleDoctor, handleInitDefault, handleLlmsTxt, handlePlugin, } from "./dispatch/simple-renderers.js";
|
|
7
|
+
import { handleSkillPublish } from "./dispatch/skill-publish.js";
|
|
8
|
+
import { handleSkillsCmd } from "./dispatch/skills-cmd.js";
|
|
9
|
+
import { handleTdd } from "./dispatch/tdd.js";
|
|
10
|
+
import { handleWorkflow } from "./dispatch/workflow.js";
|
|
11
|
+
import { FLAGS_SCHEMA, HELP_TEXT } from "./help.js";
|
|
12
|
+
import { createInkStdin, detectCI, setupUpdateNotifier } from "./runtime.js";
|
|
13
|
+
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
|
+
const cli = meow(HELP_TEXT, {
|
|
19
|
+
importMeta: import.meta,
|
|
20
|
+
flags: FLAGS_SCHEMA,
|
|
21
|
+
// Handle --help manually so `ci --help` can show ci-specific usage instead
|
|
22
|
+
// of the global banner (meow's autoHelp would print + exit before dispatch).
|
|
23
|
+
autoHelp: false,
|
|
24
|
+
});
|
|
25
|
+
const subcommand = cli.input[0] ?? "init";
|
|
26
|
+
// Global --help: every command except `ci` and `hooks` shows the global banner
|
|
27
|
+
// here. Those two own their per-command help inside their handlers.
|
|
28
|
+
if (cli.flags.help && subcommand !== "ci" && subcommand !== "hooks") {
|
|
29
|
+
console.log(HELP_TEXT);
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
const isCI = detectCI(cli.flags);
|
|
33
|
+
const inkStdin = createInkStdin();
|
|
34
|
+
switch (subcommand) {
|
|
35
|
+
case "tdd": {
|
|
36
|
+
await handleTdd(cli);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
case "ci": {
|
|
40
|
+
await handleCi(cli, { inkStdin, isCI });
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
case "hooks": {
|
|
44
|
+
await handleHooks(cli);
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
case "doctor": {
|
|
48
|
+
handleDoctor(cli, { inkStdin, isCI });
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
case "analyze": {
|
|
52
|
+
handleAnalyze(cli, { inkStdin, isCI });
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
case "workflow": {
|
|
56
|
+
await handleWorkflow(cli);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
case "llms-txt": {
|
|
60
|
+
handleLlmsTxt(cli, { inkStdin, isCI });
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case "plugin": {
|
|
64
|
+
handlePlugin(cli, { inkStdin, isCI });
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
case "skills": {
|
|
68
|
+
await handleSkillsCmd(cli, { inkStdin, isCI });
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case "skill": {
|
|
72
|
+
await handleSkillPublish(cli);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case "security": {
|
|
76
|
+
await handleSecurity(cli);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
default: {
|
|
80
|
+
handleInitDefault(cli, { inkStdin, isCI });
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=main.js.map
|
|
@@ -17,6 +17,7 @@ import { doctorClaudePreToolUse, installClaudePreToolUse, repairClaudePreToolUse
|
|
|
17
17
|
export type ClaudeHookSub = "install" | "doctor" | "repair";
|
|
18
18
|
/** Injectable seams for tests; each defaults to the real implementation. */
|
|
19
19
|
export interface ClaudeHookCmdDeps {
|
|
20
|
+
platform?: string;
|
|
20
21
|
install?: typeof installClaudePreToolUse;
|
|
21
22
|
doctor?: typeof doctorClaudePreToolUse;
|
|
22
23
|
repair?: typeof repairClaudePreToolUse;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* current.
|
|
15
15
|
*/
|
|
16
16
|
import { doctorClaudePreToolUse, installClaudePreToolUse, repairClaudePreToolUse, } from "../lib/claude-hook-manager.js";
|
|
17
|
+
import { resolvePlatformSupport } from "../lib/platform-support.js";
|
|
17
18
|
import { remediationForMessage } from "../lib/secure-refusal-remediation.js";
|
|
18
19
|
/**
|
|
19
20
|
* Warnings are NON-BLOCKING notices, so they go to stdout under their own
|
|
@@ -57,10 +58,13 @@ function renderMutation(verb, result, log, logError) {
|
|
|
57
58
|
renderWarnings(result.warnings, log);
|
|
58
59
|
return 1;
|
|
59
60
|
}
|
|
60
|
-
function renderDoctor(
|
|
61
|
+
function renderDoctor(result, log) {
|
|
62
|
+
if ("state" in result) {
|
|
63
|
+
log(`${result.platformSupport.refusalCode}: ${result.platformSupport.guidance}`);
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
const report = result;
|
|
61
67
|
log(`doctor claude: ${report.healthy ? "healthy" : "unhealthy"}`);
|
|
62
|
-
if (report.platformSupport)
|
|
63
|
-
log(` platform-support: ${report.platformSupport.state} — ${report.platformSupport.guidance}`);
|
|
64
68
|
log(` settings: ${report.settings.state} — ${report.settings.detail}`);
|
|
65
69
|
log(` asset: ${report.asset.state} — ${report.asset.detail}`);
|
|
66
70
|
log(` node: ${report.node.version ?? "unavailable"} (min-satisfied: ${report.node.satisfiesMinimum})`);
|
|
@@ -124,6 +128,11 @@ function renderDoctor(report, log) {
|
|
|
124
128
|
export async function runClaudeHookCommand(sub, projectDir, opts, deps = {}) {
|
|
125
129
|
const log = deps.log ?? ((m) => console.log(m));
|
|
126
130
|
const logError = deps.logError ?? ((m) => console.error(m));
|
|
131
|
+
const platformSupport = resolvePlatformSupport(deps.platform ?? process.platform);
|
|
132
|
+
if (platformSupport) {
|
|
133
|
+
logError(`${platformSupport.refusalCode}: ${platformSupport.guidance}`);
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
127
136
|
const install = deps.install ?? installClaudePreToolUse;
|
|
128
137
|
const doctor = deps.doctor ?? doctorClaudePreToolUse;
|
|
129
138
|
const repair = deps.repair ?? repairClaudePreToolUse;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { doctorCodexPreToolUse, installCodexPreToolUse, repairCodexPreToolUse } from "../lib/codex-hook-manager.js";
|
|
14
14
|
export type CodexHookSub = "install" | "doctor" | "repair";
|
|
15
15
|
export interface CodexHookCmdDeps {
|
|
16
|
+
platform?: string;
|
|
16
17
|
install?: typeof installCodexPreToolUse;
|
|
17
18
|
doctor?: typeof doctorCodexPreToolUse;
|
|
18
19
|
repair?: typeof repairCodexPreToolUse;
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* install correctly reports blocked until the user grants trust.
|
|
12
12
|
*/
|
|
13
13
|
import { doctorCodexPreToolUse, installCodexPreToolUse, repairCodexPreToolUse, } from "../lib/codex-hook-manager.js";
|
|
14
|
+
import { resolvePlatformSupport } from "../lib/platform-support.js";
|
|
14
15
|
function renderWarnings(warnings, log) {
|
|
15
16
|
if (warnings.length === 0)
|
|
16
17
|
return;
|
|
@@ -42,10 +43,13 @@ function renderMutation(verb, result, log, logError) {
|
|
|
42
43
|
renderWarnings(result.warnings, log);
|
|
43
44
|
return 1;
|
|
44
45
|
}
|
|
45
|
-
function renderDoctor(
|
|
46
|
+
function renderDoctor(result, log) {
|
|
47
|
+
if ("state" in result) {
|
|
48
|
+
log(`${result.platformSupport.refusalCode}: ${result.platformSupport.guidance}`);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
const report = result;
|
|
46
52
|
log(`doctor codex: ${report.healthy ? "healthy" : "unhealthy"}`);
|
|
47
|
-
if (report.platformSupport)
|
|
48
|
-
log(` platform-support: ${report.platformSupport.state} — ${report.platformSupport.guidance}`);
|
|
49
53
|
log(` hooks.json: ${report.hooksJson.state}`);
|
|
50
54
|
log(` config: [features] hooks=${report.config.featuresHooks} (readable: ${report.config.readable})`);
|
|
51
55
|
log(` trust: ${report.trust.state}`);
|
|
@@ -88,6 +92,11 @@ function renderDoctor(report, log) {
|
|
|
88
92
|
export async function runCodexHookCommand(sub, _cwd, opts, deps = {}) {
|
|
89
93
|
const log = deps.log ?? ((m) => console.log(m));
|
|
90
94
|
const logError = deps.logError ?? ((m) => console.error(m));
|
|
95
|
+
const platformSupport = resolvePlatformSupport(deps.platform ?? process.platform);
|
|
96
|
+
if (platformSupport) {
|
|
97
|
+
logError(`${platformSupport.refusalCode}: ${platformSupport.guidance}`);
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
91
100
|
const install = deps.install ?? installCodexPreToolUse;
|
|
92
101
|
const doctor = deps.doctor ?? doctorCodexPreToolUse;
|
|
93
102
|
const repair = deps.repair ?? repairCodexPreToolUse;
|
|
@@ -1,5 +1,20 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import { detectStack } from "../lib/common.js";
|
|
3
|
+
import { refreshContextDir } from "../lib/context.js";
|
|
4
|
+
import { execFileAsync } from "../lib/exec.js";
|
|
5
|
+
import { listInstalledPlugins } from "../lib/plugin.js";
|
|
1
6
|
import type { DoctorResult } from "../types/index.js";
|
|
2
7
|
export type CheckStatus = "ok" | "fail" | "skip";
|
|
8
|
+
type DoctorFilesystem = Pick<typeof fs, "pathExists" | "readJson" | "readdir">;
|
|
9
|
+
export interface DoctorDeps {
|
|
10
|
+
platform?: string;
|
|
11
|
+
cwd?: () => string;
|
|
12
|
+
filesystem?: DoctorFilesystem;
|
|
13
|
+
exec?: typeof execFileAsync;
|
|
14
|
+
stackDetector?: typeof detectStack;
|
|
15
|
+
pluginLister?: typeof listInstalledPlugins;
|
|
16
|
+
contextRefresher?: typeof refreshContextDir;
|
|
17
|
+
}
|
|
3
18
|
interface RemoteInfo {
|
|
4
19
|
host: "github" | "gitlab" | "other";
|
|
5
20
|
owner: string;
|
|
@@ -10,6 +25,6 @@ export declare function parseRemote(url: string): RemoteInfo | null;
|
|
|
10
25
|
/**
|
|
11
26
|
* Run comprehensive health checks for the project and framework.
|
|
12
27
|
*/
|
|
13
|
-
export declare function runDoctor(projectDir?: string): Promise<DoctorResult>;
|
|
28
|
+
export declare function runDoctor(projectDir?: string, deps?: DoctorDeps): Promise<DoctorResult>;
|
|
14
29
|
export {};
|
|
15
30
|
//# sourceMappingURL=doctor.d.ts.map
|
package/dist/commands/doctor.js
CHANGED
|
@@ -4,40 +4,45 @@ import { FORGE_ROOT, MODULES_DIR, PLUGINS_DIR, TEMPLATES_DIR, } from "../constan
|
|
|
4
4
|
import { detectStack } from "../lib/common.js";
|
|
5
5
|
import { refreshContextDir } from "../lib/context.js";
|
|
6
6
|
import { execFileAsync } from "../lib/exec.js";
|
|
7
|
+
import { resolvePlatformSupport } from "../lib/platform-support.js";
|
|
7
8
|
import { listInstalledPlugins } from "../lib/plugin.js";
|
|
8
|
-
/** Resolve a binary name to its full path, returns null if not found */
|
|
9
|
-
async function which(bin) {
|
|
9
|
+
/** Resolve a binary name to its first full path, returns null if not found. */
|
|
10
|
+
async function which(bin, exec, platform) {
|
|
10
11
|
try {
|
|
11
|
-
const
|
|
12
|
-
|
|
12
|
+
const command = platform === "win32" ? "where.exe" : "which";
|
|
13
|
+
const { stdout } = await exec(command, [bin]);
|
|
14
|
+
return (stdout
|
|
15
|
+
.split(/\r?\n/)
|
|
16
|
+
.map((entry) => entry.trim())
|
|
17
|
+
.find(Boolean) ?? null);
|
|
13
18
|
}
|
|
14
19
|
catch {
|
|
15
20
|
return null;
|
|
16
21
|
}
|
|
17
22
|
}
|
|
18
23
|
/** Read the forge manifest from a project directory */
|
|
19
|
-
async function readManifest(projectDir) {
|
|
24
|
+
async function readManifest(projectDir, filesystem) {
|
|
20
25
|
const manifestPath = path.join(projectDir, ".javi-forge", "manifest.json");
|
|
21
|
-
if (!(await
|
|
26
|
+
if (!(await filesystem.pathExists(manifestPath)))
|
|
22
27
|
return null;
|
|
23
28
|
try {
|
|
24
|
-
return (await
|
|
29
|
+
return (await filesystem.readJson(manifestPath));
|
|
25
30
|
}
|
|
26
31
|
catch {
|
|
27
32
|
return null;
|
|
28
33
|
}
|
|
29
34
|
}
|
|
30
35
|
/** Count entries in a directory */
|
|
31
|
-
async function countDir(dir) {
|
|
32
|
-
if (!(await
|
|
36
|
+
async function countDir(dir, filesystem) {
|
|
37
|
+
if (!(await filesystem.pathExists(dir)))
|
|
33
38
|
return 0;
|
|
34
|
-
const entries = await
|
|
39
|
+
const entries = await filesystem.readdir(dir);
|
|
35
40
|
return entries.filter((e) => !e.startsWith(".")).length;
|
|
36
41
|
}
|
|
37
42
|
/** Read a single git config value, "" when unset or on any error. */
|
|
38
|
-
async function gitConfigValue(cwd, key) {
|
|
43
|
+
async function gitConfigValue(cwd, key, exec) {
|
|
39
44
|
try {
|
|
40
|
-
const { stdout } = await
|
|
45
|
+
const { stdout } = await exec("git", ["config", "--get", key], {
|
|
41
46
|
cwd,
|
|
42
47
|
});
|
|
43
48
|
return stdout.trim();
|
|
@@ -70,9 +75,9 @@ export function parseRemote(url) {
|
|
|
70
75
|
* a recommendation, never a gate (a hook enforcing it was trivially bypassed
|
|
71
76
|
* with `--no-verify`).
|
|
72
77
|
*/
|
|
73
|
-
async function commitSigningCheck(cwd) {
|
|
74
|
-
const gpgSign = await gitConfigValue(cwd, "commit.gpgsign");
|
|
75
|
-
const signingKey = await gitConfigValue(cwd, "user.signingkey");
|
|
78
|
+
async function commitSigningCheck(cwd, exec) {
|
|
79
|
+
const gpgSign = await gitConfigValue(cwd, "commit.gpgsign", exec);
|
|
80
|
+
const signingKey = await gitConfigValue(cwd, "user.signingkey", exec);
|
|
76
81
|
if (gpgSign === "true" && signingKey) {
|
|
77
82
|
return {
|
|
78
83
|
label: "Commit signing",
|
|
@@ -93,8 +98,8 @@ async function commitSigningCheck(cwd) {
|
|
|
93
98
|
* probe or protection is a non-alarming `skip` (advisory, not a failure).
|
|
94
99
|
* GitLab or no `gh` → `skip` with a note to verify in the forge UI.
|
|
95
100
|
*/
|
|
96
|
-
async function branchProtectionCheck(cwd) {
|
|
97
|
-
const remote = parseRemote(await gitConfigValue(cwd, "remote.origin.url"));
|
|
101
|
+
async function branchProtectionCheck(cwd, exec, platform) {
|
|
102
|
+
const remote = parseRemote(await gitConfigValue(cwd, "remote.origin.url", exec));
|
|
98
103
|
const label = "Branch protection";
|
|
99
104
|
if (!remote || remote.host === "other") {
|
|
100
105
|
return {
|
|
@@ -103,7 +108,7 @@ async function branchProtectionCheck(cwd) {
|
|
|
103
108
|
detail: "no GitHub/GitLab origin — verify branch protection in the forge UI",
|
|
104
109
|
};
|
|
105
110
|
}
|
|
106
|
-
if (remote.host === "gitlab" || !(await which("gh"))) {
|
|
111
|
+
if (remote.host === "gitlab" || !(await which("gh", exec, platform))) {
|
|
107
112
|
return {
|
|
108
113
|
label,
|
|
109
114
|
status: "skip",
|
|
@@ -113,14 +118,14 @@ async function branchProtectionCheck(cwd) {
|
|
|
113
118
|
// gh + GitHub: resolve the default branch, then probe protection.
|
|
114
119
|
let branch = "main";
|
|
115
120
|
try {
|
|
116
|
-
const { stdout } = await
|
|
121
|
+
const { stdout } = await exec("git", ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], { cwd });
|
|
117
122
|
branch = stdout.trim().replace(/^origin\//, "") || "main";
|
|
118
123
|
}
|
|
119
124
|
catch {
|
|
120
125
|
/* fall back to main */
|
|
121
126
|
}
|
|
122
127
|
try {
|
|
123
|
-
await
|
|
128
|
+
await exec("gh", [
|
|
124
129
|
"api",
|
|
125
130
|
`repos/${remote.owner}/${remote.repo}/branches/${branch}/protection`,
|
|
126
131
|
]);
|
|
@@ -141,8 +146,22 @@ async function branchProtectionCheck(cwd) {
|
|
|
141
146
|
/**
|
|
142
147
|
* Run comprehensive health checks for the project and framework.
|
|
143
148
|
*/
|
|
144
|
-
export async function runDoctor(projectDir) {
|
|
145
|
-
const
|
|
149
|
+
export async function runDoctor(projectDir, deps = {}) {
|
|
150
|
+
const platform = deps.platform ?? process.platform;
|
|
151
|
+
const platformSupport = resolvePlatformSupport(platform);
|
|
152
|
+
if (platformSupport) {
|
|
153
|
+
return {
|
|
154
|
+
state: "unsupported-platform",
|
|
155
|
+
guidance: platformSupport.guidance,
|
|
156
|
+
sections: [],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const cwd = projectDir ?? (deps.cwd ?? process.cwd)();
|
|
160
|
+
const filesystem = deps.filesystem ?? fs;
|
|
161
|
+
const exec = deps.exec ?? execFileAsync;
|
|
162
|
+
const stackDetector = deps.stackDetector ?? detectStack;
|
|
163
|
+
const pluginLister = deps.pluginLister ?? listInstalledPlugins;
|
|
164
|
+
const contextRefresher = deps.contextRefresher ?? refreshContextDir;
|
|
146
165
|
const sections = [];
|
|
147
166
|
// ── 1. System Tools ────────────────────────────────────────────────────────
|
|
148
167
|
const toolChecks = [];
|
|
@@ -154,12 +173,12 @@ export async function runDoctor(projectDir) {
|
|
|
154
173
|
{ name: "pnpm", label: "pnpm" },
|
|
155
174
|
];
|
|
156
175
|
for (const tool of tools) {
|
|
157
|
-
const bin = await which(tool.name);
|
|
176
|
+
const bin = await which(tool.name, exec, platform);
|
|
158
177
|
if (bin) {
|
|
159
178
|
// Try to get version
|
|
160
179
|
let version = "";
|
|
161
180
|
try {
|
|
162
|
-
const { stdout } = await
|
|
181
|
+
const { stdout } = await exec(tool.name, ["--version"]);
|
|
163
182
|
version = stdout.trim().split("\n")[0] ?? "";
|
|
164
183
|
}
|
|
165
184
|
catch {
|
|
@@ -184,8 +203,8 @@ export async function runDoctor(projectDir) {
|
|
|
184
203
|
// L4/L6 (commit signing) and L5 (branch protection) are advisories, NOT hooks
|
|
185
204
|
// (hook-consolidation D9): doctor reports, nothing blocks.
|
|
186
205
|
const securityChecks = [
|
|
187
|
-
await commitSigningCheck(cwd),
|
|
188
|
-
await branchProtectionCheck(cwd),
|
|
206
|
+
await commitSigningCheck(cwd, exec),
|
|
207
|
+
await branchProtectionCheck(cwd, exec, platform),
|
|
189
208
|
];
|
|
190
209
|
sections.push({ title: "Security", checks: securityChecks });
|
|
191
210
|
// ── 2. Framework Structure ─────────────────────────────────────────────────
|
|
@@ -197,8 +216,8 @@ export async function runDoctor(projectDir) {
|
|
|
197
216
|
{ path: path.join(FORGE_ROOT, "ci-local"), label: "ci-local/" },
|
|
198
217
|
];
|
|
199
218
|
for (const dir of expectedDirs) {
|
|
200
|
-
if (await
|
|
201
|
-
const count = await countDir(dir.path);
|
|
219
|
+
if (await filesystem.pathExists(dir.path)) {
|
|
220
|
+
const count = await countDir(dir.path, filesystem);
|
|
202
221
|
structureChecks.push({
|
|
203
222
|
label: dir.label,
|
|
204
223
|
status: "ok",
|
|
@@ -216,7 +235,7 @@ export async function runDoctor(projectDir) {
|
|
|
216
235
|
sections.push({ title: "Framework Structure", checks: structureChecks });
|
|
217
236
|
// ── 3. Stack Detection ─────────────────────────────────────────────────────
|
|
218
237
|
const stackChecks = [];
|
|
219
|
-
const detection = await
|
|
238
|
+
const detection = await stackDetector(cwd);
|
|
220
239
|
if (detection) {
|
|
221
240
|
stackChecks.push({
|
|
222
241
|
label: "Detected stack",
|
|
@@ -234,7 +253,7 @@ export async function runDoctor(projectDir) {
|
|
|
234
253
|
sections.push({ title: "Stack Detection", checks: stackChecks });
|
|
235
254
|
// ── 4. Project Manifest ────────────────────────────────────────────────────
|
|
236
255
|
const manifestChecks = [];
|
|
237
|
-
const manifest = await readManifest(cwd);
|
|
256
|
+
const manifest = await readManifest(cwd, filesystem);
|
|
238
257
|
if (manifest) {
|
|
239
258
|
manifestChecks.push({
|
|
240
259
|
label: "Forge manifest",
|
|
@@ -267,7 +286,7 @@ export async function runDoctor(projectDir) {
|
|
|
267
286
|
const moduleNames = ["engram", "obsidian-brain", "memory-simple", "ghagga"];
|
|
268
287
|
for (const mod of moduleNames) {
|
|
269
288
|
const modPath = path.join(cwd, ".javi-forge", "modules", mod);
|
|
270
|
-
if (await
|
|
289
|
+
if (await filesystem.pathExists(modPath)) {
|
|
271
290
|
moduleChecks.push({ label: mod, status: "ok", detail: "installed" });
|
|
272
291
|
}
|
|
273
292
|
else {
|
|
@@ -281,9 +300,9 @@ export async function runDoctor(projectDir) {
|
|
|
281
300
|
sections.push({ title: "Installed Modules", checks: moduleChecks });
|
|
282
301
|
// ── 6. Plugins ─────────────────────────────────────────────────────────────
|
|
283
302
|
const pluginChecks = [];
|
|
284
|
-
const pluginsDirExists = await
|
|
303
|
+
const pluginsDirExists = await filesystem.pathExists(PLUGINS_DIR);
|
|
285
304
|
if (pluginsDirExists) {
|
|
286
|
-
const plugins = await
|
|
305
|
+
const plugins = await pluginLister();
|
|
287
306
|
if (plugins.length > 0) {
|
|
288
307
|
for (const plugin of plugins) {
|
|
289
308
|
pluginChecks.push({
|
|
@@ -312,7 +331,7 @@ export async function runDoctor(projectDir) {
|
|
|
312
331
|
// ── 7. Context Directory Refresh ───────────────────────────────────────────
|
|
313
332
|
const contextChecks = [];
|
|
314
333
|
try {
|
|
315
|
-
const result = await
|
|
334
|
+
const result = await contextRefresher(cwd);
|
|
316
335
|
if (result) {
|
|
317
336
|
contextChecks.push({
|
|
318
337
|
label: ".context/ refresh",
|
|
@@ -345,6 +364,6 @@ export async function runDoctor(projectDir) {
|
|
|
345
364
|
});
|
|
346
365
|
}
|
|
347
366
|
sections.push({ title: "Context Directory", checks: contextChecks });
|
|
348
|
-
return { sections };
|
|
367
|
+
return { state: "supported", sections };
|
|
349
368
|
}
|
|
350
369
|
//# sourceMappingURL=doctor.js.map
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -14,5 +14,8 @@ export type InitProjectResult = {
|
|
|
14
14
|
refusalCode: string;
|
|
15
15
|
platformSupport: PlatformSupport;
|
|
16
16
|
};
|
|
17
|
-
export
|
|
17
|
+
export interface InitProjectDeps {
|
|
18
|
+
platform?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function initProject(options: InitOptions, onStep: StepCallback, deps?: InitProjectDeps): Promise<InitProjectResult>;
|
|
18
21
|
//# sourceMappingURL=init.d.ts.map
|
package/dist/commands/init.js
CHANGED
|
@@ -16,8 +16,8 @@ import { stepMemory } from "./init/steps/memory.js";
|
|
|
16
16
|
import { stepMock } from "./init/steps/mock.js";
|
|
17
17
|
import { stepSDD } from "./init/steps/sdd.js";
|
|
18
18
|
import { stepSecurityHooks } from "./init/steps/security.js";
|
|
19
|
-
export async function initProject(options, onStep) {
|
|
20
|
-
const platformSupport = resolvePlatformSupport(process.platform);
|
|
19
|
+
export async function initProject(options, onStep, deps = {}) {
|
|
20
|
+
const platformSupport = resolvePlatformSupport(deps.platform ?? process.platform);
|
|
21
21
|
if (platformSupport) {
|
|
22
22
|
return {
|
|
23
23
|
ok: false,
|
package/dist/index.js
CHANGED
|
@@ -1,84 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { handleSecurity } from "./cli/dispatch/security.js";
|
|
7
|
-
import { handleAnalyze, handleDoctor, handleInitDefault, handleLlmsTxt, handlePlugin, } from "./cli/dispatch/simple-renderers.js";
|
|
8
|
-
import { handleSkillPublish } from "./cli/dispatch/skill-publish.js";
|
|
9
|
-
import { handleSkillsCmd } from "./cli/dispatch/skills-cmd.js";
|
|
10
|
-
import { handleTdd } from "./cli/dispatch/tdd.js";
|
|
11
|
-
import { handleWorkflow } from "./cli/dispatch/workflow.js";
|
|
12
|
-
import { FLAGS_SCHEMA, HELP_TEXT } from "./cli/help.js";
|
|
13
|
-
import { createInkStdin, detectCI, setupUpdateNotifier, } from "./cli/runtime.js";
|
|
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
|
-
const cli = meow(HELP_TEXT, {
|
|
19
|
-
importMeta: import.meta,
|
|
20
|
-
flags: FLAGS_SCHEMA,
|
|
21
|
-
// Handle --help manually so `ci --help` can show ci-specific usage instead
|
|
22
|
-
// of the global banner (meow's autoHelp would print + exit before dispatch).
|
|
23
|
-
autoHelp: false,
|
|
24
|
-
});
|
|
25
|
-
const subcommand = cli.input[0] ?? "init";
|
|
26
|
-
// Global --help: every command except `ci` and `hooks` shows the global banner
|
|
27
|
-
// here. Those two own their per-command help inside their handlers.
|
|
28
|
-
if (cli.flags.help && subcommand !== "ci" && subcommand !== "hooks") {
|
|
29
|
-
console.log(HELP_TEXT);
|
|
30
|
-
process.exit(0);
|
|
31
|
-
}
|
|
32
|
-
const isCI = detectCI(cli.flags);
|
|
33
|
-
const inkStdin = createInkStdin();
|
|
34
|
-
switch (subcommand) {
|
|
35
|
-
case "tdd": {
|
|
36
|
-
await handleTdd(cli);
|
|
37
|
-
break;
|
|
38
|
-
}
|
|
39
|
-
case "ci": {
|
|
40
|
-
await handleCi(cli, { inkStdin, isCI });
|
|
41
|
-
break;
|
|
42
|
-
}
|
|
43
|
-
case "hooks": {
|
|
44
|
-
await handleHooks(cli);
|
|
45
|
-
break;
|
|
46
|
-
}
|
|
47
|
-
case "doctor": {
|
|
48
|
-
handleDoctor(cli, { inkStdin, isCI });
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
|
-
case "analyze": {
|
|
52
|
-
handleAnalyze(cli, { inkStdin, isCI });
|
|
53
|
-
break;
|
|
54
|
-
}
|
|
55
|
-
case "workflow": {
|
|
56
|
-
await handleWorkflow(cli);
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
case "llms-txt": {
|
|
60
|
-
handleLlmsTxt(cli, { inkStdin, isCI });
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
case "plugin": {
|
|
64
|
-
handlePlugin(cli, { inkStdin, isCI });
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
case "skills": {
|
|
68
|
-
await handleSkillsCmd(cli, { inkStdin, isCI });
|
|
69
|
-
break;
|
|
70
|
-
}
|
|
71
|
-
case "skill": {
|
|
72
|
-
await handleSkillPublish(cli);
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
case "security": {
|
|
76
|
-
await handleSecurity(cli);
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
default: {
|
|
80
|
-
handleInitDefault(cli, { inkStdin, isCI });
|
|
81
|
-
break;
|
|
82
|
-
}
|
|
2
|
+
import { bootstrapCli } from "./cli/bootstrap.js";
|
|
3
|
+
const bootstrapResult = await bootstrapCli(process.platform, () => import("./cli/main.js"), (message) => console.error(message));
|
|
4
|
+
if (bootstrapResult.state === "unsupported-platform") {
|
|
5
|
+
process.exitCode = bootstrapResult.exitCode;
|
|
83
6
|
}
|
|
84
7
|
//# sourceMappingURL=index.js.map
|