create-svc 0.1.79 → 0.1.80
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 +30 -0
- package/package.json +1 -1
- package/src/service-diagnostics.test.ts +46 -0
- package/src/service-diagnostics.ts +170 -0
- package/src/service.ts +29 -0
package/README.md
CHANGED
|
@@ -17,6 +17,36 @@ Local provisioning intentionally prefers known-good CLIs over SDK-heavy orchestr
|
|
|
17
17
|
|
|
18
18
|
npm: <https://www.npmjs.com/package/create-svc>
|
|
19
19
|
|
|
20
|
+
## Install and update
|
|
21
|
+
|
|
22
|
+
For an installed CLI, use npm as the canonical owner:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -g create-svc@latest
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
That installs the `service`, `create-svc`, and `create-service` commands from the same package. Check the binary that is actually running:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
service --version
|
|
32
|
+
service doctor
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`service doctor` reports the active binary path, package root, installed package version, npm latest version, and any other `service` binaries on `PATH`. If a stale Homebrew or manually copied binary is shadowing npm, remove that stale binary and reinstall npm latest:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
which -a service
|
|
39
|
+
rm "/opt/homebrew/bin/service"
|
|
40
|
+
npm install -g create-svc@latest
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If the stale binary came from a global npm install you no longer want, remove and reinstall it through npm instead:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm uninstall -g create-svc
|
|
47
|
+
npm install -g create-svc@latest
|
|
48
|
+
```
|
|
49
|
+
|
|
20
50
|
## Usage
|
|
21
51
|
|
|
22
52
|
```bash
|
package/package.json
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { buildServiceDoctorReport, findServiceBinaries } from "./service-diagnostics";
|
|
3
|
+
|
|
4
|
+
test("findServiceBinaries finds service commands on a provided PATH", () => {
|
|
5
|
+
const binaries = findServiceBinaries({
|
|
6
|
+
pathEnv: ["/old/bin", "/fresh/bin", "/missing/bin"].join(":"),
|
|
7
|
+
isExecutable: (path) => path === "/old/bin/service" || path === "/fresh/bin/service",
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
expect(binaries).toEqual(["/old/bin/service", "/fresh/bin/service"]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("buildServiceDoctorReport includes the active binary, package root, and version", () => {
|
|
14
|
+
const report = buildServiceDoctorReport({
|
|
15
|
+
activeBinaryPath: "/fresh/bin/service",
|
|
16
|
+
packageRoot: "/fresh/lib/node_modules/create-svc",
|
|
17
|
+
packageVersion: "0.1.77",
|
|
18
|
+
latestVersion: "0.1.77",
|
|
19
|
+
serviceBinaries: ["/fresh/bin/service"],
|
|
20
|
+
getBinaryVersion: () => "0.1.77",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(report.exitCode).toBe(0);
|
|
24
|
+
expect(report.text).toContain("active binary: /fresh/bin/service");
|
|
25
|
+
expect(report.text).toContain("package root: /fresh/lib/node_modules/create-svc");
|
|
26
|
+
expect(report.text).toContain("package version: 0.1.77");
|
|
27
|
+
expect(report.text).toContain("npm latest: 0.1.77");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("buildServiceDoctorReport warns when stale service binaries are also on PATH", () => {
|
|
31
|
+
const report = buildServiceDoctorReport({
|
|
32
|
+
activeBinaryPath: "/fresh/bin/service",
|
|
33
|
+
packageRoot: "/fresh/lib/node_modules/create-svc",
|
|
34
|
+
packageVersion: "0.1.77",
|
|
35
|
+
latestVersion: "0.1.77",
|
|
36
|
+
serviceBinaries: ["/opt/homebrew/bin/service", "/fresh/bin/service"],
|
|
37
|
+
getBinaryVersion: (path) => (path === "/opt/homebrew/bin/service" ? "0.1.10" : "0.1.77"),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(report.exitCode).toBe(1);
|
|
41
|
+
expect(report.text).toContain("warning: multiple service binaries found on PATH");
|
|
42
|
+
expect(report.text).toContain("/opt/homebrew/bin/service");
|
|
43
|
+
expect(report.text).toContain("version: 0.1.10 (stale)");
|
|
44
|
+
expect(report.text).toContain('cleanup: rm "/opt/homebrew/bin/service"');
|
|
45
|
+
expect(report.text).toContain("update: npm install -g create-svc@latest");
|
|
46
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { accessSync, constants, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
type FindServiceBinariesOptions = {
|
|
6
|
+
pathEnv?: string;
|
|
7
|
+
commandName?: string;
|
|
8
|
+
isExecutable?: (path: string) => boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type BuildServiceDoctorReportOptions = {
|
|
12
|
+
activeBinaryPath: string;
|
|
13
|
+
packageRoot: string;
|
|
14
|
+
packageVersion: string;
|
|
15
|
+
latestVersion?: string;
|
|
16
|
+
latestVersionError?: string;
|
|
17
|
+
serviceBinaries: string[];
|
|
18
|
+
getBinaryVersion: (path: string) => string | undefined;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type DoctorReport = {
|
|
22
|
+
exitCode: number;
|
|
23
|
+
text: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const PACKAGE_NAME = "create-svc";
|
|
27
|
+
const SERVICE_COMMAND = "service";
|
|
28
|
+
|
|
29
|
+
export function packageRootFromModuleUrl(moduleUrl: string) {
|
|
30
|
+
return resolve(dirname(fileURLToPath(moduleUrl)), "..");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readPackageVersion(packageRoot: string) {
|
|
34
|
+
const packageJson = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")) as { version?: string };
|
|
35
|
+
return packageJson.version ?? "unknown";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function findServiceBinaries(options: FindServiceBinariesOptions = {}) {
|
|
39
|
+
const commandName = options.commandName ?? SERVICE_COMMAND;
|
|
40
|
+
const isExecutable = options.isExecutable ?? defaultIsExecutable;
|
|
41
|
+
const seen = new Set<string>();
|
|
42
|
+
const results: string[] = [];
|
|
43
|
+
|
|
44
|
+
for (const entry of (options.pathEnv ?? process.env.PATH ?? "").split(":")) {
|
|
45
|
+
if (!entry) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const candidate = join(entry, commandName);
|
|
50
|
+
if (seen.has(candidate)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
seen.add(candidate);
|
|
54
|
+
|
|
55
|
+
if (isExecutable(candidate)) {
|
|
56
|
+
results.push(candidate);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return results;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function buildServiceDoctorReport(options: BuildServiceDoctorReportOptions): DoctorReport {
|
|
64
|
+
const lines = [
|
|
65
|
+
"service doctor",
|
|
66
|
+
`active binary: ${options.activeBinaryPath}`,
|
|
67
|
+
`package root: ${options.packageRoot}`,
|
|
68
|
+
`package version: ${options.packageVersion}`,
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
if (options.latestVersion) {
|
|
72
|
+
lines.push(`npm latest: ${options.latestVersion}`);
|
|
73
|
+
} else if (options.latestVersionError) {
|
|
74
|
+
lines.push(`npm latest: unavailable (${options.latestVersionError})`);
|
|
75
|
+
} else {
|
|
76
|
+
lines.push("npm latest: unavailable");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const binaries = uniquePaths(options.serviceBinaries);
|
|
80
|
+
const binaryDiagnostics = binaries.map((path) => {
|
|
81
|
+
const version = options.getBinaryVersion(path);
|
|
82
|
+
const stale = Boolean(version && options.latestVersion && compareVersions(version, options.latestVersion) < 0);
|
|
83
|
+
return { path, version, stale };
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const staleBinaries = binaryDiagnostics.filter((binary) => binary.stale);
|
|
87
|
+
if (binaryDiagnostics.length > 1) {
|
|
88
|
+
lines.push("");
|
|
89
|
+
lines.push("warning: multiple service binaries found on PATH");
|
|
90
|
+
for (const binary of binaryDiagnostics) {
|
|
91
|
+
const version = binary.version ?? "unknown";
|
|
92
|
+
const state = binary.stale ? "stale" : binary.version && options.latestVersion ? "current" : "unknown";
|
|
93
|
+
lines.push(`- ${binary.path}`);
|
|
94
|
+
lines.push(` version: ${version} (${state})`);
|
|
95
|
+
if (binary.stale) {
|
|
96
|
+
lines.push(` cleanup: rm "${binary.path}"`);
|
|
97
|
+
lines.push(` update: npm install -g ${PACKAGE_NAME}@latest`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
exitCode: staleBinaries.length > 0 ? 1 : 0,
|
|
104
|
+
text: lines.join("\n"),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getInstalledServiceVersion(binaryPath: string) {
|
|
109
|
+
const result = safeSpawn([binaryPath, "--version"]);
|
|
110
|
+
|
|
111
|
+
if (!result || !result.success || !result.stdout) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return new TextDecoder().decode(result.stdout).trim().match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/)?.[0];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function getNpmLatestVersion() {
|
|
119
|
+
const result = safeSpawn(["npm", "view", `${PACKAGE_NAME}@latest`, "version"]);
|
|
120
|
+
|
|
121
|
+
if (!result || !result.success || !result.stdout) {
|
|
122
|
+
const stderr = result?.stderr ? new TextDecoder().decode(result.stderr).trim() : "npm view failed";
|
|
123
|
+
return { error: stderr || "npm view failed" };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { version: new TextDecoder().decode(result.stdout).trim() };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function safeSpawn(command: string[]) {
|
|
130
|
+
try {
|
|
131
|
+
return Bun.spawnSync(command, {
|
|
132
|
+
stdout: "pipe",
|
|
133
|
+
stderr: "pipe",
|
|
134
|
+
});
|
|
135
|
+
} catch {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function compareVersions(left: string, right: string) {
|
|
141
|
+
const leftParts = parseVersion(left);
|
|
142
|
+
const rightParts = parseVersion(right);
|
|
143
|
+
|
|
144
|
+
for (let i = 0; i < 3; i += 1) {
|
|
145
|
+
const diff = (leftParts[i] ?? 0) - (rightParts[i] ?? 0);
|
|
146
|
+
if (diff !== 0) {
|
|
147
|
+
return diff;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function parseVersion(version: string): [number, number, number] {
|
|
155
|
+
const [major = "0", minor = "0", patch = "0"] = version.split(/[.-]/, 3);
|
|
156
|
+
return [Number(major) || 0, Number(minor) || 0, Number(patch) || 0];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function uniquePaths(paths: string[]) {
|
|
160
|
+
return [...new Set(paths)];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function defaultIsExecutable(path: string) {
|
|
164
|
+
try {
|
|
165
|
+
accessSync(path, constants.X_OK);
|
|
166
|
+
return true;
|
|
167
|
+
} catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
package/src/service.ts
CHANGED
|
@@ -3,6 +3,13 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
import { formatScaffoldHelp, run as runScaffoldCli } from "./cli";
|
|
4
4
|
import { parseProtectMainArgs, protectMainBranch } from "./github-protection";
|
|
5
5
|
import { parseJsonc } from "./jsonc";
|
|
6
|
+
import {
|
|
7
|
+
buildServiceDoctorReport,
|
|
8
|
+
findServiceBinaries,
|
|
9
|
+
getInstalledServiceVersion,
|
|
10
|
+
getNpmLatestVersion,
|
|
11
|
+
packageRootFromModuleUrl,
|
|
12
|
+
} from "./service-diagnostics";
|
|
6
13
|
|
|
7
14
|
const SCAFFOLD_COMMANDS = new Set(["create", "new", "init"]);
|
|
8
15
|
const GENERATED_SERVICE_COMMANDS = new Set([
|
|
@@ -43,6 +50,11 @@ export async function runServiceCommand(argv: string[], cwd = process.cwd()) {
|
|
|
43
50
|
return;
|
|
44
51
|
}
|
|
45
52
|
|
|
53
|
+
if (command === "doctor") {
|
|
54
|
+
runGlobalServiceDoctor();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
console.error(formatOutsideServiceCommandError(command));
|
|
47
59
|
process.exit(1);
|
|
48
60
|
}
|
|
@@ -56,6 +68,23 @@ export function createSvcVersion() {
|
|
|
56
68
|
return packageJson.version || "unknown";
|
|
57
69
|
}
|
|
58
70
|
|
|
71
|
+
function runGlobalServiceDoctor() {
|
|
72
|
+
const latest = getNpmLatestVersion();
|
|
73
|
+
const report = buildServiceDoctorReport({
|
|
74
|
+
activeBinaryPath: process.argv[1] || "service",
|
|
75
|
+
packageRoot: packageRootFromModuleUrl(import.meta.url),
|
|
76
|
+
packageVersion: createSvcVersion(),
|
|
77
|
+
latestVersion: latest.version,
|
|
78
|
+
latestVersionError: latest.error,
|
|
79
|
+
serviceBinaries: findServiceBinaries(),
|
|
80
|
+
getBinaryVersion: getInstalledServiceVersion,
|
|
81
|
+
});
|
|
82
|
+
console.log(report.text);
|
|
83
|
+
if (report.exitCode !== 0) {
|
|
84
|
+
process.exit(report.exitCode);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
59
88
|
export function normalizeScaffoldArgs(argv: string[]) {
|
|
60
89
|
const [command, ...rest] = argv;
|
|
61
90
|
if (command && SCAFFOLD_COMMANDS.has(command)) {
|