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,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
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
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.prepareBinTargets = prepareBinTargets;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
function isExecutable(filePath) {
|
|
9
|
+
if (process.platform === 'win32') {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
node_fs_1.default.accessSync(filePath, node_fs_1.default.constants.X_OK);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
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
|
+
}
|
|
37
|
+
function inspectBinTarget(name, target) {
|
|
38
|
+
if (!node_fs_1.default.existsSync(target)) {
|
|
39
|
+
return {
|
|
40
|
+
name,
|
|
41
|
+
target,
|
|
42
|
+
reason: 'target file does not exist',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
let stats;
|
|
46
|
+
try {
|
|
47
|
+
stats = node_fs_1.default.statSync(target);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
return {
|
|
51
|
+
name,
|
|
52
|
+
target,
|
|
53
|
+
reason: `target cannot be accessed (${String(error)})`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (stats.isDirectory()) {
|
|
57
|
+
return {
|
|
58
|
+
name,
|
|
59
|
+
target,
|
|
60
|
+
reason: 'target is a directory',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const executableIssue = ensureExecutable(target, stats.mode);
|
|
64
|
+
if (executableIssue) {
|
|
65
|
+
return {
|
|
66
|
+
name,
|
|
67
|
+
target,
|
|
68
|
+
reason: executableIssue,
|
|
69
|
+
hint: `run: chmod +x ${target}`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
function formatIssues(issues) {
|
|
75
|
+
return issues
|
|
76
|
+
.map((issue) => {
|
|
77
|
+
const hint = issue.hint ? ` (${issue.hint})` : '';
|
|
78
|
+
return `- ${issue.name} -> ${issue.target}: ${issue.reason}${hint}`;
|
|
79
|
+
})
|
|
80
|
+
.join('\n');
|
|
81
|
+
}
|
|
82
|
+
function prepareBinTargets(bin) {
|
|
83
|
+
const issues = [];
|
|
84
|
+
for (const [name, target] of Object.entries(bin)) {
|
|
85
|
+
const issue = inspectBinTarget(name, target);
|
|
86
|
+
if (issue) {
|
|
87
|
+
issues.push(issue);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (issues.length === 0) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
throw new Error([
|
|
94
|
+
'invalid bin targets:',
|
|
95
|
+
formatIssues(issues),
|
|
96
|
+
'fix bin paths or file permissions in package.json, symlx.config.json, or inline --bin and run again.',
|
|
97
|
+
].join('\n'));
|
|
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)';
|
|
@@ -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,46 +1,140 @@
|
|
|
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
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const defaultOptions = {
|
|
45
|
+
const validator_1 = require("./validator");
|
|
46
|
+
const DEFAULT_OPTIONS = {
|
|
13
47
|
collision: 'prompt',
|
|
14
48
|
nonInteractive: false,
|
|
15
49
|
binDir: path_1.default.join(node_os_1.default.homedir(), '.symlx', 'bin'),
|
|
16
50
|
bin: {},
|
|
51
|
+
binResolutionStrategy: 'replace',
|
|
17
52
|
};
|
|
53
|
+
function hasBinEntries(bin) {
|
|
54
|
+
return Boolean(bin && Object.keys(bin).length > 0);
|
|
55
|
+
}
|
|
56
|
+
function computeResolvedBin(inlineBin, configFileBin, packageJSONBin, binResolutionStrategy) {
|
|
57
|
+
// Aggregates bin from all sources:
|
|
58
|
+
// inline + config + package.json + default
|
|
59
|
+
if (binResolutionStrategy === 'merge') {
|
|
60
|
+
return {
|
|
61
|
+
...(packageJSONBin ?? {}),
|
|
62
|
+
...(configFileBin ?? {}),
|
|
63
|
+
...(inlineBin ?? {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// Bin source precedence is value-aware:
|
|
67
|
+
// inline (if non-empty) -> config (if non-empty) -> package.json (if non-empty) -> default.
|
|
68
|
+
return hasBinEntries(inlineBin)
|
|
69
|
+
? inlineBin
|
|
70
|
+
: hasBinEntries(configFileBin)
|
|
71
|
+
? configFileBin
|
|
72
|
+
: hasBinEntries(packageJSONBin)
|
|
73
|
+
? packageJSONBin
|
|
74
|
+
: DEFAULT_OPTIONS.bin;
|
|
75
|
+
}
|
|
76
|
+
function withCwdPrefixedBin(cwd, bin) {
|
|
77
|
+
return Object.fromEntries(Object.entries(bin).map(([name, target]) => [
|
|
78
|
+
name,
|
|
79
|
+
path_1.default.resolve(cwd, target),
|
|
80
|
+
]));
|
|
81
|
+
}
|
|
82
|
+
function isInteractiveSession() {
|
|
83
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
84
|
+
}
|
|
18
85
|
// Function to aggregate all options from different sources in order or priority
|
|
19
|
-
function resolveOptions(inlineOptionsSchema, inlineOptions) {
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
...
|
|
86
|
+
function resolveOptions(cwd, inlineOptionsSchema, inlineOptions) {
|
|
87
|
+
const packageJSONLoadResult = (0, utils_1.loadPackageJSONOptions)(cwd);
|
|
88
|
+
const validatedPackageJSONOptions = (0, validator_1.validatePackageJSONOptions)(packageJSONLoadResult);
|
|
89
|
+
const packageJSONIssues = [
|
|
90
|
+
...packageJSONLoadResult.issues,
|
|
91
|
+
...validatedPackageJSONOptions.issues,
|
|
92
|
+
];
|
|
93
|
+
const configFileLoadResult = (0, utils_1.loadConfigFileOptions)(cwd);
|
|
94
|
+
const validatedConfigFileOptions = (0, validator_1.validateConfigFileOptions)(configFileLoadResult.options);
|
|
95
|
+
const validatedInlineOptions = (0, validator_1.validateInlineOptions)(inlineOptionsSchema, inlineOptions);
|
|
96
|
+
const inlineBin = validatedInlineOptions
|
|
97
|
+
.bin;
|
|
98
|
+
const mergedOptions = {
|
|
99
|
+
...DEFAULT_OPTIONS,
|
|
100
|
+
...(validatedPackageJSONOptions ?? {}),
|
|
33
101
|
...(validatedConfigFileOptions ?? {}),
|
|
34
102
|
...(validatedInlineOptions ?? {}),
|
|
35
103
|
};
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
104
|
+
const resolvedBin = computeResolvedBin(inlineBin, validatedConfigFileOptions.bin, validatedPackageJSONOptions.bin, mergedOptions.binResolutionStrategy);
|
|
105
|
+
const finalOptions = {
|
|
106
|
+
...mergedOptions,
|
|
107
|
+
bin: withCwdPrefixedBin(cwd, resolvedBin),
|
|
108
|
+
};
|
|
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
|
+
}
|
|
117
|
+
return finalOptions;
|
|
118
|
+
}
|
|
119
|
+
// only throw package.json error if no bin was resolved
|
|
120
|
+
const primaryIssue = packageJSONIssues[0];
|
|
121
|
+
if (primaryIssue) {
|
|
122
|
+
throw new Error(primaryIssue);
|
|
123
|
+
}
|
|
124
|
+
throw new Error([
|
|
125
|
+
'no bin entries found.',
|
|
126
|
+
'add at least one command in one of these places:',
|
|
127
|
+
'1) package.json -> "bin": { "my-cli": "./cli.js" }',
|
|
128
|
+
'2) symlx.config.json -> "bin": { "my-cli": "./cli.js" }',
|
|
129
|
+
'3) inline CLI -> symlx serve --bin my-cli=./cli.js',
|
|
130
|
+
].join('\n'));
|
|
131
|
+
}
|
|
132
|
+
function resolveInternalCollisionOption(collisionOptions, nonInteractiveOptions) {
|
|
133
|
+
if (collisionOptions !== 'prompt') {
|
|
134
|
+
return collisionOptions;
|
|
135
|
+
}
|
|
136
|
+
if (nonInteractiveOptions || !isInteractiveSession()) {
|
|
137
|
+
return 'skip';
|
|
44
138
|
}
|
|
45
|
-
return
|
|
139
|
+
return 'prompt';
|
|
46
140
|
}
|
package/dist/lib/schema.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.serveInlineOptionsSchema = exports.configFileOptionsSchema = exports.
|
|
36
|
+
exports.serveInlineOptionsSchema = exports.configFileOptionsSchema = exports.packageJSONOptionsSchema = void 0;
|
|
37
37
|
const zod_1 = require("zod");
|
|
38
38
|
const log = __importStar(require("../ui/logger"));
|
|
39
39
|
const binNameSchema = zod_1.z
|
|
@@ -43,17 +43,36 @@ const binTargetSchema = zod_1.z
|
|
|
43
43
|
.string()
|
|
44
44
|
.trim()
|
|
45
45
|
.min(1)
|
|
46
|
-
.regex(
|
|
47
|
-
const
|
|
46
|
+
.regex(/^(?!\/)(?![A-Za-z]:[\\/])(?!(?:\\\\|\/\/)).+/, 'bin target must be a relative path like ./cli.js');
|
|
47
|
+
const binRecordSchema = zod_1.z.record(binNameSchema, binTargetSchema);
|
|
48
|
+
const binEntrySchema = zod_1.z
|
|
49
|
+
.string()
|
|
50
|
+
.regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?=(?!\/)(?![A-Za-z]:[\\/])(?!(?:\\\\|\/\/)).+$/, 'expected <name=relative/path>');
|
|
51
|
+
const binEntriesToRecordSchema = zod_1.z
|
|
52
|
+
.array(binEntrySchema)
|
|
53
|
+
.optional()
|
|
54
|
+
.default([])
|
|
55
|
+
.transform((entries) => Object.fromEntries(entries.map((entry) => {
|
|
56
|
+
const [name, target] = entry.split('=', 2);
|
|
57
|
+
return [name, target];
|
|
58
|
+
})));
|
|
59
|
+
const collisionOptionEnum = zod_1.z.enum(['prompt', 'skip', 'fail', 'overwrite']);
|
|
60
|
+
// -------------------------------------------
|
|
61
|
+
// package.json Schema: Just bin for now
|
|
62
|
+
// -------------------------------------------
|
|
63
|
+
const packageJSONOptionsSchema = zod_1.z.object({
|
|
64
|
+
bin: binRecordSchema.optional(),
|
|
65
|
+
});
|
|
66
|
+
exports.packageJSONOptionsSchema = packageJSONOptionsSchema;
|
|
67
|
+
// -------------------------------------------
|
|
68
|
+
// symlx.config.json options: should allow configuring all options
|
|
69
|
+
// -------------------------------------------
|
|
48
70
|
const configFileOptionsSchema = zod_1.z.object({
|
|
49
71
|
binDir: zod_1.z
|
|
50
72
|
.string()
|
|
51
73
|
.regex(/(^|[\\/])\.[^\\/]+([\\/]|$)/, 'binDir must include a dotted folder segment')
|
|
52
74
|
.optional(),
|
|
53
|
-
collision:
|
|
54
|
-
.enum(['prompt', 'skip', 'fail', 'overwrite'])
|
|
55
|
-
.optional()
|
|
56
|
-
.catch(() => {
|
|
75
|
+
collision: collisionOptionEnum.optional().catch(() => {
|
|
57
76
|
log.warn('invalid "collision" value in config file; using default.');
|
|
58
77
|
return undefined;
|
|
59
78
|
}),
|
|
@@ -64,21 +83,21 @@ const configFileOptionsSchema = zod_1.z.object({
|
|
|
64
83
|
log.warn('invalid "nonInteractive" value in config file; using default.');
|
|
65
84
|
return undefined;
|
|
66
85
|
}),
|
|
67
|
-
bin:
|
|
86
|
+
bin: binRecordSchema.optional(),
|
|
87
|
+
binResolutionStrategy: zod_1.z.enum(['replace', 'merge']).optional(),
|
|
68
88
|
});
|
|
69
89
|
exports.configFileOptionsSchema = configFileOptionsSchema;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
bin: exports.binEntriesToRecordSchema,
|
|
90
|
+
// -------------------------------------------
|
|
91
|
+
// varying command inline options: highest priority in field:value resolution
|
|
92
|
+
// -------------------------------------------
|
|
93
|
+
const serveInlineOptionsSchema = configFileOptionsSchema
|
|
94
|
+
.pick({
|
|
95
|
+
binDir: true,
|
|
96
|
+
collision: true,
|
|
97
|
+
nonInteractive: true,
|
|
98
|
+
binResolutionStrategy: true,
|
|
99
|
+
})
|
|
100
|
+
.extend({
|
|
101
|
+
bin: binEntriesToRecordSchema,
|
|
83
102
|
});
|
|
84
103
|
exports.serveInlineOptionsSchema = serveInlineOptionsSchema;
|