devops-whc 1.0.1 → 1.0.2

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.
@@ -7,15 +7,7 @@ const whc_uapi_client_1 = require("../clients/whc-uapi-client");
7
7
  const deployment_locks_1 = require("../services/deployment-locks");
8
8
  const ssh_client_1 = require("../clients/ssh-client");
9
9
  const deploy_runtime_ops_1 = require("../services/deploy-runtime-ops");
10
- /**
11
- * Resolves the effective safety level for this deploy operation.
12
- * staging_to_live + database or everything → Level D.
13
- * All other writes remain at Level C.
14
- */
15
- function resolveDeploySafetyLevel(direction, syncScope) {
16
- if (direction === "staging_to_live" && (syncScope === "database" || syncScope === "everything")) {
17
- return "D";
18
- }
10
+ function resolveDeploySafetyLevel() {
19
11
  return "C";
20
12
  }
21
13
  async function executeWhcDeploy(config, request, deps = {}) {
@@ -26,90 +18,78 @@ async function executeWhcDeploy(config, request, deps = {}) {
26
18
  const lockService = deps.lockService ?? new deployment_locks_1.InMemoryDeploymentLockService();
27
19
  const verifyRunner = deps.verifyRunner ?? ((cfg, req) => (0, deploy_runtime_ops_1.runDeployVerification)(cfg, req, sshClient));
28
20
  const rollbackRunner = deps.rollbackRunner ?? ((cfg, req) => (0, deploy_runtime_ops_1.runDeployRollback)(cfg, req, uapiClient));
29
- const { workflow_mode, target_environment, release_intent, pipeline_id, source_profile, direction, sync_scope, repository_root, branch, rollback_branch, backup_reference, allow_emergency_without_backup, verify_after_deploy, auto_rollback_on_verify_failure, lock_key, } = request.payload;
30
- const effectiveSafetyLevel = resolveDeploySafetyLevel(direction, sync_scope);
31
- const needsBackupGate = direction === "staging_to_live" && (sync_scope === "database" || sync_scope === "everything");
21
+ const { workflow_mode, target_environment, release_intent, pipeline_id, source_profile, repository_root, branch, direction, sync_scope, backup_reference, verify_after_deploy, auto_rollback_on_verify_failure, lock_key, } = request.payload;
22
+ const effectiveSafetyLevel = resolveDeploySafetyLevel();
32
23
  const shouldVerify = verify_after_deploy ?? false;
33
- const deployLockKey = lock_key ?? buildDefaultLockKey(target_environment, workflow_mode, repository_root);
24
+ const deployLockKey = lock_key ?? `whc_deploy:${target_environment}:${workflow_mode}:${repository_root ?? "unset"}`;
34
25
  const lockOwner = request.request_id;
35
26
  const warnings = [];
36
- if ((config.safety.enforceStagingFirst ?? true) && target_environment === "live") {
37
- const isManagedPromoteFlow = workflow_mode === "managed_clone_sync" &&
38
- direction === "staging_to_live" &&
39
- release_intent === "promote";
40
- if (!isManagedPromoteFlow) {
41
- return buildErrorResponse(actionIdFactory(), startedAt, effectiveSafetyLevel, "BUSINESS_POLICY_BLOCKED", [
42
- "Staging-first policy: direct live deploy is blocked.",
43
- "Only promote flow from staging to live is allowed.",
44
- "Required payload shape for live target:",
45
- JSON.stringify({
46
- workflow_mode: "managed_clone_sync",
47
- target_environment: "live",
48
- release_intent: "promote",
49
- direction: "staging_to_live",
50
- }, null, 2),
51
- ].join("\n"));
52
- }
53
- }
54
- if (target_environment === "staging" &&
55
- config.workflowMode === "managed_clone_sync" &&
56
- workflow_mode === "git_controlled" &&
57
- !config.safety.allowStagingGitControlled) {
27
+ if ((config.safety.enforceStagingFirst ?? true) && workflow_mode === "git_deploy" && target_environment === "live" && release_intent !== "promote") {
58
28
  return buildErrorResponse(actionIdFactory(), startedAt, effectiveSafetyLevel, "BUSINESS_POLICY_BLOCKED", [
59
- "Server mode lock: staging is configured as managed_clone_sync only (WHC_WORKFLOW_MODE=managed_clone_sync).",
60
- "Request used workflow_mode='git_controlled' and was blocked to prevent cross-account deadlock loops.",
61
- "Use this payload instead:",
62
- JSON.stringify(buildStagingAlternativePayload(request), null, 2),
29
+ "Staging-first policy: direct live git deployment is blocked.",
30
+ "Only release_intent='promote' is allowed when target_environment='live'.",
63
31
  ].join("\n"));
64
32
  }
65
- if (config.safety.warnDynamicDataSync && sync_scope === "database") {
66
- warnings.push("DATABASE sync detected — existing data on target will be overwritten. Ensure a backup exists before proceeding.");
67
- }
68
- if (config.safety.warnDynamicDataSync && sync_scope === "everything") {
69
- warnings.push("FULL sync (files + database) detected — all data on target will be replaced. This is irreversible without a backup.");
70
- }
71
- if (workflow_mode === "managed_clone_sync" && release_intent === "deploy" && direction === "live_to_staging") {
72
- return buildErrorResponse(actionIdFactory(), startedAt, effectiveSafetyLevel, "BUSINESS_POLICY_BLOCKED", "Invalid deploy mindset: managed live_to_staging is an environment refresh, not a release deploy. Use release_intent='refresh' for live_to_staging, or deploy a release candidate to staging via git_controlled workflow.");
73
- }
74
- if (workflow_mode === "git_controlled" && repository_root) {
75
- if (target_environment === "staging" && config.paths.staging && repository_root !== config.paths.staging) {
76
- return buildErrorResponse(actionIdFactory(), startedAt, effectiveSafetyLevel, "BUSINESS_POLICY_BLOCKED", [
77
- `Invalid staging target for git_controlled deploy: repository_root='${repository_root}' does not match WHC_STAGING_PATH='${config.paths.staging}'.`,
78
- "This prevents accidental deploys to a live path while target_environment is staging.",
79
- "Use the configured staging path, or switch to managed_clone_sync refresh for cross-account WHC staging.",
80
- "Copy-ready payload using configured staging path:",
81
- JSON.stringify(buildSuggestedStagingGitControlledPayload(request, config.paths.staging), null, 2),
82
- ].join("\n"));
83
- }
84
- const pathOwner = extractHomeOwner(repository_root);
85
- if (pathOwner && pathOwner !== config.user) {
86
- const lines = [
87
- `Path owner mismatch: repository_root belongs to '${pathOwner}' but WHC_USER is '${config.user}'.`,
88
- ];
89
- if (target_environment === "staging") {
90
- lines.push("git_controlled via UAPI cannot deploy to a separate-account staging environment.", "WHC managed staging uses a different cPanel account and is not reachable via these credentials.", "Use managed_clone_sync + release_intent='refresh' + direction='live_to_staging' to sync the environment,", "then deploy your release candidate over SSH using whc_ssh_exec, or configure separate staging credentials.", "Suggested alternative payload (managed_clone_sync refresh):", JSON.stringify(buildStagingAlternativePayload(request), null, 2));
91
- }
92
- else {
93
- const suggestedPayload = buildSuggestedDeployPayload(request, config.user);
94
- lines.push(`Use /home/${config.user}/... for git_controlled deploy or switch credentials.`, "Copy-ready payload using current WHC_USER:", JSON.stringify(suggestedPayload, null, 2));
95
- }
96
- return buildErrorResponse(actionIdFactory(), startedAt, effectiveSafetyLevel, "BUSINESS_POLICY_BLOCKED", lines.join("\n"));
33
+ if (workflow_mode === "ssh_scp_wpcli") {
34
+ const sourceScriptHint = [
35
+ "ssh_scp_wpcli execution is not implemented in whc_deploy yet.",
36
+ "This workflow is modeled after mytho/source where deploy is:",
37
+ "1. doctor",
38
+ "2. backup",
39
+ "3. scp code sync",
40
+ "4. wp-cli runtime bootstrap",
41
+ "5. wp eval-file seed/bootstrap",
42
+ "6. HTTP/API smoke",
43
+ "Use the project-local pipeline script for real execution until source-specific deploy integration lands.",
44
+ ].join("\n");
45
+ if (request.dry_run) {
46
+ const phaseCoverage = buildPhaseCoverage({
47
+ workflowMode: workflow_mode,
48
+ verifyStatus: "skipped",
49
+ dryRun: true,
50
+ });
51
+ return {
52
+ ok: true,
53
+ action_id: actionIdFactory(),
54
+ tool: "whc_deploy",
55
+ data: {
56
+ outcome: "dry_run_preview",
57
+ workflow_mode,
58
+ target_environment,
59
+ direction,
60
+ sync_scope,
61
+ lock_key: deployLockKey,
62
+ backup_reference,
63
+ pipeline_status: derivePipelineStatus(phaseCoverage),
64
+ phase_coverage: phaseCoverage,
65
+ warnings: [...warnings, sourceScriptHint],
66
+ },
67
+ error: null,
68
+ meta: {
69
+ latency_ms: Date.now() - startedAt,
70
+ safety_level: effectiveSafetyLevel,
71
+ workflow_mode,
72
+ pipeline_id,
73
+ release_intent,
74
+ source_profile,
75
+ delivery_mechanism: "file_transfer",
76
+ },
77
+ };
97
78
  }
79
+ return buildErrorResponse(actionIdFactory(), startedAt, effectiveSafetyLevel, "BUSINESS_POLICY_BLOCKED", sourceScriptHint);
98
80
  }
99
- if (direction === "live_to_staging" && sync_scope !== "files") {
100
- warnings.push("Refreshing staging from live with database/everything scope any staging-only changes will be lost.");
81
+ if (direction || sync_scope) {
82
+ warnings.push("direction/sync_scope are legacy managed-sync fields and are ignored by git_deploy.");
101
83
  }
102
- if (needsBackupGate && !backup_reference && !allow_emergency_without_backup) {
103
- return buildErrorResponse(actionIdFactory(), startedAt, effectiveSafetyLevel, "BUSINESS_POLICY_BLOCKED", "Backup reference is required for staging_to_live database/everything sync. Provide payload.backup_reference or set allow_emergency_without_backup=true.");
84
+ if (!repository_root) {
85
+ return buildErrorResponse(actionIdFactory(), startedAt, effectiveSafetyLevel, "VALIDATION_ERROR", "git_deploy requires payload.repository_root so WHC knows which cPanel-managed repository to deploy.");
104
86
  }
105
- if (needsBackupGate && !backup_reference && allow_emergency_without_backup) {
106
- warnings.push("Emergency override enabled without backup_reference. Data loss recovery is not guaranteed.");
87
+ if (!branch) {
88
+ warnings.push("No branch was supplied; cPanel will deploy the repository's current configured HEAD.");
107
89
  }
108
- // Dry run: return preview without making changes
109
90
  if (request.dry_run) {
110
91
  const phaseCoverage = buildPhaseCoverage({
111
92
  workflowMode: workflow_mode,
112
- syncScope: sync_scope,
113
93
  verifyStatus: "skipped",
114
94
  dryRun: true,
115
95
  });
@@ -121,11 +101,10 @@ async function executeWhcDeploy(config, request, deps = {}) {
121
101
  outcome: "dry_run_preview",
122
102
  workflow_mode,
123
103
  target_environment,
124
- direction,
125
- sync_scope,
126
104
  repository_root,
127
105
  branch,
128
- rollback_branch,
106
+ direction,
107
+ sync_scope,
129
108
  lock_key: deployLockKey,
130
109
  backup_reference,
131
110
  pipeline_status: derivePipelineStatus(phaseCoverage),
@@ -140,7 +119,7 @@ async function executeWhcDeploy(config, request, deps = {}) {
140
119
  pipeline_id,
141
120
  release_intent,
142
121
  source_profile,
143
- delivery_mechanism: workflow_mode === "git_controlled" ? "git_deploy" : "managed_sync",
122
+ delivery_mechanism: "git_deploy",
144
123
  },
145
124
  };
146
125
  }
@@ -149,78 +128,9 @@ async function executeWhcDeploy(config, request, deps = {}) {
149
128
  return buildErrorResponse(actionIdFactory(), startedAt, effectiveSafetyLevel, "BUSINESS_POLICY_BLOCKED", `Concurrent deployment blocked: ${lockAcquired.reason}`);
150
129
  }
151
130
  try {
152
- // managed_clone_sync mode: use WHC staging sync
153
- if (workflow_mode === "managed_clone_sync") {
154
- if (!direction) {
155
- return buildErrorResponse(actionIdFactory(), startedAt, effectiveSafetyLevel, "VALIDATION_ERROR", "managed_clone_sync mode requires 'direction' field.");
156
- }
157
- // WHC managed sync is represented with guarded intent + recovery contract.
158
- const outcome = direction === "live_to_staging" ? "synced_live_to_staging" : "synced_staging_to_live";
159
- let verifyStatus = "skipped";
160
- let rollbackStatus = "not_needed";
161
- if (shouldVerify) {
162
- const verifyResult = await verifyRunner(config, request);
163
- if (!verifyResult.ok) {
164
- verifyStatus = "failed";
165
- if (auto_rollback_on_verify_failure) {
166
- rollbackStatus = "attempted";
167
- const rollbackResult = await rollbackRunner(config, request);
168
- rollbackStatus = rollbackResult.ok ? "succeeded" : "failed";
169
- }
170
- return buildErrorResponse(actionIdFactory(), startedAt, effectiveSafetyLevel, "TEMPORARY_UPSTREAM_ERROR", `Post-deploy verification failed: ${verifyResult.message}`);
171
- }
172
- verifyStatus = "passed";
173
- }
174
- const phaseCoverage = buildPhaseCoverage({
175
- workflowMode: workflow_mode,
176
- syncScope: sync_scope,
177
- verifyStatus,
178
- dryRun: false,
179
- });
180
- return {
181
- ok: true,
182
- action_id: actionIdFactory(),
183
- tool: "whc_deploy",
184
- data: {
185
- outcome,
186
- workflow_mode,
187
- target_environment,
188
- direction,
189
- sync_scope,
190
- verify_status: verifyStatus,
191
- rollback_status: rollbackStatus,
192
- lock_key: deployLockKey,
193
- backup_reference,
194
- pipeline_status: derivePipelineStatus(phaseCoverage),
195
- phase_coverage: phaseCoverage,
196
- warnings,
197
- },
198
- error: null,
199
- meta: {
200
- latency_ms: Date.now() - startedAt,
201
- safety_level: effectiveSafetyLevel,
202
- workflow_mode,
203
- pipeline_id,
204
- release_intent,
205
- source_profile,
206
- delivery_mechanism: "managed_sync",
207
- },
208
- };
209
- }
210
- // git_controlled mode: trigger UAPI VersionControl::deployment
211
- if (!repository_root) {
212
- return buildErrorResponse(actionIdFactory(), startedAt, effectiveSafetyLevel, "VALIDATION_ERROR", "git_controlled mode requires 'repository_root' field.");
213
- }
214
- let deployResult;
215
- try {
216
- deployResult = await uapiClient.triggerDeployment(repository_root, branch);
217
- }
218
- catch (error) {
219
- const message = error instanceof Error ? error.message : "UAPI deployment trigger failed";
220
- return buildErrorResponse(actionIdFactory(), startedAt, effectiveSafetyLevel, "AUTH_ERROR", message);
221
- }
131
+ const deployResult = await uapiClient.triggerDeployment(repository_root, branch);
222
132
  if (!deployResult.ok) {
223
- return buildErrorResponse(actionIdFactory(), startedAt, effectiveSafetyLevel, "TEMPORARY_UPSTREAM_ERROR", deployResult.message);
133
+ return buildErrorResponse(actionIdFactory(), startedAt, effectiveSafetyLevel, "TEMPORARY_UPSTREAM_ERROR", `Git deployment trigger failed: ${deployResult.message}`);
224
134
  }
225
135
  let verifyStatus = "skipped";
226
136
  let rollbackStatus = "not_needed";
@@ -239,7 +149,6 @@ async function executeWhcDeploy(config, request, deps = {}) {
239
149
  }
240
150
  const phaseCoverage = buildPhaseCoverage({
241
151
  workflowMode: workflow_mode,
242
- syncScope: sync_scope,
243
152
  verifyStatus,
244
153
  dryRun: false,
245
154
  });
@@ -248,12 +157,13 @@ async function executeWhcDeploy(config, request, deps = {}) {
248
157
  action_id: actionIdFactory(),
249
158
  tool: "whc_deploy",
250
159
  data: {
251
- outcome: "deployed",
160
+ outcome: "deployment_triggered",
252
161
  workflow_mode,
253
162
  target_environment,
254
163
  repository_root,
255
164
  branch,
256
- rollback_branch,
165
+ direction,
166
+ sync_scope,
257
167
  deployment_id: deployResult.deployment_id,
258
168
  verify_status: verifyStatus,
259
169
  rollback_status: rollbackStatus,
@@ -289,83 +199,29 @@ function buildErrorResponse(actionId, startedAt, safetyLevel, code, message) {
289
199
  meta: { latency_ms: Date.now() - startedAt, safety_level: safetyLevel },
290
200
  };
291
201
  }
292
- function buildDefaultLockKey(targetEnvironment, workflowMode, repositoryRoot) {
293
- return `whc_deploy:${targetEnvironment}:${workflowMode}:${repositoryRoot ?? "-"}`;
294
- }
295
- function extractHomeOwner(pathValue) {
296
- const match = pathValue.match(/^\/home\/([^\/]+)\//);
297
- return match?.[1];
298
- }
299
- function buildSuggestedDeployPayload(request, currentUser) {
300
- return {
301
- request_id: request.request_id,
302
- actor: request.actor,
303
- dry_run: request.dry_run,
304
- confirmed: request.confirmed,
305
- idempotency_key: request.idempotency_key,
306
- payload: {
307
- ...request.payload,
308
- target_environment: "live",
309
- repository_root: `/home/${currentUser}/public_html`,
310
- },
311
- };
312
- }
313
- function buildStagingAlternativePayload(request) {
314
- return {
315
- workflow_mode: "managed_clone_sync",
316
- target_environment: "staging",
317
- release_intent: "refresh",
318
- pipeline_id: request.payload.pipeline_id,
319
- source_profile: request.payload.source_profile,
320
- direction: "live_to_staging",
321
- sync_scope: "files",
322
- verify_after_deploy: request.payload.verify_after_deploy,
323
- auto_rollback_on_verify_failure: request.payload.auto_rollback_on_verify_failure,
324
- };
325
- }
326
- function buildSuggestedStagingGitControlledPayload(request, stagingPath) {
327
- return {
328
- request_id: request.request_id,
329
- actor: request.actor,
330
- dry_run: request.dry_run,
331
- confirmed: request.confirmed,
332
- idempotency_key: request.idempotency_key,
333
- payload: {
334
- ...request.payload,
335
- target_environment: "staging",
336
- repository_root: stagingPath,
337
- },
338
- };
339
- }
340
202
  function buildPhaseCoverage(input) {
341
203
  const notes = [];
342
- let codeState = "excluded";
343
- let runtimeState = "excluded";
344
- let dataState = "excluded";
204
+ const codeState = input.dryRun ? "excluded" : "included";
345
205
  const deploymentState = input.dryRun ? "excluded" : "included";
346
- if (input.workflowMode === "git_controlled") {
347
- codeState = input.dryRun ? "excluded" : "included";
348
- runtimeState = input.verifyStatus === "passed" ? "included" : "excluded";
349
- dataState = "excluded";
350
- notes.push("git_controlled deploy covers code transport/deployment state; data/content bootstrap is out of scope.");
206
+ if (input.workflowMode === "git_deploy") {
207
+ notes.push("git_deploy triggers a cPanel deployment task for repository-managed code delivery.", "git_deploy does not bootstrap WordPress runtime/data; use separate runtime checks or project-specific scripts for that layer.");
351
208
  }
352
- if (input.workflowMode === "managed_clone_sync") {
353
- if (input.syncScope === "files" || input.syncScope === "everything") {
354
- codeState = input.dryRun ? "excluded" : "included";
355
- }
356
- if (input.syncScope === "database" || input.syncScope === "everything") {
357
- dataState = input.dryRun ? "excluded" : "included";
358
- }
359
- runtimeState = "excluded";
360
- notes.push("managed_clone_sync does not bootstrap app runtime configuration (plugin/theme activation, rewrite, Woo setup).", "Managed verification is transport-level unless additional runtime checks are run externally.");
209
+ else {
210
+ notes.push("ssh_scp_wpcli is expected to cover code sync, WP-CLI runtime bootstrap, seed/bootstrap, and smoke gating.", "whc_deploy does not execute that source-specific flow yet; use a project-local pipeline script for real runs today.");
211
+ }
212
+ if (input.verifyStatus === "passed") {
213
+ notes.push("Post-deploy verification passed.");
214
+ }
215
+ if (input.verifyStatus === "failed") {
216
+ notes.push("Post-deploy verification failed.");
361
217
  }
362
218
  if (input.dryRun) {
363
219
  notes.push("dry_run preview only; no target state has been changed.");
364
220
  }
365
221
  return {
366
222
  code_state: codeState,
367
- runtime_state: runtimeState,
368
- data_state: dataState,
223
+ runtime_state: "excluded",
224
+ data_state: "excluded",
369
225
  deployment_state: deploymentState,
370
226
  notes,
371
227
  };
@@ -21,7 +21,7 @@ async function executeWhcPipelineStatus(config, request) {
21
21
  next_step: "Run whc_prepare then a write tool to initialize pipeline state.",
22
22
  artifacts: {
23
23
  state_file: (0, workspace_state_1.getPipelineStatusFile)(rootDir),
24
- flow_log_file: config.flowLogPath ?? ".mcp/whc-mcp/logs/flow-events.jsonl",
24
+ flow_log_file: config.flowLogPath ?? ".whc/logs/flow-events.jsonl",
25
25
  },
26
26
  },
27
27
  error: null,
@@ -57,7 +57,7 @@ async function executeWhcPipelineStatus(config, request) {
57
57
  artifacts: {
58
58
  state_file: (0, workspace_state_1.getPipelineStatusFile)(rootDir),
59
59
  manifest_file: manifestFile,
60
- flow_log_file: config.flowLogPath ?? ".mcp/whc-mcp/logs/flow-events.jsonl",
60
+ flow_log_file: config.flowLogPath ?? ".whc/logs/flow-events.jsonl",
61
61
  },
62
62
  },
63
63
  error: null,
@@ -49,7 +49,7 @@ async function executeWhcPrepare(_config, request) {
49
49
  }
50
50
  (0, workspace_state_1.ensureWorkspaceState)(rootDir);
51
51
  const gitignoreUpdated = request.payload.ensure_gitignore_rule !== false
52
- ? ensureGitignoreRule(rootDir, ".mcp/")
52
+ ? ensureGitignoreRule(rootDir, ".whc/")
53
53
  : false;
54
54
  if (!(0, node_fs_1.existsSync)(pipelineStatusFile) || request.payload.force_reinitialize) {
55
55
  const payload = {
@@ -15,7 +15,7 @@ async function executeWhcSetupRemote(config, request, deps = {}) {
15
15
  `Path owner mismatch: deploy_target_path belongs to '${pathOwner}' but WHC_USER is '${config.user}'.`,
16
16
  ];
17
17
  if (isStaging) {
18
- lines.push("WHC managed staging uses a separate cPanel account; git repo setup via this user's UAPI cannot target it.", "Use managed_clone_sync mode to sync staging from live, or configure dedicated staging credentials.");
18
+ lines.push("WHC managed staging uses a separate cPanel account; git repo setup via this user's UAPI cannot target it.", "Use git_deploy mode to drive repository-based staging refresh/deploy flows, or configure dedicated staging credentials.");
19
19
  }
20
20
  else {
21
21
  const suggestedPayload = buildSuggestedSetupRemotePayload(request, config.user);
@@ -183,7 +183,8 @@ async function executeWhcSetupRemote(config, request, deps = {}) {
183
183
  };
184
184
  }
185
185
  function buildSshRemoteHint(config, deployTargetPath) {
186
- const { username, host } = config.sshTargets.prod;
186
+ const host = config.sshTargets.prod.host || config.host;
187
+ const username = config.sshTargets.prod.username || config.user;
187
188
  return `${username}@${host}:${deployTargetPath}`;
188
189
  }
189
190
  function inferBaselineKind(sourceKind) {
@@ -226,7 +227,7 @@ function buildSetupRemotePhaseCoverage(dryRun) {
226
227
  };
227
228
  }
228
229
  function extractHomeOwner(pathValue) {
229
- const match = pathValue.match(/^\/home\/([^\/]+)\//);
230
+ const match = pathValue.match(/^\/home\/([^/]+)\//);
230
231
  return match?.[1];
231
232
  }
232
233
  function buildSuggestedSetupRemotePayload(request, currentUser) {