@domino-sdk/relay-cli 0.3.0 → 0.5.0

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.
@@ -12,7 +12,25 @@
12
12
  "auth.emailVerify",
13
13
  "auth.session"
14
14
  ],
15
- "evidence": ["tests/auth.test.mjs", "tests/email-auth.test.mjs"]
15
+ "evidence": [
16
+ "tests/auth.test.mjs",
17
+ "tests/email-auth.test.mjs"
18
+ ]
19
+ },
20
+ {
21
+ "id": "connected-accounts",
22
+ "support": "native",
23
+ "description": "Operators connect a Discord server and install the managed bot in Console Integrations. Participants sign in with Discord or link its identity. Declare integrations: [discord] and use ctx.discord.member() without a server ID. Captured membership and role reads survive recovery. No messages, writes, or X verification.",
24
+ "entrypoints": [
25
+ "auth.accounts",
26
+ "auth.start",
27
+ "auth.disconnect"
28
+ ],
29
+ "reference": "authoring",
30
+ "evidence": [
31
+ "tests/connected-accounts.test.mjs",
32
+ "apps/api/src/provider-registry.ts"
33
+ ]
16
34
  },
17
35
  {
18
36
  "id": "identity-exchange",
@@ -49,38 +67,45 @@
49
67
  {
50
68
  "id": "milestones",
51
69
  "support": "composed",
52
- "description": "Compose same-participant completion prerequisites with requires: [completed(quest)]. Arbitrary aggregate thresholds and cross-participant conditions need application logic.",
70
+ "description": "Compose same-participant completion prerequisites with requires: [completed(quest)]. Referral programs support milestones based on distinct qualified friends. Other aggregate thresholds need application logic.",
53
71
  "entrypoints": ["completed", "automatic", "defineQuest"],
54
72
  "evidence": ["tests/availability.test.mjs"]
55
73
  },
56
74
  {
57
75
  "id": "referrals",
58
- "support": "own-backend",
59
- "description": "No native referral attribution service. Your backend must issue invite codes, attribute the authenticated invitee, deduplicate claims and prevent self-referrals. A URL parameter alone is not verified attribution.",
60
- "entrypoints": [],
76
+ "support": "native",
77
+ "description": "Project-scoped invitation codes, immutable inviter attribution, configurable signup, quest or external qualification, fixed rewards and distinct-friend milestones. Selective one-level inviter bonuses add points without reducing invitee awards. Code-defined rules publish through reviewed project deployments.",
78
+ "entrypoints": [
79
+ "defineReferral",
80
+ "referrals.list",
81
+ "referrals.summary",
82
+ "referrals.accept",
83
+ "referrals.history"
84
+ ],
61
85
  "reference": "referrals",
62
- "evidence": [
63
- "packages/sdk/src/authoring.ts",
64
- "packages/sdk/src/schema.ts"
65
- ]
86
+ "evidence": ["tests/referrals.test.mjs", "packages/sdk/src/referrals.ts"]
66
87
  },
67
88
  {
68
89
  "id": "cross-participant-rewards",
69
- "support": "unsupported",
70
- "description": "Quest rewards target the completing participant. There is no supported inviter-targeted reward operation. Do not promise automatic inviter points; an application-owned reward ledger requires its own implementation.",
71
- "entrypoints": [],
90
+ "support": "native",
91
+ "description": "Referral programs can reward inviter and invitee independently and award a selective percentage of eligible points to the inviter. Bonuses accumulate fractions and support limits, suspension and correction recovery. Arbitrary participant-to-participant transfers and multi-level commissions are not supported.",
92
+ "entrypoints": ["defineReferral", "referrals.summary"],
72
93
  "reference": "referrals",
73
- "evidence": [
74
- "packages/sdk/src/authoring.ts",
75
- "packages/sdk/src/schema.ts"
76
- ]
94
+ "evidence": ["tests/referrals.test.mjs", "apps/api/src/referrals.ts"]
77
95
  },
78
96
  {
79
97
  "id": "rankings",
80
- "support": "own-backend",
81
- "description": "No participant leaderboard or referral queue ranking API. Build an authorized backend projection with explicit privacy and tie-breaking rules; never expose management credentials in a browser.",
82
- "entrypoints": [],
83
- "evidence": ["packages/sdk/src/index.ts"]
98
+ "support": "native",
99
+ "description": "Publish project-scoped points leaderboards through project deployments. Read paginated standings, your rank, and nearby competitors with shared tie ranks. Code defines balance, earned, or net calculations and time windows. Public access is explicit; participant rows use board-specific aliases. Scoped management APIs supply external points, corrections, groups, and exclusions. Referral bonus points contribute to their named balance. Cross-project balances, finalized results, and rank-based prizes are separate work.",
100
+ "entrypoints": [
101
+ "defineLeaderboard",
102
+ "leaderboards.list",
103
+ "leaderboards.standings"
104
+ ],
105
+ "evidence": [
106
+ "tests/leaderboards.test.mjs",
107
+ "packages/sdk/src/leaderboards.ts"
108
+ ]
84
109
  },
85
110
  {
86
111
  "id": "wallet-verification",
@@ -1,11 +1,18 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { action } from "../runtime.mjs";
3
3
 
4
- const references = ["participant-api", "referrals", "troubleshooting"];
4
+ const references = [
5
+ "authoring",
6
+ "participant-api",
7
+ "referrals",
8
+ "troubleshooting",
9
+ ];
5
10
  export function registerDiscoveryCommands(program) {
6
11
  program
7
12
  .command("capabilities")
8
- .description("Discover supported Domino features and their limitations")
13
+ .description(
14
+ "Read this CLI release's bundled capability catalog (not a server probe)",
15
+ )
9
16
  .argument("[id]", "Capability identifier")
10
17
  .action(
11
18
  action(async (_, id) => {
@@ -15,13 +22,25 @@ export function registerDiscoveryCommands(program) {
15
22
  "utf8",
16
23
  ),
17
24
  );
18
- if (!id) return catalog;
25
+ const { version } = JSON.parse(
26
+ await readFile(
27
+ new URL("../../package.json", import.meta.url),
28
+ "utf8",
29
+ ),
30
+ );
31
+ const provenance = {
32
+ kind: "bundled-catalog",
33
+ cliVersion: version,
34
+ serverVerified: false,
35
+ note: "Describes this CLI release. Scope flags do not make it project-specific. Verify the deployed contract before treating missing or conflicting entries as unsupported.",
36
+ };
37
+ if (!id) return { ...catalog, provenance };
19
38
  const capability = catalog.capabilities.find((item) => item.id === id);
20
39
  if (!capability)
21
40
  throw new Error(
22
41
  `Unknown capability. Choose: ${catalog.capabilities.map((item) => item.id).join(", ")}`,
23
42
  );
24
- return capability;
43
+ return { ...capability, provenance };
25
44
  }),
26
45
  );
27
46
  program
@@ -7,7 +7,7 @@ import { action } from "../runtime.mjs";
7
7
  import { request } from "../connection.mjs";
8
8
  import { checkout, requireNewDirectory } from "../git.mjs";
9
9
  import { dev } from "../dev.mjs";
10
- import { bundleCatalog, bundleProject } from "../project.mjs";
10
+ import { bundleProject } from "../project.mjs";
11
11
 
12
12
  export function registerHostingCommands(program) {
13
13
  program
@@ -29,15 +29,14 @@ export function registerHostingCommands(program) {
29
29
  throw new Error(error.stdout || error.stderr || error.message);
30
30
  }
31
31
  }
32
- const quests = project.config.quests.length
33
- ? await bundleProject(project)
34
- : { releases: [] };
35
- const catalog = await bundleCatalog(project);
32
+ const bundle = await bundleProject(project, { allowEmpty: true });
36
33
  return {
37
34
  checked: true,
38
- quests: quests.releases.length,
39
- types: catalog.types.length,
40
- collections: catalog.collections.length,
35
+ quests: bundle.releases.length,
36
+ leaderboards: bundle.leaderboards.length,
37
+ referrals: bundle.referrals.length,
38
+ types: bundle.types.length,
39
+ collections: bundle.collections.length,
41
40
  };
42
41
  }),
