@kubilaycakmak/projectfreeze 0.0.0-stage → 0.1.0-dev.5

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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -2
  3. package/bin/projectfreeze.js +4 -0
  4. package/docs/interfaces.md +98 -0
  5. package/docs/operations.md +42 -0
  6. package/examples/demo.js +72 -0
  7. package/examples/stream-test.mjs +71 -0
  8. package/examples/workload.js +29 -0
  9. package/helpers/.gitkeep +0 -0
  10. package/helpers/projectfreeze-darwin-arm64 +0 -0
  11. package/helpers/projectfreeze-darwin-arm64.sha256 +1 -0
  12. package/helpers/projectfreeze-darwin-x64 +0 -0
  13. package/helpers/projectfreeze-darwin-x64.sha256 +1 -0
  14. package/helpers/projectfreeze-linux-arm64 +0 -0
  15. package/helpers/projectfreeze-linux-arm64.sha256 +1 -0
  16. package/helpers/projectfreeze-linux-x64 +0 -0
  17. package/helpers/projectfreeze-linux-x64.sha256 +1 -0
  18. package/helpers/projectfreeze-win32-arm64.exe +0 -0
  19. package/helpers/projectfreeze-win32-arm64.exe.sha256 +1 -0
  20. package/helpers/projectfreeze-win32-x64.exe +0 -0
  21. package/helpers/projectfreeze-win32-x64.exe.sha256 +1 -0
  22. package/package.json +17 -4
  23. package/src/cli.js +135 -0
  24. package/src/contracts.d.ts +28 -0
  25. package/src/discovery.js +112 -0
  26. package/src/engine.js +820 -0
  27. package/src/errors.js +20 -0
  28. package/src/integrations.js +195 -0
  29. package/src/inventory.js +37 -0
  30. package/src/ipc.js +258 -0
  31. package/src/line-menu.js +147 -0
  32. package/src/mcp.js +113 -0
  33. package/src/platform.js +320 -0
  34. package/src/recipe.js +69 -0
  35. package/src/store.js +61 -0
  36. package/src/supervisors.js +163 -0
  37. package/src/systemd.js +104 -0
  38. package/src/tui.js +596 -0
  39. package/src/windows-supervisors.ps1 +83 -0
