Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"task": "Implement Windows PATH shim detection/repair and Unix PATH append logic in the npm postinstall script, with test coverage and a vitest config tweak to include scripts tests.",
"generated": "2026-07-03T00:00:00Z",
"dimensions": {
"component_scope": {
"score": 2,
"label": "S",
"affected": "postinstall script (scripts/postinstall.mjs), validate-secrets pre-commit script (scripts/validate-secrets.js), vitest test configuration (vitest.config.ts)",
"layers": "Infra / build tooling scripts"
},
"requirements_clarity": {
"score": 2,
"label": "S",
"status": "Clear",
"gaps": null
},
"technical_risk": {
"score": 3,
"label": "M",
"risk_factors": "Modifies the user's system PATH via setx (Windows) or shell rc-file append (Unix) during postinstall; dynamically imports the compiled dist/utils/windows-path.js helper with a warning fallback if dist/ has not been built yet; platform-specific branching (win32 vs. posix) and shell detection (zsh/bash) increase the number of edge cases to reason about.",
"mitigation": "Idempotency checks (isInUserPath / alreadyInRcFile) prevent duplicate PATH entries on repeated installs; try/catch fallbacks emit warnings instead of failing the install if the windows-path helper or npm prefix lookup fails; extensive new unit test coverage (scripts/__tests__/postinstall.test.ts, 375 lines) exercises both win32 and posix code paths with injectable platform/separator parameters."
},
"file_change_estimate": {
"score": 3,
"label": "M",
"modified_files": 3,
"modified_file_list": ["scripts/postinstall.mjs", "scripts/validate-secrets.js", "vitest.config.ts"],
"new_files": 1,
"new_file_list": ["scripts/__tests__/postinstall.test.ts"],
"affected_dirs": ["scripts", "."]
},
"dependencies": {
"score": 1,
"label": "XS",
"new_packages": [],
"version_changes": []
},
"affected_layers": {
"score": 1,
"label": "XS",
"layers_changed": ["Infra"],
"schema_migration": false,
"cross_system": false
}
},
"total": 12,
"size": "S",
"band_range": "10-14",
"files_changed": 4,
"routing": "writing-plans",
"key_reasoning": [
{ "dimension": "technical_risk", "reason": "Postinstall script now mutates the user's system PATH (setx on Windows, rc-file append on Unix) and dynamically loads a compiled helper module — a new pattern for this codebase, though heavily mitigated with idempotency checks and fallback warnings." },
{ "dimension": "file_change_estimate", "reason": "4 files touched total (3 modified, 1 new large test file); diffstat total-file-count maps to the M band under the actual-mode mapping rule even though the change is functionally narrow." }
],
"red_flags_applied": [],
"split_recommendation": null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
diff --git a/.codemie/codemie-cli.config.json b/.codemie/codemie-cli.config.json
index deed353..5f38325 100644
--- a/.codemie/codemie-cli.config.json
+++ b/.codemie/codemie-cli.config.json
@@ -10,9 +10,21 @@
"baseUrl": "https://codemie.lab.epam.com/code-assistant-api",
"model": "claude-sonnet-4-6",
"haikuModel": "claude-haiku-4-5-20251001",
- "sonnetModel": "claude-sonnet-4-6",
+ "sonnetModel": "claude-sonnet-5",
"opusModel": "claude-opus-4-8",
"name": "epm-cdme"
+ },
+ "sonnet5preview": {
+ "codeMieProject": "codemie",
+ "provider": "ai-run-sso",
+ "codeMieUrl": "https://codemie-preview.lab.epam.com/",
+ "apiKey": "sso-provided",
+ "baseUrl": "https://codemie-preview.lab.epam.com/code-assistant-api",
+ "model": "claude-sonnet-5",
+ "haikuModel": "claude-haiku-4-5-20251001",
+ "sonnetModel": "claude-sonnet-5",
+ "opusModel": "claude-opus-4-8",
+ "name": "sonnet5preview"
}
},
"codemieAssistants": [
@@ -52,5 +64,6 @@
"claude"
]
}
- ]
+ ],
+ "codemieSkills": []
}
\ No newline at end of file
diff --git a/package.json b/package.json
index 6cca35f..7c20aa9 100644
--- a/package.json
+++ b/package.json
@@ -43,8 +43,8 @@
"typecheck": "tsc --noEmit",
"format": "npm run lint:fix",
"check:pre-commit": "npm run typecheck && npm run lint",
- "lint": "eslint '{src,tests}/**/*.ts' --max-warnings=0",
- "lint:fix": "eslint '{src,tests}/**/*.ts' --fix",
+ "lint": "eslint src/**/*.ts tests/**/*.ts --max-warnings=0",
+ "lint:fix": "eslint src/**/*.ts tests/**/*.ts --fix",
"commitlint": "commitlint --edit",
"commitlint:last": "commitlint --from HEAD~1 --to HEAD --verbose",
"validate:secrets": "node scripts/validate-secrets.js",
diff --git a/scripts/__tests__/postinstall.test.ts b/scripts/__tests__/postinstall.test.ts
index 933aeea..cfb43a0 100644
--- a/scripts/__tests__/postinstall.test.ts
+++ b/scripts/__tests__/postinstall.test.ts
@@ -271,6 +271,18 @@ describe('postinstall', () => {
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('codemie'));
expect(process.exitCode).toBeFalsy();
});
+
+ it('degrades gracefully (no throw, exit 0) when the windows-path helper cannot be loaded or used', async () => {
+ vi.mocked(isInUserPath).mockRejectedValue(new Error("Cannot find module '../dist/utils/windows-path.js'"));
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+
+ const { runWindows } = await import('../postinstall.mjs');
+
+ await expect(runWindows()).resolves.toBeUndefined();
+ expect(addToUserPath).not.toHaveBeenCalled();
+ expect(process.exitCode).toBeFalsy();
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('windows-path'));
+ });
});

