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 CHANGED
@@ -1,63 +1,3 @@
1
1
  # symlx
2
2
 
3
- Temporary CLI bin linker for local development.
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...
@@ -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 === "prompt" && !options.nonInteractive && isInteractiveSession();
62
- if (options.collision === "prompt" && !usePrompts) {
63
- log.warn("prompt collision mode requested but session is non-interactive; falling back to skip");
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("no links were created");
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("running. press Ctrl+C to cleanup links.");
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);
@@ -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 rootDir = node_path_1.default.join(node_os_1.default.homedir(), ".symlx");
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 ? node_path_1.default.resolve(customBinDir) : node_path_1.default.join(rootDir, "bin");
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(rootDir, "sessions");
18
- return { rootDir, binDir, sessionDir };
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.split(node_path_1.default.delimiter).map((item) => node_path_1.default.resolve(item));
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
- return code !== "ESRCH";
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 deleteSessionFile(filePath) {
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 loadSession(filePath) {
41
+ function loadSessionFileJSON(filePath) {
43
42
  try {
44
- return readJsonFile(filePath);
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
- if (!entry.endsWith(".json")) {
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
- const filePath = node_path_1.default.join(sessionDir, entry);
85
- const record = loadSession(filePath);
90
+ // If the expected file structure has been corrupted, delete the file
91
+ const record = loadSessionFileJSON(filePath);
86
92
  if (!record) {
87
- deleteSessionFile(filePath);
93
+ deleteFile(filePath);
88
94
  continue;
89
95
  }
90
96
  if (!isProcessAlive(record.pid)) {
91
97
  cleanupLinks(record.links);
92
- deleteSessionFile(filePath);
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`, "utf8");
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
- deleteSessionFile(sessionPath);
114
+ deleteFile(sessionPath);
109
115
  }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "symlx",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Temporary local CLI bin linker",
5
5
  "license": "MIT",
6
6
  "bin": {
7
- "lnk": "dist/cli.js"
7
+ "symlx": "dist/cli.js",
8
+ "cx": "dist/cli.js"
8
9
  },
9
10
  "files": [
10
11
  "dist",