@descix/cli 1.0.20 → 1.0.21

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/bin/descix.js CHANGED
@@ -14,6 +14,7 @@ import { requireAuth } from '../lib/auth-guard.js';
14
14
  import { WorkspaceConfig, unmappedAppMessage, resolveWorkspacePath } from '../lib/workspace-config.js';
15
15
  import { CLI_VERSION } from '../lib/cli-version.js';
16
16
  import { recordInvocationOrigin } from '../lib/origin.js';
17
+ import { verifyDeployOutcome } from '../lib/deploy-outcome.js';
17
18
  // Chat session pointer + the ONE rule for when a dead pointer may be self-healed.
18
19
  import {
19
20
  getSessionInteractionId,
@@ -2589,6 +2590,28 @@ siteCommand
2589
2590
  branch: gitStatus.branch
2590
2591
  } : null,
2591
2592
  preview: options.preview || false
2593
+ }).catch(async (confirmErr) => {
2594
+ // ws-prune-504: a lost confirm RESPONSE is not a failed DEPLOY. Re-read the served
2595
+ // manifest keyed on THIS upload's token and say which it actually is — never "failed"
2596
+ // when the truth is knowable.
2597
+ console.log(chalk.yellow(`\n Confirm response lost (${confirmErr.message}) — reading the deploy record back...`));
2598
+ const verdict = await verifyDeployOutcome(apiClient, {
2599
+ communityId, appId, tokenId: token_id, preview: options.preview || false
2600
+ });
2601
+ if (verdict.outcome === 'confirmed') {
2602
+ console.log(chalk.green(`\n Deploy CONFIRMED — ${verdict.detail}.`));
2603
+ console.log(chalk.gray(` The server may still be finishing the stale-file prune; ` +
2604
+ `'descix site status -c ${communityId} -a ${appId}' shows the deploy, 'descix site list' what is served.`));
2605
+ process.exit(0);
2606
+ }
2607
+ if (verdict.outcome === 'unconfirmed') {
2608
+ console.error(chalk.red(`\n Deploy UNCONFIRMED — ${verdict.detail}.`));
2609
+ console.error(chalk.red(` Nothing was removed from the server. Re-run the upload.`));
2610
+ process.exit(1);
2611
+ }
2612
+ console.error(chalk.red(`\n Deploy outcome UNKNOWN — ${verdict.detail}.`));
2613
+ console.error(chalk.red(` Check 'descix site status -c ${communityId} -a ${appId}' before re-running.`));
2614
+ process.exit(1);
2592
2615
  });
2593
2616
 
2594
2617
  const result = confirmResponse.message;
@@ -2602,6 +2625,10 @@ siteCommand
2602
2625
  }
2603
2626
  console.log(chalk.gray(` Removed from the server: ${pruned.length}`));
2604
2627
  pruned.forEach(f => console.log(chalk.gray(` - ${f}`)));
2628
+ if (result.prune_status === 'partial') {
2629
+ console.log(chalk.yellow(` Prune PARTIAL: ${result.prune_remaining_count} stale file(s) not yet removed (soft budget).`));
2630
+ console.log(chalk.yellow(` ${result.prune_remedy}`));
2631
+ }
2605
2632
  console.log(chalk.gray(` Deployed: ${result.deployed_at}`));
2606
2633
  if (result.preview) {
2607
2634
  console.log(chalk.yellow(` Mode: PREVIEW`));
@@ -0,0 +1,45 @@
1
+ /**
2
+ * verifyDeployOutcome — the honest answer when a confirm_site_deploy response is LOST.
3
+ *
4
+ * ws-prune-504 (measured on prod 2026-10-10): a large prune held the confirm past the
5
+ * gateway's patience; the client saw "Upload failed: HTTP 504" while the deploy had in fact
6
+ * confirmed and the prune was finishing server-side. A lost RESPONSE is not a failed DEPLOY —
7
+ * and a tool that says "failed" when it does not know is lying in one direction.
8
+ *
9
+ * The read-back is keyed on the deploy TOKEN: get_site_deploy_token minted token_id for THIS
10
+ * upload, and confirm_site_deploy stamps it into the served manifest's deploy.token_id. If the
11
+ * served manifest carries our token, OUR deploy confirmed — no weaker signal (git sha, file
12
+ * count) can be forged by a concurrent deploy into a false positive the token cannot.
13
+ *
14
+ * @param {{invoke: (cmd: string, params: object) => Promise<any>}} apiClient
15
+ * @param {{communityId: string, appId: string, tokenId: string, preview?: boolean}} sent
16
+ * @returns {Promise<{outcome: 'confirmed'|'unconfirmed'|'unknown', detail: string, deploy?: object}>}
17
+ */
18
+ export async function verifyDeployOutcome(apiClient, { communityId, appId, tokenId, preview = false }) {
19
+ if (!tokenId) {
20
+ return { outcome: 'unknown', detail: 'no token_id in hand — cannot key the read-back' };
21
+ }
22
+ let response;
23
+ try {
24
+ response = await apiClient.invoke('get_site_manifest', {
25
+ community_id: communityId,
26
+ app_id: appId,
27
+ preview,
28
+ });
29
+ } catch (err) {
30
+ return { outcome: 'unknown', detail: `read-back failed too: ${err.message}` };
31
+ }
32
+ const payload = response?.message ?? response;
33
+ const deploy = payload?.manifest?.deploy;
34
+ if (!payload?.exists || !deploy) {
35
+ return { outcome: 'unconfirmed', detail: 'no deploy manifest is served for this app' };
36
+ }
37
+ if (deploy.token_id === tokenId) {
38
+ return { outcome: 'confirmed', detail: `served manifest carries this upload's token (deployed_at ${deploy.deployed_at})`, deploy };
39
+ }
40
+ return {
41
+ outcome: 'unconfirmed',
42
+ detail: `served manifest is a DIFFERENT deploy (token ${deploy.token_id || 'none'}, deployed_at ${deploy.deployed_at || 'unknown'})`,
43
+ deploy,
44
+ };
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@descix/cli",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
4
4
  "description": "DeSciX CLI and MCP Server - Unified command-line interface for DeSciX platform",
5
5
  "repository": {
6
6
  "type": "git",