43
42
  );
@@ -1,4 +1,5 @@
1
- import { writeFile } from "node:fs/promises";
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
2
3
  import { action } from "../runtime.mjs";
3
4
  import { request } from "../connection.mjs";
4
5
  import { initialize, bundleProject } from "../project.mjs";
@@ -26,6 +27,8 @@ function summary(value) {
26
27
  })),
27
28
  collections: value.collections,
28
29
  supportedInteractions: value.supportedInteractions,
30
+ leaderboards: value.leaderboards,
31
+ referrals: value.referrals,
29
32
  };
30
33
  }
31
34
  async function publish(connection, id) {
@@ -74,12 +77,15 @@ async function build({ project, connection, options }, deploy) {
74
77
  return publish(connection, preview.id);
75
78
  }
76
79
  if (options.out) {
80
+ await mkdir(dirname(options.out), { recursive: true });
77
81
  await writeFile(options.out, JSON.stringify(payload, null, 2) + "\n");
78
82
  return {
79
83
  file: options.out,
80
84
  quests: payload.releases.length,
81
85
  types: payload.types.length,
82
86
  collections: payload.collections.length,
87
+ leaderboards: payload.leaderboards.length,
88
+ referrals: payload.referrals.length,
83
89
  };
84
90
  }
85
91
  return payload;
@@ -20,9 +20,9 @@ export function registerStagingCommands(program) {
20
20
  action(async ({ connection, project, options }) => {
21
21
  if (!project)
22
22
  throw new Error("Run stage inside a hosted campaign checkout.");
23
- if (project.config.app?.kind !== "static")
23
+ if (!project.config.app)
24
24
  throw new Error(
25
- "Cloud staging currently supports static campaign checkouts with app.kind=static in relay.json. This project is not configured for cloud staging. SSR Worker apps use the Cloudflare Worker deployment workflow.",
25
+ "Configure app.kind as static or worker in relay.json before staging.",
26
26
  );
27
27
  const scoped = { ...connection, environment: "test" };
28
28
  const preflight = await request(scoped, "/preflight");
@@ -22,7 +22,7 @@ export async function connectedDev(project, connection, options) {
22
22
  const preflight = await request(connection, "/preflight");
23
23
  if (preflight.development?.status !== "configured")
24
24
  throw new Error(
25
- "Hosted development is not configured. Run domino doctor --remote --json; a Domino operator must enable previews and test sign-in.",
25
+ "Hosted development is not configured. Run domino doctor --remote --json; a Domino operator must enable preview hosting.",
26
26
  );
27
27
  const root = dirname(project.path);
28
28
  const connector = await previewConnector(options.cloudflared);
@@ -126,6 +126,7 @@ export async function connectedDev(project, connection, options) {
126
126
  session = await request(connection, "/development/session", "POST", {
127
127
  ...lease,
128
128
  port,
129
+ routing: project.config.app.routing,
129
130
  });
130
131
  const sync = developmentSync(connection, project.path);
131
132
  await sync();
@@ -143,6 +144,7 @@ export async function connectedDev(project, connection, options) {
143
144
  start("pnpm", ["run", project.config.app.devScript], {
144
145
  DOMINO_APP_PORT: String(port),
145
146
  DOMINO_PREVIEW_ORIGIN: session.url,
147
+ DOMINO_PARTICIPANT_API: connection.apiUrl,
146
148
  });
147
149
  let appReady = false;
148
150
  for (let attempt = 0; attempt < 150 && !stopping; attempt++) {
@@ -190,7 +192,7 @@ export async function connectedDev(project, connection, options) {
190
192
  )
191
193
  );
192
194
  process.stderr.write(
193
- `Preview: ${session.url}\nUses this project's hosted test data and real sign-in. Keep this terminal running. Use domino stage for a preview that stays online.\n`,
195
+ `Preview: ${session.url}\nKeep this terminal running. Use domino stage for a preview that stays online.\n`,
194
196
  );
195
197
  let renewed = Date.now();
196
198
  while (!stopping) {
@@ -0,0 +1,62 @@
1
+ import { glob } from "node:fs/promises";
2
+ import { dirname, relative, resolve } from "node:path";
3
+
4
+ export const defaultDiscovery = {
5
+ quests: ["quests/**/*.quest.ts"],
6
+ questTypes: ["quests/**/*.quest-type.ts"],
7
+ };
8
+
9
+ // These directories contain dependencies, build output, or local runtime data.
10
+ const excluded =
11
+ /(^|[/\\])(node_modules|dist|\.git|\.domino|\.domino-build|\.wrangler|\.artifacts)([/\\]|$)/;
12
+
13
+ export async function discoverEntries(project, kind) {
14
+ const root = dirname(project.path);
15
+ const discovery = project.config.discover;
16
+ const entries = new Map();
17
+ const key = (entry) =>
18
+ JSON.stringify([resolve(root, entry.entry), entry.exportName ?? "default"]);
19
+
20
+ for (const entry of project.config[kind]) {
21
+ const identity = key(entry);
22
+ if (entries.has(identity))
23
+ throw new Error(`Duplicate ${kind} entry: ${entry.entry}`);
24
+ entries.set(identity, entry);
25
+ }
26
+
27
+ if (discovery) {
28
+ const paths = [];
29
+ for await (const file of glob(discovery[kind], {
30
+ cwd: root,
31
+ withFileTypes: true,
32
+ exclude: (file) =>
33
+ excluded.test(relative(root, resolve(file.parentPath, file.name))),
34
+ })) {
35
+ if (file.isFile()) paths.push(resolve(file.parentPath, file.name));
36
+ }
37
+ for (const path of paths.sort()) {
38
+ const entry = {
39
+ entry: path,
40
+ exportName: "default",
41
+ provider: discovery.provider,
42
+ ...(kind === "quests" ? { settings: {} } : {}),
43
+ };
44
+ // Explicit entries customize a discovered default export without adding it twice.
45
+ const explicit = entries.get(key(entry));
46
+ entries.set(
47
+ key(entry),
48
+ explicit
49
+ ? {
50
+ ...explicit,
51
+ exportName: "default",
52
+ provider: explicit.provider ?? discovery.provider,
53
+ }
54
+ : entry,
55
+ );
56
+ }
57
+ }
58
+ return [...entries.values()].map((entry) => ({
59
+ ...entry,
60
+ provider: entry.provider ?? "workers-ai",
61
+ }));
62
+ }
package/cli/doctor.mjs CHANGED
@@ -4,10 +4,13 @@ import { createHash } from "node:crypto";
4
4
  import { access, readFile } from "node:fs/promises";
5
5
  import { inspectAgentSkills } from "./agents.mjs";
6
6
  import { request } from "./connection.mjs";
7
+ import { discoverEntries } from "./discovery.mjs";
8
+ import { packageManager } from "./package-manager.mjs";
7
9
 
8
- export async function diagnose({ project, connection, options }) {
10
+ export async function diagnose({ project, connection, options, scopeSources }) {
9
11
  const checks = [];
10
12
  const root = project ? dirname(project.path) : process.cwd();
13
+ const manager = await packageManager(root);
11
14
  checks.push(
12
15
  project
13
16
  ? { id: "manifest", status: "ok", message: "Project manifest is valid." }
@@ -87,7 +90,12 @@ export async function diagnose({ project, connection, options }) {
87
90
  );
88
91
 
89
92
  if (project) {
90
- const entries = [...project.config.quests, ...project.config.questTypes];
93
+ const entries = [
94
+ ...(await discoverEntries(project, "quests")),
95
+ ...(await discoverEntries(project, "questTypes")),
96
+ ...project.config.leaderboards,
97
+ ...project.config.referrals,
98
+ ];
91
99
  const directories = new Set(
92
100
  entries.map((entry) => dirname(resolve(root, entry.entry))),
93
101
  );
@@ -164,6 +172,13 @@ export async function diagnose({ project, connection, options }) {
164
172
  development: { status: "not-checked" },
165
173
  staging: "not-checked",
166
174
  };
175
+ if (options.remote && !project)
176
+ checks.push({
177
+ id: "remote-access",
178
+ status: "warning",
179
+ message:
180
+ "Remote access was not checked because no relay.json was found. This does not diagnose a failed checkout. Use its checkout error details; do not install skills in the parent directory to repair remote access.",
181
+ });
167
182
  if (options.remote && project) {
168
183
  try {
169
184
  const value = await request(connection, "/access");
@@ -232,6 +247,8 @@ export async function diagnose({ project, connection, options }) {
232
247
  project: connection.project,
233
248
  environment: connection.environment,
234
249
  },
250
+ scopeSources,
251
+ packageManager: manager,
235
252
  agents,
236
253
  readiness,
237
254
  remoteAccess,
@@ -239,6 +256,10 @@ export async function diagnose({ project, connection, options }) {
239
256
  ? "static-app-configured"
240
257
  : "use-existing-app-workflow",
241
258
  checks,
242
- next: "After setup, run the project's checks and verify a real participant action. Setup diagnostics do not prove preview or live readiness.",
259
+ next:
260
+ checks.some((check) => check.id === "sdk" && check.status === "error") &&
261
+ manager.installCommand
262
+ ? `Run ${manager.installCommand} in ${root} and wait for it to finish before rerunning doctor. Then verify a participant action.`
263
+ : "After setup, run the project's checks and verify a real participant action. Setup diagnostics do not prove preview or live readiness.",
243
264
  };
244
265
  }
package/cli/git.mjs CHANGED
@@ -3,6 +3,7 @@ import { access, mkdir, rm, writeFile } from "node:fs/promises";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { apiUrl, request, savedToken } from "./connection.mjs";
6
+ import { packageManager } from "./package-manager.mjs";
6
7
 
7
8
  export function runGit(args, { cwd, input, env = {} } = {}) {
8
9
  return new Promise((resolve, reject) => {
@@ -20,11 +21,13 @@ export function runGit(args, { cwd, input, env = {} } = {}) {
20
21
  stderr += data;
21
22
  });
22
23
  child.on("error", reject);
23
- child.on("close", (code) =>
24
- code === 0
25
- ? resolve(stdout.trim())
26
- : reject(new Error(`Git failed (${code}): ${stderr.trim()}`)),
27
- );
24
+ child.on("close", (code) => {
25
+ if (code === 0) return resolve(stdout.trim());
26
+ const error = new Error(`Git failed (${code}): ${stderr.trim()}`);
27
+ error.exitCode = code;
28
+ error.gitCommand = args[0];
29
+ reject(error);
30
+ });
28
31
  child.stdin.end(input);
29
32
  });
30
33
  }
@@ -86,7 +89,7 @@ export async function requireNewDirectory(directory) {
86
89
  export async function checkout(connection, repository, directory) {
87
90
  if (repository.status !== "ready")
88
91
  throw new Error(
89
- "Repository provisioning is incomplete. Retry domino create.",
92
+ `Repository provisioning is incomplete (status: ${repository.status}). Inspect the existing project in Console, then retry domino checkout. Do not create a replacement project.`,
90
93
  );
91
94
  const remote = new URL(repository.remote);
92
95
  if (
@@ -152,14 +155,42 @@ export async function checkout(connection, repository, directory) {
152
155
  environment: "test",
153
156
  }) + "\n",
154
157
  );
158
+ const manager = await packageManager(root);
155
159
  return {
156
160
  directory: root,
157
161
  remote: repository.remote,
158
- next: "Install dependencies with pnpm install, then run domino dev.",
162
+ packageManager: manager,
163
+ scopeFile: resolve(root, ".git/domino.json"),
164
+ next: manager.installCommand
165
+ ? `In ${root}, run ${manager.installCommand} and wait for success. Then run the project's installed domino doctor --remote --json before domino dev.`
166
+ : "Read package.json and install with the project's declared package manager and lockfile. Wait for success before running the installed domino doctor --remote --json.",
159
167
  };
160
168
  } catch (error) {
161
169
  // This directory was created exclusively by this operation and has no user edits.
162
170
  await rm(root, { recursive: true, force: true });
171
+ if (error.gitCommand) {
172
+ error.code = "CHECKOUT_GIT_FAILED";
173
+ error.details = {
174
+ phase: error.gitCommand,
175
+ exitCode: error.exitCode,
176
+ classification: /repository not found/i.test(error.message)
177
+ ? "repository-unavailable"
178
+ : /authentication failed|401|403/i.test(error.message)
179
+ ? "access-denied"
180
+ : "git-failure",
181
+ repositoryStatus: repository.status,
182
+ organization: repository.organization,
183
+ project: repository.project,
184
+ remote: repository.remote,
185
+ };
186
+ error.message += ` Repository API reported ${repository.status}; Git ${error.gitCommand} failed.`;
187
+ error.message +=
188
+ error.gitCommand === "fetch"
189
+ ? " This does not establish whether provisioning, access, or transport caused the failure. Retry checkout once with the same scope; if it repeats, share these diagnostics with the operator."
190
+ : " Resolve the Git error above before retrying checkout.";
191
+ error.message +=
192
+ " Do not recreate the project or install dependencies in its parent directory.";
193
+ }
163
194
  throw error;
164
195
  }
165
196
  }
@@ -0,0 +1,31 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ // Only generate a runnable command for a validated, exact package-manager pin.
5
+ // Never interpolate arbitrary package.json content into a suggested shell command.
6
+ export async function packageManager(directory) {
7
+ let pkg;
8
+ try {
9
+ pkg = JSON.parse(await readFile(join(directory, "package.json"), "utf8"));
10
+ } catch (error) {
11
+ if (error.code === "ENOENT")
12
+ return { declared: null, installCommand: null };
13
+ return {
14
+ declared: null,
15
+ installCommand: null,
16
+ error:
17
+ "Cannot read package.json. Restore valid JSON before installing dependencies.",
18
+ };
19
+ }
20
+ const declared =
21
+ typeof pkg?.packageManager === "string" ? pkg.packageManager : null;
22
+ const pin =
23
+ /^(pnpm|npm)@(\d+\.\d+\.\d+(?:-[\w.-]+)?)(?:\+sha\d+\.[a-fA-F0-9]+)?$/.exec(
24
+ declared ?? "",
25
+ );
26
+ if (!pin) return { declared, installCommand: null };
27
+ const [, name, version] = pin;
28
+ const install = name === "pnpm" ? "install --frozen-lockfile" : "ci";
29
+ const runner = `npx --yes ${name}@${version}`;
30
+ return { declared, installCommand: `${runner} ${install}` };
31
+ }