@ulpi/cli 0.1.0

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.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -0
  3. package/dist/auth-PN7TMQHV-2W4ICG64.js +15 -0
  4. package/dist/chunk-247GVVKK.js +2259 -0
  5. package/dist/chunk-2CLNOKPA.js +793 -0
  6. package/dist/chunk-2HEE5OKX.js +79 -0
  7. package/dist/chunk-2MZER6ND.js +415 -0
  8. package/dist/chunk-3SBPZRB5.js +772 -0
  9. package/dist/chunk-4VNS5WPM.js +42 -0
  10. package/dist/chunk-6JCMYYBT.js +1546 -0
  11. package/dist/chunk-6OCEY7JY.js +422 -0
  12. package/dist/chunk-74WVVWJ4.js +375 -0
  13. package/dist/chunk-7AL4DOEJ.js +131 -0
  14. package/dist/chunk-7LXY5UVC.js +330 -0
  15. package/dist/chunk-DBMUNBNB.js +3048 -0
  16. package/dist/chunk-JWUUVXIV.js +13694 -0
  17. package/dist/chunk-KIKPIH6N.js +4048 -0
  18. package/dist/chunk-KLEASXUR.js +70 -0
  19. package/dist/chunk-MIAQVCFW.js +39 -0
  20. package/dist/chunk-NNUWU6CV.js +1610 -0
  21. package/dist/chunk-PKD4ASEM.js +115 -0
  22. package/dist/chunk-Q4HIY43N.js +4230 -0
  23. package/dist/chunk-QJ5GSMEC.js +146 -0
  24. package/dist/chunk-SIAQVRKG.js +2163 -0
  25. package/dist/chunk-SPOI23SB.js +197 -0
  26. package/dist/chunk-YM2HV4IA.js +505 -0
  27. package/dist/codemap-RRJIDBQ5.js +636 -0
  28. package/dist/config-EGAXXCGL.js +127 -0
  29. package/dist/dist-6G7JC2RA.js +90 -0
  30. package/dist/dist-7LHZ65GC.js +418 -0
  31. package/dist/dist-LZKZFPVX.js +140 -0
  32. package/dist/dist-R5F4MX3I.js +107 -0
  33. package/dist/dist-R5ZJ4LX5.js +56 -0
  34. package/dist/dist-RJGCUS3L.js +87 -0
  35. package/dist/dist-RKOGLK7R.js +151 -0
  36. package/dist/dist-W7K4WPAF.js +597 -0
  37. package/dist/export-import-4A5MWLIA.js +53 -0
  38. package/dist/history-ATTUKOHO.js +934 -0
  39. package/dist/index.js +2120 -0
  40. package/dist/init-AY5C2ZAS.js +393 -0
  41. package/dist/launchd-LF2QMSKZ.js +148 -0
  42. package/dist/log-TVTUXAYD.js +75 -0
  43. package/dist/mcp-installer-NQCGKQ23.js +124 -0
  44. package/dist/memory-J3G24QHS.js +406 -0
  45. package/dist/ollama-3XCUZMZT-FYKHW4TZ.js +7 -0
  46. package/dist/openai-E7G2YAHU-UYY4ZWON.js +8 -0
  47. package/dist/projects-ATHDD3D6.js +271 -0
  48. package/dist/review-ADUPV3PN.js +152 -0
  49. package/dist/rules-E427DKYJ.js +134 -0
  50. package/dist/server-MOYPE4SM-N7SE2AN7.js +18 -0
  51. package/dist/server-X5P6WH2M-7K2RY34N.js +11 -0
  52. package/dist/skills/ulpi-generate-guardian/SKILL.md +511 -0
  53. package/dist/skills/ulpi-generate-guardian/references/framework-rules.md +692 -0
  54. package/dist/skills/ulpi-generate-guardian/references/language-rules.md +596 -0
  55. package/dist/skills-CX73O3IV.js +76 -0
  56. package/dist/status-4DFHDJMN.js +66 -0
  57. package/dist/templates/biome.yml +24 -0
  58. package/dist/templates/conventional-commits.yml +18 -0
  59. package/dist/templates/django.yml +30 -0
  60. package/dist/templates/docker.yml +30 -0
  61. package/dist/templates/eslint.yml +13 -0
  62. package/dist/templates/express.yml +20 -0
  63. package/dist/templates/fastapi.yml +23 -0
  64. package/dist/templates/git-flow.yml +26 -0
  65. package/dist/templates/github-flow.yml +27 -0
  66. package/dist/templates/go.yml +33 -0
  67. package/dist/templates/jest.yml +24 -0
  68. package/dist/templates/laravel.yml +30 -0
  69. package/dist/templates/monorepo.yml +26 -0
  70. package/dist/templates/nestjs.yml +21 -0
  71. package/dist/templates/nextjs.yml +31 -0
  72. package/dist/templates/nodejs.yml +33 -0
  73. package/dist/templates/npm.yml +15 -0
  74. package/dist/templates/php.yml +25 -0
  75. package/dist/templates/pnpm.yml +15 -0
  76. package/dist/templates/prettier.yml +23 -0
  77. package/dist/templates/prisma.yml +21 -0
  78. package/dist/templates/python.yml +33 -0
  79. package/dist/templates/quality-of-life.yml +111 -0
  80. package/dist/templates/ruby.yml +25 -0
  81. package/dist/templates/rust.yml +34 -0
  82. package/dist/templates/typescript.yml +14 -0
  83. package/dist/templates/vitest.yml +24 -0
  84. package/dist/templates/yarn.yml +15 -0
  85. package/dist/templates-U7T6MARD.js +156 -0
  86. package/dist/ui-L7UAWXDY.js +167 -0
  87. package/dist/ui.html +698 -0
  88. package/dist/ulpi-RMMCUAGP-JCJ273T6.js +161 -0
  89. package/dist/uninstall-6SW35IK4.js +25 -0
  90. package/dist/update-M2B4RLGH.js +61 -0
  91. package/dist/version-checker-ANCS3IHR.js +10 -0
  92. package/package.json +92 -0
