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 CHANGED
@@ -1,63 +1,3 @@
1
- # zlx
1
+ # symlx
2
2
 
3
- Temporary CLI bin linker for local development.
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("zlx")
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 zlx exits")
59
- .option("--bin-dir <dir>", "target bin directory (default: ~/.zlx/bin)")
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) => {
@@ -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 zlx behavior:
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.getZlxPaths)(options.binDir);
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 === "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);
@@ -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.getZlxPaths = getZlxPaths;
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 getZlxPaths(customBinDir) {
12
- // zlx keeps mutable runtime state under the user's home directory.
13
- const rootDir = node_path_1.default.join(node_os_1.default.homedir(), ".zlx");
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 ? 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
  }
@@ -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(`[zlx] uncaught exception: ${String(error)}\n`);
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(`[zlx] unhandled rejection: ${String(reason)}\n`);
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.ensureZlxDirectories = ensureZlxDirectories;
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
- 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,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 ensureZlxDirectories(binDir, sessionDir) {
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
- 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/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 = "[zlx]";
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
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "symlx",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Temporary local CLI bin linker",
5
5
  "license": "MIT",
6
6
  "bin": {
7
- "symlx": "dist/cli.js"
7
+ "symlx": "dist/cli.js",
8
+ "cx": "dist/cli.js"
8
9
  },
9
10
  "files": [
10
11
  "dist",