symlx 0.1.2 → 0.1.4

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,92 @@
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 validator_1 = require("./validator");
11
+ const DEFAULT_OPTIONS = {
12
+ collision: 'prompt',
13
+ nonInteractive: false,
14
+ binDir: path_1.default.join(node_os_1.default.homedir(), '.symlx', 'bin'),
15
+ bin: {},
16
+ binResolutionStrategy: 'replace',
17
+ };
18
+ function hasBinEntries(bin) {
19
+ return Boolean(bin && Object.keys(bin).length > 0);
20
+ }
21
+ function computeResolvedBin(inlineBin, configFileBin, packageJSONBin, binResolutionStrategy) {
22
+ // Aggregates bin from all sources:
23
+ // inline + config + package.json + default
24
+ if (binResolutionStrategy === 'merge') {
25
+ return {
26
+ ...(packageJSONBin ?? {}),
27
+ ...(configFileBin ?? {}),
28
+ ...(inlineBin ?? {}),
29
+ };
30
+ }
31
+ // Bin source precedence is value-aware:
32
+ // inline (if non-empty) -> config (if non-empty) -> package.json (if non-empty) -> default.
33
+ return hasBinEntries(inlineBin)
34
+ ? inlineBin
35
+ : hasBinEntries(configFileBin)
36
+ ? configFileBin
37
+ : hasBinEntries(packageJSONBin)
38
+ ? packageJSONBin
39
+ : DEFAULT_OPTIONS.bin;
40
+ }
41
+ function withCwdPrefixedBin(cwd, bin) {
42
+ return Object.fromEntries(Object.entries(bin).map(([name, target]) => [
43
+ name,
44
+ path_1.default.resolve(cwd, target),
45
+ ]));
46
+ }
47
+ // Function to aggregate all options from different sources in order or priority
48
+ function resolveOptions(cwd, inlineOptionsSchema, inlineOptions) {
49
+ const packageJSONLoadResult = (0, utils_1.loadPackageJSONOptions)(cwd);
50
+ const validatedPackageJSONOptions = (0, validator_1.validatePackageJSONOptions)(packageJSONLoadResult);
51
+ const packageJSONIssues = [
52
+ ...packageJSONLoadResult.issues,
53
+ ...validatedPackageJSONOptions.issues,
54
+ ];
55
+ const fatalPackageIssue = packageJSONIssues.find((issue) => issue.startsWith('invalid package.json'));
56
+ if (fatalPackageIssue) {
57
+ throw new Error(fatalPackageIssue);
58
+ }
59
+ const configFileLoadResult = (0, utils_1.loadConfigFileOptions)(cwd);
60
+ if (configFileLoadResult.issue) {
61
+ throw new Error(configFileLoadResult.issue);
62
+ }
63
+ const validatedConfigFileOptions = (0, validator_1.validateConfigFileOptions)(configFileLoadResult.options);
64
+ const validatedInlineOptions = (0, validator_1.validateInlineOptions)(inlineOptionsSchema, inlineOptions);
65
+ const inlineBin = validatedInlineOptions
66
+ .bin;
67
+ const mergedOptions = {
68
+ ...DEFAULT_OPTIONS,
69
+ ...(validatedPackageJSONOptions ?? {}),
70
+ ...(validatedConfigFileOptions ?? {}),
71
+ ...(validatedInlineOptions ?? {}),
72
+ };
73
+ const resolvedBin = computeResolvedBin(inlineBin, validatedConfigFileOptions.bin, validatedPackageJSONOptions.bin, mergedOptions.binResolutionStrategy);
74
+ const finalOptions = {
75
+ ...mergedOptions,
76
+ bin: withCwdPrefixedBin(cwd, resolvedBin),
77
+ };
78
+ if (Object.keys(finalOptions.bin).length > 0) {
79
+ return finalOptions;
80
+ }
81
+ const primaryIssue = packageJSONIssues[0];
82
+ if (primaryIssue) {
83
+ throw new Error(primaryIssue);
84
+ }
85
+ throw new Error([
86
+ 'no bin entries found.',
87
+ 'add at least one command in one of these places:',
88
+ '1) package.json -> "bin": { "my-cli": "./cli.js" }',
89
+ '2) symlx.config.json -> "bin": { "my-cli": "./cli.js" }',
90
+ '3) inline CLI -> symlx serve --bin my-cli=./cli.js',
91
+ ].join('\n'));
92
+ }
@@ -0,0 +1,105 @@
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.packageJSONOptionsSchema = 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(/^(?!\/)(?![A-Za-z]:[\\/])(?!(?:\\\\|\/\/)).+/, 'bin target must be a relative path like ./cli.js');
47
+ const binRecordSchema = zod_1.z.record(binNameSchema, binTargetSchema);
48
+ const binEntrySchema = zod_1.z
49
+ .string()
50
+ .regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?=(?!\/)(?![A-Za-z]:[\\/])(?!(?:\\\\|\/\/)).+$/, 'expected <name=relative/path>');
51
+ const binEntriesToRecordSchema = zod_1.z
52
+ .array(binEntrySchema)
53
+ .optional()
54
+ .default([])
55
+ .transform((entries) => Object.fromEntries(entries.map((entry) => {
56
+ const [name, target] = entry.split('=', 2);
57
+ return [name, target];
58
+ })));
59
+ // -------------------------------------------
60
+ // package.json Schema: Just bin for now
61
+ // -------------------------------------------
62
+ const packageJSONOptionsSchema = zod_1.z.object({
63
+ bin: binRecordSchema.optional(),
64
+ });
65
+ exports.packageJSONOptionsSchema = packageJSONOptionsSchema;
66
+ // -------------------------------------------
67
+ // symlx.config.json options: should allow configuring all options
68
+ // -------------------------------------------
69
+ const configFileOptionsSchema = zod_1.z.object({
70
+ binDir: zod_1.z
71
+ .string()
72
+ .regex(/(^|[\\/])\.[^\\/]+([\\/]|$)/, 'binDir must include a dotted folder segment')
73
+ .optional(),
74
+ collision: zod_1.z
75
+ .enum(['prompt', 'skip', 'fail', 'overwrite'])
76
+ .optional()
77
+ .catch(() => {
78
+ log.warn('invalid "collision" value in config file; using default.');
79
+ return undefined;
80
+ }),
81
+ nonInteractive: zod_1.z
82
+ .boolean()
83
+ .optional()
84
+ .catch(() => {
85
+ log.warn('invalid "nonInteractive" value in config file; using default.');
86
+ return undefined;
87
+ }),
88
+ bin: binRecordSchema.optional(),
89
+ binResolutionStrategy: zod_1.z.enum(['replace', 'merge']).optional(),
90
+ });
91
+ exports.configFileOptionsSchema = configFileOptionsSchema;
92
+ // -------------------------------------------
93
+ // varying command inline options: highest priority in field:value resolution
94
+ // -------------------------------------------
95
+ const serveInlineOptionsSchema = configFileOptionsSchema
96
+ .pick({
97
+ binDir: true,
98
+ collision: true,
99
+ nonInteractive: true,
100
+ binResolutionStrategy: true,
101
+ })
102
+ .extend({
103
+ bin: binEntriesToRecordSchema,
104
+ });
105
+ exports.serveInlineOptionsSchema = serveInlineOptionsSchema;
@@ -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("./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
@@ -28,25 +63,6 @@ function isProcessAlive(pid) {
28
63
  return code !== 'ESRCH';
29
64
  }
30
65
  }
