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.
- package/README.md +279 -1
- package/dist/cli.js +17 -9
- package/dist/commands/link.js +66 -0
- package/dist/commands/serve copy.js +169 -0
- package/dist/commands/serve.js +37 -67
- package/dist/commands/serve_stash.js +169 -0
- package/dist/lib/bin-targets.js +98 -0
- package/dist/lib/constants.js +4 -0
- package/dist/{services → lib}/link-manager.js +37 -18
- package/dist/lib/link-result.js +22 -0
- package/dist/lib/options.js +120 -26
- package/dist/lib/schema.js +40 -21
- package/dist/lib/serve-runtime.js +81 -0
- package/dist/{services → lib}/session-store.js +54 -25
- package/dist/lib/utils.js +107 -4
- package/dist/lib/{validate.js → validator.js} +15 -5
- package/dist/options.js +37 -0
- package/dist/postinstall.js +142 -0
- package/dist/preinstall.js +10 -0
- package/dist/ui/{collision-prompt.js → prompts.js} +2 -2
- package/dist/ui/serve-output.js +55 -0
- package/package.json +8 -4
- package/dist/core/paths.js +0 -32
- package/dist/core/types.js +0 -2
- package/dist/lib/paths.js +0 -26
- package/dist/lib/validators.js +0 -29
- package/dist/services/package-bins.js +0 -62
- /package/dist/{services → lib}/lifecycle.js +0 -0
|
@@ -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.
|
|
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("
|
|
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
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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.
|
|
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
|
|
16
|
-
const result =
|
|
16
|
+
function validatePackageJSONOptions(input) {
|
|
17
|
+
const result = schema_1.packageJSONOptionsSchema.safeParse(input || {});
|
|
17
18
|
if (!result.success) {
|
|
18
|
-
|
|
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
|
+
}
|
package/dist/options.js
ADDED
|
@@ -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.
|
|
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
|
|
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
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Temporary local CLI bin linker",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
7
|
-
"symlx": "dist/
|
|
8
|
-
"cx": "dist/
|
|
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
|
-
"
|
|
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
|
}
|