@rubytech/taskmaster 1.0.14 → 1.0.16

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.
@@ -1,12 +1,44 @@
1
1
  import { loadConfig } from "../../config/config.js";
2
2
  import { resolveTaskmasterPackageRoot } from "../../infra/taskmaster-root.js";
3
3
  import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js";
4
- import { formatDoctorNonInteractiveHint, writeRestartSentinel, } from "../../infra/restart-sentinel.js";
4
+ import { formatDoctorNonInteractiveHint, readRestartSentinel, writeRestartSentinel, } from "../../infra/restart-sentinel.js";
5
5
  import { checkUpdateStatus, compareSemverStrings } from "../../infra/update-check.js";
6
6
  import { normalizeUpdateChannel, resolveEffectiveUpdateChannel, } from "../../infra/update-channels.js";
7
7
  import { runGatewayUpdate } from "../../infra/update-runner.js";
8
8
  import { VERSION } from "../../version.js";
9
9
  import { ErrorCodes, errorShape, formatValidationErrors, validateUpdateRunParams, } from "../protocol/index.js";
10
+ let lastUpdateResult = null;
11
+ function cacheFromSentinel(status, ts, stats) {
12
+ const failedStep = stats?.steps?.find((s) => s.log?.exitCode != null && s.log.exitCode !== 0);
13
+ lastUpdateResult = {
14
+ status,
15
+ ts,
16
+ mode: stats?.mode ?? null,
17
+ before: stats?.before ?? null,
18
+ after: stats?.after ?? null,
19
+ reason: stats?.reason ?? null,
20
+ durationMs: stats?.durationMs ?? null,
21
+ failedStep: failedStep
22
+ ? { name: failedStep.name, exitCode: failedStep.log?.exitCode ?? null }
23
+ : null,
24
+ currentVersion: VERSION,
25
+ };
26
+ }
27
+ /**
28
+ * Read the restart sentinel into memory before it gets consumed by
29
+ * scheduleRestartSentinelWake. Must be called early in gateway startup.
30
+ */
31
+ export async function cacheLastUpdateSentinel() {
32
+ try {
33
+ const sentinel = await readRestartSentinel();
34
+ if (sentinel?.payload.kind === "update") {
35
+ cacheFromSentinel(sentinel.payload.status, sentinel.payload.ts, sentinel.payload.stats);
36
+ }
37
+ }
38
+ catch {
39
+ // Non-critical — worst case we don't show a result banner
40
+ }
41
+ }
10
42
  export const updateHandlers = {
11
43
  "gateway.restart": async ({ params, respond }) => {
12
44
  const reason = typeof params.reason === "string"
@@ -70,7 +102,10 @@ export const updateHandlers = {
70
102
  }, undefined);
71
103
  }
72
104
  },
