aem-ext-daemon 0.3.8 → 0.4.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/dist/capabilities/shell.d.ts +13 -0
- package/dist/capabilities/shell.js +138 -0
- package/dist/daemon.js +2 -0
- package/dist/dispatcher.js +9 -0
- package/dist/logger.d.ts +4 -3
- package/dist/logger.js +15 -25
- package/package.json +1 -1
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* aio:run streams stdout/stderr back through the WebSocket.
|
|
5
5
|
* shell:exec runs a command and returns the full output.
|
|
6
|
+
* shell:spawn starts an interactive process (for commands that need user input).
|
|
7
|
+
* shell:stdin sends input to the active interactive process.
|
|
6
8
|
*/
|
|
7
9
|
import type { DaemonConnection } from "../connection.js";
|
|
8
10
|
/** Run a shell command synchronously and return stdout + stderr. */
|
|
@@ -15,3 +17,14 @@ export declare function checkAio(): string;
|
|
|
15
17
|
* then returns the exit code.
|
|
16
18
|
*/
|
|
17
19
|
export declare function runAio(args: string[], cwd: string, requestId: string, connection: DaemonConnection): Promise<string>;
|
|
20
|
+
/**
|
|
21
|
+
* Start an interactive command. Collects output until the process goes
|
|
22
|
+
* quiet (waiting for user input) or exits, then returns what it has.
|
|
23
|
+
* Does NOT set CI=true or TERM=dumb so prompts render normally.
|
|
24
|
+
*/
|
|
25
|
+
export declare function spawnInteractive(command: string, cwd: string): Promise<string>;
|
|
26
|
+
/**
|
|
27
|
+
* Send input to the active interactive process. Waits for the process
|
|
28
|
+
* to produce more output (or go quiet again), then returns the new output.
|
|
29
|
+
*/
|
|
30
|
+
export declare function writeToProcess(input: string): Promise<string>;
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* aio:run streams stdout/stderr back through the WebSocket.
|
|
5
5
|
* shell:exec runs a command and returns the full output.
|
|
6
|
+
* shell:spawn starts an interactive process (for commands that need user input).
|
|
7
|
+
* shell:stdin sends input to the active interactive process.
|
|
6
8
|
*/
|
|
7
9
|
import { execSync, spawn } from "node:child_process";
|
|
8
10
|
import fs from "node:fs";
|
|
@@ -143,3 +145,139 @@ export function runAio(args, cwd, requestId, connection) {
|
|
|
143
145
|
}, 300_000);
|
|
144
146
|
});
|
|
145
147
|
}
|
|
148
|
+
// ─── Interactive process support ────────────────────────────
|
|
149
|
+
// Allows commands that need user input (like `aio app create`)
|
|
150
|
+
// to run through the chat UI.
|
|
151
|
+
/** The single active interactive process (one per daemon). */
|
|
152
|
+
let activeProcess = null;
|
|
153
|
+
/** How long to wait for more output before assuming the process is waiting for input. */
|
|
154
|
+
const SILENCE_TIMEOUT = 3_000;
|
|
155
|
+
/**
|
|
156
|
+
* Start an interactive command. Collects output until the process goes
|
|
157
|
+
* quiet (waiting for user input) or exits, then returns what it has.
|
|
158
|
+
* Does NOT set CI=true or TERM=dumb so prompts render normally.
|
|
159
|
+
*/
|
|
160
|
+
export function spawnInteractive(command, cwd) {
|
|
161
|
+
// Kill any existing interactive process
|
|
162
|
+
if (activeProcess && !activeProcess.done) {
|
|
163
|
+
activeProcess.child.kill("SIGTERM");
|
|
164
|
+
}
|
|
165
|
+
activeProcess = null;
|
|
166
|
+
if (!fs.existsSync(cwd)) {
|
|
167
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
168
|
+
}
|
|
169
|
+
return new Promise((resolve) => {
|
|
170
|
+
const child = spawn(command, [], {
|
|
171
|
+
cwd,
|
|
172
|
+
shell: getUserShell(),
|
|
173
|
+
env: {
|
|
174
|
+
...getEnhancedEnv(),
|
|
175
|
+
// Do NOT set CI or TERM=dumb — we want interactive prompts
|
|
176
|
+
FORCE_COLOR: "0",
|
|
177
|
+
NO_COLOR: "1",
|
|
178
|
+
},
|
|
179
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
180
|
+
});
|
|
181
|
+
const proc = { child, buffer: "", done: false, exitCode: null };
|
|
182
|
+
activeProcess = proc;
|
|
183
|
+
let silenceTimer;
|
|
184
|
+
const flushAndReturn = () => {
|
|
185
|
+
const output = proc.buffer;
|
|
186
|
+
proc.buffer = "";
|
|
187
|
+
resolve(output || "(no output)");
|
|
188
|
+
};
|
|
189
|
+
const resetTimer = () => {
|
|
190
|
+
clearTimeout(silenceTimer);
|
|
191
|
+
silenceTimer = setTimeout(() => {
|
|
192
|
+
if (!proc.done) {
|
|
193
|
+
// Process went quiet — probably waiting for input
|
|
194
|
+
flushAndReturn();
|
|
195
|
+
}
|
|
196
|
+
}, SILENCE_TIMEOUT);
|
|
197
|
+
};
|
|
198
|
+
child.stdout?.on("data", (chunk) => {
|
|
199
|
+
proc.buffer += chunk.toString();
|
|
200
|
+
resetTimer();
|
|
201
|
+
});
|
|
202
|
+
child.stderr?.on("data", (chunk) => {
|
|
203
|
+
proc.buffer += chunk.toString();
|
|
204
|
+
resetTimer();
|
|
205
|
+
});
|
|
206
|
+
child.on("close", (code) => {
|
|
207
|
+
proc.done = true;
|
|
208
|
+
proc.exitCode = code;
|
|
209
|
+
clearTimeout(silenceTimer);
|
|
210
|
+
proc.buffer += `\n[Process exited with code ${code ?? 1}]`;
|
|
211
|
+
flushAndReturn();
|
|
212
|
+
});
|
|
213
|
+
child.on("error", (err) => {
|
|
214
|
+
proc.done = true;
|
|
215
|
+
clearTimeout(silenceTimer);
|
|
216
|
+
proc.buffer += `\n[Error: ${err.message}]`;
|
|
217
|
+
flushAndReturn();
|
|
218
|
+
});
|
|
219
|
+
// Start the silence timer
|
|
220
|
+
resetTimer();
|
|
221
|
+
// Overall timeout: 5 minutes
|
|
222
|
+
setTimeout(() => {
|
|
223
|
+
if (!proc.done) {
|
|
224
|
+
child.kill("SIGTERM");
|
|
225
|
+
proc.done = true;
|
|
226
|
+
proc.buffer += "\n[Timed out after 5 minutes]";
|
|
227
|
+
flushAndReturn();
|
|
228
|
+
}
|
|
229
|
+
}, 300_000);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Send input to the active interactive process. Waits for the process
|
|
234
|
+
* to produce more output (or go quiet again), then returns the new output.
|
|
235
|
+
*/
|
|
236
|
+
export function writeToProcess(input) {
|
|
237
|
+
if (!activeProcess || activeProcess.done) {
|
|
238
|
+
return Promise.resolve(activeProcess
|
|
239
|
+
? `[Process already exited with code ${activeProcess.exitCode}]`
|
|
240
|
+
: "[No interactive process running. Use interactive_bash to start one.]");
|
|
241
|
+
}
|
|
242
|
+
const proc = activeProcess;
|
|
243
|
+
return new Promise((resolve) => {
|
|
244
|
+
let silenceTimer;
|
|
245
|
+
let resolved = false;
|
|
246
|
+
const finish = (output) => {
|
|
247
|
+
if (resolved)
|
|
248
|
+
return;
|
|
249
|
+
resolved = true;
|
|
250
|
+
clearTimeout(silenceTimer);
|
|
251
|
+
proc.child.stdout?.off("data", onData);
|
|
252
|
+
proc.child.stderr?.off("data", onData);
|
|
253
|
+
proc.child.off("close", onClose);
|
|
254
|
+
resolve(output);
|
|
255
|
+
};
|
|
256
|
+
const resetTimer = () => {
|
|
257
|
+
clearTimeout(silenceTimer);
|
|
258
|
+
silenceTimer = setTimeout(() => {
|
|
259
|
+
if (!proc.done) {
|
|
260
|
+
const output = proc.buffer;
|
|
261
|
+
proc.buffer = "";
|
|
262
|
+
finish(output || "(waiting for input...)");
|
|
263
|
+
}
|
|
264
|
+
}, SILENCE_TIMEOUT);
|
|
265
|
+
};
|
|
266
|
+
const onData = () => resetTimer();
|
|
267
|
+
const onClose = () => {
|
|
268
|
+
// Small delay to capture any final output
|
|
269
|
+
setTimeout(() => {
|
|
270
|
+
const output = proc.buffer;
|
|
271
|
+
proc.buffer = "";
|
|
272
|
+
finish(output || "(no output)");
|
|
273
|
+
}, 200);
|
|
274
|
+
};
|
|
275
|
+
proc.child.stdout?.on("data", onData);
|
|
276
|
+
proc.child.stderr?.on("data", onData);
|
|
277
|
+
proc.child.once("close", onClose);
|
|
278
|
+
// Write the input
|
|
279
|
+
proc.child.stdin?.write(input + "\n");
|
|
280
|
+
// Start silence timer
|
|
281
|
+
resetTimer();
|
|
282
|
+
});
|
|
283
|
+
}
|
package/dist/daemon.js
CHANGED
|
@@ -17,6 +17,7 @@ import { DaemonConnection } from "./connection.js";
|
|
|
17
17
|
import { ensureIdentity, getWorkspaceRoot, setWorkspaceRoot } from "./identity.js";
|
|
18
18
|
import { generatePairCode, displayPairCode } from "./pairing.js";
|
|
19
19
|
import { dispatch } from "./dispatcher.js";
|
|
20
|
+
import { getLogFilePath } from "./logger.js";
|
|
20
21
|
// Read version from package.json so it stays in sync automatically
|
|
21
22
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
23
|
const pkgPath = path.resolve(__dirname, "..", "package.json");
|
|
@@ -57,6 +58,7 @@ export class Daemon {
|
|
|
57
58
|
if (workspace) {
|
|
58
59
|
console.log(` Workspace: ${workspace}`);
|
|
59
60
|
}
|
|
61
|
+
console.log(` Log file: ${getLogFilePath()}`);
|
|
60
62
|
console.log("");
|
|
61
63
|
console.log(" Connecting...");
|
|
62
64
|
this.connection.connect();
|
package/dist/dispatcher.js
CHANGED
|
@@ -113,6 +113,15 @@ async function dispatchInner(command, payload, connection) {
|
|
|
113
113
|
: getWorkspaceRoot();
|
|
114
114
|
return shellCap.exec(payload.command, cwd);
|
|
115
115
|
}
|
|
116
|
+
case "shell:spawn": {
|
|
117
|
+
const cwd = payload.cwd
|
|
118
|
+
? validatePath(payload.cwd)
|
|
119
|
+
: getWorkspaceRoot();
|
|
120
|
+
return shellCap.spawnInteractive(payload.command, cwd);
|
|
121
|
+
}
|
|
122
|
+
case "shell:stdin": {
|
|
123
|
+
return shellCap.writeToProcess(payload.input);
|
|
124
|
+
}
|
|
116
125
|
// ─── Skills ────────────────────────────────────────
|
|
117
126
|
case "skills:sync": {
|
|
118
127
|
const workspace = getWorkspaceRoot();
|
package/dist/logger.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Daemon logger — writes timestamped entries to a log file
|
|
2
|
+
* Daemon logger — writes timestamped entries to a log file and the terminal.
|
|
3
3
|
*
|
|
4
|
-
* Log file
|
|
5
|
-
* Also prints key events to the daemon terminal (stdout).
|
|
4
|
+
* Log file: ~/.adobe-ext-builder/daemon.log (always available, regardless of workspace config)
|
|
6
5
|
*/
|
|
7
6
|
/** Log a command being dispatched. */
|
|
8
7
|
export declare function logCommand(command: string, payload: Record<string, unknown>): void;
|
|
@@ -12,3 +11,5 @@ export declare function logResult(command: string, result: string, durationMs: n
|
|
|
12
11
|
export declare function logError(command: string, error: string, durationMs: number): void;
|
|
13
12
|
/** Log a general info message. */
|
|
14
13
|
export declare function logInfo(message: string): void;
|
|
14
|
+
/** Return the log file path for display purposes. */
|
|
15
|
+
export declare function getLogFilePath(): string;
|
package/dist/logger.js
CHANGED
|
@@ -1,35 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Daemon logger — writes timestamped entries to a log file
|
|
2
|
+
* Daemon logger — writes timestamped entries to a log file and the terminal.
|
|
3
3
|
*
|
|
4
|
-
* Log file
|
|
5
|
-
* Also prints key events to the daemon terminal (stdout).
|
|
4
|
+
* Log file: ~/.adobe-ext-builder/daemon.log (always available, regardless of workspace config)
|
|
6
5
|
*/
|
|
7
6
|
import fs from "node:fs";
|
|
7
|
+
import os from "node:os";
|
|
8
8
|
import path from "node:path";
|
|
9
|
-
|
|
9
|
+
const LOG_DIR = path.join(os.homedir(), ".adobe-ext-builder");
|
|
10
|
+
const LOG_PATH = path.join(LOG_DIR, "daemon.log");
|
|
10
11
|
let logStream = null;
|
|
11
|
-
let currentLogPath = null;
|
|
12
|
-
function getLogPath() {
|
|
13
|
-
const root = getWorkspaceRoot();
|
|
14
|
-
if (!root)
|
|
15
|
-
return null;
|
|
16
|
-
return path.join(root, ".daemon.log");
|
|
17
|
-
}
|
|
18
12
|
function ensureStream() {
|
|
19
|
-
const logPath = getLogPath();
|
|
20
|
-
if (!logPath)
|
|
21
|
-
return null;
|
|
22
|
-
// Re-open if workspace changed
|
|
23
|
-
if (logPath !== currentLogPath) {
|
|
24
|
-
logStream?.end();
|
|
25
|
-
logStream = null;
|
|
26
|
-
currentLogPath = null;
|
|
27
|
-
}
|
|
28
13
|
if (!logStream) {
|
|
29
14
|
try {
|
|
30
|
-
fs.mkdirSync(
|
|
31
|
-
logStream = fs.createWriteStream(
|
|
32
|
-
currentLogPath = logPath;
|
|
15
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
16
|
+
logStream = fs.createWriteStream(LOG_PATH, { flags: "a" });
|
|
33
17
|
}
|
|
34
18
|
catch {
|
|
35
19
|
return null;
|
|
@@ -49,7 +33,7 @@ export function logCommand(command, payload) {
|
|
|
49
33
|
}
|
|
50
34
|
/** Log a command result (success). */
|
|
51
35
|
export function logResult(command, result, durationMs) {
|
|
52
|
-
const preview = result.length >
|
|
36
|
+
const preview = result.length > 500 ? result.slice(0, 500) + "..." : result;
|
|
53
37
|
const line = `[${ts()}] OK ${command} (${durationMs}ms) ${preview}`;
|
|
54
38
|
const stream = ensureStream();
|
|
55
39
|
stream?.write(line + "\n");
|
|
@@ -60,7 +44,9 @@ export function logError(command, error, durationMs) {
|
|
|
60
44
|
const line = `[${ts()}] ERR ${command} (${durationMs}ms) ${error}`;
|
|
61
45
|
const stream = ensureStream();
|
|
62
46
|
stream?.write(line + "\n");
|
|
63
|
-
|
|
47
|
+
// Print the FULL error to the terminal so users can diagnose issues
|
|
48
|
+
console.log(` ✗ ${command} (${durationMs}ms)`);
|
|
49
|
+
console.log(` Error: ${error}`);
|
|
64
50
|
}
|
|
65
51
|
/** Log a general info message. */
|
|
66
52
|
export function logInfo(message) {
|
|
@@ -68,3 +54,7 @@ export function logInfo(message) {
|
|
|
68
54
|
const stream = ensureStream();
|
|
69
55
|
stream?.write(line + "\n");
|
|
70
56
|
}
|
|
57
|
+
/** Return the log file path for display purposes. */
|
|
58
|
+
export function getLogFilePath() {
|
|
59
|
+
return LOG_PATH;
|
|
60
|
+
}
|