@virtengine/openfleet 0.25.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/.env.example +914 -0
- package/LICENSE +190 -0
- package/README.md +500 -0
- package/agent-endpoint.mjs +918 -0
- package/agent-hook-bridge.mjs +230 -0
- package/agent-hooks.mjs +1188 -0
- package/agent-pool.mjs +2403 -0
- package/agent-prompts.mjs +689 -0
- package/agent-sdk.mjs +141 -0
- package/anomaly-detector.mjs +1195 -0
- package/autofix.mjs +1294 -0
- package/claude-shell.mjs +708 -0
- package/cli.mjs +906 -0
- package/codex-config.mjs +1274 -0
- package/codex-model-profiles.mjs +135 -0
- package/codex-shell.mjs +762 -0
- package/config-doctor.mjs +613 -0
- package/config.mjs +1720 -0
- package/conflict-resolver.mjs +248 -0
- package/container-runner.mjs +450 -0
- package/copilot-shell.mjs +827 -0
- package/daemon-restart-policy.mjs +56 -0
- package/diff-stats.mjs +282 -0
- package/error-detector.mjs +829 -0
- package/fetch-runtime.mjs +34 -0
- package/fleet-coordinator.mjs +838 -0
- package/get-telegram-chat-id.mjs +71 -0
- package/git-safety.mjs +170 -0
- package/github-reconciler.mjs +403 -0
- package/hook-profiles.mjs +651 -0
- package/kanban-adapter.mjs +4491 -0
- package/lib/logger.mjs +645 -0
- package/maintenance.mjs +828 -0
- package/merge-strategy.mjs +1171 -0
- package/monitor.mjs +12207 -0
- package/openfleet.config.example.json +115 -0
- package/openfleet.schema.json +465 -0
- package/package.json +203 -0
- package/postinstall.mjs +187 -0
- package/pr-cleanup-daemon.mjs +978 -0
- package/preflight.mjs +408 -0
- package/prepublish-check.mjs +90 -0
- package/presence.mjs +328 -0
- package/primary-agent.mjs +282 -0
- package/publish.mjs +151 -0
- package/repo-root.mjs +29 -0
- package/restart-controller.mjs +100 -0
- package/review-agent.mjs +557 -0
- package/rotate-agent-logs.sh +133 -0
- package/sdk-conflict-resolver.mjs +973 -0
- package/session-tracker.mjs +880 -0
- package/setup.mjs +3937 -0
- package/shared-knowledge.mjs +410 -0
- package/shared-state-manager.mjs +841 -0
- package/shared-workspace-cli.mjs +199 -0
- package/shared-workspace-registry.mjs +537 -0
- package/shared-workspaces.json +18 -0
- package/startup-service.mjs +1070 -0
- package/sync-engine.mjs +1063 -0
- package/task-archiver.mjs +801 -0
- package/task-assessment.mjs +550 -0
- package/task-claims.mjs +924 -0
- package/task-complexity.mjs +581 -0
- package/task-executor.mjs +5111 -0
- package/task-store.mjs +753 -0
- package/telegram-bot.mjs +9281 -0
- package/telegram-sentinel.mjs +2010 -0
- package/ui/app.js +867 -0
- package/ui/app.legacy.js +1464 -0
- package/ui/app.monolith.js +2488 -0
- package/ui/components/charts.js +226 -0
- package/ui/components/chat-view.js +567 -0
- package/ui/components/command-palette.js +587 -0
- package/ui/components/diff-viewer.js +190 -0
- package/ui/components/forms.js +327 -0
- package/ui/components/kanban-board.js +451 -0
- package/ui/components/session-list.js +305 -0
- package/ui/components/shared.js +473 -0
- package/ui/index.html +70 -0
- package/ui/modules/api.js +297 -0
- package/ui/modules/icons.js +461 -0
- package/ui/modules/router.js +81 -0
- package/ui/modules/settings-schema.js +261 -0
- package/ui/modules/state.js +679 -0
- package/ui/modules/telegram.js +331 -0
- package/ui/modules/utils.js +270 -0
- package/ui/styles/animations.css +140 -0
- package/ui/styles/base.css +98 -0
- package/ui/styles/components.css +1915 -0
- package/ui/styles/kanban.css +286 -0
- package/ui/styles/layout.css +809 -0
- package/ui/styles/sessions.css +827 -0
- package/ui/styles/variables.css +188 -0
- package/ui/styles.css +141 -0
- package/ui/styles.monolith.css +1046 -0
- package/ui/tabs/agents.js +1417 -0
- package/ui/tabs/chat.js +74 -0
- package/ui/tabs/control.js +887 -0
- package/ui/tabs/dashboard.js +515 -0
- package/ui/tabs/infra.js +537 -0
- package/ui/tabs/logs.js +783 -0
- package/ui/tabs/settings.js +1487 -0
- package/ui/tabs/tasks.js +1385 -0
- package/ui-server.mjs +4073 -0
- package/update-check.mjs +465 -0
- package/utils.mjs +172 -0
- package/ve-kanban.mjs +654 -0
- package/ve-kanban.ps1 +1365 -0
- package/ve-kanban.sh +18 -0
- package/ve-orchestrator.mjs +340 -0
- package/ve-orchestrator.ps1 +6546 -0
- package/ve-orchestrator.sh +18 -0
- package/vibe-kanban-wrapper.mjs +41 -0
- package/vk-error-resolver.mjs +470 -0
- package/vk-log-stream.mjs +914 -0
- package/whatsapp-channel.mjs +520 -0
- package/workspace-monitor.mjs +581 -0
- package/workspace-reaper.mjs +405 -0
- package/workspace-registry.mjs +238 -0
- package/worktree-manager.mjs +1266 -0
package/preflight.mjs
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
|
|
5
|
+
const isWindows = process.platform === "win32";
|
|
6
|
+
const MIN_FREE_GB = Number(process.env.CODEX_MONITOR_MIN_FREE_GB || "10");
|
|
7
|
+
const MIN_FREE_BYTES = MIN_FREE_GB * 1024 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
function runCommand(command, args, options = {}) {
|
|
10
|
+
try {
|
|
11
|
+
const useShell = options.shell ?? isWindows;
|
|
12
|
+
// DEP0190 fix: Node.js 24+ warns when passing args array with shell: true.
|
|
13
|
+
// Join command + args into a single string when using shell mode.
|
|
14
|
+
if (useShell && args && args.length > 0) {
|
|
15
|
+
const fullCommand = [command, ...args].join(" ");
|
|
16
|
+
return spawnSync(fullCommand, {
|
|
17
|
+
encoding: "utf8",
|
|
18
|
+
windowsHide: true,
|
|
19
|
+
shell: true,
|
|
20
|
+
...options,
|
|
21
|
+
// Ensure shell stays true (override any options.shell)
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return spawnSync(command, args, {
|
|
25
|
+
encoding: "utf8",
|
|
26
|
+
windowsHide: true,
|
|
27
|
+
// On Windows, use shell to resolve .cmd/.ps1 shims (pnpm, gh, etc.)
|
|
28
|
+
shell: useShell,
|
|
29
|
+
...options,
|
|
30
|
+
});
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return { error, status: -1, stdout: "", stderr: error?.message || "" };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatBytes(value) {
|
|
37
|
+
if (!Number.isFinite(value)) return "unknown";
|
|
38
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
39
|
+
let idx = 0;
|
|
40
|
+
let num = value;
|
|
41
|
+
while (num >= 1024 && idx < units.length - 1) {
|
|
42
|
+
num /= 1024;
|
|
43
|
+
idx += 1;
|
|
44
|
+
}
|
|
45
|
+
return `${num.toFixed(num >= 10 || idx === 0 ? 0 : 1)} ${units[idx]}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function formatDuration(ms) {
|
|
49
|
+
const sec = Math.round(ms / 1000);
|
|
50
|
+
if (sec < 60) return `${sec}s`;
|
|
51
|
+
const min = Math.round(sec / 60);
|
|
52
|
+
return `${min}m`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readOutput(res) {
|
|
56
|
+
if (!res) return "";
|
|
57
|
+
return String(res.stdout || "").trim();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function checkGitConfig(repoRoot) {
|
|
61
|
+
const name = runCommand("git", ["config", "--get", "user.name"], {
|
|
62
|
+
cwd: repoRoot,
|
|
63
|
+
});
|
|
64
|
+
const email = runCommand("git", ["config", "--get", "user.email"], {
|
|
65
|
+
cwd: repoRoot,
|
|
66
|
+
});
|
|
67
|
+
const nameValue = readOutput(name);
|
|
68
|
+
const emailValue = readOutput(email);
|
|
69
|
+
const ok = Boolean(nameValue) && Boolean(emailValue);
|
|
70
|
+
return {
|
|
71
|
+
ok,
|
|
72
|
+
name: nameValue,
|
|
73
|
+
email: emailValue,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function checkWorktreeClean(repoRoot) {
|
|
78
|
+
const status = runCommand("git", ["status", "--porcelain"], {
|
|
79
|
+
cwd: repoRoot,
|
|
80
|
+
});
|
|
81
|
+
const raw = readOutput(status);
|
|
82
|
+
const dirtyFiles = raw ? raw.split(/\r?\n/).filter(Boolean) : [];
|
|
83
|
+
return {
|
|
84
|
+
ok: dirtyFiles.length === 0,
|
|
85
|
+
dirtyFiles,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseDiskFromPosix(repoRoot) {
|
|
90
|
+
const res = runCommand("df", ["-kP", repoRoot]);
|
|
91
|
+
if (res.status !== 0) return null;
|
|
92
|
+
const lines = readOutput(res).split(/\r?\n/).filter(Boolean);
|
|
93
|
+
if (lines.length < 2) return null;
|
|
94
|
+
const parts = lines[1].split(/\s+/);
|
|
95
|
+
if (parts.length < 6) return null;
|
|
96
|
+
const totalKb = Number(parts[1]);
|
|
97
|
+
const availKb = Number(parts[3]);
|
|
98
|
+
if (!Number.isFinite(totalKb) || !Number.isFinite(availKb)) return null;
|
|
99
|
+
return {
|
|
100
|
+
totalBytes: totalKb * 1024,
|
|
101
|
+
freeBytes: availKb * 1024,
|
|
102
|
+
mount: parts[5],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseDiskFromWindows(repoRoot) {
|
|
107
|
+
const driveMatch = repoRoot.match(/^([A-Za-z]):/);
|
|
108
|
+
const drive = driveMatch ? driveMatch[1].toUpperCase() : null;
|
|
109
|
+
if (!drive) return null;
|
|
110
|
+
const ps = `Get-PSDrive -Name ${drive} | Select-Object Used,Free | ConvertTo-Json -Compress`;
|
|
111
|
+
const res = runCommand("powershell", ["-NoProfile", "-Command", ps]);
|
|
112
|
+
if (res.status !== 0) return null;
|
|
113
|
+
const raw = readOutput(res);
|
|
114
|
+
if (!raw) return null;
|
|
115
|
+
try {
|
|
116
|
+
const parsed = JSON.parse(raw);
|
|
117
|
+
const free = Number(parsed?.Free);
|
|
118
|
+
const used = Number(parsed?.Used);
|
|
119
|
+
if (!Number.isFinite(free) || !Number.isFinite(used)) return null;
|
|
120
|
+
return {
|
|
121
|
+
totalBytes: free + used,
|
|
122
|
+
freeBytes: free,
|
|
123
|
+
mount: `${drive}:`,
|
|
124
|
+
};
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function checkDiskSpace(repoRoot) {
|
|
131
|
+
const info = isWindows
|
|
132
|
+
? parseDiskFromWindows(repoRoot)
|
|
133
|
+
: parseDiskFromPosix(repoRoot);
|
|
134
|
+
if (!info) {
|
|
135
|
+
return { ok: true, info: null };
|
|
136
|
+
}
|
|
137
|
+
const ok = info.freeBytes >= MIN_FREE_BYTES;
|
|
138
|
+
return { ok, info };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function checkToolVersion(label, command, args, hint) {
|
|
142
|
+
const res = runCommand(command, args);
|
|
143
|
+
if (res.error || res.status !== 0) {
|
|
144
|
+
return {
|
|
145
|
+
label,
|
|
146
|
+
ok: false,
|
|
147
|
+
version: "missing",
|
|
148
|
+
hint,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const raw = readOutput(res);
|
|
152
|
+
const version = raw ? raw.split(/\r?\n/)[0] : "unknown";
|
|
153
|
+
return { label, ok: true, version };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseEnvBool(value, fallback = false) {
|
|
157
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
158
|
+
const raw = String(value).trim().toLowerCase();
|
|
159
|
+
if (["1", "true", "yes", "on", "y"].includes(raw)) return true;
|
|
160
|
+
if (["0", "false", "no", "off", "n"].includes(raw)) return false;
|
|
161
|
+
return fallback;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isShellModeRequested() {
|
|
165
|
+
const script = String(process.env.ORCHESTRATOR_SCRIPT || "")
|
|
166
|
+
.trim()
|
|
167
|
+
.toLowerCase();
|
|
168
|
+
if (script.endsWith(".sh")) return true;
|
|
169
|
+
if (script.endsWith(".ps1")) return false;
|
|
170
|
+
if (parseEnvBool(process.env.CODEX_MONITOR_SHELL_MODE, false)) return true;
|
|
171
|
+
return !isWindows;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function checkToolchain() {
|
|
175
|
+
const shellMode = isShellModeRequested();
|
|
176
|
+
const requiredTools = new Set([
|
|
177
|
+
"git",
|
|
178
|
+
"gh",
|
|
179
|
+
"node",
|
|
180
|
+
shellMode ? "shell" : "pwsh",
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
const tools = [
|
|
184
|
+
checkToolVersion(
|
|
185
|
+
"git",
|
|
186
|
+
"git",
|
|
187
|
+
["--version"],
|
|
188
|
+
"Install Git and ensure it is on PATH.",
|
|
189
|
+
),
|
|
190
|
+
checkToolVersion(
|
|
191
|
+
"gh",
|
|
192
|
+
"gh",
|
|
193
|
+
["--version"],
|
|
194
|
+
"Install GitHub CLI (gh) and ensure it is on PATH.",
|
|
195
|
+
),
|
|
196
|
+
checkToolVersion(
|
|
197
|
+
"node",
|
|
198
|
+
"node",
|
|
199
|
+
["--version"],
|
|
200
|
+
"Install Node.js 18+ and ensure it is on PATH.",
|
|
201
|
+
),
|
|
202
|
+
checkToolVersion(
|
|
203
|
+
"pnpm",
|
|
204
|
+
"pnpm",
|
|
205
|
+
["--version"],
|
|
206
|
+
"Install pnpm (npm install -g pnpm) and ensure it is on PATH.",
|
|
207
|
+
),
|
|
208
|
+
checkToolVersion(
|
|
209
|
+
"go",
|
|
210
|
+
"go",
|
|
211
|
+
["version"],
|
|
212
|
+
"Install Go 1.21+ and ensure it is on PATH.",
|
|
213
|
+
),
|
|
214
|
+
checkToolVersion(
|
|
215
|
+
"pwsh",
|
|
216
|
+
"pwsh",
|
|
217
|
+
["-NoProfile", "-Command", "$PSVersionTable.PSVersion.ToString()"],
|
|
218
|
+
"Install PowerShell 7+ (pwsh) and ensure it is on PATH.",
|
|
219
|
+
),
|
|
220
|
+
];
|
|
221
|
+
const bashVersion = checkToolVersion(
|
|
222
|
+
"bash",
|
|
223
|
+
"bash",
|
|
224
|
+
["--version"],
|
|
225
|
+
"Install bash and ensure it is on PATH.",
|
|
226
|
+
);
|
|
227
|
+
const shVersion = checkToolVersion(
|
|
228
|
+
"sh",
|
|
229
|
+
"sh",
|
|
230
|
+
["--version"],
|
|
231
|
+
"Install sh and ensure it is on PATH.",
|
|
232
|
+
);
|
|
233
|
+
const shellTool = {
|
|
234
|
+
label: "shell",
|
|
235
|
+
ok: bashVersion.ok || shVersion.ok,
|
|
236
|
+
version: bashVersion.ok ? bashVersion.version : shVersion.version,
|
|
237
|
+
hint: "Install bash/sh and ensure it is on PATH.",
|
|
238
|
+
};
|
|
239
|
+
tools.push(shellTool);
|
|
240
|
+
|
|
241
|
+
// Only required tools determine pass/fail — optional tools are warnings
|
|
242
|
+
const ok = tools
|
|
243
|
+
.filter((tool) => requiredTools.has(tool.label))
|
|
244
|
+
.every((tool) => tool.ok);
|
|
245
|
+
return { ok, tools, requiredTools };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function checkGhAuth() {
|
|
249
|
+
const tokenPresent = Boolean(
|
|
250
|
+
process.env.GH_TOKEN || process.env.GITHUB_TOKEN,
|
|
251
|
+
);
|
|
252
|
+
const auth = runCommand("gh", ["auth", "status", "-h", "github.com"]);
|
|
253
|
+
if (auth.status === 0) {
|
|
254
|
+
return { ok: true, method: "gh" };
|
|
255
|
+
}
|
|
256
|
+
if (tokenPresent) {
|
|
257
|
+
return { ok: true, method: "token" };
|
|
258
|
+
}
|
|
259
|
+
return { ok: false, method: "none", error: readOutput(auth) };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function runPreflightChecks(options = {}) {
|
|
263
|
+
const repoRoot = resolve(options.repoRoot || process.cwd());
|
|
264
|
+
const errors = [];
|
|
265
|
+
const warnings = [];
|
|
266
|
+
|
|
267
|
+
const toolchain = checkToolchain();
|
|
268
|
+
let ghAuth = { ok: false, method: "unknown" };
|
|
269
|
+
for (const tool of toolchain.tools) {
|
|
270
|
+
if (tool.ok) continue;
|
|
271
|
+
const entry = { title: `Missing tool: ${tool.label}`, message: tool.hint };
|
|
272
|
+
if (toolchain.requiredTools.has(tool.label)) {
|
|
273
|
+
errors.push(entry);
|
|
274
|
+
} else {
|
|
275
|
+
warnings.push(entry);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const gitConfig = checkGitConfig(repoRoot);
|
|
280
|
+
if (!gitConfig.ok) {
|
|
281
|
+
errors.push({
|
|
282
|
+
title: "Git identity not configured",
|
|
283
|
+
message:
|
|
284
|
+
'Set git user.name and user.email (git config --global user.name "Your Name"; git config --global user.email "you@example.com").',
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const worktree = checkWorktreeClean(repoRoot);
|
|
289
|
+
if (!worktree.ok) {
|
|
290
|
+
const sample = worktree.dirtyFiles.slice(0, 12).join(os.EOL);
|
|
291
|
+
const suffix = worktree.dirtyFiles.length > 12 ? `${os.EOL}…` : "";
|
|
292
|
+
// Downgrade to warning — orchestrator uses separate worktrees so main
|
|
293
|
+
// worktree changes don't block operation.
|
|
294
|
+
warnings.push({
|
|
295
|
+
title: "Worktree has uncommitted changes",
|
|
296
|
+
message:
|
|
297
|
+
`Consider committing or stashing changes.${os.EOL}` +
|
|
298
|
+
`${sample}${suffix}`,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (toolchain.ok) {
|
|
303
|
+
ghAuth = checkGhAuth();
|
|
304
|
+
if (!ghAuth.ok) {
|
|
305
|
+
errors.push({
|
|
306
|
+
title: "GitHub CLI not authenticated",
|
|
307
|
+
message:
|
|
308
|
+
"Run `gh auth login` (or set GH_TOKEN/GITHUB_TOKEN) then verify with `gh auth status`.",
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const disk = checkDiskSpace(repoRoot);
|
|
314
|
+
if (!disk.ok && disk.info) {
|
|
315
|
+
errors.push({
|
|
316
|
+
title: "Low disk space",
|
|
317
|
+
message: `Free space on ${disk.info.mount} is ${formatBytes(disk.info.freeBytes)}; keep at least ${MIN_FREE_GB} GB free.`,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
ok: errors.length === 0,
|
|
323
|
+
errors,
|
|
324
|
+
warnings,
|
|
325
|
+
details: {
|
|
326
|
+
toolchain,
|
|
327
|
+
gitConfig,
|
|
328
|
+
worktree,
|
|
329
|
+
ghAuth,
|
|
330
|
+
disk,
|
|
331
|
+
minFreeBytes: MIN_FREE_BYTES,
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function formatPreflightReport(result, options = {}) {
|
|
337
|
+
const header = options.header || "openfleet preflight";
|
|
338
|
+
const lines = [];
|
|
339
|
+
lines.push(`=== ${header} ===`);
|
|
340
|
+
lines.push(
|
|
341
|
+
`Status: ${result.ok ? "OK" : "FAILED"} (${result.errors.length} error(s), ${result.warnings.length} warning(s))`,
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
const toolchain = result.details?.toolchain;
|
|
345
|
+
if (toolchain?.tools?.length) {
|
|
346
|
+
lines.push("Toolchain:");
|
|
347
|
+
for (const tool of toolchain.tools) {
|
|
348
|
+
const status = tool.ok ? tool.version : "missing";
|
|
349
|
+
lines.push(` - ${tool.label}: ${status}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const gitConfig = result.details?.gitConfig;
|
|
354
|
+
if (gitConfig) {
|
|
355
|
+
lines.push(`Git: ${gitConfig.ok ? "configured" : "missing identity"}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const ghAuth = result.details?.ghAuth;
|
|
359
|
+
if (ghAuth) {
|
|
360
|
+
lines.push(`GitHub auth: ${ghAuth.ok ? ghAuth.method : "missing"}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const disk = result.details?.disk;
|
|
364
|
+
if (disk?.info) {
|
|
365
|
+
lines.push(
|
|
366
|
+
`Disk: ${formatBytes(disk.info.freeBytes)} free of ${formatBytes(disk.info.totalBytes)} (${disk.info.mount})`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const worktree = result.details?.worktree;
|
|
371
|
+
if (worktree) {
|
|
372
|
+
lines.push(
|
|
373
|
+
`Worktree: ${worktree.ok ? "clean" : `${worktree.dirtyFiles.length} change(s)`}`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (result.errors.length) {
|
|
378
|
+
lines.push("Errors:");
|
|
379
|
+
for (const err of result.errors) {
|
|
380
|
+
lines.push(` - ${err.title}`);
|
|
381
|
+
if (err.message) {
|
|
382
|
+
const messageLines = String(err.message).split(/\r?\n/);
|
|
383
|
+
for (const msgLine of messageLines) {
|
|
384
|
+
lines.push(` ${msgLine}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (result.warnings.length) {
|
|
391
|
+
lines.push("Warnings:");
|
|
392
|
+
for (const warn of result.warnings) {
|
|
393
|
+
lines.push(` - ${warn.title}`);
|
|
394
|
+
if (warn.message) {
|
|
395
|
+
const messageLines = String(warn.message).split(/\r?\n/);
|
|
396
|
+
for (const msgLine of messageLines) {
|
|
397
|
+
lines.push(` ${msgLine}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (!result.ok && options.retryMs) {
|
|
404
|
+
lines.push(`Next check in ${formatDuration(options.retryMs)}.`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return lines.join(os.EOL);
|
|
408
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* prepublish-check.mjs — Pre-publish validation gate.
|
|
5
|
+
*
|
|
6
|
+
* Scans all .mjs files for local `import ... from "./foo.mjs"` statements
|
|
7
|
+
* and verifies each imported file is listed in package.json's `files` array.
|
|
8
|
+
* Also checks for duplicate entries in `files`.
|
|
9
|
+
*
|
|
10
|
+
* Run automatically via `prepublishOnly` script, or manually:
|
|
11
|
+
* node prepublish-check.mjs
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
15
|
+
import { resolve, dirname } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const pkg = JSON.parse(
|
|
20
|
+
readFileSync(resolve(__dirname, "package.json"), "utf8"),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
if (!pkg.version) {
|
|
24
|
+
console.error("❌ Missing version in package.json");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const filesArray = pkg.files || [];
|
|
29
|
+
const filesSet = new Set(filesArray);
|
|
30
|
+
|
|
31
|
+
// ── Check for duplicates ─────────────────────────────────────────────────────
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
const duplicates = [];
|
|
34
|
+
for (const f of filesArray) {
|
|
35
|
+
if (seen.has(f)) duplicates.push(f);
|
|
36
|
+
seen.add(f);
|
|
37
|
+
}
|
|
38
|
+
if (duplicates.length > 0) {
|
|
39
|
+
console.error(
|
|
40
|
+
`❌ Duplicate entries in files array: ${duplicates.join(", ")}`,
|
|
41
|
+
);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Scan all .mjs files for local imports ────────────────────────────────────
|
|
46
|
+
const mjsFiles = readdirSync(__dirname).filter(
|
|
47
|
+
(f) => f.endsWith(".mjs") && f !== "prepublish-check.mjs",
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const importPattern = /from\s+["']\.\/([^"']+)["']/g;
|
|
51
|
+
const missing = [];
|
|
52
|
+
|
|
53
|
+
for (const file of mjsFiles) {
|
|
54
|
+
// Only check files that are in the files array (i.e., will be published)
|
|
55
|
+
if (!filesSet.has(file)) continue;
|
|
56
|
+
|
|
57
|
+
const content = readFileSync(resolve(__dirname, file), "utf8");
|
|
58
|
+
let match;
|
|
59
|
+
importPattern.lastIndex = 0;
|
|
60
|
+
|
|
61
|
+
while ((match = importPattern.exec(content)) !== null) {
|
|
62
|
+
const imported = match[1];
|
|
63
|
+
// Skip if it's a comment line
|
|
64
|
+
const lineStart = content.lastIndexOf("\n", match.index) + 1;
|
|
65
|
+
const line = content.slice(lineStart, match.index).trimStart();
|
|
66
|
+
if (
|
|
67
|
+
line.startsWith("//") ||
|
|
68
|
+
line.startsWith("*") ||
|
|
69
|
+
line.startsWith("/*")
|
|
70
|
+
) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (!filesSet.has(imported)) {
|
|
74
|
+
missing.push({ file, imported });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (missing.length > 0) {
|
|
80
|
+
console.error("❌ Local imports not in package.json files array:");
|
|
81
|
+
for (const { file, imported } of missing) {
|
|
82
|
+
console.error(` ${file} → import from "./${imported}"`);
|
|
83
|
+
}
|
|
84
|
+
console.error("\nAdd these to the 'files' array in package.json.");
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log(
|
|
89
|
+
`✅ ${pkg.name}@${pkg.version} — ${filesArray.length} files, ${mjsFiles.length} .mjs scanned, 0 missing imports`,
|
|
90
|
+
);
|