onmymachine 0.1.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 +94 -0
- package/bin/onmymachine.js +90 -0
- package/package.json +19 -0
- package/src/collect.js +84 -0
- package/src/diff.js +55 -0
- package/src/redact.js +16 -0
- package/src/registry.js +39 -0
- package/src/render.js +82 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sreejith
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# onmymachine
|
|
2
|
+
|
|
3
|
+
> Kill "works on my machine" in 10 seconds.
|
|
4
|
+
|
|
5
|
+
<!-- TODO(record): asciinema/vhs demo before launch -->
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
## The problem
|
|
9
|
+
|
|
10
|
+
Your code runs fine. Your teammate pulls it and it crashes. Now you're playing
|
|
11
|
+
20 questions on Slack: *"What Node version? What's in your PATH? Do you even
|
|
12
|
+
have Docker?"*
|
|
13
|
+
|
|
14
|
+
Stop asking. Diff the machines.
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
**You** (the person it works for):
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npx onmymachine
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This writes `onmymachine.json` — a fingerprint of your dev environment.
|
|
25
|
+
Send that file to your teammate (Slack, email, carrier pigeon).
|
|
26
|
+
|
|
27
|
+
**Your teammate** (the person it's broken for):
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npx onmymachine diff onmymachine.json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Tools
|
|
35
|
+
✖ node snapshot: 20.11.0 this machine: 22.3.0
|
|
36
|
+
● docker in snapshot (27.1.1) — missing here
|
|
37
|
+
|
|
38
|
+
Env values
|
|
39
|
+
✖ JAVA_HOME snapshot: ~/jdk-21 this machine: ~/jdk-17
|
|
40
|
+
|
|
41
|
+
✓ 24 tools match
|
|
42
|
+
3 differences found — one of these might be your "works on my machine".
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Done. No more guessing.
|
|
46
|
+
|
|
47
|
+
## What it captures
|
|
48
|
+
|
|
49
|
+
- **Tool versions** — node, npm, python, git, docker, java, go, rustc, and ~20 more
|
|
50
|
+
- **System** — OS, release, architecture, shell
|
|
51
|
+
- **Env vars** — all *names*; values only for dev-relevant vars (`JAVA_HOME`, `GOPATH`, ...)
|
|
52
|
+
- **PATH** — every entry, so "it's not even on my PATH" gets caught too
|
|
53
|
+
|
|
54
|
+
## Privacy
|
|
55
|
+
|
|
56
|
+
Built to be safe to share:
|
|
57
|
+
|
|
58
|
+
- **No hostname, no username** — never collected
|
|
59
|
+
- Paths under your home directory become `~`
|
|
60
|
+
- Anything that looks like a secret (`*_TOKEN`, `*_KEY`, `*PASSWORD*`, ...) is `[redacted]`
|
|
61
|
+
- **Zero network calls, zero telemetry, zero dependencies** — read the entire
|
|
62
|
+
source over coffee
|
|
63
|
+
|
|
64
|
+
## More
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
onmymachine --label "sree-laptop" # name your snapshot
|
|
68
|
+
onmymachine diff snap.json --all # also show what matches
|
|
69
|
+
onmymachine diff snap.json --json # machine-readable diff
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**CI drift guard:** commit a golden snapshot of your build box; `onmymachine
|
|
73
|
+
diff golden.json` exits `1` when the runner drifts.
|
|
74
|
+
|
|
75
|
+
## FAQ
|
|
76
|
+
|
|
77
|
+
**Why not just use Docker?**
|
|
78
|
+
Because the bug report says "works on my machine", not "works in my container".
|
|
79
|
+
Real development happens on hosts — with host Node, host PATH, host env vars.
|
|
80
|
+
|
|
81
|
+
**Windows?**
|
|
82
|
+
First-class. Windows, macOS, and Linux.
|
|
83
|
+
|
|
84
|
+
**Does it upload my snapshot anywhere?**
|
|
85
|
+
Never. It writes a local file. You choose who sees it.
|
|
86
|
+
|
|
87
|
+
## Contributing
|
|
88
|
+
|
|
89
|
+
Issues and PRs welcome. The whole tool is ~400 lines of dependency-free
|
|
90
|
+
Node — `npm test` runs everything.
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { collect } from '../src/collect.js';
|
|
5
|
+
import { diffSnapshots } from '../src/diff.js';
|
|
6
|
+
import { renderSnapshotSummary, renderDiff, shouldColor } from '../src/render.js';
|
|
7
|
+
|
|
8
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
9
|
+
|
|
10
|
+
const HELP = `onmymachine — kill "works on my machine" in 10 seconds.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
onmymachine snapshot this machine to onmymachine.json
|
|
14
|
+
onmymachine snapshot [options]
|
|
15
|
+
onmymachine diff <file> compare a snapshot file with this machine
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
-o, --out <file> snapshot output path (default: onmymachine.json)
|
|
19
|
+
--label <name> label this snapshot (e.g. your name)
|
|
20
|
+
--json print machine-readable JSON to stdout
|
|
21
|
+
--all diff: show matching entries too
|
|
22
|
+
-h, --help show this help
|
|
23
|
+
-v, --version show version
|
|
24
|
+
|
|
25
|
+
Exit codes: 0 ok / no differences · 1 differences found · 2 error`;
|
|
26
|
+
|
|
27
|
+
function fail(msg) {
|
|
28
|
+
process.stderr.write(`error: ${msg}\n\nRun onmymachine --help for usage.\n`);
|
|
29
|
+
process.exit(2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function main() {
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = parseArgs({
|
|
36
|
+
args: process.argv.slice(2),
|
|
37
|
+
allowPositionals: true,
|
|
38
|
+
options: {
|
|
39
|
+
out: { type: 'string', short: 'o', default: 'onmymachine.json' },
|
|
40
|
+
label: { type: 'string' },
|
|
41
|
+
json: { type: 'boolean', default: false },
|
|
42
|
+
all: { type: 'boolean', default: false },
|
|
43
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
44
|
+
version: { type: 'boolean', short: 'v', default: false },
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
} catch (e) {
|
|
48
|
+
fail(e.message);
|
|
49
|
+
}
|
|
50
|
+
const { values, positionals } = parsed;
|
|
51
|
+
|
|
52
|
+
if (values.help) { console.log(HELP); return; }
|
|
53
|
+
if (values.version) { console.log(pkg.version); return; }
|
|
54
|
+
|
|
55
|
+
const command = positionals[0] ?? 'snapshot';
|
|
56
|
+
const color = shouldColor();
|
|
57
|
+
|
|
58
|
+
if (command === 'snapshot') {
|
|
59
|
+
const snapshot = await collect({ label: values.label });
|
|
60
|
+
if (values.json) { console.log(JSON.stringify(snapshot, null, 2)); return; }
|
|
61
|
+
writeFileSync(values.out, JSON.stringify(snapshot, null, 2) + '\n');
|
|
62
|
+
console.log(renderSnapshotSummary(snapshot, { color }));
|
|
63
|
+
console.log(`\nWrote ${values.out}`);
|
|
64
|
+
console.log(`Send it to your teammate and have them run:\n npx onmymachine diff ${values.out}`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (command === 'diff') {
|
|
69
|
+
const file = positionals[1];
|
|
70
|
+
if (!file) fail('diff needs a snapshot file: onmymachine diff <file>');
|
|
71
|
+
let snapshot;
|
|
72
|
+
try {
|
|
73
|
+
snapshot = JSON.parse(readFileSync(file, 'utf8'));
|
|
74
|
+
} catch (e) {
|
|
75
|
+
fail(`cannot read snapshot file "${file}": ${e.message}`);
|
|
76
|
+
}
|
|
77
|
+
if (snapshot.$schema !== 'onmymachine/v1') {
|
|
78
|
+
process.stderr.write(`warning: unexpected schema "${snapshot.$schema}" — trying anyway\n`);
|
|
79
|
+
}
|
|
80
|
+
const current = await collect();
|
|
81
|
+
const diff = diffSnapshots(snapshot, current);
|
|
82
|
+
if (values.json) console.log(JSON.stringify(diff, null, 2));
|
|
83
|
+
else console.log(renderDiff(diff, { color, all: values.all }));
|
|
84
|
+
process.exit(diff.differences === 0 ? 0 : 1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fail(`unknown command "${command}"`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
main().catch((e) => fail(e.stack ?? String(e)));
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "onmymachine",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Kill \"works on my machine\": snapshot your dev environment, share the file, diff it on another machine.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "onmymachine": "bin/onmymachine.js" },
|
|
7
|
+
"engines": { "node": ">=18" },
|
|
8
|
+
"files": ["bin", "src", "README.md", "LICENSE"],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test",
|
|
11
|
+
"prepublishOnly": "npm test"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["cli", "environment", "diff", "debugging", "works-on-my-machine", "devtools", "devx"],
|
|
14
|
+
"author": "Sreejith <iSreejith@gmail.com> (https://github.com/iSreejith)",
|
|
15
|
+
"repository": { "type": "git", "url": "git+https://github.com/iSreejith/onmymachine.git" },
|
|
16
|
+
"bugs": { "url": "https://github.com/iSreejith/onmymachine/issues" },
|
|
17
|
+
"homepage": "https://github.com/iSreejith/onmymachine#readme",
|
|
18
|
+
"license": "MIT"
|
|
19
|
+
}
|
package/src/collect.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import { exec, execFile } from 'node:child_process';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { TOOLS, parseVersion } from './registry.js';
|
|
5
|
+
import { redactValue, normalizeHome } from './redact.js';
|
|
6
|
+
|
|
7
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
8
|
+
|
|
9
|
+
export const DEV_ENV_ALLOWLIST = [
|
|
10
|
+
'NODE_ENV', 'JAVA_HOME', 'JDK_HOME', 'GOPATH', 'GOROOT', 'PYTHONPATH',
|
|
11
|
+
'VIRTUAL_ENV', 'CONDA_DEFAULT_ENV', 'NVM_DIR', 'ANDROID_HOME',
|
|
12
|
+
'CARGO_HOME', 'RUSTUP_HOME', 'DOTNET_ROOT', 'MAVEN_HOME', 'GRADLE_HOME',
|
|
13
|
+
'DOCKER_HOST', 'KUBECONFIG', 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY',
|
|
14
|
+
'LANG', 'LC_ALL', 'TZ', 'TERM', 'SHELL', 'EDITOR',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export function defaultProbe(cmd, args, timeoutMs = 5000) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const done = (err, stdout, stderr) => {
|
|
20
|
+
const out = `${stdout || ''}\n${stderr || ''}`.trim(); // java prints to stderr
|
|
21
|
+
resolve(out ? out : null);
|
|
22
|
+
};
|
|
23
|
+
const opts = { timeout: timeoutMs, windowsHide: true };
|
|
24
|
+
if (process.platform === 'win32') {
|
|
25
|
+
// cmd/args come only from the static TOOLS registry — never user input.
|
|
26
|
+
// A shell is required on Windows to resolve .cmd shims (npm, pnpm, ...).
|
|
27
|
+
exec(`${cmd} ${args.join(' ')}`, opts, done);
|
|
28
|
+
} else {
|
|
29
|
+
execFile(cmd, args, opts, done);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function detectShell(env = process.env, platform = process.platform) {
|
|
35
|
+
if (platform === 'win32') {
|
|
36
|
+
if (env.PSModulePath) return 'powershell';
|
|
37
|
+
if (env.ComSpec) return 'cmd';
|
|
38
|
+
return 'unknown';
|
|
39
|
+
}
|
|
40
|
+
const sh = env.SHELL || '';
|
|
41
|
+
return sh ? sh.split('/').pop() : 'unknown';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function collect({
|
|
45
|
+
probe = defaultProbe,
|
|
46
|
+
env = process.env,
|
|
47
|
+
platform = process.platform,
|
|
48
|
+
home = os.homedir(),
|
|
49
|
+
label,
|
|
50
|
+
} = {}) {
|
|
51
|
+
const toolEntries = await Promise.all(
|
|
52
|
+
TOOLS.map(async (t) => [t.name, parseVersion(await probe(t.cmd, t.args))]),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const values = {};
|
|
56
|
+
for (const name of DEV_ENV_ALLOWLIST) {
|
|
57
|
+
if (env[name] !== undefined) {
|
|
58
|
+
values[name] = redactValue(name, normalizeHome(env[name], home, platform));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const rawPath = env.PATH ?? env.Path ?? '';
|
|
63
|
+
const path = rawPath
|
|
64
|
+
.split(platform === 'win32' ? ';' : ':')
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
.map((p) => normalizeHome(p, home, platform));
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
$schema: 'onmymachine/v1',
|
|
70
|
+
meta: {
|
|
71
|
+
version: pkg.version,
|
|
72
|
+
createdAt: new Date().toISOString(),
|
|
73
|
+
...(label ? { label } : {}),
|
|
74
|
+
},
|
|
75
|
+
system: {
|
|
76
|
+
platform,
|
|
77
|
+
release: os.release(),
|
|
78
|
+
arch: os.arch(),
|
|
79
|
+
shell: detectShell(env, platform),
|
|
80
|
+
},
|
|
81
|
+
tools: Object.fromEntries(toolEntries),
|
|
82
|
+
env: { names: Object.keys(env).sort(), values, path },
|
|
83
|
+
};
|
|
84
|
+
}
|
package/src/diff.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
function pairStatus(a, b) {
|
|
2
|
+
if (a === b) return 'match';
|
|
3
|
+
if (a != null && b != null) return 'mismatch';
|
|
4
|
+
return a != null ? 'only-in-snapshot' : 'only-on-this-machine';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function setDiff(a, b) {
|
|
8
|
+
const sa = new Set(a), sb = new Set(b);
|
|
9
|
+
return {
|
|
10
|
+
onlyInSnapshot: [...sa].filter(x => !sb.has(x)).sort(),
|
|
11
|
+
onlyOnThisMachine: [...sb].filter(x => !sa.has(x)).sort(),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function diffSnapshots(snapshot, current) {
|
|
16
|
+
const tools = [];
|
|
17
|
+
const toolNames = new Set([
|
|
18
|
+
...Object.keys(snapshot.tools ?? {}),
|
|
19
|
+
...Object.keys(current.tools ?? {}),
|
|
20
|
+
]);
|
|
21
|
+
for (const name of [...toolNames].sort()) {
|
|
22
|
+
const a = snapshot.tools?.[name] ?? null;
|
|
23
|
+
const b = current.tools?.[name] ?? null;
|
|
24
|
+
tools.push({ name, snapshot: a, current: b, status: pairStatus(a, b) });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const system = ['platform', 'release', 'arch', 'shell'].map((key) => {
|
|
28
|
+
const a = snapshot.system?.[key] ?? null;
|
|
29
|
+
const b = current.system?.[key] ?? null;
|
|
30
|
+
return { key, snapshot: a, current: b, status: a === b ? 'match' : 'mismatch' };
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const envValues = [];
|
|
34
|
+
const valueNames = new Set([
|
|
35
|
+
...Object.keys(snapshot.env?.values ?? {}),
|
|
36
|
+
...Object.keys(current.env?.values ?? {}),
|
|
37
|
+
]);
|
|
38
|
+
for (const name of [...valueNames].sort()) {
|
|
39
|
+
const a = snapshot.env?.values?.[name] ?? null;
|
|
40
|
+
const b = current.env?.values?.[name] ?? null;
|
|
41
|
+
envValues.push({ name, snapshot: a, current: b, status: pairStatus(a, b) });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const envNames = setDiff(snapshot.env?.names ?? [], current.env?.names ?? []);
|
|
45
|
+
const path = setDiff(snapshot.env?.path ?? [], current.env?.path ?? []);
|
|
46
|
+
|
|
47
|
+
const differences =
|
|
48
|
+
tools.filter(t => t.status !== 'match').length +
|
|
49
|
+
system.filter(s => s.status !== 'match').length +
|
|
50
|
+
envValues.filter(v => v.status !== 'match').length +
|
|
51
|
+
envNames.onlyInSnapshot.length + envNames.onlyOnThisMachine.length +
|
|
52
|
+
path.onlyInSnapshot.length + path.onlyOnThisMachine.length;
|
|
53
|
+
|
|
54
|
+
return { tools, system, envValues, envNames, path, differences };
|
|
55
|
+
}
|
package/src/redact.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const SECRET_NAME = /(token|secret|key|passwd|password|pwd|credential|auth|private)/i;
|
|
2
|
+
|
|
3
|
+
export function isSecretName(name) {
|
|
4
|
+
return SECRET_NAME.test(String(name));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function redactValue(name, value) {
|
|
8
|
+
return isSecretName(name) ? '[redacted]' : value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function normalizeHome(value, home, platform = process.platform) {
|
|
12
|
+
if (!home || typeof value !== 'string') return value;
|
|
13
|
+
const escaped = home.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
14
|
+
const flags = platform === 'win32' ? 'gi' : 'g';
|
|
15
|
+
return value.replace(new RegExp(escaped, flags), '~');
|
|
16
|
+
}
|
package/src/registry.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const VERSION_RE = /\d+\.\d+(?:\.\d+)?/;
|
|
2
|
+
|
|
3
|
+
export function parseVersion(output) {
|
|
4
|
+
if (!output) return null;
|
|
5
|
+
const m = String(output).match(VERSION_RE);
|
|
6
|
+
return m ? m[0] : null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const v = (name, cmd = name, args = ['--version']) => ({ name, cmd, args });
|
|
10
|
+
|
|
11
|
+
export const TOOLS = [
|
|
12
|
+
v('node'),
|
|
13
|
+
v('npm'),
|
|
14
|
+
v('pnpm'),
|
|
15
|
+
v('yarn'),
|
|
16
|
+
v('bun'),
|
|
17
|
+
v('deno'),
|
|
18
|
+
v('python'),
|
|
19
|
+
v('python3'),
|
|
20
|
+
v('pip'),
|
|
21
|
+
v('git'),
|
|
22
|
+
v('docker'),
|
|
23
|
+
v('java', 'java', ['-version']),
|
|
24
|
+
v('go', 'go', ['version']),
|
|
25
|
+
v('rustc'),
|
|
26
|
+
v('cargo'),
|
|
27
|
+
v('ruby'),
|
|
28
|
+
v('gem'),
|
|
29
|
+
v('php'),
|
|
30
|
+
v('composer'),
|
|
31
|
+
v('dotnet'),
|
|
32
|
+
v('kubectl', 'kubectl', ['version', '--client']),
|
|
33
|
+
v('terraform', 'terraform', ['version']),
|
|
34
|
+
v('aws'),
|
|
35
|
+
v('gcc'),
|
|
36
|
+
v('clang'),
|
|
37
|
+
v('make'),
|
|
38
|
+
v('cmake'),
|
|
39
|
+
];
|
package/src/render.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export function shouldColor(stream = process.stdout, env = process.env) {
|
|
2
|
+
return Boolean(stream.isTTY) && !env.NO_COLOR;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function palette(enabled) {
|
|
6
|
+
const wrap = (code) => (s) => (enabled ? `\x1b[${code}m${s}\x1b[0m` : String(s));
|
|
7
|
+
return {
|
|
8
|
+
red: wrap('31'), green: wrap('32'), yellow: wrap('33'),
|
|
9
|
+
cyan: wrap('36'), bold: wrap('1'), dim: wrap('2'),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const pad = (s, n) => String(s).padEnd(n);
|
|
14
|
+
|
|
15
|
+
export function renderSnapshotSummary(snapshot, { color = false } = {}) {
|
|
16
|
+
const c = palette(color);
|
|
17
|
+
const lines = [];
|
|
18
|
+
const label = snapshot.meta?.label ? ` (${snapshot.meta.label})` : '';
|
|
19
|
+
lines.push(c.bold(`onmymachine snapshot${label}`));
|
|
20
|
+
const sys = snapshot.system ?? {};
|
|
21
|
+
lines.push(c.dim(`${sys.platform} ${sys.release} · ${sys.arch} · shell: ${sys.shell}`));
|
|
22
|
+
lines.push('');
|
|
23
|
+
const tools = Object.entries(snapshot.tools ?? {});
|
|
24
|
+
const installed = tools.filter(([, v]) => v);
|
|
25
|
+
for (const [name, version] of installed) {
|
|
26
|
+
lines.push(` ${c.green('✓')} ${pad(name, 12)} ${version}`);
|
|
27
|
+
}
|
|
28
|
+
const missing = tools.length - installed.length;
|
|
29
|
+
if (missing > 0) lines.push(c.dim(` ${missing} of ${tools.length} tools not detected`));
|
|
30
|
+
lines.push('');
|
|
31
|
+
lines.push(c.dim(`${(snapshot.env?.names ?? []).length} env var names · ${(snapshot.env?.path ?? []).length} PATH entries`));
|
|
32
|
+
return lines.join('\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function renderDiff(diff, { color = false, all = false } = {}) {
|
|
36
|
+
const c = palette(color);
|
|
37
|
+
const lines = [];
|
|
38
|
+
|
|
39
|
+
const statusLine = (label, a, b, status) => {
|
|
40
|
+
if (status === 'mismatch') {
|
|
41
|
+
return ` ${c.red('✖')} ${pad(label, 14)} snapshot: ${c.red(a)} this machine: ${c.red(b)}`;
|
|
42
|
+
}
|
|
43
|
+
if (status === 'only-in-snapshot') {
|
|
44
|
+
return ` ${c.yellow('●')} ${pad(label, 14)} in snapshot (${a}) — ${c.yellow('missing here')}`;
|
|
45
|
+
}
|
|
46
|
+
if (status === 'only-on-this-machine') {
|
|
47
|
+
return ` ${c.yellow('●')} ${pad(label, 14)} on this machine (${b}) — ${c.yellow('missing in snapshot')}`;
|
|
48
|
+
}
|
|
49
|
+
return ` ${c.green('✓')} ${pad(label, 14)} ${a}`;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const section = (title, rows, toLine) => {
|
|
53
|
+
const visible = rows.filter(r => all || r.status !== 'match');
|
|
54
|
+
if (visible.length === 0) return;
|
|
55
|
+
lines.push(c.bold(title));
|
|
56
|
+
for (const r of visible) lines.push(toLine(r));
|
|
57
|
+
lines.push('');
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
section('Tools', diff.tools, r => statusLine(r.name, r.snapshot, r.current, r.status));
|
|
61
|
+
section('System', diff.system, r => statusLine(r.key, r.snapshot, r.current, r.status));
|
|
62
|
+
section('Env values', diff.envValues, r => statusLine(r.name, r.snapshot, r.current, r.status));
|
|
63
|
+
|
|
64
|
+
const listSection = (title, group) => {
|
|
65
|
+
if (group.onlyInSnapshot.length === 0 && group.onlyOnThisMachine.length === 0) return;
|
|
66
|
+
lines.push(c.bold(title));
|
|
67
|
+
for (const n of group.onlyInSnapshot) lines.push(` ${c.yellow('●')} ${n} — only in snapshot`);
|
|
68
|
+
for (const n of group.onlyOnThisMachine) lines.push(` ${c.yellow('●')} ${n} — only on this machine`);
|
|
69
|
+
lines.push('');
|
|
70
|
+
};
|
|
71
|
+
listSection('Env var names', diff.envNames);
|
|
72
|
+
listSection('PATH entries', diff.path);
|
|
73
|
+
|
|
74
|
+
const matched = diff.tools.filter(t => t.status === 'match').length;
|
|
75
|
+
if (matched > 0) lines.push(c.green(`✓ ${matched} tools match`));
|
|
76
|
+
lines.push(
|
|
77
|
+
diff.differences === 0
|
|
78
|
+
? c.green('No differences — same setup. The bug is elsewhere. 🙃')
|
|
79
|
+
: c.bold(`${diff.differences} differences found — one of these might be your "works on my machine".`),
|
|
80
|
+
);
|
|
81
|
+
return lines.join('\n');
|
|
82
|
+
}
|