symlx 0.1.3 → 0.1.5

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.
@@ -0,0 +1,81 @@
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.startServeSession = startServeSession;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ function resolveCollisionSummary(options, dependencies) {
9
+ if (options.collision !== 'prompt') {
10
+ return {
11
+ requested: options.collision,
12
+ effective: options.collision,
13
+ };
14
+ }
15
+ const shouldFallbackToSkip = options.nonInteractive || !dependencies.isInteractiveSession();
16
+ if (!shouldFallbackToSkip) {
17
+ return {
18
+ requested: options.collision,
19
+ effective: 'prompt',
20
+ };
21
+ }
22
+ return {
23
+ requested: options.collision,
24
+ effective: 'skip',
25
+ warning: 'prompt collision mode requested but session is non-interactive; falling back to skip (use --collision overwrite|fail to avoid skips)',
26
+ };
27
+ }
28
+ function formatSkippedLinks(skippedLinks) {
29
+ const visibleSkippedLinks = skippedLinks.slice(0, 5);
30
+ const details = visibleSkippedLinks
31
+ .map((skip) => `- ${skip.name}: ${skip.reason}`)
32
+ .join('\n');
33
+ const remainingCount = skippedLinks.length - visibleSkippedLinks.length;
34
+ const remaining = remainingCount > 0 ? `\n- ...and ${remainingCount} more` : '';
35
+ return `${details}${remaining}`;
36
+ }
37
+ function assertLinksCreated(linkResult) {
38
+ if (linkResult.created.length > 0) {
39
+ return;
40
+ }
41
+ if (linkResult.skipped.length === 0) {
42
+ throw new Error('no links were created');
43
+ }
44
+ throw new Error([
45
+ 'no links were created because all candidate commands were skipped.',
46
+ formatSkippedLinks(linkResult.skipped),
47
+ 'use --collision overwrite or --collision fail for stricter behavior.',
48
+ ].join('\n'));
49
+ }
50
+ async function startServeSession(params) {
51
+ const { options, dependencies, promptCollisionResolver } = params;
52
+ const currentWorkingDirectory = dependencies.resolveWorkingDirectory();
53
+ const homeDirectory = dependencies.resolveHomeDirectory();
54
+ const sessionDirectory = node_path_1.default.join(homeDirectory, '.symlx', 'sessions');
55
+ const collision = resolveCollisionSummary(options, dependencies);
56
+ dependencies.cleanupStaleSessions(sessionDirectory);
57
+ dependencies.ensureDirectories(options.binDir, sessionDirectory);
58
+ dependencies.assertValidBinTargets(options.bin);
59
+ const linkResult = await dependencies.createLinks({
60
+ bins: options.bin,
61
+ binDir: options.binDir,
62
+ policy: collision.effective,
63
+ collisionResolver: collision.effective === 'prompt' ? promptCollisionResolver : undefined,
64
+ });
65
+ assertLinksCreated(linkResult);
66
+ const sessionPath = dependencies.createSessionFilePath(sessionDirectory);
67
+ const sessionRecord = {
68
+ pid: dependencies.resolveProcessId(),
69
+ cwd: currentWorkingDirectory,
70
+ createdAt: dependencies.resolveTimestamp(),
71
+ links: linkResult.created,
72
+ };
73
+ dependencies.persistSession(sessionPath, sessionRecord);
74
+ dependencies.registerLifecycleCleanup(() => {
75
+ dependencies.cleanupSession(sessionPath, sessionRecord.links);
76
+ });
77
+ return {
78
+ collision,
79
+ waitUntilStopped: dependencies.waitUntilStopped,
80
+ };
81
+ }
@@ -36,15 +36,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.cleanupLinks = cleanupLinks;
39
40
  exports.ensureSymlxDirectories = ensureSymlxDirectories;
40
41
  exports.cleanupStaleSessions = cleanupStaleSessions;
