symlx 0.1.1 → 0.1.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 +1 -61
- package/dist/commands/serve.js +10 -8
- package/dist/core/paths.js +9 -5
- package/dist/services/session-store.js +22 -16
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,63 +1,3 @@
|
|
|
1
1
|
# symlx
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## What it does
|
|
6
|
-
|
|
7
|
-
`symlx serve` reads the current project's `package.json` `bin` entries and symlinks them into:
|
|
8
|
-
|
|
9
|
-
`~/.symlx/bin`
|
|
10
|
-
|
|
11
|
-
While `symlx serve` is running, those commands can be used from anywhere (if `~/.symlx/bin` is on your `PATH`).
|
|
12
|
-
|
|
13
|
-
When the process exits, it removes only the links it created.
|
|
14
|
-
|
|
15
|
-
## Command framework and structure
|
|
16
|
-
|
|
17
|
-
`symlx` uses:
|
|
18
|
-
|
|
19
|
-
- `commander` for command orchestration and typed options.
|
|
20
|
-
- `prompts` for interactive/TUI collision handling.
|
|
21
|
-
|
|
22
|
-
Project layout:
|
|
23
|
-
|
|
24
|
-
- `src/commands` command handlers
|
|
25
|
-
- `src/services` bin/session/lifecycle services
|
|
26
|
-
- `src/core` shared types/path helpers
|
|
27
|
-
- `src/ui` terminal UI prompts/logging
|
|
28
|
-
|
|
29
|
-
## Usage
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
npx symlx serve
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
or if installed globally:
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
symlx serve
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### Serve options
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
symlx serve --collision prompt
|
|
45
|
-
symlx serve --collision skip
|
|
46
|
-
symlx serve --collision fail
|
|
47
|
-
symlx serve --collision overwrite
|
|
48
|
-
symlx serve --non-interactive
|
|
49
|
-
symlx serve --bin-dir /custom/bin
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
If your shell does not already include `~/.symlx/bin`, add:
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
export PATH="$HOME/.symlx/bin:$PATH"
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Notes
|
|
59
|
-
|
|
60
|
-
- Prompt mode lets you choose overwrite/skip/abort when a command name already exists.
|
|
61
|
-
- In non-interactive sessions, prompt mode falls back to skip.
|
|
62
|
-
- Stale links from dead sessions are cleaned on the next startup.
|
|
63
|
-
- `kill -9` cannot cleanup instantly, but stale cleanup runs next time.
|
|
3
|
+
tomorrow...
|
package/dist/commands/serve.js
CHANGED
|
@@ -54,23 +54,25 @@ async function runServe(options) {
|
|
|
54
54
|
const cwd = process.cwd();
|
|
55
55
|
const paths = (0, paths_1.getSymlxPaths)(options.binDir);
|
|
56
56
|
// Prepare runtime directories and recover stale sessions from previous abnormal exits.
|
|
57
|
-
(0, session_store_1.ensureSymlxDirectories)(paths.binDir, paths.sessionDir);
|
|
58
57
|
(0, session_store_1.cleanupStaleSessions)(paths.sessionDir);
|
|
58
|
+
(0, session_store_1.ensureSymlxDirectories)(paths.binDir, paths.sessionDir);
|
|
59
59
|
const bins = (0, package_bins_1.readBins)(cwd);
|
|
60
60
|
// Prompt policy only works when we can interact with a TTY.
|
|
61
|
-
const usePrompts = options.collision ===
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
const usePrompts = options.collision === 'prompt' &&
|
|
62
|
+
!options.nonInteractive &&
|
|
63
|
+
isInteractiveSession();
|
|
64
|
+
if (options.collision === 'prompt' && !usePrompts) {
|
|
65
|
+
log.warn('prompt collision mode requested but session is non-interactive; falling back to skip');
|
|
64
66
|
}
|
|
65
67
|
// Link creation returns both successful links and explicit skips.
|
|
66
68
|
const linkResult = await (0, link_manager_1.createLinks)({
|
|
67
69
|
bins,
|
|
68
70
|
binDir: paths.binDir,
|
|
69
71
|
policy: options.collision,
|
|
70
|
-
collisionResolver: usePrompts ? collision_prompt_1.promptCollisionDecision : undefined
|
|
72
|
+
collisionResolver: usePrompts ? collision_prompt_1.promptCollisionDecision : undefined,
|
|
71
73
|
});
|
|
72
74
|
if (linkResult.created.length === 0) {
|
|
73
|
-
throw new Error(
|
|
75
|
+
throw new Error('no links were created');
|
|
74
76
|
}
|
|
75
77
|
// Session file is the source of truth for cleaning this exact run's links.
|
|
76
78
|
const sessionPath = (0, session_store_1.createSessionFilePath)(paths.sessionDir);
|
|
@@ -78,7 +80,7 @@ async function runServe(options) {
|
|
|
78
80
|
pid: process.pid,
|
|
79
81
|
cwd,
|
|
80
82
|
createdAt: new Date().toISOString(),
|
|
81
|
-
links: linkResult.created
|
|
83
|
+
links: linkResult.created,
|
|
82
84
|
};
|
|
83
85
|
(0, session_store_1.persistSession)(sessionPath, sessionRecord);
|
|
84
86
|
// Always cleanup linked commands when this process leaves.
|
|
@@ -95,7 +97,7 @@ async function runServe(options) {
|
|
|
95
97
|
if (!(0, paths_1.pathContainsDir)(process.env.PATH, paths.binDir)) {
|
|
96
98
|
log.info(`add this to your shell config if needed:\nexport PATH="${paths.binDir}:$PATH"`);
|
|
97
99
|
}
|
|
98
|
-
log.info(
|
|
100
|
+
log.info('running. press Ctrl+C to cleanup links.');
|
|
99
101
|
// Keep process alive indefinitely; lifecycle handlers handle termination and cleanup.
|
|
100
102
|
await new Promise(() => {
|
|
101
103
|
setInterval(() => undefined, 60_000);
|
package/dist/core/paths.js
CHANGED
|
@@ -10,12 +10,14 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
10
10
|
// Central place for runtime paths so every command/service resolves locations consistently.
|
|
11
11
|
function getSymlxPaths(customBinDir) {
|
|
12
12
|
// symlx keeps mutable runtime state under the user's home directory.
|
|
13
|
-
const
|
|
13
|
+
const rootSymlxDir = node_path_1.default.join(node_os_1.default.homedir(), '.symlx');
|
|
14
14
|
// Commands are linked here unless the caller overrides with --bin-dir.
|
|
15
|
-
const binDir = customBinDir
|
|
15
|
+
const binDir = customBinDir
|
|
16
|
+
? node_path_1.default.resolve(customBinDir)
|
|
17
|
+
: node_path_1.default.join(rootSymlxDir, 'bin');
|
|
16
18
|
// Session files live separately from bins and are used for stale cleanup.
|
|
17
|
-
const sessionDir = node_path_1.default.join(
|
|
18
|
-
return {
|
|
19
|
+
const sessionDir = node_path_1.default.join(rootSymlxDir, 'sessions');
|
|
20
|
+
return { binDir, sessionDir };
|
|
19
21
|
}
|
|
20
22
|
// Checks if PATH already contains a directory so we can avoid noisy setup hints.
|
|
21
23
|
function pathContainsDir(currentPath, targetDir) {
|
|
@@ -23,6 +25,8 @@ function pathContainsDir(currentPath, targetDir) {
|
|
|
23
25
|
return false;
|
|
24
26
|
}
|
|
25
27
|
const resolvedTarget = node_path_1.default.resolve(targetDir);
|
|
26
|
-
const parts = currentPath
|
|
28
|
+
const parts = currentPath
|
|
29
|
+
.split(node_path_1.default.delimiter)
|
|
30
|
+
.map((item) => node_path_1.default.resolve(item));
|
|
27
31
|
return parts.includes(resolvedTarget);
|
|
28
32
|
}
|
|
@@ -10,27 +10,26 @@ exports.createSessionFilePath = createSessionFilePath;
|
|
|
10
10
|
exports.cleanupSession = cleanupSession;
|
|
11
11
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
12
|
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
-
// Lightweight JSON reader used for session metadata files.
|
|
14
|
-
function readJsonFile(filePath) {
|
|
15
|
-
const raw = node_fs_1.default.readFileSync(filePath, "utf8");
|
|
16
|
-
return JSON.parse(raw);
|
|
17
|
-
}
|
|
18
13
|
// Checks whether a PID from a previous session is still alive.
|
|
19
14
|
function isProcessAlive(pid) {
|
|
15
|
+
// PIDs are always positive integer typically less the 2^15
|
|
20
16
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
21
17
|
return false;
|
|
22
18
|
}
|
|
23
19
|
try {
|
|
20
|
+
// Check on the process without killing it (signal 0)
|
|
24
21
|
process.kill(pid, 0);
|
|
25
22
|
return true;
|
|
26
23
|
}
|
|
27
24
|
catch (error) {
|
|
28
25
|
const code = error.code;
|
|
29
|
-
|
|
26
|
+
// If the error code is not ESRCH, then it means the process does not exist
|
|
27
|
+
// If it's EPERM, then it's running, you simply don't have permission to signal it (fair enough)
|
|
28
|
+
return code !== 'ESRCH';
|
|
30
29
|
}
|
|
31
30
|
}
|
|
32
31
|
// Session files are best-effort state; deletion failure should not fail the command.
|
|
33
|
-
function
|
|
32
|
+
function deleteFile(filePath) {
|
|
34
33
|
try {
|
|
35
34
|
node_fs_1.default.unlinkSync(filePath);
|
|
36
35
|
}
|
|
@@ -39,9 +38,10 @@ function deleteSessionFile(filePath) {
|
|
|
39
38
|
}
|
|
40
39
|
}
|
|
41
40
|
// Invalid/corrupted session files are ignored and later removed.
|
|
42
|
-
function
|
|
41
|
+
function loadSessionFileJSON(filePath) {
|
|
43
42
|
try {
|
|
44
|
-
|
|
43
|
+
const raw = node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
44
|
+
return JSON.parse(raw);
|
|
45
45
|
}
|
|
46
46
|
catch {
|
|
47
47
|
return undefined;
|
|
@@ -67,6 +67,7 @@ function cleanupLinks(links) {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
// -----------------------------------------------------------
|
|
70
71
|
// Ensures runtime directories exist before linking/saving sessions.
|
|
71
72
|
function ensureSymlxDirectories(binDir, sessionDir) {
|
|
72
73
|
node_fs_1.default.mkdirSync(binDir, { recursive: true });
|
|
@@ -74,28 +75,33 @@ function ensureSymlxDirectories(binDir, sessionDir) {
|
|
|
74
75
|
}
|
|
75
76
|
// Reaps stale sessions left behind by crashes/kill -9 and removes their symlinks.
|
|
76
77
|
function cleanupStaleSessions(sessionDir) {
|
|
78
|
+
// If the directory does not exist, return early
|
|
77
79
|
if (!node_fs_1.default.existsSync(sessionDir)) {
|
|
78
80
|
return;
|
|
79
81
|
}
|
|
82
|
+
// Loop through the files within the session
|
|
80
83
|
for (const entry of node_fs_1.default.readdirSync(sessionDir)) {
|
|
81
|
-
|
|
84
|
+
const filePath = node_path_1.default.join(sessionDir, entry);
|
|
85
|
+
// Delete any files that are not .json, session files can only be JSON
|
|
86
|
+
if (!entry.endsWith('.json')) {
|
|
87
|
+
deleteFile(filePath);
|
|
82
88
|
continue;
|
|
83
89
|
}
|
|
84
|
-
|
|
85
|
-
const record =
|
|
90
|
+
// If the expected file structure has been corrupted, delete the file
|
|
91
|
+
const record = loadSessionFileJSON(filePath);
|
|
86
92
|
if (!record) {
|
|
87
|
-
|
|
93
|
+
deleteFile(filePath);
|
|
88
94
|
continue;
|
|
89
95
|
}
|
|
90
96
|
if (!isProcessAlive(record.pid)) {
|
|
91
97
|
cleanupLinks(record.links);
|
|
92
|
-
|
|
98
|
+
deleteFile(filePath);
|
|
93
99
|
}
|
|
94
100
|
}
|
|
95
101
|
}
|
|
96
102
|
// Persists currently linked commands so future runs can clean stale state.
|
|
97
103
|
function persistSession(sessionPath, record) {
|
|
98
|
-
node_fs_1.default.writeFileSync(sessionPath, `${JSON.stringify(record, null, 2)}\n`,
|
|
104
|
+
node_fs_1.default.writeFileSync(sessionPath, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
|
|
99
105
|
}
|
|
100
106
|
// Produces unique session file names to avoid collisions across concurrent runs.
|
|
101
107
|
function createSessionFilePath(sessionDir) {
|
|
@@ -105,5 +111,5 @@ function createSessionFilePath(sessionDir) {
|
|
|
105
111
|
// Cleanup for the active process/session.
|
|
106
112
|
function cleanupSession(sessionPath, links) {
|
|
107
113
|
cleanupLinks(links);
|
|
108
|
-
|
|
114
|
+
deleteFile(sessionPath);
|
|
109
115
|
}
|
package/package.json
CHANGED