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 +30 -0
- package/package.json +1 -1
- package/src/github-protection.ts +1 -1
- package/src/service-diagnostics.test.ts +46 -0
- package/src/service-diagnostics.ts +170 -0
- package/src/service.ts +30 -1
- package/templates/shared/README.md +1 -1
- package/templates/targets/workers/README.md +1 -1
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
package/src/github-protection.ts
CHANGED
|
@@ -41,7 +41,7 @@ export type BranchProtectionRequest = {
|
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
const DEFAULT_BRANCH = "main";
|
|
44
|
-
const DEFAULT_REQUIRED_CHECKS = ["test"
|
|
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
|
|
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
|
|
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
|
|
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.
|