42
+ exports.generateSessionFilePath = generateSessionFilePath;
43
+ exports.generateSessionRecord = generateSessionRecord;
41
44
  exports.persistSession = persistSession;
42
- exports.createSessionFilePath = createSessionFilePath;
43
- exports.cleanupSession = cleanupSession;
45
+ exports.registerLifecycleSessionCleanup = registerLifecycleSessionCleanup;
44
46
  const node_fs_1 = __importDefault(require("node:fs"));
45
47
  const node_path_1 = __importDefault(require("node:path"));
46
48
  const log = __importStar(require("../ui/logger"));
47
- const utils_1 = require("../lib/utils");
49
+ const utils_1 = require("./utils");
48
50
  // Checks whether a PID from a previous session is still alive.
49
51
  function isProcessAlive(pid) {
50
52
  // PIDs are always positive integer typically less the 2^15
@@ -63,15 +65,6 @@ function isProcessAlive(pid) {
63
65
  return code !== 'ESRCH';
64
66
  }
65
67
  }
66
- // Session files are best-effort state; deletion failure should not fail the command.
67
- function deleteFile(filePath) {
68
- try {
69
- node_fs_1.default.unlinkSync(filePath);
70
- }
71
- catch {
72
- // Best-effort cleanup.
73
- }
74
- }
75
68
  // Removes only symlinks that still point to the exact targets we created.
76
69
  // This avoids deleting user-managed commands with the same name.
77
70
  function cleanupLinks(links) {
@@ -92,7 +85,6 @@ function cleanupLinks(links) {
92
85
  }
93
86
  }
94
87
  }
95
- // -----------------------------------------------------------
96
88
  // Ensures runtime directories exist before linking/saving sessions.
97
89
  function ensureSymlxDirectories(binDir, sessionDir) {
98
90
  node_fs_1.default.mkdirSync(binDir, { recursive: true });
@@ -110,19 +102,19 @@ function cleanupStaleSessions(sessionDir) {
110
102
  const filePath = node_path_1.default.join(sessionDir, entry);
111
103
  // Delete any files that are not .json, session files can only be JSON
112
104
  if (!entry.endsWith('.json')) {
113
- deleteFile(filePath);
105
+ (0, utils_1.deleteFile)(filePath);
114
106
  continue;
115
107
  }
116
108
  // If the expected file structure has been corrupted, delete the file
117
109
  const record = (0, utils_1.loadJSONFile)(filePath);
118
110
  if (!record) {
119
- deleteFile(filePath);
111
+ (0, utils_1.deleteFile)(filePath);
120
112
  continue;
121
113
  }
122
114
  // If process is dead, unlink the command from the bin and delete the session file
123
115
  if (!isProcessAlive(record.pid)) {
124
116
  cleanupLinks(record.links);
125
- deleteFile(filePath);
117
+ (0, utils_1.deleteFile)(filePath);
126
118
  cleanUpCount++;
127
119
  }
128
120
  }
@@ -130,17 +122,54 @@ function cleanupStaleSessions(sessionDir) {
130
122
  log.info(`cleaned up ${cleanUpCount} expired session${cleanUpCount > 1 ? 's' : ''}`);
131
123
  }
132
124
  }
