@kubilaycakmak/projectfreeze 0.0.0-stage → 0.1.0-dev.2
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 +27 -2
- package/bin/projectfreeze.js +4 -0
- package/docs/interfaces.md +84 -0
- package/docs/operations.md +40 -0
- package/examples/demo.js +49 -0
- package/examples/stream-test.mjs +71 -0
- package/examples/workload.js +29 -0
- package/helpers/.gitkeep +0 -0
- package/helpers/projectfreeze-darwin-arm64 +0 -0
- package/helpers/projectfreeze-darwin-arm64.sha256 +1 -0
- package/helpers/projectfreeze-darwin-x64 +0 -0
- package/helpers/projectfreeze-darwin-x64.sha256 +1 -0
- package/helpers/projectfreeze-linux-arm64 +0 -0
- package/helpers/projectfreeze-linux-arm64.sha256 +1 -0
- package/helpers/projectfreeze-linux-x64 +0 -0
- package/helpers/projectfreeze-linux-x64.sha256 +1 -0
- package/helpers/projectfreeze-win32-arm64.exe +0 -0
- package/helpers/projectfreeze-win32-arm64.exe.sha256 +1 -0
- package/helpers/projectfreeze-win32-x64.exe +0 -0
- package/helpers/projectfreeze-win32-x64.exe.sha256 +1 -0
- package/package.json +17 -4
- package/src/cli.js +117 -0
- package/src/contracts.d.ts +28 -0
- package/src/discovery.js +94 -0
- package/src/engine.js +734 -0
- package/src/errors.js +20 -0
- package/src/integrations.js +195 -0
- package/src/ipc.js +258 -0
- package/src/line-menu.js +132 -0
- package/src/mcp.js +110 -0
- package/src/platform.js +274 -0
- package/src/recipe.js +66 -0
- package/src/store.js +61 -0
- package/src/supervisors.js +163 -0
- package/src/systemd.js +104 -0
- package/src/tui.js +443 -0
- package/src/windows-supervisors.ps1 +83 -0
package/src/discovery.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { access, realpath, stat } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, join, relative, sep } from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { identity, key, manifestStart, matches, scan } from './platform.js';
|
|
5
|
+
import { Fault } from './errors.js';
|
|
6
|
+
|
|
7
|
+
const manifests = ['package.json', 'pyproject.toml', 'Cargo.toml', 'go.mod', 'composer.json', 'Gemfile', '.git'];
|
|
8
|
+
|
|
9
|
+
export async function directory(value) {
|
|
10
|
+
try {
|
|
11
|
+
const path = await realpath(value);
|
|
12
|
+
if (!(await stat(path)).isDirectory()) throw new Error();
|
|
13
|
+
return path;
|
|
14
|
+
} catch { throw new Fault('USAGE', 'Provide an existing project directory.'); }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function projectId(path) {
|
|
18
|
+
return `p-${createHash('sha256').update(path).digest('hex').slice(0, 16)}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function within(parent, child) {
|
|
22
|
+
if (!child) return false;
|
|
23
|
+
const rel = relative(parent, child);
|
|
24
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !rel.includes(':'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function rootFor(start, cache) {
|
|
28
|
+
if (!start) return undefined;
|
|
29
|
+
if (!cache.has(start)) cache.set(start, (async () => {
|
|
30
|
+
let path = start;
|
|
31
|
+
while (path !== dirname(path)) {
|
|
32
|
+
for (const file of manifests) {
|
|
33
|
+
if (await access(join(path, file)).then(() => true, () => false)) {
|
|
34
|
+
return { path: await realpath(path).catch(() => path), manifest: file };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
path = dirname(path);
|
|
38
|
+
}
|
|
39
|
+
})());
|
|
40
|
+
return cache.get(start);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function candidates(projects, rows = undefined) {
|
|
44
|
+
const processes = rows ?? await scan();
|
|
45
|
+
const groups = new Map();
|
|
46
|
+
const cache = new Map();
|
|
47
|
+
const claimed = new Set(projects.flatMap(project => project.members.map(key)));
|
|
48
|
+
const observedAt = new Date().toISOString();
|
|
49
|
+
const byPid = new Map(processes.map(p => [p.pid, p]));
|
|
50
|
+
for (const process of processes) {
|
|
51
|
+
if (claimed.has(key(process))) continue;
|
|
52
|
+
const root = await rootFor(manifestStart(process), cache) ?? await rootFor(process.cwd, cache);
|
|
53
|
+
if (!root) continue;
|
|
54
|
+
const id = projectId(root.path);
|
|
55
|
+
const group = groups.get(id) ?? { id, name: basename(root.path), cwd: root.path, state: 'discovered',
|
|
56
|
+
confidence: 'observed', observedAt, members: [], unresolved: [] };
|
|
57
|
+
const evidence = [`manifest:${root.manifest}`];
|
|
58
|
+
if (within(root.path, process.cwd)) evidence.push('working-directory');
|
|
59
|
+
if (within(root.path, process.projectHint)) evidence.push('script-path');
|
|
60
|
+
if (process.image) evidence.push('executable');
|
|
61
|
+
if (process.ports.length) evidence.push('listening-socket');
|
|
62
|
+
const parent = byPid.get(process.parent);
|
|
63
|
+
if (parent && within(root.path, parent.cwd)) evidence.push('observed-parent');
|
|
64
|
+
if (process.parent <= 1 || !parent) evidence.push('parent-outside-scan');
|
|
65
|
+
if (process.session === process.pid || process.group === process.pid) evidence.push('separate-session-or-group');
|
|
66
|
+
group.members.push({ ...publicProcess(process), evidence });
|
|
67
|
+
group.unresolved.push(identity(process));
|
|
68
|
+
groups.set(id, group);
|
|
69
|
+
}
|
|
70
|
+
return [...groups.values()];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function publicProcess(row) {
|
|
74
|
+
const { projectHint, cpuTime, ...visible } = row;
|
|
75
|
+
return visible;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function summarize(project, rows, discovered) {
|
|
79
|
+
const processes = rows.filter(row => project.members.some(member => matches(member, row))).map(publicProcess);
|
|
80
|
+
const owners = new Map();
|
|
81
|
+
for (const p of processes) {
|
|
82
|
+
for (const port of p.ports) owners.set(`${port.address}/${port.port}`, port);
|
|
83
|
+
}
|
|
84
|
+
return { id: project.id, name: project.name, cwd: project.cwd, state: project.state,
|
|
85
|
+
confidence: project.members.length ? 'reviewed-or-contained' : 'registered',
|
|
86
|
+
processes, processCount: processes.length, ports: [...owners.values()],
|
|
87
|
+
rssSum: processes.some(p => p.rss !== undefined) ? processes.reduce((n, p) => n + (p.rss ?? 0), 0) : undefined,
|
|
88
|
+
cpuPercent: processes.some(p => p.cpuPercent !== undefined) ? processes.reduce((n, p) => n + (p.cpuPercent ?? 0), 0) : undefined,
|
|
89
|
+
dependencies: project.recipe?.dependencies ?? [], containers: project.recipe?.containers ?? [], systemd: project.recipe?.systemd ?? [],
|
|
90
|
+
supervisors: project.recipe?.supervisors?.map(({ kind, name, shared }) => ({ kind, name, shared })) ?? [],
|
|
91
|
+
excluded: project.excluded, unresolved: discovered?.unresolved ?? [], observedAt: new Date().toISOString(),
|
|
92
|
+
restart: project.recipe ? { configured: true, gracefulStop: Boolean(project.recipe.stop) } : { configured: false },
|
|
93
|
+
metrics: 'CPU is sampled as percent of one core. RSS sums resident pages and can count shared pages more than once.' };
|
|
94
|
+
}
|