@@ -0,0 +1,28 @@
1
+ export type Identity = { pid: number; birth: string; boot: string };
2
+ export type Port = { address: string; port: number };
3
+ export type Process = Identity & {
4
+ parent: number; group: number; session: number; state: string;
5
+ cwd?: string; image?: string; cpuTime?: number; rss?: number; ports: Port[];
6
+ cpuPercent?: number; projectHint?: string;
7
+ };
8
+ export type Recipe = {
9
+ argv: string[]; cwd: string; instance?: string; env: string[];
10
+ dependencies: string[]; ready?: Port; stop?: string[]; timeout: number;
11
+ restart: 'never' | 'on-failure'; cgroup?: string; containers: Container[];
12
+ systemd: { unit: string; shared: boolean }[];
13
+ supervisors: { kind: 'launchd' | 'scm' | 'task'; name: string; shared: boolean; plist?: string }[];
14
+ };
15
+ export type Container = { project: string; service: string; shared: boolean };
16
+ export type Member = Identity & { source: 'adopted' | 'launched' | 'contained'; suspended?: boolean };
17
+ export type Project = {
18
+ id: string; name: string; cwd: string; instance?: string; state: string; members: Member[];
19
+ excluded: Identity[]; root?: Identity; recipe?: Recipe; job?: string; cgroup?: string;
20
+ frozenContainers?: string[]; stoppedContainers?: string[]; sleepingIntent?: boolean;
21
+ frozenUnits?: string[]; stoppedUnits?: string[];
22
+ supervisorPins?: Record<string, string>; pausedSupervisors?: string[]; stoppedSupervisors?: string[]; disabledTasks?: string[];
23
+ };
24
+ export type Outcome = { target: string; status: string; code?: string; message?: string };
25
+ export type Operation = {
26
+ id: string; action: string; projects: string[]; status: string;
27
+ started: string; ended?: string; outcomes: Outcome[]; pending?: string;
28
+ };
@@ -0,0 +1,112 @@
1
+ import { access, readFile, 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, instance) {
18
+ const hash = createHash('sha256').update(path);
19
+ if (instance !== undefined) hash.update('\0').update(instance);
20
+ return `p-${hash.digest('hex').slice(0, 16)}`;
21
+ }
22
+
23
+ function within(parent, child) {
24
+ if (!child) return false;
25
+ const rel = relative(parent, child);
26
+ return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !rel.includes(':'));
27
+ }
28
+
29
+ async function projectFreezeCli(root, hint, cache) {
30
+ if (root.manifest !== 'package.json' || !hint) return false;
31
+ const script = relative(root.path, hint).split(sep).join('/');
32
+ if ((process.platform === 'win32' ? script.toLowerCase() : script) !== 'bin/projectfreeze.js') return false;
33
+ if (!cache.has(root.path)) cache.set(root.path, (async () => {
34
+ const file = join(root.path, 'package.json');
35
+ const info = await stat(file);
36
+ if (!info.isFile()) return false;
37
+ const manifest = JSON.parse(await readFile(file, 'utf8'));
38
+ return manifest.name === '@kubilaycakmak/projectfreeze' && manifest.bin?.projectfreeze === 'bin/projectfreeze.js';
39
+ })().catch(() => false));
40
+ return cache.get(root.path);
41
+ }
42
+
43
+ async function rootFor(start, cache) {
44
+ if (!start) return undefined;
45
+ if (!cache.has(start)) cache.set(start, (async () => {
46
+ let path = start;
47
+ while (path !== dirname(path)) {
48
+ for (const file of manifests) {
49
+ if (await access(join(path, file)).then(() => true, () => false)) {
50
+ return { path: await realpath(path).catch(() => path), manifest: file };
51
+ }
52
+ }
53
+ path = dirname(path);
54
+ }
55
+ })());
56
+ return cache.get(start);
57
+ }
58
+
59
+ export async function candidates(projects, rows = undefined) {
60
+ const processes = rows ?? await scan();
61
+ const groups = new Map();
62
+ const cache = new Map();
63
+ const cliPackages = new Map();
64
+ const claimed = new Set(projects.flatMap(project => project.members.map(key)));
65
+ const observedAt = new Date().toISOString();
66
+ const byPid = new Map(processes.map(p => [p.pid, p]));
67
+ for (const process of processes) {
68
+ if (claimed.has(key(process))) continue;
69
+ const root = await rootFor(manifestStart(process), cache) ?? await rootFor(process.cwd, cache);
70
+ if (!root) continue;
71
+ if (await projectFreezeCli(root, process.projectHint, cliPackages)) continue;
72
+ const id = projectId(root.path);
73
+ const group = groups.get(id) ?? { id, name: basename(root.path), cwd: root.path, state: 'discovered',
74
+ confidence: 'observed', observedAt, members: [], unresolved: [] };
75
+ const evidence = [`manifest:${root.manifest}`];
76
+ if (within(root.path, process.cwd)) evidence.push('working-directory');
77
+ if (within(root.path, process.projectHint)) evidence.push('script-path');
78
+ if (process.image) evidence.push('executable');
79
+ if (process.ports.length) evidence.push('listening-socket');
80
+ const parent = byPid.get(process.parent);
81
+ if (parent && within(root.path, parent.cwd)) evidence.push('observed-parent');
82
+ if (process.parent <= 1 || !parent) evidence.push('parent-outside-scan');
83
+ if (process.session === process.pid || process.group === process.pid) evidence.push('separate-session-or-group');
84
+ group.members.push({ ...publicProcess(process), evidence });
85
+ group.unresolved.push(identity(process));
86
+ groups.set(id, group);
87
+ }
88
+ return [...groups.values()];
89
+ }
90
+
91
+ export function publicProcess(row) {
92
+ const { projectHint, cpuTime, ...visible } = row;
93
+ return visible;
94
+ }
95
+
96
+ export function summarize(project, rows, discovered) {
97
+ const processes = rows.filter(row => project.members.some(member => matches(member, row))).map(publicProcess);
98
+ const owners = new Map();
99
+ for (const p of processes) {
100
+ for (const port of p.ports) owners.set(`${port.address}/${port.port}`, port);
101
+ }
102
+ return { id: project.id, name: project.name, cwd: project.cwd, instance: project.instance, state: project.state,
103
+ confidence: project.members.length ? 'reviewed-or-contained' : 'registered',
104
+ processes, processCount: processes.length, ports: [...owners.values()],
105
+ rssSum: processes.some(p => p.rss !== undefined) ? processes.reduce((n, p) => n + (p.rss ?? 0), 0) : undefined,
106
+ cpuPercent: processes.some(p => p.cpuPercent !== undefined) ? processes.reduce((n, p) => n + (p.cpuPercent ?? 0), 0) : undefined,
107
+ dependencies: project.recipe?.dependencies ?? [], containers: project.recipe?.containers ?? [], systemd: project.recipe?.systemd ?? [],
108
+ supervisors: project.recipe?.supervisors?.map(({ kind, name, shared }) => ({ kind, name, shared })) ?? [],
109
+ excluded: project.excluded, unresolved: discovered?.unresolved ?? [], observedAt: new Date().toISOString(),
110
+ restart: project.recipe ? { configured: true, gracefulStop: Boolean(project.recipe.stop) } : { configured: false },
111
+ metrics: 'CPU is sampled as percent of one core. RSS sums resident pages and can count shared pages more than once.' };
112
+ }