create-svc 0.1.79 → 0.1.81

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.81",
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",
@@ -41,7 +41,7 @@ export type BranchProtectionRequest = {
41
41
  };
42
42
 
43
43
  const DEFAULT_BRANCH = "main";
44
- const DEFAULT_REQUIRED_CHECKS = ["test", "deploy"];
44
+ const DEFAULT_REQUIRED_CHECKS = ["test"];
45
45
  const decoder = new TextDecoder();
46
46
  const encoder = new TextEncoder();
47
47
 
@@ -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)) {
@@ -180,7 +209,7 @@ export function generatedServiceCommandHelp(argv: string[]) {
180
209
  "Usage:",
181
210
  " service protect-main [--repo owner/name] [--branch main]",
182
211
  "",
183
- "Reconciles generated service branch protection with required test and deploy checks.",
212
+ "Reconciles generated service branch protection with required pull request checks.",
184
213
  ].join("\n");
185
214
  }
186
215
 
@@ -154,7 +154,7 @@ Generated repositories include GitHub Actions workflows for CI and production de
154
154
  {{COMMAND_PROTECT_MAIN}}
155
155
  ```
156
156
 
157
- The protection requires the generated `test` and `deploy` status checks, pull requests, stale-review dismissal, conversation resolution, and admin enforcement. If GitHub permissions are missing, rerun the command with a token that has repo admin access or fine-grained `Administration: write`.
157
+ The protection requires the generated pull request `test` status check, pull requests, stale-review dismissal, conversation resolution, and admin enforcement. If GitHub permissions are missing, rerun the command with a token that has repo admin access or fine-grained `Administration: write`.
158
158
 
159
159
  Go variants use Atlas for migrations:
160
160
 
@@ -79,7 +79,7 @@ Generated repositories include GitHub Actions workflows for CI and production de
79
79
  {{COMMAND_PROTECT_MAIN}}
80
80
  ```
81
81
 
82
- The protection requires the generated `test` and `deploy` status checks, pull requests, stale-review dismissal, conversation resolution, and admin enforcement. If GitHub permissions are missing, rerun the command with a token that has repo admin access or fine-grained `Administration: write`.
82
+ The protection requires the generated pull request `test` status check, pull requests, stale-review dismissal, conversation resolution, and admin enforcement. If GitHub permissions are missing, rerun the command with a token that has repo admin access or fine-grained `Administration: write`.
83
83
 
84
84
  The Trigger.dev CLI is installed in this generated package as a dev dependency
85
85
  from the `trigger.dev` npm package.