73
- "update.run": async ({ params, respond }) => {
105
+ "update.lastResult": async ({ respond }) => {
106
+ respond(true, { ok: true, result: lastUpdateResult }, undefined);
107
+ },
108
+ "update.run": async ({ params, respond, context }) => {
74
109
  if (!validateUpdateRunParams(params)) {
75
110
  respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid update.run params: ${formatValidationErrors(validateUpdateRunParams.errors)}`));
76
111
  return;
@@ -89,6 +124,26 @@ export const updateHandlers = {
89
124
  const timeoutMs = typeof timeoutMsRaw === "number" && Number.isFinite(timeoutMsRaw)
90
125
  ? Math.max(1000, Math.floor(timeoutMsRaw))
91
126
  : undefined;
127
+ const progress = {
128
+ onStepStart: (step) => {
129
+ context.broadcast("update.progress", {
130
+ phase: "step-start",
131
+ name: step.name,
132
+ index: step.index,
133
+ total: step.total,
134
+ });
135
+ },
136
+ onStepComplete: (step) => {
137
+ context.broadcast("update.progress", {
138
+ phase: "step-done",
139
+ name: step.name,
140
+ index: step.index,
141
+ total: step.total,
142
+ durationMs: step.durationMs,
143
+ ok: step.exitCode === 0 || step.exitCode === null,
144
+ });
145
+ },
146
+ };
92
147
  let result;
93
148
  try {
94
149
  const root = (await resolveTaskmasterPackageRoot({
@@ -100,6 +155,7 @@ export const updateHandlers = {
100
155
  timeoutMs,
101
156
  cwd: root,
102
157
  argv1: process.argv[1],
158
+ progress,
103
159
  });
104
160
  }
105
161
  catch (err) {
@@ -145,6 +201,11 @@ export const updateHandlers = {
145
201
  catch {
146
202
  sentinelPath = null;
147
203
  }
204
+ context.broadcast("update.progress", {
205
+ phase: "complete",
206
+ status: result.status,
207
+ reason: result.reason ?? null,
208
+ });
148
209
  const restart = scheduleGatewaySigusr1Restart({
149
210
  delayMs: restartDelayMs,
150
211
  reason: "update.run",
@@ -3,7 +3,7 @@ import { normalizeControlUiBasePath } from "./control-ui-shared.js";
3
3
  import { resolveHooksConfig } from "./hooks.js";
4
4
  import { isLoopbackHost, resolveGatewayBindHost } from "./net.js";
5
5
  export async function resolveGatewayRuntimeConfig(params) {
6
- const bindMode = params.bind ?? params.cfg.gateway?.bind ?? "loopback";
6
+ const bindMode = params.bind ?? params.cfg.gateway?.bind ?? "lan";
7
7
  const customBindHost = params.cfg.gateway?.customBindHost;
8
8
  const bindHost = params.host ?? (await resolveGatewayBindHost(bindMode, customBindHost));
9
9
  const controlUiEnabled = params.controlUiEnabled ?? params.cfg.gateway?.controlUi?.enabled ?? true;
@@ -8,6 +8,7 @@ import { loadInternalHooks } from "../hooks/loader.js";
8
8
  import { startPluginServices } from "../plugins/services.js";
9
9
  import { startBrowserControlServerIfEnabled } from "./server-browser.js";
10
10
  import { scheduleRestartSentinelWake, shouldWakeFromRestartSentinel, } from "./server-restart-sentinel.js";
11
+ import { cacheLastUpdateSentinel } from "./server-methods/update.js";
11
12
  export async function startGatewaySidecars(params) {
12
13
  // Start Taskmaster browser control server (unless disabled via config).
13
14
  let browserControl = null;
@@ -111,9 +112,13 @@ export async function startGatewaySidecars(params) {
111
112
  params.log.warn(`plugin services failed to start: ${String(err)}`);
112
113
  }
113
114
  if (shouldWakeFromRestartSentinel()) {
114
- setTimeout(() => {
115
- void scheduleRestartSentinelWake({ deps: params.deps });
116
- }, 750);
115
+ // Cache the sentinel in memory BEFORE it gets consumed, so update.lastResult
116
+ // can serve it to the UI after reconnect.
117
+ void cacheLastUpdateSentinel().finally(() => {
118
+ setTimeout(() => {
119
+ void scheduleRestartSentinelWake({ deps: params.deps });
120
+ }, 750);
121
+ });
117
122
  }
118
123
  return { browserControl, pluginServices };
119
124
  }
@@ -224,7 +224,7 @@ timeoutMs = 10_000) {
224
224
  }
225
225
  export async function startGatewayServer(port, opts) {
226
226
  const mod = await serverModulePromise;
227
- return await mod.startGatewayServer(port, opts);
227
+ return await mod.startGatewayServer(port, { bind: "loopback", ...opts });
228
228
  }
229
229
  export async function startServerWithClient(token, opts) {
230
230
  let port = await getFreePort();
@@ -55,10 +55,7 @@ async function main() {
55
55
  defaultRuntime.error(`Invalid --port (${portRaw})`);
56
56
  process.exit(1);
57
57
  }
58
- const bindRaw = argValue(args, "--bind") ??
59
- process.env.TASKMASTER_GATEWAY_BIND ??
60
- cfg.gateway?.bind ??
61
- "loopback";
58
+ const bindRaw = argValue(args, "--bind") ?? process.env.TASKMASTER_GATEWAY_BIND ?? cfg.gateway?.bind ?? "lan";
62
59
  const bind = bindRaw === "loopback" ||
63
60
  bindRaw === "lan" ||
64
61
  bindRaw === "auto" ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/taskmaster",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "AI-powered business assistant for small businesses",
5
5
  "publishConfig": {
6
6
  "access": "public"