@youdie006/prodex 0.2.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 +21 -0
- package/README.md +418 -0
- package/dist/banner.d.ts +5 -0
- package/dist/banner.js +47 -0
- package/dist/banner.js.map +1 -0
- package/dist/bundle.d.ts +15 -0
- package/dist/bundle.js +30 -0
- package/dist/bundle.js.map +1 -0
- package/dist/chatgpt-browser.d.ts +119 -0
- package/dist/chatgpt-browser.js +857 -0
- package/dist/chatgpt-browser.js.map +1 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +3502 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +58 -0
- package/dist/config.js +277 -0
- package/dist/config.js.map +1 -0
- package/dist/http-mcp.d.ts +20 -0
- package/dist/http-mcp.js +236 -0
- package/dist/http-mcp.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-tools.d.ts +395 -0
- package/dist/mcp-tools.js +167 -0
- package/dist/mcp-tools.js.map +1 -0
- package/dist/mcp.d.ts +37 -0
- package/dist/mcp.js +229 -0
- package/dist/mcp.js.map +1 -0
- package/dist/repo-write.d.ts +47 -0
- package/dist/repo-write.js +427 -0
- package/dist/repo-write.js.map +1 -0
- package/dist/repo.d.ts +27 -0
- package/dist/repo.js +386 -0
- package/dist/repo.js.map +1 -0
- package/dist/safe-file.d.ts +25 -0
- package/dist/safe-file.js +295 -0
- package/dist/safe-file.js.map +1 -0
- package/dist/schema.d.ts +402 -0
- package/dist/schema.js +109 -0
- package/dist/schema.js.map +1 -0
- package/dist/store.d.ts +157 -0
- package/dist/store.js +1402 -0
- package/dist/store.js.map +1 -0
- package/docs/claude.md +130 -0
- package/docs/clients.md +75 -0
- package/docs/http-mcp.md +223 -0
- package/package.json +67 -0
- package/scripts/release-check.mjs +436 -0
- package/scripts/release-pack.mjs +481 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { chmod, copyFile, lstat, mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
+
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const args = parseArgs(process.argv.slice(2));
|
|
15
|
+
if (args.help) {
|
|
16
|
+
console.log(formatHelp());
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
await releasePack(args);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error(`release pack failed: ${errorMessage(error)}`);
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function releasePack(args) {
|
|
26
|
+
const root = path.resolve(args.root ?? repoRoot);
|
|
27
|
+
const destination = path.resolve(args.packDestination ?? root);
|
|
28
|
+
|
|
29
|
+
const rebuilt = await ensureDistBuiltForSource(root);
|
|
30
|
+
const packageJson = await readPackageJson(root);
|
|
31
|
+
const binPaths = packageBinPaths(packageJson.bin);
|
|
32
|
+
const files = await readPackedFiles(root);
|
|
33
|
+
const staging = await mkdtemp(path.join(tmpdir(), "prodex-release-pack-"));
|
|
34
|
+
let packedTarball;
|
|
35
|
+
try {
|
|
36
|
+
await copyPackedFilesToStaging(root, staging, files, binPaths);
|
|
37
|
+
assertPackedReleaseCheck(files);
|
|
38
|
+
await runReleaseMetadataCheck(staging);
|
|
39
|
+
await mkdir(destination, { recursive: true });
|
|
40
|
+
const { stdout } = await runNpmPack(["pack", "--json", "--ignore-scripts", "--pack-destination", destination], staging, "npm pack");
|
|
41
|
+
packedTarball = resolvePackedTarball(destination, stdout);
|
|
42
|
+
await assertPackedTarballCreated(packedTarball);
|
|
43
|
+
} finally {
|
|
44
|
+
if (!args.keepWorkdir) {
|
|
45
|
+
await rm(staging, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(`release_pack=ok tarball=${packedTarball} file_modes=ok staging=${args.keepWorkdir ? staging : "removed"}`);
|
|
50
|
+
if (rebuilt) console.log("release_pack_build: rebuilt dist from source before packing");
|
|
51
|
+
console.log("release_pack_next: run `npm run release:verify` and `prodex release status` before publishing this tarball.");
|
|
52
|
+
const gitStatus = await readReleaseGitStatus(root);
|
|
53
|
+
console.log(`release_pack_${gitStatus.line}`);
|
|
54
|
+
if (gitStatus.next) console.log(`release_pack_git_next: ${gitStatus.next}`);
|
|
55
|
+
console.log(`release_pack_verify: npm publish --dry-run ${shellQuote(packedTarball)}`);
|
|
56
|
+
if (isReleaseGitReady(gitStatus)) {
|
|
57
|
+
console.log(
|
|
58
|
+
"release_pack_publish_guard: npm publish <tarball> bypasses prepublishOnly; run the release_pack_verify command first, then publish only that verified tarball if it succeeds."
|
|
59
|
+
);
|
|
60
|
+
console.log(`release_pack_publish: npm publish ${shellQuote(packedTarball)}`);
|
|
61
|
+
} else {
|
|
62
|
+
console.log("release_pack_publish_blocked: fix git readiness before npm publish; run `prodex release status`, then rerun release pack after blockers are clear.");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function ensureDistBuiltForSource(root) {
|
|
67
|
+
// A source checkout must rebuild dist/ before packing so the tarball never freezes a stale build:
|
|
68
|
+
// npm pack runs with --ignore-scripts so prepack never fires, and dist/ is gitignored, so neither
|
|
69
|
+
// the pack step nor a clean git tree would catch a stale dist. An installed package ships a prebuilt
|
|
70
|
+
// dist/ but no src/tsconfig, so there is nothing to build and we skip.
|
|
71
|
+
const tsconfigPath = path.join(root, "tsconfig.json");
|
|
72
|
+
const srcDir = path.join(root, "src");
|
|
73
|
+
if (!(await pathExists(tsconfigPath)) || !(await pathExists(srcDir))) return false;
|
|
74
|
+
await execFileAsync(npmCommand, ["run", "build"], { cwd: root, maxBuffer: 20 * 1024 * 1024 });
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function pathExists(target) {
|
|
79
|
+
try {
|
|
80
|
+
await lstat(target);
|
|
81
|
+
return true;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isReleaseGitReady(gitStatus) {
|
|
88
|
+
return gitStatus.line.startsWith("git: ok ");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function assertPackedReleaseCheck(files) {
|
|
92
|
+
const hasReleaseCheck = files.some((file) => normalizePackagePath(file.path) === "scripts/release-check.mjs");
|
|
93
|
+
if (!hasReleaseCheck) {
|
|
94
|
+
throw new Error("packed files must include scripts/release-check.mjs for release metadata validation");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function copyPackedFilesToStaging(root, staging, files, binPaths) {
|
|
99
|
+
for (const file of files) {
|
|
100
|
+
const packagePath = normalizePackagePath(file.path);
|
|
101
|
+
const source = path.join(root, packagePath);
|
|
102
|
+
const sourceRelative = path.relative(root, source);
|
|
103
|
+
if (sourceRelative.startsWith("..") || path.isAbsolute(sourceRelative)) {
|
|
104
|
+
throw new Error(`packed file path escapes package root: ${packagePath}`);
|
|
105
|
+
}
|
|
106
|
+
let sourceStat;
|
|
107
|
+
try {
|
|
108
|
+
sourceStat = await lstat(source);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (isMissingFileError(error)) {
|
|
111
|
+
throw new Error(`packed file listed by npm was not found: ${packagePath}`);
|
|
112
|
+
}
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
|
|
116
|
+
throw new Error(`packed file must be a regular non-symlink file: ${packagePath}`);
|
|
117
|
+
}
|
|
118
|
+
if (sourceStat.nlink > 1) {
|
|
119
|
+
throw new Error(`packed files must not have hard links: ${packagePath}`);
|
|
120
|
+
}
|
|
121
|
+
const target = path.join(staging, packagePath);
|
|
122
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
123
|
+
await copyFile(source, target);
|
|
124
|
+
await chmod(target, binPaths.has(packagePath) ? 0o755 : 0o644);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function readPackedFiles(root) {
|
|
129
|
+
const { stdout } = await runNpmPack(["pack", "--json", "--dry-run", "--ignore-scripts"], root, "npm pack dry-run");
|
|
130
|
+
let entries;
|
|
131
|
+
try {
|
|
132
|
+
entries = JSON.parse(stdout);
|
|
133
|
+
} catch {
|
|
134
|
+
throw new Error("npm pack dry-run did not return valid JSON");
|
|
135
|
+
}
|
|
136
|
+
const files = entries?.[0]?.files;
|
|
137
|
+
if (!Array.isArray(files)) {
|
|
138
|
+
throw new Error("npm pack dry-run did not return a file list");
|
|
139
|
+
}
|
|
140
|
+
for (const file of files) {
|
|
141
|
+
if (typeof file?.path !== "string" || file.path.trim() === "") {
|
|
142
|
+
throw new Error("npm pack dry-run file entry is missing a path");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return files;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function readPackageJson(root) {
|
|
149
|
+
const packageJsonPath = path.join(root, "package.json");
|
|
150
|
+
let raw;
|
|
151
|
+
try {
|
|
152
|
+
raw = await readFile(packageJsonPath, "utf8");
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (isMissingFileError(error)) {
|
|
155
|
+
throw new Error(`release metadata failed: package.json not found at ${packageJsonPath}`);
|
|
156
|
+
}
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
return JSON.parse(raw);
|
|
161
|
+
} catch {
|
|
162
|
+
throw new Error(`release metadata failed: package.json is not valid JSON at ${packageJsonPath}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function resolvePackedTarball(destination, stdout) {
|
|
167
|
+
let entries;
|
|
168
|
+
try {
|
|
169
|
+
entries = JSON.parse(stdout);
|
|
170
|
+
} catch {
|
|
171
|
+
throw new Error("npm pack did not return valid JSON");
|
|
172
|
+
}
|
|
173
|
+
const filename = entries?.[0]?.filename;
|
|
174
|
+
if (typeof filename !== "string" || !filename.endsWith(".tgz")) {
|
|
175
|
+
throw new Error(`could not determine npm pack tarball from output: ${stdout}`);
|
|
176
|
+
}
|
|
177
|
+
const resolvedDestination = path.resolve(destination);
|
|
178
|
+
const tarballPath = path.isAbsolute(filename) ? path.resolve(filename) : path.resolve(resolvedDestination, filename);
|
|
179
|
+
const relative = path.relative(resolvedDestination, tarballPath);
|
|
180
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
181
|
+
throw new Error(`npm pack reported tarball outside pack destination: ${filename}`);
|
|
182
|
+
}
|
|
183
|
+
return tarballPath;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function assertPackedTarballCreated(packedTarball) {
|
|
187
|
+
let tarballStat;
|
|
188
|
+
try {
|
|
189
|
+
tarballStat = await lstat(packedTarball);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (isMissingFileError(error)) {
|
|
192
|
+
throw new Error(`npm pack did not create expected tarball: ${packedTarball}`);
|
|
193
|
+
}
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
if (tarballStat.isSymbolicLink() || !tarballStat.isFile()) {
|
|
197
|
+
throw new Error(`npm pack did not create a regular tarball file: ${packedTarball}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function run(command, commandArgs, cwd) {
|
|
202
|
+
return execFileAsync(command, commandArgs, {
|
|
203
|
+
cwd,
|
|
204
|
+
timeout: 120_000,
|
|
205
|
+
maxBuffer: 20 * 1024 * 1024
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function runNpmPack(commandArgs, cwd, label) {
|
|
210
|
+
try {
|
|
211
|
+
return await run(npmCommand, commandArgs, cwd);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
throw new Error(`${label} failed: ${commandFailureDetail(error)}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function runReleaseMetadataCheck(staging) {
|
|
218
|
+
try {
|
|
219
|
+
await run(process.execPath, [path.join(staging, "scripts", "release-check.mjs"), "--metadata-only", "--root", staging], staging);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
throw new Error(commandFailureDetail(error));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function packageBinPaths(bin) {
|
|
226
|
+
const paths = new Set();
|
|
227
|
+
if (typeof bin === "string") {
|
|
228
|
+
paths.add(normalizePackagePath(bin));
|
|
229
|
+
} else if (bin && typeof bin === "object") {
|
|
230
|
+
for (const value of Object.values(bin)) {
|
|
231
|
+
if (typeof value === "string") paths.add(normalizePackagePath(value));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return paths;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function normalizePackagePath(value) {
|
|
238
|
+
return value.replaceAll("\\", "/").replace(/^\.\/+/, "");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function parseArgs(values) {
|
|
242
|
+
const parsed = {
|
|
243
|
+
root: undefined,
|
|
244
|
+
packDestination: undefined,
|
|
245
|
+
keepWorkdir: false,
|
|
246
|
+
help: false
|
|
247
|
+
};
|
|
248
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
249
|
+
const arg = values[index];
|
|
250
|
+
if (arg === "--help" || arg === "-h") {
|
|
251
|
+
parsed.help = true;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (arg === "--root") {
|
|
255
|
+
const value = values[index + 1];
|
|
256
|
+
if (!value || value.startsWith("-")) {
|
|
257
|
+
throw new Error("release pack flags failed: --root requires a value");
|
|
258
|
+
}
|
|
259
|
+
parsed.root = value;
|
|
260
|
+
index += 1;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (arg === "--pack-destination") {
|
|
264
|
+
const value = values[index + 1];
|
|
265
|
+
if (!value || value.startsWith("-")) {
|
|
266
|
+
throw new Error("release pack flags failed: --pack-destination requires a value");
|
|
267
|
+
}
|
|
268
|
+
parsed.packDestination = value;
|
|
269
|
+
index += 1;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (arg === "--keep-workdir") {
|
|
273
|
+
parsed.keepWorkdir = true;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (arg.startsWith("-")) {
|
|
277
|
+
throw new Error(`release pack flags failed: ${unknownOptionMessage(arg, ["--root", "--pack-destination", "--keep-workdir", "--help"])}`);
|
|
278
|
+
}
|
|
279
|
+
throw new Error(`release pack flags failed: unexpected argument ${arg}`);
|
|
280
|
+
}
|
|
281
|
+
if (!parsed.help && !parsed.packDestination) {
|
|
282
|
+
throw new Error("release pack flags failed: --pack-destination is required");
|
|
283
|
+
}
|
|
284
|
+
return parsed;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function formatHelp() {
|
|
288
|
+
return `Usage: npm run release:pack -- --pack-destination <dir> [options]
|
|
289
|
+
|
|
290
|
+
Create a publish tarball from a temporary staging directory with normalized package file modes.
|
|
291
|
+
|
|
292
|
+
Options:
|
|
293
|
+
--pack-destination <dir> Directory where the .tgz tarball is written
|
|
294
|
+
--root <dir> Package root to pack (default: current prodex checkout)
|
|
295
|
+
--keep-workdir Keep the temporary staging directory for inspection
|
|
296
|
+
--help, -h Show this help text
|
|
297
|
+
`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function unknownOptionMessage(option, candidates) {
|
|
301
|
+
const suggestion = closestFlagSuggestion(option, candidates);
|
|
302
|
+
return `unknown option ${option}${suggestion ? `. Did you mean \`${suggestion}\`?` : ""}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function closestFlagSuggestion(option, candidates) {
|
|
306
|
+
let best;
|
|
307
|
+
for (const candidate of candidates) {
|
|
308
|
+
const distance = editDistance(option, candidate);
|
|
309
|
+
const prefixMatch = candidate.startsWith(option);
|
|
310
|
+
if (!best || prefixMatch || distance < best.distance) {
|
|
311
|
+
best = { candidate, distance, prefixMatch };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return best && (best.prefixMatch || best.distance <= 2) ? best.candidate : undefined;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function editDistance(left, right) {
|
|
318
|
+
const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
319
|
+
const current = Array.from({ length: right.length + 1 }, () => 0);
|
|
320
|
+
for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
|
|
321
|
+
current[0] = leftIndex;
|
|
322
|
+
for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
|
|
323
|
+
const substitutionCost = left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1;
|
|
324
|
+
current[rightIndex] = Math.min(
|
|
325
|
+
previous[rightIndex] + 1,
|
|
326
|
+
current[rightIndex - 1] + 1,
|
|
327
|
+
previous[rightIndex - 1] + substitutionCost
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
previous.splice(0, previous.length, ...current);
|
|
331
|
+
}
|
|
332
|
+
return previous[right.length];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function errorMessage(error) {
|
|
336
|
+
return error instanceof Error ? error.message : String(error);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function shellQuote(value) {
|
|
340
|
+
return /^[A-Za-z0-9_./:@=-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function readReleaseGitStatus(root) {
|
|
344
|
+
try {
|
|
345
|
+
const insideWorkTree = (await gitStdout(root, ["rev-parse", "--is-inside-work-tree"])).trim();
|
|
346
|
+
if (insideWorkTree !== "true") {
|
|
347
|
+
return {
|
|
348
|
+
line: "git: blocked not a git worktree",
|
|
349
|
+
next: "initialize a git repo and commit the release state before public release"
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
} catch {
|
|
353
|
+
return {
|
|
354
|
+
line: "git: blocked not a git worktree",
|
|
355
|
+
next: "initialize a git repo and commit the release state before public release"
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const [branch, commit, statusOutput, branchStatusOutput, remoteOutput, upstream] = await Promise.all([
|
|
360
|
+
gitStdout(root, ["rev-parse", "--abbrev-ref", "HEAD"]).then((value) => value.trim() || "unknown", () => "unknown"),
|
|
361
|
+
gitStdout(root, ["rev-parse", "--short", "HEAD"]).then((value) => value.trim() || "unknown", () => "unknown"),
|
|
362
|
+
gitStdout(root, ["status", "--porcelain"]).then((value) => value.trim(), () => ""),
|
|
363
|
+
gitStdout(root, ["status", "--porcelain=v1", "--branch"]).then((value) => value.trim(), () => ""),
|
|
364
|
+
gitStdout(root, ["remote"]).then((value) => value.trim(), () => ""),
|
|
365
|
+
gitStdout(root, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]).then(
|
|
366
|
+
(value) => value.trim(),
|
|
367
|
+
() => ""
|
|
368
|
+
)
|
|
369
|
+
]);
|
|
370
|
+
const dirtyCount = statusOutput ? statusOutput.split(/\r?\n/).filter(Boolean).length : 0;
|
|
371
|
+
const remotes = remoteOutput ? remoteOutput.split(/\r?\n/).map((remote) => remote.trim()).filter(Boolean) : [];
|
|
372
|
+
const remoteText = remotes.length > 0 ? remotes.join(",") : "none";
|
|
373
|
+
const gitContext = `branch=${branch} commit=${commit}`;
|
|
374
|
+
|
|
375
|
+
if (dirtyCount > 0) {
|
|
376
|
+
return {
|
|
377
|
+
line: `git: blocked worktree has uncommitted changes files=${dirtyCount} ${gitContext} remote=${remoteText}`,
|
|
378
|
+
next: "commit or stash local changes before release"
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (branch === "HEAD") {
|
|
383
|
+
return {
|
|
384
|
+
line: `git: blocked detached HEAD commit=${commit} remote=${remoteText}`,
|
|
385
|
+
next: "check out a release branch before public release"
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (remotes.length === 0) {
|
|
390
|
+
return {
|
|
391
|
+
line: `git: blocked no remote configured ${gitContext}`,
|
|
392
|
+
next: `add a remote, then push with upstream tracking: git remote add origin <git-url>; ${formatGitPushUpstreamCommand(branch)}`
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const relation = parseGitBranchRelation(branchStatusOutput);
|
|
397
|
+
const effectiveUpstream = upstream || relation.upstream || "";
|
|
398
|
+
if (!upstream && relation.gone && relation.upstream) {
|
|
399
|
+
return {
|
|
400
|
+
line: `git: blocked upstream is gone ${gitContext} remote=${remoteText} upstream=${relation.upstream}`,
|
|
401
|
+
next: "restore upstream tracking before public release"
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!effectiveUpstream) {
|
|
406
|
+
return {
|
|
407
|
+
line: `git: blocked no upstream configured ${gitContext} remote=${remoteText}`,
|
|
408
|
+
next: `push the branch with upstream tracking: ${formatGitPushUpstreamCommand(branch)}`
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (relation.gone) {
|
|
413
|
+
return {
|
|
414
|
+
line: `git: blocked upstream is gone ${gitContext} remote=${remoteText} upstream=${effectiveUpstream}`,
|
|
415
|
+
next: "restore upstream tracking before public release"
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
if (relation.ahead > 0 && relation.behind > 0) {
|
|
419
|
+
return {
|
|
420
|
+
line: `git: blocked branch diverged ahead=${relation.ahead} behind=${relation.behind} ${gitContext} remote=${remoteText} upstream=${effectiveUpstream}`,
|
|
421
|
+
next: "sync the branch with upstream before public release"
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
if (relation.ahead > 0) {
|
|
425
|
+
return {
|
|
426
|
+
line: `git: blocked branch has unpushed commits ahead=${relation.ahead} ${gitContext} remote=${remoteText} upstream=${effectiveUpstream}`,
|
|
427
|
+
next: "push local commits before public release"
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
if (relation.behind > 0) {
|
|
431
|
+
return {
|
|
432
|
+
line: `git: blocked branch is behind upstream behind=${relation.behind} ${gitContext} remote=${remoteText} upstream=${effectiveUpstream}`,
|
|
433
|
+
next: "sync the branch with upstream before public release"
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
line: `git: ok ${gitContext} remote=${remoteText} upstream=${effectiveUpstream}`
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function formatGitPushUpstreamCommand(branch) {
|
|
443
|
+
return `git push -u origin ${shellQuote(branch)}`;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function parseGitBranchRelation(statusOutput) {
|
|
447
|
+
const branchLine = statusOutput.split(/\r?\n/).find((line) => line.startsWith("## ")) ?? "";
|
|
448
|
+
const relationText = /\[([^\]]+)\]/.exec(branchLine)?.[1] ?? "";
|
|
449
|
+
const upstream = /\.\.\.([^\s\[]+)/.exec(branchLine)?.[1];
|
|
450
|
+
const ahead = Number(/\bahead (\d+)\b/.exec(relationText)?.[1] ?? 0);
|
|
451
|
+
const behind = Number(/\bbehind (\d+)\b/.exec(relationText)?.[1] ?? 0);
|
|
452
|
+
return { ahead, behind, gone: /\bgone\b/.test(relationText), upstream };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function gitStdout(root, args) {
|
|
456
|
+
const { stdout } = await execFileAsync("git", args, { cwd: root });
|
|
457
|
+
return stdout;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function commandFailureDetail(error) {
|
|
461
|
+
const failed = error && typeof error === "object" ? error : {};
|
|
462
|
+
const stderr = firstOutputLine(failed.stderr);
|
|
463
|
+
if (stderr) return stderr;
|
|
464
|
+
const stdout = firstOutputLine(failed.stdout);
|
|
465
|
+
if (stdout) return stdout;
|
|
466
|
+
if (typeof failed.code === "number") return `exit code ${failed.code}`;
|
|
467
|
+
if (typeof failed.signal === "string" && failed.signal) return `signal ${failed.signal}`;
|
|
468
|
+
return "failed without output";
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function firstOutputLine(value) {
|
|
472
|
+
if (typeof value !== "string") return undefined;
|
|
473
|
+
return value
|
|
474
|
+
.split(/\r?\n/)
|
|
475
|
+
.map((line) => line.trim())
|
|
476
|
+
.find(Boolean);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function isMissingFileError(error) {
|
|
480
|
+
return error && typeof error === "object" && "code" in error && error.code === "ENOENT";
|
|
481
|
+
}
|