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 CHANGED
@@ -1,3 +1,281 @@
1
1
  # symlx
2
2
 
3
- tomorrow...
3
+ Temporary command linker for local CLI development.
4
+
5
+ `symlx serve` links command names from your project into a runnable bin directory for the lifetime of the process.
6
+ When `symlx` stops, those links are cleaned up.
7
+ `symlx link` creates the same links once and exits immediately.
8
+
9
+ ## Why symlx
10
+
11
+ During CLI development, running `node dist/cli.js` repeatedly is noisy.
12
+ `npm link` has generally been buggy and slow to pick recent code changes.
13
+ `symlx` gives you the real command experience (`my-cli --help`) without a global publish/install cycle.
14
+
15
+ Core guarantees:
16
+
17
+ - Links are session-scoped and cleaned on exit.
18
+ - Collision behavior is explicit (`prompt`, `skip`, `fail`, `overwrite`).
19
+ - Option resolution is deterministic.
20
+ - PATH setup for `~/.symlx/bin` is automated on install (with opt-out).
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npx symlx serve
26
+ # or
27
+ npm i -g symlx
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ In a CLI project with:
33
+
34
+ ```json
35
+ {
36
+ "name": "my-cli",
37
+ "bin": {
38
+ "my-cli": "./dist/cli.js"
39
+ }
40
+ }
41
+ ```
42
+
43
+ run:
44
+
45
+ ```bash
46
+ symlx link
47
+ ```
48
+
49
+ Then use your CLI normally:
50
+
51
+ ```bash
52
+ my-cli --help
53
+ ```
54
+
55
+ Use `symlx serve` when you want temporary session-scoped links with auto-cleanup on exit.
56
+
57
+ ## Alias
58
+
59
+ `symlx` can be clackful for power users, hence its alias: `cx`.
60
+
61
+ Equivalent commands:
62
+
63
+ ```bash
64
+ symlx serve
65
+ symlx link
66
+ cx serve
67
+ cx link
68
+ ```
69
+
70
+ ## Command Reference
71
+
72
+ ## `symlx serve`
73
+
74
+ Links commands from resolved bin mappings and keeps the process alive until interrupted.
75
+
76
+ ### Options
77
+
78
+ | Option | Type | Default | Description |
79
+ | -------------------------------------- | ------------------------------------- | -------------- | --------------------------------------------------------------------- |
80
+ | `--bin-dir <dir>` | string | `~/.symlx/bin` | Target directory where command links are created. |
81
+ | `--collision <policy>` | `prompt \| skip \| fail \| overwrite` | `prompt` | What to do when a command name already exists in bin dir. |
82
+ | `--bin-resolution-strategy <strategy>` | `replace \| merge` | `replace` | How to resolve `bin` across `package.json`, config, and inline flags. |
83
+ | `--non-interactive` | boolean | `false` | Disable prompts and force non-interactive behavior. |
84
+ | `--bin <name=path>` (repeatable) | string[] | `[]` | Inline bin mapping (for quick overrides/ad-hoc runs). |
85
+
86
+ Examples:
87
+
88
+ ```bash
89
+ symlx serve --collision overwrite
90
+ symlx serve --bin admin=dist/admin.js --bin worker=dist/worker.js
91
+ symlx serve --bin-resolution-strategy merge
92
+ ```
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
+
108
+ ## Bin Resolution Model
109
+
110
+ `symlx` resolves options from three user sources plus defaults:
111
+
112
+ 1. `package.json`
113
+ 2. `symlx.config.json`
114
+ 3. inline CLI flags
115
+
116
+ Scalar fields (`collision`, `binDir`, `nonInteractive`, `binResolutionStrategy`) follow normal override order:
117
+
118
+ `defaults -> package.json-derived -> config -> inline`
119
+
120
+ `bin` uses strategy mode:
121
+
122
+ - `replace` (default): first non-empty wins by priority`inline > config > package.json > default`
123
+ - `merge`: combines all
124
+ `package.json + config + inline` (right-most source overrides key collisions)
125
+
126
+ ## Supported Bin Sources
127
+
128
+ ## `package.json`
129
+
130
+ `bin` supports both npm-compatible linking:
131
+
132
+ ```json
133
+ {
134
+ "name": "my-cli",
135
+ "bin": "./dist/cli.js"
136
+ }
137
+ ```
138
+
139
+ ```json
140
+ {
141
+ "bin": {
142
+ "my-cli": "./dist/cli.js",
143
+ "my-admin": "./dist/admin.js"
144
+ }
145
+ }
146
+ ```
147
+
148
+ If `bin` is a string, `name` is required so command name can be inferred.
149
+
150
+ ## `symlx.config.json`
151
+
152
+ ```json
153
+ {
154
+ "binDir": "~/.symlx/bin",
155
+ "collision": "prompt",
156
+ "nonInteractive": false,
157
+ "binResolutionStrategy": "replace",
158
+ "bin": {
159
+ "my-cli": "./dist/cli.js"
160
+ }
161
+ }
162
+ ```
163
+
164
+ Notes:
165
+
166
+ - In case of invalid config values, `symlx` fallback to defaults (with warnings).
167
+ - `binDir` is treated as critical and must pass validation.
168
+
169
+ ## Inline Flags
170
+
171
+ ```bash
172
+ symlx serve --bin my-cli=dist/cli.js
173
+
174
+ # multiple inline bins
175
+ symlx serve \
176
+ --bin xin-ping=./cli.js \
177
+ --bin admin=./scripts/admin.js
178
+ ```
179
+
180
+ `name` rules:
181
+
182
+ - lowercase letters, digits, `-`
183
+ - no spaces
184
+
185
+ `path` rules:
186
+
187
+ - must be relative (for example `dist/cli.js` or `./dist/cli.js`)
188
+ - absolute paths are rejected
189
+
190
+ ## Collision Policies
191
+
192
+ - `prompt`: ask per conflict (interactive TTY only)
193
+ - `skip`: keep existing command, skip link
194
+ - `fail`: stop on first conflict
195
+ - `overwrite`: replace existing entry
196
+
197
+ If `prompt` is requested in non-interactive mode, symlx falls back to `skip` and warns.
198
+
199
+ ## Install-Time PATH Setup
200
+
201
+ On install, `symlx` updates shell profile PATH block
202
+
203
+ Managed path:
204
+
205
+ ```bash
206
+ $HOME/.symlx/bin
207
+ ```
208
+
209
+ Opt out:
210
+
211
+ ```bash
212
+ SYMLX_SKIP_PATH_SETUP=1 npm i -g symlx
213
+ ```
214
+
215
+ To set a custome bin PATH:
216
+
217
+ ```bash
218
+ symlx serve --bin-dir ~/.symlx/bin
219
+ ```
220
+
221
+ ## Runtime Safety Checks
222
+
223
+ Before linking, symlx prepares each resolved bin target:
224
+
225
+ - file exists
226
+ - target is not a directory
227
+ - target is made executable automatically on unix-like systems when possible
228
+
229
+ Missing targets, directories, and permission-update failures still fail early with actionable messages.
230
+
231
+ ## Exit Behavior
232
+
233
+ - `Ctrl+C` (SIGINT), SIGTERM, SIGHUP, uncaught exception, and unhandled rejection trigger cleanup.
234
+ - Session metadata is stored under `~/.symlx/sessions`.
235
+ - Stale sessions leftover due to hard crashes are cleaned on startup.
236
+
237
+ ## Troubleshooting
238
+
239
+ ## "no bin entries found"
240
+
241
+ Add a bin mapping in at least one place:
242
+
243
+ - `package.json -> bin`
244
+ - `symlx.config.json -> bin`
245
+ - `--bin name=path`
246
+
247
+ ## "command conflicts at ..."
248
+
249
+ Use a collision mode:
250
+
251
+ ```bash
252
+ symlx serve --collision overwrite
253
+ # or
254
+ symlx serve --collision fail
255
+ ```
256
+
257
+ ## "package.json not found"
258
+
259
+ Run in your project root, or pass bins inline/config.
260
+
261
+ ## Development
262
+
263
+ ```bash
264
+ pnpm install
265
+ pnpm run check
266
+ pnpm run build
267
+ pnpm run test
268
+ ```
269
+
270
+ ## Extending Commands (Contributor Contract)
271
+
272
+ To add a new command while preserving set conventions:
273
+
274
+ 1. Define command surface in `src/cli.ts`.
275
+ 2. Keep orchestration in `src/commands/*`.
276
+ 3. Reuse `resolveOptions()` for deterministic source handling.
277
+ 4. Validate user-facing options with zod schemas in `src/lib/schema.ts`.
278
+ 5. Add behavior coverage in `test/*.test.ts`.
279
+ 6. Update this README command reference and examples.
280
+
281
+ The goal is consistent behavior across all current and future commands.
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
- function collectBinEntry(value, previous = []) {
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();
@@ -50,12 +48,22 @@ async function main() {
50
48
  .showHelpAfterError();
51
49
  program
52
50
  .command('serve')
53
- .description("Link this project's package.json bins until symlx exits")
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, [])
51
+ .description("Link this project's bin commands until symlx exits")
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)
58
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);
59
67
  await program.parseAsync(process.argv);
