@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.
- package/cli/capabilities.json +45 -20
- package/cli/commands/discovery.mjs +23 -4
- package/cli/commands/hosting.mjs +7 -8
- package/cli/commands/project.mjs +7 -1
- package/cli/commands/staging.mjs +2 -2
- package/cli/connected-dev.mjs +4 -2
- package/cli/discovery.mjs +62 -0
- package/cli/doctor.mjs +24 -3
- package/cli/git.mjs +38 -7
- package/cli/package-manager.mjs +31 -0
- package/cli/project.mjs +138 -37
- package/cli/runtime.mjs +8 -1
- package/cli/skills/domino/SKILL.md +36 -6
- package/cli/skills/domino/references/authoring.md +109 -2
- package/cli/skills/domino/references/existing-app.md +2 -0
- package/cli/skills/domino/references/hosted.md +14 -2
- package/cli/skills/domino/references/participant-api.md +55 -2
- package/cli/skills/domino/references/referrals.md +57 -8
- package/cli/skills/domino/references/troubleshooting.md +18 -0
- package/cli.mjs +4 -1
- package/dist/index.d.ts +1070 -4
- package/dist/index.js +191 -7
- package/package.json +2 -2
package/cli/capabilities.json
CHANGED
|
@@ -12,7 +12,25 @@
|
|
|
12
12
|
"auth.emailVerify",
|
|
13
13
|
"auth.session"
|
|
14
14
|
],
|
|
15
|
-
"evidence": [
|
|
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)].
|
|
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": "
|
|
59
|
-
"description": "
|
|
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": "
|
|
70
|
-
"description": "
|
|
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": "
|
|
81
|
-
"description": "
|
|
82
|
-
"entrypoints": [
|
|
83
|
-
|
|
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 = [
|
|
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(
|
|
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
|
-
|
|
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
|
package/cli/commands/hosting.mjs
CHANGED
|
@@ -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 {
|
|
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
|
|
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:
|
|
39
|
-
|
|
40
|
-
|
|
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
|
);
|
package/cli/commands/project.mjs
CHANGED
|
@@ -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;
|
package/cli/commands/staging.mjs
CHANGED
|
@@ -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
|
|
23
|
+
if (!project.config.app)
|
|
24
24
|
throw new Error(
|
|
25
|
-
"
|
|
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");
|
package/cli/connected-dev.mjs
CHANGED
|
@@ -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
|
|
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}\
|
|
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 = [
|
|
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:
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|