Conversation
mateacademy-ai-mentor
left a comment
There was a problem hiding this comment.
Review Summary
I’m requesting changes because there are a couple of critical blockers that violate explicit requirements and can break CLI behavior.
-
Critical issues to fix:
- Enforce exactly two CLI arguments (Checklist 1.2/2.2). In src/app.js, when
args.length !== 2you log an error but execution continues andmove(oldPath, newPath)may receiveundefined. After logging, immediatelyprocess.exit(1)orreturnso the program does not proceed. Also ensure the CLI invocation wrapsmove(...).catch(err => { console.error(err.message); process.exit(1); })to exit non-zero on errors. - Trailing-slash semantics (Checklist 1.3, 3.1–3.3). Add an explicit check before
fs.stat(newPath):const trailing = newPath.endsWith(path.sep) || newPath.endsWith('/'). Iftrailingis true, treatnewPathas a directory. Verify the directory exists and is a directory; if not, throw an error. If it exists, set the destination topath.join(newPath, path.basename(oldPath)). - Error signaling for missing destination directory. Where the destination directory does not exist, the spec expects an error; currently you
console.errorand return. Replace these withthrow new Error('Destination directory does not exist')(and similar for other invalid cases). Keep printing to stderr and exiting non-zero only in the CLI wrapper.
- Enforce exactly two CLI arguments (Checklist 1.2/2.2). In src/app.js, when
-
What you did well:
- Validates source is a file and uses
fs.rename, correctly removing the source on success, and handles existing destination directories by joining the basename.
- Validates source is a file and uses
-
Minor/optional:
- When
oldPath === newPath, your no-op return is acceptable; optionally print a brief message or keep it silent for minimalism.
- When
Make these targeted updates and you should meet the spec and pass the related tests.
✨ Please rate this review in the Platform's notification center1. I will be grateful for your feedback! ✨
Important
Fix the issues, commit the changes, and then re-request my review using the Re-request review button2 in the Reviewers tab.
Tip
If you have any comments about this review, please leave a message in the Add a comment section below and mention me with the @mateacademy-ai-mentor tag3.
I will read your message during my next review of your pull request. Please do not reply directly to my comments, as that feature is not supported yet.
Caution
Do not close the pull request until the review is complete. Closing it early can lead to unexpected behavior and disrupt the review process.
Footnotes
| if (args.length !== 2) { | ||
| console.error('Exactly two arguments are required'); | ||
| } |
There was a problem hiding this comment.
When the CLI is invoked with a number of args different than two, the code logs an error but does not stop execution. After the error message the script continues, destructures args and calls move with possibly undefined values. Requirement: the app must be executable via node index with exactly two arguments (checklist items 1.2 and 2.2). Change: after logging the error you should exit/return immediately (for example, call process.exit(1) or return) so the program does not continue. This enforces the "exactly two arguments" rule and avoids confusing follow-up errors.
src/app.js
Outdated
| async function move(oldPath, newPath) { | ||
| if (!oldPath || !newPath) { | ||
| console.error('Source and destination are required.'); | ||
|
|
||
| return; | ||
| } |
There was a problem hiding this comment.
The move() function does an early check for missing oldPath/newPath and returns — that's good defensive code. However, the top-level CLI should enforce argument count and not rely on move() to handle bad argument counts. Enforcing the check at the entry-point keeps behavior clear and consistent with the requirement that the executable accepts exactly two positional arguments.
src/app.js
Outdated
| try { | ||
| const destStats = await fs.stat(newPath); | ||
|
|
||
| if (destStats.isDirectory()) { | ||
| const baseName = path.basename(src); | ||
|
|
||
| dest = path.join(newPath, baseName); | ||
| } |
There was a problem hiding this comment.
The code detects existing destination directories via fs.stat(newPath) and handles that case. But it does not explicitly handle the rule: "if destination path string ends with a slash ('/') it must be considered as a directory" (requirement 3.1). Add an explicit check for trailing slash (for example const destHasTrailingSlash = newPath.endsWith(path.sep) || newPath.endsWith('/')) and, if true, treat newPath as a directory: verify it exists and is a directory, then set dest = path.join(newPath, path.basename(src)). If the directory does not exist, throw/return an error (do not attempt to rename to a path with a trailing slash). This will make the behavior match the spec exactly.
src/app.js
Outdated
| } catch (err) { | ||
| const destDir = path.dirname(newPath); | ||
|
|
||
| try { | ||
| const dirStats = await fs.stat(destDir); | ||
|
|
||
| if (!dirStats.isDirectory()) { | ||
| console.error('Destination directory does not exist'); | ||
|
|
||
| return; | ||
| } | ||
| } catch { | ||
| console.error('Destination directory does not exist'); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| dest = newPath; | ||
| } |
There was a problem hiding this comment.
The catch branch that handles fs.stat(newPath) failures currently computes destDir = path.dirname(newPath) and checks that destDir exists. That works for the case where newPath is intended as a filename in an existing directory. It does not, however, properly cover cases where the original newPath ends with a trailing slash (e.g. dir/). If newPath ends with a slash and the directory does not exist your code must throw an error per spec. Update this branch to consult the trailing-slash check and either build path.join(destDir, basename(src)) (if directory exists) or throw an error (if it doesn't).
src/app.js
Outdated
| const src = oldPath; | ||
| let dest = newPath; | ||
| const srcStats = await fs.stat(src); | ||
|
|
||
| if (!srcStats.isFile()) { |
There was a problem hiding this comment.
Good: the code validates that the source exists and is a file using fs.stat(src) and srcStats.isFile(), and uses fs.rename which satisfies the requirement that the source is removed after a successful move (checklist items 3.4 and 3.5). Keep this behavior.
src/app.js
Outdated
| if (oldPath === newPath) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
Minor: when oldPath === newPath the function silently returns. That's acceptable, but consider whether you want to print a message or exit with a non-zero code depending on how strict you need to be. The task description doesn't require output in this case, so this is optional.
mateacademy-ai-mentor
left a comment
There was a problem hiding this comment.
Review Summary
Decision: Request changes
Your implementation is close, but several critical blockers prevent it from meeting the assignment’s requirements.
Critical issues to fix:
- Error propagation in
move(src/app.js: around lines 9–13, 62–63): the top-leveltry/catchswallows errors (only logs). This prevents the CLI’s.catch(...)from running and the process from exiting non-zero. Remove the outer catch or rethrow after logging so callers see rejections. - CLI arg enforcement (src/app.js: ~70–72): when
args.length !== 2you log but continue. Per spec, immediatelyprocess.exit(1)(and don’t callmove). This is required so the CLI exits non-zero on invalid usage. - Ensure CLI catch executes (src/app.js: ~76–79): once
movepropagates errors, the existing.catch(err => { console.error(err.message); process.exit(1); })will work as intended. - Cross-device fallback (src/app.js: rename call ~59–61): implement EXDEV handling. On
err.code === 'EXDEV', copy todest(e.g.,fs.copyFileor streaming), thenunlink(oldPath)only after a successful copy. If the copy fails, remove the partially writtendest. - Overwrite safety (same block ~59–61): before moving, check if
destalready exists (fs.stat). If it does, throw a clear error (e.g.,Destination already exists) instead of relying on platform-dependent rename behavior.
Minor but required by spec:
- Silence on success (src/app.js: line ~61): remove the success
console.log. The tool should be quiet on success.
What you did well:
- Source validation with
fs.statandisFile()is correct, and your directory-detection/trailing-slash logic is on the right track. Once you let errors propagate, these checks will cause proper non-zero exits when invalid.
Make these changes and you should meet the checklist and expected CLI behavior. Keep going—you’re very close.
✨ Please rate this review in the Platform's notification center1. I will be grateful for your feedback! ✨
Important
Fix the issues, commit the changes, and then re-request my review using the Re-request review button2 in the Reviewers tab.
Tip
If you have any comments about this review, please leave a message in the Add a comment section below and mention me with the @mateacademy-ai-mentor tag3.
I will read your message during my next review of your pull request. Please do not reply directly to my comments, as that feature is not supported yet.
Caution
Do not close the pull request until the review is complete. Closing it early can lead to unexpected behavior and disrupt the review process.
Footnotes
| try { | ||
| if (!oldPath || !newPath) { | ||
| throw new Error('Exactly two arguments are required'); | ||
| } | ||
|
|
There was a problem hiding this comment.
Critical: The move function wraps its entire body in a try/catch and the catch only logs the error. This swallows errors and prevents the CLI wrapper from receiving a rejected promise. Remove the top-level try/catch here (or rethrow the error after logging) so callers can handle errors and the process can exit with non-zero. This is required so tests and the CLI detect failures (see checklist items about exiting non-zero on errors).
| } catch (err) { | ||
| console.error('Move error:', err.message); |
There was a problem hiding this comment.
Because of the catch above, thrown errors (e.g., missing source, missing destination directory) are printed but not propagated. The catch block should either rethrow the error or be removed. Currently this behavior prevents the CLI wrapper's .catch from executing. Let errors propagate so the process can exit 1 when move fails.
| if (args.length !== 2) { | ||
| console.error('Exactly two arguments are required'); | ||
| } |
There was a problem hiding this comment.
CLI argument enforcement: you log an error when args.length !== 2 but do not stop execution. Per the spec you must enforce exactly two positional arguments and exit non-zero if the count is incorrect. Call process.exit(1) (or return after exiting) here and do not call move when the arguments are invalid.
| move(oldPath, newPath).catch((err) => { | ||
| console.error(err); | ||
| process.exit(1); | ||
| }); |
There was a problem hiding this comment.
The CLI wrapper attaches a .catch to move(...) expecting rejections to be handled, but because move currently swallows errors this catch will rarely fire. Ensure move rejects on error (remove swallowing catch) so this .catch can call process.exit(1) and signal failure to the caller/test harness.
|
|
||
| await fs.rename(oldPath, dest); | ||
| console.log(`Moved ${oldPath} to ${dest}`); |
There was a problem hiding this comment.
Functional gap: fs.rename is invoked directly. On cross-device moves rename can fail with code === 'EXDEV'. Implement a fallback: on EXDEV do a copy (fs.copyFile or streaming copy) from oldPath to dest, then unlink(oldPath) only after a successful copy. Also ensure you remove any partially copied dest if the copy fails. This is required to match mv behavior across filesystems.
|
|
||
| await fs.rename(oldPath, dest); | ||
| console.log(`Moved ${oldPath} to ${dest}`); |
There was a problem hiding this comment.
Overwrite safety: before renaming/copying, check whether the computed dest already exists. The assignment expects you not to silently overwrite; either fail with an error or rely on rename to error consistently. It's safer to stat dest and throw if it exists rather than risking platform-dependent overwrite behavior.
| } | ||
|
|
||
| await fs.rename(oldPath, dest); | ||
| console.log(`Moved ${oldPath} to ${dest}`); |
There was a problem hiding this comment.
The success console.log(Moved ${oldPath} to ${dest}) prints output on successful moves. The specification states no extra output is required on success. Remove this line to keep the utility silent on success.
| const srcStats = await fs.stat(oldPath); | ||
|
|
||
| if (!srcStats.isFile()) { | ||
| throw new Error('Source is not a file'); | ||
| } |
There was a problem hiding this comment.
Validation: your source validation (stat + isFile()) is correct. However, if fs.stat(oldPath) throws (source missing), that error must propagate to the CLI wrapper so the process can exit non-zero. After removing the swallow-catch the existing logic will correctly cause a rejection on missing source.
| const trailing = newPath.endsWith(path.sep) || newPath.endsWith('/'); | ||
| let dest; | ||
|
|
||
| if (trailing) { | ||
| const destStats = await fs.stat(newPath); | ||
|
|
||
| if (!destStats.isDirectory()) { | ||
| throw new Error('Destination is not a directory'); | ||
| } | ||
|
|
||
| dest = path.join(newPath, path.basename(oldPath)); | ||
| } else { |
There was a problem hiding this comment.
Trailing-slash handling: you correctly detect trailing slash with newPath.endsWith(path.sep) || newPath.endsWith('/'). When trailing is true you stat newPath; ensure that if stat throws because the directory does not exist you allow that error to propagate (and cause a non-zero exit) rather than only logging it. With the top-level catch removed this will behave as required by the spec.
mateacademy-ai-mentor
left a comment
There was a problem hiding this comment.
Review Summary
Educational approval: You’ve implemented many core behaviors correctly, and this pass is to help you progress while you address remaining edge cases and error handling.
What you did well:
- Solid validation of the source file with
fs.stat(oldPath)andisFile()(lines 18–22). - Correct handling of trailing slash and existing destination directory logic to derive
dest(lines 27–34 and 36–43). - Proper destination parent-directory check when
newPathdoesn’t exist (lines 44–53), and clean exportmodule.exports = { move }.
What needs improvement (why and how):
- Error propagation: The top-level try/catch in
move(lines 9–64) swallows errors, preventing required rejections and non-zero CLI exits. Remove the outer catch or rethrow after logging so callers observe failures. - CLI arg validation: When
args.length !== 2(lines 70–72), exit immediately withprocess.exit(1)and don’t callmoveto meet the CLI requirement. - Cross-device moves: Wrap
await fs.rename(oldPath, dest)(around line 60) in try/catch; onerr.code === 'EXDEV', performawait fs.copyFile(oldPath, dest)thenawait fs.unlink(oldPath). If copy fails, remove any partialdest. - Deterministic overwrite behavior: Before renaming/copying, check if
destalready exists; if so, throw a clear error likeDestination already existsto avoid platform-dependent overwrites. - Silence on success: Remove the success
console.log(line 61) to keep the tool quiet when everything works.
You’re very close—your path resolution and validation logic are on the right track. Tightening error propagation and adding the EXDEV fallback will align the tool with mv-like semantics and the checklist. Keep going—you’ve built a solid foundation!
✨ Please rate this review in the Platform's notification center1. I will be grateful for your feedback! ✨
Footnotes
| try { | ||
| if (!oldPath || !newPath) { | ||
| throw new Error('Exactly two arguments are required'); | ||
| } | ||
|
|
||
| if (oldPath === newPath) { | ||
| return; | ||
| } | ||
|
|
||
| const srcStats = await fs.stat(oldPath); | ||
|
|
||
| if (!srcStats.isFile()) { | ||
| throw new Error('Source is not a file'); | ||
| } | ||
|
|
||
| const trailing = newPath.endsWith(path.sep) || newPath.endsWith('/'); | ||
| let dest; | ||
|
|
||
| if (trailing) { | ||
| const destStats = await fs.stat(newPath); | ||
|
|
||
| if (!destStats.isDirectory()) { | ||
| throw new Error('Destination is not a directory'); | ||
| } | ||
|
|
||
| dest = path.join(newPath, path.basename(oldPath)); | ||
| } else { | ||
| try { | ||
| const destStats = await fs.stat(newPath); | ||
|
|
||
| if (destStats.isDirectory()) { | ||
| dest = path.join(newPath, path.basename(oldPath)); | ||
| } else { | ||
| dest = newPath; | ||
| } | ||
| } catch (err) { | ||
| if (err.code === 'ENOENT') { | ||
| const destDir = path.dirname(newPath); | ||
| const dirStats = await fs.stat(destDir); | ||
|
|
||
| if (!dirStats.isDirectory()) { | ||
| throw new Error('Destination directory does not exist'); | ||
| } | ||
|
|
||
| dest = newPath; | ||
| } else { | ||
| throw err; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| await fs.rename(oldPath, dest); | ||
| console.log(`Moved ${oldPath} to ${dest}`); | ||
| } catch (err) { | ||
| console.error('Move error:', err.message); | ||
| } |
There was a problem hiding this comment.
Top-level error swallowing in move() — prevents callers from observing failures.
You wrap the entire move body in a try/catch that only logs the error and does not rethrow (lines 9–64). Per the specification the function must reject on error so the CLI and tests can observe a failure and exit non-zero. Remove this outer try/catch or rethrow the error after logging so callers receive the rejection. This addresses checklist items about error handling and CLI exit behavior.
| if (args.length !== 2) { | ||
| console.error('Exactly two arguments are required'); | ||
| } |
There was a problem hiding this comment.
CLI argument validation must exit immediately on invalid usage.
At lines 70–72 you log when args.length !== 2 but then continue to call move. The program should not proceed — call process.exit(1) (or throw) immediately after logging to ensure a non-zero exit on invalid usage, as required by the task.
| } | ||
|
|
||
| await fs.rename(oldPath, dest); | ||
| console.log(`Moved ${oldPath} to ${dest}`); |
There was a problem hiding this comment.
fs.rename has no EXDEV fallback — add copy+unlink on cross-device errors.
You call await fs.rename(oldPath, dest) (line 60) without handling err.code === 'EXDEV'. Implement a try/catch around fs.rename; if the error code is EXDEV, perform a safe copy (e.g., fs.copyFile(oldPath, dest) or stream copy) and only await fs.unlink(oldPath) after a successful copy. Also ensure that if copy fails you remove any partially-created dest file to avoid leaving partial results. This satisfies the cross-device handling requirement in the checklist.
| } | ||
|
|
||
| await fs.rename(oldPath, dest); | ||
| console.log(`Moved ${oldPath} to ${dest}`); |
There was a problem hiding this comment.
Do not log success — tool should be silent on success.
Remove the console.log('Moved ...') on line 61. The program should produce no output when the operation succeeds (per project expectations).
| try { | ||
| const destStats = await fs.stat(newPath); | ||
|
|
||
| if (destStats.isDirectory()) { | ||
| dest = path.join(newPath, path.basename(oldPath)); | ||
| } else { | ||
| dest = newPath; | ||
| } |
There was a problem hiding this comment.
Check for existing destination to avoid platform-dependent overwrite.
Before performing the rename/copy you should explicitly check whether dest exists and fail with a clear error (for example: Destination already exists). The current code calculates dest but does not check for its existence prior to fs.rename (see block where dest is determined, lines 36–43). Add a fs.stat(dest) or fs.access(dest) check and throw if it exists so behavior is deterministic and tests won't depend on platform rename semantics.
| if (trailing) { | ||
| const destStats = await fs.stat(newPath); | ||
|
|
||
| if (!destStats.isDirectory()) { | ||
| throw new Error('Destination is not a directory'); | ||
| } | ||
|
|
||
| dest = path.join(newPath, path.basename(oldPath)); |
There was a problem hiding this comment.
Trailing-slash directory handling is implemented but errors must propagate.
The trailing-slash branch (lines 27–34) correctly attempts fs.stat(newPath) and verifies it's a directory. However, because the outer catch swallows errors, a missing directory will only be logged, not cause the process to fail. Once you remove/rework the swallow-catch, the ENOENT will correctly surface as required by the spec.
| const srcStats = await fs.stat(oldPath); | ||
|
|
||
| if (!srcStats.isFile()) { | ||
| throw new Error('Source is not a file'); | ||
| } |
There was a problem hiding this comment.
Source validation is correct — ensure errors are not swallowed.
You call fs.stat(oldPath) and verify isFile() (lines 18–22), which matches the requirement to only support files as sources. Ensure that ENOENT or the Source is not a file error is allowed to propagate to the caller (remove the top-level suppression) so the CLI exits non-zero on invalid sources.
| } catch (err) { | ||
| if (err.code === 'ENOENT') { | ||
| const destDir = path.dirname(newPath); | ||
| const dirStats = await fs.stat(destDir); | ||
|
|
||
| if (!dirStats.isDirectory()) { | ||
| throw new Error('Destination directory does not exist'); | ||
| } | ||
|
|
||
| dest = newPath; |
There was a problem hiding this comment.
Destination parent-directory check is correct but must propagate.
When fs.stat(newPath) throws ENOENT you check the parent directory and throw Destination directory does not exist (lines 44–53). This matches checklist requirements — keep this logic but ensure the error reaches the caller (remove the outer swallow-catch).
| }); | ||
| } | ||
|
|
||
| module.exports = { move }; |
There was a problem hiding this comment.
module.exports is fine — keep move async and rejecting on error for tests.
module.exports = { move } (line 82) is correct. Tests or consumers will expect move() to reject on error; ensure move does not swallow errors so exported function behaves as expected.
No description provided.