@drisp/cli 0.6.6 → 0.6.7

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 CHANGED
@@ -2,12 +2,11 @@
2
2
  import "./chunk-HXBCZAP7.js";
3
3
  import {
4
4
  CREDENTIAL_SOURCES_TRIED,
5
+ ExecutionStore,
5
6
  HARNESS_PROCESS_PRESETS,
6
7
  IndexedTimeline,
7
8
  PORTABLE_PROVIDER_ENV_VARS,
8
9
  RUN_EXIT_CODE,
9
- RunnerStartupError,
10
- alreadyRunningMessage,
11
10
  attachRuntimeEventLoop,
12
11
  bootstrapRuntimeConfig,
13
12
  buildApiKeyHelperSettings,
@@ -18,19 +17,22 @@ import {
18
17
  classifyFailure,
19
18
  compactText,
20
19
  computeInputRows,
21
- createDashboardDecisionInbox,
20
+ connectAlreadyConnectedSchema,
21
+ connectRedeemResponseSchema,
22
+ connectStartResponseSchema,
22
23
  createFeedMapper,
23
- createPairedFeedPublisher,
24
+ createLocalFeedSink,
24
25
  createRuntime,
25
26
  createSessionStore,
26
27
  credentialHelperKey,
27
28
  cursorToVisualPosition,
28
- dashboardClientConfigPath,
29
29
  detectClaudeVersion,
30
+ expandHome,
30
31
  extractFriendlyServerName,
31
32
  extractPermissionSnapshot,
32
33
  fit,
33
34
  fitAnsi,
35
+ forgetRunner,
34
36
  formatClock,
35
37
  formatProbeCommand,
36
38
  formatRunLabel,
@@ -45,6 +47,8 @@ import {
45
47
  getMostRecentAthenaSession,
46
48
  getSessionMeta,
47
49
  hintGlyphs,
50
+ hubHttpUrl,
51
+ identityHubOrigin,
48
52
  isBashToolResponse,
49
53
  isCommandPrefix,
50
54
  isScopedPermissionsRequest,
@@ -55,26 +59,22 @@ import {
55
59
  localAnswerDecision,
56
60
  lookupAllCredentials,
57
61
  lookupCredential,
62
+ machineInstallationId,
58
63
  makeSkippedProbe,
59
64
  mergeFeedItems,
60
65
  messageGlyphs,
61
- normalizeDashboardUrl,
66
+ overriddenIdentityPath,
62
67
  parseFrontmatter,
63
68
  parseToolName,
69
+ platformRunnerServices,
64
70
  probeSkipReason,
65
71
  processRegistry,
66
72
  progressGlyphs,
67
- readAttachmentMirror,
68
- readDashboardClientConfig,
69
- readPidLock,
70
- readRunnerStatusFile,
71
- refreshDashboardAccessToken,
72
73
  register,
73
74
  registerCleanupOnExit,
74
75
  releaseMcpAsset,
75
- removeAttachmentMirror,
76
- removeDashboardClientConfig,
77
76
  renderInputLines,
77
+ require_string_width,
78
78
  resolveClaudeBinary,
79
79
  resolveClaudeSettingsSurfacePaths,
80
80
  resolveEffectiveCapabilities,
@@ -84,28 +84,32 @@ import {
84
84
  resolveRuntimeAuthOverlay,
85
85
  resolveToolColumn,
86
86
  runClaudeAuthStatus,
87
+ runConfiguredRunner,
87
88
  runExec,
89
+ runMachineRunners,
88
90
  runProbe,
89
- runnerStatePaths,
91
+ runnerConfigDirectory,
92
+ runnerConfigSchema,
93
+ runnerDisconnectResponseSchema,
94
+ runnerIdentities,
95
+ runnerServiceUnit,
96
+ runnerSoftwareVersion,
97
+ serviceInstallationId,
90
98
  sessionsDir,
91
99
  sliceAnsi,
92
100
  spaces,
93
101
  startDashboardDecisionDrain,
94
- startRunnerProcess,
95
102
  startWorkflowExecution,
96
- stringWidth,
97
- stripAnsi,
98
103
  supportsSessionApproval,
104
+ teamIdentityPath,
99
105
  todoGlyphSet,
100
106
  visualPositionToOffset,
101
- waitForShutdownSignal,
102
107
  wrapText,
103
- writeAttachmentMirror,
104
- writeDashboardClientConfig
105
- } from "./chunk-QGAGV2OI.js";
108
+ writeIdentityOnce
109
+ } from "./chunk-7SNGX25Q.js";
106
110
  import {
107
111
  generateId as generateId2
108
- } from "./chunk-USK5VSQ7.js";
112
+ } from "./chunk-PQIB66SG.js";
109
113
  import {
110
114
  McpOptionsStep,
111
115
  StepSelector,
@@ -144,17 +148,19 @@ import {
144
148
  wakesFreshAfterHandover,
145
149
  writeGlobalConfig,
146
150
  writeProjectConfig
147
- } from "./chunk-7FYAABQ4.js";
148
- import "./chunk-PZ5AY32C.js";
151
+ } from "./chunk-CIPO2P2Q.js";
152
+ import {
153
+ __toESM
154
+ } from "./chunk-G3PMV62Z.js";
149
155
 
150
156
  // src/app/entry/cli.tsx
151
157
  import { render } from "ink";
152
158
  import meow from "meow";
153
- import fs8 from "fs";
154
- import { createRequire as createRequire2 } from "module";
155
- import os7 from "os";
156
- import path8 from "path";
157
- import { fileURLToPath as fileURLToPath2 } from "url";
159
+ import fs6 from "fs";
160
+ import { createRequire } from "module";
161
+ import os5 from "os";
162
+ import path6 from "path";
163
+ import { fileURLToPath } from "url";
158
164
 
159
165
  // src/app/shell/sessionUiState.ts
160
166
  var DEFAULT_STATIC_FLOOR = 0;
@@ -2175,7 +2181,7 @@ function useFeed(runtime, messages = [], initialAllowedTools, sessionStore, opti
2175
2181
  const feedEventsRef = useRef([]);
2176
2182
  const notifiedRuntimeErrorRef = useRef(null);
2177
2183
  const dashboardFeedPublisher = useMemo3(
2178
- () => options?.dashboardFeedPublisher ?? createPairedFeedPublisher(),
2184
+ () => options?.dashboardFeedPublisher ?? createLocalFeedSink(),
2179
2185
  [options?.dashboardFeedPublisher]
2180
2186
  );
2181
2187
  const ownsFeedPublisher = !options?.dashboardFeedPublisher;
@@ -2214,10 +2220,7 @@ function useFeed(runtime, messages = [], initialAllowedTools, sessionStore, opti
2214
2220
  useEffect(() => {
2215
2221
  const athenaSessionId = resolveAthenaSessionId();
2216
2222
  if (!athenaSessionId) return;
2217
- if (!dashboardDecisionInboxRef.current) {
2218
- if (!readDashboardClientConfig()) return;
2219
- dashboardDecisionInboxRef.current = createDashboardDecisionInbox();
2220
- }
2223
+ if (!dashboardDecisionInboxRef.current) return;
2221
2224
  const inbox = dashboardDecisionInboxRef.current;
2222
2225
  const drain = startDashboardDecisionDrain({
2223
2226
  runtime,
@@ -2749,7 +2752,7 @@ function useWorkflowSessionController(base, input) {
2749
2752
  }
2750
2753
  setIsRunning(false);
2751
2754
  }, [base, cancelCurrentRun]);
2752
- const spawn2 = useCallback6(
2755
+ const spawn = useCallback6(
2753
2756
  async (prompt, continuation, _configOverride) => {
2754
2757
  await cancelCurrentRun();
2755
2758
  const latest = input.store?.getLatestRun();
@@ -2821,7 +2824,7 @@ function useWorkflowSessionController(base, input) {
2821
2824
  }, []);
2822
2825
  return {
2823
2826
  ...base,
2824
- startTurn: spawn2,
2827
+ startTurn: spawn,
2825
2828
  isRunning,
2826
2829
  interrupt,
2827
2830
  kill,
@@ -3014,7 +3017,7 @@ import React2, {
3014
3017
  } from "react";
3015
3018
  import { Box as Box8, Text as Text11, useInput as useInput7 } from "ink";
3016
3019
 
3017
- // node_modules/chalk/source/vendor/ansi-styles/index.js
3020
+ // ../../node_modules/chalk/source/vendor/ansi-styles/index.js
3018
3021
  var ANSI_BACKGROUND_OFFSET = 10;
3019
3022
  var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
3020
3023
  var wrapAnsi256 = (offset = 0) => (code) => `\x1B[${38 + offset};5;${code}m`;
@@ -3200,7 +3203,7 @@ function assembleStyles() {
3200
3203
  var ansiStyles = assembleStyles();
3201
3204
  var ansi_styles_default = ansiStyles;
3202
3205
 
3203
- // node_modules/chalk/source/vendor/supports-color/index.js
3206
+ // ../../node_modules/chalk/source/vendor/supports-color/index.js
3204
3207
  import process3 from "process";
3205
3208
  import os from "os";
3206
3209
  import tty from "tty";
@@ -3332,7 +3335,7 @@ var supportsColor = {
3332
3335
  };
3333
3336
  var supports_color_default = supportsColor;
3334
3337
 
3335
- // node_modules/chalk/source/utilities.js
3338
+ // ../../node_modules/chalk/source/utilities.js
3336
3339
  function stringReplaceAll(string, substring, replacer) {
3337
3340
  let index = string.indexOf(substring);
3338
3341
  if (index === -1) {
@@ -3362,7 +3365,7 @@ function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
3362
3365
  return returnValue;
3363
3366
  }
3364
3367
 
3365
- // node_modules/chalk/source/index.js
3368
+ // ../../node_modules/chalk/source/index.js
3366
3369
  var { stdout: stdoutColor, stderr: stderrColor } = supports_color_default;
3367
3370
  var GENERATOR = /* @__PURE__ */ Symbol("GENERATOR");
3368
3371
  var STYLER = /* @__PURE__ */ Symbol("STYLER");
@@ -4968,6 +4971,27 @@ import process5 from "process";
4968
4971
  import { useState as useState11, useCallback as useCallback12, useRef as useRef12, useEffect as useEffect10 } from "react";
4969
4972
  import { useInput as useInput11 } from "ink";
4970
4973
 
4974
+ // ../../node_modules/ansi-regex/index.js
4975
+ function ansiRegex({ onlyFirst = false } = {}) {
4976
+ const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)";
4977
+ const osc = `(?:(?:\\u001B\\]|\\u009D)[^\\u0007\\u001B\\u009C\\u009D]*${ST})`;
4978
+ const csi = "[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]";
4979
+ const pattern = `${osc}|${csi}`;
4980
+ return new RegExp(pattern, onlyFirst ? void 0 : "g");
4981
+ }
4982
+
4983
+ // ../../node_modules/strip-ansi/index.js
4984
+ var regex = ansiRegex();
4985
+ function stripAnsi(string) {
4986
+ if (typeof string !== "string") {
4987
+ throw new TypeError(`Expected a \`string\`, got \`${typeof string}\``);
4988
+ }
4989
+ if (!string.includes("\x1B") && !string.includes("\x9B")) {
4990
+ return string;
4991
+ }
4992
+ return string.replace(regex, "");
4993
+ }
4994
+
4971
4995
  // src/shared/utils/clipboard.ts
4972
4996
  import { execSync } from "child_process";
4973
4997
  function copyToClipboard(text) {
@@ -5254,14 +5278,14 @@ function extractWebSearch(_input, response) {
5254
5278
  return { type: "text", content: extractTextContent(response), maxLines: 10 };
5255
5279
  }
5256
5280
  function extractNotebookEdit(input, _response) {
5257
- const path9 = typeof input["notebook_path"] === "string" ? input["notebook_path"] : "";
5281
+ const path7 = typeof input["notebook_path"] === "string" ? input["notebook_path"] : "";
5258
5282
  const mode = typeof input["edit_mode"] === "string" ? input["edit_mode"] : "replace";
5259
5283
  const source = typeof input["new_source"] === "string" ? input["new_source"] : "";
5260
- if (!source) return { type: "text", content: `${mode} cell in ${path9}` };
5284
+ if (!source) return { type: "text", content: `${mode} cell in ${path7}` };
5261
5285
  return {
5262
5286
  type: "code",
5263
5287
  content: source,
5264
- language: detectLanguage(path9),
5288
+ language: detectLanguage(path7),
5265
5289
  maxLines: 10
5266
5290
  };
5267
5291
  }
@@ -5355,6 +5379,7 @@ import { highlight } from "cli-highlight";
5355
5379
  // src/shared/markdown/renderMarkdown.ts
5356
5380
  import { Marked } from "marked";
5357
5381
  import { markedTerminal } from "marked-terminal";
5382
+ var import_string_width = __toESM(require_string_width(), 1);
5358
5383
  import Table from "cli-table3";
5359
5384
 
5360
5385
  // src/shared/utils/hyperlink.ts
@@ -5558,11 +5583,11 @@ function inferContinuationPrefix(line) {
5558
5583
  /^(\s*(?:(?:[•*-]|\d+\.)\s+)?(?:\[[ xX]\]\s+)?[^:\n]{1,40}:\s+)/
5559
5584
  );
5560
5585
  if (fieldMatch) {
5561
- return " ".repeat(stringWidth(fieldMatch[1]));
5586
+ return " ".repeat((0, import_string_width.default)(fieldMatch[1]));
5562
5587
  }
5563
5588
  const listMatch = line.match(/^(\s*(?:[•*-]|\d+\.)\s+(?:\[[ xX]\]\s+)?)/);
5564
5589
  if (listMatch) {
5565
- return " ".repeat(stringWidth(listMatch[1]));
5590
+ return " ".repeat((0, import_string_width.default)(listMatch[1]));
5566
5591
  }
5567
5592
  const indentMatch = line.match(/^(\s+)/);
5568
5593
  return indentMatch?.[1] ?? "";
@@ -5570,15 +5595,15 @@ function inferContinuationPrefix(line) {
5570
5595
  function wrapAnsiLine(line, maxWidth) {
5571
5596
  if (maxWidth <= 0) return [""];
5572
5597
  if (line.length === 0) return [""];
5573
- if (stringWidth(line) <= maxWidth) return [line];
5598
+ if ((0, import_string_width.default)(line) <= maxWidth) return [line];
5574
5599
  const chunks = [];
5575
5600
  let remaining = line;
5576
5601
  let firstLine = true;
5577
5602
  const continuationPrefix = inferContinuationPrefix(line);
5578
- const continuationWidth = stringWidth(continuationPrefix);
5603
+ const continuationWidth = (0, import_string_width.default)(continuationPrefix);
5579
5604
  while (remaining.length > 0) {
5580
5605
  const availableWidth = firstLine ? maxWidth : Math.max(1, maxWidth - continuationWidth);
5581
- if (stringWidth(remaining) <= availableWidth) {
5606
+ if ((0, import_string_width.default)(remaining) <= availableWidth) {
5582
5607
  chunks.push(firstLine ? remaining : continuationPrefix + remaining);
5583
5608
  break;
5584
5609
  }
@@ -5589,7 +5614,7 @@ function wrapAnsiLine(line, maxWidth) {
5589
5614
  const wrapped = chunk.slice(0, breakAt);
5590
5615
  if (wrapped.trim().length > 0) {
5591
5616
  chunk = wrapped;
5592
- consumedWidth = Math.min(availableWidth, stringWidth(wrapped) + 1);
5617
+ consumedWidth = Math.min(availableWidth, (0, import_string_width.default)(wrapped) + 1);
5593
5618
  }
5594
5619
  }
5595
5620
  chunks.push(firstLine ? chunk : continuationPrefix + chunk);
@@ -5658,13 +5683,14 @@ function renderMarkdown(options) {
5658
5683
  }
5659
5684
 
5660
5685
  // src/ui/layout/renderDetailLines.ts
5686
+ var import_string_width2 = __toESM(require_string_width(), 1);
5661
5687
  var MAX_HIGHLIGHT_SIZE = 5e4;
5662
5688
  function wrapAnsiLine2(line, maxWidth) {
5663
5689
  if (maxWidth <= 0) return [""];
5664
5690
  if (line.length === 0) return [""];
5665
- if (stringWidth(line) <= maxWidth) return [line];
5691
+ if ((0, import_string_width2.default)(line) <= maxWidth) return [line];
5666
5692
  const chunks = [];
5667
- const visualWidth = stringWidth(line);
5693
+ const visualWidth = (0, import_string_width2.default)(line);
5668
5694
  for (let start = 0; start < visualWidth; start += maxWidth) {
5669
5695
  chunks.push(sliceAnsi(line, start, start + maxWidth));
5670
5696
  }
@@ -5785,10 +5811,10 @@ function extractToolSubject(toolName, toolInput) {
5785
5811
  const parts = [];
5786
5812
  const pattern = str(input, "pattern");
5787
5813
  const glob = str(input, "glob");
5788
- const path9 = str(input, "path");
5814
+ const path7 = str(input, "path");
5789
5815
  if (pattern) parts.push(`"${pattern}"`);
5790
5816
  if (glob) parts.push(glob);
5791
- if (path9) parts.push(`in ${path9}`);
5817
+ if (path7) parts.push(`in ${path7}`);
5792
5818
  return parts.length > 0 ? parts.join(" ") : void 0;
5793
5819
  }
5794
5820
  case "Skill":
@@ -8185,6 +8211,9 @@ function buildHeaderModel(input) {
8185
8211
  };
8186
8212
  }
8187
8213
 
8214
+ // src/ui/header/renderLines.ts
8215
+ var import_string_width3 = __toESM(require_string_width(), 1);
8216
+
8188
8217
  // src/ui/header/contextBar.ts
8189
8218
  function formatTokenCount(value) {
8190
8219
  if (value === null) return "--";
@@ -8411,7 +8440,7 @@ function renderHeaderLines(model, width, hasColor, theme) {
8411
8440
  };
8412
8441
  const currentLeft = [...leftTokens];
8413
8442
  const totalTarget = Math.max(1, width - 1);
8414
- while (currentLeft.length > 1 && stringWidth(buildLine(currentLeft)) + MIN_GAP + stringWidth(ctxText) > totalTarget) {
8443
+ while (currentLeft.length > 1 && (0, import_string_width3.default)(buildLine(currentLeft)) + MIN_GAP + (0, import_string_width3.default)(ctxText) > totalTarget) {
8415
8444
  let minIdx = 1;
8416
8445
  let minPri = currentLeft[1].priority;
8417
8446
  for (let i = 2; i < currentLeft.length; i++) {
@@ -8423,10 +8452,10 @@ function renderHeaderLines(model, width, hasColor, theme) {
8423
8452
  currentLeft.splice(minIdx, 1);
8424
8453
  }
8425
8454
  const left = buildLine(currentLeft);
8426
- const usedWidth = stringWidth(left) + stringWidth(ctxText);
8455
+ const usedWidth = (0, import_string_width3.default)(left) + (0, import_string_width3.default)(ctxText);
8427
8456
  const gapWidth = Math.max(MIN_GAP, totalTarget - usedWidth);
8428
8457
  const line = `${left}${" ".repeat(gapWidth)}${ctxText}`;
8429
- const lineWidth = stringWidth(line);
8458
+ const lineWidth = (0, import_string_width3.default)(line);
8430
8459
  const padded = lineWidth < totalTarget ? line + " ".repeat(totalTarget - lineWidth) : line;
8431
8460
  return [padded];
8432
8461
  }
@@ -12018,12 +12047,12 @@ function shouldShowSetup({
12018
12047
  }
12019
12048
 
12020
12049
  // src/app/entry/execCommand.ts
12021
- import crypto2 from "crypto";
12050
+ import crypto3 from "crypto";
12022
12051
 
12023
12052
  // src/app/entry/resumeResolution.ts
12024
- import crypto from "crypto";
12053
+ import crypto2 from "crypto";
12025
12054
  function resolveResumeTarget(input) {
12026
- const createSessionId = input.createSessionId ?? crypto.randomUUID;
12055
+ const createSessionId = input.createSessionId ?? crypto2.randomUUID;
12027
12056
  const getSessionMetaFn = input.getSessionMetaFn ?? getSessionMeta;
12028
12057
  const getMostRecentSessionFn = input.getMostRecentSessionFn ?? getMostRecentAthenaSession;
12029
12058
  const getLatestRunFn = input.getLatestRunFn ?? getLatestRunForSession;
@@ -12085,7 +12114,7 @@ function continueFlagToRequest(continueFlag) {
12085
12114
  }
12086
12115
  async function runExecCommand(input, deps = {}) {
12087
12116
  const logError = deps.logError ?? console.error;
12088
- const createSessionId = deps.createSessionId ?? crypto2.randomUUID;
12117
+ const createSessionId = deps.createSessionId ?? crypto3.randomUUID;
12089
12118
  const now = deps.now ?? Date.now;
12090
12119
  const runExecFn = deps.runExecFn ?? runExec;
12091
12120
  const getMostRecentSessionFn = deps.getMostRecentSessionFn ?? getMostRecentAthenaSession;
@@ -12970,1564 +12999,518 @@ Default: ${DEFAULT_MARKETPLACE_SLUG3}`
12970
12999
  }
12971
13000
  }
12972
13001
 
12973
- // src/app/entry/runnerCommand.ts
12974
- import crypto3 from "crypto";
12975
- import { spawn } from "child_process";
12976
- import fs6 from "fs";
12977
- import { createRequire } from "module";
12978
- import os5 from "os";
12979
- import path6 from "path";
12980
- import { fileURLToPath } from "url";
13002
+ // src/app/entry/runnerCommandV2.ts
13003
+ import { existsSync } from "fs";
13004
+ import { join as join2 } from "path";
12981
13005
 
12982
- // src/infra/daemon/serviceUnit.ts
12983
- import fs5 from "fs";
12984
- import os4 from "os";
12985
- import path5 from "path";
12986
- function installServiceUnit(options) {
12987
- const platform = options.platform ?? process.platform;
12988
- const env2 = options.env ?? process.env;
12989
- const home = env2["HOME"] ?? os4.homedir();
12990
- if (platform === "darwin") {
12991
- const target = options.targetPath ?? path5.join(home, "Library", "LaunchAgents", "ai.drisp.daemon.plist");
12992
- const paths = runnerStatePaths(env2);
12993
- const plist = renderLaunchdPlist({
12994
- label: "ai.drisp.daemon",
12995
- nodeBinary: options.nodeBinary,
12996
- daemonEntry: options.runnerEntry,
12997
- workingDirectory: home,
12998
- stdoutPath: paths.logPath,
12999
- stderrPath: paths.logPath
13000
- });
13001
- writeIfChanged(target, plist);
13002
- return {
13003
- ok: true,
13004
- platform: "darwin",
13005
- path: target,
13006
- loadCommand: `launchctl load -w ${target}`,
13007
- startCommand: "launchctl start ai.drisp.daemon"
13008
- };
13009
- }
13010
- if (platform === "linux") {
13011
- const target = options.targetPath ?? path5.join(home, ".config", "systemd", "user", "drisp-daemon.service");
13012
- const unit = renderSystemdUnit({
13013
- description: "Drisp runner",
13014
- nodeBinary: options.nodeBinary,
13015
- daemonEntry: options.runnerEntry
13016
- });
13017
- writeIfChanged(target, unit);
13018
- return {
13019
- ok: true,
13020
- platform: "linux",
13021
- path: target,
13022
- loadCommand: "systemctl --user daemon-reload",
13023
- startCommand: "systemctl --user enable --now drisp-daemon.service"
13024
- };
13025
- }
13026
- return {
13027
- ok: false,
13028
- platform: "unsupported",
13029
- message: `service install not supported on ${platform}`
13030
- };
13031
- }
13032
- function writeIfChanged(target, content) {
13033
- const dir = path5.dirname(target);
13034
- fs5.mkdirSync(dir, { recursive: true, mode: 448 });
13035
- let existing = null;
13036
- try {
13037
- existing = fs5.readFileSync(target, "utf-8");
13038
- } catch (err) {
13039
- if (err.code !== "ENOENT") throw err;
13040
- }
13041
- if (existing === content) return;
13042
- fs5.writeFileSync(target, content, { mode: 384 });
13043
- }
13044
- function renderLaunchdPlist(input) {
13045
- const argv = [input.nodeBinary, input.daemonEntry].map((s) => ` <string>${escapeXml(s)}</string>`).join("\n");
13046
- return `<?xml version="1.0" encoding="UTF-8"?>
13047
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
13048
- <plist version="1.0">
13049
- <dict>
13050
- <key>Label</key>
13051
- <string>${escapeXml(input.label)}</string>
13052
- <key>ProgramArguments</key>
13053
- <array>
13054
- ${argv}
13055
- </array>
13056
- <key>RunAtLoad</key>
13057
- <true/>
13058
- <key>KeepAlive</key>
13059
- <dict>
13060
- <key>SuccessfulExit</key>
13061
- <false/>
13062
- </dict>
13063
- <key>WorkingDirectory</key>
13064
- <string>${escapeXml(input.workingDirectory)}</string>
13065
- <key>StandardOutPath</key>
13066
- <string>${escapeXml(input.stdoutPath)}</string>
13067
- <key>StandardErrorPath</key>
13068
- <string>${escapeXml(input.stderrPath)}</string>
13069
- <key>ProcessType</key>
13070
- <string>Background</string>
13071
- </dict>
13072
- </plist>
13073
- `;
13074
- }
13075
- function renderSystemdUnit(input) {
13076
- return `[Unit]
13077
- Description=${input.description}
13078
- After=network-online.target
13079
-
13080
- [Service]
13081
- Type=simple
13082
- ExecStart=${input.nodeBinary} ${input.daemonEntry}
13083
- Restart=always
13084
- RestartSec=5
13085
- SuccessExitStatus=0
13086
- StandardOutput=journal
13087
- StandardError=journal
13088
-
13089
- [Install]
13090
- WantedBy=default.target
13091
- `;
13092
- }
13093
- function escapeXml(input) {
13094
- return input.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
13095
- }
13096
-
13097
- // src/app/entry/runnerCommand.ts
13098
- var USAGE3 = `Usage: drisp runner [subcommand] [options]
13099
-
13100
- The runner is the one long-lived process that pairs this machine with the
13101
- hub, receives its Runs over the instance socket, and executes them. Its
13102
- state lives in ~/.local/state/drisp/ (runner.pid, runner.db, runner.log,
13103
- runner.status.json).
13104
-
13105
- Subcommands:
13106
- (none) Run the runner in the foreground. Exits non-zero with
13107
- "already running (pid N)" when another runner holds the pid file.
13108
- --detach Start the runner in the background and verify it reaches the
13109
- hub's socket.
13110
- stop Stop the background runner (SIGTERM to the pid in runner.pid).
13111
- restart Stop, then start detached.
13112
- status Show pairing, the runner process (from runner.pid and
13113
- runner.status.json), socket health, and token freshness.
13114
- Non-zero on any unhealthy axis. --runner <id> also checks the
13115
- hub-side runner binding.
13116
- logs [--tail N] [--follow]
13117
- Tail the runner log at ~/.local/state/drisp/runner.log.
13118
- runs [--active] [--limit N]
13119
- List runs the runner has handled (last 100, from the status file).
13120
- install Generate a launchd plist (macOS) or systemd user unit (linux)
13121
- so the runner starts automatically on login.
13122
- pair <token> --url <dashboard-origin> [--name <machine-name>]
13123
- Pair this machine with the hub and start the runner.
13124
- unpair Stop the runner, revoke the refresh token, and remove the
13125
- local config.
13126
- refresh Mint a short-lived access token (rotates the refresh token).
13127
- doctor Verify pairing health. With --runner <id>, also confirms the
13128
- hub-side runner is bound to this instance.
13129
- list Print the local attachment mirror (the hub-side runners bound
13130
- to this instance at the last pair/refresh).
13131
-
13132
- Options:
13133
- --detach Run in the background (no subcommand)
13134
- --url <origin> Dashboard origin (required for pair)
13135
- --runner <id> Hub-side runner id (optional for "doctor" / "status")
13136
- --name <name> Friendly machine name (optional, defaults to hostname)
13137
- --tail N Number of trailing log lines (default 20)
13138
- --follow Stream new log lines until interrupted
13139
- --active Show only active runs
13140
- --limit N Cap the number of runs returned
13141
- --json Emit machine-readable JSON output
13142
-
13143
- "drisp dashboard <subcommand>" is a deprecated alias of this command and is
13144
- removed in 0.7.0.
13145
- `;
13146
- var require_ = createRequire(import.meta.url);
13147
- var cachedVersion = null;
13148
- function readPackageVersion() {
13149
- if (cachedVersion !== null) return cachedVersion;
13006
+ // ../../modules/runtime/src/connect.ts
13007
+ import { readFile, rename } from "fs/promises";
13008
+ import { hostname, platform, release } from "os";
13009
+ import { setTimeout as sleep } from "timers/promises";
13010
+ var LOOPBACK = ["localhost", "127.0.0.1", "[::1]"];
13011
+ function hubOrigin(value) {
13012
+ let url;
13150
13013
  try {
13151
- const injected = "0.6.6";
13152
- if (typeof injected === "string" && injected.length > 0) {
13153
- cachedVersion = injected;
13154
- return cachedVersion;
13155
- }
13014
+ url = new URL(value);
13156
13015
  } catch {
13016
+ throw new Error(`${value} is not a Hub URL.`);
13017
+ }
13018
+ if (url.username || url.password || url.search || url.hash)
13019
+ throw new Error("The Hub URL must not carry credentials, a query or a fragment.");
13020
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && LOOPBACK.includes(url.hostname)))
13021
+ throw new Error("The Hub URL must use https (http is allowed only on this machine).");
13022
+ return new URL(url.origin);
13023
+ }
13024
+ var refusals = {
13025
+ connect_code_invalid: "This Connect code is unknown, already used or expired. Get a new one in Settings \u2192 Runners.",
13026
+ rate_limited: "Too many Connect attempts from this address. Wait ten minutes and try again.",
13027
+ unauthorized: "The member who created this code is no longer in the Team."
13028
+ };
13029
+ var requestRefusals = {
13030
+ authorization_expired: "The code expired before anyone approved it. Run drisp runner connect again for a new one.",
13031
+ access_denied: "The request to connect this machine was denied.",
13032
+ rate_limited: "Too many Connect requests from this address. Wait a few minutes and try again.",
13033
+ unauthorized: "The member who approved this machine is no longer in the Team."
13034
+ };
13035
+ var thisMachine = (installationId) => ({
13036
+ installationId,
13037
+ hostname: hostname().slice(0, 255) || "machine",
13038
+ os: `${platform()} ${release()}`.slice(0, 64),
13039
+ softwareVersion: runnerSoftwareVersion
13040
+ });
13041
+ var post = (origin, path7, body) => fetch(new URL(path7, origin), {
13042
+ method: "POST",
13043
+ headers: { "Content-Type": "application/json" },
13044
+ body: JSON.stringify(body)
13045
+ });
13046
+ async function connectTarget(origin, configPath, directory) {
13047
+ const single = configPath === void 0 ? void 0 : expandHome(configPath);
13048
+ if (single !== void 0) await refuseExisting(single);
13049
+ const presented = single === void 0 ? await machineInstallationId(directory) : crypto.randomUUID();
13050
+ const held = single === void 0 ? await runnerIdentities(directory) : [];
13051
+ const forTeam = (orgId) => held.filter(({ config }) => config.orgId === orgId && identityHubOrigin(config) === origin.origin);
13052
+ const ownIdentity = ({ orgId, runnerId }) => forTeam(orgId).find(({ config }) => config.runnerId === runnerId);
13053
+ return { single, presented, forTeam, ownIdentity };
13054
+ }
13055
+ async function alreadyConnected(existing, { forTeam, ownIdentity }) {
13056
+ const own = ownIdentity(existing);
13057
+ const problem = own ? await credentialAccepted(own.config) ? void 0 : "lapsed" : forTeam(existing.orgId).length ? "other_runner" : "missing";
13058
+ const runner = { orgId: existing.orgId, runnerId: existing.runnerId, name: existing.name };
13059
+ if (own && problem === void 0) return { status: "already_connected", ...runner, path: own.path };
13060
+ const path7 = (own ?? forTeam(existing.orgId)[0])?.path;
13061
+ return { status: "identity_unusable", ...runner, problem, ...path7 ? { path: path7 } : {} };
13062
+ }
13063
+ async function connectWithCode({
13064
+ code,
13065
+ hub,
13066
+ replace = false,
13067
+ configPath = overriddenIdentityPath(),
13068
+ directory = runnerConfigDirectory()
13069
+ }) {
13070
+ const origin = hubOrigin(hub);
13071
+ const target = await connectTarget(origin, configPath, directory);
13072
+ const { presented, forTeam, ownIdentity } = target;
13073
+ let redemption = await redeem(origin, code, presented);
13074
+ if (redemption.status === "already_connected" && !ownIdentity(redemption.existing)) {
13075
+ const replacedEarlier = forTeam(redemption.existing.orgId).find(
13076
+ ({ config }) => config.installationId !== presented
13077
+ );
13078
+ if (replacedEarlier) redemption = await redeem(origin, code, replacedEarlier.config.installationId);
13079
+ }
13080
+ if (redemption.status === "already_connected") {
13081
+ const outcome = await alreadyConnected(redemption.existing, target);
13082
+ if (outcome.status === "already_connected" || !replace) return outcome;
13083
+ redemption = await redeem(origin, code, crypto.randomUUID());
13084
+ if (redemption.status === "already_connected")
13085
+ throw new Error("The Hub refused a new installation identity as already connected; try again.");
13086
+ }
13087
+ return saveIdentity(origin, redemption.credential, target, directory);
13088
+ }
13089
+ async function connectByApproval({
13090
+ hub,
13091
+ onCode,
13092
+ signal,
13093
+ configPath = overriddenIdentityPath(),
13094
+ directory = runnerConfigDirectory()
13095
+ }) {
13096
+ const origin = hubOrigin(hub);
13097
+ const target = await connectTarget(origin, configPath, directory);
13098
+ const refused = (body, status) => new Error(requestRefusals[body.error ?? ""] ?? `The Hub refused to connect this machine (${body.error ?? status}).`);
13099
+ const started = await post(origin, "/v2/runners/connect/start", thisMachine(target.presented));
13100
+ const startBody = await started.json().catch(() => ({}));
13101
+ if (!started.ok) throw refused(startBody, started.status);
13102
+ const { deviceCode, code, verificationUrl, expiresAt, intervalSec } = connectStartResponseSchema.parse(startBody);
13103
+ onCode({ code, verificationUrl, expiresAt });
13104
+ for (; ; ) {
13105
+ await sleep(intervalSec * 1e3, void 0, { signal });
13106
+ const polled = await post(origin, "/v2/runners/connect/poll", { deviceCode }).catch(() => void 0);
13107
+ const body = await polled?.json().catch(() => ({})) ?? {};
13108
+ if (polled?.ok) return saveIdentity(origin, connectRedeemResponseSchema.parse(body), target, directory);
13109
+ if (polled?.status === 409 && body.error === "runner_already_connected")
13110
+ return alreadyConnected(connectAlreadyConnectedSchema.parse(body), target);
13111
+ if (polled && polled.status < 500 && body.error !== "authorization_pending") throw refused(body, polled.status);
13112
+ if (Date.now() >= expiresAt && body.error !== "authorization_pending")
13113
+ throw refused({ error: "authorization_expired" }, 409);
13114
+ }
13115
+ }
13116
+ async function saveIdentity(origin, credential, { single, forTeam }, directory) {
13117
+ const endpoint = new URL(`/v2/orgs/${encodeURIComponent(credential.orgId)}/connect`, origin);
13118
+ endpoint.protocol = endpoint.protocol === "https:" ? "wss:" : "ws:";
13119
+ const config = runnerConfigSchema.parse({
13120
+ endpoint: endpoint.href,
13121
+ token: credential.token,
13122
+ orgId: credential.orgId,
13123
+ runnerId: credential.runnerId,
13124
+ installationId: credential.installationId,
13125
+ hubEpoch: credential.hubEpoch,
13126
+ capacity: 1,
13127
+ // Each Runner keeps its own store: the store pins its Runner and Hub epoch, and one installation identity can
13128
+ // be several Runners (one per Team, and a new one when the Hub enrols this machine again).
13129
+ dataDirectory: `~/.local/share/drisp/${pathSegment(credential.installationId)}/${pathSegment(credential.runnerId)}`,
13130
+ name: credential.name
13131
+ });
13132
+ const replaced = [];
13133
+ for (const { path: path8 } of forTeam(credential.orgId)) {
13134
+ const aside = `${path8}.replaced-${Date.now()}`;
13135
+ await rename(path8, aside);
13136
+ replaced.push(aside);
13157
13137
  }
13138
+ const path7 = single ?? teamIdentityPath(origin, credential.orgId, directory);
13139
+ try {
13140
+ await writeIdentityOnce(path7, JSON.stringify(config, null, 2) + "\n");
13141
+ } catch (error) {
13142
+ if (error.code === "EEXIST")
13143
+ throw new Error(`Another Connect wrote a Runner identity for this Team at ${path7} meanwhile; it was left unchanged.`);
13144
+ throw error;
13145
+ }
13146
+ return { status: "connected", config, path: path7, replaced };
13147
+ }
13148
+ function pathSegment(value) {
13149
+ const segment = encodeURIComponent(value);
13150
+ if (segment === "." || segment === "..") throw new Error(`The Hub returned an unusable identifier (${value}).`);
13151
+ return segment;
13152
+ }
13153
+ async function redeem(origin, code, installationId) {
13154
+ const response = await post(origin, "/v2/runners/connect", { code, ...thisMachine(installationId) });
13155
+ const body = await response.json().catch(() => ({}));
13156
+ if (response.status === 409 && body.error === "runner_already_connected")
13157
+ return { status: "already_connected", existing: connectAlreadyConnectedSchema.parse(body) };
13158
+ if (!response.ok)
13159
+ throw new Error(refusals[body.error ?? ""] ?? `The Hub refused the Connect code (${body.error ?? response.status}).`);
13160
+ return { status: "connected", credential: connectRedeemResponseSchema.parse(body) };
13161
+ }
13162
+ async function credentialAccepted(config) {
13163
+ const url = hubHttpUrl(config.endpoint, "/credentials/renew");
13164
+ const response = await fetch(url, {
13165
+ method: "POST",
13166
+ headers: { Authorization: `Bearer ${config.token}`, "Content-Type": "application/json" },
13167
+ body: JSON.stringify({ hubEpoch: config.hubEpoch }),
13168
+ signal: AbortSignal.timeout(1e4)
13169
+ });
13170
+ if (response.ok) return true;
13171
+ const { error } = await response.json().catch(() => ({}));
13172
+ if (response.status === 401 || error === "recovery_required") return false;
13173
+ throw new Error(`The Hub could not check this machine's credential for the Team (${error ?? response.status}).`);
13174
+ }
13175
+ async function refuseExisting(target) {
13176
+ const existing = await readFile(target, "utf8").then((text) => text, () => void 0);
13177
+ if (existing === void 0) return;
13178
+ let name;
13158
13179
  try {
13159
- const pkg = require_("../../../package.json");
13160
- cachedVersion = pkg.version ?? "0.0.0";
13180
+ name = runnerConfigSchema.safeParse(JSON.parse(existing)).data?.name;
13161
13181
  } catch {
13162
- cachedVersion = "0.0.0";
13163
13182
  }
13164
- return cachedVersion;
13183
+ throw new Error(`This machine already has a Runner${name ? ` (${name})` : ""} configured at ${target}.`);
13184
+ }
13185
+ async function disconnectRunner({
13186
+ identity: { path: path7, config },
13187
+ serviceManager,
13188
+ othersRemain
13189
+ }) {
13190
+ const name = config.name ?? config.runnerId;
13191
+ const wasRunning = serviceManager ? (await serviceManager.status()).running : false;
13192
+ const restore = async () => {
13193
+ if (wasRunning) await serviceManager?.start();
13194
+ };
13195
+ await serviceManager?.stop();
13196
+ const url = hubHttpUrl(config.endpoint, "/runner/disconnect");
13197
+ let response;
13198
+ try {
13199
+ response = await fetch(url, {
13200
+ method: "POST",
13201
+ headers: { Authorization: `Bearer ${config.token}`, "Content-Type": "application/json" },
13202
+ body: JSON.stringify({ hubEpoch: config.hubEpoch }),
13203
+ signal: AbortSignal.timeout(15e3)
13204
+ });
13205
+ } catch (error) {
13206
+ await restore();
13207
+ throw new Error(`The Hub could not be reached, so ${name} is still connected: ${error instanceof Error ? error.message : String(error)}`);
13208
+ }
13209
+ const body = await response.json().catch(() => ({}));
13210
+ let result;
13211
+ if (response.ok) result = { name, stoppedRuns: runnerDisconnectResponseSchema.parse(body).stoppedRuns };
13212
+ else if (response.status === 410) result = { name, note: "The Hub had already disconnected it." };
13213
+ else if (response.status === 401)
13214
+ result = { name, note: "The Hub no longer accepts its credential: it lapsed after 30 days without contact, or was revoked." };
13215
+ else {
13216
+ await restore();
13217
+ throw new Error(`The Hub refused to disconnect ${name} (${body.error ?? response.status}); it is still connected.`);
13218
+ }
13219
+ await forgetRunner(path7);
13220
+ if (othersRemain) await restore();
13221
+ else await serviceManager?.remove();
13222
+ return result;
13165
13223
  }
13166
- function defaultFingerprint() {
13167
- const seed = [
13168
- os5.hostname(),
13169
- os5.userInfo().username,
13170
- os5.platform(),
13171
- os5.arch()
13172
- ].join("\0");
13173
- return crypto3.createHash("sha256").update(seed).digest("hex");
13224
+
13225
+ // src/app/entry/runnerCommandV2.ts
13226
+ var usage = `Usage: drisp runner [connect [<code>] --hub <url> [--foreground] [--replace] | disconnect [<team>] | status | recover | help]
13227
+
13228
+ Connect this machine from Settings \u2192 Runners in the web console, which shows the
13229
+ command to paste here:
13230
+ drisp runner connect K7QF-M2PX --hub https://hub.example
13231
+ Or start from this machine: without a code, connect prints one to approve in the
13232
+ web console, where you choose the Team, and waits for the approval:
13233
+ drisp runner connect --hub https://hub.example
13234
+
13235
+ A machine is one Runner per Team. Connect writes that Team's Runner identity and
13236
+ credential to ~/.config/drisp/runners/<hub>/<team>.json, readable only by you, then
13237
+ installs and starts a background service (launchd on macOS, systemd --user on Linux)
13238
+ that runs this machine's Runner for every Team it serves, at login. Connecting again to
13239
+ the same Team reports the Runner the machine already is and changes nothing. When this
13240
+ machine can no longer serve that Runner (its credential is gone or has lapsed), Connect
13241
+ says so without using the code, and --replace connects a new Runner in its place.
13242
+ drisp runner status lists the Teams and the service's state. --foreground skips the
13243
+ service and runs the Runner in this terminal, for containers and CI; drisp runner runs
13244
+ every Team's Runner there again later. DRISP_HUB_URL may stand in for --hub, and
13245
+ DRISP_RUNNER_CONFIG selects one identity file of a separate installation instead, which
13246
+ gets its own service.
13247
+ drisp runner disconnect takes this machine out of a Team: the Hub stops the Runs on
13248
+ its Runner there, and this machine deletes that Team's credential, removing the
13249
+ service when no Team is left. Name the Team (its id or Runner name from status) when
13250
+ the machine serves several. A Runner disconnected from Settings does the same by
13251
+ itself the next time it reaches the Hub.
13252
+ For an uncertain crash, stop the service and every harness first, then:
13253
+ drisp runner recover <run-id> confirmed-stopped
13254
+ This records an explicit failed outcome; it never retries execution.
13255
+ `;
13256
+ function printApprovalRequest({
13257
+ code,
13258
+ verificationUrl,
13259
+ expiresAt
13260
+ }) {
13261
+ const minutes = Math.max(1, Math.round((expiresAt - Date.now()) / 6e4));
13262
+ process.stdout.write(
13263
+ `To connect this machine, approve code ${code.slice(0, 4)}-${code.slice(4)} in the web console:
13264
+ ${verificationUrl}
13265
+ The code works once and expires in ${minutes} minutes. Waiting for approval\u2026
13266
+ `
13267
+ );
13174
13268
  }
13175
- function defaultHostInfo(name) {
13269
+ var serviceStarted = (service) => `The Runner runs as a background service (${service.kind} ${service.name}) and starts again at login.`;
13270
+ var servicesFor = (options) => options.services === void 0 ? platformRunnerServices() : options.services;
13271
+ async function serviceInstallation(config) {
13272
+ const single = overriddenIdentityPath();
13176
13273
  return {
13177
- hostname: os5.hostname(),
13178
- user: os5.userInfo().username,
13179
- platform: os5.platform(),
13180
- arch: os5.arch(),
13181
- name: name ?? os5.hostname()
13274
+ installationId: await serviceInstallationId(config),
13275
+ ...single ? { configPath: single } : {}
13182
13276
  };
13183
13277
  }
13184
- async function runRunnerCommand(input, deps = {}) {
13185
- const logOut = deps.logOut ?? ((m) => process.stdout.write(m + "\n"));
13186
- const logError = deps.logError ?? ((m) => process.stderr.write(m + "\n"));
13187
- const fetchImpl = deps.fetch ?? fetch;
13188
- const now = deps.now ?? (() => Date.now());
13189
- const fingerprint = deps.fingerprint ?? defaultFingerprint;
13190
- const readConfig2 = deps.readConfig ?? (() => readDashboardClientConfig());
13191
- const writeConfig = deps.writeConfig ?? ((c2) => writeDashboardClientConfig(c2));
13192
- const removeConfig = deps.removeConfig ?? (() => removeDashboardClientConfig());
13193
- const configPath = deps.configPath ?? (() => dashboardClientConfigPath());
13194
- const readMirror = deps.readMirror ?? (() => readAttachmentMirror());
13195
- const writeMirror = deps.writeMirror ?? ((m) => writeAttachmentMirror(m));
13196
- const removeMirror = deps.removeMirror ?? (() => removeAttachmentMirror());
13197
- const packageVersion = deps.packageVersion ?? readPackageVersion();
13198
- const { subcommand, subcommandArgs, flags } = input;
13199
- if (subcommand === "help" || subcommand === "--help") {
13200
- logOut(USAGE3);
13201
- return 0;
13278
+ async function ensureService(services, config, identityPath) {
13279
+ let service;
13280
+ let installed = false;
13281
+ try {
13282
+ const { installationId, configPath } = await serviceInstallation(config);
13283
+ service = services(installationId);
13284
+ await service.install(runnerServiceUnit(configPath));
13285
+ installed = true;
13286
+ if ((await service.status()).running) await service.stop();
13287
+ await service.start();
13288
+ return service;
13289
+ } catch (error) {
13290
+ throw new Error(
13291
+ [
13292
+ `Connected, but the background service did not start: ${error instanceof Error ? error.message : String(error)}`,
13293
+ `The identity is saved to ${identityPath}: run drisp runner to run the Runner in the foreground.`,
13294
+ // launchd loads an installed LaunchAgent at the next login and RunAtLoad starts it. A systemd unit
13295
+ // is enabled only by the start that just failed, so it has no such fallback.
13296
+ ...installed && service?.kind === "launchd" ? [
13297
+ `The LaunchAgent ${service.name} is installed, so launchd starts the Runner at your next login.`
13298
+ ] : []
13299
+ ].join("\n")
13300
+ );
13202
13301
  }
13203
- if (subcommand === "pair") {
13204
- const token = subcommandArgs[0];
13205
- if (!token) {
13206
- logError("runner pair: missing pairing token");
13207
- logError(USAGE3);
13208
- return 2;
13209
- }
13210
- if (subcommandArgs.length > 1) {
13211
- logError(`runner pair: unexpected argument ${subcommandArgs[1]}`);
13212
- return 2;
13213
- }
13214
- if (!flags.url) {
13215
- logError("runner pair: --url <dashboard-origin> is required");
13216
- return 2;
13217
- }
13218
- let origin;
13219
- try {
13220
- origin = normalizeDashboardUrl(flags.url);
13221
- } catch (err) {
13222
- logError(
13223
- `runner pair: ${err instanceof Error ? err.message : String(err)}`
13224
- );
13225
- return 2;
13226
- }
13227
- const fp = fingerprint();
13228
- const body = {
13229
- token,
13230
- fingerprint: fp,
13231
- hostInfo: (deps.hostInfo ?? (() => defaultHostInfo(flags.name)))(),
13232
- capabilities: {
13233
- instanceSocket: true,
13234
- runtimeDaemon: true,
13235
- cliVersion: packageVersion,
13236
- // Legacy field — older dashboards read `version`. Drop in a
13237
- // follow-up release once the dashboard accepts only `cliVersion`.
13238
- version: packageVersion
13239
- }
13240
- };
13241
- let response;
13242
- try {
13243
- response = await fetchImpl(`${origin}/api/instances/pair`, {
13244
- method: "POST",
13245
- headers: { "content-type": "application/json" },
13246
- body: JSON.stringify(body)
13247
- });
13248
- } catch (err) {
13249
- logError(
13250
- `runner pair: failed to reach ${origin}: ${err instanceof Error ? err.message : String(err)}`
13251
- );
13252
- return 1;
13253
- }
13254
- if (!response.ok) {
13255
- const message = await safeReadError(response);
13256
- logError(
13257
- `runner pair: ${origin} returned ${response.status}${message ? ` \u2014 ${message}` : ""}`
13258
- );
13259
- return 1;
13260
- }
13261
- let parsed;
13262
- try {
13263
- parsed = parsePairResponse(await response.json());
13264
- } catch (err) {
13265
- logError(
13266
- `runner pair: invalid response from ${origin}: ${err instanceof Error ? err.message : String(err)}`
13267
- );
13268
- return 1;
13269
- }
13270
- if (parsed.requiredCliVersion && compareSemver(packageVersion, parsed.requiredCliVersion) < 0) {
13271
- logError(
13272
- `runner pair: cli version ${packageVersion} is older than the dashboard's required >=${parsed.requiredCliVersion}.`
13273
- );
13274
- logError(
13275
- "runner pair: upgrade with `npm i -g @drisp/cli` then re-run pair."
13276
- );
13277
- return 1;
13278
- }
13279
- const config = {
13280
- dashboardUrl: origin,
13281
- instanceId: parsed.instanceId,
13282
- refreshToken: parsed.refreshToken,
13283
- fingerprint: fp,
13284
- pairedAt: now()
13285
- };
13286
- writeConfig(config);
13287
- try {
13288
- writeMirror({
13289
- instanceId: parsed.instanceId,
13290
- fetchedAt: now(),
13291
- attachments: (parsed.runners ?? []).map((r) => ({
13292
- runnerId: r.runnerId,
13293
- ...r.name !== void 0 ? { name: r.name } : {},
13294
- ...r.executionTarget !== void 0 ? { executionTarget: r.executionTarget } : {},
13295
- ...r.remoteInstanceId !== void 0 ? { remoteInstanceId: r.remoteInstanceId } : {}
13296
- }))
13297
- });
13298
- } catch (err) {
13299
- logError(
13300
- `runner pair: failed to write attachment mirror: ${err instanceof Error ? err.message : String(err)}`
13301
- );
13302
- }
13303
- const priorStop = await (deps.stopRunner ?? defaultStopRunner)({});
13304
- const started = await (deps.startDetachedRunner ?? defaultStartDetachedRunner)({
13305
- log: (msg) => logOut(msg)
13306
- });
13307
- const daemonStart = {
13308
- ...started,
13309
- ...priorStop.wasRunning ? { restarted: true } : {}
13310
- };
13311
- if (flags.json) {
13312
- logOut(
13313
- JSON.stringify({
13314
- ok: true,
13315
- instanceId: parsed.instanceId,
13316
- dashboardUrl: origin,
13317
- configPath: configPath(),
13318
- runner: daemonStart,
13319
- ...parsed.runners ? { runners: parsed.runners } : {},
13320
- ...parsed.capabilityAck ? { capabilityAck: parsed.capabilityAck } : {},
13321
- ...parsed.requiredCliVersion ? { requiredCliVersion: parsed.requiredCliVersion } : {}
13322
- })
13302
+ }
13303
+ async function runRunnerCommandV2(input, options = {}) {
13304
+ try {
13305
+ if (input.flags.detach)
13306
+ throw new Error(
13307
+ "v1 --detach is unavailable: drisp runner connect installs the Runner as a background service."
13323
13308
  );
13324
- } else {
13325
- logOut(`runner: paired to ${origin} as ${parsed.instanceId}`);
13326
- if (parsed.runners && parsed.runners.length > 0) {
13327
- for (const runner of parsed.runners) {
13328
- logOut(
13329
- `runner: bound runner ${runner.name ?? runner.runnerId} (${runner.runnerId})`
13309
+ switch (input.subcommand) {
13310
+ case "connect": {
13311
+ const [code, extra] = input.subcommandArgs;
13312
+ if (extra) throw new Error(`Unexpected argument ${extra}.`);
13313
+ const hub = input.flags.hub ?? process.env["DRISP_HUB_URL"];
13314
+ if (!hub)
13315
+ throw new Error(
13316
+ "Pass --hub <url>, the Hub shown with the code in Settings \u2192 Runners."
13317
+ );
13318
+ const services = input.flags.foreground ? null : servicesFor(options);
13319
+ const outcome = code ? await connectWithCode({
13320
+ code,
13321
+ hub,
13322
+ replace: Boolean(input.flags.replace)
13323
+ }) : await connectByApproval({
13324
+ hub,
13325
+ onCode: printApprovalRequest,
13326
+ signal: options.signal
13327
+ });
13328
+ if (outcome.status === "already_connected") {
13329
+ process.stdout.write(
13330
+ `This machine is already connected to this Team as ${outcome.name}. Its identity in ${outcome.path} is unchanged; start it with drisp runner.
13331
+ `
13330
13332
  );
13333
+ return 0;
13331
13334
  }
13332
- } else {
13333
- logOut("runner: no runner bound to this pairing token.");
13334
- logOut(
13335
- "runner: bind a runner from runner settings, then this machine will receive its runs."
13336
- );
13337
- }
13338
- if (parsed.capabilityAck === void 0) {
13339
- logOut(
13340
- "runner: dashboard did not echo capabilityAck (older server). Continuing."
13341
- );
13342
- }
13343
- if (daemonStart.ok) {
13344
- const status = daemonStart.connected ? `runner connected (verified socket open${daemonStart.restarted ? ", restarted with the new token" : ""})` : "runner started but did not reach the socket within 10s";
13345
- logOut(`runner: ${status}`);
13346
- if (!daemonStart.connected) {
13347
- logOut(
13348
- "runner pair: pairing succeeded; tail logs with `drisp runner logs --follow`."
13335
+ if (outcome.status === "identity_unusable") {
13336
+ const why = {
13337
+ missing: "it no longer holds that Runner's credential",
13338
+ other_runner: `its identity in ${outcome.path} belongs to another Runner`,
13339
+ lapsed: `the Hub no longer accepts the credential in ${outcome.path} (it expired or was revoked)`
13340
+ }[outcome.problem];
13341
+ const retry = [
13342
+ "drisp runner connect",
13343
+ code ?? "<code>",
13344
+ "--hub",
13345
+ hub,
13346
+ ...input.flags.foreground ? ["--foreground"] : [],
13347
+ "--replace"
13348
+ ].join(" ");
13349
+ throw new Error(
13350
+ `This machine is already connected to this Team as ${outcome.name}, but ${why}. ` + (code ? `The code was not used: connect a new Runner in its place with ${retry}. ` : `Connect a new Runner in its place with a Connect code from Settings \u2192 Runners: ${retry}. `) + `The old ${outcome.name} stays listed in Settings \u2192 Runners, offline.`
13349
13351
  );
13350
13352
  }
13351
- } else {
13352
- logError(
13353
- `runner: runner did not start${daemonStart.message ? ` \u2014 ${daemonStart.message}` : ""}`
13354
- );
13355
- logOut(
13356
- "runner pair: pairing succeeded; start the runner with `drisp runner --detach`."
13353
+ const { config, path: path7, replaced } = outcome;
13354
+ for (const aside of replaced)
13355
+ process.stdout.write(
13356
+ `The Hub no longer recognised this machine's earlier identity for this Team; it was moved aside to ${aside}.
13357
+ `
13358
+ );
13359
+ process.stdout.write(
13360
+ `Connected as ${config.name ?? config.runnerId}. Identity saved to ${path7}.
13361
+ `
13357
13362
  );
13363
+ if (services) {
13364
+ const service = await ensureService(services, config, path7);
13365
+ process.stdout.write(`${serviceStarted(service)}
13366
+ `);
13367
+ return 0;
13368
+ }
13369
+ if (!input.flags.foreground)
13370
+ process.stdout.write(
13371
+ "No launchd or systemd here to run the Runner as a background service; running it in the foreground instead.\n"
13372
+ );
13373
+ await runConfiguredRunner(path7, options.signal);
13374
+ return 0;
13358
13375
  }
13359
- logOut("runner: ready. Click Run in the dashboard.");
13360
- }
13361
- return 0;
13362
- }
13363
- if (subcommand === "status") {
13364
- if (subcommandArgs.length > 0) {
13365
- logError(`runner status: unexpected argument ${subcommandArgs[0]}`);
13366
- return 2;
13367
- }
13368
- const config = readConfig2();
13369
- if (!config) {
13370
- if (flags.json) {
13371
- logOut(JSON.stringify({ ok: false, paired: false }));
13372
- } else {
13373
- logOut("runner: not paired");
13374
- }
13375
- return 1;
13376
- }
13377
- const readStatus = deps.readRunnerStatus ?? defaultReadRunnerStatus;
13378
- let report;
13379
- try {
13380
- report = readStatus();
13381
- } catch (err) {
13382
- report = {
13383
- running: false,
13384
- error: err instanceof Error ? err.message : String(err)
13385
- };
13386
- }
13387
- let runnerHealth;
13388
- if (flags.runner) {
13389
- const refreshResult = await tryRefresh("refresh");
13390
- if (refreshResult.ok) {
13391
- runnerHealth = await fetchRunnerHealth(
13392
- fetchImpl,
13393
- config.dashboardUrl,
13394
- flags.runner,
13395
- refreshResult.token
13376
+ case "disconnect": {
13377
+ const [team, extra] = input.subcommandArgs;
13378
+ if (extra) throw new Error(`Unexpected argument ${extra}.`);
13379
+ const identities = await runnerIdentities();
13380
+ if (!identities.length)
13381
+ throw new Error("This machine has no Runner to disconnect.");
13382
+ const matching = team ? identities.filter(
13383
+ ({ config }) => config.orgId === team || config.name === team
13384
+ ) : identities;
13385
+ if (matching.length !== 1)
13386
+ throw new Error(
13387
+ (team ? `No single Runner on this machine matches ${team}.` : "This machine serves several Teams: name the one to leave.") + ` Its Teams: ${identities.map(({ config }) => `${config.orgId} (${config.name ?? config.runnerId})`).join(", ")}.`
13388
+ );
13389
+ const identity = matching[0];
13390
+ const services = servicesFor(options);
13391
+ const { name, stoppedRuns, note } = await disconnectRunner({
13392
+ identity,
13393
+ serviceManager: services ? services(await serviceInstallationId(identity.config)) : null,
13394
+ othersRemain: identities.length > 1
13395
+ });
13396
+ process.stdout.write(
13397
+ `Disconnected ${name}.${stoppedRuns ? ` ${stoppedRuns === 1 ? "1 Run on it was" : `${stoppedRuns} Runs on it were`} stopped.` : ""}${note ? ` ${note}` : ""} Its credential was deleted${identities.length > 1 ? "; the service runs on for the other Teams" : " and its service removed"}.
13398
+ `
13396
13399
  );
13397
- } else {
13398
- runnerHealth = {
13399
- id: flags.runner,
13400
- matches: false,
13401
- error: "could not refresh access token"
13402
- };
13400
+ return 0;
13403
13401
  }
13404
- }
13405
- const socketHealthy = report.running && report.status.socketConnected;
13406
- const runnerOk = !runnerHealth || runnerHealth.matches;
13407
- const ok = report.running && socketHealthy && runnerOk;
13408
- if (flags.json) {
13409
- logOut(
13410
- JSON.stringify({
13411
- ok,
13412
- paired: true,
13413
- instanceId: config.instanceId,
13414
- dashboardUrl: config.dashboardUrl,
13415
- pairedAt: config.pairedAt,
13416
- ...config.lastRefreshAt !== void 0 ? { lastRefreshAt: config.lastRefreshAt } : {},
13417
- configPath: configPath(),
13418
- runner: report.running ? {
13419
- running: true,
13420
- pid: report.status.pid,
13421
- startedAt: report.status.startedAt,
13422
- updatedAt: report.status.updatedAt,
13423
- socketConnected: report.status.socketConnected,
13424
- ...report.status.wireMode !== void 0 ? { wireMode: report.status.wireMode } : {},
13425
- ...report.status.lastFrameAt !== void 0 ? { lastFrameAt: report.status.lastFrameAt } : {},
13426
- activeRuns: report.status.activeRuns,
13427
- completedRuns: report.status.completedRuns,
13428
- ...report.status.refreshState ? { refreshState: report.status.refreshState } : {}
13429
- } : {
13430
- running: false,
13431
- ...report.error ? { error: report.error } : {}
13432
- },
13433
- ...runnerHealth ? { binding: runnerHealth } : {}
13434
- })
13435
- );
13436
- } else {
13437
- logOut(
13438
- `runner: paired to ${config.dashboardUrl} as ${config.instanceId}`
13439
- );
13440
- if (runnerHealth) {
13441
- if (runnerHealth.matches) {
13442
- logOut(
13443
- `binding: ${runnerHealth.id} bound to this instance (executionTarget=remote)`
13444
- );
13445
- } else {
13446
- logError(
13447
- `binding: ${runnerHealth.id} ${runnerHealth.error ?? "not bound"}`
13402
+ case "recover": {
13403
+ const [runId, confirmation] = input.subcommandArgs;
13404
+ if (!runId || confirmation !== "confirmed-stopped")
13405
+ throw new Error(
13406
+ "Stop the service and verify every harness is stopped, then use recover <run-id> confirmed-stopped."
13448
13407
  );
13449
- }
13450
- }
13451
- if (report.running) {
13452
- const r = report.status;
13453
- const uptimeSec = Math.max(
13454
- 0,
13455
- Math.floor((now() - r.startedAt) / 1e3)
13456
- );
13457
- logOut(
13458
- `runner: running (pid ${r.pid}, uptime ${formatDuration2(uptimeSec)}, ${r.completedRuns} runs completed, ${r.activeRuns} active)`
13459
- );
13460
- logOut(
13461
- `socket: ${r.socketConnected ? "connected" : "disconnected"}${r.wireMode ? ` (${r.wireMode} frame names)` : ""}${r.lastFrameAt !== void 0 ? ` (last frame ${formatDuration2(
13462
- Math.max(0, Math.floor((now() - r.lastFrameAt) / 1e3))
13463
- )} ago)` : ""}`
13408
+ const config = await recoveryIdentity(runId);
13409
+ const store = new ExecutionStore(
13410
+ join2(config.dataDirectory, "execution.sqlite"),
13411
+ {
13412
+ orgId: config.orgId,
13413
+ runnerId: config.runnerId,
13414
+ installationId: config.installationId,
13415
+ hubEpoch: config.hubEpoch
13416
+ },
13417
+ config.capacity
13464
13418
  );
13465
- if (r.refreshState) {
13466
- if (r.refreshState.cooldownUntilMs !== void 0 && r.refreshState.cooldownUntilMs > now()) {
13467
- const remainingSec = Math.ceil(
13468
- (r.refreshState.cooldownUntilMs - now()) / 1e3
13469
- );
13470
- if (r.refreshState.hubUnreachable) {
13471
- logError(
13472
- `refresh: circuit-broken \u2014 hub unreachable; probing it and reconnecting as soon as it answers (retries anyway in ${formatDuration2(
13473
- remainingSec
13474
- )})`
13475
- );
13476
- } else {
13477
- logError(
13478
- `refresh: circuit-broken \u2014 sleeping for ${formatDuration2(
13479
- remainingSec
13480
- )} before retry. Re-pair if this persists.`
13481
- );
13482
- }
13483
- } else {
13484
- logOut(
13485
- `refresh: ${r.refreshState.recentFailures} recent failure(s); next reconnect will retry`
13486
- );
13487
- }
13419
+ try {
13420
+ store.resolveRecovery(runId);
13421
+ if (config.portableCheckpoints)
13422
+ store.queueCheckpoint(runId, crypto.randomUUID());
13423
+ } finally {
13424
+ store.close();
13488
13425
  }
13489
- } else {
13490
- logOut(
13491
- `runner: NOT running${report.error ? ` (${report.error})` : ""}. Start it with \`drisp runner --detach\`.`
13426
+ process.stdout.write(
13427
+ "Recovery outcome recorded. Start the runner to publish it.\n"
13492
13428
  );
13429
+ return 0;
13493
13430
  }
13494
- if (config.lastRefreshAt !== void 0) {
13495
- logOut(
13496
- `token: last refreshed ${formatDuration2(
13497
- Math.max(0, Math.floor((now() - config.lastRefreshAt) / 1e3))
13498
- )} ago`
13431
+ case "status": {
13432
+ const identities = await runnerIdentities();
13433
+ if (!identities.length)
13434
+ throw new Error(
13435
+ "This machine is not connected to a Team. Settings \u2192 Runners shows the command to connect it."
13436
+ );
13437
+ const services = servicesFor(options);
13438
+ const service = services ? services(
13439
+ (await serviceInstallation(identities[0].config)).installationId
13440
+ ) : void 0;
13441
+ process.stdout.write(
13442
+ JSON.stringify(
13443
+ {
13444
+ configured: true,
13445
+ // One entry per Team this machine serves; credentials are never printed.
13446
+ teams: identities.map(
13447
+ ({ path: path7, config: { token: _token, ...config } }) => ({
13448
+ ...config,
13449
+ path: path7
13450
+ })
13451
+ ),
13452
+ service: service ? {
13453
+ manager: service.kind,
13454
+ name: service.name,
13455
+ ...await service.status()
13456
+ } : { manager: null, installed: false, running: false },
13457
+ note: "Connection and execution state are visible in the Hub."
13458
+ },
13459
+ null,
13460
+ 2
13461
+ ) + "\n"
13499
13462
  );
13463
+ return 0;
13500
13464
  }
13465
+ case "":
13466
+ await runMachineRunners(options.signal, {
13467
+ services: servicesFor(options)
13468
+ });
13469
+ return 0;
13470
+ case "help":
13471
+ case "--help":
13472
+ process.stdout.write(usage);
13473
+ return 0;
13474
+ default:
13475
+ throw new Error(
13476
+ `Unsupported v2 runner command: ${input.subcommand}.
13477
+ ${usage}`
13478
+ );
13501
13479
  }
13502
- return ok ? 0 : 1;
13503
- }
13504
- if (subcommand === "logs") {
13505
- if (subcommandArgs.length > 0) {
13506
- logError(`runner logs: unexpected argument ${subcommandArgs[0]}`);
13507
- return 2;
13508
- }
13509
- const tail = flags.tail ?? 20;
13510
- const follow = flags.follow ?? false;
13511
- try {
13512
- const tailFn = deps.tailRunnerLog ?? defaultTailRunnerLog;
13513
- return await tailFn({ tail, follow });
13514
- } catch (err) {
13515
- logError(
13516
- `runner logs: ${err instanceof Error ? err.message : String(err)}`
13517
- );
13518
- return 1;
13519
- }
13520
- }
13521
- if (subcommand === "runs") {
13522
- if (subcommandArgs.length > 0) {
13523
- logError(`runner runs: unexpected argument ${subcommandArgs[0]}`);
13524
- return 2;
13525
- }
13526
- const readStatus = deps.readRunnerStatus ?? defaultReadRunnerStatus;
13527
- let report;
13528
- try {
13529
- report = readStatus();
13530
- } catch (err) {
13531
- logError(
13532
- `runner runs: ${err instanceof Error ? err.message : String(err)}`
13533
- );
13534
- return 1;
13535
- }
13536
- if (!report.running) {
13537
- logError(`runner runs: ${report.error ?? "runner not running"}`);
13538
- return 1;
13539
- }
13540
- let runs = report.status.runs;
13541
- if (typeof flags.limit === "number" && flags.limit > 0) {
13542
- runs = runs.slice(-flags.limit);
13543
- }
13544
- if (flags.active) {
13545
- runs = runs.filter((run) => run.status === "running");
13546
- }
13547
- if (flags.json) {
13548
- logOut(JSON.stringify({ ok: true, runs }));
13549
- return 0;
13550
- }
13551
- if (runs.length === 0) {
13552
- logOut("runner: no runs recorded");
13553
- return 0;
13554
- }
13555
- logOut(["runId", "started", "duration", "status"].join(" "));
13556
- for (const run of runs) {
13557
- const duration = run.endedAt ? formatDuration2(
13558
- Math.max(0, Math.floor((run.endedAt - run.startedAt) / 1e3))
13559
- ) : "\u2014";
13560
- logOut(
13561
- [
13562
- run.runId,
13563
- formatDuration2(
13564
- Math.max(0, Math.floor((now() - run.startedAt) / 1e3))
13565
- ) + " ago",
13566
- duration,
13567
- run.status
13568
- ].join(" ")
13569
- );
13570
- }
13571
- return 0;
13480
+ } catch (error) {
13481
+ process.stderr.write(
13482
+ `drisp runner: ${error instanceof Error ? error.message : String(error)}
13483
+ `
13484
+ );
13485
+ return 1;
13572
13486
  }
13573
- const performRefreshImpl = deps.performRefresh ?? (async (_label) => refreshDashboardAccessToken({ fetch: fetchImpl, now }));
13574
- async function tryRefresh(label) {
13487
+ }
13488
+ async function recoveryIdentity(runId) {
13489
+ const identities = await runnerIdentities();
13490
+ if (!identities.length)
13491
+ throw new Error("This machine is not connected to a Team.");
13492
+ for (const { config } of identities) {
13493
+ const file = join2(config.dataDirectory, "execution.sqlite");
13494
+ if (!existsSync(file)) continue;
13495
+ const store = new ExecutionStore(
13496
+ file,
13497
+ {
13498
+ orgId: config.orgId,
13499
+ runnerId: config.runnerId,
13500
+ installationId: config.installationId,
13501
+ hubEpoch: config.hubEpoch
13502
+ },
13503
+ config.capacity
13504
+ );
13575
13505
  try {
13576
- const token = await performRefreshImpl(label);
13577
- return { ok: true, token };
13578
- } catch (err) {
13579
- logError(
13580
- `dashboard ${label}: ${err instanceof Error ? err.message : String(err)}`
13581
- );
13582
- return { ok: false, code: 1 };
13506
+ store.run(runId);
13507
+ return config;
13508
+ } catch {
13509
+ } finally {
13510
+ store.close();
13583
13511
  }
13584
13512
  }
13585
- if (subcommand === "refresh") {
13586
- if (subcommandArgs.length > 0) {
13587
- logError(`runner refresh: unexpected argument ${subcommandArgs[0]}`);
13588
- return 2;
13589
- }
13590
- if (!readConfig2()) {
13591
- logError('runner refresh: not paired. Run "drisp runner pair" first.');
13592
- return 1;
13593
- }
13594
- const result = await tryRefresh("refresh");
13595
- if (!result.ok) return result.code;
13596
- const { token } = result;
13597
- if (flags.json) {
13598
- const rotated = readConfig2();
13599
- logOut(
13600
- JSON.stringify({
13601
- ok: true,
13602
- instanceId: token.instanceId,
13603
- accessToken: token.accessToken,
13604
- refreshToken: rotated?.refreshToken,
13605
- expiresInSec: token.expiresInSec
13606
- })
13607
- );
13608
- } else {
13609
- logOut(`runner: refreshed access token for instance ${token.instanceId}`);
13610
- }
13611
- return 0;
13612
- }
13613
- if (subcommand === "") {
13614
- if (subcommandArgs.length > 0) {
13615
- logError(`runner: unexpected argument ${subcommandArgs[0]}`);
13616
- return 2;
13617
- }
13618
- if (!readConfig2()) {
13619
- logError('runner: not paired. Run "drisp runner pair" first.');
13620
- return 1;
13621
- }
13622
- if (flags.detach) {
13623
- const result = await (deps.startDetachedRunner ?? defaultStartDetachedRunner)({ log: (msg) => logOut(msg) });
13624
- if (flags.json) {
13625
- logOut(JSON.stringify(result));
13626
- } else if (result.ok) {
13627
- logOut(
13628
- result.connected ? "runner: started and connected" : "runner: started; socket not yet verified (tail logs)"
13629
- );
13630
- } else {
13631
- logError(`runner: ${result.message ?? "unknown failure"}`);
13632
- }
13633
- return result.ok ? 0 : 1;
13634
- }
13635
- let runner;
13636
- try {
13637
- runner = await startRunnerProcess({
13638
- readConfig: readConfig2,
13639
- refreshAccessToken: async () => performRefreshImpl("connect"),
13640
- ...deps.makeInstanceSocketClient ? { makeInstanceSocketClient: deps.makeInstanceSocketClient } : {},
13641
- ...deps.executeRemoteAssignment ? { executeRemoteAssignment: deps.executeRemoteAssignment } : {},
13642
- ...deps.fetchAttachments ? { fetchAttachments: deps.fetchAttachments } : {},
13643
- ...deps.statePaths ? { statePaths: deps.statePaths() } : {},
13644
- writeMirror,
13645
- retryInitialConnect: false,
13646
- cliVersion: readPackageVersion(),
13647
- log: (level, message) => {
13648
- if (level === "error" || level === "warn") {
13649
- logError(`runner: ${message}`);
13650
- } else {
13651
- logOut(`runner: ${message}`);
13652
- }
13653
- }
13654
- });
13655
- } catch (err) {
13656
- logError(`runner: ${err instanceof Error ? err.message : String(err)}`);
13657
- return err instanceof RunnerStartupError ? err.exitCode : 1;
13658
- }
13659
- logOut(`runner: foreground runtime connected (pid ${runner.pid})`);
13660
- const wait = deps.waitForShutdown ?? waitForShutdownSignal;
13661
- const reason = await wait();
13662
- await runner.stop(reason);
13663
- logOut(`runner: stopped (${reason})`);
13664
- return 0;
13665
- }
13666
- if (subcommand === "stop") {
13667
- if (subcommandArgs.length > 0) {
13668
- logError(`runner stop: unexpected argument ${subcommandArgs[0]}`);
13669
- return 2;
13670
- }
13671
- const result = await (deps.stopRunner ?? defaultStopRunner)({});
13672
- if (flags.json) {
13673
- logOut(JSON.stringify(result));
13674
- } else if (!result.wasRunning) {
13675
- logOut("runner: not running");
13676
- } else if (result.ok) {
13677
- logOut("runner: stopped");
13678
- } else {
13679
- logError(`runner stop: ${result.message ?? "unknown failure"}`);
13680
- }
13681
- return result.ok ? 0 : 1;
13682
- }
13683
- if (subcommand === "restart") {
13684
- if (subcommandArgs.length > 0) {
13685
- logError(`runner restart: unexpected argument ${subcommandArgs[0]}`);
13686
- return 2;
13687
- }
13688
- const stopResult = await (deps.stopRunner ?? defaultStopRunner)({});
13689
- if (stopResult.wasRunning && !stopResult.ok) {
13690
- logError(
13691
- `runner restart: stop failed: ${stopResult.message ?? "unknown"}`
13692
- );
13693
- return 1;
13694
- }
13695
- const startResult = await (deps.startDetachedRunner ?? defaultStartDetachedRunner)({ log: (msg) => logOut(msg) });
13696
- if (flags.json) {
13697
- logOut(JSON.stringify({ restart: true, ...startResult }));
13698
- } else if (startResult.ok) {
13699
- logOut(
13700
- startResult.connected ? "runner: restarted and connected" : "runner: restarted; socket not yet verified"
13701
- );
13702
- } else {
13703
- logError(`runner restart: ${startResult.message ?? "unknown"}`);
13704
- }
13705
- return startResult.ok ? 0 : 1;
13706
- }
13707
- if (subcommand === "install") {
13708
- if (subcommandArgs.length > 0) {
13709
- logError(`runner install: unexpected argument ${subcommandArgs[0]}`);
13710
- return 2;
13711
- }
13712
- const installer = deps.installServiceUnit ?? defaultInstallServiceUnit;
13713
- const result = installer();
13714
- if (flags.json) {
13715
- logOut(JSON.stringify(result));
13716
- } else if (result.ok) {
13717
- logOut(`runner: wrote service unit at ${result.path}`);
13718
- logOut(`runner: load with: ${result.loadCommand}`);
13719
- logOut(`runner: start with: ${result.startCommand}`);
13720
- } else {
13721
- logError(`runner install: ${result.message ?? "unsupported platform"}`);
13722
- }
13723
- return result.ok ? 0 : 1;
13724
- }
13725
- if (subcommand === "doctor") {
13726
- let reportDoctor2 = function(ok) {
13727
- if (flags.json) {
13728
- logOut(
13729
- JSON.stringify({
13730
- ok,
13731
- paired: true,
13732
- instanceId: config.instanceId,
13733
- dashboardUrl: config.dashboardUrl,
13734
- pairedAt: config.pairedAt,
13735
- ...refreshed?.lastRefreshAt !== void 0 ? { lastRefreshAt: refreshed.lastRefreshAt } : {},
13736
- configPath: configPath(),
13737
- ...runnerReport ? { runner: runnerReport } : {}
13738
- })
13739
- );
13740
- } else {
13741
- logOut(
13742
- `runner: paired to ${config.dashboardUrl} as ${config.instanceId}`
13743
- );
13744
- if (token) {
13745
- logOut(`runner: refresh token rotated, access token minted`);
13746
- }
13747
- if (runnerReport) {
13748
- if (runnerReport.matches) {
13749
- logOut(
13750
- `runner: runner ${runnerReport.id} bound to this instance (executionTarget=remote, remoteInstanceId=${runnerReport.remoteInstanceId})`
13751
- );
13752
- } else {
13753
- logError(
13754
- `runner doctor: runner ${runnerReport.id} not bound \u2014 ${runnerReport.error ?? "unknown reason"}`
13755
- );
13756
- }
13757
- }
13758
- }
13759
- return ok ? 0 : 1;
13760
- };
13761
- var reportDoctor = reportDoctor2;
13762
- if (subcommandArgs.length > 0) {
13763
- logError(`runner doctor: unexpected argument ${subcommandArgs[0]}`);
13764
- return 2;
13765
- }
13766
- const config = readConfig2();
13767
- if (!config) {
13768
- if (flags.json) {
13769
- logOut(JSON.stringify({ ok: false, paired: false }));
13770
- } else {
13771
- logOut("runner: not paired");
13772
- logOut('runner doctor: run "drisp runner pair" before using doctor.');
13773
- }
13774
- return 1;
13775
- }
13776
- let token = null;
13777
- if (flags.runner) {
13778
- const refreshResult = await tryRefresh("refresh");
13779
- if (!refreshResult.ok) return refreshResult.code;
13780
- token = refreshResult.token;
13781
- }
13782
- const refreshed = readConfig2();
13783
- let runnerReport;
13784
- let runnerOk = !flags.runner;
13785
- if (flags.runner && token) {
13786
- const runnerId = flags.runner;
13787
- const accessToken = token.accessToken;
13788
- const expectedInstanceId = token.instanceId;
13789
- const url = new URL(
13790
- `/api/runners/${encodeURIComponent(runnerId)}`,
13791
- config.dashboardUrl
13792
- ).toString();
13793
- let response;
13794
- try {
13795
- response = await fetchImpl(url, {
13796
- method: "GET",
13797
- headers: {
13798
- authorization: `Bearer ${accessToken}`,
13799
- accept: "application/json"
13800
- }
13801
- });
13802
- } catch (err) {
13803
- runnerOk = false;
13804
- runnerReport = {
13805
- id: runnerId,
13806
- matches: false,
13807
- error: `request failed: ${err instanceof Error ? err.message : String(err)}`
13808
- };
13809
- return reportDoctor2(false);
13810
- }
13811
- if (response.status === 404) {
13812
- runnerOk = false;
13813
- runnerReport = {
13814
- id: runnerId,
13815
- matches: false,
13816
- error: "runner not found"
13817
- };
13818
- } else if (response.status === 405 || response.status === 501) {
13819
- runnerOk = false;
13820
- runnerReport = {
13821
- id: runnerId,
13822
- matches: false,
13823
- error: "runner check unavailable: dashboard endpoint missing (GET /api/runners/<id>)"
13824
- };
13825
- } else if (!response.ok) {
13826
- runnerOk = false;
13827
- const message = await safeReadError(response);
13828
- runnerReport = {
13829
- id: runnerId,
13830
- matches: false,
13831
- error: `dashboard returned ${response.status}${message ? ` \u2014 ${message}` : ""}`
13832
- };
13833
- } else {
13834
- let body;
13835
- try {
13836
- body = await response.json();
13837
- } catch (err) {
13838
- runnerOk = false;
13839
- runnerReport = {
13840
- id: runnerId,
13841
- matches: false,
13842
- error: `invalid response body: ${err instanceof Error ? err.message : String(err)}`
13843
- };
13844
- return reportDoctor2(false);
13845
- }
13846
- const obj = typeof body === "object" && body !== null ? body : {};
13847
- const executionTarget = typeof obj["executionTarget"] === "string" ? obj["executionTarget"] : void 0;
13848
- const remoteInstanceId = typeof obj["remoteInstanceId"] === "string" ? obj["remoteInstanceId"] : void 0;
13849
- const matchesTarget = executionTarget === "remote";
13850
- const matchesInstance = remoteInstanceId === expectedInstanceId;
13851
- const matches = matchesTarget && matchesInstance;
13852
- runnerOk = matches;
13853
- const reasons = [];
13854
- if (!matchesTarget) {
13855
- reasons.push(
13856
- `executionTarget is "${executionTarget ?? "unset"}" (expected "remote")`
13857
- );
13858
- }
13859
- if (!matchesInstance) {
13860
- reasons.push(
13861
- `remoteInstanceId is "${remoteInstanceId ?? "unset"}" (expected "${expectedInstanceId}")`
13862
- );
13863
- }
13864
- runnerReport = {
13865
- id: runnerId,
13866
- executionTarget,
13867
- remoteInstanceId,
13868
- matches,
13869
- error: reasons.length > 0 ? reasons.join("; ") : void 0
13870
- };
13871
- }
13872
- }
13873
- return reportDoctor2(runnerOk);
13874
- }
13875
- if (subcommand === "console") {
13876
- const message = "dashboard console is deprecated; paired dashboard feed sync now routes dashboard UI and channel decisions.";
13877
- if (flags.json) {
13878
- logOut(JSON.stringify({ ok: false, deprecated: true, message }));
13879
- } else {
13880
- logError(message);
13881
- }
13882
- return 1;
13883
- }
13884
- if (subcommand === "list") {
13885
- if (subcommandArgs.length > 0) {
13886
- logError(`runner list: unexpected argument ${subcommandArgs[0]}`);
13887
- return 2;
13888
- }
13889
- const config = readConfig2();
13890
- if (!config) {
13891
- if (flags.json) {
13892
- logOut(JSON.stringify({ ok: false, paired: false }));
13893
- } else {
13894
- logError('runner list: not paired. Run "drisp runner pair" first.');
13895
- }
13896
- return 1;
13897
- }
13898
- const mirror = readMirror();
13899
- if (!mirror) {
13900
- if (flags.json) {
13901
- logOut(
13902
- JSON.stringify({
13903
- ok: true,
13904
- paired: true,
13905
- instanceId: config.instanceId,
13906
- attachments: [],
13907
- mirror: null
13908
- })
13909
- );
13910
- } else {
13911
- logOut(
13912
- `runner list: paired as ${config.instanceId}, no attachment mirror on disk.`
13913
- );
13914
- logOut(
13915
- "runner list: re-run `drisp runner pair` to refresh the mirror."
13916
- );
13917
- }
13918
- return 0;
13919
- }
13920
- if (flags.json) {
13921
- logOut(
13922
- JSON.stringify({
13923
- ok: true,
13924
- paired: true,
13925
- instanceId: mirror.instanceId,
13926
- fetchedAt: mirror.fetchedAt,
13927
- attachments: mirror.attachments
13928
- })
13929
- );
13930
- } else {
13931
- logOut(`runner: instance ${mirror.instanceId}`);
13932
- logOut(
13933
- `runner: mirror fetched ${new Date(mirror.fetchedAt).toISOString()}`
13934
- );
13935
- if (mirror.attachments.length === 0) {
13936
- logOut("runner: no runners attached.");
13937
- } else {
13938
- logOut(`runner: ${mirror.attachments.length} runner(s) attached:`);
13939
- for (const a of mirror.attachments) {
13940
- const label = a.name ? `${a.name} (${a.runnerId})` : a.runnerId;
13941
- const target = a.executionTarget ? ` [${a.executionTarget}]` : "";
13942
- logOut(` - ${label}${target}`);
13943
- }
13944
- }
13945
- }
13946
- return 0;
13947
- }
13948
- if (subcommand === "unpair") {
13949
- if (subcommandArgs.length > 0) {
13950
- logError(`runner unpair: unexpected argument ${subcommandArgs[0]}`);
13951
- return 2;
13952
- }
13953
- const config = readConfig2();
13954
- if (!config) {
13955
- if (flags.json) {
13956
- logOut(JSON.stringify({ ok: true, paired: false }));
13957
- } else {
13958
- logOut("runner unpair: not paired (nothing to do)");
13959
- }
13960
- return 0;
13961
- }
13962
- const stopResult = await (deps.stopRunner ?? defaultStopRunner)({});
13963
- if (!flags.json) {
13964
- if (stopResult.wasRunning) {
13965
- if (stopResult.ok) {
13966
- logOut("runner: stopped");
13967
- } else {
13968
- logError(`runner: stop failed: ${stopResult.message ?? "unknown"}`);
13969
- }
13970
- } else {
13971
- logOut("runner: not running (skipping stop)");
13972
- }
13973
- }
13974
- let revokeOk = false;
13975
- let revokeMessage;
13976
- try {
13977
- const refreshResult = await tryRefresh("refresh");
13978
- if (refreshResult.ok) {
13979
- const url = new URL(
13980
- `/api/instances/${encodeURIComponent(config.instanceId)}/revoke`,
13981
- config.dashboardUrl
13982
- ).toString();
13983
- const response = await fetchImpl(url, {
13984
- method: "POST",
13985
- headers: {
13986
- authorization: `Bearer ${refreshResult.token.accessToken}`,
13987
- "content-type": "application/json"
13988
- },
13989
- body: JSON.stringify({})
13990
- });
13991
- if (response.ok || response.status === 404) {
13992
- revokeOk = true;
13993
- } else {
13994
- const detail = await safeReadError(response);
13995
- revokeMessage = `dashboard returned ${response.status}${detail ? ` \u2014 ${detail}` : ""}`;
13996
- }
13997
- } else {
13998
- revokeMessage = "could not refresh access token";
13999
- }
14000
- } catch (err) {
14001
- revokeMessage = err instanceof Error ? err.message : String(err);
14002
- }
14003
- if (!flags.json) {
14004
- if (revokeOk) {
14005
- logOut(`runner: revoking refresh token at ${config.dashboardUrl}`);
14006
- logOut("runner: refresh token revoked");
14007
- } else {
14008
- logError(
14009
- `runner: revoke failed${revokeMessage ? `: ${revokeMessage}` : ""}`
14010
- );
14011
- logOut(
14012
- "runner: WARNING \u2014 refresh token may still be valid until you revoke it from the dashboard UI."
14013
- );
14014
- }
14015
- }
14016
- removeConfig();
14017
- removeMirror();
14018
- if (flags.json) {
14019
- logOut(
14020
- JSON.stringify({
14021
- ok: true,
14022
- runner: stopResult,
14023
- revoke: {
14024
- ok: revokeOk,
14025
- ...revokeMessage ? { message: revokeMessage } : {}
14026
- }
14027
- })
14028
- );
14029
- } else {
14030
- logOut(
14031
- `runner: unpaired (credentials removed${stopResult.wasRunning ? ", runner stopped" : ""})`
14032
- );
14033
- }
14034
- return 0;
14035
- }
14036
- logError(`Unknown runner subcommand: ${subcommand}`);
14037
- logError(USAGE3);
14038
- return 2;
14039
- }
14040
- function resolveRunnerEntry() {
14041
- let here;
14042
- try {
14043
- here = fileURLToPath(import.meta.url);
14044
- } catch {
14045
- return null;
14046
- }
14047
- const candidates = [
14048
- path6.join(path6.dirname(here), "runner.js"),
14049
- // When invoked via `npm run start`, `here` may be the unbundled source
14050
- // path. Walk up until we hit a `dist/` sibling.
14051
- path6.join(path6.dirname(here), "..", "..", "..", "dist", "runner.js")
14052
- ];
14053
- for (const candidate of candidates) {
14054
- try {
14055
- fs6.accessSync(candidate, fs6.constants.R_OK);
14056
- return candidate;
14057
- } catch {
14058
- }
14059
- }
14060
- return null;
14061
- }
14062
- var RUNNER_ENV_ALLOWLIST = [
14063
- "HOME",
14064
- "PATH",
14065
- "LANG",
14066
- "LC_ALL",
14067
- "ATHENA_DASHBOARD_ORIGIN"
14068
- ];
14069
- function buildRunnerEnv(env2) {
14070
- const out = {};
14071
- for (const key of RUNNER_ENV_ALLOWLIST) {
14072
- if (env2[key] !== void 0) out[key] = env2[key];
14073
- }
14074
- for (const [key, value] of Object.entries(env2)) {
14075
- if (key.startsWith("XDG_") && value !== void 0) {
14076
- out[key] = value;
14077
- }
14078
- }
14079
- return out;
14080
- }
14081
- function readRunnerPid(paths) {
14082
- const current = readPidLock(paths.pidPath);
14083
- if (current.state === "held") return { ...current, pidPath: paths.pidPath };
14084
- const legacy = readPidLock(paths.legacyPidPath);
14085
- if (legacy.state === "held") return { ...legacy, pidPath: paths.legacyPidPath };
14086
- return { ...current, pidPath: paths.pidPath };
14087
- }
14088
- function isProcessAlive(pid) {
14089
- try {
14090
- process.kill(pid, 0);
14091
- return true;
14092
- } catch (err) {
14093
- return err.code === "EPERM";
14094
- }
14095
- }
14096
- var SOCKET_PROBE_TIMEOUT_MS = 1e4;
14097
- var SOCKET_PROBE_INTERVAL_MS = 200;
14098
- async function defaultStartDetachedRunner(opts) {
14099
- const paths = runnerStatePaths();
14100
- const existing = readRunnerPid(paths);
14101
- if (existing.state === "held") {
14102
- return {
14103
- ok: false,
14104
- pid: existing.pid,
14105
- message: alreadyRunningMessage(existing.pid, existing.pidPath)
14106
- };
14107
- }
14108
- const entry = resolveRunnerEntry();
14109
- if (!entry) {
14110
- return { ok: false, message: "cannot resolve runner.js entry path" };
14111
- }
14112
- let child;
14113
- try {
14114
- child = spawn(process.execPath, [entry], {
14115
- detached: true,
14116
- stdio: "ignore",
14117
- env: buildRunnerEnv(process.env)
14118
- });
14119
- } catch (err) {
14120
- return {
14121
- ok: false,
14122
- message: err instanceof Error ? err.message : String(err)
14123
- };
14124
- }
14125
- child.unref();
14126
- let earlyExit;
14127
- let spawnError;
14128
- const onError = (err) => {
14129
- spawnError = err.message;
14130
- };
14131
- const onExit = (code, signal) => {
14132
- earlyExit = `runner exited early (code=${code ?? "null"}, signal=${signal ?? "null"})`;
14133
- };
14134
- child.once("error", onError);
14135
- child.once("exit", onExit);
14136
- const probeDeadline = Date.now() + SOCKET_PROBE_TIMEOUT_MS;
14137
- let connected = false;
14138
- let everReached = false;
14139
- let pid;
14140
- while (Date.now() < probeDeadline) {
14141
- if (spawnError) break;
14142
- if (earlyExit) break;
14143
- const report = defaultReadRunnerStatus();
14144
- if (report.running) {
14145
- everReached = true;
14146
- pid = report.status.pid;
14147
- if (report.status.socketConnected) {
14148
- connected = true;
14149
- break;
14150
- }
14151
- }
14152
- await new Promise((r) => setTimeout(r, SOCKET_PROBE_INTERVAL_MS));
14153
- }
14154
- child.off("error", onError);
14155
- child.off("exit", onExit);
14156
- if (spawnError) {
14157
- return { ok: false, message: `runner failed to start: ${spawnError}` };
14158
- }
14159
- if (earlyExit && !connected) {
14160
- return { ok: false, message: earlyExit };
14161
- }
14162
- if (!connected && !everReached) {
14163
- return {
14164
- ok: false,
14165
- message: `runner did not report status within ${SOCKET_PROBE_TIMEOUT_MS}ms (see ${paths.logPath})`
14166
- };
14167
- }
14168
- opts.log(
14169
- connected ? `runner: socket verified${pid !== void 0 ? ` (pid ${pid})` : ""}` : "runner: started but socket not yet connected"
14170
- );
14171
- return {
14172
- ok: true,
14173
- connected,
14174
- ...pid !== void 0 ? { pid } : {},
14175
- message: connected ? "started, socket verified" : "started, socket not yet verified"
14176
- };
14177
- }
14178
- async function defaultStopRunner(opts = {}) {
14179
- const paths = runnerStatePaths();
14180
- const existing = readRunnerPid(paths);
14181
- if (existing.state !== "held") {
14182
- return { ok: true, wasRunning: false, message: "runner not running" };
14183
- }
14184
- try {
14185
- process.kill(existing.pid, "SIGTERM");
14186
- } catch (err) {
14187
- if (err.code === "ESRCH") {
14188
- return { ok: true, wasRunning: false, message: "runner not running" };
14189
- }
14190
- return {
14191
- ok: false,
14192
- wasRunning: true,
14193
- message: err instanceof Error ? err.message : String(err)
14194
- };
14195
- }
14196
- const deadline = Date.now() + (opts.timeoutMs ?? 5e3);
14197
- while (Date.now() < deadline) {
14198
- const after = readPidLock(existing.pidPath);
14199
- if (after.state !== "held" || after.pid !== existing.pid) {
14200
- return { ok: true, wasRunning: true };
14201
- }
14202
- await new Promise((r) => setTimeout(r, 100));
14203
- }
14204
- return { ok: false, wasRunning: true, message: "runner did not exit in time" };
14205
- }
14206
- function defaultReadRunnerStatus() {
14207
- const paths = runnerStatePaths();
14208
- const existing = readRunnerPid(paths);
14209
- if (existing.state !== "held") {
14210
- return { running: false, error: "runner not running" };
14211
- }
14212
- const status = readRunnerStatusFile(paths.statusPath);
14213
- if (!status || status.pid !== existing.pid || !isProcessAlive(status.pid)) {
14214
- return {
14215
- running: false,
14216
- error: `runner pid ${existing.pid} is alive but has not written ${paths.statusPath} yet`
14217
- };
14218
- }
14219
- return { running: true, status };
14220
- }
14221
- async function defaultTailRunnerLog(opts) {
14222
- const paths = runnerStatePaths();
14223
- let stream2 = null;
14224
- let watcher = null;
14225
- try {
14226
- const stat = fs6.statSync(paths.logPath);
14227
- const size = stat.size;
14228
- const buf = Buffer.alloc(Math.min(size, opts.tail * 1024));
14229
- const fd = fs6.openSync(paths.logPath, "r");
14230
- try {
14231
- fs6.readSync(fd, buf, 0, buf.length, Math.max(0, size - buf.length));
14232
- } finally {
14233
- fs6.closeSync(fd);
14234
- }
14235
- const lines = buf.toString("utf-8").split("\n").filter((l) => l.length > 0);
14236
- const tail = lines.slice(-opts.tail);
14237
- for (const line of tail) {
14238
- process.stdout.write(line + "\n");
14239
- }
14240
- if (!opts.follow) return 0;
14241
- } catch (err) {
14242
- if (err.code === "ENOENT") {
14243
- process.stderr.write(
14244
- `runner logs: log file ${paths.logPath} does not exist yet
14245
- `
14246
- );
14247
- return opts.follow ? 0 : 1;
14248
- }
14249
- throw err;
14250
- }
14251
- let position = fs6.statSync(paths.logPath).size;
14252
- return await new Promise((resolve) => {
14253
- let pollTimer = null;
14254
- const drain = () => {
14255
- try {
14256
- const stat = fs6.statSync(paths.logPath);
14257
- if (stat.size < position) {
14258
- position = 0;
14259
- }
14260
- if (stat.size > position) {
14261
- const fd = fs6.openSync(paths.logPath, "r");
14262
- const buf = Buffer.alloc(stat.size - position);
14263
- try {
14264
- fs6.readSync(fd, buf, 0, buf.length, position);
14265
- } finally {
14266
- fs6.closeSync(fd);
14267
- }
14268
- position = stat.size;
14269
- process.stdout.write(buf);
14270
- }
14271
- } catch (err) {
14272
- if (err.code !== "ENOENT") {
14273
- process.stderr.write(
14274
- `runner logs: tail error: ${err instanceof Error ? err.message : String(err)}
14275
- `
14276
- );
14277
- }
14278
- }
14279
- };
14280
- try {
14281
- watcher = fs6.watch(paths.logPath, { persistent: true }, () => {
14282
- drain();
14283
- });
14284
- } catch {
14285
- pollTimer = setInterval(drain, 500);
14286
- pollTimer.unref();
14287
- }
14288
- const onSignal = () => {
14289
- if (watcher) {
14290
- watcher.close();
14291
- watcher = null;
14292
- }
14293
- if (pollTimer) {
14294
- clearInterval(pollTimer);
14295
- pollTimer = null;
14296
- }
14297
- if (stream2) {
14298
- stream2.close();
14299
- stream2 = null;
14300
- }
14301
- resolve(0);
14302
- };
14303
- process.once("SIGINT", onSignal);
14304
- process.once("SIGTERM", onSignal);
14305
- });
14306
- }
14307
- function formatDuration2(seconds) {
14308
- if (!Number.isFinite(seconds) || seconds < 0) return "?";
14309
- if (seconds < 60) return `${seconds}s`;
14310
- if (seconds < 3600) {
14311
- const m = Math.floor(seconds / 60);
14312
- const s = seconds % 60;
14313
- return s > 0 ? `${m}m${s}s` : `${m}m`;
14314
- }
14315
- if (seconds < 86400) {
14316
- const h2 = Math.floor(seconds / 3600);
14317
- const m = Math.floor(seconds % 3600 / 60);
14318
- return m > 0 ? `${h2}h${m}m` : `${h2}h`;
14319
- }
14320
- const d = Math.floor(seconds / 86400);
14321
- const h = Math.floor(seconds % 86400 / 3600);
14322
- return h > 0 ? `${d}d${h}h` : `${d}d`;
14323
- }
14324
- async function fetchRunnerHealth(fetchImpl, dashboardUrl, runnerId, token) {
14325
- const url = new URL(
14326
- `/api/runners/${encodeURIComponent(runnerId)}`,
14327
- dashboardUrl
14328
- ).toString();
14329
- let response;
14330
- try {
14331
- response = await fetchImpl(url, {
14332
- method: "GET",
14333
- headers: {
14334
- authorization: `Bearer ${token.accessToken}`,
14335
- accept: "application/json"
14336
- }
14337
- });
14338
- } catch (err) {
14339
- return {
14340
- id: runnerId,
14341
- matches: false,
14342
- error: `request failed: ${err instanceof Error ? err.message : String(err)}`
14343
- };
14344
- }
14345
- if (!response.ok) {
14346
- return {
14347
- id: runnerId,
14348
- matches: false,
14349
- error: `dashboard returned ${response.status}`
14350
- };
14351
- }
14352
- let body;
14353
- try {
14354
- body = await response.json();
14355
- } catch (err) {
14356
- return {
14357
- id: runnerId,
14358
- matches: false,
14359
- error: `invalid response body: ${err instanceof Error ? err.message : String(err)}`
14360
- };
14361
- }
14362
- const obj = typeof body === "object" && body !== null ? body : {};
14363
- const executionTarget = typeof obj["executionTarget"] === "string" ? obj["executionTarget"] : void 0;
14364
- const remoteInstanceId = typeof obj["remoteInstanceId"] === "string" ? obj["remoteInstanceId"] : void 0;
14365
- const matches = executionTarget === "remote" && remoteInstanceId === token.instanceId;
14366
- const reasons = [];
14367
- if (executionTarget !== "remote") {
14368
- reasons.push(
14369
- `executionTarget=${executionTarget ?? "unset"} (expected "remote")`
14370
- );
14371
- }
14372
- if (remoteInstanceId !== token.instanceId) {
14373
- reasons.push(
14374
- `remoteInstanceId=${remoteInstanceId ?? "unset"} (expected "${token.instanceId}")`
14375
- );
14376
- }
14377
- return {
14378
- id: runnerId,
14379
- matches,
14380
- ...executionTarget !== void 0 ? { executionTarget } : {},
14381
- ...remoteInstanceId !== void 0 ? { remoteInstanceId } : {},
14382
- ...reasons.length > 0 ? { error: reasons.join("; ") } : {}
14383
- };
14384
- }
14385
- function defaultInstallServiceUnit() {
14386
- const entry = resolveRunnerEntry();
14387
- if (!entry) {
14388
- return {
14389
- ok: false,
14390
- platform: "unsupported",
14391
- message: "cannot resolve runner.js entry path"
14392
- };
14393
- }
14394
- return installServiceUnit({
14395
- runnerEntry: entry,
14396
- nodeBinary: process.execPath
14397
- });
14398
- }
14399
- function compareSemver(a, b) {
14400
- const parseN = (s) => {
14401
- const parts = s.replace(/^v/, "").split("-")[0].split(".");
14402
- return [
14403
- Number.parseInt(parts[0] ?? "0", 10) || 0,
14404
- Number.parseInt(parts[1] ?? "0", 10) || 0,
14405
- Number.parseInt(parts[2] ?? "0", 10) || 0
14406
- ];
14407
- };
14408
- const av = parseN(a);
14409
- const bv = parseN(b);
14410
- for (let i = 0; i < 3; i += 1) {
14411
- if (av[i] < bv[i]) return -1;
14412
- if (av[i] > bv[i]) return 1;
14413
- }
14414
- return 0;
14415
- }
14416
- async function safeReadError(response) {
14417
- try {
14418
- const text = await response.text();
14419
- if (text.length === 0) return "";
14420
- try {
14421
- const parsed = JSON.parse(text);
14422
- if (typeof parsed === "object" && parsed !== null && typeof parsed["error"] === "string") {
14423
- return parsed["error"];
14424
- }
14425
- } catch {
14426
- }
14427
- return text.length > 200 ? text.slice(0, 200) + "\u2026" : text;
14428
- } catch {
14429
- return "";
14430
- }
14431
- }
14432
- function parsePairResponse(raw) {
14433
- if (typeof raw !== "object" || raw === null) {
14434
- throw new Error("expected object");
14435
- }
14436
- const obj = raw;
14437
- const instanceId = obj["instanceId"];
14438
- const refreshToken = obj["refreshToken"];
14439
- if (typeof instanceId !== "string" || instanceId.length === 0) {
14440
- throw new Error("missing instanceId");
14441
- }
14442
- if (typeof refreshToken !== "string" || refreshToken.length === 0) {
14443
- throw new Error("missing refreshToken");
14444
- }
14445
- return {
14446
- instanceId,
14447
- refreshToken,
14448
- ...typeof obj["jti"] === "string" ? { jti: obj["jti"] } : {},
14449
- ...typeof obj["accessToken"] === "string" ? { accessToken: obj["accessToken"] } : {},
14450
- ...typeof obj["expiresInSec"] === "number" ? { expiresInSec: obj["expiresInSec"] } : {},
14451
- ...Array.isArray(obj["runners"]) ? {
14452
- runners: obj["runners"].map(parsePairedRunner).filter((runner) => runner !== null)
14453
- } : {},
14454
- ...typeof obj["requiredCliVersion"] === "string" ? { requiredCliVersion: obj["requiredCliVersion"] } : {},
14455
- ...typeof obj["capabilityAck"] === "object" && obj["capabilityAck"] !== null ? { capabilityAck: parseCapabilityAck(obj["capabilityAck"]) } : {}
14456
- };
14457
- }
14458
- function parseCapabilityAck(raw) {
14459
- if (typeof raw !== "object" || raw === null) return {};
14460
- const obj = raw;
14461
- const ack = {};
14462
- if (typeof obj["runtimeDaemon"] === "boolean") {
14463
- ack.runtimeDaemon = obj["runtimeDaemon"];
14464
- }
14465
- if (typeof obj["instanceSocket"] === "boolean") {
14466
- ack.instanceSocket = obj["instanceSocket"];
14467
- }
14468
- return ack;
14469
- }
14470
- function parsePairedRunner(raw) {
14471
- if (typeof raw !== "object" || raw === null) return null;
14472
- const obj = raw;
14473
- const runnerId = obj["runnerId"];
14474
- if (typeof runnerId !== "string" || runnerId.length === 0) return null;
14475
- return {
14476
- runnerId,
14477
- ...typeof obj["name"] === "string" ? { name: obj["name"] } : {},
14478
- ...typeof obj["executionTarget"] === "string" ? { executionTarget: obj["executionTarget"] } : {},
14479
- ...typeof obj["remoteInstanceId"] === "string" ? { remoteInstanceId: obj["remoteInstanceId"] } : {}
14480
- };
14481
- }
14482
-
14483
- // src/app/entry/dashboardCommand.ts
14484
- var DASHBOARD_DEPRECATION_NOTICE = "drisp dashboard is deprecated and is removed in 0.7.0; use drisp runner instead.";
14485
- function mapDashboardToRunner(input) {
14486
- const { subcommand, subcommandArgs, flags } = input;
14487
- const runner = (next, args = [], extraFlags = {}) => ({
14488
- kind: "runner",
14489
- input: {
14490
- subcommand: next,
14491
- subcommandArgs: args,
14492
- flags: { ...flags, ...extraFlags }
14493
- }
14494
- });
14495
- if (subcommand === "" || subcommand === "help" || subcommand === "--help") {
14496
- return runner("help");
14497
- }
14498
- if (subcommand === "connect") return runner("", subcommandArgs);
14499
- if (subcommand === "daemon") {
14500
- const [mode, ...rest] = subcommandArgs;
14501
- switch (mode) {
14502
- case "foreground":
14503
- return runner("", rest);
14504
- case "start":
14505
- return runner("", rest, { detach: true });
14506
- case "stop":
14507
- case "restart":
14508
- case "install":
14509
- return runner(mode, rest);
14510
- case "reload":
14511
- return {
14512
- kind: "noop",
14513
- message: 'runner: reload is a no-op (the runner re-reads its pairing on every reconnect); use "drisp runner restart" to cycle it.'
14514
- };
14515
- default:
14516
- return runner("daemon", subcommandArgs);
14517
- }
14518
- }
14519
- return runner(subcommand, subcommandArgs);
14520
- }
14521
- async function runDashboardCommand(input, deps = {}) {
14522
- const logOut = deps.logOut ?? ((m) => process.stdout.write(m + "\n"));
14523
- const logError = deps.logError ?? ((m) => process.stderr.write(m + "\n"));
14524
- logError(DASHBOARD_DEPRECATION_NOTICE);
14525
- const mapped = mapDashboardToRunner(input);
14526
- if (mapped.kind === "noop") {
14527
- logOut(mapped.message);
14528
- return 0;
14529
- }
14530
- return runRunnerCommand(mapped.input, deps);
13513
+ throw new Error(`No Runner on this machine holds Run ${runId}.`);
14531
13514
  }
14532
13515
 
14533
13516
  // src/app/entry/doctorCommand.ts
@@ -14535,9 +13518,9 @@ import process7 from "process";
14535
13518
 
14536
13519
  // src/harnesses/claude/system/doctorEnvironment.ts
14537
13520
  import { execFileSync } from "child_process";
14538
- import * as fs7 from "fs";
14539
- import * as os6 from "os";
14540
- import * as path7 from "path";
13521
+ import * as fs5 from "fs";
13522
+ import * as os4 from "os";
13523
+ import * as path5 from "path";
14541
13524
  var MANAGED_POLICY_KEYS = [
14542
13525
  "forceLoginMethod",
14543
13526
  "forceLoginOrgUUID",
@@ -14633,20 +13616,20 @@ function tryRichAuth(claudeBinary) {
14633
13616
  }
14634
13617
  }
14635
13618
  function collectEnvironment(options = {}) {
14636
- const platform = options.platform ?? process.platform;
13619
+ const platform2 = options.platform ?? process.platform;
14637
13620
  const env2 = options.env ?? process.env;
14638
- const homeDir = options.homeDir ?? os6.homedir();
13621
+ const homeDir = options.homeDir ?? os4.homedir();
14639
13622
  const cwd = options.cwd ?? process.cwd();
14640
13623
  const resolveBinary = options.resolveClaudeBinaryFn ?? resolveClaudeBinary;
14641
13624
  const detectVersion = options.detectClaudeVersionFn ?? detectClaudeVersion;
14642
13625
  const runAuthStatus = options.runClaudeAuthStatusFn ?? runClaudeAuthStatus;
14643
13626
  const resolveForwarder = options.resolveHookForwarderCommandFn ?? resolveHookForwarderCommand;
14644
- const statFn = options.statFn ?? ((p) => fs7.statSync(p));
14645
- const readFileFn = options.readFileFn ?? ((p) => fs7.readFileSync(p, "utf8"));
13627
+ const statFn = options.statFn ?? ((p) => fs5.statSync(p));
13628
+ const readFileFn = options.readFileFn ?? ((p) => fs5.readFileSync(p, "utf8"));
14646
13629
  const settingsPaths = resolveClaudeSettingsSurfacePaths(
14647
13630
  homeDir,
14648
13631
  cwd,
14649
- platform,
13632
+ platform2,
14650
13633
  env2
14651
13634
  );
14652
13635
  const claudeBinary = resolveBinary();
@@ -14687,14 +13670,14 @@ function collectEnvironment(options = {}) {
14687
13670
  });
14688
13671
  let globalConfigPresent = false;
14689
13672
  try {
14690
- statFn(path7.join(homeDir, ".claude.json"));
13673
+ statFn(path5.join(homeDir, ".claude.json"));
14691
13674
  globalConfigPresent = true;
14692
13675
  } catch {
14693
13676
  globalConfigPresent = false;
14694
13677
  }
14695
13678
  const hookForwarder = resolveForwarder();
14696
13679
  return {
14697
- platform,
13680
+ platform: platform2,
14698
13681
  cwd,
14699
13682
  claudeBinary,
14700
13683
  claudeVersion,
@@ -15167,20 +14150,20 @@ function runRunsCommand(input) {
15167
14150
  // src/app/entry/cli.tsx
15168
14151
  import { jsx as jsx24 } from "react/jsx-runtime";
15169
14152
  function resolvePackageJsonPath(entryUrl) {
15170
- let currentDir = path8.dirname(fileURLToPath2(entryUrl));
14153
+ let currentDir = path6.dirname(fileURLToPath(entryUrl));
15171
14154
  for (; ; ) {
15172
- const candidatePath = path8.join(currentDir, "package.json");
15173
- if (fs8.existsSync(candidatePath)) {
14155
+ const candidatePath = path6.join(currentDir, "package.json");
14156
+ if (fs6.existsSync(candidatePath)) {
15174
14157
  return candidatePath;
15175
14158
  }
15176
- const parentDir = path8.dirname(currentDir);
14159
+ const parentDir = path6.dirname(currentDir);
15177
14160
  if (parentDir === currentDir) {
15178
14161
  throw new Error("Could not locate package.json for CLI metadata.");
15179
14162
  }
15180
14163
  currentDir = parentDir;
15181
14164
  }
15182
14165
  }
15183
- var require2 = createRequire2(import.meta.url);
14166
+ var require2 = createRequire(import.meta.url);
15184
14167
  var { version } = require2(resolvePackageJsonPath(import.meta.url));
15185
14168
  var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
15186
14169
  "setup",
@@ -15221,7 +14204,7 @@ async function flushTelemetryOptOut(config) {
15221
14204
  deviceId: deviceId2,
15222
14205
  telemetryEnabled: config.telemetry,
15223
14206
  appVersion: version,
15224
- os: `${os7.platform()}-${os7.arch()}`
14207
+ os: `${os5.platform()}-${os5.arch()}`
15225
14208
  });
15226
14209
  trackTelemetryOptedOut();
15227
14210
  await shutdownTelemetry();
@@ -15339,9 +14322,8 @@ var cli = meow(
15339
14322
  mcp <sub> Manage personal MCP servers (add, remove, list)
15340
14323
  skill <sub> Manage personal skills (install, remove, list)
15341
14324
  marketplace <sub> Manage marketplace sources (add, refresh, remove, list)
15342
- runner [sub] Run the runner that pairs this machine with the hub and executes its Runs
15343
- (no subcommand: foreground; --detach: background; status, stop, restart,
15344
- logs, runs, install, pair, unpair, refresh, doctor, list)
14325
+ runner [sub] Run the runner that connects this machine to a Team and executes its Runs
14326
+ (no subcommand: foreground; connect [code] --hub <url> [--replace], disconnect [team], status, recover, help)
15345
14327
  telemetry [action] Manage anonymous telemetry (enable/disable/status)
15346
14328
  doctor Diagnose Claude headless setup (use with --harness=claude)
15347
14329
 
@@ -15369,9 +14351,6 @@ var cli = meow(
15369
14351
  --ephemeral Do not persist Athena session data (run mode)
15370
14352
  --timeout-ms Hard timeout for the run in milliseconds
15371
14353
  --workflow Override the active workflow for this run only (no config change)
15372
- --detach Start the runner in the background (runner)
15373
- --url Dashboard origin (runner pair)
15374
- --name Friendly machine name (runner pair)
15375
14354
  --dry-run Print resolved bootstrap (workflow, isolation, plugins, harness) and exit (run mode)
15376
14355
  --project Scope workflow command to project config (workflow use)
15377
14356
  --global Scope workflow command to global config (workflow use, default)
@@ -15466,33 +14445,21 @@ var cli = meow(
15466
14445
  workflow: {
15467
14446
  type: "string"
15468
14447
  },
15469
- url: {
15470
- type: "string"
15471
- },
15472
- name: {
15473
- type: "string"
15474
- },
15475
- runner: {
14448
+ hub: {
15476
14449
  type: "string"
15477
14450
  },
15478
- tail: {
15479
- type: "number"
15480
- },
15481
- follow: {
14451
+ foreground: {
15482
14452
  type: "boolean",
15483
14453
  default: false
15484
14454
  },
15485
- detach: {
14455
+ replace: {
15486
14456
  type: "boolean",
15487
14457
  default: false
15488
14458
  },
15489
- active: {
14459
+ detach: {
15490
14460
  type: "boolean",
15491
14461
  default: false
15492
14462
  },
15493
- limit: {
15494
- type: "number"
15495
- },
15496
14463
  dryRun: {
15497
14464
  type: "boolean",
15498
14465
  default: false
@@ -15520,7 +14487,7 @@ var cli = meow(
15520
14487
  }
15521
14488
  );
15522
14489
  async function main() {
15523
- const projectDir = path8.resolve(cli.flags.projectDir);
14490
+ const projectDir = path6.resolve(cli.flags.projectDir);
15524
14491
  const [command, ...commandArgs] = cli.input;
15525
14492
  const isRunCommand = command === "run" || command === "exec";
15526
14493
  if (command && !KNOWN_COMMANDS.has(command)) {
@@ -15570,7 +14537,7 @@ Available commands: ${[...KNOWN_COMMANDS].join(", ")}`
15570
14537
  await exitWith(1);
15571
14538
  return;
15572
14539
  }
15573
- const { default: WorkflowInstallWizard } = await import("./WorkflowInstallWizard-R76OV74M.js");
14540
+ const { default: WorkflowInstallWizard } = await import("./WorkflowInstallWizard-TQ4SF5KB.js");
15574
14541
  const { waitUntilExit } = render(
15575
14542
  /* @__PURE__ */ jsx24(
15576
14543
  WorkflowInstallWizard,
@@ -15632,23 +14599,18 @@ Available commands: ${[...KNOWN_COMMANDS].join(", ")}`
15632
14599
  }
15633
14600
  if (command === "runner" || command === "dashboard") {
15634
14601
  const [subcommand = "", ...subcommandArgs] = commandArgs;
15635
- const input = {
15636
- subcommand,
15637
- subcommandArgs,
15638
- flags: {
15639
- url: typeof cli.flags.url === "string" ? cli.flags.url : void 0,
15640
- name: typeof cli.flags.name === "string" ? cli.flags.name : void 0,
15641
- runner: typeof cli.flags.runner === "string" ? cli.flags.runner : void 0,
15642
- json: Boolean(cli.flags.json),
15643
- ...cli.flags.detach ? { detach: true } : {},
15644
- ...typeof cli.flags.tail === "number" ? { tail: cli.flags.tail } : {},
15645
- ...cli.flags.follow ? { follow: true } : {},
15646
- ...cli.flags.active ? { active: true } : {},
15647
- ...typeof cli.flags.limit === "number" ? { limit: cli.flags.limit } : {}
15648
- }
15649
- };
15650
14602
  await exitWith(
15651
- command === "runner" ? await runRunnerCommand(input) : await runDashboardCommand(input)
14603
+ await runRunnerCommandV2({
14604
+ subcommand,
14605
+ subcommandArgs,
14606
+ flags: {
14607
+ json: Boolean(cli.flags.json),
14608
+ ...cli.flags.detach ? { detach: true } : {},
14609
+ ...typeof cli.flags.hub === "string" ? { hub: cli.flags.hub } : {},
14610
+ ...cli.flags.foreground ? { foreground: true } : {},
14611
+ ...cli.flags.replace ? { replace: true } : {}
14612
+ }
14613
+ })
15652
14614
  );
15653
14615
  return;
15654
14616
  }
@@ -15725,8 +14687,8 @@ Available commands: ${[...KNOWN_COMMANDS].join(", ")}`
15725
14687
  const showSetup = isRunCommand ? false : shouldShowSetup({
15726
14688
  cliInput: cli.input,
15727
14689
  setupComplete: globalConfig.setupComplete,
15728
- globalConfigExists: fs8.existsSync(
15729
- path8.join(os7.homedir(), ".config", "athena", "config.json")
14690
+ globalConfigExists: fs6.existsSync(
14691
+ path6.join(os5.homedir(), ".config", "athena", "config.json")
15730
14692
  )
15731
14693
  });
15732
14694
  let runtimeConfig;
@@ -15783,7 +14745,7 @@ Available commands: ${[...KNOWN_COMMANDS].join(", ")}`
15783
14745
  deviceId: resolvedDeviceId,
15784
14746
  telemetryEnabled: globalConfig.telemetry,
15785
14747
  appVersion: version,
15786
- os: `${os7.platform()}-${os7.arch()}`
14748
+ os: `${os5.platform()}-${os5.arch()}`
15787
14749
  });
15788
14750
  trackAppLaunched({ version, harness: runtimeConfig.harness });
15789
14751
  if (!isRunCommand && isFirstRun && globalConfig.telemetry !== false) {