@hue-run/sdk 0.3.0 → 0.3.1

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.
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ import { realpath } from "node:fs/promises";
3
+ import { parseArgs } from "node:util";
4
+ import { FileSetupCheckpointAdapter, setupRunId } from "./checkpoint.js";
5
+ import { detectSetupProject } from "./detect.js";
6
+ import { renderHumanEvent, renderJsonlEvent, renderPlainEvent, selectSetupOutputMode, } from "./render.js";
7
+ import { runSetup } from "./runner.js";
8
+ import { SETUP_EVENT_CONTRACT_VERSION } from "./types.js";
9
+ const commands = new Set(["setup", "resume", "status", "claim"]);
10
+ function writeEvent(event, mode, width) {
11
+ const line = mode === "jsonl"
12
+ ? renderJsonlEvent(event)
13
+ : mode === "plain"
14
+ ? renderPlainEvent(event, width)
15
+ : renderHumanEvent(event, width, true);
16
+ if (line)
17
+ process.stdout.write(`${line}\n`);
18
+ }
19
+ async function main() {
20
+ const agentRequested = process.argv.slice(2).includes("--agent");
21
+ let parsed;
22
+ try {
23
+ parsed = parseArgs({
24
+ args: process.argv.slice(2),
25
+ allowPositionals: true,
26
+ strict: true,
27
+ options: {
28
+ agent: { type: "boolean", default: false },
29
+ format: { type: "string" },
30
+ project: { type: "string" },
31
+ help: { type: "boolean", short: "h", default: false },
32
+ },
33
+ });
34
+ }
35
+ catch {
36
+ if (!agentRequested) {
37
+ process.stderr.write("Usage: hue <setup|resume|status|claim> [--agent|--format plain|jsonl] [--project PATH]\n");
38
+ return 2;
39
+ }
40
+ const event = {
41
+ contractVersion: SETUP_EVENT_CONTRACT_VERSION,
42
+ event: "run.failed",
43
+ runId: "setup_invalid_command",
44
+ sequence: 1,
45
+ timestamp: new Date().toISOString(),
46
+ code: "invalid_arguments",
47
+ message: "Invalid command arguments.",
48
+ resumable: false,
49
+ };
50
+ process.stdout.write(`${renderJsonlEvent(event)}\n`);
51
+ return 2;
52
+ }
53
+ if (parsed.values.help) {
54
+ if (parsed.values.agent) {
55
+ const event = {
56
+ contractVersion: SETUP_EVENT_CONTRACT_VERSION,
57
+ event: "run.failed",
58
+ runId: "setup_help",
59
+ sequence: 1,
60
+ timestamp: new Date().toISOString(),
61
+ code: "help_requested",
62
+ message: "Use hue --help without --agent to read interactive help.",
63
+ resumable: false,
64
+ };
65
+ process.stdout.write(`${renderJsonlEvent(event)}\n`);
66
+ return 2;
67
+ }
68
+ process.stdout.write("Usage: hue <setup|resume|status|claim> [--agent|--format plain|jsonl] [--project PATH]\n");
69
+ return 0;
70
+ }
71
+ const command = parsed.positionals[0];
72
+ const format = parsed.values.format;
73
+ const validFormat = format === undefined || format === "human" || format === "plain" || format === "jsonl";
74
+ if (!command ||
75
+ !commands.has(command) ||
76
+ parsed.positionals.length !== 1 ||
77
+ !validFormat ||
78
+ (parsed.values.agent && format !== undefined && format !== "jsonl")) {
79
+ if (parsed.values.agent) {
80
+ const event = {
81
+ contractVersion: SETUP_EVENT_CONTRACT_VERSION,
82
+ event: "run.failed",
83
+ runId: "setup_invalid_command",
84
+ sequence: 1,
85
+ timestamp: new Date().toISOString(),
86
+ code: "invalid_arguments",
87
+ message: "Invalid command arguments.",
88
+ resumable: false,
89
+ };
90
+ process.stdout.write(`${renderJsonlEvent(event)}\n`);
91
+ }
92
+ else
93
+ process.stderr.write("Usage: hue <setup|resume|status|claim> [--agent|--format plain|jsonl] [--project PATH]\n");
94
+ return 2;
95
+ }
96
+ const mode = selectSetupOutputMode({
97
+ agent: parsed.values.agent,
98
+ explicit: format,
99
+ isTTY: process.stdout.isTTY,
100
+ env: process.env,
101
+ });
102
+ const width = Math.max(24, process.stdout.columns ?? 80);
103
+ const controller = new AbortController();
104
+ let terminalEmitted = false;
105
+ const interrupt = () => controller.abort();
106
+ process.once("SIGINT", interrupt);
107
+ process.once("SIGTERM", interrupt);
108
+ try {
109
+ const root = await realpath(parsed.values.project ?? process.cwd());
110
+ await runSetup({
111
+ command: command,
112
+ mode,
113
+ runId: setupRunId(root),
114
+ projectRoot: root,
115
+ project: { detect: detectSetupProject },
116
+ checkpoints: new FileSetupCheckpointAdapter(),
117
+ signal: controller.signal,
118
+ emit: (event) => {
119
+ if (event.event === "run.completed" || event.event === "run.failed")
120
+ terminalEmitted = true;
121
+ writeEvent(event, mode, width);
122
+ },
123
+ });
124
+ return 0;
125
+ }
126
+ catch {
127
+ if (mode === "jsonl" && !terminalEmitted) {
128
+ const event = {
129
+ contractVersion: SETUP_EVENT_CONTRACT_VERSION,
130
+ event: "run.failed",
131
+ runId: "setup_preflight_failed",
132
+ sequence: 1,
133
+ timestamp: new Date().toISOString(),
134
+ code: "setup_failed",
135
+ message: "Setup session could not start. Check the project path and local state directory.",
136
+ resumable: false,
137
+ };
138
+ process.stdout.write(`${renderJsonlEvent(event)}\n`);
139
+ }
140
+ else if (mode !== "jsonl" && !terminalEmitted) {
141
+ process.stderr.write("Setup session could not start. Check the project path and local state directory.\n");
142
+ }
143
+ return controller.signal.aborted ? 130 : 1;
144
+ }
145
+ finally {
146
+ process.removeListener("SIGINT", interrupt);
147
+ process.removeListener("SIGTERM", interrupt);
148
+ }
149
+ }
150
+ process.exitCode = await main();
@@ -0,0 +1,3 @@
1
+ import type { SetupProjectDetection } from "./types.js";
2
+ /** Reads bounded manifest and lockfile metadata without importing or executing project code. */
3
+ export declare function detectSetupProject(projectRoot: string): Promise<SetupProjectDetection>;
@@ -0,0 +1,146 @@
1
+ import { createHash } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { open, realpath } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ const MAX_MANIFEST_BYTES = 1024 * 1024;
6
+ async function readManifest(root, name) {
7
+ let handle;
8
+ try {
9
+ handle = await open(join(root, name), constants.O_RDONLY | constants.O_NOFOLLOW);
10
+ }
11
+ catch (error) {
12
+ if (error.code === "ENOENT")
13
+ return undefined;
14
+ throw error;
15
+ }
16
+ try {
17
+ const info = await handle.stat();
18
+ if (!info.isFile())
19
+ return undefined;
20
+ if (info.size > MAX_MANIFEST_BYTES)
21
+ return "";
22
+ return await handle.readFile("utf8");
23
+ }
24
+ finally {
25
+ await handle.close();
26
+ }
27
+ }
28
+ function hasAny(source, names) {
29
+ return names.some((name) => {
30
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
31
+ return new RegExp(`(?:^|[^A-Za-z0-9_.-])${escaped}(?:$|[^A-Za-z0-9_.-])`, "iu").test(source);
32
+ });
33
+ }
34
+ /** Reads bounded manifest and lockfile metadata without importing or executing project code. */
35
+ export async function detectSetupProject(projectRoot) {
36
+ const root = await realpath(projectRoot);
37
+ const names = [
38
+ "package.json",
39
+ "tsconfig.json",
40
+ "bun.lock",
41
+ "bun.lockb",
42
+ "package-lock.json",
43
+ "npm-shrinkwrap.json",
44
+ "pnpm-lock.yaml",
45
+ "yarn.lock",
46
+ "pyproject.toml",
47
+ "uv.lock",
48
+ "poetry.lock",
49
+ "requirements.txt",
50
+ ];
51
+ const contents = new Map();
52
+ await Promise.all(names.map(async (name) => {
53
+ const content = await readManifest(root, name);
54
+ if (content !== undefined)
55
+ contents.set(name, content);
56
+ }));
57
+ const node = contents.get("package.json") ?? "";
58
+ let nodeDependencies = new Set();
59
+ let declaredPackageManager;
60
+ if (node) {
61
+ try {
62
+ const manifest = JSON.parse(node);
63
+ if (typeof manifest.packageManager === "string")
64
+ declaredPackageManager = manifest.packageManager.split("@", 1)[0];
65
+ for (const section of [
66
+ "dependencies",
67
+ "devDependencies",
68
+ "peerDependencies",
69
+ "optionalDependencies",
70
+ ]) {
71
+ const dependencies = manifest[section];
72
+ if (dependencies && typeof dependencies === "object" && !Array.isArray(dependencies))
73
+ nodeDependencies = new Set([...nodeDependencies, ...Object.keys(dependencies)]);
74
+ }
75
+ }
76
+ catch {
77
+ // Malformed package metadata is reported only through absent detections; setup never executes it.
78
+ }
79
+ }
80
+ const python = [
81
+ contents.get("pyproject.toml"),
82
+ contents.get("requirements.txt"),
83
+ contents.get("uv.lock"),
84
+ contents.get("poetry.lock"),
85
+ ]
86
+ .filter((value) => value !== undefined)
87
+ .join("\n");
88
+ const languages = [];
89
+ if (contents.has("tsconfig.json") || nodeDependencies.has("typescript"))
90
+ languages.push("typescript");
91
+ if (python || contents.has("pyproject.toml"))
92
+ languages.push("python");
93
+ const packageManagers = [];
94
+ if (contents.has("bun.lock") || contents.has("bun.lockb") || declaredPackageManager === "bun")
95
+ packageManagers.push("bun");
96
+ if (contents.has("package-lock.json") ||
97
+ contents.has("npm-shrinkwrap.json") ||
98
+ declaredPackageManager === "npm")
99
+ packageManagers.push("npm");
100
+ if (contents.has("pnpm-lock.yaml") || declaredPackageManager === "pnpm")
101
+ packageManagers.push("pnpm");
102
+ if (contents.has("yarn.lock") || declaredPackageManager === "yarn")
103
+ packageManagers.push("yarn");
104
+ if (contents.has("uv.lock") || /\[tool\.uv(?:\.|\])/u.test(python))
105
+ packageManagers.push("uv");
106
+ if (contents.has("poetry.lock") || /\[tool\.poetry\]/u.test(python))
107
+ packageManagers.push("poetry");
108
+ if (contents.has("requirements.txt") && !packageManagers.includes("uv"))
109
+ packageManagers.push("pip");
110
+ const frameworks = [];
111
+ if (nodeDependencies.has("next"))
112
+ frameworks.push("nextjs");
113
+ if (nodeDependencies.has("@nestjs/core"))
114
+ frameworks.push("nestjs");
115
+ if (nodeDependencies.has("express"))
116
+ frameworks.push("express");
117
+ if (hasAny(python, ["fastapi"]))
118
+ frameworks.push("fastapi");
119
+ if (hasAny(python, ["django"]))
120
+ frameworks.push("django");
121
+ if (hasAny(python, ["flask"]))
122
+ frameworks.push("flask");
123
+ if (nodeDependencies.has("ai") ||
124
+ [...nodeDependencies].some((name) => name.startsWith("@ai-sdk/")))
125
+ frameworks.push("vercel-ai-sdk");
126
+ const hueTs = nodeDependencies.has("@hue-run/sdk") || nodeDependencies.has("hue-run");
127
+ const huePy = hasAny(python, ["hue-run", "hue_sdk"]);
128
+ const otelTs = nodeDependencies.has("@vercel/otel") ||
129
+ [...nodeDependencies].some((name) => name.startsWith("@opentelemetry/"));
130
+ const otelPy = /(?:^|[^A-Za-z0-9_.-])opentelemetry[-_]/iu.test(python);
131
+ const presence = (ts, py) => ts && py ? "multiple" : ts ? "typescript" : py ? "python" : "absent";
132
+ const facts = {
133
+ languages,
134
+ packageManagers,
135
+ frameworks,
136
+ hue: presence(hueTs, huePy),
137
+ openTelemetry: presence(otelTs, otelPy),
138
+ };
139
+ return {
140
+ root,
141
+ fingerprint: createHash("sha256")
142
+ .update(`${root}\0${JSON.stringify(facts)}`)
143
+ .digest("hex"),
144
+ ...facts,
145
+ };
146
+ }
@@ -0,0 +1,109 @@
1
+ import type { SetupPlan, SetupProjectDetection } from "./types.js";
2
+ /** @inline */
3
+ interface SetupStateBase {
4
+ /** Checkpoint format version. */
5
+ format: 1;
6
+ /** Current state-machine phase. */
7
+ phase: "created" | "detecting" | "local-ready";
8
+ /** Stable installer-session identifier, unrelated to Hue Runs. */
9
+ runId: string;
10
+ /** Canonical project root. */
11
+ projectRoot: string;
12
+ }
13
+ /** @inline */
14
+ interface CreatedSetupState extends SetupStateBase {
15
+ /** Initial phase before any adapter work. */
16
+ phase: "created";
17
+ }
18
+ /** @inline */
19
+ interface DetectingSetupState extends SetupStateBase {
20
+ /** Safely resumable static-detection phase. */
21
+ phase: "detecting";
22
+ }
23
+ /** @inline */
24
+ interface LocalReadySetupState extends SetupStateBase {
25
+ /** Local inspection is complete; a later integration may configure telemetry. */
26
+ phase: "local-ready";
27
+ /** Saved bounded project facts. */
28
+ project: SetupProjectDetection;
29
+ /** Saved deterministic plan. */
30
+ plan: SetupPlan;
31
+ }
32
+ /** Persisted phase of the pure setup state machine. */
33
+ export type SetupMachineState = CreatedSetupState | DetectingSetupState | LocalReadySetupState;
34
+ /** @inline */
35
+ interface StartSetupInput {
36
+ /** Requests the next safe local effect. */
37
+ type: "start";
38
+ }
39
+ /** @inline */
40
+ interface ProjectDetectedSetupInput {
41
+ /** Supplies a completed project detection. */
42
+ type: "project.detected";
43
+ /** Static detection supplied by the project adapter. */
44
+ project: SetupProjectDetection;
45
+ }
46
+ /** Input returned by an injected setup adapter. */
47
+ export type SetupMachineInput = StartSetupInput | ProjectDetectedSetupInput;
48
+ /** Side effect requested by the pure state machine. */
49
+ export interface SetupEffect {
50
+ /** Adapter operation requested by the machine. */
51
+ type: "detect-project";
52
+ /** Canonical root to inspect. */
53
+ root: string;
54
+ }
55
+ /** @inline */
56
+ interface StartStepTransitionEvent {
57
+ /** Transition-event discriminator. */
58
+ event: "step.started";
59
+ /** Static detection step. */
60
+ step: "detect-project";
61
+ }
62
+ /** @inline */
63
+ interface ProjectDetectedTransitionEvent {
64
+ /** Transition-event discriminator. */
65
+ event: "project.detected";
66
+ /** Completed detection. */
67
+ project: SetupProjectDetection;
68
+ }
69
+ /** @inline */
70
+ interface CompleteStepTransitionEvent {
71
+ /** Transition-event discriminator. */
72
+ event: "step.completed";
73
+ /** Static detection step. */
74
+ step: "detect-project";
75
+ /** Detection never changes project files. */
76
+ outcome: "unchanged";
77
+ }
78
+ /** @inline */
79
+ interface PlanReadyTransitionEvent {
80
+ /** Transition-event discriminator. */
81
+ event: "plan.ready";
82
+ /** Deterministic plan. */
83
+ plan: SetupPlan;
84
+ }
85
+ /** @inline */
86
+ interface ConfigureTelemetryRequiredTransitionEvent {
87
+ /** Transition-event discriminator. */
88
+ event: "action.required";
89
+ /** Project action needed next. */
90
+ action: "configure";
91
+ /** Secret-free explanation. */
92
+ message: string;
93
+ }
94
+ /** @inline */
95
+ type SetupTransitionEvent = StartStepTransitionEvent | ProjectDetectedTransitionEvent | CompleteStepTransitionEvent | PlanReadyTransitionEvent | ConfigureTelemetryRequiredTransitionEvent;
96
+ /** Pure transition result. Events are templates completed by the runner. */
97
+ export interface SetupTransition {
98
+ /** State to checkpoint before executing another effect. */
99
+ state: SetupMachineState;
100
+ /** Event templates for the runner to sequence and timestamp. */
101
+ events: SetupTransitionEvent[];
102
+ /** Optional side effect to execute after saving the state. */
103
+ effect?: SetupEffect;
104
+ }
105
+ /** Creates deterministic initial setup state without reading the filesystem. */
106
+ export declare function createInitialSetupState(runId: string, projectRoot: string): SetupMachineState;
107
+ /** Advances the setup state without I/O, clocks, randomness, or environment access. */
108
+ export declare function transitionSetup(state: SetupMachineState, input: SetupMachineInput): SetupTransition;
109
+ export {};
@@ -0,0 +1,43 @@
1
+ /** Creates deterministic initial setup state without reading the filesystem. */
2
+ export function createInitialSetupState(runId, projectRoot) {
3
+ return { format: 1, phase: "created", runId, projectRoot };
4
+ }
5
+ /** Advances the setup state without I/O, clocks, randomness, or environment access. */
6
+ export function transitionSetup(state, input) {
7
+ if ((state.phase === "created" || state.phase === "detecting") && input.type === "start") {
8
+ const next = { ...state, phase: "detecting" };
9
+ return {
10
+ state: next,
11
+ events: [{ event: "step.started", step: "detect-project" }],
12
+ effect: { type: "detect-project", root: state.projectRoot },
13
+ };
14
+ }
15
+ if (state.phase === "detecting" && input.type === "project.detected") {
16
+ const plan = {
17
+ steps: ["detect-project", "configure-telemetry", "verify-receipt", "claim-project"],
18
+ mutatesProject: false,
19
+ backendRequired: true,
20
+ };
21
+ return {
22
+ state: {
23
+ format: 1,
24
+ phase: "local-ready",
25
+ runId: state.runId,
26
+ projectRoot: state.projectRoot,
27
+ project: input.project,
28
+ plan,
29
+ },
30
+ events: [
31
+ { event: "project.detected", project: input.project },
32
+ { event: "step.completed", step: "detect-project", outcome: "unchanged" },
33
+ { event: "plan.ready", plan },
34
+ {
35
+ event: "action.required",
36
+ action: "configure",
37
+ message: "Local inspection is complete. Telemetry configuration is not available in this build; no project files were changed.",
38
+ },
39
+ ],
40
+ };
41
+ }
42
+ throw new Error(`Invalid setup transition from ${state.phase} using ${input.type}`);
43
+ }
@@ -0,0 +1,16 @@
1
+ import type { SetupEvent } from "./types.js";
2
+ /** Supported setup transcript formats. */
3
+ export type SetupOutputMode = "human" | "plain" | "jsonl";
4
+ /** Renders one newline-free JSON object for JSONL output. */
5
+ export declare function renderJsonlEvent(event: SetupEvent): string;
6
+ /** Renders one ANSI-free append-only transcript entry. */
7
+ export declare function renderPlainEvent(event: SetupEvent, width?: number): string;
8
+ /** Renders one lightweight append-only terminal entry; it never moves the cursor or clears the screen. */
9
+ export declare function renderHumanEvent(event: SetupEvent, width?: number, color?: boolean): string;
10
+ /** Chooses a safe default: ANSI only on an ordinary interactive terminal. */
11
+ export declare function selectSetupOutputMode(input: {
12
+ agent?: boolean;
13
+ explicit?: SetupOutputMode;
14
+ isTTY?: boolean;
15
+ env?: NodeJS.ProcessEnv;
16
+ }): SetupOutputMode;
@@ -0,0 +1,111 @@
1
+ const ansi = {
2
+ cyan: "\u001b[36m",
3
+ green: "\u001b[32m",
4
+ yellow: "\u001b[33m",
5
+ red: "\u001b[31m",
6
+ dim: "\u001b[2m",
7
+ reset: "\u001b[0m",
8
+ };
9
+ function wrap(text, width, prefix) {
10
+ const available = Math.max(20, width - prefix.length);
11
+ const words = text.split(/\s+/u);
12
+ const lines = [];
13
+ let line = "";
14
+ for (const word of words) {
15
+ if (line && line.length + word.length + 1 > available) {
16
+ lines.push(line);
17
+ line = word;
18
+ }
19
+ else
20
+ line = line ? `${line} ${word}` : word;
21
+ }
22
+ if (line)
23
+ lines.push(line);
24
+ return lines.map((value) => `${prefix}${value}`).join("\n");
25
+ }
26
+ function summary(event) {
27
+ switch (event.event) {
28
+ case "run.started":
29
+ return `Hue setup session: ${event.command}${event.resumed ? " (resuming)" : ""}`;
30
+ case "project.detected": {
31
+ const languages = event.project.languages.length
32
+ ? event.project.languages.join(" + ")
33
+ : "unknown language";
34
+ const managers = event.project.packageManagers.length
35
+ ? ` via ${event.project.packageManagers.join(" + ")}`
36
+ : "";
37
+ return `Detected ${languages}${managers}; Hue ${event.project.hue}, OpenTelemetry ${event.project.openTelemetry}.`;
38
+ }
39
+ case "plan.ready":
40
+ return `Plan ready: ${event.plan.steps.length} bounded steps; project mutation is ${event.plan.mutatesProject ? "enabled" : "disabled"}.`;
41
+ case "step.started":
42
+ return `Starting ${event.step}.`;
43
+ case "step.completed":
44
+ return `Completed ${event.step} (${event.outcome}).`;
45
+ case "file.changed":
46
+ return `${event.change === "created" ? "Created" : "Updated"} ${event.path}.`;
47
+ case "diagnostic":
48
+ return `${event.code}: ${event.message}`;
49
+ case "action.required":
50
+ return `${event.message}${event.command ? ` Next: ${event.command}.` : ""}`;
51
+ case "trial.created":
52
+ return `Anonymous trial ${event.trialId} created; expires ${event.expiresAt}.`;
53
+ case "receipt.verified":
54
+ return `Instrumentation receipt ${event.receiptId} verified for trace ${event.traceId}.`;
55
+ case "claim.required":
56
+ return `Claim ${event.claimId} is ready: ${event.url}`;
57
+ case "claim.completed":
58
+ return `Claim ${event.claimId} completed.`;
59
+ case "run.completed":
60
+ return `Setup session ${event.outcome.replace("_", " ")}; checkpoint ${event.checkpointed ? "saved" : "not created"}.`;
61
+ case "run.failed":
62
+ return `${event.code}: ${event.message}`;
63
+ }
64
+ }
65
+ /** Renders one newline-free JSON object for JSONL output. */
66
+ export function renderJsonlEvent(event) {
67
+ return JSON.stringify(event);
68
+ }
69
+ /** Renders one ANSI-free append-only transcript entry. */
70
+ export function renderPlainEvent(event, width = 80) {
71
+ const text = summary(event);
72
+ if (!text)
73
+ return "";
74
+ const marker = event.event === "run.failed"
75
+ ? "error"
76
+ : event.event === "action.required"
77
+ ? "action"
78
+ : event.event;
79
+ return wrap(text, width, `[${marker}] `);
80
+ }
81
+ /** Renders one lightweight append-only terminal entry; it never moves the cursor or clears the screen. */
82
+ export function renderHumanEvent(event, width = 80, color = true) {
83
+ const text = summary(event);
84
+ if (!text)
85
+ return "";
86
+ const [symbol, tone] = event.event === "run.failed"
87
+ ? ["×", ansi.red]
88
+ : event.event === "action.required"
89
+ ? ["◆", ansi.yellow]
90
+ : event.event === "run.completed"
91
+ ? ["└", ansi.green]
92
+ : event.event === "step.started"
93
+ ? ["◇", ansi.cyan]
94
+ : event.event === "diagnostic"
95
+ ? ["│", ansi.dim]
96
+ : ["│", ansi.green];
97
+ const prefix = `${symbol} `;
98
+ const rendered = wrap(text, width, prefix);
99
+ return color ? `${tone}${rendered}${ansi.reset}` : rendered;
100
+ }
101
+ /** Chooses a safe default: ANSI only on an ordinary interactive terminal. */
102
+ export function selectSetupOutputMode(input) {
103
+ if (input.agent)
104
+ return "jsonl";
105
+ if (input.explicit === "jsonl" || input.explicit === "plain")
106
+ return input.explicit;
107
+ const env = input.env ?? process.env;
108
+ if (!input.isTTY || env.NO_COLOR !== undefined || env.TERM === "dumb" || env.CI !== undefined)
109
+ return "plain";
110
+ return "human";
111
+ }
@@ -0,0 +1,101 @@
1
+ import type { SetupMachineState } from "./machine.js";
2
+ import { type SetupEvent, type SetupProjectDetection } from "./types.js";
3
+ /** Non-secret setup-trial identity returned by a future account-attachment adapter. */
4
+ export interface SetupBackendTrial {
5
+ /** Non-secret trial identifier. */
6
+ trialId: string;
7
+ /** ISO-8601 trial expiration. */
8
+ expiresAt: string;
9
+ }
10
+ /** Instrumentation-only receipt evidence returned by a future backend adapter. */
11
+ export interface SetupBackendReceipt {
12
+ /** Non-secret receipt identifier. */
13
+ receiptId: string;
14
+ /** Verified lowercase trace identifier; it does not establish content or Scenario suitability. */
15
+ traceId: string;
16
+ }
17
+ /** @inline */
18
+ interface SetupBackendClaimRequired {
19
+ /** Claim is ready for a person. */
20
+ status: "required";
21
+ /** Non-secret claim identifier. */
22
+ claimId: string;
23
+ /** User-facing URL, which must never be checkpointed. */
24
+ url: string;
25
+ }
26
+ /** @inline */
27
+ interface SetupBackendClaimCompleted {
28
+ /** Claim has been confirmed by the backend. */
29
+ status: "completed";
30
+ /** Non-secret claim identifier. */
31
+ claimId: string;
32
+ }
33
+ /** Claim state returned by a future backend adapter. Claim URLs are never checkpointed. */
34
+ export type SetupBackendClaim = SetupBackendClaimRequired | SetupBackendClaimCompleted;
35
+ /** Installer-only network boundary for account attachment. It never creates a Scenario, worker, evaluation, or Hue Run. */
36
+ export interface SetupBackendAdapter {
37
+ /** Creates or idempotently recovers an anonymous trial hard-pinned to `trial_metadata_v1`. */
38
+ createTrial(input: {
39
+ runId: string;
40
+ projectFingerprint: string;
41
+ idempotencyKey: string;
42
+ }, signal?: AbortSignal): Promise<SetupBackendTrial>;
43
+ /**
44
+ * Returns instrumentation-only receipt evidence, or `undefined` while pending.
45
+ * A receipt never authorizes content capture or Scenario publication.
46
+ */
47
+ verifyReceipt(input: {
48
+ trialId: string;
49
+ idempotencyKey: string;
50
+ }, signal?: AbortSignal): Promise<SetupBackendReceipt | undefined>;
51
+ /** Reads account-claim state after receipt verification without opening the claim URL. */
52
+ getClaim(input: {
53
+ trialId: string;
54
+ idempotencyKey: string;
55
+ }, signal?: AbortSignal): Promise<SetupBackendClaim>;
56
+ }
57
+ /** Static project-inspection boundary. */
58
+ export interface SetupProjectAdapter {
59
+ /** Inspects bounded metadata without executing project code. */
60
+ detect(root: string, signal?: AbortSignal): Promise<SetupProjectDetection>;
61
+ }
62
+ /** Secret-free durable state boundary. */
63
+ export interface SetupCheckpointAdapter {
64
+ /** Loads and validates state for this exact project identity. */
65
+ load(runId: string, projectRoot: string): Promise<SetupMachineState | undefined>;
66
+ /** Durably saves secret-free state before the next effect. */
67
+ save(state: SetupMachineState): Promise<void>;
68
+ }
69
+ /** Options for one deterministic setup invocation. */
70
+ export interface SetupRunOptions {
71
+ /** CLI operation being orchestrated. */
72
+ command: "setup" | "resume" | "status" | "claim";
73
+ /** Renderer mode recorded in `run.started`. */
74
+ mode: "human" | "plain" | "jsonl";
75
+ /** Stable installer-session identifier, unrelated to Hue Runs. */
76
+ runId: string;
77
+ /** Canonical project root. */
78
+ projectRoot: string;
79
+ /** Injected static-inspection adapter. */
80
+ project: SetupProjectAdapter;
81
+ /** Injected secret-free checkpoint adapter. */
82
+ checkpoints: SetupCheckpointAdapter;
83
+ /** Reserved injection point for the follow-up integration; intentionally unused by this slice. */
84
+ backend?: SetupBackendAdapter;
85
+ /** Receives each ordered event exactly once. */
86
+ emit(event: SetupEvent): void | Promise<void>;
87
+ /** Injectable clock for deterministic tests. */
88
+ now?: () => Date;
89
+ /** Optional cancellation signal. */
90
+ signal?: AbortSignal;
91
+ }
92
+ /** Result of one setup invocation. */
93
+ export interface SetupRunResult {
94
+ /** Non-fabricated local outcome. */
95
+ outcome: "ready" | "action_required" | "unchanged";
96
+ /** Last validated state, when one exists. */
97
+ state?: SetupMachineState;
98
+ }
99
+ /** Runs one installer setup-session command and emits one terminal event; it never launches a Hue Run. */
100
+ export declare function runSetup(options: SetupRunOptions): Promise<SetupRunResult>;
101
+ export {};