symlx 0.1.0 → 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 +2 -62
- package/dist/cli.js +3 -3
- package/dist/commands/serve.js +12 -10
- package/dist/core/paths.js +12 -8
- package/dist/services/lifecycle.js +2 -2
- package/dist/services/session-store.js +24 -18
- package/dist/ui/logger.js +1 -1
- 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
|
-
`zlx serve` reads the current project's `package.json` `bin` entries and symlinks them into:
|
|
8
|
-
|
|
9
|
-
`~/.zlx/bin`
|
|
10
|
-
|
|
11
|
-
While `zlx serve` is running, those commands can be used from anywhere (if `~/.zlx/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
|
-
`zlx` 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 zlx serve
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
or if installed globally:
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
zlx serve
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### Serve options
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
zlx serve --collision prompt
|
|
45
|
-
zlx serve --collision skip
|
|
46
|
-
zlx serve --collision fail
|
|
47
|
-
zlx serve --collision overwrite
|
|
48
|
-
zlx serve --non-interactive
|
|
49
|
-
zlx serve --bin-dir /custom/bin
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
If your shell does not already include `~/.zlx/bin`, add:
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
export PATH="$HOME/.zlx/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/cli.js
CHANGED
|
@@ -50,13 +50,13 @@ async function main() {
|
|
|
50
50
|
// Commander orchestrates top-level commands/options and help output.
|
|
51
51
|
const program = new commander_1.Command();
|
|
52
52
|
program
|
|
53
|
-
.name("
|
|
53
|
+
.name("symlx")
|
|
54
54
|
.description("Temporary CLI bin linker with lifecycle cleanup")
|
|
55
55
|
.showHelpAfterError();
|
|
56
56
|
program
|
|
57
57
|
.command("serve")
|
|
58
|
-
.description("Link this project's package.json bins until
|
|
59
|
-
.option("--bin-dir <dir>", "target bin directory (default: ~/.
|
|
58
|
+
.description("Link this project's package.json bins until symlx exits")
|
|
59
|
+
.option("--bin-dir <dir>", "target bin directory (default: ~/.symlx/bin)")
|
|
60
60
|
.option("--collision <policy>", "collision mode: prompt|skip|fail|overwrite", "prompt")
|
|
61
61
|
.option("--non-interactive", "disable interactive prompts", false)
|
|
62
62
|
.action(async (options) => {
|
package/dist/commands/serve.js
CHANGED
|
@@ -45,32 +45,34 @@ const log = __importStar(require("../ui/logger"));
|
|
|
45
45
|
function isInteractiveSession() {
|
|
46
46
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
47
47
|
}
|
|
48
|
-
// Main
|
|
48
|
+
// Main symlx behavior:
|
|
49
49
|
// 1) resolve bins from package.json
|
|
50
50
|
// 2) create links
|
|
51
51
|
// 3) persist session
|
|
52
52
|
// 4) keep process alive and cleanup on exit
|
|
53
53
|
async function runServe(options) {
|
|
54
54
|
const cwd = process.cwd();
|
|
55
|
-
const paths = (0, paths_1.
|
|
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.ensureZlxDirectories)(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
|
@@ -3,19 +3,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.getSymlxPaths = getSymlxPaths;
|
|
7
7
|
exports.pathContainsDir = pathContainsDir;
|
|
8
8
|
const node_os_1 = __importDefault(require("node:os"));
|
|
9
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
10
|
// Central place for runtime paths so every command/service resolves locations consistently.
|
|
11
|
-
function
|
|
12
|
-
//
|
|
13
|
-
const
|
|
11
|
+
function getSymlxPaths(customBinDir) {
|
|
12
|
+
// symlx keeps mutable runtime state under the user's home directory.
|
|
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
|
}
|
|
@@ -24,12 +24,12 @@ function registerLifecycleCleanup(cleanup) {
|
|
|
24
24
|
process.on("SIGHUP", onSignal);
|
|
25
25
|
// Fatal process events still attempt cleanup before exiting with failure.
|
|
26
26
|
process.on("uncaughtException", (error) => {
|
|
27
|
-
process.stderr.write(`[
|
|
27
|
+
process.stderr.write(`[symlx] uncaught exception: ${String(error)}\n`);
|
|
28
28
|
runCleanup();
|
|
29
29
|
process.exit(1);
|
|
30
30
|
});
|
|
31
31
|
process.on("unhandledRejection", (reason) => {
|
|
32
|
-
process.stderr.write(`[
|
|
32
|
+
process.stderr.write(`[symlx] unhandled rejection: ${String(reason)}\n`);
|
|
33
33
|
runCleanup();
|
|
34
34
|
process.exit(1);
|
|
35
35
|
});
|
|
@@ -3,34 +3,33 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.ensureSymlxDirectories = ensureSymlxDirectories;
|
|
7
7
|
exports.cleanupStaleSessions = cleanupStaleSessions;
|
|
8
8
|
exports.persistSession = persistSession;
|
|
9
9
|
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,35 +67,41 @@ function cleanupLinks(links) {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
// -----------------------------------------------------------
|
|
70
71
|
// Ensures runtime directories exist before linking/saving sessions.
|
|
71
|
-
function
|
|
72
|
+
function ensureSymlxDirectories(binDir, sessionDir) {
|
|
72
73
|
node_fs_1.default.mkdirSync(binDir, { recursive: true });
|
|
73
74
|
node_fs_1.default.mkdirSync(sessionDir, { recursive: true });
|
|
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/dist/ui/logger.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.info = info;
|
|
4
4
|
exports.warn = warn;
|
|
5
5
|
exports.error = error;
|
|
6
|
-
const PREFIX = "[
|
|
6
|
+
const PREFIX = "[symlx]";
|
|
7
7
|
// Thin logging wrappers keep formatting centralized and easy to swap later.
|
|
8
8
|
function info(message) {
|
|
9
9
|
process.stdout.write(`${PREFIX} ${message}\n`);
|
package/package.json
CHANGED