@higherdev/cli 0.35.0 → 0.37.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.
@@ -22,6 +22,15 @@ function unitPath(value) {
22
22
  function unitEnvironment(name, value) {
23
23
  return `"${name}=${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%")}"`;
24
24
  }
25
+ /**
26
+ * A systemd user service inherits the supplementary groups the user manager had
27
+ * when it started, so a docker group granted later never reaches the runner.
28
+ * Starting through `sg <group>` picks the group up per membership in /etc/group.
29
+ */
30
+ export function actionsRunnerExecStart(runnerDir, viaGroup) {
31
+ const service = path.join(runnerDir, "bin/runsvc.sh");
32
+ return viaGroup ? `/usr/bin/sg ${viaGroup} -c "${unitPath(service)}"` : unitPath(service);
33
+ }
25
34
  export function actionsRunnerSystemdUnit(input) {
26
35
  return `[Unit]
27
36
  Description=HDX GitHub Actions runner${input.repo ? ` for ${input.repo}` : ""}
@@ -32,7 +41,7 @@ Wants=network-online.target
32
41
  Type=simple
33
42
  WorkingDirectory=${unitPath(input.runnerDir)}
34
43
  Environment=${unitEnvironment("PATH", input.path)}
35
- ExecStart=${unitPath(path.join(input.runnerDir, "bin/runsvc.sh"))}
44
+ ExecStart=${actionsRunnerExecStart(input.runnerDir, input.viaGroup)}
36
45
  Restart=always
37
46
  RestartSec=5
38
47
  KillMode=process
@@ -105,10 +114,13 @@ export async function installActionsRunner(options, deps = {}) {
105
114
  throw new Error(`Existing Actions runner in ${runnerDir} belongs to another host or repository.`);
106
115
  }
107
116
  }
117
+ // Jobs with service containers need the Docker socket; run through the docker group when the user has it.
118
+ const groups = (await run("id", ["-nG"]).catch(() => ({ stdout: "" }))).stdout.trim().split(/\s+/);
119
+ const viaGroup = groups.includes("docker") && await exists("/usr/bin/sg") ? "docker" : null;
108
120
  await mkdir(path.dirname(unitPath), { recursive: true });
109
- await writeFile(unitPath, actionsRunnerSystemdUnit({ runnerDir, path: pathValue, repo: options.repo }), "utf8");
121
+ await writeFile(unitPath, actionsRunnerSystemdUnit({ runnerDir, path: pathValue, repo: options.repo, viaGroup }), "utf8");
110
122
  await run("systemctl", ["--user", "daemon-reload"]);
111
123
  await run("systemctl", ["--user", "enable", "--now", unitName]);
112
124
  return [`GitHub Actions runner ${options.host} registered to ${options.repo}.`,
113
- `labels: self-hosted, hdx, ${options.host}`, `systemd unit: ${unitPath}`];
125
+ `labels: self-hosted, hdx, ${options.host}`, `systemd unit: ${unitPath}${viaGroup ? ` (via sg ${viaGroup})` : ""}`];
114
126
  }
@@ -97,6 +97,20 @@ export function parseTuiAttach(text) {
97
97
  const paths = key ? words.slice(0, -1) : words;
98
98
  return paths.length === 1 ? { path: paths[0], ...(key ? { ticketKey: key } : {}) } : null;
99
99
  }
100
+ /**
101
+ * Text that a Finder drop produces: one or more absolute paths, plain, quoted, or with
102
+ * escaped spaces. Terminal.app inserts a drop as typed text rather than a bracketed
103
+ * paste, so the input component and the Enter handler both check this before treating
104
+ * a line starting with "/" as a slash command.
105
+ */
106
+ export function looksLikeDroppedPaths(text) {
107
+ const trimmed = text.trim();
108
+ if (!/^(?:\/|'\/|"\/)/.test(trimmed))
109
+ return false;
110
+ // A slash command is one word without a second "/" (e.g. /architect, /ticket HD-1).
111
+ const firstWord = trimmed.split(/\s+/)[0].replace(/^['"]/, "");
112
+ return firstWord.slice(1).includes("/") || /^['"]/.test(trimmed);
113
+ }
100
114
  export async function detectDroppedPaths(pasted, isFile = async (path) => (await stat(path)).isFile()) {
101
115
  const text = pasted.trim();
102
116
  if (!text)
package/dist/tui/App.js CHANGED
@@ -33,7 +33,7 @@ import { editFor, editableKeys, nextValue, seedFor, settingsRows } from "./setti
33
33
  import { appendLines, runLabels, toStreamLines } from "./stream.js";
34
34
  import { UI } from "./theme.js";
35
35
  import { WorkspaceLoads } from "./workspace-load.js";
36
- import { attachmentChip, detectDroppedPaths, uploadAttachment } from "../attachments.js";
36
+ import { attachmentChip, detectDroppedPaths, looksLikeDroppedPaths, uploadAttachment } from "../attachments.js";
37
37
  let messageSeq = 0;
38
38
  const nextId = () => `m${messageSeq++}`;
39
39
  const tuiPrompt = (question) => promptOnStdin(question, true);
@@ -457,7 +457,7 @@ export function App({ initial, availableUpdate: initialUpdate = null, updateDeps
457
457
  })();
458
458
  }, [agentName, config, pendingAttachments, setThread, workspace.id]);
459
459
  const receiveDrop = useCallback((pasted) => {
460
- if (!/^(?:\/|'\/|"\/)/.test(pasted.trim()))
460
+ if (!looksLikeDroppedPaths(pasted))
461
461
  return false;
462
462
  void (async () => {
463
463
  const paths = await detectDroppedPaths(pasted);
@@ -520,6 +520,12 @@ export function App({ initial, availableUpdate: initialUpdate = null, updateDeps
520
520
  }, [view, ticketKey, visibleStory?.latest_headline, visibleStory?.timeline, config]);
521
521
  const run = useCallback(async (raw) => {
522
522
  const text = raw.trim();
523
+ // A dropped path that reached the line as typed text is an attachment, not a command.
524
+ if (looksLikeDroppedPaths(text) && (await detectDroppedPaths(text)).length) {
525
+ setDraft("");
526
+ receiveDrop(text);
527
+ return;
528
+ }
523
529
  if (view === "settings") {
524
530
  const open = editingRef.current;
525
531
  if (open) {
@@ -73,6 +73,11 @@ export default function TextInput({ value, onChange, onSubmit, onCancel, onUp, o
73
73
  // most of them. Ink reports that as ordinary input with key.return
74
74
  // false, so without this the Enter is filtered out with the other
75
75
  // control bytes and the line just sits at the prompt unsent.
76
+ // A Finder drop in Terminal.app arrives as one multi-character chunk of
77
+ // typed text, not a bracketed paste, so it is offered to the paste
78
+ // handler first.
79
+ if (input.length > 1 && onPasteText?.(input))
80
+ return;
76
81
  if (input.length > 1 && /[\r\n]/.test(input)) {
77
82
  const parts = input.split(/\r\n|\r|\n/);
78
83
  const submits = parts.length === 2 && parts[1] === "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@higherdev/cli",
3
- "version": "0.35.0",
3
+ "version": "0.37.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",