@@ -0,0 +1,111 @@
1
+ id: quality-of-life
2
+ name: Quality of Life
3
+ category: universal
4
+
5
+ # ============================================================================
6
+ # QUALITY OF LIFE TEMPLATE
7
+ # ============================================================================
8
+ # This template provides core safety rules without blocking legitimate work.
9
+ #
10
+ # Three-Tier Philosophy:
11
+ # 1. UNIVERSAL SAFETY - Block truly dangerous operations (narrow patterns)
12
+ # 2. WORKFLOW GUIDANCE - Guide best practices (context-aware, not blocking)
13
+ # 3. PRODUCTIVITY - Auto-approve safe operations (broad patterns)
14
+ #
15
+ # Built-In Protection:
16
+ # The evaluator automatically blocks dangerous commands with proper regex:
17
+ # - rm -rf / (root filesystem deletion)
18
+ # - sudo rm (privileged deletion)
19
+ # - chmod 777 (insecure permissions)
20
+ # - git push --force (without --force-with-lease)
21
+ # - git push to main/master
22
+ # - Database destruction (prisma reset, artisan wipe, rails drop, SQL DROP)
23
+ #
24
+ # You don't need custom rules for these unless you want stricter policies.
25
+ # ============================================================================
26
+
27
+ rules:
28
+ preconditions:
29
+ read-before-write:
30
+ enabled: true
31
+ trigger: PreToolUse
32
+ matcher: "Write|Edit|MultiEdit"
33
+ requires_read: true
34
+ message: "Read {file_path} before editing it."
35
+ locked: true
36
+ priority: 10
37
+ read-before-edit:
38
+ enabled: true
39
+ trigger: PreToolUse
40
+ matcher: "Edit|MultiEdit"
41
+ requires_read: true
42
+ message: "Read {file_path} first."
43
+ locked: true
44
+ priority: 10
45
+ no-empty-overwrites:
46
+ enabled: true
47
+ trigger: PreToolUse
48
+ matcher: "Write"
49
+ message: "You're overwriting with empty content. Provide actual content."
50
+ locked: true
51
+ priority: 10
52
+ branch-before-commit:
53
+ enabled: true
54
+ trigger: PreToolUse
55
+ matcher: "Bash"
56
+ command_pattern: "git commit"
57
+ requires: [branch_created]
58
+ message: "Create a feature branch first: git checkout -b feature/your-feature-name"
59
+ priority: 20
60
+
61
+ permissions:
62
+ auto-approve-reads:
63
+ enabled: true
64
+ trigger: PermissionRequest
65
+ matcher: "Read|LS|Glob|Grep"
66
+ decision: allow
67
+ priority: 100
68
+
69
+ block-env-files:
70
+ enabled: true
71
+ trigger: PreToolUse
72
+ matcher: "Write|Edit"
73
+ file_pattern: ".env*"
74
+ decision: deny
75
+ message: "Cannot edit .env files directly. Use .env.example as template."
76
+ priority: 50
77
+
78
+ # ============================================================================
79
+ # OPTIONAL RULES (Disabled by Default)
80
+ # ============================================================================
81
+ # The evaluator has built-in protection that blocks:
82
+ # - rm -rf / (root filesystem deletion)
83
+ # - sudo rm (privileged deletion)
84
+ # - chmod 777 (insecure permissions)
85
+ # - git push --force (without --force-with-lease)
86
+ # - git push to main/master
87
+ # - Database destruction (prisma reset, artisan wipe, etc.)
88
+ #
89
+ # Enable these rules only if you need CUSTOM patterns beyond the built-in protection.
90
+
91
+ custom-dangerous-commands:
92
+ enabled: false
93
+ trigger: PreToolUse
94
+ matcher: "Bash"
95
+ command_pattern: "your-custom-pattern-here"
96
+ decision: deny
97
+ message: "Custom dangerous command blocked."
98
+ # TIP: command_pattern uses PREFIX matching, not regex
99
+ # EXAMPLE: "docker rm -f" blocks "docker rm -f container-name"
100
+ # WARNING: Setting this rule disables built-in protection - be careful!
101
+
102
+ custom-auto-approvals:
103
+ enabled: false
104
+ trigger: PermissionRequest
105
+ matcher: "Bash"
106
+ command_pattern: "npm test"
107
+ decision: allow
108
+ message: "Auto-approved test command."
109
+ # TIP: Customize command_pattern for your project tools
110
+ # EXAMPLES: "pnpm", "vitest", "pytest", "cargo test"
111
+ # NOTE: Stack-specific templates provide better auto-approvals
@@ -0,0 +1,25 @@
1
+ id: ruby
2
+ name: Ruby
3
+ category: runtime
4
+ variables:
5
+ test_command: "bundle exec rspec"
6
+ lint_command: "bundle exec rubocop"
7
+ package_manager: bundle
8
+
9
+ rules:
10
+ preconditions:
11
+ test-before-commit:
12
+ enabled: true
13
+ trigger: PreToolUse
14
+ matcher: "Bash"
15
+ command_pattern: "git commit"
16
+ requires: [tests_run]
17
+ message: "Run {test_command} before committing."
18
+ postconditions:
19
+ bundle-install:
20
+ enabled: true
21
+ trigger: PostToolUse
22
+ matcher: "Write|Edit"
23
+ file_pattern: "Gemfile"
24
+ run: "bundle install"
25
+ timeout: 60000
@@ -0,0 +1,34 @@
1
+ id: rust
2
+ name: Rust
3
+ category: runtime
4
+ variables:
5
+ test_command: "cargo test"
6
+ lint_command: "cargo clippy"
7
+ build_command: "cargo build"
8
+
9
+ rules:
10
+ preconditions:
11
+ test-before-commit:
12
+ enabled: true
13
+ trigger: PreToolUse
14
+ matcher: "Bash"
15
+ command_pattern: "git commit"
16
+ requires: [tests_run]
17
+ message: "Run {test_command} before committing."
18
+ postconditions:
19
+ cargo-check-after-edit:
20
+ enabled: true
21
+ trigger: PostToolUse
22
+ matcher: "Write|Edit|MultiEdit"
23
+ file_pattern: "*.rs"
24
+ run: "cargo check"
25
+ timeout: 60000
26
+ block_on_failure: false
27
+ permissions:
28
+ auto-approve-cargo:
29
+ enabled: true
30
+ trigger: PermissionRequest
31
+ matcher: "Bash"
32
+ command_pattern: "cargo"
33
+ decision: allow
34
+ message: "Auto-approved cargo command."
@@ -0,0 +1,14 @@
1
+ id: typescript
2
+ name: TypeScript
3
+ category: language
4
+
5
+ rules:
6
+ postconditions:
7
+ typecheck-after-edit:
8
+ enabled: true
9
+ trigger: PostToolUse
10
+ matcher: "Write|Edit|MultiEdit"
11
+ file_pattern: "*.{ts,tsx}"
12
+ run: "npx tsc --noEmit"
13
+ timeout: 30000
14
+ block_on_failure: false
@@ -0,0 +1,24 @@
1
+ id: vitest
2
+ name: Vitest
3
+ category: test-runner
4
+ variables:
5
+ test_command: "npx vitest run"
6
+
7
+ rules:
8
+ permissions:
9
+ auto-approve-vitest:
10
+ enabled: true
11
+ trigger: PermissionRequest
12
+ matcher: "Bash"
13
+ command_pattern: "vitest"
14
+ decision: allow
15
+ message: "Auto-approved vitest command."
16
+ postconditions:
17
+ run-tests-after-edit:
18
+ enabled: true
19
+ trigger: PostToolUse
20
+ matcher: "Write|Edit|MultiEdit"
21
+ file_pattern: "*.{test,spec}.{ts,tsx,js,jsx}"
22
+ run: "npx vitest run {file_path}"
23
+ timeout: 30000
24
+ block_on_failure: false
@@ -0,0 +1,15 @@
1
+ id: yarn
2
+ name: Yarn
3
+ category: package-manager
4
+ variables:
5
+ package_manager: yarn
6
+
7
+ rules:
8
+ permissions:
9
+ auto-approve-yarn:
10
+ enabled: true
11
+ trigger: PermissionRequest
12
+ matcher: "Bash"
13
+ command_pattern: "yarn"
14
+ decision: allow
15
+ message: "Auto-approved yarn command."
@@ -0,0 +1,156 @@
1
+ import {
2
+ deleteUserTemplate,
3
+ exportUserTemplate,
4
+ importUserTemplate,
5
+ listUserTemplates,
6
+ loadBundledTemplates,
7
+ saveUserTemplate
8
+ } from "./chunk-6OCEY7JY.js";
9
+ import {
10
+ loadRulesSync
11
+ } from "./chunk-SIAQVRKG.js";
12
+ import "./chunk-KIKPIH6N.js";
13
+ import "./chunk-7LXY5UVC.js";
14
+ import "./chunk-4VNS5WPM.js";
15
+
16
+ // src/commands/templates.ts
17
+ import * as fs from "fs";
18
+ import * as path from "path";
19
+ import chalk from "chalk";
20
+ async function runTemplates(args, _projectDir) {
21
+ const subcommand = args[0];
22
+ switch (subcommand) {
23
+ case "list":
24
+ return listTemplates();
25
+ case "save":
26
+ return saveTemplate(args[1]);
27
+ case "delete":
28
+ return deleteTemplate(args[1]);
29
+ case "export":
30
+ return exportTemplate(args[1], args[2]);
31
+ case "import":
32
+ return importTemplate(args[1]);
33
+ default:
34
+ console.log(`
35
+ Usage: ulpi templates <subcommand>
36
+
37
+ Subcommands:
38
+ list List bundled and user templates
39
+ save Save current rules as a user template
40
+ delete Delete a user template
41
+ export Export a template to file
42
+ import Import a template from file
43
+ `.trim());
44
+ }
45
+ }
46
+ function listTemplates() {
47
+ console.log(chalk.bold("\nBundled Templates:\n"));
48
+ const bundled = loadBundledTemplates();
49
+ for (const t of bundled) {
50
+ console.log(` ${chalk.cyan(t.id.padEnd(25))} ${chalk.dim(t.category)} \u2014 ${t.name}`);
51
+ }
52
+ console.log(chalk.bold("\nUser Templates:\n"));
53
+ const userTemplates = listUserTemplates();
54
+ if (userTemplates.length === 0) {
55
+ console.log(chalk.dim(" No user templates saved."));
56
+ } else {
57
+ for (const t of userTemplates) {
58
+ console.log(` ${chalk.green(t.name.padEnd(25))} ${t.description ?? ""}`);
59
+ }
60
+ }
61
+ console.log("");
62
+ }
63
+ function saveTemplate(name) {
64
+ if (!name) {
65
+ console.log(chalk.red("Usage: ulpi templates save <name>"));
66
+ return;
67
+ }
68
+ const projectRulesPath = path.join(process.cwd(), ".ulpi", "guards.yml");
69
+ if (!fs.existsSync(projectRulesPath)) {
70
+ console.log(chalk.red("No guards.yml found in current project."));
71
+ console.log(chalk.dim("Run 'ulpi init' first to generate rules."));
72
+ return;
73
+ }
74
+ const config = loadRulesSync(projectRulesPath);
75
+ if (!config) {
76
+ console.log(chalk.red("Failed to parse guards.yml"));
77
+ return;
78
+ }
79
+ const extractRules = (section) => {
80
+ const result = {};
81
+ for (const [key, rule] of Object.entries(section)) {
82
+ const { id, type, ...rest } = rule;
83
+ result[key] = rest;
84
+ }
85
+ return result;
86
+ };
87
+ const template = {
88
+ name,
89
+ description: `Saved from ${config.project.name} on ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
90
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
91
+ stack: config.project,
92
+ layers: [],
93
+ // No bundled layer references since this is a custom save
94
+ rules: {
95
+ preconditions: extractRules(config.preconditions),
96
+ postconditions: extractRules(config.postconditions),
97
+ permissions: extractRules(config.permissions),
98
+ pipelines: extractRules(config.pipelines)
99
+ },
100
+ skills: []
101
+ // Could extract skill references from rules if needed
102
+ };
103
+ try {
104
+ saveUserTemplate(template);
105
+ console.log(chalk.green(`\u2713 Template "${name}" saved`));
106
+ console.log(chalk.dim(` Location: ~/.ulpi/templates/${name}.yml`));
107
+ console.log(chalk.dim(` Rules: ${Object.keys(config.preconditions).length} preconditions, ${Object.keys(config.permissions).length} permissions`));
108
+ } catch (err) {
109
+ console.log(chalk.red(`Failed to save template: ${err instanceof Error ? err.message : String(err)}`));
110
+ }
111
+ }
112
+ function deleteTemplate(id) {
113
+ if (!id) {
114
+ console.log(chalk.red("Usage: ulpi templates delete <id>"));
115
+ return;
116
+ }
117
+ const deleted = deleteUserTemplate(id);
118
+ if (deleted) {
119
+ console.log(chalk.green(`\u2713 Template "${id}" deleted`));
120
+ } else {
121
+ console.log(chalk.red(`Template "${id}" not found`));
122
+ }
123
+ }
124
+ function exportTemplate(name, outputPath) {
125
+ if (!name) {
126
+ console.log(chalk.red("Usage: ulpi templates export <name> [output-path]"));
127
+ return;
128
+ }
129
+ const exported = exportUserTemplate(name);
130
+ if (exported) {
131
+ if (outputPath) {
132
+ fs.writeFileSync(outputPath, exported, "utf-8");
133
+ console.log(chalk.green(`\u2713 Template "${name}" exported to ${outputPath}`));
134
+ } else {
135
+ console.log(exported);
136
+ }
137
+ } else {
138
+ console.log(chalk.red(`Template "${name}" not found`));
139
+ }
140
+ }
141
+ function importTemplate(inputPath) {
142
+ if (!inputPath) {
143
+ console.log(chalk.red("Usage: ulpi templates import <file-path>"));
144
+ return;
145
+ }
146
+ try {
147
+ const content = fs.readFileSync(inputPath, "utf-8");
148
+ const imported = importUserTemplate(content);
149
+ console.log(chalk.green(`\u2713 Template imported as "${imported.name}"`));
150
+ } catch (err) {
151
+ console.log(chalk.red(`Failed to import template: ${err instanceof Error ? err.message : String(err)}`));
152
+ }
153
+ }
154
+ export {
155
+ runTemplates
156
+ };
@@ -0,0 +1,167 @@
1
+ import {
2
+ createApiServer,
3
+ setUiServerPort
4
+ } from "./chunk-Q4HIY43N.js";
5
+ import {
6
+ attachWebSocket
7
+ } from "./chunk-7AL4DOEJ.js";
8
+ import {
9
+ generateApiSecret,
10
+ setApiSecret
11
+ } from "./chunk-MIAQVCFW.js";
12
+ import "./chunk-2MZER6ND.js";
13
+ import "./chunk-3SBPZRB5.js";
14
+ import "./chunk-2CLNOKPA.js";
15
+ import "./chunk-SPOI23SB.js";
16
+ import "./chunk-6OCEY7JY.js";
17
+ import "./chunk-SIAQVRKG.js";
18
+ import "./chunk-NNUWU6CV.js";
19
+ import "./chunk-YM2HV4IA.js";
20
+ import "./chunk-KIKPIH6N.js";
21
+ import {
22
+ API_LOCK_FILE,
23
+ getApiHost,
24
+ getApiPort
25
+ } from "./chunk-7LXY5UVC.js";
26
+ import "./chunk-4VNS5WPM.js";
27
+
28
+ // src/commands/ui.ts
29
+ import chalk from "chalk";
30
+
31
+ // ../api/dist/index.js
32
+ import * as fs from "fs";
33
+ import * as path from "path";
34
+ function writeApiLockFile(port, secret) {
35
+ try {
36
+ const dir = path.dirname(API_LOCK_FILE);
37
+ fs.mkdirSync(dir, { recursive: true });
38
+ const lockData = { port, pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
39
+ if (secret) lockData.secret = secret;
40
+ fs.writeFileSync(API_LOCK_FILE, JSON.stringify(lockData), { encoding: "utf-8", mode: 384 });
41
+ } catch {
42
+ }
43
+ }
44
+ function removeApiLockFile() {
45
+ try {
46
+ fs.unlinkSync(API_LOCK_FILE);
47
+ } catch {
48
+ }
49
+ }
50
+ async function main() {
51
+ const { getApiPort: getApiPort2, getApiHost: getApiHost2 } = await import("./dist-RKOGLK7R.js");
52
+ const { generateApiSecret: generateApiSecret2, setApiSecret: setApiSecret2 } = await import("./auth-PN7TMQHV-2W4ICG64.js");
53
+ const port = getApiPort2();
54
+ const host = getApiHost2();
55
+ const projectDir = process.argv[2] || process.cwd();
56
+ const secret = generateApiSecret2();
57
+ setApiSecret2(secret);
58
+ const { createApiServer: createApiServer2 } = await import("./server-MOYPE4SM-N7SE2AN7.js");
59
+ const { attachWebSocket: attachWebSocket2 } = await import("./server-X5P6WH2M-7K2RY34N.js");
60
+ const server = createApiServer2(projectDir);
61
+ const wss = attachWebSocket2(server, projectDir);
62
+ const shutdown = () => {
63
+ console.log("\nShutting down ULPI API server...");
64
+ removeApiLockFile();
65
+ for (const client of wss.clients) {
66
+ client.close();
67
+ }
68
+ wss.close();
69
+ server.close(() => {
70
+ console.log("Server stopped.");
71
+ process.exit(0);
72
+ });
73
+ setTimeout(() => process.exit(0), 5e3).unref();
74
+ };
75
+ process.on("SIGTERM", shutdown);
76
+ process.on("SIGINT", shutdown);
77
+ server.listen(port, host, () => {
78
+ writeApiLockFile(port, secret);
79
+ console.log(`ULPI API server running at http://${host}:${port}`);
80
+ console.log(` WebSocket: ws://${host}:${port}/ws`);
81
+ console.log(` API: http://${host}:${port}/api/health`);
82
+ console.log(` Project: ${projectDir}`);
83
+ });
84
+ }
85
+ var isDirectRun = process.argv[1] && (process.argv[1].endsWith("index.js") || process.argv[1].endsWith("index.mjs"));
86
+ if (isDirectRun) {
87
+ main().catch((err) => {
88
+ console.error(
89
+ "Fatal:",
90
+ err instanceof Error ? err.message : String(err)
91
+ );
92
+ process.exit(1);
93
+ });
94
+ }
95
+
96
+ // src/commands/ui.ts
97
+ async function runUI(args, projectDir) {
98
+ const portIndex = args.indexOf("--port");
99
+ let port = getApiPort();
100
+ if (portIndex !== -1 && args[portIndex + 1]) {
101
+ const parsed = parseInt(args[portIndex + 1], 10);
102
+ if (isNaN(parsed) || parsed < 1 || parsed > 65535) {
103
+ console.log(chalk.red("Invalid port number. Must be between 1 and 65535."));
104
+ process.exit(1);
105
+ }
106
+ port = parsed;
107
+ }
108
+ const host = getApiHost();
109
+ try {
110
+ const secret = generateApiSecret();
111
+ setApiSecret(secret);
112
+ setUiServerPort(port);
113
+ const server = createApiServer(projectDir);
114
+ const wss = attachWebSocket(server, projectDir);
115
+ const shutdown = () => {
116
+ console.log("\nShutting down ULPI server...");
117
+ removeApiLockFile();
118
+ for (const client of wss.clients) {
119
+ client.close();
120
+ }
121
+ wss.close();
122
+ server.close(() => {
123
+ console.log("Server stopped.");
124
+ process.exit(0);
125
+ });
126
+ setTimeout(() => process.exit(0), 5e3).unref();
127
+ };
128
+ process.on("SIGTERM", shutdown);
129
+ process.on("SIGINT", shutdown);
130
+ await new Promise((resolve, reject) => {
131
+ server.on("error", (err) => {
132
+ if (err.code === "EADDRINUSE") {
133
+ console.error(
134
+ `Port ${port} is already in use. Try a different port with --port.`
135
+ );
136
+ }
137
+ reject(err);
138
+ });
139
+ server.listen(port, host, () => {
140
+ writeApiLockFile(port, secret);
141
+ console.log(`
142
+ ULPI server running at:
143
+ `);
144
+ console.log(` http://${host}:${port}
145
+ `);
146
+ console.log(` WebSocket: ws://${host}:${port}/ws`);
147
+ console.log(` API: http://${host}:${port}/api/rules
148
+ `);
149
+ console.log(`Project dir: ${projectDir}
150
+ `);
151
+ console.log(`Press Ctrl+C to stop.
152
+ `);
153
+ resolve();
154
+ });
155
+ });
156
+ } catch (err) {
157
+ console.error(
158
+ chalk.red(
159
+ `Failed to start UI server: ${err instanceof Error ? err.message : String(err)}`
160
+ )
161
+ );
162
+ process.exit(1);
163
+ }
164
+ }
165
+ export {
166
+ runUI
167
+ };