symlx 0.1.2 → 0.1.3

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/dist/cli.js CHANGED
@@ -35,38 +35,27 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  })();
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
37
  const commander_1 = require("commander");
38
- const serve_1 = require("./commands/serve");
39
38
  const log = __importStar(require("./ui/logger"));
40
- // Accepted values for --collision.
41
- const ALLOWED_COLLISIONS = new Set(["prompt", "skip", "fail", "overwrite"]);
42
- // Converts raw CLI input into a validated union type used by the serve command.
43
- function parseCollisionPolicy(value) {
44
- if (!ALLOWED_COLLISIONS.has(value)) {
45
- throw new Error(`invalid collision policy "${value}". expected: prompt|skip|fail|overwrite`);
46
- }
47
- return value;
39
+ const serve_1 = require("./commands/serve");
40
+ function collectBinEntry(value, previous = []) {
41
+ previous.push(value);
42
+ return previous;
48
43
  }
49
44
  async function main() {
50
45
  // Commander orchestrates top-level commands/options and help output.
51
46
  const program = new commander_1.Command();
52
47
  program
53
- .name("symlx")
54
- .description("Temporary CLI bin linker with lifecycle cleanup")
48
+ .name('symlx')
49
+ .description('Temporary CLI bin linker with lifecycle cleanup')
55
50
  .showHelpAfterError();
56
51
  program
57
- .command("serve")
52
+ .command('serve')
58
53
  .description("Link this project's package.json bins until symlx exits")
59
- .option("--bin-dir <dir>", "target bin directory (default: ~/.symlx/bin)")
60
- .option("--collision <policy>", "collision mode: prompt|skip|fail|overwrite", "prompt")
61
- .option("--non-interactive", "disable interactive prompts", false)
62
- .action(async (options) => {
63
- // Delegate all runtime behavior to the command module.
64
- await (0, serve_1.runServe)({
65
- binDir: options.binDir,
66
- collision: parseCollisionPolicy(options.collision),
67
- nonInteractive: options.nonInteractive
68
- });
69
- });
54
+ .option('--bin-dir <dir>', 'target bin directory (default: ~/.symlx/bin)')
55
+ .option('--collision <policy>', 'collision mode: prompt|skip|fail|overwrite', 'prompt')
56
+ .option('--non-interactive', 'disable interactive prompts', false)
57
+ .option('--bin <name=path>', 'custom bin mapping (repeatable), e.g. --bin my-cli=./cli.js', collectBinEntry, [])
58
+ .action(serve_1.serveCommand);
70
59
  await program.parseAsync(process.argv);
71
60
  }
72
61
  // Centralized fatal error boundary for command execution.
@@ -33,14 +33,17 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.runServe = runServe;
37
- const paths_1 = require("../core/paths");
36
+ exports.serveCommand = serveCommand;
37
+ const paths_1 = require("../lib/paths");
38
38
  const link_manager_1 = require("../services/link-manager");
39
39
  const lifecycle_1 = require("../services/lifecycle");
40
+ // @ts-ignore
40
41
  const package_bins_1 = require("../services/package-bins");
41
42
  const session_store_1 = require("../services/session-store");
42
43
  const collision_prompt_1 = require("../ui/collision-prompt");
43
44
  const log = __importStar(require("../ui/logger"));
45
+ const options_1 = require("../lib/options");
46
+ const schema_1 = require("../lib/schema");
44
47
  // Prompts require an interactive terminal; scripts/CI should avoid prompt mode.
45
48
  function isInteractiveSession() {
46
49
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
@@ -50,7 +53,7 @@ function isInteractiveSession() {
50
53
  // 2) create links
51
54
  // 3) persist session
52
55
  // 4) keep process alive and cleanup on exit
53
- async function runServe(options) {
56
+ async function run(options) {
54
57
  const cwd = process.cwd();
55
58
  const paths = (0, paths_1.getSymlxPaths)(options.binDir);
56
59
  // Prepare runtime directories and recover stale sessions from previous abnormal exits.
@@ -103,3 +106,7 @@ async function runServe(options) {
103
106
  setInterval(() => undefined, 60_000);
104
107
  });
105
108
  }
109
+ function serveCommand(inlineOptions) {
110
+ const options = (0, options_1.resolveOptions)(schema_1.serveInlineOptionsSchema, inlineOptions);
111
+ return run(options);
112
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveOptions = resolveOptions;
7
+ const path_1 = __importDefault(require("path"));
8
+ const node_os_1 = __importDefault(require("node:os"));
9
+ const utils_1 = require("./utils");
10
+ const validate_1 = require("./validate");
11
+ const package_bins_1 = require("../services/package-bins");
12
+ const defaultOptions = {
13
+ collision: 'prompt',
14
+ nonInteractive: false,
15
+ binDir: path_1.default.join(node_os_1.default.homedir(), '.symlx', 'bin'),
16
+ bin: {},
17
+ };
18
+ // Function to aggregate all options from different sources in order or priority
19
+ function resolveOptions(inlineOptionsSchema, inlineOptions) {
20
+ // Load the bin from package.json
21
+ const packageJSONOptions = (0, package_bins_1.loadPackageJSONOptions)(process.cwd());
22
+ // Load and validate the options from the config file,
23
+ // silently overriding invalid non-critical values with defaults or inline based on order of priority
24
+ const configFileOptions = (0, utils_1.loadConfigFileOptions)();
25
+ const validatedConfigFileOptions = (0, validate_1.validateConfigFileOptions)(configFileOptions);
26
+ // Validate the CLI inline options if available
27
+ const validatedInlineOptions = (0, validate_1.validateInlineOptions)(inlineOptionsSchema, inlineOptions);
28
+ // Default options first
29
+ // -> Config file options override defaults
30
+ // -> CLI inline options overrides config file options
31
+ const finalOptiions = {
32
+ ...defaultOptions,
33
+ ...(validatedConfigFileOptions ?? {}),
34
+ ...(validatedInlineOptions ?? {}),
35
+ };
36
+ if (!Object.entries(finalOptiions.bin).length) {
37
+ throw new Error([
38
+ 'no bin entries found. add at least one bin in any of these places:',
39
+ '1) package.json -> "bin": { "my-cli": "./cli.js" }',
40
+ '2) symlx.config.json -> "bin": { "my-cli": "./cli.js" }',
41
+ '3) inline CLI -> symlx serve --bin my-cli=./cli.js',
42
+ '4) if package.json "bin" is a string, set a valid package.json "name" (used to infer the bin name).',
43
+ ].join('\n'));
44
+ }
45
+ return finalOptiions;
46
+ }
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getSymlxPaths = getSymlxPaths;
7
+ exports.pathContainsDir = pathContainsDir;
8
+ const node_os_1 = __importDefault(require("node:os"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ function getSymlxPaths(binDir) {
11
+ // Keep mutable runtime state under the user's home directory.
12
+ // Session files live separately from bins and are used for stale cleanup.
13
+ const sessionDir = node_path_1.default.join(node_os_1.default.homedir(), '.symlx', 'sessions');
14
+ return { binDir, sessionDir };
15
+ }
16
+ // Checks if PATH already contains a directory so we can avoid noisy setup hints.
17
+ function pathContainsDir(currentPath, targetDir) {
18
+ if (!currentPath) {
19
+ return false;
20
+ }
21
+ const resolvedTarget = node_path_1.default.resolve(targetDir);
22
+ const parts = currentPath
23
+ .split(node_path_1.default.delimiter)
24
+ .map((item) => node_path_1.default.resolve(item));
25
+ return parts.includes(resolvedTarget);
26
+ }
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.serveInlineOptionsSchema = exports.configFileOptionsSchema = exports.binEntriesToRecordSchema = void 0;
37
+ const zod_1 = require("zod");
38
+ const log = __importStar(require("../ui/logger"));
39
+ const binNameSchema = zod_1.z
40
+ .string()
41
+ .regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, 'invalid bin name');
42
+ const binTargetSchema = zod_1.z
43
+ .string()
44
+ .trim()
45
+ .min(1)
46
+ .regex(/^\.{1,2}\//, 'bin target must be a relative path like ./cli.js');
47
+ const customBinSchema = zod_1.z.record(binNameSchema, binTargetSchema);
48
+ const configFileOptionsSchema = zod_1.z.object({
49
+ binDir: zod_1.z
50
+ .string()
51
+ .regex(/(^|[\\/])\.[^\\/]+([\\/]|$)/, 'binDir must include a dotted folder segment')
52
+ .optional(),
53
+ collision: zod_1.z
54
+ .enum(['prompt', 'skip', 'fail', 'overwrite'])
55
+ .optional()
56
+ .catch(() => {
57
+ log.warn('invalid "collision" value in config file; using default.');
58
+ return undefined;
59
+ }),
60
+ nonInteractive: zod_1.z
61
+ .boolean()
62
+ .optional()
63
+ .catch(() => {
64
+ log.warn('invalid "nonInteractive" value in config file; using default.');
65
+ return undefined;
66
+ }),
67
+ bin: customBinSchema.optional(),
68
+ });
69
+ exports.configFileOptionsSchema = configFileOptionsSchema;
70
+ const binEntrySchema = zod_1.z
71
+ .string()
72
+ .regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?=\.{1,2}\/.+$/, 'expected <name=./relative/path>');
73
+ exports.binEntriesToRecordSchema = zod_1.z
74
+ .array(binEntrySchema)
75
+ .optional()
76
+ .default([])
77
+ .transform((entries) => Object.fromEntries(entries.map((entry) => {
78
+ const [name, target] = entry.split('=', 2);
79
+ return [name, target];
80
+ })));
81
+ const serveInlineOptionsSchema = configFileOptionsSchema.extend({
82
+ bin: exports.binEntriesToRecordSchema,
83
+ });
84
+ exports.serveInlineOptionsSchema = serveInlineOptionsSchema;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadJSONFile = loadJSONFile;
7
+ exports.loadConfigFileOptions = loadConfigFileOptions;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ // Invalid/corrupted JSON files are ignored.
11
+ function loadJSONFile(filePath) {
12
+ try {
13
+ const raw = node_fs_1.default.readFileSync(filePath, 'utf8');
14
+ return JSON.parse(raw);
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ }
20
+ function loadConfigFileOptions() {
21
+ const cwd = process.cwd();
22
+ const configPath = node_path_1.default.join(cwd, 'symlx.config.json');
23
+ const configFileOptions = loadJSONFile(configPath);
24
+ return configFileOptions;
25
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateInlineOptions = validateInlineOptions;
4
+ exports.validateConfigFileOptions = validateConfigFileOptions;
5
+ const schema_1 = require("./schema");
6
+ function formatIssues(error) {
7
+ const details = error.issues
8
+ .map((issue) => {
9
+ const pathLabel = issue.path.length > 0 ? issue.path.join('.') : 'value';
10
+ return `${pathLabel}: ${issue.message}`;
11
+ })
12
+ .join('; ');
13
+ return details || 'invalid input';
14
+ }
15
+ function validateInlineOptions(schema, input, label = 'input') {
16
+ const result = schema.safeParse(input || {});
17
+ if (!result.success) {
18
+ throw new Error(`invalid ${label}: ${formatIssues(result.error)}`);
19
+ }
20
+ return result.data;
21
+ }
22
+ function validateConfigFileOptions(input, label = 'input') {
23
+ const result = schema_1.configFileOptionsSchema.safeParse(input || {});
24
+ if (!result.success) {
25
+ throw new Error(`invalid ${label}: ${formatIssues(result.error)}`);
26
+ }
27
+ return result.data;
28
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.serveOptionsSchema = exports.collisionPolicySchema = void 0;
4
+ exports.validate = validate;
5
+ const zod_1 = require("zod");
6
+ const collisionPolicySchema = zod_1.z.enum(['prompt', 'skip', 'fail', 'overwrite']);
7
+ exports.collisionPolicySchema = collisionPolicySchema;
8
+ const serveOptionsSchema = zod_1.z.object({
9
+ binDir: zod_1.z.string().trim().min(1).optional(),
10
+ collision: collisionPolicySchema,
11
+ nonInteractive: zod_1.z.boolean(),
12
+ });
13
+ exports.serveOptionsSchema = serveOptionsSchema;
14
+ function formatIssues(error) {
15
+ const details = error.issues
16
+ .map((issue) => {
17
+ const pathLabel = issue.path.length > 0 ? issue.path.join('.') : 'value';
18
+ return `${pathLabel}: ${issue.message}`;
19
+ })
20
+ .join('; ');
21
+ return details || 'invalid input';
22
+ }
23
+ function validate(schema, input, label = 'input') {
24
+ const result = schema.safeParse(input);
25
+ if (!result.success) {
26
+ throw new Error(`invalid ${label}: ${formatIssues(result.error)}`);
27
+ }
28
+ return result.data;
29
+ }
@@ -13,7 +13,7 @@ function tryLstat(filePath) {
13
13
  }
14
14
  catch (error) {
15
15
  const code = error.code;
16
- if (code === "ENOENT") {
16
+ if (code === 'ENOENT') {
17
17
  return undefined;
18
18
  }
19
19
  throw error;
@@ -54,17 +54,17 @@ function toConflict(name, linkPath, target, node) {
54
54
  target,
55
55
  reason: node.existingTarget
56
56
  ? `already linked to ${node.existingTarget}`
57
- : "already exists as symlink",
57
+ : 'already exists as symlink',
58
58
  existingTarget: node.existingTarget,
59
- isSymlink: true
59
+ isSymlink: true,
60
60
  };
61
61
  }
62
62
  return {
63
63
  name,
64
64
  linkPath,
65
65
  target,
66
- reason: "already exists as a file",
67
- isSymlink: false
66
+ reason: 'already exists as a file',
67
+ isSymlink: false,
68
68
  };
69
69
  }
70
70
  // Creates symlinks for all project bins according to the selected collision strategy.
@@ -79,27 +79,34 @@ async function createLinks(params) {
79
79
  if (existingNode) {
80
80
  const conflict = toConflict(name, linkPath, target, existingNode);
81
81
  // Reusing the exact same link is always a no-op.
82
- if (conflict.existingTarget && node_path_1.default.resolve(conflict.existingTarget) === node_path_1.default.resolve(target)) {
83
- skipped.push({ name, linkPath, reason: "already linked to requested target" });
82
+ if (conflict.existingTarget &&
83
+ node_path_1.default.resolve(conflict.existingTarget) === node_path_1.default.resolve(target)) {
84
+ skipped.push({
85
+ name,
86
+ linkPath,
87
+ reason: 'already linked to requested target',
88
+ });
84
89
  continue;
85
90
  }
86
91
  let decision;
87
- if (policy === "skip") {
88
- decision = "skip";
92
+ if (policy === 'skip') {
93
+ decision = 'skip';
89
94
  }
90
- else if (policy === "overwrite") {
91
- decision = "overwrite";
95
+ else if (policy === 'overwrite') {
96
+ decision = 'overwrite';
92
97
  }
93
- else if (policy === "fail") {
98
+ else if (policy === 'fail') {
94
99
  throw new Error(`command "${name}" conflicts at ${linkPath}: ${conflict.reason}`);
95
100
  }
96
101
  else {
97
- decision = collisionResolver ? await collisionResolver(conflict) : "skip";
102
+ decision = collisionResolver
103
+ ? await collisionResolver(conflict)
104
+ : 'skip';
98
105
  }
99
- if (decision === "abort") {
106
+ if (decision === 'abort') {
100
107
  throw new Error(`aborted on collision for command "${name}"`);
101
108
  }
102
- if (decision === "skip") {
109
+ if (decision === 'skip') {
103
110
  skipped.push({ name, linkPath, reason: conflict.reason });
104
111
  continue;
105
112
  }
@@ -3,56 +3,60 @@ 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.readBins = readBins;
6
+ exports.loadPackageJSONOptions = loadPackageJSONOptions;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
+ const utils_1 = require("../lib/utils");
9
10
  // npm allows `bin` as a string; in that form the command name defaults to package name
10
11
  // (without scope for scoped packages).
11
12
  function inferBinName(packageName) {
12
13
  if (!packageName) {
13
- throw new Error("package.json is missing `name`, needed when `bin` is a string");
14
+ return undefined;
14
15
  }
15
- if (packageName.startsWith("@")) {
16
- const parts = packageName.split("/");
16
+ if (packageName.startsWith('@')) {
17
+ const parts = packageName.split('/');
17
18
  if (parts.length !== 2 || !parts[1]) {
18
- throw new Error(`invalid package name: ${packageName}`);
19
+ return undefined;
19
20
  }
20
21
  return parts[1];
21
22
  }
22
23
  return packageName;
23
24
  }
24
- function readJsonFile(filePath) {
25
- const raw = node_fs_1.default.readFileSync(filePath, "utf8");
26
- return JSON.parse(raw);
27
- }
28
25
  // Loads and validates all bin entries for the current project.
29
26
  // Returned map is command name => absolute target file path.
30
- function readBins(cwd) {
31
- const packageJsonPath = node_path_1.default.join(cwd, "package.json");
27
+ function loadPackageJSONOptions(cwd) {
28
+ const packageJsonPath = node_path_1.default.join(cwd, 'package.json');
32
29
  if (!node_fs_1.default.existsSync(packageJsonPath)) {
33
- throw new Error(`missing package.json in ${cwd}`);
34
- }
35
- const packageJson = readJsonFile(packageJsonPath);
36
- if (!packageJson.bin) {
37
- throw new Error("package.json has no `bin` field");
38
- }
39
- const bins = new Map();
40
- if (typeof packageJson.bin === "string") {
41
- bins.set(inferBinName(packageJson.name), node_path_1.default.resolve(cwd, packageJson.bin));
30
+ return {
31
+ bin: {},
32
+ };
33
+ }
34
+ const packageJson = (0, utils_1.loadJSONFile)(packageJsonPath);
35
+ if (!packageJson || !packageJson.bin) {
36
+ return {
37
+ bin: {},
38
+ };
39
+ }
40
+ const bins = {};
41
+ if (typeof packageJson.bin === 'string') {
42
+ const inferredBinName = inferBinName(packageJson.name);
43
+ if (inferredBinName) {
44
+ bins[inferredBinName] = node_path_1.default.resolve(cwd, packageJson.bin);
45
+ }
42
46
  }
43
47
  else {
44
48
  for (const [name, relTarget] of Object.entries(packageJson.bin)) {
45
- bins.set(name, node_path_1.default.resolve(cwd, relTarget));
46
- }
47
- }
48
- if (bins.size === 0) {
49
- throw new Error("no bin entries found");
50
- }
51
- // Fail fast if package.json points to non-existing executables.
52
- for (const [name, target] of bins.entries()) {
53
- if (!node_fs_1.default.existsSync(target)) {
54
- throw new Error(`bin target for "${name}" does not exist: ${target}`);
49
+ bins[name] = node_path_1.default.resolve(cwd, relTarget);
55
50
  }
56
51
  }
57
- return bins;
52
+ // if (bins.size === 0) {
53
+ // throw new Error('no bin entries found');
54
+ // }
55
+ // // Fail fast if package.json points to non-existing executables.
56
+ // for (const [name, target] of bins.entries()) {
57
+ // if (!fs.existsSync(target)) {
58
+ // throw new Error(`bin target for "${name}" does not exist: ${target}`);
59
+ // }
60
+ // }
61
+ return { bin: {} };
58
62
  }
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -10,6 +43,8 @@ exports.createSessionFilePath = createSessionFilePath;
10
43
  exports.cleanupSession = cleanupSession;
11
44
  const node_fs_1 = __importDefault(require("node:fs"));
12
45
  const node_path_1 = __importDefault(require("node:path"));
46
+ const log = __importStar(require("../ui/logger"));
47
+ const utils_1 = require("../lib/utils");
13
48
  // Checks whether a PID from a previous session is still alive.
14
49
  function isProcessAlive(pid) {
15
50
  // PIDs are always positive integer typically less the 2^15
@@ -37,16 +72,6 @@ function deleteFile(filePath) {
37
72
  // Best-effort cleanup.
38
73
  }
39
74
  }
40
- // Invalid/corrupted session files are ignored and later removed.
41
- function loadSessionFileJSON(filePath) {
42
- try {
43
- const raw = node_fs_1.default.readFileSync(filePath, 'utf8');
44
- return JSON.parse(raw);
45
- }
46
- catch {
47
- return undefined;
48
- }
49
- }
50
75
  // Removes only symlinks that still point to the exact targets we created.
51
76
  // This avoids deleting user-managed commands with the same name.
52
77
  function cleanupLinks(links) {
@@ -79,6 +104,7 @@ function cleanupStaleSessions(sessionDir) {
79
104
  if (!node_fs_1.default.existsSync(sessionDir)) {
80
105
  return;
81
106
  }
107
+ let cleanUpCount = 0;
82
108
  // Loop through the files within the session
83
109
  for (const entry of node_fs_1.default.readdirSync(sessionDir)) {
84
110
  const filePath = node_path_1.default.join(sessionDir, entry);
@@ -88,16 +114,21 @@ function cleanupStaleSessions(sessionDir) {
88
114
  continue;
89
115
  }
90
116
  // If the expected file structure has been corrupted, delete the file
91
- const record = loadSessionFileJSON(filePath);
117
+ const record = (0, utils_1.loadJSONFile)(filePath);
92
118
  if (!record) {
93
119
  deleteFile(filePath);
94
120
  continue;
95
121
  }
122
+ // If process is dead, unlink the command from the bin and delete the session file
96
123
  if (!isProcessAlive(record.pid)) {
97
124
  cleanupLinks(record.links);
98
125
  deleteFile(filePath);
126
+ cleanUpCount++;
99
127
  }
100
128
  }
129
+ if (cleanUpCount > 0) {
130
+ log.info(`cleaned up ${cleanUpCount} expired session${cleanUpCount > 1 ? 's' : ''}`);
131
+ }
101
132
  }
102
133
  // Persists currently linked commands so future runs can clean stale state.
103
134
  function persistSession(sessionPath, record) {
@@ -9,32 +9,32 @@ const prompts_1 = __importDefault(require("prompts"));
9
9
  // Returning "abort" bubbles up as an error to stop the current serve run.
10
10
  async function promptCollisionDecision(conflict) {
11
11
  const response = await (0, prompts_1.default)({
12
- type: "select",
13
- name: "decision",
12
+ type: 'select',
13
+ name: 'decision',
14
14
  message: `command "${conflict.name}" already exists`,
15
15
  choices: [
16
16
  {
17
- title: "Overwrite existing command",
18
- value: "overwrite",
19
- description: `Replace ${conflict.linkPath}`
17
+ title: 'Overwrite existing command',
18
+ value: 'overwrite',
19
+ description: `Replace ${conflict.linkPath}`,
20
20
  },
21
21
  {
22
- title: "Skip this command",
23
- value: "skip",
24
- description: conflict.reason
22
+ title: 'Skip this command',
23
+ value: 'skip',
24
+ description: conflict.reason,
25
25
  },
26
26
  {
27
- title: "Abort",
28
- value: "abort",
29
- description: "Stop serve without linking remaining commands"
30
- }
27
+ title: 'Abort',
28
+ value: 'abort',
29
+ description: 'Stop serve without linking remaining commands',
30
+ },
31
31
  ],
32
- initial: 1
32
+ initial: 1,
33
33
  }, {
34
- onCancel: () => false
34
+ onCancel: () => false,
35
35
  });
36
36
  if (!response.decision) {
37
- return "abort";
37
+ return 'abort';
38
38
  }
39
39
  return response.decision;
40
40
  }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "symlx",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Temporary local CLI bin linker",
5
5
  "license": "MIT",
6
6
  "bin": {
7
- "symlx": "dist/cli.js",
8
- "cx": "dist/cli.js"
7
+ "symlx": "dist/clii.js",
8
+ "cx": "dist/clii.js"
9
9
  },
10
10
  "files": [
11
11
  "dist",
@@ -26,7 +26,8 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "commander": "^12.1.0",
29
- "prompts": "^2.4.2"
29
+ "prompts": "^2.4.2",
30
+ "zod": "^4.3.6"
30
31
  },
31
32
  "scripts": {
32
33
  "build": "tsc -p tsconfig.json && chmod +x dist/cli.js",