symlx 0.1.4 → 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 +22 -11
- package/dist/cli.js +16 -9
- package/dist/commands/link.js +66 -0
- package/dist/commands/serve copy.js +169 -0
- package/dist/commands/serve.js +32 -110
- package/dist/commands/serve_stash.js +169 -0
- package/dist/lib/bin-targets.js +23 -5
- package/dist/lib/constants.js +4 -0
- package/dist/lib/link-manager.js +37 -18
- package/dist/lib/link-result.js +22 -0
- package/dist/lib/options.js +55 -7
- package/dist/lib/schema.js +2 -4
- package/dist/lib/serve-runtime.js +81 -0
- package/dist/lib/session-store.js +50 -11
- package/dist/lib/utils.js +2 -1
- package/dist/lib/validator.js +2 -0
- package/dist/options.js +37 -0
- package/dist/ui/prompts.js +2 -2
- package/dist/ui/serve-output.js +55 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@ Temporary command linker for local CLI development.
|
|
|
4
4
|
|
|
5
5
|
`symlx serve` links command names from your project into a runnable bin directory for the lifetime of the process.
|
|
6
6
|
When `symlx` stops, those links are cleaned up.
|
|
7
|
+
`symlx link` creates the same links once and exits immediately.
|
|
7
8
|
|
|
8
9
|
## Why symlx
|
|
9
10
|
|
|
@@ -42,7 +43,7 @@ In a CLI project with:
|
|
|
42
43
|
run:
|
|
43
44
|
|
|
44
45
|
```bash
|
|
45
|
-
symlx
|
|
46
|
+
symlx link
|
|
46
47
|
```
|
|
47
48
|
|
|
48
49
|
Then use your CLI normally:
|
|
@@ -51,7 +52,7 @@ Then use your CLI normally:
|
|
|
51
52
|
my-cli --help
|
|
52
53
|
```
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
Use `symlx serve` when you want temporary session-scoped links with auto-cleanup on exit.
|
|
55
56
|
|
|
56
57
|
## Alias
|
|
57
58
|
|
|
@@ -61,7 +62,9 @@ Equivalent commands:
|
|
|
61
62
|
|
|
62
63
|
```bash
|
|
63
64
|
symlx serve
|
|
65
|
+
symlx link
|
|
64
66
|
cx serve
|
|
67
|
+
cx link
|
|
65
68
|
```
|
|
66
69
|
|
|
67
70
|
## Command Reference
|
|
@@ -88,6 +91,20 @@ symlx serve --bin admin=dist/admin.js --bin worker=dist/worker.js
|
|
|
88
91
|
symlx serve --bin-resolution-strategy merge
|
|
89
92
|
```
|
|
90
93
|
|
|
94
|
+
## `symlx link`
|
|
95
|
+
|
|
96
|
+
Links commands from resolved bin mappings and exits immediately.
|
|
97
|
+
|
|
98
|
+
It uses the exact same options and resolution behavior as `symlx serve`, but it does not keep a live session.
|
|
99
|
+
|
|
100
|
+
Examples:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
symlx link
|
|
104
|
+
symlx link --collision overwrite
|
|
105
|
+
symlx link --bin admin=dist/admin.js
|
|
106
|
+
```
|
|
107
|
+
|
|
91
108
|
## Bin Resolution Model
|
|
92
109
|
|
|
93
110
|
`symlx` resolves options from three user sources plus defaults:
|
|
@@ -203,13 +220,13 @@ symlx serve --bin-dir ~/.symlx/bin
|
|
|
203
220
|
|
|
204
221
|
## Runtime Safety Checks
|
|
205
222
|
|
|
206
|
-
Before linking, symlx
|
|
223
|
+
Before linking, symlx prepares each resolved bin target:
|
|
207
224
|
|
|
208
225
|
- file exists
|
|
209
226
|
- target is not a directory
|
|
210
|
-
- target is executable on unix-like systems
|
|
227
|
+
- target is made executable automatically on unix-like systems when possible
|
|
211
228
|
|
|
212
|
-
|
|
229
|
+
Missing targets, directories, and permission-update failures still fail early with actionable messages.
|
|
213
230
|
|
|
214
231
|
## Exit Behavior
|
|
215
232
|
|
|
@@ -227,12 +244,6 @@ Add a bin mapping in at least one place:
|
|
|
227
244
|
- `symlx.config.json -> bin`
|
|
228
245
|
- `--bin name=path`
|
|
229
246
|
|
|
230
|
-
## "target is not executable"
|
|
231
|
-
|
|
232
|
-
```bash
|
|
233
|
-
chmod +x dist/cli.js # or your target executable
|
|
234
|
-
```
|
|
235
|
-
|
|
236
247
|
## "command conflicts at ..."
|
|
237
248
|
|
|
238
249
|
Use a collision mode:
|
package/dist/cli.js
CHANGED
|
@@ -36,11 +36,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
37
|
const commander_1 = require("commander");
|
|
38
38
|
const log = __importStar(require("./ui/logger"));
|
|
39
|
+
const link_1 = require("./commands/link");
|
|
39
40
|
const serve_1 = require("./commands/serve");
|
|
40
|
-
|
|
41
|
-
previous.push(value);
|
|
42
|
-
return previous;
|
|
43
|
-
}
|
|
41
|
+
const options_1 = require("./options");
|
|
44
42
|
async function main() {
|
|
45
43
|
// Commander orchestrates top-level commands/options and help output.
|
|
46
44
|
const program = new commander_1.Command();
|
|
@@ -51,12 +49,21 @@ async function main() {
|
|
|
51
49
|
program
|
|
52
50
|
.command('serve')
|
|
53
51
|
.description("Link this project's bin commands until symlx exits")
|
|
54
|
-
.option(
|
|
55
|
-
.option(
|
|
56
|
-
.option(
|
|
57
|
-
.option(
|
|
58
|
-
.option(
|
|
52
|
+
.option(...options_1.binDirOption)
|
|
53
|
+
.option(...options_1.collisionOption)
|
|
54
|
+
.option(...options_1.binResolutionStrategyOption)
|
|
55
|
+
.option(...options_1.nonInteractiveOption)
|
|
56
|
+
.option(...options_1.binOption)
|
|
59
57
|
.action(serve_1.serveCommand);
|
|
58
|
+
program
|
|
59
|
+
.command('link')
|
|
60
|
+
.description("Link this project's bin commands once and exit")
|
|
61
|
+
.option(...options_1.binDirOption)
|
|
62
|
+
.option(...options_1.collisionOption)
|
|
63
|
+
.option(...options_1.binResolutionStrategyOption)
|
|
64
|
+
.option(...options_1.nonInteractiveOption)
|
|
65
|
+
.option(...options_1.binOption)
|
|
66
|
+
.action(link_1.linkCommand);
|
|
60
67
|
await program.parseAsync(process.argv);
|
|
61
68
|
}
|
|
62
69
|
// Centralized fatal error boundary for command execution.
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.linkCommand = linkCommand;
|
|
40
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
41
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
42
|
+
const log = __importStar(require("../ui/logger"));
|
|
43
|
+
const serve_output_1 = require("../ui/serve-output");
|
|
44
|
+
const options_1 = require("../lib/options");
|
|
45
|
+
const schema_1 = require("../lib/schema");
|
|
46
|
+
const bin_targets_1 = require("../lib/bin-targets");
|
|
47
|
+
const link_manager_1 = require("../lib/link-manager");
|
|
48
|
+
const session_store_1 = require("../lib/session-store");
|
|
49
|
+
const constants_1 = require("../lib/constants");
|
|
50
|
+
async function linkCommand(inlineOptions) {
|
|
51
|
+
const cwd = process.cwd();
|
|
52
|
+
const homeDirectory = node_os_1.default.homedir();
|
|
53
|
+
const sessionDir = node_path_1.default.join(homeDirectory, '.symlx', 'sessions');
|
|
54
|
+
const options = (0, options_1.resolveOptions)(cwd, schema_1.serveInlineOptionsSchema, inlineOptions);
|
|
55
|
+
const internalCollisionOption = (0, options_1.resolveInternalCollisionOption)(options.collision, options.nonInteractive);
|
|
56
|
+
(0, session_store_1.cleanupStaleSessions)(sessionDir);
|
|
57
|
+
(0, session_store_1.ensureSymlxDirectories)(options.binDir, sessionDir);
|
|
58
|
+
(0, bin_targets_1.prepareBinTargets)(options.bin);
|
|
59
|
+
const linkResult = await (0, link_manager_1.createLinks)(options.bin, options.binDir, internalCollisionOption);
|
|
60
|
+
(0, link_manager_1.assertLinksCreated)(linkResult);
|
|
61
|
+
if (options.collision === 'prompt' && internalCollisionOption !== 'prompt') {
|
|
62
|
+
log.warn(constants_1.PROMPT_FALLBACK_WARNING);
|
|
63
|
+
}
|
|
64
|
+
(0, serve_output_1.printLinkOutcome)(options.binDir, linkResult);
|
|
65
|
+
(0, serve_output_1.printPathHintIfNeeded)(options.binDir, process.env.PATH);
|
|
66
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.serveCommand = serveCommand;
|
|
40
|
+
const path_1 = __importDefault(require("path"));
|
|
41
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
42
|
+
const log = __importStar(require("../ui/logger"));
|
|
43
|
+
const utils_1 = require("../lib/utils");
|
|
44
|
+
const link_manager_1 = require("../lib/link-manager");
|
|
45
|
+
const bin_targets_1 = require("../lib/bin-targets");
|
|
46
|
+
const lifecycle_1 = require("../lib/lifecycle");
|
|
47
|
+
const session_store_1 = require("../lib/session-store");
|
|
48
|
+
const prompts_1 = require("../ui/prompts");
|
|
49
|
+
const options_1 = require("../lib/options");
|
|
50
|
+
const schema_1 = require("../lib/schema");
|
|
51
|
+
// Prompts require an interactive terminal; scripts/CI should avoid prompt mode.
|
|
52
|
+
function isInteractiveSession() {
|
|
53
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
54
|
+
}
|
|
55
|
+
function prepareRuntimeDirectories(binDirectory, sessionDirectory) {
|
|
56
|
+
(0, session_store_1.ensureSymlxDirectories)(binDirectory, sessionDirectory);
|
|
57
|
+
(0, session_store_1.cleanupStaleSessions)(sessionDirectory);
|
|
58
|
+
}
|
|
59
|
+
function resolveRuntimeCollisionMode(options) {
|
|
60
|
+
if (options.collision !== 'prompt') {
|
|
61
|
+
return { policy: options.collision };
|
|
62
|
+
}
|
|
63
|
+
const canPromptForCollision = !options.nonInteractive && isInteractiveSession();
|
|
64
|
+
if (canPromptForCollision) {
|
|
65
|
+
return {
|
|
66
|
+
policy: 'prompt',
|
|
67
|
+
resolver: prompts_1.promptCollisionDecision,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
log.warn('prompt collision mode requested but session is non-interactive; falling back to skip (use --collision overwrite|fail to avoid skips)');
|
|
71
|
+
return { policy: 'skip' };
|
|
72
|
+
}
|
|
73
|
+
function buildServeRuntime(options) {
|
|
74
|
+
const currentWorkingDirectory = process.cwd();
|
|
75
|
+
const sessionDirectory = path_1.default.join(node_os_1.default.homedir(), '.symlx', 'sessions');
|
|
76
|
+
prepareRuntimeDirectories(options.binDir, sessionDirectory);
|
|
77
|
+
(0, bin_targets_1.assertValidBinTargets)(options.bin);
|
|
78
|
+
return {
|
|
79
|
+
cwd: currentWorkingDirectory,
|
|
80
|
+
options,
|
|
81
|
+
sessionDirectory,
|
|
82
|
+
bins: new Map(Object.entries(options.bin)),
|
|
83
|
+
collisionMode: resolveRuntimeCollisionMode(options),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function createRuntimeLinks(runtime) {
|
|
87
|
+
return (0, link_manager_1.createLinks)({
|
|
88
|
+
bins: runtime.bins,
|
|
89
|
+
binDir: runtime.options.binDir,
|
|
90
|
+
policy: runtime.collisionMode.policy,
|
|
91
|
+
collisionResolver: runtime.collisionMode.resolver,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function formatSkippedLinks(skippedLinks) {
|
|
95
|
+
const maxVisibleSkips = 5;
|
|
96
|
+
const visibleSkips = skippedLinks.slice(0, maxVisibleSkips);
|
|
97
|
+
const visibleSkipLines = visibleSkips
|
|
98
|
+
.map((skip) => `- ${skip.name}: ${skip.reason}`)
|
|
99
|
+
.join('\n');
|
|
100
|
+
const hiddenSkipsCount = skippedLinks.length - maxVisibleSkips;
|
|
101
|
+
const hiddenSkipsLine = hiddenSkipsCount > 0 ? `\n- ...and ${hiddenSkipsCount} more` : '';
|
|
102
|
+
return `${visibleSkipLines}${hiddenSkipsLine}`;
|
|
103
|
+
}
|
|
104
|
+
function assertLinkCreationSucceeded(linkResult) {
|
|
105
|
+
if (linkResult.created.length > 0) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (linkResult.skipped.length === 0) {
|
|
109
|
+
throw new Error('no links were created');
|
|
110
|
+
}
|
|
111
|
+
throw new Error([
|
|
112
|
+
'no links were created because all candidate commands were skipped.',
|
|
113
|
+
formatSkippedLinks(linkResult.skipped),
|
|
114
|
+
'use --collision overwrite or --collision fail for stricter behavior.',
|
|
115
|
+
].join('\n'));
|
|
116
|
+
}
|
|
117
|
+
function persistServeSession(runtime, links) {
|
|
118
|
+
const sessionPath = (0, session_store_1.createSessionFilePath)(runtime.sessionDirectory);
|
|
119
|
+
const record = {
|
|
120
|
+
pid: process.pid,
|
|
121
|
+
cwd: runtime.cwd,
|
|
122
|
+
createdAt: new Date().toISOString(),
|
|
123
|
+
links,
|
|
124
|
+
};
|
|
125
|
+
(0, session_store_1.persistSession)(sessionPath, record);
|
|
126
|
+
return { sessionPath, record };
|
|
127
|
+
}
|
|
128
|
+
function registerServeSessionCleanup(session) {
|
|
129
|
+
(0, lifecycle_1.registerLifecycleCleanup)(() => {
|
|
130
|
+
(0, session_store_1.cleanupSession)(session.sessionPath, session.record.links);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function reportLinkCreation(runtime, linkResult) {
|
|
134
|
+
const createdLinks = linkResult.created;
|
|
135
|
+
log.info(`linked ${createdLinks.length} command${createdLinks.length > 1 ? 's' : ''} into ${runtime.options.binDir}`);
|
|
136
|
+
for (const link of createdLinks) {
|
|
137
|
+
log.info(`${link.name} -> ${link.target}`);
|
|
138
|
+
}
|
|
139
|
+
for (const skippedLink of linkResult.skipped) {
|
|
140
|
+
log.warn(`skip "${skippedLink.name}": ${skippedLink.reason} (${skippedLink.linkPath})`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function reportPathHint(binDirectory) {
|
|
144
|
+
if ((0, utils_1.pathContainsDir)(process.env.PATH, binDirectory)) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
log.info(`add this to your shell config if needed:\nexport PATH="${binDirectory}:$PATH"`);
|
|
148
|
+
}
|
|
149
|
+
function waitUntilProcessExit() {
|
|
150
|
+
return new Promise(() => {
|
|
151
|
+
setInterval(() => undefined, 60_000);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async function run(options) {
|
|
155
|
+
const runtime = buildServeRuntime(options);
|
|
156
|
+
const linkResult = await createRuntimeLinks(runtime);
|
|
157
|
+
assertLinkCreationSucceeded(linkResult);
|
|
158
|
+
const session = persistServeSession(runtime, linkResult.created);
|
|
159
|
+
registerServeSessionCleanup(session);
|
|
160
|
+
reportLinkCreation(runtime, linkResult);
|
|
161
|
+
reportPathHint(runtime.options.binDir);
|
|
162
|
+
log.info('running. press Ctrl+C to cleanup links.');
|
|
163
|
+
await waitUntilProcessExit();
|
|
164
|
+
}
|
|
165
|
+
function serveCommand(inlineOptions) {
|
|
166
|
+
const currentWorkingDirectory = process.cwd();
|
|
167
|
+
const options = (0, options_1.resolveOptions)(currentWorkingDirectory, schema_1.serveInlineOptionsSchema, inlineOptions);
|
|
168
|
+
return run(options);
|
|
169
|
+
}
|
package/dist/commands/serve.js
CHANGED
|
@@ -37,124 +37,46 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.serveCommand = serveCommand;
|
|
40
|
-
const path_1 = __importDefault(require("path"));
|
|
41
40
|
const node_os_1 = __importDefault(require("node:os"));
|
|
41
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
42
42
|
const log = __importStar(require("../ui/logger"));
|
|
43
|
-
const
|
|
44
|
-
const link_manager_1 = require("../lib/link-manager");
|
|
45
|
-
const bin_targets_1 = require("../lib/bin-targets");
|
|
46
|
-
const lifecycle_1 = require("../lib/lifecycle");
|
|
47
|
-
const session_store_1 = require("../lib/session-store");
|
|
48
|
-
const prompts_1 = require("../ui/prompts");
|
|
43
|
+
const serve_output_1 = require("../ui/serve-output");
|
|
49
44
|
const options_1 = require("../lib/options");
|
|
50
45
|
const schema_1 = require("../lib/schema");
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
function
|
|
56
|
-
(0, session_store_1.cleanupStaleSessions)(sessionDir);
|
|
57
|
-
(0, session_store_1.ensureSymlxDirectories)(binDir, sessionDir);
|
|
58
|
-
}
|
|
59
|
-
function resolveCollisionHandling(options) {
|
|
60
|
-
if (options.collision !== 'prompt') {
|
|
61
|
-
return { policy: options.collision };
|
|
62
|
-
}
|
|
63
|
-
const canPrompt = !options.nonInteractive && isInteractiveSession();
|
|
64
|
-
if (!canPrompt) {
|
|
65
|
-
log.warn('prompt collision mode requested but session is non-interactive; falling back to skip (use --collision overwrite|fail to avoid skips)');
|
|
66
|
-
return { policy: 'skip' };
|
|
67
|
-
}
|
|
68
|
-
return {
|
|
69
|
-
policy: 'prompt',
|
|
70
|
-
collisionResolver: prompts_1.promptCollisionDecision,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
async function linkCommands(options, collisionHandling) {
|
|
74
|
-
return (0, link_manager_1.createLinks)({
|
|
75
|
-
bins: new Map(Object.entries(options.bin)),
|
|
76
|
-
binDir: options.binDir,
|
|
77
|
-
policy: collisionHandling.policy,
|
|
78
|
-
collisionResolver: collisionHandling.collisionResolver,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
function ensureLinksWereCreated(linkResult) {
|
|
82
|
-
if (linkResult.created.length === 0) {
|
|
83
|
-
if (linkResult.skipped.length === 0) {
|
|
84
|
-
throw new Error('no links were created');
|
|
85
|
-
}
|
|
86
|
-
const details = linkResult.skipped
|
|
87
|
-
.slice(0, 5)
|
|
88
|
-
.map((skip) => `- ${skip.name}: ${skip.reason}`)
|
|
89
|
-
.join('\n');
|
|
90
|
-
const remainingCount = linkResult.skipped.length - 5;
|
|
91
|
-
const remaining = remainingCount > 0 ? `\n- ...and ${remainingCount} more` : '';
|
|
92
|
-
throw new Error([
|
|
93
|
-
'no links were created because all candidate commands were skipped.',
|
|
94
|
-
details,
|
|
95
|
-
`${remaining}\nuse --collision overwrite or --collision fail for stricter behavior.`,
|
|
96
|
-
].join('\n'));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
function persistActiveSession(params) {
|
|
100
|
-
const { sessionDir, cwd, links } = params;
|
|
101
|
-
const sessionPath = (0, session_store_1.createSessionFilePath)(sessionDir);
|
|
102
|
-
const sessionRecord = {
|
|
103
|
-
pid: process.pid,
|
|
104
|
-
cwd,
|
|
105
|
-
createdAt: new Date().toISOString(),
|
|
106
|
-
links,
|
|
107
|
-
};
|
|
108
|
-
(0, session_store_1.persistSession)(sessionPath, sessionRecord);
|
|
109
|
-
return { sessionPath, sessionRecord };
|
|
110
|
-
}
|
|
111
|
-
function registerSessionCleanup(sessionPath, links) {
|
|
112
|
-
(0, lifecycle_1.registerLifecycleCleanup)(() => {
|
|
113
|
-
(0, session_store_1.cleanupSession)(sessionPath, links);
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
function printLinkOutcome(binDir, linkResult) {
|
|
117
|
-
const createdLinks = linkResult.created;
|
|
118
|
-
log.info(`linked ${createdLinks.length} command${createdLinks.length > 1 ? 's' : ''} into ${binDir}`);
|
|
119
|
-
for (const link of createdLinks) {
|
|
120
|
-
log.info(`${link.name} -> ${link.target}`);
|
|
121
|
-
}
|
|
122
|
-
for (const skip of linkResult.skipped) {
|
|
123
|
-
log.warn(`skip "${skip.name}": ${skip.reason} (${skip.linkPath})`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
function printPathHintIfNeeded(binDir) {
|
|
127
|
-
if ((0, utils_1.pathContainsDir)(process.env.PATH, binDir)) {
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
log.info(`add this to your shell config if needed:\nexport PATH="${binDir}:$PATH"`);
|
|
131
|
-
}
|
|
132
|
-
function waitIndefinitely() {
|
|
46
|
+
const bin_targets_1 = require("../lib/bin-targets");
|
|
47
|
+
const link_manager_1 = require("../lib/link-manager");
|
|
48
|
+
const session_store_1 = require("../lib/session-store");
|
|
49
|
+
const constants_1 = require("../lib/constants");
|
|
50
|
+
function waitUntilStopped() {
|
|
133
51
|
return new Promise(() => {
|
|
134
52
|
setInterval(() => undefined, 60_000);
|
|
135
53
|
});
|
|
136
54
|
}
|
|
137
|
-
async function
|
|
138
|
-
const cwd = process.cwd();
|
|
139
|
-
const sessionDir = path_1.default.join(node_os_1.default.homedir(), '.symlx', 'sessions');
|
|
140
|
-
prepareRuntimeDirectories(options.binDir, sessionDir);
|
|
141
|
-
(0, bin_targets_1.assertValidBinTargets)(options.bin);
|
|
142
|
-
const collisionHandling = resolveCollisionHandling(options);
|
|
143
|
-
const linkResult = await linkCommands(options, collisionHandling);
|
|
144
|
-
ensureLinksWereCreated(linkResult);
|
|
145
|
-
const { sessionPath, sessionRecord } = persistActiveSession({
|
|
146
|
-
sessionDir,
|
|
147
|
-
cwd,
|
|
148
|
-
links: linkResult.created,
|
|
149
|
-
});
|
|
150
|
-
registerSessionCleanup(sessionPath, sessionRecord.links);
|
|
151
|
-
printLinkOutcome(options.binDir, linkResult);
|
|
152
|
-
printPathHintIfNeeded(options.binDir);
|
|
153
|
-
log.info('running. press Ctrl+C to cleanup links.');
|
|
154
|
-
await waitIndefinitely();
|
|
155
|
-
}
|
|
156
|
-
function serveCommand(inlineOptions) {
|
|
55
|
+
async function serveCommand(inlineOptions) {
|
|
157
56
|
const cwd = process.cwd();
|
|
57
|
+
const homeDirectory = node_os_1.default.homedir();
|
|
58
|
+
const sessionDir = node_path_1.default.join(homeDirectory, '.symlx', 'sessions');
|
|
59
|
+
// resolve options by merge or otherwise and resolve collision based on interactiveness
|
|
158
60
|
const options = (0, options_1.resolveOptions)(cwd, schema_1.serveInlineOptionsSchema, inlineOptions);
|
|
159
|
-
|
|
61
|
+
const internalCollisionOption = (0, options_1.resolveInternalCollisionOption)(options.collision, options.nonInteractive);
|
|
62
|
+
// prepare
|
|
63
|
+
(0, session_store_1.cleanupStaleSessions)(sessionDir);
|
|
64
|
+
(0, session_store_1.ensureSymlxDirectories)(options.binDir, sessionDir);
|
|
65
|
+
(0, bin_targets_1.prepareBinTargets)(options.bin);
|
|
66
|
+
// link creation
|
|
67
|
+
const linkResult = await (0, link_manager_1.createLinks)(options.bin, options.binDir, internalCollisionOption);
|
|
68
|
+
(0, link_manager_1.assertLinksCreated)(linkResult);
|
|
69
|
+
// session management
|
|
70
|
+
const sessionPath = (0, session_store_1.generateSessionFilePath)(sessionDir);
|
|
71
|
+
const sessionRecord = (0, session_store_1.generateSessionRecord)(cwd, linkResult.created);
|
|
72
|
+
(0, session_store_1.persistSession)(sessionPath, sessionRecord);
|
|
73
|
+
(0, session_store_1.registerLifecycleSessionCleanup)(sessionPath, sessionRecord.links);
|
|
74
|
+
// logs
|
|
75
|
+
if (options.collision === 'prompt' && internalCollisionOption !== 'prompt') {
|
|
76
|
+
log.warn(constants_1.PROMPT_FALLBACK_WARNING);
|
|
77
|
+
}
|
|
78
|
+
(0, serve_output_1.printLinkOutcome)(options.binDir, linkResult);
|
|
79
|
+
(0, serve_output_1.printPathHintIfNeeded)(options.binDir, process.env.PATH);
|
|
80
|
+
log.info('running. press Ctrl+C to cleanup links.');
|
|
81
|
+
await waitUntilStopped();
|
|
160
82
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.serveCommand = serveCommand;
|
|
40
|
+
const path_1 = __importDefault(require("path"));
|
|
41
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
42
|
+
const log = __importStar(require("../ui/logger"));
|
|
43
|
+
const utils_1 = require("../lib/utils");
|
|
44
|
+
const link_manager_1 = require("../lib/link-manager");
|
|
45
|
+
const bin_targets_1 = require("../lib/bin-targets");
|
|
46
|
+
const lifecycle_1 = require("../lib/lifecycle");
|
|
47
|
+
const session_store_1 = require("../lib/session-store");
|
|
48
|
+
const prompts_1 = require("../ui/prompts");
|
|
49
|
+
const options_1 = require("../lib/options");
|
|
50
|
+
const schema_1 = require("../lib/schema");
|
|
51
|
+
// Prompts require an interactive terminal; scripts/CI should avoid prompt mode.
|
|
52
|
+
function isInteractiveSession() {
|
|
53
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
54
|
+
}
|
|
55
|
+
function prepareRuntimeDirectories(binDirectory, sessionDirectory) {
|
|
56
|
+
(0, session_store_1.ensureSymlxDirectories)(binDirectory, sessionDirectory);
|
|
57
|
+
(0, session_store_1.cleanupStaleSessions)(sessionDirectory);
|
|
58
|
+
}
|
|
59
|
+
function resolveRuntimeCollisionMode(options) {
|
|
60
|
+
if (options.collision !== 'prompt') {
|
|
61
|
+
return { policy: options.collision };
|
|
62
|
+
}
|
|
63
|
+
const canPromptForCollision = !options.nonInteractive && isInteractiveSession();
|
|
64
|
+
if (canPromptForCollision) {
|
|
65
|
+
return {
|
|
66
|
+
policy: 'prompt',
|
|
67
|
+
resolver: prompts_1.promptCollisionDecision,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
log.warn('prompt collision mode requested but session is non-interactive; falling back to skip (use --collision overwrite|fail to avoid skips)');
|
|
71
|
+
return { policy: 'skip' };
|
|
72
|
+
}
|
|
73
|
+
function buildServeRuntime(options) {
|
|
74
|
+
const currentWorkingDirectory = process.cwd();
|
|
75
|
+
const sessionDirectory = path_1.default.join(node_os_1.default.homedir(), '.symlx', 'sessions');
|
|
76
|
+
prepareRuntimeDirectories(options.binDir, sessionDirectory);
|
|
77
|
+
(0, bin_targets_1.assertValidBinTargets)(options.bin);
|
|
78
|
+
return {
|
|
79
|
+
cwd: currentWorkingDirectory,
|
|
80
|
+
options,
|
|
81
|
+
sessionDirectory,
|
|
82
|
+
bins: new Map(Object.entries(options.bin)),
|
|
83
|
+
collisionMode: resolveRuntimeCollisionMode(options),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function createRuntimeLinks(runtime) {
|
|
87
|
+
return (0, link_manager_1.createLinks)({
|
|
88
|
+
bins: runtime.bins,
|
|
89
|
+
binDir: runtime.options.binDir,
|
|
90
|
+
policy: runtime.collisionMode.policy,
|
|
91
|
+
collisionResolver: runtime.collisionMode.resolver,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function formatSkippedLinks(skippedLinks) {
|
|
95
|
+
const maxVisibleSkips = 5;
|
|
96
|
+
const visibleSkips = skippedLinks.slice(0, maxVisibleSkips);
|
|
97
|
+
const visibleSkipLines = visibleSkips
|
|
98
|
+
.map((skip) => `- ${skip.name}: ${skip.reason}`)
|
|
99
|
+
.join('\n');
|
|
100
|
+
const hiddenSkipsCount = skippedLinks.length - maxVisibleSkips;
|
|
101
|
+
const hiddenSkipsLine = hiddenSkipsCount > 0 ? `\n- ...and ${hiddenSkipsCount} more` : '';
|
|
102
|
+
return `${visibleSkipLines}${hiddenSkipsLine}`;
|
|
103
|
+
}
|
|
104
|
+
function assertLinkCreationSucceeded(linkResult) {
|
|
105
|
+
if (linkResult.created.length > 0) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (linkResult.skipped.length === 0) {
|
|
109
|
+
throw new Error('no links were created');
|
|
110
|
+
}
|
|
111
|
+
throw new Error([
|
|
112
|
+
'no links were created because all candidate commands were skipped.',
|
|
113
|
+
formatSkippedLinks(linkResult.skipped),
|
|
114
|
+
'use --collision overwrite or --collision fail for stricter behavior.',
|
|
115
|
+
].join('\n'));
|
|
116
|
+
}
|
|
117
|
+
function persistServeSession(runtime, links) {
|
|
118
|
+
const sessionPath = (0, session_store_1.createSessionFilePath)(runtime.sessionDirectory);
|
|
119
|
+
const record = {
|
|
120
|
+
pid: process.pid,
|
|
121
|
+
cwd: runtime.cwd,
|
|
122
|
+
createdAt: new Date().toISOString(),
|
|
123
|
+
links,
|
|
124
|
+
};
|
|
125
|
+
(0, session_store_1.persistSession)(sessionPath, record);
|
|
126
|
+
return { sessionPath, record };
|
|
127
|
+
}
|
|
128
|
+
function registerServeSessionCleanup(session) {
|
|
129
|
+
(0, lifecycle_1.registerLifecycleCleanup)(() => {
|
|
130
|
+
(0, session_store_1.cleanupSession)(session.sessionPath, session.record.links);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function reportLinkCreation(runtime, linkResult) {
|
|
134
|
+
const createdLinks = linkResult.created;
|
|
135
|
+
log.info(`linked ${createdLinks.length} command${createdLinks.length > 1 ? 's' : ''} into ${runtime.options.binDir}`);
|
|
136
|
+
for (const link of createdLinks) {
|
|
137
|
+
log.info(`${link.name} -> ${link.target}`);
|
|
138
|
+
}
|
|
139
|
+
for (const skippedLink of linkResult.skipped) {
|
|
140
|
+
log.warn(`skip "${skippedLink.name}": ${skippedLink.reason} (${skippedLink.linkPath})`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function reportPathHint(binDirectory) {
|
|
144
|
+
if ((0, utils_1.pathContainsDir)(process.env.PATH, binDirectory)) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
log.info(`add this to your shell config if needed:\nexport PATH="${binDirectory}:$PATH"`);
|
|
148
|
+
}
|
|
149
|
+
function waitUntilProcessExit() {
|
|
150
|
+
return new Promise(() => {
|
|
151
|
+
setInterval(() => undefined, 60_000);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async function run(options) {
|
|
155
|
+
const runtime = buildServeRuntime(options);
|
|
156
|
+
const linkResult = await createRuntimeLinks(runtime);
|
|
157
|
+
assertLinkCreationSucceeded(linkResult);
|
|
158
|
+
const session = persistServeSession(runtime, linkResult.created);
|
|
159
|
+
registerServeSessionCleanup(session);
|
|
160
|
+
reportLinkCreation(runtime, linkResult);
|
|
161
|
+
reportPathHint(runtime.options.binDir);
|
|
162
|
+
log.info('running. press Ctrl+C to cleanup links.');
|
|
163
|
+
await waitUntilProcessExit();
|
|
164
|
+
}
|
|
165
|
+
function serveCommand(inlineOptions) {
|
|
166
|
+
const currentWorkingDirectory = process.cwd();
|
|
167
|
+
const options = (0, options_1.resolveOptions)(currentWorkingDirectory, schema_1.serveInlineOptionsSchema, inlineOptions);
|
|
168
|
+
return run(options);
|
|
169
|
+
}
|
package/dist/lib/bin-targets.js
CHANGED
|
@@ -3,7 +3,7 @@ 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.prepareBinTargets = prepareBinTargets;
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
function isExecutable(filePath) {
|
|
9
9
|
if (process.platform === 'win32') {
|
|
@@ -17,6 +17,23 @@ function isExecutable(filePath) {
|
|
|
17
17
|
return false;
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
function ensureExecutable(filePath, currentMode) {
|
|
21
|
+
if (process.platform === 'win32' || isExecutable(filePath)) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const executeBits = (currentMode & 0o444) >> 2;
|
|
25
|
+
const nextMode = (currentMode | executeBits) & 0o777;
|
|
26
|
+
try {
|
|
27
|
+
node_fs_1.default.chmodSync(filePath, nextMode);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return `target permissions could not be updated (${String(error)})`;
|
|
31
|
+
}
|
|
32
|
+
if (isExecutable(filePath)) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return 'target permissions could not be updated';
|
|
36
|
+
}
|
|
20
37
|
function inspectBinTarget(name, target) {
|
|
21
38
|
if (!node_fs_1.default.existsSync(target)) {
|
|
22
39
|
return {
|
|
@@ -43,11 +60,12 @@ function inspectBinTarget(name, target) {
|
|
|
43
60
|
reason: 'target is a directory',
|
|
44
61
|
};
|
|
45
62
|
}
|
|
46
|
-
|
|
63
|
+
const executableIssue = ensureExecutable(target, stats.mode);
|
|
64
|
+
if (executableIssue) {
|
|
47
65
|
return {
|
|
48
66
|
name,
|
|
49
67
|
target,
|
|
50
|
-
reason:
|
|
68
|
+
reason: executableIssue,
|
|
51
69
|
hint: `run: chmod +x ${target}`,
|
|
52
70
|
};
|
|
53
71
|
}
|
|
@@ -61,7 +79,7 @@ function formatIssues(issues) {
|
|
|
61
79
|
})
|
|
62
80
|
.join('\n');
|
|
63
81
|
}
|
|
64
|
-
function
|
|
82
|
+
function prepareBinTargets(bin) {
|
|
65
83
|
const issues = [];
|
|
66
84
|
for (const [name, target] of Object.entries(bin)) {
|
|
67
85
|
const issue = inspectBinTarget(name, target);
|
|
@@ -75,6 +93,6 @@ function assertValidBinTargets(bin) {
|
|
|
75
93
|
throw new Error([
|
|
76
94
|
'invalid bin targets:',
|
|
77
95
|
formatIssues(issues),
|
|
78
|
-
'fix bin paths
|
|
96
|
+
'fix bin paths or file permissions in package.json, symlx.config.json, or inline --bin and run again.',
|
|
79
97
|
].join('\n'));
|
|
80
98
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PROMPT_FALLBACK_WARNING = void 0;
|
|
4
|
+
exports.PROMPT_FALLBACK_WARNING = 'prompt collision mode requested but session is non-interactive; falling back to skip (use --collision overwrite|fail to avoid skips)';
|
package/dist/lib/link-manager.js
CHANGED
|
@@ -4,8 +4,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.createLinks = createLinks;
|
|
7
|
+
exports.assertLinksCreated = assertLinksCreated;
|
|
7
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const prompts_1 = require("../ui/prompts");
|
|
9
11
|
// lstat wrapper that treats missing files as "not found" but rethrows real IO errors.
|
|
10
12
|
function tryLstat(filePath) {
|
|
11
13
|
try {
|
|
@@ -69,13 +71,15 @@ function toConflict(name, linkPath, target, node) {
|
|
|
69
71
|
}
|
|
70
72
|
// Creates symlinks for all project bins according to the selected collision strategy.
|
|
71
73
|
// This function is pure with regard to policy: caller decides interactive vs non-interactive.
|
|
72
|
-
async function createLinks(
|
|
73
|
-
const { bins, binDir, policy, collisionResolver } = params;
|
|
74
|
+
async function createLinks(bins, binDir, collisionOption) {
|
|
74
75
|
const created = [];
|
|
75
76
|
const skipped = [];
|
|
76
|
-
|
|
77
|
+
// Link every executable in the bin options
|
|
78
|
+
for (const [name, target] of Object.entries(bins)) {
|
|
77
79
|
const linkPath = node_path_1.default.join(binDir, name);
|
|
80
|
+
// check is there's an existing binary on the device
|
|
78
81
|
const existingNode = inspectExistingNode(linkPath);
|
|
82
|
+
// If there's a conflicting binary, handle the conflict
|
|
79
83
|
if (existingNode) {
|
|
80
84
|
const conflict = toConflict(name, linkPath, target, existingNode);
|
|
81
85
|
// Reusing the exact same link is always a no-op.
|
|
@@ -88,25 +92,21 @@ async function createLinks(params) {
|
|
|
88
92
|
});
|
|
89
93
|
continue;
|
|
90
94
|
}
|
|
91
|
-
|
|
92
|
-
if (policy === 'skip') {
|
|
93
|
-
decision = 'skip';
|
|
94
|
-
}
|
|
95
|
-
else if (policy === 'overwrite') {
|
|
96
|
-
decision = 'overwrite';
|
|
97
|
-
}
|
|
98
|
-
else if (policy === 'fail') {
|
|
95
|
+
if (collisionOption === 'fail') {
|
|
99
96
|
throw new Error(`command "${name}" conflicts at ${linkPath}: ${conflict.reason}`);
|
|
100
97
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
98
|
+
let collisionDecision;
|
|
99
|
+
if (collisionOption === 'prompt') {
|
|
100
|
+
collisionDecision = await (0, prompts_1.promptCollisionResolver)(conflict);
|
|
101
|
+
if (collisionDecision === 'abort') {
|
|
102
|
+
throw new Error(`aborted on collision for command "${name}"`);
|
|
103
|
+
}
|
|
105
104
|
}
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
else {
|
|
106
|
+
// After here, resulting decision can only either be 'skip' or 'overwrite'
|
|
107
|
+
collisionDecision = collisionOption;
|
|
108
108
|
}
|
|
109
|
-
if (
|
|
109
|
+
if (collisionDecision === 'skip') {
|
|
110
110
|
skipped.push({ name, linkPath, reason: conflict.reason });
|
|
111
111
|
continue;
|
|
112
112
|
}
|
|
@@ -117,3 +117,22 @@ async function createLinks(params) {
|
|
|
117
117
|
}
|
|
118
118
|
return { created, skipped };
|
|
119
119
|
}
|
|
120
|
+
function assertLinksCreated(linkResult) {
|
|
121
|
+
if (linkResult.created.length > 0) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (linkResult.skipped.length === 0) {
|
|
125
|
+
throw new Error('no links were created');
|
|
126
|
+
}
|
|
127
|
+
const details = linkResult.skipped
|
|
128
|
+
.slice(0, 5)
|
|
129
|
+
.map((skip) => `- ${skip.name}: ${skip.reason}`)
|
|
130
|
+
.join('\n');
|
|
131
|
+
const remainingCount = linkResult.skipped.length - 5;
|
|
132
|
+
const remaining = remainingCount > 0 ? `\n- ...and ${remainingCount} more` : '';
|
|
133
|
+
throw new Error([
|
|
134
|
+
'no links were created because all candidate commands were skipped.',
|
|
135
|
+
details,
|
|
136
|
+
`${remaining}\nuse --collision overwrite or --collision fail for stricter behavior.`,
|
|
137
|
+
].join('\n'));
|
|
138
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertLinksCreated = assertLinksCreated;
|
|
4
|
+
function assertLinksCreated(linkResult) {
|
|
5
|
+
if (linkResult.created.length > 0) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
if (linkResult.skipped.length === 0) {
|
|
9
|
+
throw new Error('no links were created');
|
|
10
|
+
}
|
|
11
|
+
const details = linkResult.skipped
|
|
12
|
+
.slice(0, 5)
|
|
13
|
+
.map((skip) => `- ${skip.name}: ${skip.reason}`)
|
|
14
|
+
.join('\n');
|
|
15
|
+
const remainingCount = linkResult.skipped.length - 5;
|
|
16
|
+
const remaining = remainingCount > 0 ? `\n- ...and ${remainingCount} more` : '';
|
|
17
|
+
throw new Error([
|
|
18
|
+
'no links were created because all candidate commands were skipped.',
|
|
19
|
+
details,
|
|
20
|
+
`${remaining}\nuse --collision overwrite or --collision fail for stricter behavior.`,
|
|
21
|
+
].join('\n'));
|
|
22
|
+
}
|
package/dist/lib/options.js
CHANGED
|
@@ -1,11 +1,46 @@
|
|
|
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
|
};
|
|
5
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
39
|
exports.resolveOptions = resolveOptions;
|
|
40
|
+
exports.resolveInternalCollisionOption = resolveInternalCollisionOption;
|
|
7
41
|
const path_1 = __importDefault(require("path"));
|
|
8
42
|
const node_os_1 = __importDefault(require("node:os"));
|
|
43
|
+
const log = __importStar(require("../ui/logger"));
|
|
9
44
|
const utils_1 = require("./utils");
|
|
10
45
|
const validator_1 = require("./validator");
|
|
11
46
|
const DEFAULT_OPTIONS = {
|
|
@@ -44,6 +79,9 @@ function withCwdPrefixedBin(cwd, bin) {
|
|
|
44
79
|
path_1.default.resolve(cwd, target),
|
|
45
80
|
]));
|
|
46
81
|
}
|
|
82
|
+
function isInteractiveSession() {
|
|
83
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
84
|
+
}
|
|
47
85
|
// Function to aggregate all options from different sources in order or priority
|
|
48
86
|
function resolveOptions(cwd, inlineOptionsSchema, inlineOptions) {
|
|
49
87
|
const packageJSONLoadResult = (0, utils_1.loadPackageJSONOptions)(cwd);
|
|
@@ -52,14 +90,7 @@ function resolveOptions(cwd, inlineOptionsSchema, inlineOptions) {
|
|
|
52
90
|
...packageJSONLoadResult.issues,
|
|
53
91
|
...validatedPackageJSONOptions.issues,
|
|
54
92
|
];
|
|
55
|
-
const fatalPackageIssue = packageJSONIssues.find((issue) => issue.startsWith('invalid package.json'));
|
|
56
|
-
if (fatalPackageIssue) {
|
|
57
|
-
throw new Error(fatalPackageIssue);
|
|
58
|
-
}
|
|
59
93
|
const configFileLoadResult = (0, utils_1.loadConfigFileOptions)(cwd);
|
|
60
|
-
if (configFileLoadResult.issue) {
|
|
61
|
-
throw new Error(configFileLoadResult.issue);
|
|
62
|
-
}
|
|
63
94
|
const validatedConfigFileOptions = (0, validator_1.validateConfigFileOptions)(configFileLoadResult.options);
|
|
64
95
|
const validatedInlineOptions = (0, validator_1.validateInlineOptions)(inlineOptionsSchema, inlineOptions);
|
|
65
96
|
const inlineBin = validatedInlineOptions
|
|
@@ -76,8 +107,16 @@ function resolveOptions(cwd, inlineOptionsSchema, inlineOptions) {
|
|
|
76
107
|
bin: withCwdPrefixedBin(cwd, resolvedBin),
|
|
77
108
|
};
|
|
78
109
|
if (Object.keys(finalOptions.bin).length > 0) {
|
|
110
|
+
if (finalOptions.binResolutionStrategy === 'merge' &&
|
|
111
|
+
packageJSONIssues.length > 0) {
|
|
112
|
+
log.warn([
|
|
113
|
+
'bin resolution strategy is merge, but could not resolve bin from package.json:',
|
|
114
|
+
...packageJSONIssues,
|
|
115
|
+
].join('\n'));
|
|
116
|
+
}
|
|
79
117
|
return finalOptions;
|
|
80
118
|
}
|
|
119
|
+
// only throw package.json error if no bin was resolved
|
|
81
120
|
const primaryIssue = packageJSONIssues[0];
|
|
82
121
|
if (primaryIssue) {
|
|
83
122
|
throw new Error(primaryIssue);
|
|
@@ -90,3 +129,12 @@ function resolveOptions(cwd, inlineOptionsSchema, inlineOptions) {
|
|
|
90
129
|
'3) inline CLI -> symlx serve --bin my-cli=./cli.js',
|
|
91
130
|
].join('\n'));
|
|
92
131
|
}
|
|
132
|
+
function resolveInternalCollisionOption(collisionOptions, nonInteractiveOptions) {
|
|
133
|
+
if (collisionOptions !== 'prompt') {
|
|
134
|
+
return collisionOptions;
|
|
135
|
+
}
|
|
136
|
+
if (nonInteractiveOptions || !isInteractiveSession()) {
|
|
137
|
+
return 'skip';
|
|
138
|
+
}
|
|
139
|
+
return 'prompt';
|
|
140
|
+
}
|
package/dist/lib/schema.js
CHANGED
|
@@ -56,6 +56,7 @@ const binEntriesToRecordSchema = zod_1.z
|
|
|
56
56
|
const [name, target] = entry.split('=', 2);
|
|
57
57
|
return [name, target];
|
|
58
58
|
})));
|
|
59
|
+
const collisionOptionEnum = zod_1.z.enum(['prompt', 'skip', 'fail', 'overwrite']);
|
|
59
60
|
// -------------------------------------------
|
|
60
61
|
// package.json Schema: Just bin for now
|
|
61
62
|
// -------------------------------------------
|
|
@@ -71,10 +72,7 @@ const configFileOptionsSchema = zod_1.z.object({
|
|
|
71
72
|
.string()
|
|
72
73
|
.regex(/(^|[\\/])\.[^\\/]+([\\/]|$)/, 'binDir must include a dotted folder segment')
|
|
73
74
|
.optional(),
|
|
74
|
-
collision:
|
|
75
|
-
.enum(['prompt', 'skip', 'fail', 'overwrite'])
|
|
76
|
-
.optional()
|
|
77
|
-
.catch(() => {
|
|
75
|
+
collision: collisionOptionEnum.optional().catch(() => {
|
|
78
76
|
log.warn('invalid "collision" value in config file; using default.');
|
|
79
77
|
return undefined;
|
|
80
78
|
}),
|
|
@@ -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,11 +36,13 @@ 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"));
|
|
@@ -120,17 +122,54 @@ function cleanupStaleSessions(sessionDir) {
|
|
|
120
122
|
log.info(`cleaned up ${cleanUpCount} expired session${cleanUpCount > 1 ? 's' : ''}`);
|
|
121
123
|
}
|
|
122
124
|
}
|
|
123
|
-
// Persists currently linked commands so future runs can clean stale state.
|
|
124
|
-
function persistSession(sessionPath, record) {
|
|
125
|
-
node_fs_1.default.writeFileSync(sessionPath, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
|
|
126
|
-
}
|
|
127
125
|
// Produces unique session file names to avoid collisions across concurrent runs.
|
|
128
|
-
function
|
|
126
|
+
function generateSessionFilePath(sessionDir) {
|
|
129
127
|
const unique = `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
|
|
130
128
|
return node_path_1.default.join(sessionDir, `${unique}.json`);
|
|
131
129
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
});
|
|
136
175
|
}
|
package/dist/lib/utils.js
CHANGED
|
@@ -43,8 +43,9 @@ function loadConfigFileOptions(cwd) {
|
|
|
43
43
|
return {};
|
|
44
44
|
}
|
|
45
45
|
const result = readJSONFileWithIssue(configPath, 'symlx.config.json');
|
|
46
|
+
// throw error if there were file reading issues
|
|
46
47
|
if (result.issue) {
|
|
47
|
-
|
|
48
|
+
throw new Error(result.issue);
|
|
48
49
|
}
|
|
49
50
|
return { options: result.data };
|
|
50
51
|
}
|
package/dist/lib/validator.js
CHANGED
|
@@ -20,6 +20,8 @@ function validatePackageJSONOptions(input) {
|
|
|
20
20
|
}
|
|
21
21
|
return { ...result.data, issues: [] };
|
|
22
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
|
|
23
25
|
function validateConfigFileOptions(input, label = 'input') {
|
|
24
26
|
const result = schema_1.configFileOptionsSchema.safeParse(input || {});
|
|
25
27
|
if (!result.success) {
|
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;
|
package/dist/ui/prompts.js
CHANGED
|
@@ -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
|
+
}
|