javi-forge 1.37.1 → 1.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/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 +7 -2
|
@@ -30,7 +30,6 @@ export interface ClaudeHookAssetClassification {
|
|
|
30
30
|
}
|
|
31
31
|
export interface ClaudeHookDoctorReport {
|
|
32
32
|
healthy: boolean;
|
|
33
|
-
platformSupport?: PlatformSupport;
|
|
34
33
|
settings: {
|
|
35
34
|
state: ClaudeHookComponentState;
|
|
36
35
|
version?: number;
|
|
@@ -73,6 +72,12 @@ export interface ClaudeHookDoctorReport {
|
|
|
73
72
|
*/
|
|
74
73
|
nodeOnPath: NodeOnPathProbe;
|
|
75
74
|
}
|
|
75
|
+
export interface ClaudeHookDoctorRefusal {
|
|
76
|
+
state: "unsupported-platform";
|
|
77
|
+
healthy: false;
|
|
78
|
+
platformSupport: PlatformSupport;
|
|
79
|
+
}
|
|
80
|
+
export type ClaudeHookDoctorResult = ClaudeHookDoctorReport | ClaudeHookDoctorRefusal;
|
|
76
81
|
/**
|
|
77
82
|
* Classify the asset into one of nine states from observed bytes only. Never
|
|
78
83
|
* trusts a claimed hash: the full-file SHA is always recomputed and compared to
|
|
@@ -158,8 +163,8 @@ export interface ManagedSettingsPaths {
|
|
|
158
163
|
file: string;
|
|
159
164
|
dropInDir: string;
|
|
160
165
|
}
|
|
161
|
-
/** Static managed-settings locations
|
|
162
|
-
export declare function resolveManagedSettingsPaths(platform: NodeJS.Platform): ManagedSettingsPaths;
|
|
166
|
+
/** Static managed-settings locations for supported hosts only (no fs). */
|
|
167
|
+
export declare function resolveManagedSettingsPaths(platform: NodeJS.Platform): ManagedSettingsPaths | null;
|
|
163
168
|
/**
|
|
164
169
|
* Probe one settings source path for the two documented hook-neutralizing
|
|
165
170
|
* flags. Fail-closed: only a genuinely-absent path or a cleanly-parsed source
|
|
@@ -210,7 +215,7 @@ export declare function doctorClaudePreToolUse(projectDir: string, options?: {
|
|
|
210
215
|
/** Injectable read-only ACL capability probe (defaults to the real one). */
|
|
211
216
|
aclProbe?: () => Promise<AclCapability>;
|
|
212
217
|
platform?: NodeJS.Platform;
|
|
213
|
-
}): Promise<
|
|
218
|
+
}): Promise<ClaudeHookDoctorResult>;
|
|
214
219
|
interface ClaudeHookMutationResultWithReport {
|
|
215
220
|
ok: boolean;
|
|
216
221
|
changed: string[];
|
|
@@ -257,9 +262,9 @@ export interface ClaudeHookRunDeps {
|
|
|
257
262
|
export declare function _run(projectDir: string, mode: "install" | "repair", options: {
|
|
258
263
|
force?: boolean;
|
|
259
264
|
}, deps: ClaudeHookRunDeps): Promise<ClaudeHookMutationResult>;
|
|
260
|
-
export declare function installClaudePreToolUse(projectDir: string): Promise<ClaudeHookMutationResult>;
|
|
265
|
+
export declare function installClaudePreToolUse(projectDir: string, deps?: ClaudeHookRunDeps): Promise<ClaudeHookMutationResult>;
|
|
261
266
|
export declare function repairClaudePreToolUse(projectDir: string, options?: {
|
|
262
267
|
force?: boolean;
|
|
263
|
-
}): Promise<ClaudeHookMutationResult>;
|
|
268
|
+
}, deps?: ClaudeHookRunDeps): Promise<ClaudeHookMutationResult>;
|
|
264
269
|
export {};
|
|
265
270
|
//# sourceMappingURL=claude-hook-manager.d.ts.map
|
|
@@ -258,15 +258,8 @@ export async function probeNodeOnPath(spawn = defaultNodeSpawn) {
|
|
|
258
258
|
}
|
|
259
259
|
return { status: "resolved", version: banner, major: Number(match[1]) };
|
|
260
260
|
}
|
|
261
|
-
/** Static managed-settings locations
|
|
261
|
+
/** Static managed-settings locations for supported hosts only (no fs). */
|
|
262
262
|
export function resolveManagedSettingsPaths(platform) {
|
|
263
|
-
if (platform === "darwin") {
|
|
264
|
-
const base = "/Library/Application Support/ClaudeCode";
|
|
265
|
-
return {
|
|
266
|
-
file: `${base}/managed-settings.json`,
|
|
267
|
-
dropInDir: `${base}/managed-settings.d`,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
263
|
if (platform === "win32") {
|
|
271
264
|
const base = "C:\\Program Files\\ClaudeCode";
|
|
272
265
|
return {
|
|
@@ -274,6 +267,8 @@ export function resolveManagedSettingsPaths(platform) {
|
|
|
274
267
|
dropInDir: `${base}\\managed-settings.d`,
|
|
275
268
|
};
|
|
276
269
|
}
|
|
270
|
+
if (platform !== "linux")
|
|
271
|
+
return null;
|
|
277
272
|
const base = "/etc/claude-code";
|
|
278
273
|
return {
|
|
279
274
|
file: `${base}/managed-settings.json`,
|
|
@@ -379,10 +374,10 @@ export async function probeExecution(projectDir, componentStates, env = {}) {
|
|
|
379
374
|
const homeDir = env.homeDir ?? os.homedir();
|
|
380
375
|
const processEnv = env.env ?? process.env;
|
|
381
376
|
const resolved = resolveManagedSettingsPaths(platform);
|
|
382
|
-
const managedFile = env.managedFile !== undefined ? env.managedFile : resolved
|
|
377
|
+
const managedFile = env.managedFile !== undefined ? env.managedFile : resolved?.file;
|
|
383
378
|
const dropInDir = env.managedDropInDir !== undefined
|
|
384
379
|
? env.managedDropInDir
|
|
385
|
-
: resolved
|
|
380
|
+
: resolved?.dropInDir;
|
|
386
381
|
const specs = [
|
|
387
382
|
{
|
|
388
383
|
label: "project",
|
|
@@ -485,6 +480,14 @@ export async function probeExecution(projectDir, componentStates, env = {}) {
|
|
|
485
480
|
* The `execution` verdict is INDEPENDENT of `healthy`.
|
|
486
481
|
*/
|
|
487
482
|
export async function doctorClaudePreToolUse(projectDir, options) {
|
|
483
|
+
const platformSupport = resolvePlatformSupport(options?.platform ?? process.platform);
|
|
484
|
+
if (platformSupport) {
|
|
485
|
+
return {
|
|
486
|
+
state: "unsupported-platform",
|
|
487
|
+
healthy: false,
|
|
488
|
+
platformSupport,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
488
491
|
const manifest = options?.manifest ?? (await readManifest());
|
|
489
492
|
const currentAssetSha = manifest.asset.sha256;
|
|
490
493
|
const asset = await classifyAssetState(path.join(projectDir, ".claude", "hooks", ASSET_NAME), manifest);
|
|
@@ -538,9 +541,7 @@ export async function doctorClaudePreToolUse(projectDir, options) {
|
|
|
538
541
|
execution.blockers.some((blocker) => blocker.startsWith("guard:"))) {
|
|
539
542
|
remediation.add(aclRemediation);
|
|
540
543
|
}
|
|
541
|
-
const platformSupport = resolvePlatformSupport(options?.platform ?? process.platform);
|
|
542
544
|
return {
|
|
543
|
-
...(platformSupport ? { platformSupport } : {}),
|
|
544
545
|
healthy,
|
|
545
546
|
settings: {
|
|
546
547
|
state: settings.state,
|
|
@@ -740,11 +741,17 @@ export async function _run(projectDir, mode, options, deps) {
|
|
|
740
741
|
// report, so the run never spawns `node --version` twice and the warning, the
|
|
741
742
|
// report row and the verdict can never disagree about the same PATH.
|
|
742
743
|
const nodeOnPath = await (deps.nodeProbe ?? probeNodeOnPath)();
|
|
743
|
-
const doctor = () =>
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
744
|
+
const doctor = async () => {
|
|
745
|
+
const result = await doctorFn(projectDir, {
|
|
746
|
+
manifest,
|
|
747
|
+
aclProbe: deps.aclProbe,
|
|
748
|
+
execution: { nodeProbe: async () => nodeOnPath },
|
|
749
|
+
});
|
|
750
|
+
if ("state" in result) {
|
|
751
|
+
throw new Error("supported Claude lifecycle received unsupported doctor result");
|
|
752
|
+
}
|
|
753
|
+
return result;
|
|
754
|
+
};
|
|
748
755
|
// Non-blocking runtime notice, computed once and carried by EVERY outcome
|
|
749
756
|
// (success, no-op and refusal alike) — it never gates the mutation.
|
|
750
757
|
const warnings = nodeOnPathWarnings(nodeOnPath);
|
|
@@ -832,10 +839,10 @@ export async function _run(projectDir, mode, options, deps) {
|
|
|
832
839
|
report: await doctor(),
|
|
833
840
|
};
|
|
834
841
|
}
|
|
835
|
-
export function installClaudePreToolUse(projectDir) {
|
|
836
|
-
return _run(projectDir, "install", {},
|
|
842
|
+
export function installClaudePreToolUse(projectDir, deps = {}) {
|
|
843
|
+
return _run(projectDir, "install", {}, deps);
|
|
837
844
|
}
|
|
838
|
-
export function repairClaudePreToolUse(projectDir, options) {
|
|
839
|
-
return _run(projectDir, "repair", options ?? {},
|
|
845
|
+
export function repairClaudePreToolUse(projectDir, options, deps = {}) {
|
|
846
|
+
return _run(projectDir, "repair", options ?? {}, deps);
|
|
840
847
|
}
|
|
841
848
|
//# sourceMappingURL=claude-hook-manager.js.map
|
|
@@ -102,7 +102,6 @@ export declare function classifyCodexHooksJson(value: unknown, expectedCommand:
|
|
|
102
102
|
export type CodexTrustState = "trusted" | "untrusted";
|
|
103
103
|
export interface CodexHookDoctorReport {
|
|
104
104
|
healthy: boolean;
|
|
105
|
-
platformSupport?: PlatformSupport;
|
|
106
105
|
hooksJson: {
|
|
107
106
|
state: ClaudeHookComponentState;
|
|
108
107
|
detail?: string;
|
|
@@ -128,6 +127,12 @@ export interface CodexHookDoctorReport {
|
|
|
128
127
|
};
|
|
129
128
|
remediation: string[];
|
|
130
129
|
}
|
|
130
|
+
export interface CodexHookDoctorRefusal {
|
|
131
|
+
state: "unsupported-platform";
|
|
132
|
+
healthy: false;
|
|
133
|
+
platformSupport: PlatformSupport;
|
|
134
|
+
}
|
|
135
|
+
export type CodexHookDoctorResult = CodexHookDoctorReport | CodexHookDoctorRefusal;
|
|
131
136
|
export interface CodexDoctorOptions {
|
|
132
137
|
platform?: NodeJS.Platform;
|
|
133
138
|
manifest?: Manifest;
|
|
@@ -135,7 +140,7 @@ export interface CodexDoctorOptions {
|
|
|
135
140
|
nodeVersion?: string;
|
|
136
141
|
nodeProbe?: () => Promise<NodeOnPathProbe>;
|
|
137
142
|
}
|
|
138
|
-
export declare function doctorCodexPreToolUse(homeDir?: string, options?: CodexDoctorOptions): Promise<
|
|
143
|
+
export declare function doctorCodexPreToolUse(homeDir?: string, options?: CodexDoctorOptions): Promise<CodexHookDoctorResult>;
|
|
139
144
|
interface CodexHookMutationResultWithReport {
|
|
140
145
|
ok: boolean;
|
|
141
146
|
changed: string[];
|
|
@@ -158,6 +163,7 @@ interface CodexHookLifecycleRefusal {
|
|
|
158
163
|
}
|
|
159
164
|
export type CodexHookMutationResult = CodexHookMutationResultWithReport | CodexHookLifecycleRefusal;
|
|
160
165
|
export interface CodexHookRunDeps {
|
|
166
|
+
homeDirProvider?: () => string;
|
|
161
167
|
secureFs?: PlatformSecureFs | null;
|
|
162
168
|
clock?: () => Date;
|
|
163
169
|
nonce?: () => string;
|
|
@@ -171,8 +177,8 @@ export interface CodexHookRunDeps {
|
|
|
171
177
|
export declare function _runCodex(homeDir: string, _mode: "install" | "repair", _options: {
|
|
172
178
|
force?: boolean;
|
|
173
179
|
}, deps: CodexHookRunDeps): Promise<CodexHookMutationResult>;
|
|
174
|
-
export declare function installCodexPreToolUse(homeDir?: string): Promise<CodexHookMutationResult>;
|
|
180
|
+
export declare function installCodexPreToolUse(homeDir?: string, deps?: CodexHookRunDeps): Promise<CodexHookMutationResult>;
|
|
175
181
|
export declare function repairCodexPreToolUse(homeDir?: string, options?: {
|
|
176
182
|
force?: boolean;
|
|
177
|
-
}): Promise<CodexHookMutationResult>;
|
|
183
|
+
}, deps?: CodexHookRunDeps): Promise<CodexHookMutationResult>;
|
|
178
184
|
//# sourceMappingURL=codex-hook-manager.d.ts.map
|
|
@@ -295,10 +295,19 @@ async function readManifest() {
|
|
|
295
295
|
throw new Error(`unreadable claude-hooks manifest: ${read.reason}`);
|
|
296
296
|
return JSON.parse(read.content);
|
|
297
297
|
}
|
|
298
|
-
export async function doctorCodexPreToolUse(homeDir
|
|
298
|
+
export async function doctorCodexPreToolUse(homeDir, options = {}) {
|
|
299
|
+
const platformSupport = resolvePlatformSupport(options.platform ?? process.platform);
|
|
300
|
+
if (platformSupport) {
|
|
301
|
+
return {
|
|
302
|
+
state: "unsupported-platform",
|
|
303
|
+
healthy: false,
|
|
304
|
+
platformSupport,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const resolvedHomeDir = homeDir ?? os.homedir();
|
|
299
308
|
const manifest = options.manifest ?? (await readManifest());
|
|
300
309
|
const assetPath = options.assetPath ?? SHIPPED_CODEX_ASSET;
|
|
301
|
-
const { hooksFile, configFile } = codexConfigPaths(
|
|
310
|
+
const { hooksFile, configFile } = codexConfigPaths(resolvedHomeDir);
|
|
302
311
|
const expectedCommand = expectedCodexCommand(assetPath);
|
|
303
312
|
// hooks.json registration.
|
|
304
313
|
const hooksRead = await readText(hooksFile);
|
|
@@ -373,9 +382,7 @@ export async function doctorCodexPreToolUse(homeDir = os.homedir(), options = {}
|
|
|
373
382
|
}
|
|
374
383
|
if (!node.satisfiesMinimum)
|
|
375
384
|
remediation.push("install Node 22 or newer");
|
|
376
|
-
const platformSupport = resolvePlatformSupport(options.platform ?? process.platform);
|
|
377
385
|
return {
|
|
378
|
-
...(platformSupport ? { platformSupport } : {}),
|
|
379
386
|
healthy: status === "runnable",
|
|
380
387
|
hooksJson,
|
|
381
388
|
config: { featuresHooks, readable: configReadable },
|
|
@@ -421,11 +428,17 @@ export async function _runCodex(homeDir, _mode, _options, deps) {
|
|
|
421
428
|
const { codexDir, hooksFile, configFile } = codexConfigPaths(homeDir);
|
|
422
429
|
const expectedCommand = expectedCodexCommand(assetPath);
|
|
423
430
|
const nodeOnPath = await (deps.nodeProbe ?? probeNodeOnPath)();
|
|
424
|
-
const doctor = () =>
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
431
|
+
const doctor = async () => {
|
|
432
|
+
const result = await doctorFn(homeDir, {
|
|
433
|
+
manifest,
|
|
434
|
+
assetPath,
|
|
435
|
+
nodeProbe: async () => nodeOnPath,
|
|
436
|
+
});
|
|
437
|
+
if ("state" in result) {
|
|
438
|
+
throw new Error("supported Codex lifecycle received unsupported doctor result");
|
|
439
|
+
}
|
|
440
|
+
return result;
|
|
441
|
+
};
|
|
429
442
|
if (!secureFs) {
|
|
430
443
|
return {
|
|
431
444
|
ok: false,
|
|
@@ -537,10 +550,28 @@ export async function _runCodex(homeDir, _mode, _options, deps) {
|
|
|
537
550
|
report: await doctor(),
|
|
538
551
|
};
|
|
539
552
|
}
|
|
540
|
-
|
|
541
|
-
return
|
|
553
|
+
function unsupportedCodexLifecycle(platformSupport) {
|
|
554
|
+
return {
|
|
555
|
+
ok: false,
|
|
556
|
+
changed: [],
|
|
557
|
+
backups: [],
|
|
558
|
+
errors: [platformSupport.refusalCode],
|
|
559
|
+
warnings: [platformSupport.guidance],
|
|
560
|
+
lifecycleRefusal: platformSupport,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
export function installCodexPreToolUse(homeDir, deps = {}) {
|
|
564
|
+
const platformSupport = resolvePlatformSupport(deps.platform ?? process.platform);
|
|
565
|
+
if (platformSupport)
|
|
566
|
+
return Promise.resolve(unsupportedCodexLifecycle(platformSupport));
|
|
567
|
+
const resolvedHomeDir = homeDir ?? (deps.homeDirProvider ?? os.homedir)();
|
|
568
|
+
return _runCodex(resolvedHomeDir, "install", {}, deps);
|
|
542
569
|
}
|
|
543
|
-
export function repairCodexPreToolUse(homeDir =
|
|
544
|
-
|
|
570
|
+
export function repairCodexPreToolUse(homeDir, options, deps = {}) {
|
|
571
|
+
const platformSupport = resolvePlatformSupport(deps.platform ?? process.platform);
|
|
572
|
+
if (platformSupport)
|
|
573
|
+
return Promise.resolve(unsupportedCodexLifecycle(platformSupport));
|
|
574
|
+
const resolvedHomeDir = homeDir ?? (deps.homeDirProvider ?? os.homedir)();
|
|
575
|
+
return _runCodex(resolvedHomeDir, "repair", options ?? {}, deps);
|
|
545
576
|
}
|
|
546
577
|
//# sourceMappingURL=codex-hook-manager.js.map
|
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
export declare const
|
|
2
|
-
readonly
|
|
3
|
-
|
|
4
|
-
export declare const LIFECYCLE_SUPPORT: {
|
|
5
|
-
readonly UNSUPPORTED: "unsupported";
|
|
6
|
-
};
|
|
7
|
-
export declare const PLATFORM_REFUSAL: {
|
|
8
|
-
readonly MACOS_LIFECYCLE_UNSUPPORTED: "macos-lifecycle-unsupported";
|
|
1
|
+
export declare const HOST_SUPPORT_STATE: {
|
|
2
|
+
readonly SUPPORTED: "supported";
|
|
3
|
+
readonly UNSUPPORTED: "unsupported-platform";
|
|
9
4
|
};
|
|
5
|
+
export declare const HOST_SUPPORT_GUIDANCE = "javi-forge supports Linux and Windows only.";
|
|
6
|
+
export type SupportedHostPlatform = "linux" | "win32";
|
|
7
|
+
export interface SupportedHost {
|
|
8
|
+
state: typeof HOST_SUPPORT_STATE.SUPPORTED;
|
|
9
|
+
platform: SupportedHostPlatform;
|
|
10
|
+
}
|
|
10
11
|
export interface PlatformSupport {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
refusalCode: typeof PLATFORM_REFUSAL.MACOS_LIFECYCLE_UNSUPPORTED;
|
|
15
|
-
guidance: string;
|
|
12
|
+
state: typeof HOST_SUPPORT_STATE.UNSUPPORTED;
|
|
13
|
+
refusalCode: typeof HOST_SUPPORT_STATE.UNSUPPORTED;
|
|
14
|
+
guidance: typeof HOST_SUPPORT_GUIDANCE;
|
|
16
15
|
}
|
|
17
|
-
export
|
|
16
|
+
export type HostSupportResult = SupportedHost | PlatformSupport;
|
|
17
|
+
/**
|
|
18
|
+
* Classifies every host value without aliases or fallthrough.
|
|
19
|
+
*/
|
|
20
|
+
export declare function classifyHostPlatform(platform: string): HostSupportResult;
|
|
21
|
+
/** Returns an unsupported result for legacy callers, otherwise undefined. */
|
|
18
22
|
export declare function resolvePlatformSupport(platform: string): PlatformSupport | undefined;
|
|
19
23
|
//# sourceMappingURL=platform-support.d.ts.map
|
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
export const
|
|
2
|
-
|
|
1
|
+
export const HOST_SUPPORT_STATE = {
|
|
2
|
+
SUPPORTED: "supported",
|
|
3
|
+
UNSUPPORTED: "unsupported-platform",
|
|
3
4
|
};
|
|
4
|
-
export const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (platform !== "darwin")
|
|
13
|
-
return undefined;
|
|
5
|
+
export const HOST_SUPPORT_GUIDANCE = "javi-forge supports Linux and Windows only.";
|
|
6
|
+
/**
|
|
7
|
+
* Classifies every host value without aliases or fallthrough.
|
|
8
|
+
*/
|
|
9
|
+
export function classifyHostPlatform(platform) {
|
|
10
|
+
if (platform === "linux" || platform === "win32") {
|
|
11
|
+
return { state: HOST_SUPPORT_STATE.SUPPORTED, platform };
|
|
12
|
+
}
|
|
14
13
|
return {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
refusalCode: PLATFORM_REFUSAL.MACOS_LIFECYCLE_UNSUPPORTED,
|
|
19
|
-
guidance: MACOS_DEPRECATION_GUIDANCE,
|
|
14
|
+
state: HOST_SUPPORT_STATE.UNSUPPORTED,
|
|
15
|
+
refusalCode: HOST_SUPPORT_STATE.UNSUPPORTED,
|
|
16
|
+
guidance: HOST_SUPPORT_GUIDANCE,
|
|
20
17
|
};
|
|
21
18
|
}
|
|
19
|
+
/** Returns an unsupported result for legacy callers, otherwise undefined. */
|
|
20
|
+
export function resolvePlatformSupport(platform) {
|
|
21
|
+
const result = classifyHostPlatform(platform);
|
|
22
|
+
return result.state === HOST_SUPPORT_STATE.UNSUPPORTED ? result : undefined;
|
|
23
|
+
}
|
|
22
24
|
//# sourceMappingURL=platform-support.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* POSIX `PlatformSecureFs` adapters for the SkillGuard transactional installer
|
|
3
|
-
* (Slice 3a). This is the ONLY place a shell tool runs (`getfacl` on Linux
|
|
4
|
-
*
|
|
3
|
+
* (Slice 3a). This is the ONLY place a shell tool runs (`getfacl` on Linux)
|
|
4
|
+
* and the ONLY place `os`/`fs` ownership bits are interpreted.
|
|
5
5
|
* interpreted. Everything fails closed: a missing/erroring/timed-out ACL tool,
|
|
6
6
|
* an extended/named/mask/default/inherited ACL entry, a foreign-owned or
|
|
7
7
|
* group/other-writable directory, or any inconclusive result refuses rather than
|
|
@@ -38,7 +38,7 @@ export interface PosixAclAdapter {
|
|
|
38
38
|
* mask) `w`, OR any named-group with effective `w`. Everything else (base
|
|
39
39
|
* entries, a lone `mask::`, effective-non-write named entries, x-only, trusted
|
|
40
40
|
* named users, `default:*`) proceeds. Same fail-closed spawn edges as
|
|
41
|
-
* `proveClean`.
|
|
41
|
+
* `proveClean`.
|
|
42
42
|
*/
|
|
43
43
|
proveNoEndangeringAcl(target: string): Promise<SecureResult<void>>;
|
|
44
44
|
}
|
|
@@ -51,13 +51,8 @@ export declare const ACL_DETAIL: {
|
|
|
51
51
|
readonly getfaclAbsent: "getfacl absent";
|
|
52
52
|
readonly getfaclTimeout: "getfacl timeout";
|
|
53
53
|
readonly extendedAclEntry: "extended ACL entry";
|
|
54
|
-
readonly macosLsAbsent: "/bin/ls absent";
|
|
55
|
-
readonly macosLsTimeout: "ls timeout";
|
|
56
|
-
readonly macosAclFlag: "ACL present (+ flag)";
|
|
57
|
-
readonly macosAceListed: "ACE listed";
|
|
58
54
|
};
|
|
59
55
|
export declare function createLinuxAclAdapter(spawn?: SpawnFn, stat?: StatFn): PosixAclAdapter;
|
|
60
|
-
export declare function createMacosAclAdapter(spawn?: SpawnFn): PosixAclAdapter;
|
|
61
56
|
/**
|
|
62
57
|
* Whether the host's ACL adapter is RESOLVABLE — an install-time capability
|
|
63
58
|
* question, deliberately separate from the per-target prover above. It never
|
|
@@ -65,10 +60,10 @@ export declare function createMacosAclAdapter(spawn?: SpawnFn): PosixAclAdapter;
|
|
|
65
60
|
*/
|
|
66
61
|
export type AclCapability = {
|
|
67
62
|
status: "available";
|
|
68
|
-
tool: "getfacl"
|
|
63
|
+
tool: "getfacl";
|
|
69
64
|
} | {
|
|
70
65
|
status: "absent";
|
|
71
|
-
tool: "getfacl"
|
|
66
|
+
tool: "getfacl";
|
|
72
67
|
} | {
|
|
73
68
|
status: "unknown";
|
|
74
69
|
tool: string;
|
|
@@ -89,9 +84,9 @@ export type AclCapability = {
|
|
|
89
84
|
export declare function probeAclCapability(spawn?: SpawnFn, platform?: NodeJS.Platform): Promise<AclCapability>;
|
|
90
85
|
export declare function createPosixSecureFs(acl: PosixAclAdapter): PlatformSecureFs;
|
|
91
86
|
/**
|
|
92
|
-
* Select the secure filesystem for
|
|
93
|
-
* adapter
|
|
94
|
-
*
|
|
87
|
+
* Select the secure filesystem for supported hosts. Linux uses the `getfacl`
|
|
88
|
+
* adapter and win32 uses the digest-bound PowerShell helper over a lazily-spawned
|
|
89
|
+
* session (Slice 3b). Every other platform returns
|
|
95
90
|
* `null` so the manager refuses with `windows-secure-object-unavailable` and
|
|
96
91
|
* mutates nothing.
|
|
97
92
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* POSIX `PlatformSecureFs` adapters for the SkillGuard transactional installer
|
|
3
|
-
* (Slice 3a). This is the ONLY place a shell tool runs (`getfacl` on Linux
|
|
4
|
-
*
|
|
3
|
+
* (Slice 3a). This is the ONLY place a shell tool runs (`getfacl` on Linux)
|
|
4
|
+
* and the ONLY place `os`/`fs` ownership bits are interpreted.
|
|
5
5
|
* interpreted. Everything fails closed: a missing/erroring/timed-out ACL tool,
|
|
6
6
|
* an extended/named/mask/default/inherited ACL entry, a foreign-owned or
|
|
7
7
|
* group/other-writable directory, or any inconclusive result refuses rather than
|
|
@@ -57,10 +57,6 @@ export const ACL_DETAIL = {
|
|
|
57
57
|
getfaclAbsent: "getfacl absent",
|
|
58
58
|
getfaclTimeout: "getfacl timeout",
|
|
59
59
|
extendedAclEntry: "extended ACL entry",
|
|
60
|
-
macosLsAbsent: "/bin/ls absent",
|
|
61
|
-
macosLsTimeout: "ls timeout",
|
|
62
|
-
macosAclFlag: "ACL present (+ flag)",
|
|
63
|
-
macosAceListed: "ACE listed",
|
|
64
60
|
};
|
|
65
61
|
// --- Linux getfacl adapter (Algorithm D) -------------------------------------
|
|
66
62
|
const LINUX_BASE_ENTRY = /^(user|group|other)::/;
|
|
@@ -208,39 +204,6 @@ export function createLinuxAclAdapter(spawn = defaultSpawn, stat = defaultStat)
|
|
|
208
204
|
},
|
|
209
205
|
};
|
|
210
206
|
}
|
|
211
|
-
// --- macOS /bin/ls -lde adapter (Algorithm E) --------------------------------
|
|
212
|
-
const MACOS_ACE_LINE = /^\s*\d+:\s/;
|
|
213
|
-
export function createMacosAclAdapter(spawn = defaultSpawn) {
|
|
214
|
-
const adapter = {
|
|
215
|
-
async proveClean(target) {
|
|
216
|
-
const res = await spawn("/bin/ls", ["-lde", "--", target]);
|
|
217
|
-
if (res.spawnError)
|
|
218
|
-
return refuse("unsupported-posix-acl", ACL_DETAIL.macosLsAbsent);
|
|
219
|
-
if (res.timedOut)
|
|
220
|
-
return refuse("unsupported-posix-acl", ACL_DETAIL.macosLsTimeout);
|
|
221
|
-
if (res.code !== 0) {
|
|
222
|
-
return refuse("unsupported-posix-acl", `ls exit ${res.code}`);
|
|
223
|
-
}
|
|
224
|
-
const lines = res.stdout.split("\n");
|
|
225
|
-
const modeLine = lines[0] ?? "";
|
|
226
|
-
if (modeLine[10] === "+") {
|
|
227
|
-
return refuse("unsupported-posix-acl", ACL_DETAIL.macosAclFlag);
|
|
228
|
-
}
|
|
229
|
-
if (lines.some((line) => MACOS_ACE_LINE.test(line))) {
|
|
230
|
-
return refuse("unsupported-posix-acl", ACL_DETAIL.macosAceListed);
|
|
231
|
-
}
|
|
232
|
-
return ok();
|
|
233
|
-
},
|
|
234
|
-
// DEFERRED (user: Linux only): darwin ancestors stay STRICT = status-quo
|
|
235
|
-
// over-refusal, not the reported Linux bug. `/bin/ls -lde` ACE text carries
|
|
236
|
-
// no numeric mask to compute effective rights from, so the path-endangering
|
|
237
|
-
// narrowing is a documented follow-up. Alias to the strict proof.
|
|
238
|
-
proveNoEndangeringAcl(target) {
|
|
239
|
-
return adapter.proveClean(target);
|
|
240
|
-
},
|
|
241
|
-
};
|
|
242
|
-
return adapter;
|
|
243
|
-
}
|
|
244
207
|
/**
|
|
245
208
|
* Probe the ACL adapter READ-ONLY: it resolves and runs a version/list argv and
|
|
246
209
|
* inspects nothing on disk. It creates, modifies and removes nothing, and its
|
|
@@ -254,29 +217,27 @@ export async function probeAclCapability(spawn = defaultSpawn, platform = proces
|
|
|
254
217
|
if (platform === "win32") {
|
|
255
218
|
return { status: "not-applicable", tool: "windows-secure-object" };
|
|
256
219
|
}
|
|
257
|
-
if (platform !== "linux"
|
|
220
|
+
if (platform !== "linux") {
|
|
258
221
|
return {
|
|
259
222
|
status: "unknown",
|
|
260
223
|
tool: platform,
|
|
261
224
|
detail: `no POSIX ACL adapter for platform ${platform}`,
|
|
262
225
|
};
|
|
263
226
|
}
|
|
264
|
-
const tool =
|
|
265
|
-
const args =
|
|
227
|
+
const tool = "getfacl";
|
|
228
|
+
const args = ["--version"];
|
|
266
229
|
const res = await spawn(tool, args);
|
|
267
230
|
if (res.spawnError)
|
|
268
231
|
return { status: "absent", tool };
|
|
269
232
|
if (res.timedOut) {
|
|
270
|
-
// The argv differs per platform (`getfacl --version` on linux, `/bin/ls -ld /`
|
|
271
|
-
// on darwin), so the detail names the probe, not a hardcoded flag.
|
|
272
233
|
return { status: "unknown", tool, detail: `${tool} probe timeout` };
|
|
273
234
|
}
|
|
274
235
|
if (res.code !== 0) {
|
|
275
236
|
return { status: "unknown", tool, detail: `${tool} exit ${res.code}` };
|
|
276
237
|
}
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
if (
|
|
238
|
+
// A zero exit whose banner does not name getfacl is a foreign binary on PATH,
|
|
239
|
+
// not proof of the adapter — report ignorance, not success.
|
|
240
|
+
if (!res.stdout.toLowerCase().includes("getfacl")) {
|
|
280
241
|
return {
|
|
281
242
|
status: "unknown",
|
|
282
243
|
tool,
|
|
@@ -504,9 +465,9 @@ export function createPosixSecureFs(acl) {
|
|
|
504
465
|
return secureFs;
|
|
505
466
|
}
|
|
506
467
|
/**
|
|
507
|
-
* Select the secure filesystem for
|
|
508
|
-
* adapter
|
|
509
|
-
*
|
|
468
|
+
* Select the secure filesystem for supported hosts. Linux uses the `getfacl`
|
|
469
|
+
* adapter and win32 uses the digest-bound PowerShell helper over a lazily-spawned
|
|
470
|
+
* session (Slice 3b). Every other platform returns
|
|
510
471
|
* `null` so the manager refuses with `windows-secure-object-unavailable` and
|
|
511
472
|
* mutates nothing.
|
|
512
473
|
*
|
|
@@ -521,8 +482,6 @@ export function createPosixSecureFs(acl) {
|
|
|
521
482
|
export function selectSecureFs(platform = process.platform) {
|
|
522
483
|
if (platform === "linux")
|
|
523
484
|
return createPosixSecureFs(createLinuxAclAdapter());
|
|
524
|
-
if (platform === "darwin")
|
|
525
|
-
return createPosixSecureFs(createMacosAclAdapter());
|
|
526
485
|
if (platform === "win32")
|
|
527
486
|
return createWindowsSecureFs(createPs1Session());
|
|
528
487
|
return null;
|
|
@@ -74,9 +74,8 @@ export interface PlatformSecureFs {
|
|
|
74
74
|
* ones (a lone mask, an effective-non-write or x-only or trusted named entry, a
|
|
75
75
|
* `default:*`). Called by the engine on ANCESTOR (non-managed) controlling dirs
|
|
76
76
|
* only; managed containers keep the strict proof. Selection is by the
|
|
77
|
-
* managed-containers role, NEVER by `process.platform`.
|
|
78
|
-
* the
|
|
79
|
-
* strict no-op alias (deferred).
|
|
77
|
+
* managed-containers role, NEVER by `process.platform`. Supported adapters
|
|
78
|
+
* implement the same role contract without host branching in this engine.
|
|
80
79
|
*/
|
|
81
80
|
proveNoEndangeringAcl(target: string): Promise<SecureResult<void>>;
|
|
82
81
|
/** Create ONE child directory exclusively at mode, reopen+verify, return its handle. */
|
package/dist/types/index.d.ts
CHANGED
|
@@ -69,9 +69,14 @@ export interface DoctorSection {
|
|
|
69
69
|
title: string;
|
|
70
70
|
checks: DoctorCheck[];
|
|
71
71
|
}
|
|
72
|
-
export
|
|
72
|
+
export type DoctorResult = {
|
|
73
|
+
state: "supported";
|
|
73
74
|
sections: DoctorSection[];
|
|
74
|
-
}
|
|
75
|
+
} | {
|
|
76
|
+
state: "unsupported-platform";
|
|
77
|
+
guidance: string;
|
|
78
|
+
sections: [];
|
|
79
|
+
};
|
|
75
80
|
export interface PluginManifest {
|
|
76
81
|
name: string;
|
|
77
82
|
version: string;
|
package/dist/ui/Doctor.d.ts
CHANGED
package/dist/ui/Doctor.js
CHANGED
|
@@ -15,6 +15,11 @@ const STATUS_COLOR = {
|
|
|
15
15
|
fail: theme.error,
|
|
16
16
|
skip: theme.muted,
|
|
17
17
|
};
|
|
18
|
+
export function unsupportedDoctorMessage(result) {
|
|
19
|
+
return result.state === "unsupported-platform"
|
|
20
|
+
? `unsupported-platform: ${result.guidance}`
|
|
21
|
+
: undefined;
|
|
22
|
+
}
|
|
18
23
|
export default function Doctor() {
|
|
19
24
|
const { exit } = useApp();
|
|
20
25
|
const isCI = useCIMode();
|
|
@@ -52,6 +57,14 @@ export default function Doctor() {
|
|
|
52
57
|
if (input.toLowerCase() === "q" || key.return || key.escape)
|
|
53
58
|
exit();
|
|
54
59
|
}, { isActive: !isCI });
|
|
60
|
+
const unsupportedMessage = result
|
|
61
|
+
? unsupportedDoctorMessage(result)
|
|
62
|
+
: undefined;
|
|
63
|
+
if (unsupportedMessage) {
|
|
64
|
+
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
65
|
+
React.createElement(Header, { subtitle: "doctor" }),
|
|
66
|
+
React.createElement(Text, { color: theme.error }, unsupportedMessage)));
|
|
67
|
+
}
|
|
55
68
|
// Compute health score
|
|
56
69
|
const allChecks = result?.sections.flatMap((s) => s.checks) ?? [];
|
|
57
70
|
const passed = allChecks.filter((c) => c.status === "ok").length;
|
package/lib/common.sh
CHANGED
|
@@ -39,16 +39,12 @@ log_info() { echo -e " ${CYAN}[INFO]${NC} $1"; }
|
|
|
39
39
|
log_step() { echo -e "${YELLOW}$1${NC}"; }
|
|
40
40
|
|
|
41
41
|
# =============================================================================
|
|
42
|
-
# sed_inplace -
|
|
42
|
+
# sed_inplace - GNU sed -i for supported Linux/WSL hosts
|
|
43
43
|
# =============================================================================
|
|
44
44
|
# Usage: sed_inplace "s/foo/bar/" file.txt
|
|
45
45
|
# =============================================================================
|
|
46
46
|
sed_inplace() {
|
|
47
|
-
|
|
48
|
-
sed -i "$@"
|
|
49
|
-
else
|
|
50
|
-
sed -i '' "$@"
|
|
51
|
-
fi
|
|
47
|
+
sed -i "$@"
|
|
52
48
|
}
|
|
53
49
|
|
|
54
50
|
# =============================================================================
|
|
@@ -130,7 +126,7 @@ detect_stack() {
|
|
|
130
126
|
STACK_TYPE="java-gradle"
|
|
131
127
|
BUILD_TOOL="gradle"
|
|
132
128
|
|
|
133
|
-
# Detect Java version from build files
|
|
129
|
+
# Detect Java version from build files
|
|
134
130
|
if [[ -f "$project_dir/build.gradle.kts" ]]; then
|
|
135
131
|
JAVA_VERSION=$(grep -E 'languageVersion\s*=\s*JavaLanguageVersion\.of\(' "$project_dir/build.gradle.kts" 2>/dev/null | grep -o '[0-9]\+' | head -1 || echo "21")
|
|
136
132
|
elif [[ -f "$project_dir/build.gradle" ]]; then
|