@drisp/cli 0.4.4 → 0.4.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
@@ -1,9 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import "./chunk-HXBCZAP7.js";
3
- import {
4
- channelSidecarDir,
5
- rotateGatewayToken
6
- } from "./chunk-6TJHAUNB.js";
7
3
  import {
8
4
  CREDENTIAL_SOURCES_TRIED,
9
5
  EXEC_EXIT_CODE,
@@ -15,6 +11,8 @@ import {
15
11
  buildProbeConfigs,
16
12
  classifyFailure,
17
13
  connect,
14
+ createDashboardDecisionInbox,
15
+ createDashboardFeedPublisher,
18
16
  createFeedMapper,
19
17
  createRelayPermissionCallback,
20
18
  createRelayQuestionCallback,
@@ -51,7 +49,6 @@ import {
51
49
  processRegistry,
52
50
  progressGlyphs,
53
51
  readGatewayClientConfig,
54
- readPidLock,
55
52
  register,
56
53
  registerCleanupOnExit,
57
54
  resolveClaudeBinary,
@@ -70,10 +67,19 @@ import {
70
67
  todoGlyphSet,
71
68
  writeGatewayClientConfig,
72
69
  wsClientOptionsForEndpoint
73
- } from "./chunk-JAPBSL7D.js";
70
+ } from "./chunk-TQKNZNDP.js";
74
71
  import {
75
72
  generateId as generateId2
76
73
  } from "./chunk-BTKQ67RE.js";
74
+ import {
75
+ rotateGatewayToken
76
+ } from "./chunk-D4W7RB25.js";
77
+ import {
78
+ readAttachmentMirror,
79
+ readPidLock,
80
+ removeAttachmentMirror,
81
+ writeAttachmentMirror
82
+ } from "./chunk-ZVOGOZNT.js";
77
83
  import {
78
84
  dashboardClientConfigPath,
79
85
  disableTelemetry,
@@ -94,7 +100,7 @@ import {
94
100
  trackTelemetryOptedOut,
95
101
  writeDashboardClientConfig,
96
102
  writeGatewayTrace
97
- } from "./chunk-4CRZXLIP.js";
103
+ } from "./chunk-WRHKXH5M.js";
98
104
  import {
99
105
  McpOptionsStep,
100
106
  StepSelector,
@@ -129,7 +135,7 @@ import {
129
135
  useWorkflowSessionController,
130
136
  writeGlobalConfig,
131
137
  writeProjectConfig
132
- } from "./chunk-5VK2ZMVV.js";
138
+ } from "./chunk-A54HGVML.js";
133
139
 
134
140
  // src/app/entry/cli.tsx
135
141
  import { render } from "ink";
@@ -1739,14 +1745,65 @@ function useFeed(runtime, messages = [], initialAllowedTools, sessionStore, opti
1739
1745
  const abortRef = useRef(new AbortController());
1740
1746
  const feedEventsRef = useRef([]);
1741
1747
  const notifiedRuntimeErrorRef = useRef(null);
1748
+ const dashboardFeedPublisher = useMemo3(
1749
+ () => options?.dashboardFeedPublisher ?? createDashboardFeedPublisher(),
1750
+ [options?.dashboardFeedPublisher]
1751
+ );
1752
+ const dashboardDecisionInboxRef = useRef(
1753
+ options?.dashboardDecisionInbox ?? null
1754
+ );
1742
1755
  rulesRef.current = rules;
1743
1756
  feedEventsRef.current = feedEvents;
1744
1757
  sessionStoreRef.current = sessionStore;
1758
+ if (options?.dashboardDecisionInbox) {
1759
+ dashboardDecisionInboxRef.current = options.dashboardDecisionInbox;
1760
+ }
1761
+ const resolveAthenaSessionId = useCallback5(
1762
+ (fallback) => {
1763
+ if (options?.athenaSessionId) return options.athenaSessionId;
1764
+ try {
1765
+ const sessionId = sessionStoreRef.current?.getAthenaSession().id;
1766
+ if (sessionId) return sessionId;
1767
+ } catch {
1768
+ }
1769
+ return fallback;
1770
+ },
1771
+ [options?.athenaSessionId]
1772
+ );
1745
1773
  useEffect(() => {
1746
1774
  return () => {
1747
1775
  sessionStoreRef.current = void 0;
1748
1776
  };
1749
1777
  }, []);
1778
+ useEffect(() => {
1779
+ const athenaSessionId = resolveAthenaSessionId();
1780
+ if (!athenaSessionId) return;
1781
+ if (!dashboardDecisionInboxRef.current) {
1782
+ if (!readDashboardClientConfig()) return;
1783
+ dashboardDecisionInboxRef.current = createDashboardDecisionInbox();
1784
+ }
1785
+ const inbox = dashboardDecisionInboxRef.current;
1786
+ const applyPending = () => {
1787
+ const rows = inbox.pendingForSession({ athenaSessionId, limit: 25 });
1788
+ for (const row of rows) {
1789
+ runtime.sendDecision(row.requestId, row.decision);
1790
+ inbox.markConsumed({ id: row.id });
1791
+ }
1792
+ };
1793
+ applyPending();
1794
+ const interval = setInterval(
1795
+ applyPending,
1796
+ options?.dashboardDecisionPollIntervalMs ?? 1e3
1797
+ );
1798
+ return () => {
1799
+ clearInterval(interval);
1800
+ };
1801
+ }, [
1802
+ runtime,
1803
+ options?.dashboardDecisionInbox,
1804
+ options?.dashboardDecisionPollIntervalMs,
1805
+ resolveAthenaSessionId
1806
+ ]);
1750
1807
  const resetSession = useCallback5(() => {
1751
1808
  const newMapper = createFeedMapper();
1752
1809
  mapperRef.current = newMapper;
@@ -1857,18 +1914,37 @@ function useFeed(runtime, messages = [], initialAllowedTools, sessionStore, opti
1857
1914
  },
1858
1915
  [runtime, dequeueQuestion]
1859
1916
  );
1860
- const emitNotification = useCallback5((message, title) => {
1861
- const mapper = mapperRef.current;
1862
- const syntheticRuntime = buildSyntheticNotificationEvent(
1863
- mapper,
1864
- message,
1865
- title
1866
- );
1867
- const newEvents = mapper.mapEvent(syntheticRuntime);
1868
- if (!abortRef.current.signal.aborted) {
1869
- feedStoreRef.current.pushEvents(newEvents);
1870
- }
1871
- }, []);
1917
+ const publishDashboardFeedEvents = useCallback5(
1918
+ (feedEventsToPublish, runtimeEvent) => {
1919
+ if (feedEventsToPublish.length === 0) return;
1920
+ const athenaSessionId = resolveAthenaSessionId(
1921
+ feedEventsToPublish[0]?.session_id ?? runtimeEvent?.sessionId
1922
+ );
1923
+ if (!athenaSessionId) return;
1924
+ dashboardFeedPublisher.publish({
1925
+ origin: options?.dashboardOrigin ?? "local",
1926
+ athenaSessionId,
1927
+ feedEvents: feedEventsToPublish
1928
+ });
1929
+ },
1930
+ [dashboardFeedPublisher, options?.dashboardOrigin, resolveAthenaSessionId]
1931
+ );
1932
+ const emitNotification = useCallback5(
1933
+ (message, title) => {
1934
+ const mapper = mapperRef.current;
1935
+ const syntheticRuntime = buildSyntheticNotificationEvent(
1936
+ mapper,
1937
+ message,
1938
+ title
1939
+ );
1940
+ const newEvents = mapper.mapEvent(syntheticRuntime);
1941
+ if (!abortRef.current.signal.aborted) {
1942
+ feedStoreRef.current.pushEvents(newEvents);
1943
+ publishDashboardFeedEvents(newEvents, syntheticRuntime);
1944
+ }
1945
+ },
1946
+ [publishDashboardFeedEvents]
1947
+ );
1872
1948
  const refreshRuntimeStatus = useCallback5(
1873
1949
  (notify = false) => {
1874
1950
  const nextIsServerRunning = runtime.getStatus() === "running";
@@ -1953,6 +2029,7 @@ function useFeed(runtime, messages = [], initialAllowedTools, sessionStore, opti
1953
2029
  }
1954
2030
  }
1955
2031
  feedStoreRef.current.pushEvents(newFeedEvents);
2032
+ publishDashboardFeedEvents(newFeedEvents, runtimeEvent);
1956
2033
  }
1957
2034
  } finally {
1958
2035
  doneCause();
@@ -1988,6 +2065,7 @@ function useFeed(runtime, messages = [], initialAllowedTools, sessionStore, opti
1988
2065
  });
1989
2066
  if (feedEvent) {
1990
2067
  feedStoreRef.current.pushEvents([feedEvent]);
2068
+ publishDashboardFeedEvents([feedEvent]);
1991
2069
  if (feedEvent.kind === "permission.decision" && feedEvent.cause?.hook_request_id) {
1992
2070
  dequeuePermission(feedEvent.cause.hook_request_id);
1993
2071
  }
@@ -2011,7 +2089,8 @@ function useFeed(runtime, messages = [], initialAllowedTools, sessionStore, opti
2011
2089
  dequeueQuestion,
2012
2090
  refreshRuntimeStatus,
2013
2091
  options?.relayPermission,
2014
- options?.relayQuestion
2092
+ options?.relayQuestion,
2093
+ publishDashboardFeedEvents
2015
2094
  ]);
2016
2095
  const items = useMemo3(() => {
2017
2096
  const done = startPerfStage("state.derive", {
@@ -2108,6 +2187,7 @@ function HookProvider({
2108
2187
  runtimeFactory = createRuntime,
2109
2188
  allowedTools,
2110
2189
  athenaSessionId,
2190
+ attachmentId,
2111
2191
  children
2112
2192
  }) {
2113
2193
  const runtime = useMemo4(
@@ -2155,6 +2235,7 @@ function HookProvider({
2155
2235
  void startSessionBridge({
2156
2236
  runtimeId: athenaSessionId,
2157
2237
  defaultAgentId: "main",
2238
+ ...attachmentId !== void 0 ? { attachmentId } : {},
2158
2239
  signal: controller.signal
2159
2240
  }).then((bridge) => {
2160
2241
  if (!bridge) return;
@@ -2171,7 +2252,7 @@ function HookProvider({
2171
2252
  return null;
2172
2253
  });
2173
2254
  };
2174
- }, [athenaSessionId]);
2255
+ }, [athenaSessionId, attachmentId]);
2175
2256
  useEffect2(() => {
2176
2257
  return () => {
2177
2258
  sessionStore.close();
@@ -10948,6 +11029,350 @@ function executeCommand(command, args, ctx) {
10948
11029
  }
10949
11030
  }
10950
11031
 
11032
+ // src/app/runner/assignmentHandler.ts
11033
+ function parseRunSpec(value) {
11034
+ if (typeof value !== "object" || value === null) return null;
11035
+ const obj = value;
11036
+ const prompt = obj["prompt"];
11037
+ if (typeof prompt !== "string" || prompt.trim().length === 0) return null;
11038
+ const env2 = obj["env"];
11039
+ const workflow = obj["workflow"];
11040
+ return {
11041
+ prompt,
11042
+ sessionId: typeof obj["sessionId"] === "string" && obj["sessionId"].length > 0 ? obj["sessionId"] : void 0,
11043
+ projectDir: typeof obj["projectDir"] === "string" && obj["projectDir"].length > 0 ? obj["projectDir"] : void 0,
11044
+ workflow: typeof workflow === "object" && workflow !== null && typeof workflow["ref"] === "string" ? { ref: workflow["ref"] } : void 0,
11045
+ env: typeof env2 === "object" && env2 !== null ? Object.fromEntries(
11046
+ Object.entries(env2).filter(
11047
+ (entry) => typeof entry[1] === "string"
11048
+ )
11049
+ ) : void 0,
11050
+ timeoutSec: typeof obj["timeoutSec"] === "number" && Number.isFinite(obj["timeoutSec"]) ? obj["timeoutSec"] : void 0
11051
+ };
11052
+ }
11053
+ function workflowNameFromRef(ref) {
11054
+ if (!ref) return void 0;
11055
+ const [name] = ref.split("@", 1);
11056
+ return name && name.length > 0 ? name : void 0;
11057
+ }
11058
+ function eventKindOf(event) {
11059
+ if (event.type === "exec.completed") {
11060
+ const data = event.data;
11061
+ return data?.success === false ? "error" : "completion";
11062
+ }
11063
+ return typeof event.type === "string" && event.type.length > 0 ? event.type : "progress";
11064
+ }
11065
+ function eventPayloadOf(event) {
11066
+ if (event.type === "exec.completed") {
11067
+ const data = event.data;
11068
+ if (data?.success === false) {
11069
+ return {
11070
+ ...typeof event.data === "object" && event.data !== null ? event.data : {},
11071
+ message: typeof data.failure === "object" && data.failure !== null && typeof data.failure.message === "string" ? data.failure.message : "remote execution failed"
11072
+ };
11073
+ }
11074
+ }
11075
+ return event.data ?? null;
11076
+ }
11077
+ function withEnv(env2, fn) {
11078
+ if (!env2 || Object.keys(env2).length === 0) return fn();
11079
+ const previous = /* @__PURE__ */ new Map();
11080
+ for (const [key, value] of Object.entries(env2)) {
11081
+ previous.set(key, process.env[key]);
11082
+ process.env[key] = value;
11083
+ }
11084
+ return fn().finally(() => {
11085
+ for (const [key, value] of previous) {
11086
+ if (value === void 0) {
11087
+ delete process.env[key];
11088
+ } else {
11089
+ process.env[key] = value;
11090
+ }
11091
+ }
11092
+ });
11093
+ }
11094
+ async function executeAssignment(input) {
11095
+ const {
11096
+ envelope,
11097
+ bridge,
11098
+ dispatchId,
11099
+ location,
11100
+ projectDir: fallbackProjectDir = process.cwd(),
11101
+ runExecFn = runExec,
11102
+ bootstrapRuntimeConfigFn = bootstrapRuntimeConfig,
11103
+ now = Date.now,
11104
+ abortSignal
11105
+ } = input;
11106
+ const runId = envelope.runId;
11107
+ let seq = 0;
11108
+ let terminalSent = false;
11109
+ let deferredFailedCompletion = null;
11110
+ let lastTerminalFailureMessage = null;
11111
+ const nextSeq = () => {
11112
+ seq += 1;
11113
+ return seq;
11114
+ };
11115
+ const sendProgress = async (kind, payload, ts = now()) => {
11116
+ await bridge.sendRunEvent({
11117
+ location,
11118
+ runId,
11119
+ seq: nextSeq(),
11120
+ ts,
11121
+ kind,
11122
+ payload
11123
+ });
11124
+ };
11125
+ const sendTerminal = async (eventKind, payload, ts = now()) => {
11126
+ if (terminalSent) return;
11127
+ terminalSent = true;
11128
+ const envelopeText = JSON.stringify({
11129
+ kind: "run_event",
11130
+ runId,
11131
+ seq: nextSeq(),
11132
+ ts,
11133
+ eventKind,
11134
+ payload
11135
+ });
11136
+ await bridge.completeTurn({
11137
+ dispatchId,
11138
+ location,
11139
+ text: envelopeText,
11140
+ idempotencyKey: `run_event:${runId}:terminal`
11141
+ });
11142
+ };
11143
+ await sendProgress("progress", { message: "assignment received" });
11144
+ const spec = parseRunSpec(envelope.runSpec);
11145
+ if (!spec) {
11146
+ await sendTerminal("error", { message: "remote assignment missing prompt" });
11147
+ return;
11148
+ }
11149
+ const projectDir = spec.projectDir ?? fallbackProjectDir;
11150
+ let runtimeConfig;
11151
+ try {
11152
+ runtimeConfig = bootstrapRuntimeConfigFn({
11153
+ projectDir,
11154
+ showSetup: false,
11155
+ isolationPreset: "minimal",
11156
+ workflowOverride: workflowNameFromRef(spec.workflow?.ref)
11157
+ });
11158
+ } catch (err) {
11159
+ await sendTerminal("error", {
11160
+ message: err instanceof Error ? err.message : String(err)
11161
+ });
11162
+ return;
11163
+ }
11164
+ for (const warning of runtimeConfig.warnings) {
11165
+ await sendProgress("warning", { message: warning });
11166
+ }
11167
+ let buffered = "";
11168
+ const pendingProgress = [];
11169
+ const stdout = {
11170
+ write(chunk) {
11171
+ buffered += chunk;
11172
+ let newline = buffered.indexOf("\n");
11173
+ while (newline >= 0) {
11174
+ const line = buffered.slice(0, newline).trim();
11175
+ buffered = buffered.slice(newline + 1);
11176
+ if (line.length > 0) {
11177
+ try {
11178
+ const event = JSON.parse(line);
11179
+ const data = event.data;
11180
+ if (event.type === "exec.completed" && data?.success === false) {
11181
+ deferredFailedCompletion = event;
11182
+ } else if (event.type === "exec.completed") {
11183
+ deferredFailedCompletion = event;
11184
+ } else {
11185
+ pendingProgress.push(
11186
+ sendProgress(eventKindOf(event), eventPayloadOf(event), now())
11187
+ );
11188
+ }
11189
+ } catch {
11190
+ pendingProgress.push(sendProgress("progress", { line }));
11191
+ }
11192
+ }
11193
+ newline = buffered.indexOf("\n");
11194
+ }
11195
+ return true;
11196
+ }
11197
+ };
11198
+ const stderr = {
11199
+ write(chunk) {
11200
+ const text = chunk.trim();
11201
+ if (text.length > 0) pendingProgress.push(sendProgress("stderr", { text }));
11202
+ return true;
11203
+ }
11204
+ };
11205
+ try {
11206
+ await withEnv(spec.env, async () => {
11207
+ const result = await runExecFn({
11208
+ prompt: spec.prompt,
11209
+ projectDir,
11210
+ harness: runtimeConfig.harness,
11211
+ athenaSessionId: spec.sessionId ?? `athena-${runId}`,
11212
+ isolationConfig: runtimeConfig.isolationConfig,
11213
+ pluginMcpConfig: runtimeConfig.pluginMcpConfig,
11214
+ workflow: runtimeConfig.workflow,
11215
+ workflowPlan: runtimeConfig.workflowPlan,
11216
+ json: true,
11217
+ verbose: false,
11218
+ ephemeral: false,
11219
+ timeoutMs: spec.timeoutSec ? spec.timeoutSec * 1e3 : void 0,
11220
+ signal: abortSignal,
11221
+ stdout,
11222
+ stderr
11223
+ });
11224
+ await Promise.all(pendingProgress);
11225
+ if (deferredFailedCompletion) {
11226
+ const data = typeof deferredFailedCompletion.data === "object" && deferredFailedCompletion.data !== null ? deferredFailedCompletion.data : {};
11227
+ const ts = typeof deferredFailedCompletion.ts === "number" ? deferredFailedCompletion.ts : now();
11228
+ const success = data.success !== false;
11229
+ const eventKind = success ? "completion" : "error";
11230
+ const message = !success ? result.failure?.message ?? eventPayloadOf(deferredFailedCompletion).message ?? "remote execution failed" : void 0;
11231
+ if (message) lastTerminalFailureMessage = message;
11232
+ await sendTerminal(
11233
+ eventKind,
11234
+ success ? {
11235
+ ...data,
11236
+ exitCode: result.exitCode,
11237
+ athenaSessionId: result.athenaSessionId,
11238
+ adapterSessionId: result.adapterSessionId,
11239
+ finalMessage: result.finalMessage,
11240
+ tokens: result.tokens,
11241
+ durationMs: result.durationMs,
11242
+ success: true
11243
+ } : {
11244
+ ...data,
11245
+ success: result.success,
11246
+ exitCode: result.exitCode,
11247
+ athenaSessionId: result.athenaSessionId,
11248
+ adapterSessionId: result.adapterSessionId,
11249
+ finalMessage: result.finalMessage,
11250
+ tokens: result.tokens,
11251
+ durationMs: result.durationMs,
11252
+ message
11253
+ },
11254
+ ts
11255
+ );
11256
+ return;
11257
+ }
11258
+ if (result.failure && result.failure.message !== lastTerminalFailureMessage) {
11259
+ await sendTerminal("error", {
11260
+ success: result.success,
11261
+ exitCode: result.exitCode,
11262
+ athenaSessionId: result.athenaSessionId,
11263
+ adapterSessionId: result.adapterSessionId,
11264
+ finalMessage: result.finalMessage,
11265
+ tokens: result.tokens,
11266
+ durationMs: result.durationMs,
11267
+ message: result.failure.message
11268
+ });
11269
+ return;
11270
+ }
11271
+ await sendTerminal("completion", {
11272
+ success: result.success,
11273
+ exitCode: result.exitCode,
11274
+ athenaSessionId: result.athenaSessionId,
11275
+ adapterSessionId: result.adapterSessionId,
11276
+ finalMessage: result.finalMessage,
11277
+ tokens: result.tokens,
11278
+ durationMs: result.durationMs
11279
+ });
11280
+ });
11281
+ } catch (err) {
11282
+ await Promise.all(pendingProgress);
11283
+ await sendTerminal("error", {
11284
+ message: err instanceof Error ? err.message : String(err)
11285
+ });
11286
+ }
11287
+ }
11288
+
11289
+ // src/app/runner/envelope.ts
11290
+ function parseRunnerEnvelope(text) {
11291
+ let value;
11292
+ try {
11293
+ value = JSON.parse(text);
11294
+ } catch {
11295
+ return null;
11296
+ }
11297
+ if (typeof value !== "object" || value === null) return null;
11298
+ const obj = value;
11299
+ const runId = obj["runId"];
11300
+ if (typeof runId !== "string" || runId.length === 0) return null;
11301
+ const kind = obj["kind"];
11302
+ if (kind === "job_assignment") {
11303
+ return { kind, runId, runSpec: obj["runSpec"] };
11304
+ }
11305
+ if (kind === "cancel") {
11306
+ return { kind, runId };
11307
+ }
11308
+ return null;
11309
+ }
11310
+
11311
+ // src/app/runner/runnerSession.ts
11312
+ function createRunnerSession(opts) {
11313
+ const {
11314
+ bridge,
11315
+ projectDir,
11316
+ runExecFn = runExec,
11317
+ bootstrapRuntimeConfigFn = bootstrapRuntimeConfig,
11318
+ now = Date.now
11319
+ } = opts;
11320
+ const inflight = /* @__PURE__ */ new Map();
11321
+ function startAssignment(envelope, dispatchId, location) {
11322
+ const controller = new AbortController();
11323
+ inflight.set(envelope.runId, controller);
11324
+ return executeAssignment({
11325
+ envelope,
11326
+ bridge,
11327
+ dispatchId,
11328
+ location,
11329
+ projectDir,
11330
+ runExecFn,
11331
+ bootstrapRuntimeConfigFn,
11332
+ now,
11333
+ abortSignal: controller.signal
11334
+ }).finally(() => {
11335
+ if (inflight.get(envelope.runId) === controller) {
11336
+ inflight.delete(envelope.runId);
11337
+ }
11338
+ });
11339
+ }
11340
+ return {
11341
+ handleDispatch(input) {
11342
+ const envelope = parseRunnerEnvelope(input.text);
11343
+ if (!envelope) {
11344
+ return { recognised: false, completed: Promise.resolve() };
11345
+ }
11346
+ if (envelope.kind === "job_assignment") {
11347
+ const completed = startAssignment(
11348
+ envelope,
11349
+ input.dispatchId,
11350
+ input.location
11351
+ );
11352
+ return { recognised: true, completed };
11353
+ }
11354
+ const controller = inflight.get(envelope.runId);
11355
+ if (controller) controller.abort();
11356
+ return { recognised: true, completed: Promise.resolve() };
11357
+ }
11358
+ };
11359
+ }
11360
+
11361
+ // src/app/runner/dispatchRouter.ts
11362
+ function makeDispatchRouter(opts) {
11363
+ return (payload) => {
11364
+ if (opts.runnerSession) {
11365
+ const result = opts.runnerSession.handleDispatch({
11366
+ text: payload.inbound.text,
11367
+ dispatchId: payload.dispatchId,
11368
+ location: payload.inbound.location
11369
+ });
11370
+ if (result.recognised) return;
11371
+ }
11372
+ opts.fallback(payload);
11373
+ };
11374
+ }
11375
+
10951
11376
  // src/ui/components/SessionPicker.tsx
10952
11377
  import { useState as useState11 } from "react";
10953
11378
  import { Box as Box11, Text as Text17, useInput as useInput12, useStdout as useStdout5 } from "ink";
@@ -13318,17 +13743,25 @@ function AppContent({
13318
13743
  [addMessage, currentSessionId, feedEvents.length, spawnHarness]
13319
13744
  );
13320
13745
  submitDispatchAsTurnRef.current = submitDispatchAsTurn;
13746
+ const runnerSession = useMemo17(
13747
+ () => sessionBridge ? createRunnerSession({ bridge: sessionBridge, projectDir }) : null,
13748
+ [sessionBridge, projectDir]
13749
+ );
13321
13750
  useEffect14(() => {
13322
13751
  if (!sessionBridge) return;
13323
- const off = sessionBridge.onTurnDispatch((payload) => {
13324
- if (isHarnessRunningRef.current || pendingDispatchRef.current) {
13325
- queuedDispatchRef.current = payload;
13326
- return;
13752
+ const router = makeDispatchRouter({
13753
+ runnerSession,
13754
+ fallback: (payload) => {
13755
+ if (isHarnessRunningRef.current || pendingDispatchRef.current) {
13756
+ queuedDispatchRef.current = payload;
13757
+ return;
13758
+ }
13759
+ submitDispatchAsTurnRef.current?.(payload);
13327
13760
  }
13328
- submitDispatchAsTurnRef.current?.(payload);
13329
13761
  });
13762
+ const off = sessionBridge.onTurnDispatch(router);
13330
13763
  return off;
13331
- }, [sessionBridge]);
13764
+ }, [sessionBridge, runnerSession]);
13332
13765
  useEffect14(() => {
13333
13766
  if (isHarnessRunning) return;
13334
13767
  if (pendingDispatchRef.current) return;
@@ -14363,6 +14796,7 @@ function App({
14363
14796
  isolationPreset,
14364
14797
  ascii,
14365
14798
  athenaSessionId: initialAthenaSessionId,
14799
+ attachmentId,
14366
14800
  initialTelemetryDiagnosticsConsent
14367
14801
  }) {
14368
14802
  const [clearCount, setClearCount] = useState18(0);
@@ -14578,6 +15012,7 @@ function App({
14578
15012
  runtimeState.isolation?.allowedTools
14579
15013
  ),
14580
15014
  athenaSessionId,
15015
+ ...attachmentId !== void 0 ? { attachmentId } : {},
14581
15016
  children: /* @__PURE__ */ jsx24(
14582
15017
  AppContent,
14583
15018
  {
@@ -15414,744 +15849,385 @@ function runChannelCommand(input, deps = {}) {
15414
15849
 
15415
15850
  // src/app/entry/dashboardCommand.ts
15416
15851
  import crypto3 from "crypto";
15417
- import { spawn as spawn2 } from "child_process";
15418
- import fs7 from "fs";
15852
+ import { spawn } from "child_process";
15853
+ import fs6 from "fs";
15419
15854
  import { createRequire } from "module";
15420
15855
  import os5 from "os";
15421
- import path7 from "path";
15422
- import { fileURLToPath as fileURLToPath2 } from "url";
15856
+ import path6 from "path";
15857
+ import { fileURLToPath } from "url";
15423
15858
 
15424
- // src/app/entry/gatewayCommand.ts
15425
- import { spawn } from "child_process";
15859
+ // src/infra/daemon/serviceUnit.ts
15426
15860
  import fs5 from "fs";
15861
+ import os4 from "os";
15427
15862
  import path5 from "path";
15428
- import { fileURLToPath } from "url";
15429
- var USAGE4 = `Usage: athena-flow gateway <subcommand> [--json]
15430
-
15431
- Subcommands:
15432
- start Run the gateway daemon in foreground (only mode in this build).
15433
- Options: [--bind <host:port>] [--insecure]
15434
- [--tls-cert <path>] [--tls-key <path>]
15435
- [--grace-period-ms <n>]
15436
- status Print daemon pid, uptime, and version.
15437
- probe Send a ping RPC and report reachability + latency.
15438
- link Store a remote WS/WSS gateway endpoint for this user.
15439
- unlink Restore local UDS gateway mode for this user.
15440
- rotate-token Regenerate the gateway token file (server-side).
15441
- Restart the daemon to drop existing connections; clients
15442
- must re-run "athena gateway link --token <new>".
15443
- reload-channels Reload channel sidecars without restarting the daemon.
15444
- `;
15445
- function defaultResolveDaemonEntry() {
15446
- const here = path5.dirname(fileURLToPath(import.meta.url));
15447
- return path5.resolve(here, "athena-gateway.js");
15863
+ function installServiceUnit(options) {
15864
+ const platform = options.platform ?? process.platform;
15865
+ const env2 = options.env ?? process.env;
15866
+ const home = env2["HOME"] ?? os4.homedir();
15867
+ if (platform === "darwin") {
15868
+ const target = options.targetPath ?? path5.join(home, "Library", "LaunchAgents", "ai.drisp.daemon.plist");
15869
+ const paths = daemonStatePaths(env2);
15870
+ const plist = renderLaunchdPlist({
15871
+ label: "ai.drisp.daemon",
15872
+ nodeBinary: options.nodeBinary,
15873
+ daemonEntry: options.daemonEntry,
15874
+ workingDirectory: home,
15875
+ stdoutPath: paths.logPath,
15876
+ stderrPath: paths.logPath
15877
+ });
15878
+ writeIfChanged(target, plist);
15879
+ return {
15880
+ ok: true,
15881
+ platform: "darwin",
15882
+ path: target,
15883
+ loadCommand: `launchctl load -w ${target}`,
15884
+ startCommand: "launchctl start ai.drisp.daemon"
15885
+ };
15886
+ }
15887
+ if (platform === "linux") {
15888
+ const target = options.targetPath ?? path5.join(home, ".config", "systemd", "user", "drisp-daemon.service");
15889
+ const unit = renderSystemdUnit({
15890
+ description: "Drisp dashboard runtime daemon",
15891
+ nodeBinary: options.nodeBinary,
15892
+ daemonEntry: options.daemonEntry
15893
+ });
15894
+ writeIfChanged(target, unit);
15895
+ return {
15896
+ ok: true,
15897
+ platform: "linux",
15898
+ path: target,
15899
+ loadCommand: "systemctl --user daemon-reload",
15900
+ startCommand: "systemctl --user enable --now drisp-daemon.service"
15901
+ };
15902
+ }
15903
+ return {
15904
+ ok: false,
15905
+ platform: "unsupported",
15906
+ message: `service install not supported on ${platform}`
15907
+ };
15448
15908
  }
15449
- function readToken(tokenPath) {
15909
+ function writeIfChanged(target, content) {
15910
+ const dir = path5.dirname(target);
15911
+ fs5.mkdirSync(dir, { recursive: true, mode: 448 });
15912
+ let existing = null;
15450
15913
  try {
15451
- return fs5.readFileSync(tokenPath, "utf-8").trim();
15914
+ existing = fs5.readFileSync(target, "utf-8");
15452
15915
  } catch (err) {
15453
- const code = err.code;
15454
- if (code === "ENOENT") {
15455
- throw new Error(
15456
- `gateway token missing at ${tokenPath}. Start the daemon with "athena gateway start" first.`
15457
- );
15458
- }
15459
- throw err;
15916
+ if (err.code !== "ENOENT") throw err;
15460
15917
  }
15918
+ if (existing === content) return;
15919
+ fs5.writeFileSync(target, content, { mode: 384 });
15461
15920
  }
15462
- function flagJson(args) {
15463
- return args.includes("--json");
15464
- }
15465
- async function runGatewayCommand(input, deps = {}) {
15466
- const logOut = deps.logOut ?? ((m) => process.stdout.write(m + "\n"));
15467
- const logError = deps.logError ?? ((m) => process.stderr.write(m + "\n"));
15468
- const resolveDaemonEntry2 = deps.resolveDaemonEntry ?? defaultResolveDaemonEntry;
15469
- const resolveSocketPath = deps.resolveSocketPath ?? (() => resolveGatewayPaths().socketPath);
15470
- const resolveTokenPath = deps.resolveTokenPath ?? (() => resolveGatewayPaths().tokenPath);
15471
- const readClientConfig = deps.readClientConfig ?? readGatewayClientConfig;
15472
- const writeClientConfig = deps.writeClientConfig ?? ((config) => writeGatewayClientConfig(config));
15473
- const connectGateway = deps.connectGateway ?? defaultConnectGateway;
15474
- const spawnDaemon = deps.spawnDaemon ?? ((entry, args) => spawn(process.execPath, [entry, ...args], { stdio: "inherit" }));
15475
- const { subcommand, subcommandArgs } = input;
15476
- if (!subcommand || subcommand === "help" || subcommand === "--help") {
15477
- logOut(USAGE4);
15478
- return 0;
15479
- }
15480
- if (subcommand === "start") {
15481
- const entry = resolveDaemonEntry2();
15482
- const child = spawnDaemon(entry, subcommandArgs);
15483
- return await new Promise((resolve) => {
15484
- child.once("exit", (code) => resolve(code ?? 0));
15485
- child.once("error", (err) => {
15486
- logError(`gateway start: failed to spawn daemon: ${err.message}`);
15487
- resolve(1);
15488
- });
15489
- });
15490
- }
15491
- if (subcommand === "link") {
15492
- const parsed = parseLinkArgs(subcommandArgs);
15493
- if (!parsed.ok) {
15494
- logError(parsed.message);
15495
- return 2;
15921
+ function renderLaunchdPlist(input) {
15922
+ const argv = [input.nodeBinary, input.daemonEntry].map((s) => ` <string>${escapeXml(s)}</string>`).join("\n");
15923
+ return `<?xml version="1.0" encoding="UTF-8"?>
15924
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
15925
+ <plist version="1.0">
15926
+ <dict>
15927
+ <key>Label</key>
15928
+ <string>${escapeXml(input.label)}</string>
15929
+ <key>ProgramArguments</key>
15930
+ <array>
15931
+ ${argv}
15932
+ </array>
15933
+ <key>RunAtLoad</key>
15934
+ <true/>
15935
+ <key>KeepAlive</key>
15936
+ <dict>
15937
+ <key>SuccessfulExit</key>
15938
+ <false/>
15939
+ </dict>
15940
+ <key>WorkingDirectory</key>
15941
+ <string>${escapeXml(input.workingDirectory)}</string>
15942
+ <key>StandardOutPath</key>
15943
+ <string>${escapeXml(input.stdoutPath)}</string>
15944
+ <key>StandardErrorPath</key>
15945
+ <string>${escapeXml(input.stderrPath)}</string>
15946
+ <key>ProcessType</key>
15947
+ <string>Background</string>
15948
+ </dict>
15949
+ </plist>
15950
+ `;
15951
+ }
15952
+ function renderSystemdUnit(input) {
15953
+ return `[Unit]
15954
+ Description=${input.description}
15955
+ After=network-online.target
15956
+
15957
+ [Service]
15958
+ Type=simple
15959
+ ExecStart=${input.nodeBinary} ${input.daemonEntry}
15960
+ Restart=always
15961
+ RestartSec=5
15962
+ SuccessExitStatus=0
15963
+ StandardOutput=journal
15964
+ StandardError=journal
15965
+
15966
+ [Install]
15967
+ WantedBy=default.target
15968
+ `;
15969
+ }
15970
+ function escapeXml(input) {
15971
+ return input.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
15972
+ }
15973
+
15974
+ // src/app/entry/dashboardCommand.ts
15975
+ var USAGE4 = `Usage: athena dashboard <subcommand> [options]
15976
+
15977
+ Subcommands:
15978
+ pair <token> --url <dashboard-origin> [--name <machine-name>]
15979
+ Pair this machine with the dashboard, install the runtime
15980
+ daemon, and verify it reaches the dashboard socket.
15981
+ status Show pairing, runner binding, daemon process state, socket
15982
+ health, and token freshness. Non-zero on any unhealthy axis.
15983
+ logs [--tail N] [--follow]
15984
+ Tail the daemon log file at
15985
+ ~/.local/state/drisp/dashboard-daemon.log.
15986
+ runs [--active] [--limit N]
15987
+ List runs the daemon has handled (in-memory ring, last 100).
15988
+ refresh Mint a short-lived access token (rotates the refresh token).
15989
+ daemon foreground
15990
+ Run the daemon in the foreground (process supervisors,
15991
+ debugging).
15992
+ daemon start|stop|restart|reload
15993
+ Local IPC against the daemon UDS.
15994
+ daemon install
15995
+ Generate a launchd plist (macOS) or systemd user unit (linux)
15996
+ so the daemon starts automatically on login.
15997
+ doctor Verify pairing health. With --runner <id>, also confirms the
15998
+ runner is bound to this instance.
15999
+ list Print the local attachment mirror (the runners the dashboard
16000
+ reported as bound to this instance at the last pair/refresh).
16001
+ connect Deprecated alias for "daemon foreground".
16002
+ unpair Stop the daemon, revoke the refresh token, and remove the
16003
+ local config.
16004
+
16005
+ Options:
16006
+ --url <origin> Dashboard origin (required for pair)
16007
+ --runner <id> Runner id (optional for "doctor")
16008
+ --name <name> Friendly machine name (optional, defaults to hostname)
16009
+ --tail N Number of trailing log lines (default 20)
16010
+ --follow Stream new log lines until interrupted
16011
+ --active Show only active runs
16012
+ --limit N Cap the number of runs returned
16013
+ --json Emit machine-readable JSON output
16014
+ `;
16015
+ var require_ = createRequire(import.meta.url);
16016
+ var cachedVersion = null;
16017
+ function readPackageVersion() {
16018
+ if (cachedVersion !== null) return cachedVersion;
16019
+ try {
16020
+ const injected = "0.4.7";
16021
+ if (typeof injected === "string" && injected.length > 0) {
16022
+ cachedVersion = injected;
16023
+ return cachedVersion;
15496
16024
  }
15497
- writeClientConfig({
15498
- mode: "remote",
15499
- url: parsed.url,
15500
- token: parsed.token,
15501
- ...parsed.tlsCaPath !== void 0 ? { tlsCaPath: parsed.tlsCaPath } : {}
15502
- });
15503
- logOut(`gateway: linked remote endpoint ${parsed.url}`);
16025
+ } catch {
16026
+ }
16027
+ try {
16028
+ const pkg = require_("../../../package.json");
16029
+ cachedVersion = pkg.version ?? "0.0.0";
16030
+ } catch {
16031
+ cachedVersion = "0.0.0";
16032
+ }
16033
+ return cachedVersion;
16034
+ }
16035
+ function defaultFingerprint() {
16036
+ const seed = [
16037
+ os5.hostname(),
16038
+ os5.userInfo().username,
16039
+ os5.platform(),
16040
+ os5.arch()
16041
+ ].join("\0");
16042
+ return crypto3.createHash("sha256").update(seed).digest("hex");
16043
+ }
16044
+ function defaultHostInfo(name) {
16045
+ return {
16046
+ hostname: os5.hostname(),
16047
+ user: os5.userInfo().username,
16048
+ platform: os5.platform(),
16049
+ arch: os5.arch(),
16050
+ name: name ?? os5.hostname()
16051
+ };
16052
+ }
16053
+ async function runDashboardCommand(input, deps = {}) {
16054
+ const logOut = deps.logOut ?? ((m) => process.stdout.write(m + "\n"));
16055
+ const logError = deps.logError ?? ((m) => process.stderr.write(m + "\n"));
16056
+ const fetchImpl = deps.fetch ?? fetch;
16057
+ const now = deps.now ?? (() => Date.now());
16058
+ const fingerprint = deps.fingerprint ?? defaultFingerprint;
16059
+ const readConfig2 = deps.readConfig ?? (() => readDashboardClientConfig());
16060
+ const writeConfig = deps.writeConfig ?? ((c2) => writeDashboardClientConfig(c2));
16061
+ const removeConfig = deps.removeConfig ?? (() => removeDashboardClientConfig());
16062
+ const configPath = deps.configPath ?? (() => dashboardClientConfigPath());
16063
+ const readMirror = deps.readMirror ?? (() => readAttachmentMirror());
16064
+ const writeMirror = deps.writeMirror ?? ((m) => writeAttachmentMirror(m));
16065
+ const removeMirror = deps.removeMirror ?? (() => removeAttachmentMirror());
16066
+ const packageVersion = deps.packageVersion ?? readPackageVersion();
16067
+ const { subcommand, subcommandArgs, flags } = input;
16068
+ if (!subcommand || subcommand === "help" || subcommand === "--help") {
16069
+ logOut(USAGE4);
15504
16070
  return 0;
15505
16071
  }
15506
- if (subcommand === "rotate-token") {
15507
- const json = flagJson(subcommandArgs);
15508
- const extras = subcommandArgs.filter((a) => a !== "--json");
15509
- if (extras.length > 0) {
15510
- logError(`gateway rotate-token: unexpected argument ${extras[0]}`);
16072
+ if (subcommand === "pair") {
16073
+ const token = subcommandArgs[0];
16074
+ if (!token) {
16075
+ logError("dashboard pair: missing pairing token");
16076
+ logError(USAGE4);
15511
16077
  return 2;
15512
16078
  }
15513
- const tokenPath = resolveTokenPath();
16079
+ if (subcommandArgs.length > 1) {
16080
+ logError(`dashboard pair: unexpected argument ${subcommandArgs[1]}`);
16081
+ return 2;
16082
+ }
16083
+ if (!flags.url) {
16084
+ logError("dashboard pair: --url <dashboard-origin> is required");
16085
+ return 2;
16086
+ }
16087
+ let origin;
15514
16088
  try {
15515
- const newToken = rotateGatewayToken(tokenPath);
15516
- if (json) {
15517
- logOut(JSON.stringify({ ok: true, token: newToken, tokenPath }));
15518
- } else {
15519
- logOut(newToken);
15520
- logOut(
15521
- `gateway: rotated token at ${tokenPath}. Restart the daemon to drop existing connections, then re-run "athena gateway link --token <new>" on each client.`
15522
- );
15523
- }
15524
- return 0;
16089
+ origin = normalizeDashboardUrl(flags.url);
15525
16090
  } catch (err) {
15526
- const message = err instanceof Error ? err.message : String(err);
15527
- if (json) {
15528
- logOut(JSON.stringify({ ok: false, message }));
15529
- } else {
15530
- logError(`gateway rotate-token: ${message}`);
15531
- }
15532
- return 1;
15533
- }
15534
- }
15535
- if (subcommand === "unlink") {
15536
- if (subcommandArgs.length > 0) {
15537
- logError("gateway unlink does not accept arguments");
16091
+ logError(
16092
+ `dashboard pair: ${err instanceof Error ? err.message : String(err)}`
16093
+ );
15538
16094
  return 2;
15539
16095
  }
15540
- writeClientConfig({ mode: "local" });
15541
- logOut("gateway: using local gateway endpoint");
15542
- return 0;
15543
- }
15544
- if (subcommand === "probe") {
15545
- const json = flagJson(subcommandArgs);
15546
- const socketPath = resolveSocketPath();
15547
- const tokenPath = resolveTokenPath();
15548
- const endpoint = readClientConfig();
15549
- const startedAt = Date.now();
16096
+ const fp = fingerprint();
16097
+ const body = {
16098
+ token,
16099
+ fingerprint: fp,
16100
+ hostInfo: (deps.hostInfo ?? (() => defaultHostInfo(flags.name)))(),
16101
+ capabilities: {
16102
+ instanceSocket: true,
16103
+ runtimeDaemon: true,
16104
+ cliVersion: packageVersion,
16105
+ // Legacy field — older dashboards read `version`. Drop in a
16106
+ // follow-up release once the dashboard accepts only `cliVersion`.
16107
+ version: packageVersion
16108
+ }
16109
+ };
16110
+ let response;
15550
16111
  try {
15551
- const client = await connectGateway({
15552
- endpoint,
15553
- socketPath,
15554
- tokenPath,
15555
- timeoutMs: 3e3
16112
+ response = await fetchImpl(`${origin}/api/instances/pair`, {
16113
+ method: "POST",
16114
+ headers: { "content-type": "application/json" },
16115
+ body: JSON.stringify(body)
15556
16116
  });
15557
- const res = await client.request("ping", {});
15558
- client.close();
15559
- const latencyMs = Date.now() - startedAt;
15560
- if (json) {
15561
- logOut(
15562
- JSON.stringify({
15563
- ok: true,
15564
- reachable: true,
15565
- latency_ms: latencyMs,
15566
- daemon_pid: res.daemonPid,
15567
- daemon_uptime_ms: res.uptimeMs
15568
- })
15569
- );
15570
- } else {
15571
- logOut(
15572
- `gateway: reachable pid=${res.daemonPid} uptime=${res.uptimeMs}ms latency=${latencyMs}ms`
15573
- );
15574
- }
15575
- return 0;
15576
16117
  } catch (err) {
15577
- return reportProbeFailure(err, json, logOut, logError);
16118
+ logError(
16119
+ `dashboard pair: failed to reach ${origin}: ${err instanceof Error ? err.message : String(err)}`
16120
+ );
16121
+ return 1;
15578
16122
  }
15579
- }
15580
- if (subcommand === "reload-channels") {
15581
- const json = flagJson(subcommandArgs);
15582
- const extras = subcommandArgs.filter((a) => a !== "--json");
15583
- if (extras.length > 0) {
15584
- logError(`gateway reload-channels: unexpected argument ${extras[0]}`);
15585
- return 2;
16123
+ if (!response.ok) {
16124
+ const message = await safeReadError(response);
16125
+ logError(
16126
+ `dashboard pair: ${origin} returned ${response.status}${message ? ` \u2014 ${message}` : ""}`
16127
+ );
16128
+ return 1;
15586
16129
  }
15587
- const socketPath = resolveSocketPath();
15588
- const tokenPath = resolveTokenPath();
15589
- const endpoint = readClientConfig();
16130
+ let parsed;
15590
16131
  try {
15591
- const client = await connectGateway({
15592
- endpoint,
15593
- socketPath,
15594
- tokenPath,
15595
- timeoutMs: 3e3
15596
- });
15597
- const res = await client.request("channels.reload", {});
15598
- client.close();
15599
- if (json) {
15600
- logOut(JSON.stringify({ ok: true, ...res }));
15601
- } else if (res.results.length === 0) {
15602
- logOut("gateway: channels reloaded (no channel sidecars found)");
15603
- } else {
15604
- logOut("gateway: channels reloaded");
15605
- for (const result of res.results) {
15606
- logOut(formatChannelReloadResult(result));
15607
- }
15608
- }
15609
- return 0;
16132
+ parsed = parsePairResponse(await response.json());
15610
16133
  } catch (err) {
15611
- return reportProbeFailure(err, json, logOut, logError);
16134
+ logError(
16135
+ `dashboard pair: invalid response from ${origin}: ${err instanceof Error ? err.message : String(err)}`
16136
+ );
16137
+ return 1;
15612
16138
  }
15613
- }
15614
- if (subcommand === "status") {
15615
- const json = flagJson(subcommandArgs);
15616
- const socketPath = resolveSocketPath();
15617
- const tokenPath = resolveTokenPath();
15618
- const endpoint = readClientConfig();
16139
+ if (parsed.requiredCliVersion && compareSemver(packageVersion, parsed.requiredCliVersion) < 0) {
16140
+ logError(
16141
+ `dashboard pair: cli version ${packageVersion} is older than the dashboard's required >=${parsed.requiredCliVersion}.`
16142
+ );
16143
+ logError(
16144
+ "dashboard pair: upgrade with `npm i -g @drisp/cli` then re-run pair."
16145
+ );
16146
+ return 1;
16147
+ }
16148
+ const config = {
16149
+ dashboardUrl: origin,
16150
+ instanceId: parsed.instanceId,
16151
+ refreshToken: parsed.refreshToken,
16152
+ fingerprint: fp,
16153
+ pairedAt: now()
16154
+ };
16155
+ writeConfig(config);
15619
16156
  try {
15620
- const client = await connectGateway({
15621
- endpoint,
15622
- socketPath,
15623
- tokenPath,
15624
- timeoutMs: 3e3
16157
+ writeMirror({
16158
+ instanceId: parsed.instanceId,
16159
+ fetchedAt: now(),
16160
+ attachments: (parsed.runners ?? []).map((r) => ({
16161
+ runnerId: r.runnerId,
16162
+ ...r.name !== void 0 ? { name: r.name } : {},
16163
+ ...r.executionTarget !== void 0 ? { executionTarget: r.executionTarget } : {},
16164
+ ...r.remoteInstanceId !== void 0 ? { remoteInstanceId: r.remoteInstanceId } : {}
16165
+ }))
15625
16166
  });
15626
- const res = await client.request("status", {});
15627
- client.close();
15628
- if (json) {
15629
- logOut(JSON.stringify(res));
15630
- } else {
15631
- const listenerSummary = formatListenerSummary(res.listener);
15632
- const channelSummary = formatChannelSummary(res.channels);
15633
- const runtimeSummary = formatRuntimeSummary(res.runtimes[0]);
15634
- logOut(
15635
- `gateway: running pid=${res.daemonPid} uptime=${res.uptimeMs}ms version=${res.version} ${listenerSummary}${channelSummary}${runtimeSummary}`
15636
- );
15637
- }
15638
- return 0;
15639
16167
  } catch (err) {
15640
- return reportProbeFailure(err, json, logOut, logError);
16168
+ logError(
16169
+ `dashboard pair: failed to write attachment mirror: ${err instanceof Error ? err.message : String(err)}`
16170
+ );
15641
16171
  }
15642
- }
15643
- logError(`Unknown gateway subcommand: ${subcommand}`);
15644
- logError(USAGE4);
15645
- return 2;
15646
- }
15647
- async function defaultConnectGateway(opts) {
15648
- if (opts.endpoint.mode === "remote") {
15649
- return connect({
15650
- socketPath: opts.socketPath,
15651
- token: opts.endpoint.token,
15652
- timeoutMs: opts.timeoutMs,
15653
- transport: createWsClientTransport(
15654
- wsClientOptionsForEndpoint({
15655
- url: opts.endpoint.url,
15656
- timeoutMs: opts.timeoutMs,
15657
- tlsCaPath: opts.endpoint.tlsCaPath
15658
- })
15659
- )
16172
+ const daemonStart = await (deps.startRuntimeDaemon ?? defaultStartRuntimeDaemon)({
16173
+ log: (msg) => logOut(msg)
15660
16174
  });
15661
- }
15662
- return connect({
15663
- socketPath: opts.socketPath,
15664
- token: readToken(opts.tokenPath),
15665
- timeoutMs: opts.timeoutMs
15666
- });
15667
- }
15668
- function parseLinkArgs(args) {
15669
- const positional = [];
15670
- let token;
15671
- let tlsCaPath;
15672
- for (let i = 0; i < args.length; i += 1) {
15673
- const arg = args[i];
15674
- if (arg === "--token") {
15675
- const v = args[i + 1];
15676
- if (!v || v.startsWith("--")) {
15677
- return { ok: false, message: "gateway link --token requires a value" };
15678
- }
15679
- token = v;
15680
- i += 1;
15681
- continue;
15682
- }
15683
- if (arg === "--tls-ca") {
15684
- const v = args[i + 1];
15685
- if (!v || v.startsWith("--")) {
15686
- return { ok: false, message: "gateway link --tls-ca requires a path" };
15687
- }
15688
- tlsCaPath = v;
15689
- i += 1;
15690
- continue;
15691
- }
15692
- if (arg.startsWith("--token=")) {
15693
- token = arg.slice("--token=".length);
15694
- continue;
15695
- }
15696
- if (arg.startsWith("--tls-ca=")) {
15697
- tlsCaPath = arg.slice("--tls-ca=".length);
15698
- continue;
15699
- }
15700
- if (arg.startsWith("--")) {
15701
- return { ok: false, message: `gateway link: unknown option ${arg}` };
15702
- }
15703
- positional.push(arg);
15704
- }
15705
- const url = positional[0];
15706
- if (!url) {
15707
- return { ok: false, message: "gateway link requires a ws:// or wss:// URL" };
15708
- }
15709
- if (positional.length > 1) {
15710
- return { ok: false, message: "gateway link accepts exactly one URL" };
15711
- }
15712
- if (!isSupportedGatewayUrl(url)) {
15713
- return { ok: false, message: "gateway link URL must use ws:// or wss://" };
15714
- }
15715
- if (!token) {
15716
- return { ok: false, message: "gateway link requires --token <token>" };
15717
- }
15718
- if (tlsCaPath !== void 0 && tlsCaPath.length === 0) {
15719
- return { ok: false, message: "gateway link --tls-ca requires a path" };
15720
- }
15721
- return {
15722
- ok: true,
15723
- url,
15724
- token,
15725
- ...tlsCaPath !== void 0 ? { tlsCaPath } : {}
15726
- };
15727
- }
15728
- function reportProbeFailure(err, json, logOut, logError) {
15729
- const message = err instanceof Error ? err.message : String(err);
15730
- if (err instanceof GatewayUnreachableError) {
15731
- if (json) {
16175
+ if (flags.json) {
15732
16176
  logOut(
15733
16177
  JSON.stringify({
15734
- ok: false,
15735
- reachable: false,
15736
- reason: "unreachable",
15737
- message
16178
+ ok: true,
16179
+ instanceId: parsed.instanceId,
16180
+ dashboardUrl: origin,
16181
+ configPath: configPath(),
16182
+ daemon: daemonStart,
16183
+ ...parsed.runners ? { runners: parsed.runners } : {},
16184
+ ...parsed.capabilityAck ? { capabilityAck: parsed.capabilityAck } : {},
16185
+ ...parsed.requiredCliVersion ? { requiredCliVersion: parsed.requiredCliVersion } : {}
15738
16186
  })
15739
16187
  );
15740
16188
  } else {
15741
- logError(`gateway: not reachable \u2014 ${message}`);
16189
+ logOut(`dashboard: paired to ${origin} as ${parsed.instanceId}`);
16190
+ if (parsed.runners && parsed.runners.length > 0) {
16191
+ for (const runner of parsed.runners) {
16192
+ logOut(
16193
+ `dashboard: bound runner ${runner.name ?? runner.runnerId} (${runner.runnerId})`
16194
+ );
16195
+ }
16196
+ } else {
16197
+ logOut("dashboard: no runner bound to this pairing token.");
16198
+ logOut(
16199
+ "dashboard: bind a runner from runner settings, then this machine will receive its runs."
16200
+ );
16201
+ }
16202
+ if (parsed.capabilityAck === void 0) {
16203
+ logOut(
16204
+ "dashboard: dashboard did not echo capabilityAck (older server). Continuing."
16205
+ );
16206
+ }
16207
+ if (daemonStart.ok) {
16208
+ const status = daemonStart.alreadyRunning ? "runtime daemon already running, restarted with new token" : daemonStart.connected ? "runtime daemon connected (verified socket open)" : "runtime daemon started but did not reach the socket within 10s";
16209
+ logOut(`dashboard: ${status}`);
16210
+ if (!daemonStart.connected && !daemonStart.alreadyRunning) {
16211
+ logOut(
16212
+ "dashboard pair: pairing succeeded; tail logs with `drisp dashboard logs --follow`."
16213
+ );
16214
+ }
16215
+ } else {
16216
+ logError(
16217
+ `dashboard: runtime daemon did not start${daemonStart.message ? ` \u2014 ${daemonStart.message}` : ""}`
16218
+ );
16219
+ logOut(
16220
+ "dashboard pair: pairing succeeded; start the daemon with `drisp dashboard daemon start`."
16221
+ );
16222
+ }
16223
+ logOut("dashboard: ready. Click Run in the dashboard.");
15742
16224
  }
15743
- return 1;
16225
+ return 0;
15744
16226
  }
15745
- if (err instanceof GatewayUnauthorizedError) {
15746
- if (json) {
15747
- logOut(
15748
- JSON.stringify({
15749
- ok: false,
15750
- reachable: true,
15751
- reason: "unauthorized",
15752
- message
15753
- })
15754
- );
15755
- } else {
15756
- logError(`gateway: unauthorized \u2014 ${message}`);
15757
- }
15758
- return 1;
15759
- }
15760
- if (json) {
15761
- logOut(JSON.stringify({ ok: false, reason: "error", message }));
15762
- } else {
15763
- logError(`gateway: ${message}`);
15764
- }
15765
- return 1;
15766
- }
15767
- function formatRuntimeSummary(r) {
15768
- if (!r) return " runtime=<none>";
15769
- const lastRebindAt = r.binding.state !== "none" ? r.binding.lastRebindAt : void 0;
15770
- const rebind = lastRebindAt !== void 0 ? ` rebound=${formatElapsed(Date.now() - lastRebindAt)}` : "";
15771
- return ` runtime=${r.runtimeId} binding=${r.binding.state} pid=${r.pid}${rebind}`;
15772
- }
15773
- function formatChannelReloadResult(result) {
15774
- const suffix = result.reason ? `: ${result.reason}` : "";
15775
- return ` ${result.id} ${result.action}${suffix}`;
15776
- }
15777
- function formatChannelSummary(channels) {
15778
- if (channels.length === 0) return " channels=<none>";
15779
- const parts = channels.map((channel) => {
15780
- const note = channel.note ? `(${channel.note})` : "";
15781
- return `${channel.id}:${channel.state}${note}`;
15782
- });
15783
- return ` channels=${parts.join(",")}`;
15784
- }
15785
- function formatListenerSummary(listener) {
15786
- if (listener.kind === "uds") {
15787
- return `listener=uds:${listener.socketPath}`;
15788
- }
15789
- const flags = [];
15790
- if (listener.tls) flags.push("tls");
15791
- if (listener.insecure) flags.push("insecure");
15792
- const suffix = flags.length > 0 ? ` (${flags.join(",")})` : "";
15793
- return `listener=${listener.url}${suffix}`;
15794
- }
15795
-
15796
- // src/infra/daemon/serviceUnit.ts
15797
- import fs6 from "fs";
15798
- import os4 from "os";
15799
- import path6 from "path";
15800
- function installServiceUnit(options) {
15801
- const platform = options.platform ?? process.platform;
15802
- const env2 = options.env ?? process.env;
15803
- const home = env2["HOME"] ?? os4.homedir();
15804
- if (platform === "darwin") {
15805
- const target = options.targetPath ?? path6.join(home, "Library", "LaunchAgents", "ai.drisp.daemon.plist");
15806
- const paths = daemonStatePaths(env2);
15807
- const plist = renderLaunchdPlist({
15808
- label: "ai.drisp.daemon",
15809
- nodeBinary: options.nodeBinary,
15810
- daemonEntry: options.daemonEntry,
15811
- workingDirectory: home,
15812
- stdoutPath: paths.logPath,
15813
- stderrPath: paths.logPath
15814
- });
15815
- writeIfChanged(target, plist);
15816
- return {
15817
- ok: true,
15818
- platform: "darwin",
15819
- path: target,
15820
- loadCommand: `launchctl load -w ${target}`,
15821
- startCommand: "launchctl start ai.drisp.daemon"
15822
- };
15823
- }
15824
- if (platform === "linux") {
15825
- const target = options.targetPath ?? path6.join(home, ".config", "systemd", "user", "drisp-daemon.service");
15826
- const unit = renderSystemdUnit({
15827
- description: "Drisp dashboard runtime daemon",
15828
- nodeBinary: options.nodeBinary,
15829
- daemonEntry: options.daemonEntry
15830
- });
15831
- writeIfChanged(target, unit);
15832
- return {
15833
- ok: true,
15834
- platform: "linux",
15835
- path: target,
15836
- loadCommand: "systemctl --user daemon-reload",
15837
- startCommand: "systemctl --user enable --now drisp-daemon.service"
15838
- };
15839
- }
15840
- return {
15841
- ok: false,
15842
- platform: "unsupported",
15843
- message: `service install not supported on ${platform}`
15844
- };
15845
- }
15846
- function writeIfChanged(target, content) {
15847
- const dir = path6.dirname(target);
15848
- fs6.mkdirSync(dir, { recursive: true, mode: 448 });
15849
- let existing = null;
15850
- try {
15851
- existing = fs6.readFileSync(target, "utf-8");
15852
- } catch (err) {
15853
- if (err.code !== "ENOENT") throw err;
15854
- }
15855
- if (existing === content) return;
15856
- fs6.writeFileSync(target, content, { mode: 384 });
15857
- }
15858
- function renderLaunchdPlist(input) {
15859
- const argv = [input.nodeBinary, input.daemonEntry].map((s) => ` <string>${escapeXml(s)}</string>`).join("\n");
15860
- return `<?xml version="1.0" encoding="UTF-8"?>
15861
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
15862
- <plist version="1.0">
15863
- <dict>
15864
- <key>Label</key>
15865
- <string>${escapeXml(input.label)}</string>
15866
- <key>ProgramArguments</key>
15867
- <array>
15868
- ${argv}
15869
- </array>
15870
- <key>RunAtLoad</key>
15871
- <true/>
15872
- <key>KeepAlive</key>
15873
- <dict>
15874
- <key>SuccessfulExit</key>
15875
- <false/>
15876
- </dict>
15877
- <key>WorkingDirectory</key>
15878
- <string>${escapeXml(input.workingDirectory)}</string>
15879
- <key>StandardOutPath</key>
15880
- <string>${escapeXml(input.stdoutPath)}</string>
15881
- <key>StandardErrorPath</key>
15882
- <string>${escapeXml(input.stderrPath)}</string>
15883
- <key>ProcessType</key>
15884
- <string>Background</string>
15885
- </dict>
15886
- </plist>
15887
- `;
15888
- }
15889
- function renderSystemdUnit(input) {
15890
- return `[Unit]
15891
- Description=${input.description}
15892
- After=network-online.target
15893
-
15894
- [Service]
15895
- Type=simple
15896
- ExecStart=${input.nodeBinary} ${input.daemonEntry}
15897
- Restart=always
15898
- RestartSec=5
15899
- SuccessExitStatus=0
15900
- StandardOutput=journal
15901
- StandardError=journal
15902
-
15903
- [Install]
15904
- WantedBy=default.target
15905
- `;
15906
- }
15907
- function escapeXml(input) {
15908
- return input.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
15909
- }
15910
-
15911
- // src/app/entry/dashboardCommand.ts
15912
- var USAGE5 = `Usage: athena dashboard <subcommand> [options]
15913
-
15914
- Subcommands:
15915
- pair <token> --url <dashboard-origin> [--name <machine-name>]
15916
- Pair this machine with the dashboard, install the runtime
15917
- daemon, and verify it reaches the dashboard socket.
15918
- status Show pairing, runner binding, daemon process state, socket
15919
- health, and token freshness. Non-zero on any unhealthy axis.
15920
- logs [--tail N] [--follow]
15921
- Tail the daemon log file at
15922
- ~/.local/state/drisp/dashboard-daemon.log.
15923
- runs [--active] [--limit N]
15924
- List runs the daemon has handled (in-memory ring, last 100).
15925
- refresh Mint a short-lived access token (rotates the refresh token).
15926
- daemon foreground
15927
- Run the daemon in the foreground (process supervisors,
15928
- debugging).
15929
- daemon start|stop|restart|reload
15930
- Local IPC against the daemon UDS.
15931
- daemon install
15932
- Generate a launchd plist (macOS) or systemd user unit (linux)
15933
- so the daemon starts automatically on login.
15934
- doctor Verify pairing health. With --runner <id>, also confirms the
15935
- runner is bound to this instance.
15936
- console enable <runnerId>
15937
- Configure the console channel for a runner (opinionated;
15938
- writes the sidecar and reloads the gateway).
15939
- console link <runnerId>
15940
- Primitive: write ~/.config/athena/channels/console.json for
15941
- the given runner. Prefer "console enable".
15942
- connect Deprecated alias for "daemon foreground".
15943
- unpair Stop the daemon, revoke the refresh token, and remove the
15944
- local config.
15945
-
15946
- Options:
15947
- --url <origin> Dashboard origin (required for pair)
15948
- --runner <id> Runner id (required for "console enable"/"console
15949
- link", optional for "doctor")
15950
- --name <name> Friendly machine name (optional, defaults to hostname)
15951
- --tail N Number of trailing log lines (default 20)
15952
- --follow Stream new log lines until interrupted
15953
- --active Show only active runs
15954
- --limit N Cap the number of runs returned
15955
- --json Emit machine-readable JSON output
15956
- `;
15957
- var require_ = createRequire(import.meta.url);
15958
- var cachedVersion = null;
15959
- function readPackageVersion() {
15960
- if (cachedVersion !== null) return cachedVersion;
15961
- try {
15962
- const injected = "0.4.4";
15963
- if (typeof injected === "string" && injected.length > 0) {
15964
- cachedVersion = injected;
15965
- return cachedVersion;
15966
- }
15967
- } catch {
15968
- }
15969
- try {
15970
- const pkg = require_("../../../package.json");
15971
- cachedVersion = pkg.version ?? "0.0.0";
15972
- } catch {
15973
- cachedVersion = "0.0.0";
15974
- }
15975
- return cachedVersion;
15976
- }
15977
- function defaultFingerprint() {
15978
- const seed = [
15979
- os5.hostname(),
15980
- os5.userInfo().username,
15981
- os5.platform(),
15982
- os5.arch()
15983
- ].join("\0");
15984
- return crypto3.createHash("sha256").update(seed).digest("hex");
15985
- }
15986
- function defaultHostInfo(name) {
15987
- return {
15988
- hostname: os5.hostname(),
15989
- user: os5.userInfo().username,
15990
- platform: os5.platform(),
15991
- arch: os5.arch(),
15992
- name: name ?? os5.hostname()
15993
- };
15994
- }
15995
- async function runDashboardCommand(input, deps = {}) {
15996
- const logOut = deps.logOut ?? ((m) => process.stdout.write(m + "\n"));
15997
- const logError = deps.logError ?? ((m) => process.stderr.write(m + "\n"));
15998
- const fetchImpl = deps.fetch ?? fetch;
15999
- const now = deps.now ?? (() => Date.now());
16000
- const fingerprint = deps.fingerprint ?? defaultFingerprint;
16001
- const readConfig2 = deps.readConfig ?? (() => readDashboardClientConfig());
16002
- const writeConfig = deps.writeConfig ?? ((c2) => writeDashboardClientConfig(c2));
16003
- const removeConfig = deps.removeConfig ?? (() => removeDashboardClientConfig());
16004
- const configPath = deps.configPath ?? (() => dashboardClientConfigPath());
16005
- const packageVersion = deps.packageVersion ?? readPackageVersion();
16006
- const { subcommand, subcommandArgs, flags } = input;
16007
- if (!subcommand || subcommand === "help" || subcommand === "--help") {
16008
- logOut(USAGE5);
16009
- return 0;
16010
- }
16011
- if (subcommand === "pair") {
16012
- const token = subcommandArgs[0];
16013
- if (!token) {
16014
- logError("dashboard pair: missing pairing token");
16015
- logError(USAGE5);
16016
- return 2;
16017
- }
16018
- if (subcommandArgs.length > 1) {
16019
- logError(`dashboard pair: unexpected argument ${subcommandArgs[1]}`);
16020
- return 2;
16021
- }
16022
- if (!flags.url) {
16023
- logError("dashboard pair: --url <dashboard-origin> is required");
16024
- return 2;
16025
- }
16026
- let origin;
16027
- try {
16028
- origin = normalizeDashboardUrl(flags.url);
16029
- } catch (err) {
16030
- logError(
16031
- `dashboard pair: ${err instanceof Error ? err.message : String(err)}`
16032
- );
16033
- return 2;
16034
- }
16035
- const fp = fingerprint();
16036
- const body = {
16037
- token,
16038
- fingerprint: fp,
16039
- hostInfo: (deps.hostInfo ?? (() => defaultHostInfo(flags.name)))(),
16040
- capabilities: {
16041
- instanceSocket: true,
16042
- consoleAdapter: true,
16043
- runtimeDaemon: true,
16044
- cliVersion: packageVersion,
16045
- // Legacy field — older dashboards read `version`. Drop in a
16046
- // follow-up release once the dashboard accepts only `cliVersion`.
16047
- version: packageVersion
16048
- }
16049
- };
16050
- let response;
16051
- try {
16052
- response = await fetchImpl(`${origin}/api/instances/pair`, {
16053
- method: "POST",
16054
- headers: { "content-type": "application/json" },
16055
- body: JSON.stringify(body)
16056
- });
16057
- } catch (err) {
16058
- logError(
16059
- `dashboard pair: failed to reach ${origin}: ${err instanceof Error ? err.message : String(err)}`
16060
- );
16061
- return 1;
16062
- }
16063
- if (!response.ok) {
16064
- const message = await safeReadError(response);
16065
- logError(
16066
- `dashboard pair: ${origin} returned ${response.status}${message ? ` \u2014 ${message}` : ""}`
16067
- );
16068
- return 1;
16069
- }
16070
- let parsed;
16071
- try {
16072
- parsed = parsePairResponse(await response.json());
16073
- } catch (err) {
16074
- logError(
16075
- `dashboard pair: invalid response from ${origin}: ${err instanceof Error ? err.message : String(err)}`
16076
- );
16077
- return 1;
16078
- }
16079
- if (parsed.requiredCliVersion && compareSemver(packageVersion, parsed.requiredCliVersion) < 0) {
16080
- logError(
16081
- `dashboard pair: cli version ${packageVersion} is older than the dashboard's required >=${parsed.requiredCliVersion}.`
16082
- );
16083
- logError(
16084
- "dashboard pair: upgrade with `npm i -g @drisp/cli` then re-run pair."
16085
- );
16086
- return 1;
16087
- }
16088
- const config = {
16089
- dashboardUrl: origin,
16090
- instanceId: parsed.instanceId,
16091
- refreshToken: parsed.refreshToken,
16092
- fingerprint: fp,
16093
- pairedAt: now()
16094
- };
16095
- writeConfig(config);
16096
- const daemonStart = await (deps.startRuntimeDaemon ?? defaultStartRuntimeDaemon)({
16097
- log: (msg) => logOut(msg)
16098
- });
16099
- if (flags.json) {
16100
- logOut(
16101
- JSON.stringify({
16102
- ok: true,
16103
- instanceId: parsed.instanceId,
16104
- dashboardUrl: origin,
16105
- configPath: configPath(),
16106
- daemon: daemonStart,
16107
- ...parsed.runners ? { runners: parsed.runners } : {},
16108
- ...parsed.capabilityAck ? { capabilityAck: parsed.capabilityAck } : {},
16109
- ...parsed.requiredCliVersion ? { requiredCliVersion: parsed.requiredCliVersion } : {}
16110
- })
16111
- );
16112
- } else {
16113
- logOut(`dashboard: paired to ${origin} as ${parsed.instanceId}`);
16114
- if (parsed.runners && parsed.runners.length > 0) {
16115
- for (const runner of parsed.runners) {
16116
- logOut(
16117
- `dashboard: bound runner ${runner.name ?? runner.runnerId} (${runner.runnerId})`
16118
- );
16119
- }
16120
- } else {
16121
- logOut("dashboard: no runner bound to this pairing token.");
16122
- logOut(
16123
- "dashboard: bind a runner from runner settings, then this machine will receive its runs."
16124
- );
16125
- }
16126
- if (parsed.capabilityAck === void 0) {
16127
- logOut(
16128
- "dashboard: dashboard did not echo capabilityAck (older server). Continuing."
16129
- );
16130
- }
16131
- if (daemonStart.ok) {
16132
- const status = daemonStart.alreadyRunning ? "runtime daemon already running, restarted with new token" : daemonStart.connected ? "runtime daemon connected (verified socket open)" : "runtime daemon started but did not reach the socket within 10s";
16133
- logOut(`dashboard: ${status}`);
16134
- if (!daemonStart.connected && !daemonStart.alreadyRunning) {
16135
- logOut(
16136
- "dashboard pair: pairing succeeded; tail logs with `drisp dashboard logs --follow`."
16137
- );
16138
- }
16139
- } else {
16140
- logError(
16141
- `dashboard: runtime daemon did not start${daemonStart.message ? ` \u2014 ${daemonStart.message}` : ""}`
16142
- );
16143
- logOut(
16144
- "dashboard pair: pairing succeeded; start the daemon with `drisp dashboard daemon start`."
16145
- );
16146
- }
16147
- logOut("dashboard: ready. Click Run in the dashboard.");
16148
- }
16149
- return 0;
16150
- }
16151
- if (subcommand === "status") {
16152
- if (subcommandArgs.length > 0) {
16153
- logError(`dashboard status: unexpected argument ${subcommandArgs[0]}`);
16154
- return 2;
16227
+ if (subcommand === "status") {
16228
+ if (subcommandArgs.length > 0) {
16229
+ logError(`dashboard status: unexpected argument ${subcommandArgs[0]}`);
16230
+ return 2;
16155
16231
  }
16156
16232
  const config = readConfig2();
16157
16233
  if (!config) {
@@ -16738,149 +16814,79 @@ async function runDashboardCommand(input, deps = {}) {
16738
16814
  return reportDoctor2(runnerOk);
16739
16815
  }
16740
16816
  if (subcommand === "console") {
16741
- const sub = subcommandArgs[0];
16742
- if (sub === "enable") {
16743
- const runnerId = subcommandArgs[1];
16744
- if (!runnerId) {
16745
- logError("dashboard console enable: missing <runnerId>");
16746
- return 2;
16747
- }
16748
- if (subcommandArgs.length > 2) {
16749
- logError(
16750
- `dashboard console enable: unexpected argument ${subcommandArgs[2]}`
16751
- );
16752
- return 2;
16753
- }
16754
- const config = readConfig2();
16755
- if (!config) {
16756
- logError(
16757
- 'dashboard console enable: not paired. Run "drisp dashboard pair" first.'
16758
- );
16759
- return 1;
16760
- }
16761
- const refreshResult = await tryRefresh("refresh");
16762
- if (!refreshResult.ok) return refreshResult.code;
16763
- const health = await fetchRunnerHealth(
16764
- fetchImpl,
16765
- config.dashboardUrl,
16766
- runnerId,
16767
- refreshResult.token
16768
- );
16769
- if (!health.matches) {
16770
- logError(
16771
- `dashboard console enable: runner ${runnerId} is not bound to this instance${health.error ? ` (${health.error})` : ""}`
16772
- );
16773
- return 1;
16774
- }
16775
- return await runDashboardCommand(
16776
- {
16777
- subcommand: "console",
16778
- subcommandArgs: ["link", runnerId],
16779
- flags: input.flags
16780
- },
16781
- deps
16782
- );
16817
+ const message = "dashboard console is deprecated; paired dashboard feed sync now routes dashboard UI and channel decisions.";
16818
+ if (flags.json) {
16819
+ logOut(JSON.stringify({ ok: false, deprecated: true, message }));
16820
+ } else {
16821
+ logError(message);
16783
16822
  }
16784
- if (sub === "link") {
16785
- const runnerId = subcommandArgs[1];
16786
- if (!runnerId) {
16787
- logError("dashboard console link: missing <runnerId>");
16788
- return 2;
16789
- }
16790
- if (subcommandArgs.length > 2) {
16791
- logError(
16792
- `dashboard console link: unexpected argument ${subcommandArgs[2]}`
16793
- );
16794
- return 2;
16795
- }
16796
- const config = readConfig2();
16797
- if (!config) {
16798
- logError(
16799
- 'dashboard console link: not paired. Run "drisp dashboard pair" first.'
16800
- );
16801
- return 1;
16802
- }
16803
- let brokerUrl;
16804
- try {
16805
- brokerUrl = consoleBrokerUrl(config.dashboardUrl, runnerId);
16806
- } catch (err) {
16807
- logError(
16808
- `dashboard console link: ${err instanceof Error ? err.message : String(err)}`
16809
- );
16810
- return 1;
16811
- }
16812
- const dir = (deps.channelDir ?? channelSidecarDir)();
16813
- let previousBroker;
16814
- const target = path7.join(dir, "console.json");
16815
- try {
16816
- const existing = JSON.parse(fs7.readFileSync(target, "utf-8"));
16817
- if (typeof existing.broker_url === "string") {
16818
- previousBroker = existing.broker_url;
16819
- }
16820
- } catch {
16821
- }
16822
- try {
16823
- fs7.mkdirSync(dir, { recursive: true, mode: 448 });
16824
- } catch (err) {
16825
- logError(
16826
- `dashboard console link: failed to create ${dir}: ${err instanceof Error ? err.message : String(err)}`
16827
- );
16828
- return 1;
16829
- }
16830
- const payload = {
16831
- broker_url: brokerUrl,
16832
- runner_id: runnerId,
16833
- dashboard_config: true
16834
- };
16835
- const tmp = `${target}.tmp`;
16836
- try {
16837
- fs7.writeFileSync(tmp, JSON.stringify(payload, null, 2) + "\n", {
16838
- mode: 384
16839
- });
16840
- fs7.renameSync(tmp, target);
16841
- } catch (err) {
16842
- try {
16843
- fs7.unlinkSync(tmp);
16844
- } catch {
16845
- }
16823
+ return 1;
16824
+ }
16825
+ if (subcommand === "list") {
16826
+ if (subcommandArgs.length > 0) {
16827
+ logError(`dashboard list: unexpected argument ${subcommandArgs[0]}`);
16828
+ return 2;
16829
+ }
16830
+ const config = readConfig2();
16831
+ if (!config) {
16832
+ if (flags.json) {
16833
+ logOut(JSON.stringify({ ok: false, paired: false }));
16834
+ } else {
16846
16835
  logError(
16847
- `dashboard console link: failed to write ${target}: ${err instanceof Error ? err.message : String(err)}`
16836
+ 'dashboard list: not paired. Run "drisp dashboard pair" first.'
16848
16837
  );
16849
- return 1;
16850
16838
  }
16851
- const reload = await (deps.reloadGatewayChannels ?? defaultReloadGatewayChannels)();
16839
+ return 1;
16840
+ }
16841
+ const mirror = readMirror();
16842
+ if (!mirror) {
16852
16843
  if (flags.json) {
16853
16844
  logOut(
16854
16845
  JSON.stringify({
16855
16846
  ok: true,
16856
- runnerId,
16857
- brokerUrl,
16858
- path: target,
16859
- ...previousBroker ? { previousBrokerUrl: previousBroker } : {},
16860
- gatewayReload: reload
16847
+ paired: true,
16848
+ instanceId: config.instanceId,
16849
+ attachments: [],
16850
+ mirror: null
16861
16851
  })
16862
16852
  );
16863
16853
  } else {
16864
- if (previousBroker && previousBroker !== brokerUrl) {
16865
- logOut(`console: replaced existing config (was: ${previousBroker})`);
16866
- }
16867
- logOut(`console: linked runner ${runnerId} at ${brokerUrl}`);
16868
- logOut(`console: wrote ${target}`);
16869
- if (reload.ok) {
16870
- logOut(`console: gateway channels reloaded (${reload.message})`);
16871
- } else {
16872
- logError(`console: gateway reload skipped: ${reload.message}`);
16873
- logOut(
16874
- "console: start or reload the gateway before using the Console tab."
16875
- );
16876
- }
16854
+ logOut(
16855
+ `dashboard list: paired as ${config.instanceId}, no attachment mirror on disk.`
16856
+ );
16857
+ logOut(
16858
+ "dashboard list: re-run `drisp dashboard pair` to refresh the mirror."
16859
+ );
16877
16860
  }
16878
16861
  return 0;
16879
16862
  }
16880
- logError(
16881
- `dashboard console: unknown subcommand ${sub ? sub : "(missing)"}. Expected "enable <runnerId>" or "link <runnerId>".`
16882
- );
16883
- return 2;
16863
+ if (flags.json) {
16864
+ logOut(
16865
+ JSON.stringify({
16866
+ ok: true,
16867
+ paired: true,
16868
+ instanceId: mirror.instanceId,
16869
+ fetchedAt: mirror.fetchedAt,
16870
+ attachments: mirror.attachments
16871
+ })
16872
+ );
16873
+ } else {
16874
+ logOut(`dashboard: instance ${mirror.instanceId}`);
16875
+ logOut(
16876
+ `dashboard: mirror fetched ${new Date(mirror.fetchedAt).toISOString()}`
16877
+ );
16878
+ if (mirror.attachments.length === 0) {
16879
+ logOut("dashboard: no runners attached.");
16880
+ } else {
16881
+ logOut(`dashboard: ${mirror.attachments.length} runner(s) attached:`);
16882
+ for (const a of mirror.attachments) {
16883
+ const label = a.name ? `${a.name} (${a.runnerId})` : a.runnerId;
16884
+ const target = a.executionTarget ? ` [${a.executionTarget}]` : "";
16885
+ logOut(` - ${label}${target}`);
16886
+ }
16887
+ }
16888
+ }
16889
+ return 0;
16884
16890
  }
16885
16891
  if (subcommand === "unpair") {
16886
16892
  if (subcommandArgs.length > 0) {
@@ -16953,6 +16959,7 @@ async function runDashboardCommand(input, deps = {}) {
16953
16959
  }
16954
16960
  }
16955
16961
  removeConfig();
16962
+ removeMirror();
16956
16963
  if (flags.json) {
16957
16964
  logOut(
16958
16965
  JSON.stringify({
@@ -16972,7 +16979,7 @@ async function runDashboardCommand(input, deps = {}) {
16972
16979
  return 0;
16973
16980
  }
16974
16981
  logError(`Unknown dashboard subcommand: ${subcommand}`);
16975
- logError(USAGE5);
16982
+ logError(USAGE4);
16976
16983
  return 2;
16977
16984
  }
16978
16985
  function defaultWaitForShutdown() {
@@ -16984,36 +16991,21 @@ function defaultWaitForShutdown() {
16984
16991
  };
16985
16992
  process.once("SIGINT", onSignal);
16986
16993
  process.once("SIGTERM", onSignal);
16987
- });
16988
- }
16989
- async function defaultReloadGatewayChannels() {
16990
- const out = [];
16991
- const err = [];
16992
- const code = await runGatewayCommand(
16993
- { subcommand: "reload-channels", subcommandArgs: [] },
16994
- {
16995
- logOut: (m) => out.push(m),
16996
- logError: (m) => err.push(m)
16997
- }
16998
- );
16999
- return {
17000
- ok: code === 0,
17001
- message: (code === 0 ? out.join("\n") : err.join("\n")) || (code === 0 ? "gateway channels reloaded" : "gateway not reachable")
17002
- };
16994
+ });
17003
16995
  }
17004
16996
  function resolveDaemonEntry() {
17005
16997
  let here;
17006
16998
  try {
17007
- here = fileURLToPath2(import.meta.url);
16999
+ here = fileURLToPath(import.meta.url);
17008
17000
  } catch {
17009
17001
  return null;
17010
17002
  }
17011
17003
  const candidates = [
17012
- path7.join(path7.dirname(here), "dashboard-daemon.js"),
17004
+ path6.join(path6.dirname(here), "dashboard-daemon.js"),
17013
17005
  // When invoked via `npm run start`, `here` may be the unbundled source
17014
17006
  // path. Walk up until we hit a `dist/` sibling.
17015
- path7.join(
17016
- path7.dirname(here),
17007
+ path6.join(
17008
+ path6.dirname(here),
17017
17009
  "..",
17018
17010
  "..",
17019
17011
  "..",
@@ -17023,7 +17015,7 @@ function resolveDaemonEntry() {
17023
17015
  ];
17024
17016
  for (const candidate of candidates) {
17025
17017
  try {
17026
- fs7.accessSync(candidate, fs7.constants.R_OK);
17018
+ fs6.accessSync(candidate, fs6.constants.R_OK);
17027
17019
  return candidate;
17028
17020
  } catch {
17029
17021
  }
@@ -17081,7 +17073,7 @@ async function defaultStartRuntimeDaemon(opts) {
17081
17073
  }
17082
17074
  let child;
17083
17075
  try {
17084
- child = spawn2(process.execPath, [entry], {
17076
+ child = spawn(process.execPath, [entry], {
17085
17077
  detached: true,
17086
17078
  stdio: "ignore",
17087
17079
  env: buildDaemonEnv(process.env)
@@ -17208,14 +17200,14 @@ async function defaultTailDaemonLog(opts) {
17208
17200
  let stream2 = null;
17209
17201
  let watcher = null;
17210
17202
  try {
17211
- const stat = fs7.statSync(paths.logPath);
17203
+ const stat = fs6.statSync(paths.logPath);
17212
17204
  const size = stat.size;
17213
17205
  const buf = Buffer.alloc(Math.min(size, opts.tail * 1024));
17214
- const fd = fs7.openSync(paths.logPath, "r");
17206
+ const fd = fs6.openSync(paths.logPath, "r");
17215
17207
  try {
17216
- fs7.readSync(fd, buf, 0, buf.length, Math.max(0, size - buf.length));
17208
+ fs6.readSync(fd, buf, 0, buf.length, Math.max(0, size - buf.length));
17217
17209
  } finally {
17218
- fs7.closeSync(fd);
17210
+ fs6.closeSync(fd);
17219
17211
  }
17220
17212
  const lines = buf.toString("utf-8").split("\n").filter((l) => l.length > 0);
17221
17213
  const tail = lines.slice(-opts.tail);
@@ -17229,253 +17221,612 @@ async function defaultTailDaemonLog(opts) {
17229
17221
  `dashboard logs: log file ${paths.logPath} does not exist yet
17230
17222
  `
17231
17223
  );
17232
- return opts.follow ? 0 : 1;
17224
+ return opts.follow ? 0 : 1;
17225
+ }
17226
+ throw err;
17227
+ }
17228
+ let position = fs6.statSync(paths.logPath).size;
17229
+ return await new Promise((resolve) => {
17230
+ let pollTimer = null;
17231
+ const drain = () => {
17232
+ try {
17233
+ const stat = fs6.statSync(paths.logPath);
17234
+ if (stat.size < position) {
17235
+ position = 0;
17236
+ }
17237
+ if (stat.size > position) {
17238
+ const fd = fs6.openSync(paths.logPath, "r");
17239
+ const buf = Buffer.alloc(stat.size - position);
17240
+ try {
17241
+ fs6.readSync(fd, buf, 0, buf.length, position);
17242
+ } finally {
17243
+ fs6.closeSync(fd);
17244
+ }
17245
+ position = stat.size;
17246
+ process.stdout.write(buf);
17247
+ }
17248
+ } catch (err) {
17249
+ if (err.code !== "ENOENT") {
17250
+ process.stderr.write(
17251
+ `dashboard logs: tail error: ${err instanceof Error ? err.message : String(err)}
17252
+ `
17253
+ );
17254
+ }
17255
+ }
17256
+ };
17257
+ try {
17258
+ watcher = fs6.watch(paths.logPath, { persistent: true }, () => {
17259
+ drain();
17260
+ });
17261
+ } catch {
17262
+ pollTimer = setInterval(drain, 500);
17263
+ pollTimer.unref();
17264
+ }
17265
+ const onSignal = () => {
17266
+ if (watcher) {
17267
+ watcher.close();
17268
+ watcher = null;
17269
+ }
17270
+ if (pollTimer) {
17271
+ clearInterval(pollTimer);
17272
+ pollTimer = null;
17273
+ }
17274
+ if (stream2) {
17275
+ stream2.close();
17276
+ stream2 = null;
17277
+ }
17278
+ resolve(0);
17279
+ };
17280
+ process.once("SIGINT", onSignal);
17281
+ process.once("SIGTERM", onSignal);
17282
+ });
17283
+ }
17284
+ function formatDuration2(seconds) {
17285
+ if (!Number.isFinite(seconds) || seconds < 0) return "?";
17286
+ if (seconds < 60) return `${seconds}s`;
17287
+ if (seconds < 3600) {
17288
+ const m = Math.floor(seconds / 60);
17289
+ const s = seconds % 60;
17290
+ return s > 0 ? `${m}m${s}s` : `${m}m`;
17291
+ }
17292
+ if (seconds < 86400) {
17293
+ const h2 = Math.floor(seconds / 3600);
17294
+ const m = Math.floor(seconds % 3600 / 60);
17295
+ return m > 0 ? `${h2}h${m}m` : `${h2}h`;
17296
+ }
17297
+ const d = Math.floor(seconds / 86400);
17298
+ const h = Math.floor(seconds % 86400 / 3600);
17299
+ return h > 0 ? `${d}d${h}h` : `${d}d`;
17300
+ }
17301
+ async function fetchRunnerHealth(fetchImpl, dashboardUrl, runnerId, token) {
17302
+ const url = new URL(
17303
+ `/api/runners/${encodeURIComponent(runnerId)}`,
17304
+ dashboardUrl
17305
+ ).toString();
17306
+ let response;
17307
+ try {
17308
+ response = await fetchImpl(url, {
17309
+ method: "GET",
17310
+ headers: {
17311
+ authorization: `Bearer ${token.accessToken}`,
17312
+ accept: "application/json"
17313
+ }
17314
+ });
17315
+ } catch (err) {
17316
+ return {
17317
+ id: runnerId,
17318
+ matches: false,
17319
+ error: `request failed: ${err instanceof Error ? err.message : String(err)}`
17320
+ };
17321
+ }
17322
+ if (!response.ok) {
17323
+ return {
17324
+ id: runnerId,
17325
+ matches: false,
17326
+ error: `dashboard returned ${response.status}`
17327
+ };
17328
+ }
17329
+ let body;
17330
+ try {
17331
+ body = await response.json();
17332
+ } catch (err) {
17333
+ return {
17334
+ id: runnerId,
17335
+ matches: false,
17336
+ error: `invalid response body: ${err instanceof Error ? err.message : String(err)}`
17337
+ };
17338
+ }
17339
+ const obj = typeof body === "object" && body !== null ? body : {};
17340
+ const executionTarget = typeof obj["executionTarget"] === "string" ? obj["executionTarget"] : void 0;
17341
+ const remoteInstanceId = typeof obj["remoteInstanceId"] === "string" ? obj["remoteInstanceId"] : void 0;
17342
+ const matches = executionTarget === "remote" && remoteInstanceId === token.instanceId;
17343
+ const reasons = [];
17344
+ if (executionTarget !== "remote") {
17345
+ reasons.push(
17346
+ `executionTarget=${executionTarget ?? "unset"} (expected "remote")`
17347
+ );
17348
+ }
17349
+ if (remoteInstanceId !== token.instanceId) {
17350
+ reasons.push(
17351
+ `remoteInstanceId=${remoteInstanceId ?? "unset"} (expected "${token.instanceId}")`
17352
+ );
17353
+ }
17354
+ return {
17355
+ id: runnerId,
17356
+ matches,
17357
+ ...executionTarget !== void 0 ? { executionTarget } : {},
17358
+ ...remoteInstanceId !== void 0 ? { remoteInstanceId } : {},
17359
+ ...reasons.length > 0 ? { error: reasons.join("; ") } : {}
17360
+ };
17361
+ }
17362
+ function defaultInstallServiceUnit() {
17363
+ const entry = resolveDaemonEntry();
17364
+ if (!entry) {
17365
+ return {
17366
+ ok: false,
17367
+ platform: "unsupported",
17368
+ message: "cannot resolve dashboard-daemon.js entry path"
17369
+ };
17370
+ }
17371
+ return installServiceUnit({
17372
+ daemonEntry: entry,
17373
+ nodeBinary: process.execPath
17374
+ });
17375
+ }
17376
+ function compareSemver(a, b) {
17377
+ const parseN = (s) => {
17378
+ const parts = s.replace(/^v/, "").split("-")[0].split(".");
17379
+ return [
17380
+ Number.parseInt(parts[0] ?? "0", 10) || 0,
17381
+ Number.parseInt(parts[1] ?? "0", 10) || 0,
17382
+ Number.parseInt(parts[2] ?? "0", 10) || 0
17383
+ ];
17384
+ };
17385
+ const av = parseN(a);
17386
+ const bv = parseN(b);
17387
+ for (let i = 0; i < 3; i += 1) {
17388
+ if (av[i] < bv[i]) return -1;
17389
+ if (av[i] > bv[i]) return 1;
17390
+ }
17391
+ return 0;
17392
+ }
17393
+ async function safeReadError(response) {
17394
+ try {
17395
+ const text = await response.text();
17396
+ if (text.length === 0) return "";
17397
+ try {
17398
+ const parsed = JSON.parse(text);
17399
+ if (typeof parsed === "object" && parsed !== null && typeof parsed["error"] === "string") {
17400
+ return parsed["error"];
17401
+ }
17402
+ } catch {
17403
+ }
17404
+ return text.length > 200 ? text.slice(0, 200) + "\u2026" : text;
17405
+ } catch {
17406
+ return "";
17407
+ }
17408
+ }
17409
+ function parsePairResponse(raw) {
17410
+ if (typeof raw !== "object" || raw === null) {
17411
+ throw new Error("expected object");
17412
+ }
17413
+ const obj = raw;
17414
+ const instanceId = obj["instanceId"];
17415
+ const refreshToken = obj["refreshToken"];
17416
+ if (typeof instanceId !== "string" || instanceId.length === 0) {
17417
+ throw new Error("missing instanceId");
17418
+ }
17419
+ if (typeof refreshToken !== "string" || refreshToken.length === 0) {
17420
+ throw new Error("missing refreshToken");
17421
+ }
17422
+ return {
17423
+ instanceId,
17424
+ refreshToken,
17425
+ ...typeof obj["jti"] === "string" ? { jti: obj["jti"] } : {},
17426
+ ...typeof obj["accessToken"] === "string" ? { accessToken: obj["accessToken"] } : {},
17427
+ ...typeof obj["expiresInSec"] === "number" ? { expiresInSec: obj["expiresInSec"] } : {},
17428
+ ...Array.isArray(obj["runners"]) ? {
17429
+ runners: obj["runners"].map(parsePairedRunner).filter((runner) => runner !== null)
17430
+ } : {},
17431
+ ...typeof obj["requiredCliVersion"] === "string" ? { requiredCliVersion: obj["requiredCliVersion"] } : {},
17432
+ ...typeof obj["capabilityAck"] === "object" && obj["capabilityAck"] !== null ? { capabilityAck: parseCapabilityAck(obj["capabilityAck"]) } : {}
17433
+ };
17434
+ }
17435
+ function parseCapabilityAck(raw) {
17436
+ if (typeof raw !== "object" || raw === null) return {};
17437
+ const obj = raw;
17438
+ const ack = {};
17439
+ if (typeof obj["runtimeDaemon"] === "boolean") {
17440
+ ack.runtimeDaemon = obj["runtimeDaemon"];
17441
+ }
17442
+ if (typeof obj["instanceSocket"] === "boolean") {
17443
+ ack.instanceSocket = obj["instanceSocket"];
17444
+ }
17445
+ return ack;
17446
+ }
17447
+ function parsePairedRunner(raw) {
17448
+ if (typeof raw !== "object" || raw === null) return null;
17449
+ const obj = raw;
17450
+ const runnerId = obj["runnerId"];
17451
+ if (typeof runnerId !== "string" || runnerId.length === 0) return null;
17452
+ return {
17453
+ runnerId,
17454
+ ...typeof obj["name"] === "string" ? { name: obj["name"] } : {},
17455
+ ...typeof obj["executionTarget"] === "string" ? { executionTarget: obj["executionTarget"] } : {},
17456
+ ...typeof obj["remoteInstanceId"] === "string" ? { remoteInstanceId: obj["remoteInstanceId"] } : {}
17457
+ };
17458
+ }
17459
+
17460
+ // src/app/entry/gatewayCommand.ts
17461
+ import { spawn as spawn2 } from "child_process";
17462
+ import fs7 from "fs";
17463
+ import path7 from "path";
17464
+ import { fileURLToPath as fileURLToPath2 } from "url";
17465
+ var USAGE5 = `Usage: athena-flow gateway <subcommand> [--json]
17466
+
17467
+ Subcommands:
17468
+ start Run the gateway daemon in foreground (only mode in this build).
17469
+ Options: [--bind <host:port>] [--insecure]
17470
+ [--tls-cert <path>] [--tls-key <path>]
17471
+ [--grace-period-ms <n>]
17472
+ status Print daemon pid, uptime, and version.
17473
+ probe Send a ping RPC and report reachability + latency.
17474
+ link Store a remote WS/WSS gateway endpoint for this user.
17475
+ unlink Restore local UDS gateway mode for this user.
17476
+ rotate-token Regenerate the gateway token file (server-side).
17477
+ Restart the daemon to drop existing connections; clients
17478
+ must re-run "athena gateway link --token <new>".
17479
+ reload-channels Reload channel sidecars without restarting the daemon.
17480
+ `;
17481
+ function defaultResolveDaemonEntry() {
17482
+ const here = path7.dirname(fileURLToPath2(import.meta.url));
17483
+ return path7.resolve(here, "athena-gateway.js");
17484
+ }
17485
+ function readToken(tokenPath) {
17486
+ try {
17487
+ return fs7.readFileSync(tokenPath, "utf-8").trim();
17488
+ } catch (err) {
17489
+ const code = err.code;
17490
+ if (code === "ENOENT") {
17491
+ throw new Error(
17492
+ `gateway token missing at ${tokenPath}. Start the daemon with "athena gateway start" first.`
17493
+ );
17233
17494
  }
17234
17495
  throw err;
17235
17496
  }
17236
- let position = fs7.statSync(paths.logPath).size;
17237
- return await new Promise((resolve) => {
17238
- let pollTimer = null;
17239
- const drain = () => {
17240
- try {
17241
- const stat = fs7.statSync(paths.logPath);
17242
- if (stat.size < position) {
17243
- position = 0;
17244
- }
17245
- if (stat.size > position) {
17246
- const fd = fs7.openSync(paths.logPath, "r");
17247
- const buf = Buffer.alloc(stat.size - position);
17248
- try {
17249
- fs7.readSync(fd, buf, 0, buf.length, position);
17250
- } finally {
17251
- fs7.closeSync(fd);
17252
- }
17253
- position = stat.size;
17254
- process.stdout.write(buf);
17255
- }
17256
- } catch (err) {
17257
- if (err.code !== "ENOENT") {
17258
- process.stderr.write(
17259
- `dashboard logs: tail error: ${err instanceof Error ? err.message : String(err)}
17260
- `
17261
- );
17262
- }
17263
- }
17264
- };
17265
- try {
17266
- watcher = fs7.watch(paths.logPath, { persistent: true }, () => {
17267
- drain();
17268
- });
17269
- } catch {
17270
- pollTimer = setInterval(drain, 500);
17271
- pollTimer.unref?.();
17272
- }
17273
- const onSignal = () => {
17274
- if (watcher) {
17275
- watcher.close();
17276
- watcher = null;
17277
- }
17278
- if (pollTimer) {
17279
- clearInterval(pollTimer);
17280
- pollTimer = null;
17281
- }
17282
- if (stream2) {
17283
- stream2.close();
17284
- stream2 = null;
17285
- }
17286
- resolve(0);
17287
- };
17288
- process.once("SIGINT", onSignal);
17289
- process.once("SIGTERM", onSignal);
17290
- });
17291
17497
  }
17292
- function formatDuration2(seconds) {
17293
- if (!Number.isFinite(seconds) || seconds < 0) return "?";
17294
- if (seconds < 60) return `${seconds}s`;
17295
- if (seconds < 3600) {
17296
- const m = Math.floor(seconds / 60);
17297
- const s = seconds % 60;
17298
- return s > 0 ? `${m}m${s}s` : `${m}m`;
17498
+ function flagJson(args) {
17499
+ return args.includes("--json");
17500
+ }
17501
+ async function runGatewayCommand(input, deps = {}) {
17502
+ const logOut = deps.logOut ?? ((m) => process.stdout.write(m + "\n"));
17503
+ const logError = deps.logError ?? ((m) => process.stderr.write(m + "\n"));
17504
+ const resolveDaemonEntry2 = deps.resolveDaemonEntry ?? defaultResolveDaemonEntry;
17505
+ const resolveSocketPath = deps.resolveSocketPath ?? (() => resolveGatewayPaths().socketPath);
17506
+ const resolveTokenPath = deps.resolveTokenPath ?? (() => resolveGatewayPaths().tokenPath);
17507
+ const readClientConfig = deps.readClientConfig ?? readGatewayClientConfig;
17508
+ const writeClientConfig = deps.writeClientConfig ?? ((config) => writeGatewayClientConfig(config));
17509
+ const connectGateway = deps.connectGateway ?? defaultConnectGateway;
17510
+ const spawnDaemon = deps.spawnDaemon ?? ((entry, args) => spawn2(process.execPath, [entry, ...args], { stdio: "inherit" }));
17511
+ const { subcommand, subcommandArgs } = input;
17512
+ if (!subcommand || subcommand === "help" || subcommand === "--help") {
17513
+ logOut(USAGE5);
17514
+ return 0;
17299
17515
  }
17300
- if (seconds < 86400) {
17301
- const h2 = Math.floor(seconds / 3600);
17302
- const m = Math.floor(seconds % 3600 / 60);
17303
- return m > 0 ? `${h2}h${m}m` : `${h2}h`;
17516
+ if (subcommand === "start") {
17517
+ const entry = resolveDaemonEntry2();
17518
+ const child = spawnDaemon(entry, subcommandArgs);
17519
+ return await new Promise((resolve) => {
17520
+ child.once("exit", (code) => resolve(code ?? 0));
17521
+ child.once("error", (err) => {
17522
+ logError(`gateway start: failed to spawn daemon: ${err.message}`);
17523
+ resolve(1);
17524
+ });
17525
+ });
17304
17526
  }
17305
- const d = Math.floor(seconds / 86400);
17306
- const h = Math.floor(seconds % 86400 / 3600);
17307
- return h > 0 ? `${d}d${h}h` : `${d}d`;
17308
- }
17309
- async function fetchRunnerHealth(fetchImpl, dashboardUrl, runnerId, token) {
17310
- const url = new URL(
17311
- `/api/runners/${encodeURIComponent(runnerId)}`,
17312
- dashboardUrl
17313
- ).toString();
17314
- let response;
17315
- try {
17316
- response = await fetchImpl(url, {
17317
- method: "GET",
17318
- headers: {
17319
- authorization: `Bearer ${token.accessToken}`,
17320
- accept: "application/json"
17321
- }
17527
+ if (subcommand === "link") {
17528
+ const parsed = parseLinkArgs(subcommandArgs);
17529
+ if (!parsed.ok) {
17530
+ logError(parsed.message);
17531
+ return 2;
17532
+ }
17533
+ writeClientConfig({
17534
+ mode: "remote",
17535
+ url: parsed.url,
17536
+ token: parsed.token,
17537
+ ...parsed.tlsCaPath !== void 0 ? { tlsCaPath: parsed.tlsCaPath } : {}
17322
17538
  });
17323
- } catch (err) {
17324
- return {
17325
- id: runnerId,
17326
- matches: false,
17327
- error: `request failed: ${err instanceof Error ? err.message : String(err)}`
17328
- };
17539
+ logOut(`gateway: linked remote endpoint ${parsed.url}`);
17540
+ return 0;
17329
17541
  }
17330
- if (!response.ok) {
17331
- return {
17332
- id: runnerId,
17333
- matches: false,
17334
- error: `dashboard returned ${response.status}`
17335
- };
17542
+ if (subcommand === "rotate-token") {
17543
+ const json = flagJson(subcommandArgs);
17544
+ const extras = subcommandArgs.filter((a) => a !== "--json");
17545
+ if (extras.length > 0) {
17546
+ logError(`gateway rotate-token: unexpected argument ${extras[0]}`);
17547
+ return 2;
17548
+ }
17549
+ const tokenPath = resolveTokenPath();
17550
+ try {
17551
+ const newToken = rotateGatewayToken(tokenPath);
17552
+ if (json) {
17553
+ logOut(JSON.stringify({ ok: true, token: newToken, tokenPath }));
17554
+ } else {
17555
+ logOut(newToken);
17556
+ logOut(
17557
+ `gateway: rotated token at ${tokenPath}. Restart the daemon to drop existing connections, then re-run "athena gateway link --token <new>" on each client.`
17558
+ );
17559
+ }
17560
+ return 0;
17561
+ } catch (err) {
17562
+ const message = err instanceof Error ? err.message : String(err);
17563
+ if (json) {
17564
+ logOut(JSON.stringify({ ok: false, message }));
17565
+ } else {
17566
+ logError(`gateway rotate-token: ${message}`);
17567
+ }
17568
+ return 1;
17569
+ }
17336
17570
  }
17337
- let body;
17338
- try {
17339
- body = await response.json();
17340
- } catch (err) {
17341
- return {
17342
- id: runnerId,
17343
- matches: false,
17344
- error: `invalid response body: ${err instanceof Error ? err.message : String(err)}`
17345
- };
17571
+ if (subcommand === "unlink") {
17572
+ if (subcommandArgs.length > 0) {
17573
+ logError("gateway unlink does not accept arguments");
17574
+ return 2;
17575
+ }
17576
+ writeClientConfig({ mode: "local" });
17577
+ logOut("gateway: using local gateway endpoint");
17578
+ return 0;
17346
17579
  }
17347
- const obj = typeof body === "object" && body !== null ? body : {};
17348
- const executionTarget = typeof obj["executionTarget"] === "string" ? obj["executionTarget"] : void 0;
17349
- const remoteInstanceId = typeof obj["remoteInstanceId"] === "string" ? obj["remoteInstanceId"] : void 0;
17350
- const matches = executionTarget === "remote" && remoteInstanceId === token.instanceId;
17351
- const reasons = [];
17352
- if (executionTarget !== "remote") {
17353
- reasons.push(
17354
- `executionTarget=${executionTarget ?? "unset"} (expected "remote")`
17355
- );
17580
+ if (subcommand === "probe") {
17581
+ const json = flagJson(subcommandArgs);
17582
+ const socketPath = resolveSocketPath();
17583
+ const tokenPath = resolveTokenPath();
17584
+ const endpoint = readClientConfig();
17585
+ const startedAt = Date.now();
17586
+ try {
17587
+ const client = await connectGateway({
17588
+ endpoint,
17589
+ socketPath,
17590
+ tokenPath,
17591
+ timeoutMs: 3e3
17592
+ });
17593
+ const res = await client.request("ping", {});
17594
+ client.close();
17595
+ const latencyMs = Date.now() - startedAt;
17596
+ if (json) {
17597
+ logOut(
17598
+ JSON.stringify({
17599
+ ok: true,
17600
+ reachable: true,
17601
+ latency_ms: latencyMs,
17602
+ daemon_pid: res.daemonPid,
17603
+ daemon_uptime_ms: res.uptimeMs
17604
+ })
17605
+ );
17606
+ } else {
17607
+ logOut(
17608
+ `gateway: reachable pid=${res.daemonPid} uptime=${res.uptimeMs}ms latency=${latencyMs}ms`
17609
+ );
17610
+ }
17611
+ return 0;
17612
+ } catch (err) {
17613
+ return reportProbeFailure(err, json, logOut, logError);
17614
+ }
17356
17615
  }
17357
- if (remoteInstanceId !== token.instanceId) {
17358
- reasons.push(
17359
- `remoteInstanceId=${remoteInstanceId ?? "unset"} (expected "${token.instanceId}")`
17360
- );
17616
+ if (subcommand === "reload-channels") {
17617
+ const json = flagJson(subcommandArgs);
17618
+ const extras = subcommandArgs.filter((a) => a !== "--json");
17619
+ if (extras.length > 0) {
17620
+ logError(`gateway reload-channels: unexpected argument ${extras[0]}`);
17621
+ return 2;
17622
+ }
17623
+ const socketPath = resolveSocketPath();
17624
+ const tokenPath = resolveTokenPath();
17625
+ const endpoint = readClientConfig();
17626
+ try {
17627
+ const client = await connectGateway({
17628
+ endpoint,
17629
+ socketPath,
17630
+ tokenPath,
17631
+ timeoutMs: 3e3
17632
+ });
17633
+ const res = await client.request("channels.reload", {});
17634
+ client.close();
17635
+ if (json) {
17636
+ logOut(JSON.stringify({ ok: true, ...res }));
17637
+ } else if (res.results.length === 0) {
17638
+ logOut("gateway: channels reloaded (no channel sidecars found)");
17639
+ } else {
17640
+ logOut("gateway: channels reloaded");
17641
+ for (const result of res.results) {
17642
+ logOut(formatChannelReloadResult(result));
17643
+ }
17644
+ }
17645
+ return 0;
17646
+ } catch (err) {
17647
+ return reportProbeFailure(err, json, logOut, logError);
17648
+ }
17361
17649
  }
17362
- return {
17363
- id: runnerId,
17364
- matches,
17365
- ...executionTarget !== void 0 ? { executionTarget } : {},
17366
- ...remoteInstanceId !== void 0 ? { remoteInstanceId } : {},
17367
- ...reasons.length > 0 ? { error: reasons.join("; ") } : {}
17368
- };
17369
- }
17370
- function defaultInstallServiceUnit() {
17371
- const entry = resolveDaemonEntry();
17372
- if (!entry) {
17373
- return {
17374
- ok: false,
17375
- platform: "unsupported",
17376
- message: "cannot resolve dashboard-daemon.js entry path"
17377
- };
17650
+ if (subcommand === "status") {
17651
+ const json = flagJson(subcommandArgs);
17652
+ const socketPath = resolveSocketPath();
17653
+ const tokenPath = resolveTokenPath();
17654
+ const endpoint = readClientConfig();
17655
+ try {
17656
+ const client = await connectGateway({
17657
+ endpoint,
17658
+ socketPath,
17659
+ tokenPath,
17660
+ timeoutMs: 3e3
17661
+ });
17662
+ const res = await client.request("status", {});
17663
+ client.close();
17664
+ if (json) {
17665
+ logOut(JSON.stringify(res));
17666
+ } else {
17667
+ const listenerSummary = formatListenerSummary(res.listener);
17668
+ const channelSummary = formatChannelSummary(res.channels);
17669
+ const runtimeSummary = formatRuntimeSummary(res.runtimes[0]);
17670
+ logOut(
17671
+ `gateway: running pid=${res.daemonPid} uptime=${res.uptimeMs}ms version=${res.version} ${listenerSummary}${channelSummary}${runtimeSummary}`
17672
+ );
17673
+ }
17674
+ return 0;
17675
+ } catch (err) {
17676
+ return reportProbeFailure(err, json, logOut, logError);
17677
+ }
17378
17678
  }
17379
- return installServiceUnit({
17380
- daemonEntry: entry,
17381
- nodeBinary: process.execPath
17382
- });
17679
+ logError(`Unknown gateway subcommand: ${subcommand}`);
17680
+ logError(USAGE5);
17681
+ return 2;
17383
17682
  }
17384
- function compareSemver(a, b) {
17385
- const parseN = (s) => {
17386
- const parts = s.replace(/^v/, "").split("-")[0].split(".");
17387
- return [
17388
- Number.parseInt(parts[0] ?? "0", 10) || 0,
17389
- Number.parseInt(parts[1] ?? "0", 10) || 0,
17390
- Number.parseInt(parts[2] ?? "0", 10) || 0
17391
- ];
17392
- };
17393
- const av = parseN(a);
17394
- const bv = parseN(b);
17395
- for (let i = 0; i < 3; i += 1) {
17396
- if (av[i] < bv[i]) return -1;
17397
- if (av[i] > bv[i]) return 1;
17683
+ async function defaultConnectGateway(opts) {
17684
+ if (opts.endpoint.mode === "remote") {
17685
+ return connect({
17686
+ socketPath: opts.socketPath,
17687
+ token: opts.endpoint.token,
17688
+ timeoutMs: opts.timeoutMs,
17689
+ transport: createWsClientTransport(
17690
+ wsClientOptionsForEndpoint({
17691
+ url: opts.endpoint.url,
17692
+ timeoutMs: opts.timeoutMs,
17693
+ tlsCaPath: opts.endpoint.tlsCaPath
17694
+ })
17695
+ )
17696
+ });
17398
17697
  }
17399
- return 0;
17400
- }
17401
- function consoleBrokerUrl(dashboardUrl, runnerId) {
17402
- const url = new URL(dashboardUrl);
17403
- if (url.protocol === "https:") url.protocol = "wss:";
17404
- else if (url.protocol === "http:") url.protocol = "ws:";
17405
- else throw new Error(`unsupported dashboard protocol: ${url.protocol}`);
17406
- url.pathname = `/api/runners/${encodeURIComponent(runnerId)}/console/adapter`;
17407
- url.search = "";
17408
- url.hash = "";
17409
- return url.toString();
17698
+ return connect({
17699
+ socketPath: opts.socketPath,
17700
+ token: readToken(opts.tokenPath),
17701
+ timeoutMs: opts.timeoutMs
17702
+ });
17410
17703
  }
17411
- async function safeReadError(response) {
17412
- try {
17413
- const text = await response.text();
17414
- if (text.length === 0) return "";
17415
- try {
17416
- const parsed = JSON.parse(text);
17417
- if (typeof parsed === "object" && parsed !== null && typeof parsed["error"] === "string") {
17418
- return parsed["error"];
17704
+ function parseLinkArgs(args) {
17705
+ const positional = [];
17706
+ let token;
17707
+ let tlsCaPath;
17708
+ for (let i = 0; i < args.length; i += 1) {
17709
+ const arg = args[i];
17710
+ if (arg === "--token") {
17711
+ const v = args[i + 1];
17712
+ if (!v || v.startsWith("--")) {
17713
+ return { ok: false, message: "gateway link --token requires a value" };
17419
17714
  }
17420
- } catch {
17715
+ token = v;
17716
+ i += 1;
17717
+ continue;
17421
17718
  }
17422
- return text.length > 200 ? text.slice(0, 200) + "\u2026" : text;
17423
- } catch {
17424
- return "";
17719
+ if (arg === "--tls-ca") {
17720
+ const v = args[i + 1];
17721
+ if (!v || v.startsWith("--")) {
17722
+ return { ok: false, message: "gateway link --tls-ca requires a path" };
17723
+ }
17724
+ tlsCaPath = v;
17725
+ i += 1;
17726
+ continue;
17727
+ }
17728
+ if (arg.startsWith("--token=")) {
17729
+ token = arg.slice("--token=".length);
17730
+ continue;
17731
+ }
17732
+ if (arg.startsWith("--tls-ca=")) {
17733
+ tlsCaPath = arg.slice("--tls-ca=".length);
17734
+ continue;
17735
+ }
17736
+ if (arg.startsWith("--")) {
17737
+ return { ok: false, message: `gateway link: unknown option ${arg}` };
17738
+ }
17739
+ positional.push(arg);
17425
17740
  }
17426
- }
17427
- function parsePairResponse(raw) {
17428
- if (typeof raw !== "object" || raw === null) {
17429
- throw new Error("expected object");
17741
+ const url = positional[0];
17742
+ if (!url) {
17743
+ return { ok: false, message: "gateway link requires a ws:// or wss:// URL" };
17430
17744
  }
17431
- const obj = raw;
17432
- const instanceId = obj["instanceId"];
17433
- const refreshToken = obj["refreshToken"];
17434
- if (typeof instanceId !== "string" || instanceId.length === 0) {
17435
- throw new Error("missing instanceId");
17745
+ if (positional.length > 1) {
17746
+ return { ok: false, message: "gateway link accepts exactly one URL" };
17436
17747
  }
17437
- if (typeof refreshToken !== "string" || refreshToken.length === 0) {
17438
- throw new Error("missing refreshToken");
17748
+ if (!isSupportedGatewayUrl(url)) {
17749
+ return { ok: false, message: "gateway link URL must use ws:// or wss://" };
17750
+ }
17751
+ if (!token) {
17752
+ return { ok: false, message: "gateway link requires --token <token>" };
17753
+ }
17754
+ if (tlsCaPath !== void 0 && tlsCaPath.length === 0) {
17755
+ return { ok: false, message: "gateway link --tls-ca requires a path" };
17439
17756
  }
17440
17757
  return {
17441
- instanceId,
17442
- refreshToken,
17443
- ...typeof obj["jti"] === "string" ? { jti: obj["jti"] } : {},
17444
- ...typeof obj["accessToken"] === "string" ? { accessToken: obj["accessToken"] } : {},
17445
- ...typeof obj["expiresInSec"] === "number" ? { expiresInSec: obj["expiresInSec"] } : {},
17446
- ...Array.isArray(obj["runners"]) ? {
17447
- runners: obj["runners"].map(parsePairedRunner).filter((runner) => runner !== null)
17448
- } : {},
17449
- ...typeof obj["requiredCliVersion"] === "string" ? { requiredCliVersion: obj["requiredCliVersion"] } : {},
17450
- ...typeof obj["capabilityAck"] === "object" && obj["capabilityAck"] !== null ? { capabilityAck: parseCapabilityAck(obj["capabilityAck"]) } : {}
17758
+ ok: true,
17759
+ url,
17760
+ token,
17761
+ ...tlsCaPath !== void 0 ? { tlsCaPath } : {}
17451
17762
  };
17452
17763
  }
17453
- function parseCapabilityAck(raw) {
17454
- if (typeof raw !== "object" || raw === null) return {};
17455
- const obj = raw;
17456
- const ack = {};
17457
- if (typeof obj["runtimeDaemon"] === "boolean") {
17458
- ack.runtimeDaemon = obj["runtimeDaemon"];
17764
+ function reportProbeFailure(err, json, logOut, logError) {
17765
+ const message = err instanceof Error ? err.message : String(err);
17766
+ if (err instanceof GatewayUnreachableError) {
17767
+ if (json) {
17768
+ logOut(
17769
+ JSON.stringify({
17770
+ ok: false,
17771
+ reachable: false,
17772
+ reason: "unreachable",
17773
+ message
17774
+ })
17775
+ );
17776
+ } else {
17777
+ logError(`gateway: not reachable \u2014 ${message}`);
17778
+ }
17779
+ return 1;
17459
17780
  }
17460
- if (typeof obj["consoleAdapter"] === "boolean") {
17461
- ack.consoleAdapter = obj["consoleAdapter"];
17781
+ if (err instanceof GatewayUnauthorizedError) {
17782
+ if (json) {
17783
+ logOut(
17784
+ JSON.stringify({
17785
+ ok: false,
17786
+ reachable: true,
17787
+ reason: "unauthorized",
17788
+ message
17789
+ })
17790
+ );
17791
+ } else {
17792
+ logError(`gateway: unauthorized \u2014 ${message}`);
17793
+ }
17794
+ return 1;
17462
17795
  }
17463
- if (typeof obj["instanceSocket"] === "boolean") {
17464
- ack.instanceSocket = obj["instanceSocket"];
17796
+ if (json) {
17797
+ logOut(JSON.stringify({ ok: false, reason: "error", message }));
17798
+ } else {
17799
+ logError(`gateway: ${message}`);
17465
17800
  }
17466
- return ack;
17801
+ return 1;
17467
17802
  }
17468
- function parsePairedRunner(raw) {
17469
- if (typeof raw !== "object" || raw === null) return null;
17470
- const obj = raw;
17471
- const runnerId = obj["runnerId"];
17472
- if (typeof runnerId !== "string" || runnerId.length === 0) return null;
17473
- return {
17474
- runnerId,
17475
- ...typeof obj["name"] === "string" ? { name: obj["name"] } : {},
17476
- ...typeof obj["executionTarget"] === "string" ? { executionTarget: obj["executionTarget"] } : {},
17477
- ...typeof obj["remoteInstanceId"] === "string" ? { remoteInstanceId: obj["remoteInstanceId"] } : {}
17478
- };
17803
+ function formatRuntimeSummary(r) {
17804
+ if (!r) return " runtime=<none>";
17805
+ const lastRebindAt = r.binding.state !== "none" ? r.binding.lastRebindAt : void 0;
17806
+ const rebind = lastRebindAt !== void 0 ? ` rebound=${formatElapsed(Date.now() - lastRebindAt)}` : "";
17807
+ return ` runtime=${r.runtimeId} binding=${r.binding.state} pid=${r.pid}${rebind}`;
17808
+ }
17809
+ function formatChannelReloadResult(result) {
17810
+ const suffix = result.reason ? `: ${result.reason}` : "";
17811
+ return ` ${result.id} ${result.action}${suffix}`;
17812
+ }
17813
+ function formatChannelSummary(channels) {
17814
+ if (channels.length === 0) return " channels=<none>";
17815
+ const parts = channels.map((channel) => {
17816
+ const note = channel.note ? `(${channel.note})` : "";
17817
+ return `${channel.id}:${channel.state}${note}`;
17818
+ });
17819
+ return ` channels=${parts.join(",")}`;
17820
+ }
17821
+ function formatListenerSummary(listener) {
17822
+ if (listener.kind === "uds") {
17823
+ return `listener=uds:${listener.socketPath}`;
17824
+ }
17825
+ const flags = [];
17826
+ if (listener.tls) flags.push("tls");
17827
+ if (listener.insecure) flags.push("insecure");
17828
+ const suffix = flags.length > 0 ? ` (${flags.join(",")})` : "";
17829
+ return `listener=${listener.url}${suffix}`;
17479
17830
  }
17480
17831
 
17481
17832
  // src/app/entry/doctorCommand.ts
@@ -18175,7 +18526,7 @@ var cli = meow(
18175
18526
  workflow <sub> Manage workflows (install, list, search, remove, upgrade, use)
18176
18527
  marketplace <sub> Manage marketplace sources (add, remove, list)
18177
18528
  channel <sub> Manage external channels
18178
- dashboard <sub> Manage dashboard remote-instance pairing (pair, status, refresh, connect, unpair)
18529
+ dashboard <sub> Manage dashboard pairing and runtime daemon (pair, status, daemon, unpair)
18179
18530
  telemetry [action] Manage anonymous telemetry (enable/disable/status)
18180
18531
  doctor Diagnose Claude headless setup (use with --harness=claude)
18181
18532
 
@@ -18200,15 +18551,8 @@ var cli = meow(
18200
18551
  --bot-token Telegram bot token (channel telegram configure)
18201
18552
  --user-id Telegram allowed user id (channel telegram configure)
18202
18553
  --chat-id Telegram destination chat id (defaults to --user-id)
18203
- --token Gateway link token (gateway link)
18204
18554
  --url Dashboard origin (dashboard pair)
18205
18555
  --name Friendly machine name (dashboard pair)
18206
- --tls-ca Gateway custom CA path (gateway link)
18207
- --tls-cert Gateway TLS certificate path (gateway start)
18208
- --tls-key Gateway TLS private key path (gateway start)
18209
- --bind Gateway listen address host:port (gateway start)
18210
- --insecure Allow plain WS on non-loopback trusted tunnels (gateway start)
18211
- --grace-period-ms Gateway reconnect grace period in milliseconds (gateway start)
18212
18556
  --dry-run Print resolved bootstrap (workflow, isolation, plugins, harness) and exit (exec mode)
18213
18557
  --project Scope workflow command to project config (workflow use)
18214
18558
  --global Scope workflow command to global config (workflow use, default)
@@ -18368,6 +18712,9 @@ var cli = meow(
18368
18712
  },
18369
18713
  apiKey: {
18370
18714
  type: "string"
18715
+ },
18716
+ attachmentId: {
18717
+ type: "string"
18371
18718
  }
18372
18719
  }
18373
18720
  }
@@ -18417,7 +18764,7 @@ Available commands: ${[...KNOWN_COMMANDS].join(", ")}`
18417
18764
  await exitWith(1);
18418
18765
  return;
18419
18766
  }
18420
- const { default: WorkflowInstallWizard } = await import("./WorkflowInstallWizard-2MC5A7W4.js");
18767
+ const { default: WorkflowInstallWizard } = await import("./WorkflowInstallWizard-X754ND4V.js");
18421
18768
  const { waitUntilExit } = render(
18422
18769
  /* @__PURE__ */ jsx25(
18423
18770
  WorkflowInstallWizard,
@@ -18680,6 +19027,7 @@ Available commands: ${[...KNOWN_COMMANDS].join(", ")}`
18680
19027
  isolationPreset,
18681
19028
  ascii: cli.flags.ascii,
18682
19029
  showSetup,
19030
+ ...cli.flags.attachmentId !== void 0 ? { attachmentId: cli.flags.attachmentId } : {},
18683
19031
  initialTelemetryDiagnosticsConsent: globalConfig.telemetryDiagnostics
18684
19032
  }
18685
19033
  ),