31
- // Session files are best-effort state; deletion failure should not fail the command.
32
- function deleteFile(filePath) {
33
- try {
34
- node_fs_1.default.unlinkSync(filePath);
35
- }
36
- catch {
37
- // Best-effort cleanup.
38
- }
39
- }
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
66
  // Removes only symlinks that still point to the exact targets we created.
51
67
  // This avoids deleting user-managed commands with the same name.
52
68
  function cleanupLinks(links) {
@@ -67,7 +83,6 @@ function cleanupLinks(links) {
67
83
  }
68
84
  }
69
85
  }
70
- // -----------------------------------------------------------
71
86
  // Ensures runtime directories exist before linking/saving sessions.
72
87
  function ensureSymlxDirectories(binDir, sessionDir) {
73
88
  node_fs_1.default.mkdirSync(binDir, { recursive: true });
@@ -79,25 +94,31 @@ function cleanupStaleSessions(sessionDir) {
79
94
  if (!node_fs_1.default.existsSync(sessionDir)) {
80
95
  return;
81
96
  }
97
+ let cleanUpCount = 0;
82
98
  // Loop through the files within the session
83
99
  for (const entry of node_fs_1.default.readdirSync(sessionDir)) {
84
100
  const filePath = node_path_1.default.join(sessionDir, entry);
85
101
  // Delete any files that are not .json, session files can only be JSON
86
102
  if (!entry.endsWith('.json')) {
87
- deleteFile(filePath);
103
+ (0, utils_1.deleteFile)(filePath);
88
104
  continue;
89
105
  }
90
106
  // If the expected file structure has been corrupted, delete the file
91
- const record = loadSessionFileJSON(filePath);
107
+ const record = (0, utils_1.loadJSONFile)(filePath);
92
108
  if (!record) {
93
- deleteFile(filePath);
109
+ (0, utils_1.deleteFile)(filePath);
94
110
  continue;
95
111
  }
112
+ // If process is dead, unlink the command from the bin and delete the session file
96
113
  if (!isProcessAlive(record.pid)) {
97
114
  cleanupLinks(record.links);
98
- deleteFile(filePath);
115
+ (0, utils_1.deleteFile)(filePath);
116
+ cleanUpCount++;
99
117
  }
100
118
  }
119
+ if (cleanUpCount > 0) {
120
+ log.info(`cleaned up ${cleanUpCount} expired session${cleanUpCount > 1 ? 's' : ''}`);
121
+ }
101
122
  }
102
123
  // Persists currently linked commands so future runs can clean stale state.
103
124
  function persistSession(sessionPath, record) {
@@ -111,5 +132,5 @@ function createSessionFilePath(sessionDir) {
111
132
  // Cleanup for the active process/session.
112
133
  function cleanupSession(sessionPath, links) {
113
134
  cleanupLinks(links);
114
- deleteFile(sessionPath);
135
+ (0, utils_1.deleteFile)(sessionPath);
115
136
  }
@@ -0,0 +1,127 @@
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
+ exports.loadPackageJSONOptions = loadPackageJSONOptions;
9
+ exports.deleteFile = deleteFile;
10
+ exports.pathContainsDir = pathContainsDir;
11
+ const node_fs_1 = __importDefault(require("node:fs"));
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ // Invalid/corrupted JSON files are ignored.
14
+ function loadJSONFile(filePath) {
15
+ try {
16
+ const raw = node_fs_1.default.readFileSync(filePath, 'utf8');
17
+ return JSON.parse(raw);
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ }
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) {
41
+ const configPath = node_path_1.default.join(cwd, 'symlx.config.json');
42
+ if (!node_fs_1.default.existsSync(configPath)) {
43
+ return {};
44
+ }
45
+ const result = readJSONFileWithIssue(configPath, 'symlx.config.json');
46
+ if (result.issue) {
47
+ return { issue: result.issue };
48
+ }
49
+ return { options: result.data };
50
+ }
51
+ // npm allows `bin` as a string; in that form the command name defaults to package name
52
+ // (without scope for scoped packages).
53
+ function inferBinName(packageName) {
54
+ if (!packageName) {
55
+ return undefined;
56
+ }
57
+ if (packageName.startsWith('@')) {
58
+ const parts = packageName.split('/');
59
+ if (parts.length !== 2 || !parts[1]) {
60
+ return undefined;
61
+ }
62
+ return parts[1];
63
+ }
64
+ return packageName;
65
+ }
66
+ // Loads and validates all bin entries for the current project.
67
+ // Returned map is command name => absolute target file path.
68
+ function loadPackageJSONOptions(cwd) {
69
+ const packageJsonPath = node_path_1.default.join(cwd, 'package.json');
70
+ if (!node_fs_1.default.existsSync(packageJsonPath)) {
71
+ return {
72
+ bin: {},
73
+ issues: [`package.json not found at ${packageJsonPath}`],
74
+ };
75
+ }
76
+ const parsedPackageJSON = readJSONFileWithIssue(packageJsonPath, 'package.json');
77
+ if (parsedPackageJSON.issue) {
78
+ return {
79
+ bin: {},
80
+ issues: [parsedPackageJSON.issue],
81
+ };
82
+ }
83
+ const packageJson = parsedPackageJSON.data;
84
+ if (!packageJson || !packageJson.bin) {
85
+ return {
86
+ bin: {},
87
+ issues: [],
88
+ };
89
+ }
90
+ const bin = {};
91
+ const issues = [];
92
+ if (typeof packageJson.bin === 'string') {
93
+ const inferredBinName = inferBinName(packageJson.name);
94
+ if (inferredBinName) {
95
+ bin[inferredBinName] = packageJson.bin;
96
+ }
97
+ else {
98
+ issues.push('bin field is a string, but could not infer name, set a valid package.json "name"');
99
+ }
100
+ }
101
+ else {
102
+ for (const [name, relTarget] of Object.entries(packageJson.bin)) {
103
+ bin[name] = relTarget;
104
+ }
105
+ }
106
+ return { bin, issues };
107
+ }
108
+ // Session files are best-effort state; deletion failure should not fail the command.
109
+ function deleteFile(filePath) {
110
+ try {
111
+ node_fs_1.default.unlinkSync(filePath);
112
+ }
113
+ catch {
114
+ // Best-effort cleanup.
115
+ }
116
+ }
117
+ // Checks if PATH already contains a directory so we can avoid noisy setup hints.
118
+ function pathContainsDir(currentPath, targetDir) {
119
+ if (!currentPath) {
120
+ return false;
121
+ }
122
+ const resolvedTarget = node_path_1.default.resolve(targetDir);
123
+ const parts = currentPath
124
+ .split(node_path_1.default.delimiter)
125
+ .map((item) => node_path_1.default.resolve(item));
126
+ return parts.includes(resolvedTarget);
127
+ }
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validatePackageJSONOptions = validatePackageJSONOptions;
4
+ exports.validateConfigFileOptions = validateConfigFileOptions;
5
+ exports.validateInlineOptions = validateInlineOptions;
6
+ const schema_1 = require("./schema");
7
+ function formatIssues(error) {
8
+ const details = error.issues
9
+ .map((issue) => {
10
+ const pathLabel = issue.path.length > 0 ? issue.path.join('.') : 'value';
11
+ return `${pathLabel}: ${issue.message}`;
12
+ })
13
+ .join('; ');
14
+ return details || 'invalid input';
15
+ }
16
+ function validatePackageJSONOptions(input) {
17
+ const result = schema_1.packageJSONOptionsSchema.safeParse(input || {});
18
+ if (!result.success) {
19
+ return { bin: {}, issues: result.error.issues.map((i) => i.message) };
20
+ }
21
+ return { ...result.data, issues: [] };
22
+ }
23
+ function validateConfigFileOptions(input, label = 'input') {
24
+ const result = schema_1.configFileOptionsSchema.safeParse(input || {});
25
+ if (!result.success) {
26
+ throw new Error(`invalid ${label}: ${formatIssues(result.error)}`);
27
+ }
28
+ return result.data;
29
+ }
30
+ function validateInlineOptions(schema, input, label = 'input') {
31
+ const result = schema.safeParse(input || {});
32
+ if (!result.success) {
33
+ throw new Error(`invalid ${label}: ${formatIssues(result.error)}`);
34
+ }
35
+ return result.data;
36
+ }
@@ -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();
@@ -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
  }