mellos-mapping 0.19.0 → 0.20.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/README.md +366 -56
- package/README.zh-CN.md +319 -47
- package/dist/hook-session-start.mjs +239 -0
- package/dist/mmap.mjs +338 -0
- package/dist/server.mjs +1737 -897
- package/dist/store-paths.mjs +107 -0
- package/dist/watch.mjs +1612 -902
- package/lib/domain/ops.d.ts +171 -0
- package/lib/domain/ops.js +384 -0
- package/lib/domain/types.d.ts +283 -0
- package/lib/domain/types.js +153 -0
- package/lib/render/canvas.d.ts +50 -0
- package/lib/render/canvas.js +210 -0
- package/lib/render/draw.d.ts +37 -0
- package/lib/render/draw.js +111 -0
- package/lib/render/layout.d.ts +89 -0
- package/lib/render/layout.js +200 -0
- package/lib/render/options.d.ts +39 -0
- package/lib/render/options.js +10 -0
- package/lib/render/render.d.ts +88 -0
- package/lib/render/render.js +128 -0
- package/lib/render/routing.d.ts +56 -0
- package/lib/render/routing.js +244 -0
- package/lib/render/skins.d.ts +54 -0
- package/lib/render/skins.js +99 -0
- package/lib/render/width.d.ts +24 -0
- package/lib/render/width.js +139 -0
- package/lib/render/zoom-geometry.d.ts +52 -0
- package/lib/render/zoom-geometry.js +56 -0
- package/lib/semantics/semantics.d.ts +169 -0
- package/lib/semantics/semantics.js +380 -0
- package/lib/semantics/vocabulary.d.ts +79 -0
- package/lib/semantics/vocabulary.js +112 -0
- package/lib/store/format.d.ts +67 -0
- package/lib/store/format.js +334 -0
- package/lib/store/store.d.ts +296 -0
- package/lib/store/store.js +734 -0
- package/package.json +41 -5
- package/scripts/codex-register.mjs +89 -20
- package/scripts/install-mmap-command.mjs +293 -0
- package/scripts/mmap.mjs +213 -0
- package/scripts/open-pane.mjs +115 -254
- package/scripts/pane-core.mjs +418 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mellos-mapping",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.2",
|
|
4
4
|
"mcpName": "io.github.GuangminJu/mellos-mapping",
|
|
5
5
|
"description": "A live layered dependency map for bottom-up development — MCP server + terminal pane. Ghost the design first, then light nodes up from the bottom as they are built and verified.",
|
|
6
6
|
"type": "module",
|
|
@@ -25,16 +25,49 @@
|
|
|
25
25
|
],
|
|
26
26
|
"bin": {
|
|
27
27
|
"mellos-mapping": "dist/server.mjs",
|
|
28
|
-
"mellos-mapping-watch": "dist/watch.mjs"
|
|
28
|
+
"mellos-mapping-watch": "dist/watch.mjs",
|
|
29
|
+
"mmap": "dist/mmap.mjs"
|
|
30
|
+
},
|
|
31
|
+
"exports": {
|
|
32
|
+
"./domain/types": {
|
|
33
|
+
"types": "./lib/domain/types.d.ts",
|
|
34
|
+
"default": "./lib/domain/types.js"
|
|
35
|
+
},
|
|
36
|
+
"./domain/ops": {
|
|
37
|
+
"types": "./lib/domain/ops.d.ts",
|
|
38
|
+
"default": "./lib/domain/ops.js"
|
|
39
|
+
},
|
|
40
|
+
"./format": {
|
|
41
|
+
"types": "./lib/store/format.d.ts",
|
|
42
|
+
"default": "./lib/store/format.js"
|
|
43
|
+
},
|
|
44
|
+
"./store": {
|
|
45
|
+
"types": "./lib/store/store.d.ts",
|
|
46
|
+
"default": "./lib/store/store.js"
|
|
47
|
+
},
|
|
48
|
+
"./semantics": {
|
|
49
|
+
"types": "./lib/semantics/semantics.d.ts",
|
|
50
|
+
"default": "./lib/semantics/semantics.js"
|
|
51
|
+
},
|
|
52
|
+
"./render": {
|
|
53
|
+
"types": "./lib/render/render.d.ts",
|
|
54
|
+
"default": "./lib/render/render.js"
|
|
55
|
+
},
|
|
56
|
+
"./server": "./dist/server.mjs",
|
|
57
|
+
"./package.json": "./package.json"
|
|
29
58
|
},
|
|
30
59
|
"publishConfig": {
|
|
31
60
|
"registry": "https://registry.npmjs.org/"
|
|
32
61
|
},
|
|
33
62
|
"files": [
|
|
34
63
|
"dist",
|
|
64
|
+
"lib",
|
|
35
65
|
"README.zh-CN.md",
|
|
36
66
|
"scripts/codex-register.mjs",
|
|
37
|
-
"scripts/
|
|
67
|
+
"scripts/install-mmap-command.mjs",
|
|
68
|
+
"scripts/mmap.mjs",
|
|
69
|
+
"scripts/open-pane.mjs",
|
|
70
|
+
"scripts/pane-core.mjs"
|
|
38
71
|
],
|
|
39
72
|
"engines": {
|
|
40
73
|
"node": ">=18"
|
|
@@ -42,8 +75,11 @@
|
|
|
42
75
|
"scripts": {
|
|
43
76
|
"test": "vitest run",
|
|
44
77
|
"typecheck": "tsc --noEmit",
|
|
45
|
-
"
|
|
46
|
-
"
|
|
78
|
+
"typecheck:packages": "tsc -p tsconfig.packages.json",
|
|
79
|
+
"build": "node build.mjs && tsc -p tsconfig.lib.json",
|
|
80
|
+
"check:package": "node scripts/check-package-surface.mjs",
|
|
81
|
+
"prepack": "npm run build",
|
|
82
|
+
"verify": "npm run typecheck && npm run typecheck:packages && npm run test && npm run build && npm run check:package"
|
|
47
83
|
},
|
|
48
84
|
"devDependencies": {
|
|
49
85
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
@@ -12,37 +12,106 @@
|
|
|
12
12
|
* directory, which is exactly the contract dist/server.mjs already expects.
|
|
13
13
|
* The registered path is absolute and version-specific, so re-run after
|
|
14
14
|
* every plugin update.
|
|
15
|
+
*
|
|
16
|
+
* Two Windows facts shape the process handling below:
|
|
17
|
+
* - A missing `codex` is NOT a spawn error there. The call goes through
|
|
18
|
+
* cmd.exe (only a shell resolves both codex.exe and an npm .cmd shim), so
|
|
19
|
+
* "not recognized as an internal or external command" arrives as an
|
|
20
|
+
* ordinary non-zero exit, and a check for spawnSync's `error` field can
|
|
21
|
+
* never fire. PATH is probed up front instead.
|
|
22
|
+
* - The child writes its diagnostics in the console's OEM code page.
|
|
23
|
+
* Decoding those bytes as UTF-8 turns every non-English message into
|
|
24
|
+
* mojibake, so they are passed through as bytes and the terminal decodes
|
|
25
|
+
* them as it decodes everything else it prints.
|
|
26
|
+
*
|
|
27
|
+
* Pure helpers are exported for the spec; registration runs only as an entry
|
|
28
|
+
* point, so importing this file is inert.
|
|
15
29
|
*/
|
|
16
30
|
import { spawnSync } from 'node:child_process';
|
|
31
|
+
import { realpathSync } from 'node:fs';
|
|
17
32
|
import { dirname, join } from 'node:path';
|
|
18
33
|
import { fileURLToPath } from 'node:url';
|
|
19
34
|
|
|
20
|
-
|
|
21
|
-
const
|
|
35
|
+
/** cmd.exe's exit code for a command it cannot resolve on PATH. */
|
|
36
|
+
export const COMMAND_NOT_FOUND_EXIT_CODE = 9009;
|
|
37
|
+
|
|
38
|
+
/** Quote one argument for a cmd.exe command line. */
|
|
39
|
+
export function quoteForShell(arg) {
|
|
40
|
+
return /[\s"]/.test(arg) ? `"${arg}"` : arg;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* What went wrong, in the user's terms.
|
|
45
|
+
* @param status - the child's exit code (null = it never ran).
|
|
46
|
+
* @returns the line to print; a missing CLI is named as such on every
|
|
47
|
+
* platform, because that is the failure a first-time reader actually hits.
|
|
48
|
+
*/
|
|
49
|
+
export function describeFailure(status) {
|
|
50
|
+
return status === COMMAND_NOT_FOUND_EXIT_CODE || status === null
|
|
51
|
+
? 'codex CLI not found on PATH — install Codex first, then re-run this script.'
|
|
52
|
+
: `codex mcp add failed (exit ${status}).`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Whether `codex` resolves on PATH. */
|
|
56
|
+
function codexOnPath() {
|
|
57
|
+
const probe =
|
|
58
|
+
process.platform === 'win32'
|
|
59
|
+
? spawnSync('where', ['codex'], { stdio: 'ignore', windowsHide: true })
|
|
60
|
+
: spawnSync('command', ['-v', 'codex'], { shell: true, stdio: 'ignore' });
|
|
61
|
+
return probe.error === undefined && probe.status === 0;
|
|
62
|
+
}
|
|
22
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Run the codex CLI. Output is captured as BYTES (no `encoding`), so the
|
|
66
|
+
* child's console-code-page diagnostics reach the terminal unaltered.
|
|
67
|
+
*/
|
|
23
68
|
function codex(args) {
|
|
24
69
|
if (process.platform !== 'win32') {
|
|
25
|
-
return spawnSync('codex', args, { stdio: 'pipe'
|
|
70
|
+
return spawnSync('codex', args, { stdio: 'pipe' });
|
|
26
71
|
}
|
|
27
72
|
// Windows: codex may be an .exe or an npm .cmd shim; only a shell resolves
|
|
28
73
|
// both. The shell needs explicit quoting for paths with spaces.
|
|
29
|
-
const line = ['codex', ...args].map(
|
|
30
|
-
return spawnSync(line, { shell: true, stdio: 'pipe',
|
|
74
|
+
const line = ['codex', ...args].map(quoteForShell).join(' ');
|
|
75
|
+
return spawnSync(line, { shell: true, stdio: 'pipe', windowsHide: true });
|
|
31
76
|
}
|
|
32
77
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
);
|
|
44
|
-
|
|
78
|
+
function main() {
|
|
79
|
+
const pluginRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
80
|
+
const serverPath = join(pluginRoot, 'dist', 'server.mjs');
|
|
81
|
+
|
|
82
|
+
if (!codexOnPath()) {
|
|
83
|
+
console.error(describeFailure(COMMAND_NOT_FOUND_EXIT_CODE));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Idempotent: drop any previous (possibly stale, version-specific) entry.
|
|
88
|
+
codex(['mcp', 'remove', 'mellos-mapping']);
|
|
89
|
+
|
|
90
|
+
const added = codex(['mcp', 'add', 'mellos-mapping', '--', 'node', serverPath]);
|
|
91
|
+
if (added.status !== 0) {
|
|
92
|
+
if (added.stderr) process.stderr.write(added.stderr);
|
|
93
|
+
console.error(describeFailure(added.status));
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
if (added.stdout) process.stdout.write(added.stdout);
|
|
97
|
+
console.log(`mellos-mapping MCP registered with Codex: node ${serverPath}`);
|
|
98
|
+
console.log('State files resolve to each session’s working directory (.mellos/map.json).');
|
|
45
99
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Run only as an entry point, so the spec can import the helpers above
|
|
103
|
+
* without registering anything. Each script in this directory carries its own
|
|
104
|
+
* copy: package.json ships them file-by-file, so a shared scripts/ module
|
|
105
|
+
* would simply be missing from an npm install.
|
|
106
|
+
*/
|
|
107
|
+
function launchedAsEntry() {
|
|
108
|
+
const entry = process.argv[1];
|
|
109
|
+
if (entry === undefined) return false;
|
|
110
|
+
try {
|
|
111
|
+
return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (launchedAsEntry()) main();
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Put `mmap` on the PATH of a plugin install.
|
|
4
|
+
*
|
|
5
|
+
* node scripts/install-mmap-command.mjs [--uninstall] [--json]
|
|
6
|
+
*
|
|
7
|
+
* npm users get `mmap` for free — package.json declares it in `bin`. Claude
|
|
8
|
+
* Code installs this plugin by CLONING the repo, with no npm install and no
|
|
9
|
+
* bin shims, so the one command a human is meant to type has nowhere to be
|
|
10
|
+
* typed from. This script is that missing step, and nothing more: it writes
|
|
11
|
+
* two tiny shims into `%LOCALAPPDATA%\mellos-mapping\bin` — `mmap.cmd` for
|
|
12
|
+
* cmd/PowerShell, `mmap` for git-bash — and adds that one directory to the
|
|
13
|
+
* USER PATH.
|
|
14
|
+
*
|
|
15
|
+
* Nobody has to run it by hand: the SessionStart hook
|
|
16
|
+
* (src/hook/session-start.ts) notices a missing or stale shim when a session
|
|
17
|
+
* starts and runs this script with `--json`, which prints the outcome as one
|
|
18
|
+
* JSON line instead of prose — that outcome object is the contract between
|
|
19
|
+
* the two files. The script stays a standalone command for the cases the
|
|
20
|
+
* hook does not cover: `--uninstall`, and re-adding a PATH entry the user
|
|
21
|
+
* removed while the shims still exist (the hook checks only the shims).
|
|
22
|
+
*
|
|
23
|
+
* What it will NEVER do, and says so instead:
|
|
24
|
+
*
|
|
25
|
+
* - Touch the SYSTEM PATH. This is a per-user tool.
|
|
26
|
+
* - Rewrite a user PATH that `setx` cannot carry safely. `setx` writes a
|
|
27
|
+
* plain string and truncates past SETX_VALUE_MAX, so a PATH holding
|
|
28
|
+
* `%USERPROFILE%`-style references would come back with the references
|
|
29
|
+
* flattened, and a long one would come back SHORTER. Both are damage that
|
|
30
|
+
* outlives this tool, so in either case the shims are still installed and
|
|
31
|
+
* the exact line to add by hand is printed.
|
|
32
|
+
*
|
|
33
|
+
* Windows-first, deliberately: this plugin's pane is Windows Terminal. On
|
|
34
|
+
* other platforms `mmap` is `npm i -g mellos-mapping`, or a shell alias.
|
|
35
|
+
*
|
|
36
|
+
* Pure helpers are exported for the spec; the installer runs only as an entry
|
|
37
|
+
* point, so importing this file is inert.
|
|
38
|
+
*/
|
|
39
|
+
import { spawnSync } from 'node:child_process';
|
|
40
|
+
import { chmodSync, existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
41
|
+
import { join } from 'node:path';
|
|
42
|
+
|
|
43
|
+
import { launchedAsEntry, pluginRootOf } from './pane-core.mjs';
|
|
44
|
+
|
|
45
|
+
export const USAGE = 'usage: node scripts/install-mmap-command.mjs [--uninstall] [--json]';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Longest value `setx` stores without truncating it, in characters. Documented
|
|
49
|
+
* by the tool itself ("the maximum size is 1024 characters") and the reason
|
|
50
|
+
* this script would rather refuse than shorten someone's PATH.
|
|
51
|
+
*/
|
|
52
|
+
export const SETX_VALUE_MAX = 1024;
|
|
53
|
+
|
|
54
|
+
/** The one directory this installer owns, given a `%LOCALAPPDATA%`. */
|
|
55
|
+
export function binDirIn(localAppData) {
|
|
56
|
+
return join(localAppData, 'mellos-mapping', 'bin');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The cmd/PowerShell shim: forwards every argument, prints nothing of its own. */
|
|
60
|
+
export function cmdShim(mmapPath) {
|
|
61
|
+
return ['@echo off', `node "${mmapPath}" %*`, ''].join('\r\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The git-bash shim. The target is written with forward slashes: a Windows
|
|
66
|
+
* path inside sh double quotes keeps its backslashes verbatim, and `\U` in
|
|
67
|
+
* `C:\Users` is one escape away from being someone else's bug.
|
|
68
|
+
*/
|
|
69
|
+
export function shShim(mmapPath) {
|
|
70
|
+
return ['#!/bin/sh', `exec node "${mmapPath.replaceAll('\\', '/')}" "$@"`, ''].join('\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** PATH entries as written, empties dropped — the unit every rule below works on. */
|
|
74
|
+
function entriesOf(rawPath) {
|
|
75
|
+
return rawPath.split(';').filter((e) => e.trim() !== '');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Two PATH entries naming the same directory: case and a trailing slash do not count. */
|
|
79
|
+
function samePathEntry(a, b) {
|
|
80
|
+
const norm = (s) => s.trim().replace(/^"|"$/g, '').replace(/[\\/]+$/, '').toLowerCase();
|
|
81
|
+
return norm(a) === norm(b);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Is `dir` already on this PATH? Then installing it again must change nothing. */
|
|
85
|
+
export function pathContains(rawPath, dir) {
|
|
86
|
+
return entriesOf(rawPath).some((e) => samePathEntry(e, dir));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** `rawPath` with `dir` appended — unchanged when it is already there. */
|
|
90
|
+
export function pathWith(rawPath, dir) {
|
|
91
|
+
return pathContains(rawPath, dir) ? rawPath : [...entriesOf(rawPath), dir].join(';');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** `rawPath` with every mention of `dir` removed. */
|
|
95
|
+
export function pathWithout(rawPath, dir) {
|
|
96
|
+
return entriesOf(rawPath)
|
|
97
|
+
.filter((e) => !samePathEntry(e, dir))
|
|
98
|
+
.join(';');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Why `setx` must not be used to write `nextValue`, or undefined when it can.
|
|
103
|
+
*
|
|
104
|
+
* @param rawValue - the USER PATH exactly as the registry holds it.
|
|
105
|
+
* @param nextValue - what this script would write.
|
|
106
|
+
* @returns a sentence naming the damage that would be done, for the caller to
|
|
107
|
+
* print beside the manual instructions.
|
|
108
|
+
*/
|
|
109
|
+
export function setxRefusal(rawValue, nextValue) {
|
|
110
|
+
if (rawValue.includes('%')) {
|
|
111
|
+
return 'your user PATH contains %VARIABLE% references, and setx would store them flattened to whatever they mean right now';
|
|
112
|
+
}
|
|
113
|
+
if (nextValue.length > SETX_VALUE_MAX) {
|
|
114
|
+
return `your user PATH would be ${nextValue.length} characters and setx truncates past ${SETX_VALUE_MAX}`;
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* What bringing the PATH from `raw` to `wanted` takes — decided, not done.
|
|
121
|
+
* The single place the write/refuse/no-op judgment lives; both the prose
|
|
122
|
+
* path and the `--json` path execute exactly this plan.
|
|
123
|
+
* @returns {{action: 'unchanged'} | {action: 'write'} | {action: 'refused', reason: string}}
|
|
124
|
+
*/
|
|
125
|
+
export function planPath(raw, wanted) {
|
|
126
|
+
if (wanted === raw) return { action: 'unchanged' };
|
|
127
|
+
const reason = setxRefusal(raw, wanted);
|
|
128
|
+
return reason === undefined ? { action: 'write' } : { action: 'refused', reason };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The USER PATH exactly as the registry holds it — unexpanded, so a
|
|
133
|
+
* `%USERPROFILE%` in it is still a `%USERPROFILE%` here.
|
|
134
|
+
*
|
|
135
|
+
* node's own `process.env.Path` is the merged, expanded, per-process value and
|
|
136
|
+
* writing THAT back would fold the machine's PATH into the user's.
|
|
137
|
+
* @returns the raw value, or '' when the user has no PATH of their own.
|
|
138
|
+
*/
|
|
139
|
+
function readUserPath() {
|
|
140
|
+
const r = spawnSync('reg', ['query', 'HKCU\\Environment', '/v', 'Path'], { encoding: 'utf8', windowsHide: true });
|
|
141
|
+
const m = (r.stdout ?? '').match(/^\s*Path\s+REG_(?:EXPAND_)?SZ\s+(.*)$/m);
|
|
142
|
+
return m === null ? '' : m[1].trimEnd();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Ask Windows to store the user's PATH, and to tell running shells about it. */
|
|
146
|
+
function writeUserPath(value) {
|
|
147
|
+
const r = spawnSync('setx', ['PATH', value], { encoding: 'utf8', windowsHide: true });
|
|
148
|
+
return r.status === 0 ? { ok: true, value: undefined } : { ok: false, error: (r.stderr || r.stdout || '').trim() };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Bring the USER PATH to where it should be, saying out loud what changes
|
|
153
|
+
* before anything does.
|
|
154
|
+
* @param wanted - the PATH value this run wants.
|
|
155
|
+
* @param raw - the PATH value as it stands.
|
|
156
|
+
*/
|
|
157
|
+
function applyPath(raw, wanted) {
|
|
158
|
+
const plan = planPath(raw, wanted);
|
|
159
|
+
if (plan.action === 'unchanged') {
|
|
160
|
+
console.log('Your user PATH already says what it should — leaving it alone.');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (plan.action === 'refused') {
|
|
164
|
+
console.log(`NOT touching your user PATH: ${plan.reason}.`);
|
|
165
|
+
console.log('Add (or remove) this entry yourself, in Settings > "Edit environment variables for your account":');
|
|
166
|
+
console.log(` ${wanted}`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
console.log('Changing ONE thing in your Windows user environment — your user PATH becomes:');
|
|
170
|
+
console.log(` ${wanted}`);
|
|
171
|
+
const written = writeUserPath(wanted);
|
|
172
|
+
if (!written.ok) {
|
|
173
|
+
console.error(`setx refused: ${written.error}`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The install itself, with the narration stripped out: write the shims, bring
|
|
180
|
+
* the PATH in line, report what happened as data. This outcome object IS the
|
|
181
|
+
* `--json` output and the contract the SessionStart hook reads — change a
|
|
182
|
+
* field here and src/hook/session-start.ts must follow.
|
|
183
|
+
*
|
|
184
|
+
* @returns one of
|
|
185
|
+
* {kind: 'not-built', missing} — dist/mmap.mjs absent, nothing written;
|
|
186
|
+
* {kind: 'installed', binDir, cmdPath, shPath, path, wanted, reason?} —
|
|
187
|
+
* shims written; `path` says what happened to the USER PATH:
|
|
188
|
+
* 'unchanged' — the entry was already there,
|
|
189
|
+
* 'updated' — the entry was appended,
|
|
190
|
+
* 'refused' — setx would damage this PATH (`reason` says how); the
|
|
191
|
+
* entry in `wanted` must be added by hand,
|
|
192
|
+
* 'error' — setx itself failed (`reason` is its message).
|
|
193
|
+
*/
|
|
194
|
+
export function install(localAppData, pluginRoot) {
|
|
195
|
+
const mmapPath = join(pluginRoot, 'dist', 'mmap.mjs');
|
|
196
|
+
if (!existsSync(mmapPath)) return { kind: 'not-built', missing: mmapPath };
|
|
197
|
+
|
|
198
|
+
const binDir = binDirIn(localAppData);
|
|
199
|
+
const cmdPath = join(binDir, 'mmap.cmd');
|
|
200
|
+
const shPath = join(binDir, 'mmap');
|
|
201
|
+
mkdirSync(binDir, { recursive: true });
|
|
202
|
+
writeFileSync(cmdPath, cmdShim(mmapPath));
|
|
203
|
+
writeFileSync(shPath, shShim(mmapPath));
|
|
204
|
+
try {
|
|
205
|
+
chmodSync(shPath, 0o755); // git-bash honors the execute bit it can see
|
|
206
|
+
} catch {
|
|
207
|
+
// A filesystem without POSIX modes still runs the shim through `sh`;
|
|
208
|
+
// refusing to install over a chmod would be theatre.
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const raw = readUserPath();
|
|
212
|
+
const wanted = pathWith(raw, binDir);
|
|
213
|
+
const plan = planPath(raw, wanted);
|
|
214
|
+
if (plan.action === 'unchanged') return { kind: 'installed', binDir, cmdPath, shPath, path: 'unchanged', wanted };
|
|
215
|
+
if (plan.action === 'refused') {
|
|
216
|
+
return { kind: 'installed', binDir, cmdPath, shPath, path: 'refused', wanted, reason: plan.reason };
|
|
217
|
+
}
|
|
218
|
+
const written = writeUserPath(wanted);
|
|
219
|
+
return written.ok
|
|
220
|
+
? { kind: 'installed', binDir, cmdPath, shPath, path: 'updated', wanted }
|
|
221
|
+
: { kind: 'installed', binDir, cmdPath, shPath, path: 'error', wanted, reason: written.error };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function main() {
|
|
225
|
+
const argv = process.argv.slice(2);
|
|
226
|
+
const uninstall = argv.includes('--uninstall');
|
|
227
|
+
const json = argv.includes('--json');
|
|
228
|
+
const unknown = argv.find((a) => a !== '--uninstall' && a !== '--json');
|
|
229
|
+
if (unknown !== undefined) {
|
|
230
|
+
console.error(`unknown argument "${unknown}"\n${USAGE}`);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
if (uninstall && json) {
|
|
234
|
+
console.error(`--uninstall talks to a human; it has no --json mode\n${USAGE}`);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
if (process.platform !== 'win32') {
|
|
238
|
+
console.error('This installer is for Windows. Elsewhere, `npm i -g mellos-mapping` provides the same `mmap` command.');
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
const localAppData = process.env['LOCALAPPDATA'];
|
|
242
|
+
if (localAppData === undefined || localAppData === '') {
|
|
243
|
+
console.error('LOCALAPPDATA is not set — there is no per-user place to install into.');
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (uninstall) {
|
|
248
|
+
const binDir = binDirIn(localAppData);
|
|
249
|
+
const raw = readUserPath();
|
|
250
|
+
for (const path of [join(binDir, 'mmap.cmd'), join(binDir, 'mmap')]) {
|
|
251
|
+
rmSync(path, { force: true });
|
|
252
|
+
console.log(`removed ${path}`);
|
|
253
|
+
}
|
|
254
|
+
applyPath(raw, pathWithout(raw, binDir));
|
|
255
|
+
console.log('Close your terminal app entirely and reopen it for the change to take effect — a new tab keeps the old PATH.');
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const outcome = install(localAppData, pluginRootOf(import.meta.url));
|
|
260
|
+
if (json) {
|
|
261
|
+
console.log(JSON.stringify(outcome));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (outcome.kind === 'not-built') {
|
|
265
|
+
console.error(`the plugin is not built — run "npm run build" (missing ${outcome.missing})`);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
console.log(`wrote ${outcome.cmdPath}`);
|
|
269
|
+
console.log(`wrote ${outcome.shPath}`);
|
|
270
|
+
switch (outcome.path) {
|
|
271
|
+
case 'unchanged':
|
|
272
|
+
console.log('Your user PATH already says what it should — leaving it alone.');
|
|
273
|
+
break;
|
|
274
|
+
case 'updated':
|
|
275
|
+
console.log('Changed ONE thing in your Windows user environment — your user PATH is now:');
|
|
276
|
+
console.log(` ${outcome.wanted}`);
|
|
277
|
+
break;
|
|
278
|
+
case 'refused':
|
|
279
|
+
console.log(`NOT touching your user PATH: ${outcome.reason}.`);
|
|
280
|
+
console.log('Add this entry yourself, in Settings > "Edit environment variables for your account":');
|
|
281
|
+
console.log(` ${outcome.binDir}`);
|
|
282
|
+
break;
|
|
283
|
+
case 'error':
|
|
284
|
+
console.error(`setx refused: ${outcome.reason}`);
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
console.log(
|
|
288
|
+
'Close Windows Terminal entirely and reopen it — a running terminal, new tabs included, keeps the old PATH.',
|
|
289
|
+
);
|
|
290
|
+
console.log('Then type `mmap` in any project: it opens the map pane, or closes the open one.');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (launchedAsEntry(process.argv[1], import.meta.url)) main();
|