133
- // Persists currently linked commands so future runs can clean stale state.
134
- function persistSession(sessionPath, record) {
135
- node_fs_1.default.writeFileSync(sessionPath, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
136
- }
137
125
  // Produces unique session file names to avoid collisions across concurrent runs.
138
- function createSessionFilePath(sessionDir) {
126
+ function generateSessionFilePath(sessionDir) {
139
127
  const unique = `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
140
128
  return node_path_1.default.join(sessionDir, `${unique}.json`);
141
129
  }
142
- // Cleanup for the active process/session.
143
- function cleanupSession(sessionPath, links) {
144
- cleanupLinks(links);
145
- deleteFile(sessionPath);
130
+ function generateSessionRecord(cwd, links) {
131
+ return {
132
+ pid: process.pid,
133
+ cwd,
134
+ createdAt: new Date().toISOString(),
135
+ links,
136
+ };
137
+ }
138
+ // Persists currently linked commands so future runs can clean stale state.
139
+ function persistSession(sessionPath, record) {
140
+ node_fs_1.default.writeFileSync(sessionPath, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
141
+ }
142
+ // Registers robust process-exit handling so linked commands are removed reliably.
143
+ // Cleanup is idempotent and can be triggered by normal exit, signals, or fatal errors.
144
+ function registerLifecycleSessionCleanup(sessionPath, links) {
145
+ let cleaned = false;
146
+ const runCleanup = () => {
147
+ if (cleaned) {
148
+ return;
149
+ }
150
+ cleaned = true;
151
+ cleanupLinks(links);
152
+ (0, utils_1.deleteFile)(sessionPath);
153
+ };
154
+ // Normal termination path.
155
+ process.on('exit', runCleanup);
156
+ const onSignal = () => {
157
+ runCleanup();
158
+ process.exit(0);
159
+ };
160
+ // Common interactive stop signals.
161
+ process.on('SIGINT', onSignal);
162
+ process.on('SIGTERM', onSignal);
163
+ process.on('SIGHUP', onSignal);
164
+ // Fatal process events still attempt cleanup before exiting with failure.
165
+ process.on('uncaughtException', (error) => {
166
+ process.stderr.write(`[symlx] uncaught exception: ${String(error)}\n`);
167
+ runCleanup();
168
+ process.exit(1);
169
+ });
170
+ process.on('unhandledRejection', (reason) => {
171
+ process.stderr.write(`[symlx] unhandled rejection: ${String(reason)}\n`);
172
+ runCleanup();
173
+ process.exit(1);
174
+ });
146
175
  }
package/dist/lib/utils.js CHANGED
@@ -5,6 +5,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.loadJSONFile = loadJSONFile;
7
7
  exports.loadConfigFileOptions = loadConfigFileOptions;
8
+ exports.loadPackageJSONOptions = loadPackageJSONOptions;
9
+ exports.deleteFile = deleteFile;
10
+ exports.pathContainsDir = pathContainsDir;
8
11
  const node_fs_1 = __importDefault(require("node:fs"));
9
12
  const node_path_1 = __importDefault(require("node:path"));
10
13
  // Invalid/corrupted JSON files are ignored.
@@ -17,9 +20,109 @@ function loadJSONFile(filePath) {
17
20
  return undefined;
18
21
  }
19
22
  }
20
- function loadConfigFileOptions() {
21
- const cwd = process.cwd();
23
+ function formatReadError(error) {
24
+ if (error instanceof Error && error.message) {
25
+ return error.message;
26
+ }
27
+ return String(error);
28
+ }
29
+ function readJSONFileWithIssue(filePath, label) {
30
+ try {
31
+ const raw = node_fs_1.default.readFileSync(filePath, 'utf8');
32
+ return { data: JSON.parse(raw) };
33
+ }
34
+ catch (error) {
35
+ return {
36
+ issue: `invalid ${label} at ${filePath}: ${formatReadError(error)}`,
37
+ };
38
+ }
39
+ }
40
+ function loadConfigFileOptions(cwd) {
22
41
  const configPath = node_path_1.default.join(cwd, 'symlx.config.json');
23
- const configFileOptions = loadJSONFile(configPath);
24
- return configFileOptions;
42
+ if (!node_fs_1.default.existsSync(configPath)) {
43
+ return {};
44
+ }
45
+ const result = readJSONFileWithIssue(configPath, 'symlx.config.json');
46
+ // throw error if there were file reading issues
47
+ if (result.issue) {
48
+ throw new Error(result.issue);
49
+ }
50
+ return { options: result.data };
51
+ }
52
+ // npm allows `bin` as a string; in that form the command name defaults to package name
53
+ // (without scope for scoped packages).
54
+ function inferBinName(packageName) {
55
+ if (!packageName) {
56
+ return undefined;
57
+ }
58
+ if (packageName.startsWith('@')) {
59
+ const parts = packageName.split('/');
60
+ if (parts.length !== 2 || !parts[1]) {
61
+ return undefined;
62
+ }
63
+ return parts[1];
64
+ }
65
+ return packageName;
66
+ }
67
+ // Loads and validates all bin entries for the current project.
68
+ // Returned map is command name => absolute target file path.
69
+ function loadPackageJSONOptions(cwd) {
70
+ const packageJsonPath = node_path_1.default.join(cwd, 'package.json');
71
+ if (!node_fs_1.default.existsSync(packageJsonPath)) {
72
+ return {
73
+ bin: {},
74
+ issues: [`package.json not found at ${packageJsonPath}`],
75
+ };
76
+ }
77
+ const parsedPackageJSON = readJSONFileWithIssue(packageJsonPath, 'package.json');
78
+ if (parsedPackageJSON.issue) {
79
+ return {
80
+ bin: {},
81
+ issues: [parsedPackageJSON.issue],
82
+ };
83
+ }
84
+ const packageJson = parsedPackageJSON.data;
85
+ if (!packageJson || !packageJson.bin) {
86
+ return {
87
+ bin: {},
88
+ issues: [],
89
+ };
90
+ }
91
+ const bin = {};
92
+ const issues = [];
93
+ if (typeof packageJson.bin === 'string') {
94
+ const inferredBinName = inferBinName(packageJson.name);
95
+ if (inferredBinName) {
96
+ bin[inferredBinName] = packageJson.bin;
97
+ }
98
+ else {
99
+ issues.push('bin field is a string, but could not infer name, set a valid package.json "name"');
100
+ }
101
+ }
102
+ else {
103
+ for (const [name, relTarget] of Object.entries(packageJson.bin)) {
104
+ bin[name] = relTarget;
105
+ }
106
+ }
107
+ return { bin, issues };
108
+ }
109
+ // Session files are best-effort state; deletion failure should not fail the command.
110
+ function deleteFile(filePath) {
111
+ try {
112
+ node_fs_1.default.unlinkSync(filePath);
113
+ }
114
+ catch {
115
+ // Best-effort cleanup.
116
+ }
117
+ }
118
+ // Checks if PATH already contains a directory so we can avoid noisy setup hints.
119
+ function pathContainsDir(currentPath, targetDir) {
120
+ if (!currentPath) {
121
+ return false;
122
+ }
123
+ const resolvedTarget = node_path_1.default.resolve(targetDir);
124
+ const parts = currentPath
125
+ .split(node_path_1.default.delimiter)
126
+ .map((item) => node_path_1.default.resolve(item));
127
+ return parts.includes(resolvedTarget);
25
128
  }
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.validateInlineOptions = validateInlineOptions;
3
+ exports.validatePackageJSONOptions = validatePackageJSONOptions;
4
4
  exports.validateConfigFileOptions = validateConfigFileOptions;
5
+ exports.validateInlineOptions = validateInlineOptions;
5
6
  const schema_1 = require("./schema");
6
7
  function formatIssues(error) {
7
8
  const details = error.issues
@@ -12,13 +13,15 @@ function formatIssues(error) {
12
13
  .join('; ');
13
14
  return details || 'invalid input';
14
15
  }
15
- function validateInlineOptions(schema, input, label = 'input') {
16
- const result = schema.safeParse(input || {});
16
+ function validatePackageJSONOptions(input) {
17
+ const result = schema_1.packageJSONOptionsSchema.safeParse(input || {});
17
18
  if (!result.success) {
18
- throw new Error(`invalid ${label}: ${formatIssues(result.error)}`);
19
+ return { bin: {}, issues: result.error.issues.map((i) => i.message) };
19
20
  }
20
- return result.data;
21
+ return { ...result.data, issues: [] };
21
22
  }
23
+ // it's better ux/dx to throw if there's an error in the config file
24
+ // provided it's available than falling back to defaults and leaving the user guessing
22
25
  function validateConfigFileOptions(input, label = 'input') {
23
26
  const result = schema_1.configFileOptionsSchema.safeParse(input || {});
24
27
  if (!result.success) {
@@ -26,3 +29,10 @@ function validateConfigFileOptions(input, label = 'input') {
26
29
  }
27
30
  return result.data;
28
31
  }
32
+ function validateInlineOptions(schema, input, label = 'input') {
33
+ const result = schema.safeParse(input || {});
34
+ if (!result.success) {
35
+ throw new Error(`invalid ${label}: ${formatIssues(result.error)}`);
36
+ }
37
+ return result.data;
38
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.binOption = exports.nonInteractiveOption = exports.binResolutionStrategyOption = exports.collisionOption = exports.binDirOption = void 0;
4
+ function collectBinEntry(value, previous = []) {
5
+ previous.push(value);
6
+ return previous;
7
+ }
8
+ const binDirOption = [
9
+ '--bin-dir <dir>',
10
+ 'target bin directory (default: ~/.symlx/bin)',
11
+ ];
12
+ exports.binDirOption = binDirOption;
13
+ const collisionOption = [
14
+ '--collision <policy>',
15
+ 'collision mode: prompt|skip|fail|overwrite',
16
+ 'prompt',
17
+ ];
18
+ exports.collisionOption = collisionOption;
19
+ const binResolutionStrategyOption = [
20
+ '--bin-resolution-strategy <strategy>',
21
+ 'bin precedence strategy: replace|merge',
22
+ 'replace',
23
+ ];
24
+ exports.binResolutionStrategyOption = binResolutionStrategyOption;
25
+ const nonInteractiveOption = [
26
+ '--non-interactive',
27
+ 'disable interactive prompts',
28
+ false,
29
+ ];
30
+ exports.nonInteractiveOption = nonInteractiveOption;
31
+ const binOption = [
32
+ '--bin <name=path>',
33
+ 'custom bin mapping (repeatable), e.g. --bin my-cli=dist/cli.js',
34
+ collectBinEntry,
35
+ [],
36
+ ];
37
+ exports.binOption = binOption;
@@ -0,0 +1,142 @@
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
+ const node_fs_1 = __importDefault(require("node:fs"));
7
+ const node_os_1 = __importDefault(require("node:os"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const PREFIX = '[symlx]';
10
+ const START = '# >>> symlx path >>>';
11
+ const END = '# <<< symlx path <<<';
12
+ const BIN_PATH = '$HOME/.symlx/bin';
13
+ const PROFILE_BASENAMES = ['.zprofile', '.zshrc', '.bashrc'];
14
+ function info(message) {
15
+ process.stdout.write(`${PREFIX} ${message}\n`);
16
+ }
17
+ function warn(message) {
18
+ process.stderr.write(`${PREFIX} ${message}\n`);
19
+ }
20
+ function printManualPathSetupGuidance() {
21
+ if (process.platform === 'win32') {
22
+ info('manual setup (PowerShell):');
23
+ info('[Environment]::SetEnvironmentVariable("Path", "$env:USERPROFILE\\\\.symlx\\\\bin;$env:Path", "User")');
24
+ info('then open a new terminal');
25
+ return;
26
+ }
27
+ info('manual setup: add this to ~/.zshrc, ~/.zprofile, or ~/.bashrc');
28
+ info(START);
29
+ info('if [[ ":$PATH:" != *":$HOME/.symlx/bin:"* ]]; then');
30
+ info(' export PATH="$HOME/.symlx/bin:$PATH"');
31
+ info('fi');
32
+ info(END);
33
+ info('then run: source ~/.zshrc (or your active shell profile)');
34
+ }
35
+ function resolveProfilePaths(homeDir) {
36
+ return PROFILE_BASENAMES.map((basename) => node_path_1.default.join(homeDir, basename));
37
+ }
38
+ function toHomeRelativePath(filePath, homeDir) {
39
+ if (filePath.startsWith(`${homeDir}${node_path_1.default.sep}`)) {
40
+ return `~/${node_path_1.default.relative(homeDir, filePath)}`;
41
+ }
42
+ return filePath;
43
+ }
44
+ function getPreferredSourcePath(updatedPaths) {
45
+ const shell = process.env.SHELL ?? '';
46
+ const preferredBasename = shell.includes('zsh')
47
+ ? '.zshrc'
48
+ : shell.includes('bash')
49
+ ? '.bashrc'
50
+ : undefined;
51
+ if (!preferredBasename) {
52
+ return updatedPaths[0];
53
+ }
54
+ return (updatedPaths.find((filePath) => node_path_1.default.basename(filePath) === preferredBasename) ??
55
+ updatedPaths[0]);
56
+ }
57
+ function escapeRegExp(value) {
58
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
59
+ }
60
+ function buildPathBlock() {
61
+ return [
62
+ START,
63
+ `if [[ ":$PATH:" != *":${BIN_PATH}:"* ]]; then`,
64
+ ` export PATH="${BIN_PATH}:$PATH"`,
65
+ 'fi',
66
+ END,
67
+ ].join('\n');
68
+ }
69
+ function ensureTrailingNewline(value) {
70
+ return value.endsWith('\n') ? value : `${value}\n`;
71
+ }
72
+ function upsertProfileBlock(filePath, block) {
73
+ const exists = node_fs_1.default.existsSync(filePath);
74
+ const current = exists ? node_fs_1.default.readFileSync(filePath, 'utf8') : '';
75
+ const normalizedCurrent = current.replace(/\r\n/g, '\n');
76
+ const markerPattern = new RegExp(`${escapeRegExp(START)}[\\s\\S]*?${escapeRegExp(END)}\\n?`, 'm');
77
+ let next;
78
+ if (markerPattern.test(normalizedCurrent)) {
79
+ next = normalizedCurrent.replace(markerPattern, `${block}\n`);
80
+ }
81
+ else if (normalizedCurrent.trim().length === 0) {
82
+ next = `${block}\n`;
83
+ }
84
+ else {
85
+ next = `${ensureTrailingNewline(normalizedCurrent)}\n${block}\n`;
86
+ }
87
+ if (next === normalizedCurrent) {
88
+ return false;
89
+ }
90
+ node_fs_1.default.writeFileSync(filePath, next, 'utf8');
91
+ return true;
92
+ }
93
+ function run() {
94
+ if (process.env.SYMLX_SKIP_PATH_SETUP === '1') {
95
+ info('skipping PATH setup because SYMLX_SKIP_PATH_SETUP=1');
96
+ printManualPathSetupGuidance();
97
+ return;
98
+ }
99
+ if (process.platform === 'win32') {
100
+ info('skipping shell profile PATH setup on Windows');
101
+ printManualPathSetupGuidance();
102
+ return;
103
+ }
104
+ const homeDir = node_os_1.default.homedir();
105
+ if (!homeDir) {
106
+ warn('could not resolve home directory; skipping PATH setup');
107
+ printManualPathSetupGuidance();
108
+ return;
109
+ }
110
+ const block = buildPathBlock();
111
+ const profilePaths = resolveProfilePaths(homeDir);
112
+ const existingPaths = profilePaths.filter((filePath) => node_fs_1.default.existsSync(filePath));
113
+ const targets = existingPaths.length > 0 ? existingPaths : [profilePaths[0]];
114
+ const updated = [];
115
+ for (const target of targets) {
116
+ try {
117
+ const changed = upsertProfileBlock(target, block);
118
+ if (changed) {
119
+ updated.push(target);
120
+ }
121
+ }
122
+ catch (error) {
123
+ warn(`could not update ${target}: ${String(error)}`);
124
+ }
125
+ }
126
+ if (updated.length > 0) {
127
+ info(`added ${BIN_PATH} to PATH in:`);
128
+ for (const target of updated) {
129
+ info(`- ${target}`);
130
+ }
131
+ const preferredSourcePath = getPreferredSourcePath(updated);
132
+ if (preferredSourcePath) {
133
+ const sourceTarget = toHomeRelativePath(preferredSourcePath, homeDir);
134
+ info(`run now: source ${sourceTarget}`);
135
+ }
136
+ info('or open a new shell to apply immediately');
137
+ }
138
+ else {
139
+ info(`PATH setup already present (${BIN_PATH})`);
140
+ }
141
+ }
142
+ run();
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ const PREFIX = '[symlx]';
3
+ function info(message) {
4
+ process.stdout.write(`${PREFIX} ${message}\n`);
5
+ }
6
+ function run() {
7
+ info('notice: install will update your shell profile PATH with $HOME/.symlx/bin');
8
+ info('set SYMLX_SKIP_PATH_SETUP=1 to skip automatic PATH setup');
9
+ }
10
+ run();
@@ -3,11 +3,11 @@ 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.promptCollisionDecision = promptCollisionDecision;
6
+ exports.promptCollisionResolver = promptCollisionResolver;
7
7
  const prompts_1 = __importDefault(require("prompts"));
8
8
  // Interactive collision resolver for --collision prompt.
9
9
  // Returning "abort" bubbles up as an error to stop the current serve run.
10
- async function promptCollisionDecision(conflict) {
10
+ async function promptCollisionResolver(conflict) {
11
11
  const response = await (0, prompts_1.default)({
12
12
  type: 'select',
13
13
  name: 'decision',
@@ -0,0 +1,55 @@
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.printLinkOutcome = printLinkOutcome;
37
+ exports.printPathHintIfNeeded = printPathHintIfNeeded;
38
+ const utils_1 = require("../lib/utils");
39
+ const log = __importStar(require("./logger"));
40
+ function printLinkOutcome(binDir, linkResult) {
41
+ const createdLinks = linkResult.created;
42
+ log.info(`linked ${createdLinks.length} command${createdLinks.length > 1 ? 's' : ''} into ${binDir}`);
43
+ for (const link of createdLinks) {
44
+ log.info(`${link.name} -> ${link.target}`);
45
+ }
46
+ for (const skip of linkResult.skipped) {
47
+ log.warn(`skip "${skip.name}": ${skip.reason} (${skip.linkPath})`);
48
+ }
49
+ }
50
+ function printPathHintIfNeeded(binDir, currentPath) {
51
+ if ((0, utils_1.pathContainsDir)(currentPath, binDir)) {
52
+ return;
53
+ }
54
+ log.info(`add this to your shell config if needed:\nexport PATH="${binDir}:$PATH"`);
55
+ }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "symlx",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Temporary local CLI bin linker",
5
5
  "license": "MIT",
6
6
  "bin": {
7
- "symlx": "dist/clii.js",
8
- "cx": "dist/clii.js"
7
+ "symlx": "dist/cli.js",
8
+ "cx": "dist/cli.js"
9
9
  },
10
10
  "files": [
11
11
  "dist",
@@ -31,6 +31,10 @@
31
31
  },
32
32
  "scripts": {
33
33
  "build": "tsc -p tsconfig.json && chmod +x dist/cli.js",
34
- "check": "tsc -p tsconfig.json --noEmit"
34
+ "watch": "tsc -p tsconfig.json --watch --preserveWatchOutput",
35
+ "check": "tsc -p tsconfig.json --noEmit",
36
+ "test": "pnpm run build && tsc -p tsconfig.test.json && node --test .tmp-tests/test/**/*.test.js",
37
+ "preinstall": "node dist/preinstall.js",
38
+ "postinstall": "node dist/postinstall.js"
35
39
  }
36
40
  }