symlx 0.1.2 → 0.1.3
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/dist/cli.js +12 -23
- package/dist/commands/serve.js +10 -3
- package/dist/lib/options.js +46 -0
- package/dist/lib/paths.js +26 -0
- package/dist/lib/schema.js +84 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/utils.js +25 -0
- package/dist/lib/validate.js +28 -0
- package/dist/lib/validators.js +29 -0
- package/dist/services/link-manager.js +22 -15
- package/dist/services/package-bins.js +35 -31
- package/dist/services/session-store.js +42 -11
- package/dist/ui/collision-prompt.js +15 -15
- package/package.json +5 -4
package/dist/cli.js
CHANGED
|
@@ -35,38 +35,27 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
})();
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
37
|
const commander_1 = require("commander");
|
|
38
|
-
const serve_1 = require("./commands/serve");
|
|
39
38
|
const log = __importStar(require("./ui/logger"));
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (!ALLOWED_COLLISIONS.has(value)) {
|
|
45
|
-
throw new Error(`invalid collision policy "${value}". expected: prompt|skip|fail|overwrite`);
|
|
46
|
-
}
|
|
47
|
-
return value;
|
|
39
|
+
const serve_1 = require("./commands/serve");
|
|
40
|
+
function collectBinEntry(value, previous = []) {
|
|
41
|
+
previous.push(value);
|
|
42
|
+
return previous;
|
|
48
43
|
}
|
|
49
44
|
async function main() {
|
|
50
45
|
// Commander orchestrates top-level commands/options and help output.
|
|
51
46
|
const program = new commander_1.Command();
|
|
52
47
|
program
|
|
53
|
-
.name(
|
|
54
|
-
.description(
|
|
48
|
+
.name('symlx')
|
|
49
|
+
.description('Temporary CLI bin linker with lifecycle cleanup')
|
|
55
50
|
.showHelpAfterError();
|
|
56
51
|
program
|
|
57
|
-
.command(
|
|
52
|
+
.command('serve')
|
|
58
53
|
.description("Link this project's package.json bins until symlx exits")
|
|
59
|
-
.option(
|
|
60
|
-
.option(
|
|
61
|
-
.option(
|
|
62
|
-
.
|
|
63
|
-
|
|
64
|
-
await (0, serve_1.runServe)({
|
|
65
|
-
binDir: options.binDir,
|
|
66
|
-
collision: parseCollisionPolicy(options.collision),
|
|
67
|
-
nonInteractive: options.nonInteractive
|
|
68
|
-
});
|
|
69
|
-
});
|
|
54
|
+
.option('--bin-dir <dir>', 'target bin directory (default: ~/.symlx/bin)')
|
|
55
|
+
.option('--collision <policy>', 'collision mode: prompt|skip|fail|overwrite', 'prompt')
|
|
56
|
+
.option('--non-interactive', 'disable interactive prompts', false)
|
|
57
|
+
.option('--bin <name=path>', 'custom bin mapping (repeatable), e.g. --bin my-cli=./cli.js', collectBinEntry, [])
|
|
58
|
+
.action(serve_1.serveCommand);
|
|
70
59
|
await program.parseAsync(process.argv);
|
|
71
60
|
}
|
|
72
61
|
// Centralized fatal error boundary for command execution.
|
package/dist/commands/serve.js
CHANGED
|
@@ -33,14 +33,17 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.
|
|
37
|
-
const paths_1 = require("../
|
|
36
|
+
exports.serveCommand = serveCommand;
|
|
37
|
+
const paths_1 = require("../lib/paths");
|
|
38
38
|
const link_manager_1 = require("../services/link-manager");
|
|
39
39
|
const lifecycle_1 = require("../services/lifecycle");
|
|
40
|
+
// @ts-ignore
|
|
40
41
|
const package_bins_1 = require("../services/package-bins");
|
|
41
42
|
const session_store_1 = require("../services/session-store");
|
|
42
43
|
const collision_prompt_1 = require("../ui/collision-prompt");
|
|
43
44
|
const log = __importStar(require("../ui/logger"));
|
|
45
|
+
const options_1 = require("../lib/options");
|
|
46
|
+
const schema_1 = require("../lib/schema");
|
|
44
47
|
// Prompts require an interactive terminal; scripts/CI should avoid prompt mode.
|
|
45
48
|
function isInteractiveSession() {
|
|
46
49
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
@@ -50,7 +53,7 @@ function isInteractiveSession() {
|
|
|
50
53
|
// 2) create links
|
|
51
54
|
// 3) persist session
|
|
52
55
|
// 4) keep process alive and cleanup on exit
|
|
53
|
-
async function
|
|
56
|
+
async function run(options) {
|
|
54
57
|
const cwd = process.cwd();
|
|
55
58
|
const paths = (0, paths_1.getSymlxPaths)(options.binDir);
|
|
56
59
|
// Prepare runtime directories and recover stale sessions from previous abnormal exits.
|
|
@@ -103,3 +106,7 @@ async function runServe(options) {
|
|
|
103
106
|
setInterval(() => undefined, 60_000);
|
|
104
107
|
});
|
|
105
108
|
}
|
|
109
|
+
function serveCommand(inlineOptions) {
|
|
110
|
+
const options = (0, options_1.resolveOptions)(schema_1.serveInlineOptionsSchema, inlineOptions);
|
|
111
|
+
return run(options);
|
|
112
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveOptions = resolveOptions;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
9
|
+
const utils_1 = require("./utils");
|
|
10
|
+
const validate_1 = require("./validate");
|
|
11
|
+
const package_bins_1 = require("../services/package-bins");
|
|
12
|
+
const defaultOptions = {
|
|
13
|
+
collision: 'prompt',
|
|
14
|
+
nonInteractive: false,
|
|
15
|
+
binDir: path_1.default.join(node_os_1.default.homedir(), '.symlx', 'bin'),
|
|
16
|
+
bin: {},
|
|
17
|
+
};
|
|
18
|
+
// Function to aggregate all options from different sources in order or priority
|
|
19
|
+
function resolveOptions(inlineOptionsSchema, inlineOptions) {
|
|
20
|
+
// Load the bin from package.json
|
|
21
|
+
const packageJSONOptions = (0, package_bins_1.loadPackageJSONOptions)(process.cwd());
|
|
22
|
+
// Load and validate the options from the config file,
|
|
23
|
+
// silently overriding invalid non-critical values with defaults or inline based on order of priority
|
|
24
|
+
const configFileOptions = (0, utils_1.loadConfigFileOptions)();
|
|
25
|
+
const validatedConfigFileOptions = (0, validate_1.validateConfigFileOptions)(configFileOptions);
|
|
26
|
+
// Validate the CLI inline options if available
|
|
27
|
+
const validatedInlineOptions = (0, validate_1.validateInlineOptions)(inlineOptionsSchema, inlineOptions);
|
|
28
|
+
// Default options first
|
|
29
|
+
// -> Config file options override defaults
|
|
30
|
+
// -> CLI inline options overrides config file options
|
|
31
|
+
const finalOptiions = {
|
|
32
|
+
...defaultOptions,
|
|
33
|
+
...(validatedConfigFileOptions ?? {}),
|
|
34
|
+
...(validatedInlineOptions ?? {}),
|
|
35
|
+
};
|
|
36
|
+
if (!Object.entries(finalOptiions.bin).length) {
|
|
37
|
+
throw new Error([
|
|
38
|
+
'no bin entries found. add at least one bin in any of these places:',
|
|
39
|
+
'1) package.json -> "bin": { "my-cli": "./cli.js" }',
|
|
40
|
+
'2) symlx.config.json -> "bin": { "my-cli": "./cli.js" }',
|
|
41
|
+
'3) inline CLI -> symlx serve --bin my-cli=./cli.js',
|
|
42
|
+
'4) if package.json "bin" is a string, set a valid package.json "name" (used to infer the bin name).',
|
|
43
|
+
].join('\n'));
|
|
44
|
+
}
|
|
45
|
+
return finalOptiions;
|
|
46
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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.getSymlxPaths = getSymlxPaths;
|
|
7
|
+
exports.pathContainsDir = pathContainsDir;
|
|
8
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
function getSymlxPaths(binDir) {
|
|
11
|
+
// Keep mutable runtime state under the user's home directory.
|
|
12
|
+
// Session files live separately from bins and are used for stale cleanup.
|
|
13
|
+
const sessionDir = node_path_1.default.join(node_os_1.default.homedir(), '.symlx', 'sessions');
|
|
14
|
+
return { binDir, sessionDir };
|
|
15
|
+
}
|
|
16
|
+
// Checks if PATH already contains a directory so we can avoid noisy setup hints.
|
|
17
|
+
function pathContainsDir(currentPath, targetDir) {
|
|
18
|
+
if (!currentPath) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const resolvedTarget = node_path_1.default.resolve(targetDir);
|
|
22
|
+
const parts = currentPath
|
|
23
|
+
.split(node_path_1.default.delimiter)
|
|
24
|
+
.map((item) => node_path_1.default.resolve(item));
|
|
25
|
+
return parts.includes(resolvedTarget);
|
|
26
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.serveInlineOptionsSchema = exports.configFileOptionsSchema = exports.binEntriesToRecordSchema = void 0;
|
|
37
|
+
const zod_1 = require("zod");
|
|
38
|
+
const log = __importStar(require("../ui/logger"));
|
|
39
|
+
const binNameSchema = zod_1.z
|
|
40
|
+
.string()
|
|
41
|
+
.regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, 'invalid bin name');
|
|
42
|
+
const binTargetSchema = zod_1.z
|
|
43
|
+
.string()
|
|
44
|
+
.trim()
|
|
45
|
+
.min(1)
|
|
46
|
+
.regex(/^\.{1,2}\//, 'bin target must be a relative path like ./cli.js');
|
|
47
|
+
const customBinSchema = zod_1.z.record(binNameSchema, binTargetSchema);
|
|
48
|
+
const configFileOptionsSchema = zod_1.z.object({
|
|
49
|
+
binDir: zod_1.z
|
|
50
|
+
.string()
|
|
51
|
+
.regex(/(^|[\\/])\.[^\\/]+([\\/]|$)/, 'binDir must include a dotted folder segment')
|
|
52
|
+
.optional(),
|
|
53
|
+
collision: zod_1.z
|
|
54
|
+
.enum(['prompt', 'skip', 'fail', 'overwrite'])
|
|
55
|
+
.optional()
|
|
56
|
+
.catch(() => {
|
|
57
|
+
log.warn('invalid "collision" value in config file; using default.');
|
|
58
|
+
return undefined;
|
|
59
|
+
}),
|
|
60
|
+
nonInteractive: zod_1.z
|
|
61
|
+
.boolean()
|
|
62
|
+
.optional()
|
|
63
|
+
.catch(() => {
|
|
64
|
+
log.warn('invalid "nonInteractive" value in config file; using default.');
|
|
65
|
+
return undefined;
|
|
66
|
+
}),
|
|
67
|
+
bin: customBinSchema.optional(),
|
|
68
|
+
});
|
|
69
|
+
exports.configFileOptionsSchema = configFileOptionsSchema;
|
|
70
|
+
const binEntrySchema = zod_1.z
|
|
71
|
+
.string()
|
|
72
|
+
.regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?=\.{1,2}\/.+$/, 'expected <name=./relative/path>');
|
|
73
|
+
exports.binEntriesToRecordSchema = zod_1.z
|
|
74
|
+
.array(binEntrySchema)
|
|
75
|
+
.optional()
|
|
76
|
+
.default([])
|
|
77
|
+
.transform((entries) => Object.fromEntries(entries.map((entry) => {
|
|
78
|
+
const [name, target] = entry.split('=', 2);
|
|
79
|
+
return [name, target];
|
|
80
|
+
})));
|
|
81
|
+
const serveInlineOptionsSchema = configFileOptionsSchema.extend({
|
|
82
|
+
bin: exports.binEntriesToRecordSchema,
|
|
83
|
+
});
|
|
84
|
+
exports.serveInlineOptionsSchema = serveInlineOptionsSchema;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadJSONFile = loadJSONFile;
|
|
7
|
+
exports.loadConfigFileOptions = loadConfigFileOptions;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
// Invalid/corrupted JSON files are ignored.
|
|
11
|
+
function loadJSONFile(filePath) {
|
|
12
|
+
try {
|
|
13
|
+
const raw = node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
14
|
+
return JSON.parse(raw);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function loadConfigFileOptions() {
|
|
21
|
+
const cwd = process.cwd();
|
|
22
|
+
const configPath = node_path_1.default.join(cwd, 'symlx.config.json');
|
|
23
|
+
const configFileOptions = loadJSONFile(configPath);
|
|
24
|
+
return configFileOptions;
|
|
25
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateInlineOptions = validateInlineOptions;
|
|
4
|
+
exports.validateConfigFileOptions = validateConfigFileOptions;
|
|
5
|
+
const schema_1 = require("./schema");
|
|
6
|
+
function formatIssues(error) {
|
|
7
|
+
const details = error.issues
|
|
8
|
+
.map((issue) => {
|
|
9
|
+
const pathLabel = issue.path.length > 0 ? issue.path.join('.') : 'value';
|
|
10
|
+
return `${pathLabel}: ${issue.message}`;
|
|
11
|
+
})
|
|
12
|
+
.join('; ');
|
|
13
|
+
return details || 'invalid input';
|
|
14
|
+
}
|
|
15
|
+
function validateInlineOptions(schema, input, label = 'input') {
|
|
16
|
+
const result = schema.safeParse(input || {});
|
|
17
|
+
if (!result.success) {
|
|
18
|
+
throw new Error(`invalid ${label}: ${formatIssues(result.error)}`);
|
|
19
|
+
}
|
|
20
|
+
return result.data;
|
|
21
|
+
}
|
|
22
|
+
function validateConfigFileOptions(input, label = 'input') {
|
|
23
|
+
const result = schema_1.configFileOptionsSchema.safeParse(input || {});
|
|
24
|
+
if (!result.success) {
|
|
25
|
+
throw new Error(`invalid ${label}: ${formatIssues(result.error)}`);
|
|
26
|
+
}
|
|
27
|
+
return result.data;
|
|
28
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.serveOptionsSchema = exports.collisionPolicySchema = void 0;
|
|
4
|
+
exports.validate = validate;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const collisionPolicySchema = zod_1.z.enum(['prompt', 'skip', 'fail', 'overwrite']);
|
|
7
|
+
exports.collisionPolicySchema = collisionPolicySchema;
|
|
8
|
+
const serveOptionsSchema = zod_1.z.object({
|
|
9
|
+
binDir: zod_1.z.string().trim().min(1).optional(),
|
|
10
|
+
collision: collisionPolicySchema,
|
|
11
|
+
nonInteractive: zod_1.z.boolean(),
|
|
12
|
+
});
|
|
13
|
+
exports.serveOptionsSchema = serveOptionsSchema;
|
|
14
|
+
function formatIssues(error) {
|
|
15
|
+
const details = error.issues
|
|
16
|
+
.map((issue) => {
|
|
17
|
+
const pathLabel = issue.path.length > 0 ? issue.path.join('.') : 'value';
|
|
18
|
+
return `${pathLabel}: ${issue.message}`;
|
|
19
|
+
})
|
|
20
|
+
.join('; ');
|
|
21
|
+
return details || 'invalid input';
|
|
22
|
+
}
|
|
23
|
+
function validate(schema, input, label = 'input') {
|
|
24
|
+
const result = schema.safeParse(input);
|
|
25
|
+
if (!result.success) {
|
|
26
|
+
throw new Error(`invalid ${label}: ${formatIssues(result.error)}`);
|
|
27
|
+
}
|
|
28
|
+
return result.data;
|
|
29
|
+
}
|
|
@@ -13,7 +13,7 @@ function tryLstat(filePath) {
|
|
|
13
13
|
}
|
|
14
14
|
catch (error) {
|
|
15
15
|
const code = error.code;
|
|
16
|
-
if (code ===
|
|
16
|
+
if (code === 'ENOENT') {
|
|
17
17
|
return undefined;
|
|
18
18
|
}
|
|
19
19
|
throw error;
|
|
@@ -54,17 +54,17 @@ function toConflict(name, linkPath, target, node) {
|
|
|
54
54
|
target,
|
|
55
55
|
reason: node.existingTarget
|
|
56
56
|
? `already linked to ${node.existingTarget}`
|
|
57
|
-
:
|
|
57
|
+
: 'already exists as symlink',
|
|
58
58
|
existingTarget: node.existingTarget,
|
|
59
|
-
isSymlink: true
|
|
59
|
+
isSymlink: true,
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
return {
|
|
63
63
|
name,
|
|
64
64
|
linkPath,
|
|
65
65
|
target,
|
|
66
|
-
reason:
|
|
67
|
-
isSymlink: false
|
|
66
|
+
reason: 'already exists as a file',
|
|
67
|
+
isSymlink: false,
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
// Creates symlinks for all project bins according to the selected collision strategy.
|
|
@@ -79,27 +79,34 @@ async function createLinks(params) {
|
|
|
79
79
|
if (existingNode) {
|
|
80
80
|
const conflict = toConflict(name, linkPath, target, existingNode);
|
|
81
81
|
// Reusing the exact same link is always a no-op.
|
|
82
|
-
if (conflict.existingTarget &&
|
|
83
|
-
|
|
82
|
+
if (conflict.existingTarget &&
|
|
83
|
+
node_path_1.default.resolve(conflict.existingTarget) === node_path_1.default.resolve(target)) {
|
|
84
|
+
skipped.push({
|
|
85
|
+
name,
|
|
86
|
+
linkPath,
|
|
87
|
+
reason: 'already linked to requested target',
|
|
88
|
+
});
|
|
84
89
|
continue;
|
|
85
90
|
}
|
|
86
91
|
let decision;
|
|
87
|
-
if (policy ===
|
|
88
|
-
decision =
|
|
92
|
+
if (policy === 'skip') {
|
|
93
|
+
decision = 'skip';
|
|
89
94
|
}
|
|
90
|
-
else if (policy ===
|
|
91
|
-
decision =
|
|
95
|
+
else if (policy === 'overwrite') {
|
|
96
|
+
decision = 'overwrite';
|
|
92
97
|
}
|
|
93
|
-
else if (policy ===
|
|
98
|
+
else if (policy === 'fail') {
|
|
94
99
|
throw new Error(`command "${name}" conflicts at ${linkPath}: ${conflict.reason}`);
|
|
95
100
|
}
|
|
96
101
|
else {
|
|
97
|
-
decision = collisionResolver
|
|
102
|
+
decision = collisionResolver
|
|
103
|
+
? await collisionResolver(conflict)
|
|
104
|
+
: 'skip';
|
|
98
105
|
}
|
|
99
|
-
if (decision ===
|
|
106
|
+
if (decision === 'abort') {
|
|
100
107
|
throw new Error(`aborted on collision for command "${name}"`);
|
|
101
108
|
}
|
|
102
|
-
if (decision ===
|
|
109
|
+
if (decision === 'skip') {
|
|
103
110
|
skipped.push({ name, linkPath, reason: conflict.reason });
|
|
104
111
|
continue;
|
|
105
112
|
}
|
|
@@ -3,56 +3,60 @@ 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.loadPackageJSONOptions = loadPackageJSONOptions;
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const utils_1 = require("../lib/utils");
|
|
9
10
|
// npm allows `bin` as a string; in that form the command name defaults to package name
|
|
10
11
|
// (without scope for scoped packages).
|
|
11
12
|
function inferBinName(packageName) {
|
|
12
13
|
if (!packageName) {
|
|
13
|
-
|
|
14
|
+
return undefined;
|
|
14
15
|
}
|
|
15
|
-
if (packageName.startsWith(
|
|
16
|
-
const parts = packageName.split(
|
|
16
|
+
if (packageName.startsWith('@')) {
|
|
17
|
+
const parts = packageName.split('/');
|
|
17
18
|
if (parts.length !== 2 || !parts[1]) {
|
|
18
|
-
|
|
19
|
+
return undefined;
|
|
19
20
|
}
|
|
20
21
|
return parts[1];
|
|
21
22
|
}
|
|
22
23
|
return packageName;
|
|
23
24
|
}
|
|
24
|
-
function readJsonFile(filePath) {
|
|
25
|
-
const raw = node_fs_1.default.readFileSync(filePath, "utf8");
|
|
26
|
-
return JSON.parse(raw);
|
|
27
|
-
}
|
|
28
25
|
// Loads and validates all bin entries for the current project.
|
|
29
26
|
// Returned map is command name => absolute target file path.
|
|
30
|
-
function
|
|
31
|
-
const packageJsonPath = node_path_1.default.join(cwd,
|
|
27
|
+
function loadPackageJSONOptions(cwd) {
|
|
28
|
+
const packageJsonPath = node_path_1.default.join(cwd, 'package.json');
|
|
32
29
|
if (!node_fs_1.default.existsSync(packageJsonPath)) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
return {
|
|
31
|
+
bin: {},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const packageJson = (0, utils_1.loadJSONFile)(packageJsonPath);
|
|
35
|
+
if (!packageJson || !packageJson.bin) {
|
|
36
|
+
return {
|
|
37
|
+
bin: {},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const bins = {};
|
|
41
|
+
if (typeof packageJson.bin === 'string') {
|
|
42
|
+
const inferredBinName = inferBinName(packageJson.name);
|
|
43
|
+
if (inferredBinName) {
|
|
44
|
+
bins[inferredBinName] = node_path_1.default.resolve(cwd, packageJson.bin);
|
|
45
|
+
}
|
|
42
46
|
}
|
|
43
47
|
else {
|
|
44
48
|
for (const [name, relTarget] of Object.entries(packageJson.bin)) {
|
|
45
|
-
bins
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (bins.size === 0) {
|
|
49
|
-
throw new Error("no bin entries found");
|
|
50
|
-
}
|
|
51
|
-
// Fail fast if package.json points to non-existing executables.
|
|
52
|
-
for (const [name, target] of bins.entries()) {
|
|
53
|
-
if (!node_fs_1.default.existsSync(target)) {
|
|
54
|
-
throw new Error(`bin target for "${name}" does not exist: ${target}`);
|
|
49
|
+
bins[name] = node_path_1.default.resolve(cwd, relTarget);
|
|
55
50
|
}
|
|
56
51
|
}
|
|
57
|
-
|
|
52
|
+
// if (bins.size === 0) {
|
|
53
|
+
// throw new Error('no bin entries found');
|
|
54
|
+
// }
|
|
55
|
+
// // Fail fast if package.json points to non-existing executables.
|
|
56
|
+
// for (const [name, target] of bins.entries()) {
|
|
57
|
+
// if (!fs.existsSync(target)) {
|
|
58
|
+
// throw new Error(`bin target for "${name}" does not exist: ${target}`);
|
|
59
|
+
// }
|
|
60
|
+
// }
|
|
61
|
+
return { bin: {} };
|
|
58
62
|
}
|
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
@@ -10,6 +43,8 @@ exports.createSessionFilePath = createSessionFilePath;
|
|
|
10
43
|
exports.cleanupSession = cleanupSession;
|
|
11
44
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
45
|
const node_path_1 = __importDefault(require("node:path"));
|
|
46
|
+
const log = __importStar(require("../ui/logger"));
|
|
47
|
+
const utils_1 = require("../lib/utils");
|
|
13
48
|
// Checks whether a PID from a previous session is still alive.
|
|
14
49
|
function isProcessAlive(pid) {
|
|
15
50
|
// PIDs are always positive integer typically less the 2^15
|
|
@@ -37,16 +72,6 @@ function deleteFile(filePath) {
|
|
|
37
72
|
// Best-effort cleanup.
|
|
38
73
|
}
|
|
39
74
|
}
|
|
40
|
-
// Invalid/corrupted session files are ignored and later removed.
|
|
41
|
-
function loadSessionFileJSON(filePath) {
|
|
42
|
-
try {
|
|
43
|
-
const raw = node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
44
|
-
return JSON.parse(raw);
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
return undefined;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
75
|
// Removes only symlinks that still point to the exact targets we created.
|
|
51
76
|
// This avoids deleting user-managed commands with the same name.
|
|
52
77
|
function cleanupLinks(links) {
|
|
@@ -79,6 +104,7 @@ function cleanupStaleSessions(sessionDir) {
|
|
|
79
104
|
if (!node_fs_1.default.existsSync(sessionDir)) {
|
|
80
105
|
return;
|
|
81
106
|
}
|
|
107
|
+
let cleanUpCount = 0;
|
|
82
108
|
// Loop through the files within the session
|
|
83
109
|
for (const entry of node_fs_1.default.readdirSync(sessionDir)) {
|
|
84
110
|
const filePath = node_path_1.default.join(sessionDir, entry);
|
|
@@ -88,16 +114,21 @@ function cleanupStaleSessions(sessionDir) {
|
|
|
88
114
|
continue;
|
|
89
115
|
}
|
|
90
116
|
// If the expected file structure has been corrupted, delete the file
|
|
91
|
-
const record =
|
|
117
|
+
const record = (0, utils_1.loadJSONFile)(filePath);
|
|
92
118
|
if (!record) {
|
|
93
119
|
deleteFile(filePath);
|
|
94
120
|
continue;
|
|
95
121
|
}
|
|
122
|
+
// If process is dead, unlink the command from the bin and delete the session file
|
|
96
123
|
if (!isProcessAlive(record.pid)) {
|
|
97
124
|
cleanupLinks(record.links);
|
|
98
125
|
deleteFile(filePath);
|
|
126
|
+
cleanUpCount++;
|
|
99
127
|
}
|
|
100
128
|
}
|
|
129
|
+
if (cleanUpCount > 0) {
|
|
130
|
+
log.info(`cleaned up ${cleanUpCount} expired session${cleanUpCount > 1 ? 's' : ''}`);
|
|
131
|
+
}
|
|
101
132
|
}
|
|
102
133
|
// Persists currently linked commands so future runs can clean stale state.
|
|
103
134
|
function persistSession(sessionPath, record) {
|
|
@@ -9,32 +9,32 @@ const prompts_1 = __importDefault(require("prompts"));
|
|
|
9
9
|
// Returning "abort" bubbles up as an error to stop the current serve run.
|
|
10
10
|
async function promptCollisionDecision(conflict) {
|
|
11
11
|
const response = await (0, prompts_1.default)({
|
|
12
|
-
type:
|
|
13
|
-
name:
|
|
12
|
+
type: 'select',
|
|
13
|
+
name: 'decision',
|
|
14
14
|
message: `command "${conflict.name}" already exists`,
|
|
15
15
|
choices: [
|
|
16
16
|
{
|
|
17
|
-
title:
|
|
18
|
-
value:
|
|
19
|
-
description: `Replace ${conflict.linkPath}
|
|
17
|
+
title: 'Overwrite existing command',
|
|
18
|
+
value: 'overwrite',
|
|
19
|
+
description: `Replace ${conflict.linkPath}`,
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
|
-
title:
|
|
23
|
-
value:
|
|
24
|
-
description: conflict.reason
|
|
22
|
+
title: 'Skip this command',
|
|
23
|
+
value: 'skip',
|
|
24
|
+
description: conflict.reason,
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
|
-
title:
|
|
28
|
-
value:
|
|
29
|
-
description:
|
|
30
|
-
}
|
|
27
|
+
title: 'Abort',
|
|
28
|
+
value: 'abort',
|
|
29
|
+
description: 'Stop serve without linking remaining commands',
|
|
30
|
+
},
|
|
31
31
|
],
|
|
32
|
-
initial: 1
|
|
32
|
+
initial: 1,
|
|
33
33
|
}, {
|
|
34
|
-
onCancel: () => false
|
|
34
|
+
onCancel: () => false,
|
|
35
35
|
});
|
|
36
36
|
if (!response.decision) {
|
|
37
|
-
return
|
|
37
|
+
return 'abort';
|
|
38
38
|
}
|
|
39
39
|
return response.decision;
|
|
40
40
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symlx",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Temporary local CLI bin linker",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
7
|
-
"symlx": "dist/
|
|
8
|
-
"cx": "dist/
|
|
7
|
+
"symlx": "dist/clii.js",
|
|
8
|
+
"cx": "dist/clii.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"commander": "^12.1.0",
|
|
29
|
-
"prompts": "^2.4.2"
|
|
29
|
+
"prompts": "^2.4.2",
|
|
30
|
+
"zod": "^4.3.6"
|
|
30
31
|
},
|
|
31
32
|
"scripts": {
|
|
32
33
|
"build": "tsc -p tsconfig.json && chmod +x dist/cli.js",
|