codeharbor 0.1.17 → 0.1.19

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
@@ -93,6 +93,8 @@ Notes:
93
93
  - Service commands auto-elevate with `sudo` when root privileges are required.
94
94
  - `codeharbor service install --with-admin` and `install-linux-easy.sh --enable-admin-service` now install
95
95
  `/etc/sudoers.d/codeharbor-restart` for non-root service users, so Admin UI restart actions work out-of-box.
96
+ - `npm install -g codeharbor@latest` now performs best-effort restart for active `codeharbor(.service)` units on Linux
97
+ so upgrades take effect immediately (set `CODEHARBOR_SKIP_POSTINSTALL_RESTART=1` to disable).
96
98
  - If your environment blocks interactive `sudo`, use explicit fallback:
97
99
  - `sudo <node-bin> <codeharbor-cli-script> service ...`
98
100
 
package/dist/cli.js CHANGED
@@ -3128,7 +3128,7 @@ function renderMatrixHtml(body, msgtype) {
3128
3128
  let match;
3129
3129
  while ((match = codeFencePattern.exec(normalized)) !== null) {
3130
3130
  const before = normalized.slice(cursor, match.index);
3131
- const renderedBefore = renderTextSection(before);
3131
+ const renderedBefore = renderMarkdownSection(before);
3132
3132
  if (renderedBefore) {
3133
3133
  sections.push(renderedBefore);
3134
3134
  }
@@ -3141,29 +3141,142 @@ function renderMatrixHtml(body, msgtype) {
3141
3141
  cursor = match.index + match[0].length;
3142
3142
  }
3143
3143
  const tail = normalized.slice(cursor);
3144
- const renderedTail = renderTextSection(tail);
3144
+ const renderedTail = renderMarkdownSection(tail);
3145
3145
  if (renderedTail) {
3146
3146
  sections.push(renderedTail);
3147
3147
  }
3148
3148
  if (sections.length === 0) {
3149
3149
  sections.push("<p>(\u7A7A\u6D88\u606F)</p>");
3150
3150
  }
3151
- const badge = msgtype === "m.notice" ? `<p><font color="#8a5a00"><b>\u{1F4E3} CodeHarbor \u63D0\u793A</b></font></p>` : `<p><font color="#1f7a5a"><b>\u{1F916} AI \u56DE\u590D</b></font></p>`;
3151
+ const badge = msgtype === "m.notice" ? `<p><font color="#8a5a00"><b>CodeHarbor \u63D0\u793A</b></font></p>` : `<p><font color="#1f7a5a"><b>CodeHarbor AI \u56DE\u590D</b></font></p>`;
3152
3152
  return `<div>${badge}${sections.join("")}</div>`;
3153
3153
  }
3154
- function renderTextSection(raw) {
3154
+ function renderMarkdownSection(raw) {
3155
3155
  if (!raw.trim()) {
3156
3156
  return "";
3157
3157
  }
3158
- const normalized = raw.replace(/\r\n/g, "\n").trim();
3159
- const paragraphs = normalized.split(/\n{2,}/);
3160
- const rendered = paragraphs.map((paragraph) => {
3161
- const escaped = escapeHtml(paragraph);
3162
- const inlineCode = escaped.replace(/`([^`\n]+)`/g, "<code>$1</code>");
3163
- return `<p>${inlineCode.replace(/\n/g, "<br/>")}</p>`;
3164
- }).join("");
3158
+ const lines = raw.replace(/\r\n/g, "\n").trim().split("\n");
3159
+ const blocks = [];
3160
+ let index = 0;
3161
+ while (index < lines.length) {
3162
+ const line = lines[index];
3163
+ const trimmed = line.trim();
3164
+ if (!trimmed) {
3165
+ index += 1;
3166
+ continue;
3167
+ }
3168
+ const headingMatch = /^(#{1,6})\s+(.+)$/.exec(trimmed);
3169
+ if (headingMatch) {
3170
+ const level = Math.min(6, headingMatch[1].length + 1);
3171
+ blocks.push(`<h${level}>${renderInlineMarkup(headingMatch[2])}</h${level}>`);
3172
+ index += 1;
3173
+ continue;
3174
+ }
3175
+ if (/^(?:-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
3176
+ blocks.push("<hr/>");
3177
+ index += 1;
3178
+ continue;
3179
+ }
3180
+ if (/^>\s?/.test(trimmed)) {
3181
+ const quoteLines = [];
3182
+ while (index < lines.length) {
3183
+ const current = lines[index].trim();
3184
+ if (!current) {
3185
+ break;
3186
+ }
3187
+ if (!/^>\s?/.test(current)) {
3188
+ break;
3189
+ }
3190
+ quoteLines.push(current.replace(/^>\s?/, ""));
3191
+ index += 1;
3192
+ }
3193
+ if (quoteLines.length > 0) {
3194
+ blocks.push(`<blockquote><p>${quoteLines.map((entry) => renderInlineMarkup(entry)).join("<br/>")}</p></blockquote>`);
3195
+ }
3196
+ continue;
3197
+ }
3198
+ if (/^\s*[-*]\s+/.test(line)) {
3199
+ const items = [];
3200
+ while (index < lines.length && /^\s*[-*]\s+/.test(lines[index])) {
3201
+ items.push(lines[index].replace(/^\s*[-*]\s+/, "").trim());
3202
+ index += 1;
3203
+ }
3204
+ blocks.push(`<ul>${items.map((item) => `<li>${renderInlineMarkup(item)}</li>`).join("")}</ul>`);
3205
+ continue;
3206
+ }
3207
+ if (/^\s*\d+\.\s+/.test(line)) {
3208
+ const items = [];
3209
+ while (index < lines.length && /^\s*\d+\.\s+/.test(lines[index])) {
3210
+ items.push(lines[index].replace(/^\s*\d+\.\s+/, "").trim());
3211
+ index += 1;
3212
+ }
3213
+ blocks.push(`<ol>${items.map((item) => `<li>${renderInlineMarkup(item)}</li>`).join("")}</ol>`);
3214
+ continue;
3215
+ }
3216
+ const paragraphLines = [];
3217
+ while (index < lines.length) {
3218
+ const current = lines[index];
3219
+ if (!current.trim()) {
3220
+ break;
3221
+ }
3222
+ if (isBlockBoundaryLine(current)) {
3223
+ break;
3224
+ }
3225
+ paragraphLines.push(current.trimEnd());
3226
+ index += 1;
3227
+ }
3228
+ if (paragraphLines.length > 0) {
3229
+ blocks.push(`<p>${paragraphLines.map((entry) => renderInlineMarkup(entry)).join("<br/>")}</p>`);
3230
+ continue;
3231
+ }
3232
+ index += 1;
3233
+ }
3234
+ return blocks.join("");
3235
+ }
3236
+ function isBlockBoundaryLine(line) {
3237
+ const trimmed = line.trim();
3238
+ if (!trimmed) {
3239
+ return false;
3240
+ }
3241
+ return /^(#{1,6})\s+/.test(trimmed) || /^(?:-{3,}|\*{3,}|_{3,})$/.test(trimmed) || /^>\s?/.test(trimmed) || /^\s*[-*]\s+/.test(trimmed) || /^\s*\d+\.\s+/.test(trimmed);
3242
+ }
3243
+ function renderInlineMarkup(raw) {
3244
+ if (!raw) {
3245
+ return "";
3246
+ }
3247
+ const inlineCodeSegments = [];
3248
+ const withPlaceholders = raw.replace(/`([^`\n]+)`/g, (_match, code) => {
3249
+ const token = `@@CHCODE${inlineCodeSegments.length}@@`;
3250
+ inlineCodeSegments.push(`<code>${escapeHtml(code)}</code>`);
3251
+ return token;
3252
+ });
3253
+ let rendered = escapeHtml(withPlaceholders);
3254
+ rendered = rendered.replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/g, (_match, label, url) => {
3255
+ const safeUrl = sanitizeLinkUrl(url);
3256
+ if (!safeUrl) {
3257
+ return escapeHtml(label);
3258
+ }
3259
+ return `<a href="${escapeHtml(safeUrl)}">${escapeHtml(label)}</a>`;
3260
+ });
3261
+ rendered = rendered.replace(/\*\*([^*\n]+)\*\*/g, "<strong>$1</strong>");
3262
+ rendered = rendered.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
3263
+ rendered = rendered.replace(/(^|[^_])_([^_\n]+)_/g, "$1<em>$2</em>");
3264
+ for (let i = 0; i < inlineCodeSegments.length; i += 1) {
3265
+ rendered = rendered.replace(`@@CHCODE${i}@@`, inlineCodeSegments[i]);
3266
+ }
3165
3267
  return rendered;
3166
3268
  }
3269
+ function sanitizeLinkUrl(url) {
3270
+ try {
3271
+ const parsed = new URL(url);
3272
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
3273
+ return null;
3274
+ }
3275
+ return parsed.toString();
3276
+ } catch {
3277
+ return null;
3278
+ }
3279
+ }
3167
3280
  function escapeHtml(value) {
3168
3281
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
3169
3282
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeharbor",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Instant-messaging bridge for Codex CLI sessions",
5
5
  "license": "MIT",
6
6
  "main": "dist/cli.js",
@@ -17,6 +17,7 @@
17
17
  },
18
18
  "files": [
19
19
  "dist",
20
+ "scripts/postinstall-restart.cjs",
20
21
  ".env.example",
21
22
  "README.md",
22
23
  "LICENSE"
@@ -55,6 +56,7 @@
55
56
  "e2e:install": "playwright install chromium",
56
57
  "test:watch": "vitest test",
57
58
  "test:legacy": "bash ./scripts/run-legacy-tests.sh",
59
+ "postinstall": "node ./scripts/postinstall-restart.cjs",
58
60
  "prepare": "npm run build",
59
61
  "prepublishOnly": "npm run changelog:check && npm run typecheck && npm run lint && npm run test:coverage && npm run build",
60
62
  "changelog:check": "bash ./scripts/check-changelog.sh",
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { execFileSync } = require("node:child_process");
5
+
6
+ const LOG_PREFIX = "[codeharbor postinstall]";
7
+ const MAIN_SERVICE = "codeharbor.service";
8
+ const ADMIN_SERVICE = "codeharbor-admin.service";
9
+
10
+ function isTruthy(value) {
11
+ if (!value) {
12
+ return false;
13
+ }
14
+ const normalized = String(value).trim().toLowerCase();
15
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
16
+ }
17
+
18
+ function isGlobalInstall() {
19
+ return isTruthy(process.env.npm_config_global);
20
+ }
21
+
22
+ function hasRootPrivileges() {
23
+ if (typeof process.getuid !== "function") {
24
+ return true;
25
+ }
26
+ return process.getuid() === 0;
27
+ }
28
+
29
+ function commandExists(file) {
30
+ try {
31
+ execFileSync(file, ["--version"], { stdio: "ignore" });
32
+ return true;
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ function runCommand(file, args, options) {
39
+ return execFileSync(file, args, {
40
+ encoding: "utf8",
41
+ stdio: ["ignore", "pipe", "pipe"],
42
+ ...options,
43
+ });
44
+ }
45
+
46
+ function listUnitFiles(unitName) {
47
+ try {
48
+ return runCommand("systemctl", ["list-unit-files", unitName, "--no-legend", "--no-pager"]);
49
+ } catch {
50
+ return "";
51
+ }
52
+ }
53
+
54
+ function isUnitInstalled(unitName) {
55
+ const output = listUnitFiles(unitName).trim();
56
+ if (!output) {
57
+ return false;
58
+ }
59
+ return output.split(/\r?\n/).some((line) => line.trim().startsWith(unitName));
60
+ }
61
+
62
+ function isUnitActive(unitName) {
63
+ try {
64
+ const output = runCommand("systemctl", ["is-active", unitName], {}).trim();
65
+ return output === "active";
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ function restartUnit(unitName) {
72
+ if (hasRootPrivileges()) {
73
+ runCommand("systemctl", ["restart", unitName], {});
74
+ return;
75
+ }
76
+
77
+ runCommand("sudo", ["-n", "systemctl", "restart", unitName], {});
78
+ }
79
+
80
+ function main() {
81
+ if (isTruthy(process.env.CODEHARBOR_SKIP_POSTINSTALL_RESTART)) {
82
+ console.log(`${LOG_PREFIX} skip restart: CODEHARBOR_SKIP_POSTINSTALL_RESTART is set.`);
83
+ return;
84
+ }
85
+
86
+ const forceRestart = isTruthy(process.env.CODEHARBOR_FORCE_POSTINSTALL_RESTART);
87
+ if (!forceRestart && !isGlobalInstall()) {
88
+ return;
89
+ }
90
+
91
+ if (process.platform !== "linux") {
92
+ return;
93
+ }
94
+
95
+ if (!commandExists("systemctl")) {
96
+ return;
97
+ }
98
+
99
+ const candidates = [MAIN_SERVICE, ADMIN_SERVICE].filter((unitName) => isUnitInstalled(unitName));
100
+ const activeUnits = candidates.filter((unitName) => isUnitActive(unitName));
101
+ if (activeUnits.length === 0) {
102
+ return;
103
+ }
104
+
105
+ const restarted = [];
106
+ const failed = [];
107
+
108
+ for (const unitName of activeUnits) {
109
+ try {
110
+ restartUnit(unitName);
111
+ restarted.push(unitName);
112
+ } catch (error) {
113
+ failed.push({ unitName, error });
114
+ }
115
+ }
116
+
117
+ if (restarted.length > 0) {
118
+ console.log(`${LOG_PREFIX} restarted: ${restarted.join(", ")}`);
119
+ }
120
+ if (failed.length > 0) {
121
+ for (const failure of failed) {
122
+ const message = failure.error instanceof Error ? failure.error.message : String(failure.error);
123
+ console.warn(`${LOG_PREFIX} failed to restart ${failure.unitName}: ${message}`);
124
+ }
125
+ console.warn(`${LOG_PREFIX} run "codeharbor service restart --with-admin" manually if needed.`);
126
+ }
127
+ }
128
+
129
+ try {
130
+ main();
131
+ } catch (error) {
132
+ const message = error instanceof Error ? error.message : String(error);
133
+ console.warn(`${LOG_PREFIX} unexpected error: ${message}`);
134
+ }