@restormel/doctor 0.1.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,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Allotment Technology Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # @restormel/doctor
2
+
3
+ Restormel Doctor is an **open-source** CLI that checks your local setup and surfaces actionable “what to fix next” issues. It is designed to be the **entrypoint** into the Restormel Platform.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npx @restormel/doctor
9
+ ```
10
+
11
+ Or install globally / in a repo:
12
+
13
+ ```bash
14
+ pnpm add -D @restormel/doctor
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```bash
20
+ restormel-doctor
21
+ ```
22
+
23
+ ### Output formats
24
+
25
+ - **Text (default)**:
26
+
27
+ ```bash
28
+ restormel-doctor
29
+ ```
30
+
31
+ - **JSON (for CI/artifacts)**:
32
+
33
+ ```bash
34
+ restormel-doctor --format json
35
+ restormel-doctor --format json --out doctor.json
36
+ ```
37
+
38
+ ### Exit codes
39
+
40
+ - `0`: no blocking issues found
41
+ - `1`: blocking issues found
42
+ - `2`: usage/config error (unexpected failure)
43
+
44
+ ## What it checks (v1)
45
+
46
+ - Framework detection (Next.js / React / SvelteKit / Astro)
47
+ - `restormel.config.json` presence + parseability
48
+ - Suggested Restormel packages installed
49
+ - Local key store presence (keys are never printed raw)
50
+
51
+ ## Relationship to Restormel Keys
52
+
53
+ - `@restormel/doctor` is standalone and open source.
54
+ - Restormel Keys (dashboard) will expose a **Pro** Healthcheck UI that aggregates Doctor + Validate signals across integrations, models, routes, and policies.
55
+
@@ -0,0 +1,6 @@
1
+ export declare const CONFIG_FILENAME = "restormel.config.json";
2
+ export interface RestormelConfig {
3
+ framework?: string;
4
+ providers?: string[];
5
+ }
6
+ export declare function readConfig(cwd: string): Promise<RestormelConfig | null>;
package/dist/config.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Project config for Restormel. No secrets — only framework and provider list.
3
+ */
4
+ import { readFile } from "fs/promises";
5
+ import { existsSync } from "fs";
6
+ import { join } from "path";
7
+ export const CONFIG_FILENAME = "restormel.config.json";
8
+ const DEFAULT_CONFIG = {
9
+ framework: "none",
10
+ providers: [],
11
+ };
12
+ export async function readConfig(cwd) {
13
+ const path = join(cwd, CONFIG_FILENAME);
14
+ if (!existsSync(path))
15
+ return null;
16
+ try {
17
+ const raw = await readFile(path, "utf-8");
18
+ const data = JSON.parse(raw);
19
+ return { ...DEFAULT_CONFIG, ...data };
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
@@ -0,0 +1,8 @@
1
+ export type FrameworkId = "next" | "react" | "sveltekit" | "astro" | "none";
2
+ export interface DetectedFramework {
3
+ id: FrameworkId;
4
+ name: string;
5
+ hasAppRouter?: boolean;
6
+ packagePaths: string[];
7
+ }
8
+ export declare function detectFramework(cwd: string): Promise<DetectedFramework>;
package/dist/detect.js ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Framework and stack detection for Doctor.
3
+ * Supports: Next.js App Router, generic React, SvelteKit, Astro.
4
+ */
5
+ import { readFile } from "fs/promises";
6
+ import { existsSync } from "fs";
7
+ import { join } from "path";
8
+ const NEXT_APP_ROUTER_MARKERS = ["app/layout.tsx", "app/layout.js", "app/page.tsx", "app/page.js"];
9
+ export async function detectFramework(cwd) {
10
+ const pkgPath = join(cwd, "package.json");
11
+ if (!existsSync(pkgPath)) {
12
+ return { id: "none", name: "None", packagePaths: [] };
13
+ }
14
+ const raw = await readFile(pkgPath, "utf-8");
15
+ let pkg;
16
+ try {
17
+ pkg = JSON.parse(raw);
18
+ }
19
+ catch {
20
+ return { id: "none", name: "None", packagePaths: [] };
21
+ }
22
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
23
+ if (deps["next"]) {
24
+ const hasAppRouter = NEXT_APP_ROUTER_MARKERS.some((m) => existsSync(join(cwd, m)));
25
+ return {
26
+ id: "next",
27
+ name: hasAppRouter ? "Next.js (App Router)" : "Next.js",
28
+ hasAppRouter,
29
+ packagePaths: ["@restormel/keys", "@restormel/keys-react"],
30
+ };
31
+ }
32
+ if (deps["@sveltejs/kit"]) {
33
+ return {
34
+ id: "sveltekit",
35
+ name: "SvelteKit",
36
+ packagePaths: ["@restormel/keys", "@restormel/keys-svelte"],
37
+ };
38
+ }
39
+ if (deps["astro"]) {
40
+ return {
41
+ id: "astro",
42
+ name: "Astro",
43
+ packagePaths: ["@restormel/keys", "@restormel/keys-elements"],
44
+ };
45
+ }
46
+ if (deps["react"] || deps["react-dom"]) {
47
+ return {
48
+ id: "react",
49
+ name: "React",
50
+ packagePaths: ["@restormel/keys", "@restormel/keys-react"],
51
+ };
52
+ }
53
+ return { id: "none", name: "None", packagePaths: ["@restormel/keys"] };
54
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @restormel/doctor — standalone OSS healthcheck entrypoint.
4
+ */
5
+ import { Command } from "commander";
6
+ import chalk from "chalk";
7
+ import { existsSync } from "fs";
8
+ import { join } from "path";
9
+ import { detectFramework } from "./detect.js";
10
+ import { readConfig, CONFIG_FILENAME } from "./config.js";
11
+ import { readStore } from "./store.js";
12
+ function toExitCode(report) {
13
+ return report.ok ? 0 : 1;
14
+ }
15
+ function writeOutput(report, format) {
16
+ if (format === "json") {
17
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
18
+ return;
19
+ }
20
+ console.log(chalk.cyan("Restormel Doctor"));
21
+ console.log("");
22
+ for (const c of report.checks) {
23
+ const icon = c.status === "ok" ? chalk.green("✓") : c.status === "warn" ? chalk.yellow("○") : chalk.red("✗");
24
+ const label = chalk.white(c.label);
25
+ const msg = c.message ? chalk.gray(`— ${c.message}`) : "";
26
+ console.log(`${icon} ${label} ${msg}`.trimEnd());
27
+ }
28
+ console.log("");
29
+ console.log(report.ok ? chalk.green("OK") : chalk.red("Issues found"));
30
+ }
31
+ async function runDoctor() {
32
+ const cwd = process.cwd();
33
+ const checks = [];
34
+ const detected = await detectFramework(cwd);
35
+ checks.push({
36
+ id: "framework",
37
+ label: "Framework detection",
38
+ status: detected.id === "none" ? "warn" : "ok",
39
+ message: detected.name,
40
+ details: { id: detected.id, hasAppRouter: detected.hasAppRouter ?? false },
41
+ });
42
+ const config = await readConfig(cwd);
43
+ checks.push({
44
+ id: "config",
45
+ label: CONFIG_FILENAME,
46
+ status: config ? "ok" : "fail",
47
+ message: config ? "found" : "not found (run keys init)",
48
+ details: config ?? undefined,
49
+ });
50
+ const missingPkgs = [];
51
+ for (const p of detected.packagePaths) {
52
+ const pkgPath = join(cwd, "node_modules", p);
53
+ const found = existsSync(pkgPath);
54
+ if (!found)
55
+ missingPkgs.push(p);
56
+ }
57
+ checks.push({
58
+ id: "packages",
59
+ label: "Suggested Restormel packages",
60
+ status: missingPkgs.length === 0 ? "ok" : "fail",
61
+ message: missingPkgs.length === 0 ? "installed" : `missing: ${missingPkgs.join(", ")}`,
62
+ details: { suggested: detected.packagePaths, missing: missingPkgs },
63
+ });
64
+ const store = await readStore(cwd);
65
+ checks.push({
66
+ id: "keys",
67
+ label: "Local key store",
68
+ status: store.keys.length === 0 ? "warn" : "ok",
69
+ message: store.keys.length === 0 ? "no keys stored" : `${store.keys.length} key(s) stored`,
70
+ details: {
71
+ keys: store.keys.map((k) => ({ id: k.id, provider: k.provider, mask: k.mask, label: k.label })),
72
+ },
73
+ });
74
+ const ok = checks.every((c) => c.status !== "fail");
75
+ return { ok, cwd, checks };
76
+ }
77
+ async function main() {
78
+ const program = new Command();
79
+ program
80
+ .name("restormel-doctor")
81
+ .description("Restormel Doctor — open-source CLI for setup and health checks")
82
+ .version("0.1.0")
83
+ .option("--format <format>", "Output format: text|json", "text")
84
+ .option("--out <path>", "Write JSON output to a file (requires --format json)");
85
+ program.parse();
86
+ const opts = program.opts();
87
+ const format = (opts.format ?? "text");
88
+ if (format !== "text" && format !== "json") {
89
+ console.error(chalk.red("Invalid --format. Use text or json."));
90
+ process.exit(2);
91
+ }
92
+ if (opts.out && format !== "json") {
93
+ console.error(chalk.red("--out requires --format json."));
94
+ process.exit(2);
95
+ }
96
+ try {
97
+ const report = await runDoctor();
98
+ if (opts.out) {
99
+ const { writeFile } = await import("fs/promises");
100
+ await writeFile(opts.out, JSON.stringify(report, null, 2) + "\n", "utf-8");
101
+ }
102
+ writeOutput(report, format);
103
+ process.exit(toExitCode(report));
104
+ }
105
+ catch (e) {
106
+ console.error(chalk.red("Doctor failed:"), e instanceof Error ? e.message : String(e));
107
+ process.exit(2);
108
+ }
109
+ }
110
+ void main();
@@ -0,0 +1,15 @@
1
+ export declare const STORE_DIR = ".restormel";
2
+ export declare const STORE_FILENAME = "key-store.json";
3
+ export interface StoredKey {
4
+ id: string;
5
+ provider: string;
6
+ label?: string;
7
+ /** Masked for display only (e.g. sk-...abc). Never log or expose. */
8
+ mask?: string;
9
+ }
10
+ export interface KeyStoreData {
11
+ keys: Array<StoredKey & {
12
+ apiKey: string;
13
+ }>;
14
+ }
15
+ export declare function readStore(cwd: string): Promise<KeyStoreData>;
package/dist/store.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Local key store. Lives under .restormel/ and must be in .gitignore.
3
+ * Contains secrets; never commit. Doctor never prints raw keys.
4
+ */
5
+ import { readFile } from "fs/promises";
6
+ import { existsSync } from "fs";
7
+ import { join } from "path";
8
+ export const STORE_DIR = ".restormel";
9
+ export const STORE_FILENAME = "key-store.json";
10
+ function storePath(cwd) {
11
+ return join(cwd, STORE_DIR, STORE_FILENAME);
12
+ }
13
+ export async function readStore(cwd) {
14
+ const path = storePath(cwd);
15
+ if (!existsSync(path))
16
+ return { keys: [] };
17
+ try {
18
+ const raw = await readFile(path, "utf-8");
19
+ const data = JSON.parse(raw);
20
+ return Array.isArray(data.keys) ? data : { keys: [] };
21
+ }
22
+ catch {
23
+ return { keys: [] };
24
+ }
25
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@restormel/doctor",
3
+ "version": "0.1.0",
4
+ "description": "Restormel Doctor — open-source CLI for setup and health checks.",
5
+ "keywords": [
6
+ "restormel",
7
+ "doctor",
8
+ "healthcheck",
9
+ "cli",
10
+ "byok",
11
+ "llm"
12
+ ],
13
+ "author": "Allotment Technology Ltd",
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/Allotment-Technology-Ltd/restormel-keys.git"
18
+ },
19
+ "homepage": "https://github.com/Allotment-Technology-Ltd/restormel-keys#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/Allotment-Technology-Ltd/restormel-keys/issues"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "type": "module",
27
+ "main": "dist/index.js",
28
+ "types": "dist/index.d.ts",
29
+ "bin": {
30
+ "restormel-doctor": "./dist/index.js"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsc",
39
+ "dev": "tsc --watch",
40
+ "typecheck": "tsc --noEmit",
41
+ "test": "vitest run"
42
+ },
43
+ "dependencies": {
44
+ "chalk": "^5.3.0",
45
+ "commander": "^12.1.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^25.5.0",
49
+ "typescript": "^5.7.2",
50
+ "vitest": "^4.1.0"
51
+ },
52
+ "engines": {
53
+ "node": ">=18"
54
+ }
55
+ }