describe('runUnix', () => {
diff --git a/scripts/postinstall.mjs b/scripts/postinstall.mjs
index 163c549..a23b51e 100644
--- a/scripts/postinstall.mjs
+++ b/scripts/postinstall.mjs
@@ -69,11 +69,31 @@ export async function runWindows() {
console.warn(`\n⚠️ Expected CodeMie command shims not found in ${dir}: ${missing.join(', ')}\n`);
}

- const { isInUserPath, addToUserPath } = await import('../dist/utils/windows-path.js');
+ let isInUserPath, addToUserPath;
+ try {
+ ({ isInUserPath, addToUserPath } = await import('../dist/utils/windows-path.js'));
+ } catch (error) {
+ console.warn(`\n⚠️ Could not load the windows-path PATH helper (dist/ may not be built yet): ${error.message}\n`);
+ return;
+ }
+
+ let alreadyInPath;
+ try {
+ alreadyInPath = await isInUserPath(dir);
+ } catch (error) {
+ console.warn(`\n⚠️ Could not check the windows-path PATH helper: ${error.message}\n`);
+ return;
+ }
+ if (alreadyInPath) return;

- if (await isInUserPath(dir)) return;
+ let result;
+ try {
+ result = await addToUserPath(dir);
+ } catch (error) {
+ console.warn(`\n⚠️ Could not use the windows-path PATH helper: ${error.message}\n`);
+ return;
+ }

- const result = await addToUserPath(dir);
if (result.success) {
console.log(`\n✓ Added ${dir} to PATH\n Open a new terminal to use codemie\n`);
return;
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"decision": "approve",
"rationale": "CR-001 resolved: runWindows() now wraps the dynamic import of dist/utils/windows-path.js and both its calls (isInUserPath, addToUserPath) in separate try/catch blocks, each degrading to a console.warn + return (no exitCode set) on failure — matching the non-fatal treatment already used for missing npm prefix and missing shim files. A new test ('degrades gracefully ... when the windows-path helper cannot be loaded or used') exercises this via a rejected isInUserPath call; manually confirmed via a standalone node script that a genuine dynamic-import-of-missing-module rejection is caught the same way. Full test suite (27 postinstall tests + 26 windows-path tests), lint, and typecheck all pass. This is a small, purely-defensive, mechanical fix-up with no new security-sensitive surface or behavior change to the paths already reviewed in the final round, so the narrow blind+edge-case confirmation pass was not run — nothing about this fix-up suggested a new high-risk issue. The fix-up diff also shows unrelated pre-existing uncommitted local changes to .codemie/codemie-cli.config.json and package.json (user-directed local-only changes, not part of this task) — noise in the diff, not reviewed as part of this change.",
"confidence": "high",
"risk_flags": [],
"business_review": [
{"criterion": "getNpmPrefix/getShimDir/isInPath/getShellRcFile/alreadyInRcFile/getExpectedShimNames/findMissingShims/runWindows/runUnix/run all exported with spec'd signatures", "status": "pass", "notes": "Confirmed by acceptance lens against live file."},
{"criterion": "Direct-execution guard prevents side effects on import", "status": "pass", "notes": "isMain check via fileURLToPath(import.meta.url) === process.argv[1]."},
{"criterion": "Windows flow steps 1-7 implemented in order", "status": "pass", "notes": "prefix -> dir -> missing-shim warn -> dynamic import -> isInUserPath no-op -> addToUserPath success/failure handling, matches spec exactly."},
{"criterion": "Unix flow unchanged except delimiter fix", "status": "pass", "notes": "Same logic, isInPath now takes explicit sep derived from plat."},
{"criterion": "Exit policy table (only addToUserPath failure exits 1)", "status": "pass", "notes": "Now also correctly holds for the previously-unhandled missing-dist/-helper failure mode, which degrades to exit 0 like the other non-actionable failures."},
{"criterion": "Reuse windows-path.ts via dist/, no duplicated PATH logic", "status": "pass", "notes": "windows-path.ts untouched, only dynamically imported."},
{"criterion": "10 required test cases present", "status": "pass", "notes": "All 10 present plus the new CR-001 regression test."},
{"criterion": "vitest.config.ts includes scripts/**/*.test.ts", "status": "pass", "notes": "Confirmed."},
{"criterion": "Out-of-scope items absent (isolated prefix redirect, hook executor fix, install.cmd fix, install.ps1/native-installer changes)", "status": "pass", "notes": "git diff confirms zero changes to those files."}
],
"standards_review": [
{"standard": "git-workflow.md: Conventional Commits format + allowed scopes", "status": "pass", "notes": "Fix-up commit uses fix(cli): ... format with a valid scope."},
{"standard": "code-quality.md: naming, .js import extensions, function size, comments explain why", "status": "pass", "notes": "console.log/warn/error usage remains a justified exception for this standalone lifecycle script."},
{"standard": "security-practices.md: no hardcoded secrets, path validation, no injection", "status": "pass", "notes": "No new secrets or unvalidated paths introduced by the fix-up."}
],
"finding_status": [
{"id": "CR-001", "status": "resolved"}
],
"findings": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"decision": "request-changes",
"rationale": "Blind and edge-case lenses independently flagged the same critical bug: runWindows()'s unguarded `await import('../dist/utils/windows-path.js')` throws uncaught if dist/ is absent. Confirmed against .github/workflows/ci.yml: the windows-latest job runs `npm ci` (which triggers postinstall) at lines 212-213, before 'Download build artifacts' populates dist/ at lines 216-220 — so this will crash the Windows CI 'Install dependencies' step, and will do the same for any Windows contributor's fresh `npm install` before `npm run build`. The test suite fully mocks the dist import (vi.mock('../../dist/utils/windows-path.js', ...)), so this failure mode has zero coverage. This also contradicts the spec's own stated exit policy, which intends only addToUserPath failure to be non-graceful — a missing dist/ was never considered a failure mode in the design. Acceptance lens found zero blocking findings against the approved spec/plan otherwise (all 10 required test cases present, all exported functions and the exit-policy table match exactly). Two other findings (runUnix's untested plat='win32' interaction; validate-secrets.js's shell:true removal for non-.exe engine binaries) are deferred as non-blocking maintainability notes, not confirmed regressions. One finding (unused isWindows variable) was a false positive from the blind lens's lack of full-file context — isWindows is still used in resolveCommand(). Test-assertion-depth gaps on 3 test cases (console.log/error content not spied in every case) are non-blocking per the acceptance lens — the underlying behavior is correct.",
"confidence": "high",
"risk_flags": ["breaking-change"],
"business_review": [
{"criterion": "getNpmPrefix/getShimDir/isInPath/getShellRcFile/alreadyInRcFile/getExpectedShimNames/findMissingShims/runWindows/runUnix/run all exported with spec'd signatures", "status": "pass", "notes": "Confirmed by acceptance lens against live file."},
{"criterion": "Direct-execution guard prevents side effects on import", "status": "pass", "notes": "isMain check via fileURLToPath(import.meta.url) === process.argv[1]."},
{"criterion": "Windows flow steps 1-7 implemented in order", "status": "pass", "notes": "prefix -> dir -> missing-shim warn -> dynamic import -> isInUserPath no-op -> addToUserPath success/failure handling, matches spec exactly."},
{"criterion": "Unix flow unchanged except delimiter fix", "status": "pass", "notes": "Same logic, isInPath now takes explicit sep derived from plat."},
{"criterion": "Exit policy table (only addToUserPath failure exits 1)", "status": "partial", "notes": "Table is correctly implemented for the failure modes the spec anticipated, but the spec did not anticipate a missing dist/utils/windows-path.js as a failure mode, and the current unguarded import crashes rather than degrading to exit 0 like the other non-actionable failures. This is the CR-001 gap."},
{"criterion": "Reuse windows-path.ts via dist/, no duplicated PATH logic", "status": "pass", "notes": "windows-path.ts untouched, only dynamically imported."},
{"criterion": "10 required test cases present", "status": "pass", "notes": "All 10 present; 3 have shallower assertions than ideal but exercise correct code paths (non-blocking)."},
{"criterion": "vitest.config.ts includes scripts/**/*.test.ts", "status": "pass", "notes": "Confirmed."},
{"criterion": "Out-of-scope items absent (isolated prefix redirect, hook executor fix, install.cmd fix, install.ps1/native-installer changes)", "status": "pass", "notes": "git diff confirms zero changes to those files."}
],
"standards_review": [
{"standard": "git-workflow.md: Conventional Commits format + allowed scopes", "status": "pass", "notes": "All 8 commits use valid type(scope): subject format with scopes from the allowed list (cli, deps)."},
{"standard": "code-quality.md: naming, .js import extensions, function size, comments explain why", "status": "pass", "notes": "console.log/warn/error usage (vs logger.debug) is a justified exception — this is a standalone lifecycle script outside src/ producing user-facing install output, not application debug logging, matching the pre-existing convention in the same file before this diff."},
{"standard": "security-practices.md: no hardcoded secrets, path validation, no injection", "status": "pass", "notes": "No secrets introduced. getExpectedShimNames() builds its path from __dirname (not user input). runWindows/runUnix pass a directory derived from trusted local `npm config get prefix` output into addToUserPath(), which already validates it (validateDirectoryPath rejects metacharacters, requires absolute path) before touching the registry — no new unvalidated exec/registry calls introduced by this diff."}
],
"findings": [
{
"id": "CR-001",
"severity": "critical",
"triage": "patch",
"title": "Unguarded dynamic import of dist/utils/windows-path.js crashes postinstall (and Windows CI) when dist/ doesn't exist yet",
"file": "scripts/postinstall.mjs",
"problem": "runWindows() does `const { isInUserPath, addToUserPath } = await import('../dist/utils/windows-path.js');` with no try/catch. This module only exists after `npm run build`. On the windows-latest CI job (.github/workflows/ci.yml lines 212-220), `npm ci` runs at the 'Install dependencies' step — which triggers this postinstall script — before the 'Download build artifacts' step populates dist/. The unguarded import throws ERR_MODULE_NOT_FOUND, which propagates uncaught through runWindows() -> run() at module top level, crashing the npm ci process itself. The same failure hits any Windows contributor doing a fresh `npm install` before ever running `npm run build` (dist/ is gitignored).",
"impact": "Breaks the Windows CI job's dependency-install step entirely (not just a degraded warning), and breaks a fresh Windows clone's first `npm install`. This is a regression from the pre-existing script, which never touched dist/ and never threw uncaught. It also contradicts the spec's own intent that only addToUserPath failure should be non-graceful — everything else should degrade to a warning and exit 0.",
"recommendation": "Wrap the dynamic import (and the isInUserPath/addToUserPath calls that depend on it) in try/catch inside runWindows(). On failure, console.warn a message noting the PATH helper could not be loaded and return without setting process.exitCode — the same non-fatal degradation already used for a missing npm prefix or missing shim files."
}
]
}
Loading
Loading