60
68
  }
61
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
+ }
@@ -32,81 +32,51 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
36
39
  exports.serveCommand = serveCommand;
37
- const paths_1 = require("../lib/paths");
38
- const link_manager_1 = require("../services/link-manager");
39
- const lifecycle_1 = require("../services/lifecycle");
40
- // @ts-ignore
41
- const package_bins_1 = require("../services/package-bins");
42
- const session_store_1 = require("../services/session-store");
43
- const collision_prompt_1 = require("../ui/collision-prompt");
40
+ const node_os_1 = __importDefault(require("node:os"));
41
+ const node_path_1 = __importDefault(require("node:path"));
44
42
  const log = __importStar(require("../ui/logger"));
43
+ const serve_output_1 = require("../ui/serve-output");
45
44
  const options_1 = require("../lib/options");
46
45
  const schema_1 = require("../lib/schema");
47
- // Prompts require an interactive terminal; scripts/CI should avoid prompt mode.
48
- function isInteractiveSession() {
49
- return Boolean(process.stdin.isTTY && process.stdout.isTTY);
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() {
51
+ return new Promise(() => {
52
+ setInterval(() => undefined, 60_000);
53
+ });
50
54
  }
51
- // Main symlx behavior:
52
- // 1) resolve bins from package.json
53
- // 2) create links
54
- // 3) persist session
55
- // 4) keep process alive and cleanup on exit
56
- async function run(options) {
55
+ async function serveCommand(inlineOptions) {
57
56
  const cwd = process.cwd();
58
- const paths = (0, paths_1.getSymlxPaths)(options.binDir);
59
- // Prepare runtime directories and recover stale sessions from previous abnormal exits.
60
- (0, session_store_1.cleanupStaleSessions)(paths.sessionDir);
61
- (0, session_store_1.ensureSymlxDirectories)(paths.binDir, paths.sessionDir);
62
- const bins = (0, package_bins_1.readBins)(cwd);
63
- // Prompt policy only works when we can interact with a TTY.
64
- const usePrompts = options.collision === 'prompt' &&
65
- !options.nonInteractive &&
66
- isInteractiveSession();
67
- if (options.collision === 'prompt' && !usePrompts) {
68
- log.warn('prompt collision mode requested but session is non-interactive; falling back to skip');
69
- }
70
- // Link creation returns both successful links and explicit skips.
71
- const linkResult = await (0, link_manager_1.createLinks)({
72
- bins,
73
- binDir: paths.binDir,
74
- policy: options.collision,
75
- collisionResolver: usePrompts ? collision_prompt_1.promptCollisionDecision : undefined,
76
- });
77
- if (linkResult.created.length === 0) {
78
- throw new Error('no links were created');
79
- }
80
- // Session file is the source of truth for cleaning this exact run's links.
81
- const sessionPath = (0, session_store_1.createSessionFilePath)(paths.sessionDir);
82
- const sessionRecord = {
83
- pid: process.pid,
84
- cwd,
85
- createdAt: new Date().toISOString(),
86
- links: linkResult.created,
87
- };
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
60
+ const options = (0, options_1.resolveOptions)(cwd, schema_1.serveInlineOptionsSchema, inlineOptions);
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);
88
72
  (0, session_store_1.persistSession)(sessionPath, sessionRecord);
89
- // Always cleanup linked commands when this process leaves.
90
- (0, lifecycle_1.registerLifecycleCleanup)(() => {
91
- (0, session_store_1.cleanupSession)(sessionPath, sessionRecord.links);
92
- });
93
- log.info(`linked ${linkResult.created.length} command(s) into ${paths.binDir}`);
94
- for (const link of linkResult.created) {
95
- log.info(`${link.name} -> ${link.target}`);
96
- }
97
- for (const skip of linkResult.skipped) {
98
- log.warn(`skip "${skip.name}": ${skip.reason} (${skip.linkPath})`);
99
- }
100
- if (!(0, paths_1.pathContainsDir)(process.env.PATH, paths.binDir)) {
101
- log.info(`add this to your shell config if needed:\nexport PATH="${paths.binDir}:$PATH"`);
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);
102
77
  }
78
+ (0, serve_output_1.printLinkOutcome)(options.binDir, linkResult);
79
+ (0, serve_output_1.printPathHintIfNeeded)(options.binDir, process.env.PATH);
103
80
  log.info('running. press Ctrl+C to cleanup links.');
104
- // Keep process alive indefinitely; lifecycle handlers handle termination and cleanup.
105
- await new Promise(() => {
106
- setInterval(() => undefined, 60_000);
107
- });
108
- }
109
- function serveCommand(inlineOptions) {
110
- const options = (0, options_1.resolveOptions)(schema_1.serveInlineOptionsSchema, inlineOptions);
111
- return run(options);
81
+ await waitUntilStopped();
112
82
  }