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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-svc",
3
- "version": "0.1.79",
3
+ "version": "0.1.80",
4
4
  "description": "Local microservice bootstrap CLI for Cloud Run and Workers services with Neon-backed data.",
5
5
  "module": "index.ts",
6
6
  "type": "module",
@@ -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)) {