castle-web-cli 0.4.60 → 0.4.61

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.
@@ -26,6 +26,17 @@ export type GraphqlFetch = (
26
26
  variables: Record<string, unknown>,
27
27
  ) => Promise<GraphqlResponse>;
28
28
 
29
+ export type PlatformHandler = (
30
+ command: unknown,
31
+ params: Record<string, unknown>,
32
+ ctx: HostContext,
33
+ ) => Promise<unknown>;
34
+
35
+ export interface HostCapabilities {
36
+ graphqlFetch: GraphqlFetch;
37
+ platformHandler?: PlatformHandler;
38
+ }
39
+
29
40
  export interface SerializedCommandError {
30
41
  code: string;
31
42
  message: string;
@@ -43,5 +54,5 @@ export function executeCommand(
43
54
  ctx: HostContext,
44
55
  command: unknown,
45
56
  params: unknown,
46
- graphqlFetch: GraphqlFetch,
57
+ capabilities: HostCapabilities,
47
58
  ): Promise<HostResult>;
@@ -24,11 +24,22 @@ const COMMAND_NAMES = [
24
24
  "leaderboard.save",
25
25
  "user.getCurrent",
26
26
  "time.getServerTime",
27
+ "pass.has",
28
+ "pass.offer",
27
29
  ];
30
+ // Platform/capability commands: NOT serviced by graphqlFetch. They're dispatched
31
+ // to the host's optional platformHandler (mobile renders native UI; web shows an
32
+ // upsell). A host with no platformHandler returns the command's normalized
33
+ // "unavailable" outcome rather than an error — capability divergence is the
34
+ // host's concern, never a deck-facing gate.
35
+ const PLATFORM_COMMAND_NAMES = ["pass.offer"];
28
36
  function isCommandName(value) {
29
37
  return (typeof value === "string" &&
30
38
  COMMAND_NAMES.includes(value));
31
39
  }
40
+ function isPlatformCommand(command) {
41
+ return PLATFORM_COMMAND_NAMES.includes(command);
42
+ }
32
43
  class HostCommandError extends Error {
33
44
  code;
34
45
  command;
@@ -41,7 +52,7 @@ class HostCommandError extends Error {
41
52
  this.extensions = extensions;
42
53
  }
43
54
  }
44
- export async function executeCommand(ctx, command, params, graphqlFetch) {
55
+ export async function executeCommand(ctx, command, params, capabilities) {
45
56
  if (!isCommandName(command)) {
46
57
  return {
47
58
  ok: false,
@@ -53,14 +64,18 @@ export async function executeCommand(ctx, command, params, graphqlFetch) {
53
64
  };
54
65
  }
55
66
  try {
56
- const data = await runCommand(ctx, command, (params ?? {}), graphqlFetch);
67
+ const data = await runCommand(ctx, command, (params ?? {}), capabilities);
57
68
  return { ok: true, data };
58
69
  }
59
70
  catch (error) {
60
71
  return { ok: false, error: toSerializedError(error, command) };
61
72
  }
62
73
  }
63
- function runCommand(ctx, command, params, gql) {
74
+ function runCommand(ctx, command, params, caps) {
75
+ if (isPlatformCommand(command)) {
76
+ return runPlatformCommand(ctx, command, params, caps);
77
+ }
78
+ const gql = caps.graphqlFetch;
64
79
  switch (command) {
65
80
  case "deckStorage.load":
66
81
  return deckStorageLoad(ctx, gql);
@@ -78,7 +93,68 @@ function runCommand(ctx, command, params, gql) {
78
93
  return Promise.resolve(userGetCurrent(ctx));
79
94
  case "time.getServerTime":
80
95
  return timeGetServerTime(gql);
96
+ case "pass.has":
97
+ return passHas(ctx, params, gql);
98
+ // Platform commands are handled above; listed here to keep the switch
99
+ // exhaustive over CommandName.
100
+ case "pass.offer":
101
+ return runPlatformCommand(ctx, command, params, caps);
102
+ }
103
+ }
104
+ // Dispatch a platform/capability command to the host's platformHandler. No
105
+ // handler → the command's normalized "unavailable" outcome (a SUCCESS, not an
106
+ // error: a deck on a host without this capability still gets one uniform
107
+ // result). The handler's return is normalized so a malformed outcome can't
108
+ // leak through to the deck.
109
+ async function runPlatformCommand(ctx, command, params, caps) {
110
+ switch (command) {
111
+ case "pass.offer":
112
+ return passesOffer(ctx, params, caps);
113
+ default:
114
+ return unavailableOutcome(command);
115
+ }
116
+ }
117
+ async function passHas(ctx, params, gql) {
118
+ const deckId = requireDeckId(ctx, "pass.has");
119
+ const passId = asString(params.passId, "passId", "pass.has");
120
+ const data = await graphql(gql, PASSES_FOR_DECK_QUERY, { deckId }, "pass.has");
121
+ const match = (data.passesForDeck ?? []).find((p) => p?.passId === passId);
122
+ return { hasPass: match?.isActive === true };
123
+ }
124
+ async function passesOffer(ctx, params, caps) {
125
+ const passId = asString(params.passId, "passId", "pass.offer");
126
+ // Hosts without bricks support (dev CLI — no handler at all) get a normalized
127
+ // unavailable, never an error or a thrown MISSING_DECK_ID.
128
+ if (!caps.platformHandler)
129
+ return { status: "unavailable" };
130
+ // Real transaction path (mobile native sheet; web upsell): the pass belongs
131
+ // to a deck, so a trusted deckId is required before handing off.
132
+ const deckId = requireDeckId(ctx, "pass.offer");
133
+ const outcome = await caps.platformHandler("pass.offer", { passId, deckId }, ctx);
134
+ return normalizePassOutcome(outcome);
135
+ }
136
+ function normalizePassOutcome(value) {
137
+ const record = typeof value === "object" && value !== null
138
+ ? value
139
+ : {};
140
+ const status = record.status;
141
+ const valid = [
142
+ "purchased",
143
+ "alreadyOwned",
144
+ "cancelled",
145
+ "unavailable",
146
+ ];
147
+ if (typeof status === "string" && valid.includes(status)) {
148
+ return { status: status };
149
+ }
150
+ return { status: "cancelled" };
151
+ }
152
+ function unavailableOutcome(command) {
153
+ // Only passes exists today; keep this total over future platform commands.
154
+ if (command === "pass.offer") {
155
+ return { status: "unavailable" };
81
156
  }
157
+ return { status: "unavailable" };
82
158
  }
83
159
  async function deckStorageLoad(ctx, gql) {
84
160
  const deckId = requireDeckId(ctx, "deckStorage.load");
@@ -362,6 +438,14 @@ const SAVE_LEADERBOARD_MUTATION = `
362
438
  )
363
439
  }
364
440
  `;
441
+ const PASSES_FOR_DECK_QUERY = `
442
+ query CastlePassesForDeck($deckId: ID!) {
443
+ passesForDeck(deckId: $deckId) {
444
+ passId
445
+ isActive
446
+ }
447
+ }
448
+ `;
365
449
  const SERVER_TIME_QUERY = `
366
450
  query CastleServerTime {
367
451
  serverTime {
package/dist/ide.js CHANGED
@@ -670,6 +670,9 @@ const IDE_STYLES = `
670
670
  margin-top: 6px;
671
671
  max-height: 65px;
672
672
  overflow-y: auto;
673
+ /* Hide the scrollbar (Windows draws a persistent one; macOS auto-hides). */
674
+ scrollbar-width: none;
675
+ -ms-overflow-style: none;
673
676
  font-size: 12px;
674
677
  line-height: 1.5;
675
678
  color: #57606a;
@@ -683,6 +686,7 @@ const IDE_STYLES = `
683
686
  );
684
687
  mask-image: linear-gradient(to bottom, transparent, #000000 5px, #000000 100%);
685
688
  }
689
+ .task-feed::-webkit-scrollbar { width: 0; height: 0; }
686
690
  .task-row { display: flex; align-items: center; gap: 8px; }
687
691
  .task-title {
688
692
  flex: 1 1 auto;
package/dist/serve.js CHANGED
@@ -93,7 +93,10 @@ async function devHostContext(projectDir) {
93
93
  }
94
94
  async function handleCastleCommand(ws, projectDir, requestId, command, params) {
95
95
  const ctx = await devHostContext(projectDir);
96
- const result = await executeCommand(ctx, command, params, castleGraphql);
96
+ // The dev server services data commands via GraphQL but supplies no
97
+ // platformHandler, so platform/capability commands (e.g. passes.purchase)
98
+ // resolve to their normalized "unavailable" outcome.
99
+ const result = await executeCommand(ctx, command, params, { graphqlFetch: castleGraphql });
97
100
  if (ws.readyState === WebSocket.OPEN) {
98
101
  ws.send(JSON.stringify({ type: 'castle_command_response', castleSdk: 1, requestId, ...result }));
99
102
  }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "castle-web-cli",
3
- "version": "0.4.60",
3
+ "version": "0.4.61",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "castle-web": "./dist/index.js"
7
7
  },
8
8
  "scripts": {
9
- "build": "tsc && cp -R src/castle-host dist/castle-host && rm -rf kits && cp -R ../kits kits",
9
+ "build": "tsc && rm -rf dist/castle-host && cp -R src/castle-host dist/castle-host && rm -rf kits && cp -R ../kits kits",
10
10
  "dev": "tsc --watch",
11
11
  "check": "eslint . && jscpd && tsc --noEmit"
12
12
  },