jules-orchestrator-kit 0.52.8 → 0.53.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.
@@ -1,214 +0,0 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import { redactSecrets } from "../security.mjs";
3
-
4
- function deepFreeze(value) {
5
- if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
6
- for (const child of Object.values(value)) deepFreeze(child);
7
- return Object.freeze(value);
8
- }
9
-
10
- function finalizePlan(plan) {
11
- const { planHash: _oldHash, ...unsigned } = plan;
12
- const planHash = "sha256:" + createHash("sha256").update(JSON.stringify(unsigned)).digest("hex");
13
- return deepFreeze({ ...unsigned, planHash });
14
- }
15
-
16
- /**
17
- * @typedef {import("../ux/capabilities.mjs").ActionPlan} ActionPlan
18
- * @typedef {import("../ux/queue-model.mjs").QueueSnapshot} QueueSnapshot
19
- */
20
-
21
- /**
22
- * Plan task lifecycle action into an immutable ActionPlan.
23
- * @param {QueueSnapshot} snapshot
24
- * @param {Object} intent
25
- * @param {string} intent.kind
26
- * @param {string} intent.taskId
27
- * @param {Record<string, any>} [intent.answers]
28
- * @returns {Promise<ActionPlan>}
29
- */
30
- export async function planTaskAction(snapshot, intent = {}) {
31
- const taskId = typeof intent.taskId === "string" ? intent.taskId : "";
32
- if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(taskId)) {
33
- throw new Error("Task id must be a safe queue identifier.");
34
- }
35
- const task = (snapshot?.tasks || []).find((t) => t.id === taskId);
36
- if (!task) {
37
- throw new Error(`Task ${taskId} not found in queue snapshot.`);
38
- }
39
-
40
- const root = snapshot.repository;
41
- const planId = `PLAN-TASK-${Date.now()}-${randomUUID().slice(0, 8)}`;
42
- const sidecarRelPath = `.agent/jules-queue/${taskId}.state.json`;
43
-
44
- if (intent.kind === "retry") {
45
- const allowedStates = ["failed", "deferred", "quarantined", "cancelled"];
46
- if (!allowedStates.includes(task.state)) {
47
- throw new Error(`Task ${taskId} in state '${task.state}' cannot be retried.`);
48
- }
49
-
50
- const nextAttempt = (Number.isFinite(task.attempt) ? task.attempt : 0) + 1;
51
- const sidecarContent = JSON.stringify(
52
- {
53
- schema: "agentctl/task-state-v1",
54
- taskId,
55
- revision: (Number.isFinite(task.stateRevision) ? task.stateRevision : 0) + 1,
56
- state: "pending",
57
- attempt: nextAttempt,
58
- maxAttempts: task.maxAttempts,
59
- createdAt: task.createdAt,
60
- updatedAt: new Date().toISOString(),
61
- },
62
- null,
63
- 2
64
- );
65
-
66
- return finalizePlan({
67
- schema: "agentctl/action-plan-v1",
68
- id: planId,
69
- kind: "task.retry",
70
- title: `Retry task ${taskId}`,
71
- summary: `Reset state to pending and increment attempt count to ${nextAttempt}`,
72
- risk: "moderate",
73
- repository: root,
74
- createdAt: new Date().toISOString(),
75
- preconditions: [
76
- {
77
- kind: "file-hash",
78
- target: sidecarRelPath,
79
- expected: task.stateHash, // Checked prior state hash if sidecar exists
80
- },
81
- ],
82
- fileMutations: [
83
- {
84
- operation: "replace",
85
- path: sidecarRelPath,
86
- newContent: sidecarContent,
87
- },
88
- ],
89
- commandEffects: [],
90
- stateTransitions: [{ taskId, from: task.state, to: "pending" }],
91
- preview: {
92
- unifiedDiff: `--- a/${sidecarRelPath}\n+++ b/${sidecarRelPath}\n@@ -state @@\n- state: ${task.state}\n+ state: pending\n- attempt: ${task.attempt}\n+ attempt: ${nextAttempt}\n`,
93
- warnings: [],
94
- estimatedImpact: [`Resets ${taskId} to pending state (Attempt ${nextAttempt})`],
95
- },
96
- confirmation: {
97
- mode: "keypress",
98
- prompt: `Confirm retry of ${taskId} (Attempt ${nextAttempt})?`,
99
- },
100
- planHash: "sha256:" + createHash("sha256").update(planId).digest("hex"),
101
- });
102
- }
103
-
104
- if (intent.kind === "quarantine-override") {
105
- if (task.state !== "quarantined") {
106
- throw new Error(`Task ${taskId} is not in quarantined state.`);
107
- }
108
-
109
- const reason = redactSecrets(String(intent.answers?.reason || "").trim());
110
- if (!reason) {
111
- throw new Error("Quarantine override requires a non-empty reason.");
112
- }
113
-
114
- const sidecarContent = JSON.stringify(
115
- {
116
- schema: "agentctl/task-state-v1",
117
- taskId,
118
- revision: (Number.isFinite(task.stateRevision) ? task.stateRevision : 0) + 1,
119
- state: "pending",
120
- attempt: task.attempt,
121
- maxAttempts: task.maxAttempts,
122
- quarantineOverrideReason: reason,
123
- updatedAt: new Date().toISOString(),
124
- },
125
- null,
126
- 2
127
- );
128
-
129
- return finalizePlan({
130
- schema: "agentctl/action-plan-v1",
131
- id: planId,
132
- kind: "task.quarantine-override",
133
- title: `Override quarantine for ${taskId}`,
134
- summary: `Bypass quarantine with reason: "${reason}"`,
135
- risk: "high",
136
- repository: root,
137
- createdAt: new Date().toISOString(),
138
- preconditions: [
139
- {
140
- kind: "file-hash",
141
- target: sidecarRelPath,
142
- expected: task.stateHash,
143
- },
144
- ],
145
- fileMutations: [
146
- {
147
- operation: "replace",
148
- path: sidecarRelPath,
149
- newContent: sidecarContent,
150
- },
151
- ],
152
- commandEffects: [],
153
- stateTransitions: [{ taskId, from: "quarantined", to: "pending" }],
154
- preview: {
155
- unifiedDiff: `--- a/${sidecarRelPath}\n+++ b/${sidecarRelPath}\n@@ -state @@\n- state: quarantined\n+ state: pending\n+ overrideReason: "${reason}"\n`,
156
- warnings: ["Bypassing quarantine filter requires verified operator review."],
157
- estimatedImpact: [`Re-enables ${taskId} for dispatch`],
158
- },
159
- confirmation: {
160
- mode: "typed-task-id",
161
- prompt: `Type '${taskId}' to confirm quarantine override:`,
162
- expected: taskId,
163
- requireReason: true,
164
- },
165
- planHash: "sha256:" + createHash("sha256").update(planId).digest("hex"),
166
- });
167
- }
168
-
169
- if (intent.kind === "delete-task") {
170
- if (task.state === "running" || task.state === "verifying") {
171
- throw new Error(`Task ${taskId} is currently active (${task.state}) and cannot be deleted.`);
172
- }
173
-
174
- const trashRelPath = `.agent/jules-queue/.trash/${taskId}.md`;
175
- return finalizePlan({
176
- schema: "agentctl/action-plan-v1",
177
- id: planId,
178
- kind: "task.delete",
179
- title: `Delete task ${taskId}`,
180
- summary: `Move task file ${task.taskFile} to trash directory`,
181
- risk: "moderate",
182
- repository: root,
183
- createdAt: new Date().toISOString(),
184
- preconditions: [
185
- {
186
- kind: "file-hash",
187
- target: task.taskFile,
188
- expected: task.envelopeHash,
189
- },
190
- ],
191
- fileMutations: [
192
- {
193
- operation: "move",
194
- path: trashRelPath,
195
- fromPath: task.taskFile,
196
- },
197
- ],
198
- commandEffects: [],
199
- stateTransitions: [{ taskId, from: task.state, to: "cancelled" }],
200
- preview: {
201
- unifiedDiff: `--- a/${task.taskFile}\n+++ /dev/null\n`,
202
- warnings: [],
203
- estimatedImpact: [`Moves ${taskId} to trash`],
204
- },
205
- confirmation: {
206
- mode: "keypress",
207
- prompt: `Delete task ${taskId}?`,
208
- },
209
- planHash: "sha256:" + createHash("sha256").update(planId).digest("hex"),
210
- });
211
- }
212
-
213
- throw new Error(`Unsupported task action intent: ${intent.kind}`);
214
- }
@@ -1,278 +0,0 @@
1
- import {
2
- readFileSync,
3
- mkdirSync,
4
- existsSync,
5
- unlinkSync,
6
- renameSync,
7
- openSync,
8
- writeSync,
9
- fsyncSync,
10
- closeSync,
11
- } from "node:fs";
12
- import { join, dirname, resolve, relative, isAbsolute } from "node:path";
13
- import { createHash, randomUUID } from "node:crypto";
14
- import { execFileSync } from "node:child_process";
15
- import { createOperationReceipt } from "./receipts.mjs";
16
- import { withVfsMutex } from "../state.mjs";
17
-
18
- /**
19
- * @typedef {import("../ux/capabilities.mjs").TerminalSessionOptions} TerminalSessionOptions
20
- */
21
-
22
- /**
23
- * Calculate file SHA-256 hash.
24
- * @param {string} filePath
25
- * @returns {string | null}
26
- */
27
- function getFileHash(filePath) {
28
- if (!existsSync(filePath)) return null;
29
- try {
30
- const data = readFileSync(filePath);
31
- return "sha256:" + createHash("sha256").update(data).digest("hex");
32
- } catch {
33
- return null;
34
- }
35
- }
36
-
37
- function resolveWithinRoot(root, candidate, label) {
38
- if (typeof candidate !== "string" || !candidate || candidate.includes("\0")) {
39
- throw new Error(`Invalid ${label} path`);
40
- }
41
- const targetPath = resolve(root, candidate);
42
- const rel = relative(resolve(root), targetPath);
43
- if (rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(rel)) {
44
- throw new Error(`${label} path escapes repository root: ${candidate}`);
45
- }
46
- return targetPath;
47
- }
48
-
49
- function atomicWriteFile(filePath, content) {
50
- const tmpPath = `${filePath}.${randomUUID()}.tmp`;
51
- let fd;
52
- try {
53
- fd = openSync(tmpPath, "wx", 0o600);
54
- writeSync(fd, content, "utf-8");
55
- fsyncSync(fd);
56
- closeSync(fd);
57
- fd = undefined;
58
- renameSync(tmpPath, filePath);
59
- } catch (err) {
60
- if (fd !== undefined) {
61
- try { closeSync(fd); } catch (_) {}
62
- }
63
- try { unlinkSync(tmpPath); } catch (_) {}
64
- throw err;
65
- }
66
- }
67
-
68
- function throwIfAborted(signal) {
69
- if (signal?.aborted) {
70
- throw signal.reason || new Error("Action plan application aborted");
71
- }
72
- }
73
-
74
- /**
75
- * Verify preconditions of an ActionPlan against target repository state.
76
- * @param {import("../ux/capabilities.mjs").ActionPlan} plan
77
- * @param {string} root
78
- */
79
- export function verifyPreconditions(plan, root) {
80
- for (const cond of plan.preconditions || []) {
81
- if (cond.kind === "file-hash") {
82
- const targetPath = resolveWithinRoot(root, cond.target, "Precondition target");
83
- const actualHash = getFileHash(targetPath);
84
- if (actualHash !== cond.expected) {
85
- throw new Error(
86
- `Precondition failed [file-hash]: ${cond.target} expected ${cond.expected}, received ${actualHash}`
87
- );
88
- }
89
- } else if (cond.kind === "lock-free") {
90
- const lockTarget = String(cond.target || "");
91
- const lockBase = resolveWithinRoot(root, join(".agent", "state", "locks", lockTarget), "Lock target");
92
- const lockPaths = [`${lockBase}.json`, `${lockBase}.lock`];
93
- if (lockPaths.some((lockPath) => existsSync(lockPath))) {
94
- throw new Error(`Precondition failed [lock-free]: Lock ${cond.target} is active.`);
95
- }
96
- }
97
- }
98
- }
99
-
100
- /**
101
- * Execute an ActionPlan within a transactional boundaries (atomic apply + rollback).
102
- * @param {any} plan
103
- * @param {Object} options
104
- * @param {string} options.root
105
- * @param {AbortSignal} [options.signal]
106
- * @returns {Promise<import("./receipts.mjs").OperationReceipt>}
107
- */
108
- export async function applyActionPlan(plan, options = {}) {
109
- const root = resolve(options.root || process.cwd());
110
- const mutexDir = join(root, ".agent", "state", ".action-plan.mutex");
111
- mkdirSync(dirname(mutexDir), { recursive: true });
112
- return withVfsMutex(mutexDir, () => applyActionPlanLocked(plan, { ...options, root }));
113
- }
114
-
115
- async function applyActionPlanLocked(plan, options) {
116
- const root = options.root;
117
- const startTime = Date.now();
118
- throwIfAborted(options.signal);
119
-
120
- // 1. Verify Preconditions
121
- verifyPreconditions(plan, root);
122
-
123
- // 2. Prepare Intent Journaling
124
- if (!plan || typeof plan.id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(plan.id)) {
125
- throw new Error("Invalid action plan id");
126
- }
127
- const journalDir = join(root, ".agent", "state", "journal");
128
- if (!existsSync(journalDir)) {
129
- mkdirSync(journalDir, { recursive: true });
130
- }
131
- const journalPath = join(journalDir, `${plan.id}.intent.json`);
132
- atomicWriteFile(journalPath, JSON.stringify({ plan, startedAt: new Date().toISOString() }, null, 2));
133
-
134
- /** @type {Array<{ path: string, originalContent: string | null, operation?: string, fromPath?: string, sourceContent?: string }>} */
135
- const rollbackStack = [];
136
- const mutationsApplied = [];
137
- const effectsExecuted = [];
138
-
139
- try {
140
- // 3. Apply File Mutations
141
- for (const mutation of plan.fileMutations || []) {
142
- throwIfAborted(options.signal);
143
- const targetPath = resolveWithinRoot(root, mutation.path, "Mutation target");
144
-
145
- if (mutation.operation === "move") {
146
- const sourcePath = resolveWithinRoot(root, mutation.fromPath, "Move source");
147
- if (sourcePath === targetPath) {
148
- throw new Error(`Move source and target are identical: ${mutation.path}`);
149
- }
150
- if (!existsSync(sourcePath)) {
151
- throw new Error(`Move source does not exist: ${mutation.fromPath}`);
152
- }
153
- if (existsSync(targetPath)) {
154
- throw new Error(`Move target already exists: ${mutation.path}`);
155
- }
156
- const sourceContent = readFileSync(sourcePath, "utf-8");
157
- const sourceHash = "sha256:" + createHash("sha256").update(sourceContent).digest("hex");
158
- mkdirSync(dirname(targetPath), { recursive: true });
159
- rollbackStack.push({
160
- path: targetPath,
161
- originalContent: null,
162
- operation: "move",
163
- fromPath: sourcePath,
164
- sourceContent,
165
- });
166
- renameSync(sourcePath, targetPath);
167
- mutationsApplied.push({ path: mutation.path, operation: "move", hashBefore: sourceHash });
168
- continue;
169
- }
170
-
171
- if (!["delete", "create", "replace"].includes(mutation.operation)) {
172
- throw new Error(`Unsupported file mutation operation: ${mutation.operation}`);
173
- }
174
-
175
- const parentDir = dirname(targetPath);
176
- if (!existsSync(parentDir)) {
177
- mkdirSync(parentDir, { recursive: true });
178
- }
179
-
180
- const existingContent = existsSync(targetPath) ? readFileSync(targetPath, "utf-8") : null;
181
- const hashBefore = existingContent !== null
182
- ? "sha256:" + createHash("sha256").update(existingContent).digest("hex")
183
- : undefined;
184
-
185
- if (mutation.operation === "create" && existingContent !== null) {
186
- throw new Error(`Create target already exists: ${mutation.path}`);
187
- }
188
- rollbackStack.push({ path: targetPath, originalContent: existingContent, operation: mutation.operation });
189
-
190
- if (mutation.operation === "delete") {
191
- if (existsSync(targetPath)) {
192
- unlinkSync(targetPath);
193
- }
194
- mutationsApplied.push({ path: mutation.path, operation: "delete", hashBefore });
195
- } else {
196
- const newContent = mutation.newContent ?? "";
197
- atomicWriteFile(targetPath, newContent);
198
-
199
- const hashAfter = "sha256:" + createHash("sha256").update(newContent).digest("hex");
200
- mutationsApplied.push({ path: mutation.path, operation: mutation.operation, hashBefore, hashAfter });
201
- }
202
- }
203
-
204
- // 4. Execute Command Effects
205
- for (const effect of plan.commandEffects || []) {
206
- throwIfAborted(options.signal);
207
- const execPath = effect.executable;
208
- const args = Array.isArray(effect.args) ? effect.args : [];
209
- const cwd = effect.cwd ? resolveWithinRoot(root, effect.cwd, "Command working directory") : root;
210
-
211
- try {
212
- execFileSync(execPath, args, {
213
- cwd,
214
- timeout: effect.timeoutMs || 30000,
215
- env: process.env,
216
- });
217
- effectsExecuted.push({ executable: execPath, args, exitCode: 0 });
218
- } catch (err) {
219
- const exitCode = typeof err.status === "number" ? err.status : 1;
220
- effectsExecuted.push({ executable: execPath, args, exitCode });
221
- throw new Error(`Command effect failed (${execPath} ${args.join(" ")}): exit code ${exitCode}`);
222
- }
223
- }
224
-
225
- // 5. Clean up Journal & Record Success Receipt
226
- if (existsSync(journalPath)) {
227
- unlinkSync(journalPath);
228
- }
229
-
230
- const receipt = createOperationReceipt(root, {
231
- actionPlanId: plan.id,
232
- kind: plan.kind,
233
- title: plan.title,
234
- repository: plan.repository || "local",
235
- status: "success",
236
- durationMs: Date.now() - startTime,
237
- mutationsApplied,
238
- effectsExecuted,
239
- });
240
-
241
- return receipt;
242
- } catch (err) {
243
- // Execute Rollback Stack in reverse order
244
- for (let i = rollbackStack.length - 1; i >= 0; i--) {
245
- const mutation = rollbackStack[i];
246
- try {
247
- if (mutation.operation === "move") {
248
- if (existsSync(mutation.path) && !existsSync(mutation.fromPath)) {
249
- renameSync(mutation.path, mutation.fromPath);
250
- }
251
- } else if (mutation.originalContent === null) {
252
- if (existsSync(mutation.path)) unlinkSync(mutation.path);
253
- } else {
254
- atomicWriteFile(mutation.path, mutation.originalContent);
255
- }
256
- } catch {
257
- // Best-effort rollback
258
- }
259
- }
260
-
261
- if (existsSync(journalPath)) {
262
- try { unlinkSync(journalPath); } catch (_) {}
263
- }
264
-
265
- const failedReceipt = createOperationReceipt(root, {
266
- actionPlanId: plan.id,
267
- kind: plan.kind,
268
- title: plan.title,
269
- repository: plan.repository || "local",
270
- status: "rolled-back",
271
- durationMs: Date.now() - startTime,
272
- mutationsApplied,
273
- effectsExecuted,
274
- });
275
-
276
- throw new Error(`Action plan execution failed and was rolled back: ${err.message}. Receipt: ${failedReceipt.receiptId}`);
277
- }
278
- }
@@ -1,180 +0,0 @@
1
- import { isatty } from "node:tty";
2
-
3
- /**
4
- * @typedef {Object} TerminalCapabilities
5
- * @property {boolean} inputIsTTY
6
- * @property {boolean} outputIsTTY
7
- * @property {number} columns
8
- * @property {number} rows
9
- * @property {0 | 4 | 8 | 24} colorDepth
10
- * @property {boolean} ansi
11
- * @property {boolean} unicode
12
- * @property {boolean} alternateScreen
13
- * @property {false} mouse
14
- * @property {NodeJS.Platform} platform
15
- * @property {string} term
16
- * @property {boolean} noColor
17
- * @property {boolean} forceColor
18
- * @property {boolean} reducedMotion
19
- */
20
-
21
- /**
22
- * @typedef {Object} TerminalSessionOptions
23
- * @property {import("node:stream").Readable} [input]
24
- * @property {import("node:stream").Writable} [output]
25
- * @property {import("node:stream").Writable} [errorOutput]
26
- * @property {boolean} [jsonMode]
27
- * @property {boolean} [alternateScreen]
28
- * @property {"auto" | "always" | "never"} [color]
29
- * @property {"auto" | "always" | "never"} [unicode]
30
- * @property {boolean} [reducedMotion]
31
- */
32
-
33
- /**
34
- * Detect current terminal capabilities based on streams, environment variables, and CLI options.
35
- * @param {TerminalSessionOptions} [options]
36
- * @returns {TerminalCapabilities}
37
- */
38
- export function detectTerminalCapabilities(options = {}) {
39
- const input = options.input || process.stdin;
40
- const output = options.output || process.stdout;
41
- const env = process.env || {};
42
- const argv = process.argv || [];
43
-
44
- const inputIsTTY = Boolean(input && (input.isTTY || (typeof input.fd === "number" && isatty(input.fd))));
45
- const outputIsTTY = Boolean(output && (output.isTTY || (typeof output.fd === "number" && isatty(output.fd))));
46
-
47
- const term = String(env.TERM || "").toLowerCase();
48
- const colorTerm = String(env.COLORTERM || "").toLowerCase();
49
-
50
- // Environment color flags
51
- const envNoColor = Boolean(env.NO_COLOR !== undefined && env.NO_COLOR !== "");
52
- let envForceColor = false;
53
- let envDisableColor = false;
54
-
55
- if (env.FORCE_COLOR !== undefined) {
56
- if (env.FORCE_COLOR === "0" || env.FORCE_COLOR === "false") {
57
- envDisableColor = true;
58
- } else {
59
- envForceColor = true;
60
- }
61
- }
62
-
63
- // CLI argument overrides
64
- const cliNoColor = argv.includes("--no-color");
65
- const cliColor = argv.includes("--color");
66
- const cliAscii = argv.includes("--ascii");
67
- const jsonMode = Boolean(options.jsonMode || argv.includes("--json"));
68
-
69
- const noColor = envNoColor || cliNoColor || options.color === "never";
70
- const forceColor = (envForceColor || cliColor || options.color === "always") && !envDisableColor;
71
-
72
- // Priority evaluation for ANSI support:
73
- // --no-color / options.color === "never" > NO_COLOR > FORCE_COLOR=0 > jsonMode > --color / FORCE_COLOR > auto TTY capability
74
- let ansi = false;
75
- if (jsonMode) {
76
- ansi = false;
77
- } else if (noColor || envDisableColor) {
78
- ansi = false;
79
- } else if (forceColor) {
80
- ansi = true;
81
- } else {
82
- ansi = outputIsTTY && term !== "dumb";
83
- }
84
-
85
- // Color depth calculation
86
- /** @type {0 | 4 | 8 | 24} */
87
- let colorDepth = 0;
88
- if (ansi) {
89
- if (colorTerm === "truecolor" || colorTerm === "24bit" || env.FORCE_COLOR === "3") {
90
- colorDepth = 24;
91
- } else if (term.includes("256color") || colorTerm === "256color" || env.FORCE_COLOR === "2") {
92
- colorDepth = 8;
93
- } else {
94
- colorDepth = 4;
95
- }
96
- }
97
-
98
- // Unicode support detection
99
- let unicode = true;
100
- if (cliAscii || options.unicode === "never") {
101
- unicode = false;
102
- } else if (options.unicode === "always") {
103
- unicode = true;
104
- } else {
105
- const lang = String(env.LANG || env.LC_ALL || env.LC_CTYPE || "").toLowerCase();
106
- const isUtf8 = lang.includes("utf-8") || lang.includes("utf8");
107
- // Default to true on non-windows or utf-8 windows terminals
108
- unicode = isUtf8 || process.platform !== "win32" || Boolean(env.WT_SESSION);
109
- }
110
-
111
- // Terminal dimensions
112
- const columns = Math.max(10, output.columns || input.columns || process.stdout.columns || 80);
113
- const rows = Math.max(5, output.rows || input.rows || process.stdout.rows || 24);
114
-
115
- const reducedMotion = Boolean(
116
- options.reducedMotion ||
117
- env.REDUCED_MOTION === "1" ||
118
- env.REDUCED_MOTION === "true"
119
- );
120
-
121
- const alternateScreen = options.alternateScreen !== false && ansi && outputIsTTY;
122
-
123
- return {
124
- inputIsTTY,
125
- outputIsTTY,
126
- columns,
127
- rows,
128
- colorDepth,
129
- ansi,
130
- unicode,
131
- alternateScreen,
132
- mouse: false,
133
- platform: process.platform,
134
- term: env.TERM || "",
135
- noColor,
136
- forceColor,
137
- reducedMotion,
138
- };
139
- }
140
-
141
- /**
142
- * Get display symbol set based on unicode capability.
143
- * @param {TerminalCapabilities} caps
144
- */
145
- export function getSymbols(caps) {
146
- if (caps.unicode) {
147
- return {
148
- select: "❯",
149
- checked: "●",
150
- unchecked: "○",
151
- success: "✓",
152
- warning: "!",
153
- failure: "✗",
154
- info: "ℹ",
155
- bullet: "•",
156
- ellipsis: "…",
157
- spinner: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
158
- branchMiddle: "├─",
159
- branchEnd: "└─",
160
- arrowUp: "↑",
161
- arrowDown: "↓",
162
- };
163
- }
164
- return {
165
- select: ">",
166
- checked: "[x]",
167
- unchecked: "[ ]",
168
- success: "OK",
169
- warning: "WARN",
170
- failure: "ERR",
171
- info: "INFO",
172
- bullet: "*",
173
- ellipsis: "...",
174
- spinner: ["-", "\\", "|", "/"],
175
- branchMiddle: "+-",
176
- branchEnd: "`-",
177
- arrowUp: "^",
178
- arrowDown: "v",
179
- };
180
- }