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.
Files changed (41) hide show
  1. package/README.md +5 -5
  2. package/assets/claude-hooks/javi-forge-skillguard-pre-tool-use.mjs +13 -11
  3. package/assets/claude-hooks/manifest.json +35 -1
  4. package/ci-local/README.md +3 -6
  5. package/ci-local/ci-local.ps1 +36 -10
  6. package/ci-local/ci-local.sh +26 -9
  7. package/ci-local/install.ps1 +36 -14
  8. package/ci-local/install.sh +27 -10
  9. package/ci-local/lib/common.sh +3 -7
  10. package/ci-local/uninstall.ps1 +50 -0
  11. package/ci-local/uninstall.sh +38 -3
  12. package/dist/cli/bootstrap.d.ts +12 -0
  13. package/dist/cli/bootstrap.js +12 -0
  14. package/dist/cli/dispatch/simple-renderers.d.ts +8 -1
  15. package/dist/cli/dispatch/simple-renderers.js +11 -2
  16. package/dist/cli/main.d.ts +2 -0
  17. package/dist/cli/main.js +85 -0
  18. package/dist/commands/claude-hooks.d.ts +1 -0
  19. package/dist/commands/claude-hooks.js +12 -3
  20. package/dist/commands/codex-hooks.d.ts +1 -0
  21. package/dist/commands/codex-hooks.js +12 -3
  22. package/dist/commands/doctor.d.ts +16 -1
  23. package/dist/commands/doctor.js +54 -35
  24. package/dist/commands/init.d.ts +4 -1
  25. package/dist/commands/init.js +2 -2
  26. package/dist/index.js +4 -81
  27. package/dist/lib/claude-hook-manager.d.ts +11 -6
  28. package/dist/lib/claude-hook-manager.js +28 -21
  29. package/dist/lib/codex-hook-manager.d.ts +10 -4
  30. package/dist/lib/codex-hook-manager.js +44 -13
  31. package/dist/lib/platform-support.d.ts +18 -14
  32. package/dist/lib/platform-support.js +19 -17
  33. package/dist/lib/secure-fs-posix.d.ts +8 -13
  34. package/dist/lib/secure-fs-posix.js +11 -52
  35. package/dist/lib/secure-fs-transaction.d.ts +2 -3
  36. package/dist/lib/secure-fs-transaction.js +4 -2
  37. package/dist/types/index.d.ts +7 -2
  38. package/dist/ui/Doctor.d.ts +2 -0
  39. package/dist/ui/Doctor.js +13 -0
  40. package/lib/common.sh +3 -7
  41. package/package.json +10 -4
@@ -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
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
19
- PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
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 declare function handleDoctor(_cli: CLI, ctx: RendererCtx): void;
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
- render(React.createElement(CIContextProvider, { isCI: ctx.isCI },
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) {
@@ -0,0 +1,2 @@
1
+ export declare function runCli(): Promise<void>;
2
+ //# sourceMappingURL=main.d.ts.map
@@ -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(report, log) {
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(report, log) {
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
@@ -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 { stdout } = await execFileAsync("which", [bin]);
12
- return stdout.trim() || null;
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 fs.pathExists(manifestPath)))
26
+ if (!(await filesystem.pathExists(manifestPath)))
22
27
  return null;
23
28
  try {
24
- return (await fs.readJson(manifestPath));
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 fs.pathExists(dir)))
36
+ async function countDir(dir, filesystem) {
37
+ if (!(await filesystem.pathExists(dir)))
33
38
  return 0;
34
- const entries = await fs.readdir(dir);
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 execFileAsync("git", ["config", "--get", key], {
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 execFileAsync("git", ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], { cwd });
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 execFileAsync("gh", [
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 cwd = projectDir ?? process.cwd();
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 execFileAsync(tool.name, ["--version"]);
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 fs.pathExists(dir.path)) {
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 detectStack(cwd);
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 fs.pathExists(modPath)) {
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 fs.pathExists(PLUGINS_DIR);
303
+ const pluginsDirExists = await filesystem.pathExists(PLUGINS_DIR);
285
304
  if (pluginsDirExists) {
286
- const plugins = await listInstalledPlugins();
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 refreshContextDir(cwd);
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
@@ -14,5 +14,8 @@ export type InitProjectResult = {
14
14
  refusalCode: string;
15
15
  platformSupport: PlatformSupport;
16
16
  };
17
- export declare function initProject(options: InitOptions, onStep: StepCallback): Promise<InitProjectResult>;
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
@@ -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 { createRequire } from "node:module";
3
- import meow from "meow";
4
- import { handleCi } from "./cli/dispatch/ci.js";
5
- import { handleHooks } from "./cli/dispatch/hooks.js";
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