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.
- package/README.md +66 -7
- package/bin/agentctl.mjs +172 -1
- package/bin/init.js +87 -74
- package/index.mjs +10 -0
- package/package.json +5 -2
- package/scripts/jules-create.mjs +5 -16
- package/scripts/jules-patch.mjs +18 -10
- package/scripts/stale-base-check.mjs +1 -1
- package/src/ci-templates.mjs +199 -0
- package/src/config.mjs +39 -11
- package/src/engine.mjs +2 -21
- package/src/env-aliases.mjs +80 -0
- package/src/mcp.mjs +19 -1
- package/src/ops/doctor-registry.mjs +53 -32
- package/src/ops/next-step.mjs +38 -7
- package/src/profiles-io.mjs +77 -0
- package/src/profiles.mjs +188 -0
- package/src/provider-readiness.mjs +215 -0
- package/src/security.mjs +42 -1
- package/src/tui.mjs +14 -3
- package/src/wizard-init.mjs +32 -4
- package/src/ops/doctor-planner.mjs +0 -220
- package/src/ops/receipts.mjs +0 -171
- package/src/ops/swarm-actions.mjs +0 -156
- package/src/ops/task-actions.mjs +0 -214
- package/src/ops/transaction.mjs +0 -278
- package/src/ux/capabilities.mjs +0 -180
- package/src/ux/diff-viewer.mjs +0 -289
- package/src/ux/layout.mjs +0 -195
- package/src/ux/log-viewer.mjs +0 -110
- package/src/ux/palette.mjs +0 -179
- package/src/ux/queue-model.mjs +0 -206
- package/src/ux/renderer.mjs +0 -188
- package/src/ux/swarm-model.mjs +0 -146
- package/src/ux/terminal-session.mjs +0 -215
- package/src/ux/widgets.mjs +0 -323
- /package/src/{ux/key-decoder.mjs → key-decoder.mjs} +0 -0
package/src/ops/task-actions.mjs
DELETED
|
@@ -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
|
-
}
|
package/src/ops/transaction.mjs
DELETED
|
@@ -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
|
-
}
|
package/src/ux/capabilities.mjs
DELETED
|
@@ -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
|
-
}
|