projectmedic 0.0.0-stage → 1.0.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/LICENSE ADDED
@@ -0,0 +1,8 @@
1
+
2
+ ISC License
3
+
4
+ Copyright (c) 2026 Akhil Pal
5
+
6
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
7
+
8
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,62 @@
1
- # Temporary Holding Version
1
+ # Project Doctor
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A Node.js project health checker that identifies missing or misconfigured project metadata and files.
4
+
5
+ ## Features
6
+
7
+ - Validates `package.json`
8
+ - Checks package name, version, and description
9
+ - Checks for a README, `.gitignore`, and license
10
+ - Checks for a test script and repository metadata
11
+ - Checks for a Node.js version requirement
12
+ - Provides a JavaScript API and command-line interface
13
+ - Supports JSON output
14
+
15
+ ## Requirements
16
+
17
+ - Node.js
18
+
19
+ ## Usage
20
+
21
+ Run the checker against the current directory:
22
+
23
+ ```bash
24
+ node bin/doctor.js .
25
+ ```
26
+
27
+ Check another project:
28
+
29
+ ```bash
30
+ node bin/doctor.js /path/to/project
31
+ ```
32
+
33
+ Output a JSON report:
34
+
35
+ ```bash
36
+ node bin/doctor.js . --json
37
+ ```
38
+
39
+ ## Library API
40
+
41
+ ```js
42
+ import { inspectProject } from "project-doctor";
43
+
44
+ const report = await inspectProject("./my-project");
45
+
46
+ console.log(report.summary);
47
+ console.log(report.findings);
48
+ ```
49
+
50
+ The report contains the project path, a summary of check results, and individual findings.
51
+
52
+ ## Development
53
+
54
+ Run the test suite:
55
+
56
+ ```bash
57
+ npm test
58
+ ```
59
+
60
+ ## License
61
+
62
+ ISC
package/bin/doctor.js ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ import { inspectProject } from "../src/index.js";
3
+
4
+ const args = process.argv.slice(2);
5
+ const jsonMode = args.includes("--json");
6
+ const projectPath = args.find((arg) => !arg.startsWith("--")) || ".";
7
+
8
+ try {
9
+ const report = await inspectProject(projectPath);
10
+
11
+ if (report.summary.failed > 0) {
12
+ process.exitCode = 1;
13
+ }
14
+
15
+ if (jsonMode) {
16
+ console.log(JSON.stringify(report, null, 2));
17
+ } else {
18
+ console.log("\nProject Doctor\n");
19
+
20
+ for (const finding of report.findings) {
21
+ const symbol =
22
+ finding.status === "pass"
23
+ ? "✓"
24
+ : finding.status === "warn"
25
+ ? "⚠"
26
+ : "✗";
27
+
28
+ console.log(`${symbol} ${finding.message}`);
29
+ }
30
+
31
+ console.log(`\n${report.summary.total} checks completed`);
32
+ console.log(
33
+ `${report.summary.passed} passed, ` +
34
+ `${report.summary.warnings} warnings, ` +
35
+ `${report.summary.failed} failed`
36
+ );
37
+ }
38
+ } catch (error) {
39
+ if (jsonMode) {
40
+ console.log(JSON.stringify({ error: error.message }, null, 2));
41
+ } else {
42
+ console.error(`Project Doctor: ${error.message}`);
43
+ }
44
+
45
+ process.exitCode = 1;
46
+ }
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
1
  {
2
2
  "name": "projectmedic",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "A Node.js project health checker that identifies missing or misconfigured project metadata and files.",
5
+ "license": "ISC",
6
+ "author": "",
7
+ "type": "module",
8
+ "main": "./src/index.js",
9
+ "exports": {
10
+ ".": "./src/index.js"
11
+ },
12
+ "bin": {
13
+ "project-doctor": "bin/doctor.js"
14
+ },
15
+ "files": [
16
+ "src/",
17
+ "bin/",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "keywords": [
22
+ "nodejs",
23
+ "project-health",
24
+ "cli",
25
+ "developer-tools",
26
+ "project-checker"
27
+ ],
28
+ "scripts": {
29
+ "test": "node --test"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/amethystakira/project-doctor.git"
34
+ },
35
+ "engines": {
36
+ "node": ">=22.0.0"
37
+ }
38
+ }
package/src/finding.js ADDED
@@ -0,0 +1,14 @@
1
+ // Create a standardized result for every project check.
2
+ export function createFinding(id, name, status, message) {
3
+ return { id, name, status, message };
4
+ }
5
+
6
+ // Report a check that could not run because package.json is unavailable.
7
+ export function createSkippedFinding(id, name) {
8
+ return createFinding(
9
+ id,
10
+ name,
11
+ "warn",
12
+ "Check skipped because package.json is missing or invalid"
13
+ );
14
+ }
package/src/index.js ADDED
@@ -0,0 +1,71 @@
1
+
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { scanProject } from "./scanner.js";
5
+
6
+ // Import all project checks.
7
+ import { checkPackageJson } from "./rules/package-json.js";
8
+ import { checkPackageName } from "./rules/package-name.js";
9
+ import { checkPackageVersion } from "./rules/package-version.js";
10
+ import { checkDescription } from "./rules/description.js";
11
+ import { checkReadme } from "./rules/readme.js";
12
+ import { checkGitignore } from "./rules/gitignore.js";
13
+ import { checkLicense } from "./rules/license.js";
14
+ import { checkTestScript } from "./rules/test-script.js";
15
+ import { checkRepository } from "./rules/repository.js";
16
+ import { checkNodeVersion } from "./rules/node-version.js";
17
+
18
+ const rules = [
19
+ checkPackageJson,
20
+ checkPackageName,
21
+ checkPackageVersion,
22
+ checkDescription,
23
+ checkReadme,
24
+ checkGitignore,
25
+ checkLicense,
26
+ checkTestScript,
27
+ checkRepository,
28
+ checkNodeVersion
29
+ ];
30
+
31
+ // Inspect a project and return its complete health report.
32
+ export async function inspectProject(projectPath = ".") {
33
+ if (typeof projectPath !== "string" || !projectPath.trim()) {
34
+ throw new TypeError("Project path must be a non-empty string");
35
+ }
36
+
37
+ const absolutePath = path.resolve(projectPath);
38
+
39
+ // Reject missing paths and paths that are not directories.
40
+ let stats;
41
+
42
+ try {
43
+ stats = await import("node:fs/promises").then(
44
+ ({ stat }) => stat(absolutePath)
45
+ );
46
+ } catch {
47
+ throw new Error(`Project path does not exist: ${absolutePath}`);
48
+ }
49
+
50
+ if (!stats.isDirectory()) {
51
+ throw new Error(`Project path is not a directory: ${absolutePath}`);
52
+ }
53
+
54
+ const findings = await scanProject(absolutePath, rules);
55
+
56
+ const summary = {
57
+ total: findings.length,
58
+ passed: findings.filter((item) => item.status === "pass").length,
59
+ warnings: findings.filter((item) => item.status === "warn").length,
60
+ failed: findings.filter((item) => item.status === "fail").length
61
+ };
62
+
63
+ return {
64
+ path: absolutePath,
65
+ summary,
66
+ findings
67
+ };
68
+ }
69
+
70
+ // Allow the package to be imported without running the CLI.
71
+ export { rules };
package/src/package.js ADDED
@@ -0,0 +1,19 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ // Read and validate the project's package.json.
5
+ export async function readPackageJson(projectPath) {
6
+ const packagePath = path.join(projectPath, "package.json");
7
+ const content = await fs.readFile(packagePath, "utf8");
8
+ const data = JSON.parse(content);
9
+
10
+ if (
11
+ data === null ||
12
+ typeof data !== "object" ||
13
+ Array.isArray(data)
14
+ ) {
15
+ throw new Error("package.json must contain a JSON object");
16
+ }
17
+
18
+ return data;
19
+ }
@@ -0,0 +1,25 @@
1
+
2
+ import { createFinding, createSkippedFinding } from "../finding.js";
3
+
4
+ // Check whether package.json has a description.
5
+ export async function checkDescription(context) {
6
+ if (context.packageJsonError) {
7
+ return createSkippedFinding("description", "description");
8
+ }
9
+
10
+ if (context.packageJson.description) {
11
+ return createFinding(
12
+ "description",
13
+ "description",
14
+ "pass",
15
+ "Package description exists"
16
+ );
17
+ }
18
+
19
+ return createFinding(
20
+ "description",
21
+ "description",
22
+ "warn",
23
+ "Package description is missing"
24
+ );
25
+ }
@@ -0,0 +1,18 @@
1
+
2
+ import path from "node:path";
3
+ import { pathExists } from "../scanner.js";
4
+ import { createFinding } from "../finding.js";
5
+
6
+ // Check whether .gitignore exists.
7
+ export async function checkGitignore(context) {
8
+ const exists = await pathExists(
9
+ path.join(context.projectPath, ".gitignore")
10
+ );
11
+
12
+ return createFinding(
13
+ "gitignore",
14
+ ".gitignore",
15
+ exists ? "pass" : "warn",
16
+ exists ? ".gitignore exists" : ".gitignore is missing"
17
+ );
18
+ }
@@ -0,0 +1,18 @@
1
+
2
+ import path from "node:path";
3
+ import { pathExists } from "../scanner.js";
4
+ import { createFinding } from "../finding.js";
5
+
6
+ // Check whether LICENSE exists.
7
+ export async function checkLicense(context) {
8
+ const exists = await pathExists(
9
+ path.join(context.projectPath, "LICENSE")
10
+ );
11
+
12
+ return createFinding(
13
+ "license",
14
+ "LICENSE",
15
+ exists ? "pass" : "warn",
16
+ exists ? "LICENSE exists" : "LICENSE is missing"
17
+ );
18
+ }
@@ -0,0 +1,21 @@
1
+
2
+ import { createFinding, createSkippedFinding } from "../finding.js";
3
+
4
+ // Check whether a Node.js version requirement is configured.
5
+ export async function checkNodeVersion(context) {
6
+ if (context.packageJsonError) {
7
+ return createSkippedFinding("node-version", "Node.js version");
8
+ }
9
+
10
+ const nodeVersion = context.packageJson.engines?.node?.trim();
11
+ const exists = Boolean(nodeVersion);
12
+
13
+ return createFinding(
14
+ "node-version",
15
+ "Node.js version",
16
+ exists ? "pass" : "warn",
17
+ exists
18
+ ? `Node.js requirement is "${nodeVersion}"`
19
+ : "Node.js version requirement is missing"
20
+ );
21
+ }
@@ -0,0 +1,15 @@
1
+ import { createFinding } from "../finding.js";
2
+
3
+ // Check whether package.json exists and contains valid JSON.
4
+ export async function checkPackageJson(context) {
5
+ const valid = !context.packageJsonError;
6
+
7
+ return createFinding(
8
+ "package-json",
9
+ "package.json",
10
+ valid ? "pass" : "fail",
11
+ valid
12
+ ? "package.json exists and is valid"
13
+ : "package.json is missing or invalid"
14
+ );
15
+ }
@@ -0,0 +1,27 @@
1
+
2
+ import { createFinding, createSkippedFinding } from "../finding.js";
3
+
4
+ // Check whether package.json has a name.
5
+ export async function checkPackageName(context) {
6
+ const { packageJson, packageJsonError } = context;
7
+
8
+ if (context.packageJsonError) {
9
+ return createSkippedFinding("package-name", "package name");
10
+ }
11
+
12
+ if (packageJson.name) {
13
+ return createFinding(
14
+ "package-name",
15
+ "package name",
16
+ "pass",
17
+ `Package name is "${packageJson.name}"`
18
+ );
19
+ }
20
+
21
+ return createFinding(
22
+ "package-name",
23
+ "package name",
24
+ "fail",
25
+ "Package name is missing"
26
+ );
27
+ }
@@ -0,0 +1,27 @@
1
+
2
+ import { createFinding, createSkippedFinding } from "../finding.js";
3
+
4
+ // Check whether package.json has a version.
5
+ export async function checkPackageVersion(context) {
6
+ const { packageJson, packageJsonError } = context;
7
+
8
+ if (packageJsonError) {
9
+ return createSkippedFinding("package-version", "package version");
10
+ }
11
+
12
+ if (packageJson.version) {
13
+ return createFinding(
14
+ "package-version",
15
+ "package version",
16
+ "pass",
17
+ `Package version is "${packageJson.version}"`
18
+ );
19
+ }
20
+
21
+ return createFinding(
22
+ "package-version",
23
+ "package version",
24
+ "fail",
25
+ "Package version is missing"
26
+ );
27
+ }
@@ -0,0 +1,18 @@
1
+
2
+ import path from "node:path";
3
+ import { pathExists } from "../scanner.js";
4
+ import { createFinding } from "../finding.js";
5
+
6
+ // Check whether README.md exists.
7
+ export async function checkReadme(context) {
8
+ const exists = await pathExists(
9
+ path.join(context.projectPath, "README.md")
10
+ );
11
+
12
+ return createFinding(
13
+ "readme",
14
+ "README",
15
+ exists ? "pass" : "warn",
16
+ exists ? "README.md exists" : "README.md is missing"
17
+ );
18
+ }
@@ -0,0 +1,26 @@
1
+
2
+ import { createFinding, createSkippedFinding } from "../finding.js";
3
+
4
+ // Check whether repository metadata is configured.
5
+ export async function checkRepository(context) {
6
+ if (context.packageJsonError) {
7
+ return createSkippedFinding("repository", "repository");
8
+ }
9
+
10
+ const repository = context.packageJson.repository;
11
+
12
+ const exists = Boolean(
13
+ typeof repository === "string"
14
+ ? repository.trim()
15
+ : repository?.url?.trim()
16
+ );
17
+
18
+ return createFinding(
19
+ "repository",
20
+ "repository",
21
+ exists ? "pass" : "warn",
22
+ exists
23
+ ? "Repository metadata exists"
24
+ : "Repository metadata is missing"
25
+ );
26
+ }
@@ -0,0 +1,18 @@
1
+
2
+ import { createFinding, createSkippedFinding } from "../finding.js";
3
+
4
+ // Check whether a test script is configured.
5
+ export async function checkTestScript(context) {
6
+ if (context.packageJsonError) {
7
+ return createSkippedFinding("test-script", "test script");
8
+ }
9
+
10
+ const exists = Boolean(context.packageJson.scripts?.test);
11
+
12
+ return createFinding(
13
+ "test-script",
14
+ "test script",
15
+ exists ? "pass" : "warn",
16
+ exists ? "Test script exists" : "Test script is missing"
17
+ );
18
+ }
package/src/scanner.js ADDED
@@ -0,0 +1,45 @@
1
+
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { readPackageJson } from "./package.js";
5
+
6
+ // Check whether a path exists.
7
+ export async function pathExists(filePath) {
8
+ try {
9
+ await fs.access(filePath);
10
+ return true;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ // Load reusable project data once.
17
+ export async function createProjectContext(projectPath) {
18
+ const absolutePath = path.resolve(projectPath);
19
+ let packageJson = null;
20
+ let packageJsonError = null;
21
+
22
+ try {
23
+ packageJson = await readPackageJson(absolutePath);
24
+ } catch (error) {
25
+ packageJsonError = error;
26
+ }
27
+
28
+ return {
29
+ projectPath: absolutePath,
30
+ packageJson,
31
+ packageJsonError
32
+ };
33
+ }
34
+
35
+ // Run all checks using the same project context.
36
+ export async function scanProject(projectPath, rules) {
37
+ const context = await createProjectContext(projectPath);
38
+ const findings = [];
39
+
40
+ for (const rule of rules) {
41
+ findings.push(await rule(context));
42
+ }
43
+
44
+ return findings;
45
+ }