@domino-sdk/relay-cli 0.1.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.
- package/README.md +14 -0
- package/cli/agents.mjs +111 -0
- package/cli/commands/agents.mjs +50 -0
- package/cli/commands/api.mjs +34 -0
- package/cli/commands/auth.mjs +60 -0
- package/cli/commands/hosting.mjs +91 -0
- package/cli/commands/inspect.mjs +47 -0
- package/cli/commands/project.mjs +160 -0
- package/cli/commands/staging.mjs +94 -0
- package/cli/connection.mjs +114 -0
- package/cli/dev.mjs +231 -0
- package/cli/device-login.mjs +80 -0
- package/cli/doctor.mjs +156 -0
- package/cli/git.mjs +165 -0
- package/cli/project.mjs +206 -0
- package/cli/runtime.mjs +39 -0
- package/cli/skills/domino/SKILL.md +30 -0
- package/cli/skills/domino/references/authoring.md +15 -0
- package/cli/skills/domino/references/existing-app.md +19 -0
- package/cli/skills/domino/references/hosted.md +17 -0
- package/cli.mjs +112 -0
- package/dist/index.d.ts +5724 -0
- package/dist/index.js +428 -0
- package/package.json +41 -0
package/cli/git.mjs
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { access, mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { apiUrl, request, savedToken } from "./connection.mjs";
|
|
6
|
+
|
|
7
|
+
export function runGit(args, { cwd, input, env = {} } = {}) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const child = spawn("git", args, {
|
|
10
|
+
cwd,
|
|
11
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", ...env },
|
|
12
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
13
|
+
});
|
|
14
|
+
let stdout = "",
|
|
15
|
+
stderr = "";
|
|
16
|
+
child.stdout.on("data", (data) => {
|
|
17
|
+
stdout += data;
|
|
18
|
+
});
|
|
19
|
+
child.stderr.on("data", (data) => {
|
|
20
|
+
stderr += data;
|
|
21
|
+
});
|
|
22
|
+
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
|
+
);
|
|
28
|
+
child.stdin.end(input);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
32
|
+
const helper = () =>
|
|
33
|
+
`!${quote(process.execPath)} ${quote(fileURLToPath(new URL("../cli.mjs", import.meta.url)))} credential-helper`;
|
|
34
|
+
|
|
35
|
+
export async function credentialHelper(operation, input, cwd = process.cwd()) {
|
|
36
|
+
if (operation !== "get") return "";
|
|
37
|
+
const values = Object.fromEntries(
|
|
38
|
+
input
|
|
39
|
+
.trim()
|
|
40
|
+
.split("\n")
|
|
41
|
+
.map((line) => {
|
|
42
|
+
const at = line.indexOf("=");
|
|
43
|
+
return [line.slice(0, at), line.slice(at + 1)];
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
const remote = new URL(
|
|
47
|
+
await runGit(["config", "--get", "domino.remote"], { cwd }),
|
|
48
|
+
);
|
|
49
|
+
if (
|
|
50
|
+
values.protocol !== remote.protocol.slice(0, -1) ||
|
|
51
|
+
values.host !== remote.host ||
|
|
52
|
+
values.path !== remote.pathname.slice(1)
|
|
53
|
+
)
|
|
54
|
+
return "";
|
|
55
|
+
const base = apiUrl(
|
|
56
|
+
await runGit(["config", "--get", "domino.apiUrl"], { cwd }),
|
|
57
|
+
);
|
|
58
|
+
const token = process.env.RELAY_MANAGEMENT_TOKEN ?? (await savedToken(base));
|
|
59
|
+
if (!token) throw new Error("Sign in using domino login.");
|
|
60
|
+
// Verify revocation before supplying the credential to Git. Never print it through normal CLI output.
|
|
61
|
+
await request(
|
|
62
|
+
{
|
|
63
|
+
apiUrl: base,
|
|
64
|
+
token,
|
|
65
|
+
organization: await runGit(["config", "--get", "domino.organization"], {
|
|
66
|
+
cwd,
|
|
67
|
+
}),
|
|
68
|
+
project: await runGit(["config", "--get", "domino.project"], { cwd }),
|
|
69
|
+
environment: "test",
|
|
70
|
+
},
|
|
71
|
+
"/access",
|
|
72
|
+
);
|
|
73
|
+
return `username=domino\npassword=${token}\n\n`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function requireNewDirectory(directory) {
|
|
77
|
+
try {
|
|
78
|
+
await access(directory);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error.code === "ENOENT") return;
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`Directory already exists: ${directory}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function checkout(connection, repository, directory) {
|
|
87
|
+
if (repository.status !== "ready")
|
|
88
|
+
throw new Error(
|
|
89
|
+
"Repository provisioning is incomplete. Retry domino create.",
|
|
90
|
+
);
|
|
91
|
+
const remote = new URL(repository.remote);
|
|
92
|
+
if (
|
|
93
|
+
remote.protocol !== "https:" ||
|
|
94
|
+
remote.username ||
|
|
95
|
+
remote.password ||
|
|
96
|
+
remote.search ||
|
|
97
|
+
remote.hash
|
|
98
|
+
)
|
|
99
|
+
throw new Error("Repository remote must use HTTPS without credentials.");
|
|
100
|
+
const root = resolve(directory);
|
|
101
|
+
await requireNewDirectory(root);
|
|
102
|
+
await mkdir(dirname(root), { recursive: true });
|
|
103
|
+
await mkdir(root);
|
|
104
|
+
try {
|
|
105
|
+
await runGit(["init", "--initial-branch", repository.defaultBranch, "."], {
|
|
106
|
+
cwd: root,
|
|
107
|
+
});
|
|
108
|
+
for (const [name, value] of Object.entries({
|
|
109
|
+
apiUrl: connection.apiUrl,
|
|
110
|
+
organization: repository.organization,
|
|
111
|
+
project: repository.project,
|
|
112
|
+
remote: repository.remote,
|
|
113
|
+
}))
|
|
114
|
+
await runGit(["config", "--local", `domino.${name}`, value], {
|
|
115
|
+
cwd: root,
|
|
116
|
+
});
|
|
117
|
+
await runGit(["config", "--local", "credential.useHttpPath", "true"], {
|
|
118
|
+
cwd: root,
|
|
119
|
+
});
|
|
120
|
+
await runGit(
|
|
121
|
+
["config", "--local", `credential.${repository.remote}.helper`, ""],
|
|
122
|
+
{ cwd: root },
|
|
123
|
+
);
|
|
124
|
+
await runGit(
|
|
125
|
+
[
|
|
126
|
+
"config",
|
|
127
|
+
"--local",
|
|
128
|
+
"--add",
|
|
129
|
+
`credential.${repository.remote}.helper`,
|
|
130
|
+
helper(),
|
|
131
|
+
],
|
|
132
|
+
{ cwd: root },
|
|
133
|
+
);
|
|
134
|
+
await runGit(["remote", "add", "origin", repository.remote], { cwd: root });
|
|
135
|
+
await runGit(["fetch", "origin"], { cwd: root });
|
|
136
|
+
await runGit(
|
|
137
|
+
[
|
|
138
|
+
"checkout",
|
|
139
|
+
"-B",
|
|
140
|
+
repository.defaultBranch,
|
|
141
|
+
`origin/${repository.defaultBranch}`,
|
|
142
|
+
],
|
|
143
|
+
{ cwd: root },
|
|
144
|
+
);
|
|
145
|
+
// Local connection scope never dirties the tracked campaign definition.
|
|
146
|
+
await writeFile(
|
|
147
|
+
resolve(root, ".git/domino.json"),
|
|
148
|
+
JSON.stringify({
|
|
149
|
+
apiUrl: connection.apiUrl,
|
|
150
|
+
organization: repository.organization,
|
|
151
|
+
project: repository.project,
|
|
152
|
+
environment: "test",
|
|
153
|
+
}) + "\n",
|
|
154
|
+
);
|
|
155
|
+
return {
|
|
156
|
+
directory: root,
|
|
157
|
+
remote: repository.remote,
|
|
158
|
+
next: "Install dependencies with pnpm install, then run domino dev.",
|
|
159
|
+
};
|
|
160
|
+
} catch (error) {
|
|
161
|
+
// This directory was created exclusively by this operation and has no user edits.
|
|
162
|
+
await rm(root, { recursive: true, force: true });
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
package/cli/project.mjs
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, access } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { buildQuest } from "@domino-sdk/relay/build";
|
|
5
|
+
import { runGit } from "./git.mjs";
|
|
6
|
+
|
|
7
|
+
export const projectSchema = z
|
|
8
|
+
.object({
|
|
9
|
+
$schema: z.string().optional(),
|
|
10
|
+
apiUrl: z.string().url(),
|
|
11
|
+
organization: z.string().min(1),
|
|
12
|
+
project: z.string().min(1),
|
|
13
|
+
environment: z.enum(["test", "live"]).default("test"),
|
|
14
|
+
app: z
|
|
15
|
+
.object({
|
|
16
|
+
kind: z.literal("static"),
|
|
17
|
+
directory: z.string().min(1),
|
|
18
|
+
devScript: z.string().min(1),
|
|
19
|
+
checkScript: z.string().min(1),
|
|
20
|
+
buildScript: z.string().min(1),
|
|
21
|
+
})
|
|
22
|
+
.strict()
|
|
23
|
+
.optional(),
|
|
24
|
+
questTypes: z
|
|
25
|
+
.array(
|
|
26
|
+
z
|
|
27
|
+
.object({
|
|
28
|
+
entry: z.string().min(1),
|
|
29
|
+
exportName: z.string().optional(),
|
|
30
|
+
provider: z
|
|
31
|
+
.enum([
|
|
32
|
+
"workers-ai",
|
|
33
|
+
"fixture-pass",
|
|
34
|
+
"fixture-fail",
|
|
35
|
+
"fixture-unclear",
|
|
36
|
+
"fixture-error",
|
|
37
|
+
])
|
|
38
|
+
.default("workers-ai"),
|
|
39
|
+
})
|
|
40
|
+
.strict(),
|
|
41
|
+
)
|
|
42
|
+
.default([]),
|
|
43
|
+
collections: z
|
|
44
|
+
.array(
|
|
45
|
+
z
|
|
46
|
+
.object({
|
|
47
|
+
id: z.string().regex(/^[\w-]+$/),
|
|
48
|
+
title: z.string().min(1).max(120),
|
|
49
|
+
})
|
|
50
|
+
.strict(),
|
|
51
|
+
)
|
|
52
|
+
.default([]),
|
|
53
|
+
supportedInteractions: z
|
|
54
|
+
.array(z.enum(["photo", "quiz", "claim", "staff", "automatic"]))
|
|
55
|
+
.min(1)
|
|
56
|
+
.optional(),
|
|
57
|
+
quests: z
|
|
58
|
+
.array(
|
|
59
|
+
z
|
|
60
|
+
.object({
|
|
61
|
+
entry: z.string().min(1),
|
|
62
|
+
exportName: z.string().optional(),
|
|
63
|
+
settings: z.record(z.string(), z.unknown()).default({}),
|
|
64
|
+
settingRenames: z.record(z.string(), z.string()).optional(),
|
|
65
|
+
provider: z
|
|
66
|
+
.enum([
|
|
67
|
+
"fixture-pass",
|
|
68
|
+
"fixture-fail",
|
|
69
|
+
"fixture-unclear",
|
|
70
|
+
"fixture-error",
|
|
71
|
+
"workers-ai",
|
|
72
|
+
])
|
|
73
|
+
.default("workers-ai"),
|
|
74
|
+
})
|
|
75
|
+
.strict(),
|
|
76
|
+
)
|
|
77
|
+
.max(50)
|
|
78
|
+
.default([]),
|
|
79
|
+
})
|
|
80
|
+
.strict();
|
|
81
|
+
|
|
82
|
+
export async function readJson(path) {
|
|
83
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function loadProject(path) {
|
|
87
|
+
const config = projectSchema.parse(await readJson(path));
|
|
88
|
+
const dir = dirname(path);
|
|
89
|
+
// Worktrees keep shared connection metadata in the common Git directory.
|
|
90
|
+
const gitDir = await runGit(["rev-parse", "--git-common-dir"], {
|
|
91
|
+
cwd: dir,
|
|
92
|
+
}).catch(() => null);
|
|
93
|
+
if (gitDir) {
|
|
94
|
+
try {
|
|
95
|
+
const local = z
|
|
96
|
+
.object({
|
|
97
|
+
apiUrl: z.string().url(),
|
|
98
|
+
organization: z.string(),
|
|
99
|
+
project: z.string(),
|
|
100
|
+
environment: z.enum(["test", "live"]),
|
|
101
|
+
})
|
|
102
|
+
.strict()
|
|
103
|
+
.parse(await readJson(resolve(dir, gitDir, "domino.json")));
|
|
104
|
+
Object.assign(config, local);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error.code !== "ENOENT") throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { path, config };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function findProject(path) {
|
|
113
|
+
if (path) return loadProject(resolve(path));
|
|
114
|
+
let dir = process.cwd();
|
|
115
|
+
while (true) {
|
|
116
|
+
const path = join(dir, "relay.json");
|
|
117
|
+
try {
|
|
118
|
+
return await loadProject(path);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error.code !== "ENOENT") throw error;
|
|
121
|
+
}
|
|
122
|
+
if (dirname(dir) === dir) return null;
|
|
123
|
+
dir = dirname(dir);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function bundleProject(project) {
|
|
128
|
+
const releases = [];
|
|
129
|
+
for (const quest of project.config.quests) {
|
|
130
|
+
const artifact = await buildQuest(
|
|
131
|
+
resolve(dirname(project.path), quest.entry),
|
|
132
|
+
{ exportName: quest.exportName },
|
|
133
|
+
);
|
|
134
|
+
releases.push({
|
|
135
|
+
...artifact,
|
|
136
|
+
settings: quest.settings,
|
|
137
|
+
...(quest.settingRenames ? { settingRenames: quest.settingRenames } : {}),
|
|
138
|
+
provider: quest.provider,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const catalog = await bundleCatalog(project);
|
|
142
|
+
if (
|
|
143
|
+
!releases.length &&
|
|
144
|
+
!catalog.types.length &&
|
|
145
|
+
!catalog.collections.length &&
|
|
146
|
+
!catalog.supportedInteractions
|
|
147
|
+
)
|
|
148
|
+
throw new Error(
|
|
149
|
+
"No resources configured. Add quests, questTypes, collections, or supportedInteractions to relay.json.",
|
|
150
|
+
);
|
|
151
|
+
return { releases, ...catalog };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function initialize(directory, config) {
|
|
155
|
+
const root = resolve(directory);
|
|
156
|
+
const files = {
|
|
157
|
+
"relay.json": JSON.stringify(projectSchema.parse(config), null, 2) + "\n",
|
|
158
|
+
"RELAY.md": `# Relay project
|
|
159
|
+
|
|
160
|
+
Use Codex or Claude to author your quests with @domino-sdk/relay/authoring. Project scope and quest entries are in relay.json. Relay imposes no framework, source directory, package manager, or application build configuration. Keep your existing project setup. Add @domino-sdk/relay with your package manager, or link the local SDK while it is unpublished. The CLI is supplied by @domino-sdk/relay-cli, separately from the runtime SDK. Commit relay.json and RELAY.md; credentials live in your user config directory.
|
|
161
|
+
|
|
162
|
+
Run domino build or domino deploy --dry-run to bundle the complete manifest locally. Run domino deploy --preview to inspect effective remote settings without publishing, and domino deploy to atomically publish quests, questTypes, collections, and supportedInteractions to test. For live, export domino deploy --dry-run --out deployment.json and review the project bundle in Console. Run domino quests or domino releases to inspect the server. Live publication requires Console.
|
|
163
|
+
|
|
164
|
+
Author quests with @domino-sdk/relay/authoring. Each entry points to any authored module relative to relay.json and default-exports defineQuest, or specifies exportName. No dedicated quests directory is required. Example entry: {"entry":"src/community/welcome.ts","settings":{},"provider":"fixture-pass"}. Fixture providers simulate verification; use workers-ai only on an API with the AI binding configured. Settings are deployment defaults validated by the server at publication. Console custom values persist in Relay and take precedence on every deployment; no source edits are needed to retain them. Deploy output reports defaults, custom values, and effective values. Use Console to reset a setting to its deployment default. Removing or invalidating a custom setting blocks the whole batch. For a rename, add settingRenames: {"oldName":"newName"} to the quest entry to carry custom values forward. A dry run builds locally and does not preview remote custom values. Multiple quests deploy as one batch so prerequisites can refer to other quests in the batch.
|
|
165
|
+
|
|
166
|
+
References: @domino-sdk/relay/authoring provides declarations; @domino-sdk/relay/build bundles without executing author code on the host; domino --help lists commands. Set RELAY_MANAGEMENT_TOKEN for CI. Use --json for machine-readable output. Never put tokens in relay.json or source files.
|
|
167
|
+
`,
|
|
168
|
+
};
|
|
169
|
+
// Preflight every generated path before writing anything. Existing projects are never overwritten.
|
|
170
|
+
for (const name of Object.keys(files)) {
|
|
171
|
+
try {
|
|
172
|
+
await access(join(root, name));
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (error.code === "ENOENT") continue;
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
throw new Error(`Refusing to overwrite ${join(root, name)}`);
|
|
178
|
+
}
|
|
179
|
+
for (const [name, content] of Object.entries(files)) {
|
|
180
|
+
await mkdir(dirname(join(root, name)), { recursive: true });
|
|
181
|
+
await writeFile(join(root, name), content, { flag: "wx" });
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
directory: root,
|
|
185
|
+
files: Object.keys(files),
|
|
186
|
+
next: "Add @domino-sdk/relay to your existing package with your package manager, then configure quest entries in relay.json and run domino build.",
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function bundleCatalog(project) {
|
|
191
|
+
const types = [];
|
|
192
|
+
for (const type of project.config.questTypes) {
|
|
193
|
+
const artifact = await buildQuest(
|
|
194
|
+
resolve(dirname(project.path), type.entry),
|
|
195
|
+
{ exportName: type.exportName },
|
|
196
|
+
);
|
|
197
|
+
types.push({ ...artifact, provider: type.provider });
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
types,
|
|
201
|
+
collections: project.config.collections,
|
|
202
|
+
...(project.config.supportedInteractions
|
|
203
|
+
? { supportedInteractions: project.config.supportedInteractions }
|
|
204
|
+
: {}),
|
|
205
|
+
};
|
|
206
|
+
}
|
package/cli/runtime.mjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { findProject } from "./project.mjs";
|
|
2
|
+
import { apiUrl } from "./connection.mjs";
|
|
3
|
+
|
|
4
|
+
// Share scope resolution and output across commands; build and HTTP modules stay independent.
|
|
5
|
+
export function action(handler) {
|
|
6
|
+
return async (...args) => {
|
|
7
|
+
const command = args.at(-1);
|
|
8
|
+
const options = command.optsWithGlobals();
|
|
9
|
+
const project = await findProject(options.config);
|
|
10
|
+
const connection = {};
|
|
11
|
+
for (const key of ["apiUrl", "organization", "project", "environment"]) {
|
|
12
|
+
const env =
|
|
13
|
+
key === "apiUrl" ? "RELAY_API_URL" : `RELAY_${key.toUpperCase()}`;
|
|
14
|
+
connection[key] =
|
|
15
|
+
options[key] ?? process.env[env] ?? project?.config[key];
|
|
16
|
+
}
|
|
17
|
+
connection.apiUrl = apiUrl(connection.apiUrl ?? "https://relay.domino.run");
|
|
18
|
+
if (
|
|
19
|
+
connection.environment !== undefined &&
|
|
20
|
+
!["test", "live"].includes(connection.environment)
|
|
21
|
+
)
|
|
22
|
+
throw new Error("Environment must be test or live.");
|
|
23
|
+
const result = await handler(
|
|
24
|
+
{ options, project, connection },
|
|
25
|
+
...command.processedArgs,
|
|
26
|
+
);
|
|
27
|
+
console.log(JSON.stringify(result, null, options.json ? undefined : 2));
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function readStdin() {
|
|
32
|
+
if (process.stdin.isTTY)
|
|
33
|
+
throw new Error(
|
|
34
|
+
"Pipe input to stdin; interactive prompts are not supported.",
|
|
35
|
+
);
|
|
36
|
+
let input = "";
|
|
37
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
38
|
+
return input;
|
|
39
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: domino
|
|
3
|
+
description: Set up a Domino project, integrate Domino into an existing app, author quests, or verify and stage a participant experience with the Domino CLI and SDK.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Build with Domino
|
|
7
|
+
|
|
8
|
+
Deliver the requested participant experience and evidence that its main action works. Domino owns verification, completion, points, and reward execution. The app owns its presentation and integration with participant identity.
|
|
9
|
+
|
|
10
|
+
## Establish the project
|
|
11
|
+
|
|
12
|
+
Read the project's agent instructions and owner brief, such as CAMPAIGN.md, when present. Inspect package scripts and relay.json before selecting commands. Use the installed CLI through the project's package manager, such as `pnpm exec domino`; the examples below abbreviate this to `domino`.
|
|
13
|
+
|
|
14
|
+
Run `domino doctor --json` to inspect setup without running application code or contacting the service. Missing setup produces exit 1 with diagnostic JSON on stdout. Invalid configuration uses the CLI's standard error JSON on stderr. Fix the reported prerequisite, then rerun the check.
|
|
15
|
+
|
|
16
|
+
Use `domino login` for browser authorization when remote work is needed. The user approves the displayed code in Console. Credentials stay in the CLI credential store. Run `domino doctor --remote --json` to verify access and inspect the effective organization, project, API, and environment before remote operations. Flags and RELAY_* environment variables override relay.json.
|
|
17
|
+
|
|
18
|
+
Choose the reference that matches the task:
|
|
19
|
+
|
|
20
|
+
- For a new Domino-hosted app, checkout, or hosted preview, read [hosted projects](references/hosted.md).
|
|
21
|
+
- For integration into an existing codebase, read [existing apps](references/existing-app.md).
|
|
22
|
+
- For quest definitions, reusable types, collections, or publication, read [authoring](references/authoring.md).
|
|
23
|
+
|
|
24
|
+
If no CLI is installed, use the project's documented package installation path. The current SDK and CLI are private packages distributed with standalone starter archives. Do not invent a public npm install command. Report the missing distribution if no package source is available.
|
|
25
|
+
|
|
26
|
+
## Verify the result
|
|
27
|
+
|
|
28
|
+
Run the app's checks and exercise the requested participant action through the running app. Verify the resulting progress or completion state. Keep fixture-provider results distinct from real external verification.
|
|
29
|
+
|
|
30
|
+
Return the working preview or local URL, what participant action you verified, and any remaining launch dependency. A successful build, installed skill, or healthy doctor result is not proof of participant behavior. Live publication requires human review in Console; hosted staging is a simulation.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Authoring and publication
|
|
2
|
+
|
|
3
|
+
Read the installed `@domino-sdk/relay/authoring` declarations and the project's existing quest modules. Define the requested behavior with the supported SDK helpers rather than inventing capabilities. Authored entries in relay.json resolve relative to that manifest and can select a named export with `exportName`.
|
|
4
|
+
|
|
5
|
+
Source owns reusable quest definitions, collection slots, and deployment defaults. Console owns operator-created quest instances, their placement, and custom setting values. Keep identifiers stable. Registering a new reusable type version does not upgrade existing instances.
|
|
6
|
+
|
|
7
|
+
Entry `settings` supplies deployment defaults. Remote custom values take precedence on subsequent deployments. A removed or incompatible setting can block the batch. Use `settingRenames` for a deliberate rename, or have the operator reset a custom value in Console. Changing source defaults does not clear overrides.
|
|
8
|
+
|
|
9
|
+
Use `domino build --json` or `domino deploy --dry-run --json` for offline bundling. These do not validate effective remote settings. Use `domino deploy --preview --json` to resolve and validate the remote batch without publishing. Check the selected scope before publication.
|
|
10
|
+
|
|
11
|
+
Use `domino deploy --environment test --json` when test publication is part of the task. It publishes configured quests, types, collections, and supported interactions together. After a lost publication response, use the returned `domino deploy --resume <id>` recovery command. Starting another deployment may create another immutable release.
|
|
12
|
+
|
|
13
|
+
For human live review, export `domino deploy --dry-run --out deployment.json` and present that bundle for Console approval. This publishes community behavior, not the participant application. Existing attempts retain their pinned releases and promised rewards.
|
|
14
|
+
|
|
15
|
+
Fixture providers simulate verification. `workers-ai` requires a configured service binding. A fixture success demonstrates the flow but does not establish that a real external activity was verified.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Existing apps
|
|
2
|
+
|
|
3
|
+
Inspect the app's framework, package manager, workspace boundaries, authentication, and deployment scripts. Identify where the requested participant experience belongs. Keep its repository, UI conventions, login, and hosting.
|
|
4
|
+
|
|
5
|
+
Use the Domino project selected by the user. If no remote project exists, create it in Console. `domino create` provisions a hosted repository and is not the existing-app integration path.
|
|
6
|
+
|
|
7
|
+
From the intended package or project root, run `domino init --project <id>` to create relay.json and RELAY.md. Init verifies access and does not install packages or generate sample quests. If these files already exist, inspect the existing connection and continue from it. Init deliberately refuses to overwrite them.
|
|
8
|
+
|
|
9
|
+
Run `domino agents install` to install this guidance for both Codex and Claude, or select one with `--agent codex` or `--agent claude`. The installer leaves existing AGENTS.md and CLAUDE.md files intact. It can resume missing files, but stops before replacing customized Domino guidance.
|
|
10
|
+
|
|
11
|
+
Add the SDK to the package that owns the authored modules using the app's package manager and an available Domino package distribution. Resolve SDK imports there before adding quest entries to relay.json. Quest files can follow the existing source layout.
|
|
12
|
+
|
|
13
|
+
For participant UI, inspect the installed `@domino-sdk/relay` and `@domino-sdk/relay/browser` exports. Use the supplied quest controllers for submissions, recovery, and changed rules. Keep stable quest IDs and collection slots. Participants explicitly review changed rules before resubmitting.
|
|
14
|
+
|
|
15
|
+
Keep the application's existing login. Map its authenticated user through a supported trusted backend identity integration. Determine the server credential and allowed project scope before implementing that exchange. Browser-provided user IDs are not verified identities. Management tokens belong only on trusted servers or in CLI credentials, never browser bundles.
|
|
16
|
+
|
|
17
|
+
For same-origin participant routing, inspect `@domino-sdk/relay/portal-proxy` and the app's server route conventions. Its proxy allowlist excludes management routes. Verify session transport, sign-in callbacks where relevant, and a participant action through that boundary. If production identity provisioning is unavailable, report that dependency explicitly and keep the demonstration scoped to test.
|
|
18
|
+
|
|
19
|
+
Read [authoring](authoring.md) when adding quests or collection slots. Run the app's normal checks and development server. Use its existing preview deployment process when available. `domino deploy` publishes Domino behavior; it does not deploy the app. `domino stage` requires a supported Domino-hosted static repository, so it is not a general deployment command for existing apps.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Hosted projects
|
|
2
|
+
|
|
3
|
+
If Console already created the project and repository, run `domino checkout <project>`. Use `domino create <name>` only when the requested hosted project does not exist. Both require authentication. Use `domino whoami --json` to inspect accessible projects when the target is unclear.
|
|
4
|
+
|
|
5
|
+
For a failure after remote project creation, inspect the existing project's state before repeating creation. Keep partially created local directories intact while recovering. Checkout accepts `--directory` for a fresh destination.
|
|
6
|
+
|
|
7
|
+
Install dependencies from the lockfile. The starter includes pinned SDK, CLI, and local-runtime archives in `.domino`; upgrade them through a platform package release rather than modifying their contents. Read CAMPAIGN.md for the owner's brief.
|
|
8
|
+
|
|
9
|
+
Run `domino dev` for the isolated local runtime and participant app. Local preview identities and fixture providers simulate participation. Keep demonstration content separate from the owner's live catalog.
|
|
10
|
+
|
|
11
|
+
Use `domino check` and the app's build script to verify source. Exercise the requested participant flow in the running app before staging.
|
|
12
|
+
|
|
13
|
+
When the task includes sharing a preview, commit the intended changes and push using the repository's configured remote. Run `domino stage --json` after the push. The command builds the exact pushed commit and returns its staging URL. Git pushes alone save source; they do not activate a preview or publish live.
|
|
14
|
+
|
|
15
|
+
If stage submission loses its response, repeat the printed commit and request ID. Use `domino builds --json` and `domino logs <build-id> --json` to recover progress after disconnecting. A failed build leaves the previous preview active. Report the URL returned for the successful build rather than constructing one.
|
|
16
|
+
|
|
17
|
+
Cloud staging supports the starter's static app contract. It does not host arbitrary SSR or customer Worker code. Staging uses separate simulated participant data and does not prove live provider readiness. Hosted app live activation remains a separate platform milestone.
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command, Option, CommanderError } from "commander";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { getCACertificates, setDefaultCACertificates } from "node:tls";
|
|
5
|
+
|
|
6
|
+
// Honor OS-trusted authorities, including managed enterprise and development CAs.
|
|
7
|
+
setDefaultCACertificates([
|
|
8
|
+
...getCACertificates("default"),
|
|
9
|
+
...getCACertificates("system"),
|
|
10
|
+
]);
|
|
11
|
+
import { registerAuthCommands } from "./cli/commands/auth.mjs";
|
|
12
|
+
import { registerProjectCommands } from "./cli/commands/project.mjs";
|
|
13
|
+
import { registerInspectCommands } from "./cli/commands/inspect.mjs";
|
|
14
|
+
import { registerApiCommand } from "./cli/commands/api.mjs";
|
|
15
|
+
import { registerStagingCommands } from "./cli/commands/staging.mjs";
|
|
16
|
+
import { registerHostingCommands } from "./cli/commands/hosting.mjs";
|
|
17
|
+
import { registerAgentCommands } from "./cli/commands/agents.mjs";
|
|
18
|
+
import { credentialHelper } from "./cli/git.mjs";
|
|
19
|
+
|
|
20
|
+
const { version } = JSON.parse(
|
|
21
|
+
await readFile(new URL("./package.json", import.meta.url), "utf8"),
|
|
22
|
+
);
|
|
23
|
+
const argv = process.argv.slice(2);
|
|
24
|
+
if (argv[0] === "credential-helper") {
|
|
25
|
+
try {
|
|
26
|
+
let input = "";
|
|
27
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
28
|
+
process.stdout.write(await credentialHelper(argv[1], input));
|
|
29
|
+
process.exit(0);
|
|
30
|
+
} catch {
|
|
31
|
+
process.stderr.write("Domino Git authentication failed. Run domino login.\n");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Preserve the original HTTP wrapper spelling for existing automation.
|
|
36
|
+
if (["GET", "POST", "PUT", "DELETE", "PATCH"].includes(argv[0]))
|
|
37
|
+
argv.unshift("api");
|
|
38
|
+
// Also covers parse errors before Commander has reached the --json option.
|
|
39
|
+
const json = argv
|
|
40
|
+
.slice(0, argv.includes("--") ? argv.indexOf("--") : undefined)
|
|
41
|
+
.includes("--json");
|
|
42
|
+
const program = new Command()
|
|
43
|
+
.name("domino")
|
|
44
|
+
.description("Author and deploy Relay quests from any existing project")
|
|
45
|
+
.version(version)
|
|
46
|
+
.option("--api-url <url>", "API base URL (RELAY_API_URL or relay.json)")
|
|
47
|
+
.option(
|
|
48
|
+
"--organization <id>",
|
|
49
|
+
"Organization scope (RELAY_ORGANIZATION or relay.json)",
|
|
50
|
+
)
|
|
51
|
+
.option("--project <id>", "Project scope (RELAY_PROJECT or relay.json)")
|
|
52
|
+
.addOption(
|
|
53
|
+
new Option(
|
|
54
|
+
"--environment <name>",
|
|
55
|
+
"Environment scope (RELAY_ENVIRONMENT or relay.json)",
|
|
56
|
+
).choices(["test", "live"]),
|
|
57
|
+
)
|
|
58
|
+
.option(
|
|
59
|
+
"--config <file>",
|
|
60
|
+
"Use this manifest instead of the nearest relay.json",
|
|
61
|
+
)
|
|
62
|
+
.option("--json", "Emit machine-readable JSON results and errors")
|
|
63
|
+
.addHelpCommand("help [command]", "Display help for a command")
|
|
64
|
+
.configureHelp({ showGlobalOptions: true })
|
|
65
|
+
.configureOutput({
|
|
66
|
+
writeOut: (text) => {
|
|
67
|
+
if (json)
|
|
68
|
+
console.log(
|
|
69
|
+
JSON.stringify(
|
|
70
|
+
text.trim() === version ? { version } : { help: text },
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
else process.stdout.write(text);
|
|
74
|
+
},
|
|
75
|
+
// The catch below emits one consistent error, including Commander parse errors.
|
|
76
|
+
writeErr: () => {},
|
|
77
|
+
})
|
|
78
|
+
.exitOverride()
|
|
79
|
+
.addHelpText(
|
|
80
|
+
"after",
|
|
81
|
+
`
|
|
82
|
+
Scope: flags > RELAY_* environment variables > nearest relay.json.
|
|
83
|
+
API default: https://relay.domino.run. Init defaults to test.
|
|
84
|
+
Credentials: RELAY_MANAGEMENT_TOKEN > saved token for the API origin.
|
|
85
|
+
Run domino login for browser sign-in. Live publication requires Console.
|
|
86
|
+
Commands never prompt. Results go to stdout; errors go to stderr. Exit 1 on failure.`,
|
|
87
|
+
)
|
|
88
|
+
.action(() => program.outputHelp());
|
|
89
|
+
|
|
90
|
+
registerAuthCommands(program);
|
|
91
|
+
registerProjectCommands(program);
|
|
92
|
+
registerInspectCommands(program);
|
|
93
|
+
registerApiCommand(program);
|
|
94
|
+
registerHostingCommands(program);
|
|
95
|
+
registerStagingCommands(program);
|
|
96
|
+
registerAgentCommands(program);
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
await program.parseAsync(argv, { from: "user" });
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (!(error instanceof CommanderError && error.exitCode === 0)) {
|
|
102
|
+
console.error(
|
|
103
|
+
json
|
|
104
|
+
? JSON.stringify({
|
|
105
|
+
error: error.message,
|
|
106
|
+
...(error.status ? { status: error.status } : {}),
|
|
107
|
+
})
|
|
108
|
+
: `Error: ${error.message}`,
|
|
109
|
+
);
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
}
|
|
112
|
+
}
|